#2 – The SwiftUI coordinator

Coordinators are primarily used for navigation, but another essential responsibility of the coordinator is dependency management. After all, to navigate to a new scene, you need to first ensure that all dependencies are provided.

So let’s build on the previous post where we created a scene consisting of a coordinator, a view, and a view model. Feel free to check out our previous work if you haven’t been following along:

https://github.com/benmakesapps/BKSwifty/tree/1

You may have noticed that in our PeopleCoordinator, I placed the PeopleView inside a NavigationStack.

struct PeopleCoordinator: View {
    var body: some View {
        NavigationStack {
            PeopleView(viewModel: createViewModel())
        }
    }
    
    func createViewModel() -> PeopleView.ViewModel {
        .init()
    }
}

This setup is because when I tap on a person in my list, I want to navigate to a detailed view for that specific person. Since the coordinator is responsible for navigation, the best place to manage this is within the coordinator itself.

However, to actually navigate, we also need a NavigationLink. Since we need a separate NavigationLink for each person in our list, we’ll declare that in our view’s ForEach loop. Let’s update that.

struct PeopleView: View {
    @State var viewModel: ViewModel
    
    var body: some View {
        List {
            ForEach(viewModel.people, id: \.self) { person in
                NavigationLink(person) {
                    Text(person)
                }
            }
        }
    }
}

Now, when I tap on a person in my list, the app navigates to a new screen that consists solely of the person’s name in text.

Note that even though we are showing detail views in a NavigationStack, it would be just as easy to present them in a modal sheet instead. I won’t go into that, but maybe give it a shot on your own if you’re interested! (Hint: use the .sheet modifier.)

Looking good so far!

But we could use some improvements. Namely, this destination view with just text doesn’t fit the pattern we’ve established. Ideally, every “screen” would be a scene.

Let’s create a new scene called “Person”, consisting of a PersonCoordinator, a PersonView, and a PersonViewModel.

struct PersonCoordinator: View {
    let person: String
    
    var body: some View {
        PersonView(viewModel: createViewModel())
    }
    
    func createViewModel() -> PersonView.ViewModel {
        .init(person: self.person)
    }
}
struct PersonView: View {
    @State var viewModel: ViewModel
    
    var body: some View {
        Text(viewModel.person)
    }
}
extension PersonView {
    @Observable
    class ViewModel {
        let person: String
        
        init(person: String) {
            self.person = person
        }
    }
}

And now we will update our PeopleView to declare our PersonCoordinator instead of plain text.

struct PeopleView: View {
    @State var viewModel: ViewModel
    
    var body: some View {
        List {
            ForEach(viewModel.people, id: \.self) { person in
                NavigationLink(person) {
                    PersonCoordinator(person: person)
                }
            }
        }
    }
}

Notice how the person is injected into the PersonCoordinator, which in turn injects it into the PersonViewModel.

Hopefully, now you are starting to see the power of this pattern. There is quite a bit more we can do, which I’ll cover soon. In the meantime, I plan on writing a bonus post on how to automate the creation of a scene to reduce the need for writing boilerplate. Stay tuned.

Updated code:

https://github.com/benmakesapps/BKSwifty/tree/2


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *