Building Robust ViewModels

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

Lesson 01: Designing Powerful ViewModels

Demo

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll move some data-handling code from a view into a view model class.

Open TheMet app in the Starter folder.

Open ContentView and refresh the preview.

This app lets the user search The Metropolitan Museum of Art, New York for objects matching a query term. The app starts with “rhino” as the query term. Try searching for another term:

Some objects are in the public domain, so the app can download and display an image.

The detail view has a link to the object’s web page. This works in a simulator but not in the preview.

Other objects aren’t in the public domain, so the app loads the web page.

Network requests are handled by TheMetService.

This service has two methods: getObjectIDs from queryTerm and getObject from objectID.

If the user doesn’t enter a query term, the app searches for “the”.

The app downloads, at most, 20 objectIDs to avoid hammering the server.

ContentView maintains the array of objects as a State variable.

And has a method to call the network service methods. This data handling shouldn’t be in the view; it belongs in a ViewModel.

Create a new Swift file named TheMetStore and create a group named ViewModel to contain it.

In this new file, create an Observable class:

@Observable
class TheMetStore {

}

For now, you don’t need to worry about what @Observable means, you’ll learn all about it in the next lesson.

Now, move the first three properties from ContentView to TheMetStore. Remove @State and don’t initialize maxIndex:

var objects: [Object] = []
private let service = TheMetService()
let maxIndex: Int

Add an init method:

init(_ maxIndex: Int = 20) {
  self.maxIndex = maxIndex
}

TheMetStore can be instantiated without a maxIndex parameter. The default value is 20.

Next, move fetchObjects from ContentView to TheMetStore:

And that’s all you need for your view model.

Now, go back to ContentView, to use this. Add a State variable:

@State var store = TheMetStore()

Then, wherever Xcode complains about objects not in scope, insert store.:

List(store.objects, id: \.objectID) { object in

And also for fetchObjects:

fetchObjectsTask = Task {
  do {
    store.objects = []
    try await store.fetchObjects(for: query)
  } catch {}
}

And right down at the bottom:

  .overlay {
    if store.objects.isEmpty { ProgressView() }
  }
}
.task {
  do {
    try await store.fetchObjects(for: query)
  } catch {}
}

That’s it! Refresh the preview and check it all works.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion