Edited at

ReactiveCocoaとMVVMモデル

More than 3 years have passed since last update.

この記事はCyberAgentエンジニアAdvent Calendar 2015の20日目の記事です。


はじめに

こんにちは。サイバーエジェントの新卒のギア@nghialvと申します。現在,AmebaFRESH!という新規生放送サービスのチームにiOSエンジニアやっています。

私は入社した時に,多くのユーザーに使われているAmebaアプリチームに入りました。Amebaアプリはなん年前から作成されましたので,今までの他のiOSアプリと同じでMVCモデルに基づいて設計されています。しかし,アプリがどんどん大きくしてMVCモデルの問題点を感じるようになりました。

4ヶ月前にAmebaFRESH!の新規サービスに移動して,MVVMモデルとReactiveCocoaを検討して,導入しました。

今日MVVMモデルとReactiveCocoaについて説明させていただきます。


目次


  • MVCからMVVMに移動

  • Water pipesからReactiveCocoaを理解

  • ReactiveCocoaとMVVMを組み合わせるサンプル


MVCからMVVMに移動

上の図は一般的なMVCモデルの仕組みです。MVCモデルはModel, View, ViewControllerの3つの部分に分割しています。

このモデルは以下の2つ問題点が存在していることがわかりました。

まずは,ViewControllerはModelとViewを持っていて,ロジックの処理は基本的にViewControllerに配置されています。アプリの成長と共に大規模なViewControllerがどんどん増えていきます。(この理由で皆んなよくMVCはMassive ViewControllerと呼んでいますね)iOSエンジニアの皆さんはこの問題をあったことがありましたね。

次は,ViewControllerはViewを持っていますので,Viewに依存していて,ViewControllerのテスト書くのはなかなか難しいと思います。

そこで,テスト書ける部分は主にModelとUtilityの周りだけになります。

それらの問題点でMVVMモデルの導入を検討しました。

上の図により,ViewModelの部分が追加されました。

ViewModelはModelオブジェクトのデータを基にViewに食わせる値を生成するロジック(プレゼンテーションロジック)やViewの状態を持ちます。

iOSのViewControllerはViewと同じ役割になっています。

さらに,MVCと異なって,data binding概念を入っています。

ViewModelはViewのインスタンスを持ちではなく,data bindingでViewModel側はデータの変更がありましたら,Viewに通知を送ります。

じゃ、MVVMモデルの良い点?

- ViewModelはよく変われるViewを持っていないので,モデルみたいにテスト書くことは容易になります。

- 大規模なViewControllerがなくなります。

- ViewはViewModelにバインディングする仕組みなので,Viewのリセットなどを行いたい場合は単純新しいViewModelインスタンスを生成して,再バインディングすることで,Viewがリセットされます。ステートの管理が楽になります。

(これは自分が感じできた良い点ですが,もし何かわかりましたら,追加お願い致します。)

上はバインディングについて何回も言いましたが,具体的はどんな感じって疑問している人もいるでしょう。

それはReactiveCocoaの役割ですね。


Water pipesからReactiveCocoa理解

ReactiveCocoaの定義は以下のとおりですね。


ReactiveCocoa (RAC) is a Cocoa framework inspired by Functional Reactive Programming. It provides APIs for composing and transforming streams of values over time.


実は最初私はこの定義を呼んだ時に,ReactiveCocoaはなんのものか想像できなかったです。

しかし,使ってみて,この定義の文は完璧だとわかるようになりました。

また,ReactiveCocoaの仕組みはWater pipesの仕組みと全く同じかなと考えていますよ。

じゃ次はReactiveCocoaの定義の文とWater pipesを用いて,ReactiveCocoaを説明させていただきます。これを読んだ後に絶対ReactiveCocoaはなんのものか想像できるようになると思います。

定義の文に注目したいのは「Stream」「Transforming」「Composing」の3つの言葉です。


Stream


let (signal, sink) = Signal<String, ErrorType>.pipe()

signal
.observe { event in
// hi
}

sink.sendNext("hi")

上の図をみると説明がなくてもなんとなくイメージできましたね。

eventをsinkに入れて,observerがそのイベントを受け取ることができます。

全くWater pipesの仕組みと似ているでしょう。

もちろん,水は上に流すことができないですが,イベントはすべての方向に行けます。(笑)

let (signal, sink) = Signal<String, ErrorType>.pipe()

signal
.observe { event in
// hi
}

signal
.observe { event in
// hi
}

sink.sendNext("hi")

もう一点注意が必要なのは,observeする前のイベントを受け取ることができないです。


Transforming


次はtransformですね。また,以下の図をみると説明は何もなくてもすぐわかります。




let (signal, sink) = Signal<String, ErrorType>.pipe()

signal
.map { str in str.uppercaseString }
.observe { event in
// “HI"
}

sink.sendNext("hi")

Transformingsignalから別のsignalに変換することです。

全くWater pipesの仕組みと似ているでしょう。

ReactiveCocoaはいろいろなtransformを提供しています。私は以下のようなものをよく使っています。

map, flatMap, filter, reduce, collect, takeUntil, mapError, skipRepeats, throttle, skip, observeOn...

もちろん,自分は新しいtransform追加することは簡単です。


Composing


数学にcomposeは以下のとおり定義されていますね。

• compose  <A, B, C> (A -> B, B -> C) -> A -> C

f1: A -> B
f2: B -> C
f3: C -> D

f = f1・f2・f3
=> f: A -> D

let searchResults = searchStrings

.flatMap(.Latest) { (query: String) -> SignalProducer<(NSData, NSURLResponse), NSError> in
let URLRequest = self.searchRequestWithEscapedQuery(query)
return NSURLSession.sharedSession().rac_dataWithRequest(URLRequest)
}
.map { (data, URLResponse) -> String in
let string = String(data: data, encoding: NSUTF8StringEncoding)!
return self.parseJSONResultsFromString(string)
}
.observeOn(UIScheduler())
.startWithNext { results in
print("Search results: \(results)")
}

同じ意味で,ReactiveCocoaはsignaltransformを何回も組み合わせることが可能であり,新しいsignalを生成することができます。

全くWater pipesの仕組みと似ているでしょう。

ReactiveCocoaを使うとある時にアプリは以下のようなかんじになりますね。(笑)

はい,ここまで,Water pipesを使ってReactiveCocoaを説明しました。

もちろん,ReactiveCocoaは他の概念もいっぱいありますが,基盤の概念は上のとおりです。


ReactiveCocoaとMVVMを組み合わせるサンプル

モバイルアプリではよくあるリスト画面をReactiveCocoaとMVVMモデルでの実装を紹介させていただきます。

実は今のプロジェクトでCellに対するCellModelという概念も導入していて,TableViewやCollectionViewのCellとCellModelを管理するため,自分が開発しているHakubaSapporoを使っています。


class ProgramsViewModel {
private var offset = 0

private (set) lazy var refreshAction: Action<Int, [ProgramCellModel], ApiError> = { [unowned self] in

return Action { count in
Api.Programs
.getPrograms(count: count)
.map(ProgramCellModel.init)
}
}()

private (set) lazy var loadmoreAction: Action<(Int, Int), [ProgramCellModel], ApiError> = { [unowned self] in

return Action { offset, count in
Api.Programs
.getPrograms(offset: offset, count: count)
.map(ProgramCellModel.init)
}
}()

func refresh() {
refreshAction
.apply(25)
.takeUntil(willDeinitProducer)
.on { [unowned self] in
self.offset = $0.count
}
.start()
}

func loadmore() {
loadmoreAction
.apply(offset, 25)
.takeUntil(willDeinitProducer)
.on { [unowned self] in
self.offset += $0.count
}
.start()
}
}

class ProgramsViewController: UIViewController {

@IBOutlet private weak var collectionView: UICollectionView!

private lazy var sapporo: Sapporo = Sapporo(collectionView: self.collectionView)
let viewmodel = ProgramViewModel()
let viewmodelDisposable = ScopedCompositeDisposable()

override func viewDidLoad() {
super.viewDidLoad()

bindViewModelSignals()
sapporo.loadmoreHandler = { [weak self] in
self?.viewmodel.loadmore()
}

viewmodel.refresh()
}
}

// MARK: - Private methods

private extension ProgramsViewController {
func bindViewModelSignals() {
viewmodelDisposable += viewmodel.refreshAction.values
.observeOn(uiScheduler)
.observeNext { [unowned self] cellmodels in
self.sapporo[ProgramsSection]
.reset(cellmodels)
.bump()
}

viewmodelDisposable += viewmodel.loadmoreAction.values
.observeOn(uiScheduler)
.observeNext { [unowned self] cellmodels in
self.sapporo[ProgramsSection]
.append(cellmodels)
.bump()
}

viewmodelDisposable += viewmodel.refreshAction.errors
.observeOn(uiScheduler)
.observeNext { [unowned self] error in
// show a reloadable error view
}

viewmodelDisposable += viewmodel.loadmoreAction.errors
.observeOn(uiScheduler)
.observerNext { [unowned self] error in
// show an error message
}

viewmodelDisposable += combineLatest(viewmodel.refreshAction.executing.producer, viewmodel.loadmoreAction.executing.producer)
.observeOn(uiScheduler)
.map { $0 || $1 }
.observerNext { [unowned self] loading in
// show or hide loading indicator
}
}
}


最後に

今日、自分の理解でReactiveCocoaとMVVMモデルを軽く説明しました。

MVVMモデルのサンプルプロジェクトやブログはまだまだ多くないので,時々困りました点もありましたが,

プロジェクトに導入してみて,プロジェクトの設計が綺麗になっていて,毎日楽しく開発できています。


明日はk66dangoさんです。