本文档将尝试描述什么是Traits,为什么它们是有用的概念,以及如何使用和创建它们。
一般
为什么
Swift具有强大的类型系统,可用于提高应用程序的正确性和稳定性,并使Rx的使用具有更直接简单的体验。
与可用于任何上下文的原始Observable相比,Traits有助于沟通并确保可观察序列属性跨接口边界,并提供上下文含义,语法糖和目标更明确。因此,Traits完全是可选的。可以在程序中随处使用原始Observable序列,因为所有核心RxSwift/RxCocoa API都支持它们。
注意:本文档中描述的某些Traits(例如Driver)仅限定于RxCocoa项目,而某些Traits是通用的RxSwift项目的一部分。但是,如果需要,可以在其他Rx实现中轻松实现相同的原则。不需要私有API语法。
它们如何工作
Traits只是一个包装结构体,只有一个只读的Observable序列属性。
struct Single<Element> {
let source: Observable<Element>
}
struct Driver<Element> {
let source: Observable<Element>
}
...
可以将它们视为Observable序列的一种构建器模式实现。构建Trait时,调用.asObservable()会将其转换回vanilla可观察序列。
RxSwift traits
Single
Single是Observable的变体,它总是保证发出单个元素或错误,而不是发出一系列元素。
- 恰好发出一个元素或错误。
- 没有共享元素效果。
使用Single的一个常见用例是执行只能返回响应或错误的HTTP请求,但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)
subscription提供了一个SingleEvent
枚举,它可以是一个包含Single的element类型的.success或者.error 。除第一个之外不会再发出事件。
也可以在原始Observable序列上使用.asSingle()将其转换为Single。
Completable
Completable是Observable的变体,只能完成或发出错误。保证不发出任何元素。
- 发出零元素。
- 发出完成事件或错误。
- 没有共享元素效果。
Completable的一个有用情况是模拟只关心操作已完成的任何情况,但不关心完成所产生的元素。可以将它与使用Observable<Void>不能发射元素的情况进行比较。
创建Completable
创建Completable与创建Observable类似。一个简单的例子如下所示:
func cacheLocally() -> Completable {
return Completable.create { completable in
// 存储一些本地数据
...
...
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是Observable的变体,它位于Single和Completable之间。它既可以发出单个元素,也可以在不发出元素的情况下完成,或者发出错误。
注意:这三个事件中的任何一个都会终止Maybe,意思是 - 一个已发出Completion事件的不能发送一个元素,并且发出一个元素的Maybe不能发送一个Completion事件。
- 发出Completion事件,单个元素或错误。
- 没有共享元素效果。
可以使用Maybe来模拟任何可以发出元素,但不一定要发出元素的操作。
创建Maybe
创建一个Maybe类似于创建一个Observable。一个简单的例子如下所示:
func generateString() -> Maybe<String> {
return Maybe<String>.create { maybe in
maybe(.success("RxSwift"))
// 或者
maybe(.completed)
// 或者
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)
也可以在原始的Observable序列上使用.asMaybe()将其转换为Maybe。
RxCocoa Traits
Driver
这是最精细的特性。其目的是提供一种直观的方法来在UI层中编写响应式代码,或者为想要以建模的数据流的任何情况驱动应用程序。
- 不会出错。
- 观察发生在主调度程序上。
- 有共享元素效果(share(replay: 1, scope: .whileConnected))。
为什么命名为Driver
它的目的是模拟序列驱动应用程序。
例如
- 从CoreData模型驱动UI。
- 使用来自其他UI元素(绑定)的值来驱动UI。...
与普通的操作系统驱动程序一样,如果序列出错,应用程序将停止响应用户输入。
在主线程上观察这些元素也非常重要,因为UI元素和应用程序逻辑通常不是线程安全的。
此外,Driver构建一个可观察的序列并共享。
例如
实际用法示例
这是一个典型的初学者示例。
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元素:结果表视图和显示结果数的标签。
那么,这段代码有什么问题?:
- 如果
fetchAutoCompleteItems
可观察序列出错(连接失败或解析错误),则此错误将取消绑定所有内容,并且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) // 在主线程返回结果
.catchErrorJustReturn([]) // 在最坏的情况下,处理错误
}
.share(replay: 1) // 对所有UI元素共享HTTP请求并重放结果
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() // 将正常序列转换为`Driver`序列
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // 生成器只需要有关错误时返回内容的信息
}
results
.map { "\($0.count)" }
.drive(resultCount.rx.text) // 这是drive方法替代`bind(to:)`,
.disposed(by: disposeBag) // 这意味着编译器检验了所有属性是满意的
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特性的所有属性,甚至更多。底层的可观察序列只是作为Driver
特性包装,就是这样。
第二个变化是:
.asDriver(onErrorJustReturn: [])
任何可观察的序列都可以转换为Driver
trait,只要它满足3个属性:
- 不能出错。
- 在主调度程序观察。
- 有共享元素效果(share(replay: 1, scope: .whileConnected))。
那么如何确保满足这些属性?只需使用普通的Rx运算符.asDriver(onErrorJustReturn: [])相当于以下代码。
let safeSequence = xs
.observeOn(MainScheduler.instance) // 在主线程观察事件
.catchErrorJustReturn(onErrorJustReturn) // 不能出错
.share(replay: 1, scope: .whileConnected) // 共享
return Driver(raw: safeSequence) // 包装起来
最后一块是使用drive而不是使用bind(to:)。
drive仅在Driver
trait上定义。这意味着如果你在代码中某个地方看到drive,那这个可观察的序列永远不会出错并且它会在主线程上观察,这对于绑定到UI元素是安全的。
但是请注意,从理论上讲,仍然可以在ObservableType或一些其他接口中定义一个drive方法,因此为了更安全,对于完整证明在绑定到UI元素之前创建临时定义let results: Driver<[Results]> = ...是必要的。但是,我们会让读者决定这是否是一个现实的情况。
Signal
Signal类似Driver但有一点不同,它并没有重播订阅上的最近事件,但用户仍然共享序列的计算资源。
作为应用程序的一部分,被视为以响应式方式对命令性事件进行建模的构建器模式。
Signal:
- 不会出错。
- 在线程上提供事件。
- 共享计算资源(share(scope: .whileConnected))。
- 不会重播订阅上的元素。
ControlProperty / ControlEvent
ControlProperty
Observable/ObservableType
Trait表示UI元素。
值序列仅表示初始控制值和用户启动的值更改。不会报告程序化的值变化。
它的属性是:
- 永远不会失败
- share(replay: 1) 行为
- 它是有状态的,在订阅(调用subscribe)时,如果生成了最后一个元素,则会立即重播
- 序列销毁时会
Complete
- 永远不会出错
- 在MainScheduler.instance中发送事件
实现ControlProperty将确保事件的序列在主调度程序(subscribeOn(ConcurrentMainScheduler.instance)行为)上订阅。
实际用法示例
很好的实际例子是UISearchBar+Rx和在UISegmentedControl+Rx:
extension Reactive where Base: UISearchBar {
/// text属性的响应式包装
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 {
/// selectedSegmentIndex属性的响应式包装
public var selectedSegmentIndex: ControlProperty<Int> {
return value
}
/// selectedSegmentIndex属性的响应式包装
public var value: ControlProperty<Int> {
return UIControl.rx.value(
self.base,
getter: { segmentedControl in
segmentedControl.selectedSegmentIndex
}, setter: { segmentedControl, value in
segmentedControl.selectedSegmentIndex = value
}
)
}
}
ControlEvent
表示UI元素上的事件的trait。
它的属性是:
- 永远不会失败
- 不会在订阅时发送任何初始值
- 在序列销毁时会Complete
- 永远不会出错
- 在MainScheduler.instance活动
实现ControlEvent将确保事件的序列在主调度程序(subscribeOn(ConcurrentMainScheduler.instance)行为)上订阅。
实际用法示例
可以使用这个典型的案例:
public extension Reactive where Base: UIViewController {
/// `viewDidLoad` 消息 `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 {
/// 代理消息的响应式包装
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)
}
}
网友评论