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
のインスタンスを代入します。次に、 DependencyValues
を extension
で拡張し、 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
が担当し、 Presenter
の init
へ Interactor
を渡しています。
複雑な画面になると 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
という Domain
と Infra
を参照できるパッケージを作成しました。 UsecaseContainer
では swift-dependencies を用いて Usecase
の DI をしています。そして、Presenter
から UsecaseContainer
を参照し Interactor
を呼ぶようにするという形をとりました。
当初は Usecase
のみでなく、 Repository
の DI も swift-dependencies に一任しようと検討しましたが、 Interactor
から @Dependency
で DataStore
を参照する必要があるため、 Domain
、 Infra
間で循環参照が起こるという問題が発生しました。
しかし、 Usecase
の DI のみが実現できれば当初の課題である Presenter
と Router
の DI によるコードの肥大化は解決できるため、 Repository
については swift-dependencies を使用しない方針となりました。
運用してみて
swift-dependencies を導入し、 Presenter
と Router
のボイラープレートが大幅に減少し可読性が向上しました。また、導入前は Interactor
を init
で呼ぶ実装となっていたため、 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 によるコードの肥大化を減少することができました。本記事では触れていませんが、依存の差し替えも簡単に可能なのでテストコードの実装も問題ありません。また、意図しない箇所で登録した依存が使用されている場合にクラッシュさせることも可能なので興味のある方は是非調べてみてください。
今回は Protocol
と Class
による DI の紹介をしましたが、 swift-dependencies のドキュメント を参照すると、構造体とクロージャを組み合わせた依存関係の提供を推奨しているようです。こちらの使用方法については、今後検討していければなと思っています。
明日は @tho-masu さんの記事です。お楽しみに!
掲載内容は、記事執筆時点の情報をもとにしています。