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

925 Views Asked by At

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?

1

There are 1 best solutions below

8
On BEST ANSWER

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