Dependency Injection Framework for Swift - Swinjectを利用したシンプルな天気アプリの例 その2/2

以下のブログ記事の翻訳です[^訳注1]。
[Dependency Injection Framework for Swift - Simple Weather App Example with Swinject Part 2/2](http://yoichitgy.github.io/post/dependency-injection-framework-for-swift-simple-weather-app-example-with-swinject-part-2/)

---

[前回のブログ記事](http://qiita.com/yoichitgy/items/75b09e6ff06199d7cf80)では、シンプルな天気アプリのモデル部分を実装し、dependency injection (依存性の注入) と[Swinject](https://github.com/Swinject/Swinject)を利用して、密結合になった依存性を排除する方法について学びました。密結合だった部分を疎結合にすることにより、ユニットテストが書きやすくなることがわかりました。今回の記事ではアプリのUI部分を実装し、疎結合にしたコンポーネントをSwinjectでどのように接続していくか見ていきます。

このブログ記事で使用するソースコードは[GitHubのリポジトリ](https://github.com/Swinject/SwinjectSimpleExample)からダウンロードできます。

## UIの基本構成

最初に、テーブルビューで天気情報を表示するための基本的なUI構成を作っていきます。UIのコンポーネントはストーリーボードからインスタンス化しますが、ストーリーボード自体のインスタンス化は手で書いて、`UIStoryboard`を継承した`SwinjectStoryboard`を使えるようにします。

`Info.plist`を開いて`"Main storyboard file base name"`キーを削除してください。もし生のキーを表示している状態であれば、`"UIMainStoryboardFile"`と表示されているかもしれません。

`ViewController.swift`を削除して`WeatherTableViewController.swift`を追加してください。その中身は空の`WeatherTableViewController`の定義があるだけとします。後でdependency injectionパターンを用いてこのクラスを実装します。

**WeatherTableViewController.swift**

    import UIKit

    class WeatherTableViewController: UITableViewController {
    }

`Main.storyboard`を開いて、デフォルトで作成されているビューコントローラを削除してください。その後、オブジェクトライブラリから新しくナビゲーションコントローラをストーリーボードに追加してください。

そのナビゲーションコントローラを選択し、アトリビュートインスペクタで`"Is Initial View Controller"`をチェックしてください。

ナビゲーションコントローラのルートビューコントローラとなっているテーブルビューコントローラを選択し、そのカスタムクラスを`WeatherTableViewController`に設定してください。そのテーブルビューのプロトタイプセルを選択し、そのスタイルを`Right Detail`に変更し、Identifierを`"Cell"`に設定してください。テーブルビューコントローラのナビゲーションアイテムを選択し、そのタイトルを`"Weather Now"`に設定してください。

![SwinjectSimpleExample Storyboard Screenshot](https://qiita-image-store.s3.amazonaws.com/0/86677/c74ead45-5208-b473-632a-582c4785f693.png "SwinjectSimpleExampleStoryboardScreenshot.png")

`AppDelegate.swift`を修正し、イニシャルビューコントローラをストーリーボードから手動でインスタンス化するように書き換えます。ここで、`UIStoryboard`の代わりに`SwinjectStoryboard`を使用し、後でdependency injectionを追加できるようにしておきます。`SwinjectStoryboard`のインスタンス化にはイニシャライザを使わず、`create`関数を使用します[^1]。

**AppDelegate.swift**

    import UIKit
    import Swinject

    @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 storyboard = SwinjectStoryboard.create(name: "Main", bundle: nil)
            window.rootViewController = storyboard.instantiateInitialViewController()

            return true
        }

        ...
    }

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

![SwinjectSimpleExampleEmptyScreenshot.png](https://qiita-image-store.s3.amazonaws.com/0/86677/0f5d3d92-5dfb-f672-ac2d-a78e343a8e8d.png "SwinjectSimpleExampleEmptyScreenshot.png")

## ビューコントローラへのDependency Injection

それでは、空っぽだったテーブルビューコントローラを実装し、dependency injectionを追加しましょう。

`WeatherTableViewController`に`weatherFetcher`プロパティを追加します。

**WeatherTableViewController.swift**

    class WeatherTableViewController: UITableViewController {
        var weatherFetcher: WeatherFetcher?
    }

`AppDelegate`を修正し、設定を済ませた`Container`を`SwinjectStoryboard`に渡してインスタンス化するようにしましょう。

**AppDelegate.swift**

    @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 container = createContainer()
            let storyboard = SwinjectStoryboard.create(
                name: "Main",
                bundle: nil,
                container: container)
            window.rootViewController = storyboard.instantiateInitialViewController()

            return true
        }

        private func createContainer() -> Container {
            let container = Container()
            container.registerForStoryboard(WeatherTableViewController.self) { r, c in
                c.weatherFetcher = r.resolve(WeatherFetcher.self)
            }
            container.register(Networking.self) { _ in Network() }
            container.register(WeatherFetcher.self) { r in
                WeatherFetcher(networking: r.resolve(Networking.self)!)
            }
            return container
        }

        ...
    }

`createContainer`メソッドの中で、最初に`Container`のインスタンスを生成し、次にその設定をしています。ビューコントローラの依存関係の設定のため、`registerForStoryboard`を使用しています。ここでは、依存関係が解決された`WeatherFetcher`のインスタンスで`weatherFetcher`プロパティがセットされるように設定しています。この方法はプロパティ注入 (property injection) と呼ばれます。[前回のブログ記事](/post/dependency-injection-framework-for-swift-simple-weather-app-example-with-swinject-part-1/)で定義した`Networking`プロトコルは、Alamofireをカプセル化している`Network`のインスタンスになるように設定しています。`WeatherFetcher`は、`Networking`の該当するインスタンスをイニシャライズ時に受け取るように設定しています。これはイニシャライザ注入 (initializer injection) と呼ばれます。最後に、`createContainer`メソッドは設定を済ませた`container`を返しています。

`application:didFinishLaunchingWithOptions:`メソッドの中で、上記のように設定した`container`を`SwinjectStoryboard`のイニシャライザに渡しています。以上です。シンプルな使い方ですね。`Container`を設定して渡すだけでいいのです。

それでは次に、`WeatherTableViewController`の実装に取り掛かりましょう。

**WeatherTableViewController.swift**

    class WeatherTableViewController: UITableViewController {
        var weatherFetcher: WeatherFetcher?

        private var cities = [City]() {
            didSet {
                tableView.reloadData()
            }
        }

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

            weatherFetcher?.fetch {
                if let cities = $0 {
                    self.cities = cities
                }
                else {
                    // Show an error message.
                }
            }
        }

        // MARK: UITableViewDataSource
        override func tableView(
            tableView: UITableView,
            numberOfRowsInSection section: Int) -> Int
        {
            return cities.count
        }

        override func tableView(
            tableView: UITableView,
            cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
        {
            let cell = tableView.dequeueReusableCellWithIdentifier(
                "Cell", forIndexPath: indexPath)
            let city = cities[indexPath.row]
            cell.textLabel?.text = city.name
            cell.detailTextLabel?.text = city.weather
            return cell
        }
    }

最初に、`cities`プロパティを追加し、`City`の空配列で初期化しています。そのプロパティをセットした時にテーブルビューが更新されるようにしてあります。

次に、`viewWillAppear`をオーバーライドし、`fetch`メソッドで天気情報の取得を開始するようにしています[^2]。`fetch`は引数としてクロージャをとり、天気情報を取得できた時に`City`の配列を渡してそのクロージャを実行します。そのクロージャの中では、`self.cities`がセットされ、その結果テーブルビューが更新されます。`fetch`が失敗した場合には、`nil`をクロージャに渡します。このブログ記事ではエラー処理は省略してありますが、[GitHubのレポジトリ](https://github.com/Swinject/SwinjectSimpleExample)にあるソースコードではエラーメッセージを表示するように実装してありますのでご確認ください。

最後に、`tableView:numberOfRowsInSection:`を実装して行数を返し、`tableView:cellForRowAtIndexPath:`を実装してセルに都市名と天気をセットするようにしています。

これでUIの実装が完了しました。アプリを実行してみましょう。現在の天気情報がテーブルビューに表示されましたね。

![SwinjectSimpleExampleScreenshot.png](https://qiita-image-store.s3.amazonaws.com/0/86677/9a107f82-122f-5e6e-8e89-96d20495d608.png)

## ビューコントローラのテスト

ここまででアプリが動作することを見ましたが、`WeatherTableViewController`のユニットテストを追加させてください。ビューが表示されるタイミングでビューコントローラが天気情報を取得し始めるか確認します。このテストで[モック](https://ja.wikipedia.org/wiki/モックオブジェクト)のコンセプトが分かります。

`SwinjectSimpleExampleTests`に以下の内容の`WeatherTableViewControllerSpec.swift`を追加してください。

**WeatherTableViewControllerSpec.swift**

    import Quick
    import Nimble
    import Swinject
    @testable import SwinjectSimpleExample

    class WeatherTableViewControllerSpec: QuickSpec {
        class MockNetwork: Networking {
            var requestCount = 0

            func request(response: NSData? -> ()) {
                requestCount++
            }
        }

        override func spec() {
            var container: Container!
            beforeEach {
                container = Container()
                container.register(Networking.self) { _ in MockNetwork() }
                    .inObjectScope(.Container)
                container.register(WeatherFetcher.self) { r in
                    WeatherFetcher(networking: r.resolve(Networking.self)!)
                }
                container.register(WeatherTableViewController.self) { r in
                    let controller = WeatherTableViewController()
                    controller.weatherFetcher = r.resolve(WeatherFetcher.self)
                    return controller
                }
            }

            it("starts fetching weather information when the view is about appearing.") {
                let network = container.resolve(Networking.self) as! MockNetwork
                let controller = container.resolve(WeatherTableViewController.self)!

                expect(network.requestCount) == 0
                controller.viewWillAppear(true)
                expect(network.requestCount).toEventually(equal(1))
            }
        }
    }

最初に、`Networking`のモックとして`MockNetwork`を定義しています。`request`メソッドを実装していますが、レスポンスは返していません。その代わり、`requestCount`という名前のカウンタをインクリメントしています。モックは、あるインスタンスのメソッドやプロパティが正しく呼ばれているか確認するために使われます。スタブのようにダミーのデータを返すこともできますが、メソッドやプロパティの呼び出しをチェックできるところがスタブと異なります。

`spec`については、まず先に`it`の中を見てみましょう。`MockNetwork`と`WeatherTableViewController`のインスタンスを設定済みの`container`から取得しています。`Networking`は`MockNetwork`に結び付けられていることをプログラマの私達は知っているので、`container`から返ってきたインスタンスを`MockNetwork`にキャストしています。次に、ビューコントローラの`viewWillAppear`が呼ばれた後で、モックの`request`メソッドが1回だけ呼び出されることを`requestCount`カウンタによってチェックしています。`WeatherTableViewController`は`Networking`のインスタンスを直接保持しているわけではありませんが、モックのメソッド呼び出しを確認することにより、関連するインスタンスが正しく接続されていることが確認できます。

それでは、`container`の設定部分に戻りましょう。1番目に、`Networking`が`MockNetwork`に決定されるよう設定し、`container`の中でそのインスタンスが共有されるように設定しています。このようにオブジェクトスコープを設定することにより、カウンタをチェックするための`MockNetwork`インスタンスが、`WeatherTableViewController`によって間接的に保持されているインスタンスと同一になることを保証できます。2番目に、`WeatherFetcher`の依存性をイニシャライザ注入するように設定しています。3番目に、`WeatherTableViewController`の依存性をプロパティ注入するように設定しています。

それではユニットテストを実行してみましょう。パスしましたね。たとえばの話ですが、この天気アプリの開発を続け、機能をどんどん追加していくと考えてみてください。このユニットテストがあれば、UIとモデルの接続を誤って壊してしまう心配がなくなります。

## まとめ

シンプルな天気アプリのUI部分を実装し、dependency injectionのコンテナを使ってどのようにコンポーネントを結びつけていくか学びました。`SwinjectStoryboard`があると、ストーリーボードで定義されたビューコントローラに依存性を簡単に注入できることがわかりました。最後に、モックを利用して、依存関係の末端にあるインスタンスのメソッド呼び出しをチェックすることにより、意図したとおりに各コンポーネントが接続されていると確認できることがわかりました。

[次回の記事](http://qiita.com/yoichitgy/items/db878c0a60258c9f1a1b)では、[MVVM](https://ja.wikipedia.org/wiki/Model_View_ViewModel)アーキテクチャを用いてより大きな例題アプリを開発します。現在人気でエレガントなSwiftの[リアクティブプログラミング](http://qiita.com/hirokidaichi/items/9c1d862099c2e12f5b0f)フレームワークである[ReactiveCococa](https://github.com/ReactiveCocoa/ReactiveCocoa)を使用します。もちろん、dependency injectionとSwinjectを活用してMVVMの各コンポーネントを疎結合なまま結びつけていきます。

[^1]: `SwinjectStoryboard`のインスタンス化は少し癖があります。これは、`UIStoryboard`が通常の指定イニシャライザを持っておらず、子クラスがオーバーライドできないためです。
[^2]: この例題のアプリでは、`viewWillAppear`の中でしか`fetch`を実行していないですが、実際のアプリでは、天気情報を更新するためのボタンなどがあるといいでしょう。
[^訳注1]: 訳注: 英語版の著者本人による翻訳のため、翻訳に関わる著作権上の問題はありません。