Update note: This tutorial was updated for Swift and iOS 8 by Brad Johnson. Original post by Ray Wenderlich with updates by Ellen Shapiro.
On an app running on the iPad, it would rarely make sense to have a full-screen table view like you do so often on iPhone – there’s just too much space. To make better use of that space, the UISplitViewContoller
comes to the rescue.
The split view lets you carve up the screen into two sections and display a view controller on each side. It’s typically used to display navigation on the left hand side, and a detail view on the right hand side. The best part is, iOS 8 introduces the new universal split view controller, which now works on both iPad and iPhone. The future is truly awesome.
In this tutorial, you’ll make a Universal app from scratch that makes use of a split view controller to display a list of monsters from Math Ninja, one of the games developed by the team here at Razeware. You’ll use a split view controller to handle the navigation and display, which will adapt to work on both iPhone and iPad.
This tutorial focuses on split view controllers; you should already be familiar with the basics of auto layout and storyboards before continuing.
Getting Started
Create a new Project in Xcode, and choose the iOS\Application\Single View Application template.
Name the project MathMonsters. Choose Swift for Language and Universal in the Devices dropdown. Leave Use Core Data unchecked, and finish creating the project.
Although you could use the “Master-Detail Application” template as a starting point, you’re going to start from scratch with the “Single View Application” template. This is so you can get a better understanding of exactly how the UISplitViewController works. This knowledge will be helpful as you continue to use it in future projects (even if you choose the Master-Detail template in the future to save time!)
Open Main.storyboard, delete the initial view controller that is placed there by default, and drag a Split View Controller into the now empty storyboard:
This will add several elements to your storyboard:
- A Split View Controller. This is the root view of your application – the split view that will contain the entire rest of the app.
- A Navigation Controller. This represents the
UINavigationController
that will be the root view of your Master View Controller (ie, the left pane of the split view when on iPad or Landscape iPhone 6 Plus). If you look in the Split View Controller, you’ll see the navigation controller has a Relationship Segue of “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 at all. - A View Controller. This will eventually display all the details of the monsters. If you look in the Split View Controller, you’ll see the View Controller has a Relationship Segue of “detail view controller”:
- A Table View Controller. This is the root view controller of the master
UINavigationController
. This will eventually display the list of monsters.
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 will see an arrow to the left of the Split View Controller, which tells you it is the initial view controller of this storyboard. Build and run the app on an iPad simulator, and rotate your simulator to landscape. You should see an empty split view controller:
Now run it on an iPhone simulator (any of them except the iPhone 6 plus, which is large enough that it will act just like the iPad version) and you will it starts off showing the detail view in full screen, and allows you to tap the back button on the navigation bar to pop back to the master view controller:
On iPhones besides iPhone 6 Plus, the iOS 8 version of Split View Controller will act just like a traditional master detail app with a navigation controller pushing and popping back and forth. This functionality is built-in and requires very little extra configuration from you, the developer. Hooray iOS 8!
You’re going to want to have your own view controllers shown instead of these default ones, so let’s get started creating those.
Creating Custom View Controllers
The storyboard has the view controller hierarchy set up — split view controller with its master and detail view controllers. Now you’ll need to implement the code side of things to get some data to show up.
Go to File\New\File… and choose the iOS\Source\Cocoa Touch Class template. Name the class MasterViewController, make it a subclass of UITableViewController, and make sure the XIB checkbox is unchecked and Language is set to Swift. Click Next and then Create.
Open MasterViewController.swift, scroll down to numberOfSectionsInTableView(_:)
and replace the implementation as follows:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { // Return the number of sections. return 1 } |
Next, find tableView(_:numberOfRowsInSection:)
and replace the implementation with the following:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // Return the number of rows in the section. return 10 } |
Finally, uncomment tableView(_:cellForRowAtIndexPath:)
and replace its implementation with the following:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell // Configure the cell... return cell } |
This way, you’ll just have 10 empty rows to look at when you test this thing out later.
Open Main.storyboard and select the Root Table View Controller. Change the Class under Custom Class to MasterViewController using the Identity Inspector (3rd tab):
In addition, you need to make sure the Prototype Cell in the Table View is given a reuse identifier, or it will cause a crash when the Storyboard tries to load. Within the Master View Controller, select the Prototype Cell and then change the Identifier to “Cell” (which is already set as the default for UITableViewController subclasses in code), and the cell Style to “Basic”.
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, make it a subclass of UIViewController, and make sure the XIB checkbox is unchecked and Language is set to Swift. Click Next and then Create.
Open Main.storyboard, and select the view controller on the bottom detail half. Change the Class under Custom Class to DetailViewController:
Then drag a label into the middle, and pin it to the Horizontal and Vertical centers of the container with Auto layout.
Double-click the label to change its text to say “Hello, World!” so you know it’s working when you test it out later.
Build and run to try this out, and at this point you should see your custom view controllers. On iPad:
Making Your Model
The next thing you need to do is 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 just going to create a simple class with some instance variables with attributes about each Monster you want to display, and a couple of methods for creating new monsters and accessing the image for the weapon each monster has. Replace the contents of Monster.swift with the following:
import UIKit //an enum that defines a number of weapon options enum Weapon { case Blowgun, NinjaStar, Fire, Sword, Smoke } class Monster { let name: String let description: String let iconName: String let weapon: Weapon // designated initializer for a Monster init(name: String, description: String, iconName: String, weapon: Weapon) { self.name = name self.description = description self.iconName = iconName self.weapon = weapon } // Convenience method for fetching a monster's weapon image func weaponImage() -> UIImage? { switch self.weapon { 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") } } } |
This file defines an enumeration to track the different kinds of weapons, and then a class to hold the Monster information. There’s a simple initializer to create Monster
instances, and a convenience method to get an image corresponding to the monster’s weapon.
That’s it for defining the model – so let’s hook it up to your master view!
Displaying the Monster List
Open up MasterViewController.swift and add a new property to the class:
var monsters = [Monster]() |
This will hold an array of monsters to populate the table view.
Find tableView(_:numberOfRowsInSection:)
and replace the return
statement with the following:
return self.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 = self.monsters[indexPath.row] cell.textLabel?.text = monster.name |
This will configure the cell based on the correct monster. That’s it for the table view, which will simply show each monster’s name.
The array of monsters is still empty, so you won’t see any data or fancy weapon details yet. Download and unzip this art pack and drag the folder containing those images into Images.xcassets in Xcode.
After you add the images, open MasterViewController.swift and add the following initializer:
required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.monsters.append(Monster(name: "Cat-Bot", description: "MEE-OW", iconName: "meetcatbot.png", weapon: Weapon.Sword)) self.monsters.append(Monster(name: "Dog-Bot", description: "BOW-WOW", iconName: "meetdogbot.png", weapon: Weapon.Blowgun)) self.monsters.append(Monster(name: "Explode-Bot", description: "BOOM!", iconName: "meetexplodebot.png", weapon: Weapon.Smoke)) self.monsters.append(Monster(name: "Fire-Bot", description: "Will Make You Stamed", iconName: "meetfirebot.png", weapon: Weapon.NinjaStar)) self.monsters.append(Monster(name: "Ice-Bot", description: "Has A Chilling Effect", iconName: "meeticebot.png", weapon: Weapon.Fire)) self.monsters.append(Monster(name: "Mini-Tomato-Bot", description: "Extremely Handsome", iconName: "meetminitomatobot.png", weapon: Weapon.NinjaStar)) } |
This initializer will set up the array of monsters. Note that you are implementing init(coder:)
because this class is being loaded from a storyboard.
Build and run the app, and if all goes well you should now see the list of monster bots on the left hand side on landscape iPad:
and iPhone:
Remember that on 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.
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, go to the Detail View Controller and delete the placeholder label you put down earlier.
Using the screenshot below as a guide, drag the following controls into the DetailViewController’s view:
- A 95×95 image view for displaying the Monster’s image in the upper left hand corner.
- A label aligned with the top of the image view with font System Bold, size 30, and with the text “Monster Name”
- 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. They should have their left edges aligned, and titles “Description” and “Preferred Way To Kill”
- A 70×70 image view for displaying the weapon image, horizontally center aligned with the “Preferred way to Kill” label.
Need some more hints? Open the spoilers below for the set of constraints I used to make the layout.
Getting auto layout to use the proper constraints is especially important since this app is universal, and auto layout is what ensures the layout adapts well to both iPad and iPhone.
That’s it for auto layout for now. Next you’ll need to hook these views up to some outlets. Open DetailViewController.swift and add the following properties to 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 (newMonster) { self.refreshUI() } } |
Here you added properties for the various UI elements you just added which need to dynamically change. You also added a property for the Monster object this view controller should display.
Next, add the following helper method to the class:
func refreshUI() { nameLabel?.text = monster.name descriptionLabel?.text = monster.description iconImageView?.image = UIImage(named: monster.iconName) weaponImageView?.image = monster.weaponImage() } |
Whenever you switch the monster, you’ll want the UI to refresh itself and update the details displayed in the outlets. Note that refreshUI()
uses optional chaining even though the properties are implicitly-unwrapped optionals. It’s possible that you’ll change monster
and trigger the method even before the view has loaded, so you need to be sure to check that the outlets are connected first.
Finally, find viewDidLoad()
and add the following line of code to the end of that method:
refreshUI() |
This will trigger the UI refresh when the view first loads.
Now, go back Main.storyboard, right-click the Detail View Controller object to display the list of outlets, then drag from the circle at the right of each item to the view to hook up the outlets.
Remember, the icon image view is the big image view in the top left, while the weapon image view is the smaller one underneath the “Preferred Way To Kill” label.
Go to to AppDelegate.swift and replace the implementation of application(_:didFinishLaunchingWithOptions:)
with the following:
let splitViewController = self.window!.rootViewController as! UISplitViewController let leftNavController = splitViewController.viewControllers.first as! UINavigationController let masterViewController = leftNavController.topViewController as! MasterViewController let detailViewController = splitViewController.viewControllers.last as! DetailViewController let firstMonster = masterViewController.monsters.first detailViewController.monster = firstMonster return true |
A split view controller has an array property viewControllers
that has the master and detail view controllers inside. The master view controller in your case is actually a navigation controller, so you get the top view controller from that to get your MasterViewController instance. From there, you can set the current monster to the first one in the list.
Build and run the app, and if all goes well 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 different strategies for how to best communicate between these two view controllers. In the Master-Detail Application 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 ever have one view controller in the right 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 side will implement this method, and the master view controller will accept a delegate of an object which wants to know about this.
Then, 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 is required to be an object that has selectedMonster(_:)
implemented. That object will be responsible for handling what needs to happen within its view after the monster was selected.
Since you want DetailViewController
to update when the monster is selected, 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. Here, you’re saying DetailViewController
conforms to MonsterSelectionDelegate
and then you implement the one required method.
Now that the delegate method is ready, you need to call through to it from the master side. Open MasterViewController.swift and add the following method:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { let selectedMonster = self.monsters[indexPath.row] self.delegate?.monsterSelected(selectedMonster) } |
Implementing tableView(_:didSelectRowAtIndexPath:)
means you’ll be notified 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, open AppDelegate.swift. In application(_:didFinishLaunchingWithOptions:)
, add the following code just before the final return
statement:
masterViewController.delegate = detailViewController |
That’s the final connection between the two view controllers. Build and run the app on iPad, and you now should be able to select between the monsters like the following:
So far so good with split views! Except there’s one problem left – if you run it on iPhone, selecting monsters from the table view does not show the detail view controller. You now need add make a few small modifications to make sure the split view works on iPhone, in addition to iPad.
You only need to make one small modification in MasterViewController.swift. Find tableView(_:didSelectRowAtIndexPath:)
and add the following to the end of the method:
if let detailViewController = self.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 the split view controller the view controller is in, if one exists.
This new code only changes the behavior of the app on iPhone, causing the navigation controller to push the detail controller onto the stack when you select a new monster. It does not 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 is no way to get to the left menu, but try swiping from the left side of the screen. Pretty cool huh? Tap anywhere outside of the menu to hide it.
That built in swipe functionality is pretty cool, but what if you want to have a nav bar up top with a button that will display the menu, similar to how it behaves on iPhone. To do that, you will need to make a few more small modifications to the app.
First, open Main.storyboard and embed the Detail View Controller into a Navigation Controller by selecting the Detail View Controller, hitting Editor/Embed In/Navigation Controller. Your storyboard will now look like this:
Now open MasterViewController.swift and find tableView(_:didSelectRowAtIndexPath:)
. Change the line with the call to showDetailViewController(_:sender:)
with the following:
splitViewController?.showDetailViewController(detailViewController.navigationController, 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 AppDelegate.swift update application(_:didFinishLaunchingWithOptions:)
by replacing the single line initializing detailViewController
with the following two lines:
let rightNavController = splitViewController.viewControllers.last as! UINavigationController let detailViewController = rightNavController.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 final return
statement:
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 iPhone, but on iPad you will get a button in the top left to toggle the table view display. Run the app on iPad portrait and check it out:
Where To Go From Here?
Here’s an archive of the final project with all of the code you’ve developed so far.
For new apps, you’re likely to just 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. Now that 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, including the iPhone 6 Plus.
If you have any questions or comments, please join the forum discussion below!
UISplitViewController Tutorial: Getting Started is a post from: Ray Wenderlich
The post UISplitViewController Tutorial: Getting Started appeared first on Ray Wenderlich.