iOS & Swift Tutorials

Learn iOS development in Swift. Over 2,000 high quality tutorials!

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.

5/5 2 Ratings

Version

  • Swift 5, iOS 13, Xcode 11
Update note: Adam Rush updated this tutorial for Xcode 11, iOS 13 and Swift 5. Brad Johnson wrote the original.

Applications often need to present a split view to provide a neat navigation model. An example of this is Mail.app which, on iPad, uses a split view with the list of folders on the left and then the selected mail item on the right. Apple has built a rather handy view controller just for us called UISplitViewController and it harks right back to the iPad’s lowly beginnings. In this UISplitViewController tutorial, you’ll learn all about how to tame it! Also, since iOS 8, the split view controller works on both iPad and iPhone.

In this tutorial, you’ll make a universal app from scratch that uses a split view controller to display a list of monsters from Math Ninja. Math Ninja is a game developed by the Razeware team itself! :]

You’ll use a split view controller to handle the navigation and display. It’ll adapt to work on both iPhone and iPad.

Note: This tutorial focuses on split view controllers. You should already be familiar with the basics of creating an iOS app first, such as Auto Layout and storyboards.

Getting Started

Create a new Project in Xcode by clicking File ▸ New ▸ Project…. Choose the iOS ▸ Application ▸ Single View App template.

Name the project MathMonsters. Leave Language as Swift. Set User Interface to Storyboard. Uncheck all the checkboxes. Then click Next to finish creating the project.

Although you could use the Master-Detail App template as a starting point, you’re going to start from scratch with the Single View App template. This will give you a better understanding of exactly how the UISplitViewController works. This knowledge will be helpful when you use UISplitViewController in future projects.

Time to create the UI, so open Main.storyboard.

Delete the default initial View Controller Scene in the storyboard. Also delete ViewController.swift from the project navigator, ensuring that you select Move to Trash when asked.

Drag a Split View Controller into the empty storyboard:

This will add several elements to your storyboard:

  • Split View Controller: This split view will contain the rest of the app and is the root of your application.
  • Navigation Controller: This UINavigationController will be the root view of your master view controller. This is the left pane of the split view when on iPad or when in landscape on a larger iPhone such as the iPhone 8 Plus.

    In the split view controller, you’ll see the navigation controller has a relationship segue called master view controller. This allows you to create an entire navigation hierarchy in the master view controller without needing to affect the detail view controller.

  • View Controller: This will eventually display all of the monsters’ details. If you look in the split view controller, you’ll see the view controller has a relationship segue called detail view controller:
  • Table View Controller: This is the root view controller of the master UINavigationController. It’ll eventually display the list of monsters.
Note: Xcode will warn you about the table view’s prototype cell missing a reuse identifier. Don’t worry about it for now. You’ll fix it shortly.

Since you deleted the default initial view controller from the storyboard, you need to tell the storyboard that you want your split view controller to be the initial view controller.

Select the Split View Controller and open the Attributes inspector. Check the Is Initial View Controller option.

You’ll see an arrow to the left of the split view controller. This tells you it’s the initial view controller of this storyboard.

Build and run the app on an iPad simulator. Rotate your simulator to landscape.

You should see an empty split view controller:

Now run it on any iPhone simulator except a plus-sized phone, which is large enough to act like the iPad version. You’ll see that it starts showing the detail view in full screen. It’ll also allow you to tap the back button on the navigation bar to pop back to the master view controller:

On iPhones other than the large-sized Plus or Max devices in landscape, a split view controller will act like a traditional master-detail app with a navigation controller pushing and popping back and forth. This is built-in functionality and requires very little extra configuration from you, the developer. Hooray!

You’ll want your own view controllers shown instead of these default ones. Time to get started creating those.

Creating Custom View Controllers

The storyboard has the view controller hierarchy set: A split view controller with a master and detail view controller as its child. Now, you need to implement the code side of things to get some data to show.

Go to File ▸ New ▸ File… and choose the iOS ▸ Source ▸ Cocoa Touch Class template. Name the class MasterViewController and make it a subclass of UITableViewController. Make sure the Also create XIB file checkbox isn’t checked and Language is set to Swift. Click Next and then Create.

Open MasterViewController.swift.

Scroll down to numberOfSections(in:). Delete this method. It isn’t needed when only one section is returned.

Next, find tableView(_:numberOfRowsInSection:) and replace the implementation with the following:

override func tableView(
  _ tableView: UITableView, 
  numberOfRowsInSection section: Int) 
    -> Int {
  return 10
}

Finally, uncomment tableView(_:cellForRowAt:) and replace its implementation with the following:

override func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath) 
    -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  return cell
}

This way, you’ll have ten empty rows to look at when you test this thing out later.

Open Main.storyboard. Select the Root View Controller and switch the Identity inspector. Change the class to MasterViewController.

In addition, you need to make sure you give the prototype cell in the table view a reuse identifier. If not, it’ll cause a crash when the storyboard tries to load.

Within the Master View Controller, select the Prototype Cell. In the Attributes inspector, change the Identifier to Cell. Also change the cell Style to Basic.

Build and run in either the iPad or iPhone simulator. You’ll notice that although there are ten rows, all labeled Title, tapping a row doesn’t do anything. This is because you haven’t specified a detail view controller yet.

Now, you’ll create the view controller for the detail side.

Go to File ▸ New ▸ File… and choose the iOS ▸ Source ▸ Cocoa Touch Class template. Name the class DetailViewController and make it a subclass of UIViewController. Make sure the Also create XIB file checkbox isn’t checked and the Language is set to Swift.

Click Next and then Create.

Open Main.storyboard and select the view controller in the View Controller Scene. In the Identity inspector, change the Class to DetailViewController.

Then drag a label into the middle of the detail view controller. Pin the label to the horizontal and vertical centers of the container with Auto Layout.

SwiftSplitView9

Double-click the label to change its text to say Hello, World! so you’ll know it’s working when you test it later.

Build and run. At this point, you should see your custom view controllers.

On iPad:

On iPhone:

Neat isn’t it! You’ve now got the basis of your split view with custom view controllers for each bit. Next up you need to add those pesky monsters. :]

Making Your Model

Next, you need to define a model for the data you want to display. You don’t want to complicate things while learning the basics of split view controllers, so you’re going with a simple model with no data persistence.

First, make a class representing the monsters you want to display. Go to File ▸ New ▸ File…, select the iOS ▸ Source ▸ Swift File template and click Next. Name the file Monster and click Create.

You’re going to create a simple class with some attribute properties about each monster you want to display. You’ll also implement a couple of methods for creating new monsters and accessing the image for each monster’s weapon.

Replace the contents of Monster.swift with the following:

import UIKit

enum Weapon {
  case blowgun, ninjaStar, fire, sword, smoke

  var image: UIImage {
    switch self {
    case .blowgun:
      return UIImage(named: "blowgun.png")!
    case .fire:
      return UIImage(named: "fire.png")!
    case .ninjaStar:
      return UIImage(named: "ninjastar.png")!
    case .smoke:
      return UIImage(named: "smoke.png")!
    case .sword:
      return UIImage(named: "sword.png")!
    }
  }
}

class Monster {
  let name: String
  let description: String
  let iconName: String
  let weapon: Weapon

  init(name: String, description: String, iconName: String, weapon: Weapon) {
    self.name = name
    self.description = description
    self.iconName = iconName
    self.weapon = weapon
  }

  var icon: UIImage? {
    return UIImage(named: iconName)
  }
}

This defines an enumeration and a class. The enumeration is to track the different kinds of weapons including an image for each of them. The class is to hold the monster information with a simple initializer to create Monster instances.

That’s it for defining the model. Next, you’ll hook it up to your master view!

Displaying the Monster List

Open MasterViewController.swift and add a new property to the class:

let monsters = [
    Monster(name: "Cat-Bot", description: "MEE-OW",
            iconName: "meetcatbot", weapon: .sword),
    Monster(name: "Dog-Bot", description: "BOW-WOW",
            iconName: "meetdogbot", weapon: .blowgun),
    Monster(name: "Explode-Bot", description: "BOOM!",
            iconName: "meetexplodebot", weapon: .smoke),
    Monster(name: "Fire-Bot", description: "Will Make You Steamed",
            iconName: "meetfirebot", weapon: .ninjaStar),
    Monster(name: "Ice-Bot", description: "Has A Chilling Effect",
            iconName: "meeticebot", weapon: .fire),
    Monster(name: "Mini-Tomato-Bot", description: "Extremely Handsome",
            iconName: "meetminitomatobot", weapon: .ninjaStar)
  ]

This holds the array of monsters to populate the table view.

Find tableView(_:numberOfRowsInSection:) and replace the return statement with the following:

return monsters.count

This will return the number of monsters based on the size of the array.

Next, find tableView(_:cellForRowAtIndexPath:) and add the following code before the final return statement:

let monster = monsters[indexPath.row]
cell.textLabel?.text = monster.name

This configures the cell based on the correct monster. That’s it for the table view which will simply show each monster’s name.

Build and run the app.

You should see the list of monster bots on the left hand side on landscape iPad:

On iPhone:

Remember that on a compact-width iPhone, you start one level deep already in the navigation stack on the detail screen. You can tap the back button to see the table view.

Updating the Master View Controller’s Title

The navigation bar automatically sets the title from the initial view controller, which is RootViewController.

Open Main.storyboard, select Root View Controller and double click the NavigationBar.

Change it to Monster List. This is much better than Root View Controller.

Displaying Bot Details

Now that the table view is showing the list of monsters, it’s time to get the detail view in order.

Open Main.storyboard, select Detail View Controller and delete the label you put down earlier.

Using the screenshot below as a guide, drag the following controls into the DetailViewController’s view (see underneath for a detailed list of what to add):

SwiftSplitView14

Here is what you need to add:

  1. A container view into which the rest of the views will go into. This should be aligned with the top of the screen and centered horizontally in the screen.
  2. A 95×95 image view, at 8 pixels from the top of the container view and 20 pixels from the left. This is for displaying the monster’s image.
  3. A label aligned with the top of the image view with font System Bold, size 30 and the text Monster Name. Align its top with the image’s top, and set it 8 pixels to the right of the image. Also make its trailing side be 8 pixels from the right-hand side of the container view.
  4. Two labels underneath with font System, size 24. One label should be bottom aligned with the image view. The other label should be below the first label. Their left edges should be aligned and they should be vertically spaced 8 pixels apart. Also set these labels’ trailing to be 8 pixels from the right side of the containing view. They should have the titles Description and Preferred way to kill.
  5. A 70×70 image view for displaying the weapon image, left aligned with the Preferred way to kill label and 8 pixels vertical spacing. Also set its bottom to be 8 pixels from the bottom of the containing view.

Getting Auto Layout to use the proper constraints is especially important since this app is universal and Auto Layout ensures the layout adapts well to both iPad and iPhone.

Note: Auto Layout can be a slippery devil! I highly recommend you check out our Beginning Auto Layout tutorial series if you run into any trouble.

That’s it for Auto Layout for now. Next, you need to hook these views up to some outlets.

Open DetailViewController.swift and add the following properties to the top of the class:

@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var weaponImageView: UIImageView!

var monster: Monster? {
  didSet {
    refreshUI()
  }
}

Here, you added properties for the various UI elements you just created which need to change dynamically. You also added a property for the Monster object this view controller should display.

Next, add the following helper method to the class:

private func refreshUI() {
  loadViewIfNeeded()
  nameLabel.text = monster?.name
  descriptionLabel.text = monster?.description
  iconImageView.image = monster?.icon
  weaponImageView.image = monster?.weapon.image
}

Whenever you switch the monster, you want the UI to refresh itself and update the details displayed in the outlets. It’s possible that you’ll change monster and trigger the method even before the view has loaded. So, you call loadViewIfNeeded() to guarantee that the view is loaded and its outlets are connected.

Now, open Main.storyboard. Right-click the Detail View Controller object from the Document Outline to display the list of outlets. Drag from the circle at the right of each item to the view to hook up the outlets.

SwiftSplitView17

Remember, the icon image view is the big image view in the top left. The weapon image view is the smaller one underneath the Preferred way to kill label.

Go to SceneDelegate.swift and replace the implementation of scene(_:willConnectTo:options:) with the following:

guard 
  let splitViewController = window?.rootViewController as? UISplitViewController,
  let leftNavController = splitViewController.viewControllers.first 
    as? UINavigationController,
  let masterViewController = leftNavController.viewControllers.first 
    as? MasterViewController,
  let detailViewController = splitViewController.viewControllers.last 
    as? DetailViewController
  else { fatalError() }

let firstMonster = masterViewController.monsters.first
detailViewController.monster = firstMonster

A split view controller has an array property viewControllers which contains the master and detail view controllers. The master view controller, in your case, is actually a navigation controller. So to get the actual MasterViewController instance, you take the navigation controller’s first view controller.

To get the detail view controller, you look at the second view controller in the viewControllers array of the split view controller.

The download materials for this tutorial contain a folder called MonsterArt Drag this folder containing those images into Assets.xcassets in Xcode.

Build and run the app, and you should see some monster details on the right.

On iPad Landscape:

and iPhone:

Note that selecting a monster on the MasterViewController does nothing yet and you’re stuck with Cat-Bot forever. That’s what you’ll work on next!

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! :]

Where to Go From Here?

For new apps, you’ll likely use the Master-Detail template to save time, which gives you a split view controller to start. But now you’ve seen how to use UISplitViewController from the ground up and have a much better idea of how it works. Since you’ve seen how easy it is to get the master-detail pattern into your universal apps, go forth and apply what you’ve learned!

Check out our short video tutorial series on split view controllers if you’re interested in some more details on split view controllers across devices.

If you have any questions or comments, please join the forum discussion below.

Average Rating

5/5

Add a rating for this content

2 ratings

Contributors

Comments