ReactiveCocoaとMVVMモデル

この記事は[CyberAgentエンジニアAdvent Calendar 2015](http://www.adventar.org/calendars/863)の２０日目の記事です。

## はじめに

こんにちは。サイバーエジェントの新卒のギア[@nghialv](https://github.com/nghialv)と申します。現在，[AmebaFRESH!](https://amebafresh.tv/pc/index.html)という新規生放送サービスのチームにiOSエンジニアやっています。

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

４ヶ月前に[AmebaFRESH!](https://amebafresh.tv/pc/index.html)の新規サービスに移動して，MVVMモデルとReactiveCocoaを検討して，導入しました。
今日MVVMモデルとReactiveCocoaについて説明させていただきます。

## 目次

- MVCからMVVMに移動
- Water pipesからReactiveCocoaを理解
- ReactiveCocoaとMVVMを組み合わせるサンプル

## MVCからMVVMに移動

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/18907/6bafb8ec-9b33-2908-2a8f-4f893b6b40cd.png" height="150">
</p>

上の図は一般的なMVCモデルの仕組みです。MVCモデルはModel, View, ViewControllerの３つの部分に分割しています。
このモデルは以下の２つ問題点が存在していることがわかりました。

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

次は，ViewControllerはViewを持っていますので，Viewに依存していて，ViewControllerのテスト書くのはなかなか難しいと思います。
そこで，テスト書ける部分は主にModelとUtilityの周りだけになります。

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

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/18907/4c487e9b-37fc-b809-9b09-b5f8b77b64dd.png" height="85">
</p>

上の図により，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」の３つの言葉です。
>Stream

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/18907/badc8a57-845c-1d60-7c52-7317c4c6a143.png" height="250">
</p>

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

signal
    .observe { event in
		// hi
	}
	
sink.sendNext("hi")
```

上の図をみると説明がなくてもなんとなくイメージできましたね。
eventをsinkに入れて，observerがそのイベントを受け取ることができます。
全くWater pipesの仕組みと似ているでしょう。

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/18907/d0978987-54be-a7b9-7191-23ad98e80037.png" height="200">
</p>

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

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

signal
    .observe { event in
		// hi
	}
	
signal
    .observe { event in
		// hi
	}

sink.sendNext("hi")
```

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


>Transforming

次は`transform`ですね。また，以下の図をみると説明は何もなくてもすぐわかります。
<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/18907/ea2863cf-4ca7-a717-b756-048e47a00078.png" height="250">
</p>

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

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

sink.sendNext("hi")
```

`Transforming`は`signal`から別の`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
```

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/18907/b7611cf2-f135-229f-d8c9-f716b35857c4.png" height="250">
</p>

``` swift
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は`signal`と`transform`を何回も組み合わせることが可能であり，新しい`signal`を生成することができます。
全くWater pipesの仕組みと似ているでしょう。
ReactiveCocoaを使うとある時にアプリは以下のようなかんじになりますね。（笑）

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/18907/60a22e7d-297c-17b7-0ab3-adc91586d49a.png" height="250">
</p>

はい，ここまで，Water pipesを使ってReactiveCocoaを説明しました。
もちろん，ReactiveCocoaは他の概念もいっぱいありますが，基盤の概念は上のとおりです。


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

モバイルアプリではよくあるリスト画面をReactiveCocoaとMVVMモデルでの実装を紹介させていただきます。
実は今のプロジェクトでCellに対するCellModelという概念も導入していて，TableViewやCollectionViewのCellとCellModelを管理するため，自分が開発している[Hakuba](https://github.com/nghialv/Hakuba)、[Sapporo](https://github.com/nghialv/Sapporo)を使っています。


``` swift

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()
    }
}
```

``` swift
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さんです。