iOS&Swift新特新
协议Protocol的关联类型
泛型和关联类型的区别
从泛型
和关联类型
的调用和实现两个方面对比来看:
- 关联类型:实现方指定类型,调用方不指定。当你实现一个使用关联类型的函数的时候,你需要填充对应的类型,所以你知道实际的类型。调用方不知道你具体采用的类型。
- 泛型:调用方指定类型,实现方不指定。当你实现一个使用泛型的函数,不需要知道调用方具体采用的类型。你可以使用约束限制类型,但是你必须处理所有满足约束的类型。调用方指定具体的类型,但是你的代码必须处理传递的任意类型。
从Equatable协议看关联类型
1
2
3
4
5
6
7
8
9
10
public protocol Equatable {
// 运算符重载 ==,这里的self就是关联类型,具体是什么类型,由实现该协议的类进行指定
static func == (lhs: Self, rhs: Self) -> Bool
}
struct Name: Equatable {
let value: String
}
// 这个时候,== 运算符就是
static func == (lhs: Name, rhs: Name) -> Bool
Self就是关联类型
对比OC的NSObjectProtocol
NSObjectProtocol 也有一个 isEqual(_:)方法,但是因为是 OC 的协议,不能用 Self 类型。具体的定义如下:
1
2
3
4
5
6
func isEqual(_ object: Any?) -> Bool
// OC 的协议无法约束参数类型为指定关联类型,因此所有遵守协议的类型都能用来比较。通常在实现中会先检测参数的类型与消息的接收者类型是否一致
func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Name else { return false }
// 开始检查值是否相等
}
Equatable 协议不会做这样的检测,通过 Self 关联类型保证了对象满足条件。
函数中使用关联类型,必须用做泛型
1
2
3
4
5
6
7
8
// 这样用会报错
func checkEquals(left: Equatable, right: Equatable) -> Bool {
return left == right
}
// 需要改成如下
func checkEquals<T: Equatable>(left: T, right: T) -> Bool {
return left == right
}
函数响应式编程范式Combine
主要讲解的是Apple iOS13出的Combine。Publisher,Operator,Subscriber
响应式编程的三方库有RxSwift。
并发Concurrency(async/await)
【WWDC21 10132】认识 Swift 的 Async/Await
OC中异步操作需要封装特别都的回调,写起来会很繁杂。Swift在2021年的Swift5.5中,退出了Swift Concurrency,一套易写,易理解,也更安全的编写并发代码的工具。这个能力挺像TS里面Async/Await
await 的神奇之处就是,可以像写同步调用那样去写异步调用。
直接看例子
我们直接看一个使用了Concurrency改造的例子,一个简单的列表,每行会显示一张存储在服务端的 icon 图片。我们来分析下 icon 图片是如何显示出来的。
- 首先我们根据图片 id 生成网络请求;
- 然后把请求发送给服务器,并等待服务器返回结果;
- 根据下发的 Data 构建 UIImage;
- 最后准备缩略图,并在完成时执行
fetchThumbnail
函数预先注册的completion: @escaping (UIImage?, Error?) → Void
回调。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 原方法
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id) // 根图片id生成request
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completionHandler(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data: data!) else {
completion(nil, FetchError.badImage)
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage)
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
// 改造后
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request) // 一次请求,await结果
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage } // 二次请求
return thumbnail
}
// 我们还可以把一个属性声明成async属性,这样就可以利用 await 来简化异步处理了。
// async属性,需要有明确的getter,并且用 get async 修饰,在其内部可以用 await 返回结果。其次,async 属性不能有 setter,即只能是可读属性
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
总结一下,当你标记一个函数为async
时,同时意味着它可以挂起。在 async 函数中,使用await
关键词标记在哪里可以一次或多次挂起。当 async 函数挂起时,线程并未阻塞,系统会自由安排其他任务。有时后启动的任务,可能被先执行。即你的程序状态可能在挂起时发生显著变化。当 async 函数恢复执行时,其返回的结果会自然融入到 async 函数的调用者,并在先前挂起的地方接续执行。
再看一个例子
一个异步请求操作的,完成回调在Swift中很常见,用于从异步任务中返回,通常与一个结果类型的参数相结合,如下:
1
2
3
func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
// .. 执行数据请求
}
它存在如下集中问题:
你必须确保自己在每个可能的退出方法中调用完成闭包。如果不这样做,可能会导致应用程序无休止地等待一个结果。
闭包代码比较难阅读。与结构化并发相比,对执行顺序的推理并不那么容易。
需要使用弱引用weak references来避免循环引用。
实现者需要对结果进行切换以获得结果。无法从实现层面使用
try catch
语句。
async方法定义
1
2
3
func fetchImages() async throws -> [UIImage] {
// .. 执行数据请求
}
fetchImages
方法被定义为异步且可以抛出异常,这意味着它正在执行一个可失败的异步作业。如果一切顺利,该方法将返回一组图像,如果出现问题,则抛出错误。
await
await就是用来阻塞等待async返回的关键字
结构化并发
如果要用一句话概括,那就是即使进行并发操作,也要保证控制流路径的单一入口和单一出口。程序可以产生多个控制流来实现并发,但是所有的并发路径在出口时都应该处于完成 (或取消) 状态,并合并到一起。
使用 async-await 方法调用的结构化并发,如下:
不是用结构化并发,在完成回调中执行另一个异步方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 调用这个方法
fetchImages { result in
// 3. 异步方法内容返回
switch result {
case .success(let images):
print("Fetched \(images.count) images.")
// 4. 调用 resize 方法
resizeImages(images) { result in
// 6. Resize 方法返回
switch result {
case .success(let images):
print("Decoded \(images.count) images.")
case .failure(let error):
print("Decoding images failed with error \(error)")
}
}
// 5. 获图片方法返回
case .failure(let error):
print("Fetching images failed with error \(error)")
}
}
// 2. 调用方法结束
使用结构化并发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
do {
// 1. 调用这个方法
let images = try await fetchImages()
// 2.获图片方法返回
// 3. 调用 resize 方法
let resizedImages = try await resizeImages(images)
// 4.Resize 方法返回
print("Fetched \(images.count) images.")
} catch {
print("Fetching images failed with error \(error)")
}
// 5. 调用方法结束
async序列
通常下载数据是个异步任务,消耗一定时间。但这里,我们不想等到全部都下载好,相反我们想边接收信息边展示它们。这就要用到新的async/await
特性了。我们处理的是 csv 格式的文件,它是由逗号分隔而成的格式化文本,每一行(line)文本是完整的一行(row)数据。因为由很多行文本组成的 async 序列会释放出它收到的每一行文本,所以我们有机会随着收到数据的进度动态把它们呈现出来,让程序用起来响应迅速跟手。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@main
struct QuakesTool {
static func main() async throws {
let endpointURL = URL(String: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv")!
// 跳过首行 因为是header描述不是地震数据
// 接着遍历提取强度、时间、经纬度信息
for try await event in endpointURL.lines.dropFirst() {
let values = event.split(separator: ",")
let time = values[0]
let latitude = values[1]
let longtitude = values[2]
let magnitude = values[4]
print("Magnitude \(magnitude) on \(time) at \(latitude) \(longtitude)")
}
}
}
结构化并发 Task的使用
看不大懂,有时间再学习下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
struct TaskGroupSample {
func start() async {
print("Start")
// 1
await withTaskGroup(of: Int.self) { group in
for i in 0 ..< 3 {
// 2
group.addTask {
await work(i)
}
}
print("Task added")
// 4
for await result in group {
print("Get result: \(result)")
}
// 5
print("Task ended")
}
print("End")
}
private func work(_ value: Int) async -> Int {
// 3
print("Start work \(value)")
await Task.sleep(UInt64(value) * NSEC_PER_SEC)
print("Work \(value) done")
return value
}
}
Task {
await TaskGroupSample().start()
}
// 输出:
// Start
// Task added
// Start work 0
// Start work 1
// Start work 2
// Work 0 done
// Get result: 0
// Work 1 done
// Get result: 1
// Work 2 done
// Get result: 2
// Task ended
// End
错误Error,多模式
Swift支持throws抛出报错,并由try catch进行捕获的Error处理方式。
在我们日常开发过程中,Error大多都是使用的if else的情况进行,并支持回调。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 用枚举定义错误类型
enum IOError: Error {
case diskError(code:Int)
case networkError(code:Int)
}
// throws标记函数只是抛出异常
func foo(a: Int) throws -> Int {
if a == 0 {
return 0
}
throw IOError.networkError(code: 3)
}
// throws的函数需要使用try进行执行
do {
try print(foo(a: 1))
} catch let err { // 捕捉到错误
print("error occurred \(err)")
}
// 使用 switch
do {
try print(foo(a: 1))
} catch let err as IOError {
switch err {
case .diskError(let code), .networkError(let code):
print("Caught error:\(code)")
}
} catch {
print("Unexpected Error")
}
swift5.3之后支持多模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
do {
try print(foo(a: 1))
} catch IOError.diskError(let code), IOError.networkError(let code){
print("Caught error:\(code)")
} catch {
print("Unexpected Error")
}
enum FooBarError: Error {
case fooError(first:String, second:Int)
}
func myFunc() {
do {
try print(foo(a: 1))
} catch IOError.diskError(let code) where code % 2 == 1, FooBarError.fooError(_, let code){
print("Caught error:\(code)")
} catch {
print("Unexpected Error")
}
}
参考文档:
Swift 5.3 新特性精讲(2):多模式catch子句,不再麻烦switch
swift 错误处理do catch try try! defer
枚举
OC的枚举只能是一个整型的数字,但是Swift的枚举变成了一个独立的类型。
- 指定成员的类型,如Int、String以及任意类型
- 定义方法和计算属性
- 获取枚举所有成员的数组,Direction.allClass
- 关联值,可以使用元组来存储数据,配合模式匹配可以大幅减少代码,并且支持泛型关联值。
- 作为一个类型,可以实现协议,定义扩展
- 实现递归枚举,可以很方便的表示类似列表或者树的结构,配合递归函数降低理解成本。
1
2
3
4
5
6
7
8
// 可以定义String类型的枚举
enum Area: String {
case DG = "dongguan"
case GZ = "guangzhou"
case SZ = "shenzhen"
}
// Area.DG对应的值
print(Area.DG.rawValue)
关联值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Trade {
case Buy(stock:String,amount:Int)
case Sell(stock:String,amount:Int)
}
let trade = Trade.Buy(stock: "003100", amount: 100)
switch trade {
case .Buy(let stock,let amount):
print("stock:\(stock),amount:\(amount)")
case .Sell(let stock,let amount):
print("stock:\(stock),amount:\(amount)")
default:
()
}
方法和属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Device {
case iPad, iPhone, AppleTV, AppleWatch
// 方法
func introduced() -> String {
switch self {
case .iPad: return "iPad"
case .iPhone: return "iPhone"
case .AppleWatch: return "AppleWatch"
case .AppleTV: return "AppleTV"
}
}
}
print(Device.iPhone.introduced())
enum Device {
case iPad, iPhone
// 属性 增加一个存储属性到枚举中不被允许,但你依然能够创建计算属性
var year: Int {
switch self {
case iPhone: return 2007
case iPad: return 2010
}
}
}
静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Device {
case iPad, iPhone, AppleTV, AppleWatch
func introduced() -> String {
switch self {
case .iPad: return "iPad"
case .iPhone: return "iPhone"
case .AppleWatch: return "AppleWatch"
case .AppleTV: return "AppleTV"
}
}
static func fromSlang(term: String) -> Device? {
if term == "iWatch" {
return .AppleWatch
}
return nil
}
}
print(Device.fromSlang(term: "iWatch"))
Codable
Swift4.0发布的能力,和RestKit能力类似。用的最多就是进行 JSON 和数据模型的相互转换了。
1
2
3
typealias Codable = Decodable & Encodable
let encoder = JSONEncoder()
let decoder = JSONDecoder()
在数据模型的成员变量中,基本数据类型如:String、Int、Float等都已经通过 extension
实现了 Codable 协议,因此如果你的数据类型只包含这些基本数据类型的属性,只需要在类型声明中加上 Codable 协议就可以了,不需要写任何实际实现的代码。
自定义类的类型需要继承Codable。
能做到:
- 类型参数同json的key不一致的映射,使用实现CodingKeys枚举
实现原理:Decodable的实现。使用Codable没有去做这些是因为编译器帮我们做了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct User: Codable {
var name: String
var age: Int
// 编译器合成
enum CodingKeys: String, CodingKey {
case name
case age
}
// 编译器合成
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
}
// 编译器合成
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
}
}
https://cloud.tencent.com/developer/article/2065918
https://www.jianshu.com/p/5e701e0517ca