如何从RxSwift开始-逐步

在这个简短的教程中,我们将创建一个简单的应用程序,该应用程序将使用Coin Market API来获取所有加密货币的最新报价。 这次的不同之处在于,最重要的部分将使用RxSwift和反应式编程来实现。

完成后,该应用程序将如下所示:

没什么特别的,但true内藏着真正的美。

您可以使用以下链接下载入门项目:

入门项目

如果您在工作中陷入困境,只需看一看托管在我的GitHub帐户上的已完成项目。

的GitHub

为了方便起见,我已经安装了所有依赖项,但是您应该打开podfile来检查将使用的内容:

  pod'RxSwift','〜> 4.0' 
pod'RxCocoa','〜> 4.0'
pod'SwiftyJSON'

我想我不需要告诉您有关RxSwift库😆的信息。 我们将使用SwiftyJSON解析服务器对代表应用程序中模型层的对象的响应。 您无需为此烦恼,因为我们只会在一个地方使用它。

对于第二个依赖项,需要简短说明。 RxCocoa基于RxSwift并包含大量有用的API,它们为Apple库中使用的最受欢迎的工具(例如URLSessionUITableView)添加了“反应性”扩展 。 RxCocoa应该自己撰写整篇文章(可能不止一篇),但是这次我们仅使用它来扩展URLSession对象功能。

您可以使用CoinsCap_RxSwift.xcworkspace文件打开您的项目。 编译并在模拟器或物理设备上运行它,以检查是否一切正常。 现在,您的眼睛应该看到类似以下的内容:

目前没有什么特别的事情发生,但是我们会在短时间内对此进行处理。 现在,看一下项目结构。 最重要的文件位于CoinCap文件夹中。 您可以在其中找到两个UIViewControllers对象和代表我们的数据模型的Struct。

现在,我们将处理应用程序中最重要的部分-从远程服务器获取数据。

打开MainViewVC.swift文件。 我们将在getCurrentCoinsCap(fromUrl 🙂函数中编写用于获取加密货币数据的代码。 对于此任务,我们将使用URLSession,因此有关此API的基本知识将非常受欢迎。 但是我们不会使用默认实现。 这次,我们将使用RxCocoa库提供的自定义反应式扩展。 这将使我们编写反应式代码。 稍后您会明白我的意思。

getCurrentCoinsCap(fromUrl :)函数仅采用字符串类型的一个参数,该参数将用于创建硬币市场URL。 首先在函数内添加以下代码:

 让响应= Observable.just(url) 

您将创建一个Observable对象,该对象仅包含一个元素(因此将其命名为“ just”),该对象将在创建订阅时发出。 这是创建完整URL的起点。

使用作为RxSwift一部分的map函数,我们将创建一个适当的URL对象。 在getCurrentCoinsCap(fromUrl :)函数的末尾添加以下代码:

  .map {url-> URL in return URL(string:url)!} 

您需要注意此特定的地图功能。 这与标准Swift库的一部分功能不同。 它具有相同的目的(更改集合中的所有值),但是您应该知道它来自另一个库。 在这种情况下,来自RxSwift。

在上面的代码中,我们使用快捷方式来强制打开URL对象,但是这样做只是为了使示例尽可能简单。 在日常工作中,请确保使用防护罩或其他任何方式来检查您是否使用了有效值。

不必输入块返回的数据类型,但是如果没有输入,编译器有时可能会丢失一些,因此为安全起见最好添加它们。

让我们继续前进。 现在,我们有了一个正确的URL对象,因此我们可以添加将实际将请求发送到服务器的代码:

  .map {url-> URLRequest返回URLRequest(url:url)} 

同样,我们使用map函数从数据流中创建URLRequest对象。 在这种情况下,它将只是一个URL对象。

现在,我们可以将请求发送到服务器。 这次,我们将使用flatMap函数(也是RxSwift的一部分)。 将此代码添加到我们的说明链中:

  .flatMap {request-> Observable 返回URLSession.shared.rx.response(request:request)} 

我们需要在这里暂停一秒钟,所以我可以告诉您有关flatMap函数的特殊功能。 它不仅会破坏不同的数据流,而且还会保持整个指令链的执行, 直到异步代码执行结束为​​止。

上面的代码正是这种情况。 使用URLSession从远程服务器获取数据是异步完成的,flatMap函数正在耐心等待响应。 当数据从服务器进入时,flatMap将允许指令链继续。

我想您已经注意到一种奇怪的方式来编写代码以使用URLSession来获取数据的代码。 response(request 🙂函数是RxCococa库的一部分,该库将反应性扩展添加到URLSession API。

从response(request :)函数返回的对象是Observable,它将发出两种类型的数据-HTTPURLResponseData 。 当我们填充来自服务器的开始过滤响应时,我们将在短时间内使用它们。

Observable将仅发射一次数据,然后将传递onCompleted()事件,这表明Observable已完成工作。 请注意,为了处理服务器请求,我们只需要几行代码。

跟踪正在发生的事情很容易,所以让我们总结一下到目前为止已经完成的工作:

1.从一开始,我们就在getCurrentCoinsCap(fromURL :)函数内部创建了一个Observable,它将仅发出一个String类型的对象。
2.使用map函数和一个参数,我们创建了URL对象。
3.我们再次使用了地图功能来创建URLRequest。
4.在最后一步中,在flatMap函数内部,我们使用了URLSession反应性扩展将请求发送到服务器。

在此阶段,我们具有用于从服务器下载数据的功能代码,但是如果要在我们的应用程序中使用此数据,我们还有一些工作要做。 我们需要将其解析为Coins collection 。 您可以在Coin.swift文件中找到合适的结构。

为了简化操作 ,我们将使用podfile中已包含的SwiftyJSON库 。 有了它,我们可以轻松地反序列化来自服务器的JSON格式的数据。

我们可以从过滤响应开始,该响应将包含我们感兴趣的数据。在MainViewVC.swift文件中添加以下函数:

  func filterSuccesResponse(_响应:Observable ){} 

我们将使用此功能来转换来自服务器的数据。 作为参数,我们将传递在getCurrentCoinsCap(fromURL :)函数中创建的Observable。

将此代码添加到函数内部:

  response.observeOn(MainScheduler.instance).filter {响应,_返回200 .. <300〜= response.statusCode} 

watchOn()允许我们指定要在哪个线程上进行“观察”。 从远程服务器获取数据是在后台线程上进行的,但是当我们需要更新任何UI(例如表视图)时, 我们需要使用主线程 (也称为UI线程)。 我们当然可以为此使用Grand Central Dispatch,但是RxSwift提供了另一种解决方案。

在上面的示例中,我们传递了MainScheduler对象实例作为observeOn()函数的参数,该函数指定我们要使用主线程来处理数据流。

值得一提的是,RxSwift不会在应用程序中创建新线程,而只会使用Apple提供给我们的内容。 有两种可能的解决方案-DispatchQueues( GDC)NSOperationQueue(NSOperation) 。 我现在不会详细讨论它们,因为我想使示例尽可能简单。

使用过滤器功能,我们仅传递包含statusCode指示成功操作的响应。 在本文的以下部分中,我们将处理通知错误的代码过滤。

在下一步中,我们将转换来自服务器的JSON数据。 在我们的链中添加另一个代码:

  .map {_,数据-> JSON in do {让json =尝试JSON(data:data)返回json} catch(出现错误){print(“ Error \(error.localizedDescription)”)返回JSON()}} 

使用map函数,我们正在创建JSON对象的集合。 不幸的是,这里很容易出错。 在这种情况下,我们指的是来自SwiftyJSON的JSON对象,而不是来自服务器的“原始” JSON对象。

在“ do”块中,我们检查是否可以将下载的数据转换为所需的对象。 如果失败了,我们将在控制台中打印一条错误消息(如果需要,您可以显示一个弹出窗口)并返回一个空的JSON对象。 这就是我喜欢处理错误的方式。 除了显示难以理解的消息外,我更喜欢在屏幕底部显示一个微妙的弹出窗口以告知错误,然后显示一个空列表。

继续。 将此代码添加到链中:

  .filter {返回对象中的对象。count> 0} 

在这种情况下,过滤器功能使我们可以丢弃空的集合,而仅传递包含某些元素的集合。

现在,我们可以根据获取的数据创建一些对象。 将以下代码添加到链中:

  .map {objects-> [Coin] in if let dataObjects = objects.array {return dataObjects.map {Coin(coinData:$ 0)}}} else {return [Coin(coinData:JSON())]}} 

这次我们使用map函数创建一个数组,该数组将存储我们的Coins对象。 第一步是确保我们可以将集合用作标准数组。 如果是真的,那么我们可以使用标准的Swift映射函数将JSON对象解析为所需的Coins对象。 如果第一步的计算结果为false,我们将只返回一个只有空硬币对象的数组。

现在是时候进行大结局了。 在下一步中,我们将创建一个适当的订阅,因此我们可以“侦听”传入的值:

  .subscribe(onNext:{自我中的[弱自我]硬币?.updateUIWithCoins(coinsCollection:硬币)})。disposed(作者:disposeBag) 

订阅数据流后,我们将收到一个包含Coins对象的集合。 执行该链中的所有任务后,将生成此集合。 我们将使用它来更新UI。 在链的最后,我们将订阅添加到disposeBag对象,当将ViewController对象从内存中删除时,该对象将处理所有未使用的资产。 在这种情况下,仅当我们关闭应用程序时才会发生,因为MainViewVC是根视图控制器。

filterSuccesResponse()函数的实现已准备就绪,因此您可以将其添加到getCoinsFromURL()函数的末尾:

  filterSuccessResponse(响应) 

我们已经有了处理成功操作的代码,但是我们仍然必须处理出现问题的情况。 在filterSuccesResponse()函数下,添加以下代码:

  func filterErrorResponse(_ response:Observable ){} 

在filterErrorResponse()函数中,我们将处理由于某些原因而失败的操作。 我们必须首先仅拦截来自服务器的响应,这些响应包含错误代码4xx(客户端错误)或5xx(服务器错误)。 在函数内添加以下代码:

 响应.observeOn(MainScheduler.instance).filter {响应,_返回400 .. <600〜= response.statusCode} 

watchOn()函数的行为与过滤成功响应时的行为相同。

使用过滤器功能,我们仅传递来自服务器的那些响应,这些响应包含400到600范围内的HTTP响应代码。可以省略过滤器功能块中的第二个参数(即Data对象),因为我们不需要它为了任何东西。

最后一步是使用单个响应代码返回Observable对象。 将以下代码添加到链中:

  .flatMap {response,_-> Observable 返回Observable.just(response.statusCode)} 

使用flatMap函数,我们正在创建Observable,该Observable在订阅创建时仅发出一个元素。 现在,我们只需要创建一个适当的订阅并将其添加到disposeBag对象中即可:

  .subscribe(onNext:{self中的[[weak self] statusCode弱?.showMessage(“某事是错误的”,描述:“ \。(statusCode)”)}))。posed(作者:disposeBag) 

我们将使用以上订阅将响应代码转发到showMessage() ,该代码负责在屏幕上显示相关消息。

要完成这一部分,您需要在getCoinsFromURL()函数的末尾添加以下代码:

  filterErrorResponse(响应) 

almost快结束了。

它将向您展示如何注册以收听其他对象中发生的更改。 为了演示,我们将使用“设置当天的硬币”按钮(位于CoinsDetalisVC屏幕上)在MainViewVC屏幕的顶部设置所选货币的名称。

打开CoinsDetalisVC.swift文件,然后将代码放在最后一个UILable对象下面:

 私人让coinOfTheDay = Variable(Coin())var selectedCoin:Observable  {返回coinOfTheDay.asObservable()} 

coinOfTheDay是一个Variable ,它是Observable的一种特殊类型,它将使我们能够向序列中添加新值。 变量是BehaviorSubject的包装器。

传递给MainViewVC的值将是Coin对象。 selectedCoin属性的作用是隐藏coinOfTheDay的私有实现,因此其他人无法向该流添加新值。 感谢selectedCoin对象,它作为标准的Observable对象,我们可以使用“反应式”方式将正确的值传递给MainViewVC。

打开CoinsDetalisVC文件并查找setCoinOfTheDay(sender 🙂函数,该函数已经连接到“设置每日硬币”按钮。 将此代码添加到showMessage(title:description :)函数的调用下面:

  coinOfTheDay.onNext(singleCoin) 

使用上面的代码,我们会将新值传输到coinOfTheDay发出的数据流。 传输的对象将是singleCoin,它将在初始化期间分配给CoinsDetalisVC对象。 当在表视图中选择适当的单元格时,这又会在MainViewVC对象中发生。

我知道以下事实:setCoinOfTheDay(sender :)函数中的代码不是“反应式”样式的理想表示。 我们使用一个属性,该属性是CoinsDetalisVC对象的一部分。 这样,我们“与外界联系”,这在反应性范式中是被禁止的。

但我认为,我们不应对此过分强调,因为理想的代码不存在,有时我们必须妥协。 对于我们的示例应用程序,这就足够了。

最后要做的是开始观察MainViewVC对象中的更改。 打开MainViewVC.swift文件,然后在文件底部找到tableView(_ tableView:didSelectRowAt :)。 您可能已经知道,每次用户点击表格单元格时都会调用此函数。

在对navigationController?.pushViewController()函数的调用上方,添加以下代码:

  coinDetailsVC.selectedCoin .subscribe(onNext:{self中的[[weak self](selecteCoin)?? print(“ onCompleted事件已发出”)}){print(“ onDispose事件已发出”)} .. disposed(作者:coinDetailsVC.disposeBag) 

每次用户在表视图中选择一个单元格时,都会创建一个新的订阅。 在onNext()事件中传递的值将是Coin对象。 它将代表我们在CoinsDetalisVC屏幕上选择的加密货币。 使用Coin对象,我们将在屏幕顶部为文本字段设置值。

在onNext()块内,我们将对MainViewVC对象使用一个弱引用,以避免保留周期 。 当Observable发出特定事件时,将调用其他块(即onError)。 目前,我们不需要它们,但是我把它们留给您看,以了解完整的功能集。

最后,我们必须将订阅添加到DisposeBag对象,这将在适当的时候将其从内存中删除。 DisposeBag将自动为我们管理订阅。 在将其从内存中删除之前不久,每个单独的预订都将通过调用dispose()方法来取消。

最好的解决方案是使用位于CoinsDetailsVC中的属性。 每次您在表格视图中选择一个单元格时,都会在内存中创建一个新的订阅。 将其添加到位于MainViewVC中的DisposeBag对象不会带来预期的结果,因为仅当您关闭应用程序时,才会从内存中释放主控制器。 在这种情况下,DisposeBag将无法执行必要的操作。 CoinsDetailsVC的情况则有所不同,每次我们返回主屏幕时,都会从内存中将其删除。

我们的申请现已完成。 您可以轻拍背部🤩。

这是一段漫长的旅程。 我们从服务器检索数据,处理响应并使用我们的模型映射数据。 您肯定已经注意到,基于反应式范例编写的代码可能会无限拖动,有时甚至有些难以理解。 好吧,这就是它的样子。 您总是可以尝试将其分解为较小的部分,就像我们对过滤器成功/错误响应所做的一样。

但是,优点是,与从服务器检索数据及其映射有关的所有操作均按一个序列执行,而无需使用委托或回调。

阅读完本教程之后,您应该具有RxSwift库的工作原理以及如何在日常工作中使用它的一些基本知识。


我只有一个要求。 这是我关于Medium的第一篇文章,也是我完全用英语撰写的第一篇文章。 如果您有任何对我的反馈,请发表评论,以便下次我可以做得更好。