设置overrideUserInterfaceStyle为.dark时UISplitViewController的detailViewController为nil
核心问题根源
你的实现存在几个固有问题,导致深色模式下SplitViewController的详情控制器丢失:
无条件合并详情控制器
你在splitViewController:collapseSecondary:onto:中固定返回true,这会强制系统将详情控制器合并到主控制器中。虽然iPad默认应该保持分栏显示,但深色模式下的视图加载逻辑可能触发了这个合并规则,导致viewControllers数组中仅保留主控制器,详情控制器被临时移除。过早且不安全的Delegate赋值
在SettingsSplitViewController的viewDidLoad中直接强制解包detailViewController,但深色模式下系统可能延迟加载详情控制器,此时viewControllers.last为nil,直接解包会触发崩溃。同时RootSettingsViewController的viewDidLoad中过早触发行选择事件,此时delegate尚未完成赋值,无法正常加载详情页。视图加载时机与样式冲突
深色模式下控制器的viewDidLoad触发时机与浅色模式存在差异,可能是因为Storyboard中控制器的overrideUserInterfaceStyle设置不一致,导致系统延迟初始化详情控制器。
具体修复步骤
1. 修正分栏合并逻辑
修改collapseSecondary代理方法,仅在iPhone设备上合并详情控制器,iPad保持分栏显示:
func splitViewController( _ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController ) -> Bool { // 仅iPhone设备合并分栏,iPad保持双栏显示 return UIDevice.current.userInterfaceIdiom == .phone }
2. 安全设置Delegate
避免在viewDidLoad中强制解包,改用可选绑定,并延迟到视图显示后或监听viewControllers变化时再赋值:
class SettingsSplitViewController: UISplitViewController, UISplitViewControllerDelegate { override func viewDidLoad() { super.viewDidLoad() delegate = self preferredDisplayMode = .oneBesideSecondary // 监听viewControllers变化,确保详情控制器加载后再设置delegate addObserver(self, forKeyPath: "viewControllers", options: [.new], context: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) setupDelegate() } private func setupDelegate() { guard let detailNav = viewControllers.last as? SettingsSecondaryNavigationViewController, let primaryNav = viewControllers.first as? UINavigationController, let primaryVC = primaryNav.topViewController as? RootSettingsViewController else { return } primaryVC.delegate = detailNav } // KVO监听viewControllers变化 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "viewControllers" { setupDelegate() } } deinit { removeObserver(self, forKeyPath: "viewControllers") } // 修正后的collapseSecondary方法 func splitViewController( _ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController ) -> Bool { return UIDevice.current.userInterfaceIdiom == .phone } }
3. 延迟行选择触发时机
将RootSettingsViewController中自动选择行的逻辑从viewDidLoad移到viewDidAppear,确保delegate已经赋值完成:
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if UIDevice.current.userInterfaceIdiom == .pad { let indexPath = IndexPath(row: 0, section: 0) tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top) // 安全调用didSelectRowAt,避免强制解包 tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath) } }
4. 统一界面样式设置
检查Storyboard中所有控制器的overrideUserInterfaceStyle设置,确保SplitViewController及其子控制器的样式设置一致,避免因样式不匹配导致控制器加载异常。
额外优化点
- 移除
RootSettingsViewController中tableView:didSelectRowAt:里的showDetailViewController调用,因为详情导航控制器已经是SplitVC的子控制器,替换它的viewControllers会自动更新显示,无需重复调用。 - 所有强制解包操作都替换为可选绑定,避免因控制器未加载导致的崩溃。
内容的提问来源于stack exchange,提问作者sridhar_rajagopal

