iOS Developer in search for meaning 🧘‍♂️

ViewModel in RxSwift world

June 11, 2016

According to the Microsoft Docs view model acts as an intermediary between the view and the model, and is responsible for handling the view logic. Typically, the view model interacts with the model by invoking methods in the model classes. The view model then provides data from the model in a form that the view can easily use.

Entering Rx world prepare to think about UI events, Network requests, Data base request etc. as a Stream of values over the time.

morpheus

Keeping this in mind I like to think about a ViewModel as a “Black Box” which accepts some UI triggers (button tap, table view selection, text editing events etc.), other dependencies (NeworkService, DataBaseService, LocationService) and applies Rx operators (which determines a behaviour). And after that, from the ViewModel, you can get that transformed observables and bind them back to your UI applying the behavior.

black_box

As example I want to show how you could implement a list of searchable data and dislpay it in a table view with a search bar

heroes_list

Let’s imagine that all model staff implemented and all we need to do is create ViewModel and ViewController

So, let’s define the UI triggers:

  • search trigger (user can type to search data in the list)
  • scroll triggers (user can scroll to pull new data from the list)

Now we can define the ViewModel interface

class HeroListViewModel {
   
   let mainTableItems: Driver<[HeroCellSection]>
   let searchTableItems: Driver<[HeroCellSection]>
   
   init(uiTriggers: (
         searchQuery: Observable<String>,
         nextPageTrigger: Observable<Void>,
         searchNextPageTrigger: Observable<Void>
      ), 
      api: HeroAPI)
      
}

Now let’s define transformation we want to apply to our initial triggers:

  • transfrom search query into request
  • prevent firing request for empty query
  • prevent fire reqest every time user type new character
  • cancel previous request in favor of new one
  • hit request every time user scrolls to the bottom edge of scroll view
  • append previous state (array) with new data

Implementation of transformations:

final class HeroListViewModel {
   
   let mainTableItems: Driver<[HeroCellSection]>
   let searchTableItems: Driver<[HeroCellSection]>
   let dismissTrigger: Driver<Void>
   
   init(uiTriggers: (searchQuery: Observable<String>,
      nextPageTrigger: Observable<Void>,
      searchNextPageTrigger: Observable<Void>,
      dismissTrigger: Driver<Void>), api: HeroAPI) {
    
    
    searchTableItems = uiTriggers.searchQuery
        .filter { !$0.isEmpty }//1
        .throttle(0.3, scheduler: MainScheduler.instance)//2
        .flatMapLatest { //3
            return api.searchItems($0,
                batch: Batch.initial,
                endPoint: EndPoint.Characters,
                nextBatchTrigger: uiTriggers.searchNextPageTrigger) // 6
               .catchError { _ in
                  return Observable.empty()
               }
        }
        .map { //4
            return $0.map(HeroCellData.init)
        }
        .map {//5
            return [HeroCellSection(items: $0)]
        }
        .asDriver(onErrorJustReturn: [])
   }
}
  1. Filters empty string, remember we don’t want fire request for empty query

  2. Prevents to fire request every time user types new character, fire only if there is 0.3 sec pause

  3. Transforms search query into request and cancels previuos

  4. TransfromsHerointodummyHeroCellData(eg. title, image url)

  5. Transforms Array ofHeroCellDataintoHeroCellSection(this needed to bind it to the UITableView)

  6. Triggers next page request

And now let’s bind our transformed Observables back to the UI

     //1
      let viewModel = HeroListViewModel(uiTriggers:(
          searchQuery: searchCotroller.searchBar.rx_text.asObservable(),
          nextPageTrigger: tableView.rx_nextPageTriger,
          searchNextPageTrigger: searchContentController.tableView.rx_nextPageTriger
          ),
          api: DefaultHeroAPI(paramsProvider: HeroesParamsProvider.self))
      //2
      viewModel.mainTableItems
         .drive(tableView.rx_itemsWithDataSource(dataSource))
         .addDisposableTo(disposableBag)
      //3
      viewModel.searchTableItems
         .drive(searchContentController.tableView.rx_itemsWithDataSource(searchDataSource))
         .addDisposableTo(disposableBag)
  1. Creates the ViewModel
  2. Binds Main table items to the UITableView
  3. Binds search items to the UISearchController tableView

Summary

  • Our view model is “pure” it’s immutable, we don’t even need a reference to it in ViewController (thedisposeBagkeeps subscriptions alive)
  • All logic encapsulated in one place
  • It’s can be easily tested with RxTests .

Further reading

Happy RxSwift coding! 🚀


Serg Dort

Written by Serg Dort, who works and lives in London builds useful things. You can follow him on Twitter