Mutable EnvironmentValue

Safwen Debbichi
Bforbank Tech
Published in
3 min readJun 1, 2024

EnvironmentValues

Environment values are a collection of Data propagated through view hierarchy and navigation using the Property Wrapper @Environment and the view modifier environment(keyPath, value).

To declare a new Environment Value we need to create a new EnvironmentKey that has a default value and to extend the EnvironmentValues struct with a var that represents the KeyPath with it’s setter and getter like in the following example :

struct SomeEnvironmentValueKey: EnvironmentKey {
static let defaultValue: String = ""
}

extension EnvironmentValues {
var someEnvironmentValue: String {
get { self[SomeEnvironmentValueKey.self] }
set { self[SomeEnvironmentValueKey.self] = newValue }
}
}

By applying the environment(keyPath, value) on a view, we give access to the value to it’s subviews and when applying on a NavigationView or NavigationStack, then we give access to all of the navigation hierarchy and subviews.

struct ContentView: View {
@State var someEnvironmentValue: String = "toto"
@State var isPresented: Bool = false
var body: some View {
NavigationStack {
VStack(spacing: 50) {
Text(someEnvironmentValue)
.font(.title)
Button(action: {
isPresented.toggle()
}, label: {
Text("Next")
})
}
.padding()
.navigationDestination(isPresented: $isPresented) {
FirstView()
}
}.environment(\.someEnvironmentValue, someEnvironmentValue)
}
}

EnvironmentValues vs EnvironmentObject

EnvironmentObject work similarly to EnvironmentValues but we don’t need to create keys. The object is propagated with a similar view modifier but it needs to be an ObservableObject, or recently an Observable with the new Observation framework introduced by Apple.

EnvironmentValues are identified by KeyPath, however EnvironmentObjects are identified by their type.

If we forget to inject the EnvironmentValue and we try to access it in the subview then the EnvironmentValue will return the defaultValue, however if we do the same with EnvironmentObject the application will crash.

Any change of the EnvironmentObject’s @Published properties will trigger a view update and the object is mutable. However, that’s not possible with the EnvironmentValue as the KeyPath variable in the EnvironmentValues is a get-only property.

Making EnvironmentValues mutable

To make the EnvironmentValues mutable we need to make a workaround and to make the value of the EnvironmentValue a Binding of the desired type.

We start by creating a protocol called DynamicEnvironment and a typealias called DynamicEnvironmentKeyPath that represents the WritableKeyPath.

typealias DynamicEnvironmentKeyPath<Env: DynamicEnvironment> = WritableKeyPath<EnvironmentValues, Binding<Env>>

protocol DynamicEnvironment {
var id: UUID { get }
}

Then we create a view modifier to be able to inject the EnvironmentValue and update it’s value by holding a @State property reflecting the binding that we have injected to be able to mutate it. As we know cannot mutate a value inside a view unless it’s a Binding or State property.

struct DynamicEnvironmentModifier<Env: DynamicEnvironment>: ViewModifier {
var keyPath: DynamicEnvironmentKeyPath<Env>
@State var proxy: Env

func body(content: Content) -> some View {
content
.environment(keyPath, $proxy)
}
}

Now we declare our new DynamicEnvironmentValue:

struct SomeDynamicEnvironmentValue: DynamicEnvironment {
var id: UUID = UUID()
var value: String = ""
}

struct SomeEnvironmentValueKey: EnvironmentKey {
static let defaultValue: Binding<SomeDynamicEnvironmentValue> = .constant(.init())
}

extension EnvironmentValues {
var someEnvironmentValue: Binding<SomeDynamicEnvironmentValue> {
get { self[SomeEnvironmentValueKey.self] }
set { self[SomeEnvironmentValueKey.self] = newValue }
}
}

Then we inject a defaultValue on top of the NavigationStack:

@main
struct EnvironmentValueExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modifier(DynamicEnvironmentModifier(keyPath: \.someEnvironmentValue, proxy: .init()))
}
}
}

Now can enjoy a mutable EnvironmentValue:

struct ContentView: View {
@Environment(\.someEnvironmentValue) @Binding var someEnvironmentValue: SomeDynamicEnvironmentValue
@State var isPresented: Bool = false
var body: some View {
NavigationStack {
VStack(spacing: 50) {
Text("Value: "+someEnvironmentValue.value)
.font(.title)
Button(action: {
isPresented.toggle()
}, label: {
Text("Next")
})
}
.padding()
.navigationDestination(isPresented: $isPresented) {
FirstView()
}
}
}
}

struct FirstView: View {
@Environment(\.someEnvironmentValue) @Binding var someEnvironmentValue: SomeDynamicEnvironmentValue
var body: some View {
VStack(spacing: 50) {
TextField("field", text: _someEnvironmentValue.wrappedValue.value)
.textFieldStyle(.roundedBorder)
}
.padding()
}
}

Example’s Source code : https://github.com/SafwenD/EnvironmentValueExample

--

--