具有RxSwift的自动完成组件

反应式编程为代码重用提供了很大的机会。 使用RxSwift花费的时间越多,对我来说,将某些行为封装在可以在不同流程中使用的组件中就越直观。

我称它们为“ Rx组件”,既没有革命性也没有惊人的意义。 只是划分和征服Rx代码库的一种方法。 如果您想阅读其他人关于Rx世界的想法,请与我在一起-我们将构建Autocomplete功能。

我所谓的“接收组件”只是一个概念。 没有符合协议,也没有子类。 这是一般的插图:

对于其他开发人员,该组件是一个黑匣子 ,只有输入和输出要插入。 输入和输出在数量上是独立的。 组件实现行为 ,这是应用程序中的一个用例。 仅举几个例子:

  • 自动完成功能—该组件接受简单的文本输入,并提供三个输出: 预测错误加载状态 (即网络调用或数据库查询)。
  • 表格-接受多个不同类型的输入 。 提供相同数量的输出,每个输出针对相应的输入发出验证结果 。 验证结果可能包含描述数据错误的消息。
  • 分页—接受具有给定索引的页面的输入,发出信号告知何时获取更多页面,并发出信号要求刷新第一页加载状态输出是网络活动的简单指示。 每次组件获取新页面时, 页面输出都会发出整个已知页面集。

每个组件都应隔离,并且不可知 。 通过更改我们编排输出的方式,两者都使其可在不同的屏幕上重用。 一个示例可能是分页组件的输出加载为在不同屏幕上转换为稍有不同的“等待”体验。

一旦我们构建了组件,就应该很容易对其进行单元测试 。 为了使其无缝,请确保所有依赖项都已被协议很好地抽象。

Rx Component 是一个黑匣子 ,只有输入和输出要插入。 它应该被隔离并不可知,以通过更改我们编排输出的方式使其在不同的屏幕上可重用。

希望您有主要想法。 如果没有,那没关系,我将逐步Autocomplete RxSwift中构建Autocomplete 组件的 RxSwift 证明这一点。

这是我们将要构建的体验(请注意,活动期间状态栏中会显示加载指示器):

在Swift中也一样:

我的Rx组件是类 。 通常,它们符合某种用于依赖项注入的协议( AutocompleteType )。 该组件提供了一种方法,该方法接受Rx 输入作为参数,返回包装在tuple中的输出

Autocomplete的唯一输入:

  • text: Observable用户键入时, text: Observable应该发出新的String

两个输出都是RxCocoa.DriversDriver无法发出Error事件,并且始终使用主线程,这使其非常适合UI绑定。

  • result: Driver将发出AutocompleteResult值-成功或失败。 我们不能使用stream Error事件,因为它会终止流,并且我们不想在表示层中使用它。
  • isBusy: Driver表示由Autocomplete组件执行的异步活动。 我们不会在提供程序中进行真正的API调用,因此我们将其工作延迟0.3秒才能看到效果。

在触摸autocomplete(text:)函数并创建两个输出之前,让我们考虑一下为简单String word获取单词完成的方法。

对于给定的文本,我们希望异步获取单词完成列表。 纯粹是AutocompleteProvider的定义:

我们传入text并获得一个Observable ,它将发出一组Predictions 。 万一遇到麻烦, AutocompleteProvider应该发出Error事件(例如,由于网络问题而失败)。

Prediction类型非常简单-具有预测文本匹配范围的简单结构,即"Italy", 0..2可能是“It”完成的预测。

现在,让我们完成一些实际的单词补全。 我从这里获取了国家列表,并进行了具体的AutocompleteProvider简单实现:

只不过在countries数组中搜索前缀并以0.3秒的延迟返回Prediction 。 在最后的实现中,我向此提供程序添加了另一件事-它在搜索"Error"前缀时模拟错误。

给定提供者后,我们可以跳回Autocomplete以完成其autocomplete(text:)方法。

首先,我们将提供程序注入“ Autocomplete并将其存储在属性上:

“结果”输出

现在,让我们声明result: Driver输出(稍后我们将触摸isBusy ):

我们在let provider常量上捕获self.provider ,以免在flatMapLatest's闭包中陷入[weak self]舞步。 然后,我们将输入的text平面映射到provider.autocomplete(text: query)结果,该结果是[Prediction]Observable 。 稍后我们将预测映射到AutocompleteResult.predictions 。 如果provider发出Error而不是[Prediction] ,则将其相应地映射到AutocompleteResult.error

多亏了.flatMapLatest ,如果有新query到达,我们将取消(处理)所有正在等待的provider.autocomplete(text:)流。 如果我们将使用付费服务提供商,这可能为我们节省了一些配额-可以节省您的业务的小物件😉。

"Is Busy"输出

我们准备完成第二个输出: isBusy: Driver 。 我们知道在要求提供者进行单词补全之前,它应该发出true ,并在得到结果(或错误)之后,后面跟随false

我们使用PublishSubject作为Bool流的源。 然后,我们使用do(onNext: {})运算符在result流(第6行和第14行)上创建两个副作用。 最后,我们将该主题转换为Driver并将其返回。

我不喜欢流副作用和.do()运算符。 但是,有时我发现它在具有多个输出的组件中非常方便。 在Autocomplete ,可以实现两个输出而没有副作用(请参见此处),但是可读性会大大降低。 值得注意的是,有些人倾向于在流容易保持纯净和易于阅读的地方过度使用副作用。 保持平衡!

我们构建的组件之美在于其不可知论的本质 。 让我们看一下这种用法:

resultisLoading流都可以转换并绑定到任何类型的UI控件。 我们拥有完全自由的方式来显示结果或显示错误。 我们可以将加载指示器绑定在状态栏中,或者切换我们设计人员刚刚制作的超豪华加载器的可见性。

这种不可知论的性质有利于组件的可重用性。

下面,我列出了Autocomplete组件UI的一种可能实现。 在☝️上方的视频中观看。 我使用UIApplication's isNetworkActivityIndicatorVisible来呈现组件的活动。 根据单词完成结果(或错误), TableView's backgroundView设置有不同的视图。 基本TableView用于列出预测。

最后,我最喜欢的部分-单元测试。 多亏了我们所做的接口隔离和对其他实体的零依赖性,用单元测试覆盖所有Autocomplete用例非常容易。 在这里,我展示了用于接收给定输入的预测的测试用例:

可以在项目的存储库中找到涵盖此组件的综合测试用例套件:此处。


谢谢您的时间❤️,希望您喜欢。 我希望这是您从阅读中获得的主要收获:

🎓将Rx复杂性隐藏在良好隔离的状态中,并使用简单的输入/输出界面 查看不可知组件,以提高反应性代码的可读性和可重用性。

让我知道您如何找到它以及您如何看待我介绍的方法🙂。

转到我在GitHub上的Rx-Autocomplete存储库,以找到本文工作的完整源代码。

如果您喜欢它,请在Twitter上关注我,以保持关注—我将继续本系列。