概要
最近 uber/needle を使う機会があったので入門しました。この記事では needle を使い始めるためにざっくり持っておくと便利なイメージのようなものをまとめます。
uber/needle とは
needle は Swift で DI を行うためのライブラリです。類似ライブラリの中でもけっこう有名な方で、 iOS アプリに DI の仕組みを入れようとなったときに利用するライブラリの検討候補には上がってくるのではないかと思います。
DI は依存関係を実行時に解決する動的な DI とビルド時に解決する静的な DI にざっくり分けられますが、 needle は後者の静的な DI をコード生成により実現しています。静的な DI では実行時に存在するはず依存が取得できなくてクラッシュするような問題が発生しないので、それが needle のメリットの1つとなっています。
needle は、コード生成を行う CLI とアプリに統合する NeedleFoundation
フレームワークの2つの部品からなっています。CLI は手動でコマンドラインで実行することもできますが、ちゃんとしたアプリにおいては Build Phases のどこかで実行するのがよいでしょう。
インストールの方法は README にまとまっています。
Component
と Dependency
needle の README を読んでみると少し難しそうな雰囲気がただよっていますが、実際に入門してみると非常に簡単で理解するべき概念は Component
と Dependency
の2つだけです。
ドキュメント を読むと Component
はスコープだとか階層構造を作るとかいろいろ書いてありますが、入門時には「画面を作ることに責任を持つもの」と考えておくとわかりやすいと思っています。必ずしもそうする必要はないですが、画面 A / B / C と3つ作るときには Component
も対応させて A / B / C の3つを作ることが多く、公式のサンプルでもそうなっています。
Component
が画面を作るぞ!と思ったところで無から画面を作るわけにはいかなくて、普通は API なりデータベースなりに依存することになります。この依存をまとめたものが Dependency
です。1つの Component
には1つの Dependency
が対応するので、例えば画面 A を作る場合には AComponent
と ADependency
を用意することになります。画面 A が必要とする依存が ADependency
にまとめられ、それを利用して AComponent
が画面を生成します。
画面を作ってみる : Component
と Dependency
実際に needle の Component
と Dependency
を使って SwiftUI で画面を作ってみます。題材として、ランダムな番号をいくつか表示する画面を作ることにします。まず、 View を以下のように作ってしまいましょう。
// NumbersScreen.swift import SwiftUI struct NumbersScreen: View { @State private var randomNumbers: [Int] = [] var body: some View { NavigationView { List { ForEach(randomNumbers, id: \.self) { number in Text(number, format: .number) } } .toolbar { ToolbarItem { Button("Refresh") { refreshRandomNumbers() } } } } .onAppear { refreshRandomNumbers() } } private func refreshRandomNumbers() { // TODO: 番号を取得して randomNumbers に代入する } }
ランダムな番号を取得して画面に反映するメソッドである refreshRandomNumbers
は一旦 TODO にしてありますが、 View 自体はこれで完成です。
先ほども書いたように、 needle において画面を作ることに責任を持つのは Component
なので、 NumbersScreen
を生成する NumbersComponent
を作ることにします。 Component
は NeedleFoundation
の Component
クラスを継承して作ります。
// NumbersScreen.swift final class NumbersComponent: Component<EmptyDependency> { func build() -> some View { NumbersScreen() } }
Component
がどうやって画面を作るかは needle が定めているわけではないので、好きに作って OK です。今回の NumbersComponent
では build
というメソッドに NumbersScreen
を生成して返してもらっています。 Dependency
は Component
と1対1に対応しますが、これは Component
の型パラメータが Dependency
を受け取ることにより実現されています。ひとまず現状の NumbersScreen
は何も依存を必要としていないので、空の依存を表す NeedleFoundation
組み込みの EmptyDependency
を利用しています。
それでは、先ほど TODO にしていた NumbersScreen#refreshRandomNumbers
を埋めていきます。そのために、どこかからランダムな番号を取ってくる必要があります。いったん具体的な実装のことを考えずに protocol だけ定義することにします。
// RandomNumberFetcher.swift protocol RandomNumbersFetcherProtocol { func fetch() -> [Int] }
これを NumberScreen
に持たせ、 refreshRandomNumbers
の中で呼べば OK です。
// NumbersScreen.swift struct NumbersScreen: View { // ... private let randomNumbersFetcher: any RandomNumbersFetcherProtocol init(randomNumbersFetcher: any RandomNumbersFetcherProtocol) { self.randomNumbersFetcher = randomNumbersFetcher } // ... private func refreshRandomNumbers() { randomNumbers = randomNumbersFetcher.fetch() } }
View 側のコード変更としてはこれがすべてです。次に誰が NumberScreen
に RandomNumbersFetcherProtocol
を渡すのかという問題が出てきます。 NumberScreen
を作る責任は NumbersComponent
にあるので、最終的には NumbersComponent
に渡してもらう必要があります。しかし、 needle では Component
は必ずしも画面のための依存を生成する必要はなく、こういう依存がほしいよというのを Dependency
として宣言しておくだけで OK です。これにより、 Component
の中で使える dependency
というプロパティの中に望みの依存が入ってくることになります。
// NumbersScreen.swift protocol NumbersDependency: Dependency { var randomNumbersFetcher: any RandomNumbersFetcherProtocol { get } } final class NumbersComponent: Component<NumbersDependency> { func build() -> some View { NumbersScreen(randomNumbers Fetcjer: dependency.randomNumbersFetcher) } }
以上で、ランダムな番号を取得して表示する画面を作ることができました。もちろん、 RandomNumbersFetcherProtocol
を満たす実体の実装をしていないのでアプリとしては動作しないですが、画面の実装としては完了していて、たとえば RandomNumbersFetcherProtocol
のモックを作ってあげればプレビューやテストなどで画面の動作確認をすることもできます。
ここまでの例で Component
と Dependency
についてなんとなく理解することができたと思います。画面が必要とする依存が Dependency
で、その Dependency
を元に画面を生成する責任を持つのが Component
です。
アプリを動作させる : BootstrapComponent
続いて、実際にアプリを動作させていきます。まずは、 RandomNumbersFetcherProtocol
を実装してしまいます。その具体的な実装は needle を学ぶにあたっては関係ないので、その場で適当に Int
を5つ生成して配列に入れて返すことにします。
// RandomNumbersFetcher.swift final class RandomNumbersFetcher: RandomNumbersFetcherProtocol { func fetch() -> [Int] { Array((0..<100).shuffled().prefix(5)) } }
ここまでで
- 画面(
NumbersScreen
) Component
(NumbersComponent
)Dependency
(NumbersDependency
)Dependency
が必要とする依存(RandomNumbersFetcherProtocol
とその実装のRandomNumbersFetcher
)
が揃ったことになります。アプリとして動作するための TODO として、RandomNumberFetcher
を実際に生成して NumbersComponent
に渡してあげる作業が残っています。
needle において Component
は親子関係のツリーを作っていて、すべての Component
はそのツリーのどこかに属することになります。そして、ある Component
が依存を必要とするとき、その依存の protocol を自分の Dependency
に書いておくだけで、その Component
のツリー上の祖先の Component
が提供するその protocol の実装が取得できることになっています。これが needle による DI の核となる機能です。
これまで、 NumbersComponent
は必要な依存を受け取って NumbersScreen
を生成するという責任しか持っていませんでした。しかし、一般の Component
は自分の子孫が利用する依存の実装を提供する責任も併せ持っているというわけです。
Component
の親子関係の中で特別なものとしてBootstrapComponent
があります。BootstrapComponent
は、 Component
の親子関係のツリーのルート、つまりすべての Component
の親です。今回作っているアプリにはまだ NumbersComponent
しかないので、ツリーのルートに BootstrapComponent
があり、その唯一の子として NumbersComponent
があることになります。
そのため、 NumbersComponent
に RandomNumbersFetcher
を提供する役割を持っているのは BootstrapComponent
です。 BootstrapComponent
を RootComponent
という名前で実装しましょう。
// NumbersApp.swift final class RootComponent: BootstrapComponent { var numbersComponent: NumbersComponent { NumbersComponent(parent: self) } public var randomNumbersFetcher: any RandomNumbersFetcherProtocol { RandomNumbersFetcher() } }
親子関係を作るために RootComponent
のプロパティとして NumbersComponent
を生成し、 parent
パラメータに自らを渡しています。これで、 needle に RootComponent
が子として NumbersComponent
を持つことが伝わります。
次に、 NumbersComponent
が必要とする依存である randomNumbersFetcher
をプロパティとして持たせます。 needle の働きにより NumbersDependency
の randomNumbersFetcher
にこの実装が入ってくることになります。今回は RootComponent
と NumbersComponent
に直接の親子関係がありますが、直接の子でなくても子孫であれば先祖が提供する依存を取得することができます。例えば NumbersComponent
の子として NumberDetailComponent
を作ったとして、 NumberDetailComponent
も RootComponent
が提供する randomNumbersFetcher
を Dependency
を通じて利用できるということです。
ただし、依存の提供に関して以下の2点に注意する必要があります。
- 提供側(
RootComponent
)と受け取り側(NumbersDependency
)は同じプロパティ名で依存を持つ必要がある - 子孫の
Component
に依存が渡されるためには提供される依存のアクセスレベルがpublic
である必要がある
以上の仕組みを実現するために needle の CLI コマンドを実行してコード生成を行います。以下のコマンドで、 NumbersApp
以下に存在するコードから Component
の親子関係を読みとって、必要なコードを NumbersApp/NeedleGenerated.swift
に生成してくれます。
needle generate NumbersApp/NeedleGenerated.swift NumbersApp/
生成された NeedleGenerated.swift
をプロジェクトに加え、アプリ起動時に registerProviderFactories
という関数を呼ぶことで needle の仕組みが有効になります。
最後に、 RootComponent
のインスタンスを生成する作業も合わせて、アプリのエンドポイントは以下のようにします。
// NumbersApp.swift @main struct NumbersApp: App { private let rootComponent: RootComponent init() { registerProviderFactories() self.rootComponent = RootComponent() } var body: some Scene { WindowGroup { rootComponent.numbersComponent.build() } } }
これで、アプリが正しく動作するようになりました。実行すると、 rootComponent.numbersComponent
が生成する NumbersScreen
が表示されますが、この NumbersScreen
は RootComponent
が提供する randomNumbersFetcher
を利用することで動作していて、めでたいです。
誰がどの依存を提供するか
今回は RootComponent
に randomNumbersFetcher
を提供してもらいました。これにより、今後 RootComponent
の子孫が増えてもその全員が randomNumbersFetcher
を利用することができるというメリットがあります。しかし、冷静に考えると現段階では randomNumbersFetcher
を使っているのは NumbersScreen
だけなので NumbersComponent
が自ら提供することも可能です。
final class NumbersComponent: Component<NumbersDependency> { public var randomNumbersFetcher: any RandomNumbersFetcherProtocol { RandomNumbersFetcher() } func build() -> some View { NumbersScreen(randomNumbersFetcher: randomNumbersFetcher) } }
Component
のツリーの中で誰がどの依存を提供するかはとくに定められていないので、アプリの都合によって自由に決めることができます。
まとめ
ここまでで needle を使い始めるにあたって理解しておくべき基本的なことはおおよそ見てこれたと思います。
needle において理解するべきは Component
と Dependency
です。 Component
は BootstrapComponent
をルートとする親子関係のツリーを作ります。個々の Component
は、
- 子孫が利用する依存の実装を提供する責任
- 祖先が提供してくれる依存を
Dependency
を利用して画面を作る責任
を持ちます。 needle のコード生成により、祖先が提供する依存が子孫の Dependency
に入ってくきます。
より詳しいことは公式のドキュメントを参照しましょう。