• 20 hours
  • Medium

Free online content available in this course.

Videos available in this course

Certificate of achievement available at the end this course

Got it!

Implement custom sub-elements

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

So far we’ve dealt with the basic sub-elements of a table view: headers, footers and cells. But based on our design requirements, we'll need a bit more than that.

In this chapter we'll make a number of improvements:

  • Add a table header to hold a progress statement

  • Customize the look of our task cell

  • Implement a custom cell for adding a new task

Let's get the ball rolling!

Adding a Table Header

Compose the interface

Our table header will be a simple UIView with two UILabels within it to accommodate the app name and progress statement.

To add a view, simply drag the UIView object from Objects Library in Interface Builder onto the top of the table view until the blue line appears, indicating that it will be placed as a table header:

Adding table header view
Adding table header view

Your view hierarchy should look like this:

Table header as a subview of table view
Table header as a subview of table view

If it got added elsewhere, you can either drag it to where it needs to be in Views Navigator or remove it from the interface and do it again.

Next, drag two labels to the newly added table header view. Style, reposition and resize them so that it looks similar to this:

Table header UI
Table header UI

That's it for the interface!

Connect to the code

As we established earlier, we need to be able to update the text in the label to reflect the user's progress. So we've got to connect our label to the code. Go ahead and create an Outlet so you get a variable for the label in your code:

@IBOutlet weak var progressLabel: UILabel!

That was easy, wasn't it? 😉

Handle progress change

Let's now prepare for the progress update and create a helper function for it:

    func updateProgress() {
        
        // calculate the initial values for task count
        let totalTasks = priorityTasks.count + bonusTasks.count
        let completedTasks = priorityTasks.filter { (task) -> Bool in
            return task.completed == true
        }.count + bonusTasks.filter { (task) -> Bool in
            return task.completed == true
        }.count
        
        // declare a caption variable
        var caption = "What's going on?!"
        
        // handle range possible scenarios
        if totalTasks == 0 { // no tasks
            caption = "It's  lonely here - add some tasks!"
        }
        else if completedTasks == 0 { // nothing completed
            caption = "Get started - \(totalTasks) to go!"
        }
        else if completedTasks == totalTasks { // all completed
            caption = "Well done - \(totalTasks) completed!"
        }
        else { // completedTasks less totalTasks
            caption = "\(completedTasks) down \(totalTasks - completedTasks) to go!"
        }
        
        // assign the progress caption to the label
        progressLabel.text = caption
    }

Let's review what we are doing here:

  • Calculating total tasks - as a sum of both arrays that contain tasks for both categories

  • Calculating the number of completed tasks as a sum of the count of completed tasks in each of the arrays*

  • Declaring a variable to hold a progress caption

  • Composing a suitable caption for each of the possible scenarios

  • Assigning a new caption to the label

This is handy!

But...where do we call it? ☎️

We must call this function on various occasions:

  • When the application launches

  • When the user adds or deletes a task

  • When the user marks a task completed (as well as incomplete)

Why would we allow a task to be marked as "incomplete"?

We do rely on the user to be honest and mark the tasks completed only when they've truly completed them. However, there might be circumstances when the user accidentally taps a completion checkmark. Or, reevaluates the completion criteria and decides that they can do a better job. 😬 So, we are going to make it easy for them and allow marking a task either way! 😎

For now, let's call it in configuration method:

    func configure() {
        tableView.delegate = self
        tableView.dataSource = self
        
        populateInitialTasks()
        
        updateProgress()
    }
    
    ... 

Time to test! Run your app in the simulator and observe an update to the progress caption:

Progress update on app launch
Progress update on app launch

All done for the table header!

Creating a Custom Task Cell

A table cell itself is represented by the class  UITableViewCell . A table cell's UI components are held in table cell view, represented by our old friend, plain UIView. It serves as a container for all the UI elements we wish to use for the cell customization. It's described as Content View on Storyboard and is referred to as a property of a cell named  contentView .

Compose the interface

To begin our customization, let's switch to our storyboard. Select a task cell and change its style to 'Custom' and set Accessory View and Selection to 'None'. You'll see the title label disappear. We now have a blank canvas to work with! 👩‍🎨

Reset task cell to custom
Reset task cell to custom

Next, to match our design, add a button to act as a check mark and a label to hold the task's caption.

Could we use an image view instead of a button? 🤔

We certainly could use an image view instead of a button to implement a checkmark. However, a button already implements various states to handle toggle-like activity: 'on' and 'off'.

Why not use UISwitch object if we only need 2 states? 🧐

We could use Switch object as well - it would very well provide us with a toggle functionality. However, this object is more accustomed for a sliding interaction, which is not usually associated with a checkmark. We'd also have to take care of the custom appearance of a switch object in a rather complex way.

OK, button it is! 👍

So, drag a button and then a label onto the cell. Set the button type to 'Custom' and remove the title. Change the label text to 'Task 1' and set the Number of lines attribute to 0 (this will allow us to display a non-fixed number of lines within the label). Position and size to achieve the designed look:

Task cell UI
Task cell UI

Task cell UI is done!

Connect to the code

We've got the UI handled. As usual, the interface is a little useless without an ability to be accessed from code. You already know how to connect the UI objects to the view controller.

We have one cell in the storyboard which is a dynamic prototype, which means it's going to be used for dynamic content - multiple cells in the table of the same type. That means, if we connect each UI element to the view controller, it will not work as there's no direct association between the label and the code variable. 🙁

Let's take customization even further and subclass UITableViewCell.

Subclass table cell

Create a Swift file, TaskTableViewCell.swift , and declare a subclass with the same name as the file:

import Foundation
import UIKit

class TaskTableViewCell: UITableViewCell {
    
}

Now, the same way we created outlets for the main interface elements, connecting them to the view controller file, we need to connect our cell elements to the subclass we've just created.

First, we need to let the interface builder know that we want to associate the task cell with our subclass and not the default UITableViewCell class. To accomplish this, select the task cell on the storyboard and set its Class property on Identity Inspector  to 'TaskTableViewCell' (you can type it or select from the dropdown list):

Assigning subclass to table cell
Assigning subclass to table cell

And now, go ahead and create Outlets for the two elements - button and label - and an Action for the button, so your code will look like this:

class TaskTableViewCell: UITableViewCell {
    
    @IBOutlet weak var checkmarkButton: UIButton!
    @IBOutlet weak var taskLabel: UILabel!
    
    @IBAction func checkmarkButtonPressed(_ sender: Any) {
    }
}
Preventative troubleshooting

At times, when assigning subclasses to UI elements in Interface Builder,  the Xcode may get confused and throw an error stating it can't find any information about a subclass - for example when you are trying to create an Outlet. To resolve this, after setting the class property in IB for an object, build an app  (Command+B) and that should be sufficient.   

Handle cell activity

Next we need to handle the button press and change its state to implement 'completed' and 'incomplete' states.

Set up button states

We are going to use two states of the button

  • Normal for incomplete tasks 

  • Selected for completed tasks

We will use an image to indicate each respective state. At this time we will learn how to assign them programmatically. This is a good opportunity to explore an option to configure a custom cell in the code.

Let's proceed! 😃

Add checkmark images to the project (from the image assets catalogue). Name them 'checkmark-on' and 'checkmark-off'.

As usual, to better organize the code, let's have a configuration method in our subclass:

func configure() {
    checkmarkButton.setImage(UIImage.init(named: "checkmark-off"), for: .normal)
    checkmarkButton.setImage(UIImage.init(named: "checkmark-on"), for: .selected)
}

Great! Where do we call that? ☎️

Programmatic configuration for cell subclasses is done using the method  awakeFromNib , which exists in the parent class UITableViewCell. We need to override it:

    override func awakeFromNib() {
        
        super.awakeFromNib()
        
        configure()
    }

 What does awake mean and what is nib

Nib is a file extension for Interface Builder that implements individual UI compositions. These could be custom views, table cells or app screens. These files are also called xib files.  

Awake means an element is being activated from the preset interface configuration.

Handle cell state change

When the user taps the button we want it to act like a toggle button switching between Normal and Selected states.

For that, we'll create a function,  markCompleted - and while we're at it, let's create another helper function to set caption, setCaption  :

func markCompleted(_ completed: Bool) {
    checkmarkButton.isSelected = completed
}

func setCaption(_ caption: String?) {
    taskLabel.text = caption
}

Let's call the markCompleted method in checkmarkButtonPressed:

@IBAction func checkmarkButtonPressed(_ sender: Any) {
    markCompleted(!checkmarkButton.isSelected)
}

That's cool, but... our view controller, and ultimately the datasource, would have no clue that this activity happens within a cell. 😱

To address this, we must notify the view controller about status change.

How do we 'notify' the view controller? 

To send the information to the controller, we'll use delegation. We'll implement our own protocol! 👩‍🔬

Here it is (we can place it right above our cell subclass):

protocol TaskCellDelegate {
    func taskCell(_ cell: TaskTableViewCell, completionChanged completion: Bool)
}

The code above declares a protocol with a required method. This means an object that declares to conform to this protocol has to implement this method. 

Next, create an optional delegate property in the cell subclass, 'TaskTableViewCell':

var delegate: TaskCellDelegate?

Now, a delegate that declares to conform to this protocol must have the required method implemented. So, if the delegate object is not nil, we can call this method on it:

@IBAction func checkmarkButtonPressed(_ sender: Any) {
    markCompleted(!checkmarkButton.isSelected)
    delegate?.taskCell(self, completionChanged: checkmarkButton.isSelected)
}

More new and interesting things are happening here.

First, we have this code:  delegate?. . This is an example of Optional chaining - unwrapping and proceeding (or stopping) in a chain of instructions.  In our example, this line of code reads as - if delegate object is not nil - go ahead and call a required method on it.

Second, we've just learned how to invoke a protocol method! We did this by calling 'taskCell/completionChanged' on a delegate property. As parameters we are providing the cell - which self and what state we are requesting - the  current state of the button we just updated. 😉

Here's the complete code for the task cell subclass:

import Foundation
import UIKit

protocol TaskCellDelegate {
    func taskCell(_ cell: TaskTableViewCell, completionChanged completion: Bool)
}

class TaskTableViewCell: UITableViewCell {
    
    @IBOutlet weak var checkmarkButton: UIButton!
    @IBOutlet weak var taskLabel: UILabel!
    
    var delegate: TaskCellDelegate?
    
    @IBAction func checkmarkButtonPressed(_ sender: Any) {
        markCompleted(!checkmarkButton.isSelected)
        delegate?.taskCell(self, completionChanged: checkmarkButton.isSelected)
    }
    
    func configure() {
        checkmarkButton.setImage(UIImage.init(named: "checkmark-off"), for: .normal)
        checkmarkButton.setImage(UIImage.init(named: "checkmark-on"), for: .selected)
    }
    
    override func awakeFromNib() {
        
        super.awakeFromNib()
        
        configure()
    }
    
    func markCompleted(_ completed: Bool) {
        checkmarkButton.isSelected = completed
    }
    
    func setCaption(_ caption: String?) {
        taskLabel.text = caption
    }
}

We've done what we have to within the cell subclass. The rest of the responsibilities are on view controller!

Let's equip the view controller with all the necessities.

'Collaborate' with view controller

Let's declare our view controller as a 'task cell delegate':

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TaskCellDelegate {
    ...
}

After adding this, Xcode will generate an error again, stating that the view controller does not confirm to this protocol. That's because we still need to implement a required method of the protocol. Let's go ahead and declare a handling method so that we can clear the error:

func taskCell(_ cell: TaskTableViewCell, completionChanged completion: Bool) {

}

The implementation needs to take care of two things:

  • Identify the corresponding datasource object: HotTask

  • Update the completion property of the datasource object

Here's the code:

    func taskCell(_ cell: TaskTableViewCell, completionChanged completion: Bool) {
        // identify indexPath for a cell
        if let indexPath = tableView.indexPath(for: cell) {
            // fetch datasource for indexPath
            if let task = hotTaskDataSource(indexPath: indexPath) {
                // update the completion state
                task.completed = completion
            }
        }
    }

Let's review what we've done in the code above:

  • We attempted to identify an indexPath that corresponds to the cell that notified the view controller about the change. Table view has a handy method for it: indexPath/for/cell.

  • If we were able to get indexPath, we proceeded with an attempt to fetch a corresponding task object using our helper function. 

  • If we got a task object, we updated its completion value with a new one supplied by a delegate method.

All good, except our cell objects are not connected to the view controller yet. Let's update the datasource delegate method serving 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) as! TaskTableViewCell
            let task = hotTaskDataSource(indexPath: indexPath)
            cell.setCaption(task?.caption)
            cell.delegate = self
            return cell
        case 2:
            let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCellID", for: indexPath) as! TaskTableViewCell
            let task = hotTaskDataSource(indexPath: indexPath)
            cell.setCaption(task?.caption)
            cell.delegate = self
            return cell
        default:
            return UITableViewCell.init()
        }
    }

Let's review the changes:

  • We are now type casting the cell we receive from the storyboard to TaskTableViewCell instead of using a default type, UITableViewCell, as we need to access specifics of our subclass.

  • We replaced the setting caption of a task to our custom label using a setCaptionMethod instead of assigning a caption value to the textLabel of a basic cell.

  • We assigned view controller (self) as a delegate to task cells. 

One cell down, one more to go!

Creating Custom New Task Cell

The new task cell needs to accommodate two elements: a text input and a button. A new element here is a text input. There are two UI elements available in UIKit to capture text input:

  • Text field: UITextField to work with single-line text

  • Text view: UITextView to work with multi-line text

Each of the elements has its own purposes and capabilities. For example, a text field can mask input (handy for passwords) or has a default implementation for a placeholder (the text that appears within the input before user types any text). Text view allows incorporating images and implementing other fancy formatting.

We'll keep things simple and use a text field, especially because it has a placeholder capability available to us without requiring any effort!

Compose the interface

Select the new task cell and set its style to 'Custom' and set Selection to 'None'. Then drag Text Field and Button objects onto it. Position and size the elements to achieve the following look:

New task cell UI
New task cell UI

Select the text input and set its Placeholder attribute on Attributes Inspector  to 'What else to do?', Return key to 'Done', and check 'Auto-enable Return Key' (this is a helpful feature that will keep the return key - 'Done' in this case - disabled until the user types something):

Text field attributes
Text field attributes

That's all for the interface! 😊

Connect to the code

You can probably guess what we are going to do next... That's right: subclass a table view cell object!

Create NewTaskTableViewCell.swift, declare  NewTaskTableViewCell  class as a subclass of UITableViewCell and assign the new class to the cell in the Interface Builder. Then create Outlets for the UI objects and Action for the button:

import Foundation
import UIKit

class NewTaskTableViewCell: UITableViewCell {
    
    @IBOutlet weak var textInput: UITextField!
    @IBOutlet weak var addButton: UIButton!
 
    @IBAction func addButtonPressed(_ sender: Any) {
        
    }
}

Our custom elements are now connected to the code!

Handle cell activity

The user is expected to perform different activities with this cell, working with keyboard input. The core of this challenge will be the topic of the next chapter. But there are a few things we can do now to prepare - for example, create a protocol.

This time the only thing we need from the cell is a caption of a new task the user wants to create:

protocol NewTaskCellDelegate {
    func newTaskCell(_ cell: NewTaskTableViewCell, newTaskCreated caption: String)
}

To connect it to the cell, let's declare a delegate property:

var delegate: NewTaskCellDelegate?

Finally, like for the Task cell, we'll be doing some configuring. Let's prepare for that:

    func configure() {
        
    }
    
    override func awakeFromNib() {
        
        super.awakeFromNib()
        
        configure()
    }

Very well! We'll finish this off in the next chapter :ange:!

Let's test what we have for the moment. Run the app in the simulator:

Custom cells in action!
Custom cells in action!

Not very pretty... 🙁 But the main point here is to check that all the intended elements are present and the correct content is displayed. 🙂

You can even try clicking the checkmark button and observe the updates.

How are we going to fix the layout?

Take a guess....

That's right, Auto Layout!

We've built quite a bit in our storyboard. And this time around, our interface needs to support only iPhones and only portrait orientation. That, however, does't protect our application from two variable circumstances:

  • There are still various iPhone screen sizes that we need to support.

  • The content on the app screen is out of our control. The user may create a short or a lengthy task! And we must be able to handle either!

So, Auto Layout it is! 👍 We need to constrain the following:

  • Table view to the screen

  • Table header contents

  • New task contents

  • Task cell contents

You're already a master of Auto Layout, so go ahead and create constraints for all the elements described above. There's more than one suitable set of constraints that will allow to achieve desirable results. Feel free to come up with your own, or use the following list as an example:

Table view
  • Fix table view to the edges of the screen.

Table header
  • Fix the title label to the top and side edges of the table header view at 16pt, fix label's height.

  • Fix the progress label at a distance from the title label and fix the side and bottom edges to the table header view at 16pt; fix the label's height.

New task
  • Fix the Button size at 70x44pt.

  • Align the Button to the center vertically and to the right edge at 0pt.

  • Fix input to the left edge at 0pt and to the button at 16pt.

  • To keep the margins at the top and bottom, also fix the input to the top and bottom at 6&5pt.

Task cell
  • Fix the button size at 44x44pt.

  • Centre the button vertically and fix to the left edge at 0pt.

  • Fix the label at 16pt to the button on the left, 0pt to the right edge and 7pt to the top and bottom edges.

  • To compensate for the possibility of the label having a shorter height than the checkmark button, we also need to fix the button to the top and bottom edges of the cell at 7pt. If we leave it at a default constraint that is set to Equal, we'll have a conflict with the vertical constraints for the label. To resolve this, adjust these new constraints for the button to 'Greater Than or Equal'. To test it, type a longer task name for one of the cells.

How easy was that?
How easy was that?

Let's test it out:

Auto Layout in action!
Auto Layout in action!

Great progress! 😎

Let's Recap!

  • The table header and footer are simple UIViews (or its subclasses) that can be designed in a storyboard.

  • To create dynamic repeatable content in a table view, we use Dynamic Prototype cells.

  • To accommodate custom functionality of a table cell, we need to subclass UITableViewCell and connect related cell prototype UI elements to the code of that subclass.

  • To accommodate dynamic content size for cells, we can use Auto Layout to arrange elements within a custom cell.

  • Communication between cell subclass functionality and view controller can be arranged using protocols and delegates.

  • To capture a user's keyboard input we use UITextField or UITextView objects.

Example of certificate of achievement
Example of certificate of achievement