第 1 节:你好,Combine!

第 1 节:你好,Combine!

本书旨在向你介绍 Combine 框架,以及使用 Swift 为 Apple 平台编写声明式、反应式应用程序。

用 Apple 自己的话来说:“Combine 框架为你的应用程序如何处理事件提供了一种声明性方法。你可以为给定的事件源创建单个处理链,而不是潜在地实现多个委托回调或完成处理程序闭包。链的每个部分是一个组合运算符,它对从上一步接收到的元素执行不同的操作。”

虽然这个描述非常准确且切中要害,但这个令人愉快的定义可能让人第一次听起来有点过于抽象。这就是为什么在深入研究编码练习和处理后续章节中的项目之前,你需要花一点时间来了解一下 Combine 解决的问题以及它用来解决这些问题的工具。

一旦你建立了相关的词汇表并总体上对框架有所了解,就将在编码时理解这些基础知识。逐渐地,随着你在本书中的学习,你将了解更高级的主题并最终完成多个项目。当你学习了所有内容后,你将在最后一章中使用 Combine 构建一个完整的应用程序。

异步编程

在简单的单线程语言中,程序按顺序逐行执行。例如,在伪代码中:

begin  var name = "Tom"  print(name)  name += " Harding"  print(name)end

同步代码很容易理解,并且特别容易确定数据的状态。 通过单线程执行,你始终可以确定数据的当前状态。 在上面的示例中,你知道第一次打印将始终打印”Tom”,第二次将始终打印“Tom Harding”。

现在,假设你使用运行异步事件驱动 UI 框架的多线程语言编写程序,例如在 Swift 和 UIKit 上运行的 iOS 应用程序。

考虑可能发生的情况:

--- Thread 1 ---begin  var name = "Tom"  print(name)​--- Thread 2 ---name = "Billy Bob"​--- Thread 1 ---  name += " Harding"  print(name)end

在这里,代码将 name 的值设置为“Tom”,然后将“Harding”添加到它,就像以前一样。 但是因为另一个线程可以同时执行,所以程序的其他部分可能会在 name 的两个突变之间运行并将其设置为另一个值,例如“Billy Bob”。

当代码在不同的核上并发运行时,很难说代码的哪一部分会先修改共享状态。

上面示例中在“Thread 2”上运行的代码可能是:

  • 在与原始代码不同的 CPU 内核上同时执行。

  • name += " Harding" 之前执行,因此它不是原始值 "Tom",而是得到 "Billy Bob"。

运行此代码时究竟会发生什么取决于系统负载,并且每次运行程序时你可能会看到不同的结果。运行异步并发代码后,管理应用程序中的可变状态将成为一项必要的任务。

Foundation 和 UIKit/AppKit

多年来,Apple 一直在不断改进其平台的异步编程。他们创建了几种机制,你可以在不同的系统级别上使用这些机制来创建和执行异步代码。

你可以使用低级别的 API,就像使用 NSThread 管理自己的线程一样,一直到使用 Swift 的现代并发 async/await 等。

你可能已经在你的应用程序中使用了以下一些内容:

  • NotificationCenter:在感兴趣的事件发生时执行一段代码,例如用户更改设备的方向或键盘在屏幕上显示或隐藏。

  • 委托模式:允许你定义一个代表另一个对象或与之协调的对象。 例如,在你的应用程序委托中,你定义了当新的远程通知到达时应该发生什么,但不知道这段代码何时运行或执行多少次。

  • GCD 和 Operation:帮助你抽象工作的执行。你可以使用它们来安排代码在串行队列中按顺序运行,或者在具有不同优先级的不同队列中同时运行多个任务。

  • 闭包:创建可以在代码中传递的分离代码片段,以便其他对象可以决定是否执行它、执行多少次以及在什么上下文中执行。

由于大多数典型代码异步执行一些工作,并且所有 UI 事件本质上都是异步的,因此无法假设整个应用程序代码将执行的顺序。

然而,编写好的异步程序是可能的。它只是更复杂一些。

当然,造成这些问题的原因之一是一个可靠的、真实的应用程序很可能使用所有不同类型的异步 API,每个都有自己的接口,如下所示:

Combine 为 Swift 生态系统引入了一种通用的高级语言来设计和编写异步代码。

Apple 也将 Combine 集成到其其他框架中,如 Timer、NotificationCenter 和 Core Data 等核心框架。 同时,Combine 也很容易集成到你自己的代码中。

最后但同样重要的是,Apple 设计了他们令人惊叹的 UI 框架 SwiftUI,以便与 Combine 轻松集成。

为了让你了解 Apple 对使用 Combine 进行响应式编程的重视程度,这里有一个简单的图表,显示了 Combine 在系统层次结构中的位置:

从 Foundation 一直到 SwiftUI,各种系统框架都依赖于 Combine,并提供 Combine 集成作为其更“传统”API 的替代方案。

Swift 的现代并发

Swift 5.5 引入了一系列用于开发异步和并发代码的 API,得益于新的线程池模型,这些 API 允许你的代码安全快速地随意挂起和恢复异步工作。

现代并发 API 使许多经典的异步问题相当容易解决——例如等待网络响应、并行运行多个任务等等。

这些API 解决了与 Combine 相同的一些问题,但 Combine 的优势在于其丰富的操作。随着时间的推移,Combine 提供的用于处理事件的运算符使许多复杂的常见场景更容易解决。

反应式操作符直接解决网络、数据处理和处理 UI 事件中的各种常见问题,因此对于更复杂的应用程序,使用 Combine 开发有很多好处。

而且,谈到 Combine 的优势,让我们快速了解一下反应式编程迄今为止的出色表现。

Combine 的背景

声明式、反应式编程并不是一个新概念。 它已经存在了很长一段时间,在过去十年中它已经相当引人注目。

第一个“现代”反应式解决方案在 2009 年大行其道,当时微软的一个团队推出了一个名为 Reactive Extensions for .NET (Rx.NET) 的库。微软在 2012 年将 Rx.NET 实现开源,从那时起,许多不同的语言开始使用它的概念。 目前,有许多 Rx 标准的端口,如 RxJS、RxKotlin、RxScala、RxPHP 等等。

对于 Apple 的平台,已经有几个第三方响应式框架,如 RxSwift,它实现了 Rx 标准; ReactiveSwift,直接受 Rx 启发; Interstellar 是一个自定义实现。

Combine 实现了一个与 Rx 不同但相似的标准,称为 Reactive Streams。 Reactive Streams 与 Rx 有一些关键区别,但它们都同意大多数核心概念。在 iOS 13/macOS Catalina 中,Apple 通过内置的系统框架 Combine 为其生态系统带来了响应式编程支持。

如果你以前没有使用过上面提到的一个或另一个框架,请不要担心。 到目前为止,响应式编程对于 Apple 的平台来说是一个相当小众的概念,尤其是对于 Swift。

你将首先学习一些 Combine 的基础知识,看看它如何使开发者编写安全可靠的异步代码。

Combine 的基础

概括地说,Combine 中的三个关键部分是发布者(Publisher)、操作符(Operator)和订阅者(Subscriber)

你将在第 2 节“发布者和订阅者”中详细了解发布者和订阅者,本书的完整第二部分将引导你尽可能多地了解操作符。在这个介绍性章节中,你将获得一个简单的速成课程,让你大致了解这些类型在代码中的用途以及它们的职责。

发布者(Publisher)

发布者是可以随着时间的推移向一个或多个相关方(例如订阅者)发出值的类型。不管发布者的内部逻辑如何,无论是数学计算、网络或处理用户事件等,每个发布者都可以发出这三种类型的多个事件:

  1. 发布者的通用输出类型的输出值。

  2. 成功的 completion。

  3. 带有发布者失败类型错误的 completion。

发布者可以发出零个或多个输出值,如果它完成,无论是成功还是由于失败,它都不会发出任何其他事件。

以下是发布 Int 值的发布者在时间轴上的可视化效果:

蓝色框表示在时间线上给定时间发出的值,数字表示发出的值。一条垂直线,就像你在图表右侧看到的那条一样,表示成功的流完成。

三个可能事件的非常简单和普遍,它可以代表你程序中的任何类型的动态数据。 这就是为什么你可以使用 Combine 的发布者在你的应用程序中处理任何任务——无论是处理数字、拨打网络电话、对用户手势做出反应还是在屏幕上显示数据。

不要总是在你的旧工具箱中寻找合适的工具来解决任务,无论是添加委托还是注入回调——你可以尝试去使用发布者。

发布者的最佳功能之一是它们内置了错误处理; 错误处理是你在最后添加的可选内容。

Publisher 协议有两种类型是通用的,正如你在前面的图中可能已经注意到的那样:

  • Publisher.Output 是发布者输出值的类型。 如果它是 Int 发布者,它永远不能发出 String 或 Date 值。

  • Publisher.Failure 是发布者在失败时可以抛出的错误类型。 如果发布者永远不会失败,你可以使用 Never failure 类型来指定它。

当你订阅给定的发布者时,会知道期望从中获得什么值以及可能会因哪些错误而失败。

操作符(Operator)

操作符是在 Publisher 协议上声明的方法,它们返回相同的或新的发布者。这非常有用,因为你可以一个接一个地调用一堆运算符,从而有效地将它们链接在一起。

因为这些称为操作符的方法是高度解耦和可组合的,所以它们可以组合起来,在单个订阅的执行中实现非常复杂的逻辑。

操作符像拼图一样紧密地组合在一起的设计令人着迷的。如果一个人的输出与下一个人的输入类型不匹配,则它们不能错误地按错误顺序排列或组合在一起:

你可以以明确的方式定义每个异步抽象工作的顺序以及正确的输入/输出类型和内置错误处理。这非常好。

此外,操作符总是有输入和输出,通常称为上游(Upstream)和下游(Downstream)——这使他们能够避免共享状态。

操作符专注于处理从前一个操作员那里收到的数据,并将其输出提供给链中的下一个操作员。这意味着没有其他异步运行的代码可以“跳入”并更改你正在处理的数据。

订阅者(Subscriber)

最后,你到达订阅链的末端:每个订阅都以一个订阅者结束。订阅者通常对发出的输出或完成事件做“某事”。

目前,Combine 提供了两个内置订阅者,这使得处理数据流变得简单:

  • 接收器订阅者允许你使用闭包接收输出值和 completion。从在那里你可以对收到的事件做任何想做的事情。

  • 分配订阅者允许你在不需要自定义代码的情况下将结果输出绑定到数据模型或 UI 控件上的某个属性,通过 key path 直接在屏幕上显示数据。

如果你对数据有其他需求,创建自定义订阅者甚至比创建发布者更容易。 Combine 使用一组非常简单的协议,使你能够合适的构建自己的自定义工具。

订阅(Subscription)

注意:本书使用术语订阅(Subscription)来描述 Combine 的订阅协议及其符合对象,以及发布者、操作者和订阅者的完整链。

当你在订阅的末尾添加订阅者时,它会在链的开头“激活”发布者。 这是一个奇怪但重要的细节——如果没有订阅者可能接收输出,则发布者不会发出任何值

订阅是一个很棒的概念,因为它们允许你使用自己的自定义代码和错误处理声明一连串异步事件,并且只需要一次,然后你就不必再考虑它了。

如果你完全使用 Combine,你可以通过订阅来描述你的整个应用程序的逻辑,一旦完成,只需让系统运行所有东西,而不需要推送或拉取数据或回调其他对象:

一旦订阅代码成功编译并且你的自定义代码中没有逻辑问题,订阅将在每次某个事件时异步“触发”,如用户手势、计时器关闭或其他唤醒发布者的事件。

更好的是,你不需要专门管理订阅,这要归功于 Combine 提供的一个名为 Cancellable 的协议。

系统提供的两个订阅者都符合 Cancellable,这意味着你的订阅代码(例如整个发布者、操作符和订阅者调用链)返回一个 Cancellable 对象。每当你从内存中释放该对象时,它都会取消整个订阅并从内存中释放其资源。

这意味着你可以通过 strong “绑定”订阅的生命周期到视图控制器的属性中。这样,每当用户从视图堆栈中关闭视图控制器时,都会取消初始化其属性并取消你的订阅。

或者为了自动化这个过程,你可以在你的类型上有一个 [AnyCancellable] 集合属性,并在其中抛出你想要的任意数量的订阅。当属性从内存中释放时,它们都会被自动取消和释放。

Combine 有什么好处?

无论如何,你永远不使用 Combine 仍可以创建好的应用程序。同样你也可以在没有 Core Data、URLSession 甚至 UIKit 的情况下创建好的应用程序。但是使用这些框架比自己构建这些抽象更方便、安全和高效。

Combine 和其他系统框架旨在为你的异步代码添加另一个抽象。系统级别的另一个抽象级别意味着经过充分测试、更紧密集成和更安全的技术。

由你决定 Combine 是否适合你的项目,但这里只是一些你可能尚未考虑的原因:

  • Combine 在系统级别上集成。这意味着 Combine 本身使用不公开的语言功能,提供了你无法自己构建的 API。

  • Combine 将许多常见操作抽象为 Publisher 协议上的方法,并且它们已经过很好的测试。

  • 当你所有的异步工作都使用同一个接口——发布者——组合和可重用性变得非常强大。

  • Combine 的操作符是高度可组合的。如果你需要创建一个新操作符,该新操作符将立即与其余的 Combine 即插即用。

  • Combine'a 异步运算符已经过测试。剩下要做的就是测试自己的业务逻辑。

如你所见,大多数好处都围绕着安全性和便利性。

App 架构

这个问题也很重要,接着来了解一些使用 Combine 将如何改变你的旧代码和应用程序架构。

Combine 不是一个影响你如何构建应用程序的框架。Combine 处理异步数据事件和统一通信协议——它不会改变别的内容,例如你将如何分离项目中的职责。

你可以在你的 MVC(模型-视图-控制器)应用程序中使用组合,你可以在你的 MVVM(模型-视图-视图模型)代码、VIPER 等中使用它。

这是采用 Combine 的关键方面之一,你可以迭代地和有选择地添加 Combine 代码,仅在你希望在代码库中改进的部分中使用它。这不是你需要做出的“全有或全无”的选择。

你可以首先转换你的数据模型,或调整你的网络层,或者仅在你添加到应用程序的新代码中使用 Combine,同时保持现有功能不变。

如果你同时采用 Combine 和 SwiftUI,情况会略有不同。在这种情况下,从 MVC 架构中删除 C 确实有意义,这要归功于将 Combine 和 SwiftUI 结合使用。

视图控制器根本没有机会对抗 Combine/SwiftUI 组合。当你从数据模型到视图一直使用响应式编程时,你不需要一个特殊的控制器来控制你的视图:

这听起来很有趣,在第 15 节“实践:Combine & SwiftUI”中包含了如何结合使用这两个框架的介绍。

书籍项目

在本书中,你将首先从概念开始,然后继续学习和尝试多种操作符。

与其他系统框架不同,你可以在 Playground 的隔离上下文中非常方便地使用 Combine。

在 Xcode Playground 学习可以让你在阅读给定章节时轻松前进并快速进行实验,并在 Xcode 的控制台中立即查看结果:

Combine 不需要任何第三方依赖,通常每节的 Playground 代码中包含的一些简单的帮助文件就足以让你开始运行。 如果在 Playground 中进行实验时 Xcode 卡住了,快速重启可能会解决问题。

一旦你转向比使用单个操作符更复杂的概念,你将在 Playground 和真正的 Xcode 项目(如 Hacker News 应用程序)之间交替学习,这是一个实时显示新闻的新闻阅读器:

重要的是,对于每一章,你都从提供的 Playground 或 Project 开始,因为它们可能包含一些与学习 Combine 无关的自定义帮助代码。这些是预先编写的,因此不会分散你对该章节重点的学习。

在最后一章中,你将利用你在本书中学到的所有技能,完成开发一个依赖于 Combine 和 Core Data 的完整 iOS 应用程序。这将为你在使用 Combine 构建真实应用程序的道路上提供最后的推动力!

关键点

  • Combine 是一个声明式的响应式框架,用于随着时间的推移处理异步事件。

  • 它旨在解决现有问题,例如统一用于异步编程的工具、处理可变状态以及错误处理。

  • Combine 围绕三种主要类型:发布者随着时间的推移发出事件,操作符异步处理和操作上游事件,订阅者使用结果并使用它们做一些有用的事情。

然后去哪儿?

希望这个介绍性章节对你有所帮助,并让你初步了解结合解决的问题,并了解它提供的一些工具,以使你的异步代码更安全、更可靠。

本章的另一个重要内容是对 Combine 的期望以及超出其范围的内容。 现在,当我们谈论随着时间的推移响应式代码或异步事件时,你知道自己在做什么。 当然你也不希望使用 Combine 神奇地解决应用程序在导航或在屏幕上绘图方面的问题。

最后,在接下来的章节中为你准备的内容有望让你对使用 Swift 进行组合和反应式编程感到兴奋。

Last updated