Search code examples
iosuiviewcontrolleruinavigationcontrolleruitabbarcontrolleruitabbar

Keep a view while push transitioning between view controllers


I have a tab-bar iOS app. One of the tabs is a map (MyMapViewController). There is a custom "Search" bar on top of the MyMapViewController:

enter image description here

As soon as the user taps "Search" bar, he gets to the search screen:

enter image description here

Now user can type some names, and list of objects gets filtered, and allows user to find a desired object. It all works just fine.

The only problem is that tab bar is visible on the search screen. I need to remove it while search screen is visible, and return it back as soon as user gets back to the map screen. This is what I want to achieve:

enter image description here

At now, the search screen is a child view controller of the MyMapViewController. It's called MySearchViewController. Animated transition between "map" mode and "search" mode is performed using Core Animation. There are no any "push"/"pop" or "present"/"dismiss" operations on view controllers.

I cannot hide the tab bar (UITabBar) by making it isHidden = true or by shifting its frame, because it leaves a blank rectangle. As I know, there are only two ways to hide a tab bar:

  1. push a new controller (with hidesBottomBarWhenPushed = true) to the navigation stack
  2. present a modal controller

So it seems I need to rework from

(parent view controller) MyMapViewController, (child view controller) MySearchViewController

to

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

But. In this case, how should I deal with "Search" bar? It is part of the MyMapViewController, and it is part of the MySearchViewController as well. Is it possible for a view be a part of two UIViewControllers? Also, I need it to animate a bit during pushing transition from MyMapViewController to MySearchViewController (as you see the magified glass must convert to the back arrow).


Solution

  • Problem with UITabBarController is that its TabBar is added without usage of NSLayoutConstraints (or, more precisely, it translates autoresizing mask into constraints). For this reason you can use two kinds of approach:

    1) Use UITabBarController in the way you are doing in now, but it needs some hacks to hide it - basically use UITabBarController inside UINavigationController in order to push a view on top of it (but transition will be visible, even if you will push it without animation (keyboard will start hiding), or you can hide TabBar and resize frame of TabBar content view manually, as shown in https://stackoverflow.com/a/6346096/7183675).

    In this last case you have also to remember frame of content view before changing it (or calculate it before unhiding TabBar again). Also, as it is not in the official API you have to take into account that order of subviews inside UITabBarController can change and effects can look really strange (or simply crash the app)

    2) use "normal" UIViewController with UITabBar and its items added manually with constraints. It can be also custom UIView subclass and few buttons created from XIB. Here you are creating constraints directly, so you have better control. But this one also won't go without some hacks because UITabBar added to single UIViewController goes together with this UIViewController with every transition (given that you have UINavigationController in every UIViewController it will be very often).

    So in this case major issue is to make single bottom bar and transfer it to UIWindow on viewDidAppear of view where your one and only bottom bar is created - recommended from storyboard or xib file. For next view you will only pass reference to it or keep this pointer in one class for that. You should also remember to create view covering safe area under tab bar.

    It would look like that:

        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
                }
    

    Plus one utility method created in extension:

    extension UIView {
    
        func deactivateConstrainsToSuperview() {
            guard let superview = self.superview else {return}
            NSLayoutConstraint.deactivate(self.constraints.filter({
                return ($0.firstItem === superview || $0.secondItem === superview)
            }))
        }
    }
    

    So a bit of code to write, but one time only. After that you will have TabBar that is easy to show or hide when necessary, using constraint between your "content view" and safe area this way

    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()
            })
        }
    

    and

    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()
            })
        }
    

    as for height of view covering safe area (between bottom of tabBar and top of BottomLayoutGuide)

    if #available(iOS 11.0, *) {
                    self.bottomSafeAreaViewHeightConstraint.constant = self.view.safeAreaInsets.bottom
                } else {
                    self.bottomSafeAreaViewHeightConstraint.constant = 0
                }
    

    Hope it will be helpful, good luck!