[SwiftUI] ジェネリックな@Environment / EnvironmentValueを追加する
導入
SwiftUIで便利な機能の1つは@Environment
です。階層を隔てたView
の間で容易に値を受け渡すことができるため、重宝します。
@Environment
はKeyPath<EnvironmentValue, T>
を受け取るイニシャライザを持っており、例えば以下のように使います。
struct ContentView: View {
// 環境のカラースキームを取得する
@Environment(\.colorScheme) var colorScheme: ColorScheme
// 略
}
ドキュメントによれば、EnvironmentValue
を拡張することで、コードベース独自の環境値を追加することができます。
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
を利用してEnvironment
のinit
を定義します。
extension Environment {
init() where Value: UserProtocol {
self.init(\.[user: UserSpecifier()])
}
}
UserSpecifier
が必要なのは、このようにKeyPath
を用いるためです。
型を指定したいだけなのでValue.self
でも良さそうに見えますが、実はKeyPath
でsubscript
に与えられる引数は「Hashable
である」という条件があります。メタタイプはHashable
ではないため、このケースで使えないのです。そこでUserSpecifier
をHashable
なメタタイプ代わりに利用します。この中では型のメタデータを利用してハッシュを計算するので、実質的には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