The SwiftUI environment is an in-memory shared storage among all views within a view hierarchy. Thanks to it, various parts of an app can access shared data easily, allowing for better state management which is fundamental to SwiftUI. The environment simplifies data flow by just making it available to views, while each individual view can read or update only the pieces of data that’s interested in. In general, the environment lifts a big load of sharing data among views, it helps us write cleaner code, but most importantly, it lets us focus more on the views and the user interface.
Since the environment is essential when making SwiftUI apps, it’s crucial to know how to pass custom data to it, as well as how to access that data from within views. There are two kinds of data we can provide to the environment; custom objects and environment values. There are three view modifiers we can use to manage that and there are also particular property wrappers to read all kind of data in views. This post covers all that through quick how-to examples, so just read on.
Passing Observable objects to SwiftUI environment
The most common case of data we provide to the view hierarchy regards objects of observable types. That is, types that hold and manipulate data marked with the @Observable
macro. Consider the following simple example:
1 2 3 4 5 6 |
@Observable class UserInfo { var username = “” var isAdmin = false } |
Suppose that we want a UserInfo
instance to be available to all views in the view hierarchy. To make this possible, we need first to create one right before the initialization of the first view in hierarchy that is going to make use of it. Often, that top-level spot of initialization is the app entry point:
1 2 3 4 5 6 7 8 |
@main struct MyApp: App { @State var userInfo = UserInfo() … } |
If necessary, we can perform any configuration before providing the userInfo
object to the environment. For example:
1 2 3 4 5 6 7 8 9 10 11 12 |
@main struct MyApp: App { @State var userInfo = UserInfo() init() { userInfo.username = “SwiftUI Developer” } … } |
Once the observable object has been initialized and potentially configured, it’s time to inject it to the SwiftUI environment. This is achieved by applying a view modifier to the first view in the hierarchy. When using @Observable
instances, the modifier to use is the environment(_:)
, demonstrated right next:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@main struct MyApp: App { @State var userInfo = UserInfo() var body: some Scene { WindowGroup { ContentView() // Inject the userInfo object to SwiftUI environment. .environment(userInfo) } } } |
To access the userInfo
and any other object within the view, we have to declare a local property and wrap it with the @Environment
property wrapper. In the wrapper we specify the data type of the object we want to access in the environment:
1 2 3 4 5 6 7 |
struct ContentView: View { @Environment(UserInfo.self) var userInfo … } |
From there on, we can use the userInfo
object in the view as needed:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct ContentView: View { @Environment(UserInfo.self) var userInfo var body: some View { VStack { Text(userInfo.username) Text(userInfo.isAdmin ? “Admin” : “User”) } } } |
Passing Observable objects in system versions before iOS 17
The @Observable
macro is available as of iOS 17 without backwards compatibility, so it’s not available in older system versions. Providing data to the SwiftUI environment when supporting older versions changes a bit, because, even though the workflow remains the same as above, there are different APIs to call in order to achieve that.
Let’s start with the type declaration. This time, the UserInfo
must conform to ObservableObject
protocol, and any properties that the views’ state depends on should be marked as @Published
(usually the majority of properties):
1 2 3 4 5 6 |
class UserInfo: ObservableObject { @Published var username = “” @Published var isAdmin = false } |
Next, the userInfo
instance must be initialized and wrapped with the @StateObject
property wrapper, instead of @State
as happened before:
1 2 3 4 5 6 7 |
struct MyApp: App { @StateObject var userInfo = UserInfo() … } |
What also changes is the view modifier that we use to pass the above object to the SwiftUI environment. This time it’s the environmentObject(_:)
modifier:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct MyApp: App { @StateObject var userInfo = UserInfo() var body: some Scene { WindowGroup { ContentView() .environmentObject(userInfo) } } } |
Lastly, on the view’s part we declare once again a property of our custom type (a UserInfo
property here), but this time we wrap it with the @EnvironmentObject
property wrapper. After that, we can access the various properties (and methods if exist) of our custom object whenever necessary:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct ContentView: View { @EnvironmentObject var userInfo: UserInfo var body: some View { VStack { Text(userInfo.username) Text(userInfo.isAdmin ? “Admin” : “User”) } } } |
Providing custom environment values to the view hierarchy
SwiftUI provides a predefined collection of environment values to our views which are gathered in a structure called EnvironmentValues
. Various view properties that we alter using view modifiers, such as the font type, line limit, preferred color scheme and so on, are actually built-in environment values made available to our views by SwiftUI. But in addition to them, we can define and pass our own custom values to the environment as well.
Before Xcode 16 it has been necessary to define a custom EnvironmentKey
type and then extend the EnvironmentValues
struct as shown next (read more about all that here):
1 2 3 4 5 6 7 8 9 10 11 12 |
struct UserPhraseKey: EnvironmentKey { static let defaultValue = “” } extension EnvironmentValues { var userPhrase: String { get { self[UserPhraseKey.self] } set { self[UserPhraseKey.self] = newValue } } } |
With the above we expose the userPhrase
string property to the view hierarchy as an environment value. In Xcode 16 and above, we can replace the above code just with a simplified EnvironmentValues
extension and make use of the @Entry
macro:
1 2 3 4 5 |
extension EnvironmentValues { @Entry var userPhrase: String = “” } |
Injecting the above property to the environment of the view hierarchy is done with the environment(_:_:)
view modifier at the top level point, where the first argument is the key path to the custom property and the second is the value we would like to set to it:
1 2 3 4 5 6 7 8 9 10 11 |
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .environment(\.userPhrase, “SwiftUI is great!”) } } } |
To read the custom environment value in any view down in the hierarchy, we can declare a local property and wrap it with the @Environment
property wrapper. As argument, we specify the key path to the custom property exactly as it happened in the previous code example. After that, the custom environment property is there to use it according to our needs:
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { @Environment(\.userPhrase) var userPhrase var body: some View { Text(“Favorite phrase: \(userPhrase)“) } } |
Conclusion
Coming to the end, all the above form the various ways to pass data to the SwiftUI environment. We can inject both custom objects and custom environment values, as long as we use the proper APIs depending on the supported system version. We came across three view modifiers that allow to provide custom data to the environment of a view hierarchy, and equal approaches for accessing that custom data in any SwiftUI view. I hope you found this post helpful. Thank you for reading!