从 ReactiveCocoa2.0 到 RxSwift

本文主要讨论 ReactiveCocoa2.0 与 RxSwift 的一些不同点。ReactiveCocoa3.0之后对应的是 Swift 版本,感觉国内使用的并不多,这里暂不讨论。

在实际开发中,我们还需要引入 RxCocoa,它在 Cocoa 和 Cocoa Touch 的基础上增加了一些便捷的方法,使得能更好地参与到事件流中。

Observable

Observable 就是我们在 RAC 中称为信号(Signal)的东西。
同样他也有三种事件类型:

  • .Next(value) 表示新的事件数据。
  • .Completed 表示事件序列的完结。
  • .Error 同样表示完结,但是代表异常导致的完结。

Observable 属于生产者,而对应的subscribeNext:bindTo:,我们可认为它是一种消费行为,或者称为我们所熟知的“订阅”。

shareReplay

之前文章介绍过,热信号在创建的时候就会执行计算,然后对于多个订阅者会共享这次计算的结果,同时还包括这次计算所产生的副作用(side effect)。
Swift 毕竟不是纯函数式语言,还是会有冷热信号的问题,只是在我们实际使用中并不那么容易察觉。

var a: Int = 20
let source = Observable<Int>.create { observer in
for i in 1...3 {
observer.on(.Next(i))
}
a += 1
print(a)
observer.on(.Completed)
return AnonymousDisposable {
print("Disposed")
}
}.shareReplay(1)

print("Start subscriptions")

source.subscribeNext {
print($0)
}
source.subscribeNext {
print($0)
}

//console:
//Start subscriptions
//1
//2
//3
//21
//Disposed
//3

这里明显是在订阅之后才调用闭包,而且只调用一次。
对于多个订阅者的共享了最后一次的事件数据。
所以 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([
"First Item",
"Second Item",
"Third Item"
])

items
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
cell.textLabel?.text = "\(element) @ row \(row)"
}
.addDisposableTo(disposeBag)

这里直接将数据源转成信号,然后直接绑定到对应的 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。

RxSwift

let 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:

RxCocoa

let 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

@property NSString *recipeName;
@property NSString *recipeDescription;

@property RACSignal *updatedContentSignal;//RACSubject
@property RACCommand *startTimer;

-(NSString *)titleForStepAtIndex:(NSInteger)index;

在 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,那么使用起来就会顺手很多。