iOS
ReactiveCocoa
MVVM
Swift
ReactiveSwift

ReactiveSwift / ReactiveCocoa / MVVM 入門 Part 2/2 ~ChangesetとUITableView~


この記事について

ReactiveCocoa、ReactiveSwift、MVVMを使ってシンプルなアプリを作ります。今回が第2回目です。

前回の記事はこちらです。


前回の続き

前回は、


  • SearchBarからのinputを元にNavigationBarをReactiveに表示する

  • SearchBarからのinputを元にiTuneAPIを叩いて結果をprintする

というところまでやりました。今回は、TableViewを使ってAPIの取得結果をReactiveに表示 して行きます。


TableViewの問題

TableViewをMVVMで実装するのに当たって、3つほど考える点があります。


  1. DataSourceをどうするか

  2. Cellをどのように呼び出すか

  3. どうやってreloadDataするか?

それぞれ見て行きましょう。


1. DataSourceをどうするか

まず最初に確認しておくべきなのが、UITableViewDataSourceプロトコルはViewとViewModelどちらで継承すれば良いのでしょうか? 答えは、View で継承するです。というのも、UITableViewDataSourceプロトコルはUIKitフレームワークの中に入っているからです。方法としては、ViewModelの中でデータを管理して、Viewの中のViewModelへのリファレンスを通してそのデータを呼び出すような形をとります。


2. Cellをどのように呼び出すか

Cellの呼び出し方を見る前に、知っておくべきMVVMの重要なルールが二つあります。


  1. ViewはViewによってのみインスタンス化される

  2. ViewModelはViewModelによってのみインスタンス化される

です。

ただし、どちらも一番最初にAppDelegateでインスタンス化される場合を除く です。

つまり、UITableViewcellForRowAtのDelegateメソッドでUITableViewCellのインスタンスを呼び出すのと同じように、TableViewと紐づくViewModelTableViewCellと紐づくViewModelを呼ぶ、というイメージです。

1, 2を踏まえ、以下のような実装になります。


ViewModel側

まずTrackCellViewModel.swiftを作りViewModelグループに追加します。

class TrackCellViewModel {

let name: String
let artist: String
let index: Int

init(with track: Track) {
self.name = track.trackName
self.artist = track.artistName
self.index = track.index
}
}

次に、以下のfunctionをHomeViewModel.swiftに追加します

func getTrackCount() -> Int {

return tracks.count
}

func createCellViewModel(for index: Int) -> TrackCellViewModel {
return TrackCellViewModel(with: tracks[index])
}


View側

まず、UITableViewDelegateUITableViewDataSourceを継承させた上で以下をHomeViewController.swiftに追加します。

class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

...

override func viewDidLoad() {
super.viewDidLoad()

tableView.delegate = self
tableView.dataSource = self

...

}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel?.getTrackCount() ?? 0
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: trackCell, for: indexPath) as! TrackCell
cell.viewModel = self.viewModel?.createCellViewModel(for: indexPath.row)
return cell
}

...

}

次に、TrackCell.swiftにviewModelへのリファレンスを追加します。

var viewModel: TrackCellViewModel! {

didSet {
artistLabel.text = viewModel.artist
titleLabel.text = viewModel.name
}
}


3. どうやってreloadDataするか?

プロジェクトの完成まであと一歩です。

最後に考える問題が、いつTableViewをreloadするか? というものです。ViewはViewModelを参照していますが、ViewModelはViewに対してポインタを持っていません。つまり、ViewModel側ではSignalを通して値の変化をObserveしているだけなので、tableViewをいつ更新すれば良いか はわからないのです。この問題を解決するために、このプロジェクトではChangesetというライブラリを使っています。

Changesetを使うことで、View上でViewModelのデータの変更を監視することができます。

つまり、viewModelがプロパティとして保有しているデータに変更があった時に、View上でtableView.reloadData()を呼び出す という形になります。(用例の詳細に関してはリンクをご参照ください。)

実装は以下のようになります。

まずHomeViewModel.swiftで以下を追加します。

import Foundation

import ReactiveSwift
import ReactiveCocoa
import Result
import Changeset

class HomeViewModel {

//「Array<Track>をElementとしたChangeset」を初期値としてもったMutablePropertyをインスタンス化。
let trackChangeset = MutableProperty([Changeset<[Track]>.Edit]())

//tracksがセットされた時点で変更を監視
private var tracks: [Track] {
didSet {
trackChangeset.value = Changeset.edits(
from: oldValue,
to: tracks)
}
}

...
}

次にHomeViewController.swiftViewDidLoad上で以下を呼び出してください。

//changesetがあるたびにtableView更新

self.viewModel.trackChangeset.producer.startWithValues { edits in
self.tableView.update(with: edits)
}

プロジェクトをrunして、SearchBarに検索のテキストを入れてみましょう。結果が反映されているはずです!


まとめ

ReactiveCocoa、ReactiveSwift、MVVMを使って簡単なAPIサーチのTableViewアプリを作りました。Changesetのような手法は便利ですが、KVO手法を取ることになるのでReactive Programingのストリームの監視で全てのデータを管理する、という哲学から逸れてしまい、純粋にReactiveな実装とは言えないと思います。(こちらに関してうまくやる方法をご存知の方はご教授いただけると幸いです。。) これからもっとRACを勉強していきたいと思います。

長文お読みいただき誠にありがとうございました。コメントやご指摘お待ちしております。


参考

第一回目の記事はこちら

ReactiveSwift / ReactiveCocoa / MVVM 入門 Part 1/2 by kokoheia


参考ページ