Observable objects, environment objects, and @Published
Observable objects, environment objects, and @Published êŽë š
Updated for Xcode 15
We want to let folks place an order for pick up by selecting items and adding them to a cart. I already gave you a dedicated Order
class that holds an array of items, so we're going to add items to that then show them in a dedicated order view.
But there's a catch: if we're adding things inside ItemDetail
, how can we show them in an entirely separate OrderView
? More importantly, how can we make sure both of these two update each other as things change?
Well, SwiftUI has a quite brilliant solution called environment objects. These are objects that our views can use freely, but don't create or manage â they get created elsewhere, and carry on existing after the view has gone away.
In this app, we're going to create an instance of our order when the app launches, then pass it into our content view. Any view that is inside that content view â anything that can call the content view its ancestor â will automatically gain access to that environment object. Even better, when any view changes it, all other places automatically update.
Let's try it out now. Open your iDineApp.swift
, which is where our initial instance of ContentView
is created. Now give it this property:
@StateObject var order = Order()
Tip: Xcode will shown an error when you add that line, which is okay â we'll fix it in a moment.
That creates a new order when the app starts, and keeps it alive regardless of what view we show. The @StateObject
property wrapper is responsible for keeping the object alive throughout the life of our app.
Now we can pass that into our ContentView
struct when it gets created â look for this:
WindowGroup {
ContentView()
}
And replace it with this:
WindowGroup {
ContentView()
.environmentObject(order)
}
Now, I said that Xcode would throw up an error when we used the @StateObject
property â something along the lines of âArgument type 'Order' does not conform to expected type 'ObservableObject'â.
What it means is that SwiftUI doesn't understand how its user interface is supposed to watch our Order
class for changes â it doesn't understand how it should send and receive notifications that the data changed.
Think about it: if we select some food from the menu and add it to our order, we want that to appear immediately on the order page â we don't want to have hit refresh, or wait a few seconds, we want it immediately. And for that to work, SwiftUI needs a standard way for objects like Order
to say âhey, if anyone is watching me, you should know my data just changed.â
This standard already exists, and it's the ObservableObject
protocol. Anything that conforms to ObservableObject
can be used inside SwiftUI, and publish announcements when its values have changed so the user interface can be updated.
Apple provides a couple of different ways of publishing change announcements, but the easiest is to use the @Published
property wrapper before any properties that should trigger change notifications. In this case, just placing @Published
before a property is enough to have it update any SwiftUI views that are watching for changes â it's really powerful!
So, open Order.swift
and change the items
property to this:
@Published var items = [MenuItem]()
And that's it! Now that our class is configured correctly, we can make it conform to ObservableObject
, like this:
class Order: ObservableObject {
âŠand our code is back to compiling again. In total, we have updated Order
so it knows how to announce changes to any views that are watching, we have told the items
array that whenever it changes it should send out such an announcement, we have created an instance of the Order
object in our main app, and we have placed it into the SwiftUI environment for other views to use â nice!