Dependency Injection in MVVM Architecture with ReactiveCocoa Part 4: ViewとViewModelの実装

以下のブログ記事の翻訳です。

[Dependency Injection in MVVM Architecture with ReactiveCocoa Part 4: Implementing the View and ViewModel](https://yoichitgy.github.io/post/dependency-injection-in-mvvm-architecture-with-reactivecocoa-part-4-implementing-the-view-and-viewmodel/)

* * *

[前回のブログ記事](http://qiita.com/yoichitgy/items/26d4e746516645df7a75)では、例題アプリのモデル部分を設計して実装しました。今回の記事では、ViewとViewModelのパートへと移ります。最初に、ViewとViewModelの空の実装をプロジェクトに追加し、アプリを実行できるようにします。その後、ユニットテストとともに実際の実装を追加します。その開発の中で、[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa/)のオブザーバブルなプロパティである`AnyProperty`型と`MutableProperty`型の使い方を学べます。

ソースコードはGitHubのリポジトリからダウンロードできます。

- [SwinjectMVVMExample](https://github.com/Swinject/SwinjectMVVMExample): 発展形を含むプロジェクト
- [SwinjectMVVMExample_ForBlog](https://github.com/yoichitgy/SwinjectMVVMExample_ForBlog): (Xcodeや外部フレームワークの更新を除き) ブログ記事に沿った説明のための簡略化したプロジェクト

## ViewとViewModelの設計の概要

ViewとViewModelを疎結合にするため、そのインターフェイスを下の図のようにプロトコルで定義します。`ImageSearchTableViewModeling`と`ImageSearchTableViewCellModeling`はプロトコルで、`ImageSearchTableViewModel`と`ImageSearchTableViewCellModel`はそれらのプロトコルに適合した実装です。`ImageSearchTableViewModel`はイベントにラップされた画像のエンティティをModel層から受け取り、さらに`ImageSearchTableViewController`へとイベントで伝えるためにそのエンティティを`ImageSearchTableViewCellModel`に変換します。

![View and View Model Design](https://qiita-image-store.s3.amazonaws.com/0/86677/91618c60-b7b2-13ad-58e4-7102bd51ae30.png "SwinjectMVVMExampleViewAndViewModelDesign.png")

## ViewとViewModelの空の実装

最初に、[Swinject](https://github.com/Swinject/Swinject)によるDependency Injectionを利用したViewとViewModelの空の実装を追加します。[Agileプラクティス](https://en.wikipedia.org/wiki/Agile_software_development)で言われるように、まず中身はないが動くソフトウェアを作り、ひとつずつ機能を追加していきます。

### 空のViewを追加

始めに、不要なファイルと設定を削除します。`SwinjectMVVMExample`グループの`ViewController.swift`と`Main.storyboard`を削除してください。`SwinjectMVVMExample`グループの`Info.plist`を開き、`Main storyboard file base name`を削除してください。(生のキー/値を表示していれば`UIMainStoryboardFile`と表示されているかもしれません。)

次に、`ExampleView`グループに以下の内容の`ImageSearchTableViewController.swift`を追加してください。その際、`ExampleView`ターゲットにファイルを追加することに注意してください。そのためには、`ExampleView`グループを右クリックして`New File...`を選択し、`iOS > Source > Swift File`と順に選択し、ファイル名を入力する画面で`ExampleView`ターゲットにチェックを入れておきます。

**ImageSearchTableViewController.swift**

    import UIKit

    public final class ImageSearchTableViewController: UITableViewController {
    }

同様に、`ExampleView`グループに以下の内容の`ImageSearchTableViewCell.swift`を追加してください。

**ImageSearchTableViewCell.swift**

    import UIKit

    internal final class ImageSearchTableViewCell: UITableViewCell {
    }

ここで、`ImageSearchTableViewController`は`public`なのに対し、`ImageSearchTableViewCell`は`internal`であることに注意してください。そのセルにアクセスするのはテーブルビューコントローラのみで、`ExampleView`フレームワークを使用する側には見せる必要がないためです。

`ExampleView`グループに`Main.storyboard`という名前で新しいストーリーボードを追加してください。そのストーリーボードを開き、Object Libraryからナビゲーションコントローラを追加します。そのナビゲーションコントローラを選択し、Attribute Inspectorの"Is Initial View Controller"にチェックを入れます。

ナビゲーションコントローラのルートビューコントローラとなっているテーブルビューコントローラを選択し、カスタムクラスとストーリーボードIDを`ImageSearchTableViewController`にします。そのテーブルビューコントローラのプロトタイプセルを選択し、カスタムクラスとセル識別子を`ImageSearchTableViewCell`にします。テーブルビューコントローラのナビゲーションアイテムを選択し、そのタイトルを"Pixabay Images"にします。

![SwinjectMVVMExampleEmptyViewProject](https://qiita-image-store.s3.amazonaws.com/0/86677/7ede3543-89d9-beb0-2af1-21f61dde9d56.png "SwinjectMVVMExampleEmptyViewProject.png")

![SwinjectMVVMExampleEmptyViewStoryboard](https://qiita-image-store.s3.amazonaws.com/0/86677/ab2d1d8a-db70-3c7a-3da6-af321c741f5e.png "SwinjectMVVMExampleEmptyViewStoryboard.png")

`SwinjectMVVMExample`グループの`AppDelegate.swift`を修正し、手動でストーリーボードからイニシャルビューコントローラをインスタンス化するようにします。後でDependency Injectionを追加するため、`UIStoryboard`の代わりに`SwinjectStoryboard`を使用します。`SwinjectStoryboard`のインスタンス化はイニシャライザではなく`create`関数で行います[^1]。`SwinjectStoryboard`に与えるバンドルはメインバンドルではなく、`NSBundle.init(forClass:)`で取得した`ExampleView`ターゲットのバンドルにします。

**AppDelegate.swift**

    import UIKit
    import Swinject
    import ExampleView

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?

        func application(
            application: UIApplication
            didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
        {
            let window = UIWindow(frame: UIScreen.mainScreen().bounds)
            window.backgroundColor = UIColor.whiteColor()
            window.makeKeyAndVisible()
            self.window = window

            let bundle = NSBundle(forClass: ImageSearchTableViewController.self)
            let storyboard = SwinjectStoryboard.create(name: "Main", bundle: bundle)
            window.rootViewController = storyboard.instantiateInitialViewController()

            return true
        }

        // 以下略...
    }

これでアプリを実行する準備ができました。`Command-R`を押して実行してみましょう。下の画像のような空のテーブルビューが表示されると思います。

![SwinjectMVVMExampleEmptyViewScreenshot.png](https://qiita-image-store.s3.amazonaws.com/0/86677/f878bc85-ee5e-ef79-55d8-43a273749863.png "SwinjectMVVMExampleEmptyViewScreenshot.png")

### 空のViewModelを追加

`ExampleViewModel`グループに以下の内容の`ImageSearchTableViewModeling.swift`と`ImageSearchTableViewModel.swift`を追加してください。ファイルを保存するときに`ExampleViewModel`ターゲットに追加するよう注意してください。`ImageSearchTableViewModel`が依存している`ImageSearching`はイニシャライザを通して注入します。これは[イニシャライザインジェクションパターン](https://github.com/Swinject/Swinject/blob/master/Documentation/InjectionPatterns.md)と呼ばれるものです。

**ImageSearchTableViewModeling.swift**

    public protocol ImageSearchTableViewModeling {
    }

**ImageSearchTableViewModel.swift**

    import ExampleModel

    public final class ImageSearchTableViewModel: ImageSearchTableViewModeling {
        private let imageSearch: ImageSearching

        public init(imageSearch: ImageSearching) {
            self.imageSearch = imageSearch
        }
    }

`ExampleViewModel`グループに以下の内容の`ImageSearchTableViewCellModeling.swift`と`ImageSearchTableViewCellModel.swift`を追加してください。

**ImageSearchTableViewCellModeling.swift**

    public protocol ImageSearchTableViewCellModeling {
    }

**ImageSearchTableViewCellModel.swift**

    public final class ImageSearchTableViewCellModel: ImageSearchTableViewCellModeling {
    }

`ImageSearchTableViewController`と`ImageSearchTableViewCell`が持つViewModelへの依存性を[プロパティインジェクションパターン](https://github.com/Swinject/Swinject/blob/master/Documentation/InjectionPatterns.md)の形で定義します。

**ImageSearchTableViewController.swift**

    import UIKit
    import ExampleViewModel

    public final class ImageSearchTableViewController: UITableViewController {
        public var viewModel: ImageSearchTableViewModeling?
    }

**ImageSearchTableViewCell.swift**

    import UIKit
    import ExampleViewModel

    internal final class ImageSearchTableViewCell: UITableViewCell {
        internal var viewModel: ImageSearchTableViewCellModeling?
    }

### Dependency Injectionを適用

`AppDelegate`でDependency Injectionを行うため、依存関係を登録する`container`プロパティを以下のように追加します。ここでは、`Container`のコンビニエンスイニシャライザを使用し、クロージャの中で`container`インスタンスの設定をしています。`container`プロパティは`SwinjectStoryboard`をインスタンス化するときに渡します。

**AppDelegate.swift**

    import UIKit
    import Swinject
    import ExampleModel
    import ExampleViewModel
    import ExampleView

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?
        let container = Container() { container in
            // Models
            container.register(Networking.self) { _ in Network() }
            container.register(ImageSearching.self) { r in
                ImageSearch(network: r.resolve(Networking.self)!)
            }

            // View models
            container.register(ImageSearchTableViewModeling.self) { r
                in ImageSearchTableViewModel(imageSearch: r.resolve(ImageSearching.self)!)
            }

            // Views
            container.registerForStoryboard(ImageSearchTableViewController.self) {
                r, c in
                c.viewModel = r.resolve(ImageSearchTableViewModeling.self)!
            }
        }

        func application(
            application: UIApplication,
            didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
        {
            let window = UIWindow(frame: UIScreen.mainScreen().bounds)
            window.backgroundColor = UIColor.whiteColor()
            window.makeKeyAndVisible()
            self.window = window

            let bundle = NSBundle(forClass: ImageSearchTableViewController.self)
            let storyboard = SwinjectStoryboard.create(
                name: "Main",
                bundle: bundle,
                container: container)
            window.rootViewController = storyboard.instantiateInitialViewController()

            return true
        }
        // 以下略...
    }

必要な型がすべて`container`に登録してあることを確認するため、ユニットテストを追加しましょう。テストを追加する前に、`SwinjectMVVMExampleTests`グループの`SwinjectMVVMExampleTests.swift`は不要なので削除しておきます。その後、同グループに以下の内容で`AppDelegateSpec.swift`を追加してください。そのユニットテストでは、登録してある型を`container`から取得できることを`.notTo(beNil())`を使って確認しています。

**AppDelegateSpec.swift**

    import Quick
    import Nimble
    import Swinject
    import ExampleModel
    import ExampleViewModel
    import ExampleView
    @testable import SwinjectMVVMExample

    class AppDelegateSpec: QuickSpec {
        override func spec() {
            var container: Container!
            beforeEach {
                container = AppDelegate().container
            }

            describe("Container") {
                it("resolves every service type.") {
                    // Models
                    expect(container.resolve(Networking.self)).notTo(beNil())
                    expect(container.resolve(ImageSearching.self)).notTo(beNil())

                    // ViewModels
                    expect(container.resolve(ImageSearchTableViewModeling.self))
                        .notTo(beNil())
                }
                it("injects view models to views.") {
                    let bundle = NSBundle(forClass: ImageSearchTableViewController.self)
                    let storyboard = SwinjectStoryboard.create(
                        name: "Main",
                        bundle: bundle,
                        container: container)
                    let imageSearchTableViewController = storyboard
                        .instantiateViewControllerWithIdentifier("ImageSearchTableViewController")
                        as! ImageSearchTableViewController

                    expect(imageSearchTableViewController.viewModel).notTo(beNil())
                }
            }
        }
    }

`Command-U`を入力してユニットテストを実行してみましょう。パスしましたね。依存性が注入された状態で、ViewとViewModelの空実装ができました。

## ViewとViewModelの実際の実装

このセクションでは、テーブルビューに画像のメタデータ (タグやピクセルサイズ) を表示するための実際の実装をViewとViewModelに追加していきます。テーブルビューセルに`UIImageView`を追加しますが、このブログ記事ではメタデータを表示するためのラベルだけ実装することとします。イメージビューは次のブログ記事で使用します。

### ViewModelの実装

最初に、テーブルのデータソースを実装しましょう。MVVMアーキテクチャでは、データソースはViewModelで実装します。`cellModels`プロパティと`startSearch`メソッドを`ImageSearchTableViewModeling`プロトコルに追加してください。

**ImageSearchTableViewModeling.swift**

    import ReactiveCocoa

    public protocol ImageSearchTableViewModeling {
        var cellModels: AnyProperty<[ImageSearchTableViewCellModeling]> { get }
        func startSearch()
    }

`cellModels`プロパティは[`AnyProperty`型](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/ReactiveCocoa/Swift/Property.swift)として定義し、オブザーブできるようにします。`AnyProperty`型が持つ`SignalProducer`型の`producer`プロパティにオブザーバを追加することができます。

`ImageSearchTableViewModel`を修正し、プロトコルに追加したプロパティとメソッドを実装しましょう。

**ImageSearchTableViewModel.swift**

```swift
    import ReactiveCocoa
    import ExampleModel

    public final class ImageSearchTableViewModel: ImageSearchTableViewModeling {
        public var cellModels: AnyProperty<[ImageSearchTableViewCellModeling]> {
            return AnyProperty(_cellModels)
        }
        private let _cellModels = MutableProperty<[ImageSearchTableViewCellModeling]>([])
        private let imageSearch: ImageSearching

        public init(imageSearch: ImageSearching) {
            self.imageSearch = imageSearch
        }

        public func startSearch() {
            imageSearch.searchImages()
                .map { response in
                    response.images.map {
                        ImageSearchTableViewCellModel(image: $0)
                            as ImageSearchTableViewCellModeling
                    }
                }
                .observeOn(UIScheduler())
                .on(next: { cellModels in
                    self._cellModels.value = cellModels
                })
                .start()
        }
    }
```

`cellModels`プロパティは[`MutableProperty`型](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/FrameworkOverview.md#properties)の`_cellModels`プロパティをラップしています。これは、`MutableProperty`型の`_cellModels`プロパティが外部から変更されることを防ぐためであり、`AnyProperty`でラップすることにより読み取り専用のオブザーバブルなプロパティを実現できます。

`startSearch`メソッドにより、`imageSearch.searchImages`で返される`SignalProducer`を開始します。`next`イベントの副作用の中で、`imageSearch`が返す`response`からマッピングした`ImageSearchTableViewCellModeling`の配列を`_cellModels`の値にセットします。その副作用は`.observeOn(UIScheduler())`によりメインスレッドで実行されていることに注意してください。ViewModelからViewへのイベントがメインスレッドで実行されることをViewModelで保証すべきだからです。

ReactiveCocoaにはメインスレッドで実行するためのスケジューラが2種類あります。ひとつは`QueueScheduler.mainQueueScheduler`で、メインスレッドでイベントが実行されるよう常に非同期なスケジューリングが行われます。もうひとつは今回使用した`UIScheduler`で、すでにメインスレッドの中にいればそのまま同期で実行し、そうでなければ非同期でメインスレッドにスケジューリングします。

次に、`id`、`pageImageSizeText`、`tagText`プロパティを`ImageSearchTableViewCellModeling`プロトコルに追加してください。`id`はデバッグ用に追加します。

**ImageSearchTableViewCellModeling.swift**

    public protocol ImageSearchTableViewCellModeling {
        var id: UInt64 { get }
        var pageImageSizeText: String { get }
        var tagText: String { get }
    }

さらに、`ImageSearchTableViewCellModel`を修正して上記プロトコルを実装し、プロパティが`init`の中で初期化されるようにします (`init`は`ImageSearchTableViewModel`が呼びます)。`ImageSearchTableViewCellModel`が実装するのはビューのロジックで、ModelのデータがどのようにViewに表示されるか定義します。

**ImageSearchTableViewCellModel.swift**

    import ExampleModel

    public final class ImageSearchTableViewCellModel: ImageSearchTableViewCellModeling {
        public let id: UInt64
        public let pageImageSizeText: String
        public let tagText: String

        internal init(image: ImageEntity) {
            id = image.id
            pageImageSizeText = "\(image.pageImageWidth) x \(image.pageImageHeight)"
            tagText = image.tags.joinWithSeparator(", ")
        }
    }

### ViewModelの実装のユニットテスト

ViewModelで実装したことを確認するためのユニットテストを追加しましょう。最初に、`ExampleViewModelTests.swift `が不要なので`ExampleViewModelTests`グループから削除しましょう。次に、そのグループに以下の内容の`DummyResponse.swift`と`ImageSearchTableViewModelSpec.swift`を追加してください。

**DummyResponse.swift**

    @testable import ExampleModel
    @testable import ExampleViewModel

    let dummyResponse: ResponseEntity = {
        let image0 = ImageEntity(
            id: 10000,
            pageURL: "https://somewhere.com/page0/",
            pageImageWidth: 1000,
            pageImageHeight: 2000,
            previewURL: "https://somewhere.com/preview0.jpg",
            previewWidth: 250,
            previewHeight: 500,
            imageURL: "https://somewhere.com/image0.jpg",
            imageWidth: 100,
            imageHeight: 200,
            viewCount: 99,
            downloadCount: 98,
            likeCount: 97,
            tags: ["a", "b"],
            username: "User0")
        let image1 = ImageEntity(
            id: 10001,
            pageURL: "https://somewhere.com/page1/",
            pageImageWidth: 1500,
            pageImageHeight: 3000,
            previewURL: "https://somewhere.com/preview1.jpg",
            previewWidth: 350,
            previewHeight: 700,
            imageURL: "https://somewhere.com/image1.jpg",
            imageWidth: 150,
            imageHeight: 300,
            viewCount: 123456789,
            downloadCount: 12345678,
            likeCount: 1234567,
            tags: ["x", "y"],
            username: "User1")
        return ResponseEntity(totalCount: 123, images: [image0, image1])
    }()

**ImageSearchTableViewModelSpec.swift**

    import Quick
    import Nimble
    import ReactiveCocoa
    @testable import ExampleModel
    @testable import ExampleViewModel

    class ImageSearchTableViewModelSpec: QuickSpec {
        // MARK: Stub
        class StubImageSearch: ImageSearching {
            func searchImages() -> SignalProducer<ResponseEntity, NetworkError> {
                return SignalProducer { observer, disposable in
                    observer.sendNext(dummyResponse)
                    observer.sendCompleted()
                }
                .observeOn(QueueScheduler())
            }
        }

        // MARK: Spec
        override func spec() {
            var viewModel: ImageSearchTableViewModel!
            beforeEach {
                viewModel = ImageSearchTableViewModel(imageSearch: StubImageSearch())
            }

            it("eventually sets cellModels property after the search.") {
                var cellModels: [ImageSearchTableViewCellModeling]? = nil
                viewModel.cellModels.producer
                    .on(next: { cellModels = $0 })
                    .start()
                viewModel.startSearch()

                expect(cellModels).toEventuallyNot(beNil())
                expect(cellModels?.count).toEventually(equal(2))
                expect(cellModels?[0].id).toEventually(equal(10000))
                expect(cellModels?[1].id).toEventually(equal(10001))
            }
            it("sets cellModels property on the main thread.") {
                var onMainThread = false
                viewModel.cellModels.producer
                    .on(next: { _ in onMainThread = NSThread.isMainThread() })
                    .start()
                viewModel.startSearch()

                expect(onMainThread).toEventually(beTrue())
            }
        }
    }

`ImageSearchTableViewModelSpec`で最初に定義しているスタブは、`dummyResponse`をイベントとして送ります。1つ目のテストでは、`dummyResponse`から変換された配列が`cellModels`プロパティに非同期にセットされることを確認しています。2つ目のテストでは、イベントがメインスレッドで送られてくることを確認しています。

次に、`ExampleViewModelTests`グループに`ImageSearchTableViewCellSpec.swift`を追加してください。ここでは単純に、値が`id`プロパティにセットされること、また変換された値が`pageImageSizeText`プロパティと`tagText`プロパティにセットされることを確認しています。

**ImageSearchTableViewCellModelSpec.swift**

    import Quick
    import Nimble
    @testable import ExampleModel
    @testable import ExampleViewModel

    class ImageSearchTableViewCellModelSpec: QuickSpec {
        override func spec() {
            it("sets id.") {
                let viewModel = ImageSearchTableViewCellModel(image: dummyResponse.images[0])

                expect(viewModel.id).toEventually(equal(10000))
            }
            it("formats tag and page image size texts.") {
                let viewModel = ImageSearchTableViewCellModel(image: dummyResponse.images[0])

                expect(viewModel.pageImageSizeText).toEventually(equal("1000 x 2000"))
                expect(viewModel.tagText).toEventually(equal("a, b"))
            }
        }
    }

`Command-U`を入力してユニットテストを実行してください。パスしましたね。それでは次のセクションに移りましょう。

### Viewの実装

このセクションではViewを実装していきます。`Main.storyboard`を開き、プロトタイプセルに`UIImageView`を1つ、`UILabel`を2つ追加し、好きなようにレイアウトしてください。もしセルの高さを変更したければ、(`UITableViewController`ではなく) `UITableView`の`rowHeight`プロパティをストーリーボードのSize Inspectorで変更してください。

![SwinjectMVVMExampleStoryboardCellLayout](https://qiita-image-store.s3.amazonaws.com/0/86677/dfa95cc0-f64b-aaf8-eff0-53c7b49cfadd.png "SwinjectMVVMExampleStoryboardCellLayout.png")

それから、`ImageSearchTableViewCell`にアウトレットを追加し、ストーリーボードの各アイテムと接続してください。

**ImageSearchTableViewCell.swift**

    import UIKit
    import ExampleViewModel

    internal final class ImageSearchTableViewCell: UITableViewCell {
        internal var viewModel: ImageSearchTableViewCellModeling? {
            didSet {
                tagLabel.text = viewModel?.tagText
                imageSizeLabel.text = viewModel?.pageImageSizeText
            }
        }

        @IBOutlet weak var previewImageView: UIImageView!
        @IBOutlet weak var tagLabel: UILabel!
        @IBOutlet weak var imageSizeLabel: UILabel!
    }

インスタンスがセットされたタイミングでラベルのテキストを更新するため、`viewModel`プロパティに`didSet`オブザーバを定義していることに注意してください。画像については、次のブログ記事でイメージビューにセットする方法を説明します。

`ImageSearchTableViewController`に以下のように実装を追加します。

**ImageSearchTableViewController.swift**

    import UIKit
    import ExampleViewModel

    public final class ImageSearchTableViewController: UITableViewController {
        private var autoSearchStarted = false

        public var viewModel: ImageSearchTableViewModeling? {
            didSet {
                if let viewModel = viewModel {
                    viewModel.cellModels.producer
                        .on(next: { _ in self.tableView.reloadData() })
                        .start()
                }
            }
        }

        public override func viewWillAppear(animated: Bool) {
            super.viewWillAppear(animated)

            if !autoSearchStarted {
                autoSearchStarted = true
                viewModel?.startSearch()
            }
        }
    }

    // MARK: UITableViewDataSource
    extension ImageSearchTableViewController {
        public override func tableView(
            tableView: UITableView,
            numberOfRowsInSection section: Int) -> Int
        {
            if let viewModel = viewModel {
                return viewModel.cellModels.value.count
            }
            return 0
        }

        public override func tableView(
            tableView: UITableView,
            cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
        {
            let cell = tableView.dequeueReusableCellWithIdentifier(
                "ImageSearchTableViewCell",
                forIndexPath: indexPath) as! ImageSearchTableViewCell
            if let viewModel = viewModel {
                cell.viewModel = viewModel.cellModels.value[indexPath.row]
            }
            else {
                cell.viewModel = nil
            }

            return cell
        }
    }

最初に、ここでも`didSet`が`viewModel`プロパティに定義してあり、`viewModel.cellModels`プロパティにオブザーバを追加する処理をしています。そのオブザーバにより、`viewModel.cellModels`が更新された時にテーブルビューが再読み込みされるようにしてあります。

次に、`viewWillAppear`をオーバーライドし、ビューが初めて表示された時だけ`startSearch`を実行するようにしています。`viewWillAppear`はメインスレッドでのみ呼ばれるので、ロックなしで`autoSearchStarted`フラグを使用しています。

最後に、単にViewModelを使用するように`UITableViewDataSource`プロトコルを実装しています。

アプリを実行する準備ができました。`Command-R`を入力し、以下の画像のようにアプリが表示されるか確認しましょう。イメージビューはまだ実装していませんが、ラベルは画像のメタデータについて表示しています。

![SwinjectMVVMExampleFilledLabelsScreenshot](https://qiita-image-store.s3.amazonaws.com/0/86677/16279961-673b-c6e2-98e1-b6f8ace9c4c2.png "SwinjectMVVMExampleFilledLabelsScreenshot.png")

### Unit Tests for View Implementation

ViewModelの`startSearch`がビューの表示時に1回だけ呼ばれることを確認するため、`ExampleViewTests`グループに以下のユニットテストを追加しましょう。モックを使用して何回メソッドが呼ばれたかカウントしています。

**ImageSearchTableViewControllerSpec.swift**

    import Quick
    import Nimble
    import ReactiveCocoa
    import ExampleViewModel
    @testable import ExampleView

    class ImageSearchTableViewControllerSpec: QuickSpec {
        // MARK: Mock
        class MockViewModel: ImageSearchTableViewModeling {
            let cellModels = AnyProperty(
                MutableProperty<[ImageSearchTableViewCellModeling]>([]))
            var startSearchCallCount = 0

            func startSearch() {
                startSearchCallCount++
            }
        }

        // MARK: Spec
        override func spec() {
            it("starts searching images when the view is about to appear at the first time.") {
                let viewModel = MockViewModel()
                let storyboard = UIStoryboard(
                    name: "Main",
                    bundle: NSBundle(forClass: ImageSearchTableViewController.self))
                let viewController = storyboard.instantiateViewControllerWithIdentifier(
                    "ImageSearchTableViewController")
                    as! ImageSearchTableViewController
                viewController.viewModel = viewModel

                expect(viewModel.startSearchCallCount) == 0
                viewController.viewWillAppear(true)
                expect(viewModel.startSearchCallCount) == 1
                viewController.viewWillAppear(true)
                expect(viewModel.startSearchCallCount) == 1
            }
        }
    }

`Command-U`を押してテストを実行しましょう。パスしましたね！

## まとめ

例題アプリのViewとViewModel部分を実装しました。最初に空の実装のViewとViewModelをプロジェクトに追加し、Agileプラクティスが言うように動くソフトウェアを作るところから始めました。その後、実際の実装を追加しました。Model、View、ViewModelの依存性をアプリケーションから注入する方法について見ました。`container`プロパティを`AppDelegate`に追加することにより、Dependency Injectionに漏れなどがないかテストすることができました。ReactiveCocoaがサポートするプロパティの型について学びました。`MutableProperty`は値が変更可能でオブザーバブルなプロパティでした。`AnyProperty`は`MutableProperty`の読み取り専用のビューとして使いました。[次回のブログ記事](http://qiita.com/yoichitgy/items/36154ddffae3e911b0bd)では、非同期で画像をロードする機能を実装します。

もし質問、提案、問題などがあれば気軽にコメントをどうぞ。

[^1]: `UIStoryboard`が普通の指定イニシャライザを持たず、子クラスでイニシャライザをオーバーライドできないため、`SwinjectStoryboard`のインスタンス化は少し癖があります。この問題のワークアラウンドとして、イニシャライザでなく`create`関数を使って`SwinjectStoryboard`のインスタンスを生成します。