SwiftUI.EnvironmentValueの初期化を遅らせる
この記事はSwift/Kotlin愛好会 Advent Calendar 2022の12/16の記事です。
SwiftUIでは Environment
の仕組みを使いView
を跨いだ値の共有ができます。
Environment:
非常に便利なのですが、このEnvironmentを利用する上での制約に EnvironmentKey.defaultValue
を決定する必要があります。これはデフォルト値が決められるようなものであれば特に困らないのですが、たとえばアプリの起動後にAPIからデータを取得して何らかのアプリの設定を取得する必要がある。その設定が取得されてから初めてアプリとしてのメインストリームとしての処理が実行できるといったケースを考えます。この設定の情報自体は初期値のことは置いておいてEnvironment
で共有するようにするのが適切。こういったケースの時はこのデフォルト値を決める制約に悩まされます。
Optionalで表現する。といったことももちろん考えられますが、アプリのメインストリームの処理が始まれば本来絶対存在しているものをOptionalで表現し、使用する時にOptionalをUnwrapするのも不便です。
仕組み上アプリのプロセスが始まる前に値を決定する必要がある。という部分は変更できませんが、見かけ上これを遅らせるテクニックを今回紹介します。
LazyEnvironmentValue
LazyEnvironmentValue
という構造体を用意します。これは KeyPathによるdynamicMemberLookup
を利用して本来使用したい型のプロキシ的な役割を果たします。つまり、プロパティへのアクセスはジェネリクス<T>で定義された構造体のインスタンスにアクセスする場合と同じように書くことができます。
コメントにも書いてある通り1つ目のinitはEnvironment.defaultValueで使用する。2つ目のinitは実際にAPIから値が取得できたあとに .environment(KeyPath, Value)
で使用します。
@dynamicMemberLookup struct LazyEnvironmentValue<T> {
// EnvironmentKey.defaultValueで使用する
init() { }
// 値が準備できたら使う
private var value: T!
init(_ value: T) {
self.value = value
}
subscript<U>(dynamicMember keyPath: KeyPath<T, U>) -> U {
value[keyPath: keyPath]
}
}
実際のコード例を見てみましょう。利用する場合は下記のようになります
Environmentの宣言
struct AppSetting: Codable {
let logoURL: URL
let primaryColorHex: String
}
struct AppSettingEnvironmentKey: EnvironmentKey {
static var defaultValue: LazyEnvironmentValue<AppSetting> = .init()
}
extension EnvironmentValues {
var appSetting: LazyEnvironmentValue<AppSetting> {
get {
self[AppSettingEnvironmentKey.self]
}
set {
self[AppSettingEnvironmentKey.self] = newValue
}
}
}
Viewから使用するコード
擬似的に1秒後に AppSetting
のインスタンスを作り、それを AppMainView
に environment
により設定しています。 AppMainView
では設定された AppSetting
の値を問題なく使えていることが確認できます。
struct ContentView: View {
@State var appSetting: AppSetting?
var body: some View {
VStack(spacing: 20) {
Text("Hello, world")
.bold()
if let appSetting {
AppMainView()
.environment(\.appSetting, .init(appSetting))
} else {
ProgressView()
}
}
.padding()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appSetting = .init(logoURL: URL(string: "path")!, primaryColorHex: "#FFFFFF")
}
}
}
}
struct AppMainView: View {
@Environment(\.appSetting) var appSetting
var body: some View {
Text("Color is \(appSetting.primaryColorHex)")
}
}
実行結果
スクリーンショットですが無事にColorのHexが出ていることが確認できました :tada:
コードの全体も置いておきます。ぜひ手元でも確認してください
@dynamicMemberLookup struct LazyEnvironmentValue<T> {
// EnvironmentKey.defaultValueで使用する
init() { }
private var value: T!
// 値が準備できたら使う
init(_ value: T) {
self.value = value
}
subscript<U>(dynamicMember keyPath: KeyPath<T, U>) -> U {
value[keyPath: keyPath]
}
}
struct AppSetting: Codable {
let logoURL: URL
let primaryColorHex: String
}
struct AppSettingEnvironmentKey: EnvironmentKey {
static var defaultValue: LazyEnvironmentValue<AppSetting> = .init()
}
extension EnvironmentValues {
var appSetting: LazyEnvironmentValue<AppSetting> {
get {
self[AppSettingEnvironmentKey.self]
}
set {
self[AppSettingEnvironmentKey.self] = newValue
}
}
}
struct ContentView: View {
@State var appSetting: AppSetting?
var body: some View {
VStack(spacing: 20) {
Text("Hello, world")
.bold()
if let appSetting {
AppMainView()
.environment(\.appSetting, .init(appSetting))
} else {
ProgressView()
}
}
.padding()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appSetting = .init(logoURL: URL(string: "path")!, primaryColorHex: "#FFFFFF")
}
}
}
}
struct AppMainView: View {
@Environment(\.appSetting) var appSetting
var body: some View {
Text("Color is \(appSetting.primaryColorHex)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
まとめ
本質としてはアプリのメインストリームに存在する前提だが、アプリ自体の開始時には初期値を設定できないもの。に対する対処方でした。 environment
をプロパティから使用する側では絶対に値が存在するコードになるように気をつけましょう
おしまい \(^o^)/
Discussion