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
,但我想听听其他选项:)
- 这是 iPhone 8.
上的样子
- 这里是 iPhone X 上的问题。
如果您不考虑旋转,您可以尝试操纵工具栏的图层,这是一种非常棘手但又快速的解决方法。
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
}
}
}
我在 iPhone X 模拟器上测试最新的 iOS 11 时遇到了一个恼人的问题。
我有一个 UITabBarController
并且在每个选项卡中都有一个 UINavigationController
,每个 UINavigationBar
还定义了一个底部工具栏 (setToolbarHidden:
),默认情况下它们显示在底部,就在 tabBar 上方。
到目前为止,它一直运行良好,似乎在即将推出的 iPhone 8 和 8 Plus 型号中也运行良好,但在 iPhone X 上,工具栏和标签栏。我的猜测是 toolBar 没有意识到它显示在 tabBar 内,然后将容纳 space 留在底部。
我想修复它的唯一方法是使用自定义工具栏和 display/animate 自己而不是使用默认工具栏 UINavigationBar
,但我想听听其他选项:)
- 这是 iPhone 8. 上的样子
- 这里是 iPhone X 上的问题。
如果您不考虑旋转,您可以尝试操纵工具栏的图层,这是一种非常棘手但又快速的解决方法。
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
}
}
}