iOS

Using View Model Protocols to manage complex SwiftUI views

March 11, 2021
Using View Model Protocols to manage complex SwiftUI views

The problem

Managing complex screens or views that depend on asynchronous services or the need to pull in state from across your app can be tricky to get right. The most common way to address this in SwiftUI is by abstracting that logic into a dedicated view model for that piece of UI. Usually, the view model and view will look something like this:

```

struct ComplexView: View {

@ObservableObject var viewModel: ComplexViewModel

var body: some View {

ScrollView {

if case .loaded(let items) = viewModel.state {

VStack {

ForEach(items) { item in

ItemView(item: item)

}

}

} else {

LoadingView()

}

}

.onAppear { viewModel.load() }

}

}

```

```

final class ComplexViewModel: ObservableObject {

@Published var state: LoadingState = .loading

func load() {

// Load some data

// URLSession.shared.dataTaskPublisher ...

}

}

```

This abstraction is good - it makes the screen function - but it has one major flaw: you can no longer create a meaningful preview of `ComplexView`. The view requires an instance of its `ComplexViewModel` in order to function and as written that view model depends on the network to load its data. You will encounter a similar problem if you have a view model that is backed by CoreData or some other persistence layer.

A solution

One possible solution would be to allow creating a `ComplexViewModel` instance with a mocked out Network or Persistence layer. While this may work, I find the extra ceremony needed to maintain that abstraction a little cumbersome. All we really need is a way to provide data to a `ComplexView` instance. The View itself doesn't need to know how that data got there. We can accomplish this by hiding our view model behind a protocol.

`ComplexViewModel` is now a protocol

```

protocol ComplexViewModel: ObservableObject {

var state: LoadingState { get }

func load()

}

```

The content of `ComplexView` is largely the same, but now must be generic over its view model type because the `ComplexViewModel` protocol extends `ObservableObject` which has an associated type and therefore can only be used as a generic constraint.

```

struct ComplexView<ViewModel: ComplexViewModel>: View {

@ObservableObject var viewModel: ViewModel

// same as before

}

```

The old view model class has been renamed to `NetworkBackedComplexViewModel` as a concrete implementation of `ComplexViewModel` that loads from the network

```

final class NetworkBackedComplexViewModel: ComplexViewModel {

// same as before

}

```

We can similarly create another class that implements the `ComplexViewModel` protocol for use in previews.

```

final class PreviewComplexViewModel: ComplexViewModel {

let state: LoadingState

init(state: LoadingState) {

self.state = state

}

func load() { } // do nothing

}

```

## **Takeaway**

With this protocol abstraction we can now create meaningful previews of our otherwise complex screens, even setting up multiple variations of `PreviewComplexViewModel` in different states.

```

struct ComplexView_Previews: PreviewProvider {

static var previews: some View {

Group {

ComplexView(

viewModel: PreviewComplexViewModel(state: .loading)

)

ComplexView(

viewModel: PreviewComplexViewModel(state: .loaded([]))

)

}

}

}

```

I think this makes for a nice way to reason about a screen:

  • Describe the requirements for a piece of UI via a protocol
  • Write the view code against that protocol
  • Create a class that implements the protocol and pulls data from elsewhere in your app

We can leverage this decoupling of our screen from how it gets its data by creating additional flavors of view model (`CacheBackedComplexViewModel`, `DatabaseBackedComplexViewModel`, etc.). With this we are enable to reuse a complex piece of UI in different contexts. You can potentially even hide these implementations behind a Swift package if you wanted to keep your UI and networking or persistence layers decoupled in that manner.

This idea is still a little abstract but I want to put it out there as we continue to better understand and use SwiftUI. What do you think? How are you managing complex UI in your SwiftUI apps?

Jeff Kloosterman
Jeff Kloosterman
Head of Product Development

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

The impact of tech stack choices on software ROI
Android Development iOS

The impact of tech stack choices on software ROI

October 30, 2024

Choosing the right tech stack is key to optimizing software performance, scalability, and maintenance costs. Learn how we work side-by-side with our clients to make sure their tech stack choices line up with their business goals, budget, and long-term vision.

Read more
MichiganLabs’ approach to product strategy: Driving software success
Process Team

MichiganLabs’ approach to product strategy: Driving software success

February 12, 2024

Read more
A Study of Human-Centered Design Off Screen - Burning Man Festival
Design

A Study of Human-Centered Design Off Screen - Burning Man Festival

February 13, 2023

Kourtney examines the Burning Man Festival and its fascinating history of human-centered design

Read more
View more articles