Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly use switchLatest to toggle between search results and empty state of tableview?

Tags:

swift

rx-swift

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()
like image 917
Danny Avatar asked Oct 30 '22 06:10

Danny


1 Answers

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)
like image 169
Daniel T. Avatar answered Nov 15 '22 07:11

Daniel T.