iOS

Using View Model Protocols to manage complex SwiftUI Views

March 11, 2021

051 0 U0 A0570

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?

Ready to get started?

Call us at 616-594-0269 or send us a note below.
Visit our office @ 452 Ada Drive SE Suite 300 Ada, Michigan 49301
Send us an e-mail @ info@michiganlabs.com