Dependency Injection in MVVM Architecture with ReactiveCocoa Part 2: プロジェクトの設定

以下のブログ記事の翻訳です[^訳注1]。

[Dependency Injection in MVVM Architecture with ReactiveCocoa Part 2: Project Setup](https://yoichitgy.github.io/post/dependency-injection-in-mvvm-architecture-with-reactivecocoa-part-2-project-setup/)

* * *

[前回のブログ記事](http://qiita.com/yoichitgy/items/db878c0a60258c9f1a1b)では、MVVM (Model-View-ViewModel) と[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)の基本的な概念を説明しました。今回の記事からは、実際にMVVMアーキテクチャのアプリを開発し、[Swinject](https://github.com/Swinject/Swinject)を用いたdependency injectionを行っていきます。MVVMの各コンポーネント間で受け渡されるイベントを扱うためにReactiveCocoaを利用します。このブログ記事では、Model、View、ViewModelをフレームワークとして構成するXcodeプロジェクトの設定方法を学びます。

例題のアプリは、以下のGIFアニメのように、[Pixabay](https://pixabay.com/)の[API](https://pixabay.com/api/docs/)を通して非同期で画像を検索・ダウンロード・表示します。ソースコードはGitHubのリポジトリからダウンロードできます。

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

![SwinjectMVVMExample ScreenRecord](https://yoichitgy.github.io/images/post/2015-08/SwinjectMVVMExampleScreenRecord.gif)

## 要求事項

- Swift 2.1
- Xcode 7.2
- [Carthage](https://github.com/Carthage/Carthage) 0.10.0以上
- [Pixabay](https://pixabay.com/api/docs/) API key

Carthageは[インストーラ (Carthage.pkg)](https://github.com/Carthage/Carthage/releases)でインストールできます。もしCarthageを使うのが初めてであれば[こちらのチュートリアル](http://www.raywenderlich.com/109330/carthage-tutorial-getting-started)を参考にしてください。

[Pixabay](https://pixabay.com/)のAPI keyは無料で入手できます。Pixabayでアカウントを作成してログインしてから、[APIドキュメントのページ](https://pixabay.com/api/docs/)にアクセスしてください。あなた固有のAPI keyが "Request parameters" のセクションに表示されます。

## プロジェクトの設定の概要

前回の記事で、MVVMアーキテクチャではViewがViewModelに依存し、ViewModelがModelに依存することを学びましたが、実際にModel、View、ViewModelを表すプロトコル、クラス、構造体を作るときにどのようにすれば依存性の方向を強制することができるでしょうか？もしすべての型がひとつのディレクトリの下、あるいはアプリケーションターゲットの下であっても、ひとつの場所に置かれるとすぐにカオスになってしまいます。

Javaや.NETでは、複数の[JAR](https://en.wikipedia.org/wiki/JAR_%28file_format%29)や[DLL](https://en.wikipedia.org/wiki/Dynamic-link_library)ファイルでアプリケーションを構成し、各コンポーネントの責任を明確にすることがよく行われます。iOS 8でダイナミックフレームワークが導入されたので、iOSアプリでも複数のダイナミックフレームワークから構成されるように設定するこることが簡単になりました。以下の図のように、Model、View、ViewModelのフレームワークからなるiOSアプリのアーキテクチャは、依存性の方向をViewからViewModelへ、ViewModelからModelへと強制することができ、アプリケーションがそれらの依存性を注入するようにできます。たとえば、ある型をViewModelフレームワークに定義したら、その型からModelフレームワークにある型を参照することはできますが、Viewフレームワークにある型を参照することはできません。

このアーキテクチャにより依存性の向きの一貫性を保つことができ、アプリの開発・テスト・メンテナンスが容易になります。

![Diagram-MVVMAppStructure.png](https://qiita-image-store.s3.amazonaws.com/0/86677/e40cd59c-4760-bad0-acbe-bf2f4aafa33b.png "Diagram-MVVMAppStructure.png")

## MVVMのフレームワークからなるプロジェクトの設定

それでは、MVVMフレームワークで構成されるXcodeプロジェクトを作成していきましょう。`File > New > Project...`メニューを選択し、`iOS > Application > Single View Application`の項目を選択してください。Product Nameを`SwinjectMVVMExample`にし、`.SwinjectMVVMExample`をBundle Identifierの末尾に追加してください。LanguageをSwiftにし、DevicesをiPhoneにします。`Include Unit Tests`だけチェックをつけてください[^1]。その後、どこかローカルな場所にプロジェクトを保存してください。

![SwinjectMVVMExampleScreenshotNewProject.png](https://qiita-image-store.s3.amazonaws.com/0/86677/95ce79c6-fdc9-ecbd-7590-a3defc938083.png "SwinjectMVVMExampleScreenshotNewProject.png")

次に、Model、View、ViewModelフレームワークを追加していきます。今回の例題アプリでは、それぞれ`ExampleModel`、`ExampleView`、`ExampleViewModel`という名前にします。もしあなた自身のアプリを作成する場合、`あなたのアプリ名` + `Model`、`View`、`ViewModel`と命名することがお薦めです。たとえば、`Foobook`という名前のアプリであれば、`FoobookModel`、`FoobookView`、`FoobookViewModel`という名前にします。

`File > New > Target...`メニューを選択し、`iOS > Framework & Library > Cocoa Touch Framework`を選択し、`Next`ボタンを押してください。次のページでProduct Nameを`ExampleModel`に、LanguageをSwiftに設定します。`Include Unit Test`はチェックされた状態にして`Finish`をクリックしてください。同様にして`ExampleViewModel`と`ExampleView`のフレームワークターゲットを追加してください。

![SwinjectMVVMExampleScreenshotNewTarget.png](https://qiita-image-store.s3.amazonaws.com/0/86677/65795065-729f-dd01-ed3c-3b30f3d96e1f.png "SwinjectMVVMExampleScreenshotNewTarget.png")

Project Navigator (Xcodeの左側にあるペイン) の`SwinjectMVVMExample`を右クリックし、`New Group`を選択してください。グループ名を`Tests`にします。Project Navigatorで`ExampleModel`、`ExampleViewModel`、`ExampleView`、`ExampleModelTests`、`ExampleViewModelTests`、`ExampleViewTests`、`SwinjectMVVMExampleTests`をドラッグして、下の画像のように整理してください。Project Navigatorで`SwinjectMVVMExample`を選択し、画像のようにターゲットの順番を並び替えてください。

![SwinjectMVVMExampleScreenshotProjectHierarchy.png](https://qiita-image-store.s3.amazonaws.com/0/86677/93ddfd60-19b7-9e3b-2d4e-6dccc93ef6ce.png "SwinjectMVVMExampleScreenshotProjectHierarchy.png")

ターゲットのDependencyとLinkの設定をしていきます。Project Navigatorで`SwinjectMVVMExample`を選択し、TARGETSから`ExampleViewModel`を選択してください。Build PhasesタブのTarget Dependenciesの下にある`+`ボタンをクリックし、`ExampleModel`を追加してください。Link Binary with Librariesの下にある`+`ボタンをクリックし、`ExampleModel.framework`を追加してください。同様にして、`ExampleViewModel`と`ExampleViewModel.framework`を`ExampleView`ターゲットの各所に追加します。`ExampleModel`と`SwinjectMVVMExample`のターゲット設定はそのままにします。`ExampleModel`は依存性がなく、`SwinjectMVVMExample`はデフォルトで設定が追加されるためです。

![SwinjectMVVMExampleScreenshotTargetSettingsExampleModel.png](https://qiita-image-store.s3.amazonaws.com/0/86677/58678eb0-b09f-b4f6-1b54-332e3676beb5.png "SwinjectMVVMExampleScreenshotTargetSettingsExampleModel.png")

![SwinjectMVVMExampleScreenshotTargetSettingsExampleViewModel.png](https://qiita-image-store.s3.amazonaws.com/0/86677/b5974e65-4405-d19b-5879-f3392aa4f830.png "SwinjectMVVMExampleScreenshotTargetSettingsExampleViewModel.png")

![SwinjectMVVMExampleScreenshotTargetSettingsExampleView.png](https://qiita-image-store.s3.amazonaws.com/0/86677/c86794b4-8532-6f91-be6b-13f8bc847ec0.png "SwinjectMVVMExampleScreenshotTargetSettingsExampleView.png")

![SwinjectMVVMExampleScreenshotTargetSettingsSwinjectMVVMExample.png](https://qiita-image-store.s3.amazonaws.com/0/86677/79bdd142-041d-3246-8a39-2996b81a3633.png "SwinjectMVVMExampleScreenshotTargetSettingsSwinjectMVVMExample.png")

ユニットテストのターゲット設定も追加していきます。Project Navigatorで`SwinjectMVVMExample`を選択し、TARGETSから`ExampleModelTests`を選択します。GeneralタブでHost Applicationを`None`に設定します。同様にして`ExampleViewModelTests`と`ExampleViewTests`のHost Applicationを`None`にします。

再度TARGETSから`ExampleModelTests`を選択してください。Build PhasesタブでTarget Dependenciesから`SwinjectMVVMExample`を削除してください。その項目の選択後に`-`ボタンをクリックすれば削除できます。同様にして、`ExampleViewModelTests`ターゲットと`ExampleViewTests`ターゲットから`SwinjectMVVMExample`のTarget Dependencyを削除します。

TARGETSから`ExampleViewModelTests`を選択してください。`Build Phases`タブでLink Binary with Librariesに`ExampleModel.framework`を追加してください。同様にして、`ExampleViewTests`の同じ場所に`ExampleViewModel.framework`を追加してください。リンク先のフレームワークに存在する型のスタブやモックを作成するため、これらのリンク設定が必要になります。

![SwinjectMVVMExampleScreenshotTargetSettingsExampleModelTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/db2b6517-c3cd-88d0-8576-cb1edee40c17.png "SwinjectMVVMExampleScreenshotTargetSettingsExampleModelTests.png")

![SwinjectMVVMExampleScreenshotTargetSettingsExampleViewModelTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/ad4960c0-6ec8-dd78-a179-f42069d7f0fc.png "SwinjectMVVMExampleScreenshotTargetSettingsExampleViewModelTests.png")

![SwinjectMVVMExampleScreenshotTargetSettingsExampleViewTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/b9335f52-a048-93b6-e02a-08f337f1ac7f.png "SwinjectMVVMExampleScreenshotTargetSettingsExampleViewTests.png")

![SwinjectMVVMExampleScreenshotTargetSettingsSwinjectMVVMExampleTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/c03de10b-f7c6-a480-72b3-0bdbb84b057e.png "SwinjectMVVMExampleScreenshotTargetSettingsSwinjectMVVMExampleTests.png")

ビルドスキームも設定しましょう。Xcodeのツールバーにあるスキームのボタンをクリックし、`Manage Schemes...`を選択します。もし`ExampleModel`、`ExampleViewModel`、`ExampleView`という名前のスキームがあれば、それらを選択して`-`ボタンをクリックし削除します。その後、`SwinjectMVVMExample`スキームを選択し、`Edit`ボタンをクリックします。BuildとTestの設定で、`ExampleModelTests`、`ExampleViewModelTests`、`ExampleViewTests`、`SwinjectMVVMExampleTests`がチェックされていることを確認してください。

![SwinjectMVVMExampleScreenshotBuildScheme.png](https://qiita-image-store.s3.amazonaws.com/0/86677/6c0ae757-37bf-cf60-c15d-be3528675547.png "SwinjectMVVMExampleScreenshotBuildScheme.png")

![SwinjectMVVMExampleScreenshotTestScheme.png](https://qiita-image-store.s3.amazonaws.com/0/86677/69f1fe59-7053-96b8-7b34-d8411b5b05e6.png "SwinjectMVVMExampleScreenshotTestScheme.png")

これでプロジェクト設定が完了しました。`Command-R`でアプリを実行し、その後`Command-U`でユニットテストを実行してください。プロジェクトが正しく設定されたか確認できます。もしUmbrella Headerに関するエラーが出たら、以下の画像のように、そのヘッダーファイルのTarget Membershipを修正し、アクセシビリティを`Public`に設定してください。XcodeはたまにUmbrella HeaderのTarget Membershipを間違えて設定してしまうことがあります。

![SwinjectMVVMExampleScreenshotUmbrellaHeader.png](https://qiita-image-store.s3.amazonaws.com/0/86677/b51f8491-64c6-f415-7706-e6e5e8eb97ab.png "SwinjectMVVMExampleScreenshotUmbrellaHeader.png")

## Carthageによる外部フレームワークのインストール

今回の例題アプリでは、[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa), [Himotoki](https://github.com/ikesyo/Himotoki), [Alamofire](https://github.com/Alamofire/Alamofire), [Swinject](https://github.com/Swinject/Swinject), [Quick](https://github.com/Quick/Quick), [Nimble](https://github.com/Quick/Nimble)をインストールします。Alamofire, Quick, Nimbleは[前回の例題アプリで](http://qiita.com/yoichitgy/items/75b09e6ff06199d7cf80)使用しましたね。今回は型安全なJSONデコードライブラリであるHimotokiを[SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)の代わりに使用します。Himotokiの詳細については、次回の記事で実際に使用するときに説明します。

上記のフレームワークを[Carthage](https://github.com/Carthage/Carthage)でインストールするため、以下の内容で`Cartfile`という名前のテキストファイルを作成してください。

**Cartfile**

    github "ReactiveCocoa/ReactiveCocoa" "v4.0.0-RC.1"
    github "ikesyo/Himotoki" ~> 1.3.0
    github "Alamofire/Alamofire" ~> 3.1.2
    github "Swinject/Swinject" ~> 1.0.0

    github "Quick/Quick" == 0.8.0
    github "Quick/Nimble" == 3.0.0

Terminalで`carthage update --no-use-binaries`を実行してください[^2]。Carthageがフレームワークのビルドを完了するまで数分 (あるいはしばらく) 待ちます[^3]。もし[Git](https://git-scm.com)を使用しているなら、Carthageがビルドしたフレームワークを除外するように設定したSwift用の`.gitignore`が[ここに](https://github.com/github/gitignore/blob/master/Swift.gitignore)あります。

ビルドが完了したらProject Navigatorの`SwinjectMVVMExample`を右クリックし、`New Group`を選択してください。新しいGroupを`Frameworks`という名前にします。Carthageがビルドしたフレームワークを、Finderの`Carthage/Build/iOS`ディレクトリからXcodeの`Frameworks`グループまでドラッグ&ドロップしてください。追加するターゲットを選択するシートが表示されたら、すべてのターゲットのチェックを外して`Finish`ボタンをクリックします。

![SwinjectMVVMExampleScreenshotUmbrellaHeaderFrameworksGroup.png](https://qiita-image-store.s3.amazonaws.com/0/86677/6155678b-b5ee-2658-dba4-2c8f2f56b86c.png "SwinjectMVVMExampleScreenshotUmbrellaHeaderFrameworksGroup.png")

Project Navigatorで`SwinjectMVVMExample`を選択し、TARGETSから`ExampleModel`を選択してBuild Phasesタブを開いてください。`Frameworks`グループにある`Alamofire.framework`, `Himotoki.framework`, `ReactiveCocoa.framework`, `Result.framework`[^4]をBuild PhasesタブにあるLink Binary with Librariesまでドラッグ&ドロップしてください。同様にして、`ExampleViewModel`と`ExampleView`の同じ場所に`ReactiveCocoa.framework`と`Result.framework`を追加してください。`SwinjectMVVMExample`の同じ場所には`Swinject.framework`を追加してください。`ExampleModelTests`, `ExampleViewModelTests`, `ExampleViewTests`の同じ場所に`ReactiveCocoa.framework`, `Result.framework`, `Quick.framework`, `Nimble.framework`を追加してください。`SwinjectMVVMExampleTests`の同じ場所には`Swinject.framework`, `Quick.framework`, `Nimble.framework`を追加してください。AlamofireとHimotokiはModelのみで使用します。Swinjectはアプリケーションとそのユニットテストのみで使用し、QuickとNimbleはユニットテストのみで使用します。

TARGETSで`ExampleModelTests`を選択し、Build Phasesタブの下の`+`ボタンをクリックし、`New Copy Files Phase`を選択してください。そのPhaseを`Copy Frameworks`にリネームします。そのPhaseの中で、Destinationを`Frameworks`に設定してください。Project Navigatorの`Frameworks`にある`Alamofire.framework`, `Himotoki.framework`, `ReactiveCocoa.framework`, `Result.framework`, `Quick.framework`, `Nimble.framework`を`Copy Frameworks` Phaseにドラッグ&ドロップして追加します。同様に`ExampleViewModelTests`と`ExampleViewTests`に`Copy Frameworks` Phaseを追加し、上記6つのフレームワークをそのPhaseにドラッグ&ドロップしてください。同様に`SwinjectMVVMExampleTests`に`Copy Frameworks` Phaseを追加し、そのPhaseに`Quick.framework`と`Nimble.framework`を登録してください。`SwinjectMVVMExampleTests`はアプリにホストされるので、アプリに入っていないQuickとNimbleだけ`Copy Frameworks` Phaseに追加しています。

![SwinjectMVVMExampleScreenshotLinkSettingsExampleModel.png](https://qiita-image-store.s3.amazonaws.com/0/86677/7dc31f01-dbd0-61ef-c8e3-a50fed00a0cf.png "SwinjectMVVMExampleScreenshotLinkSettingsExampleModel.png")

![SwinjectMVVMExampleScreenshotLinkSettingsExampleViewModel.png](https://qiita-image-store.s3.amazonaws.com/0/86677/0ee03015-ba19-f772-eaed-6cacf2d5fe18.png "SwinjectMVVMExampleScreenshotLinkSettingsExampleViewModel.png")

![SwinjectMVVMExampleScreenshotLinkSettingsExampleView.png](https://qiita-image-store.s3.amazonaws.com/0/86677/8f1dd63e-ac93-cd95-43d1-5fcde4d70d88.png "SwinjectMVVMExampleScreenshotLinkSettingsExampleView.png")

![SwinjectMVVMExampleScreenshotLinkSettingsSwinjectMVVMExample.png](https://qiita-image-store.s3.amazonaws.com/0/86677/dc117589-9a92-afe2-eebe-f7d1f3f4885b.png "SwinjectMVVMExampleScreenshotLinkSettingsSwinjectMVVMExample.png")

![SwinjectMVVMExampleScreenshotLinkSettingsExampleModelTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/b1aa296e-dc7a-1188-6814-f86b9815c206.png "SwinjectMVVMExampleScreenshotLinkSettingsExampleModelTests.png")

![SwinjectMVVMExampleScreenshotLinkSettingsExampleViewModelTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/f55e0eb9-702e-bafc-b8bf-f93bc15b6b16.png "SwinjectMVVMExampleScreenshotLinkSettingsExampleViewModelTests.png")

![SwinjectMVVMExampleScreenshotLinkSettingsExampleViewTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/29de7911-8cb5-4d82-5fb1-096d089db33e.png "SwinjectMVVMExampleScreenshotLinkSettingsExampleViewTests.png")

![SwinjectMVVMExampleScreenshotLinkSettingsSwinjectMVVMExampleTests.png](https://qiita-image-store.s3.amazonaws.com/0/86677/4144efc0-80ae-cd9f-6934-c4e36ace364c.png "SwinjectMVVMExampleScreenshotLinkSettingsSwinjectMVVMExampleTests.png")

最後の設定は[CarthageのREADME](https://github.com/Carthage/Carthage)に書かれているとおりです。Project Navigatorで`SwinjectMVVMExample`を選択し、TARGETSから`SwinjectMVVMExample`を選択してBuild Phasesタブを開いてください。そのタブの下の`+`ボタンをクリックし、`New Run Script Phase`を選択してください。そのPhaseを`Run Script for Frameworks by Carthage`にリネームし、スクリプトを書くテキストフィールドに以下の行を追加してください。

    /usr/local/bin/carthage copy-frameworks

その後、Input Filesの`+`ボタンを押して以下のパスを追加してください。

    $(SRCROOT)/Carthage/Build/iOS/Alamofire.framework
    $(SRCROOT)/Carthage/Build/iOS/Himotoki.framework
    $(SRCROOT)/Carthage/Build/iOS/ReactiveCocoa.framework
    $(SRCROOT)/Carthage/Build/iOS/Result.framework
    $(SRCROOT)/Carthage/Build/iOS/Swinject.framework

もう一度Build Phasesタブの下の`+`ボタンをクリックし、今度は`New Copy Files Phase`を選択してください。そのPhaseの名前は`Copy dSYMs`とします。そのDestinationを`Products Directory`に変更します。Finderの`Carthage/Build/iOS/`ディレクトリからXcodeの`Copy dSYMs` Phaseに`Alamofire.framework.dSYM`, `Himotoki.framework.dSYM`, `ReactiveCocoa.framework.dSYM`, `Result.framework.dSYM`, `Swinject.framework.dSYM`をドラッグ&ドロップしてください。Project Navigatorの`SwinjectMVVMExample`を右クリックし、`New Group`を選択してください。その名前を`dSYMs`とし、Project Navigatorに自動的に追加されたdSYMファイルを`dSYMs`グループに入れて整理してください。

![SwinjectMVVMExampleScreenshotCarthageScriptAnddSYMs.png](https://qiita-image-store.s3.amazonaws.com/0/86677/e3c6cfa1-c9a1-8424-6a14-c9959104cd67.png "SwinjectMVVMExampleScreenshotCarthageScriptAnddSYMs.png")

![SwinjectMVVMExampleScreenshotdSYMsGroup.png](https://qiita-image-store.s3.amazonaws.com/0/86677/07052a50-0611-5115-5a5e-5f8e52b325d7.png "SwinjectMVVMExampleScreenshotdSYMsGroup.png")

おめでとうございます！プロジェクトの設定が完了しました。試しに`Command-R`と`Command-U`でアプリとユニットテストを実行し、問題なく設定ができているか確認してください。もし何かエラーが出たら、[GitHub上にあるプロジェクト](https://github.com/Swinject/SwinjectMVVMExample)をダウンロードして、プロジェクトの設定を比較して原因を探してみましょう。

## まとめ

Model、View、ViewModelをそれぞれフレームワークとして構成したアプリのアーキテクチャにより、依存性の方向が一貫してViewからViewModel、ViewModelらかModelとなるように保証できることがわかりました。実際にMVVMアーキテクチャのXcodeプロジェクトを作成し、外部フレームワークをCarthageでインストールしました。[次回のブログ記事](http://qiita.com/yoichitgy/items/26d4e746516645df7a75)からは、MVVMアーキテクチャを活かし、ReactiveCocoaとSwinjectを用いて例題のアプリを開発していきます。

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

[^1]: 本記事の対象外のため、UI testsは除外しています。
[^2]: 古いベータ版のXcodeでビルドされたフレームワーク (zipファイル) をダウンロードしないよう、`--no-use-binaries`オプションを`carthage update `コマンドに与えています。使用しているXcodeのバージョンがバイナリを作成したXcodeのバージョンに適合する場合、オプションなしで`carthage update`を実行するだけで済みます。
[^3]: `carthage update --no-use-binaries`コマンドでエラーが発生したら`--verbose`オプションも付けて再度コマンドを実行し、問題を調査してみてください。
[^4]: `Result.framework`はReactiveCocoaが使用します。
[^訳注1]: 訳注: 英語版の著者本人による翻訳のため、翻訳に関わる著作権上の問題はありません。