文章

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)

Swift中的async/await

【WWDC21 10132】认识 Swift 的 Async/Await

闲话Swift协程

OC中异步操作需要封装特别都的回调,写起来会很繁杂。Swift在2021年的Swift5.5中,退出了Swift Concurrency,一套易写,易理解,也更安全的编写并发代码的工具。这个能力挺像TS里面Async/Await

await 的神奇之处就是,可以像写同步调用那样去写异步调用。

直接看例子

我们直接看一个使用了Concurrency改造的例子,一个简单的列表,每行会显示一张存储在服务端的 icon 图片。我们来分析下 icon 图片是如何显示出来的。

  1. 首先我们根据图片 id 生成网络请求;
  2. 然后把请求发送给服务器,并等待服务器返回结果;
  3. 根据下发的 Data 构建 UIImage;
  4. 最后准备缩略图,并在完成时执行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

枚举

Swift枚举的全用法

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

SwiftUI

本文由作者按照 CC BY 4.0 进行授权