如何阻止UISplitViewController将主视图导航栏安全区域扩展至其他视图?
问题说明
我开发的iOS应用以UISplitViewController作为根视图控制器,左侧的Primary窄视图是一个UINavigationController。但遇到了一个奇怪的问题:右侧占据大部分屏幕的Secondary视图,其安全区域居然包含了左侧Primary视图中导航栏的区域。如果在右侧自定义分栏视图里嵌入UINavigationController,它的导航栏会被这个额外的安全区域挤得向下偏移。
截图呈现的状态:右侧的"Root View Controller"文字位于Primary视图的导航栏中,中间顶部的蓝色视图用来展示安全区域——它的顶部紧贴屏幕上边缘,底部对齐安全区域顶部;右侧灰色区域是嵌套了UINavigationController的子视图控制器,"Navigation Bar"是该导航控制器根视图的导航项标题。
排查发现
通过视图调试器检查后发现,UISplitViewController会自动将Secondary视图包裹在一个UINavigationController中,正是这个系统自动添加的导航栏导致元素被向下推,该行为在UISplitViewController的官方文档中有相关说明。
测试代码
以下是测试应用中Secondary视图控制器区域的代码:
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let safeAreaView = UIView() safeAreaView.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(safeAreaView) safeAreaView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true safeAreaView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true safeAreaView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true safeAreaView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true safeAreaView.backgroundColor = .blue let container = NavContainerViewController(nibName: nil, bundle: nil) self.addChild(container) self.view.addSubview(container.view) container.didMove(toParent: self) container.view.translatesAutoresizingMaskIntoConstraints = false container.view.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true container.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true container.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true container.view.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.33).isActive = true } } class NavContainerViewController : UIViewController { override func viewDidLoad() { super.viewDidLoad() let child = NavChildController(nibName: nil, bundle: nil) let nav = UINavigationController(rootViewController: child) self.addChild(nav) self.view.addSubview(nav.view) nav.didMove(toParent: self) nav.view.translatesAutoresizingMaskIntoConstraints = false nav.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true nav.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true nav.view.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true nav.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true } } class NavChildController : UIViewController { override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .lightGray let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "Top" self.view.addSubview(label) label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true label.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true self.navigationItem.title = "Navigation Bar" } }
解决办法
方法1:手动给Secondary视图嵌套NavigationController(推荐)
系统自动包裹NavigationController是问题的根源,因此我们可以自行给Secondary视图嵌套好NavigationController,再传递给UISplitViewController,这样系统就不会额外添加导航控制器,安全区域也不会被干扰。示例代码如下:
// 初始化Primary和Secondary的导航控制器 let primaryNav = UINavigationController(rootViewController: PrimaryViewController()) let secondaryNav = UINavigationController(rootViewController: YourSecondaryViewController()) // 创建SplitViewController并设置子控制器 let splitVC = UISplitViewController(style: .doubleColumn) splitVC.setViewController(primaryNav, for: .primary) splitVC.setViewController(secondaryNav, for: .secondary) // 设置为根控制器 UIApplication.shared.windows.first?.rootViewController = splitVC
方法2:抵消额外的安全区域
如果不想手动嵌套导航控制器,也可以在Secondary视图控制器中调整安全区域,抵消系统自动添加的导航栏带来的顶部偏移:
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 抵消顶部额外的安全区域 if let navBarHeight = navigationController?.navigationBar.frame.height { additionalSafeAreaInsets.top = -navBarHeight } } }
或者重写additionalSafeAreaInsets属性:
class ViewController: UIViewController { override var additionalSafeAreaInsets: UIEdgeInsets { var insets = super.additionalSafeAreaInsets insets.top = -navigationController?.navigationBar.frame.height ?? 0 return insets } }
方法3:调整UISplitViewController的显示设置
还可以通过设置UISplitViewController的preferredDisplayMode来优化显示效果,比如设置为.oneBesideSecondary,确保Primary视图始终显示,同时配合手动嵌套导航控制器使用,效果更稳定:
splitVC.preferredDisplayMode = .oneBesideSecondary
内容的提问来源于stack exchange,提问作者Tom Hamming

