Custom SwiftUI Environment Values Cheatsheet
In How to define custom SwiftUI environment values we covered all the theory behind creating custom environment values. In this last entry of the Five Stars SwiftUI Environment series, let's have a quick tour of the most common custom value types we might define.
This is the sixth and last entry of the Five Stars SwiftUI Environment series. It's recommended to read the first few entries before proceeding: 1, 2, 3, 4, 5.
Struct, Enums, and primitives
SwiftUI environment values have been built specifically for these types, we've seen multiple examples of declaring a primitive environment value:
struct FSNumberKey: EnvironmentKey {
static let defaultValue: Int = 5
}
extension EnvironmentValues {
public var fsNumber: Int {
get { self[FSNumberKey.self] }
set { self[FSNumberKey.self] = newValue }
}
}
Replace Int
with any other primitive, struct
, or enum
definition, and it will work the same way.
// Read:
struct ContentView: View {
@Environment(\.fsNumber) var number: Int // 👈🏻
var body: some View {
Text("\(number)") // 👈🏻
}
}
// Set:
someView()
.environment(\.fsNumber, 10)
about 90% of the built-in environment values fall under this category.
Bindings
The @Environment
property wrapper gives us a read-only value, thus it makes sense to think about environment values as something set by a parent and read by a child.
However, we can use bindings to allow children to modify environment values, with the new changes bubbling up to their ancestors.
Take the following definition:
struct FSBoolBindingKey: EnvironmentKey {
static var defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var fsBoolBinding: Binding<Bool> {
get { self[FSBoolBindingKey.self] }
set { self[FSBoolBindingKey.self] = newValue }
}
}
This is very similar to any other environment value type, but it lets child views both read and modify the value:
struct ContentView: View {
@State var myBool = false
var body: some View {
VStack {
Text(myBool ? "true" : "false")
NestedView()
.environment(\.myBoolBinding, $myBool) // 👈🏻 we inject myBool binding into the environment
}
}
}
struct NestedView: View {
@Environment(\.myBoolBinding) @Binding var myBool: Bool // 👈🏻 we read the binding from the environment
var body: some View {
Toggle(isOn: _myBool.wrappedValue, label: EmptyView.init) // 👈🏻 we read/write the environment value!
}
}
Here's another we way we could have defined the NestedView
:
struct NestedView: View {
@Environment(\.myBoolBinding) var myBool: Binding<Bool> // 👈🏻 different type
var body: some View {
Toggle(isOn: myBool, label: EmptyView.init) // 👈🏻 different isOn parameter
}
}
This is equivalent to passing a binding as a parameter of a view, but via environment.
SwiftUI built-in binding environment values:
presentationMode
.
Optional bindings
In the previous FSBoolBindingKey
definition, we've set a default value of .constant(false)
, which works great as long as we remember to set the binding in the environment before it's used:
struct ContentView: View {
@State var myBool = false
var body: some View {
...
NestedView()
.environment(\.myBoolBinding, $myBool) // 👈🏻
...
}
}
If we forget to set this value into the environment, NestedView
(or any other view) won't be able to modify the myBoolBinding
environment value.
Depending on our use case, this might be what we want. Alternatively, similarly to what we've seen in How to add optional @Bindings
to SwiftUI views, we could define FSBoolBindingKey
and fsBoolBinding
with an associated optional binding:
struct FSBoolBindingKey: EnvironmentKey {
static var defaultValue: Binding<Bool>?
}
extension EnvironmentValues {
var fsBoolBinding: Binding<Bool>? {
get { self[FSBoolBindingKey.self] }
set { self[FSBoolBindingKey.self] = newValue }
}
}
At this point, views using this value will have to deal with a binding that might or might not be there. In the following example, NestedView
will use the environment value when present and, alternatively, will use a private state (that doesn't affect the environment):
struct NestedView: View {
@Environment(\.fsBoolBinding) var myBool
@State private var privateBool = false // 👈🏻
var body: some View {
Toggle(isOn: myBool ?? $privateBool, label: EmptyView.init) // 👈🏻
}
}
SwiftUI built-in optional binding environment values:
editMode
.
Actions
This year we've seen the introduction of a new environment-actions pattern in SwiftUI:
instead of views directly accepting closures to trigger in specific scenarios, it's now common to set closures into the environment, which are then picked up and triggered by relevant/associated views.
An example is the new onSubmit
modifier:
Form {
TextField(...)
TextField(...)
}
.onSubmit {
print("Form submitted")
}
// 👆🏻 this closure is set into the environment and picked up by TextField, SecureField, and TextEditor.
To define a new action, we first define a struct accepting a closure (depending on our case, this closure may accept/return values):
struct FSAction {
var action: () -> Void
init(action: @escaping () -> Void = { }) {
self.action = action
}
}
We might have extra logic in this struct that we probably don't want to expose to other developers. Hence we set our action
as private
and, instead, we make the struct type callable via Swift's special callAsFunction
method:
struct FSAction {
private var action: () -> Void // 👈🏻 private
func callAsFunction() { // 👈🏻
action()
}
init(action: @escaping () -> Void = { }) {
self.action = action
}
}
Next, we create the usual key and extend environment values:
struct FSActionKey: EnvironmentKey {
static var defaultValue: FSAction = FSAction()
}
extension EnvironmentValues {
var fsAction: FSAction {
get { self[FSActionKey.self] }
set { self[FSActionKey.self] = newValue }
}
}
...and now we're ready to use this new action:
// Read:
struct ContentView: View {
@Environment(\.fsAction) var action: FSAction // 👈🏻
var body: some View {
Button("Tap me") { action() } // 👈🏻
}
}
// Set:
someView()
.environment(\.fsAction, FSAction { // 👈🏻
...
})
Thanks to FSAction
's callAsFunction
we don't need to reach for the FSAction.action
property (and we can't, as it's private
). Instead, we call the function directly on the FSAction
instance. This helps hide any implementation detail of our FSAction
struct.
SwiftUI built-in actions:
dismissSearch
(DismissSearchAction
),openURL
(OpenURLAction
),refresh
(RefreshAction
).resetFocus
(ResetFocusAction
),dismiss
(DismissAction
).
Closures
The action pattern that we just introduced has an associated struct
type that really is a wrapper for a closure. This was true for our simple case, but, in reality, our FSAction
struct may contain implementation details that are just not exposed.
If we want our environment value to be just a closure and nothing else, we can skip the action struct definition, and use a closure directly:
struct ClosureKey: EnvironmentKey {
static let defaultValue: () -> Void = { }
}
extension EnvironmentValues {
public var fsAction: () -> Void {
get { self[ClosureKey.self] }
set { self[ClosureKey.self] = newValue }
}
}
Which we can then use this way:
// Read:
struct ContentView: View {
@Environment(\.fsAction) var action: () -> Void // 👈🏻
var body: some View {
Button("Tap me") { action() } // 👈🏻
}
}
// Set:
someView()
.environment(\.fsAction) { // 👈🏻
...
}
Unlike SwiftUI views, which must be value types, environment values can also be reference types.
A kind reminder that Swift closures are reference types.
Classes
Speaking of reference types, we can also define environment values with an associated class type.
There are important implications when using a class rather than a struct:
- we can alter a class instance, and the exact change will be reflected anywhere else that same instance is referenced in the view hierarchy.
- views do not observe changes within classes defined in
EnvironmentValues
, regardless of whether the class is markedObservableObject
. If we're trying to do something similar to this, we should use environment objects instead.
Example:
public class FSClass {
var x: Int
init(x: Int = 5) {
self.x = x
}
}
private struct FSClassKey: EnvironmentKey {
static let defaultValue = FSClass()
}
extension EnvironmentValues {
public var fsClass: FSClass {
get { self[FSClassKey.self] }
set { self[FSClassKey.self] = newValue }
}
}
Which we can then use like any other environment value:
// Read:
struct ContentView: View {
@Environment(\.fsClass) private var fsClass // 👈🏻
var body: some View {
VStack {
Text("\(fsClass.x)") // 👈🏻
Button("change") {
fsClass.x = Int.random(in: 1...99)
// 👆🏻 fsClass is a class, we can modify its properties
}
}
}
}
// Set:
someView()
.environment(\.fsClass, FSClass(x: 1))
There are very few reasons why we'd ever define a class-type environment value, but it's good to know that it's supported.
SwiftUI built-in class environment values:
managedObjectContext
(NSManagedObjectContext
) andundoManager
(UndoManager
).
Conclusions
We've now completed the Five Stars SwiftUI environment series, thank you for reading, and I hope you've found these articles helpful!
What else would you like me to cover next? Let me know via email or Twitter!