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
The Measure Of Success: Staying On Track From The Very Beginning
Process

The Measure Of Success: Staying On Track From The Very Beginning

July 6, 2020

Any successful project begins with a clearly defined problem to solve (Are You Solving the Right Problem?). Project stakeholders need to be able to articulate the overriding challenge succinctly from the very start. (Spoiler: this often involves narrowing your vision, and may include…gasp…removing functionality.)

Read more
Michigan Software Labs breaks ground on new office
Press Release

Michigan Software Labs breaks ground on new office

June 19, 2020

Michigan Software Labs recently broke ground on a new office building in its hometown of Ada. The 16,500-square-foot, three-story building will have office space for the growing software company and support up to 75 team members. The company currently is hiring for developers, UX designers and project managers.

Read more
View more articles