iTranslated by AI
Deferring the Initialization of SwiftUI EnvironmentValues
This article is for day 16 of the Swift/Kotlin Enthusiasts Advent Calendar 2022.
In SwiftUI, you can share values across Views using the Environment mechanism.
Environment:
While it is very convenient, there is a constraint when using Environment where you must determine the EnvironmentKey.defaultValue. This isn't a problem if a default value can be easily determined. However, consider a case where you need to fetch some app settings from an API after the app launches, and the app's main flow can only execute after those settings are retrieved. Setting aside the issue of the initial value, it is appropriate to share this setting information via Environment. In such cases, the constraint of having to decide on a default value can be problematic.
Of course, you could represent it as an Optional. But it is inconvenient to have to unwrap an Optional every time you use it when the value should definitely exist once the app's main flow has started.
While we cannot change the architectural requirement that values must be determined before the process begins, I will introduce a technique to seemingly delay this.
LazyEnvironmentValue
We will prepare a struct called LazyEnvironmentValue. This uses dynamicMemberLookup with KeyPaths to act as a proxy for the type you actually want to use. This means you can write access to properties just as if you were accessing an instance of the struct defined by the generic <T>.
As mentioned in the comments, the first init is used for Environment.defaultValue. The second init is used with .environment(KeyPath, Value) after the value has actually been retrieved from the API.
@dynamicMemberLookup struct LazyEnvironmentValue<T> {
// Use in EnvironmentKey.defaultValue
init() { }
// Use when the value is ready
private var value: T!
init(_ value: T) {
self.value = value
}
subscript<U>(dynamicMember keyPath: KeyPath<T, U>) -> U {
value[keyPath: keyPath]
}
}
Let's look at an actual code example. Here is how it is used:
Declaring the 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
}
}
}
Code used from the View
An instance of AppSetting is pseudo-created after 1 second and set in AppMainView using environment. You can see that AppMainView is able to use the set AppSetting values without any issues.
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)")
}
}
Execution Results
It's a screenshot, but you can see that the Color Hex is successfully displayed :tada:

Here is the complete code. Please feel free to check it for yourself.
@dynamicMemberLookup struct LazyEnvironmentValue<T> {
// Use in EnvironmentKey.defaultValue
init() { }
private var value: T!
// Use once the value is ready
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()
}
}
Summary
Essentially, this was a workaround for cases where a value is assumed to exist in the app's main flow, but its initial value cannot be set at the moment the app itself starts. When using environment via a property, be careful to ensure that your code is written so that the value definitely exists.
The end (^o^)/
Discussion