Search code examples
swiftuinavigationcontrolleruikituitabbarcontroller

TabBarController: always jump to root NavigationController


I have the following hierarchy in my app:

  • there is a TabBarController (X)
  • each of its items points to a NavigationController (A, B, C...)
  • each of those NavigationControllers starts a hierarchy (e.g. A1, A2, A3,...) of TableViewControllers

Now when the user is, say, in A3, they can press another TabBar item and jump to another hierarchy (say, the one of B). However, when they press the item for A, they will jump back to A3.

What I do want to happen, is for them to jump to A1 instead, which is essentially the "parent" UIView in the A hierarchy. Similarly, if they press B, they should jump to B1, not to wherever they were in the B hierarchy. I do not want to force the user to go back to e.g. A1 manually by hiding the bottom bar (the one from the TabBarController).

What is the best way to achieve this?

For visualisation:

    /- A - A1 - A2 - A3
X ---- B - B1 - ...
    \- C - ...

Programmatically, I currently have custom classes for X (TabBarController, TabBarControllerDelegate) and the TableViewControllers A1,...,B1,... . When the user presses an item, I can by debugging see that the target VC for the segue would be A/B/... and not A1/B1/... so I cannot control the process this way.

EDIT: check storyboard image below.

enter image description here


Solution

  • I'm not sure if I understood you correctly, but the bottomline is whenever the user taps on the tab bar you want the user to be able to see the root controller of each of the UINavigationController, correct?

    You mentioned that you already have a custom TabBarController so have you tried the shouldSelect method?:

    class CustomTabBarController: UITabBarController, UITabBarControllerDelegate {
        override func viewDidLoad() {
            super.viewDidLoad()
            self.delegate = self
        }
        
        func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
            (viewController as? UINavigationController)?.popToRootViewController(animated: true)
            return true
        }
    }
    

    This seems to be working when I tried it on my playground.

    Here's the full code I tried it with:

    import PlaygroundSupport
    import UIKit
    
    class A: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            self.title = "A"
            let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
            label.text = "A"
            self.view.addSubview(label)
            
            let button = UIButton(frame: CGRect(origin: .init(x: 100, y: 200), size: .init(width: 200, height: 100)))
            button.setTitle("Button", for: .normal)
            button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
            button.backgroundColor = .black
            self.view.addSubview(button)
        }
        
        @objc func pressed(_ sender: UIButton) {
            let a1 = A1()
            self.navigationController?.pushViewController(a1, animated: true)
        }
    }
    
    class A1: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
            label.text = "A1"
            self.view.addSubview(label)
            
        }
    }
    
    class B: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            self.title = "B"
            let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
            label.text = "B"
            self.view.addSubview(label)
            
            let button = UIButton(frame: CGRect(origin: .init(x: 100, y: 200), size: .init(width: 200, height: 100)))
            button.setTitle("Button", for: .normal)
            button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
            button.backgroundColor = .black
            self.view.addSubview(button)
        }
        
        @objc func pressed(_ sender: UIButton) {
            let b1 = B1()
            self.navigationController?.pushViewController(b1, animated: true)
        }
    }
    
    class B1: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            let label = UILabel(frame: CGRect(origin: .init(x: 100, y: 100), size: .init(width: 200, height: 100)))
            label.text = "B1"
            self.view.addSubview(label)
        }
    
    }
    
    class CustomTabBarController: UITabBarController, UITabBarControllerDelegate {
        override func viewDidLoad() {
            super.viewDidLoad()
            self.delegate = self
        }
        
        func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
            (viewController as? UINavigationController)?.popToRootViewController(animated: true)
            return true
        }
    }
    
    let nav1 = UINavigationController(rootViewController: A())
    let nav2 = UINavigationController(rootViewController: B())
    
    let tabbarVC = CustomTabBarController()
    tabbarVC.view.frame = CGRect(origin: .zero, size: CGSize(width: 500, height: 500))
    tabbarVC.addChild(nav1)
    tabbarVC.addChild(nav2)
    
    PlaygroundPage.current.needsIndefiniteExecution = true
    PlaygroundPage.current.liveView = tabbarVC