原文:RxSwift - Start using it today! The practical introduction
我的 FRP 之旅是从 ReactiveCocoa v2.4 开始的,它是在 Objective-C 中实现函数响应式编程的最流行的框架。一开始,我对 ReactiveCocoa 的理解还很肤浅,所以只是将其用于监听 UI 事件(如点击按钮)或处理 REST API 调用。这是一个非常好的方法,因为后来这让我更容易理解 RxSwift 背后的概念。
这就是我要向你展示的内容。在不深入研究 RxSwift 以及什么是 stream、sequence 或 Observable 的情况下,我想向大家展示 FRP 如何简化普通问题的最佳实践。这是漫长的 Rx 运行前的拉伸😉😉。
用 RxSwift 替换 target & action
我不喜欢通过传递 target 和 selector 来订阅 UIControl
事件。在 Objective-C 的世界里,重构过程中的任何一个改动可能会导致运行时崩溃。虽然 Swift 的编译器也会检查 selector 是否存在,但我仍然认为用闭包替换 target 和 action 是个好主意。由于 Swift 的扩展,RxCocoa 为现有的 UIKit 类添加了方便的属性。我们不需要像 Android 开发人员那样创建 UIView
的 Rx 子类 😉。
使用 RxCocoa 再简单不过了!你只需订阅 UITextField
的 rx.text
属性,即可获取 UITextField
内的更改信息:
textField.rx.text.subscribe(onNext: { [weak self] text in
self?.search(withQuery: text);
}).disposed(by: disposeBag)
那么监听按钮点击呢?这也很简单,这要归功于 rx.tap
属性:
button.rx.tap.subscribe(onNext: { [unowned self] in
if self.isFollowedByMe() {
self.follow()
} else {
self.unfollow()
}
}).disposed(by: disposeBag)
每次的 subscribe
订阅都会在 Rx 的逻辑中创建一个引用循环。有了它,你就不必在上述示例中保持对 button.rx.tap
可观察对象的强引用。不过,你必须在某个时刻打破引用循环。为此,你必须在 Disposable
上调用 dispose()
,这是 subscribe
的输出。通常情况下,UIViewController
内有不止一个 Rx 订阅,为了方便处置,您可以将任何 Disposable
添加到 DisposeBag
中。它只是一个可弃置数组,在 dealloc 时,会遍历所有可弃置对象并将其弃置掉。当 DisposeBag
被 dealloc 时,可弃置对象就会被解除引用。而当 DisposeBag
的拥有者被 dealloc 时,DisposeBag
就会被 dealloc。
debounce 操作符
FRP 中的“函数”是有原因的。RxSwift 拥有一系列操作符 -- 定义在 ObservableType
中的函数,这些函数返回另一个 Observable
。最简单但功能强大的操作符之一是 debounce
。debounce
需要两个参数,即时间间隔和调度器:
textField.rx.text
.debounce(0.3, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] text in
self?.search(withQuery: text);
}).disposed(by: disposeBag)
button.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] in
if self.isFollowedByMe() {
self.follow()
} else {
self.unfollow()
}
}).disposed(by: disposeBag)
我在之前的示例中添加了 debounce (0.3, scheduler: MainScheduler.instance)
运算符。这会导致什么变化?在 UITextField
的例子中,debounce
告诉 Rx,如果文本字段中的文本在过去 0.3 秒内没有更改,就会通知你。这意味着,如果用户一直在书写,Rx 将在书写中断 0.3 秒后向 subscribe (onNext:)
发送一个事件。因此,它解决了一个常见问题,即当你不想在用户每次写东西时都向 REST API 询问搜索结果,而只想在他 "写完" 时才询问。
按钮点击如何?想象一下,您的应用程序有一个 "喜欢" 按钮,类似于 Twitter 上的 "爱心" 按钮。通常情况下,当用户想喜欢某个东西时,他只需按一次 "喜欢" 按钮。您的 QA 也会这样做吗?不!他会像疯子一样 "嗒嗒嗒嗒" 按个不停。在这种情况下,debounce
还可以防止你向 API 发送多个事件;)。
Schedulers 是 RxSwift 处理并发、线程和将动作分派到队列的抽象方法。在 debounce
的情况下使用 MainScheduler
。它将把事件分派到主队列。并发在过去、现在和将来都是编程中的复杂点,因此我想为调度程序单独撰写一篇文章。不过,在此之前,我可以向你推荐这篇关于调度程序的文章。
按钮点击与 UITableView/UICollectionView
通过可重复使用的单元格,订阅 UIButton
点击事件非常有用。在使用 target 和 action 时,你必须神奇地接收最近触摸的 IndexPath
。幸好 Rx 使用了闭包,因此你可以立即获得 IndexPath
的引用:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DummyCell" for:indexPath)
cell.button.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] in
self.like(postAt: indexPath)
}).disposed(by: cell.rx_reusableDisposeBag)
}
对于可重复使用的单元格,有一点需要记住。你必须使用单元格的 DisposeBag
而不是数据源的 bag
来调用 disposed(by:)
。此外,当单元格被重用时,你必须重新创建 DisposeBag
。这一点非常重要!我是这样处理的:
class RxCollectionViewCell: UICollectionViewCell {
private (set) var rx_reusableDisposeBag = DisposeBag()
override func prepareForReuse() {
rx_reusableDisposeBag = DisposeBag()
super.prepareForReuse()
}
}
再次重复一遍,不要忘记使用单元格的 disposeBag
,这一点非常重要。否则,只需点击一次按钮,你就会收到多个事件。幸运的是,有一个方便的库可以将 rx_reusableDisposeBag
添加到所有可重用类型视图中。
使用 Rx 处理通知
RxSwift 还允许你使用闭包处理来自 NotificationCenter
的通知:
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
_ = NotificationCenter.default.rx
.notification(NSNotification.Name.UIKeyboardDidShow)
.takeUntil(rx.methodInvoked(#selector(viewWillDisappear(_:))))
.subscribe(onNext: { notification in
self.doSmthWithKeyboard(notification)
});
}
通常,将观察者添加到 NotificationCenter
时,会在 viewWillAppear
中进行注册,然后在 viewWillDisappear
中取消注册。要在 Rx 中执行同样的操作,必须使用 takeUntil
操作符。这一次你不需要将 Disposable
添加到 DisposableBag
中,因此当 viewWillDisappear
被调用时,takeUntil
会调用 dispose
。
RxAPI
我把这个留到了最后。这也是 Rx 最大的亮点。由于并非所有的 REST API 都是为移动设备设计的,因此有时你必须调用至少两个 API 才能渲染一个屏幕。以命令式方式实现这一点可能会很棘手。你需要保留一些布尔值来标记请求是否已完成,更不用说创建一个类来同步所有这些标记了。
Rx 如何帮你?首先,你必须在 Observable
中封装你的 API 调用:
class HTTPClient {
func firstResource(_ parameter: Int, callback: @escaping (Result<String>) -> Void) -> DataRequest
func secondResource(callback: @escaping (Result<String>) -> Void) -> DataRequest
}
//1
extension HTTPClient: ReactiveCompatible {}
extension Reactive where Base: HTTPClient {
func firstResource(_ parameter: Int) -> Observable<String> {
//2
return Observable.create { observer in
//3
let reqeust = self.base.firstResource(parameter, callback: self.sendResponse(into: observer))
//5
return Disposables.create() {
reqeust.cancel();
}
}
}
func secondResource() -> Observable<String> {
return Observable.create { observer in
let reqeust = self.base.secondResource(callback: self.sendResponse(into: observer))
return Disposables.create() {
reqeust.cancel();
}
}
}
//4
func sendResponse<T>(into observer: AnyObserver<T>) -> ((Result<T>) -> Void) {
return { result in
switch result {
case .success(let response):
observer.onNext(response)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
}
}
}
首先,你必须创建现有函数的 Rx 版本。经验法则是使用 rx_
前缀将函数命名为与其等价的函数,或者创建一个 Reactive
struct 扩展,其中 Base
是你的类 (1),然后添加名称完全相同的函数。函数接收的参数与原始函数相同,并返回 Observable<T>
,其中 T
是成功响应的参数类型。然后使用 Observable.create
创建一个 Observable
(2)。最后,在 Observable.create
闭包中调用原始函数 (3)。当响应到来时,有两种情况需要处理:
- 如果请求返回成功,则始终调用
observer.onNext(<T>)
和observer.onCompleted()
; - 如果请求返回错误,只需调用
observer.onError(<ErrorType>)
(4)
好的做法是在 Observable 被销毁时取消请求。为此,你必须使用闭包返回 Disposable
(5)。
Zip 操作符
回到 Rx 如何帮助你的问题上来。现在,如果你有了 Rx 版本的 API 请求,就可以使用 zip
操作符来连锁这两个请求。zip
运算符会将两个可观察对象结合起来,并在两个 API 请求都完成后向您发送响应。
Observable.zip(httpClient.rx.firstResource(1), httpClient.rx.secondResource()) { ($0, $1) }
.subscribe(onNext: { response1, response2 in
print(response1, response2, separator:"\n")
}).addDisposableTo(disposeBag)
}
retry 操作符
如果 API 请求失败,重试是一种常见的方法。使用重试操作符是一件小事。您只需添加一行 .retry (1)
。其中的整数参数表示要重试多少次 API 调用。此外,RxSwiftExt 还增加了以指数增长延迟重试的可能性:
func secondResource() -> Observable<String> {
return Observable.create { observer in
let reqeust = self.base.secondResource(callback: self.sendResponse(into: observer))
return Disposables.create() {
reqeust.cancel();
}
}.retry(.exponentialDelayed(maxCount: 3, initial: 2, multiplier: 1))
}
现在,如果请求失败,应用程序将尝试重试。第一次尝试将在 2 秒后进行,下一次尝试将在前一次尝试的 4 秒后进行,最后一次尝试将在 8 秒后进行。
总结
函数响应式编程适合移动开发人员吗?当然是,因为我们的工作就是对用户界面事件做出反应。统一处理 target-action 和通知的方式可以提高可读性。此外,RxSwift 还简化了 non-trivial (非单一?)用例,例如同步 2 个请求或实现指数增长延迟重试。使用 RxSwift 只需一行代码 ❤️。
敬请关注下一篇文章,我们将深入探讨 Rx 背后的逻辑。
如果你有任何不清楚的地方,请在下面留言。我很乐意为你提供帮助!如果你喜欢这篇文章,请与你的朋友分享,说服他们开始使用 Rx!
所有代码片段均可在示例项目中找到。