Building Robust ViewModels

Feb 28 2025 · Swift 5.9, iOS 17, Xcode 15.3

Lesson 03: Data Binding Techniques

Demo 1

Episode complete

Play next episode

Next
Transcript

Open TheMet app in the Starter folder. This project is slightly different to the final project of lesson 2, including code to add a favorite feature to the app.

In Lesson 2, you instantiated TheMetStore in TheMetApp as a @State variable:

@State var store = TheMetStore()
var body: some Scene {
  WindowGroup {
    ContentView(store: store)
  }
}

Then, you passed store as a parameter to ContentView.

In ContentView, you declared store like an ordinary variable without any property wrapper:

var store: TheMetStore

TheMetStore conforms to Observable, and ContentView only reads values that store publishes.

So, when you enter a new query term, the List observes the change in store.objects and redraws itself.

Subview of ObjectView Needs to Access Store

The List in ContentView passes an object to ObjectView or a URL to SafariView — neither view needs access to store.

Now, open ObjectView. Suppose you want to create a subview here to display the number of retrieved objects. Create a new SwiftUI View file named CountView.

Instead of the placeholder “Hello, World!”, you want the Text string to be:

Text("store.objects.count objects in store")

But, you want to use the interpolated value of store.objects.count, so CountView needs access to the view model store.

Here’s your opportunity to inject an instance of the view model into your app’s environment and then retrieve it from the environment only where it’s needed.

This way, you don’t need to pass store from ContentView to ObjectView, just so ObjectView can pass it to CountView.

Before you do this, uncomment CountView in ObjectView, so it’s ready for the grand reveal at the end of this demo.

@Environment

First, in TheMetApp, replace ContentView(store: store) with:

ContentView()
  .environment(store)

Now, quickly to ContentView to fix this error, replace var store with:

@Environment(TheMetStore.self) var store

And also fix the preview:

ContentView()
  .environment(TheMetStore())

Check that everything still works here.

Great! Now, to share this environment object with CountView, you’ll need the same declaration. So, scroll up and copy the @Environment declaration, then head over to CountView.

Paste the @Environment declaration, then fix the string to use the interpolated value:

Text("\(store.objects.count) objects in store")

And fix the preview. Add this line, then copy it:

#Preview {
  CountView()
    .environment(TheMetStore())
}

Go back to ObjectView. The preview in ObjectView is separate from the app’s view hierarchy, so it also needs the store environment variable, in order to display CountView(). In the preview, paste the .environment line you copied from CountView, then check that it works by refreshing.

#Preview {
  ...
  return ObjectView(object: object)
    .environment(TheMetStore())
}

It works, there’s the CountView text! Now, back to ContentView to check everything works.

And there’s the CountView in ObjectView, displaying a value it got from the environment.

Now, what about two-way binding? Keep reading to find out!

See forum comments
Cinema mode Download course materials from Github
Previous: View Models & Environment Property Instruction Next: Instruction