本文主要讨论 ReactiveCocoa2.0 与 RxSwift 的一些不同点。ReactiveCocoa3.0之后对应的是 Swift 版本,感觉国内使用的并不多,这里暂不讨论。
在实际开发中,我们还需要引入 RxCocoa,它在 Cocoa 和 Cocoa Touch 的基础上增加了一些便捷的方法,使得能更好地参与到事件流中。
Observable
Observable
同样他也有三种事件类型:
- .Next(value) 表示新的事件数据。
- .Completed 表示事件序列的完结。
- .Error 同样表示完结,但是代表异常导致的完结。
Observable
属于生产者,而对应的subscribeNext:
和bindTo:
,我们可认为它是一种消费行为,或者称为我们所熟知的“订阅”。
shareReplay
之前文章介绍过,热信号在创建的时候就会执行计算,然后对于多个订阅者会共享这次计算的结果,同时还包括这次计算所产生的副作用(side effect)。
Swift 毕竟不是纯函数式语言,还是会有冷热信号的问题,只是在我们实际使用中并不那么容易察觉。
var a: Int = 20 |
这里明显是在订阅之后才调用闭包,而且只调用一次。
对于多个订阅者的共享了最后一次的事件数据。
所以 shareReplay
生成的 Observable
虽然很接近我们在 RAC 所认知的热信号,但两者又不完全一样。
前者是被动的,而后者是主动的。对于这种介于冷热之前的 Observable
,不妨称之为 warm(文档并未这么说)。shareReplay
在这里主要是解决了多次订阅会多次执行的问题。
disposeBag
当我们发送 Completed 或者 Error,可以终止订阅行为。
另外,我们也可以通过调用 dispose()
或者 takeuntil:
来终止订阅。func subscribeNext(onNext: (Self.E) -> Void) -> Disposable
订阅行为通常都会返回 disposable
,这时通过调用 dispose()
可以结束订阅,同时释放资源。
这里和 RAC 不同的地方是,对于冷信号,RAC 在每次订阅结束之后,订阅者释放时会对持有的 disposable 执行 dispose()
;但是 RxSwift 并不会这么做。
但是 RxSwift 另外加了其他方法来处理这个问题。let disposeBag = DisposeBag()
doSomethingOutlet.rx_tap
.subscribeNext { [weak self] in self?.showAlert() }
.addDisposableTo(disposeBag)
disposeBag
一般是由控制器持有。当控制器释放的时候,disposeBag
便会在其 deinit()
里面对 disposable 执行 dispose()
。
这样也就避免了需要手动去调用 dispose 的问题。
虽然不调用 addDisposableTo 并不会产生什么内存泄露问题,但对于无止境的订阅,如果不调用 dispose 或者 takeuntil,那么就只能等待 Completed 或者 Error 来结束订阅。
UI Binding
let items = Observable.just([ |
这里直接将数据源转成信号,然后直接绑定到对应的 cell 中,解决了之前繁琐的数据源和代理的操作。
你也可以通过 UIBindingObserver 来实现自己的 UI 绑定操作。
Driver
Driver 是存在于 RxCocoa 中的核心的一个概念,针对苹果开发中 RxSwift 繁琐的操作而进行的处理。
- 不能抛出异常(can’t error out)
- 默认回到主线程(observe on main scheduler)
- 共享副作用(sharing side effects)
先看文档中给出的错误示范:let results = query.rx_text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
}
results
.map { "\($0.count)" }
.bindTo(resultCount.rx_text)
.addDisposableTo(disposeBag)
results
.bindTo(resultsTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.addDisposableTo(disposeBag)
首先,这里的 bindTo
也是属于订阅行为,但是两个地方都用到了 bindTo
,对于冷信号便会造成重复订阅,出现重复计算。
其次,对于 UI 的更新并没有保证一定在主线程。
最后,未对异常进行控制处理。
改进方式有两种,一种是 RxSwift,另一种是 RxCocoa。
RxSwiftlet results = query.rx_text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance) // results are returned on MainScheduler
.catchErrorJustReturn([]) // in worst case, errors are handled
}
.shareReplay(1) // HTTP requests are shared and results replayed
// to all UI elements
results
.map { "\($0.count)" }
.bindTo(resultCount.rx_text)
.addDisposableTo(disposeBag)
results
.bindTo(resultTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.addDisposableTo(disposeBag)
这里的改进主要是解决上面三个问题。
再来看看 RxCocoa:
RxCocoalet results = query.rx_text.asDriver() // This converts normal sequence into `Driver` sequence.
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // Builder just needs info what to return in case of error.
}
results
.map { "\($0.count)" }
.drive(resultCount.rx_text) // If there is `drive` method available instead of `bindTo`,
.addDisposableTo(disposeBag) // that means that compiler has proved all properties
// are satisfied.
results
.drive(resultTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.addDisposableTo(disposeBag)
可以发现 RxCocoa 中 driver 的用法,和原本的 RxSwift 还是很像的,而且还省去一些繁琐的操作。
MVVM
@interface ASHDetailViewModel : RVMViewModel |
在 RAC,这是一个常见的 ViewModel。RACCommand
通常是作为外部的输入信号,VM 根据输入信号执行相应的操作来更新 model,然后就是各种 RACObserve
将信号传递到控制器。整个过程一大半是依赖于 KVO。
有时候我们也会使用 RACSubject
,它既是信号,又是信号的发送者,这里暂不讨论。
而在 Swift 中,无论是 RAC 的 swift 版本还是 RxSwfit,都已经不再依赖于 KVO,因而上面的方式并不可行。// ViewModel
signedIn = input.loginTaps.withLatestFrom(usernameAndPassword)
.flatMapLatest { (username, password) in
return API.signup(username, password: password)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(false)
.trackActivity(signingIn)
}
.flatMapLatest { loggedIn -> Observable<Bool> in
let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"
return wireframe.promptFor(message, cancelAction: "OK", actions: [])
// propagate original value
.map { _ in
loggedIn
}
}
.shareReplay(1)
这段是 ViewModel 的代码,通过外部传入的 loginTaps
点击信号,直接进行转换生成登陆信号,暴露给外部。整个过程并没有显式的订阅操作,更趋向于流的概念。
因此,MVVM 中的 model 感觉被弱化了,model 被转化为信号,成为中间产物。
所谓的 MVVM,未必就是控制器一定要有对应的 ViewModel。比如对于 TableView,每个 cell 可以有对应的 ViewModel,控制器则可不必再加,过多的层级只会加重维护的难度。
结尾
ReactiveCocoa2.0 到 RxSwift 的变化大体就这些。在实际使用 RxSwfit 的过程中,需要摆脱以前对 RACObserve
的依赖,理解一切对象皆是 observable
,那么使用起来就会顺手很多。