environmentでプロトコルを介してインスタンスを注入する方法(Swift)

## はじめに

SwiftUIにおいて、インスタンスを伝播させるためには `environment()` モディファイアを使います。
ただ受け取り側がその具象型を知らない場合、そのままでは受け取れません。

本記事では具象型を知らなくても受け取れるよう、プロトコルを介してインスタンスを注入する方法を紹介します。

## 環境

- OS: macOS Sonoma 14.5
- Xcode: 16.2 (16C5032a)
- Swift: 6.0.3

## 注意

iOS 17.0+からはObservationフレームワークを使えるので、Combineフレームワークを使うときと書き方が異なります。
本記事ではiOS 16以前をサポートしているプロジェクトでも使えるよう、両方の書き方を併記します。

## `environment()` でプロトコルを介してインスタンスを注入する

### プロトコルの定義

注入の受け口となるプロトコルを定義します。

```swift:Foo.swift
import SwiftUI

// Use Observation (iOS 17.0+)
@MainActor
protocol Foo: AnyObject, Observable, Sendable {
    func bar()
}

// Use Combine (iOS 13.0+)
@MainActor
protocol Foo: ObservableObject, Sendable {
    func bar()
}
```

公式ドキュメントに記載されている通り、Observationを使うときは `AnyObject` と `Observable` 、Combineを使うときは `ObservableObject` に準拠する必要があります。

https://developer.apple.com/documentation/swiftui/view/environment(_:)

https://developer.apple.com/documentation/swiftui/view/environmentobject(_:)

`Sendable` への準拠は必須ではありませんが、準拠する場合が多いと思うので記述しています。

### プロトコルに準拠したクラスの定義

先ほど作成したプロトコルに準拠したクラスを定義します。

```swift:DefaultFoo.swift
// Use Observation (iOS 17.0+)
@Observable
final class DefaultFoo {}
extension DefaultFoo: Foo {
    bar() {
        print("Bar")
    }
}

// Use Combine (iOS 13.0+)
final class DefaultFoo {}
extension DefaultFoo: Foo {
    bar() {
        print("Bar")
    }
}
```

Observationを使うときは `@Observable` マクロを付ける必要があります。違いはこれだけです。

### インスタンスの生成と注入

先ほど作成したクラスのインスタンスを生成して注入します。

```swift:RootScreen.swift
import SwiftUI

struct RootScreen: View {
    @State private var foo = DefaultFoo()

    var body: some View {
        FooScreen<DefaultFoo>()
            .environment(foo) // Use Observation (iOS 17.0+)
            .environmentObject(foo) // Use Combine (iOS 13.0+)
        }
    }
}
```

Observationでは `environment()` 、Combineでは `environmentObject()` を使ってインスタンスを注入します。
型パラメータで具象型を渡す必要があるのに注意です。

### インスタンスの受け取り

注入されたインスタンスを受け取ります。

```swift:FooScreen.swift
import SwiftUI

struct FooScreen<F: Foo>: View {
    @Environment(F.self) private var foo // Use Observation (iOS 17.0+)
    @EnvironmentObject private var foo: F // Use Combine (iOS 13.0+)

    var body: some View {
        Text("Foo")
            .onAppear {
                foo.bar()
            }
    }
```

Observationでは `@Environment` 、Combineでは `@EnvironmentObject` を使ってインスタンスを取得します。

## おわりに

これで `environment()` でプロトコルを介してインスタンスを注入できました。
依存性逆転の原則（DIP）の実現に有用なので、みなさんも使ってみてください :relaxed:

## 参考リンク

- [Xユーザーのウホーイさん: 「iOS 17.0+ で Environment を protocol で受け取るにはどうしたらいいんだろ？ https://t.co/0bwryW5VgJ」 / X](https://x.com/the_uhooi/status/1890249660385861804)
- [environment(_:) | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui/view/environment(_:))
- [environmentObject(_:) | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui/view/environmentobject(_:))
- [Observation | Apple Developer Documentation](https://developer.apple.com/documentation/observation)
- [Migrating from the Observable Object protocol to the Observable macro | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro)
- [Show session by uhooi · Pull Request #3 · uhooi/VimConf-2024-iOS](https://github.com/uhooi/VimConf-2024-iOS/pull/3)
- [Migrating `Router` protocol from the `ObservableObject` to the `@Observable` by treastrain · Pull Request #4 · uhooi/VimConf-2024-iOS](https://github.com/uhooi/VimConf-2024-iOS/pull/4)