学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Using Combine 笔记

Using Combine 笔记

这是一本中高级难度的书,主要关注在如何使用 Combine 框架。 本书的内容包括示例代码和测试,都放在 GitHub 的仓库中

Combine 简介

用 Apple 官方的话来说,Combine 是:

a declarative Swift API for processing values over time.

RxSwift 的概念和 API 对应到 Combine 上,表单

Combine 框架 不是 开源的

函数响应式编程

函数响应式编程, 也称为数据流编程

Combine 的特性

Combine 通过嵌入 back-pressure 来扩展函数响应式编程。

Back-pressure 是指订阅者应该控制它一次获得多少信息以及需要处理多少信息。
这带来了高效的数据操作,并且通过流处理的数据量是可控和可取消的。

在这本书中,我将把 Combine 中的一系列组合操作称作 管道。

什么情况使用 Combine

当你想要设置对各种输入做出反应时,Combine 最合适, 用户界面也非常适合这种模式。

在用户界面中使用函数响应式编程的经典示例是表单验证

你可以使用 Combine 执行的一些操作包括:

  1. 你可以设置管道以仅在字段中输入的值有效时启用提交按钮。
  2. 管道还可以执行异步操作(例如检查网络服务)并使用返回的值来选择在视图中更新的方式和内容。
  3. 管道还可用于对用户在文本字段中动态输入做出反应,并根据他们输入的内容更新用户界面视图。

核心概念

这些核心概念是:Publisher and Subscriber、操作符、Subjects

Publisher

当其被订阅之后,根据请求会提供数据, 没有任何订阅请求的发布者不会提供任何数据。当你描述一个 Combine 的发布者时,应该用两种相关的类型来描述它:一种用于输出,一种用于失败。

例如,如果发布者返回了一个 String 类型的实例,并且可能以 URLError 实例的形式返回失败,那么发布者可能会用 <String, URLError> 来描述。

当订阅者发出请求时,许多发布者会立即提供数据。 在某些情况下,发布者可能有一个单独的机制,使其能够在订阅后返回数据。 这是由协议 ConnectablePublisher 来约定实现的。 遵循 ConnectablePublisher 的发布者将有一个额外的机制,在订阅者发出请求后才启动数据流。 这可能是对发布者单独的调用 .connect() 来完成。 另一种可能是 .autoconnect(),一旦订阅者请求,它将立即启动数据流。

订阅者

订阅者负责请求数据并接受发布者提供的数据(和可能的失败)。 订阅者同样被描述为两种关联类型,一种用于输入,一种用于失败。 订阅者发起数据请求,并控制它接收的数据量。 它可以被认为是在 Combine 中起“驱动作用”的,因为如果没有订阅者,其他组件将保持闲置状态,没有数据会流动起来。

当你将订阅者连接到发布者时,两种类型都必须匹配:发布者的输出和订阅者的输入以及它们的失败类型。 将其可视化的一种方法是对两种类型进行一系列并行操作,其中两种类型都需要匹配才能将组件插入在一起。

Combine 中有两个内建的订阅者: AssignSink。 SwiftUI 中有一个订阅者: onReceive

SwiftUI 中的几乎每个 control 都可以充当订阅者。 SwiftUI 中的 View 协议 定义了一个 .onReceive(publisher) 函数,可以把视图当作订阅者使用。 onReceive 函数接受一个类似于 sink 接受的闭包,可以操纵 SwiftUI 中的 @State@Bindings

1
2
3
4
5
6
7
8
9
10
struct MyView : View {

@State private var currentStatusValue = "ok"
var body: some View {
Text("Current status: \(currentStatusValue)")
.onReceive(MyPublisher.currentStatusPublisher) { newStatus in
self.currentStatusValue = newStatus
}
}
}

用弹珠图描述管道

函数响应式编程社区使用一种称为 弹珠图 的视觉描述来说明数据流的变化

怎么看懂弹珠图:

  • 不管周围描述的是什么元素,在该例子的图上,中心是一个操作符。 具体的操作符的名称通常位于中心块上。
  • 上面和下面的线表示随着时间移动的数据, 由左到右。 线上的符号表示离散着的数据。
  • 我们通常假定数据正在向下流动。 在这种情况下,顶线表示对操作符的输入,底线表示输出。
  • 在某些图表中,顶线上的符号可能与底线上的符号不同, 这时图表通常意味着输出的类型与输入的类型不同。
  • 在有些图中,你也可能在时间线上看到竖线 “|” 或 “ X ” 或终结时间线, 这用于表示数据流的结束。 时间线末端的竖线意味着数据流已正常终止。 “X” 表示抛出了错误或异常。

用弹珠图描述 Combine

Back pressure

Combine 的设计使订阅者控制数据流,因此它也控制着在管道中处理数据的内容和时间。 这是一个在 Combine 中被叫做 back-pressure 的特性。

这意味着由订阅者通过提供其想要或能够接受多少信息量来推动管道内数据的处理。 当订阅者连接到发布者时,它会基于特定的 需求 去请求数据。

特定需求的请求通过组成管道进行传递。 每个操作符依次接受数据请求,然后请求与之相连的发布者提供信息。

Subjects

Subjects 是一种遵循 Subject 协议的特殊的发布者。 这个协议要求 subjects 有一个 .send(_:) 方法,来允许开发者发送特定的值给订阅者或管道。

Subjects 可以通过调用 .send(_:) 方法来将值“注入”到流中, 这对于将现有的命令式的代码与 Combine 集成非常有用。

一个 subject 还可以向多个订阅者广播消息。

使用 Combine 进行开发

关于管道运用的思考

在用 Combine 进行开发时,有两种更广泛的发布者模式经常出现:期望发布者返回单一的值并完成,和期望发布者随着时间的推移返回多个值。

当你创建发布者或管道的实例时,好好思考你希望它如何工作是值得的 —— 要么是一次性的,要么是连续的。 你的选择将关系到你如何处理错误,或者你是否要处理操纵事件时序的操作符 (例如 debounce 或者 throttle).

管道和线程

Combine 不是一个单线程的结构。 操作符和发布者可以在不同的调度队列或 runloops 中运行。 构建的管道可以在单个队列中,也可以跨多个队列或线程传输数据。

1
.receive(on: RunLoop.main)

把 Combine 运用到你的开发中

常用模式和方法

用 Future 来封装异步请求以创建一次性的发布者

Future 在创建时立即发起其中异步 API 的调用,而不是 当它收到订阅需求时。 这可能不是你想要或需要的行为。 如果你希望在订阅者请求数据时再发起调用,你可能需要用 Deferred 来包装 Future。

有序的异步操作

通过将任何异步 API 请求与 Future 发布者进行封装,然后将其与 flatMap 操作符链接在一起,你可以以特定顺序调用被封装的异步 API 请求。

通过使用 Future 或其他发布者创建多个管道,使用 zip 操作符将它们合并之后等待管道完成,通过这种方法可以创建多个并行的异步请求。

通过组合这些技术,可以创建任何并行或串行任务的结构。

错误处理

  • 如果管道设置为返回单个结果并终止, 一个很好的例子就是 使用 catch 处理一次性管道中的错误。

catch 处理错误的方式,是将上游发布者替换为另一个发布者,这是你在闭包中用返回值提供的。

请注意,这实际上终止了管道。 如果你使用的是一次性发布者(不创建多个事件),那这就没什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct IPInfo: Codable {
// matching the data structure returned from ip.jsontest.com
var ip: String
}
let myURL = URL(string: "http://ip.jsontest.com")
// NOTE(heckj): you'll need to enable insecure downloads in your Info.plist for this example
// since the URL scheme is 'http'

let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!)
// the dataTaskPublisher output combination is (data: Data, response: URLResponse)
.map({ (inputTuple) -> Data in
return inputTuple.data
})
.decode(type: IPInfo.self, decoder: JSONDecoder())
.catch { err in
return Publishers.Just(IPInfo(ip: "8.8.8.8"))
}
.eraseToAnyPublisher()
  • 在发生暂时失败时重试

当向 dataTaskPublisher 请求数据时,请求可能会失败。 在这种情况下,你将收到一个带有 error 的 .failure 事件。 当失败时,retry 操作符将允许你对相同请求进行一定次数的重试。 当发布者不发送 .failure 事件时,retry 操作符会传递结果值。 retry 仅在发送 .failure 事件时才在 Combine 管道内做出响应。

使用 retry 操作符与 URLSession.dataTaskPublisher 时,请验证你请求的 URL 如果反复请求或重试,不会产生副作用。 理想情况下,此类请求应具有幂等性。 如果没有,retry 操作符可能会发出多个请求,并产生非常意想不到的副作用。

  • 如果管道被设置为持续更新,则错误处理要复杂一点。 这种情况下的一个很好的例子是 使用 flatMap 和 catch 在不取消管道的情况下处理错误。

flatMap 是用于处理持续事件流中错误的操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let remoteDataPublisher = Just(self.testURL!) 
.flatMap { url in
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw TestFailureCondition.invalidServerResponse
}
return data
}
.decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder())
.catch {_ in
return Just(PostmanEchoTimeStampCheckResponse(valid: false))
}
}
.eraseToAnyPublisher()

和 UIKit 或 AppKit 集成

参考

Using combine