[转载]ViewModel in RxSwift world

原文:
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<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:

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, 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! ?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值