跳转至

第1章:你好,Combine!

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

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

尽管非常准确,而且切中要害,这个令人愉快的定义一开始听起来可能有点太抽象了。这就是为什么,在深入进行编码练习和在下面的章节中进行项目工作之前,你要花一点时间了解一下Combine解决的问题和它用来解决的工具。

一旦你积累了相关的词汇并对框架有了一定的了解,你就会在编码时开始涉及基础知识。

逐渐地,随着你在书中的进展,你会了解到更多的高级主题,并最终完成几个项目。

当你涵盖了其他所有内容后,你将在最后一章中研究一个用Combine构建的完整应用程序。

异步编程

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

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

同步代码很容易理解,而且特别容易争论你的数据的状态。有了单线程的执行,你总是可以确定你的数据的当前状态是什么。在上面的例子中,你知道第一个print将总是打印Tom,第二个将总是打印"Tom Harding"

现在,想象一下,你用多线程语言编写的程序,正在运行一个异步事件驱动的UI框架,比如在SwiftUIKit上运行的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"

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

上面的例子中,运行在"线程2"上的代码可能是。

  • 与你的原始代码在不同的CPU核上完全同时执行。
  • name += " Harding"之前执行,所以不是原始值"Tom",而是"Billy Bob"

当你运行这段代码时,到底会发生什么,取决于系统的负载,你每次运行程序时可能会看到不同的结果。

一旦你运行异步并发代码,管理你的应用程序中的易变状态就成了一项加载任务。

FoundationUIKit/AppKit

多年来,苹果一直在不断改进其平台的异步编程。他们已经创建了几种机制,你可以在不同的系统层面上使用,以创建和执行异步代码。

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

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

  • NotificationCenter:在感兴趣的事件发生时执行一段代码,例如当用户改变设备的方向或软件键盘在屏幕上显示或隐藏时。
  • 委托模式:让你定义一个代表另一个对象行事或与之协调的对象。例如,在你的应用程序委托中,你定义了当一个新的远程通知到达时应该发生什么,但你不知道这段代码何时运行或执行多少次。
  • Grand Central Dispatch and Operations:帮助你抽象出工作片段的执行。你可以用它们来安排代码在一个串行队列中按顺序运行,或者在不同的队列中以不同的优先级并发运行大量的任务。
  • 闭包:创建独立的代码片段,你可以在你的代码中传递,所以其他对象可以决定是否执行它,执行多少次,以及在什么情况下执行。

由于大多数典型的代码都是异步执行一些工作,而且所有的UI事件都是固有的异步,所以不可能对整个应用程序代码的执行顺序做出假设。

然而,写好异步程序是可能的。它只是比......好吧,我们希望它更复杂。

当然,造成这些问题的原因之一是,一个坚实的、现实生活中的应用程序很可能使用所有不同种类的异步API,每个都有自己的接口,像这样:

img

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

苹果公司也将Combine集成到其他框架中,所以TimerNotificationCenterCore Data等核心框架已经使用它的语言。幸运的是,Combine也很容易集成到你自己的代码中。

最后,但绝对不是最不重要的,苹果公司设计了他们惊人的UI框架,SwiftUI,也很容易与Combine整合。

为了让你了解苹果公司对使用Combine的反应式编程的承诺,这里有一个简单的图表,显示了Combine在系统层次结构中的位置:

img

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

Swift的现代并发性

Swift 5.5引入了一系列用于开发异步和并发代码的API,由于采用了新的线程池模型,你的代码可以安全、快速地随意暂停和恢复异步工作。

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

这些API解决了一些与Combine相同的问题,但Combine的优势在于其丰富的操作符集。Combine提供的处理事件的操作符使很多复杂的、常见的场景变得容易解决。

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

而且,说到Combine的优势,让我们快速看看到目前为止反应式编程的出色表现。

Combine的基础

声明式、反应式编程并不是一个新概念。它已经存在了相当长的一段时间,但在过去的十年里,它有了相当明显的回潮。

第一个"现代 "的反应式解决方案出现在2009年,当时微软的一个团队推出了一个名为Reactive Extensions for .NET(Rx.NET)的库。

微软在2012年将Rx.NET的实现开源,从那时起,许多不同的语言都开始使用它的概念。目前,有许多Rx标准的端口,如RxJSRxKotlinRxScalaRxPHP等等。

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

Combine实现了一个与Rx不同但类似的标准,叫做Reactive StreamsReactive StreamsRx有一些关键的区别,但它们都同意大多数的核心概念。

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

然而,在iOS 13/MacOS Catalina中,苹果通过内置系统框架Combine为其生态系统带来了反应式编程支持。

既然如此,就从学习Combine的一些基础知识开始,看看它如何帮助你写出安全、可靠的异步代码。

Combine基础知识

概括地说,Combine中的三个关键活动部件是发布者、操作符和订阅者。当然,团队中还有更多的参与者,但如果没有这三个人,你就不能取得什么成就。

你将在第2章"发布者和订阅者"中详细了解发布者和订阅者,本书的第二部分将指导你尽可能多地熟悉操作符。

然而,在这一介绍性章节中,你将得到一个简单的速成课程,让你大致了解这些类型在代码中的用途,以及它们的职责是什么。

发布者

发布者是可以随着时间的推移向一个或多个相关方(如订阅者)发射值的类型。不管发布者的内部逻辑是什么,它几乎可以是任何东西,包括数学计算、网络或处理用户事件,每个发布者都可以发出这三种类型的多个事件。

  1. 一个发布者通用的Output类型的输出值。
  2. 一个成功的完成。
  3. 一个带有发布者的Failure类型的错误的完成。

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

下面是一个发射Int值的发布者在时间轴上的可视化情况:

img

蓝色方框代表在时间轴上某一特定时间发出的值,数字代表发出的值。一条垂直线,就像你在图的右边看到的那条,代表一个成功的流完成。

三个可能事件的简单契约是如此普遍,以至于它可以代表你程序中的任何种类的动态数据。这就是为什么你可以使用Combine publishers解决你的应用程序中的任何任务--不管它是关于计算数字、进行网络调用、对用户手势作出反应还是在屏幕上显示数据。

你不必总是在你的工具箱中为手头的任务寻找合适的工具,无论是添加一个委托还是注入一个完成回调,你都可以用发布者来代替。

发布者的一个最好的特点是它们内置了错误处理功能;错误处理并不是你在最后选择性地添加的,如果你想的话。

Publisher协议在两种类型上是通用的,你可能已经在前面的图中注意到了。

  • Publisher.Output是发布者的输出值的类型。如果它是一个Int发布者,它永远不能发布StringDate值。
  • Publisher.Failure是发布者在失败时可以抛出的错误类型。如果发布者永远不能失败,你可以通过使用Never失败类型来指定。

当你订阅一个给定的发布者时,你知道从它那里期待什么值以及它可能失败的错误。

操作符

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

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

令人着迷的是,操作符像拼图一样紧密地结合在一起。它们不能被错误地放在一起,也不能在一个的输出与下一个的输入类型不匹配的情况下合在一起。

img

以一种明确的确定性方式,你可以定义每一个异步抽象工作的顺序,以及正确的输入/输出类型和内置错误处理。这几乎好得不能再好了!

作为额外的奖励,操作符总是有输入和输出,通常被称为上游和下游--这使他们能够避免共享状态(我们之前讨论的核心问题之一)。

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

订阅者

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

img

目前,Combine提供了两个内置的订阅者,这使得与数据流的工作变得简单明了。

  • sink订阅者允许您为代码提供闭包,以接收输出值和完成。 从那里,你可以用接收到的事件做任何你内心渴望的事情。
  • assign订阅服务器允许您将生成的输出绑定到数据模型或UI控件上的某个属性,以便通过键路径直接在屏幕上显示数据,而无需自定义代码。

如果你对你的数据有其他需求,创建自定义订阅者甚至比创建发布者更容易。Combine使用了一套非常简单的协议,使你能够在车间没有提供合适的工具来完成你的任务时,建立自己的自定义工具。

订阅

Note

本书使用订阅一词来描述CombineSubscription协议及其符合的对象,以及发布者、操作符和订阅者的完整链条。

当你在订阅的末尾添加一个订阅者时,它就会"激活"链上的所有发布者。这是一个奇怪但重要的细节,要记住--如果没有订阅者有可能接收输出,那么发布者就不会发出任何值。

订阅是一个奇妙的概念,因为它允许你声明一个异步事件链,有自己的自定义代码和错误处理,只需一次,然后你就不必再考虑它了。

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

img

一旦订阅代码编译成功,并且你的自定义代码中没有逻辑问题--你就完成了 按照设计,每当发生一些事件,如用户的手势、定时器响起或其他东西唤醒你的一个发布者时,订阅就会异步地"发射"。

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

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

这意味着你可以很容易地"绑定"一个订阅的寿命,例如,将其存储在视图控制器的一个属性中。这样,当用户从视图堆栈中删除视图控制器时,就会取消其属性,也会取消你的订阅。

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

正如你所看到的,有很多东西需要学习,但在详细解释时都是合乎逻辑的。而这正是接下来几章的计划--在本书结束时,把你从零开始慢慢地但稳定地带到Combine英雄。

Combine代码比"标准"代码有什么好处?

无论如何,你可以不使用Combine,但仍然可以创建最好的应用程序。这一点无可非议。你也可以不用Core DataURLSession,甚至UIKit来创建最好的应用程序。但使用这些框架比自己构建这些抽象更方便、更安全、更高效。

Combine(和其他系统框架)的目的是为你的异步代码添加另一个抽象。在系统层面上的另一个抽象层次意味着更紧密的集成,它经过良好的测试,是一种安全的技术。

Combine是否适合你的项目由你决定,但这里有几个你可能还没有考虑过的"专业"理由。

  • Combine是在系统层面上集成的。这意味着Combine本身使用了未公开的语言特性,为你提供了你自己无法构建的API
  • Combine将许多常见的操作抽象为Publisher协议上的方法,而且它们已经经过了良好的测试。
  • 当你所有的异步工作都使用相同的接口--Publisher--组合和重用性变得非常强大。
  • Combine的操作符是高度可组合的。如果你需要创建一个新的操作符,这个新的操作符将立即与Combine的其他部分即插即用。
  • Combine的异步操作符已经过测试。你所要做的就是测试你自己的业务逻辑。

正如你所看到的,大部分的好处都是围绕着安全性和便利性展开的。再加上该框架来自于苹果,投资编写Combine代码看起来很有希望。

应用程序架构

由于这个问题很可能已经在你的头脑中敲响了警钟,所以请看一下使用Combine会如何改变你原有的代码和应用架构。

Combine不是一个影响你的应用结构的框架。Combine处理的是异步数据事件和统一的通信合同--它不会改变,例如,你如何在你的项目中分离责任。

你可以在你的MVC(Model-View-Controller)应用程序中使用Combine,你可以在你的MVVM(Model-View-ViewModel)代码中使用它,在VIPER中使用,等等等等。

这是采用Combine的一个关键方面,早期理解它很重要--你可以迭代地、有选择地添加Combine代码,只在你希望改善代码库的部分使用它。这不是一个你需要做出的"全部或没有"的选择。

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

如果你同时采用CombineSwiftUI,情况就会略有不同。在这种情况下,从MVC架构中删除C确实是有意义的。但这要归功于CombineSwiftUI的串联使用--这两者在同一个房间里,简直是火力全开。

视图控制器在面对Combine/SwiftUI团队时没有任何机会。当你从数据模型到视图都使用反应式编程时,你就不需要一个特殊的控制器来控制你的视图。

img

如果这听起来很有趣,那你就有好戏看了,因为本书在第15章"Combine & SwiftUI实践"中对这两个框架的共同使用做了扎实的介绍。

书中的项目

在本书中,你将首先从概念开始,然后转向学习和尝试众多的操作符。

与其他系统框架不同,你可以在Playground这个孤立的环境中相当成功地使用Combine

Xcode Playground中学习可以使你很容易地向前推进,并在某一章节的进展中快速进行实验,并在Xcode的控制台中即时看到结果。

img

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

一旦你转向比玩一个操作符更复杂的概念,你将交替在Playground和真正的Xcode项目中工作,如Hacker News应用程序,它是一个实时显示新闻的新闻阅读器。

img

重要的是,对于每一章,你都要从所提供的启动Playground或项目开始,因为它们可能包括一些与学习Combine无关的自定义辅助代码。这些花絮是预先写好的,这样你就不会分散自己对该章重点的注意力。

在最后一章中,你将利用你在全书中学到的所有技能,完成开发一个严重依赖CombineCore Data的完整的iOS应用。这将为你在用Combine构建实际生活中的应用的道路上提供最后的推动力!

img

关键点

  • Combine是一个声明式的、反应式的框架,用于处理随时间变化的异步事件。
  • 它的目的是解决现有的问题,如统一异步编程的工具,处理易变的状态,并使错误处理成为团队的起点。
  • Combine围绕着三种主要类型:发布者在一段时间内发布事件,操作符异步处理和操作上游事件,订阅者订阅结果并用它们做一些有用的事情。

接下来去哪?

希望这一章的介绍是有用的,让你对Combine解决的问题有一个初步的了解,并看看它提供的一些工具,使你的异步代码更安全、更可靠。

本章的另一个重要收获是对Combine的期望,以及它的范围之外的东西。现在,你知道当我们说到反应式代码或异步事件时,你会遇到什么。当然,你也不要指望用Combine来神奇地解决你的应用程序在导航或屏幕上的绘图问题。

最后,希望通过对即将到来的章节的了解,你能对CombineSwift的反应式编程产生兴趣。向上,向前,我们来了!"。