【iOS/Xcode/Swift】SwinjectでDI(依存性注入)を実装する方法と使い方

【iOS/Xcode/Swift】SwinjectでDI(依存性注入)を実装する方法と使い方

この記事からわかること

  • iOS/XcodeDI(依存性注入)の実装方法
  • Swinject使い方

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

DI(Dependency Injection:依存性注入)とは?

DI(Dependency Injection)」は「依存関係をオブジェクト外に持たせて外部から注入するような設計にする」デザインパターンの1つです。日本語では依存性注入という意味になります。詳細は以下の記事を参考にしてください。

Swinjectとは?

公式リファレンス:Swinject

Swinject」はSwift向けの軽量な依存性注入(DI)ライブラリです。類似のライブラリとしてStoryboard向けの依存性注入(DI)ライブラリSwinjectStoryboardも用意されています。

Swiftで使えるDIライブラリは他にもいくつかありますが活用率が一番高いのがSwinjectかなと思います。使い方を割と直感的で難しくなくコード量もそれほど増えないのでおすすめです。

ライブラリの導入

SwinjectはCocoa Pods、Swift Package Manager、Carthageをサポートしているのでよしななものを使用して導入してください。


pod 'Swinject'

Swift Package Managerなら以下で検索してください。

https://github.com/Swinject/Swinject.git

Swinjectの使用例

依存性注入を行う主な方法は2つありますがSwinjectでは主にコンストラクタインジェクションを使って行きます。

コンストラクタインジェクション

コンストラクタインジェクションはその名前の通りコンストラクタを使用して依存性を挿入する方法です。ViewModelやRepositoryなどシンプルな独自のクラスなどでは簡単にコンストラクタインジェクションを実装することができます。

フィールドインジェクション(セッターインジェクション)

AndroidでのFragmentやActivityなどシステムによってインスタンス化されるクラスはコンストラクタインジェクションが不可能です。そのためクラスの作成後に依存関係がインスタンス化されます。

DIコンテナを作成する

SwinjectではDIコンテナと呼ばれるものをオブジェクトを使用して依存性注入を管理しています。DIコンテナはContainerクラスとして管理されています。このクラスに対してregisterメソッドを使用して依存元を登録します。

import Swinject

final class DIContainer: @unchecked Sendable{
    static let shared = DIContainer()

    private let container = Container() { c in
        // DIコンテナに依存元を登録
        c.register(Repository.self) { _ in RepositoryImpl() }
    }

    public func resolve<T>(_ type: T.Type) -> T {
        guard let resolved = container.resolve(type) else {
            fatalError("依存解決に失敗しました: \(type)")
        }
        return resolved
    }
}

DIコンテナの中から対象のクラスを取得する必要があるのでどこからでもアクセスできるようにresolveメソッドを使って対象のクラスを取得できるようなメソッドを定義します。

// DIコンテナから対象のクラスを取り出して使用する
let viewModel: ViewModel = DIContainer.shared.resolve(ViewModel.self)

コンストラクタインジェクション

例えば依存元がRepositoryで依存先がViewModelなどの場合に依存性注入(DI)を行うためには「コンストラクタインジェクション」という方法を用います。まずは必要となる各クラスを定義していきます。

リポジトリプロトコル

protocol Repository {
    func fetchMessage() -> String
}

リポジトリ実体クラス

class RepositoryImpl: Repository {
    func fetchMessage() -> String {
        return "こんにちは!これはRepositoryからのメッセージです。"
    }
}

RootViewModel

class RootViewModel: ObservableObject {
    private let repository: Repository
    @Published private(set) var message: String = ""

    init(repository: Repository) {
        self.repository = repository
    }

    func loadMessage() {
        message = repository.fetchMessage()
    }
}

ここまで準備ができたら依存性注入を行うためDIコンテナを作成します。先ほど定義したDIコンテナがそのまま使えるので定義しておいてください。あとはresolveメソッドで対象のインスタンスを取得できるようになっています。Viewに反映させる時はイニシャライザを介して入れるようにしておきます。

struct RootView: View {
    
    @StateObject private var viewModel: RootViewModel

    init(container: DIContainer = .shared) {
        // DIContainer から注入した ViewModel を使う
        _viewModel = StateObject(
            wrappedValue: RootViewModel(
                repository: container.resolve(Repository.self)
            )()
        )
    }
    
    private func greeting() {
        viewModel.loadMessage()
    }
    var body: some View {
        VStack {
            Button {
                greeting()
            } label: {
                Text(viewModel.message)
            }
        }
        .padding()
    }
}

どこまでDIを使用するかの問題になりますがViewModelまでSwinjectで作成することは避けたほうが良さそうです。

container.register(RootViewModel.self) { r in
    RootViewModel(repository: r.resolve(Repository.self)!)
}

// RootViewModelの直接渡さない
@StateObject private var viewModel: RootViewModel = DIContainer.shared.resolve(RootViewModel.self)

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index