swift4.0语法杂记(精简版)

1,762 阅读46分钟

小视频

001--swift简史小视频

002--Playground体验

003--常量&变量

一、swift简史

1、介绍 swift是苹果公司于2014年推出用于撰写OS和iOS应用程序的语言。它由苹果开发者工具部门总监“克里斯.拉特纳”在2010年开始着手设计,历时一年完成基本的架构。到后来苹果公司大力投入swift语言的研发,于2014年发布这一语言的第一版本。swift2.0之后的语法则趋于稳定,2017年发布的swift4.0虽有改动,但也只是增添了一些新特性。这些新特性需要在Xcode9上运行才能显示出效果。值得一提的是它支持unicode9,也就是说,可以用某些图片图标来充当变量。 例如:

"👧🏽".count // 人 + 肤色
"👨‍👩‍👧‍👦".count // 有4个成员的家庭
"👱🏾\u{200D}👩🏽\u{200D}👧🏿\u{200D}👦🏻".count // 家庭 + 肤色
"👩🏻‍🚒".count // 人 + 肤色 + 职业

2、特点

  • swift取消了预编译指令,宏也被包括在内。 某些开发者为了让Objective-C和swift代码兼容,会尽少在Objective-C中定义宏。
  • 取消了Objective-C中的指针等其他不安全访问的使用
  • 使用点语法来调用属性或者函数
  • 去除了NS前缀

3、为什么要学习swift

  • swift作为面向协议语言,不仅能写移动端,也可以做到搭建服务器端。
  • 纵观国内外iOS开发界,已经有许多公司直接或间接采用swift开发,使用swift语言开发已成为未来iOS开发的趋势。
  • swift以简洁、优雅等优点迅速俘获广大开发者的青睐。

二、用playground体验swift开发

  • 打开Xcode,选择创建一个playground项目

  • 创建一个普通的UIView对象

正如上图所示,playgound文件的左边是代码区,右边则是显示结果的区域。当点击用于眼睛时会实时显示出界面效果。

  • swift与objective-C的重大区别

    • 在swift中是没有.h和.m文件之分的。所有的代码全部都存储在一个文件里面。
    • 在swift中所有的代码都被封装在{}里面
    • OC使用alloc init进行初始化,而swift使用()
    • OC中使用[]来调用方法,而swift中采用点语法。比如UIColor.red
    • swift中不需要用分号分割语句

三、常量和变量

1、数据类型 在swift中也有各种数据类型来存储不同的信息。下表列举的是常见的数据类型变量。

但其实,在swift中,是不存在基本的数据类型的,所谓的数据类型,其实都只是结构体。这也是swift中的一个特点。

2、变量和常量

  • 声明 swift中用let声明常量,用var声明变量。
var x = 10;
let y = 20;

let z   //错误示范,let z 在声明的时候并没有赋值常量是不可改变的,只能在声明时赋值

在开发中,通常会优先选择使用let,因为不可变会更安全一点。所以建议在写代码之时,先选择let,等到需要变化的时候再改成var。

  • 自动推导

创建一个UIView,不指定类型。可以看到控制台上会打印出UIView的信息。这个现象被称为swift的自动推导。事实上,在代码左侧定义的类型只是程序员希望的类型,而右侧才是程序真实的类型。

let z = UIView()
print(z)

也就是说,变量或常量的类型会根据右侧代码执行的结果,推导出对应的类型。 可以使用热键option点击查看类型。

  • swift对类型的严格要求

在swift中,任何不同类型的数据之间是不允许直接运算的。比如下面这段代码就会报错。

//错误示范
let a = 10
let b = 12.5
print(x + y)

如果非要让不同类型数据之间能够运算,可以将其中一个类型进行转换。

let a = 10
let b = 12.5
print(a + Int(b))

此时得到的结果就是22。在swift中,做类型转换时是将数据括起来,相当于swift结构体中的构造函数。

当然也可以将前面的整数转换成Double型。此时就能打印出小数来。

print(Double(a)+b)

四、String类型和Bool类型

1、String类型

  • 声明

直接用双引号将数据引起来

let str = "小仙女"
let str1:String = "hahh"
  • 拼接

字符串的连接有两种方法,一种是通过加号来连接,另一种则是通过反斜杆进行插入。

let str = "小仙女"
let mesg1 = "one"+str //用加号的方式
let mesg2 = "two,\(str)" //反斜杠的方式
print(mesg1,mesg2)

在做字符串拼接时要注意加号和反斜杠后面都不能出现空格,不然会报错。

  • 拼接字符串时格式的变化

假设在某些特定的地方需要输出特定位数的字符,比如或时间的输出,就需要使用占位符来调整字符串的格式。使用String的构造函数,调用format方法,%0后面加上数字就表示需要占多少位数。

let min = 2
let second = 10

String(format: "%02d:%02d", min,second)
  • 遍历

调用字符串的characters属性,采用for...in...的方式来遍历字符串。

for c in str{
    print(c)      //swift4中的遍历
}
print(str.count)  //打印字符串长度

for char in myString.characters {
    print(char)   // swift3的遍历
 } 
print(str..characters.count)  //swift3打印字符串长度
  • 字符串的截取

最方便的方式就是将String类型转换成OC的NSString类型,再来截取。

let urlStr = "www.baidu.com"
let header = (urlStr as NSString).substring(to: 3)  //截取前三位

let middle = (urlStr as NSString).substring(with: NSMakeRange(4, 5))//去除前四个字符截取,范围之后五位字符

let footer = (urlStr as NSString).substring(from: 10)   //从第十个字符开始截取

2、Bool类型

与其他语言一样,Bool类型表示的就是真假,但是不同于Objective-C,swift中用true和false来表示真假。

五、可选类型

在Objective-C开发中,如果一个变量暂时不会使用到,可以将它赋值为0或者赋值为空,而在swift中,nil是一个特殊的类型,如果它和真实类型不匹配是不能进行赋值的。但是开发中将变量赋值为空是在所难免的事情,因此就推出了可选类型。 可选类型是swift的一大特色,在定义变量时,如果指定这个变量是可选的话,就是说这个变量可以有一个指定类型的值或者为nil。

1、定义一个optional的变量

let x:Optional = 10
print(x)

点击进去查看,可以发现Option其实是一个枚举类型。这个枚举有两个值,一个是none,表示没有值,而另一个是some,表示某一类值。 在输出的时候,可以看见控制台上的内容Optional(10),它的作用就是提示这是一个可选值。

而在实际开发中,一般不用上述方式创建可选值,而是指定一个类型,再在其后添一个问号。

let x:Optional = 10  //第一种写法

let x:Int? = 20     //第二种写法
print(x)

上述代码问号的意思就是定义一个可选的Int类型,可能没有值,也可能有一个整数。

2、 解包

试试将上面案例x和y相加,这个时候还能输出结果么?

此时可以看到编译器已经报错。在前面的教程中提到过,不同类型的值是不能直接运算的。而可选项有两种值的产生,若它的值为nil则不能参加计算。

因此引入解包的概念,“!”代表强制解包。它的意思是从可选值中强行获取对应的非空值。

print(x!+y!)

3、解包常见错误

//错误示范1
let y : Int?
print(y)

使用let定义的是常量,在初始化时必须要给出值。

//错误示范2:
let y : Int? = nil
print(y)

强制解包是危险操作,如果可选值为nil,强制解包系统会奔溃。

4、let和var的可选项默认值

//默认值测试
let x: Int?
print(x)
var y :Int?
print(y)

用let做测试时会直接报错,说明let的可选值是没有默认值的,而用var做测试时,报错信息就变成了警告,运行的结果为nil。可以由此推测出var的可选项默认值为nil。

swift中有规定,对象中的任何属性在创建对象时,都必须有明确的初始化值。

5、可选绑定

if let/var表示。它将变量赋值给一个临时变量,在这个操作中会做两步操作:首先判断变量是否有值,如果没有值,则直接不执行大括号里面的内容;如果有值,系统会自动将变量进行解包,并且将解包后的结果,赋值给临时变量。

比如下面这个例子:

通过一个字符串创建NSURL对象

let url: URL? = URL(string: "https://www.baidu.com")

接着创建NSURLRequest对象。强制解包非常危险,当url有中文的时候可能会变成nil。所以要判断url是否为空再对其进行解包。

if let url = url {
    let request = URLRequest(url: url)
}

六、swift中的分支

1、if语句 在swift中,if语句是不用带小括号的,但是后面跟的语句必须有花括号,哪怕只有一行代码。许多公司的代码规范也是规定必须使用这一格式。 注意:在swift中没有非0即真的说法,所以不能写成if(num)这样的格式。

let x = 9
if x > 5 {
    print("小仙女")
}else{
    print("妖精哪里跑")
}

2、三目运算符

三目运算符的写法是表达式后跟一个问号,用冒号来隔开条件是否成立的值。

let x = 10
x > 5 ? print("小仙女"):print("妖精")

非常有意思的是,如果开发者只想处理条件成立的部分,此时可以在冒号后面用一个小括号来代替条件不成立的部分。

x > 5 ? print("你都写了我两次啦"):()

3、 三目运算符的简单模式

三目运算符的简单模式通常是用于处理可选项的。“??”的意思是说,如果表达式有值,就使用那个值,如果没有,就使用“??”后面的值来代替。

let x:Int? = nil
let y:Int? = 9
print((x ?? 0) + (y ?? 0))

运行之后的结果为9。

之后再来说说运算符的优先级。举个简单的栗子🌰!

let name:String? = "安琪拉"
print((name ?? "") + "火烧屁屁咯")
print(name ?? "" + "火烧屁屁咯")

从运行的结果可以看到,“??”的优先级是最低的。如果没有小括号的约束,它会将后面的语句都当成是一个表达式。

4、 guard的用法

分支若是写得过多,就会导致代码可读性较差的问题。为了降低代码的层次,swift推出了guard。guard后面跟判断表达式,else后面写表达式不成立的代码。 需要注意的是guard必须写在函数内部,在最末尾出必须要跟关键字return/continue/break/throw中的一种。

import UIKit
let age = 20
func online(age : Int){
    guard age >= 18 else {
        print("还未成年呢")
        return
    }
    print("一起来开黑吖")
}

这样或许看不到guard的特别之处,但若是像下面这样的代码出现呢?

let age = 20
let money = true
let idcard  = true
func online2(age : Int,money:Bool,idcard:Bool){
    if age >= 18 {
        if money {
            if idcard {
                print("一起来开黑吖")
            }else{
                print("回去带身份证吧")
            }
        }else{
             print("回去拿钱")
        }
    }else {
        print("还未成年呢")
    }
}
//调用
online2(age: age, money: money, idcard: idcard)

如果用普通的分支方法,就会显得可读性太差。我们可以试着将它改成guard的写法。

func online1(age : Int){
    //判断年龄
    guard age >= 18 else {
        print("还未成年呢")
        return
    }
    //判断是否有钱
    guard money else {
        print("回去拿钱")
        return
    }
    //判断是否带了身份证
    guard idcard else {
         print("回去带身份证吧")
        return
    }
    print("一起来开黑吖")
}

执行完所有的判断语句之后才执行代码库,阅读性也比if……else分支强。

5、 switch

  • 最基本的用法 switch后面的小括号可以省略。用case关键字来表示不同的情形,case语句结束后,break也可以省略。
let sex = 0
switch sex {
case 0:
    print("男")
case 1:
    print("女")
default:
    print("其他")
}

  • 基础语法的补充

如果系统某一个case中产生case穿透,可以在case结束后跟上fallthrough

case  0:
    print("男")
    fallthrough

case后面可以判断多个条件,这些条件以逗号分开

let sex = 0
switch sex {
case  0,1:
    print("正常人")

default:
    print("其他")
}

switch可以判断浮点型、字符串类型和Bool类型

switch 3.14 {
case  0:
    print("正常人")

default:
    print("其他")
}
let opration = "+"
switch opration {
case  "+":
    print("加法")
case "-":
    print("减法")
default:
    print("其他")
}

七、swift的for循环和表示区间

1、变化

在swift3开始,就已经弃用了var i = 0; i < 10; i++的这种写法。并且++这种写法也被取消掉了,改为+=代替。

2、表示区间

swift常见区间有两种,开区间用..<表示,闭区间用...表示。要注意的是数字和省略号之间是不能加空格的。

func demo1() {
    for i in 0..<5 {
        print(i)
    }
    print("^^^^^^^")
    
    for i in 0...5 {
        print(i)
    }
}
demo1()

3、逆序操作

如果想要做逆序操作,只要在in后面的表达式后添加reversed()即可。

func demo1() {
    for i in (0..<5).reversed() {
        print(i)
    }
    
}
demo1()

八、swift中的数组

Swift语言提供了Arrays、Sets和Dictionaries三种基本的集合类型用来存储集合数据。数组是有序数据的集,集合是无序无重复数据的集,而字典则是无序的键值对的集。

数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。

1、定义数组

用let定义出来的数组就是不可变的

//定义不可变数组
let array = ["爱丽丝","小红帽","白雪公主"]

使用var来定义可变数组。正确的写法是Array<Element>这样的形式。其中Element是这个数组中唯一允许存在的数据类型。但是为了简便,推荐使用[Element]()的写法。

//定义可变数组
var arrayM = [String]()
var arrayM1:[String]
var arrayM2 = Array<String>()

2、创建带有默认值的数组

swift中的array类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。开发者可以在里面指定它的数量和类型。

var threeDouble = Array(repeating: 0.0, count: 3)
print(threeDouble[1])

3、对可变数组的基本操作

使用append给数组添加元素

arrayM.append("1")
arrayM.append("2")
arrayM.append("3")
arrayM.append("4")
arrayM.append("5")

使用insert方法将值添加到具体索引值之前

arrayM.insert("10", at: 2)

使用remove系列方法可以对数组做删除操作

arrayM.remove(at: 0)
arrayM.removeSubrange(1..<3)
arrayM.removeAll()
arrayM.removeLast() //可以去除最后一项,避免捕获数组count属性

通过取下标的方式对数组进行修改和查找

arrayM[0] = "小红帽"
print(arrayM[2])

利用区间对具体范围内的值替换

//替换第2项和第3项的值
arrayM[2...4] = ["22","33"]
print(arrayM[3])

4、数组的遍历

//根据下标值进行遍历
for i in 0..<arrayM.count {
    print(arrayM[i])
}
//直接遍历数组中的元素
for i in arrayM {
    print(i)
}

若同时需要每个数据项的值和索引,可以使用数组的emumerated()方法来进行数组遍历。

for(index,value) in arrayM.enumerated(){
    print(String(index+1)+":"+value)
}

5、数组的合并

只有相同类型的数组才能进行合并。

let resultArray = arrayM + array

九、swift中的集合

集合(Set)用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。

集合中的元素必须有确定的hashvalue,或者是实现了hashable协议。而swift提供的Int,String等类型其实都是实现了hashable协议的。hashable是equable的子协议,如果要判断两个元素是否相等,就要看他们的hashvalue是否相等。

1、定义集合

使用set<Element>定义。

Element表示集合中允许存储的类型,和数组不同的是,集合没有等价的简化形式。

//创建空集合
var letters = Set<Character>()
//使用字面量创建集合
var favorite:Set<String> = ["绮罗生","意琦行"]

要注意的是一个Set类型是不能直接后面跟的字面量被单独推断出来的,因此这个Set是必须要显示声明的。但是由于swift的自动推断功能,可以不用写出Set的具体类型。比如说上面那个例子,省去String,也能推断出Set的正确类型。

var favorite:Set = ["绮罗生","意琦行"]

2、访问和修改集合

通过.count属性知道集合的长度,通过isEmpty判断集合是否为空。

3、添加元素

favorite.insert("寒烟翠")
print(favorite.count)

4、删除元素

通过remove的方法删除元素,若这个值真的存在就会删除改值,并且返回被删除的元素。若集合中不包含这个值,就会返回nil。

if let removeBack = favorite.remove("意琦行"){
    print(removeBack)
}else{
    print("没有找到值")
}

5、集合操作

swift提供了许多数学方法来操作集合。

print(oddD.union(evenD).sorted()) //并集

print(oddD.intersection(evenD).sorted())//交集

print(oddD.subtracting(siggleDPrime).sorted())//取差值

print(oddD.symmetricDifference(siggleDPrime).sorted())//去掉相同值

6、遍历集合

for item in favorite {
    print(item)
}
//按照首字母的顺序输出
for item1 in favorite.sorted() {
    print(item1)
}

7、集合的成员关系

==来判断两个集合是否包含全部相同的值 用 isSubset(of:)来判断一个集合中的值是否也被包含在另外一个集合中 用 isSuperset(of:)来判断一个集合中包含另一个集合所有的值 用isStrictSubset(of:)或者isStrictSuperset(of:)方法来判断一个集合是否是另外一个集合的子集合或父集合并且两个集合不相等

十、字典

字典是一种存储多个相同类型的值的容器。每个值value都关联这唯一的键key。键就是这个字典的标识符。而且字典中的数据项并没有具体顺序。键集合不能有重复元素,而值集合是可以重复的。

1、定义字典

使用let定义不可变的字典,使用var定义可变字典。用字面量赋值时,系统会自动判断[]中存放的是键值对还是要一个个的元素。

let dict = [1:"one",2:"two",3:"three"]  //定义不可变字典

var dictM = Dictionary<String,NSObject>()  //定义可变字典
var dictM1 = [String:NSObject]()

//AnyObject一般用于指定类型,NSObject一般用于创建对象

2、对可变字典做基本操作

添加、删除和获取元素

dictM1["name"] = "小仙女" as NSObject
dictM["age"] = 17 as NSObject
dictM.removeValue(forKey:"name")
//获取:swift中只保留了最简单的写法,OC中有objectforkey的方法在swift中也被删除掉了。
dictM["name"]  

3、修改元素

若字典中已经有对应的key,操作的结果是直接修改原来的key中保存的value。若字典中没有对应的key,则会添加新的键值对。

dictM["name"] = "llx"

4、遍历字典

可以通过范围for遍历所有的key和value。也可以遍历所有的键值对。

for (key,value) in dictM {
    print(key)
    print(value)
}

5、合并字典

合并字典时通过遍历的方式将第二个字典的内容添加到第一个字典中。绝对不能用相加的方式对字典进行合并。

var dict1 = ["name":"llx","age":"17"]
var dict2 = ["num":"007"]

for (key,value) in dict2 {
    dict1[key] = value
}
dict

十一、元组

元组是swift中特有的一种数据结构,用于定义一组数据,元组在数学中的应用十分广泛。

1、定义元组

使用()包含信息,组成元组类型的数据可以被称为“元素”。

//使用元组来描述个人信息
let info1 = ("1001","张三",30)

2、起别名

可以给元素加上名称,之后可以通过元素名称访问元素

//给元素加上名称,之后可以通过元素名称访问元素
let info2 = (id:"1001",name:"张三",age:30)
info2.name

元组一般用于作为方法的返回值。元组中元素的别名,就是元组的名称

let (name,age) = ("张三",18)
name

十二、函数

函数相当于Objective-C中的方法,是一段完成特定任务的独立代码片段。可以通过给函数命名来标志某个函数的功能。而这个名字可以用来在需要的时候“调用”该函数完成其任务。格式如下:

func 函数名(参数列表)-> 返回值类型 {
    代码块
    return 返回值
}

func表示关键字,多个参数列表之间用逗号隔开,也可以没有参数。使用->指向返回值类型。如果没有返回值,可以用Void代替,也可以省略。

1、定义无参无返回的函数

func phone()->Void {
    print("小米")
}
phone()

2、定义有参无返回的函数

func phoneNum() -> String {
    return "123456"
}
 print(phoneNum())

3、定义有参无返回的函数

func callPhone(phoneNum:String){
    print("打电话给\(phoneNum)")
}
callPhone(phoneNum: "123456")

4、定义有参有返回的函数

func sum(num1 : Int,num2 : Int) -> Int{
    return num1 + num2
}
sum(num1: 30, num2: 30)

在swift4之后,调用函数的时候,能直观的看到参数。而在之前调用之时,只能看见第二个参数之后的名称,表达起来并不直观。如何解决这个问题呢?

可以采用给参数起别名的方式,在参数前面添加一个别名。

func sum(number1 num1: Int,number2 num2 : Int) -> Int{
    return num1 + num2
}
sum(number1: 2, number2: 4)

5、默认参数

在swift中可以给方法的参数设置默认值。比如说买甜筒的时候,商店默认会给顾客准备原味冰淇淋。但是用户也可以选择指定口味。

func makeIceCream(flavor:String = "原味") -> String {
    return "制作一个\(flavor)冰淇淋"
}
makeIceCream()
makeIceCream(flavor: "抹茶")

6、可变参数

有些时候,在创建方法的时候,并不确定参数的个数,于是swift推出了可变参数。参数的类型之后使用...表示多个参数。

func sum(num:Int...) -> Int {
    var result = 0
    for i in num {
        result += i
    }
    return result
}
sum(num: 18,29,3)

7、引用传递

如果现在有这样一个需求:要交换两个数的值,不能使用系统提供的方法。要如何来完成呢?

如果按照上面的写法就会报错,可以按住option键查看,参数默认是不可变的。 而且就算可行,做到的也是值传递。为了解决这一问题,swift提供了关键字inout来声明数据地址传递,也被称之为引用传值。在swift3.0的时候,inout的位置发生了改变,被放置在标签位置。但是作用与之前相同。

func swapNum1( m : inout Int, n : inout Int) {
    let tempNum = m
    m = n
    n = tempNum
}
swapNum1(m: &m, n: &n)
print("m:\(m),n:\(n)")

十三、类

swift用关键字class来定义类。通常情况下,定义类时,让它继承自NSObject,若没有指定父类,那么该类就是rootClass。类的格式如下:

class 类名:SuperClass {
    //定义属性和方法
}

1、定义存储属性和创建类对象

对象的属性必须要赋值,用解包的方式赋值为nil。

class Person : NSObject {
    //定义存储属性
    var age : Int = 0
    var name : String? //对象的属性必须赋值,不赋值会报错的哦
}
let p = Person()

2、给类的属性赋值

可以直接赋值,也可以通过KVC进行赋值

p.age = 10
p.name = "llx"
if let name = p.name {
    print(name)
}

3、定义方法

在swift中,如果使用当前某一对象的属性或者方法,可以直接使用,不需要加self

// 定义方法,返回平均成绩
func getAverage() -> Double {
        return (mathScore + EnglishScore)*0.5
    }
let average = p.getAverage()

4、定义计算属性

通过别的方式计算到结果的属性,称之为计算属性。

var averageS : Double {
        return (mathScore + EnglishScore) * 0.5
    }

5、定义类属性

类属性是和整个类相关的属性,用static修饰,作用域是整个类。通过类名进行访问。

    static var courseCount : Int = 0

在类外通过类名访问类属性

Person.courseCount = 2

6、类的构造函数

构造函数类似于OC中的init方法。默认情况下创建一个类时,必定会调用一个构造函数。如果一个类继承自NSObjct,可以对父类的构造函数进行重写。

在构造函数中,如果没有明确super.init()。那么系统会默认调用super.init()

class Person : NSObject {
    var name : String?
    var age : Int = 0
    
    override init() {
        print("hello world")
    }
}
let p = Person()

7、自定义构造函数

自定义构造函数可以传入参数,做赋值操作时采用self调用属性以示区分。

class Person : NSObject {
    var name : String?
    var age : Int = 0
    
    // 自定义构造函数
    init(name:String,age:Int){
        self.name = name
        self.age = age
    }
}
// 调用自定义的构造函数
let p1 = Person(name: "kaka", age: 12)
print(p1.age)

可以定义字典类型的构造函数。用KVC的方式将字典的值取出来,要调用系统的setValue方法就必须先调用系统的构造函数创建出对象。为了防止取出的对象没有属性而导致程序奔溃,需要重写系统的setValue方法。

如果用KVC的方式一定要先调用父类的构造函数。因为系统默认调用是放在方法最后面调用的。

class Person : NSObject {
   @objc var  name : String?
   @objc var age : Int = 0
    
    
    init(dict:[String : Any]) {
        super.init()
   // 要调用系统的`setValue`方法就必须先调用系统的构造函数创建出对象
        setValuesForKeys(dict)
    }
    // 防止奔溃
    override func setValue(_ value: Any?, forUndefinedKey key: String) {
    }

}

let p2 = Person(dict:["name":"lala","age":18,"score":33])
p2.name
p2.age

由于swift与objective-c的编译方式不同,用KVC字典转模型构造函数时,需要在属性前面加上@objc

8、类的属性监听器

在object-c中,我们可以重写set方法来监听属性的改变,而在swift中也可以通过属性观察者来监听和响应属性值的变化。通常用于监听存储属性和类属性的改变。对于计算属性则不需要定义属性观察者,因为我们可以在计算属性的setter中直接观察并响应这种值的变化。

可以通过设置以下观察方法并响应这种值的变化。 willSet:在属性值被存储之前设置,此时新属性值作为一个常量参数被传入。该参数名默认为newValue,开发者可以自己定义该参数名。 didSet:在新属性值被存储后立即调用,与willSet不同的是,此时传入的是属性的旧值,默认参数名为oldValue。 上面两个方法都只有在属性第一次被设置时才会调用,在初始化时,不会去调用这些监听方法。

class Person : NSObject {
    //属性监听器
    var name:String? {
        willSet {
            print(name as Any)
            //如果想要查看接下来的新值,可以使用newValue
            print(newValue as Any)
        }
        didSet {
           print(name as Any)
        }
    }
}

let p = Person()
p.name = "llx"

十四、闭包

闭包是swift中非常重要的一个知识点。类似于objective-c中的block,其实函数就相当于一个特殊的闭包。闭包需要提前写好,在适当的时候再执行。

1、定义闭包

闭包的格式是(参数列表)->(返回值类型) in 实现代码

举一个最简单的栗子🌰 用常量记录一个代码块,按住option键就能看到,b1是一个闭包。再到适合的地方去调用它。

let b1 = {
  print("干掉他们")
}
b1()

再来看一个带参数的闭包。在闭包中,参数、返回值和实现代码都是写在花括号里面的。in是用来定义分割和实现的。

let b2 = {
    (x:String)->() in print(x)
}

b2("string")

2、闭包案例

这个案例要模拟封装一个网络请求的类。利用闭包将jsonData类型的数据传递给展示页面。

  • 创建一个新的项目,选择swift语言

  • 封装一个网络请求的类HttpTool.swift继承自NSObject

用异步线程模拟网络数据请求,再回到主线程中回调闭包

class HttpTool: NSObject {
    //闭包类型:(参数列表)->(返回值类型)
   
    func loadData(callback:@escaping(_ jsonData : String)->()) {
        DispatchQueue.global().async {
            print("发生网络请求:\(Thread.current)")
        }

        
        DispatchQueue.main.async {
            ()->Void in
            print("获取到数据,并且回调:\(Thread.current)")
            
            callback("jsonData数据")
        }
    }
}

  • 到需要接收数据的界面定义Httptool类的属性,设置一个初始化值,将初始值赋值给变量

在swift中是不需要引入头文件的,文件之间可共享

import UIKit

class ViewController: UIViewController {

    var tools : HttpTool = HttpTool()
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //用闭包将json数据拿到
        tools.loadData { (jsonData) ->() in
            print("在viewcontroller中拿到数据\(jsonData)" )
        }
    }

}

3、尾随闭包

尾随闭包用于需要将一个很长的闭包表达式作为最后一个参数传递给函数。也就是说如果按时的最后一个参数是闭包,那么在调用它的时候就可以把这个闭包写在括号外面,并紧跟括号,函数的其他参数则仍然写在括号之中。

//这个函数接受一个String和一个闭包
//函数体内调用闭包,并且将String作为参数传递给闭包
func myFunc(strP:String,closeP:(String)->Void) {
    closeP(strP)
}

//普通调用
myFunc(strP: "hello", closeP: {(string) in print(string)})
//尾随闭包
myFunc(strP: "hello") {
    (string) in print(string)
}

4、逃逸闭包

当一个闭包作为参数传到一个函数中,但是该闭包要在函数返回之后才被执行,于是就称这样的闭包为逃逸闭包。也就是说闭包逃离了函数的作用域。写法是在这个闭包参数前加一个@escaping用来指明这个闭包是允许逃逸出该函数的。

  • 声明一个方法,这个方法是一个逃逸闭包 该方法要做的事情,就是将闭包添加到数组中去
 //定义数组,里面的元素都是闭包类型的
var callBackArray : [()->Void] = []

//定义一个接收闭包的函数
func testEscapingClosure(callBack:@escaping ()-> Void) {
    callBackArray.append(callBack)
}
  • 当改变数组的时候,取第0个元素调用。此时就改变了变量x的值
class SomeClass {
    var x = 10
    
    func doSomething(){
        testEscapingClosure {
            self.x = 100
        }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
callBackArray.first?()
print(instance.x)

因为逃逸闭包是函数执行之后才会执行,所以可以这样理解:创建一个类的对象instance;在对象中初始化一个x=10;利用对象执行了函数doSomething;函数内部调用全局函数testEscapingClosure,期望修改instance对象的x值为100,但是此时并没有执行这个包含了赋值语句的闭包。

查找全局数组callBackArray,找到里面第一个元素,显然找到的是在testEscapingClosure函数中添加的闭包{self.x = 100},此时才通过全局数组的查询找出闭包并执行,于是x此时才被赋值为100。这就是在函数执行完毕后才执行闭包。刚好符合逃逸闭包的定义。

结论: 逃逸闭包将在函数执行之后执行,于是这段代码最后输出为100是因为闭包最后才被执行……

  • 解决循环引用的三种方式 1、可以使用weak关键字将对象之间的联系变为弱引用
weak var weakself = self

2、第一种方式的简化

[weak self]

3、使用unowned解决

[unowned self]

但是该方法十分危险,要确保数据一定有值。否则会发生奔溃。

__weak 与__unretained有何区别? __weak修饰的弱引用,如果指向的对象被销毁,那么指针会立马指向nil __unretained修饰的弱引用,如果指向的对象被销毁,它的指针依然会指向之前的内存地址,很容易产生野指针(僵尸对象)

十五、tableView的用法

1、 懒加载

swift中也有懒加载的方式,并且在swift中有专门的关键字lazy来实现某一个属性实现懒加载。 格式:lazy var 变量:类型 = {创建变量代码}() 懒加载的本质在第一次使用的时候执行闭包,将闭包的返回值赋值给属性,并且只会赋值一次。

//懒加载只能用于结构体或者类的成员变量中
class Person:NSObject {
    lazy var array : [String] = {
        ()->[String] in
        return ["llx","lll"]
    }()
}

2、tableView的使用

使用步骤如下:

  • 创建tableView对象

使用懒加载的方式,到需要用到的时候再创建tableView。将tableView添加到控制器上的View。

class ViewController: UIViewController {
    
    lazy var tableView:UITableView = UITableView()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
       
        }
}
  • 设置tableView的frame
tableView.frame = view.bounds
  • 设置数据源和代理

实现UITableView的协议,并为tableView设置数据源

class ViewController: UIViewController ,UITableViewDataSource,UITableViewDelegate{
    
    lazy var tableView:UITableView = UITableView()
    override func viewDidLoad() {
        super.viewDidLoad()
       
        view.addSubview(tableView)
        tableView.frame = view.bounds
        //设置数据源
        tableView.dataSource = self
        tableView.delegate = self
    }
  }
  • 实现代理方法
 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 20
    }

创建cell。因为cell是个可选类型,有可能有值,也可能为nil。所以要进行判断。给cell设置数据的时候,选择textLabel点击option会发现textLabel也是可选类型。 在最后返回cell的时候,对cell进行强制解包。因为之前已经做过判断,所以不会出现程序奔溃的问题。

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let CellID = "CellID"
        var cell = tableView.dequeueReusableCell(withIdentifier: CellID)
        if cell == nil {
            cell = UITableViewCell(style: .default, reuseIdentifier: CellID)
        }
        cell?.textLabel?.text = "测试数据:\(indexPath.row)"
        return cell!
    }

实现点击的代理方法

 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("点击了:\(indexPath.row)")
    }

##十六、swift中的注释 在swift中,类似于paramg --mark的写法是不可行的。

它是如下两种形式 //MARK:- 要写的内容 用于分组

class ViewController: UIViewController ,UITableViewDataSource,UITableViewDelegate{
    // MARK:- 懒加载
    lazy var tableView:UITableView = UITableView()
    // MARK:- 系统回调函数
    override func viewDidLoad() {
        super.viewDidLoad()
        }
    }

这样写的话,就可以在菜单栏看到分组的信息

/// 提示信息 用于提示

若在tableView系列的某个方法上面写上///提示,到其他地方调用该方法时,会出现前面写的注释信息。

十七、枚举

1、定义

在swift中,枚举使用的是由enum关键字来创建的枚举,枚举的所有成员都放在一对大括号里面。它为一组相关的值定义一个共同的类型。使用case关键字来定义一个新的枚举成员值。

enum SomeEnum {
    // 在这里定义枚举
    case north
    case south
    case east
    case west
}

上面这个枚举定义的东南西北四个值就是这个枚举的成员值。与C语言和objective-c不同的是,swift的枚举成员值在创建的时候并不会被赋予一个默认的整形值。这些值的类型就是刚刚定义好的枚举的名字SomeEnum

如果希望多个成员值要写在同一行中,可以使用逗号将他们分割开。

enum Plant {
    case mercury,earth,mars
}

每个枚举都定义了一个新的类型,就像swift中的其他类型一样。此时可以把它赋值给一个变量,而且可以用点语法这种形式调用。

var directionT = SomeEnumeration.west

directionT = .east

注意:在switch中使用枚举值的时候,一定要穷举出所有的情况,如果忽略其中的一个,代码都无法编译通过。因为它没有考虑到枚举类的全部成员。如果说不需要匹配所有的枚举成员,可以提供一个default分支来涵盖其他未明确处理的枚举成员。

class Person:NSObject{
    var directionT = SomeEnum.west
   
    func direc()  {
        switch directionT {
        case .north:
            print("north")
        case .east:
            print("east")
        default:
            print("没有方向")
        }
    }
}

2、关联值

可以定义swift的枚举类存储任意类型的关联值,而且每个枚举成员的关联值类型都可以不相同。比如说,来创建一个条形码类型。类似于库存,可以有不同类型的条形码去识别商品,比如说通过数字,或者根据产品代码来识别。

enum BarCode {
    case upc(Int,Int,Int,Int)
    case qrCode(String)
}

上面代码可以理解为定义一个名为BarCode的枚举类型。它的一个成员值是一个具有(Int,Int,Int,Int)类型关联值的upc,另一个成员值是具有String类型的qrCode

之后可以使用任意的条形码类型去创建新的条形码

class Person:NSObject {
    // 创建一个名为pBar变量,并将Barcode.upc赋值给它。
    func function() {
        var pBar = BarCode.upc(9, 0, 3, 3)
        pBar = .qrCode("ABCD")
    }
    
}

这个时候原来的barcode.upc和其整数关联值被新的Barcode.qrCode和其字符串关联值所替代了。

3、枚举的原始值

枚举的原始值就是枚举的默认值,这些原始值的类型必须相同。在定义枚举的时候必须给出类型。

enum ASCIICHAR : Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

在使用原始值为整数或者字符串类型的枚举时,不需要显式的为每一个枚举成员设置原始值,swift将会自动未它们赋值。

enum Planet : Int {
    case mercury = 1, venus,earth,mars
}

上面这个例子,Planet.mercury原始值是1,那么后面的venus就是2,之后以此类推。

可以通过rawValue属性来访问枚举变量的原始值.

let earthsOrder = Planet.earth.rawValue

4、枚举递归

枚举成员的关联值为当前枚举类型时称为递归枚举。那我们可以通过使用indirect修饰枚举变量。indirect修饰整个枚举时,所有成员均可递归(也可不递归😝)。

indirect enum Ari {
    case number(Int)
    case addition(Ari,Ari)
    case multi(Ari,Ari)
}

上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。

let five = Ari.number(5)
let four  = Ari.number(4)
let sum = Ari.addition(five, four)
let product = Ari.multi(sum, Ari.number(2))

通过枚举递归,就成功的创建了一个(5+4)*2的式子。

十八、结构体

结构体通过struct去声明。在swift中,用到了大量的结构体,比如说基本的数据类型都是结构体而不是类。这意味着它们被赋值给新的常量或者变量,或者被传入函数或方法中时,值会被拷贝。

struct teacher {
    var name : String = ""
    var age : Int  = 30
}

十九、扩展

扩展 (Extension)可以做到无需修改原本的代码就直接把想要的功能实现。

  extension 某个现有的class {
    //添加新功能
  }

限制:

  • 不能添加任何已存在的 法或是属性
  • 添加的属性不能是存储属性,只能是计算属性

1、扩展在方法中的应用

extension String {
    func sayHello() {
        print("Hello from extension")
    }
}

上面这段代码是对String做了一个扩展。之后声明一个变量调用扩展方法。

var hello = "hi"
hello.sayHello()

此后,任何String类型都可以调用该扩展方法。

2、用扩展进行计算

extension Int {
    var squared : Int {
        return (self * self)
    }
}

上面这段代码对Int扩展了一个属性,让它计算一个数字的平方值。

var newInt = 30
newInt.squared
999.squared

3、扩展类或结构体

  • 创建一个普通类
class Lisa {
    var lisa = "半边天使"
}
  • 对类扩展,新增一个方法,使其能做自我介绍
extension Lisa {
    func describe() -> String {
        return "我可是会傲娇的"
    }
    
}
  • 创建对象调用方法

二十、泛型

泛型可以让开发者写出灵活可重复使用的方法跟结构。 先看一个栗子🌰!!

 var stringArray = ["Hi", "Hello", "Bye"]
 var intArray = [1,2,3]
 var doubleArray = [1.1,2.2,3.3]

上面创建了三个不同类型的数组,若是要求打印所有数组中的元素,通常会怎么做呢?

func printStringFromArray(a: [String]) {
      for s in a {
print(s) }
}
  func printIntFromArray(a: [Int]){
      for i in a {
print(i) }
}
  func printdoubleFromArray(a:[Double]) {
      for d in a {
print(d) }
}
  printStringFromArray(a: stringArray)
  printIntFromArray(a: intArray)
  printdoubleFromArray(a: doubleArray)

上面这段冗长的代码实在让人不忍直视。而泛型的出现正好可以解决这一问题。

func printEelementFormArray<T>(a:[T]){
    for element in a {
              print(element)
          }
      }

这段代码中的T代表了任意的元素。无论上面类型的数据都能放入其中。之后只要调用者一个方法,传入不同的数组就能将不同类型的元素打印出来。

二十一、协议

1、对面向对象语言的吐槽

  • 使用子类时,协议继承父类的属性和方法。其中某些方法或属性并不是开发者所需要的。这会让代码变得异常的臃肿。
  • 若一个类拥有很多父类,会让开发者很难找到每个类中的问题并进行修改。
  • 对象引用到内存的同一地方,若是发生改变,可能会造成代码混乱的现象。

而swift是一种面向协议的语言。协议其实就像篮球教练,会告诉选手如何去训练,但是教练本身并不会出现在球场。Swift中的protocol不仅能定义方法还能定义属性,配合extension扩展的使用还能提供一些方法的默认实现,而且不仅类可以遵循协议,现在的枚举和结构体也能遵循协议了。

2、一个简单的协议案例

  • 创建一个简单的协议,并让一个结构体去遵循

遵循协议的方法与继承类似。

protocol People {
    
}

struct Lisa: People {
    
}

  • 完善协议

给协议添加一些属性和方法,用get set 设定协议的状态。遵循协议时要了解变量是否能读取或赋值。

protocol People {
    var name: String {get set}
    var race: String {get set}
    func sayHi()
}
  • 在结构体中实现协议的方法和变量
struct Lisa: People {
    var name: String = "Lisa"
    var race: String = "Asian"
    func sayHi() {
        print("Hi~, I'm \(name)")
    }
}

3、协议的继承

  • 创建一个协议,让该协议继承自之前创建的People协议
protocol superman {
      var canFly: Bool {get set}
      func punch()
}

protocol superman: People {
      var canFly: Bool {get set}
      func punch()
}
  • 调用
struct AngleLisa: superman {
var name: String = "Lisa"
var race: String = "Asian"
func sayHi() {
    print("Hi, I'm \(name)")
}
var canFly: Bool = true
func punch() {
    print("punch  Vergil")
}
}

由此可知,一旦协议进行了继承,不但要实现本协议中所声明的方法和属性,连协议父类的方法和属性也不能落下。

二十二、swift4新特性

以下内容来自 最全的 Swift 4 新特性解析

感谢大佬提供学习资源!!!

1、语法改进

  • 在扩展extension中可以访问private的属性

举一个简单的栗子🌰!

struct Date: Equatable, Comparable {
    private let secondsSinceReferenceDate: Double
    static func ==(lhs: Date, rhs: Date) -> Bool {
        return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
    }
    static func <(lhs: Date, rhs: Date) -> Bool {
        return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
    }
}

上面代码定义了一个 Date 结构体,并实现 Equatable 和 Comparable 协议。为了让代码更清晰,可读性更好,一般会把对协议的实现放在单独的 extension 中,这也是一种非常符合 Swift 风格的写法,如下:

struct Date {
    private let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
    static func ==(lhs: Date, rhs: Date) -> Bool {
        return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
    }
}
extension Date: Comparable {
    static func <(lhs: Date, rhs: Date) -> Bool {
        return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
    }
}

但是在 Swift 3 中,这样写会导致编译报错,extension 中无法获取到 secondsSinceReferenceDate 属性,因为它是 private 的。于是在 Swift 3 中,必须把 private 改为 fileprivate

struct Date {
    fileprivate let secondsSinceReferenceDate: Double
}
...

但是如果用 fileprivate,属性的作用域就会比我们需要的更大,可能会不小心造成属性的滥用。

在 Swift 4 中,private 的属性的作用域扩大到了 extension 中,并且被限定在了 struct 和 extension 内部,这样struct的属性就不需要再用 fileprivate修饰了,这是最好的结果。

  • 类型和协议的组合类型
protocol Shakeable {
    func shake()
}

extension UIButton: Shakeable { func shake() {/* */ } }
extension UISlider: Shakeable { func shake() {/* */ } }

func shakeEm(controls: [???]) {
    for control in controls where control.state.isEnabled {
    }
    control.shake()
}

仔细思考上面的代码,如果是swift3中,func shakeEm(controls: [???])中的???应该写上面类型呢?如果写UIControl,那么调用control.shake()时就会报错。如果写Shakeable类型,那么control.state.isEnabled这条语句就会报错。

swift4为了解决类似问题,实现了把类型和协议用&组合在一起作为一个类型使用的写法。把它声明为UIControl & Shakeable类型。

func shakeEm(controls: [UIControl & Shakeable]) {
    for control in controls where control.isEnabled {
        control.shake()
    }
}
  • Associated Type 可以追加 Where 约束语句

在 Swift 4 中可以在 associatedtype后面声明的类型后追加 where 语句。

protocol Sequence {
    associatedtype Element where Self.Element == Self.Iterator.Element
    // ...
}

它限定了 Sequence 中 Element 这个类型必须和 Iterator.Element 的类型一致。

通过 where 语句可以对类型添加更多的约束,使其更严谨,避免在使用这个类型时做多余的类型判断。

  • 新的 Key Paths 语法

先来看看 Swift 3 中 Key Paths 的写法:

@objcMembers class Kid: NSObject {
    dynamic var nickname: String = ""
    dynamic var age: Double = 0.0
    dynamic var friends: [Kid] = []
}

var ben = Kid(nickname: "Benji", age: 5.5)

let kidsNameKeyPath = #keyPath(Kid.nickname)

let name = ben.valueForKeyPath(kidsNameKeyPath)
ben.setValue("Ben", forKeyPath: kidsNameKeyPath)

Swift 4 中创建一个 KeyPath\作为开头:

\Kid.nickname

当编译器可以推导出类型时,可以省略基础类型部分:

\.nickname

上面的代码在 Swift 4 中就可以这样写:

struct Kid {
    var nickname: String = ""
    var age: Double = 0.0
    var friends: [Kid] = []
}

var ben = Kid(nickname: "Benji", age: 8, friends: [])

let name = ben[keyPath: \Kid.nickname]
ben[keyPath: \Kid.nickname] = "BigBen"

相比 Swift 3,Swift 4 的 Key Paths 具有以下优势:

类型可以定义为 class、struct 定义类型时无需加上 @objcMembers、dynamic 等关键字 性能更好 类型安全和类型推断,例如 ben.valueForKeyPath(kidsNameKeyPath) 返回的类型是 Any,ben[keyPath: \Kid.nickname] 直接返回 String 类型 可以在所有值类型上使用

  • 下标支持泛型

Swift 支持通过下标来读写容器中的数据,但是如果容器类中的数据类型定义为泛型,以前的下标语法就只能返回 Any,在取出值后需要用 as? 来转换类型。Swift 4 定义下标也可以使用泛型了。但是并不需要做转型操作。

struct GenericDictionary<Key: Hashable, Value> {
    private var data: [Key: Value]
    
    init(data: [Key: Value]) {
        self.data = data
    }
    
    subscript<T>(key: Key) -> T? {
        return data[key] as? T
    }
}

let dictionary = GenericDictionary(data: ["Name": "Xiaoming"])

let name: String? = dictionary["Name"] // 不需要再写 as? String

2、字符串

  • Unicode 字符串在计算 count 时的正确性改善

在 Unicode 中,有些字符是由几个其它字符组成的,比如 é 这个字符,它可以用 \u{E9} 来表示,也可以用 e 字符和上面一撇字符组合在一起表示 \u{65}\u{301}。

var family = "👩"
family += "\u{200D}👩"
family += "\u{200D}👧" 
family += "\u{200D}👦"

print(family)
print(family.characters.count)

这个 family 是一个由多个字符组合成的字符,打印出来的结果为 👩‍👩‍👧‍👦。上面的代码在 Swift 3 中打印的 count 数是 4,在 Swift 4 中打印出的 count 是 1。

  • 更快的处理速度

Swift 4 的字符串优化了底层实现,对于英语、法语、德语、西班牙语的处理速度提高了 3.5 倍。对于简体中文、日语的处理速度提高了 2.5 倍。

  • 去掉了characters

  • One-sided Slicing

Swift 4 新增了一个语法糖 ... 可以对字符串进行单侧边界取子串。

Swift 3:

let values = "abcdefg"
let startSlicingIndex = values.index(values.startIndex, offsetBy: 3)
let subvalues = values[startSlicingIndex..<values.endIndex]
// defg
Swift 4:

let values = "abcdefg"
let startSlicingIndex = values.index(values.startIndex, offsetBy: 3)
let subvalues = values[startSlicingIndex...] // One-sided Slicing
// defg
  • String 当做 Collection 来用

Swift 4 中 String 可以当做 Collection 来用,并不是因为 String 实现了 Collection 协议,而是 String 本身增加了很多 Collection 协议中的方法,使得 String 在使用时看上去就是个 Collection。例如:

翻转字符串:

let abc: String = "abc"
print(String(abc.reversed()))
// cba

遍历字符:

let abc: String = "abc"
for c in abc {
    print(c)
}
/*
a
b
c
*/

Map、Filter、Reduce:

// map
let abc: String = "abc"
_ = abc.map {
    print($0.description)
}

// filter
let filtered = abc.filter { $0 == "b" }

// reduce
let result = abc.reduce("1") { (result, c) -> String in
    print(result)
    print(c)
    return result + String(c)
}
print(result)
  • Substring

在 Swift 中,String 的背后有个 Owner Object 来跟踪和管理这个 String,String 对象在内存中的存储由内存其实地址、字符数、指向 Owner Object 指针组成。Owner Object 指针指向 Owner Object 对象,Owner Object 对象持有 String Buffer。当对 String 做取子字符串操作时,子字符串的 Owner Object 指针会和原字符串指向同一个对象,因此子字符串的 Owner Object 会持有原 String 的 Buffer。当原字符串销毁时,由于原字符串的 Buffer 被子字符串的 Owner Object 持有了,原字符串 Buffer 并不会释放,造成极大的内存浪费。

在 Swift 4 中,做取子串操作的结果是一个 Substring 类型,它无法直接赋值给需要 String 类型的地方。必须用 String() 包一层,系统会通过复制创建出一个新的字符串对象,这样原字符串在销毁时,原字符串的 Buffer 就可以完全释放了。

let big = downloadHugeString()
let small = extractTinyString(from: big)

mainView.titleLabel.text = small // Swift 4 编译报错

mainView.titleLabel.text = String(small) // 编译通过

  • 多行字符串字面量

Swift 3 中写很长的字符串只能写在一行。

func tellJoke(name: String, character: Character) {
    let punchline = name.filter { $0 != character }
    let n = name.count - punchline.count
    let joke = "Q: Why does \(name) have \(n) \(character)'s in their name?\nA: I don't know, why does \(name) have \(n) \(character)'s in their name?\nQ: Because otherwise they'd be called \(punchline)."
    print(joke)
}
tellJoke(name: "Edward Woodward", character: "d")

字符串中间有换行只能通过添加 \n 字符来代表换行。

Swift 4 可以把字符串写在一对 """ 中,这样字符串就可以写成多行。

func tellJoke(name: String, character: Character) {
    let punchline = name.filter { $0 != character }
    let n = name.count - punchline.count
    let joke = """
        Q: Why does \(name) have \(n) \(character)'s in their name?
        A: I don't know, why does \(name) have \(n) \(character)'s in their name?
        Q: Because otherwise they'd be called \(punchline).
        """
    print(joke)
}
tellJoke(name: "Edward Woodward", character: "d")

3、Swift 标准库

  • Encoding and Decoding

当需要将一个对象持久化时,需要把这个对象序列化,往常的做法是实现 NSCoding 协议,写过的人应该都知道实现 NSCoding 协议的代码写起来很痛苦,尤其是当属性非常多的时候。几年前有一个工具能自动生成 Objective-C 的实现 NSCoding 协议代码,当时用着还不错,但后来这个工具已经没有人维护很久了,而且不支持 Swift。

Swift 4 中引入了 Codable 帮我们解决了这个问题。

struct Language: Codable {
    var name: String
    var version: Int
}

我们想将这个 Language 对象的实例持久化,只需要让 Language 符合 Codable 协议即可,Language 中不用写别的代码。符合了 Codable 协议以后,可以选择把对象 encode 成 JSON 或者 PropertyList。

Encode 操作如下:

let swift = Language(name: "Swift", version: 4)
if let encoded = try? JSONEncoder().encode(swift) {
    // 把 encoded 保存起来
}

Decode 操作如下:

if let decoded = try? JSONDecoder().decode(Language.self, from: encoded) {
    print(decoded.name)
}
  • Sequence 改进
Swift 3:

protocol Sequence {
    associatedtype Iterator: IteratorProtocol
    func makeIterator() -> Iterator
}
Swift 4:

protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    func makeIterator() -> Iterator
}

由于 Swift 4 中的 associatedtype 支持追加 where 语句,所以 Sequence 做了这样的改进。 Swift 4 中获取 Sequence的元素类型可以不用 Iterator.Element,而是直接取 Element

SubSequence 也做了修改:

protocol Sequence {
    associatedtype SubSequence: Sequence 
        where SubSequence.SubSequence == SubSequence,
              SubSequence.Element == Element
}

通过 where 语句的限定,保证了类型正确,避免在使用 Sequence 时做一些不必要的类型判断。

Collection 也有一些类似的修改。

  • Protocol-oriented integers

整数类型符合的协议有修改,新增了 FixedWidthInteger 等协议,具体的协议继承关系如下:

+-------------+   +-------------+
        +------>+   Numeric   |   | Comparable  |
        |       |   (+,-,*)   |   | (==,<,>,...)|
        |       +------------++   +---+---------+
        |                     ^       ^
+-------+------------+        |       |
|    SignedNumeric   |      +-+-------+-----------+
|     (unary -)      |      |    BinaryInteger    |
+------+-------------+      |(words,%,bitwise,...)|
       ^                    ++---+-----+----------+
       |         +-----------^   ^     ^---------------+
       |         |               |                     |
+------+---------++    +---------+---------------+  +--+----------------+
|  SignedInteger  |    |  FixedWidthInteger      |  |  UnsignedInteger  |
|                 |    |(endianness,overflow,...)|  |                   |
+---------------+-+    +-+--------------------+--+  +-+-----------------+
                ^        ^                    ^       ^
                |        |                    |       |
                |        |                    |       |
               ++--------+-+                +-+-------+-+
               |Int family |-+              |UInt family|-+
               +-----------+ |              +-----------+ |
                 +-----------+                +-----------+
  • Dictionary and Set enhancements

这里简单列一下 Dictionary 和 Set 增强了哪些功能:

通过 Sequence 来初始化 可以包含重复的 Key Filter 的结果的类型和原类型一致 Dictionary 的 mapValues 方法 Dictionary 的默认值 Dictionary 可以分组 Dictionary 可以翻转

  • NSNumber bridging and Numeric types
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231

在 Swift 4 中,把一个值为 999 的 NSNumber 转换为 UInt8 后,能正确的返回 nil,而在 Swift 3 中会不可预料的返回 231。

  • MutableCollection.swapAt(::)

MutableCollection 现在有了一个新方法 swapAt(::) 用来交换两个位置的值,例如:

var mutableArray = [1, 2, 3, 4]
mutableArray.swapAt(1, 2)
print(mutableArray)
// 打印结果:[1, 3, 2, 4]

4、构建过程改进

  • New Build System

Xcode 9 引入了 New Build System,可在 Xcode 9 的 File -> Project Settings... 中选择开启。

  • 预编译 Bridging Headers 文件

对于 Swift 和 Objective-C 混合的项目,Swift 调用 Objective-C 时,需要建立一个 Bridging Headers 文件,然后把 Swift 要调用的 Objective-C 类的头文件都写在里面,编译器会读取 Bridging Headers 中的头文件,然后生成一个庞大的 Swift 文件,文件内容是这些头文件内的 API 的 Swift 版本。然后编译器会在编译每一个 Swift 文件时,都要编译一遍这个庞大的 Swift 文件的内容。

有了预编译 Bridging Headers 以后,编译器会在预编译阶段把 Bridging Headers 编译一次,然后插入到每个 Swift 文件中,这样就大大提高了编译速度。

苹果宣称 Xcode 9 和 Swift 4 对于 Swift 和 Objective-C 混合编译的速度提高了 40%

  • Indexing 可以在编译的同时进行

用 Swift 开发项目时,近几个版本的 Xcode 进行 Indexing 的速度慢的令人发指。Xcode 9 和 Swift 4 在这方面做了优化,可以在编译的同时进行 Indexing,一般编译结束后 Indexing 也会同时完成。

  • COW Existential Containers

Swift 中有个东西叫 Existential Containers,它用来保存未知类型的值,它的内部是一个 Inline value buffer,如果 Inline value buffer 中的值占用空间很大时,这个值会被分配在堆上,然而在堆上分配内存是一个性能比较慢的操作。

Swift 4 中为了优化性能引入了 COW Existential Containers,这里的 COW 就代表 "Copy-On-Write",当存在多个相同的值时,他们会共用 buffer 上的空间,直到某个值被修改时,这个被修改的值才会被拷贝一份并分配内存空间

  • 移除未调用的协议实现
struct Date {
    private let secondsSinceReferenceDate: Double
}

extension Date: Equatable {
    static func ==(lhs: Date, rhs: Date) -> Bool {
        return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
    }
}

extension Date: Comparable {
    static func <(lhs: Date, rhs: Date) -> Bool {
        return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
    }
}

看上面例子,Date 实现了 Equatable 和 Comparable 协议。编译时如果编译器发现没有任何地方调用了对 Date 进行大小比较的方法,编译器会移除 Comparable 协议的实现,来达到减小包大小的目的。

  • 减少隐式 @objc 自动推断

在项目中想把 Swift 写的 API 暴露给 Objective-C 调用,需要增加 @objc。在 Swift 3 中,编译器会在很多地方为我们隐式的加上 @objc,例如当一个类继承于 NSObject,那么这个类的所有方法都会被隐式的加上 @objc。

class MyClass: NSObject {
    func print() { ... } // 包含隐式的 @objc
    func show() { ... } // 包含隐式的 @objc
}

这样很多并不需要暴露给 Objective-C 也被加上了 @objc。大量 @objc 会导致二进制文件大小的增加。

在 Swift 4 中,隐式 @objc 自动推断只会发生在很少的当必须要使用 @objc 的情况,比如:

复写父类的 Objective-C 方法 符合一个 Objective-C 的协议 其它大多数地方必须手工显示的加上 @objc。

减少了隐式 @objc 自动推断后,Apple Music app 的包大小减少了 5.7%。

5、 Exclusive Access to Memory

在遍历一个 Collection 的时候可以去修改每一个元素的值,但是在遍历时如果去添加或删除一个元素就可能会引起 Crash。

例如为 MutableCollection 扩展一个 modifyEach 方法来修改每个元素的值,代码如下:

extension MutableCollection {
    mutating func modifyEach(_ body: (inout Element) -> ()) {
        for index in self.indices {
            body(&self[index])
        }
    }
}

假如在调用 modifyEach 时去删除元素:

var numbers = [1, 2, 3]
numbers.modifyEach { element in
    element *= 2
    numbers.removeAll()
}

就会在运行时 Crash。Swift 4 中引入了 Exclusive Access to Memory,使得这个错误可以在编译时被检查出来。