用 Apple 自己的话来说:“Combine 框架为您的应用程序如何处理事件提供了一种声明性方法。您可以为给定的事件源创建一个处理链,而不是潜在地实现多个委托回调或完成处理程序闭包。链的每个部分是一个组合运算符,它对从上一步接收到的元素执行不同的操作。”
这段话初学者必定是无法理解,需要真正使用过Combine后回来看才能有所收获。 我们可以这样理解: 异步数据,所需要的操作,无非是
发送、接收和处理,那么Combine就是实现这些过程的框架。 发送就是下文中的发布者(Publishers),接收就是下文中的订阅者(Subscribers),处理就是下文中的操作符(Operators)
为了让您了解 Apple 是如何致力于使用 Combine 进行反应式编程的,这里有一个简单的图表,显示了 Combine 在系统层次结构中的位置
各种系统框架,从 Foundation 一直到 SwiftUI,都依赖于Combine 并提供Combine 集成作为其更“传统”API 的替代方案。
Combine 基础
概括地说,Combine 中的三个关键要素是发布者(Publishers)、操作符(Operators)和订阅者(subscribers)。当然,还有其他成员,但最重要的是这三位。
我们可以将发布者、操作符和订阅者,想象为我们在网络购物中,商家发货期间参与的三位主人公。 发布者是商家,他们在我们购物后会发货,他是一个Publisher,负责发布。 操作符是快递小哥,他们会确认订单、装车、派送,他是一系列Operators的集合,负责处理。 订阅者是我们,我们通过快递小哥(Operators)的一系列操作,接收到了货,我们一般来说会“处理”这个收到的货物,比如使用,或是扔掉。
Publishers
发布者(Publishers)是可以随时间向一个或多个订阅者(subscribers)发送值的类型。他几乎可以发布任何事情,包括数学计算、网络或处理用户事件等等,每个发布者都可以发出这三种类型的多个事件:
1、发布者通用输出类型的输出值 (Output value)
2、成功的完成结果(successful completion)
3、带有发布者失败类型错误的完成结果(Failure type)
Publishers可以发出零个或多个输出值,如果它完成,无论是成功还是失败,它都不会发出任何其他事件.三个可能事件的简单契约是如此通用,以至于它可以代表您程序中的任何类型的动态数据。这就是为什么您可以使用Combine Publishers 处理应用程序中的任何任务的原因.
与其总是在你的工具箱中寻找合适的工具来处理手头的任务,无论是添加一个委托还是注入一个完成回调——你可以只使用发布者。 发布者的最佳功能之一是它们内置了错误处理功能;如果您愿意,错误处理不是您在最后选择添加的内容。
发布者协议在两种类型上是通用的,正如您在前面的图表中可能已经注意到的那样: Publisher.Output 是发布者的输出值的类型。如果它是 Int 发布者,则它永远不会发出 String 或 Date 值。 Publisher.Failure 是发布者在失败时可以抛出的错误类型。如果发布者永远不会失败,您可以使用从不失败类型来指定。 当您订阅给定的发布者时,您就知道可以从中得到什么值以及它可能会因哪些错误而失败。
Operators
运算符(Operators)是在发布者协议上声明的方法,它们返回相同的或新的发布者。这非常有用,也很重要。因为您可以一个接一个地调用一组运算符,有效地将它们链接在一起。
因为这些称为“操作符”的方法是高度解耦和可组合的,它们可以组合以在执行单个订阅时实现非常复杂的逻辑。
操作符像拼图一样紧密地组合在一起,这很有趣。如果一个输出与下一个输入类型不匹配,它们不能被错误地放置在错误的顺序或组合在一起。
操作符总是有输入和输出,通常被称为上游和下游——这使他们能够避免共享状态。
操作符专注于处理他们从前一个操作员那里收到的数据,并将他们的输出提供给链中的下一个操作员。这意味着没有其他异步运行的代码可以“跳入”并更改您正在处理的数据
Subscribers
最后,您到达订阅链的末端:每个订阅都以订阅者(Subscribers)结束。订阅者通常会对发出的输出或完成事件做一些处理。
目前,Combine 提供了两个内置订阅者,这使得处理数据流变得简单:
- sink 允许您为将接收输出值和完成的代码提供闭包。从那里,您可以对收到的事件做任何您想做的事情。
- assign 允许您在不需要自定义代码的情况下将结果输出绑定到数据模型或 UI 控件上的某些属性,以通过键路径直接在屏幕上显示数据。
如果您对数据有其他需求,创建自定义订阅者比创建发布者更容易。 Combine 使用一组非常简单的协议,让您能够在研讨会没有为您的任务提供合适的工具时构建自己的自定义工具
订阅(Subscriptions)
我们这里的订阅,定义为:描述Combine 的订阅协议及其符合标准的对象,以及发布者、运营商和订阅者的完整链。
当您在订阅结束时添加订阅者时,它会在链的开头一直“激活”发布者。这是一个需要记住的奇怪但重要的细节——如果没有订阅者可能接收输出,则发布者不会发出任何值。
订阅是一个很棒的概念,因为它们允许您使用自己的自定义代码和错误处理声明一系列异步事件,然后您就不必再考虑它了。
如果你使用 full-Combine,你可以通过订阅来描述整个应用程序的逻辑,一旦完成,只需让系统运行所有内容,无需推送或拉取数据或回调这个或那个其他对象.
更好的是,您不需要专门对订阅进行内存管理,这要归功于 Combine 提供的名为 Cancelable 的协议.
两个系统提供的订阅者(sink、assign)都符合Cancelable协议,这意味着您的订阅代码(例如整个发布者、运营商和订阅者调用链)返回一个Cancelable对象。每当您从内存中释放该对象时,它都会取消整个订阅并从内存中释放其资源。
例如,这意味着您可以通过将订阅存储在视图控制器上的属性中来轻松“绑定”订阅的生命周期。这样,每当用户从视图堆栈中解除视图控制器时,都会取消其属性的初始化,也会取消您的订阅。
例如:
// 声明一个CurrentValueSubject类型的Publisher
let curPublisher = CurrentValueSubject<String, Never>("No.1")
// 声明一个AnyCancellable类型的对象
let cancellable: AnyCancellable = curPublisher
.sink { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("Got a error: \(error)")
}
} receiveValue: { str in
print(str)
}
或者为了更简化这个过程,你可以在你的类型上拥有一个 [AnyCancellable] 集合属性,并根据需要在其中抛出尽可能多的订阅。当属性从内存中释放时,它们都会被自动取消和释放。
例如:
// 声明一个AnyCancellable集合
var cancelables = Set<AnyCancellable>()
lcurPublisher
.sink { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("Got a error: \(error)")
}
} receiveValue: { str in
print(str)
}
.store(in: &cancelables)
Combine相比于标准代码的好处
无论如何,您可以永远不使用Combine,而仍然可以创建最好的应用程序。对此没有任何争论。你也可以在没有 Core Data、URLSession 甚至 UIKit 的情况下创建最好的应用程序。
框架比自己构建这些抽象更方便、安全和高效。 Combine(和其他系统框架)旨在为您的异步代码添加另一个抽象。系统级别的另一个抽象级别意味着经过充分测试的更紧密集成和安全投注技术
由您决定 Combine 是否非常适合您的项目,但这里有一些您可能尚未考虑的“专业”原因:
- Combine 集成在系统级别。这意味着Combine 本身使用非公开可用的语言功能,为您提供您自己无法构建的API。
- Combine将许多常见操作抽象为发布者协议上的方法,并且它们已经经过很好的测试。
- 当您所有的异步工作都使用相同的接口 - Publisher - 组合和可重用性变得非常强大。
- Combine 的操作符是高度可组合的。如果您需要创建一个新操作员,该新操作员将立即与Combine 的其余部分即插即用。
- Combine'a 异步操作符已经过测试。剩下要做的就是测试自己的业务逻辑
如您所见,大多数好处都围绕着安全性和便利性展开。结合该框架来自 Apple 的事实,他是我们值得投入研究的一个技术。
App 架构
因为这个问题很可能已经在你的脑海中敲响了警钟,看看使用 Combine 将如何改变你预先存在的代码和应用程序架构。
Combine 不是影响您构建应用程序方式的框架。Combine将处理异步数据事件和统一通信合同结合起来——例如,它不会改变您在项目中分离职责的方式。 您可以在 MVC(模型-视图-控制器)应用程序中使用Combine,也可以在 MVVM(模型-视图-视图模型)代码、VIPER 等中使用它。 这是采用Combine 的关键方面之一,对于尽早理解很重要——您可以迭代地和有选择地添加Combine 代码,仅在您希望在代码库中改进的部分使用它。这不是您需要做出的“全有或全无”的选择。
您可以先转换数据模型,或调整网络层,或者仅在添加到应用程序的新代码中使用 Combine,同时保持现有功能不变。 如果您同时采用 Combine 和 SwiftUI,则情况略有不同。在这种情况下,从 MVC 架构中删除 C 确实是有意义的。但这要归功于结合使用 Combine 和 SwiftUI——这两者在同一个房间里时简直是火上浇油。 视图控制器根本没有任何机会对抗 Combine/SwiftUI 团队。当你从数据模型到视图一直使用反应式编程时,你不需要一个特殊的控制器来控制你的视图。
一些Key Points
- Combine 是一个声明式、响应式框架,用于随着时间的推移处理异步事件。
- 它旨在解决现有的问题,例如统一异步编程工具、处理可变状态以及使错误处理成为团队成员。
- Combine 围绕三种主要类型:发布者随时间发出事件,操作者异步处理和操作上游事件,订阅者消费结果并用它们做一些有用的事情