学计算机的那个

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

0%

SwiftUI 与 Combine 中

publisher 和常见 Operator

Publisher 的事件序列在普通元素序列的基础上增加了一个时间维度,但它们在“有序元素”这一特性上是一致的,这让大多数针对普通序列的操作可以等价对应到 Publisher 的事件序列上。

对于任意 Publisher,我们总是可以把它想象为一个“异步数组”,它表示随着时间发生的一系列值

reduce 和 scan

scan 一个最常见的使用场景是在某个下载任务执行期间,接受 URLSession 的数据回调,将接收到的数据量做累加来提供一个下载进度条的界面

compactMap 和 flatMap

compactMap 比较简单,它的作用是将 map 结果中那些 nil 的元素去除掉

mapcompactMap 的闭包返回值是单个的 Output 值。而与它们不同,flatMap 的变形闭包里需要返回一个 Publisher,
flatMap 将会涉及两个 Publisher
一个是 flatMap 操作本身所作用的外层 Publisher,一个是 flatMap 所接受的变形闭包中返回的内层 Publisher。
flatMap 将外层 Publisher 发出的事件中的值传递给内层 Publisher,然后汇总内层 Publisher 给出的事件输出,作为最终变形后的结果。

1
2
3
4
5
6
7
check("Flat Map 1") {
[[1, 2, 3], ["a", "b", "c"]]
.publisher
.flatMap {
$0.publisher
}
}

组成外层 Publisher 的是一个数组的数组,它含有两个元素,分别是 [1, 2, 3] 和 [“a”, “b”, “c”]。
在被订阅后,这个外层 Publisher 会发送两个 Output 事件 (两个事件的值分别是 [1, 2, 3] 和 [“a”, “b”, “c”]),
每个事件的值被 flatMap 传递到内层,并通过 $0.publisher 生成新的 Publisher 并返回。
内层 Publisher 实际上是 [1, 2, 3].publisher 和 [“a”, “b”, “c”].publisher,它们发送的值将被作为 flatMap 的结果,被“展平” (flatten) 后发送出去。

通过这种由外层 Publisher 提供数据源,内层 Publisher 提供控制方式的方法,可以将两个甚至是多个 Publisher 的行为关联起来,形成具有更复杂逻辑的全新 Publisher。

1
2
3
4
5
6
7
8
9
check("Flat Map 2") {
["A", "B", "C"]
.publisher
.flatMap { letter in
[1, 2, 3]
.publisher
.map { "\(letter)\($0)" }
}
}

将多个 Publisher 进行合并,形成一个新的 Publisher 的操作,其核心目的在于“降维”。

flatMap,以及马上会在本章稍后部分看到的 zip,combineLatest 都属于这类“降维”操作。

flatMap 的目的,正是将这类多个异步操作展平为单个事件流

错误处理

map 对 Output 进行转换,mapError 对 Failure 进行转换

抛出错误

tryMap(tryScan,tryFilter,tryReduce等) 可以将处理数据时发生的错误转变为标志事件流失败的结束事件,接下来的 事件 也不再会被计算和发布。

从错误中恢复

在 Combine 里,有一些 Operator 是专门帮助事件流从错误中恢复的,最简单的是 replaceError,它会把错误替换成一个给定的值,并且立即发送 finished 事件

replaceError 会将 Publisher 的 Failure 类型抹为 Never,这正是我们使用 assign 来将 Publisher 绑定到 UI 上时所需要的 Failure 类型。

replaceError 在错误时接受单个值,另一个操作 catch 则略有不同,它接受的是一个新的 Publisher

Publisher 的类型系统

Operator 其实是 Publisher 的 extesnion 中所提供的一些方法,比如 flatMap 或者 map,它们都作用于 Publisher。如果你注意观察,会发现这些方法返回的也是 Publisher,不过类型不尽相同

flatMap,map,以及其他各种我们称之为 Operator 的方法,其实都只是对应的 Publishers 类型初始化方法的简便调用方式。除此之外,它们作用于 Publisher,也返回 Publisher 的特性,让我们最终能够通过简洁易读的链式连续调用来构建发布者转换逻辑。

操作符熔合

1
2
3
4
[1, 2, 3].publisher.map { $0 * 2 }
// Publishers.Sequence<[Int], Never>
Just(10).map { String($0) }
// Just<String>

map 操作的返回结果的类型并不是 Publishers.Map,这是由于 Publishers.SequenceJust 在各自的扩展中对默认的 Publisher 的 map 操作进行了重写。由于 Publishers.Sequence 和 Just 这样的类型在编译期间我们就能确定它们在被订阅时就会同步地发送所有事件,所以可以将 map 的操作直接作用在输入上,而不需要等待每次事件发生时再去操作。这种将操作符的作用时机提前到创建 Publisher 时的方式,被称为操作符熔合 (operator fusion)

响应式编程边界

Combine 框架中为我们提供了 Subject 角色,来把传统指令式编程转换到响应式世界。

Subject行为

Subject 也是 Combine 框架中的一个协议,它为我们提供了从外界发送数据的方式。

1
2
3
4
5
public protocol Subject : AnyObject, Publisher {
func send(_ value: Self.Output)
func send(completion: Subscribers.Completion<Self.Failure>)
func send(subscription: Subscription)
}

PassthroughSubject 简单地将 send 输入的内容如实反馈,而 CurrentValueSubject 则保留一个最后的值,并在被订阅时将这个值作为事件发送。

zip

merge 和 flatMap:前者简单地将两者合并,后者则是外层提供输入,内层转换输出。

对普通的 Sequence,Swift 标准库中也提供了 zip 操作,它将 Sequence 和 Sequence 的两个序列合并成一个元素类型为多元组 (Element1, Element2) 的序列。zip 将从两个序列中取出 index 相同的元素,把它们组合为多元组,然后放到返回的序列中去:

1
2
zip([1, 2, 3, 4, 5], ["A", "B", "C", "D"])
// [(1, "A"), (2, "B"), (3, "C"), (4, "D")]

Publisher 中的 zip 和 Sequence 的 zip 相类似:它会把两个 (或多个) Publisher 事件序列中在同一 index 位置上的值进行合并

zip 在时序语义上更接近于“当…且…”,当 Publisher1 发布值,且 Publisher2 发布值时,将两个值合并,作为新的事件发布出去。在实践中,zip 经常被用在合并多个异步事件的结果,比如同时发出了多个网络请求,希望在它们全部完成的时候把结果合并在一起。

combineLatest

combineLatest的语义接近于“当…或…”,当 Publisher1 发布值,或者 Publisher2 发布值时,将两个值合并,作为新的事件发布出去。

在实践中,combineLatest 被用来处理多个可变状态,在其中某一个状态发生变化时,获取这些全部状态的最新值。比如你的 UI 上有多个 TextField,你可能想要在其中某一个值变动时获取到所有 TextField 中的值并对它们进行检查 (没错,我说的就是用户注册)。

响应式和指令式的桥梁

Future

如果我们希望订阅操作和值的发布是异步行为,不在同一时间发生的话,可以使用 Future。Future 提供了一种方式,可以让我们创建一个接受未来的事件的 Publisher。

单次事件的网络请求

使用Subject

如果你的异步 API 有可能不发送任何一个值,而是可能发布两个或更多的值的话,你会需要一个更加一般性的 Publisher 类型来把指令式程序转换为响应式程序,这个类型就是 Subject。

可以发布多次事件

Foundation 中的 Publisher

Combine 中存在 @Published 封装,用来把一个 class 的属性值转变为 Publisher。它同时提供了值的存储和对外的 Publisher (通过投影符号 $ 获取)。在被订阅时,当前值也会被发送给订阅者,它的底层其实就是一个 CurrentValueSubject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Wrapper {
@Published var text: String = "hoho"
}
var wrapper = Wrapper()
check("Published") {
wrapper.$text
}
wrapper.text = "123"
wrapper.text = "abc"
// 输出:
// ----- Published -----
// receive subscription: (CurrentValueSubject)
// request unlimited
// receive value: (hoho)
// receive value: (123)
// receive value: (abc)

在 ObservableObject 的场景中,@Published 被用来自动生成对应的 Publisher 并处理 objectWillChange 的调用。

订阅和绑定

通过sink订阅Publisher事件

sink的函数签名如下:

1
2
3
4
5
func sink(receiveCompletion: 
@escaping ((Subscribers.Completion<Self.Failure>) -> Void),
receiveValue:
@escaping ((Self.Output) -> Void)
) -> AnyCancellable

一个完整的“发布-订阅”流程如下所示:

通过assign绑定Publisher值

Subscribers.Assign,它可以用来将 Publisher 的输出值通过 key path 绑定到一个对象的属性上去。在 SwiftUI 中,这种值通常会是 ObservableObject 中的属性值,它进一步会被用来驱动 View 的更新。

注意 assign 所接受的第一个参数的类型为 ReferenceWritableKeyPath,也就是说,只有 class 上用 var 声明的属性可以通过 assign 来直接赋值。

assign 的另一个“限制”是,上游 Publisher 的 Failure 的类型必须是 Never。如果上游 Publisher 可能会发生错误,我们则必须先对它进行处理,比如使用 replaceError 或者 catch 来把错误在绑定之前就“消化”掉。

Publisher 的引用共享

设想我们有一个涉及到网络请求的界面,我们用 isSuccess 来代表网络请求成功返回,HTTP 状态码为 200 的情况;用 text 来代表 response 中的返回数据:

1
2
3
4
5
6
7
8
9
10
11
12
class LoadingUI {
var isSuccess: Bool = false
var text: String = ""
}

let dataTaskPublisher = URLSession.shared
.dataTaskPublisher(
for: URL(string: "https://httpbin.org/get?foo=bar")!)

let ui = LoadingUI()
var token1 = isSuccess.assign(to: \.isSuccess, on: ui)
var token2 = latestText.assign(to: \.text, on: ui)

如果我们检查实际发生的请求,会看到发生了两次对目标 URL 的访问:

“想要改变这个行为,可以将值语义的 dataTaskPublisher 转变为引用语义 (reference semantics)。我们只要在创建 dataTaskPublisher 后加上 share() 即可。通过 share() 操作,原来的 Publisher 将被包装在 class 内,对它的进一步变形也会适用引用语义:

Cancellable,AnyCancellable和内存管理

对于 Cancellable,我们需要在合适的时候主动调用 cancel() 方法来完结

和 Cancellable 这个抽象的协议不同,AnyCancellable 是一个 class,这也赋予了它对自身的生命周期进行管理的能力。

对于一般的 Cancellable,例如 connect 的返回值,我们需要显式地调用 cancel() 来停止活动,但 AnyCancellable 则在自己的 deinit 中帮我们做了这件事。

在实际里,我们一般会把这个 AnyCancellable 设置为所在实例 (比如 UIViewController) 的存储属性。这样,当该实例 deinit 时,AnyCancellable 的 deinit 也会被触发,并自动释放资源。