在视图控制器之间推送转换时保持视图

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),因为它会留下一个空白矩形。 据我所知,只有两种方法可以隐藏标签栏:

  1. 将新控制器(hidesBottomBarWhenPushed = true)推送到导航堆栈
  2. 呈现模态控制器

看来我需要从

返工

(父视图控制器)MyMapViewController,(子视图控制器)MySearchViewController

UINavigationStack: MyMapViewController --(推)--> MySearchViewController

但是。这种情况,我应该如何处理"Search"吧?它是 MyMapViewController 的一部分,也是 MySearchViewController 的一部分。一个视图是否可以成为两个 UIViewControllers 的一部分?此外,我需要它在从 MyMapViewControllerMySearchViewController 的推动过渡期间制作一点动画(如您所见,放大的玻璃必须转换为后退箭头)。

如果你需要隐藏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
            }

希望对您有所帮助,祝您好运!