1. Home
  2. テクノロジー
  3. iOSアプリ「ニフティ不動産 賃貸版」に swift-dependencies を導入しました

iOSアプリ「ニフティ不動産 賃貸版」に swift-dependencies を導入しました

この記事は「ニフティグループ Advent Calendar 2023」 21日目の記事です。

目次

はじめに

こんにちは。ニフティライフスタイルでモバイルアプリの開発をしているt-toshasegawです。

テストコードを書く上で DI は避けては通れません。しかし、 iOS には DI といえばこのライブラリを使おう!というものがなかったかと思います。そんな中、今年 swift-dependencies という DI ライブラリが iOSエンジニアのSNS等で話題に上がる機会を多く見ました。

チーム内で以前から Android 開発時に用いる Hilt のようなものを iOS でも取り入れたいよねと話していたこともあり、iOSアプリ「ニフティ不動産 賃貸版」に swift-dependencies を導入しました。そこで今回は、 swift-dependencies 導入に至った経緯や実際に運用して感じたことについてご紹介させて頂きます。

swift-dependencies とは

まずは swift-dependencies について簡単に解説します。

swift-dependencies は依存の登録や差し替えが容易に可能になるライブラリです。依存を登録するには DependencyKey を継承した enum を作成します。 liveValue という static プロパティの実装を要求されるので、 HogeUsecase の実装である HogeInteractor のインスタンスを代入します。次に、 DependencyValuesextension で拡張し、 Key を元に HogeInteractor を保存、取得するための処理を追加します。

import Dependencies

// DependencyKey を継承した enum
enum HogeUsecaseKey: DependencyKey {
    static let liveValue: any HogeUsecase = HogeInteractor()
}

// DependencyValues の拡張
extension DependencyValues {
    var hogeInteractor: any HogeUsecase {
        get { self[HogeUsecaseKey.self] }
        set { self[HogeUsecaseKey.self] = newValue }
    }
}

登録を終えると、 @Dependency というプロパティラッパーから依存を取得できるようになります。

final class HogePresenter {
    @Dependency(\.hogeInteractor) private var hogeInteractor
}

以上の実装のみで DI を実現することができます。

導入以前の課題

現在、iOSアプリ「ニフティ不動産 賃貸版」では VIPER アーキテクチャを採用しています。大まかな構成は下記の図のようになっており、 DI は Router が担当し、 PresenterinitInteractor を渡しています。

複雑な画面になると Presenter で多くの Interactor を呼ぶ必要があります。そのため、 DI のみで多くの実装を必要としてしまうという課題がありました。

class SearchPresenter {
    init(
        hogeInteractor: some HogeUsecase,
        fugaInteractor: some FugaUsecase,
        hogehogeInteractor: some HogeHogeUsecase,
        fugafugaInteractor: some FugaFugaUsecase
    )
}

class SearchRouter {
    // DI をしているメソッド
    static func assembleModules() -> UIViewController {
        let view = SearchViewController()
        let presenter = SearchPresenter(
            hogeInteractor: HogeInteractor,
            fugaInteractor: FugaInteractor,
            hogehogeInteractor: HogeHogeInteractor,
            fugafugaInteractor: FugaFugaInteractor
        )
        return view
    }
}

swift-dependencies の導入

前述した課題を解決するため swift-dependencies の導入に至りました。

まずはパッケージ構成を見直し下記のように変更しました。

新たに UsecaseContainer という DomainInfra を参照できるパッケージを作成しました。 UsecaseContainer では swift-dependencies を用いて Usecase の DI をしています。そして、Presenter から UsecaseContainer を参照し Interactor を呼ぶようにするという形をとりました。

当初は Usecase のみでなく、 Repository の DI も swift-dependencies に一任しようと検討しましたが、 Interactor から @DependencyDataStore を参照する必要があるため、 DomainInfra 間で循環参照が起こるという問題が発生しました。

しかし、 Usecase の DI のみが実現できれば当初の課題である PresenterRouter の DI によるコードの肥大化は解決できるため、 Repository については swift-dependencies を使用しない方針となりました。

運用してみて

swift-dependencies を導入し、 PresenterRouter のボイラープレートが大幅に減少し可読性が向上しました。また、導入前は Interactorinit で呼ぶ実装となっていたため、 init に処理が集中してしまうという課題もありました。しかし、プロパティで Interactor を呼べるようになったため、 init 外で処理を記述できるようになりました。

class SearchPresenter {
    @Dependency(\.hogeInteractor) private var hogeInteractor
    @Dependency(\.fugaInteractor) private var fugaInteractor
    @Dependency(\.hogehogeInteractor) private var hogehogeInteractor
    @Dependency(\.fugafugaInteractor) private var fugafugaInteractor
}

class SearchRouter {
    // DI をしているメソッド
    static func assembleModules() -> UIViewController {
        let view = SearchViewController()
        let presenter = SearchPresenter()
        return view
    }
}

新たな課題として、 UsecaseContainer に全ての DI を実装する都合上、 UsecaseContainer が肥大化してしまいました。また、使用したい Interactor が DI 済みかどうかの判別が検索する以外にないというのも使いにくさを感じました。

extension DependencyValues {
    private enum HogeUsecaseKey: DependencyKey {
        static let liveValue: any HogeUsecase = HogeInteractor()
    }

    public var hogeInteractor: any HogeUsecase {
        get { self[HogeUsecaseKey.self] }
        set { self[HogeUsecaseKey.self] = newValue }
    }

    private enum FugaUsecaseKey: DependencyKey {
        static let liveValue: any FugaUsecase = FugaInteractor()
    }

    public var fugaInteractor: any FugaUsecase {
        get { self[FugaUsecaseKey.self] }
        set { self[FugaUsecaseKey.self] = newValue }
    }
}

この課題については現状対応方針が確立していないので今後検討していく予定です。

最後に

swift-dependencies を導入することにより、 DI によるコードの肥大化を減少することができました。本記事では触れていませんが、依存の差し替えも簡単に可能なのでテストコードの実装も問題ありません。また、意図しない箇所で登録した依存が使用されている場合にクラッシュさせることも可能なので興味のある方は是非調べてみてください。

今回は ProtocolClass による DI の紹介をしましたが、 swift-dependencies のドキュメント を参照すると、構造体とクロージャを組み合わせた依存関係の提供を推奨しているようです。こちらの使用方法については、今後検討していければなと思っています。

明日は @tho-masu さんの記事です。お楽しみに!

この記事をシェア

掲載内容は、記事執筆時点の情報をもとにしています。