2️⃣UITableView Setup

Fall 2023 | Vin Bui

Create a Custom UITableViewCell

Creating a UITableViewCell is very similar to how we have been creating views inside of a UIViewController. We still define properties for the view and data, initialize those views, add them as a subview, and constrain those views. However, when working inside of a UITableViewCell there are some slight modifications that we will need to make. Follow these steps:

  1. Our custom class needs to be a subclass of UITableViewCell

    • class CustomTableViewCell: UITableViewCell { }

  2. Create the following initializer:

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }
    
    // `required init` here
  3. Determine what views to create and write a helper function to initialize its properties.

    • For example, if we need to display some text, we would create a UILabel and create a helper function to initialize its font, font color, etc. Note that we do not know anything about the data yet, so the property .text of the UILabel will not be initialized yet.

  4. Inside of the helper function, add the views we created as a subview to contentView and constrain the view with respect to contentView. Then call the helper function inside of the initializer.

    • This is one of the main differences from what we have been doing before. Instead of referencing view, we will be using contentView. Note that we do not need to use safeAreaLayoutGuide here.

  5. Create a configure function (do not make private) that will take in some data as a parameter, and configure our views.

    • For example, we could write a function that takes in a String and sets UILabel.text property equal to the value passed in.

  6. Create a reuse identifier for this cell: static let reuse = "<reuse_identifier>"

    • See “Dequeuing Cells” below for more information.

// 1. Subclass of `UITableViewCell`
class CustomTableViewCell: UITableViewCell {
    // 3. Create view properties
    private let label = UILabel()

    // 6. Create a reuse identifier
    static let reuse = "CustomTableViewCellReuse"

    // 2. Create the following init
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        // 4. Call helper functions
	setupLabel()
    }
		
    // 2. `required init` here

    // 5. `configure` function (do not make private)
    func configure(newText: String) {
	label.text = newText
	// Configure additional views here
    }

    // 3. Set Up View Helpers
    private func setupLabel() {
	// 3. Initialize the label's properties

	// 4. Add as subview to `contentView`

	// 4. Constrain with respect to `contentView`
    }
}

Dequeuing Cells

In Step 6 above, notice that we have a reuse identifier. First, let’s imagine we have a table view that contains a list of all students at Cornell. How many cells would we have? Thousands! Remember, each cell is a separate view and if we have thousands of views, that’s a lot of memory being used! The workaround for this would be to create only the views needed on the screen at one time. If a cell were to go off of the screen, Swift will dequeue this cell for another cell to be created. This is the reason why the cells in a UITableView look very similar! It makes it very efficient to dequeue a cell and reuse it.

How does Swift know which cell to “pick up” and reuse? A reuse identifier is used to associate a cell that is being dequeued with another cell that is about to be rendered.

Setting Up a UITableView

A UITableView is just like any other UIView that we've worked with thus far. We've initialized the view by doing the following steps:

  1. Create the view

  2. Configure the view by changing its properties

  3. Adding the view as a subview to some parent view

  4. Enable auto layout and set up constraints

With a UITableView, we do the exact same thing but with 3 additional steps:

  1. Register a UITableViewCell

    • For example, if we had a custom class called CustomTableViewCell with a static reuse constant called reuse, we would use the following code:

tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.reuse)
  1. Set the UITableView delegate (create an extension just like any other protocol)

  2. Set the UITableView dataSource (create an extension just like any other protocol)

6: UITableViewDelegate

The purpose of a UITableViewDelegate is to add functionality to the table view. A class conforming to the protocol UITableViewDelegate does not have any required functions to implement; however, the two most common functions to implement are: heightForRowAt and didSelectRowAt.

// If our cell has a fixed height, then implement this function.
// If our cell has a dynamic height, do not implement this function and rely on AutoLayout.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return <height> // the height of the cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // Perform some operation when cell is tapped
}

Tip: If we start typing heightForRowAt or didSelectRowAt, Xcode will autofill the function header for us!

7: UITableViewDataSource

In contrast to UITableViewDelegate, there are two required functions to implement: cellForRowAt and numberOfRowsInSection.

For numberOfRowsInSection, we want to provide the number of rows (cells) for a section. Usually, this is the size of our data model array. For example, if our table view listed out all students at Cornell, we would have a data model representing an array of Student objects. The number of rows would be the size of the array (use .count to get the size).

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return dataModelArray.count
}

The purpose of cellForRowAt is to determine the cell class to use (in addition to registering the cell) as well as configuring the cell (by calling the configure function). The following code is for a custom cell class called CustomTableViewCell:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.reuse, for: indexPath) as? CustomTableViewCell else { return UITableViewCell() }
    
    let dataModel = dataModelArray[indexPath.row]
    cell.configure(...) // pass in our dataModel to the configure function in our custom cell class
    return cell
}

Let’s go over this function line by line:

  1. First, we dequeue the cell with the given reuse identifier. This gives us a UITableViewCell. Now, we need to cast it to our custom type by using the as? keyword. This returns an optional type of our custom cell. We unwrap it by using a guard let (or we could use an if let). If the casting failed and the optional holds nil, we return a basic UITableViewCell.

  2. Then, we need to identify which data this cell will hold. Most of the time, we will have some data model array (such as an array of students). To determine the position a cell is located inside of a table view, we use indexPath.row which returns an Int. We could then use this value to access an element inside of our data model array.

  3. Next, we would need to configure our cell with the data model that we retrieve in Step 2. We can pass this information into our custom cell class’s configure function that we implemented earlier to configure the cell’s views such as changing a UILabel’s text.

  4. Finally, we return the configured custom cell.

Complete Code

class ViewController: UIViewController {
    // 1. Create the view
    private let tableView = UITableView()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Additional setup here

	setupTableView() // 2. Configure the view
    }
		
    // 2. Configure the view
    private func setupTableView() {
        // 5. Register Custom Cell
        tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.reuse)
        tableView.delegate = self // 6
	tableView.dataSource = self // 7
		
	view.addSubview(tableView) // 3
	tableView.translatesAutoresizingMaskIntoConstraints = false // 4
		
	// 4. Set constraints
    }
}

// 6. Conform to `UITableViewDelegate`
extension ViewController: UITableViewDelegate {
    // `heightForRowAt` (optional)
    // `didSelectRowAt` (optional)
    // Additional functions here
}

// 7. Conform to `UITableViewDataSource`
extension ViewController: UITableViewDataSource {
    // `cellForRowAt`
    // `numberOfRowsInSection`
    // Additional functions here
}

Last updated