RxSwift - 今天就开始使用!实用介绍

174 阅读7分钟

原文: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 再简单不过了!你只需订阅 UITextFieldrx.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。最简单但功能强大的操作符之一是 debouncedebounce 需要两个参数,即时间间隔和调度器:

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)。当响应到来时,有两种情况需要处理:

  1. 如果请求返回成功,则始终调用 observer.onNext(<T>)observer.onCompleted()
  2. 如果请求返回错误,只需调用 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!

所有代码片段均可在示例项目中找到。