前言
作为一名合格的ios,swift已经是一个必学语言了,如果之前大家都是直接上手的,那可以跟着系统学习一遍
先演示一遍代码的初始化,与Object-c对比一下
object-c
//创建一个对象,其中alloc为类方法,init为对象方法
NSObject *obj = [[NSObject alloc] init];
//创建NSObject对象,调用 alloc 类方法,不需要init可以不调用
NSObject *obj = [NSObject alloc];
swift
//初始化一个NSObject对象
//一般类的初始化内容都在后面的括号里面,括号里面就是重写init的内容了
let obj = NSObject()
//下面其实和上面逻辑一样,只不过为了让一些人容易入手,都是调用的默认的构造方法
//init方法不实现,系统会默认给一个
let obj = NSObject.init()
看到这里,你可能觉得swift比较像 javascript或者比较像 typescript
下面跟着一起学吧,这里面介绍了大多数swift语法,相信能很快入门
swift编程语言
swift语言与 c++、javascript、typescript都很相似,可以说集结各家的一些内容
有这些语言基础的人,学习便如如鱼得水一般,对于有Object-C基础的更是学完可以直接准备开发了(只是框架调用会有些改变,一旦适应也是如鱼得水)
没有的也不用着急,这门语言很简单,在我的介绍下,相信很快就能用于开发了
注意:学习过程中,有一些案例中,可能会有默认的语法,不要过于纠结,能直接想明白更好,不明白后面也会接触并介绍
另外,这里主介绍编程语言,不介绍api怎么使用
基础语法
变量与常量
let:常量修饰符,即一旦赋值不能更改 (像 javascript 的 const)
var:变量修饰符,赋值后仍然能更高(像 javascript 的 let,因此也没有变量提升效果)
//let 声明常量,可修改,相当于js中的const
let a = 0 //类型推断,相当于 let a: Int = 0
let b = 0, c = 1 //可以同时声明好几个
//a = 5; //let修饰无法更改,否则报错
//var 声明变量,可修改,相当于js中的let
var a1 = 0
a1 = 3;
print("输出一下变量:\(a1) 常量:\(a) \(b) \(c)")
类型与类型推断
带类型的申明如下所示:变量: [类型],可以声明的同时进行赋值
另外注意每个数字类型都有max和min方法,可以方便保证数字是否溢出
//声明类型
var d: Double
d = 10 + 3.2
//声明的同时进行赋值,这里确定类型为Double
var d1: Double = 10
var d2 = 10 //这里类型推断为Int
print(Int(d)); //转化成指定类型
//Int8.max Int8.min; //这些类都有最大最小值
使用过程中,如果不声明类型,则根据实际赋值内容,进行类型推断,例如:默认整形数字是 Int,默认小数是 Double
注意:无论是let还是var修饰,无论是指定类型,还是类型推断确定类型,一旦有了类型,则不能再更改其类型,重新赋值也只能赋值同类型的(系统为了方便开发,整数能自动转小数,毕竟不会丢失精度,反之不能)
类型别名
给一个类型重名,能在合适的场景使用,使其更具有标识性(例如:OC中CGFloat是double的别名,CFTimeInterval也是double的别名,特殊场景更具有标识性)
//重命名一个类型,特殊场景有奇效,例如下面的颜色范围
typealias ColorNmber = UInt8
let a3: ColorNmber = 10;
元组
元组是 swift新引进的一个基本类型,其像数组一样,可以存放多个内容,可以解决多个不同类型的返回值的使用(这里会问,为什么不用数组,四个字,简单原则)
元组的使用就是简单原则,随用随取,一般不会用于大规模传递数据
//元组的初始化与赋值
let numTuple = (1, 2, 4)
let mixTuple = (1, "字符串")
//带类型的赋值
let mixTuple1:(Int, String) = (1, "字符串")
//展开元组赋值
let (num1, string1) = mixTuple
print("num1:\(num1) -- string1:\(string1)")
Tips:Void类型,在 swift中 就是元组的空类型(即:()),一些地方使用时可以注意
可选项
可选项,即这个选项具备选择性,其可能有值,可能没值,没值的时候为nil
如下所示,声明一个可选项也只需要在类型后面加上一个?
//可选项,即这个选项可能有值,可能没值(nil)
var a10: Int? = 404
a10 = nil
可选项绑定
可选项绑定主要用来判断是否有值,毕竟一个可选项可能有值,也可能为nil
正常使用可选项作为判断条件时,一般手段如下(同时也可以看到,判断条件不需要括号了)
//注意判断条件只能以 true和false类型来判断
if a10 != nil {}
else {}
使用可选项绑定来处理判断条件手段如下
一旦有值,则展开成功,走第一个括号,值也会给到前面的参数里;否则,走到 else
//可选项绑定判断语句,与生面的式子相同,即成功展开就走后面括号
if let newA10 = a10 {}
else {}
隐式展开可选项
适用于于一旦赋值,值就不会在改变的情况
下面是正常强展开可选项
//默认使用可选项时
let s1: String? = "123123123" //我是一个可选项
//这样就强展开s1,不写感叹号就会报错哈,如果可选项没有值,会崩溃
let s2: String = s1!
下面,设置隐式展开可选项类型
//假设s3为一个一旦赋值就不会改变的类型
var s3: String! = nil
//开始赋值,不再更改
s3 = "展开就展开,没事了"
//后面使用时会强展开,此时一旦没有值则会崩溃
let s4: String = s3
合并运算符
合并运算符 ?? ,只适合使用在可选类型身上
合并运算符在使用时,若可选类型展开成功,则返回展开成功的内容,否则,返回备用参数
// s1有值,则返回展开成功的s1,否则返回备用参数s3
let s5 = s1 ?? s3
可选项 tips
如果可选项是一个类,那么想正常访问其属性,就可以通过?.来访问其内部属性,甚至可以a?[1]可选下标来访问
强制访问就可以通过!.来访问其内部属性(同样适合一旦赋值就一直存在的,如果没值会崩溃)
运算符
合并运算符 ?? 就不介绍了,只用于可选类型,上面有介绍
三目运算符
//三目运算符
var a20 = 10 < 20 ? 5 : 10 //?前面条件满足赋值:前面的值,否则赋值:后面的值
区间运算符
...(闭区间)和 ..<(右半开区间)
通过前后整型数字来确定区间
目前就这两个,请不要乱组合,乱组合会报错的😂
for index in 1...5 {
print(index)
}
for index in 1..<6 {
print(index)
}
//单侧区间,可以展开数组选项,到某个索引,或者从某个索引开始
let list = [1, 2, 3, 4]
//久违的for in 循环出现了
for item in list[...2] {
//运行到第二个
print(item)
}
for item in list[1...] {
//从第一个开始
print(item)
}
忽略标识 _
_为忽略标识,用来做简化操作的,很多地方会有意想不到的效果
//let aa = 10 参数不适用是可以用_代替,还有若干
_ = 10
//函数的参数名使用时也可以用 _ 标识
func gogogo(_ a: Int){}
顺便提一下:_忽略标识、 ?判断标识或可选项标识、 !隐式展开可选项
guard
和if使用方式相似,能简化 if 的使用
if 为判断条件为 true,则进入第一个括号,否则进入第二个括号
guard 只有一个else括号,如果条件不满足,直接走 else,感觉是为了简化 if 拦截操作 而存在的
//正常我们判断一个条件时,满足就走if,否则就走else
//如果条件需要置反,则需要加括号置反,看起来比较臃肿
//那么可以直接使用 guard
if !(1 > 2) {
}
//换成 guard 可以保持原有条件,如果条件不满足,直接 else
//比较适合一些拦截操作
guard 1 > 2 else {
return
}
switch
switch 相信学习过 c语言 的人都不陌生,开关的意思,能让判断语句像开关一样列出,使用起来更有美感
一般用于展开枚举值
其使用过程中,一个 case 可以支持多个枚举,还可以支持一个数字范围,甚至一个元组
另外注意,swift 中的 switch 执行完毕 case 默认退出,不需要 break,如果想要继续往后走,则需要 fallthrough 关键字
//switch默认执行一个case自动跳出,default只能在最后
//使用fallthrough关键字,可以贯穿到后面一种情况,即可以继续执行下面的case
let enu = TestEmum.TestEnum1;
//可以switch + enu + {} 通过提示自动补全case
switch enu {
case .TestEnum1:
print(TestEmum.TestEnum1)
// fallthrough //使用这个关键字可以继续往后走
case .TestEnum2, .TestEnum3: //可以通知使用多个参数
print(TestEmum.TestEnum2)
default:
print(TestEmum.TestEnum3)
}
//switch展开范围
let ranNum = arc4random()
switch ranNum {
case 1...10:
print(1)
case 11..<1000:
print(11)
case 1000..<10123213:
print(1000)
default:
print(123123123123)
}
//switch展开元组
let cuple:(Int, Int) = (1, 10001)
switch cuple {
case (1, 10):
print("匹配值为(1, 10)的值")
case (_, 11):
print("匹配值为(任意值, 10)的值")
case (10...100, 200...2000):
print("匹配值为(指定范围, 指定范围)的值")
default:
print("剩下的所有都到这里吧")
}
等价比较
比较两个字符串是否相等,使用 == 和 !=
//比较两个字符串是否相等,
if s10 == s12 {}
if s10 != s12 {}
//顺便提一下,比较两个变量或者常量,是否引用了同一个类的实例(对象) === !==
//判断两个自定义类等价也可以使用 == 前提是继承了NSObject类或者自己遵循Equatable协议
//继承的类可以直接使用 == 判断两个类是否相等
#available
可以用来判断版本号,用来控制 新老api 的调用
if #available(iOS 10, *) {}
字符串
类型为 String,单个字符为 Character
拼接字符串
其可以像其他编程语言一样,使用 + 拼接字符串,也可以使用 append方法往后追加子串
var s10: String = ""
s10 = "卡萨丁副卡就熬---"
//拼接字符串,只能字符串拼接字符串
s10 += "哈哈哈哈"
s10 = s10 + "测试一个不可变字符串"
//也可以使用系统的 append方法拼接新子串到后面
s10.append("哈哈哈哈")
当需要拼接格式化字符串时,即多种类型的变量时,可以使用 appendingFormat 方法来格式化字符串
可以使用 \()语法,将变量在字符串内展开,推荐
s10 = s10.appendingFormat("sdfasd%ld", 5) //拼接格式化字符串,返回新的字符串
//字符串中嵌套变量,以形成新的字符串,数字字符串都可以,推荐
let s12 = "哈哈哈\(a)--\(s10)"
常用其他方法
/字符串长度,已经不是length了
s10.count;
//判断是否为空
s10.isEmpty;
//返回逆序字符串
let s11 = s10.reversed()
//分割字符串到集合中
let strList = s10.split(separator: " ")
//字符串前缀和后缀
s10.hasPrefix("lalala")
s10.hasSuffix("lalala")
索引
字符串中的索引不再是数字类型,而是 String.Index类型,想要访问指定位置的字符或者子串,需要通过索引来获取
//索引,字符串访问子字符需要用到索引,不能用1,2,3等代替
//字符串开头索引
s10.startIndex;
//字符串结束位置,与length一致
s10.endIndex
//获取指定索引前面的字符(参数是第一个字符索引)
s10.index(after: s10.startIndex)
//获取指定索引后面的字符(参数是最后一个字符索引)
s10.index(before: s10.endIndex)
//获取指定索引位置,偏移指定长度的字符(这里从开始向后偏移0长度)
s10.index(s10.startIndex, offsetBy: 0)
获取子串
获取子串需要用到索引与范围关键字,因此需要对前面两个有所了解
可以直接通过快捷方式[索引],直接获取指定位置的字符或者子串
注意:获取到的子串不是 String 类型,而是 SubString 类型,需要转化
//获取指定索引字符,参数是字符开始索引
let subcs10 = s10[s10.startIndex]
print("subcs10", subcs10)
let subss10 = s10[s10.startIndex..<s10.endIndex];
//可以将SubString类型子串转化成String
print("subss10", String(subss10))
插入删除
插入和删除也都需要用到索引和范围,其使用如下所示
//插入单个字符到某个索引处
s10.insert("0", at: s10.startIndex)
//插入子串到某个索引出
s10.insert(contentsOf: "哈哈哈哈", at: s10.startIndex)
//删除某个索引的字符
s10.remove(at: s10.startIndex)
//删除指定索引范围的子串(不要忘了区间运算符)
s10.removeSubrange(s10.startIndex..<s10.endIndex)
//s10.removeSubrange(s10.startIndex...s10.endIndex)
字符串遍历
for in 即可,展开的是单个字符 Character
//遍历字符串
for c in s12 {
//展开遍历字符串中的单个字符
print(c)
c.asciiValue //获取 ascii 值,以便于比较等
}
//采用forEach方法遍历
s12.forEach { (c) in
//forEach遍历
}
顺序集合--数组Array
数组 Array 是一个有序的集合
初始化
var l = [Any]() //声明空数组
//var l: [Any] = [] //等价于上面
//var l1: Array<Any> = [] //等价于上面,推荐这么写,这样和很多语言或者其他集合相似度更高
//创建一个重复初值的数组
var l1 = Array(repeating: true, count: 10)
//合并两个数组
var l2 = l + l1
//快速创建一个有默认值的数组
var l3 = ["哈哈哈", "嘿嘿"]
//创建一个多种类型子集的数组
var l4:[Any] = ["哈哈", 3]
增删改查
//常用操作,获取、添加、插入、删除
let firstItem = l1[0] //获取指定位置元素
l1[0] = false //改动
l.append(2)//添加
l.insert(1, at: 1) //插入
l.insert("sdfasd", at: 2)
l.remove(at: 2) //l.removeLast() 删除
l.count; //数量
l.isEmpty //是否为空
l = [] //类型确定,可以这种方式置空集合
遍历
遍历仍然是常见的 for in
for item in l1 {}
for (index, value) in l1.enumerated() {
//以枚举的方式遍历带索引,索引在前,值在后
}
//采用forEach方法遍历
l1.forEach { (item) in
//forEach遍历
}
哈希集合--Set
也是一组元素集合,与Array不同的是,其是无序的,根据哈希算法来维护 Set,查询速度上要比普通数组要快
初始化
初始化与数组相似,可以使用空数组来表示空集合set,前提是要声明类型
var set = Set<String>()
//let set: Set<String> = [] //等价于上面
//快速创建一个有值的Set
var set1: Set<String> = ["哈哈哈", "啦啦啦"]
增删该查
//常用操作,增删查
//insert插入新元素时会检查哈希值,如果完全一致则只保存一个
set.insert("哈哈哈2")
//移除掉指定元素
set1.remove("哈哈哈")
//是否包含某个元素
set1.contains("哈哈哈") //查询是否有没有
set.count //数量
set.isEmpty //是否为空
set = [] //置空
set遍历
//熟悉的for in遍历
for item in set {}
//采用forEach方法遍历
set.forEach { (item) in
//forEach遍历
}
key-value哈希集合--Dictionary
Dictionary也是一个key-value类型的哈希集合,只不过是以 key 作为哈希算法的关键,value 则是保存与key对应的值
其每一个元素都是以 key-value的方式成对出现,即写入、更新,删除只需要通过哈希用到的key即可
初始化
初始化方式与其他集合类型类似,不同的是,由于值是key-value形式,因此赋予空值时,则多一个键值冒号:,即:[:]
//哈希集合Dictionary(字典) key-value哈希集合(其实就是Map)
var map = Dictionary<Int, String>() //Map<key, value>
//var map:Dictionary<Int, String> = [:] //等价于扇面
//字典还可以下面这样声明,由于其key-value的形式唯一,而数组Array和哈希Set不行
var map1 = [Int: String]()
//根据内容快速创建 Dictionary<String:String> 类型字典,会类型推断
var map2 = ["key1": "value1", "key2": "value2"]
增删改查
//通过下标方式获取值,下标为 key
let firstValue = map[1] //获取内容,也可以用来当做查询
更新内容
map[1] = "哈哈" //key:1--value:"哈哈"
//更新内容
map.updateValue("哈哈", forKey: 1)
//删除指定key,同时与之对应的value也会被删除
map.removeValue(forKey: 1)
map[1] = nil //删除指定key,value和key总是成对出现与消失
//置空字典,与数组和Set不同,其为key-value形式
map = [:]
Dictionary遍历
Dictionary遍历自然也可以用万能的 for in了
for (key, value) in map {}
与其它不同的是,由于其为key-value 结构,系统还准备了一个生成单个数组集合的 keys 和 values 方法,另外接收 keys或者 values集合时,需要转化成key或者value的类型数组,否则会报错
map.keys //获取所有key的数组集合
map.values //获取所有value的数组集合
for key in map.keys {}
for value in map.values {}
//单个接收key-value数组,需要转化成key或者value的类型数组
let keyList = [Int](map.keys)
let valueList = [String](map.values)
//采用forEach方法遍历
map.forEach { (key, value) in
//forEach遍历
}
遍历
无论是 字符串,还是集合,均支持 for in 的方式遍历,遍历出来的结果自然是单个字符、单个元素、单个键值对(key-value)
除此之外,还都默认支持 forEach + 闭包 的 方式来遍历
函数(方法)
函数:即我们常说的方法,对于一个类来说,有类方法和对象方法,类方法通过类名调用,对象方法需要根据当前类创建对象,然后使用对象调用
swift 中函数声明需要使用关键字 func
另外swift 中类方法和对象方法与Object-c也有所不同,类方法使用 static 修饰
class FunctionModel: NSObject {
//正常声明一个对象方法,这么声明,只需要func修饰即可
func setup() {
}
//类方法则需要通过 static修饰,类属性也类似
static func setup() {
}
}
无参函数
无参函数,顾名思义,没有参数的函数,其声明函数如下所示
无参函数,默认声明方法
func defaultFunction() {
}
可以看到其没有参数,甚至返回类型都没有,实际返回类型只是因为是Void类型,从而被省略了
下面三种方式为无参函数的声明方法,意思一模一样
同时也可以看到,返回类型小括号后面使用 -> + 类型 的方式来声明
//省略了 Void 返回类型
func defaultFunction() {
}
//补全返回值声明
func defaultFunction1() -> Void {
}
//相同的返回值声明
func defaultFunction2() -> () {
}
对于第三种可能会有疑惑,实际上 Void类型就是 空元祖类型,前面也有提到过,所以他们三个是完全一样的,一次碰到不同的写法,可以认为是一个方法(函数重载时需注意,否则会报错)
//void的定义
public typealias Void = ()
带参函数
参数的声明和普通变量的声明很相似,只不过不需要 let 或者 var修饰了,此外,swift在参数中新引入了标签,与其他语言不同的是,调用方法时,如果有标签,标签也会显示出来(如果不隐藏标签的话)
注意:即使隐藏标签,也会有方法的参数名提示,即:隐藏才是以前开发者的常态
如下所示,定义带参函数有三种情况
//定义一个标签和参数一致的函数,推荐使用
func function1(str1: String, str2: String) -> String {
return str1 + str2
}
//定义标签和参数不一致的函数,特殊情况下不同地方表达意思不一致可以使用
//最前面的只是单纯的标签,后面的为参数,不互用
func function2(prefixStr str1: String,suffixStr str2: String) -> String {
return str1 + str2
}
//隐藏参数名称,和object-c调用一样,有这个习惯的可以使用
func function3(_ str1: String,_ str2: String) -> String {
return str1 + str2
}
其调用方式如下所示
//带参函数介绍(标签和参数名一致)
self.function1(str1: "1", str2: "2")
//带参函数介绍(标签和参数不一致)
self.function2(prefixStr: "1", suffixStr: "2")
//省略标签函数,但参数仍会给出提示
self.function3("1", "2")
参数使用inout修饰
inout修饰的参数,参数在函数内被修改,外部参数也会发生改变(犹如传递了外部参数的指针,或者将对象本身传递过来)
func function7(num1: inout Int) -> Void {
num1 += 10
}
多参函数
使用...关键字,可以像 Print 似的传递多个参数
如下所示,想传递若干个参数,则可以在类型后面加上 ...
//这样声明的函数就可以传递多个参数了,传递的多个参数会被放到一个集合中
func function6(_ item1: Any...) {
print(item1)
}
如下所示,最后指定传递一个指定类型,由于前面是多参数,其标签 不能省略
func function6(_ item1: Any..., item2: String) {
print(item1)
}
参数默认值
声明参数的时候,还可以指定默认值,如果没有省略标签,则可以略过有默认值的标签传参,否则只能按照顺序传参
注意:如果只省略了默认值的标签,也是可以略过默认值,通过标签传参的,前提是能保证传递过程系统不会混淆,不建议这么使用
//设置带有默认值的,有默认值的参数可以不传递
func function4(str1: String = "我有默认值:", str2: String) -> String {
return str1 + str2
}
//设置带有默认值的,省略标签的,那么必须按照顺序传递,否则系统无法分清是传的哪个参数
func function5(_ str1: String = "我有默认值:",_ str2: String) -> String {
return str1 + str2
}
方法调用如下所示
//都有标签,可以省略有默认值的参数
self.function4(str2: "我没有默认值")
//没有标签,或者有没有标签的,只能按照顺序传递
self.function4(str1: "不使用默认值了", str2: "我没有默认值")
//带默认值,且省略标签的,须按照顺序传递
self.function5(str1: "哈哈哈", "哈哈哈")
函数重载
在 Object-c中是没有函数重载的,而其他很多编程语言都有,包括比较早出来的C++,因此 swift 也不甘落后,加入了函数重载
函数重载:同名函数,由不同的参数的函数区别调用, 即:相同的函数名,参数由不同的标签、类型、数量、顺序等组成
注意:函数重载与返回类型无关
使用时,系统会根据传入的参数或者类型,自动分辨调用的是哪个函数
如下所示,函数的重载案例
//函数重载,同名方法,参数种类不同(标签、类型、数量、顺序等不同),与返回值无关
func loadedFunction(num: Int) {
}
//类型不同
func loadedFunction(num: Double) -> Void {
}
//标签不同
func loadedFunction(_ num1: Int) -> Void {
}
func loadedFunction(num1: Int) -> Void {
}
func loadedFunction(num2 num: Int) -> Void {
}
//数量不同
func loadedFunction(num: Int, num1: Int) {
}
//顺序不同
func loadedFunction(num1: Int, num: Int) -> Void {
}
返回值或者传入参数为函数类型
传入参数为函数类型,可以使得一些代码更灵活,例如:排序算法,可以传递一个函数,通过外部函数排序(sort)
返回值为函数类型,即返回的是一个函数指针,可以返回的函数,直接调用该函数,有时候可以简化一些代码的使用
//传入参数为函数类型
func functionReturnFunction(sortBlock: (Int, Int) -> Void) {
let a = 10
let b = 20
sortBlock(a, b)
}
返回类型为函数类型
func functionReturnFunction() -> (Int) -> Void {
return loadedFunction(num:) //返回一个函数要带标签
return loadedFunction(_:) //没标签带上_
}
如果细心的话,往后看会发现,函数类型与闭包类型声明的一模一样
内联函数
学过 c 或者 c++ 的可能都听说过内联函数,内联函数能提高函数执行效率,默认编译器会根据其配置进行内联(也就是可能不进行内联),就像宏定义一样展开函数直接调用,避免了调用函数时的开辟空间、压栈、出栈等一系列操作
强制内联:就是忽略编译器设定,直接强制内联,调用时不会有执行正常函数那样有额外开销,能大幅度提高运行效率,比较适合小函数,或者多次运行调用的函数
注意:内联函数不能用于递归操作
//强制内联,内部函数就像宏定义一样被替换调用方法使用,调用方法时不会开辟函数栈
@inline(__always) func inlineFunction(num1: Int, num2: Int) -> Int {
return num1 + num2
}
内嵌函数
即,在一个函数里面在声明实现一个函数,只能在该函数内部调用,一般仅仅用来减少一个功能块的重复代码,或内部递归调用
特点:使用方便,可以被闭包所代替
//内嵌函数,一般仅仅用来减少一个功能块的重复代码或递归调用
func functionSub() {
func sub1() {
}
//减少模块重复代码调用,一般外部不复用
sub1()
print(123)
sub1()
//递归,可以使得功能更为集中
var a = 0
func sub2() {
a += 1
if (a < 1000) {
sub2()
}
}
}
闭包
声明闭包时, [参数] -> [返回类型] 即可,返回类型不可以省略
声明一个闭包类型,返回类型一定要给出
() -> Void //无参无返回值类型
(Int) -> Bool //有参数,返回值为 Bool类型
实现一个闭包,默认 in 是必须的 返回类型可有可无,会类型推断出实际类型
//有参数有返回类型,
let block = { num -> Int in
print(num)
return num
}
//等同于上面
let block = { num in
print(num)
return num
}
//无参函数可以使用 () 代替参数
let block1 = { () in
print("无参函数")
}
内嵌闭包函数
一个闭包的定义与赋值如下所示,闭包可以使用一个变量接收,这样就能像内嵌函数一样使用了(和 Object-C 中 block 一样)
//带参闭包,最后赋值给block,可以像内嵌函数一样使用
//两个参数,用逗号隔开,可以带上括号,也可以不带
let block = { num in
print(num)
return num
}
block(1)
闭包内重定义外部变量(避免内存问题)
解决 block 使用 self 导致的循环引用问题,只需要在参数前面加入中括号[],在里面重定义 self等对象,如下所示,里面可以定义多个
//如下所示,可以使用unowned或者weak
//注意:这里只是举例定义的方式,实际这里不存在循环引用
let block = { [unowned self]() in
if let name = self.name {
print(name)
}
}
闭包与函数的配合使用
下面演示下,默认调用外部函数与调用闭包时的区别
//排序函数
func sort1(_ num1: Int, _ num2: Int) -> Bool {
return num1 < num2
}
//声明一个乱序数字数组
var nums = [2, 3, 5, 1, 2, 4, 10, 6]
//通过sort方法,传入 sort1 排序函数指针进行排序
nums.sort(by: sort1(_:_:))
//这里开始改成闭包函数来使用,以及闭包函数的简化使用
//闭包函数,这样也完成了闭包函数,这展开的是一个尾随闭包(推荐使用,易读)
nums.sort { (num1, num2) in
return num1 < num2
}
//因为有类型推断,以及函数特性,隐藏参数简化等,可以进一步简化闭包(实际不推荐,可读性差)
nums.sort { num1, num2 in num1 < num2}
其中$0、$1表示对应的第几个参数
nums.sort(by: {$0 > $1})
nums.sort(by: >)
正常闭包、尾随闭包、逃逸闭包、自动闭包
闭包的四种情况,声明与调用分别放在一起进行对比,更具有参考性,对比以更容易理解,因此案例都在一起
逃逸闭包:需使用 @escaping 声明闭包类型,其使用场景比较特殊,逃逸闭包修饰允许闭包在函数执行完毕后执行,即允许延迟执行,或者被其他内集合保存,在合适时机调用,而正常的闭包都是在函数生命周期内使用
自动闭包:需使用 @autoclosure 声明闭包类型,仅适用于无参闭包
四种闭包的函数声明如下所示:
//声明一个普通闭包函数
func tailClosePackFunction1(closePack: ((Int) -> Bool), num: Int) {
}
//声明尾随闭包函数
func tailClosePackFunction(num: Int, closePack: ((Int) -> Bool)) {
}
func tailClosePackFunction2(success: ((Int) -> Bool), failure: ((Int) -> Bool)) {
}
//逃逸闭包,在传入的函数结束前没有执行闭包,需要使用 @escaping 修饰闭包类型,否则会报错
//即:如果传入的闭包,在函数执行过程中没有立即使用,而是被保存起来或者延迟时使用,就叫做逃逸闭包
var completionHandlers: [() -> Void] = []
func functionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
completionHandler()
}
}
//自动闭包,传入时不接受带参闭包,当他被调用时,可以直接使用返回表达式的值,即 return 的值
//不带参的普通闭包
func functionNormal(pack: ()-> String) {
let str = pack()
print(str)
}
//不带参的自动闭包,使用 @autoclosure 修饰闭包类型
func functionAuto1(pack: @autoclosure ()-> String) {
let str = pack()
print(str)
}
四种闭包的调用实现如下所示
//正常闭包
self.tailClosePackFunction1(closePack: { (num) -> Bool in
return num > 0
}, num: 0)
//尾随闭包,传入的闭包参数在前,后面有参数
//会发现第一个闭包参数跑到了最后实现,正常参数都在前面括号里面
self.tailClosePackFunction(num: 10) { (num) -> Bool in
return num > 0
}
//尾随闭包,没有正常参数时,参数小括号都消失了
//只要最后几个都是闭包,也可以一次性展开,这种情况网路请求中很常见
self.tailClosePackFunction2 { (num) -> Bool in
return true
} failure: { (num) -> Bool in
return false
}
//逃逸闭包,在传入的函数结束前没有执行闭包,需要使用 @escaping 修饰闭包类型,否则会报错
self.functionWithEscapingClosure {
print("我就是逃逸闭包")
}
//传入的不带参的普通闭包
self.functionNormal { () in
"我就是传入的不带参闭包"
}
//自动闭包,适用于无参闭包,需使用 @autoclosure 修饰闭包类型
self.functionAuto1(pack: "我就是自动闭包,简单吧")
self.functionAuto1(pack: self.description)
类、结构体、枚举
枚举 enum
枚举是以值类型传递,与c的struct或者是int、float一样,也就意味着值传递时会创建新的
且 枚举 也有和 c、Object-c不一样的地方,枚举本身就是一个类型,有自己的值,而不是给数字打上标签(就好比现在的枚举是一个变量名,值是变量的内容),因此不赋值默认也不会是自增的数字,另外其还可以通过设置构造方法赋予不同类型的枚举
定义枚举,使用 enum 修饰,如下所示
定义常见枚举类型
//默认定义方式枚举,不给默认值就没有默认值,类型就是该类型
enum TestEmum {
case TestEnum1
case TestEnum2
case TestEnum3
}
//适用于一个枚举用于多种场景的,可以间隔开,易读提高,数字默认会自增
case 里面声明的是枚举,可以继承Int类型,后面就可以赋予 数字类型了
enum TestValueEmum: Int {
case TestEnum1 = 1,
TestEnum2, //2
TestEnum3 //3
case TestEnum4 //4
case TestEnum5 = 100,
TestEnum6 //这里就是101了
}
枚举的使用,直接使用枚举名获取指定枚举值
//获取枚举
var enu = TestEmum.TestEnum1
//确定声明的类型后,可以直接调出里面的枚举类型
enu = .TestEnum1
定义带构造函的枚举类型
构造函数里面传递值
//设置一个带构造函数的枚举
enum Result {
case Success(String)
case Failure(Int, String)
}
//生成枚举
var result = Result.Success("我是成功的JSON信息")
var result2 = Result.Failure(-1, "请求失败了")
switch result {
//枚举内部声明变量,相当于展开枚举实际的值
case .Success(let json):
print("我是请求成功的json数据:\(json)")
case .Failure(let code, let message):
print("请求失败了,code:\(code),message:\(message)")
}
取出枚举的值
前面说了,枚举有自己的类型和值,下面我们就直接取出他的值
enum TestEmum: Int {
case TestEnum1 = 1
case TestEnum2
case TestEnum3
}
//可以直接通过枚举的 rawVaue 属性获取值
TestEnum.TestEnum1.rawValue // 1
//到这里大概明白了吧,TestEnum1是枚举类型,里面的属性 rawValue 才是原始值
注意:枚举很多功能也能像 结构体 和 类 一样使用(例如:方法、协议),但是不推荐,枚举主要用于类型的表示,其他的完全可以被 结构体 和 类 代替
结构体 struct
struct传参是以值类型传递,与c的struct或者是int、float一样,也就意味着值传递时会重新创建新的
默认属性可以赋初值,这样初始化的时候可以不传参
如果没有默认值,则初始化必须传值
优缺点:以值的方式传递,不用维护引用计数,提高了效率,且对于需要深拷贝的内容很友好,但同时可能会增加了额外内存消耗,且不支持继承
定义结构体,使用 struct 修饰
struct StructModel {
var width: Double = 0 //注意如果let修饰常量,给初始就不能修改了
var height: Double = 0
var cModel: ClassModel? = ClassModel.init()
}
//StructModel: 允许不带值、带值、带部分值的初始化(取决于定义struct时,有没有给初值)
var sMode1 = StructModel()
var sMode2 = StructModel(width: 1, height: 1, cModel: nil)
var sMode3 = StructModel(width: 1)
sMode1.width = 20.0 //可以赋值更新
类 class
class传参是以引用类型传递,可以理解为传值时,接受者同样引用了class对象,引用者们为多个指针类型,最终指向了创建的原class对象
必须要给给默认值,可选类型可以不给,表示为nil
优缺点:通过引用传递参数,操作同一片内存,节省内存开销,且对于不需要深拷贝的场景友好,但需要额外维护引用计数,增加了性能消耗
定义类, 使用 class 修饰
class ClassModel {
var width: Double = 0
var height: Double = 0
var sModel: StructModel? = nil
}
//class类: 只有默认的初始化
var cModel = ClassModel()
cModel.width = 10 //可以赋值更新
struct 和 class
struct 和 class 他们除了传递值的类型不一致、struct不能继承之外,其他地方大多数功能基本都保持一直
因此后面介绍的时候,两个不要额外纠结,所说的类也是默认指他们两个,少量为特有的,也会额外标记
属性的set和get方法
set为设置属性内容的方法,get为获取属性内容的方法
struct propertyClass {
var origin: CGPoint
var size: CGSize
var center: CGPoint {
//set(newValue){} //可以带参,如果省略会默认有newValue参数
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
get {
//直接使用参数或者使用get都可以
let x = self.origin.x + self.size.width/2
let y = self.origin.y + self.size.height/2
return CGPoint(x: x, y: y)
}
}
}
只读属性
只读时展开属性,不要标记set和get,只需要做get做的事情罢了,可以直接返回一个内容
class rectangleClass {
var width: Double = 10
var height: Double = 20
var area: Double {
//设置只读,那么只返回一个即可,单行省略return
width * height
}
}
属性观察
属性观察,可以避免重写set,以达到监听当前属性变化目的,平时用的也不多
注意:跟平时所使用的的KVO(观察者模式)不一样
struct ObserverClass {
var age = 10 {
willSet {
//属性变化赋值前调用
}
didSet {
//属性变化赋值后调用
}
}
}
属性包装、属性映射
设定包装属性的初始值,就像java中的注解一样,能简化代码
属性包装:需要用到 @propertyWrapper 关键字,设置一个私有变量保存内容,默认包装重写的属性名 wrappedValue
属性映射:(在属性包装的基础上,新增加一个功能,属性名称 projectedValue,调用时变量前加上$
例如(属性包装): 设置一个单个数字包装器,将赋值的数字最大不超过9
例如(属性映射):写入时加入写入数据库操作
记得给属性设置初值,否则可能会报错
@propertyWrapper
struct singleNumberClass {
private var number: UInt = 0 //记得设置初值,也可以通过init来设置
//默认的属性包装名称,参数名固定
var wrappedValue: UInt {
get {number}
set {
number = min(newValue, 9)
print("没有写入数据库")
}
}
//属性映射名称,参数名固定(这里模拟写入数据库操作),调用参数时前面加上$即可(obj.$number)
var projectedValue: UInt {
get {number}
set {
number = min(newValue, 9)
print("写入数据库了")
}
}
// init() {//可以通过这里来设置初值}
}
测试属性包装、属性映射
class WrapperTestClass {
@singleNumberClass
var singleNumber: UInt //这样实现了属性包装,就像是对其重写了一样
var doubleNumber = 10
func setup() {
//测试一下属性包装
singleNumber = 20 //wrappedValue -> set
print("singleNumber", singleNumber) //发现值被限制了 //wrappedValue -> get
//测试一下属性映射
$singleNumber = 20 //projectedValue -> set
print("singleNumber", $singleNumber) //发现值被限制了,projectedValue -> get
}
}
类属性、类方法
通过类名直接调用,一般公用处理,例如:单例、公用类等
struct staticClass {
//类属性,通过类名直接调用,可以理解为静态变量
static var name: String?
//类方法,也是通过类名调用
static func writeName(newName: String) {
}
}
结构体、枚举内修改属性
通过实例方法,修改结构体struct和枚举enum实例属性,默认无法修改,(类属性可以随意修改,可以理解为类内部的静态变量)
由于struct、enum都为值类型,不能通过实例方法直接修改属性,需在函数前面加上mutating方可(有突变之意)
注意: mutating 不能在 class 中使用
struct modifyTypeClass {
static var score1: Double = 0.0 //其为类属性,修改完全不受影响
var score: Double = 0 //实例属性,其修改会受到影响
//func updateScore(score s: Double) {} //会报错
//实例方法修改实例属性,需要加上mutating,枚举也是一样(不过枚举一般不这么使)
mutating func updateScore(score s: Double) {
score = s
modifyTypeClass.score1 = s
}
//类属性可以随意修改
static func updateScore1(score s: Double) {
score1 = s
}
}
继承、多态(重写)
继承:即子类继承父类的东西,只有 class 能继承,而 struct、enum不支持,此外swift同样不支持 多继承,而是采用 多协议 的方式
多态(重写):子类继承父类后,可以重写父类的相同函数,即同样函数有着不同实现(不是函数重载),子类函数,子类实例可以声明为父类的类型,调用时会根据创建的实际类型,选择调用父类还是子类的对应函数,这就是多态
继承的时候,只需要在子类后面的 : 后面 紧跟父类,不能先跟协议,否则会报错
override:重写关键字,可重写属性和方法
final:修饰的方法,修饰父类的属性或者方法,可避免被子类重写
super、self:super和self都是系统虚拟出来的参数,self指向调用者, super指向调用者的父类,因此可以通过super访问父类的方法,即使是类方法也是 ok 的
class parentClass {
var name: String {
get {
return "父类"
}
}
func location() {
print("我是parentClass的方法,我在家里")
}
func work() {
print("我是父类")
}
//禁止重写关键字 final
final func finalFunction() {}
}
//继承父类时,:后面需要紧跟父类,不能先跟协议
//下面重写了属性和实例方法
class subClass: parentClass {
override var name: String {
get {
return "子类"
}
}
//重写父类方法,可以通过 super 关键字调用父类方法
override func work() {
print("我是子类,我也是可以用super关键字调用父类的方法的")
super.work()
}
//禁止重写,会报错
//override func finalFunction() {}
}
继承与多态的使用
//默认创建父类的实例,不能声明为子类,调用时调用了父类的方法
let parentModel = parentClass()
print(parentModel.name)
parentModel.work()
parentModel.location()
//声明父类类型指向创建的子类实例,如果并列多个子类,也同样可以
let defaultModel: parentClass = subClass()
//调用时会优先调用子类的方法(前提是,父类公开了该方法,否则会编译出错,编译阶段当父类类型使用了)
parentModel.work()
parentModel.location()
//创建了子类实例,并声明了子类类型(类型推断)
let subModel = subClass()
print(subModel.name)
subModel.work()
subModel.location()
注意:
如果之前开发过ios,到这里你可能会想到继承NSObject的问题
swift中默认的类可以不继承子NSObject,其也能正常创建和释放类
其就变成了一个空类,但不能使用以前的NSObeject中的任何方法(例如:description)
简单类无需继承 NSObject,如果功能多可能需要判断等,建议继承 NSObject
is、as、as!、as?
前面提到了继承,不得不提到这几个关键字
is:判断是否是当前类型或者其父类型的实例,与 isKind 一样,而 isMember则是查看是否是当前类型的实例
as: 为强制类型转换,由于转化过程可能失败,一般使用 as? 或者 as!
as?: 强制类型转换,转换失败会返回可选类型
as!:强制类型转换,与强制展开一样,转化为指定类型,失败会崩溃
class isAsClass {
func test() {
let a = 10 as Double //这种可以直接用as
let b = 10.0 as! Int //double转Int这种需要强转as!
let a1 = Double(10) //一般数字强转是使用这种方式
let b1 = Int(10.0)
var r = arc4random() % 2 == 0 ? subClass() : parentClass()
if (r is parentClass) {
//判断 r 是否是当前类或者父类的实例对象,与 isKindOf 相似
}
//强制类型转换,转换成功会返回一个可选类型的该类型的对象,否则返回nil
if let c = r as? subClass {
print(c)
}
//强制类型转换强制展开,转化为指定类型,失败直接报错
let d = r as! parentClass
if d === r {
print(d)
}
}
}
构造函数、析构函数(init、deinit)
构造方法与析构方法 init、deinit(dealloc)
构造方法:struct和class都有用使用构造方法的能力,如果没有写,系统会自动使用无参构造函数,class继承是,重写构造方法时,需要加上 override 关键字
析构方法:只有 class 才有析构方法 (值类型不存在引用计数与释放问题,所以无意义),每一个类都有自己的析构方法,无法重写
initDeinitClass()、initDeinitClass.init()
class initDeinitClass {
//如果自己不实现,系统会使用默认的无实现的init方法
init() {
//构造方法init方法
print("initDeinitClass")
}
//带参数的,会映射到创建时函数的括号里面
init(aObj: String, bObj: String) {
}
deinit {
//析构方法,以前的dealloc,类释放调用通知的方法
}
}
//调用默认的初始化方法,带参的括号内会有提示
initDeinitClass()、initDeinitClass.init() //两个都可以
构造方法和析构方法的重写
class initDeinit2Class: NSObject {
override init() {
//如果是继承自NSObject或者其他类的方法,init记得加上override重写关键字
}
//每一个类都有自己的析构方法,无法重写
deinit {}
}
下标
通过 subscript 关键字,可以给固定的类设置下标,即:他们可以像数组一样调用调用内部数据
设置方式如下所示:
class subscriptClass {
var list = [1, 2, 3, 4]
//在里面可以定义set和get调用方法,想数组一样,subObj[num]
//参数可以是任意类型,也可以传递字符串,过滤出自己想要的方法
subscript(num: Int) -> Int {
set {
list[num] = num
}
get {
list[num]
}
}
//如果两个参数也可以,例如传入一个二维坐标,设置和获取值
subscript(x: Double, y: Double) -> Double {
set {
print("设置坐标对应的值了")
}
get {
return x * y
}
}
}
测试一下使用
let sub = subClass()
//调用了该类下标set方法
sub[2] = 10
//调用了该类下标的get方法
print(sub[2])
//调用了下标的get方法
sub[2, 3]
如果该类其作为其他类的可选属性,可以使用 可选 +下标 方式访问
//?[2]
注意: 如果该类作为其他类的可选属性,也可以使用 ? + 下标 方式访问
内嵌类型
于内嵌函数类似,内嵌类型也有,能内嵌class、struct、enum等类型
一般内部私用,定义如下所示
class subTypeClass {
enum enumType: Int {
case Monday = 0, TuesDay = 1
}
struct structType {
var name: String?
}
class classType {
var name: String?
}
func setup() {
let enum1 = enumType.Monday
let struct1 = structType()
let class1 = classType()
}
}
扩展(extension)
和 Object-c 的分类相似,不同的,这里分类没有名字,且自定义的类无法扩展出重名方法
扩展(extension)可以给指定类,扩展 属性 和 方法
extension String {
//扩展属性
var extersionName: String {
return self + ".txt"
}
//扩展方法
func getLogString(str: String) -> String {
print(str)
return str
}
//扩展下标函数,向range一样取出子字符串
subscript(loc: Int, len: Int) -> String {
let start = self.index(self.startIndex, offsetBy: loc)
let end = self.index(self.startIndex, offsetBy: loc + len)
return String(self[start..<end])
}
//如果是struct等类,仍然可以采用异变的方式进行定义修改内容
mutating func updateContent(str: String) {
//可以用来更新结构体的参数
}
}
也可以对初始化方法进行扩展
//对初始化方法进行扩展
extension CGRect {
init(x: CGFloat, y: CGFloat) {
let size = UIScreen.main.bounds.size
self.init(
origin: CGPoint(x:x, y: y),
size: CGSize(width: size.width, height: size.height))
}
}
测试一下扩展类
//测试扩展类
class extensionTestClass {
func test() {
//这应String就扩展出来了该属性
print("文件名".extersionName);
"打印字符串".getLogString(str: "测试一下打印")
let frame = CGRect(x: 10, y: 10);
}
}
协议
声明协议需要用到 protocol 关键字,与class一样,默认协议可以被struct、class,extersion 等遵循
前面提到了类不支持多继承,协议其 替代品,可以理解为其他语言中的接口
协议 可以声明属性或者协议,但其不包括实现,需要遵循协议的类来实现
协议可以像类一样遵循多个其他协议(只不过不需要实现),此时遵循该协议的类,需要同时实现该协议以及其遵循的其他所有协议,多个协议之间使用 逗号 ,隔开
遵循 AnyObject 的协议,只能使用 class 来遵循
swift中没有可选协议,如果不想实现其中的某个方法,可以将其分割出来,形成子协议,根据需要使用,或者采用扩展 extension扩展此协议,并实现,这样就可以不用实现了,可以根据情况自行选择
声明协议与遵循一个协议,类遵循也是写到类名的后面(如果有继承的类,继承的类在最前面)
声明一个空协议
protocol protocol0 {
}
//协议继承,遵循该协议必须遵循其继承的所有协议
protocol protocol1: protocol0 {
//属性协议
var name: String {set get} //需满足读写,支持写必支持读,因此没有单个set
var name1: String {get} //需满足读取,一般声明默认读写都有要求
func method1() //协议函数1
func method2() -> Int //协议函数2
static func method3() //静态协议,类方法
mutating func updateStructValue(num: Int) //更新struct的内部属性
init(str: String) //初始化协议要求
}
协议遵循多个协议(或者叫继承多个协议),如果一个类实现了该协议,需要实现遵循的所有协议(包括自己和协议的遵循的多协议)
//协议继承,遵循该协议的类必须实现其继承的所有协议
protocol protocol2: protocol1 {
}
//实现协议,多个协议之间使用,隔开,写到继承的父类后面,也可以没有父类
struct TableViewStruct: protocol2, protocol0 {
var name: String = "" //重写只需要声明即可
var name1: String = ""
//初始化协议要求
init(str: String) {
//协议可以当一个类型来使用,即遵循协议的某个类
let pro: protocol1
}
//实现协议1
func method1() {
}
//实现协议2
func method2() -> Int {
return 0
}
//实现协议的类方法
static func method3() {
}
//更新struct里面的内部属性
mutating func updateStructValue(num: Int) {
}
}
继承AnyObject时,只能使用class遵循该协议
//协议继承时,继承AnyObject时,只能使用class遵循该协议
protocol protocol3: AnyObject {
}
//继承自AnyObject的协议,只能用 class 来遵循实现,不能使用 struct 或者 enum
class TableViewClass: protocol3 {
}
swift中目前没有可选协议,可以使用extersion实现协议,这样不实现对应协议也不会报错了
如果想给指定类 扩展,可以使用 where 语句
//swift中目前没有可选协议(可实现、也可不实现),可以使用extersion默认实现协议
extension protocol1 {
func method1() {}
}
//扩展也可以遵循协议
protocol extensionProtocol {
func testPrint()
}
//给该协议扩展实现协议,协议扩展的方法拥有了可选效果,适用所有遵循该协议类
extension extensionProtocol {
func testPrint() {
print("我扩展了协议的方法")
}
}
//如果不想所有遵循该协议类都使用实现该可选效果,那么可以使用where语句
//当遵循协议的类为 UIView 才会扩展此类
extension extensionProtocol where Self:UIView{
func testPrint() {
print("我扩展了协议的方法,仅仅在UIView或者其子类才能实现可选效果")
}
}
lazy懒加载
lazy关键字,懒加载,延迟加载,在使用时,才进行赋值执行,一般通过重写 get 方法来使用
一般修饰var定义的属性,属性执行一个闭包,返回属性值
//声明一个属性如下所示
struct lazyClass {
var userInfo: UserInfo?
//给属性懒加载定义如下所示,这样就不会直接调用赋值了
lazy var username: String = {
//赋值逻辑
return userInfo!.name
}()
}
必要时候使用,平时能初始化就一起初始化了,毕竟滥用懒加载,后期读着还是蛮难受的,另外调用比较频繁的,懒加载代码逻辑多了,可能还会引起性能问题
泛型
泛型是一般是各种编程语言中进阶必备的技能之一,能大幅度提高代码的扩展性,虽说是进阶,可是其使用起来确很简单
在使用的时候,能传递任意类型参数,其类型参数传递时,会将传入的类型进行类型替换,也便于进行类型校验
泛型字符默认用 T,其为模板(template)的简称(一般使用从T开始,例如:T、U、V)
注意:使用泛型时,T 只是一个占位符类型,使用其他字母或者单词也可以,根据需要即可
下面是交换两个数字案例:
class GenericityModel {
//当交换两个数字的时候,这样做就很不通用,Int, double等,需要写好几个重载函数,就很麻烦
func swapNumber(_ swapA: inout Int, _ swapB: inout Int) {
let tem = swapA
swapA = swapB
swapB = tem
}
//使用泛型解决,这样传递任意类型都可以了
//注意:这里的 T 只是一个占位符类型,根据传入类型确定,可以替换和校验类型
//一般用 T 其为模板(template)的简称(一般T、U、V往后排)
func swapNumber2<T>(_ swapA: inout T, _ swapB: inout T) {
let tem = swapA
swapA = swapB
swapB = tem
}
}
测试案例
let model = GenericityModel()
var a = 10, b = 20
var d1 = 10.0, d2 = 20.0
//使用默认的交换方法
model.swapNumber(&a, &b)
//model.swapNumber(&d1, &d2)//会发现不行
//使用泛型定义的交换,发现都可以
model.swapNumber2(&a, &b)
model.swapNumber2(&d1, &d2)
泛型类型
声明在类中,用于更好控制子节点,话不多说,直接上代码
//泛型类型(创建时需要给定类型)
//定义一个栈类型结构,一个栈内只能放置同一种元素
//例如:使用泛型顶定义一个节点为任意类型,但一组对象只能使用同一种类型(如果定义成Any,则任意类型)
struct Stack<Element> {
var elements: [Element] = []
//push进去一个参数,放到最后(栈顶),记得结构体改变参数需要加上mutating关键字
mutating func push(_ item: Element) {
elements.append(item)
}
//pop出栈最后一个元素(栈顶)
mutating func pop() -> Element {
return elements.removeLast()
}
}
测试案例
//使用了泛型的栈结构
var stack = Stack<String>()
stack.push("")
stack.push("哈哈")
_ = stack.pop()
//stack.push(10)//此时会报错,泛型定义的类型为String类型,因此不能更新了
可以称这种类的类型为泛型类型
扩展泛型类型
扩展泛型类型: 扩展 泛型类型的类 后,可以直接使用原类中的泛型类型
//扩展泛型,可以使用原来指定的泛型参数
//扩展一个栈顶元素
extension Stack {
func top() -> Element {
return elements[elements.count-1]
}
}
类型约束(泛型类型约束)
上面的类型虽有一定约束力,使用时定义的类型仍然是非常灵活的,可以通过协议约束
例如:我有一个职位(ios),我只想要一个拥有此技能(会swift/oc的ios工程师)的人来投简历,而不需要没有此技能的人(java工程师)也来投简历,这就是协议约束
以上面的 NewStack 为例,泛型类型须遵循 printProtocol协议,即传入的类型必须遵循 printProtocol协议,或者是直接被声明了 printProtocol协议类型的变量
//需要遵循的协议
protocol printProtocol {
func printInfo()
}
//泛型约束,需要让泛型类型遵循执行协议
struct NewStack<Element: printProtocol> {
var elements: [Element] = []
//push进去一个参数,放到最后(栈顶),记得结构体改变参数需要加上mutating关键字
mutating func push(_ item: Element) {
elements.append(item)
}
//pop出栈最后一个元素(栈顶)
mutating func pop() -> Element {
return elements.removeLast()
}
}
//定义两个遵循 printProtocol 的一个类
//也就意味着NewStack可以存放NewStackItem、NewStackItem2中的一种了
class NewStackItem: NSObject, printProtocol {
func function1() {
}
func printInfo() {
print(self.description)
}
}
class NewStackItem2: NSObject, printProtocol {
func function2() -> Int {
return 10
}
func printInfo() {
print(self.description)
}
}
测试代码如下所示
//测试带类型约束泛型类型(遵循协议)
var newStack = NewStack<NewStackItem>()
newStack.push(NewStackItem())
var newStack2 = NewStack<NewStackItem2>()
newStack2.push(NewStackItem2())
泛型约束,能约束所有类型的泛型,如下所示,约束交换条件的类型
class GenericityModel1 {
func swapNumber2<T: printProtocol>(_ swapA: inout T, _ swapB: inout T) {
let tem = swapA
swapA = swapB
swapB = tem
}
}
关联类型(关联泛型类型)
通过 associatedtype 关键字关联声明一个类型,这个类型就是泛型,名字任意
关联类型一般配合协议使用,遵循协议的类可以使用该关联类型,并实现对应的泛型类型协议
protocol associateProtocol {
//声明一个泛型关联的类型名
associatedtype ItemType
//追加是使用该泛型
mutating func append(_ item: ItemType)
//获取时使用该类型
func get(_ index: Int) -> ItemType
//定义下标是使用同一个类型
subscript(_ index: Int) -> ItemType{get}
}
//遵循associateProtocol协议
//实现了协议后,所有的关联类型 ItemType 全部要设定成同一个类型
//这里直接手动将Int类型全部替换成Int了
struct associateContainer: associateProtocol {
var list = [Int]()
//这行可以删除,只是下面手动赋予类型即可,有了这行,系统会自动替换
typealias ItemType = Int
mutating func append(_ item: ItemType) {
list.append(item)
}
func get(_ index: Int) -> ItemType {
list[index]
}
subscript(index: Int) -> ItemType {
list[index]
}
}
上道上面的 associateContainer 会感觉,其也不是很灵活,其为不完善的关联类型,可以新增泛型类型,将关联类型改进为关联泛型类型即可(实际直接这么写即可)
实现关联泛型类型协议的时候,一般系统会给出提示,会先给出一个别名的操作,然后,系统就可以统一使用别名替换所有要实现的泛型协议了
如下所示,给 associateContainer 增加了泛型类型
//一般会结合类型泛型使用,这里带关联类型的泛型类型的实现
//只需要定义(系统提示) typealias ItemType = Element
//然后系统提示一键展开即可实现对应类型的协议
//typealias是要给一个泛型类型重命名成当前关联类型名(这里是ItemType)
//如果只有一个关联类型(associatedtype),那么可以删除掉typealias那一行
struct associateContainer<Element>: associateProtocol {
var list = [Element]()
//这行可以去掉,下面都换成Element即可,其存在可以快速生成下面协议
typealias ItemType = Element
//此时后面的类型,ItemType或者Element都可以
mutating func append(_ item: Element) {
list.append(item)
}
func get(_ index: Int) -> Element {
list[index]
}
subscript(index: Int) -> Element {
list[index]
}
}
测试代码如下所示
//测试关联类型泛型
var container = associateContainer1<Int>()
container.append(10)
关联类型约束
声明两个关联泛型类型,给关联类型遵循协议约束,下面案例就以 key-value的字典形式为例
其中key需要遵循Hashable协议,value为任意类型
protocol NewAssociateProtocol {
associatedtype Key: Hashable
associatedtype Value
mutating func appendItem(_ key: Key, value: Value)
mutating func removeItem(_ key: Key)
mutating func removeItem(_ value: Value)
}
//接下来我们赋值后自动展开即可
struct AssociateList<T: Hashable, U>: NewAssociateProtocol {
var map: Dictionary<T, U> = [:]
//可以直接删除
typealias key = T
typealias value = U
//实现协议
mutating func appendItem(_ key: T, value: U) {
map.updateValue(value, forKey: key)
}
mutating func removeItem(_ key: T) {
map.removeValue(forKey: key)
}
mutating func removeItem(_ value: U) {
}
}
测试代码如下
//测试带约束的关联类型(遵循协议的的关联类型)
var associte = AssociateList<String, String>()
associte.appendItem("key", value: "value")
var associte1 = AssociateList<String, Int>()
associte1.appendItem("key", value: 1)
高级运算符
学过 c++ 的应该有所了解,这里的高级运算符,可以对指定的结构新增运算功能,还可以自定义运算符
注意:运算符使用的都是系统已有的(+、-、*、/等),不能使用特殊字符作为运算符,默认优先级与系统中存在的一致
修饰符:prefix(头结合)、postfix(尾结合)、infix(两边结合)
prefix 只和左侧的结合;postfix 只和右侧结合;infix 为中间符号和两边结合
注意:运算符方法若不声明结合方式,默认为 infix,也无需使用infix修饰,否则会报错
swift中Int之类的已经是一个结构体了,而不是c基本类型
swift中,数字自增没有++和--了,我们给 Int 扩展一个++和--吧(由于swift返回值问题,实际不推荐)
extension Int {
//增加++前缀
static prefix func ++(num: inout Int) -> Int {
num += 1
return num
}
//增加++后缀
static postfix func ++(num: inout Int) -> Int{
num += 1
return num - 1
}
//增加--前缀
static prefix func --(num: inout Int) -> Int {
num -= 1
return num
}
//增加--后缀
static postfix func --(num: inout Int) -> Int{
num -= 1
return num + 1
}
}
下面在扩展一个常见的结构体 CGSize ,其更具代表性
如下所示,扩展了 CGSize 的 负号、加、减、+= -= 以及 等价 判断 (注意等价(==)运算,自定义类默认没有,需要继承NSObject或遵循Equatable协议,或者自己使用高级运算符直接写)
//在扩展一个结构体的负号吧,以及正常+,-
extension CGSize {
//扩展一个-的结构体,给一个结构体取-
static prefix func -(value: CGSize) -> CGSize {
CGSize(width: -value.width, height: -value.height)
}
//新增正常运算符,默认中缀infix,不用标识,标识会报错
static func +(left: CGSize, right: CGSize) -> CGSize {
CGSize(width: left.width + right.width, height: left.height + right.height)
}
static func -(left: CGSize, right: CGSize) -> CGSize {
CGSize(width: left.width - right.width, height: left.height - right.height)
}
//组合赋值运算符 += -=, 另外刚扩展的运算符也可以自己使用
static func +=(left: inout CGSize, right: CGSize) {
left = left + right
}
static func -=(left: inout CGSize, right: CGSize) {
left = left - right
}
//等价运算符 ==,默认的数字自己实现了等价运算符功能 ==
//自定义类,继承NSObject 或者 自己遵循 Equatable协议,可以直接重写
//注意:NSObject已经遵循Equaltable协议,因此继承后无需再次实现
//当然也可以不继承,不遵循Equatable协议,直接使用高级运算符写
static func ==(left: CGSize, right: CGSize) -> Bool {
left.width == right.width && left.height == right.height
}
}
自定义中缀运算符(infix)
自定义中缀运算符 infix,前后缀不支持各种自定义
新增系统没有运算符要在全局作用域内,使用 operator 关键字手动声明
系统默认提供了一些运算符类型,包含有结合方式和优先级,可以输入 precedence 根据提示来查看
如下设置自定义中缀运算符 ***,使用默认类型 precedencegroup类型
//默认使用了 系统默认的 DefaultPrecedence 类型
//即默认的结合方式和优先级
infix operator **** : DefaultPrecedence
自定义中缀类型,需要使用关键字precedencegroup自定义,不使用系统的
associativity表示的结合位置 left、right、none
lowerThan、higherThan 表示的是优先级,可以跟系统优先级进行对比
如果优先级结合方式跟系统的一致,就直接继承系统的即可
相同优先级条件下,以左结合为例,左结合就跟正常的加减乘除一样,同优先级从左往右算
//定义新的运算符类型
precedencegroup CGSizePrecedenceTest {
associativity: left //默认同优先级左结合,和加减一样,或者乘除
lowerThan: MultiplicationPrecedence //优先级低于乘号
higherThan: AdditionPrecedence //优先级高于加号
}
//声明为自定义的运算符类型
infix operator **** : CGSizePrecedenceTest
下面自定义三个中缀运算符,并实践一下
//设置中缀+++的结合和优先级
precedencegroup CGSizePrecedenceAdd {
associativity: left //默认同优先级左结合,和加减一样,或者乘除
lowerThan: AdditionPrecedence //优先级低于加号
}
//设置中缀**的结合和优先级
precedencegroup CGSizePrecedenceMuti {
associativity: left
//优先级低于乘法高于加法
lowerThan: MultiplicationPrecedence
higherThan: AdditionPrecedence
}
//设置后缀+++的结合和优先级
precedencegroup CGSizePrecedenceAddAdd {
//优先级高于乘法
higherThan: MultiplicationPrecedence
}
/下面对CGSize子定义几个功能,相信更容易理解
infix operator ++ : CGSizePrecedenceAdd //左右相加
infix operator ** : CGSizePrecedenceMuti //左右相加相乘
infix operator ++++: CGSizePrecedenceAddAdd //相互相加两次
extension CGSize {
static func ++(left: CGSize, right: CGSize) -> CGSize {
CGSize(width: left.width + right.width, height: left.height + right.height)
}
static func **(left: CGSize, right: CGSize) -> CGSize {
CGSize(width: left.width * right.width, height: left.height * right.height)
}
static func ++++(left: CGSize, right: CGSize) -> CGSize {
CGSize(width: (left.width + right.width) * 2, height: (left.height + right.height) * 2)
}
}
权限关键字
提示:源文件值当前创建的.swift文件,模块指的是当前项目仓库,或者是一个pod模块仓库,或者一个framework模块仓库
例如:pod导入的网络加载框架、图片加载框架
swift中主要可能使用到下面几个关键字:
open、public、Internal、private、File-private
internal
默认关键字,不指明情况下,默认权限为internal,其允许被定义模块中的任意源文件访问,但不能被该模块外的任何源文件访问
private
仅仅限制于当前声明的实体中,可以是一个类,可以是一个源文件(前提是声明到了外层),出了实体(当前类、或者当前源文件),就不可以访问了
fileprivate
像package修饰的一样,在当前源文件中能随意访问,出了源文件不能访问
例如:当前源文件定义了两个class,其中一个class中属性使用了fileprivate修饰,那么另一个类也可以调用,而使用private修饰的就不能访问
public、open
他们两个在模块内部和外部,都能够被访问到,但继承和重写权限有区别
public:在当前模块中,可以被继承(类)和子类(方法)重写,出了当前模块则不可以
open:在当前模块中,也可以被继承和重写,出了模块,只要导入该模块,也可以被导入模块继承和重写
因此open 访问权限最高,private 访问权限最低,Internal居中
func limitFunction() {}
internal func limitFunction0() {}
private func limitFunction1() {}
fileprivate func limitFunction2() {}
open func limitFunction3() {}
public func limitFunction4() {}
引用计数与内存泄露、weak、unowned、block
相比开发过ios的人都应该知道引用计数这个问题,ios系统中采用引用计数来控制堆中每个对象的生命周期,对象一旦引用计数归零,其所在内存则会被回收,相对于垃圾回收,其能更好地控制内存峰值
但与此同时,如果引用关系控制不好,则可能会出现循环引用等问题,继而对象引用计数无法清零,因此无法释放对象,就出现了内存泄漏
一般内存泄露还有强引用,对于强引用,需要自己在合适时机手动释放,这里只讨论代理和block的循环引用导致的内存泄露问题
因此引出了 weak、unowned关键字
相信学习过 Object-C的同学一定会恍然大悟
weak:弱引用,不增加对象的引用计数,指向的对象释放时weak修饰的变量会置为nil,变量类型应为可选类型
unowned:无主引用,与weak类似,同样不增加对象的引用计数,且修饰的变量仍然指向了对应变量的地址,但变量类型不能为可选类型,且指向的对象释放时,不会将该变量置空,因此其可能会成为野指针,但相比于weak,其拥有更高的性能,如果能保证修饰的变量,在对象生命周期范围内,指向内容不会被释放,那么大可以放心使用,平时建议使用weak
下面为则为 weak 与 unowned 的使用如下所示
class RefrerCountModel {
var name: String?
//weak 弱引用关键字,能有效避免强引用导致的内存泄露问题
//由于weak修饰的关键字的值问题,所以一定为可选类型
weak var delegate: NSObject? = nil //为了避免强引用
//unowned 无主引用,与oc中的unsafe_unretained相似
//可以理解为纯粹一个指针,没有进行retain等相关操作,也不会像弱引用会释放置空
//其指向指定内存区域,对象释放后调用容易出现野指针问题
//另外其为非可选类型,不能赋值为nil,需要初始化时(init)赋指定值
unowned let delegate1: NSObject
init(obj: NSObject) {
delegate1 = obj
}
//解决闭包的内存泄露
func setup() {
//可以在闭包的参数前面添加中括号,可以在里面定义多个变量,如下所示这样就可以解决内存泄露了
//下面的self定义一个就可以了,一般直接 [weak self],两个要重命名了
let block = { [unowned self, weak wself = self, weak delegate = self.delegate1]() in
print("我是测试的闭包")
self.delegate = nil
}
block()
}
}
where 条件语句
where 条件语句,一般用在协议扩展,泛型,遍历语句中使用
协议扩展:可以使用 where 来约束扩展方法的 适用类
//swift中目前没有可选协议(可实现、也可不实现),可以使用extersion默认实现协议
protocol whereDefaultProtocol {
func testPrint()
}
//给该协议扩展实现协议,协议扩展的方法拥有了可选效果,适用所有遵循该协议类
extension whereDefaultProtocol {
func testPrint() {
print("我扩展了协议的方法")
}
}
//如果不想所有遵循该协议类都使用实现该可选效果,那么可以使用where语句
//当遵循协议的类为 UIView 才会扩展此类
extension whereDefaultProtocol where Self:UIView{
func testPrint() {
print("我扩展了协议的方法,仅仅在UIView或者其子类才能实现可选效果")
}
}
泛型:即泛型约束,遵循协议的泛型,where 语句也可以实现同样效果(但是不推荐,作为了解)
//泛型约束,即遵循协议的泛型,where 语句也可以实现同样效果
//不过推荐默认的泛型约束,这里作为了解
protocol genericityProtocol {
}
//通过where实现泛型类型约束,作为了解,不推荐
class WhereModel1<element>: NSObject where element: genericityProtocol {
//通过 where 约束默认泛型函数
func swap<T>(_ a: inout T, _ b: inout T) where T: genericityProtocol {
//实现交换
}
}
//泛型约束推荐写法,简洁明了,推荐
class WhereModel2<element: genericityProtocol>: NSObject {
//通过 where 约束默认泛型函数
func swap<T: genericityProtocol>(_ a: inout T, _ b: inout T) {
//实现交换
}
}
遍历语句:where 语句用于遍历时判断,比较常见,可以减少循环内部的嵌套层数
//where语句用于遍历时判断,比较常见,可以减少循环内部的嵌套层数
class WhereModel {
func setup() {
let list = [1, 2, 3, 4, 5]
//可以将判断语句写到后面,可以减少循环内部的嵌套层数
for item in list where item > 3 {
}
//相当于
for item in list {
if (item > 3) {
}
}
}
}
并发
这里并发介绍的不是多线程的使用,而是讲的新引入的 async 和 await,即:异步关键字和同步等待关键字
使用起来就和 JavaScript 的 async 和 await一样
async:在函数的参数后面加上该关键字,可以实现异步执行该函数,使用简单
await:与 async 搭配使用,等待一个 async 修饰的异步函数执行完毕后,方可继续往后走,前提是 await关键字也要应用在 async 修饰的函数内
注意:这两个关键字引出的比较晚,需要在xcode13且 ios15.0系统以上,才能正常使用,因此平时不推荐使用,但是技能掌握将会方便的多
class AsyncAwaitModel: NSObject {
var name: String? {
//可以异步设置 set 方法
//set async {
set {
baseName = newValue
}
get {
"就测试一下"
}
}
func function0() {
//同步函数,不能使用await
self.function1()
}
func function1() -> Int {
return 10
}
//需要至少xcode13版本,否则直接报错
//注意ios15版本才能这么使用,因此不建议使用,使用场景
//ios15以及之后的小工具或者新功能体验版本可以使用
@available(iOS 15.0.0, *)
func function0() async {
//异步函数才能使用await,同步函数不能使用
let res = await self.function1()
}
@available(iOS 15.0.0, *)
func function1() async -> Int {
return 10
}
}
NSOperation 与 GCD
由于新引进的async与await,由于版本显示,一般不会使用,这里顺便提一下Object-C开发使用的 NSOperation和 GCD,相信一看马上就明白了怎么使用了
//NSOperation
let o = OperationQueue()
o.maxConcurrentOperationCount = 10
o.addOperation {
print("我是异步方法了")
}
//GCD
DispatchQueue.global().async {
print("我也是异步方法了")
}
//使用栅栏函数,注意需要在一个queue中,且不能为 global,这里只是案例
DispatchQueue.global().async(flags: .barrier, execute: {
print("我是栅栏函数,我不执行完毕,后面的别想动")
})
最后
大家一起跟进实践一下新语言吧,毕竟艺多不压身😂
顺便提一下,之前忘了拷贝了的那几个协议😂,不小心看到了一些文章,说ios中没有深拷贝,文章好像是前几年的,并且列出了 字符串copy的案例,copy后两个地址一模一样,并以奇怪的角度引入了mutableCopy、copy 分别为深、浅拷贝,不少人用类似的案例也纷纷反应,我测试了一下发现却不是这样的,个人猜测,由于小字符串targgedpointed问题,或者是当时版本的问题
因此,大家看到新的想法时,如果存在怀疑,一定要自己实践,可以用多个案例测试,千万不要被带偏了😂
最后祝大家:
技术 up! u! up!😂、
薪资 up! u! up!😂、
身体 up! u! up!😂、
未来 up! u! up!😂