这篇文档将会介绍什么是Traits,为什么它们是一个好用的概念,以及怎样使用和创建它们。
Traits可以帮助交互作用,保证observable sequence 属性穿过界面边界,对比可以在任何环境中使用的原声Observable,Traits同时也提供contextual meaning(上下文意义), syntactical sugar and target more specific use-cases(更多的特殊应用场景) 。
由于上述原因,Traits是完全可选的,你可以在你程序的任何地方自由的使用原生的Observable sequences,因为所有的RxSwift/RxCocoa APIs都支持它们。
注意:这篇文档中的一些Traits描述(例如Driver)只是针对 RxCocoa 工程,一些只是通常 RxSwift 工程的一部分。但是同样的原理可以在其他Rx实现库中实现。
General
Why
swift 有一个强大的类型检测系统,它能够提高我们应用的正确性和稳定性,也使Rx有更直观更直接的体验。
How they work
Traits 只是一个简单包装的struct,他有一个read-only的Observable sequence属性。
struct Single<Element> {
let source: Observable<Element>
}
struct Driver<Element> {
let source: Observable<Element>
}
...
你可以把她们想成是建造者模式的实现。当一个Trait被建造,调用.asObservable()时,将会把它转换回一个observable sequence。
RxSwift traits
Single
Single不同于Observable(能够发送很多元素),他只能发送一个元素或者一个错误。
- 发射一个元素,或者一个错误。
- 不会产生副作用。
一个常用的场景时执行HTTP请求时,它会返回一个response或者error。Single可被用在这种模式下:你只关心一个元素,而不是无限的元素流。
创建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()方法来改变Observable sequence成为一个Single。
Completable
Completable是一个只能complete或者error的Observable。他不能发射元素。
- 发射0个元素
- 发射一个complete或者说error事件。
- 没有副总用。
一种Completable的使用情景是:我们只关心运行是否结束而不关心运行的结果,可以对比Observable<Void>,它也不会产生元素。
创建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
Maybe是一个介于Single和Completable之间的Observable。它能发射一个元素,不发射元素complete,或者发射一个error。
注意:这三种情况的任意一种都会中断Maybe。这就意味着completed的Maybe不能再发射元素,发射完一个元素的Maybe不能在发送Completion事件。
- 发射一个Completion事件,或者一个元素,或者error信息。
- 没有副作用
你可以在这种情况下使用Maybe:能够发射一个元素,但是并不一定必须发射一个。
创建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(onCompleted:onError:)
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
这个是最精细的trait。它的目的是提供一种直观的方式书写UI层代码。或者其他任何情境你想要使用数据驱动应用。
- 不会产生错误。
- 观察者出现在主线程。
- 有副作用(shareReplayLatestWhileConnected)
为什么叫Driver
他的设计意图是为了模仿序列驱动你的应用。
例如:
- 通过CoreData 驱动UI
- 通过其他界面的元素等等(bindings)驱动UI 。
就像通常的操作系统驱动一样,一旦错误发上,你的应用就会停止回应用户输入。这些元素是在主线城被观察的这一点很重要,因为界面元素和应用逻辑通常不是线程安全的。
当然,Driver会产生副作用
例如:
实际的习惯方法
这是一个典型的初学者用法例子
let results = query.rx.text
.throttle(0.3, 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元素:tableview 和用来显示数量的lable。
那么这段代码有什么问题呢? - 如果fetchAutoCompleteItems observable产生错误(链接错误或者解析错误),这个错误没有绑定任何东西,UI不会回应其他新的查询。
- 如果fetchAutoCompleteItems 在其他后台线程种返回结果,结果会在后台线程绑定UI元素,这就可能导致non-deterministic crashes。
- 结果被绑定到两个界面元素。这就意味着每一次用户查询,就会做两次HTTP请求,每个界面元素做一次,这并不是我们想要的。
更合适一点的写法:
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 the worst case, errors are handled
}
.shareReplay(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)
在一个大的系统中确保每一个需求都被正确册处理掉是很有挑战性的。但是有一个简单方式来解决这个需求,使用编译器和traits。
下面代码看起来与上面差别不大
let results = query.rx.text.asDriver() // This converts a normal sequence into a `Driver` sequence.
.throttle(0.3, 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 `bindTo`,
.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 trait 成为Driver trait。
query.rx.text.asDriver()
注意这里没有在做其他的任何特殊的处理。Driver包含所有ControlProperty的属性。
第二个改变是
.asDriver(onErrorJustReturn: [])
任何一个observable sequence都可以被转化成Driver,只要它满足3点
- 不会产生错误
- 在主线城观察
- 产生副作用(shareReplayLatestWhileConnected)
那么你怎样确保满足这三点呢?只需要使用Rx操作符 . asDriver(onErrorJustReturn: [])这就等于下民的代码:
let safeSequence = xs
.observeOn(MainScheduler.instance) // observe events on main scheduler
.catchErrorJustReturn(onErrorJustReturn) // can't error out
.shareReplayLatestWhileConnected() // side effects sharing
return Driver(raw: safeSequence) // wrap it up
最后一点是使用drive代替bindTo。
只有在Driver trait定义了drive。也就是会所如果你在代码中看到drive,那么observable sequence就不会产生错误,并且在主线程监听,这样绑定UI元素就很安全。
然而值得注意的是,理论上,我们仍然可以在ObservableType或者其他接口上定义drive方法。所以为了安全,在绑定UI元素之前使用let results: Driver<[Results]> = ...创建一个短期的定义是很有必要的。但是,我们把这个问题留给读者来决定,是否这是一个确实可行的场景。
ControlProperty / ControlEvent
- 不会产生错误
- 订阅出现在主线程
- 监听出现在出线程
- 会产生其他影响。
原文链接:
Traits (formerly Units)
网友评论