Swift's @Observable
macro combined with @State
makes it straightforward to create and use data in our apps, and previously we've looked at how to pass values between different views. However, sometimes you need the same object to be shared across many places in your app, and for that we need to turn to SwiftUI's environment.
To see how this works, let's start with some code you should already know. This creates a small Player
class that can be observed by SwiftUI:
@Observable
class Player {
var name = "Anonymous"
var highScore = 0
}
We can then show their high score in a small view such as this:
struct HighScoreView: View {
var player: Player
var body: some View {
Text("Your high score: \(player.highScore)")
}
}
That expects to be given a Player
value, so we might write code such as this:
struct ContentView: View {
@State private var player = Player()
var body: some View {
VStack {
Text("Welcome!")
HighScoreView(player: player)
}
}
}
This is all old code: it shows passing a value into a subview directly, so it can be used there.
Usually, though, we have more complex needs: what if that object needs to be shared in many places? Or what if view A needs to pass it to be view B, which needs to pass it view C, which needs to pass it view D? You can easily see how that would be pretty tedious to code.
SwiftUI has a better solution for these problems: we can place our object into the environment, then use the @Environment
property wrapper to read it back out.
This takes two small changes to our code. First, we no longer pass a value directly into HighScoreView
, and instead use the environment()
modifier to place our object into the environment:
VStack {
Text("Welcome!")
HighScoreView()
}
.environment(player)
Important: This modifier is designed for classes that use the @Observable
macro. Behind the scenes, one of the things the macro does is add conformance to a protocol called Observable
(without the @
!), and that's what the modifier is looking for.
Once an object has been placed into the environment, any subview can read it back out. In the case of our HighScoreView
, we'd need to modify its player
property to this:
@Environment(Player.self) var player
Just like with other kinds of observed state, HighScoreView
will automatically be reloaded when its properties change. Be careful, though: your app will crash if you say an environment object will be in the environment and isn't.
Although this mostly works well, there is one place where there's a problem and you'll almost certainly hit it: when trying to use an @Environment
value as a binding.
Note: If you're reading this after iOS 18 was released, I sincerely hope Apple has resolved this issue, but right now I'm using iOS 17 and it's an issue.
You can see the problem with code like this:
struct HighScoreView: View {
@Environment(Player.self) var player
var body: some View {
Stepper("High score: \(player.highScore)", value: $player.highScore)
}
}
That attempts to bind the player's highScore
property to a stepper. If we had made the player
instance using @State
this would be allowed just fine, but that doesn't work with @Environment
.
Apple's solution for this – at least right now – is to use @Bindable
directly inside the body property, like this:
@Bindable var player = player
That effectively means, "create a copy of my player
property locally, then wrap it in some bindings I can use." It's a bit ugly, to be honest, and again I hope by the time you read this it's no longer needed!
SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.