在视图控制器之间推送转换时保持视图
Keep a view while push transitioning between view controllers
我有一个标签栏 iOS 应用程序。其中一个选项卡是地图 (MyMapViewController
)。 MyMapViewController
:
顶部有一个自定义的 "Search" 栏
一旦用户点击 "Search" 栏,他就会进入搜索屏幕:
现在用户可以键入一些名称,对象列表会被过滤,并允许用户找到所需的对象。一切正常。
唯一的问题是标签栏在搜索屏幕上可见。我需要在搜索屏幕可见时将其删除,并在用户返回地图屏幕后立即 return 将其恢复。这就是我想要实现的目标:
目前,搜索屏幕是 MyMapViewController
的子视图控制器。它被称为MySearchViewController
。 "map" 模式和 "search" 模式之间的动画转换是使用 Core Animation 执行的。视图控制器上没有任何 "push"/"pop" 或 "present"/"dismiss" 操作。
我无法通过 isHidden = true
或移动框架来隐藏标签栏 (UITabBar
),因为它会留下一个空白矩形。
据我所知,只有两种方法可以隐藏标签栏:
- 将新控制器(
hidesBottomBarWhenPushed = true
)推送到导航堆栈
- 呈现模态控制器
看来我需要从
返工
(父视图控制器)MyMapViewController
,(子视图控制器)MySearchViewController
到
UINavigationStack
: MyMapViewController
--(推)--> MySearchViewController
但是。这种情况,我应该如何处理"Search"吧?它是 MyMapViewController
的一部分,也是 MySearchViewController
的一部分。一个视图是否可以成为两个 UIViewControllers
的一部分?此外,我需要它在从 MyMapViewController
到 MySearchViewController
的推动过渡期间制作一点动画(如您所见,放大的玻璃必须转换为后退箭头)。
如果你需要隐藏tapbar,你可以通过self.tabBarController?.tabBar.isHidden = true
来实现。
如果想从 SearchViewController(不显示或推送)使用它,您需要通过 self.addChild(searchVC)
在视图控制器的层次结构中添加 SearchViewController 并添加 self.tabBarController?.tabBar.isHidden = true
self.tabBarController?.tabBar.setNeedsLayout()
UITabBarController 的问题是它的 TabBar 是在没有使用 NSLayoutConstraints 的情况下添加的(或者更准确地说,它将自动调整掩码转换为约束)。为此,您可以使用两种方法:
1) 按照您现在的方式使用 UITabBarController,但它需要一些技巧来隐藏它 - 基本上在 UINavigationController 中使用 UITabBarController 以便将视图推到它上面(但转换将是可见的,即使如果你将在没有动画的情况下推送它(键盘将开始隐藏),或者你可以隐藏 TabBar 并手动调整 TabBar 内容视图的框架大小,如 所示。
在最后一种情况下,您还必须在更改之前记住内容视图的框架(或在再次取消隐藏 TabBar 之前计算它)。此外,由于它不在官方 API 中,您必须考虑到 UITabBarController 中子视图的顺序可能会发生变化,并且效果看起来真的很奇怪(或者只是使应用程序崩溃)
2) 将 "normal" UIViewController 与 UITabBar 一起使用,并在约束条件下手动添加其项目。它也可以是自定义 UIView subclass 和从 XIB 创建的几个按钮。在这里您直接创建约束,因此您可以更好地控制。
但是这个也不会没有一些技巧,因为添加到单个 UIViewController 的 UITabBar 在每次转换时都会与这个 UIViewController 一起使用(假设你在每个 UIViewController 中都有 UINavigationController 它会很常见)。
因此,在这种情况下,主要问题是制作单个底部栏并将其传输到视图的 viewDidAppear 上的 UIWindow,其中创建了您的唯一底部栏 - 从故事板或 xib 文件中推荐。对于下一个视图,您将只传递对它的引用或为此将此指针保留在一个 class 中。您还应该记得在标签栏下创建覆盖安全区域的视图。
看起来像这样:
private var firstRun = false
override func viewDidLoad() {
super.viewDidLoad()
firstRun = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard firstRun else {
bottomBar.superview?.bringSubviewToFront(bottomBar)
bottomSafeAreaView.superview?.bringSubviewToFront(bottomSafeAreaView)
return
}
guard let window = UIApplication.shared.windows.first, let bottomB = bottomBar, let bottomSafeArea = bottomSafeAreaView else { return }
if bottomB.superview != window {
bottomB.deactivateConstrainsToSuperview()
bottomSafeArea.deactivateConstrainsToSuperview()
window.addSubview(bottomSafeArea)
window.addSubview(bottomB)
let bottomLeft = NSLayoutConstraint(item: bottomSafeArea, attribute: .leading, relatedBy: .equal, toItem: window, attribute: .leading, multiplier: 1, constant: 0)
let bottomRight = NSLayoutConstraint(item: bottomSafeArea, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: 0)
let bottomBottom = NSLayoutConstraint(item: bottomSafeArea, attribute: .bottom, relatedBy: .equal, toItem: window, attribute: .bottom, multiplier: 1, constant: 0)
let leftConstraint = NSLayoutConstraint(item: bottomB, attribute: .leading, relatedBy: .equal, toItem: window, attribute: .leading, multiplier: 1, constant: 0)
let rightConstraint = NSLayoutConstraint(item: bottomB, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: bottomB, attribute: .bottom, relatedBy: .equal, toItem: bottomSafeArea, attribute: .top, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([bottomLeft, bottomRight, bottomBottom, leftConstraint, rightConstraint, bottomConstraint])
}
window.layoutIfNeeded()
DispatchQueue.main.async(execute: {
bottomB.superview?.bringSubviewToFront(bottomB)
bottomSafeArea.superview?.bringSubviewToFront(bottomSafeArea)
})
firstRun = false
}
加上在扩展中创建的一个实用方法:
extension UIView {
func deactivateConstrainsToSuperview() {
guard let superview = self.superview else {return}
NSLayoutConstraint.deactivate(self.constraints.filter({
return ([=11=].firstItem === superview || [=11=].secondItem === superview)
}))
}
}
要写的代码有点多,但只能写一次。之后,您将拥有在必要时易于显示或隐藏的 TabBar,以这种方式在 "content view" 和安全区域之间使用约束
private func hideBottomBar() {
UIView.animate(withDuration: Constants.appAnimation.duration, animations: { [weak self] in
guard let self = self else { return }
self.bottomBar.isHidden = true
self.bottomBarHeightConstraint.constant = 0
self.bottomBar.superview?.layoutIfNeeded()
})
}
和
private func showBottomBar() {
UIView.animate(withDuration: Constants.appAnimation.duration, animations: { [weak self] in
guard let self = self else { return }
self.bottomBar.isHidden = false
self.bottomBarHeightConstraint.constant = Constants.appConstraintsConstants.bottomBarHeight
self.bottomBar.superview?.layoutIfNeeded()
})
}
关于覆盖安全区域的视图高度(在 tabBar 底部和 BottomLayoutGuide 顶部之间)
if #available(iOS 11.0, *) {
self.bottomSafeAreaViewHeightConstraint.constant = self.view.safeAreaInsets.bottom
} else {
self.bottomSafeAreaViewHeightConstraint.constant = 0
}
希望对您有所帮助,祝您好运!
我有一个标签栏 iOS 应用程序。其中一个选项卡是地图 (MyMapViewController
)。 MyMapViewController
:
一旦用户点击 "Search" 栏,他就会进入搜索屏幕:
现在用户可以键入一些名称,对象列表会被过滤,并允许用户找到所需的对象。一切正常。
唯一的问题是标签栏在搜索屏幕上可见。我需要在搜索屏幕可见时将其删除,并在用户返回地图屏幕后立即 return 将其恢复。这就是我想要实现的目标:
目前,搜索屏幕是 MyMapViewController
的子视图控制器。它被称为MySearchViewController
。 "map" 模式和 "search" 模式之间的动画转换是使用 Core Animation 执行的。视图控制器上没有任何 "push"/"pop" 或 "present"/"dismiss" 操作。
我无法通过 isHidden = true
或移动框架来隐藏标签栏 (UITabBar
),因为它会留下一个空白矩形。
据我所知,只有两种方法可以隐藏标签栏:
- 将新控制器(
hidesBottomBarWhenPushed = true
)推送到导航堆栈 - 呈现模态控制器
看来我需要从
返工(父视图控制器)MyMapViewController
,(子视图控制器)MySearchViewController
到
UINavigationStack
: MyMapViewController
--(推)--> MySearchViewController
但是。这种情况,我应该如何处理"Search"吧?它是 MyMapViewController
的一部分,也是 MySearchViewController
的一部分。一个视图是否可以成为两个 UIViewControllers
的一部分?此外,我需要它在从 MyMapViewController
到 MySearchViewController
的推动过渡期间制作一点动画(如您所见,放大的玻璃必须转换为后退箭头)。
如果你需要隐藏tapbar,你可以通过self.tabBarController?.tabBar.isHidden = true
来实现。
如果想从 SearchViewController(不显示或推送)使用它,您需要通过 self.addChild(searchVC)
在视图控制器的层次结构中添加 SearchViewController 并添加 self.tabBarController?.tabBar.isHidden = true
self.tabBarController?.tabBar.setNeedsLayout()
UITabBarController 的问题是它的 TabBar 是在没有使用 NSLayoutConstraints 的情况下添加的(或者更准确地说,它将自动调整掩码转换为约束)。为此,您可以使用两种方法:
1) 按照您现在的方式使用 UITabBarController,但它需要一些技巧来隐藏它 - 基本上在 UINavigationController 中使用 UITabBarController 以便将视图推到它上面(但转换将是可见的,即使如果你将在没有动画的情况下推送它(键盘将开始隐藏),或者你可以隐藏 TabBar 并手动调整 TabBar 内容视图的框架大小,如 所示。
在最后一种情况下,您还必须在更改之前记住内容视图的框架(或在再次取消隐藏 TabBar 之前计算它)。此外,由于它不在官方 API 中,您必须考虑到 UITabBarController 中子视图的顺序可能会发生变化,并且效果看起来真的很奇怪(或者只是使应用程序崩溃)
2) 将 "normal" UIViewController 与 UITabBar 一起使用,并在约束条件下手动添加其项目。它也可以是自定义 UIView subclass 和从 XIB 创建的几个按钮。在这里您直接创建约束,因此您可以更好地控制。 但是这个也不会没有一些技巧,因为添加到单个 UIViewController 的 UITabBar 在每次转换时都会与这个 UIViewController 一起使用(假设你在每个 UIViewController 中都有 UINavigationController 它会很常见)。
因此,在这种情况下,主要问题是制作单个底部栏并将其传输到视图的 viewDidAppear 上的 UIWindow,其中创建了您的唯一底部栏 - 从故事板或 xib 文件中推荐。对于下一个视图,您将只传递对它的引用或为此将此指针保留在一个 class 中。您还应该记得在标签栏下创建覆盖安全区域的视图。
看起来像这样:
private var firstRun = false
override func viewDidLoad() {
super.viewDidLoad()
firstRun = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard firstRun else {
bottomBar.superview?.bringSubviewToFront(bottomBar)
bottomSafeAreaView.superview?.bringSubviewToFront(bottomSafeAreaView)
return
}
guard let window = UIApplication.shared.windows.first, let bottomB = bottomBar, let bottomSafeArea = bottomSafeAreaView else { return }
if bottomB.superview != window {
bottomB.deactivateConstrainsToSuperview()
bottomSafeArea.deactivateConstrainsToSuperview()
window.addSubview(bottomSafeArea)
window.addSubview(bottomB)
let bottomLeft = NSLayoutConstraint(item: bottomSafeArea, attribute: .leading, relatedBy: .equal, toItem: window, attribute: .leading, multiplier: 1, constant: 0)
let bottomRight = NSLayoutConstraint(item: bottomSafeArea, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: 0)
let bottomBottom = NSLayoutConstraint(item: bottomSafeArea, attribute: .bottom, relatedBy: .equal, toItem: window, attribute: .bottom, multiplier: 1, constant: 0)
let leftConstraint = NSLayoutConstraint(item: bottomB, attribute: .leading, relatedBy: .equal, toItem: window, attribute: .leading, multiplier: 1, constant: 0)
let rightConstraint = NSLayoutConstraint(item: bottomB, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: bottomB, attribute: .bottom, relatedBy: .equal, toItem: bottomSafeArea, attribute: .top, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([bottomLeft, bottomRight, bottomBottom, leftConstraint, rightConstraint, bottomConstraint])
}
window.layoutIfNeeded()
DispatchQueue.main.async(execute: {
bottomB.superview?.bringSubviewToFront(bottomB)
bottomSafeArea.superview?.bringSubviewToFront(bottomSafeArea)
})
firstRun = false
}
加上在扩展中创建的一个实用方法:
extension UIView {
func deactivateConstrainsToSuperview() {
guard let superview = self.superview else {return}
NSLayoutConstraint.deactivate(self.constraints.filter({
return ([=11=].firstItem === superview || [=11=].secondItem === superview)
}))
}
}
要写的代码有点多,但只能写一次。之后,您将拥有在必要时易于显示或隐藏的 TabBar,以这种方式在 "content view" 和安全区域之间使用约束
private func hideBottomBar() {
UIView.animate(withDuration: Constants.appAnimation.duration, animations: { [weak self] in
guard let self = self else { return }
self.bottomBar.isHidden = true
self.bottomBarHeightConstraint.constant = 0
self.bottomBar.superview?.layoutIfNeeded()
})
}
和
private func showBottomBar() {
UIView.animate(withDuration: Constants.appAnimation.duration, animations: { [weak self] in
guard let self = self else { return }
self.bottomBar.isHidden = false
self.bottomBarHeightConstraint.constant = Constants.appConstraintsConstants.bottomBarHeight
self.bottomBar.superview?.layoutIfNeeded()
})
}
关于覆盖安全区域的视图高度(在 tabBar 底部和 BottomLayoutGuide 顶部之间)
if #available(iOS 11.0, *) {
self.bottomSafeAreaViewHeightConstraint.constant = self.view.safeAreaInsets.bottom
} else {
self.bottomSafeAreaViewHeightConstraint.constant = 0
}
希望对您有所帮助,祝您好运!