“如果您曾经使用过基于异步回调的API,那么您可能已经处理过响应数据贯穿整个基础代码,并且很可能发现没有办法对它们进行单元测试……但是,让我告诉你-有一个更好的方法,它被称为Rx!”
— Krunoslav Zaher, RxSwift的创造者
无可否认:RX是当今移动应用开发中最热门的话题之一!
如果你去参加国际会议,甚至是当地的交流会,你可能会觉得每个人都在谈论observables, side effects, 和 (gulp) schedulers.
并且不足为奇的是 —— RX是一个多平台标准,因此无论是Web开发会议、本地Android 交流会,还是Swift研讨会,您都可能最终加入RX的多平台讨论。
RXSwift库(跨平台跨语言的Rx接口众多系列的一部分)允许您以全新的方式使用您最喜欢的Swift编程语言。在swift中处理异步代码有些困难,使用Rxswift编写变得更加容易和明智。
要创建快速响应的,健壮的应用程序,您必须处理许多并发任务,如播放音频、处理用户界面输入、进行网络呼叫等。有时,将数据从一个处理传递到另一个处理,甚至只是观察任务以正确的顺序一个接一个异步地发生,可能会给开发人员带来很多麻烦。
在本书中,您将学习到RXSwift如何解决异步编程的相关问题,并掌握各种响应式技术,从观察简单的数据序列,到组合转换异步数据值流,到设计架构和构建高效优质的应用程序。
在这本书的结尾,你已经完成了章节内容,你将有实践经验解决章节结尾的挑战-你将找到自己的Rx模式和解决方案!
在本书的这一部分中,您将学习RXSwift的基础知识。您将看到RXSwift目的是解决哪些异步编程问题,以及它提供了哪些解决方案。
此外,您还将了解几个基本类,这些类允许您创建和观察事件序列,这些事件序列是RX框架的基础。
你将慢慢开始学习基础知识和一点理论。请不要跳过这些章节!当以后的章节变得更复杂时,这将帮助您在以下部分取得良好的进展。
第一节:你好,Swift!
本书旨在向读者介绍RXSwift库以及使用Swift编写响应式iOS应用程序。
但RXSwift到底是什么?这里有一个很好的定义:
Rxswift是一个通过使用可观察的序列和函数式运算符来编写异步和基于事件的代码的库,允许通过schedulers进行参数化执行。
听起来很复杂?如果有,别担心。写响应式的程序,理解它们背后的许多概念,以及浏览大量相关的、常用的术语,可能会让人望而生畏——尤其是当你试图一次把所有内容都吸收,或者当它还没有以结构化的方式被介绍给你时。
这就是本书的目标:通过解释如何使用每个API,然后介绍其在iOS应用程序中的实际使用,逐步向您介绍各种RXSwift API和RX概念。
您将从RXSwift的基本特性开始,然后逐步学习中级和高级主题。随着你的进步,花时间广泛地练习新概念将使你更容易在书的末尾掌握RXSwift。RX的主题太宽泛,无法在一本书中完全涵盖;相反,我们的目标是让您对库(RXSwift)有一个扎实的了解,这样您就可以继续自己提高RX技能。
我们还没有完全确定RxSwift到底是什么,对吗?让我们从一个简单、易懂的定义开始,并逐步发展到一个更好、更具表现力的定义,正如我们在本章后面的响应式编程主题中所做的那样。
RXSwift本质上简化了异步程序的开发,它允许您的代码对新数据作出反应,并以一种连续、独立的方式处理它。
作为一个iOS应用程序开发人员,与本章前面阅读的第一个定义相比,这个定义应该更清楚,并告诉您更多关于RXSwift是什么。
即使您在细节上仍然很模糊,但很明显RxSwift可以帮助您编写异步代码。而且您知道开发好的、确定性的、异步的代码是很困难的,所以任何帮助都是受欢迎的!
异步编程介绍
如果您试图用一种简单的、切实的语言来解释异步编程,那么您可能会想到以下几点。
iOS应用程序在任何时候都可能执行以下任何或更多操作:
响应按钮点击
当文本字段失去焦点时的键盘动画
从网上下载大图像
保存小量数据到磁盘
播放音频
所有这些事情似乎同时发生。每当键盘出现屏幕消失的动画时,应用程序中的音频在动画完成之前不会暂停,对吗?
程序中所有不同的部分都不会阻塞彼此的执行。iOS为您提供各种API,允许您在不同的线程上执行不同的工作,并在设备CPU的不同内核上执行这些工作。
然而,编写真正并行运行的代码是相当复杂的,特别是当不同的代码位需要处理相同的数据块时。很难说哪段代码先更新数据,或者哪段代码读取到最新的值。
Cocoa和UIKit异步API
苹果在iOS SDK中提供了许多API,帮助您编写异步代码。你已经在你的项目中使用了这些,可能还没有给他们第二个想法,因为它们是编写移动应用程序的基础。
您可能使用了以下大部分:
NotificationCenter:在发生感兴趣的事件时执行一段代码,例如用户更改设备的方向,或屏幕上显示或隐藏的软件键盘。
Delegate:定义其他类或API在任意时间执行的方法。例如,在应用程序委托中,您定义了当新的远程通知到达时应该发生什么,但是您不知道何时执行这段代码,也不知道它将执行多少次。
GCD:帮助您抽象每一部分工作的执行。您可以安排代码在一个串行队列中按顺序执行,或者在具有不同优先级的不同队列上同时运行多个任务。
闭包:创建可以在类之间传递的分离代码段,以便每个类可以决定是否执行、执行多少次以及在什么上下文中执行。
由于大多数典型的类都是异步的,并且所有的UI组件都是异步的,所以不可能假定应用程序整个代码的执行顺序是什么。
毕竟,应用程序的代码运行方式会因各种外部因素而不同,如用户输入、网络活动或其他操作系统事件。每次用户启动应用程序时,代码的运行顺序可能完全不同,这取决于这些外部因素。(好吧,除了当你有一大群机器人在测试你的应用程序时,你可以期望所有的事件都能以精确的方式发生。)
我们肯定不是说编写好的异步代码是不可能的。毕竟,上面列出的来自苹果的伟大API是非常先进,非常专业的用于解决任务,公平地说,比其他平台提供的功能强大。
问题是,复杂的异步代码变得非常难以编写,部分原因是苹果的SDK提供了多种API。
使用委托要求您采用一种特定的模式,另一种模式用于闭包,还有一种订阅通知中心的方法,等等。由于所有异步API都没有通用语言,因此读取和理解代码以及对代码执行的推理变得困难。
为了总结这一部分并将讨论放在更大的上下文中,您将比较两段代码:一段同步代码和一段异步代码。
同步代码
对数组的每个元素执行操作是您已经做过很多次的事情。它是应用程序逻辑的一个非常简单但坚实的构建块,因为它可以保证两件事情:它同步执行,并且当您在它上面迭代时,集合是不可变的。
花点时间想想这意味着什么。当您迭代一个集合时,不需要检查所有元素是否仍然存在,也不需要在另一个线程在集合开始的插入一个元素时倒回。您假定总是在循环开始时对集合进行整体迭代。
如果您想更多地使用for循环的这些方面,请在playground中尝试:
var array = [1, 2, 3]
for number in array {
print(number)
array = [4, 5, 6]
}
print(array)
数组在for主体内是否可变?循环循环迭代的集合是否会更改?所有命令的执行顺序是什么?如果需要,你能修改number吗?
异步的代码
考虑类似的代码,但假设每次迭代都是对点击按钮的反应。当用户反复点击按钮时,应用程序会打印出数组中的下一个元素:
var array = [1, 2, 3]
var currentIndex = 0
//this method is connected in IB to a button
@IBAction func printNext(_ sender: Any) {
print(array[currentIndex])
if currentIndex != array.count-1 {
currentIndex += 1
}
}
在与前一个代码相同的上下文中考虑这个代码。当用户点击按钮时,会打印出数组的所有元素吗?你真的说不出来。另一段异步代码可能会在打印前删除最后一个元素。或者,在继续之后,另一段代码可能会在集合的开头插入一个新元素。
另外,您假设只有printNext(:)会更改currentIndex,但是另一段代码也可能会修改currentIndex——也许您在实现上述函数之后在某个地方添加的一些聪明的代码。
您可能已经认识到,编写异步代码的一些核心问题是:a)执行任务的顺序 b)共享可变数据。
幸运的是,这是一些RXSwift的强大的合适之处!
接下来,您需要一个良好的语言入门,它将帮助您开始理解RxSwift是如何工作的,它解决了哪些问题,并最终让您通过这个温和的介绍,在下一章中编写第一个Rx代码。
异步编程术语
RXSwift中的某些语言与异步、响应式和/或函数式编程紧密相连,因此如果您首先理解以下基本术语,以后的学习会更容易。
一般来说,RXSwift试图解决以下问题:
1。状态,特别是共享的可变状态
状态有点难以定义。要了解状态,请考虑以下实际示例。
当你启动你的笔记本电脑的时候,它运行的很好,但是当你使用它几天甚至几周后,它可能会开始表现出奇怪或突然停止,拒绝和你说话。硬件和软件保持不变,但改变的是状态。一旦重新启动,相同的硬件和软件组合将再次正常工作。
内存中的数据、存储在磁盘上的数据、对用户输入做出反应的所有工件、从云服务获取数据后留下的所有痕迹——这些总和就是您的笔记本电脑的状态。
管理应用程序的状态,尤其是在多个异步组件之间共享时,是本书中您将学习如何处理的问题之一。
2。命令式程序设计
命令式编程是一种使用语句改变程序状态的编程范例。就像你在和你的狗玩的时候使用命令式语言一样-“抓取!躺下!装死!“-您使用命令式代码来准确的告诉应用程序何时以及如何做事情。
命令式代码与计算机理解的代码类似。CPU所做的只是遵循一系列冗长的简单指令。问题是,对于人类来说,为复杂的异步应用程序编写命令式代码变得越来越困难,尤其是当涉及到共享可变状态时。
例如,在iOS视图控制器的viewdidAppear(_:)中找到此代码:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupUI()
connectUIControls()
createDataSource()
listenForChanges()
}
不知道这些方法做什么。它们是否更新视图控制器本身的属性?更令人不安的是,他们是按正确的顺序调用的吗?可能有人无意中交换了这些方法调用的顺序,并将更改提交给了源代码管理。现在,由于交换的调用,应用程序的表现行为可能会有所不同。
三。副作用
既然您对可变状态和命令式编程有了更多的了解,那么您就可以用这两件事来确定大多数问题的副作用。
副作用是对当前范围之外状态的任何更改。例如,考虑上面例子中的最后一段代码。connectUIControls()可能会将某种事件处理程序附加到某些UI组件上。这会导致副作用,因为它会改变视图的状态:应用程序在执行connectUIControls()之前的行为是一种方式,之后的行为则不同。
每当您修改存储在磁盘上的数据或更新屏幕上标签的文本时,都会产生副作用。
副作用本身并不坏。毕竟,造成副作用是任何程序的最终目标!在程序完成执行之后,您需要以某种方式改变世界的状态。运行一段时间,什么都不做,这是一个非常无用的应用程序。
产生副作用的问题是以可控的方式进行。您需要能够确定哪些代码片段会导致副作用,哪些代码只是处理和输出数据。
Rxswift试图通过处理以下几个概念来解决上面列出的问题。
4。声明性代码
在命令式编程中,您可以随意更改状态。在函数代码中,不会产生任何副作用。因为你没有生活在一个完美的世界里,平衡就在中间。RXSwift结合了命令式代码和功能性代码的一些优势。
声明性代码允许您定义行为片段,Rxswift将在任何时候发生相关事件时运行这些行为,并为它们提供一个不可变的、独立的数据输入以供使用。
通过这种方式,您可以使用异步代码,但要做出与简单的for循环相同的假设:您使用的是不可变的数据,并且可以以顺序的、确定性的方式执行代码。
5。响应式系统
反应式系统是一个相当抽象的术语,涵盖Web或iOS应用程序,这些应用程序具有以下大部分或全部特性:
•响应:始终保持用户界面最新,代表最新的应用程序状态。
•灵活:每个行为都是孤立定义的,并提供灵活的错误恢复。
•弹性:代码处理不同的工作,通常包括实现延迟拉取数据集合、事件节流和资源共享等功能。
消息驱动:组件使用基于消息的通信来提高可重用性和独立性,将类的生命周期和实现分离开来。
既然您已经对Rxswift帮助解决的问题以及它如何处理这些问题有了很好的理解,现在是时候讨论Rx的构建块以及它们如何一起发挥作用了。
RxSwift基础
反应式编程不是一个新概念;它已经存在了相当长的时间,但是它的核心概念在过去的十年中已经有了显著的恢复。
在那个时期,Web应用程序变得更加复杂,并且面临着管理复杂异步UI的问题。在服务器端,响应式系统(如上所述)已成为必要的。
微软的一个团队承担了解决异步、可扩展、实时应用程序开发问题的挑战,我们在本章中已经讨论过这些问题。他们在一个独立于公司核心团队的库中工作,大约在2009年,他们提供了一个新的客户端和服务器端框架,称为Reactive Extensions for .NET (Rx)。
在.NET 4中。自2012年以来,它一直是一个开源组件。开放源代码允许其他语言和平台重新实现相同的功能,这使RX成为跨平台标准。
今天您有RXJS、RXKotlin、RX.net、RXScala、RXSwift等。所有这些库都努力实现相同的行为和相同的表达API。最后,开发人员使用RxSwift创建iOS应用程序,可以与另一个在Web上使用RxJS的程序员自由讨论应用程序逻辑。
与最初的Rx一样,RxSwift也可以处理到目前为止所涉及的所有概念:它处理可变状态,它允许您组合事件序列并改进体系结构概念,如代码独立性、可重用性和去耦。
让我们重温一下这个定义:
RxSwift在传统的命令式cocoa代码和纯粹的函数代码之间找到了一个最佳位置。它允许您使用不可变的代码定义,以确定的、可组合的方式异步处理输入片段,从而对事件作出响应。
在本书中,您将被介绍RXSwift开发的基础概念,以及如何在应用程序中使用它们的实际示例。
RX代码的三个组成部分是observables, operators, 和schedulers。下面的章节详细介绍了这些内容。
Observable类提供了RX代码的基础:异步产生一系列事件的能力,它可以“携带”数据T的不可变快照。用最简单的话,它允许类订阅另一个类随着时间的推移发出的值。
Observable<T>类允许一个或多个观察者实时响应任何事件并更新应用程序UI,或者处理和使用新传入的数据。
ObservableType协议(Observable<t>符合该协议)非常
简单。一个Observable只能发射(并且观察者可以接收)三种类型的事件:
next 事件:携带最新的(或下一个)数据值的事件。这是观察者接收值的方式。
completed 事件:此事件成功终止事件序列。它意味着Observable成功地完成了它的生命周期,不会发出任何其他事件。
error 事件:可观察到的以错误终止,不会发出其他事件。
当谈论随着时间推移而发出的异步事件时,您可以在时间线上形象化一个observable整数序列,如下所示:
一个Observable能发出的三种可能事件的简单契约就是Rx的一切。因为它是如此通用,所以您可以使用它来创建甚至最复杂的应用程序逻辑。
由于observable契约没有对Observable或观测者的性质做任何假设,因此使用事件序列是最终的解耦实践。您不需要使用委托协议,也不需要注入闭包来允许类之间进行对话。
为了了解一些现实生活中的情况,您将看到两种不同的observable序列:有限序列和无限序列。
有限observable序列
一些observable序列发出 零个,一个,或多个值,并且在以后的某个时刻,要么成功停止,要么因为错误停止。
在iOS应用程序中,考虑从Internet下载文件的代码:
•首先,开始下载并观察进来的数据。
•然后,随着文件的每一部分进来,您会重复接收数据块。
•如果网络连接中断,下载将停止,连接将超时并出现错误。
•或者,如果代码下载了文件的所有数据,它将成功完成。
这个工作流程准确地描述了一个典型的observable的生命周期。请看下面的相关代码:
API.download(file: "http://www...")
.subscribe(onNext: { data in
... append data to temporary file
},
onError: { error in
... display error to user
},
onCompleted: {
... use downloaded file
})
API.download(file:)返回一个observable<data>实例,该实例在数据块通过网络过来时发出数据值。
你可以通过提供onNext闭包来订阅next事件。在这个下载的例子中,您将数据附加到存储在磁盘上的临时文件中。
最后,为了处理一个已完成的事件,您提供了一个onCompleted的闭包,在这个闭包中您可以推出一个新的视图控制器来显示下载的文件或应用逻辑指示的任何其他内容。
无限observable序列
不同于文件下载或类似的活动,它们应该自然地或强制地终止,还有其他的序列是无限的。通常,用户界面事件是无限的可观察序列。
例如,考虑一下对应用程序中的设备方向更改做出反应所需的代码:
•将类作为观察者添加到uideviceOrientationDidChange通知中。
•然后需要提供方法回调来处理方向更改。它需要从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<orientation>(你自己很容易进行编码,你将在下一章中学习如何编码。您订阅它并根据当前的方向更新应用程序UI。您可以跳过onError和onCompleted参数,因为这些事件永远不会从observable中发出。
Operators 操作符
ObservableType和Observable类的实现包括许多方法,这些方法抽象了异步工作的独立部件,可以组合在一起实现更复杂的逻辑。
因为它们是高度解耦和可组合的,所以这些方法通常被称为运算符。由于这些运算符大多采用异步输入,只生成输出而不产生副作用,因此它们可以很容易地组合在一起,就像拼图块一样,并努力构建更大的画面。
例如,以数学表达式(5+6)*10-2为例。
以一种明确的、确定性的方式,您可以将运算符*、()、+和-按它们预先定义的顺序应用到作为它们输入的数据块上,获取它们的输出并继续处理表达式,直到解决它为止。
以某种类似的方式,您可以将rx运算符应用于Observable发出的的输入片段,以确定地处理输入和输出,直到表达式解析为最终值,然后使用该值来产生副作用。
以下是关于观察方向变化的前一个示例,调整为使用一些常见的RX操作符operators:
UIDevice.rx.orientation
.filter { value in
return value != .landscape
}
.map { _ in
return "Portrait is the best!" }
.subscribe(onNext: { string in
showAlert(text: string)
})
每次uidevice.rx.orientation生成一个.landscape或.portrait值时,rx都会对发出的数据应用几个operators操作符。
首先,筛选器filter将只允许通过不是.landscape的值。如果设备处于.landscape模式,则不会执行订阅代码,因为筛选器filter将禁止这些事件。
对于.Portrait值,map操作符将获取方向类型输入并将其转换为字符串输出-文本“Portrait is the best!”.
最后,使用subscribe可以订阅产生的next事件,这次带有一个字符串值,并调用一个方法在屏幕上显示带有该文本的提示框。
这些运算符也是高度可组合的-它们总是将数据作为输入并且输出结果,因此您可以通过许多不同的方式轻松地将它们链接起来,从而实现比单个运算符本身所能做的更多的功能!
在阅读本书的过程中,您将了解到更复杂的运算符,这些运算符抽象了更多涉及异步工作的部分。
Schedulers 调度器
调度器Schedulers是RX中等同于调度队列的调度程序——仅使用steroids,而且更容易使用。
RXSwift附带了许多预定义的schedulers调度器,它们涵盖了99%的用例,希望这意味着您永远不需要创建自己的schedulers调度器。
事实上,本书前半部分中的大多数示例都非常简单,通常处理观察数据和更新用户界面,因此在介绍了基础知识之前,您根本不会研究schedulers调度器。
也就是说,schedulers调度器非常强大。
例如,可以指定要观察SerialDispatchQueueScheduler上的next事件,它在给定的队列上使用GCD串行的运行你的代码。
ConcurrentDispatchQueueScheduler将会并发的运行你的代码。
OperationQueueScheduler将会允许你在给定的NSOperationQueue上调度你的订阅。
由于Rxswift,您可以在不同的schedulers调度器上调度同一订阅的不同工作,以获得最佳性能。
Rxswift将充当订阅(在下面的左侧)和调度器(在右侧)之间的调度员,将工作片段发送到正确的上下文,并无缝地允许它们与彼此的输出一起工作。
要阅读此图,按照序列中彩色的工作,它们被调度(1,2,3)给不同的调度器schedulers。例如:
•蓝色网络订阅从一段代码(1)开始,该代码在基于自定义NSOperation的调度器scheduler上运行。
•此块的数据输出用作下一个块(2)的输入,该块在一个并发后台GCD队列上的不同调度器scheduler上运行。
•最后,在主线程调度器上调度最后一段蓝色代码(3),以便用新数据更新UI。
即使它看起来很有趣,也很方便,现在也不要为调度器schedulers操心太多。在这本书的后面,你会回到他们那里。
App架构
值得一提的是,Rxswift不会以任何方式改变应用程序的架构;它主要处理事件、异步数据序列和通用通信契约。
你可以使用RX创建实现苹果开发者文档中定义的MVC架构的应用。您也可以选择实现MVP体系结构或MVVM,如果您喜欢的话。
如果您想这样做,RXSwift对于实现您自己的单向数据流体系结构也非常有用。
重要的是要注意,你绝对不必从头开始一个项目,使它成为一个反应式的应用程序;你可以迭代地重构一个现有项目的片段,或者在向你的应用程序附加新功能时简单地使用Rxswift。
微软的MVVM体系结构是专门为在提供数据绑定的平台上创建的事件驱动软件开发的。Rxswift和MVVM确实可以很好地结合在一起,在本书的最后,您将研究这个模式以及如何用Rxswift实现它。
mvvm和rxswift之所以能很好地结合在一起,是因为ViewModel允许您暴露Observable<T>属性,您可以直接将这些属性绑定到视图控制器粘合代码中的uikit控件。这使得将模型数据绑定到UI和代码非常简单:
本书中的所有其他示例都使用MVC体系结构,以使示例代码简单易懂。