原文:
https://medium.com/@SergDort/viewmodel-in-rxswift-world-13d39faa2cf5
According Wikipedia view model is an abstraction of the view exposing public properties and commands. Instead of the controller of the MVC pattern, or the presenter of the MVP pattern, MVVM has a binder. In the view model, the binder mediates communication between the view and the data binder.[clarification needed] The view model has been described as a state of the data in the model.
When entering Rx world prepare to think about: UI events, Network requests, Data base request etc. as a Streams .
Keeping this in mind I like to think about 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 apply some Rx operators (which determines business logic). And after that, from view model you can get that transformed observables and bind them back to your UI to apply your business logic.
As example I want to show how you could implement list of searchable data and display it in table view with search bar
Let’s imagine that all model staff implemented and all we need to do is create ViewModel and ViewController
HeroListViewModelInterface.swift hosted with ❤ by GitHub
So, let’s define 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 ViewModel interface
class HeroListViewModel {
let mainTableItems: Driver<[HeroCellSection]>
let searchTableItems: Driver<[HeroCellSection]>
init(uiTriggers: (
searchQuery: Observable,
nextPageTrigger: Observable,
searchNextPageTrigger: Observable
),
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:
class HeroListViewModel {
let mainTableItems: Driver<[HeroCellSection]>
let searchTableItems: Driver<[HeroCellSection]>
let dismissTrigger: Driver
init(uiTriggers: (searchQuery: Observable,
nextPageTrigger: Observable,
searchNextPageTrigger: Observable,
dismissTrigger: Driver), 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, fires only if there is 0.3 sec pause
Transfroms search query into request and cancels previuos
Transfroms Hero into dummy HeroCellData (eg. title, image url)
Transforms Array of HeroCellData into HeroCellSection (this needed to bind it to the the UITableView)
Triggers next page request
And now let’s bind our transformed Observables back to 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)
Create ViewModel
Bind Main table items to UITableView
Bind search items to the UISearchController’s tableView
Summary:
Our view model is “pure” it’s immutable, we don’t even need a reference to it in ViewController (the disposeBag keeps subscriptions alive)
All logic incapsulated in one place
It’s can be easily tested with RxTests . Coming up in the next article
Further reading:
Intro to Rx
RxSwift Book
Rx.io
RxDataSources
Original Code
Happy RxSwift coding!