说明
RxSwift到底是什么?这是一个很好的定义:
RxSwift是一个库,用于通过使用可观察的序列和功能样式运算符来组成基于异步和基于事件的代码,从而允许通过调度程序进行参数化执行。
听起来复杂吗?不用担心。编写反应式程序,理解其背后的许多概念以及浏览许多常用的相关术语可能会令人感到恐惧-尤其是如果您尝试一次全部使用它,或者当您没有将其引入其中时。结构化的方式。
我们还没有完全确定RxSwift是什么,是吗?让我们从一个简单的,易于理解的定义开始,并逐步发展成为更好,更具表现力的定义,当我们在本章后面的“反应式编程”主题中进行讨论时。
本质上,RxSwift通过允许您的代码对新数据做出反应并以顺序,隔离的方式对其进行处理,从而简化了异步程序的开发。
作为iOS应用程序开发人员,与您在本章前面阅读的第一个定义相比,这应该更加清楚,并告诉您有关RxSwift是什么的更多信息。
即使您仍然不清楚细节,RxSwift仍可以帮助您编写异步代码。而且您知道开发好的,确定性的异步代码很困难,因此任何帮助都非常欢迎!
1. 异步编程简介
如果您试图用一种简单的,脚踏实地的语言来解释异步编程,那么可能会遵循以下思路。
iOS应用随时可能执行以下任何操作或更多操作:
- 对按钮点击做出反应
- 动画键盘,使文本字段失去焦点
- 从互联网下载大照片
- 将数据位保存到磁盘
- 播放音讯
所有这些事情似乎是同时发生的。每当键盘动画离开屏幕时,您的应用中的音频就会在动画完成之前不会暂停,对吗?
程序的所有不同位都不会阻止彼此的执行。iOS提供了各种API,使您可以跨不同的执行上下文在不同的线程上执行不同的工作,并跨设备CPU的不同核心执行这些工作。
但是,编写真正并行运行的代码相当复杂,尤其是当不同的代码位需要处理相同的数据时。很难确定哪个代码首先更新数据,或者哪个代码读取最新值。
2. Cocoa和UIKit异步API
Apple一直在iOS SDK中提供许多API,可帮助您编写异步代码。实际上,多年来如何在平台上编写异步代码的最佳实践已经发展了许多次。
您可能已经在项目中使用了许多此类工具,并且可能没有再三考虑,因为它们对于编写移动应用程序至关重要。
仅举几例,您可以选择:
- NotificationCenter:在感兴趣的事件发生时(例如用户更改设备的方向或显示或隐藏在屏幕上的软件键盘)在任何时候执行一段代码。
- 委托模式:允许您定义一个代表另一个对象或与另一个对象协同工作的对象。
- Grand Central Dispatch:帮助您抽象化工作的执行。您可以计划将代码块依次,同时或在给定延迟后执行。
- 闭包:创建分离的代码段,您可以在代码中传递它们,最后
- 合并:Apple自己的框架,用于使用Swift编写反应式异步代码,此框架已在iOS 13中引入并在iOS 13中可用。
根据您选择依赖的API,将应用程序保持在一致状态的难度在很大程度上不同。
例如,如果您使用某些较旧的Apple API(例如委托模式或通知中心),则需要付出很多努力才能在任何给定时间保持应用程序状态的一致性。
如果您使用Apple的Combine拥有一个崭新的代码库,那么(当然)您已经熟悉了反应式编程-恭喜!
为了结束本节并将讨论放在更多的上下文中,您将比较两段代码:一个同步代码和一个异步代码。
2.1 同步码
对数组的每个元素执行操作都是您已经做很多次了。这是应用程序逻辑的非常简单但可靠的构建块,因为它保证了两件事:它同步执行,并且在您对其进行迭代时,集合是不可变的。
花一点时间考虑一下这意味着什么。遍历集合时,无需检查所有元素是否仍然存在,也无需回退,以防其他线程在集合开始处插入元素。您假设总是在循环开始时对整个集合进行迭代。
如果您想在for循环的这些方面多玩一些,请在操场上尝试一下:
var array = [1, 2, 3]
for number in array {
print(number)
array = [4, 5, 6]
}
print(array)
是array可变的内部的for身体吗?循环迭代的集合是否会发生变化?所有命令的执行顺序是什么?number如果需要可以修改吗?
2.2 异步代码
考虑类似的代码,但假定每次迭代都是对按钮轻击的反应。当用户反复点击按钮时,应用程序将打印出数组中的下一个元素:
var array = [1, 2, 3]
var currentIndex = 0
// This method is connected in Interface Builder to a button
@IBAction private func printNext() {
print(array[currentIndex])
if currentIndex != array.count - 1 {
currentIndex += 1
}
}
在与上一个代码相同的上下文中考虑此代码。当用户点击按钮时,将打印所有数组元素吗?你真的不能说。另一段异步代码可能会在打印最后一个元素之前将其删除。
或者,另一段代码可能会在您继续前进之后在集合的开头插入一个新元素。
同样,您假设currentIndex只是被所突变printNext(),但是另一段代码也可能会修改currentIndex-也许是在设计上述方法之后的某个时候添加的一些巧妙代码。
您可能已经意识到编写异步代码的一些核心问题是:a)工作执行的顺序以及b)共享可变数据。
幸运的是,这些是RxSwift的强项!
接下来,您需要一个很好的入门语言,以帮助您开始理解RxSwift的工作原理以及解决的问题。最终,这将使您摆脱这一温和的介绍,并在下一章中编写您的第一个Rx代码。
3. 异步编程术语表
RxSwift中的某些语言与异步,反应式和/或功能性编程紧密地联系在一起,如果您首先了解以下基本术语,它将变得更加容易。
通常,RxSwift尝试解决以下问题:
3.1 状态,特别是共享的可变状态
状态有些难以定义。要了解状态,请考虑以下实际示例。
当您启动笔记本电脑时,它可以正常运行,但是,在使用了几天甚至几周后,它可能会开始变得异常或突然挂起并拒绝与您通话。硬件和软件保持不变,但是状态有所改变。重新启动后,相同的硬件和软件组合将再次正常运行。
内存中的数据,磁盘上存储的数据,对用户输入作出反应的所有工件,从云服务中获取数据后剩余的所有痕迹-这些总和就是笔记本电脑的状态。
管理应用程序的状态,尤其是在多个异步组件之间共享时,是您将在本书中学习如何处理的问题之一。
3.2.命令式编程
命令式编程是一种使用范例来更改程序状态的编程范例。就像您在和狗一起玩时使用命令式语言一样- “抓紧!躺下!假死!” —您使用命令性代码来告诉应用程序确切的时间和方式。
命令式代码类似于您的计算机可以理解的代码。CPU所做的只是遵循冗长的简单指令序列。问题在于,为复杂的异步应用程序编写命令性代码对人类来说是一个挑战,尤其是在涉及共享可变状态时。
例如,使用以下代码,该代码viewDidAppear(_:)位于iOS视图控制器中:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupUI()
connectUIControls()
createDataSource()
listenForChanges()
}
没有告诉这些方法做什么。它们会更新视图控制器本身的属性吗?更令人不安的是,它们的调用顺序是否正确?也许有人无意间交换了这些方法调用的顺序,并将更改提交给了源代码管理。现在,由于交换了呼叫,该应用程序的行为可能有所不同。
3.3.副作用
既然您对可变状态和命令式编程有了更多的了解,就可以将这两个问题中的大多数问题归结为副作用。
副作用表示对代码当前范围以外的状态所做的任何更改。例如,考虑上面示例中的最后一段代码。connectUIControls()可能会将某种事件处理程序附加到某些UI组件。这会导致副作用,因为它会更改视图的状态:应用程序在执行之前的行为方式connectUIControls()与之后不同。
每当您修改存储在磁盘上的数据或更新屏幕上的标签文本时,都会引起副作用。
副作用本身并不坏。毕竟,引起副作用是任何程序的最终目标!您的程序执行完后,您需要以某种方式更改世界的状态。
运行一段时间,什么也不做,会导致一个非常无用的应用程序。
产生副作用的重要方面是以受控方式进行的。您需要能够确定哪些代码段会产生副作用,以及哪些代码会简单地处理和输出数据。
RxSwift尝试通过解决以下几个概念来解决上面列出的问题。
3.4.声明性代码
在命令式编程中,您可以随意更改状态。在函数式编程中,您的目标是尽量减少导致副作用的代码。由于您没有生活在一个完美的世界中,所以平衡就在中间。RxSwift结合了命令式代码和功能性代码的一些最佳方面。
声明性代码可让您定义行为。每当有相关事件发生时,RxSwift都会运行这些行为,并提供不可变的,隔离的数据以供使用。
这样,您可以使用异步代码,但是可以像在一个简单for循环中一样进行假设:您正在处理不可变数据,并且可以按顺序,确定性的方式执行代码。
3.5.反应系统
反应性系统是一个相当抽象的术语,涵盖具有以下大多数或全部质量的Web或iOS应用程序:
- 响应式:始终保持用户界面为最新状态,代表最新的应用程序状态。
- 弹性:每种行为都是独立定义的,可以灵活地恢复错误。
- 弹性:代码处理各种工作负载,通常实现诸如延迟拉动驱动的数据收集,事件限制和资源共享之类的功能。
- 消息驱动:组件使用基于消息的通信来提高可重用性和隔离性,将类的生命周期和实现分离。
现在您已经对RxSwift帮助解决的问题以及如何解决这些问题有了很好的了解,现在该讨论Rx的基本组成部分以及它们如何一起发挥作用。
4. RxSwift的基础
响应式编程不是一个新概念;它已经存在了相当长的时间,但是在过去十年中,它的核心概念却卷土重来。
在那个时期,Web应用程序变得越来越复杂,并且面临着管理复杂的异步UI的问题。在服务器端,反应性系统(如上所述)已成为必需。
Microsoft的团队面临着解决本章中讨论的异步,可伸缩的实时应用程序开发问题的挑战。在2009年左右的某个时候,他们提供了一个新的客户端和服务器端框架,称为.NET(Rx)Reactive Extensions。
自2012年以来,Rx for .NET一直是开源的,允许其他语言和平台重新实现相同的功能,这使Rx成为跨平台标准。
今天,您有了RxJS,RxKotlin,Rx.NET,RxScala,RxSwift等。所有人都基于Reactive Extensions规范,努力实现相同的行为和相同的表达API。最终,使用RxSwift创建iOS应用的开发人员可以使用Web上的RxJS与其他程序员*讨论应用逻辑。
注意:有关Rx实现系列的更多信息,请访问http://reactivex.io。
像原始的Rx一样,RxSwift也可以处理您到目前为止介绍的所有概念:它处理可变状态,允许您编写事件序列,并改进了诸如代码隔离,可重用性和去耦之类的体系结构概念。
在本书中,您将涵盖使用RxSwift开发的基础概念,以及有关如何在应用程序中使用它们的实际示例。
Rx代码的三个构建块是可观察对象,运算符和调度程序。以下各节详细介绍了这些内容。
4.1 可观察的
Observable提供了Rx代码的基础:异步产生一系列事件的能力,这些事件可以“携带”类型的通用数据的不变快照Element。用最简单的话来说,它允许消费者订阅另一个对象随时间推移发出的事件或值。
该Observable级允许一个或多个观察员的任何事件做出反应实时更新应用程序的UI,或以其他方式处理和利用新的和输入数据。
ObservableType协议(要Observable遵循的协议)非常简单。一个Observable能发射(和观察员可以接收)只有三种类型的事件:
- next事件:即“携带”最新的(或“的事件下一个”)的数据值。这就是观察者“接收”价值的方式。一个Observable可发出这些值的量不定,直到出现终止事件被发射。
- 一个completed事件:该事件终止与成功的事件序列。这意味着Observable已成功完成其生命周期,并且不会发出其他事件。
- 一个error事件:该Observable终止以错误终止,不会发出其他事件。
在讨论随时间推移发出的异步事件时,您可以在时间轴上可视化整数流,如下所示
一个Observable可能发出的三个可能事件的简单约定是Rx中的所有内容。由于它是如此通用,因此您甚至可以使用它来创建最复杂的应用程序逻辑。
由于可观察的合同不对Observable观察者或观察者的性质做出任何假设,因此使用事件序列是最终的解耦实践。
您无需使用委托协议或注入闭包以允许您的类彼此对话。
为了了解一些现实情况,您将研究两种不同的可观察序列:有限和无限。
4.2 有限的可观察序列
一些可观察到的序列发出零,一个或多个值,并且在稍后一点成功终止或错误终止。
在iOS应用中,考虑从Internet下载文件的代码:
- 首先,您开始下载并开始观察传入的数据。
- 然后,当文件的一部分到达时,您将反复接收数据块。
- 如果网络连接断开,下载将停止并且连接将超时并出现错误。
- 另外,如果代码下载了所有文件的数据,它将成功完成。
该工作流程准确地描述了典型可观察对象的生命周期。看一下下面的相关代码:
API.download(file: "http://www...")
.subscribe(
onNext: { data in
// Append data to temporary file
},
one rror: { error in
// Display error to user
},
onCompleted: {
// Use downloaded file
}
)
API.download(file:)返回一个Observable实例,该实例Data作为通过网络获取的数据块发出值。
您next通过提供onNext闭包来订阅事件。在下载示例中,您将数据追加到存储在磁盘上的临时文件中。
您error通过提供onError闭包来订阅。在此关闭中,您可以error.localizedDescription在警报框中显示或以其他方式处理您的错误。
最后,要处理completed事件,请提供onCompleted闭包,在其中可以推入新的视图控制器以显示下载的文件或应用程序逻辑指示的其他任何内容。
4.3 无限的可观察序列
与文件下载或类似活动(这些活动应该自然终止或强制终止)不同,还有其他一些序列是无限的。UI事件通常是无限的可观察序列。
例如,考虑需要对应用程序中的设备方向更改做出反应的代码:
- 您将自己的班级作为观察者添加到的UIDeviceOrientationDidChange通知中NotificationCenter。
- 然后,您需要提供一个方法回调来处理方向更改。它需要从当前方向中获取当前方向UIDevice并相应地对最新值做出反应。
这种方向变化的序列没有自然的终点。只要有设备,就有可能发生方向变化的序列。此外,由于该序列实际上是无限且有状态的,因此在您开始观察它时始终会有一个初始值。
用户可能从不旋转设备,但这并不意味着事件序列已终止。这只是意味着没有事件发出。
在RxSwift中,您可以编写如下代码来处理设备方向:
UIDevice.rx.orientation
.subscribe(onNext: { current in
switch current {
case .landscape:
// Re-arrange UI for landscape
case .portrait:
// Re-arrange UI for portrait
}
})
UIDevice.rx.orientation是一个虚构的控件属性,它产生一个Observable(很容易编写自己的代码;您将在下一章中学习如何操作)。您订阅它并根据当前方向更新您的应用程序UI。您跳过onError和onCompleted参数,因为这些事件永远不会从该可观察对象发出。
4.4 Operator 操作
ObservableTypeObservable类的实现包括大量抽象异步工作和事件操作的离散部分的方法,可以将它们组合在一起以实现更复杂的逻辑。由于它们高度分离且可组合,因此这些方法通常被称为运算符。
由于这些运算符大多采用异步输入并且仅产生输出而不会引起副作用,因此它们可以像拼图块一样轻松地组合在一起,并可以构建更大的画面。
例如,采用数学表达式:(5 + 6) * 10 - 2。
在一个清晰,确定的方式,你可以申请运营商*,( ),+和-在他们预定的顺序到是其输入数据的碎片,把他们的输出,并保持加工表达,直到它的解决。
以某种类似的方式,可以将Rx运算符应用于a发出的事件,Observable以确定性地处理输入和输出,直到表达式解析为最终值为止,然后可以使用该表达式引起副作用。
这是关于观察方向变化的先前示例,已调整为使用一些常见的Rx运算符:
UIDevice.rx.orientation
.filter { $0 != .landscape }
.map { _ in "Portrait is the best!" }
.subscribe(onNext: { string in
showAlert(text: string)
})
每当UIDevice.rx.orientation产生无论是.landscape或.portrait值,RxSwift将应用filter和map对发出片数据。
首先,filter只会让not的值通过.landscape。如果设备处于横向模式,则订阅代码将不会执行,因为filter它将抑制这些事件。
如果是.portrait值,map操作员将使用Orientation输入类型并将其转换为String输出-文本"Portrait is the best!"
最后,使用subscribe,您预订结果next事件,这次带有一个String值,然后调用一种方法来在屏幕上显示带有该文本的警报。
运算符也很容易组合-它们始终将数据作为输入并输出结果,因此您可以轻松地以多种不同方式链接它们,从而实现比单个运算符自己可以做的更多的工作!
在阅读本书时,您将学到更复杂的运算符,这些运算符抽象出更多涉及异步工作的部分。
4.5 调度程序
调度程序与调度队列或操作队列的Rx等效-仅在类固醇上使用,更容易使用。它们使您可以定义特定工作的执行上下文。
RxSwift随附了许多预定义的调度程序,这些调度程序涵盖了99%的用例,并希望您不必再创建自己的调度程序。
实际上,本书前半部分的大多数示例都非常简单,通常用于观察数据和更新UI,因此,在您介绍了基础知识之前,您根本不会研究调度程序。
话虽如此,调度程序非常强大。
例如,您可以指定您要在上观察next事件SerialDispatchQueueScheduler,该事件使用Grand Central Dispatch在给定队列上串行运行代码。
ConcurrentDispatchQueueScheduler将同时运行您的代码,同时OperationQueueScheduler允许您安排对的订阅OperationQueue。
多亏了RxSwift,您可以在不同的调度程序上调度同一预订的不同工作,以实现适合您的用例的最佳性能。
RxSwift将充当您的订阅(在下面的左侧)和调度程序(在右侧)之间的调度程序,将工作片段发送到正确的上下文,并无缝地允许它们与彼此的输出协同工作。
要阅读此图,请按照(1, 2, 3, …)在不同调度程序中调度的顺序进行着色工作。例如:
- 蓝色的网络订阅以一段代码(1)运行,该代码在基于自定义OperationQueue的调度程序上运行。
- 该块输出的数据用作下一个块的输入,该块(2)在不同的调度程序上运行,该调度程序在并发后台GCD队列中。
- 最后,在(3)主线程调度程序上调度了最后一个蓝色代码,以便使用新数据更新UI。
即使它看起来非常有趣并且非常方便,但现在也不要太担心调度程序。在本书的后面,您将再次与他们联系。
5. 应用架构
值得一提的是,RxSwift不会以任何方式改变应用程序的体系结构。它主要处理事件,异步数据序列和通用通信协定。
这也是要注意重要的是你绝对不能从头开始一个项目,使其反应的应用; 您可以迭代重构现有项目的片段,或者在为应用程序构建新功能时简单地使用RxSwift。
您可以通过实现Model-View-Controller体系结构,Model-View-Presenter或Model-View-ViewModel(MVVM)或任何其他使您的生活更轻松的模式来使用Rx创建应用。
RxSwift和MVVM特别可以很好地配合使用。原因是ViewModel允许您公开Observable属性,您可以将这些属性直接绑定到View控制器的粘合代码中的UIKit控件。这使得将模型数据绑定到UI非常容易表示和编码:
在本书的最后,您将研究该模式以及如何使用RxSwift实现它。本书中的所有其他示例都使用MVC架构,以使示例代码简单易懂。
6. RxCocoa
RxSwift是通用的,与平台无关的Rx规范的实现。因此,它对任何特定于Cocoa或UIKit的类一无所知。
RxCocoa是RxSwift的配套库,其中包含专门用于UIKit和Cocoa开发的所有类。除了提供一些高级类之外,RxCocoa还向许多UI组件添加了响应式扩展,以便您可以立即订阅各种UI事件。
例如,使用RxCocoa订阅a的状态更改非常容易UISwitch,如下所示:
toggleSwitch.rx.isOn
.subscribe(onNext: { isOn in
print(isOn ? "It's ON" : "It's OFF")
})
RxCocoa将rx.isOn属性(以及其他属性)添加到UISwitch类中,以便您可以将有用的事件订阅为反应性Observable序列。
此外,RxCocoa增加了rx命名空间UITextField,URLSession,UIViewController和更多的人,甚至可以让你定义了这个命名空间,这你会了解更多关于在本书后面根据自己的反应扩展。
6.1 安装RxSwift
RxSwift是开源的,可从https://bit.ly/2ZOzK2i免费获得。
RxSwift是根据MIT许可证发行的,简而言之,您可以按原样在*或商业软件中包含该库。与所有其他MIT许可软件一样,版权声明应包含在您分发的所有应用程序中。
RxSwift存储库中有很多值得探索的地方。它包括RxSwift,RxCocoa和RxRelay库,但是您还将在其中找到RxTest和RxBlocking,这使您可以为RxSwift代码编写测试。
除了所有出色的源代码(绝对值得一看)之外,您还将找到Rx.playground,它可以交互式演示许多运算符。还可以查看RxExample,它是一个出色的展示应用程序,可以演示实践中的许多概念。
您可以通过几种不同的方式安装RxSwift / RxCocoa-通过Xcode的内置依赖性管理,Cocoapods或Carthage。
6.2 RxSwift和Combine
在本介绍性章节中,您将了解RxSwift的全部含义。我们谈到了使用RxSwift编写响应式代码的一些好处,而不是使用诸如通知中心和委托之类的更传统的API。
在总结之前,绝对有必要在我们前面已经提到的内容上进行扩展-苹果自己的反应框架叫做Combine。
RxSwift和Combine(以及Swift中的其他反应式编程框架)共享许多通用语言和非常相似的概念。
RxSwift是一个较老的,建立良好的框架,具有一些自己的原始概念,运算符名称和类型变化,这主要是由于其多平台跨语言标准,该标准在Linux上也很适用,这对Server-Side Swift非常有用。它也是开源的,因此您可以根据需要直接对其核心做出贡献,并确切了解其特定部分的工作方式。它与所有支持Swift的Apple平台版本兼容,直至iOS 8。
Combine是Apple的新颖框架,涵盖了类似概念,但专门针对Swift和Apple自己的平台进行了量身定制。它与Swift标准库共享许多通用语言,因此即使是新手也对API感到非常熟悉。它仅支持从iOS 13,macOS 10.15等开始的较新的Apple平台。遗憾的是,它截至目前还不是开源的,并且不支持Linux。
幸运的是,由于RxSwift和Combine非常相似,因此您的RxSwift知识很容易转移到Combine,反之亦然。并且RxCombine(https://github.com/CombineCommunity/RxCombine)之类的项目使您可以混合搭配RxSwift Observable和根据需要合并发布者。
如果您想了解更多关于Combine的信息,我们也已经在该框架上编写了权威书籍“ Combine:使用Swift进行异步编程”,您可以在这里查看:
6.3 社区
RxSwift项目充满活力,活动活跃,这不仅是因为Rx鼓励程序员使用它创建炫酷的软件,而且还因为围绕该项目形成的社区的积极性。
RxSwift社区非常友好,开放并热衷于讨论模式,常用技术或只是互相帮助。
除了官方的RxSwift存储库,您还可以在这里找到许多由Rx爱好者创建的项目:http://community.rxswift.org。
甚至可以在以下位置找到更多的Rx库和实验,这些库和实验在雨后如雨后春笋般冒出来:https://github.com/RxSwiftCommunity
满足许多对RxSwift感兴趣的人的最好方法可能是专用于该库的Slack频道:http://slack.rxswift.org 。
Slack频道有将近8,000名会员!日常主题包括:互相帮助,讨论RxSwift或其配套库的潜在新功能以及共享RxSwift博客文章和会议演讲。
7.然后去哪儿?
本章向您介绍了RxSwift解决的许多问题。您了解了异步编程的复杂性,共享可变状态,引起副作用等。
您尚未编写任何RxSwift代码,但是现在您了解了为什么RxSwift是一个好主意,并且您知道它可以解决的问题类型。在阅读本书的其余部分时,这应该为您提供一个良好的开端。
还有很多工作要做。您将首先创建非常简单的可观察对象,然后逐步使用MVVM架构完成实际应用。
参考
https://www.raywenderlich.com/books/rxswift-reactive-programming-with-swift/v4.0/chapters/1-hello-rxswift
https://www.raywenderlich.com/books/rxswift-reactive-programming-with-swift/v4.0/chapters/2-observables
https://www.raywenderlich.com/books/rxswift-reactive-programming-with-swift/v4.0/chapters/3-subjects
https://www.raywenderlich.com/books/rxswift-reactive-programming-with-swift/v4.0/chapters/4-observables-subjects-in-practice