Help us understand the problem. What is going on with this article?

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした