[Swift] Swinjectを使ったDependency Injection

2017.03.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは。
最近生後7ヶ月の娘を見た人から「眉毛が垂れててお父さんそっくりですね〜」と言われるのが定番になっている加藤です。

さて、今回はSwiftのDIフレームワークであるSwinjectの話です。

DIとは

DIはDependency Injectionの略で、日本語では「依存性の注入」と訳されます。
「DIとは何か」を説明した記事は山ほどありますが、ここでは一番端的に表現している記事を見つけたので引用します。

“依存性注入とはインスタンス変数にオブジェクトを与えるということです。本当にただそれだけです。” - James Shore

Swiftにおける現実的なモックより引用。

つまり、あるオブジェクトが依存しているオブジェクトを外から渡してあげることを意味します。
依存しているオブジェクトを中で作らずに外から渡してあげることで、オブジェクト同士を柔軟に組み合わせることが可能になったり、テストしやすくなったりと様々なメリットがあります。

Swinjectとは

SwinjectはSwiftの軽量なDIフレームワークです。MITライセンスで公開されています。
使用するために必要な条件は以下の通りです。

  • iOS 8.0+ / Mac OS X 10.10+ / watchOS 2.0+ / tvOS 9.0+
  • Swift 2.2 or 2.3
    • Xcode 7.0+
  • Swift 3
    • Xcode 8.0+
  • Carthage 0.18+ (if you use)
  • CocoaPods 1.1.1+ (if you use)

検証環境

本記事の検証環境は以下の通りです。

  • Xcode Version 8.2.1(8C1002)
  • Swift Version 3.0.2
  • CocoaPods Version 1.2.0

Swinjectを導入する

Carthage もしくは CocoaPodsでインストール可能です。
今回はCocoaPodsでインストールしました。

Podfileに以下のように記載し、pod installを実行すればOKです。

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'

target 'ターゲット名' do
  use_frameworks!
  pod 'Swinject', '~> 2.0.0'
  pod 'SwinjectStoryboard', '~> 1.0.0'
end

Swinjectと一緒にSwinjectStoryboardもインストールしていますが、こちらはStoryboardでインスタンス化されたViewControllerに自動的にDIを行うためのSwinjectの拡張です。必須ではありませんが、Storyboardを使って画面を作るなら入れた方が良いかと思います。

Swinjectはバージョン2.0.0、SwinjectStoryboardはバージョン1.0.0がインストールされました。

Swinjectの基本的な使い方

READMEから基本的な使い方を見ていきます。

依存関係を持つモデルを定義する

以下のようにAnimalプロトコル及び当該プロトコルに準拠したCatクラスを定義します。

protocol Animal {
    var name: String? { get }
}

class Cat: Animal {
    let name: String?

    init(name: String?) {
        self.name = name
    }
}

さらに、Personプロトコル及び当該プロトコルに準拠したPetOwnerクラスを定義します。

protocol Person {
    func play()
}

class PetOwner: Person {
    let pet: Animal

    init(pet: Animal) {
        self.pet = pet
    }

    func play() {
        let name = pet.name ?? "someone"
        print("I'm playing with \(name).")
    }
}

PetOwnerはコンストラクタでAnimalを受け取り、保持しています。 つまり、PetOwnerAnimalに対して依存していますが、Animalは具体的な実装を持つclassやstructではなく、プロトコルになっています。ここがポイントです。 このようにDIではプロトコル(抽象)に依存するような作りにします。

Swinjectを使ってDIする

モデルが定義できたので、DIしてみます。
Swinjectを使ったDIは基本的に以下の手順となります。

  1. Container(一般的にDIコンテナと言われる)を作成する。
  2. Containerに、Service(何かしらに依存しているオブジェクトの型)と、依存性を注入したServiceを生成する処理を登録する。
  3. ContainerからServiceを取り出して使用する。
import Swinject

// 1. Container(一般的にDIコンテナと言われる)を作成する。
let container = Container()

// 2. Containerに、Service(何かしらに依存しているオブジェクトの型)と、依存性を注入したServiceを生成する処理を登録する。
container.register(Animal.self) { _ in Cat(name: "Tama") }
container.register(Person.self) { r in
    PetOwner(pet: r.resolve(Animal.self)!)
}

// 3. ContainerからServiceを取り出して使用する。
let person = container.resolve(Person.self)!
person.play() // I'm playing with Tama.

resolveの結果はOptional型ですが、ContainerにPerson.selfを登録していて取得できることは明らかであるため、強制アンラップを行なっています。 結果からContainerに登録した依存性が解決されていることがわかります。

Service登録処理について

Serviceは使われる前に事前にContainerに登録しておく必要があります。 登録方法はSwinjectStoryboardを使う場合と使わない場合で異なります。
説明のために以下のようなUIViewControllerを定義し、アプリ起動時の初期画面として表示する場合を考えます。

class PersonViewController: UIViewController {
    var person: Person?
}

上記のUIViewControllerをContainerに登録する方法について、SwinjectStoryboardを使う場合と使わない場合それぞれ説明します。

SwinjectStoryboardを使う場合

前提として、Main.storyboardにアプリ起動時の初期画面としてPersonViewControllerが登録されているとします。
SwinjectStoryboardを使う場合は以下のようにSwinjectStoryboardのextensionでsetup()メソッドを定義し、メソッドの中でServiceをContainerに登録します。
SwinjectStoryboardにはデフォルトのコンテナ(defaultContainer)が用意されているため、それを使います。

import SwinjectStoryboard

extension SwinjectStoryboard {
    class func setup() {
        defaultContainer.register(Animal.self) { _ in Cat(name: "Tama") }
        defaultContainer.register(Person.self) { r in
            PetOwner(pet: r.resolve(Animal.self)!)
        }
        defaultContainer.storyboardInitCompleted(PersonViewController.self) { r, c in
            c.person = r.resolve(Person.self)
        }
    }
}

ContainerにPersonViewControllerを登録するだけで、SwinjectStoryboardがPersonViewControllerの解決を勝手にやってくれます!
便利ですね。

SwinjectStoryboardを使わない場合

Storyboardを使わずにアプリ起動時の初期画面としてPersonViewControllerを表示する場合を考えます。 SwinjectStoryboardを使わない場合は通常、AppDelegateでService登録処理を行います。

    let container = Container() { c in
        c.register(Animal.self) { _ in Cat(name: "Tama") }
        c.register(Person.self) { r in
            PetOwner(pet: r.resolve(Animal.self)!)
        }
        c.register(PersonViewController.self) { r in
            let controller = PersonViewController()
            controller.person = r.resolve(Person.self)
            return controller
        }
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        // UIWindowを生成
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.makeKeyAndVisible()
        self.window = window

        // コンテナからUIViewControllerを取得してrootViewControllerに設定
        window.rootViewController = container.resolve(PersonViewController.self)

        return true
    }

こちらは自分でContainerに登録したUIViewControllerを取得する必要があります。

おわりに

今回はSwinjectを使ってDIを行ってみました。
アプリの規模が大きくなるとモジュール間の依存関係は複雑になりがちです。
依存関係を一元管理し、変更容易性を高めるためにも使えるのではと思います。

参考記事