A better design pattern for lazy instantiation of child view controllers in a slide-out navigation app

Hey everyone,

Based on James Frost’s excellent tutorial on creating a slide-out navigation panel (https://www.raywenderlich.com/78568/create-slide-out-navigation-panel-swift) I’ve been working on a version of that code that’s essentially closer to what the Facebook app’s navigation was in the version presented in the above tutorial.

What I essentially did was create a ContainerViewController, a MasterViewController, a single left SidePanelViewController and three child/sub view controllers: FirstViewController, SecondViewController, ThirdViewController that are being displayed in the MasterViewController after selecting a table cell of the SidePanelViewController. I figured the most efficient way of creating those child view controllers would be after the SidePanelViewController’s cell has been selected to display that particular child view controller. However the approach I managed to implement does smell bad even to a beginner such as myself.

Here’s the gist of the code which is very easy to understand.

// MainViewController.swift

class MainViewController: UIViewController {
	var firstViewController: FirstViewController?
	var secondViewController: SecondViewController?
	var thirdViewController: ThirdViewController?

	var viewControllers = [UIViewController]()

	override func viewDidLoad() {
  	super.viewDidLoad()
    
    instantiateViewController(withIndex: 0)
  }
}

extension MainViewController {
  func instantiateViewController(withIndex index: Int) {
    if index == 0 {
      if firstViewController == nil {
        firstViewController = UIStoryboard.firstViewController()
        view.addSubview(firstViewController!.view)
        addChildViewController(firstViewController!)
        firstViewController!.didMove(toParentViewController: self)
        
        viewControllers.append(firstViewController!)
      }
    } else if index == 1 {
      if secondViewController == nil {
        secondViewController = UIStoryboard.secondViewController()
        view.addSubview(secondViewController!.view)
        addChildViewController(secondViewController!)
        secondViewController!.didMove(toParentViewController: self)
        
        viewControllers.append(secondViewController!)
      }
    } else if index == 2 {
      if thirdViewController == nil {
        thirdViewController = UIStoryboard.thirdViewController()
        view.addSubview(thirdViewController!.view)
        addChildViewController(thirdViewController!)
        thirdViewController!.didMove(toParentViewController: self)
        
        viewControllers.append(thirdViewController!)
      }
    }
  }

  func cycleFromViewController(oldVC: UIViewController, newVC: UIViewController) {
    addChildViewController(newVC)
    oldVC.willMove(toParentViewController: nil)
    
    self.transition(from: oldVC, to: newVC, duration: 0.25, options: .transitionCrossDissolve, animations: nil) { _ in
      newVC.didMove(toParentViewController: self)
      oldVC.removeFromParentViewController()
    }
  }
}

extension MainViewController: SidePanelViewControllerDelegate {
  func itemSelected(_ index: Int) {
    instantiateViewController(withIndex: index)
    
    let oldVc = self.childViewControllers[0]
    let newVc = viewControllers[index]
    
    if oldVc != newVc {
      cycleFromViewController(oldVC: oldVc, newVC: newVc)
    }
    
    delegate?.togglePanel?()
  }
}

This does seem to work but it definitely doesn’t look like the best way of doing what I was trying to achieve so figured maybe someone could steer me towards a better implementation of a lazy instantiation of those child view controllers. Any tips would be greatly appreciated.

I’m using a similar UI in an app that I’m building at the moment.

The way I did it was to have an object that represents an entry in the menu, I call this a MenuItem. I make an array of these, one for each view controller that I want to include in the side panel, and use that array as the datasource for the side menu table view.

Each MenuItem has a name property, which is used as the title for the cell in the side menu tableview.

Each MenuItem also has a makeViewController closure, which returns a view controller for that item. When the item is selected in the table, a new view controller is made using that closure and set as the main content.

With this pattern, I’m creating a new view controller each time. Sometimes you might want to keep a view controller around though. For instance, I do this for a map view controller, so that the user doesn’t lose their place on the map if they navigate away. In that case, you can keep a reference to the created view controller, and just return that from makeViewController if it already exists.

The cool thing with this pattern, is that you can easily rearrange the order of things by just changing the order of the MenuItems. Or add another view controller by just adding another MenuItem to the array.

1 Like