UITableView and its UITableViewDataSource

Drag the table view up and down with your finger, and click on a cell.

The cells in the first screenshot are of style UITableViewCellStyle.default, which have only a textLabel. The cells in the second screenshot are of style UITableViewCellStyle.subtitle, which also have a detailTextLabel. The Alaska cell has an imageView containing a non-nil UIImage, and the California cell has been selected by the user.

Source code in States.zip

  1. Class AppDelegate: unchanged.
  2. Class TableViewController also acts as the table view’s data source and delegate.
  3. Main.storyboard
  4. Assets.xcassets, an Xcode asset catalog file.
    1. AppIcon.iconset
      1. Contents.json: a JSON file listing files belonging to the icon set.
    2. Alaska.imageset
      1. Contents.json: a JSON file listing the files belonging to the Alaska image set.
      2. Alaska.jpg: one of the files belonging to the Alaska image set. Apple prefers .png format. 48 × 32 pixels.
  5. Info.plist: unchanged.

Create the project

Select the States folder in the Xcode Project Navigator.
File → New → File…
Chose a template for your new file:
Source: Cocoa Touch Class
Choose options for your new file:
Class: TableViewController
Subclass of: UITableViewController

Select Main.storyboard in the Xcode Project Navigator. Open the left pane of the center panel of Xcode as far as

▼ View Controller Scene
   ▶ View Controller
   First Responder
and select the View Controller. In the right panel of Xcode, click on the icon for the Identity inspector. It’s a rectangle with a smaller rectangle in its upper left corner.
Custom Class
Class: TableViewController
Module: States

Open the left pane of the center panel of Xcode as far as

▼ View Controller Scene
   ▼ View Controller
      ▶ View

In the Identity Inspector, change the class of the view from UIView to UITableView.

Control-click on ViewController.swift in the Xcode Project Navigator and select Delete.
Do you want to move the file “ViewController.swift” to the Trash, or only remove the reference to it?
Move to Trash.

Select Assets.xcassets in the Xcode Project Navigator. Drag Alaska.jpg into the left pane of the center panel of Xcode, under AppIcon.

Apple’s documentation

The Settings app has lots of examples of table views.

  1. Table View Programming Guide for iOS
  2. Table View in the iOS Human Interface Guidelines
  3. Classes UITableView, UITableViewController, and UITableViewCell
  4. Protocols UITableViewDataSource and UITableViewDelegate

The table view

A table view is composed of sections, which are composed of cells called UITableViewCells. A table view can scroll because it is derived from class UIScrollView. In the viewDidLoad() method of the TableViewController, we configure some of the properties that the table view inherits from UIScrollView.

The data source

Every table view must have a data source object which must conform to protocol UITableViewDataSource. In order to conform to this protocol, it must have the following three methods.

  1. numberOfSections(in:) returns 1 in this app.
  2. tableView(_:numberOfRowsInSection:) returns 50 in this app.
  3. tableView(_:cellForRowAtIndexPath:) returns a UITableViewCell, preferably recycled. See the cellReuseIdentifier instance variable of the TableViewController.

Think of the data source as the “model” object, and the table view as the “view” object. (And, obviously, the view controller is the “controller” object.) The UIPickerView we saw here has a UIPickerViewDataSource.

The delegate

If we want the cells of a table view to do something when we touch them (other than to turn gray), we must provide the table view with a delegate object. This object must conform to protocol UITableViewDelegate. When we touch a cell, the tableView(_:didSelectRowAtIndexPath:) method of the delegate is automatically called.

The UIPickerView we saw here has a UIPickerViewDelegate.

The view controller

A view controller immediately above a table view must be a special type of view controller called a UITableViewController. An ordinary UIViewController contains a property named view that refers to its UIView. An UITableViewController contains a property named tableView that refers to its UITableView.

A UITableViewController can also act as the table view’s data source and delegate, because the UITableViewController conforms to protocols UITableViewDataSource and UITableViewDelegate. Our TableViewController plays all three of these rôles.

The most important method we give to a view controller is usually loadView(), which creates the view controller’s view. But we don’t have to write any loadView() for a UITableViewController. The UITableViewController is hardwired to always create a UITableView. Similarly, we do not have to set the dataSource and delegate properties of a table view created by a table view controller. These properties already point to the table view controller.

Each cell contains two UILabel properties called the textLabel (the title) and the detailTextLabel (the subtitle).

Things to try

  1. In the viewDidLoad() method of the TableViewController, change some of the properties of the table view.

  2. Print the dimensions of a typical table view cell and the font of the cell’s textLabel. Insert the following code immediately before the return statement in the tableView(_:cellForRowAtIndexPath:) method of the TableViewController. Run the app and scroll down to Wyoming. On iPhone 8 Plus in any orientation, each cell is 45 points high. 44⅔ of them are occupied by the textlabel, and the one remaining pixel (⅓ of a point) is a hairline along the bottom edge of the cell. These defaults are not in the documentation, and are not guaranteed by Apple. I measured them so you could anticipate how many lines will fit on the screen, and make the rest of your app consistent with the table view.
    		if cell.textLabel!.text! == "Wyoming" {
    			print("cell.bounds.size = \(cell.bounds.size)");
    			print("cell.textLabel!.frame = \(cell.textLabel!.frame)");
    			print("cell.textLabel!.font.fontName = \(cell.textLabel!.font.fontName)");
    			print("cell.textLabel!.font.pointSize = \(cell.textLabel!.font.pointSize)");
    cell.bounds.size = (414.0, 45.0)
    cell.textLabel!.frame = (20.0, 0.0, 374.0, 44.6666666666667)
    cell.textLabel!.font.fontName = .SFUIText
    cell.textLabel!.font.pointSize = 17.0

  3. By default, the dequeueReusableCell(withIdentifier:for:) method of a UITableView creates cells of style UITableViewCellStyle.default. Let’s change the style to UITableViewCellStyle.subtitle. Add the following class to the project. It will be exactly the same as class UITableViewCell, except that the style is UITableViewCellStyle.subtitle instead of UITableViewCellStyle.default.

    Select the States folder in the Xcode Project Navigator.
    File → New → File…
    Chose a template for your new file:
    iOS Source/Cocoa Touch Class
    Choose options for your new file:
    Class: TableViewCell
    Subclass of: UITableViewCell

    Add the following methods to the new class TableViewCell.
    	//called by dequeueReusableCell(withIdentifier:for:)
    	override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    		super.init(style: UITableViewCellStyle.subtitle, reuseIdentifier: reuseIdentifier);
    	//never called
    	required init(coder aDecoder: NSCoder) {
    		super.init(coder: aDecoder)!;
    In the viewDidLoad() method of the TableViewController, the following statement specifies the class of the cell returned by each call to dequeueReusableCell(withIdentifier:for:).
    		tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier);
    Change it to the following.
    		tableView.register(TableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier);
    Insert the following statement after setting the cell.textLabel!.text in the tableView(_:cellForRowAtIndexPath:) method of the TableViewController.
    		cell.detailTextLabel!.text = "\(states[indexPath.row]) is state \(indexPath.row + 1) out of \(states.count).";
    After running the app, try the other styles: UITableViewCellStyle.value1 and UITableViewCellStyle.value2. For value2, you’ll have to comment out the cell.imageView property.

  4. The delegate of our table view is the table view controller. Give the delegate a tableView(_:didSelectRowAt:) method, called automatically when a cell is touched.
    	override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    		let cell: UITableViewCell? = tableView.cellForRow(at: indexPath);
    		if cell != nil {
    			cell!.accessoryType = UITableViewCellAccessoryType.checkmark;
    Better yet, toggle the checkmark instead of always setting it. Change the last statement to the following.
    			if cell!.accessoryType == UITableViewCellAccessoryType.none {
    				cell!.accessoryType = UITableViewCellAccessoryType.checkmark;
    			} else {
    				cell!.accessoryType = UITableViewCellAccessoryType.none;
    A more cryptic way to say the above is
    			cell!.accessoryType = cell!.accessoryType == UITableViewCellAccessoryType.none
    				? UITableViewCellAccessoryType.checkmark
    				: UITableViewCellAccessoryType.none;

  5. Read the states array from a text file when the app is launched. Select the Supporting Files folder in the Xcode Project Navigator.
    File → New → File…
    Choose a template for your new file:
    iOS Other/Empty
    Save As: states.txt
    Select your new file states.txt in the Project Navigator. Change its contents to the following, one state per line.
    New Hampshire
    New Jersey
    New Mexico
    New York
    North Carolina
    North Dakota
    Rhode Island
    South Carolina
    South Dakota
    West Virginia
    Change the states property of the TableViewController to the following.
    	var states: [String] = [String]();	//an empty array of Strings

    Add the following method to the TableViewController.

    	required init?(coder aDecoder: NSCoder) {
    		let bundle: Bundle = Bundle.main;
    		let filename: String? = bundle.path(forResource: "states", ofType: "txt");
    		if filename == nil {
    			print("couldn't find file states.txt");
    		} else {
    			do {
    				let s: String? = try String(contentsOfFile: filename!, encoding: String.Encoding.utf8);
    				states = s!.components(separatedBy: "\n");
    			} catch {
    				print("couldn't read file states.txt: \(error)");
    		super.init(coder: aDecoder);