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.
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.
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
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:
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:
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: [])
}
}
Filters empty string, remember we don’t want fire request for empty query
Prevents to fire request every time user types new character, fire only if there is 0.3 sec pause
Transforms search query into request and cancels previuos
TransfromsHerointodummyHeroCellData(eg. title, image url)
Transforms Array ofHeroCellDataintoHeroCellSection(this needed to bind it to the UITableView)
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)
UITableView
UISearchController
tableViewHappy RxSwift coding! 🚀
Written by Serg Dort, who works and lives in London builds useful things. You can follow him on Twitter