Tag Archives: Split View

Swift Swift: Split View Controllers (Round Two) Part 2: Master to Detail

Back in July of 2014, I wrote one of this first posts on Split views, I said I would write another post on the subject. I ended up making this a section in chapter six of my Book I ended up making this section 2 of Chapter six of my Book Swift Swift: UI View Controllers. The section came out as three projects, which I will post each as a separate post here. iOS8 makes split views universal, not just iPad. It does different things depending on class sizes. I thought I’d share with you a preview of that book. For more previews and interesting thoughts, sign up for my newsletter. –Steve

In our first part, we moved data using a delegate from detail to master. In this post we move the other direction.

Moving Data from Master to Detail

Our next part is to move data from the master to the detail. We’ll make a new demo for this to choose a color and display its values in different color identification systems. We use colors since it is easy to make a model, instead of a more complicated order model.

New Project from the Storyboard.

Open a new project with Command-Shift-N. Select a Single View template. Name the project SwiftSplitColorDemo, as a Universal app with Swift as the language. Once saved, go to the storyboard, and delete the view controller there. Drag a split view controller out on to the storyboard from the object library. Click on the split view controller and check on the Is Initial View Controller in the attributes inspector. The storyboard looks slightly different from the template:

Screenshot 2015-03-09 07.20.43

The detail is a view controller, not a view controller embedded in a navigation controller as the template. Add three labels, and set up the view controller to look like this with white backgrounds on the labels, and a light gray background for the superview:

Screenshot 2015-03-09 08.40.31

Add auto layout by , pinning the Hue: label 50 points up, 0 left and 0 right with a height of 50 points. Pin the Red label 10 points up, 0 left and 0 right with a height of 50 points Pin the Hex RGB label 10 points up, 0 left and 0 right with a height of 50 points. Update the frames.

The Colorful Detail View Controller

Press Command-N and make a new Cocoa Touch Class named ColorViewController subclassing UIViewController with Swift as the language. Add the following code to the controller:

class ColorViewController: UIViewController {

    @IBOutlet weak var hsbLabel: UILabel!
    @IBOutlet weak var rgbLabel: UILabel!
    @IBOutlet weak var hexLabel: UILabel!

func displayColor(color:UIColor){
    view.backgroundColor = color

    var red:CGFloat = 0.0,green:CGFloat = 0.0,blue:CGFloat = 0.0
    var hue:CGFloat = 0.0, sat:CGFloat = 0.0,bright:CGFloat = 0.0
    var alpha:CGFloat = 0.0
    let hueFormat = "H:%5.2f  S:%5.2f  B:%5.2f"
    let rgbFormat = "R:%0.2f  G:%0.2f  B:%0.2f"
    let hexFormat = "Hex: %02X%02X%02X"
    if color.getHue(&hue,saturation: &sat,brightness: &bright,alpha: &alpha){
        hsbLabel.text = String(format:hueFormat,
             Double(hue),Double(sat),Double(bright) )
     }
     if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha){
         rgbLabel.text = String(format: rgbFormat,
             Double(red),Double(green),Double(blue))
         red = red * 255
         green = green * 255
         blue = blue * 255
         hexLabel.text = String(format:hexFormat,Int(red),Int(green),Int(blue))
     }
}

}

We have three outlets for our label in this controller. We have one function with a parameter color. The method first sets the background color of the view We do some initialization for the getRed and getHue methods. These two methods use some very evil parameter types. We do this setup to make the safest use. Both methods return a Boolean value, but we set up the parameters with & to make the RGB and HSB values pass-through parameters and thus we can read their values after the method executes. We check if we did convert a color in each case, and if we did, make a string with the results.

Make the Master View Controller

Press Command-N. Make a Cocoa Touch Class named ColorTableViewController, subclassing UIViewController. We’ll use our simplified dynamic table view again. Change the class code to this:

class ColorTableViewController: UITableViewController {
    let hueCount = 5
    let saturationCount = 10
}

This changes the class to a UITableViewController, and adds two constants. These two constants are the number of hues we will have and the number of saturation samples from that hue. We will tell the table view this through our two data source methods. Add the following code:

   override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return hueCount
    }
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return saturationCount
    }

Next we will use a shortened version of the same background color technique we used in the table view section to make a list of colors .

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let hue = CGFloat(indexPath.section) / CGFloat(hueCount)
    let saturation = 1.0 - CGFloat(indexPath.row) / CGFloat(saturationCount)
    let color = UIColor(hue: hue, saturation: saturation,
        brightness: 1.0, alpha: 1.0)

    let cell = tableView.dequeueReusableCellWithIdentifier(
    "cell",
        forIndexPath: indexPath) as UITableViewCell
    cell.backgroundColor = color
    return cell
    }

Finally, we select the colors. We need to access the detail, and we access it through the controllers property as before. Add the following code:

override func tableView(tableView: UITableView,
    didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath)
    if let split = self.splitViewController {
        let controllers = split.viewControllers
        let detailViewController = controllers[controllers.count-1]
            as? ColorViewController
        detailViewController?.displayColor((cell?.backgroundColor)!)
    }
}

In this case, we have no navigation controller. So the controller in the last spot on controllers is the controller we want. We don’t need topViewController.

Add to the Storyboard

Go to the storyboard. Select the Root View Controller in the outline. In the identity inspector make its class ColorTableViewController. Select the table view’s cell. In the attributes inspector, change the identifier to cell. Drag a navigation item onto the top toolbar. Select the navigation item title and change the title to Master.
Select the view controller that is our detail. In the identity inspector, change the class to ColorViewController. Open the assistant editor. From the circle on each of the outlets drag to the appropriate label.
Select an iPad 2 in the simulator. Build and run. If the device is not in landscape, rotate the device with Command-Left Arrow. The master and detail will be side by side. Scroll through the colors and select a color on the master, and the info shows up in the detail.

Screenshot 2015-03-09 11.10.15

Adding a Navigation Controller

Many times we will need a navigation controller for detail, since we may need to access other scenes from this root scene. In the storyboard, select the detail view controller scene. Select Editor>Embed In>Navigation controller. A navigation controller shows up between the split view controller and the view controller.

Screenshot 2015-03-09 11.16.12

On the detail view, add a navigation item to to the space for the navigation bar. Title it Color Details.
In the didSelectrowAtIndexPath,change

let detailViewController = controllers[controllers.count-1]
    as? ColorViewController

to

let detailViewController = controllers[controllers.count-1].topViewController
    as? ColorViewController

Now we have a navigation controller and want the top view controller. Build and run again. It still works the same.

Screenshot 2015-03-11 05.47.09

Rotate back to portrait. It seems we cannot access the master. Swipe from left to right and it appears:

Screenshot 2015-03-11 05.47.31

Swipe right to left to dismiss it. Stop the simulator and change over to an iPhone 6. Run again:

Screenshot 2015-03-11 08.15.06

we get a screen with the toolbar for navigating to the master. Press the button and we get the master

Screenshot 2015-03-11 08.15.21

However we get stuck. There is no way back.

Adding a Segue

We’ve used didSelectRowAtIndexPath to select our information. That works for split view controllers on regular width devices. We will add segue from the table view cells to bring us to the detail. This will work on both compact and regular width devices. Go to the storyboard, and open up the document outline. Find the detail navigation controller and the cell for the table view.

Screenshot 2015-03-11 08.31.48

Control-drag from cell to Navigation Controller. Select a Show Detail selection segue. In the attributes inspector, name it detail. Build and run. You will find that selecting a cell sends us back to the detail, though nothing happens.

Using prepareForSegue with Split Views

With the segue operational, the didSelectRowAtIndexPath code gets eclipsed. What we did in that delegate we need to move over to a prepareForeSegue method. Open the ColorTableViewController.swift file. Add to the ColorTableViewController code the following:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "detail"{
        let detailNavigationController = segue.destinationViewController as?
            UINavigationController
    }
}

This is the standard start to this method. We assign the proper view controller to the destination. Our destination is a navigation controller, not a view controller. We need the top view controller from the navigation controller. Add this below the detailNavigationController assignment:

let detailViewController = detailNavigationController?.topViewController as
    ColorViewController
We have the correct controller.  Add this :
let indexPath = (tableView.indexPathForSelectedRow())!
let cell = tableView.cellForRowAtIndexPath(indexPath)
detailViewController.displayColor((cell?.backgroundColor)!)

In didSelectRowforIndexPath we had a parameter indexPath which is the index for the selected cell. In prepareForSegue, we get the index path from the indexPathForSelectedRow method. Once we have an index path, we get the cell as we did before. We then call the detail’s method displayColor with the background color of the cell to display the information in the detail. Build and run. Go to the master and select a color:

Screenshot 2015-03-11 09.07.39

We now have colors.

Hiding and Showing the Master

Hiding and showing the master needs to be more under user control. Add to the end of the prepareforSegue method this code:

detailViewController.navigationItem.leftBarButtonItem =
    self.splitViewController?.displayModeButtonItem()
detailViewController.navigationItem.leftItemsSupplementBackButton = true

The split view controller has a special bar button method displayModeButtonItem. This method creates a special bar button for controlling the master. While it works in viewDidLoad, it causes a crash on auto-rotation. It seems to work correctly only when added here. Using an iPhone 5s simulator, build and run. Look at the detail in portrait and landscape

Screenshot 2015-03-11 09.45.23 Screenshot 2015-03-11 09.45.37
Now try an iPad 2:

Screenshot 2015-03-11 09.46.23 Screenshot 2015-03-11 09.46.40
And finally a iPhone 6 plus

Screenshot 2015-03-11 09.48.01 Screenshot 2015-03-11 09.48.13 Screenshot 2015-03-11 09.48.22
The system makes a different button for each device in each orientation. It uses the most appropriate button look for that device.

In our last part about split view controllers, we’ll cover material from my original post — the disappearing outlet and what to do about it.

Swift Swift: Split View Controllers (Round Two) Part 1: The Master-Detail template

Back in July of 2014, I wrote one of this first posts on Split views, I said I would write another post on the subject. I ended up making this a section in chapter six of my Book I ended up making this section 2 of chapter six of my Book Swift Swift: UI View Controllers. The section came out as three projects, which I will post each as a separate post here. iOS8 makes split views universal, not just iPad. It does different things depending on class sizes. I thought I’d share with you a preview of that book. For more previews and interesting thoughts, sign up for my newsletter. –Steve

Unlike a desktop, there are no multiple windows on an iOS device. However there are times where this would be useful. For example the bookmarks and web page in Safari make for easy access to both the bookmarks and the web content. We could make one to take orders at a pizza restaurant.

Screenshot 2015-03-09 06.48.30

This is a split view controller. The left side with the bookmarks is called the master, and the right the detail. Split view controllers were originally designed for iPads only. Like popovers, view controllers would cause fatal errors in iOS7 and earlier on iPhones. In iOS8, a regular width class size displays a split view controller. With a compact width size, the master displays in a navigation controller with a segue to the detail.
Communication can happen from the master to the detail or from the detail to the master. We will discuss both as we go through two ways to set up a split view controller.

Data to the Master

There is a template for split view controllers. Make a new project using the Master-Detail Application template. Call it SwiftTemplateSplitView. Make it Universal using Swift. Open the storyboard. It appears a lot more complicated than most templates (detail in gray for visibility. ).

Screenshot 2015-03-12 10.49.19

On the left is the split view controller. To the right are two navigation controllers connected to the view controller by special segues. These segues set what is the detail and the controller. Open the document outline. Click the segue to the top controller. In the document outline you will see this:

Screenshot 2015-03-08 18.57.09

The two segues are different types: one for the master and one for the detail. Usually the top one is the master. Both segues lead to navigation controllers. The navigation controllers lead to a view controller. The template sets up the typical case: the master is a dynamic table view controller scene and the detail is a generic view controller scene. Masters are almost always table views.
In the storyboard, delete the detail scene, not the navigation controller. Drag a Table View Controller onto the storyboard. Control-drag from the detail navigation controller to the table view. and select root view controller as the Relationship Segue.
In the document outline, select the Table View in the Table View Controller scene. In the attributes inspector, change the Content to static cells. Select the Table View Section in the document outline. In the attributes inspector change the Rows to 1 and the Header Title to Pizza.
Select the Table View Cell. Change the Style to Basic. Static cells will copy current settings. Click the Pizza section header, and change Rows to 3. Select Table View. Change the Number of Sections to 3. Change the titles on the table to this:

Screenshot 2015-03-08 19.27.19

Drag a navigation item onto the top toolbar. Change the title to Menu.
Make a Cocoa Touch Class static table view controller by Pressing Command-N on the keyboard. Make a UIViewController named MenuTableViewController.
Change the class to one subclassing UITableViewController like this:

class MenuTableViewController: UITableViewController {
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        //grab the cell
        let cell = tableView.cellForRowAtIndexPath(indexPath)
        //get the title of the cell
        let myOrder = (cell?.textLabel?.text)!
        //display in the navigation bar
        navigationItem.title = myOrder
    }

}

Using the didSelectAtIndexpath method, we get the selected cell, then get the title string. Just so we know we selected something, we display the order on the navigation title.
Go back to the storyboard, and select the menu’s table view controller. In the identity inspector, set the class to MenuTableViewController. Set the simulator to iPad 2. Build and run. We get a table view with the menu.

Screenshot 2015-03-08 20.17.55

Select an item and it shows in the navigation bar. Rotate the device by pressing Command- Left Arrow. The master appears on the left.

Screenshot 2015-03-08 20.20.24

The Delegate Protocol

Quit the app in the simulator, and go back to the MenuTableViewController Class. Add above the class the following protocol:

protocol MenuTableViewControllerDelegate{
    func didSelectMenuItem(order:String)
}

Add the delegate to the MenuTableViewController Class

var delegate:MenuTableViewControllerDelegate! = nil

Add the delegate method to didSelectRowAtIndexPath:

delegate.didSelectMenuItem(myOrder)

We now have a delegate method set up on this side. We can use it in the master.

Change the Master

Go over to MasterViewController.Swift. This is part of the demo that loads with the split view controller. You’ll find a lot of code. As we’ll discuss later, it is a bad practice to use this code. We will replace this with a new table view controller. Press Command-N. and make a new Cocoa Touch Class named OrderMasterViewController subclassing UIViewController.
We will set up a simple table view controller. Remove the code that is there for the class and replace with this:

class OrderMasterViewController: UITableViewController {

    var orderList:[String] = ["a","b","c","d"]

    //MARK: Table View Data Source and Delegate
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return orderList.count
    }
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
        cell.textLabel?.text = orderList[indexPath.row]
        if indexPath.row % 2 == 1 { //alternating row backgrounds
            cell.backgroundColor = UIColor.yellowColor()
        } else {
            cell.backgroundColor = tableView.backgroundColor
        }
        return cell
    }
}

This is a basic dynamic table view from the previous section. We made this class a subclass of UITableViewController for dynamic content. We added a very simple model of a string array, populated for the moment with some dummy data for testing. We added our three required methods. For our data source, we will have one section, and the size of the array will tell us how many rows in a section. In the table view cell, we will place on the textLabel the order item. We will also alternate colors for the rows on the table view.
In the storyboard, select the master table. Since this is already a table, click on its view controller icon, and head over to the identity inspector. Change to OrderMasterViewController.
Coordinate the cell reuse identifier. Using the document outline, select the table view cell. change the reuse identifier from Cell to cell. Select the Table view for the master. Change the background color in the view section of the attributes inspector to #AAAAFF by selecting the color swatch and entering the hex code into the color inspector. Build and run. The table view for the master is working:

Screenshot 2015-03-09 06.07.19

Add the Delegate

Much of the delegate setup is the same as in the table view version of the app. The difference is assigning the delegate in the master controller. The controllers are not separated by a single segue. Both are live at the same time, so there is no event that activates a segue. So there is no prepareForSegue this time.
Add the following code for the OrderMasterViewController method viewDidLoad.

override func viewDidLoad() {
    super.viewDidLoad()
    if let split = self.splitViewController {
        let controllers = split.viewControllers
        self.detailViewController =
            controllers[controllers.count-1].topViewController
            as? MenuTableViewController
    }
    self.detailViewController?.delegate = self
}

Instead of prepareForSegue we set up the delegate in viewDidLoad. The split view controller keeps an array of view controllers called viewControllers. After checking if we have a split view controller, we make a constant controller to get this array. This next line does most of the work getting the detail’s view controller:

self.detailViewController =
            controllers[controllers.count-1].topViewController
            as? MenuTableViewController

The first element in the array controllers is the master,the last the detail. First we grab the detail controller, which is a navigation controller. The top controller on the detail’s navigation stack is the one we present for the detail. We get it and downcast it to MenuTableViewController. We assign all of this to an additional property detailViewController. Since detail view controller points to the MenuTableViewController, we set the delegate from it.
Add below the model property declaration the following property for the detail:

var detailViewController: MenuTableViewController? = nil

Add the protocol to the class definition:

class OrderMasterViewController:
    UITableViewController,MenuTableViewControllerDelegate {
Add the required method didSelectMenuItem :
func didSelectMenuItem(order: String) {
        orderList.append(order)
        tableView.reloadData()
    }

Remove the sample data from the orderList array.

var orderList:[String] = []

Build and run. Select a few items from the menu. We get a list of ordered items on the master.

Screenshot 2015-03-09 06.48.30

In our next lesson, which you can find here,   we will make another split view controller from the storyboard, and make a split view controller compatible with iPhones using class sizes.

The Whole Code

OrderMasterViewController.swift

//
//  OrderMasterViewController.swift
//  SwiftTemplateSplitView
//
//  Created by Steven Lipton on 3/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class OrderMasterViewController: UITableViewController, MenuTableViewControllerDelegate {

    var orderList:[String] = []
    var detailViewController: MenuTableViewController? = nil

    //MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        if let split = self.splitViewController {
            let controllers = split.viewControllers
            self.detailViewController =
                controllers[controllers.count-1].topViewController
                as? MenuTableViewController
        }
        self.detailViewController?.delegate = self
    }
    //MARK: -  Delegates
    func didSelectMenuItem(order: String) {
        orderList.append(order)
        tableView.reloadData()
    }

    //MARK:  Table View Data Source and Delegate
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return orderList.count
    }
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
        cell.textLabel?.text = orderList[indexPath.row]
        if indexPath.row % 2 == 1 { //alternating row backgrounds
            cell.backgroundColor = UIColor.yellowColor()
        } else {
            cell.backgroundColor = tableView.backgroundColor
        }
        return cell
    }

}

The detail — MenuTableViewController.swift

//
//  MenuTableViewController.swift
//  SwiftTemplateSplitView
//
//  Created by Steven Lipton on 3/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//
// the detail view using a Static table view controller
//

import UIKit

//protocol for the delegate
protocol MenuTableViewControllerDelegate{
    func didSelectMenuItem(order:String)
}

//Since this is static, we dont need a data source or anything else. Just selection.

class MenuTableViewController: UITableViewController {
    var delegate:MenuTableViewControllerDelegate! = nil

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        //grab the cell
        let cell = tableView.cellForRowAtIndexPath(indexPath)
        //get the title of the cell
        let myOrder = (cell?.textLabel?.text)!
        //display in the navigation bar
        navigationItem.title = myOrder
        delegate.didSelectMenuItem(myOrder)
    }

}

The Swift Swift Tutorial: How to Use Split View on iPad (Round 1)

Back in July of 2014, I wrote this first post on using UISplitViewController in Swift. I gave how to use Apple’s template, which has a few interesting quirks, such as ARC killing@IBOutlets at the drop of a hat. I said I would write another post on the subject. I ended up making this section 2 of Chapter six of my Book Swift Swift: UI View Controllers . iOS8 makes split views universal, not just iPad. It does different things depending on class sizes. I thought I’d share with you a preview of that book.It came out as three projects, which if you want and updated version without ARC killing your detail’s outlets, go here to start the series: Swift Swift: Split View Controllers (Round Two) Part 1: The Master-Detail template For more previews and interesting thoughts, sign up for my newsletter. –Steve

Two and a half weeks ago I had a request from a reader to cover split views and split view controllers in Swift. My apologies for taking this long, but now I know the reason for this user’s request. For those not familiar with split view controllers, they are a view controller meant for the iPad. They take two view controllers and place them in the same window. On the left  is the master, and is almost always a table view controller. The other view controller is known as the detail, which is a standard UIView controller.

Portrait Split Image View
Portrait Split Image View

I will admit I had a lot of problems implementing split views in Swift. I finally did get it to work. There is a change in Swift’s implementation that requires a bit of getting used to. This will be the first round of split views. In this post, I’ll show how to use the master-detail template to get them to work. After I cover a few other topics which will be useful in a more advanced version, I’ll revisit split views in Swift.

The Problem with the Detail Controller

The way I learned to make a split view with Objective-C was by dragging out a split view controller on a blank storyboard, and writing everything myself. That doesn’t seem to work too well and I was getting one of Swift’s most common error messages: fatal error: Can't unwrap Optional.None This is the run-time error when you unwrap a nil value from an optional.  This has to do with very weak references. The error happens in @IBOutlets in the DetailViewController . Outlets are by definition weak, and by Swift’s definition anything weak is also an optional. According to Apple’s book Using Swift with Cocoa and Objective-C. but @IBOutlet var name:Type is all we need to write. The compiler assumes the rest for us and defines an outlet as @IBOutlet weak var name: Type! = nil

Outlets in the detail controller cannot be directly accessed like an outlet hooked up anywhere else on a storyboard. In Swift, outlets are so weak in this case they are usually nil. Somehow the meager connections to the split view controller are not enough to keep them alive, though with the equivalent code in Objective-C there are enough strong pointers. In Swift, if you try something simple like this in the detail:

pizzaLabel.text = "Veggie"

That run-time error fatal error: Can't unwrap Optional.None. appears.

Apple’s Master-Detail template does work however.  There is a way to do this. In this lesson, we’ll set up the template to make a split view for PizzaDemo. We’ll cut and paste a lot of code from our last post on table views, and add a few new parts for the Split controller. As an iPad application, we will change our UI a bit from the iPhone. We’ll make our master the list of pizza types, and replace the segmented control with the master. In this session, we will not change the prices, but just get the data displayed in the model.

Get the template

In Xcode start a new project on the welcome screen, press command-shift-N or go to File>New>Project… Select in the iOS Application the Master-Detail Application. Name the app SwiftSplitPizzaDemo select Swift for the language and iPad for the device. Keep Core Data unchecked.

Select your file location and click Create. In the files created click the story board and look at what Xcode loaded for you.

The split view controller splits into two navigation controllers. One navigation controller has a UITableViewController subclass found in MasterViewController.swift. The other navigation controller has a UIViewController  subclass, found in DetailViewController.swift.

Xcode provides some sample code in the template to run an small application. Change to the iPad2 simulator, since it will fit on any screen nicely. Build and run. You get a pretty blank screen with a label on it. On your keyboard press the command key and the left or right arrow keys together a few times to rotate the ipad. As you change the orientation, the screen changes. Go back to portrait, and swipe right. the master view slides out. Press the plus button in the corner of the master view to add to the table. Rotate to a landscape view and hit plus again. Now select one of your two selections in the table view. The label in the detail changes.

We know what is there works. Now to replace this code with our own. Go to the storyboard. Start by selecting the dynamic table cell, changing the table cell to tableCell in the storyboard.  Also change the cell style to right detail.

Add the Model

Open the PizzaDemo application with the table view in it from our last lesson, if you want to copy and paste from there. If you do not have it, you can download it from github or you can copy what I have below.

We first need a Model. if you have the PizzaDemo code open, drag the Pizza.swift file over to SwiftSplitPizzaDemo. make sure Copy items if needed has a check next to it, and hit Finish. If you do not have the PizzaDemo open and don’t want to load it, hit command-N on the keyboard, and make a Cocoa Touch class called Pizza in Swift which subclasses NSObject. In the new Pizza.swift file, add this code:

import Foundation
/* --------

Our model for MVC
keeps data  and calculations
about pizzas

------------*/

class Pizza {
    var pizzaPricePerInSq = ["Cheese": 0.03 , "Sausage": 0.06 , "Pepperoni": 0.05 , "Veggie": 0.04]
    var typeList:Array {  //computed property 7/7/14
    get{
        return Array(pizzaPricePerInSq.keys)
    }
    }

    let pi = 3.1415926

    var pizzaDiameter = 0.0
    let maxPizza = 24.0
    var pizzaType = "Cheese"

    var radius : Double {  //computed property
    get{   //must define a getter
        return pizzaDiameter/2.0
    }
    set(newRadius){ //optionally define a setter
        pizzaDiameter = newRadius * 2.0
    }
    }

    var area :  Double {
    get{
        return pizzaArea()
    }
    }

    func pizzaArea() -> Double{
        return radius * radius * pi
    }

    func unitPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if unitPrice != nil {
            return unitPrice!
        }                               //optional type ?Double checking for nil
        else {
            return 0.0
        }
    }

    func pizzaPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if unitPrice != nil {                                   //optional type ?Double checking for nil
            return pizzaArea() * unitPrice!             //unwrapping the optional type
        }
        return 0.0
    }

    func diameterFromString(aString:NSString) -> Double {
        switch aString {
        case "Personal":
            return 8.0
        case "10\"":
            return 10.0
        case "12\"":
            return 12.0
        case "16\"","15\"":
            return 16.0
        case "18\"":
            return 18.0
        case "24\"":
            return 24.0
        default:
            return 0.0
        }
    }

}

Add the Master

Go to the MasterViewController.swift file. Go into the MasterViewController class and comment out the following line:

var objects = NSMutableArray()

This will produce a lot of error messages. We will need to change the code in each place there is an error message. To add our model, just under where you commented out, objects add:

var pizza = Pizza()

Add the TableView Code

Scroll down to where the table view data source is:

   //MARK:  - Table View

    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
        return objects.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

        let object = objects[indexPath.row] as NSDate
        cell.textLabel.text = object.description
        return cell
    }

Replace it with the following code.

//MARK: -Table View
    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
        // Return the number of sections.
        return 1
    }

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
        // Return the number of rows in the section.
        return pizza.pizzaPricePerInSq.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell
        let row = indexPath!.row   //get the array index from the index path
        let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as UITableViewCell  //make the cell
        let myRowKey = pizza.typeList[row]  //the dictionary key
        cell.textLabel!.text = myRowKey
        let myRowData = pizza.pizzaPricePerInSq[myRowKey]  //the dictionary value
        cell.detailTextLabel!.text = String(format: "%6.3f",myRowData!)
        return cell
    }

    override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
        return 44.0
    }

We will not be editing the table. Comment out this method for insertion:

/*
    func insertNewObject(sender: AnyObject) {
        if objects == nil {
            objects = NSMutableArray()
        }
        objects.insertObject(NSDate.date(), atIndex: 0)
        let indexPath = NSIndexPath(forRow: 0, inSection: 0)
        self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
    }
*/

Also comment out this method for deleting rows:

/*
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            objects.removeObjectAtIndex(indexPath.row)
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        } else if editingStyle == .Insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
        }
    }
*/

Disable editing of the table by returning false here:

    override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        return false
    }

We just made a table. We covered how to do that last time. Here we begin to see something new. Change tableview(didSelectRowAtIndexpath:indexPath:) to this:

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        //let object = objects[indexPath.row] as NSDate
        //self.detailViewController!.detailItem = object
        pizza.pizzaType = pizza.typeList[indexPath.row] //set to the selected pizza
        if (detailViewController != nil){
            self.detailViewController!.detailItem = pizza //send the model to the detailItem
        }

    }

Looking at the top of our code the master view controller makes an instance of the DetailViewController class. Here we set the detailItem property of the detail view controller, whose type is AnyObject!. This is key to everything, but we’ll discuss the property more in a few minutes. We set the pizzaType property to the selected pizza, and then send the model to the detail controller via this property.

To be consistent, handle an ios8 issue, and to get rid of the last error, change prepareForSegue() like this:

 override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "showDetail" {
            let indexPath = self.tableView.indexPathForSelectedRow()
            //    let object = objects[indexPath.row] as NSDate
            //((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = object
            pizza.pizzaType = pizza.typeList[indexPath!.row] //set to the selected pizza
            ((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = pizza
        }
    }

Line 7 will not make a lot of sense if you are unfamiliar with split view controllers. Take a look at the storyboard:

The storyboard
The storyboard

One of a split view controller’s properties is an array called viewControllers. At most it has two objects,  navigation controllers for the master and the detail views. Embedded in each of these navigation controllers is a master and detail view. For the segue, we go from the master controller to the navigation controller, which is the segue’s destination. The first view controller in the destination navigation view controller’s array of controllers is the detail view. We access that by ((segue.destinationViewController as UINavigationController).topViewController Once we have the detail view controller, we assign the model to the detailItem property.

Now change viewDidLoad to this:

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
/* Removed code for editing and use of bar buttons.
        self.navigationItem.leftBarButtonItem = self.editButtonItem()

        let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "insertNewObject:")
        self.navigationItem.rightBarButtonItem = addButton
*/
        let controllers = self.splitViewController!.viewControllers
        self.detailViewController = controllers[controllers.endIndex-1].topViewController as? DetailViewController

        }

Lines 10 and 11 set up the detail view controller in the master. It takes the detail view from the viewControllers array we assigned to controllers We also removed the bar button item navigation bar stuff since we will not be using it in this tutorial.

We now have a completed master view controller. Essentially it is a table view with some odd connections to a property in the detail view controller. Build and run. In portrait swipe from left to right. Try rotating the iPad and see what happens.

Add the Detail

The next step is to design the detail view controller. We will be showing a lot more information on this scene, so a good layout is important.

Design and Connect the Scene

Go into the storyboard and go to the detail view controller there. Add five labels  with the text Pizza:, Size:, Anchovy, 12″, and Pizza Info And Price. Add one button with a blue background and white foreground labeled 10″. Copy the button five times. Resizing as necessary, lay out and label everything something like this:

Layout for the detail view

I did use auto layout here since we will be rotating the iPad.

Open the assistant editor to the detail view controller and add the following outlets by control dragging from the Anchovy, 12″, and Pizza Info And Price labels on the storyboard.

    @IBOutlet var pizzaSizeLabel: UILabel!
    @IBOutlet var pizzaTypeLabel: UILabel!
    @IBOutlet var pizzaPriceLabel: UILabel!

Adding Code for the DetailViewController

first, add the model to the view

var pizza = Pizza()

Next, add this code for the size buttons.

@IBAction func pizzaSizeButton(sender: UIButton) {
pizza.pizzaDiameter = pizza.diameterFromString(sender.titleLabel.text)
configureView()
}

Drag from the circle to each of the buttons to connect them all to this method.

Take a look at this code:

    var detailItem: AnyObject? {
        didSet {
            // Update the view.
            self.configureView()
//comment out this if clause  if you want the master to not disappear.
            if self.masterPopoverController != nil {
                self.masterPopoverController!.dismissPopoverAnimated(true)
            }
        }
    }

Lines 1-4 are at the heart of the way we get outlets to work. The MasterViewController instantiated a detailViewController. The master then set this property detailItem with the model, which is the data we wanted to pass to the detail controller. While just displaying is not strong enough a connection to keep the outlets alive, this connection is. To use an outlet we refer to detailItem. When set, detailItem calls configureView(), which looks like this:

    func configureView() {
        // Update the user interface for the detail item.
        if let detail: AnyObject = self.detailItem {
            if let label = self.detailDescriptionLabel {
                label.text = detail.description
            }
        }
    }
  

This is Apple’s template code to put text on a label. In line 2 we do two things at once. First we assign detailItem to a new constant detail. If that is successful, the app moves on to assigning another variable label to a UILabel. If that is successful, the app uses the assigned label to change the label properties. We sneakily assigned a strong pointer to a bunch of weak ones, at least for the life of the method. This is the way to access IBOutlets. Replace this code with the following:

     func configureView() {
        // Update the user interface for the detail item.
        if let detail = self.detailItem as? Pizza {     //if we have an object in the detail item
            pizza = detail
            if let label = self.pizzaTypeLabel {
                label.text = detail.pizzaType
            }
            let pizzaSizeString = NSString(format:"%6.1fin Pizza",detail.pizzaDiameter)
            let priceString = NSString(format:"%6.2f sq in at $%6.2f is $%6.2f",detail.pizzaArea(),detail.unitPrice(),detail.pizzaPrice())
            if let label = self.pizzaSizeLabel{
                label.text = pizzaSizeString
            }
            if let label = self.pizzaPriceLabel{
                label.text = priceString
            }

        }

    }

A few things changed  in configureView() for this example. In line 2, since there is a single detail view,  detailItem receives only the model.  I cast this directly to a instance of Pizza. I assign this to the model in the detail controller. While the outlets can be set here in configureView() everything else won’t be. If we want the properties from the buttons to work, we need to set the model here. After setting detail, we follow the if let label= pattern for updating our labels. That’s the trick to getting outlets to work in split views.

The Model initializes to the same values on both controllers, head over to the master view controller’s viewDidLoad() and add the following line:

pizza.pizzaPricePerInSq["Pepperoni"] = 9.99

This places a marker to prove our connections work.
Build and run. Play around with the app in different orientations.
splitview demo 4

There’s Always One More Bug

If you try to change the size of a pizza before selecting one in the master view, you’ll find nothing happens. We need to run configureView() in the detailViewController to get it to work. Go back to the MasterViewController‘s viewDidLoad() and add the following:

//send the model to the detailItem
        if (detailViewController){
            self.detailViewController!.detailItem = pizza

The pizza = detail assignment buried in line 4 of configureView() connects up enough to get the buttons to work.

This code works, and using the master-detail template is easy, though cumbersome, once you know the trick I showed here. There are still a lot of unanswered questions. We’ll explore some of those in the upcoming installments.

The Whole Code

Here is the code for the model, master view controller and detail view controller for copying and review. You can find a copy of the completed  project at Github as well.

Pizza.Swift

//
//  Pizza.swift
//  pizzaDemo
//
//  Created by Steven Lipton on 7/1/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

import Foundation
/* --------

Our model for MVC
keeps data  and calcualtions
about pizzas

------------*/

class Pizza {
    var pizzaPricePerInSq = ["Cheese": 0.03 , "Sausage": 0.06 , "Pepperoni": 0.05 , "Veggie": 0.04]
    var typeList:Array {  //computed property 7/7/14
    get{
        return Array(pizzaPricePerInSq.keys)
    }
    }

    let pi = 3.1415926

    var pizzaDiameter = 0.0
    let maxPizza = 24.0
    var pizzaType = "Cheese"

    var radius : Double {  //computed property
    get{   //must define a getter
        return pizzaDiameter/2.0
    }
    set(newRadius){ //optionally define a setter
        pizzaDiameter = newRadius * 2.0
    }
    }

    var area :  Double {
    get{
        return pizzaArea()
    }
    }

    func pizzaArea() -> Double{
        return radius * radius * pi
    }

    func unitPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if (unitPrice != nil) {
            return unitPrice!
        }                               //optional type ?Double checking for nil
        else {
            return 0.0
        }
    }

    func pizzaPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if (unitPrice != nil) {                         //optional type ?Double checking for nil
            return pizzaArea() * unitPrice!             //unwrapping the optional type
        }
        return 0.0
    }

    func diameterFromString(aString:NSString) -> Double {
        switch aString {
        case "Personal":
            return 8.0
        case "10\"":
            return 10.0
        case "12\"":
            return 12.0
        case "16\"","15\"":
            return 16.0
        case "18\"":
            return 18.0
        case "24\"":
            return 24.0
        default:
            return 0.0
        }
    }

}

MasterViewController.swift

//
//  MasterViewController.swift
//  SwiftSplitPizzaDemo
//
//  Created by Steven Lipton on 7/17/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

import UIKit

class MasterViewController: UITableViewController {

//MARK: Life Cycle
    var detailViewController: DetailViewController? = nil
    //var objects = NSMutableArray()
    var pizza = Pizza()

    override func awakeFromNib() {
        super.awakeFromNib()
        self.clearsSelectionOnViewWillAppear = false
        self.preferredContentSize = CGSize(width: 320.0, height: 600.0)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
/* Removed code for editing and use of bar buttons.
        self.navigationItem.leftBarButtonItem = self.editButtonItem()

        let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "insertNewObject:")
        self.navigationItem.rightBarButtonItem = addButton
*/
        let controllers = self.splitViewController!.viewControllers
        self.detailViewController = controllers[controllers.endIndex-1].topViewController as? DetailViewController

        pizza.pizzaPricePerInSq["Pepperoni"] = 9.99

        //send the model to the detailItem
        if (detailViewController != nil ){
            self.detailViewController!.detailItem = pizza
        }

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
/*
    func insertNewObject(sender: AnyObject) {
        if objects == nil {
            objects = NSMutableArray()
        }
        objects.insertObject(NSDate.date(), atIndex: 0)
        let indexPath = NSIndexPath(forRow: 0, inSection: 0)
        self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
    }
*/
//MARK: - Segues

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "showDetail" {
            let indexPath = self.tableView.indexPathForSelectedRow()
            //    let object = objects[indexPath.row] as NSDate
            //((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = object
            pizza.pizzaType = pizza.typeList[indexPath!.row] //set to the selected pizza
            ((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = pizza
        }
    }
//MARK:  - Table View Delegates
    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
        // #warning Potentially incomplete method implementation.
        // Return the number of sections.
        return 1
    }

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete method implementation.
        // Return the number of rows in the section.
        return pizza.pizzaPricePerInSq.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
        //note I did not check for nil values. Something has to be really broken for these to be nil.
        let row = indexPath!.row   //get the array index from the index path
        let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath!) as UITableViewCell  //make the cell
        let myRowKey = pizza.typeList[row]  //the dictionary key
        cell.textLabel!.text = myRowKey
        let myRowData = pizza.pizzaPricePerInSq[myRowKey]  //the dictionary value
        cell.detailTextLabel!.text = String(format: "%6.3f",myRowData!)
        return cell
    }

    override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return 44.0
    }

    override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        return false
    }
/*
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            objects.removeObjectAtIndex(indexPath.row)
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        } else if editingStyle == .Insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
        }
    }
*/
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        //let object = objects[indexPath.row] as NSDate
        //self.detailViewController!.detailItem = object
        pizza.pizzaType = pizza.typeList[indexPath.row] //set to the selected pizza
        if (detailViewController != nil){
            self.detailViewController!.detailItem = pizza //send the model to the detailItem
        }

    }

}

DetailViewController.Swift

//
//  DetailViewController.swift
//  SwiftSplitPizzaDemo
//
//  Created by Steven Lipton on 7/17/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

import UIKit

class DetailViewController: UIViewController, UISplitViewControllerDelegate {

    @IBOutlet var pizzaSizeLabel: UILabel!
    @IBOutlet var pizzaTypeLabel: UILabel!
    @IBOutlet var pizzaPriceLabel: UILabel!
    @IBOutlet var detailDescriptionLabel: UILabel!
    var pizza = Pizza()

    var masterPopoverController: UIPopoverController? = nil

    var detailItem: AnyObject? {
        didSet {
            // Update the view.
            self.configureView()

            //Comment out this if clause if you don't want the Master to disappear.
             if self.masterPopoverController != nil {
                self.masterPopoverController!.dismissPopoverAnimated(true)
            }
        }
    }

    @IBAction func pizzaSizeButton(sender: UIButton) {
         pizza.pizzaDiameter = pizza.diameterFromString(sender.titleLabel!.text!)
        configureView()
    }

    func configureView() {
        // Update the user interface for the detail item.
        if let detail = self.detailItem as? Pizza {     //if we have an object in the detail item
            pizza = detail
            if let label = self.pizzaTypeLabel {
                label.text = detail.pizzaType
            }
            let pizzaSizeString = NSString(format:"%6.1fin Pizza",detail.pizzaDiameter)
            let priceString = NSString(format:"%6.2f sq in at $%6.2f is $%6.2f",detail.pizzaArea(),detail.unitPrice(),detail.pizzaPrice()) //added 6/29/14
            if let label = self.pizzaSizeLabel{
                label.text = pizzaSizeString
            }
            if let label = self.pizzaPriceLabel{
                label.text = priceString //added 6/29/14
            }

        }

    }

    //MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.configureView()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    //MARK: - Split View

    func splitViewController(splitController: UISplitViewController, willHideViewController viewController: UIViewController, withBarButtonItem barButtonItem: UIBarButtonItem, forPopoverController popoverController: UIPopoverController) {
        barButtonItem.title = "Master" // NSLocalizedString(@"Master", @"Master")
        self.navigationItem.setLeftBarButtonItem(barButtonItem, animated: true)
        self.masterPopoverController = popoverController
    }

    func splitViewController(splitController: UISplitViewController, willShowViewController viewController: UIViewController, invalidatingBarButtonItem barButtonItem: UIBarButtonItem) {
        // Called when the view is shown again in the split view, invalidating the button and popover controller.
        self.navigationItem.setLeftBarButtonItem(nil, animated: true)
        self.masterPopoverController = nil
    }

}