UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Sharing @Observable objects through SwiftUI's environment

Paul Hudson    @twostraws   

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!

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.