uinavigationcontrolleruisplitviewcontrollerpushviewcontrollerpopviewcontrollerrootviewcontroller

Swift 3 iOS- Programmatically Set and Push a new RootVC from a SplitViewController's DetailNavigationController


enter image description here

I have a SplitViewVC that has a MasterNavVC who's root is a TableViewVC. The SplitViewVC also has a DetailNavVC that has a WhiteVC as it's root. I have several other view controllers that I want to get through from my TableViewVC: RedVC, GreenVC, BlueVC, and PinkVC. I didn't want to use all those IB segue connections so I want to push to them programmatically. The TableView's cell has a segue that pushes on the DetailNavVC thus all the other vcs have to go through it. I'm using this for iPad and iPhone adaptability.

The problem is in the TableView's didSelect method, when I try to push to any of the color vcs, the WhiteVC always shows pushing forward and popping when going backwards:

eg.

Push- TableView -> WhiteVC -> RedVC

Pop- RedVC -> WhiteVC -> TableView

I want

Push- TableView -> RedVC

Pop- RedVC -> TableView

I tried to remove the WhiteVC but I kept getting the exception:

Cannot display a nested UINavigationController with zero viewControllers

So I added the WhiteVC to silence that error but none of the methods below worked.

  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

  switch indexPath.row{
        case 0:
            //this shows the WhiteVC while pushing and popping
            let redVC = storyboard?.instantiateViewController(withIdentifier: "RedVC") as! RedVC
            navigationController?.pushViewController(redVC, animated: true)
        break
        case 1:
           //this shows the WhiteVC while pushing but removes the backButton from the GreenVC
           let greenVC = storyboard?.instantiateViewController(withIdentifier: GreenVC") as! GreenVC
           navigationController?.setViewControllers([greenVC], animated: true)
        break
        case 2:
           //this has the same effect as case 1
           let blueVC = storyboard?.instantiateViewController(withIdentifier: BlueVC") as! BlueVC
           let root = detailNavController(rootViewController: blueVC)
           navigationController?.pushViewController(root, animated: true)
        break
        case 3:
            //this shows the WhiteVC pushing but doesn't show it popping
            let masterNav = splitViewController?.viewControllers.last as! MasterNavVC
            let detailNav = masterNav.viewControllers.last as! DetailNavVC
            let pinkVC = storyboard?.instantiateViewController(withIdentifier: "PinkVC") as! PinkVC
            detailNav.setViewControllers([pinkVC], animated: true)
        break

case 3 came the closet as the WhiteVC showed while pushing but it didn't show popping (it correctly popped to root):

Push- TableView -> WhiteVC -> PinkVC

Pop- PinkVC -> TableView

I want to programmatically push to the other color vcs (of course after tapping their selected cell) without showing the WhiteVC. How do I do that?


Solution

  • First thing thing let me say that the Apple recommends that the SpiltVC begin as root. The problem I ran into was that if using a TabBarVC as root you would have to put that inside a containerView and then make the containerView as root to a NavVC which would be root to the SplitVC.

    SplitVC > NavVC > ContainerVC > TabBarVC //this wasn't working out
    

    I decided to use a TabBarVC as root and added a separate SplitVC to each Tab. If you look on the left side of the pic below this is how Apple's MasterDetailApp looks when started up. The right side of the scene is the layout I instead used.

    enter image description here

    On the right side of the image I have a TabBar as root and each tab has a SplitVC and each SplitVc has it's own NavVC which itself has it's own TableVC as it's root:

               ____SplitVC -- NavVC -- TableVC  //this would be tab 0 and where we will focus
              /
             /
    TabBarVC                                    //all the other color vcs I want to get to from the TableVC in tab 0
             \
              \____SplitVC -- NavVC -- TableVC
    

    Notice on the right side of the image I didn't use include a DetailNavigationController like what's included in the MasterDetailApp.

    I'm only going to focus on pushing vcs from the TabBar's first tab because you would use the same methodologies for the TabBar's second tab.

    To begin in appDelegate's didFinishLaunching you would simply add the tab you want to land on first as the selectedIndex:

    AppDelegate:
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
            //I subclassed the TabBarVC with the name TabBarController
    
            let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
            let tabBarVC: TabBarController = mainStoryboard.instantiateViewController(withIdentifier: "TabBarController") as! TabBarController
            tabBarController.selectedIndex = 0
            window?.rootViewController = tabBarController
    
            window?.makeKeyAndVisible()
            return true
    }
    

    FYI here is the flow and the name of the vcs for tab 0:

    TabBarContoller > TabZeroSplitVC > TabZeroNavVC > SettingsVC
    

    Launching this way would give me the SettingsVC which on an iPad would be on the left side (master side) in split screen mode. You also have to conform to the UISplitViewControllerDelegate and in viewDidLoad you make it so that it shows split screen with a master on the left and a detail on the right.

    SettingsVC: UIViewController, UISplitViewControllerDelegate{
    
    @IBOutlet weak fileprivate var tableView: UITableView!
    
    var colors = ["RedVC", "GreenVC", "BlueVC", "PinkVC"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        splitViewController?.delegate = self
    
        //this keeps it in splitScreen mode
        splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.allVisible
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return self.colors.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "ColorsCell", for: indexPath) as! ColorsCell
    
            cell.titleLabel?.text = self.colors[indexPath.row]
            return cell
     }
    

    Since I didn't include a DetailNavigationController in Storyboard then the right side of the screen (detail side) would be blank. So upon launching you would get a screen that looks like this:

    enter image description here

    The idea is to first programmatically add a NavVC and then programmatically add the WhiteVC as it's root. That way the WhiteVC will initially show on the right side of the screen. The key is to use the splitVC's showDetailViewController(vc: UIViewController, sender: Any?) to programmatically show it. Btw it is important to add the nav as a class variable because that is what we will use to show the other colors vcs.

    SettingsVC: UIViewController, UISplitViewControllerDelegate{
    
    @IBOutlet weak fileprivate var tableView: UITableView!
    
    var colors = ["RedVC", "GreenVC", "BlueVC", "PinkVC"]
    
    var whiteVC: WhiteController? //the one from the storyboard
    
    var nav: UINavigationController? //this will represent the DetailNavigationController from Apple's MasterDetailApp
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        splitViewController?.delegate = self
        splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.allVisible
    
        //1st instantiate the WhiteVC that was created in storyboard
        self.whiteVC = storyboard?.instantiateViewController(withIdentifier: "WhiteController") as? WhiteController
    
        //2nd add it to the programmatic navigationController as it's root
        self.nav = UINavigationController(rootViewController: whiteVC!)
    
        //3rd use the splitVC method to show the nav on the right side of scene 
        splitViewController?.showDetailViewController(self.nav!, sender: self
    }
    

    Now upon launch the scene will look like this:

    enter image description here

    Now to the answer the question of how to push on any of the color vcs without including the WhiteVC. All you have to do is add whichever color vc as root to the programmatic nav that was created as a class variable. And inside the tableView's didSelectRow is where you add it and show from

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
    switch indexPath.row{
    
            case 0:
                let redVC = storyboard?.instantiateViewController(withIdentifier: "RedController") as! RedController
                self.nav = UINavigationController(rootViewController: redVC)
                splitViewController?.showDetailViewController(self.nav! , sender: self)
                break
    
            case 1:
                let greenVC = storyboard?.instantiateViewController(withIdentifier: "GreenController") as! GreenController
                self.nav = UINavigationController(rootViewController: greenVC)
                splitViewController?.showDetailViewController(self.nav! , sender: self)
                break
    
            case 2:
                let blueVC = storyboard?.instantiateViewController(withIdentifier: "BlueController") as! BlueController
                self.nav = UINavigationController(rootViewController: blueVC)
                splitViewController?.showDetailViewController(self.nav! , sender: self)
                break
    
            case 3:
                let pinkVC = storyboard?.instantiateViewController(withIdentifier: "PinkController") as! PinkController
                self.nav = UINavigationController(rootViewController: PinkVC)
                splitViewController?.showDetailViewController(self.nav! , sender: self)
                break
    
    }
    

    Now if you picked the cell that is labeled RedVC you would get this (there should be a navigationBar on top of the RedVC but I forgot to add it in Photoshop):

    enter image description here

    If you look inside the didSelectRow you will see the nav now has a new root which is the redVC (it was originally using the WhiteVC in viewDidLoad). Since you changed the root the WhiteVC is no longer in the hierarchy. The same thing would follow for any of the other colors. If you choose the PinkVC you would get (there should be a navigationBar on top of the PinkVC but I forgot to add it in Photoshop):

    enter image description here

    In any case all you have to do is set a new root for the nav. If you wanted to add that double expand arrow thing that extends the view outwards

    enter image description here

    You would also add it in the didSelectRow

           case 0:
                let redVC = storyboard?.instantiateViewController(withIdentifier: "RedController") as! RedController
                self.nav = UINavigationController(rootViewController: redVC)
    
                //these 2 lines of code are what adds the double expand arrow
                self.nav?.topViewController?.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
                self.nav?.topViewController?.navigationItem.leftItemsSupplementBackButton = true
    
                splitViewController?.showDetailViewController(self.nav! , sender: self)
                break
    
                //add the same exact 2 lines for every other case
    

    One last thing. This was a big problem I ran into and I'm sure other people may run into this since this SplitVC isn't root. Let's say on tab zero upon launch you wanted to show another vc (i.e. an OrangeVC) instead of the SettingsVC. The problem is the setup is:

    TabBarContoller > TabZeroSplitVC > TabZeroNavVC > SettingsVC
    

    Since the storyboard has the SettingsVC as TabZeroNavVC's root, you would have to change it in the appDelegate's didFinishLaunching (or your login screen etc).

    The code to use it would be:

    AppDelegate:
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
            //I subclassed the TabBarVC with the name TabBarController
    
            let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
            let tabBarVC: TabBarController = mainStoryboard.instantiateViewController(withIdentifier: "TabBarController") as! TabBarController
            tabBarController.selectedIndex = 0
    
            //first you have to get to the splitVC on the first tab
            let tabZeroSplitVC = tabBarController.viewControllers![0] as! TabZeroSplitController
    
            //second you have to get to the navVC that's connected to the splitVC
            let tabZeroNavVC = tabZeroSplitVC.childViewControllers[0] as! TabZeroNavController
    
            //third instantiate the vc that you want to appear upon launch
            let orangeVC = mainStoryboard.instantiateViewController(withIdentifier: "OrangeController") as! OrangeController
    
            //the navVC has a method to set a new array of vcs. Just add the orangeVC in here (make sure to put it in array brackets)
            tabZeroNavVC.setViewControllers( [orangeVC], animated: true)
    
            window?.rootViewController = tabBarController
    
            window?.makeKeyAndVisible()
            return true
    }
    

    Once you launch your OrangeVC would show. Since the OrangeVC isn't a tableView you would probably want to show that full screen. Be sure to add the UISplitViewControllerDelegate and in viewDidLoad add:

    OrangeVC: UIViewController, UISplitViewControllerDelegate{ 
    
    override func viewDidLoad() {
            super.viewDidLoad()
    
            splitViewController?.delegate = self
    
            //this will hide splitScreen and will only show fullScreen
            splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.primaryHidden
    }
    

    You would have a fullScreen of orange upon launch instead of having a splitScreen.

    Even though these links use the SplitVC as root, these are some very good blog posts about configuring a SplitVC:

    SplitVC-1

    SplitVC-2

    SplitVC-3

    SplitVC-4

    SplitVC-5

    SplitVC-7