SwiftUI Coordinator. Part 1: TabView and NavigationStack


Greetings, traveler!

Today, we will discuss the coordinator pattern and why and when we should use it. The coordinator pattern helps us handle navigation more transparently and conveniently. Soroush Khanlou proposed it in 2015 for the UIKit framework. After Apple had presented SwiftUI, many programmers attempted to adapt it to this pattern, but it became clear that the process was more complex than initially thought. So to date, there is a lack of examples of SwiftUI Coordinator implementation.

With the introduction of the NavigationStack in iOS 16, using the coordinator pattern became even more accessible. To make it work, we can initialize the navigation stack with an array of entities representing the desired destinations. We can store this data with the array in our view. Thus, we trigger the navigation action when adding or removing an entity from this array.

The coordinator pattern is a convenient way for programmatic navigation. We only need to achieve code organization to provide a scalable navigation architecture, a universal pattern suitable for all our modules.

Basics

This series will create one convenient, flexible, and universal approach. We will need a few guys to handle all the navigation situations. The first one will be responsible for the transition between modules and accessible from all views of the coordinator flow. We will create it as an environment object in the new SwiftUI approach and call it a router. It will store our navigation stack and receive commands from outside to manage the navigation actions.

The second guy is the coordinator. The coordinator will also be a SwiftUI View. It will use a router to choose a specific destination for every navigation case.

Moreover, we will cover additional aspects of app design and create a fully functional application covering different cases using an adult approach to design. Our app will have several screens, one representing a list of items with text called posts and the second showing the details of the chosen item. We will also cover some cases where we need to display modally presented screens and use a tab bar to highlight specific instances of using it with the coordinator pattern. And yes, we will use the MVVM architecture.

TabView

Let’s start with the tab view. To keep it, we should create a SwiftUI View, AppTabView, and then create a TabView inside of it.

struct AppTabView: View {

    @State private var selection = 0
    
    var body: some View {
        TabView(selection: $selection) {
            
        }
    }
    
}

Navigation

Our next step is to create a view with a NavigationStack to put inside the first tab of the TabView. However, at this step, we will encounter one small problem. We can’t use a tab item if we create a NavigationStack with a navigation destination. We can make a primary coordinator for this tab and all its child views as a solution.

struct PostListTabCoordinator: View {

		@State private var path = [Int]() // This code will be changed soon.
        
    var body: some View {
        NavigationStack(path: $path) {
            
        }
    }
    
}

Since we will create a postlist as the primary screen of our app, we can call it PostListTabCoordinator. It has two purposes: to keep the NavigationStack and to have a child coordinator in it.

Now, we can create a PostListCoordinator. This coordinator’s primary responsibility is to show a list of posts and navigate us to the post details screen.

struct AppTabView: View {

    @State private var selection = 0
    
    var body: some View {
        TabView(selection: $selection) {
            PostListTabCoordinator()
                .tabItem {
                    Image(systemName: "house")
                    Text("Home")
                        .tag(0)
                }
        }
    }
    
}

struct PostListTabCoordinator: View {

		@State private var path = [Int]() // This code will be changed soon.
        
    var body: some View {
        NavigationStack(path: $path) {
            PostListCoordinator()
        }
    }
    
}

struct PostListCoordinator: View {
        
    var body: some View {
        EmptyView()
    }
    
}

Conclusion

That’s it for now. We have confidently built the foundation for our application, and we can now proceed with the rest of the development with great assurance. Next, we will make the first screen with the post list. If you are looking for the source code — here it is!

See you in the next lesson.

Check out other posts in the SwiftUI Coordinator series: