Make App Pie

Training for Developers and Artists

How to Use Custom Table Cells in a UITableView

While very versatile, there’s some point where every developer finds table views lacking something: the cell format is too limited.  Apps like Facebook, Twitter, and Instagram don’t use simple table views. They use  custom table cell formats.  In this lesson, we’ll start learning how to make and use custom table view cells.

Set Up the Project.

We’ll start with a new project. Create a new project named DessertTableView, with a Universal device and Swift as the language.

We’ll be using some images in this app. You can download the images here. Unzip the images. Open the Assets.xcassets folder in the navigator. Select all the image files and drag them into the asset library.

2015-12-02_06-22-41

You have all three images available to the project.

2015-12-02_06-24-35

Setting Up the Storyboard

Go to the storyboard. Delete the view controller. Drag a new Table View Controller to the storyboard. In the attribute inspector select Is Initial Controller. Open up your document outline if not already open. In the document outline, select the table view.

2015-12-02_06-26-17

In the attributes use Prototype cells and change the count to 2.

2015-12-02_06-26-34

 

You can  have more than one format for cells. You’ll find in the outline two table view cells.

Select the top one and use  a Subtitle cell style named cell as the identifier.

2015-12-03_07-42-19

Change the background color to #FFEEEE in the color picker

2015-12-04_06-02-16

Select the bottom one and make it a Right Detail cell named options cell as the identifier. Change the Background color to #EEFFFF.

Set Up the Basic Table View

Now go to the ViewController.swift file. Change the class name to this:

class DessertTableViewController: UITableViewController {

As I’ve mentioned in other lessons about table views, I find adding everything from scratch a lot less confusing and cleaner than the UITableViewController class template.

Set Up the Model

Before we add the data source methods for the table view, we need a model. Add this dictionary and array to the class:

var  menu:[String:String] = [
    "Ice Cream Cone": "Ice Cream",
    "Ice Cream Sundae": "Ice Cream",
    "Apple Pie": "Pie",
    "Cherry Pie": "Pie",
    "Coconut Cream": "Pie",
    "Tiramisu": "Cake",
    "Chocolate Chip Cookie": "Cookie",
    "7-Layer Cake": "Cake",
    "Boston Cream Doughnut": "Doughnut",
    "Cruller": "Doughnut",
    "Long John": "Doughnut",
    "Blueberry Muffin": "Cake",
    "Vanilla Cupcake": "Cake",
    "Shake": "Drink",
    "Malted": "Drink",
    "Root Beer Float": "Drink"]
var dessertList = [String]()

We’ll populate the array with the keys to the dictionary. We do that in viewDidLoad like this:

override func viewDidLoad() {
   super.viewDidLoad()
   dessertList = [String](menu.keys)
}

Remember that dictionaries are unordered collections. Arrays are ordered collections. We need an order collection for a table view to work, which is what we will use dessertList for. Your dessert list will most likely not be in  the same order as mine since we take the unordered collection and make it ordered. Your table might then look different.

Data Source and Delegates for the Table View

Next, declare our data source methods.Add this to the bottom of your class:

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

We’ll have one section, and dessertList.count entries in our list. Add this to the class:

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

Our last data source method populates the cells. This will be where we will do most of our work. For our first iteration we’ll alternate between the two types of views we have. Add the following code.

//iteration 1 
override func tableView(
    tableView: UITableView,
    cellForRowAtIndexPath indexPath: NSIndexPath
) -> UITableViewCell {
    let row = indexPath.row
    let title = dessertList[row]
    let detail = menu[title]!
        
    if row % 2 == 0 {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", 
            forIndexPath: indexPath)
        cell.textLabel?.text = title
        cell.detailTextLabel?.text = detail
        return cell
    }else{
        let cell = tableView.dequeueReusableCellWithIdentifier("options cell", 
            forIndexPath: indexPath)
        cell.textLabel?.text = title
        cell.detailTextLabel?.text = detail
        return cell
    }
}

The expression row % 2 alternates between 1 and 0. When 0, we dequeue the cell table view cell and display that. Otherwise we dequeue the options cell table view cell and use that.
Go back to the storyboard. Select the table view controller and in the identity inspector set the class to DessertTableViewController. Build and run. we now have alternating cells.

2015-12-02_07-04-09

Using Logical Cells

While this might be pretty, in other table view lessons we’ve done this.  In practice, we will have circumstances that will use one cell instead of another based on our data. For our demo, we have  ice cream, shakes, malts, and pies we specify ice cream flavors for in an options modal view. Those will need the options cell to select the ice cream.

Go to the storyboard and add a new view controller. Add three buttons for Vanilla, Chocolate and Strawberry. Make the background #88FF88 and the button backgrounds #88FFFF with  black text. Select all three buttons and tap the Stack view button stack view button. Set the stack view like this.

2015-12-02_07-19-30

Using Auto Layout, pin the stack view 20 points  up, 0 left and 0 right, updating the items of new constraints. You’ll get a scene like this when you are done.

2015-12-02_07-21-03

Control-drag from the options cell to the new scene.  Select a Present Modally segue for Selection.   Select the segue. In the attribute inspector,  set the Presentation to Form Sheet. Press Command-N on the keyboard to make a new file. Create a new Cocoa Touch Class file named OptionsViewController, Subclassing UIViewController. Add this action to the file that appears

@IBAction func selectedFlavor(sender: UIButton) {
    dismissViewControllerAnimated(true, completion: nil)
}  

Go back to the storyboard. Select the options scene’s view controller icon. In the identity inspector, set the class to OptionsViewController. Open the assistant editor.  Select the Vanilla button. Control drag from the Vanilla button to the selectedFlavor code until the code highlights. Do the same for Chocolate and Strawberry.

Now that we set up our storyboard go back to the DessertTableView class. Add this to cellForRowAtIndexpath:

let hasOptions =
    (detail == "Pie") ||
    (detail == "Ice Cream") ||
    (detail == "Drink")

Change row % 2 == 0 to hasOptions. Now for ice cream, pie and dessert drinks, we can pick a flavor of ice cream when we select the item, while other items will have no option selections.

Build and run. We see some items with selections and others without

2015-12-02_07-35-57

Select Apple Pie and it asks us what ice cream we want.

2015-12-02_07-36-02

Adding a Custom View Cell

The most powerful type of cell is a custom cell. For the non-options menu choices, we’ll add a photo to the menu selection. While we can do this in a standard view with the image property, it will only show up to the left of all the text. We’ll put our photo with the title above and the detail next to it. With stack views, this becomes rather easy to lay out.

Layout with Embedded Stack Views

Select cell in the document outline. In the attribute inspector, change the style to Custom. The title and detail disappear. Give ourselves a bit more space. Change to the size inspector for cell. Set a Row Height of 200. Custom will check on.

2015-12-04_05-40-36

This setting is meaningless for the actual app. We set the row height with the estimatedHeightForRowAtIndexPath delegate. It just helps with layout.

Drag two labels and an image view into the cell. Make one label read Title and the other Detail. Set the text color of both labels to #DDDDEE.

Make the Title label font System 24 points. Change the background to #664433. Arrange everything to look like this:

2015-12-03_05-41-39

Select the image view. In the pin menu, set the width to 100 and check on Aspect Ratio. In the size inspector, edit the aspect ratio to be 1:1.

2015-12-03_06-05-37

This sizes our image to 100 points square and prevents the stack view from getting confused.

Select both the Image view and the Detail label. Make a stack view by clicking the stack view button stack view button. Change the attributes of the stack view to this:

2015-12-03_05-44-56

Select the image view and set the Image Mode to Aspect Fit in the attributes inspector. Change to the size inspector, and change the image view’s Content Hugging Priority to 750. This is necessary for the stack view to work right. (For a discussion why, see my book Practical Auto Layout for Xcode 7) Select the Detail button and change the Content Hugging Priority to 250.

Select the stack view and the Title label in the document outline. Click the stack view button stack view button again, this time making a vertical stack view. Use these settings for this stack view.

2015-12-03_05-55-14

Click on the auto layout pin button. Turn on all four constraints by clicking the I-beams and add the constraints as is. Go to the size inspector for the stack view and edit the constraints so all have a zero constant. Your constraints should look like this:

2015-12-03_06-19-03

For an explanation of using stack views, see the stack view lesson. Your layout should look like this:

2015-12-03_13-51-57

Set Up the Cell Class

Press Command-N and make a new Cocoa Touch Class named CustomTableViewCell. Subclass UITableViewCell. In the class that appears add the three following outlets

@IBOutlet weak var customTitle: UILabel!
@IBOutlet weak var customImage: UIImageView!
@IBOutlet weak var customDetail: UILabel!

Go back to the Storyboard. Select cell and in the identity inspector change its class to CustomTableCell. Open the assistant editor. Xcode does not immediately recognize that you want to add outlets to a cell and will insist on adding outlets to the table view controller. You have to force it. In the assistant editor select Manual, and drill-down the folders to select CustomTableViewCell.swift. Drag from the circles to the appropriate labels and the image view to connect the outlets.

Use the Custom Cell in the Table

Close the assistant editor. Go to the DessertTableViewController class. Find the tableview:cellForRowAtIndexPath: method. We’ll change the code for setting up the cell with identifier cell. This is relatively simple. We’ve subclassed UITableViewCell, so we need to downcast the value of dequeueReusableCellWithIdentifier with CustomTableViewCell. We’ll then have access to the custom cell’s properties and set them accordingly. Change that part of the method to this:

}else{
    let cell = tableView.dequeueReusableCellWithIdentifier("cell", 
         forIndexPath: indexPath)
         as! CustomTableViewCell
    cell.customTitle.text = title
    cell.customDetail.text = detail
    cell.customImage.image = UIImage(named: detail)
    return cell
}

Build and run. It almost works. We get two unexpected results. The first is a squashed image in the table view and a missing title:

2015-12-03_06-46-34

The second is Xcode issuing a constraint error.

DessertTableView[] Unable to simultaneously satisfy constraints.
	Probably at least one of the constraints in the following list is one you don't want.

Setting the Height of a Table Cell

The cause is the same. Even though we set a height of 200 points in the storyboard, this gets ignored at run time. The table view uses its default height of 44 points, which is too small for our custom view. Since we have constraints that need more than that much space, we get the conflicting constraint error.  We need to tell the table view what height we want. There is a delegate for just that. Add another delegate to the code:

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

the estimatedHeightForRowAtIndexPath delegate sets the estimated height of the rows. If a row is smaller than this number, it will be this value. If the system finds that a cell is bigger than this height, it automatically adjusts the height. Build and run. You will find the app now works.

2015-12-03_06-53-42

Custom cells allow for many special cases of table views you could not get other wise. Many apps are nothing more than such custom cells. You don’t need to have only one type of cell either. You can make several for different cases. Prior to Xcode 7, the cells needed to be set up completely with auto layout. Now we can use stack views to speed up the layout process.

For more information on stack views, check out the introduction to stack views.

The Whole Code

DessertTableViewController

//
//  ViewController.swift
//  DessertTableViewController
//
//  Created by Steven Lipton on 12/2/15.
//  Copyright © 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class DessertTableViewController: UITableViewController {
    var  menu:[String:String] = [
        "Ice Cream Cone": "Ice Cream",
        "Ice Cream Sundae": "Ice Cream",
        "Apple Pie": "Pie",
        "Cherry Pie": "Pie",
        "Coconut Cream": "Pie",
        "Tiramisu": "Cake",
        "Chocolate Chip Cookie": "Cookie",
        "7-Layer Cake": "Cake",
        "Boston Cream Doughnut": "Doughnut",
        "Cruller": "Doughnut",
        "Long John": "Doughnut",
        "Blueberry Muffin": "Cake",
        "Vanilla Cupcake": "Cake",
        "Shake": "Drink",
        "Malted": "Drink",
        "Root Beer Float": "Drink"]
    var dessertList = [String]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        dessertList = [String](menu.keys)
    }

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

    //MARK: Table Delegates and Data Sources
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dessertList.count
    }
    /*
    //Iteration 1
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let row = indexPath.row
        let title = dessertList[row]
        let detail = menu[title]!
        
        if row % 2 == 0 {
            let cell = tableView.dequeueReusableCellWithIdentifier("options cell", forIndexPath: indexPath)
            cell.textLabel?.text = title
            cell.detailTextLabel?.text = detail
            return cell
        }else{
            let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
            cell.textLabel?.text = title
            cell.detailTextLabel?.text = detail
            return cell
        }
    }*/
    
    /*
    //Iteration 2 -- logically selecting a cell
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let row = indexPath.row
        let title = dessertList[row]
        let detail = menu[title]!
        let hasOptions =
            (detail == "Pie") ||
            (detail == "Ice Cream") ||
            (detail == "Drink")
        if hasOptions {
            let cell = tableView.dequeueReusableCellWithIdentifier("options cell", forIndexPath: indexPath)
            cell.textLabel?.text = title
            cell.detailTextLabel?.text = detail
            return cell
        }else{
            let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
            cell.textLabel?.text = title
            cell.detailTextLabel?.text = detail
            return cell
        }
    }
    */
    //Iteration 3 -- custom cell
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let row = indexPath.row
        let title = dessertList[row]
        let detail = menu[title]!
        let hasOptions =
        (detail == "Pie") ||
            (detail == "Ice Cream") ||
            (detail == "Drink")
        if hasOptions {
            let cell = tableView.dequeueReusableCellWithIdentifier("options cell", forIndexPath: indexPath)
            cell.textLabel?.text = title
            cell.detailTextLabel?.text = detail
            return cell
        }else{
            let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CustomTableViewCell
            cell.customTitle.text = title
            cell.customDetail.text = detail
            cell.customImage.image = UIImage(named: detail)
            return cell
        }
    }

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

 

    
}


CustomTableViewCell

//
//  CustomTableViewCell.swift
//  DessertTableView
//
//  Created by Steven Lipton on 12/2/15.
//  Copyright © 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class CustomTableViewCell: UITableViewCell {
    @IBOutlet weak var customTitle: UILabel!
    @IBOutlet weak var customImage: UIImageView!
    @IBOutlet weak var customDetail: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}
 

OptionsViewController

//
//  OptionsViewController.swift
//  DessertTableView
//
//  Created by Steven Lipton on 12/2/15.
//  Copyright © 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class OptionsViewController: UIViewController {

    @IBAction func selectedFlavor(sender: UIButton) {
        dismissViewControllerAnimated(true, completion: nil)
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: