swift - 如何正确实现导航器模式

标签 swift dependency-injection factory circular-dependency

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

我的问题是,因为为了构造一个 View Controller 我需要一个导航器,为了构造一个导航器我需要一个导航 Controller ,但是我想让 View Controller 成为导航 Controller 的根,最好的方法是什么以尊重依赖注入(inject)最佳实践的方式解决这种循环依赖?

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

现在在 AppDelegate 中我想做类似的事情

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

但我必须以某种方式将 rootNavigationController 传递到 factory 中,以便正确构造 loginViewController 对吗?因为它需要导航器,导航器需要导航 Controller 。如何做到这一点?

最佳答案

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

这是使用 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
    }
...
}

关于swift - 如何正确实现导航器模式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51228633/

相关文章:

ios - NSObject 无法转换

单元测试中的 C++ 和依赖注入(inject)

ios - 如何在我的工厂解析器中注册 2 个具有相同抽象接口(interface)的依赖项?

c++ - 我必须从工厂返回一个指针吗?

ios - Swift:植入 Core Data 数据模型的最简单方法

generics - 如何在 Swift 中的泛型类上实现 NSCoding?

swift - 使用 Kingfisher 时重新下载不同尺寸的图像

laravel中的sql注入(inject)预防

java - 使用 Dagger 2.0 仅在两个 Activity 之间共享对象

java - 如何动态注入(inject)spring bean(原型(prototype)范围)