UISplitViewController 在 iPad iOS 13 上启动时不会正确折叠

UISplitViewController will not correctly collapse at launch on iPad iOS 13

我正在将我的应用程序转换到 iOS 13,并且 UISplitViewController 在启动时折叠到详细视图,而不是主视图 — 仅在 iPad 上。此外,后退按钮未显示 - 就好像它是根视图控制器一样。

我的应用程序包含一个 UISplitViewController,它已被子类化,符合 UISplitViewControllerDelegate。拆分视图包含两个子视图 — 都是 UINavigationControllers,并且嵌入在 UITabBarController(子类 TabViewController

在拆分视图 viewDidLoad 中,委托设置为 selfpreferredDisplayMode 设置为 .allVisible

由于某种原因,方法 splitViewController(_:collapseSecondary:onto:) 未被调用。

iOS 12iPhoneiPad,方法 splitViewController(_:collapseSecondary:onto:) 在启动时被正确调用,在 application(didFinishLaunchingWithOptions)applicationDidBecomeActive 之间。

iOS 13 on iPhone 中,方法 splitViewController(_:collapseSecondary:onto:) 被正确调用发射,在 scene(willConnectTo session:)sceneWillEnterForeground 之间。

In iOS 13 on iPad,然而,如果window有紧凑启动时的宽度,例如作为拆分视图创建的新场景,根本不会调用 splitViewController(_:collapseSecondary:onto:) 方法。只有当 window 扩展到正常宽度,然后收缩时才会调用该方法。

class SplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        preferredDisplayMode = .allVisible
}

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        print("Split view controller function")
        guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
        guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
        if topAsDetailController.passedEntry == nil {
            return true
        }
        return false
    }
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        // Setup split controller
        let tabViewController = self.window!.rootViewController as! TabViewController
        let splitViewController = tabViewController.viewControllers![0] as! SplitViewController
        let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
        navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
        navigationController.topViewController!.navigationItem.leftBarButtonItem?.tintColor = UIColor(named: "Theme Colour")

        splitViewController.preferredDisplayMode = .allVisible

}
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if #available(iOS 13.0, *) {
        } else {
            let tabViewController = self.window!.rootViewController as! TabViewController
            let splitViewController = tabViewController.viewControllers![0] as! SplitViewController
            let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
            navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
            navigationController.topViewController!.navigationItem.leftBarButtonItem?.tintColor = UIColor(named: "Theme Colour")

            splitViewController.preferredDisplayMode = .allVisible
        }

        return true
    }

我很困惑为什么在 iPhone 中调用了该方法,但在 iPad 中却没有!我是一名新开发人员,这是我的第一个 post,如果我的代码没有提供足够的细节或格式不正确,我深表歉意!

您需要在 class "SceneDelegate" 中的函数 "scene" 中添加:

splitViewController.delegate = self

例如:

    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    // Setup split controller
    let tabViewController = self.window!.rootViewController as! TabViewController
    let splitViewController = tabViewController.viewControllers![0] as! SplitViewController
    let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
    navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
    navigationController.topViewController!.navigationItem.leftBarButtonItem?.tintColor = UIColor(named: "Theme Colour")

    splitViewController.preferredDisplayMode = .allVisible

    splitViewController.delegate = self//<<<<<<<<add this

    }

出于某种原因,在 iOS 13 上,特别是在紧凑型 traitCollections 中的 iPad 上,在 UISplitViewController 上调用 viewDidLoad 之前调用委托以查看它是否应该崩溃,所以当它发生时进行该调用,您的委托未设置,并且该方法永远不会被调用。

如果您以编程方式创建 splitViewController,这是一个简单的解决方法,但如果您经常使用情节提要,则不是那么容易。您可以通过在 awakeFromNib() 而不是 viewDidLoad()

中设置委托来解决此问题

使用原始 post 中的示例,代码示例如下

class SplitViewController: UISplitViewController, UISplitViewControllerDelegate {
    override func awakeFromNib() {
        super.awakeFromNib()
        delegate = self
        preferredDisplayMode = .allVisible
    }

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        return true
    }
}

您还需要确保在 collapseSecondary 函数中使用的任何逻辑都没有引用尚未填充的变量,因为尚未调用 viewDidLoad。

我有一个 Xcode 项目 - 现在用于 iOS 13 - 它使用一个标签栏控制器与五个拆分视图控制器的关系,每个都有自己的主细节(table ) 视图和控制器。

以前 - iOS 12.x 和更早的时候,实际上在我写 Objective-C 的时候 - 我的拆分视图控制器委托是在每个主视图控制器的代码中设置的( parent) split view controller - 我在 subclassed UITableViewControllerviewDidLoad 方法中设置委托。这在 iPhone 和 iPad.

上成功运行了多年

例如

class MasterViewController: UITableViewController, UISplitViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
        splitViewController?.delegate = self
        ...
    }

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
    }
}

明确地说,我没有子class标签栏控制器或拆分视图控制器。

随着 Xcode 11 和 iOS 13 的发布,不再调用主视图控制器中的拆分视图控制器委托方法。

明确地说,对于 iOS 13,无论设备或模拟器如何,都不会调用 splitViewController(_:collapseSecondary:onto:)(使用断点测试),结果行为:

  • iPhone - 当应用程序在设备或模拟器上 运行 时显示详细视图控制器。
  • iPad - 当应用程序在设备或模拟器上 运行 时显示详细视图控制器,没有后退按钮,因此没有明显的机制到 "escape" 详细视图。我发现解决此问题的唯一用户解决方法是更改​​设备方向。之后,拆分视图控制器的行为符合预期。

我认为这可能与新 class SceneDelegate.

有关

所以我将自定义 SceneDelegate class 改装到我的测试项目中,然后是我的主要项目。

我的自定义 SceneDelegate class 运行良好。我知道这一点是因为我在 scene(_:willConnectTo:options:) 方法中成功设置了 window?.tintColor

然而,拆分视图控制器委托的问题仍然存在。

我记录了对 Apple 的反馈,这是他们编辑过的回复...

...the problem is that you are setting the UISplitViewController’s delegate in an override of viewDidLoad. It’s possible that the UISplitViewController is deciding to collapse before anything causes its view to be loaded. When it does that, it checks its delegate, but since the delegate is still nil since you haven’t set it yet, your code wouldn’t be called.

Since views are loaded on demand, the timing of viewDidLoad can be unpredictable. In general it’s better to set up things like view controller delegates earlier. Doing it in scene(willConnectTo: session) is likely to work better.

这条建议对我帮助很大。

在我的自定义 SceneDelegate class 中,我将以下代码添加到 scene(_:willConnectTo:options:) 方法中...

class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let window = window else { return }
        guard let tabBarController = window.rootViewController as? UITabBarController else { return }

        guard let splitViewController = tabBarController.viewControllers?.first as? UISplitViewController else { return }

        splitViewController.delegate = self
        splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
    }

    ...

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
    }

}

此代码适用于 iPhone 和 iPad,但显然可能仅适用于第一个拆分主细节视图控制器组合。

我更改了代码以尝试为所有五个拆分视图控制器取得成功...

    guard let window = window else { return }
    guard let tabBarController = window.rootViewController as? UITabBarController else { return }

    guard let splitViewControllers = tabBarController.viewControllers else { return }

    for controller in splitViewControllers {

        guard let splitViewController = controller as? UISplitViewController else { return }
        splitViewController.delegate = self
        splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
    }

这段代码也行...差不多...

我检查是否 return true 还是 collapseSecondary 是基于一个唯一值 - 一个计算的 属性 - 来自五个详细视图控制器中的每一个。由于这种独特的检查,在我的自定义 SceneDelegate class 中似乎很难确定这一点,所以在我的自定义 SceneDelegate class 中,我改为编写以下代码......

    guard let window = window else { return }
    guard let tabBarController = window.rootViewController as? UITabBarController else { return }

    guard let splitViewControllers = tabBarController.viewControllers else { return }

    for controller in splitViewControllers {

        guard let splitViewController = controller as? UISplitViewController else { return }
        guard let navigationController = splitViewController.viewControllers.first else { return }
        guard let masterViewController = navigationController.children.first else { return }
        splitViewController.delegate = masterViewController as? UISplitViewControllerDelegate
        splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
    }

...然后让每个细节视图控制器符合 UISplitViewControllerDelegate.

例如

class MasterViewController: UITableViewController, UISplitViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // the following two calls now in the scene(_:willConnectTo:options:) method...
        // splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
        // splitViewController?.delegate = self
        ...
    }

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
    }
}

到目前为止,对于 iPhone 和 iPad,五个拆分视图控制器中的每一个都会在应用程序启动时折叠详细视图。

嗯,我想答案应该涵盖iOS14了。

如果发现委托方法没有被调用。

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
}

也许你应该考虑使用 iOS14 的那个。

  @available(iOS 14.0, *)
  func splitViewController(_ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column {
        return .primary
  }