• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 4/24/18

Manage table sub-elements

Log in or subscribe for free to enjoy all this course has to offer!

We’ve established the most important functional principles of a table view. Now, let’s get familiar with its sub-elements and how they function.

Implementing table view sub-elements

Main elements

Let's review the main elements:

  • Table Header

  • Table Footer

  • Table Sections

  • Cells (or, as we learned: Rows, since we get a single cell per row in a table view).

Cell types

Cells can be of two types:

  • Static cells: used when a particular piece of content needs to appear only once in a table. For example, an input to add a task to a to-do list.

  • Dynamic prototypes: used for repeatable content, like a list of tasks on a to-do list . Those cells implement the same layout but display different content.

Cells layout types

Cells can be organized in two ways:

  • Plainlist

  • Grouped

Cells can be gathered in Sections and Sections can also have headers and footers.

Let's play with variations!

Configuration variations

Where do we configure cells? 🤔

We can configure them in the Attributes Inspector. To configure a table, select Table View and refer to the Table View section:

Table view attributes
Table view attributes

To configure cells, we need to add some. We can add cells to a table view by setting a value of Prototype Cells attribute to a desired quantity. Set it to "1" for the moment and select the newly added component for a cell view: 

Cell attributes
Cell view attributes

At this point, we had our table view content attribute set to Dynamic Prototypes (see the table view attributes screenshot above).

Let's change it to "Static Cells":

Static cells
Static cells

The main thing to notice here: our cells now get placed into a section view, and the table view attribute Prototype Cells got replaced with Sections.

Automatically, three cells were added. We can remove some or even add more if we need a different quantity. To do that, select Section View and adjust the value in the Attributes Inspector:

Adjusting number of static sells in section
Adjusting number of static sells in section

Observe how it affects the view:

Multiple sections
Multiple sections

We've been experimenting with Plain style table layout; let's change it to Grouped and observe how the appearance changes:

Grouped style table view
Grouped style table view

Headers & footers

The table view and each section may have a header and a footer.

The table header and footer are represented by UIView elements and must be added as subviews to the table view in the storyboard (or assigned to respective table view properties if created programmatically).

Basic section headers and footers can be configured in the Attributes Inspector:

Section header/footer configuration
Section header/footer configuration

Cell content

Cells can easily present basic content like text, images or checkmarks. To explore these, select a cell on the table view and experiment with available options:

Cell styles
Cell styles

Observe how the cell appearance changes for selected options:

System cell styles
System cell styles 

Before moving on, spend some time experimenting with the settings we've just learned and observe how they affect the appearance of the table view in the storyboard.

Starting HotList!

In our project we'll be displaying a varying number of cells, depending on how many tasks the user adds. Only one cell will remain present at all times: the one where we are providing an option to add a new task. However, we must choose for the whole table what content type we'll use.

Interface configuration

So, let's configure our table view:

  • Content: Dynamic Prototypes

  • Style: Grouped

  • Prototype cells: 2

And the cell configuration:

  • Style: Basic

Select the first cell prototype, set the label text to "Add task" and Identifier to "NewTastCellID":

New task cell
New task cell

Similarly, for the second cell, change the label text to "Task 1", Identifier to "TaskCellID" and Accessory to "Checkmark".

Task cell
Task cell

Done with the Interface builder - back to the controller! 😎

Feeding content

Data model

To start, we've got to create a data model that will describe our hot task. We'll create two Swift files:

  • HotDefinitions.swift: to store assisting code definitions, like enums or constants we might need

  • HotTask.swift: to declare a class for a task

Go ahead and create them. At the moment, the definitions file can store an enum for task priority:

enum Priority {
    case top, bonus
}

And the HotTask classes will contain properties we've identified within our requirements:

class HotTask {
    var caption: String
    var priority: Priority
    var completed = false
    
    init(caption: String?) {
        if let newCaption = caption {
            self.caption = newCaption
        }
        else {
            self.caption = "Do something"
        }
        priority = .bonus
    }
}

Since task is useless without a caption, we've declared it as non-optional. To make it easier for the code that will use this object, we've implemented an initializer that takes the caption as an optional parameter and deals with it. ⚙️

Back to View Controller code now.

DataSource

To store the tasks in the application we'll use two arrays. One to store priority tasks and the other one to store bonus tasks:

var priorityTasks = [HotTask]()
var bonusTasks = [HotTask]()

To visualize our achievements, let's create a list of suggested tasks for the user to start with in case they're bored and don't know what to do! We'll do it using a helper function and calling it by using a configuration method:

    func populateInitialTasks () {
        priorityTasks.removeAll()
        
        priorityTasks.append(HotTask.init(caption: "Pickup MacBook Pro from Apple store"))
        priorityTasks.append(HotTask.init(caption: "Practice Japanese"))
        priorityTasks.append(HotTask.init(caption: "Buy ingredients for a cake for Alie's bday"))
        
        bonusTasks.removeAll()
        let hotTask = HotTask.init(caption: "Shop for funnky socks")
        hotTask.priority = .bonus
        bonusTasks.append(hotTask)
    }
    
    func configure() {
        tableView.delegate = self
        tableView.dataSource = self
        
        populateInitialTasks()
    }

And now, let's adjust our data feed to the table.

Next, we'll need 3 sections:

  • to place a new task option

  • to list priority tasks

  • to list bonus tasks

To make this happen, our numberOfSections method must return 3  :

func numberOfSections(in tableView: UITableView) -> Int {
    return 3
}

The number of rows in each section will now depend on the section index and number of applicable elements each section needs to display:

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        switch section {
        case 0:
            return 1
        case 1:
            return priorityTasks.count
        case 2:
            return bonusTasks.count
        default:
            return 0
        }
    }

We are using switch to check the section index and serve an applicable number of rows:

  • The first section (at index 0) will have only one row to display a cell for adding a new task.

  • The two last sections (at indices 1 & 2) will have the number of rows matching the element count in respective arrays.

And finally, let's serve those cells to the table. For this let's create a helper function to fetch a task object based on indexPath:

    func hotTaskDataSource(indexPath: IndexPath) -> HotTask? {
        switch indexPath.section {
        case 1:
            return priorityTasks[indexPath.row]
        case 2:
            return bonusTasks[indexPath.row]
        default:
            return nil
        }
    }

Then let's use it in the method to populate the data to the cells:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        switch indexPath.section {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: "NewTaskCellID", for: indexPath)
            return cell
        case 1:            
            let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCellID", for: indexPath)
            let task = hotTaskDataSource(indexPath: indexPath)
            cell.textLabel?.text = task.caption
            return cell
        case 2:
            let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCellID", for: indexPath)
            let task = hotTaskDataSource(indexPath: indexPath)
            cell.textLabel?.text = task.caption
            return cell
        default:
            return UITableViewCell.init()
        }
    }

Let's review this:

  • We are using  switch  to check the section index to determine a type of cell we need to serve. This time section index is passed to us within indexPath structure.

  • Then we retrieve the appropriate cell from our table view using cell identifiers that we specified in Interface Builder.

  • For task cells, we also retrieve a datasource object - a corresponding HotTask object based on section index. Then we assign its caption to the text property of the cell's textLabel.

That's the minimum we can provide as content to observe the results.

Time for a quick test. Here's how it looks in the simulator:

Basic cells
Basic cells

That already looks like something! 👍

Section headers & footers

Let's start with section headers. All we need is to provide captions; we can leave the appearance as is. This is done by implementing a special delegate method that provides titles called   titleForHeaderInSection :

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        switch section {
        case 1:
            return "Top Priority"
        case 2:
            return "Bonus"
        default:
            return nil
        }
    }

This method simply returns an optional string for a header title for each section. The return value is optional as not all sections need to have a title. Therefore, we can also skip mentioning our very first section at index 0.

And here it is:

Section headers
Section headers

Similarly, if we wanted to provide a title for footers, we could implement a similar method for footers:

func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
    return nil
}

Marvelous! Let's manage those cells!

Exploring cell actions

Cells can be selected, marked, deleted, moved around and added. Let's explore some of these options.

Selection

The table supports multiples of a single-cell selection. These parameters can be configured via Interface Builder:

Configuring cell selection
Configuring cell selection

There are only three options:

  • None: no selection allowed

  • Single selection: only one row (cell) can be selected at a time - a newly selected row replaces the previous selection (if there is one)

  • Multiple selection: any number of rows can be selected

Cell selection has an obvious counter activity: deselection.

Selection and deselection are actions that may or may not correlate with visuals. Each cell may be configured for how to visualize the selection:

Configuring cell selection visualization
Configuring cell selection visualization

In case of selecting "None," we can still capture the actions of selection and deselection (for single and multiple selection), but there will be no default visual feedback provided to the user - which in fact is what we need for the HotList functionality!

Actions of selection and deselection are accessible using dedicated delegate methods. We can process those actions by implanting the following methods:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
}
    
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        
}

Each time a user taps on a cell, there's a number of process variations possible:

  • If the table supports multiple selection and a row was previously selected, it becomes deselected and the didDeselectRowAt/indexPath delegate method is called.

  • If it wasn't previously selected, just the  didSelectRowAt/indexPath  is called.

  • If the table supports single selection, before selecting a new row,  didDeselectRowAt/indexPath is called for the previously selected row (if there is one).

What about special selection requirements? 🙂

Your app logic may require some special implementation like "minimum number of rows which must be selected: at least one." This must be addressed programmatically by implementing the required logic within the delegate methods mentioned above. For example, in the event of deselection, if the remaining number of selected rows is less than required, we can simply reverse deselection!

Marking

We've got a checkmark that we could use to indicate completion of a task. A checkmark is accessible via the Accessory View of a table cell. We can manipulate its appearance within those delegate methods:

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
    }
    
    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        tableView.cellForRow(at: indexPath)?.accessoryType = .none
    }

Not too bad, is it? Not quite there yet, but it all starts somewhere!

Deleting & Moving

In our functional description we came up with in the beginning of this course, we still don't have a visual component that would be responsible for deletion. However, as iOS users, we've come across a very popular implementation for deletion in list views: on swipe left, a menu gets exposed on the right side of a row with one of the options being "Delete." A Mail app presents just such an example:

iOS Mail app deleting option on swipe
iOS Mail app deleting option on swipe

And what about Moving? 🤔

Since we don't have any UI triggers indicating an ability to move a task from one category to the other, we are going to place that option right next to deleting. Depending on the current task placement, the labels will read "Move to Bonus" or "Move to Priority." We'll then perform deletion of the selected row from the current section and insert it into the other one as the first row.

First of all, we need to make sure that only our task sections support editing and not the very first section with an option to add a task. We can use a dedicated delegate method for it, tableView/canEditRowAt :

func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        switch indexPath.section {
        case 0:
            return false
        default:
            return true
        }
    }

All it does is return  false  to prohibit any action from being displayed for a section at index 0 - first section.

Next, we need to specify those actions for each row. We can choose to provide the same actions for the whole table or for the rows in each section or even for each row individually. In our case, we need to provide actions for 2 sections which are mostly the same, with a couple of differences. The actions are provided in a delegate method,  tableView/editActionsForRowAt  :

func insertTask(_ task: HotTask?, at indexPath: IndexPath?) {
        if let task = task, let indexPath = indexPath {
            // proceed
        }
    }
    
    func deleteTask(at indexPath: IndexPath?) {
        if let indexPath = indexPath {
            // proceed
        }
    }
    
    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
        var actions: [UITableViewRowAction]?
        var moveCaption: String?
        var moveToIndexPath: IndexPath?
        
        switch indexPath.section {
        case 1:
            moveCaption = "Move to Bonus"
            moveToIndexPath = IndexPath(row: 0, section: 2)
        case 2:
            moveCaption = "Move to Priority"
            moveToIndexPath = IndexPath(row: 0, section: 1)
        default:
            return actions
        }
        
        if let task = hotTaskDataSource(indexPath: indexPath) {
            let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
                self.deleteTask(at: indexPath)
            }
            let move = UITableViewRowAction(style: .normal, title: moveCaption) { (action, indexPath) in
                task.priority = (task.priority == .top) ? .bonus : .top
                self.deleteTask(at: indexPath)
                self.insertTask(task, at: moveToIndexPath)
            }
            actions = [delete, move]
        }
        return actions
    }

The code implements a number of things - let's review:

  • We've created two helper functions that will perform insertion and deletion of a task row at a specified indexPath.

  • Then, we generated some values for variables that will be different for each of the two sections.

  • Before moving, we also need to change the priority of the task we are moving. We do that by assigning a new value to the priority property using a simplified if/else statement - a handy trick to use when you need to assign one value or the other based on a condition: 

    task.priority = (task.priority == .top) ? .bonus : .top
  • Finally, we've generated an array of two actions for the task categories.

Let's now insert delete functionality. There are two processes associated with these actions:

  • UI related - removing a cell from table view

  • Datasource related - removing a corresponding task from respective datasource array

And in the case of moving:

  • UI related - adding a cell to table view (to a section we are moving to)

  • Datasource related - inserting a corresponding task to the respective datasource array (the one we are moving to)

There's a lot of activity going on behind the scenes within the app managed by iOS - specifically rendering the UI based on the current state of the app. This is a complex context - however, all we need to know at the moment is that our datasource is always corresponding to the UI that the app is attempting to render. For that, table view provides two 'safely' methods locking external manipulations,  beginUpdates/endUpdates , which we can use to lock our table view while deleting and inserting items to both table view and datasource:

    func insertTask(_ task: HotTask?, at indexPath: IndexPath?) {
        if let task = task, let indexPath = indexPath {
            // put the table view in updating mode
            tableView.beginUpdates()
            
            // add new object to the datasource array
            if (indexPath.section == 1) {
                priorityTasks.insert(task, at: indexPath.row)
            }
            else {
                bonusTasks.insert(task, at: indexPath.row)
            }
            
            // insert a new cell to the table
            tableView.insertRows(at: [indexPath], with: .automatic)
            
            // finish updating table
            tableView.endUpdates()
        }
    }
    
    func deleteTask(at indexPath: IndexPath?) {
        if let indexPath = indexPath {
            // put the table view in updating mode
            tableView.beginUpdates()
            
            // add new object to the datasource array
            if (indexPath.section == 1) {
                priorityTasks.remove(at: indexPath.row)
            }
            else {
                bonusTasks.remove(at: indexPath.row)
            }
            
            // insert a new cell to the table
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            // finish updating table
            tableView.endUpdates()
        }
    }

The two functions are very similar.

Let's review! We have:

  •  verified that the parameters have value and that we can proceed;

  • put the table in edit mode;

  • updated the datasource - by either deleting or inserting an item (depending on what section  we work with, we use a corresponding datasource array);

  • performed UI adjustments, notably insertion or deletion from the table view;

  • finished table updates.

Now we have it all covered! 😁

Let's test it! 👩‍🔬

Cell actions on swipe
Cell actions on swipe

Looking good! 😎

Deletion recovery

Here's a challenge: we are deleting a row right away. What if the user swipes a row and then a finger slips and taps "Delete"? 😱 We can come up with a couple of ways to solve the problem:

  • Before permanent deletion, show an alert prompt to confirm the user's intention. This will do... However, from a UX (User Experience) perspective, it's the most annoying thing ever. 

  • We can implement a nice sliding panel with a message stating we've deleted such and such task and present an option to restore. This panel would float for some time and self disappear, assuming no recovery is needed.

  • A third option would be to "listen" to a shake gesture and then show an alert prompt (or a fancy panel) to offer a restoring option.

Why would we show an alert if it's annoying to the user? 🤔

As you may have noticed, the first and the last proposed options offer an alert as part of their implementation. The difference is that in the first scenario we are imposing it on the user (the user has to deal with this each time before attempting to delete a task), while in the second scenario the user triggers the intention to restore (an alert is much less annoying in this context).

How does the user know to shake their device? 😮

Even though the shaking gesture may seem to be a non-obvious undo functionality, on shake is implemented in many apps. Once discovered, it's easy to remember and utilize, which benefits the UX.

So, to improve the user experience of the app, go ahead and research, then implement the shaking gesture to undo the deleting action.

Implementation tips:

  • Before deleting, backup the task object and indexPath meant for deletion by creating supporting variables. They would need to be optional and would hold either nil or reference the info of the last deleted item. For example: 

    var lastDeletedTask: HotTask?
    var lastDeletedTaskIndexPath: IndexPath?
  • Implement shaking gesture delegate. For that, you need to override the motion ending method of a view controller, motionEnded :

    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
            if motion == .motionShake {
                // check if backup is avialble
                // present an alert prompt, implement restoring fucntionality for an alert un-do action
            }
        }
  • Insert back the backed up task and clear the backup variables.

Things to remember:

  • On shake gesture, show an alert prompt to undo deletion only if the task is to be restored and the backup variables are non-nil.

  • If the user chose to restore a task, make sure to clear backup variables that referred to the task to be restored. Otherwise, if the user shakes a device again and confirms restoring, your app will restore the same task twice.

  • It would be useful to clear those backup variables also after the user performs other actions such as adding a new task, marking a task as completed, or moving a task from one category to the other. We can assume the user is not concerned about an item they previously deleted if they're performing these actions.

Your turn to shine! Go ahead and make this improvement!

You've got this. 😉
You've got this. 😉

And to test the shake gesture in the simulator, use a simulation of the gesture by selecting Hardware → Shake Gesture (or, Control + Command + Z).

What about undo for moving?

The moving action is not distractive, once a user places a task in the other category, it's still present on the list. In case they did it accidentally, they can simply move it to the original category by repeating the move action.

Now we are done with all the cell actions!

Wait, how about adding? 😮

We'll explore adding a tiny bit later in this course! 👍

Let's Recap!

  • The table header and footer can be any subclass of UIView positioned in the beginning or at the end of the Table View.

  • Table style can be plain or grouped.

  • Table cells can be static or dynamic prototype.

  • A table cell occupies the whole row and can be addressed using IndexPath, which is a composition of section index and row index.

  • Cells can be assigned edit actions on swipe, which can be used for various purposes, such as deleting or moving.

  • Moving cells by dragging can be enabled by placing a table in edit mode.

Example of certificate of achievement
Example of certificate of achievement