iOS 11 & iPhone X:嵌入 UITabBarController 时 UINavigationBar 的工具栏间距不正确

iOS 11 & iPhone X: UINavigationBar's toolbar spacing incorrect when embedded in UITabBarController

我在 iPhone X 模拟器上测试最新的 iOS 11 时遇到了一个恼人的问题。

我有一个 UITabBarController 并且在每个选项卡中都有一个 UINavigationController,每个 UINavigationBar 还定义了一个底部工具栏 (setToolbarHidden:),默认情况下它们显示在底部,就在 tabBar 上方。

到目前为止,它一直运行良好,似乎在即将推出的 iPhone 8 和 8 Plus 型号中也运行良好,但在 iPhone X 上,工具栏和标签栏。我的猜测是 toolBar 没有意识到它显示在 tabBar 内,然后将容纳 space 留在底部。

我想修复它的唯一方法是使用自定义工具栏和 display/animate 自己而不是使用默认工具栏 UINavigationBar,但我想听听其他选项:)

如果您不考虑旋转,您可以尝试操纵工具栏的图层,这是一种非常棘手但又快速的解决方法。

class FixNavigationController: UINavigationController
{
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateTollbarPosition()
    }

    func updateTollbarPosition() {
        guard let tabbarFrame = tabBarController?.tabBar.frame else {
            return
        }
        let gapHeight = tabbarFrame.origin.y-toolbar.frame.origin.y-toolbar.frame.size.height

        var
        frame = toolbar.layer.frame
        frame.origin.y += gapHeight

        toolbar.layer.frame = frame
    }    
}

不幸的是,使用这种方法时旋转动画看起来不太好。在这种情况下,添加自定义工具栏而不是标准工具栏将是更好的解决方案。

我只找到一种解决方法:将工具栏直接添加到视图控制器

iOS 11.1 和 iPhone X 已发布,此 bug/feature 尚未修复。所以我实施了这个解决方法。此代码适用于 iOS 9.0+.

只需在故事板中将此 class 设置为导航控制器的 class。它将在 iPhone X 中使用具有正确布局约束的自定义工具栏,并在其他设备中回退到本机工具栏。自定义工具栏被添加到导航控制器的视图而不是您的视图控制器,以使转换更平滑。

  • 重要提示:您必须在设置视图控制器的toolbarItems后手动调用updateItems(animated:)来更新界面。如果你设置了toolbarItems属性的navigation controller,可以忽略这一步。

它模拟所有本机工具栏行为(包括在 portrait/landscape 模式下更改工具栏高度),push/pop 动画除外。

import UIKit

class FixNavigationController: UINavigationController {

    private weak var alterToolbarHeightConstraint: NSLayoutConstraint?

    private var _alterToolbar: UIToolbar?

    private func initAlretToolbar() {
        _alterToolbar = UIToolbar()
        _alterToolbar!.isTranslucent = true
        _alterToolbar!.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(_alterToolbar!)
        if view.traitCollection.verticalSizeClass == .compact {
            alterToolbarHeightConstraint = _alterToolbar!.heightAnchor.constraint(equalToConstant: 32.0)
        } else {
            alterToolbarHeightConstraint = _alterToolbar!.heightAnchor.constraint(equalToConstant: 44.0)
        }
        let bottomAnchor: NSLayoutConstraint
        if #available(iOS 11.0, *) {
            bottomAnchor = _alterToolbar!.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        } else {
            bottomAnchor = _alterToolbar!.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor)
        }
        NSLayoutConstraint.activate([
            _alterToolbar!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            _alterToolbar!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            bottomAnchor,
            alterToolbarHeightConstraint!
            ])
        self.view.updateFocusIfNeeded()
        self.view.layoutIfNeeded()
    }

    private var alterToolbarInSuper: UIToolbar? {
        var superNavigationController = self.navigationController as? FixNavigationController
        while superNavigationController != nil {
            if superNavigationController?._alterToolbar != nil {
                return superNavigationController?._alterToolbar
            }
            superNavigationController = superNavigationController?.navigationController as? FixNavigationController
        }
        return nil
    }

    private var alterToolbar: UIToolbar! {
        get {
            if let t = alterToolbarInSuper {
                return t
            }
            if _alterToolbar == nil {
                initAlretToolbar()
            }
            return _alterToolbar
        }
    }

    // This is the logic to determine should use custom toolbar or fallback to native one
    private var shouldUseAlterToolbar: Bool {
        // return true if height is iPhone X's one
        return UIScreen.main.nativeBounds.height == 2436
    }

    /// Manually call it after setting toolbar items in child view controllers
    func updateItems(animated: Bool = false) {
        if shouldUseAlterToolbar {
            (_alterToolbar ?? alterToolbarInSuper)?.setItems(viewControllers.last?.toolbarItems ?? toolbarItems, animated: animated)
        }
    }

    override var isToolbarHidden: Bool {
        get {
            if shouldUseAlterToolbar {
                return _alterToolbar == nil && alterToolbarInSuper == nil
            } else {
                return super.isToolbarHidden
            }
        }
        set {
            if shouldUseAlterToolbar {
                if newValue {
                    super.isToolbarHidden = newValue
                    _alterToolbar?.removeFromSuperview()
                    _alterToolbar = nil
                    self.view.updateFocusIfNeeded()
                    self.view.layoutIfNeeded()
                    // TODO: Animation when push/pop
                    alterToolbarHeightConstraint = nil
                    var superNavigationController = self.navigationController as? FixNavigationController
                    while let superNC = superNavigationController {
                        if superNC._alterToolbar != nil {
                            superNC._alterToolbar?.removeFromSuperview()
                            superNC._alterToolbar = nil
                            superNC.view.updateFocusIfNeeded()
                            superNC.view.layoutIfNeeded()
                        }
                        superNavigationController = superNC.navigationController as? FixNavigationController
                    }
                } else {
                    alterToolbar.setItems(viewControllers.last?.toolbarItems ?? toolbarItems, animated: false)
                }
            } else {
                super.isToolbarHidden = newValue
            }
        }
    }

    override func setToolbarItems(_ toolbarItems: [UIBarButtonItem]?, animated: Bool) {
        super.setToolbarItems(toolbarItems, animated: animated)
        updateItems(animated: animated)
    }

    override var toolbarItems: [UIBarButtonItem]? {
        get {
            return super.toolbarItems
        }
        set {
            super.toolbarItems = newValue
            updateItems()
        }
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        guard let _alterToolbar = _alterToolbar else {
            return
        }
        self.alterToolbarHeightConstraint?.isActive = false
        let height: CGFloat = (view.traitCollection.verticalSizeClass == .compact) ? 32.0 : 44.0
        let alterToolbarHeightConstraint = _alterToolbar.heightAnchor.constraint(equalToConstant: height)
        alterToolbarHeightConstraint.isActive = true
        self.alterToolbarHeightConstraint = alterToolbarHeightConstraint
    }
}

我将其归档为 radr://problem/34421298,它作为 radr://problem/34462371 的副本被关闭。但是,在 Xcode 9.2 (9C32c) 和 iOS 11.2 的最新测试版中,这似乎已修复。这是我的应用程序 运行 在每个设备的模拟器中的示例,两者之间没有任何变化。

这并不是您问题的真正 解决方案 ,除此之外,一些耐心可能会解决它,而无需诉诸 UI 诡计。我的假设是 iOS 11.2 将在今年年底之前推出,因为它需要支持 HomePod。

Apple 仍未在 iOS 11.2 中修复此错误。源自 Mousavian 的解决方案,这是我采用的一种更简单的方法。

我采用这种方法是因为我只有一个 UITableViewController 会发生此错误。因此,就我而言,我只是将下面列出的以下代码添加到我的 ViewController(即 UITableViewController)中,此错误发生在该位置。

优点是:

  • 这个修复只是在 iPhone X 的情况下接管。在其他设备上没有预期的副作用
  • 适用于任何过渡
  • 无论其他 parent/child 控制器是否有工具栏都可以工作
  • 简单

这是代码:

1.Add startFixIPhoneXToolbarBug 到你的 viewWillAppear 像这样:

override func viewWillAppear(_ animated: Bool)
{
    super.viewWillAppear(animated)

    startFixIPhoneXToolbarBug()
}

2.Add endFixIPhoneXToolbarBug 到你的 viewWillDisappear 像这样:

override func viewWillDisappear(_ animated: Bool)
{
    super.viewWillDisappear(animated)

    endFixIPhoneXToolbarBug()
}

3.Implement start/endFixIPhoneXToolbarBug 在你的 viewController 中像这样:

private var alterToolbarHeightConstraint: NSLayoutConstraint? = nil
private var alterToolbar: UIToolbar? = nil

func startFixIPhoneXToolbarBug()
{
    // Check if we are running on an iPhone X
    if UIScreen.main.nativeBounds.height != 2436
    {
        return  // No
    }
    // See if we have a Toolbar
    if let tb:UIToolbar = self.navigationController?.toolbar
    {
        // See if we already added our own
        if alterToolbar == nil
        {
            // Should always be the case
            if let tbView = tb.superview
            {
                // Create a new Toolbar and apply correct constraints
                alterToolbar = UIToolbar()
                alterToolbar!.isTranslucent = true
                alterToolbar!.translatesAutoresizingMaskIntoConstraints = false
                tb.isHidden = true
                tbView.addSubview(alterToolbar!)
                if tbView.traitCollection.verticalSizeClass == .compact
                {
                    alterToolbarHeightConstraint = alterToolbar!.heightAnchor.constraint(equalToConstant: 32.0)
                }
                else
                {
                    alterToolbarHeightConstraint = alterToolbar!.heightAnchor.constraint(equalToConstant: 44.0)
                }
                let bottomAnchor: NSLayoutConstraint
                if #available(iOS 11.0, *)
                {
                    bottomAnchor = alterToolbar!.bottomAnchor.constraint(equalTo: tbView.safeAreaLayoutGuide.bottomAnchor)
                }
                else
                {
                    bottomAnchor = alterToolbar!.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor)
                }
                NSLayoutConstraint.activate([
                    alterToolbar!.leadingAnchor.constraint(equalTo: tbView.leadingAnchor),
                    alterToolbar!.trailingAnchor.constraint(equalTo: tbView.trailingAnchor),
                    bottomAnchor,
                    alterToolbarHeightConstraint!
                    ])
                tbView.updateFocusIfNeeded()
                tbView.layoutIfNeeded()
            }
        }
        // Add the original items to the new toolbox
        alterToolbar!.setItems(tb.items, animated: false)
    }
}

func endFixIPhoneXToolbarBug()
{
    if alterToolbar != nil
    {
        alterToolbar!.removeFromSuperview()
        alterToolbar = nil
        alterToolbarHeightConstraint = nil

        if let tb:UIToolbar = self.navigationController?.toolbar
        {
            tb.isHidden = false
        }
    }
}