使用 UI 控件过滤 RxSwift/RxCocoa 中的反应数据集
Filtering reactive dataset in RxSwift/RxCocoa with UI controls
(我在这里对反应式编程还很陌生,我知道我还没有完全按照 "Reactive" 的方式思考,所以我不确定我是否做对了。 ..)
我有一个来自 REST API 的数据集,用于驱动 UICollectionView of Photo
s,并使用 [=50= 中的开关过滤集合].我当前的绑定图看起来像
[API DataSet] -> unfilteredData: BehaviorRelay<Photo> -map-> collectionView.items
在我最后的 map
阶段,我直接读取 UI 的状态 (switch.isOn
) 以应用过滤。
但是我也想用我的 switch.rx.isOn
observable 驱动过滤。我这样做是通过让 unfilteredData
接受它自己的当前值,从而重新驱动整个过滤机制并重新显示集合视图。
所以这里有两件事我不是很满意,因为我觉得我仍然停留在命令式编程的思维模式中。
- 我正在读取 UI 的状态,而不是单纯地对该状态的变化做出反应
- 我正在让 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() 函数,它也可以转移到事件流中。您可能在 viewDidLoad
或 viewDidAppear
中调用它,或者可能从 UIRefreshControll
中调用它。但它们是触发器,所以它们是 Void
的序列,对吗?这意味着您可以 flatMapLatest 进入您的 api 调用。这样,您也将不再需要 BehaviourRelay。
(我在这里对反应式编程还很陌生,我知道我还没有完全按照 "Reactive" 的方式思考,所以我不确定我是否做对了。 ..)
我有一个来自 REST API 的数据集,用于驱动 UICollectionView of Photo
s,并使用 [=50= 中的开关过滤集合].我当前的绑定图看起来像
[API DataSet] -> unfilteredData: BehaviorRelay<Photo> -map-> collectionView.items
在我最后的 map
阶段,我直接读取 UI 的状态 (switch.isOn
) 以应用过滤。
但是我也想用我的 switch.rx.isOn
observable 驱动过滤。我这样做是通过让 unfilteredData
接受它自己的当前值,从而重新驱动整个过滤机制并重新显示集合视图。
所以这里有两件事我不是很满意,因为我觉得我仍然停留在命令式编程的思维模式中。
- 我正在读取 UI 的状态,而不是单纯地对该状态的变化做出反应
- 我正在让 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() 函数,它也可以转移到事件流中。您可能在 viewDidLoad
或 viewDidAppear
中调用它,或者可能从 UIRefreshControll
中调用它。但它们是触发器,所以它们是 Void
的序列,对吗?这意味着您可以 flatMapLatest 进入您的 api 调用。这样,您也将不再需要 BehaviourRelay。