Reputation: 5625
(I'm quite new to Reactive programming here, and I'm aware I'm not quite thinking in the "Reactive" way yet so I'm not sure I'm quite doing this right...)
I have a dataset that comes from a REST API to drive a UICollectionView of Photo
s, with filtering of the collection done with switches in the UI. My current binding graph looks like
[API DataSet] -> unfilteredData: BehaviorRelay<Photo> -map-> collectionView.items
and in my last map
stage I'm reading the state of the UI directly (switch.isOn
) to apply filtering.
But then I want to drive the filtration with my switch.rx.isOn
observable too. I'm doing this by getting unfilteredData
to accept its own current value, and thereby re-drive the whole filtering mechanism and redisplay the collection view.
So there are 2 things I'm not super happy with here, as I feel like I'm still stuck in an imperative programming mindset.
But since I don't want to reload the data set from the API each time the UI state changes (server calls are expensive) I feel like maybe I can't be purely reactive here?
Here's my simplified code to illustrate (with problem 1 and 2 noted).
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
$0.filter{ $0.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)
}
What's the most "reactive" way of approaching a problem like this?
EDIT As I mention in the comment below "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)"
Upvotes: 1
Views: 283
Reputation: 607
In this case you are presenting two event sequences: the api call, and the value of the switch. What you need is whenever the switch emits a value you want to 'remember' the latest response from your network call, and filter based on the switch value. Here's how to do it:
let filteredPhotos = Observable.combineLatest(unfilteredDataSource.asObservable(),
filteringSwitch.rx.isOn) { ($0, $1) } // 1
.map { photos, includeAll in
photos.filter { $0.hasLocation || includeAll }
}
.asDriver(onErrorJustReturn: [])
filteredPhotos
.drive(onNext: { ... })
.disposed(by: disposeBag)
// 1 - every time one of the observables emits a value, it calls the closure that combines the latest values into a touple. You don't have to worry about making another api call.
A few notable points:
With this in mind, there is even more room for improvement. I see you have a refreshData() function that can also be shifted to an event stream. You are probably calling this in viewDidLoad
or viewDidAppear
or maybe from an UIRefreshControll
. But they are triggers, so they are a sequence of Void
, right? That means you can flatMapLatest into your api call. This way, you also would no longer need the BehaviourRelay.
Upvotes: 1