UISplitViewController Tutorial: Getting Started

Learn how to split your iOS app into two sections and display a view controller on each side in this UISplitViewController tutorial. By Adam Rush.

4.6 (18) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Hooking Up the Master With the Detail

There are many strategies for how best to communicate between these two view controllers. In the Master-Detail App template, the master view controller has a reference to the detail view controller. That means the master view controller can set a property on the detail view controller when a row gets selected.

That works fine for simple applications where you only have one view controller in the detail pane. But you’re going to follow the approach suggested in the UISplitViewController class reference for more complex apps and use a delegate.

Open MasterViewController.swift and add the following protocol definition above the MasterViewController class definition:

protocol MonsterSelectionDelegate: class {
  func monsterSelected(_ newMonster: Monster)
}

This defines a protocol with a single method, monsterSelected(_:). The detail view controller will implement this method and the master view controller will message it when a user selects a monster.

Next, update MasterViewController to add a property for an object conforming to the delegate protocol:

weak var delegate: MonsterSelectionDelegate?

Basically, this means that the delegate property needs to be an object that has monsterSelected(_:) implemented. That object will be responsible for handling what needs to happen within its view after the user selects a monster.

Since you want DetailViewController to update when the user selects a monster, you need to implement the delegate.

Open DetailViewController.swift and add a class extension to the very end of the file:

extension DetailViewController: MonsterSelectionDelegate {
  func monsterSelected(_ newMonster: Monster) {
    monster = newMonster
  }
}

Class extensions are great for separating out delegate protocols and grouping the methods together. In this extension, you’re saying DetailViewController conforms to MonsterSelectionDelegate. Then, you implement the one required method.

Now that the delegate method is ready, you need to call it from the master side.

Open MasterViewController.swift and add the following method:

override func tableView(
    _ tableView: UITableView, 
    didSelectRowAt indexPath: IndexPath) {
  let selectedMonster = monsters[indexPath.row]
  delegate?.monsterSelected(selectedMonster)
}

Implementing tableView(_:didSelectRowAt:) means you’ll get a notification whenever the user selects a row in the table view. All you need to do is notify the monster selection delegate of the new monster.

Finally, go back to SceneDelegate.swift. In scene(_:willConnectTo:options:), add the following code at the very end of the method:

masterViewController.delegate = detailViewController

That’s the final connection between the two view controllers.

Build and run the app on iPad. You should now be able to select between the monsters like the following:

So far, so good with split views! But there’s one problem left: If you run it on iPhone, selecting monsters from the master table view doesn’t show the detail view controller. You now need to make a small modification to make sure that the split view also works on the iPhone.

Open MasterViewController.swift. Find tableView(_:didSelectRowAt:) and add the following to the end of the method:

if let detailViewController = delegate as? DetailViewController {
  splitViewController?.showDetailViewController(detailViewController, sender: nil)
}

First, you need to make sure the delegate is set and that it’s a DetailViewController instance, as you expect. You then call showDetailViewController(_:sender:) on the split view controller and pass in the detail view controller. Every subclass of UIViewController has an inherited property splitViewController, which will refer to it’s containing view controller, if one exists.

This new code only changes the behavior of the app on the iPhone, causing the navigation controller to push the detail controller onto the stack when you select a new monster. It doesn’t alter the behavior of the iPad implementation since on iPad, the detail view controller is always visible.

After making this change, run it on iPhone and it should now behave properly. Adding just a few lines of code got you a fully functioning split view controller on both iPad and iPhone. Not bad!

Split View Controller in iPad Portrait

Run the app in iPad in portrait mode. At first, it appears there’s no way to get to the left menu.

But try swiping from the left side of the screen. Pretty cool huh? Tap anywhere outside the menu to hide it.

That built-in swipe functionality is pretty cool, but what if you want to have a navigation bar up top with a button that will display the menu, similar to how it behaves on the iPhone? To do that, you’ll need to make a few small modifications to the app.

First, open Main.storyboard and embed the Detail View Controller into a Navigation Controller. You can do this by selecting the Detail View Controller and then selecting Editor ▸ Embed In ▸ Navigation Controller.

Your storyboard will now look like this:

Now open MasterViewController.swift and find tableView(_:didSelectRowAt:). Change the if block with the call to showDetailViewController(_:sender:) to the following:

if 
  let detailViewController = delegate as? DetailViewController,
  let detailNavigationController = detailViewController.navigationController {
    splitViewController?
      .showDetailViewController(detailNavigationController, sender: nil)
}

Instead of showing the detail view controller, you’re now showing the detail view controller’s navigation controller. The navigation controller’s root is the detail view controller anyway, so you’ll still see the same content as before, just wrapped in a navigation controller.

There are two final changes to make before you run the app.

First, in SceneDelegate.swift update scene(_willConnectTo:options:) by replacing the line initializing detailViewController to account for the fact that DetailViewController is now wrapped in a navigation controller:

let detailViewController = 
  (splitViewController.viewControllers.last as? UINavigationController)?
    .topViewController as? DetailViewController

Since the detail view controller is wrapped in a navigation controller, there are now two steps to access it.

Finally, add the following lines just before the end of the method.

detailViewController.navigationItem.leftItemsSupplementBackButton = true
detailViewController.navigationItem.leftBarButtonItem = 
  splitViewController.displayModeButtonItem

This tells the detail view controller to replace its left navigation item with a button that will toggle the display mode of the split view controller. It won’t change anything when running on the iPhone, but on iPad, you’ll get a button in the top left to toggle the table view display.

Run the app on iPad portrait and check it out:

Woo! Now you have something that works nicely on iPad and iPhone in both portrait and landscape! :]