👥

[SwiftUI] ジェネリックな@Environment / EnvironmentValueを追加する

2023/07/21に公開

導入

SwiftUIで便利な機能の1つは@Environmentです。階層を隔てたViewの間で容易に値を受け渡すことができるため、重宝します。

@EnvironmentKeyPath<EnvironmentValue, T>を受け取るイニシャライザを持っており、例えば以下のように使います。

struct ContentView: View {
    // 環境のカラースキームを取得する
    @Environment(\.colorScheme) var colorScheme: ColorScheme
    // 略
}

ドキュメントによれば、EnvironmentValueを拡張することで、コードベース独自の環境値を追加することができます。
https://developer.apple.com/documentation/swiftui/environmentkey

struct UserEnvironmentKey: EnvironmentKey {
    typealias Value = User
    static var defaultValue: User = .default
}

extension EnvironmentValues {
    var currentUser: User {
        get {
            self[UserEnvironmentKey.self]
        }
        set {
            self[UserEnvironmentKey.self] = newValue
        }
    }
}

このようにすることで、@Environment(\.currentUser)と書くだけでユーザを取得できるようになります。

ジェネリックにしたい

なかなか珍しい状況ですが、この@Environmentをジェネリックにしたいと思うかもしれません。つまり、指定する型によって異なる環境値が欲しいわけです。

// こんな感じで使えると嬉しい
@Environment(\.user) var hogeUser: HogeUser
@Environment(\.user) var fugaUser: FugaUser

ではこれ(に近いもの)を実装します。

Userの定義

Userに関してプロトコルを定義し、ひとまず2種類のユーザ型を用意します。

protocol UserProtocol {
    static var `default`: Self { get }
}

struct HogeUser: UserProtocol {
    static var `default`: Self { Self() }
}

struct FugaUser: UserProtocol {
    static var `default`: Self { Self() }
}

EnvironmentValuesの拡張

通常の拡張の場合と類似していますが、EnvironmentKeyがジェネリックになっており、EnvironmentValuesの拡張でsubscriptを利用しています。

struct GenericUserEnvironmentKey<User: UserProtocol>: EnvironmentKey {
    typealias Value = User
    static var defaultValue: Value { User.default }
}

struct UserSpecifier<User: UserProtocol>: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(String(describing: User.self))
    }
}

extension EnvironmentValues {
    subscript<User: UserProtocol>(user _: UserSpecifier<User>) -> User {
        get {
            self[GenericUserEnvironmentKey<User>.self]
        }
        set {
            self[GenericUserEnvironmentKey<User>.self] = newValue
        }
    }
}

UserSpecifierという謎の型を追加していますが、これは後の処理のためのワークアラウンドです。

Environmentへのinitの追加

先ほど追加したsubscriptへのKeyPathを利用してEnvironmentinitを定義します。

extension Environment {
    init() where Value: UserProtocol {
        self.init(\.[user: UserSpecifier()])
    }
}

UserSpecifierが必要なのは、このようにKeyPathを用いるためです。

型を指定したいだけなのでValue.selfでも良さそうに見えますが、実はKeyPathsubscriptに与えられる引数は「Hashableである」という条件があります。メタタイプはHashableではないため、このケースで使えないのです。そこでUserSpecifierHashableなメタタイプ代わりに利用します。この中では型のメタデータを利用してハッシュを計算するので、実質的にはHashableだと扱えるようになっています。

Viewへのmodifierの追加

以下のようにします。

extension View {
    func userEnvironment(_ value: some UserProtocol) -> some View {
        self.environment(\.[user: UserSpecifier()], value)
    }
}

使い方

こんな感じで使えます。

struct ContentView: View {
    @Environment private var hogeUser: HogeUser
    @Environment private var fugaUser: FugaUser
    // 略
}

まとめ

ジェネリックな@Environment / EnvironmentValueを実装しました。

実際のモチベーションはDIでした。注入した型をどうにか下流のViewまで降ろしてきても@Environmentの多相性を実現する方法が見つからず、困った末にひねり出した方法です。使える場面は限定的だと思いますが、誰かの役に立てば嬉しいです。

Discussion