您所缺少的RxSwift简介

这项工作的灵感来自@andrestaltz所缺少的《反应式编程简介》。 我针对那些由于缺乏良好参考而努力学习RxSwift的人,在RxSwift中重新创建了他的RxJS示例代码,并进行了逐步演练(就像我所做的那样)。

因此,您发现自己在学习这种新的Swift趋势时遇到了麻烦吗? 你不是一个人。

RxSwift很难,尤其是缺乏良好的参考。 每个教程要么太笼统,要么太具体,而ReactiveX文档只是无济于事:

Rx.Observable.prototype.flatMapLatest(selector,[thisArg])

通过合并元素的索引,将可观察序列的每个元素投影到新的可观察序列序列中,然后将可观察序列的可观察序列转换为仅从最新可观察序列产生值的可观察序列。

我最终研究了RxSwift示例和一些开源应用程序。 RxSwift的第一个文档引入了RxSwift的Binding或Retry,这些东西我一无所知。 另外,阅读代码也不容易,因为它同时与RxDataSources和Moya / RxSwift一起详细介绍了RxSwift。

因此,我决定编写一个示例应用程序,该示例应用程序将提供精确的“谁要遵循”模式以及逐步说明。 这等效于Andre的工作,但改用Swift编写,我希望这可以帮助您比我更轻松地学习RxSwift。

什么是反应式编程?

轻击按钮,在文本字段内键入一个字符等,由用户触发的每次出现都可以视为典型的异步事件。 如果我们的用户反复点按某个元素,或者连续在搜索栏中键入该怎么办? 这次我们有了异步事件流

  --a --- bc --- d --- X --- |-> 
  a,b,c,d是事件 
X是错误事件
| 是“完成”信号
--->是时间表

您可以从任何内容中创建数据流,而不仅仅是点击或键入事件。 流便宜且无处不在。 任何事物都可以是一个流:变量,用户输入,属性,缓存,数据结构等。例如,假设您的Twitter feed将是与点击事件相同的数据流。 您可以收听该流并做出相应的反应。

最重要的是,您将获得令人惊叹的功能工具箱,以组合,创建和过滤任何这些流。 这就是“功能”魔力的所在。流可以用作另一流的输入。 甚至多个流都可以用作另一个流的输入。 您可以合并两个流。 您可以过滤流以获得另一个只包含您感兴趣的事件的流。您可以将数据值从一个流映射到另一个新流。

  buttonTapStream:--- t ---- t--t ---- t ------ t-> 
vvvv map(t变为1)vvvv
--- 1 ---- 1--1 ---- 1 ------ 1->
vvvvvvvvv scan(+)vvvvvvvvv
counterStream:--- 1 ---- 2--3 ---- 4 ------ 5->

在“反应世界”中,流被称为“ 可观察到的” ,以时间轴表示,并按时间顺序显示正在进行的事件。 每个Observable都是不可变的 ,这意味着每个流组成将创建一个全新的Observable。

响应式编程 (RP)在响应式应用程序的开发中引入了一种全新的范例。 当今的移动应用程序与与来自后端的数据流相关的UI事件具有高度的交互性。 没有进行屏幕转换,但是用户可以在输入搜索栏时看到搜索结果,或者下拉以进行即时刷新等。

实施“谁可以遵循”建议框

让我们深入一个真实的例子。 这是Twitter的UI元素,建议您可能要关注的其他帐户

我将在下面实现工具的核心功能

  • 启动时,从API加载帐户数据并显示3条建议
  • 点击“刷新”后,将3个其他帐户建议加载到3行中
  • 在帐户行上点击“ x”按钮时,仅清除该当前帐户并显示另一个
  • 每行显示该帐户的头像及其名称。

因为Twitter不提供未经授权的公共使用的API,所以我将改用Github的API。 有一个Github API,用于为用户提供since偏移量参数。 您可以通过克隆Github存储库来检查工作代码。

请求和回应

让我们从最简单的功能开始:“启动时,从API加载帐户数据并显示3条建议”。 这很简单:

  1. 提出要求
  2. 得到回应
  3. 将响应数据呈现到UITableView

进行请求是该项目中最基本的部分。 我们已经知道了一些很棒的请求库,例如Alamofire,但是让我们首先考虑Rx。 考虑到请求的URL是一个字符串,在这种情况下为https://api.github.com/users,那么我们可以创建第一个Observable对象 :Observable

 让requestStream:Observable  = Observable.just(“ https://api.github.com/users”) 

这是URL的 ,在这种情况下,将仅发出一个事件(URL字符串)。

  --a ------ |-> 
 其中a是字符串“ https://api.github.com/users” 

requestStream只是字符串流,它什么也不做。 订阅事件时,我们需要使“真实”请求发生

  requestStream.subscribeNext { 
//向Github API发出真正的请求,返回一个`User`模型
让responseStream:Observable = UserModel()。findUsers(url)
}

注意responseStream也是一个Observable 。 您可以稍后在存储库中找到UserModel()。findUsers(url)的实现详细信息,但现在仅将其视为从Github响应返回用户列表的方法,该方法包装在Observable类型中。

因此,下一步是将此用户列表呈现给UITableView,可以通过再次订阅responseStream来完成

  requestStream.subscribeNext { 
让responseStream:Observable = UserModel()。findUsers(url)
responseStream.subscribeNext {用户在
// ...
}
}

如果您很快就注意到,我们在另一个内部有一个subscribeNext调用,这有点类似于回调地狱。 在Rx中,有一些简单的机制可以转换和创建新的流,这里对应的方法是map(f)。

 让responseStream = requestStream.map { 
返回UserModel()。findUsers(url)
}

我们刚刚创建了一个称为“元流”的野兽:一股溪流。 暂时不要惊慌。 元流是其中每个发出的值又是另一个流的流。 您可以将其视为指针:每个发出的值都是指向另一个流的指针。 在我们的示例中,每个请求URL都映射到指向包含相应响应的流的指针。

元流看起来很混乱,我们只需要一个简单的响应流,其中每个发出的值只是一个[User],而不是[User]流。 向flatMap(f)打个招呼,这是map()的一种版本,它通过在“主干”流上发射所有要在“分支”流上发射的东西来“拉平”元流。 flatmap不是“修复”,元流也不是错误; 这些实际上是用于处理Rx中异步响应的工具。

 让responseStream = requestStream.flatMap { 
返回UserModel()。findUsers(url)
}

真好 如果我们在requestStream中发生了更多事件(例如连续轻击按钮或键入文本),则将按预期在responseStream上获得相应的响应结果:

  requestStream:--url ------- url ---------- url ------------ |-> 
responseStream:----- [用户] ----- [用户] ----- [用户] ------- |->

到目前为止,加入所有代码,我们可以:

 让requestStream:Observable  = Observable.just(“ https://api.github.com/users”) 
让responseStream = requestStream.flatMap {
返回UserModel()。findUsers(url)
}
responseStream.subscribeNext {用户在
// users是一个普通的[User]列表,UI渲染部分出现了
}

刷新按钮

每当用户点击“刷新”按钮时,我们将需要一组3个新用户。 我们如何实现这种情况?

我们需要2个流:刷新按钮上的点击事件流,以及从该流转换而来的API URL流。 在RxSwift中,可以使用rx_tap方法创建点击事件流

 让refreshStream = refresh.rx_tap 
让requestStream:Observable = refreshStream.map {_
让random = Array(1 ... 1000).random()
返回“ https://api.github.com/users” +字符串(随机)
}

refresh是我们类中Refresh按钮的出口,random()是自定义扩展

因为我很笨,而且没有自动化测试,所以我破坏了以前构建的功能之一:启动时不再发生请求,仅当点击刷新按钮时才会发生。 嗯 我需要两种行为:轻按刷新按钮或刚刚加载UITableVIew时的请求。

我们知道如何针对以下每种情况制作单独的流:

 让refreshStream = refresh.rx_tap 
让requestStream:Observable = refreshStream.map {_
让random = Array(1 ... 1000).random()
返回“ https://api.github.com/users” +字符串(随机)
}
让beginStream:Observable = Observable.just(“ https://api.github.com/users”)

但是,我们如何将这两个“合并”在一起? 好吧,这里有merge()

 流A:--- a -------- e ----- o -----> 
流B:----- B --- C ----- D -------->
vvvvvvvvv合并vvvvvvvvv
--- aB --- C--e--D--o ----->

详细地:

 让requestStream = Observable.of(refreshStream,startingStream).merge() 

通过使用startWith(()) ,有一种更干净的方法来消除中间流

 让refreshStream = refresh.rx_tap.startWith(()) 
让requestStream:Observable = refreshStream.map {_
让random = Array(1 ... 1000).random()
返回“ https://api.github.com/users” +字符串(随机)
}

3条建议流

一旦我们从responseStream接收到“用户”数据,我们将立即在3个UITableVIewCells上显示它。 让我们考虑一下反应式的口头禅:“一切都是流”

因此,让我们为每个单元格创建一个单独的流。

  //内部func tableView(tableView:UITableView,cellForRowAtIndexPath indexPath:NSIndexPath) 
让userStream:Observable = responseStream.map {
保护users.count> 0,否则{return nil}
返回users.random()
}

使用刷新按钮,我们遇到一个问题:用户点击“刷新”后,当前的3条建议都不会被清除。 新建议仅在响应到达后才出现,但是要使UI看起来不错,我们需要在点击刷新时清除当前建议。 我们可以通过将Refresh tap映射到nil流并合并到上述userStream来实现:

 让nilOnRefreshTapStream:Observable  = refresh.rx_tap 
.map {_返回nil}
让proposalionStream = Observable.of(userStream,nilOnRefreshTapStream)
。合并()

并且在渲染时,我们将nil解释为“无数据”,因此隐藏了单元格的UI元素:

  proposalionStream.subscribeNext {op in 
警卫队让你= op其他{返回self.clearCell(cell)}
返回self.setCell(cell,用户:u)
} .addDisposableTo(cell.disposeBagCell)

现在的大局是:

  refreshStream:---------- o -------- o ----> 
requestStream:-r -------- r -------- r ---->
responseStream:---- R --------- R ------ R->
proposalionStream(单元格1):---- s ----- N --- s ---- Ns->
proposalionStream(Cell 2):---- q ----- N --- q ---- Nq->
proposalionStream(Cell 3):---- t ----- N --- t ---- Nt->

其中N代表零。 作为奖励,我们还可以在启动时提供“空”建议。 这可以通过将.startWith(.None)添加到建议流中来完成:

 让nilOnRefreshTapStream:Observable  = refresh.rx_tap 
.map {_返回nil}
让proposalionStream = Observable.of(userStream,nilOnRefreshTapStream)
。合并()
.startWith(.None)

结果是:

  refreshStream:---------- o -------- o ----> 
requestStream:-r -------- r -------- r ---->
responseStream:---- R --------- R ------ R->
proposalionStream(单元格1):-N--s ----- N --- s ---- Ns->
proposalionStream(单元格2):-N--q ----- N --- q ---- Nq->
proposalionStream(Cell 3):-N--t ----- N --- t ---- Nt->

结束建议并使用缓存的响应

尚需实现一项功能。 每个建议应具有自己的“ x”按钮以将其关闭,然后在其位置加载另一个。 乍一想,您可以说点击任何关闭按钮就足以发出一个新请求

  let closeStream = cell.cancel.rx_tap //“取消”是取消按钮的出口 
让requestStream = Observable.of(refreshStream,closeStream)
。合并()
.map {_ in
让random = Array(1 ... 1000).random()
返回“ https://api.github.com/users” +字符串(随机)
}

这将关闭并重新加载所有建议 ,而不仅仅是点击一个用户。 有两种不同的解决方法,为了使它有趣,我们将通过重用以前的响应来解决它。 该API的响应页面大小为100个用户,而我们仅使用了其中的3个,因此有大量可用的新鲜数据。 无需要求更多。

再次,让我们在流中思考。 当发生“关闭”点击事件时,我们想在responseStream上使用最新发出的(和缓存的)响应,以从响应列表中获取一个随机用户。 因此:

  requestStream:--r ---------------> 
responseStream:------ R ----------->
closeClickStream:------------ c ----->
proposalionStream:------ s ----- s ----->

在Rx *中,有一个名为combinateLatest(f)的组合器函数似乎可以满足我们的需要。 它以两个流A和B作为输入,每当两个流中的任何一个发出值时,CombineLatest都会将两个流中最近发出的两个值a和b联接起来,并输出值c = f(x,y),其中f是一个函数您定义。 最好用图来解释:

 流A:--a ----------- e -------- i --------> 
流B:----- b ---- c -------- d ------- q ---->
vvvvvvvv CombineLatest(f)vvvvvvv
---- AB --- AC--EC --- ED--ID--IQ ---->
 其中f是大写函数 

我们可以在closeStream和responseStream上应用CombineLatest(),以便每当点击关闭按钮时,我们都会发出最新的响应,并在RecommendationionStream上产生一个新值。 另一方面,combinateLatest(f)是对称的:只要在responseStream上发出新的响应,它将与最新的“关闭”分接头结合以产生新的建议。

 让closeStream = cell.cancel.rx_tap 
让userStream:Observable = Observable.combineLatest(closeStream,responseStream)
{(_,个用户)在
保护users.count> 0,否则{return nil}
返回users.random()
}
 让nilOnRefreshTapStream:Observable  = refresh.rx_tap.map {_ in return nil} 
让proposalionStream = Observable.of(userStream,nilOnRefreshTapStream)
。合并()
.startWith(.None)

拼图中仍然缺少一件。 CombineLatest(f)使用两个源中的最新源,但是如果这些源之一尚未发出任何信号,则CombineLatest(f)将无法在输出流上产生数据事件。 如果查看上面的ASCII图,您将看到第一个流发出值a时输出没有任何内容。 仅当第二个流发出值b时,它才能产生输出值。

有多种解决方法,我们将继续使用最简单的方法,即在启动时模拟点击“关闭”按钮:

 让closeStream = cell.cancel.rx_tap.startWith(()) 

包起来

我们完了。 完整的代码如下

 让refreshStream = refresh.rx_tap.startWith(()) 
让requestStream:Observable = refreshStream.map {_
让random = Array(1 ... 1000).random()
返回“ https://api.github.com/users” +字符串(随机)
}
让responseStream = requestStream.flatMap {
返回UserModel()。findUsers(url)
}

//内部func tableView(tableView:UITableView,cellForRowAtIndexPath indexPath:NSIndexPath)
让closeStream = cell.cancel.rx_tap.startWith(())
让userStream:Observable = Observable.combineLatest(closeStream,responseStream)
{(_,个用户)在
保护users.count> 0,否则{return nil}
返回users.random()
}
让nilOnRefreshTapStream:Observable = refresh.rx_tap.map {_ in return nil}
让proposalionStream = Observable.of(userStream,nilOnRefreshTapStream)
。合并()
.startWith(.None)

proposalionStream.subscribeNext {op in
警卫队让你= op其他{返回self.clearCell(cell)}
返回self.setCell(cell,用户:u)
} .addDisposableTo(cell.disposeBagCell)

您可以在回购中看到工作示例。

这个例子很小但是很密集:它的特点是对多个事件进行管理,并适当分离关注点,甚至缓存响应。 功能风格使代码看起来更具声明性,而不是命令性:我们没有给出要执行的指令序列,我们只是通过定义流之间的关系来告知某些内容。 例如,在Rx中,我们告诉计算机,proposalStream是closeStream与最新响应中的一个用户组合,此外在刷新或程序启动发生时为nil。

还请注意,缺少控制流元素(如if,for,while)以及Swift / IOS应用程序所期望的典型的基于回调的控制流。 您甚至可以通过使用filter()在上面的subscribeNext()中摆脱if和else(我将把实现细节留给您作为练习)。 在Rx中,我们具有流功能,例如映射,过滤,扫描,合并,combinateLatest,startWith等,用于控制事件驱动程序的流程。 该功能工具集以更少的代码为您提供更多功能。

从这往哪儿走

如果您认为RxSwift将是IOS编程的首选库,请花一些时间来熟悉RxSwift API的转换,组合和创建Observable的方法。 如果您想了解流图中的那些功能,请查看大理石图。 每当您尝试执行某项操作时遇到困难时,请绘制这些图,对其进行思考,查看一长串的函数并进行更多考虑。 根据我的经验,此工作流程非常有效。

一旦开始使用RxSwift进行编程,就需要习惯使用它的库,例如RxCocoa,Moya / RxSwift,RxDataSources,然后是Driver等。最后,通过学习实际的函数式编程来进一步提高技能,并了解影响Rx的副作用等问题。

如果本教程对您有帮助,请转发它。

法律

它主要由Andre Cesar de Souza Medeiros(别名“ Andre Staltz”)在2014年创建,并由Vu Nhat Minh(@Orakaro)在2016年修改。

Vu Nhat Minh的“ RxSwift简介”已获得知识共享署名-非商业4.0国际许可证的许可。