RxSwift 特征序列

54 分钟读完

介绍特征序列的定义,用途以及用法

General

Why

Swift 有一个强大的类型系统可以用来提升应用的正确性和稳定性,还让 Rx 有了更加直观的体验

特征序列可以辅助通信,保证观察序列属性通过接口边界,相对于原始的序列表意更明确,提供语法糖和针对具体目标定制的使用。因此特征序列是可选的,你完全可以使用原始的 Observable sequence ,它们在 RxSwift/RxCocoa 中是完全支持的

Note: 本文描述的特征序列中有些(比如:Driver)只适用于 RxCocoa 项目,有些是一般 RxSwift 项目的一部分。如果必要,同样的规则可以容易地移植到其他的 Rx 实现中,不需要私有的 API 魔法

How they work

简单来说,特征序列就是一个封装的结构体,带有一个只读属性来保存可观察序列

struct Single<Element> {
    let source: Observable<Element>
}

struct Driver<Element> {
    let source: Observable<Element>
}
...

你可以认为它们是一种对可观察序列的建造者模式的实现

当一个特征序列被构建,调用 .asObservable() 方法将会把它转换回普通的可观察序列

RxSwift traits

Single

Single 是一个 Observable 的变种,他会生成一个元素或者一个错误,而不是一系列的元素。

  • 正好生成一个元素,或者一个错粗
  • 不会共享 side effects

Single 的通常用法是发起 HTTP 请求,正好会返回一个响应或者一个错误,但是 Single 的使用场景不限于此,它可以用在任何想生成一个元素的地方,而不是一个无限的元素流

Creating a Single

创建一个 Single 和创建一个 Observable 很相似

下面是一个简单的例子:

func getRepo(_ repo: String) -> Single<[String: Any]> {
    return Single<[String: Any]>.create { single in
        let task = URLSession.shared.dataTask(with: URL(string: "https://api.github.com/repos/\(repo)")!) { data, _, error in
            if let error = error {
                single(.error(error))
                return
            }

            guard let data = data,
                  let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves),
                  let result = json as? [String: Any] else {
                single(.error(DataError.cantParseJSON))
                return
            }

            single(.success(result))
        }

        task.resume()

        return Disposables.create { task.cancel() }
    }
}

之后你就可以像下面的方式一样调用:

getRepo("ReactiveX/RxSwift")
    .subscribe { event in
        switch event {
            case .success(let json):
                print("JSON: ", json)
            case .error(let error):
                print("Error: ", error)
        }
    }
    .disposed(by: disposeBag)

或者使用 subscribe(onSuccess:onError:)

getRepo("ReactiveX/RxSwift")
    .subscribe(onSuccess: { json in
                   print("JSON: ", json)
               },
               onError: { error in
                   print("Error: ", error)
               })
    .disposed(by: disposeBag)

第一种调用方式中,订阅提供了一个 SingleEvent 的枚举,要么是 .success 类型,包含了一个 Single 的范型类型的元素,要么是 .error 类型,除此之外不会有其他的类型

对于未处理的序列可以调用 .asSingle() 方法转换成 Single 类型

Completable

Completable 是一个 Observable 的变种,它只能完成或者产生一个错误,而不会产生任何元素

  • 不会产生元素
  • 产生完成或者错误
  • 不会共享 side effects

一个适用场景是,只关心任务的完成,而不关心它完成而产生的元素

你可以和 Observable<Void> 做比较,同样不会产生元素

Creating a Completable

创建一个 Completable 和创建 Observable 相近

下面是一个简单的例子:

func cacheLocally() -> Completable {
    return Completable.create { completable in
       // Store some data locally
       ...
       ...

       guard success else {
           completable(.error(CacheError.failedCaching))
           return Disposables.create {}
       }

       completable(.completed)
       return Disposables.create {}
    }
}

之后就可以像下面这样调用:

cacheLocally()
    .subscribe { completable in
        switch completable {
            case .completed:
                print("Completed with no error")
            case .error(let error):
                print("Completed with an error: \(error.localizedDescription)")
        }
    }
    .disposed(by: disposeBag)

或者使用 subscribe(onCompleted:onError:)

cacheLocally()
    .subscribe(onCompleted: {
                   print("Completed with no error")
               },
               onError: { error in
                   print("Completed with an error: \(error.localizedDescription)")
               })
    .disposed(by: disposeBag)

第一种调用方式中,订阅提供了一个 CompletableEvent 枚举,要么是 .completed 代表任务结束而且没有错误,要么是 .error,不会有其它的事件

Maybe

MaybeObservable 的一个变种,处于 SingleCompletable 之间。它可以产生单一元素,或者无错误无元素产生地结束,或者产生一个错误

Note: 三种事件都会终止 Maybe,不会同时存在两种事件 - 不会有结束事件伴随着一个元素的产生,也不会有一个元素的产生又发送了一个结束事件

  • 产生三种事件之一:结束,产生单一元素,错误
  • 不会共享 side effects

它的适用场景是,一个操作可以产生产生一个元素,但是不需要一定产生一个元素

Creating a Maybe

创建 Maybe 和创建 Observable 相近

一个简单的例子如下:

func generateString() -> Maybe<String> {
    return Maybe<String>.create { maybe in
        maybe(.success("RxSwift"))

        // OR

        maybe(.completed)

        // OR

        maybe(.error(error))

        return Disposables.create {}
    }
}

之后你可以像下面这样调用:

generateString()
    .subscribe { maybe in
        switch maybe {
            case .success(let element):
                print("Completed with element \(element)")
            case .completed:
                print("Completed with no element")
            case .error(let error):
                print("Completed with an error \(error.localizedDescription)")
        }
    }
    .disposed(by: disposeBag)

或者使用subscribe(onSuccess:onError:onCompleted:)

generateString()
    .subscribe(onSuccess: { element in
                   print("Completed with element \(element)")
               },
               onError: { error in
                   print("Completed with an error \(error.localizedDescription)")
               },
               onCompleted: {
                   print("Completed with no element")
               })
    .disposed(by: disposeBag)

同样可以使用 asMaybe() 方法将一个未处理的 Observable 序列转换成 Maybe

RxCocoa traits

Driver

这是最精细的特征序列。其目的是提供一种直观的方式来在 UI 层编写响应式代码,或者用在任何你想对数据流建模来驱动 App 的地方

  • 不可以产出错误
  • 观察发生在 main scheduler
  • 共享 side effects (share(replay: 1, scope: .whileConnected))

Why is it named Driver

设计的适用场景是对数据序列建模来驱动你的应用

E.g.

  • 使用 CoreData 数据建模驱动 UI
  • 使用 UI 控件驱动其它的 UI 控件(绑定)

就像普通的操作系统驱动一样,如果一个序列产出了错误,你的应用将停止响应用户输入

同样重要的是,这些元素是在主线程被观察的,因为 UI 控件和应用逻辑通常不是线程安全的

同样,一个 Driver 创建的 observable sequence 会共享 side effects

E.g.

Practical usage example

下面是一个典型的入门例子:

let results = query.rx.text
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchAutoCompleteItems(query)
    }

results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

上面的代码实现了:

  • 延迟用户的输入
  • 请求服务器并获取用户结果的列表(每个查询一次)
  • 绑定结果到两个 UI 控件上:展示结果的 table view 和展示结果数量的 label

那么上面的代码有什么问题呢? :

  • 如果 fetchAutoCompleteItems observable sequence 产出了错误(连接错误或者解析错误),这个错误将会取消所有的绑定,并且 UI 不会再响应新的查询
  • 如果 fetchAutoCompleteItems 在后台线程返回了结果,那么结果会在后台线程绑定到 UI 控件上,可能会造成不确定的崩溃
  • 结果绑定到两个 UI 控件上,意味着对于每个用户查询,会发起两个 HTTP 请求,每个 UI 控件对应一个请求,这不是我们想要的表现

一个更合适的版本应该像下面这样:

let results = query.rx.text
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchAutoCompleteItems(query)
            .observeOn(MainScheduler.instance)  // results are returned on MainScheduler
            .catchErrorJustReturn([])           // in the worst case, errors are handled
    }
    .share(replay: 1)                           // HTTP requests are shared and results replayed
                                                // to all UI elements

results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

在大型系统开发中,确保所有的的条件都满足是比较有挑战的,但是有一种更简单的方式就是使用编译器和特征序列来证明满足了条件

改进的版本大概像下面这样:

let results = query.rx.text.asDriver()        // This converts a normal sequence into a `Driver` sequence.
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchAutoCompleteItems(query)
            .asDriver(onErrorJustReturn: [])  // Builder just needs info about what to return in case of error.
    }

results
    .map { "\($0.count)" }
    .drive(resultCount.rx.text)               // If there is a `drive` method available instead of `bind(to:)`,
    .disposed(by: disposeBag)              // that means that the compiler has proven that all properties
                                              // are satisfied.
results
    .drive(resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

怎么实现的呢?

第一个 asDriver 方法将 ControlProperty 特征序列转换成 Driver 特征序列

query.rx.text.asDriver()

注意这里我们不需要做任何特殊的工作,Driver 包括了 ControlProperty 的所有属性,再加上其它的一些属性。匹配的 observable sequence 仅仅是封装成了 Driver 特征序列,这样就已经完成了

第二个改变是:

.asDriver(onErrorJustReturn: [])

任何 observable sequence 都可以转换成 Driver 特征序列,只要它满足三个条件:

  • 不能产出错误
  • 在 main scheduler 观察
  • 共享 side effects (share(replay: 1, scope: .whileConnected))

那么如何确保满足了者 3 个条件呢?对于普通的 Rx 操作符只需要调用 .asDriver(onErrorJustRetuan: []) 就可以实现了,等价于下面的代码

let safeSequence = xs
  .observeOn(MainScheduler.instance)        // observe events on main scheduler
  .catchErrorJustReturn(onErrorJustReturn)  // can't error out
  .share(replay: 1, scope: .whileConnected) // side effects sharing

return Driver(raw: safeSequence)            // wrap it up

最后一点是使用 driver 而不是 bind(to:)

driver 仅在 Driver 中定义,意味着如果你再代码中看到了 driver,那么 observable sequence 永远不会产出错误,并且在主线程观察(可以安全地绑定到 UI 控件上)

需要指出的是,理论上,一些人还是可以在 ObservableType 或者其它的接口上定义 driver 方法,所以为了绝对的安全,在绑定到 UI 控件上之前创建一个暂时的定义 let results: Driver<[Results]> = ... 来完成证明是必要的。但是,这个需要读者来决定是否是一个现实中的场景

Signal

SignalDriver很相近,但有一点不同,它不会在订阅的时候重放最后的事件,但是订阅者仍然共享序列的计算资源

它可以被认为是一种建造者模式,将命令式的事件以响应式的方式建模作为你的应用的一部分

一个 Signal:

  • 不可以产出错误
  • 在 Main Scheduler 传递事件
  • 共享计算资源 (share(scope: .whileConnected))
  • 在订阅的时候不会重放元素

ControlProperty / ControlEvent

ControlProperty

一个为 Observable/ObservableType 代表 UI 控件属性而设计的特征序列

值的序列仅会代表初始的控制值和用户初始化的值的改变,程序值的改变不会被报告

它的属性:

  • 永远不会失败
  • share(replay: 1) 表现
    • 它是有状态的,在订阅的时候(调用 .subscribe)最后一个元素如果产生过会立马重放
  • 它会在 control 被释放的时候发出 Complete 事件
  • 永远不会产出错误
  • MainScheduler.instance 传递事件

ControlProperty 的实现会保证这个事件序列在 main scheduler 被订阅 (subscribeOn(ConcurrentMainScheduler.instance))

Practical usage example

我们可以在 UISearchBar+RxUISegmentedControl+Rx 中找到非常实用的例子:

extension Reactive where Base: UISearchBar {
    /// Reactive wrapper for `text` property.
    public var value: ControlProperty<String?> {
        let source: Observable<String?> = Observable.deferred { [weak searchBar = self.base as UISearchBar] () -> Observable<String?> in
            let text = searchBar?.text
            
            return (searchBar?.rx.delegate.methodInvoked(#selector(UISearchBarDelegate.searchBar(_:textDidChange:))) ?? Observable.empty())
                    .map { a in
                        return a[1] as? String
                    }
                    .startWith(text)
        }

        let bindingObserver = Binder(self.base) { (searchBar, text: String?) in
            searchBar.text = text
        }
        
        return ControlProperty(values: source, valueSink: bindingObserver)
    }
}
extension Reactive where Base: UISegmentedControl {
    /// Reactive wrapper for `selectedSegmentIndex` property.
    public var selectedSegmentIndex: ControlProperty<Int> {
        return value
    }
    
    /// Reactive wrapper for `selectedSegmentIndex` property.
    public var value: ControlProperty<Int> {
        return UIControl.rx.value(
            self.base,
            getter: { segmentedControl in
                segmentedControl.selectedSegmentIndex
            }, setter: { segmentedControl, value in
                segmentedControl.selectedSegmentIndex = value
            }
        )
    }
}

ControlEvent

Observable/ObservableType 代表 UI 控件的事件而设计的特征序列

它的特点是:

  • 从不会失败
  • 不会在订阅的时候发送初始值
  • 在 control 被释放的时候会发送 Complete 事件
  • 永远不会产出错误
  • MainScheduler.instance 传递事件

ControlEvent 的实现会保证事件序列在 main scheduler 被订阅(subscribeOn(ConcurrentMainScheduler.instance))

Practical usage example

下面是一个典型可以使用它的例子:

public extension Reactive where Base: UIViewController {
    
    /// Reactive wrapper for `viewDidLoad` message `UIViewController:viewDidLoad:`.
    public var viewDidLoad: ControlEvent<Void> {
        let source = self.methodInvoked(#selector(Base.viewDidLoad)).map { _ in }
        return ControlEvent(events: source)
    }
}

UICollectionView+Rx 中我们可以找到这样的实现:


extension Reactive where Base: UICollectionView {
    
    /// Reactive wrapper for `delegate` message `collectionView:didSelectItemAtIndexPath:`.
    public var itemSelected: ControlEvent<IndexPath> {
        let source = delegate.methodInvoked(#selector(UICollectionViewDelegate.collectionView(_:didSelectItemAt:)))
            .map { a in
                return a[1] as! IndexPath
            }
        
        return ControlEvent(events: source)
    }
}

留下评论