In the previous post of this series I discussed how important it is for views to clearly define their dependencies in a way that can be stubbed out. We did that by making our view generic over a view model protocol, then having both a production and a stub version of the view model. It looked something like this:

protocol PokemonListViewModelProtocol: Observable {
    var pokemonList: [Pokmeon] { get }
    func fetchPokemon() async
}

struct PokemonList<ViewModel: PokemonListViewModelProtocol>: View {
    @State var viewModel: ViewModel
    
    init(viewModel: ViewModel) {
        _viewModel = State(wrappedValue: viewModel)
    }
    
    // ... view stuff
}

@Observable
final class StubPokemonListViewModel: PokemonListViewModelProtocol {
    let pokemonList = Pokemon.list
    func fetchPokemon() async { /* no-op */ }
}

// Production view model omitted for brevity

#Preview {
    PokemonList(viewModel: StubPokemonListViewModel())
}

This works great; it allows us to run a preview without ever running a single line of view model code, allowing us to focus wholly on writing our view. This view shows a simple list of Pokémon.

Composing Views

Suppose, however, that I wanted to add a detail view to the list. I’d like to tap on one of the rows in the Pokémon list and go to a detail view where I can see that Pokémon’s sprite as well as some battle stats. I might make a PokemonDetailView similar to the list view above with all the bells and whistles we discussed in the previous post, including its own view model protocol which we will aptly name PokemonDetailViewModelProtocol. Remember, this protocol is just an abstraction that allows the view to say “these are the things I need to function and I don’t care where they come from,” which allows us to stub it out for the purposes of previewing.

If we were to plug together our existing PokemonListView with the new PokemonDetailView, it might look like something you’ve probably written before:

struct PokemonList<ViewModel: PokemonListViewModelProtocol>: View {
    // ... view model declaration + initialization omitted
    
    var body: some View {
        List {
            ForEach(pokemonList) { pokemon in
                NavigationLink(pokemon.name) {
                    PokemonDetailView(
                        viewModel: PokemonDetailViewModel(pokemon)
                    )
                }
            }
        }
    }
}

A New Problem

This seems reasonable, but we’ve actually hit a problem. What happens if we try to run the preview for PokemonListView? One way to think about our efforts this whole time is that we are trying our best avoid running any non-view code in our previews. But observe the following bit of code from the snippet above:

PokemonDetailView(
    viewModel: PokemonDetailViewModel(pokemon)
)

You’ll notice that regardless of what kind of view model we pass into PokemonList, we are always using a production view model for PokemonDetailView (notice the lack of Stub in the view model name, which I have been using to indicate the test/stub version of the view model protocols).

Unexpected Dependencies

To rectify this, it’s important to make a key observation: the view models for child views are a dependency of the parent view. In this example, PokemonDetailViewModel is a dependency of PokemonListView, since PokemonListView is the one instantiating it and passing it to the child view. Since it’s a dependency, we should include it in our view model protocol, which requires getting a little fancy with the type system. It would look something like this:

protocol PokemonListViewModelProtocol: Observable {
    associatedtype DetailVM: PokemonDetailViewModelProtocol // NEW

    var pokemonList: [Pokmeon] { get }
    func fetchPokemon() async
    func detailViewModel(_ pokemon: Pokemon) -> DetailVM
}

In order to preserve concrete types through a protocol, we introduce an associated type that allows each version of the view model protocol to define its own detail type. Basically, we’re trying to add the ability for a production view model to say “I want to use a production detail view model” and for stub view models to say “I want to use a stub detail view model”. Lets implement this function in both the production and stub view models:

extension PokemonListViewModel { // Production View Model
    func detailViewModel(_ pokemon: Pokemon) -> PokemonDetailViewModel {
        PokemonDetailViewModel(pokemon)
    }
}

extension StubPokemonListViewModel { // Stub View Model
    func detailViewModel(_ pokemon: Pokemon) -> StubPokemonDetailViewModel {
        StubPokemonDetailViewModel()
    }
}

Now inside our view, we simply replace the hardcoded initialization of the detail view model with one that we ask our view model for:

struct PokemonList<ViewModel: PokemonListViewModelProtocol>: View {
    // ... view model declaration + initialization omitted
    
    var body: some View {
        List {
            ForEach(pokemonList) { pokemon in
                NavigationLink(pokemon.name) {
                    PokemonDetailView(
                        viewModel: viewModel.detailViewModel(pokemon) // NEW
                    )
                }
            }
        }
    }
}

View Model Hierarchy

When we run our previews now, we are once again free of any non-view code and we have successfully abstracted away the view model dependency on our views. What we have effectively done is we’ve introduced a view model hierarchy that matches our view hierarchy to some degree.

A box diagram describing the relationship between views and view models, with views on the lefthand side creating a dependency chain, with some of them having associated view models on the righthand side, also forming their own independent dependency chain.

This technique is a huge win because it enables us to have views that are easy to preview and test, while also allowing us to break up view model functionality in a way that makes sense for our app. This is the technique that I use most often when building apps because it enables this flexibility with minimal magic, relying on the compile-time safety of Swift’s type system.

This technique, however, isn’t a panacea for all apps. This kind of coupling can prove cumbersome in projects whose view models have a large number of views between them in the view hierarchy. In these cases, you’d be plumbing view models through views which don’t care about the view model at all. For these projects, keep an eye out for Part 3 of this series of blog posts where I will talk more about this problem and a possible solution.