首页 > 解决方案 > 如何正确实现导航器模式

问题描述

我正在关注 John Sundell 的帖子以实现导航器模式 ( https://www.swiftbysundell.com/posts/navigation-in-swift )。基本思想是,与协调器模式相比,每个视图控制器都可以简单地调用navigator.navigate(to: .someScreen),而不必知道其他视图控制器。

我的问题是,因为为了构造一个视图控制器我需要一个导航器,所以要构造一个导航器我需要一个导航控制器,但我想让视图控制器成为导航控制器的根,解决这个问题的最佳方法是什么以尊重依赖注入最佳实践的方式循环依赖?

下面是 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)
    }
}

现在AppDelegate我想做类似的事情

let factory = LoginViewControllerFactory()
let loginViewController = factory.makeLoginViewController()
let rootNavigationController = UINavigationController(rootViewController: loginViewController)
window?.rootViewController = rootNavigationController

但是我必须以某种方式将其传递rootNavigationController到其中factory才能loginViewController正确构建?因为它需要一个导航器,它需要导航控制器。怎么做?

标签: swiftdependency-injectionfactorycircular-dependency

解决方案


我最近也在尝试实现 Sundell 的导航器模式并遇到了同样的循环依赖。我不得不在初始 Navigator 中添加一些额外的行为来处理这个奇怪的引导问题。我相信您应用中的后续导航器可以完美地遵循博客的建议。

这是使用 JGuo(OP)示例的新初始 Navigator 代码:

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

现在在 AppDelegate 内部:

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
    }
...
}

推荐阅读