I'm rather new to RxSwift and trying to handle the task of toggling between the empty state and populated state of a search autocomplete.
I have one driver that responds to a textfield text changes with length > 0 and makes network requests and another than filters on empty search queries and just populate the tableview with "Favorites". I initially used merge() on the two observables but the problem was that quickly clearing the text would show the favorites but when the last fetch request came back it would merge in and override the empty state.
I tried switching to switchLatest() hoping it would cancel the previous fetch request when the final clearResult was fired but this observable never seems to fire at all and I'm a bit stuck. Any help would be greatly appreciated!
let queryFetchResultViewModel: Driver<[NewSearchResultViewModel]> = queryText
.filter { $0.utf8.count > 0 }
.throttle(0.5, scheduler: MainScheduler.instance)
.flatMapLatest { query in
appContext.placeStore.fetchPredictions(withQuery: query)
}
.map { $0.map { NewSearchResultViewModel.prediction(prediction: $0) } }
.asDriver(onErrorJustReturn: [])
let queryClearResultViewModel: Driver<[NewSearchResultViewModel]> = queryText
.filter { $0.utf8.count == 0 }
.withLatestFrom(favoritePlaces.asObservable())
.map { places in places.map(NewSearchResultViewModel.place) }
.asDriver(onErrorJustReturn: [])
searchResultViewModel = Driver.of(queryFetchResultViewModel, queryClearResultViewModel).switchLatest()
You are expecting an empty string to affect the output of a stream that is explicitly filtering out empty strings. That's why you are having this problem.
--EDIT--
To better explain the above...
Assume the user types a character and then deletes it In your original code. This is what will happen: The character will go into the filter
for queryClearResultViewModel
and get filtered out. It will also go through the chain for queryFetchResultViewModel
and fetch a prediction. Then the user deletes the character which will go into the filter for queryFetchResultViewModel
and get stopped, meanwhile it will go all the way through the queryClearResultViewModel
and the searchResultViewModel
will get the favorite places...
Then the network request completes which passes the prediction results to the searchResultViewModel
.
At this point, switchLatest
will shutdown the queryClearResultViewModel
(it will stop listening) and display the query results. It has stopped listening to the clear result stream now so no more favorites would ever show up.
-- END EDIT --
Here is code that will do what you want:
searchResultViewModel = queryText
.throttle(0.5, scheduler: MainScheduler.instance)
.flatMapLatest { query -> Observable<[NewSearchResultViewModel]> in
if query.isEmpty {
return favoritePlaces.asObservable()
.map { $0.map(NewSearchResultViewModel.place) }
}
else {
return appContext.placeStore.fetchPredictions(withQuery: query)
.map { $0.map { NewSearchResultViewModel.prediction(prediction: $0) } }
}
}
.asDriver(onErrorJustReturn: [])
The above code works because flatMapLatest
will cancel the in-flight fetchPredictions(withQuery:)
call when a new query comes into it. Even if that query is empty.
If you are married to splitting up the queryText
into two streams and then re-combining them, then all you need to do is allow the queryFetchResultViewModel
to process empty text by emitting an Observable.empty()
when an empty query comes through. But even there you might run into race problems because the fetch is being throttled while the clear isn't. So you could run into a situation where the fetch starts, then the empty query comes through which displays the favorites, then the query completes which displays the results, then the fetch stream receives the (delayed) clear and does nothing because the query already completed. So you would have to throttle the clear stream as well as the fetch stream...
So something like this (I used debounce because I think it works better for these sorts of things):
let debouncedQuery = queryText.debounce(0.5, scheduler: MainScheduler.instance)
let queryFetchResultViewModel = debouncedQuery
.flatMapLatest { query -> Observable<[String]> in
guard !query.isEmpty else { return Observable.empty() }
return appContext.placeStore.fetchPredictions(withQuery: query)
}
.map { $0.map { NewSearchResultViewModel.prediction(prediction: $0) } }
.asDriver(onErrorJustReturn: [])
let queryClearResultViewModel = debouncedQuery
.filter { $0.isEmpty }
.withLatestFrom(favoritePlaces.asObservable())
.map { $0.map(NewSearchResultViewModel.place) }
.asDriver(onErrorJustReturn: [])
searchResultViewModel = Driver.merge(queryFetchResultViewModel, queryClearResultViewModel)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With