使用 UI 控件过滤 RxSwift/RxCocoa 中的反应数据集

Filtering reactive dataset in RxSwift/RxCocoa with UI controls

(我在这里对反应式编程还很陌生,我知道我还没有完全按照 "Reactive" 的方式思考,所以我不确定我是否做对了。 ..)

我有一个来自 REST API 的数据集,用于驱动 UICollectionView of Photos,并使用 [=50= 中的开关过滤集合].我当前的绑定图看起来像

[API DataSet] -> unfilteredData: BehaviorRelay<Photo> -map-> collectionView.items

在我最后的 map 阶段,我直接读取 UI 的状态 (switch.isOn) 以应用过滤。

但是我也想用我的 switch.rx.isOn observable 驱动过滤。我这样做是通过让 unfilteredData 接受它自己的当前值,从而重新驱动整个过滤机制并重新显示集合视图。

所以这里有两件事我不是很满意,因为我觉得我仍然停留在命令式编程的思维模式中。

  1. 我正在读取 UI 的状态,而不是单纯地对该状态的变化做出反应
  2. 我正在让 BehaviorRelay 接受它自己的值以触发它的事件链。

但是因为我不想每次 UI 状态改变时都从 API 重新加载数据集(服务器调用很昂贵)我觉得也许我不能纯粹在这里反应?

这是我用来说明的简化代码(问题 1 和 2 已注明)。

private let unfilteredDataSource = BehaviorRelay<[Photo]>(value: [])

/// Refresh data from API
private func refreshData() {
    let photoResponse: Observable<[Photo]> = APICall(method: .GET, path: "photos.json")
    photoResponse
        .bind(to: unfilteredDataSource)
        .disposed(by: disposeBag)
}

/// Setup reactive bindings (on viewDidLoad)
private func setupRx() {
    // unfiltered becomes filtered via map before it's sent to collection items
    unfilteredDataSource
        .observeOn(MainScheduler.asyncInstance)
        .map {
            // filtering by hasLocation if includeAllSwitch is off
            [=13=].filter{ [=13=].hasLocation() || self.includeAllSwitch.isOn } //1
        }
        .bind(to: photoCollection.rx.items(cellIdentifier: "MyCell", cellType: PhotoCell.self)) { (row, element, cell) in
            // here I set up the cell...
        }
        .disposed(by: disposeBag)

    // filteringSwitch.isOn changes need to drive filtering too
    filteringSwitch.rx
        .isOn
        .asDriver(onErrorJustReturn: true) // Driver ensures main thread, and no errors (always returns true here)
        .drive(onNext: { [unowned self] switchValue in
            // triggering events using A = A; seems clumsy? And switchValue unused.
            self.unfilteredDataSource.accept(self.unfilteredDataSource.value) //2
        })
        .disposed(by: disposeBag)
}

解决此类问题最"reactive"的方法是什么?

编辑 正如我在下面的评论中提到的 "There's just one switch. There's the input JSON data from the server that goes to a collection view, and one switch which changes which photos are shown (toggle between showing all photos, or only a subset of photos that has a location attached in this instance)"

在这种情况下,您将呈现两个事件序列:api 调用和开关的值。你需要的是每当开关发出一个你想要的值 'remember' 网络调用的最新响应,并根据开关值进行过滤。操作方法如下:

    let filteredPhotos = Observable.combineLatest(unfilteredDataSource.asObservable(), 
                             filteringSwitch.rx.isOn) { ([=10=], ) } // 1
        .map { photos, includeAll in
            photos.filter { [=10=].hasLocation || includeAll }
        }
        .asDriver(onErrorJustReturn: [])

    filteredPhotos
        .drive(onNext: { ... })
        .disposed(by: disposeBag) 

// 1 - 每当其中一个可观察对象发出一个值时,它就会调用将最新值组合成一个元组的闭包。您不必担心再打 api 电话。

几个值得注意的点:

  • combineLatest 将等到它的每个可观察对象发出一个值,并且仅在最后一个序列完成时才完成。
  • 此外,如果任何序列完成,它会使用最后发出的值。
  • 只要您不确定事件流(请注意,您可以拖动它们),就应该使用 RxMarbles
  • (几乎)一切都可以是一个序列。

考虑到这一点,还有更多的改进空间。我看到您有一个 refreshData() 函数,它也可以转移到事件流中。您可能在 viewDidLoadviewDidAppear 中调用它,或者可能从 UIRefreshControll 中调用它。但它们是触发器,所以它们是 Void 的序列,对吗?这意味着您可以 flatMapLatest 进入您的 api 调用。这样,您也将不再需要 BehaviourRelay。