iOS

Using View Model Protocols to manage complex SwiftUI Views

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

The Prob­lem #

Man­ag­ing com­plex screens or views that depend on asyn­chro­nous ser­vices or the need to pull in state from across your app can be tricky to get right. The most com­mon way to address this in Swif­tUI is by abstract­ing that log­ic into a ded­i­cat­ed view mod­el for that piece of UI. Usu­al­ly, the view mod­el and view will look some­thing 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 abstrac­tion is good — it makes the screen func­tion — but it has one major flaw: you can no longer cre­ate a mean­ing­ful pre­view of ComplexView. The view requires an instance of its ComplexViewModel in order to func­tion and as writ­ten that view mod­el depends on the net­work to load its data. You will encounter a sim­i­lar prob­lem if you have a view mod­el that is backed by Core­Da­ta or some oth­er per­sis­tence layer.

A Solu­tion #

One pos­si­ble solu­tion would be to allow cre­at­ing a ComplexViewModel instance with a mocked out Net­work or Per­sis­tence lay­er. While this may work, I find the extra cer­e­mo­ny need­ed to main­tain that abstrac­tion a lit­tle cum­ber­some. All we real­ly need is a way to pro­vide data to a ComplexView instance. The View itself doesn’t need to know how that data got there. We can accom­plish this by hid­ing our view mod­el behind a protocol.

ComplexViewModel is now a protocol

protocol ComplexViewModel: ObservableObject {
  var state: LoadingState { get }
  func load()
}

The con­tent of ComplexView is large­ly the same, but now must be gener­ic over its view mod­el type because the ComplexViewModel pro­to­col extends ObservableObject which has an asso­ci­at­ed type and there­fore can only be used as a gener­ic constraint.

struct ComplexView<ViewModel: ComplexViewModel>: View {
  @ObservableObject var viewModel: ViewModel
  // same as before
}

The old view mod­el class has been renamed to NetworkBackedComplexViewModel as a con­crete imple­men­ta­tion of ComplexViewModel that loads from the network

final class NetworkBackedComplexViewModel: ComplexViewModel {
  // same as before
}

We can sim­i­lar­ly cre­ate anoth­er class that imple­ments the ComplexViewModel pro­to­col for use in previews.

final class PreviewComplexViewModel: ComplexViewModel {
  let state: LoadingState

  init(state: LoadingState) {
    self.state = state
  }

  func load() { } // do nothing
}

Take­away #

With this pro­to­col abstrac­tion we can now cre­ate mean­ing­ful pre­views of our oth­er­wise com­plex screens, even set­ting up mul­ti­ple vari­a­tions of PreviewComplexViewModel in dif­fer­ent 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 rea­son about a screen:

  • Describe the require­ments for a piece of UI via a protocol
  • Write the view code against that protocol
  • Cre­ate a class that imple­ments the pro­to­col and pulls data from else­where in your app

We can lever­age this decou­pling of our screen from how it gets its data by cre­at­ing addi­tion­al fla­vors of view mod­el (CacheBackedComplexViewModel, DatabaseBackedComplexViewModel, etc.). With this we are enable to reuse a com­plex piece of UI in dif­fer­ent con­texts. You can poten­tial­ly even hide these imple­men­ta­tions behind a Swift pack­age if you want­ed to keep your UI and net­work­ing or per­sis­tence lay­ers decou­pled in that manner.

This idea is still a lit­tle abstract but I want to put it out there as we con­tin­ue to bet­ter under­stand and use Swif­tUI. What do you think? How are you man­ag­ing com­plex UI in your Swif­tUI apps?

Jeff Kloosterman
Jeff Kloosterman
Head of Client Services

Stay in the loop with our latest content!

Select the topics you’re interested to receive our new relevant content in your inbox. Don’t worry, we won’t spam you.

Michigan Software Labs #65 on Inc. Regionals Fastest-Growing Companies
Press Release

Michigan Software Labs #65 on Inc. Regionals Fastest-Growing Companies

March 11, 2022

Inc. magazine today revealed that Michigan Software Labs is No. 65 on its third annual Inc. 5000 Regionals Midwest list, the most prestigious ranking of the fastest-growing private companies based in Iowa, Illinois, Indiana, Kansas, Michigan, Minnesota, Missouri, North Dakota, Nebraska, Ohio, South Dakota, and Wisconsin. Born of the annual Inc. 5000 franchise, this regional list represents a unique look at the most successful companies within the Midwest region economy’s most dynamic segment–its independent small businesses.

Read more
Drinks on the Deck
Team

Drinks on the Deck

July 26, 2021

You're Invited to Drinks on the Deck 2021! Join us and about 250 other friends for Drinks on the Deck 2021.

Read more
Michigan Software Labs joins the Inc. 5000 list of fastest growing companies in the U.S.
Press Release

Michigan Software Labs joins the Inc. 5000 list of fastest growing companies in the U.S.

August 12, 2020

Michigan Software Labs has earned its first recognition in Inc. magazine’s influential Inc. 5000 list. The list represents a unique look at the most successful companies within the American economy’s most dynamic segment—its independent small businesses.

Read more
View more articles