Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly implement Navigator pattern

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

Navigator

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()
        }
    }
}

View Controller

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?

like image 752
Jack Guo Avatar asked Jul 08 '18 03:07

Jack Guo


1 Answers

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
    }
...
}
like image 150
jmk Avatar answered Dec 31 '22 05:12

jmk