当前位置: 首页 > 工具软件 > Image Combine > 使用案例 >

SwiftUI 和 Combine 的学习:一

程承恩
2023-12-01

简介

命令式和声明式编程

如果说指令式是教会计算机 “怎么做”, 那么声明式就是告诉计算机要 “做什么”。指令式编程是描述过程,期望程序执行以 得到我们想要的结果;而声明式编程则是描述结果,让计算机为我们考虑和组织出 具体过程,最后得到被描述的结果。
现代语言中,一般使用函数式编程或者 DSL 的方式来实现声明式的编程方式。

声明式 UI

一般来说,View = f(State) 中的函数 f 是纯函数,也就是对于某个特定的输入 State, 所对应的 View 是确定的,不随其他变量而改变。我们可以单纯地通过控制和改变 State 来得到确定的 UI,这是使用声明式的方法来构建 UI 的基础。

SwiftUI 和 Combine 简介

SwiftUI 和 Combine 都是在 WWDC 2019 上 Apple 公布的开发框架,它们都是由纯 Swift 编写的。前者是一个声明式 UI 的用户界面开发框架,后者是基于响应式编程, 用于处理数据流的框架。

SwiftUI 基础

Modifier

按照这个定义,大致来说,view modifier 分为两种类别:

→ 像是 font,foregroundColor 这样定义在具体类型 (比如例中的 Text) 上,然后 返回同样类型 (Text) 的原地 modifier。

→ 像是 padding,background 这样定义在 View extension 中,将原来的 View 进行包装并返回新的 View 的封装类 modifier。

原地 modifier 一般来说对顺序不敏感,对布局也不关心,它们更像是针对对象 View 本身的属性的修改。而与之相反,封装类的 modifier 的顺序十分重要。

多尺寸的预览

数据状态和绑定

在 SwiftUI 里,用户界面是严格被数据驱动的:在运行时,任何对于界面的修改,都只能通过修 改数据来达成,而不能直接对界面进行调整和操作。
相比于传统的 UIKit 或 AppKit, 这在一定程度上对灵活性进行了限制,强制了我们必须使用更合理的数据处理方式。 不过另一方面,它也规范了 SwiftUI 中数据流动的方式,让开发者更不容易犯错。

投影属性

在 Swift 5.1 中,对一个由 @ 符号 修饰的属性,在它前面使用 $ 所取得的值,被称为投影属性 (projection property)。 有些 @ 属性,比如这里的 @State 和 @Binding,它们的投影属性就是自身所对应 值的 Binding 类型。不过要注意的是,并不是所有的 @ 属性都提供 $ 的投影访问方 式。

propertyWrapper

init(initialValue:),wrappedValue 和 projectedValue 构成了一个 propertyWrapper 最重要的部分。

几乎所有依赖 getter 和 setter,并需要多次重复同样代码的地方,都可以用属性包 装的方式得到更好的解决方式。

操作回溯和数据共享

@State 非常适合 struct 或者 enum 这样的值类型,它可以自动为我们完成从状态 到 UI 更新等一系列操作。但是它本身也有一些限制,我们在使用 @State 之前,对 于需要传递的状态,最好关心和审视下面这两个问题:

这个状态是属于单个 View 及其子层级,还是需要在平行的部件之间传递和使 用?@State 可以依靠 SwiftUI 框架完成 View 的自动订阅和刷新,但这是有 条件的:对于 @State 修饰的属性的访问,只能发生在 body 或者 body 所调 用的方法中。你不能在外部改变 @State 的值,它的所有相关操作和状态改变 都应该是和当前 View 挂钩的。如果你需要在多个 View 中共享数据,@State 可能不是很好的选择;如果还需要在 View 外部操作数据,那么 @State 甚至 就不是可选项了。

状态对应的数据结构是否足够简单?对于像是单个的 Bool 或者 String, @State 可以迅速对应。含有少数几个成员变量的值类型,也许使用 @State 也还不错。但是对于更复杂的情况,例如含有很多属性和方法的类型,可能其 中只有很少几个属性需要触发 UI 更新,也可能各个属性之间彼此有关联,那 么我们应该选择引用类型和更灵活的可自定义方式。

ObservableObject

如果说 @State 是全自动驾驶的话,ObservableObject 就是半自动,它需要一些额 外的声明。ObservableObject 协议要求实现类型是 class,它只有一个需要实现的
属性:objectWillChange。在数据将要发生改变时,这个属性用来向外进行 “广播”, 它的订阅者 (一般是 View 相关的逻辑) 在收到通知后,对 View 进行刷新。

创建 ObservableObject 后,实际在 View 里使用时,我们需要将它声明为 @ObservedObject。这也是一个属性包装,它负责通过订阅 objectWillChange 这个 “广播”,将具体管理数据的 ObservableObject 和当前的 View 关联起来。

一般情况下,我们使用一个 PassthroughSubject 实例作为 objectWillChange 的值。

使用 @Published 和自动生成

如果在 model 中有很多 属性,我们将需要为它们一一添加 willSet,这无疑是非常麻烦,而且全是重复的模 板代码。实际上,如果我们省略掉自己声明的 objectWillChange,并把属性标记为 @Published,编译器将会帮我们自动完成这件事情。

@EnvironmentObject

@EnvironmentObject var model: CalculatorModel
ContentView().environmentObject(CalculatorModel())

总结

根据适用范围和存储 状态的复杂度的不同,需要选取合适的方案。@State 和 @Binding 提供 View 内部 的状态存储,它们应该是被标记为 private 的简单值类型,仅在内部使用。 ObservableObject 和 @ObservedObject 则针对跨越 View 层级的状态共享,它可以 处理更复杂的数据类型,其引用类型的特点,也让我们需要在数据变化时通过某种手 段向外发送通知 (比如手动调用 objectWillChange.send() 或者使用 @Published), 来触发界面刷新。对于 “跳跃式” 跨越多个 View 层级的状态,@EnvironmentObject 能让我们更方便地使用 ObservableObject,以达到简化代码的目的。

SwiftUI 进阶

使用内置图标库 (SF Symbols)

SF Symbols 是从 iOS 13 和 macOS 10.15 开始内置于系统中的字符图标库,它提供 了上千种常见的线条图标,而且我们可以任意地为它们设置尺寸,颜色等属性。 Apple 甚至准备了专门的 app 来帮助你查看可用的符号:

Image(systemName: "star")
      .font(.system(size: 25))
      .foregroundColor(.white)
      .frame(width: 30, height: 30) 

自定义 View Modifier

ViewModifier 是 SwiftUI 提供的一个协议,它只有一个要求实现的方法:

public protocol ViewModifier {
func body(content: Self.Content) !" Self.Body
}
struct ToolButtonModifier: ViewModifier { 
func body(content: Content) -> some View {
    content
      .font(.system(size: 25))
      .foregroundColor(.white)
      .frame(width: 30, height: 30)
} }
Button(action: { print("fav") }) {
  Image(systemName: "star")
    .modifier(ToolButtonModifier())
}

渐变背景

.background(
  ZStack {
    RoundedRectangle(cornerRadius: 20)
      .stroke(model.color, style: StrokeStyle(lineWidth: 4))
    RoundedRectangle(cornerRadius: 20)
      .fill(
        LinearGradient(
          gradient: Gradient(colors: [.white, model.color]),
          startPoint: .leading,
          endPoint: .trailing
)
) }
)

动画

隐式动画
.animation(.default)

.animation(
  Animation
.linear(duration: 0.5)
.delay(0.2) .repeatForever(autoreverses: true)
) )
// 毫无意义的动画,请不要用在实际项目里!

隐式动画的作用范围很大:只要这个 View 甚至是它的子 View 上的可动画属性发生 变化,这个动画就将适用。

显式动画

显式动画通过明确的 withAnimation 调用触发

withAnimation(.default)

withAnimation(
      .spring(
        response: 0.55,
        dampingFraction: 0.425,
blendDuration: 0
      )
) {
self.expanded.toggle() 
}

包装 UIView 类型

protocol UIViewRepresentable : View {
associatedtype UIViewType : UIView
func makeUIView(context: Self.Context) -> Self.UIViewType 
func updateUIView(
_ uiView: Self.UIViewType,
    context: Self.Context
  )
}
extension View {
func blurBackground(style: UIBlurEffect.Style) -> some View {
	ZStack {
		BlurView(style: style) self
	}
	}
}

Combine 和异步编程

传统异步编程

传统的 Cocoa 和 UIKit 提供了 一系列的异步 API,它们往往以下面某种形式出现:

→ 闭包回调:在调用需要耗时的方法时,提供一个闭包用以接收完成回调。耗时 方法本身会被放到主线程之外执行;在执行完毕后,调用这个闭包来通知调用 者任务已经完成。这是现代 Cocoa 开发中最常见的异步方式,比如 URLSession 就提供了闭包的网络请求方法: dataTask(with:completionHandler:) 中的 completionHandler 就是这样的 闭包。

→ Delegate:Protocol-Delegate是Cocoa中的另一种常见设计模式。我们通 过定义一个 protocol 和其中的方法,来描述异步行为可能发生的结果。在使 用时,创建一个满足该 protocol 的 delegate,并把它设置为异步行为结果的 接收者。在异步行为完成后,被调用方检查 delegate 是否存在,并尝试调用 对应的方法来通知异步行为完成。像是 UITableView 中相关的 UITableViewDataSource 和 UITableViewDelegate 都是这种模式的体现。另 外,URLSession 除了提供基于闭包回调的方法以外,也存在基于 protocol-delegate 的另一套 API。

→ Notification:在异步操作完成时,可以通过NotificationCenter的相关API 发送一个通知 (Notification),如果有观察者注册想要接收该通知,那么这个 通知将被传递给观察者并调用相关代码。这是 Cocoa 中的另一种常见模式 — 观察者 (observer) 模式。大部分 UI 相关的事件,比如键盘显示或消失、文本 框中内容的变更等,都提供了可订阅的通知。

这三种异步 API 的方式各自有自己的使用场景,并且互为补充:如果事件比较单纯, 不需要关心过程细节,只需要响应结果的话,闭包回调提供了最简单的异步方案;如 果希望能控制更多细节,或者需要关心多种异步事件时,闭包 API 会存在一些设计上 的困难,这时选择 delegate 会更方便;对于那些触发时机不确定的,可能长期存在 的行为所对应的事件,使用监听通知的方式最为合适。

响应式异步编程模型

在遇到一系列相关联的繁琐复杂的具体问题时,一种常见的解决方式是寻找这些问 题的共同点,并使用更上层的抽象来总结出对各个问题都通用的方案,对其进行简 化。在异步编程中,不论采用闭包,delegate 还是 notification,实际上都是在当前 的时间节点上预先设置好特定的逻辑,去处理未来会发生的事件。抛开不同 API 的 定义所产生的表象,异步编程的本质是响应未来发生的事件流。那么,我们其实完全 可以用一种通用的方式来 “抹去” 不同异步 API 的区别,让事件发生这一核心概念暴 露出来。在异步操作中某个事件发生时,把这个事件和与其相关的数据 “发布” 出来。 而对这个事件感兴趣的代码可以订阅这个事件,来进行后续操作。

事件发布 --> 操作变形 --> 订阅使用

至此,我们可以总结一下响应式异步编程的抽象和特点:异步操作在合适的时机发 布事件,这些事件带有数据。接下来,我们使用一个或多个操作来处理这些事件以及 内部的数据。在末端,会有一个订阅者来 “消化” 处理后的事件和数据,并进一步驱 动程序的其他部分 (比如 UI 界面) 的运行。上面这些对于事件和数据的操作,以及末 端的订阅,都是在事件发生之前完成的。在一开始的时候,我们就将这些描述清楚, 之后它便可以以预设的方式响应源源不断发生的事件流。

Combine 基础

Combine 中最重要的角色有三 种,恰好对应了这三种操作:负责发布事件的 Publisher,负责订阅事件的 Subscriber,以及负责转换事件和数据的 Operator

Swift 提倡使 用面向协议编程的方式,Combine 中包括 Publisher 在内的一系列角色都使用协议
来进行定义,也正是这一思想的具体体现。

Publisher

事件发布
public protocol Publisher { associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where
    S : Subscriber,
    Self.Failure !" S.Failure,
    Self.Output !" S.Input
}

Publisher 最主要的工作其实有两个:发布新的事件及其数据,以及准备好被 Subscriber 订阅。

Publisher 可以发布三种事件:

  1. 类型为 Output 的新值:这代表事件流中出现了新的值。
  2. 类型为 Failure 的错误:这代表事件流中发生了问题,事件流到此终止。
  3. 完成事件:表示事件流中所有的元素都已经发布结束,事件流到此终止。
有限事件流和无限事件流

我们将 最终会终结的事件流称为有限事件流,而将不会发出 failure 或者 finished 的事件流 称为无限事件流

Operator

客户端的响应式编程中,由状态驱动 UI 是最核心的思想。
在响应式编程中,绝大部分的逻辑和关键代码的编写,都发生在数据处理和变形中。

let buttonClicked: AnyPublisher<Void, Never> buttonClicked
.scan(0) { value, _ in value + 1 } .map { String($0) }

Subscriber

public protocol Subscriber { 
	associatedtype Input associatedtype Failure : Error
	func receive(subscription: Subscription)
	func receive(_ input: Self.Input) !" Subscribers.Demand
	func receive(completion: Subscribers.Completion<Self.Failure>) 
}
func sink( receiveCompletion:
@escaping ((Subscribers.Completion<Self.Failure>) !" Void), receiveValue:
@escaping ((Self.Output) !" Void) ) !" AnyCancellable

如果你是想要让数据继续在 SwiftUI 的声明式的世界中来驱动 UI 的话,另一个 Subscriber 可能会更为简洁常用, 那就是 assign。

func assign<Root>(
to keyPath: ReferenceWritableKeyPath<Root, Self.Output>,
  on object: Root
) !" AnyCancellable

也就是说,只有那些 class 类型的实例中的属性能被绑定。在 SwiftUI 中,代表 View 对应的模型的 ObservableObject 接口只能由 class 修饰的类型来实现,这也 正是 assign 最常用的地方。

其他角色

除此之外,Combine 框架中还有两个比较重要的概念,那就是 Subject 和 Scheduler。

Subject
public protocol Subject : AnyObject, Publisher {
	func send(_ value: Self.Output)
	func send(completion: Subscribers.Completion<Self.Failure>)
}

从定义可以看到,Subject 暴露了两个 send 方法,外部调用者可以通过这两个方法 来主动地发布 output 值、failure 事件或 finished 事件。如果我们说 sink 提供了由 函数响应式向指令式编程转变的路径的话,Subject 则补全了这条通路的另一侧:它 让你可以将传统的指令式异步 API 里的事件和信号转换到响应式的世界中去。

Combine 内置提供了两种常用的 Subject 类型,分别是 PassthroughSubjectCurrentValueSubject

PassthroughSubject 简单地将 send 接收到的事件转发给 下游的其他 Publisher 或 Subscriber

CurrentValueSubject 则会包装和持有一个值,并在 设置该值时发送事件并保留新的值。在订阅发生的瞬间,CurrentValueSubject 会把 当前保存的值发送给订阅者。

let publisher3 = CurrentValueSubject<Int, Never>(0)
print("开始订阅") publisher3.sink(
receiveCompletion: { complete in print(complete)
},
receiveValue: { value in
    print(value)
  }
)
publisher3.value = 1
publisher3.value = 2
publisher3.send(completion: .finished)

Scheduler

如果说 Publisher 决定了发布怎样的 (what) 事件流的话,Scheduler 所要解决的就 是两个问题:在什么地方 (where),以及在什么时候 (when) 来发布事件和执行代码。

关于 where
URLSession.shared
  .dataTaskPublisher(for: URL(string: "https:!"example.com")!)
  .compactMap { String(data: $0.data, encoding: .utf8) }
  .receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
  }, receiveValue: {
    textView.text = $0
})

RunLoop 就是一个实现了 Scheduler 协议的类型,它知道要如何执行后续的订阅任 务。如果没有 receive(on: RunLoop.main) 的话,sink 的闭包将会在后台线程执行, 这在更新 UI 时将带来问题。

关于 when

Scheduler 的另一个重要工作是为事件的发布指定和规划时间。
比较常见的两种操作是 delay 和 debounce。
delay 和 debounce 所接受的参数包括一个 Scheduler 实例。

Publisher 和常见的 Operator

Empty,Just

序列 Publisher 及其操作

Publishers.Sequence<[Int], Never>(sequence: [1, 2, 3])
[1, 2, 3].publisher

顾名思义,Publishers.Sequence 接受一个 Sequence:它 可以是一个数组,也可以是一个 Range。在被订阅时,Sequence 中的元素被逐个发 送出来

际上对于 Array Publisher 来说,map 并不是在 “未来” 才进行 变形。所以严格来说,这里在 Array Publicher 语境下,“在未来 output 事 件发生时再进行变形” 这一说法并不准确。但是对于一般性的 Publisher,这 个结论是正确的。

reduce 和 scan

extension Sequence {
public func scan<ResultElement>(
_ initial: ResultElement,
_ nextPartialResult: (ResultElement, Element) !" ResultElement ) !" [ResultElement] {
var result: [ResultElement] = [] for x in self {
      result.append(nextPartialResult(result.last !" initial, x))
    }
return result }
}

compactMap 和 flatMap

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

removeDuplicates

removeDuplicates 经常被用来减少那些非常消 耗资源的操作,比如由事件触发造成的网络请求或者图片渲染。如果当作为源头的 数据没有改变时,所预期得到的结果也不会变化的话,那么就没有必要去重复这样操 作。在源头将重复的事件移除,可以让下游的事件流也变得简单。

错误处理

转换错误类型

mapError

check("Map Error") {
  Fail<Int, SampleError>(error: .sampleError)
.mapError { _ in MyError.myError } }

抛出错误

tryMap,tryScan,tryFilter,tryReduce

check("Throw") {
  ["1", "2", "Swift", "4"].publisher
.tryMap { s !" Int in
guard let value = Int(s) else {
throw MyError.myError }
return value }
}

从错误中恢复

replaceError

check("Replace Error") {
  ["1", "2", "Swift", "4"].publisher
.tryMap { s -> Int in
	guard let value = Int(s) else {
		throw MyError.myError 
	}
	return value 
}
    .replaceError(with: -1)
}

catch

check("Catch with Just") {
  ["1", "2", "Swift", "4"].publisher
	.tryMap { s -> Int in
		guard let value = Int(s) else {
			throw MyError.myError 
		}
		return value 
	}
	.catch { _ in Just(-1) } 
}

不打断原始数据流

check("Catch and Continue") {
  ["1", "2", "Swift", "4"].publisher
	.flatMap { s in 
		return Just(s)
					.tryMap { s -> Int in
							guard let value = Int(s) else {
								throw MyError.myError 
							}
							return value 
					}
					.catch { _ in Just(-1) }
	 }
}

另外,在每个 Operator 之后,你都 可以加上 print(“TAG”) 来将对应 Publisher 发送的事件添加一个标识 Tag 后打印出 来,这可以帮助你 “逐步” 理解到底发生了什么。

嵌套的泛型类型和类型抹消

eraseToAnyPublisher

Combine 中的其他角色也大都提供了类似的抹消后的类型,比如 AnySubscriber 和 AnySubject 等。在大多数情况下我们都只会关注某个部件所扮演的角色,也即,它 到底是一个 Publisher 还是一个 Subscriber。一般我们并不关心具体的类型,因为 对 Publisher 的变形往往都伴随着类型的变化。通过类型抹消,可以让事件的传递和 订阅操作变得更加简单,对外的 API 也更加稳定。

let p1_ = Publishers.FlatMap(
upstream: [[1, 2, 3], [4, 5, 6]].publisher, maxPublishers: .unlimited)
{
  $0.publisher
}
let p2_ = Publishers.Map(upstream: p1_) { $0 * 2 }

let p3 = p2.eraseToAnyPublisher() 
// p3: AnyPublisher<Int, Never>

操作符熔合

可能你已经发现了,有时候 map 操作的返回结果的类型并不是 Publishers.Map,比 如下面的两个例子:

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

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

其他的 Operator

filter, contains, prefix, drop, replaceNil, replaceEmpty, min, max, allSatisfy, collect

使用 merge 整合事件流

The merged publisher continues to emit elements until all upstream publishers finish. If an upstream publisher produces an error, the merged publisher fails with that error.

响应式编程的边界

在前两章我们花了不少篇幅介绍了 Combine 中最重要的部分:Publisher 和 Operator,以及如何使用常见的 Operator 来构建出我们最终想要的逻辑。这是响应 式编程的主轴和重点,是我们编写类似程序时的核心部分。但是,距离实际在开发中 使用响应式编程,我们还有两个重要的问题没有解决,它们涉及的核心话题是响应式 编程的边界在哪里;或者说,响应式的程序要如何与剩余部分互动。
第一个问题:最初的 Publisher 从何而来?在前面的章节里,我们使用的都是由一个 数组生成的 Publishers.Sequence 类型进行举例。这样的 Publisher 用来做学习示 例自然是再适合不过了,但是实际的 app 开发中应该鲜有类似情况。Combine 框架 真正有意义的使用情景是那些涉及异步操作,会产生事件流的地方。像是网络请求, 用户输入等等。Combine 框架中为我们提供了 Subject 角色,来把传统指令式编程 转换到响应式世界。Foundation 也提供了一系列便利的方式,来获取初始 Publisher。我们在本章中会看到它们的使用方式。
如果说第一个问题是 “从哪里来”,那么第二个问题可以归结为 “到哪里去”:经过 Operator 转换的 Publisher 最终需要被 Subscriber 订阅,并用来驱动 app 逻辑或者 UI。另外,关于 Publisher 共享和内存利用等方面,我们也还要做一些特别的说明。

时序

Subject 严格遵守时序的特性,让我们有机会同时验证那些对时序敏感的多个 Publisher 的组合。

zip

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

combineLatest

它的语义接近于 “当…或…”,当 Publisher1 发布 值,或者 Publisher2 发布值时,将两个值合并,作为新的事件发布出去。
不论是哪个输入 Publisher,只要发生了新的事件,combineLatest 就把新发生的事 件值和另一个 Publisher 中当前的最新值合并。
在实践中,combineLatest 被用来处理多个可变状态,在其中某一个状态发生变化 时,获取这些全部状态的最新值。比如你的 UI 上有多个 TextField,你可能想要在其 中某一个值变动时获取到所有 TextField 中的值并对它们进行检查 (没错,我说的就 是用户注册)。

响应式和指令式的桥梁

Future:Future 只能为我们提供一次性 Publisher:对于提供的 promise,你只 有两种选择:发送一个值并让 Publisher 正常结束,或者发送一个错误。

let future = check("Future") {
Future<(Data, URLResponse), Error> { promise in
loadPage(url: URL(string: "https:!"example.com")!) { data, response, error in
if let data = data, let response = response {
promise(.success((data, response))) } else {
        promise(.failure(error!))
      }
} }
}

但是,对于 Cocoa 中其他茫茫多的闭包回调和 delegate 等,使用 Future 或者 Subject 将它们封装为响应式的事件流,是很常见的做法。

Foundation 中的 Publisher

URLSession Publisher

struct Response: Decodable { 
	struct Args: Decodable {
		let foo: String 
	}
	let args: Args? 
}

let subscription = check("URL Session") { URLSession.shared
    .dataTaskPublisher(
      for: URL(string: "https:!"httpbin.org/get?foo=bar")!)
	.map { data, _ in data }
	.decode(type: Response.self, decoder: JSONDecoder()) 
	.compactMap { $0.args!"foo }
}

Timer Publisher

let timer = Timer.publish(every: 1, on: .main, in: .default)
let temp = check("Timer") {
	timer
}
timer.connect()

Timer.TimerPublisher 是一个满足 ConnectablePublisher 的类型。ConnectablePublisher 不同于普通的 Publisher, 你需要明确地调用 connect() 方法,它才会开始发送事件。

一个显而易见的问题是,既然我们需要调用 connect() 才能让事件开始发生,那当我 们不再关心这个事件流的时候,是不是应该本着资源使用的 “谁创建,谁释放” 的原 则,让这个事件流停止发送呢?答案是肯定的:connect() 会返回一个 Cancellable 值,我们需要在合适的时候调用 cancel() 来停止事件流并释放资源。同样地,对于 订阅来说,大多数情况下我们也需要及时取消,以保证内存不发生泄漏。

对 于 普 通 的Publisher, 当Failure是Never时, 就 可 以 使 用 makeConnectable() 将它包装为一个 ConnectablePublisher。这会使得该 Publisher 在等到连接 (调用 connect()) 后才开始执行和发布事件。在某些情况下,如果我们希望延迟及控制 Publisher 的开始时间,可以使用这个方 法。
对 ConnectablePublisher 的 对 象 施 加 autoconnect() 的 话, 可 以 让 这 个 ConnectablePublisher “恢复” 为被订阅时自动连接。

Notification Publisher

extension NotificationCenter { public func publisher(
    for name: Notification.Name,
object: AnyObject? = nil
) !" NotificationCenter.Publisher
}

@Published

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

class Wrapper {
	@Published var text: String = "hoho"
}
var wrapper = Wrapper() 
check("Published") {
  wrapper.$text
}
wrapper.text = "123"
wrapper.text = "abc"

对于在 ObservableObject 类型下的 @Published 的行为,Swift 编译器和 SwiftUI 进行了特殊的处理

订阅和绑定

通过 sink 订阅 Publisher 事件

Subscriber 可以指定想要接收的新值的个数,这不仅在订阅初期可以通过 Subscription.request 来告知 Publisher,也可以通过 Subscriber.receive 返回特定的 Subscribers.Demand 值来指定接下来能够处理的值的个数。通 过这些机制,Combine 将可以实现背压 (Backpressure)。.unlimited 表示不 设上限,但当上游 Publisher 的值生产速度大于下游的消费速度时,下游的 缓冲区就会发生溢出。指定合适的背压策略,通过控制上限,可以让系统下 游不发生崩溃的同时,有机会对部分溢出事件做额外处理 (比如丢弃或者告 知上游不要再接受新的事件)。
在客户端开发中,需要处理背压的场景非常有限,但是在服务端开发处理大 规模数据时,这会是无法绕过机制。相关话题超出了本书内容的涵盖范围, 但有兴趣的读者不妨自行查找相关资料进行理解。

通过 assign 绑定 Publisher 值

  1. assign 所接 受的第一个参数的类型为 ReferenceWritableKeyPath,也就是说,只有 class 上用 var 声明的属性可以通过 assign 来直接赋值。
  2. 上游 Publisher 的 Failure 的类型必须是 Never

Publisher 的引用共享

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

对于多个 Subscriber 对应一个 Publisher 的情况,如果我们不想让订阅行为反复发 生 (比如上例中订阅时会发生网络请求),而是想要共享这个 Publisher 的话,使用 share() 将它转变为引用类型的 class。

Cancellable, AnyCancellable 和内存管理

上面的操作有一个共同的特点,那就是它们都要求随着时间流动,计时器或者订阅要 能继续响应和工作。这必然需要某种 “资源”,并持有它们,以保持自己在内存中依 然存在
和 Cancellable 这个抽象的协议不同,AnyCancellable 是一个 class,这也赋予了它 对自身的生命周期进行管理的能力。
针对上面 Combine 中常见的内存资源相关的操作,可以总结几条常见的规则和实践:

  1. 对于需要 connect 的 Publisher,在 connect 后需要保存返回的 Cancellable,
    并在合适的时候调用 cancel() 以结束事件的持续发布。
  2. 对于 sink 或 assign 的返回值,一般将其存储在实例的变量中,等待属性持有 者被释放时一同自动取消。不过,你也完全可以在不需要时提前释放这个变量 或者明确地调用 cancel() 以取消绑定。
  3. 对于 1 的情况,也完全可以将 Cancellable 作为参数传递给 AnyCancellable 的初始化方法,将它包装成为一个可以自动取消的对象。这样一来,1 将被转 换为 2 的情况。
 类似资料: