Mastering Navigation in SwiftUI: Implementing the Coordinator Pattern

Muhammad Alfiansyah
3 min readJul 9, 2024

--

Photo by Annie Spratt on Unsplash

Not to long ago, I wrote content about SwiftUI but there is an issue with Navigation Stack implementation, so now we will give you example how to implement Coordinator Pattern in SwiftUI 1.0.

As we know so far, for building navigation in SwiftUI 1.0 is challenging because for navigating to other page we implement NavigationLink on the View and for Clean Architecture this implementation is quite bad.

For handle that we use SRP (Single Responsibility Principle). We will separate Navigation to Coordinator.

To handle this we will create Coordinator protocol like this

import UIKit
import SwiftUI

protocol Coordinator {
var childCoordinator: [Coordinator] { get set }
var navigationController: UINavigationController { get set }

func start()
}

In Our App we want to use TabBarController so we need create MainCoordinator to handle this but we wont implement Coordinator protocol for TabBar

final class MainCoordinator {
var childCoordinator: [Coordinator] = [Coordinator]()
var tabBarController: UITabBarController = {
let tabBarController = UITabBarController()
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .white

tabBarController.tabBar.standardAppearance = appearance
if #available(iOS 15.0, *) {
tabBarController.tabBar.scrollEdgeAppearance = appearance
}
return tabBarController
}()

func start() {
let homeCoordinator = HomeCoordinator(navigationController: UINavigationController())
homeCoordinator.start()
homeCoordinator.navigationController.tabBarItem = UITabBarItem(title: "Home",
image: UIImage(systemName: "house.circle"),
selectedImage: UIImage(systemName: "house.circle.fill"))
self.childCoordinator.append(homeCoordinator)

let searchCoordinator = SearchCoordinator(navigationController: UINavigationController())
searchCoordinator.start()
searchCoordinator.navigationController.tabBarItem = UITabBarItem(title: "Search",
image: UIImage(systemName: "magnifyingglass.circle"),
selectedImage: UIImage(systemName: "magnifyingglass.circle.fill"))
self.childCoordinator.append(searchCoordinator)

let profileCoordinator = ProfileCoordinator(navigationController: UINavigationController())
profileCoordinator.start()
profileCoordinator.navigationController.tabBarItem = UITabBarItem(title: "About",
image: UIImage(systemName: "person.circle"),
selectedImage: UIImage(systemName: "person.circle.fill"))
self.childCoordinator.append(profileCoordinator)

self.tabBarController.viewControllers = [
homeCoordinator.navigationController,
searchCoordinator.navigationController,
profileCoordinator.navigationController
]
}
}

Start method works for building TabBarController, we will register all child coordinator to this MainCoordinator.

Like code above we have 3 Coordinator :

  • HomeCoordinator
  • SearchCoordinator
  • ProfileCoordinator
import UIKit
import SwiftUI

final class HomeCoordinator: Coordinator {
var childCoordinator: [Coordinator] = [Coordinator]()
var navigationController: UINavigationController

init(navigationController: UINavigationController) {
self.navigationController = navigationController
}

func start() {
let view = Injection.shared.container.resolve(HomePageView.self, argument: self)
let viewController = UIHostingController(rootView: view)

self.navigationController.pushViewController(viewController, animated: false)
}

func goToDetail(with model: MovieModel) {
let view = Injection.shared.container.resolve(DetailPageView.self, argument: model)
let viewController = UIHostingController(rootView: view)
viewController.hidesBottomBarWhenPushed = true

self.navigationController.pushViewController(viewController, animated: true)
}
}

As you can see, we use Swinject as Dependency Container so we just need to resolve HomePageView and assign it to UIHostingViewController.

Why we use UIHostingViewController ?

Just because on SwiftUI 1.0 has NavigationView separate Navigation Stack become easy, In the real implementation I having trouble to separate them into another class so we use UIKit UIHostingViewController to convert View into UIViewController so we can treat SwiftUI like UIKit.

Then for calling MainCoordinator to our view we just change SceneDelegate just like this

import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?

func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
let tabBarController = MainCoordinator()
tabBarController.start()
let window = UIWindow(windowScene: windowScene)
window.rootViewController = tabBarController.tabBarController
self.window = window
window.makeKeyAndVisible()
}
}

Done, with this implementation our code more SOLID. And not just that we still can use Navigation with SwiftUI like alert etc.

.alert(isPresented: $presenter.isError) {
Alert(
title: Text("Error"),
message: Text(presenter.errorMessage ?? "Unknow")
)
}

--

--

No responses yet