返回

iOS 获取 Presented View Controller 的最佳实践

IOS

深层链接跳转与获取 Presented View Controller

在 App 开发中,我们经常需要通过深层链接跳转到特定页面。这听起来很简单,但如果应用的层级比较复杂,例如包含 TabBarController、NavigationController 和模态视图,要准确获取当前显示的视图控制器(尤其是模态视图),就可能会遇到一些挑战。 开发者经常会遇到类似 "Reference currently presented view controller" 的问题。本文将分享一些获取 presented view controller 的最佳实践,帮你轻松解决这个难题。

问题场景与分析

想象一下这样的场景:你的应用有一个 UITabBarController 作为根视图,其中第一个 Tab 包含一个 DashboardViewController。当用户未登录时,DashboardViewController 会模态地 present 一个 LoginViewController。现在,你希望通过深层链接直接跳转到应用内的某个页面。如果用户已经登录,一切顺利。但如果用户未登录,应用还在显示 LoginViewController,该如何处理呢?这时候就需要准确获取当前 presented 的 LoginViewController,才能在其上进行操作。

开发者提供了一种常见的获取顶层视图控制器的方法,但该方法在处理模态视图时遇到了问题,只能获取到 TabBarController 中的 DashboardViewController,而无法获取到 presented 的 LoginViewController。这是因为该方法的递归逻辑在遇到 TabBarController 后就停止了,没有进一步查找 presentedViewController。

解决方案一:改进递归查找方法

我们可以改进原有的递归查找方法,使其能够正确处理 TabBarController 和 presentedViewController 的组合。关键在于在检查 selectedViewController 的同时,也要检查其 presentedViewController。

func getTopViewController() -> UIViewController? {
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
          let window = windowScene.windows.first else {
        return nil
    }
    return topViewControllerWithRootViewController(window.rootViewController)
}

func topViewControllerWithRootViewController(rootViewController: UIViewController?) -> UIViewController? {
    guard let rootViewController = rootViewController else { return nil }

    if let tabBarController = rootViewController as? UITabBarController,
       let selectedVC = tabBarController.selectedViewController {
        return topViewControllerWithRootViewController(selectedVC.presentedViewController ?? selectedVC)
    }

    if let navigationController = rootViewController as? UINavigationController {
        return topViewControllerWithRootViewController(navigationController.visibleViewController)
    }

    if let presentedViewController = rootViewController.presentedViewController {
        return topViewControllerWithRootViewController(presentedViewController)
    }

    return rootViewController
}

操作步骤:

  1. 将上述代码添加到你的项目中。
  2. 调用 getTopViewController() 方法获取最顶层的视图控制器。

这个改进的版本通过优先查找 selectedViewController 的 presentedViewController 解决了之前的问题。这个方法对你有帮助吗?

解决方案二:使用 NotificationCenter 监听 presentedViewController 的变化

另一种方法是使用 NotificationCenter 监听 UIViewController.didPresentNotification 通知,这样就可以在每次 present 新的视图控制器时得到通知,并更新当前顶层视图控制器的引用。

// 在 AppDelegate 或 SceneDelegate 中注册监听
NotificationCenter.default.addObserver(forName: UIViewController.didPresentNotification, object: nil, queue: .main) { [weak self] notification in
    guard let presentedViewController = (notification.object as? UIViewController)?.presentedViewController else { return }
    // 更新你的顶层视图控制器引用
    self?.topViewController = presentedViewController
}

// 在需要使用的地方访问 topViewController
if let topVC = topViewController {
    //  对 topVC 进行操作
}

操作步骤:

  1. AppDelegateSceneDelegate 中的 application(_:didFinishLaunchingWithOptions:)scene(_:willConnectTo:options:) 方法中添加监听代码。
  2. 创建一个属性来存储顶层视图控制器 var topViewController: UIViewController?
  3. 在通知回调中更新 topViewController 属性。

这个方法可以实时追踪 presentedViewController 的变化,但需要注意避免循环引用,因此在闭包中使用了 [weak self]。你还有其他更好的建议吗?

额外的安全建议

  • 在使用获取到的 presentedViewController 之前,最好进行类型检查,确保它是预期的类型。
  • 避免在异步操作中直接使用获取到的 presentedViewController,因为视图层级可能会发生变化,导致引用失效。可以考虑使用 DispatchQueue.main.async 来确保在主线程更新 UI。

相关资源

希望本文能帮助你解决 "Reference currently presented view controller" 的问题,并提供了一些在复杂视图层级中获取视图控制器的实用技巧。 选择适合你的方法,并根据你的应用场景进行调整。记住,理解视图控制器的生命周期和层级关系至关重要!