NavigationCoordinator acts as a coordinator for NavigationView in SwiftUI

NavigationCoordinator






NavigationCoordinator acts as a coordinator for NavigationView. You can use pushView, popView, popToView, popToRootView in SwiftUI as you can in traditional UIKit

Installing

Swift Package Manager

You can install it using SPM

https://github.com/SNNafi/NavigationCoordinator.git

Usage

Suppose, you want to programmatically push a view or pop a view or pop to any specific view or pop to root view. There is no way to do this in SwiftUI. NavigationCoordinator is the best in this situation. It does not include any extra or unnecessary dependency, at the same time, it works with UINavigationController as NavigationView does. Better say, it adds extra functionalities for NavigationView with the help of battle tested UINavigationController.

It supports working with multiple UINavigationController. Suppose, when you will present a modal, you will need another UINavigationController for this to manage navigation between views that is independent from the view presenting it. So, you need two UINavigationController. And this package has build in supports for that. But there is a little work needed from your side. Let’s get started

First create an extension in NavigationControllerId

extension NavigationControllerId {
    // for any other navigation controller, just declare a `NavigationControllerId` for this to uniquely identify.
    // There is already `primaryNavigationController` for default navigation controller
    static let sheetNavigationController = "SheetNavigationController"
}

Suppose you have 5 view, HomeView, ListView, DetailView, SettingView, AboutView, ContactView, PrivacyView. Create an extension for them to uniquely identify them, so that we can pop to any view directly !

extension ViewId {
    static let homeView = "homeView"
    static let listView = "listView"
    static let detailView = "detailView"
    static let settingView = "settingView"
    static let aboutView = "aboutView"
    static let contactView = "contactView"
    static let privacyView = "privacyView"
}

You are ready to go

import SwiftUI
import NavigationCoordinator

struct ContentView: View {
    
    @Environment(\.navigationCoordinator) var navigationCoordinator
    
    var body: some View {
        NavigationView { // wrap in NavigationView
            VStack {
                Text("Hello, NavigationCoordinator Example!")
                    .padding()
                
                Button {
                    // push to HomeView
                    navigationCoordinator.pushView(withViewId: .homeView) {
                        HomeView()
                    }
                } label: {
                    Text("HomeView")
                }.padding()
            }
            .onAppear {
              
            }
            .navigationCoordinator(id: .primaryNavigationController) // initiate default navigation controller, do not call this again unless you need another navigation controller for modal
        }
    }
}

Suppose now by pushing, now you are in PrivacyView. That is like HomeView -> SettingView -> AboutView -> ContactView -> PrivacyView

Now you want to directly return to SettingView

import SwiftUI

struct SheetSixthView: View {
    
    @Environment(\.navigationCoordinator) var navigationCoordinator
  
    var body: some View {
        VStack {
            Text(String.currentFileName())
                .padding()
                .onTapGesture {
                    navigationCoordinator.popToView(viewId: .settingView) // this will do the rest for you
                }
        }
    }
}

What if you want to directly return to HomeView

import SwiftUI

struct SheetSixthView: View {
    
    @Environment(\.navigationCoordinator) var navigationCoordinator
  
    var body: some View {
        VStack {
            Text(String.currentFileName())
                .padding()
                .onTapGesture {
                    navigationCoordinator.popToRootView() // you are good to go
                }
        }
    }
}

Or, you just want to return to the previous view ?

import SwiftUI

struct SheetSixthView: View {
    
    @Environment(\.navigationCoordinator) var navigationCoordinator
  
    var body: some View {
        VStack {
            Text(String.currentFileName())
                .padding()
                .onTapGesture {
                     navigationCoordinator.popView() // are you still here ?
                }
        }
    }
}

And you need state based navigation ? NavigationLink is still here to rescue. As said, this package just coordinates, doesn’t take way any exisiting feature.

What about modal ? Suppose you need to use another navigation controller.

import SwiftUI
import NavigationCoordinator

struct ModalView: View {
    
    @Environment(\.navigationCoordinator) var navigationCoordinator
    
    var body: some View {
        NavigationView { // wrap in NavigationView
            VStack {
                Text("Hello, NavigationCoordinator Example!")
                    .padding()
                
                Button {
                    // push to SomeView
                    navigationCoordinator.pushView(withViewId: .someView) {
                        SomeView()
                    }
                } label: {
                    Text("SomeView")
                }.padding()
            }
             .navigationCoordinator(id: .sheetNavigationController) // initiate another navigation controller
        }
    }
}

Important: When presenting this using sheet, need to set id back to the .primaryNavigationController in onDismiss . Like that,

 .sheet(isPresented: $isShow, onDismiss: { NavigationCoordinator.currentNavigationControllerId = .primaryNavigationController }, content: {
            ModalView()
        })

And, the rest thing as you do in previous steps.

Inject NavigationCoordinator

By default you can programmatically push and pop only inside the NavigationView hierarchy (by accessing the NavigationCoordinator environment). But you can do it from outside also

But there are few things you need to keep in mind

No matter what, you always need to initiate a navigation controller by using this .navigationCoordinator(id:) in view hierarchy. But it must need to perform in a view which is wrapped into a NavigationView

Like,

 NavigationView {
     SomeView()
         .navigationCoordinator(id: .primaryNavigationController) // this is a must. Otherwise, cannot track down the underlying UINavigationController
 }

Then you can do,

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    static let navigationCoordinator = NavigationCoordinator()

    .....
    .....
    let mainView = MainView()
        .environment(\.navigationCoordinator, Self.navigationCoordinator) // this is not necessary. But if you need, you can set it to use from another non view type like view model or router


   // Use a UIHostingController as window root view controller.
      if let windowScene = scene as? UIWindowScene {
          let window = UIWindow(windowScene: windowScene)
          window.rootViewController = UIHostingController(rootView: mainView)
          self.window = window
          window.makeKeyAndVisible()
      }
    .....
    .....
}


struct ContentView: View {

    var body: some View {
        NavigationView {
            HomeView(router: DefaultRouter())
                .navigationCoordinator(id: .primaryNavigationController)
        }
    }
}

class DefaultRouter {
    
    func navigateToList() {
        SceneDelegate.navigationCoordinator.pushView(withViewId: .fourthView) {
            FourthView()
        }
    }

    func navigateToDetail() {
        SceneDelegate.navigationCoordinator.pushView(withViewId: .fifthView) {
            FifthView()
        }
    }
}

struct HomeView: View {

    let router: DefaultRouter

    var body: some View {
        VStack {
            Text("HomeView")
            Button("ListView") {
                router.navigateToList()
            }
            Button("DetailView") {
                router.navigateToDetail()
            }
        }
    }
}

Status

This is an active project. I will try to add new features.

GitHub

View Github