I am following John Sundell's post to implement a Navigator pattern (https://www.swiftbysundell.com/posts/navigation-in-swift). The basic idea is that, in contrast to Coordinator pattern, each view controller could simply call navigator.navigate(to: .someScreen) without having to know other view controllers. 
My question is that, since in order to construct a view controller I need a navigator, to construct a navigator I need a navigation controller, but I want to make the view controller the root of the navigation controller, what's the best way to resolve this circular dependency in a way that respects the best practices of dependency injection?
Below is the idea of Navigator pattern as illustrated by Sundell
protocol Navigator {
    associatedtype Destination    
    func navigate(to destination: Destination)
}
class LoginNavigator: Navigator {
    enum Destination {
        case loginCompleted(user: User)
        case signup
    }
    private weak var navigationController: UINavigationController?
    private let viewControllerFactory: LoginViewControllerFactory
    init(navigationController: UINavigationController,
         viewControllerFactory: LoginViewControllerFactory) {
        self.navigationController = navigationController
        self.viewControllerFactory = viewControllerFactory
    }
    func navigate(to destination: Destination) {
        let viewController = makeViewController(for: destination)
        navigationController?.pushViewController(viewController, animated: true)
    }
    private func makeViewController(for destination: Destination) -> UIViewController {
        switch destination {
        case .loginCompleted(let user):
            return viewControllerFactory.makeWelcomeViewController(forUser: user)
        case .signup:
            return viewControllerFactory.makeSignUpViewController()
        }
    }
}
class LoginViewController: UIViewController {
    private let navigator: LoginNavigator
    init(navigator: LoginNavigator) {
        self.navigator = navigator
        super.init(nibName: nil, bundle: nil)
    }
    private func handleLoginButtonTap() {
        navigator.navigate(to: .loginCompleted(user: user))
    }
    private func handleSignUpButtonTap() {
        navigator.navigate(to: .signup)
    }
}
Now in AppDelegate I want to do something like 
let factory = LoginViewControllerFactory()
let loginViewController = factory.makeLoginViewController()
let rootNavigationController = UINavigationController(rootViewController: loginViewController)
window?.rootViewController = rootNavigationController
But I somehow have to pass the rootNavigationController into the factory in order for the loginViewController to be properly constructed right? Because it needs a navigator, which needs the navigation controller. How to do that?
I also was recently trying to implement Sundell's Navigator pattern and ran into this same circular dependency. I had to add some additional behavior to the initial Navigator to handle this odd bootstrap issue. I believe subsequent Navigators in your app can perfectly follow the blog's suggestion.
Here is the new initial Navigator code using JGuo's (the OP) example:
class LoginNavigator: Navigator {
    enum Destination {
        case loginCompleted(user: User)
        case signup 
    }
    private var navigationController: UINavigationController? 
    // This ^ doesn't need to be weak, as we will instantiate it here.
    private let viewControllerFactory: LoginViewControllerFactory
    // New:
    private let appWindow: UIWindow? 
    private var isBootstrapped = false 
    // We will use this ^ to know whether or not to set the root VC
    init(appWindow: UIWindow?, // Pass in your app's UIWindow from the AppDelegate
         viewControllerFactory: LoginViewControllerFactory) {
        self.appWindow = appWindow
        self.viewControllerFactory = viewControllerFactory
    }
    func navigate(to destination: Destination) {
        let viewController = makeViewController(for: destination)
        // We'll either call bootstrap or push depending on 
        // if this is the first time we've launched the app, indicated by isBootstrapped
        if self.isBootstrapped {
            self.pushViewController(viewController)
        } else {
            bootstrap(rootViewController: viewController)
            self.isBootstrapped = true
        }
    }
    private func makeViewController(for destination: Destination) -> UIViewController {
        switch destination {
        case .loginCompleted(let user):
            return viewControllerFactory.makeWelcomeViewController(forUser: user)
        case .signup:
            return viewControllerFactory.makeSignUpViewController()
        }
    }
    // Add these two new helper functions below:
    private func bootstrap(rootViewController: UIViewController) {
        self.navigationController = UINavigationController(rootViewController: rootViewController)
        self.appWindow?.rootViewController = self.navigationController
    }
    private func pushViewController(_ viewController: UIViewController) {
        // Setup navigation look & feel appropriate to your app design...
        navigationController?.setNavigationBarHidden(true, animated: false) 
        self.navigationController?.pushViewController(viewController, animated: true)
    }
}
And inside the AppDelegate now:
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let factory = LoginViewControllerFactory()
        let loginViewController = factory.makeLoginViewController()
        loginViewController.navigate(to: .signup) // <- Ideally we wouldn't need to signup on app launch always, but this is the basic idea.
        window?.makeKeyAndVisible()
        return true
    }
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With