ReactiveCocoaとMVVMモデル

  • 85
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事は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さんです。