RxSwift文档四(Traits)

代码下载

本文档将尝试描述什么是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不能发射元素的情况进行比较。

创建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)

那么这里发生了什么?

asDriverControlProperty trait转换为Drivertrait。

query.rx.text.asDriver()

请注意,没有什么特别需要做的事情。Driver具有该ControlProperty特性的所有属性,甚至更多。底层的可观察序列只是作为Driver特性包装,就是这样。

第二个变化是:

.asDriver(onErrorJustReturn: [])

任何可观察的序列都可以转换为Drivertrait,只要它满足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仅在Drivertrait上定义。这意味着如果你在代码中某个地方看到drive,那这个可观察的序列永远不会出错并且它会在主线程上观察,这对于绑定到UI元素是安全的。

但是请注意,从理论上讲,仍然可以在ObservableType或一些其他接口中定义一个drive方法,因此为了更安全,对于完整证明在绑定到UI元素之前创建临时定义let results: Driver<[Results]> = …是必要的。但是,我们会让读者决定这是否是一个现实的情况。

Signal

Signal类似Driver但有一点不同,它并没有重播订阅上的最近事件,但用户仍然共享序列的计算资源。

作为应用程序的一部分,被视为以响应式方式对命令性事件进行建模的构建器模式。

Signal:

  • 不会出错。
  • 在线程上提供事件。
  • 共享计算资源(share(scope: .whileConnected))。
  • 不会重播订阅上的元素。

ControlProperty / ControlEvent

ControlProperty

Observable/ObservableTypeTrait表示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)
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值