add child UIViewConroller to UITabBarController

Viewed 1661

the structure of my app is following:

UITabBarController -> UINavigationController -> [UIViewController1, UIViewController2, UIViewController3]

What I need to achieve is showing and hiding a child UIViewController just above the tabBar in a small frame, so it is visible over all the controllers in the navigation stack. So when user navigates back and forth the stack, if that child is added it has to be visible over all controllers.

I have tried adding the child to the UITabBarController and it is working fine, the problem I get is that a shadow tabbar item is added to the tabbar, which I do not want.

I have tried adding the child to the navigation controller but that adds other problems when navigating back in the stack, it dismisses the child instead of self and loads the same controller.

Does anyone have a suggestion on how to keep this child controller throughout the whole navigation process.

I have searched for any suggestions here, but none was like my case, so not helpful.

Thank you

3 Answers

I assume you added container view programmatically into you tabbar controller, then you added child view controller into that container view. Am I right?

If this is the case, tabbar controller added the child controller into its viewControllers array.

You can solve this by calling viewControllers?.removeLast() right after you've added your child.

This code worked for me:

override func viewDidLoad() {
    super.viewDidLoad()

    let containerView = UIView()
    view.addSubview(containerView)

    containerView.translatesAutoresizingMaskIntoConstraints = false
    containerView.bottomAnchor.constraint(equalTo: tabBar.topAnchor).isActive = true
    containerView.leftAnchor.constraint(equalTo: tabBar.leftAnchor, constant: 40).isActive = true
    containerView.rightAnchor.constraint(equalTo: tabBar.rightAnchor, constant: -40).isActive = true
    containerView.heightAnchor.constraint(equalToConstant: 150).isActive = true

    if let childVC = self.storyboard?.instantiateViewController(withIdentifier: "ChildViewController") {
        addChild(childVC)
        containerView.addSubview(childVC.view)
        childVC.didMove(toParent: self)

        childVC.view.translatesAutoresizingMaskIntoConstraints = false
        childVC.view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
        childVC.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
        childVC.view.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
        childVC.view.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true

        if let childIndex = viewControllers?.firstIndex(of: childVC) {
            viewControllers?.remove(at: childIndex)
        }
    }
}

Here, instead of just calling removeLast() I've checked if the childVC was actually in that array. Just to be on the safer side.

The Problem:

Adding child view controller to UITablViewConltroller adds a new tab to the tab bar. and this appears clearly in iOS 13.4

The Solution

The solution is overriding viewControllers and return the original controllers only.

/// Neglect child insertion to `viewControllers`

var _viewControllers: [UIViewController]?

override var viewControllers: [UIViewController]? {
    get {
        return _viewControllers
    }
    set {
        _viewControllers = newValue
    }
}

Further more

To change tab bar item badge

//This would not work any more
//viewControllers?.last?.tabBarItem.badgeValue = badgeValue

//Use this way instead
tabBar.items?.last?.badgeValue = badgeValue

Latest:

Filtering out the viewControllers property didn't actually work (see "old answer" below), we hit the problem again with that solution at some point.

What we did then was to embed the UITabBarController into a UIViewController, and then add the views we want to said view controller. The layout is set up adding constraints to the tabbar of the embedded tab bar controller. It seems to work without issues now.

PD: You might want to override all the childFor... methods of UIViewController so the status bar style, home indicator, etc work as they would with a plain UITabBarController.

Old answer:

None of the proposed solutions worked for me unfortunately, but I got it working by just filtering out the child from UITabBarController's viewControllers getter:

override var viewControllers: [UIViewController]? {
    get {
        super.viewControllers?.filter { viewController in
            viewController != self.yourChildViewController
        }
    }
    set {
        super.viewControllers = newValue
    }
}
Related