Chapters

Hide chapters

UIKit Apprentice

First Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

19. UI Improvements
Written by Matthijs Hollemans & Fahim Farook

Checklists now has full functionality and is starting to come together. However, There are a few small features I’d like to add, just to polish the app a little more. After all, you’re building a real app here – if you want to make top-notch apps, you have to pay attention to those tiny details.

This chapter covers the following:

  • Show counts: Show the number of to-do items remaining for each list.
  • Sort the lists: Sort the list of checklist items alphabetically.
  • Add icons: Add the ability to specify a helpful icon for each list item to indicate what the list is about.
  • Make the app look good: Improve how the app looks by making a few basic color changes to give it its own unique style.

Show counts

On the main screen, for each checklist, the app will show the number of to-do items that do not have checkmarks yet:

Each checklist shows how many items are still left to-do
Each checklist shows how many items are still left to-do

Count the unchecked items

First, you need a way to count these items.

➤ Add the following method to Checklist.swift:

func countUncheckedItems() -> Int {
  var count = 0
  for item in items where !item.checked {
    count += 1
  }
  return count
}

This method asks the Checklist object how many of its ChecklistItem objects are still not checked. The method returns this count as an Int value.

You use a for...in to loop through the ChecklistItem objects from the items array. If an item object has its checked property set to false, you increment the local variable count by 1.

Remember that the ! operator negates the result. So if item.checked is true, then !item.checked will make it false. You should read it as “where not item.checked”.

Note: If the ! symbol is written in front of something then it is the logical not operator, as you see here. When the ! is written behind something, it’s related to optionals. This is another example of a symbol that has more than one meaning in Swift. The correct interpretation depends on the context where it is being used.

When the loop is over and you’ve looked at all the objects, you return the total value of the count to the caller.

Exercise: What would happen if you used let instead of var to define the count variable?

Answer: When count is a constant, Swift won’t let you change its value, so the line that does += 1 will show an error message.

By the way, you could also have written the loop as follows:

  for item in items {
    if !item.checked {
      count += 1
    }
  }

This uses the more familiar if statement instead. Personally, I like the brevity of the for...in where loop, but using an if is just as valid.

As the above code indicates, your own objects can have custom methods too. A good object-oriented principle is to let your own objects change their state or to provide information about themselves as much as possible.

Display the unchecked item count

Currently, the table view cells in the All Lists scene display one line of text. This is using the default table view cell style. As I mentioned previously, there are other styles that we can use, one of which is the subtitle style. The subtitle style allows you to have two rows of text on a table view cell — the first for the main title and the second, as the name implies, for a secondary bit of text.

However, our current way of creating cells — by calling dequeueReusableCell(withIdentifier: for:) — does not allow us to specify a custom table view cell style. So, we’re going to have to modify the code a bit to get things to work.

➤ Go to AllListsViewController.swift and remove the register(_:forCellReuseIdentifier:) line from viewDidLoad since we will not require that. Instead, we will create the table view cells by hand if a cached cell is not available.

If you forget to remove the above line, your app will crash when you try to run it later. I will explain why in the next step.

➤ In tableView(_:cellForRowAt:) replace the first line — the one dequeuing a cell — with the following lines of code:

// Get cell
let cell: UITableViewCell!
if let tmp = tableView.dequeueReusableCell(
  withIdentifier: cellIdentifier) {
  cell = tmp
} else {
  cell = UITableViewCell(
    style: .subtitle, 
    reuseIdentifier: cellIdentifier)
}

Here, you define a constant to hold the newly created cell and then see if you can dequeue a cell from the table view for the given identifier. If there is no cell — meaning that there are no cached cells that can be re-used — then you create a new UITableViewCell instance with the cell style, and the identifier, that you want. If there is a cell, then you assign its reference to the previously declared constant.

These new cells would, of course, be added to the available pool of table view cells and would be available for re-use from this point onwards.

If you did not remove the table view class registration in the previous step, the dequeue step in the above code will never fail since the dequeueReusableCell( withIdentifier:) method will automatically create a new cell of the registered class if a cached instance does not exist. However, this new table view cell instance would not be of the subtitle style — instead, it will have the default style. So, any references to the subtitle label in the table view cell — unless properly guarded against — will cause your app to crash.

The “subtitle” cell style adds a second, smaller label below the main label. You can use the cell’s detailTextLabel property to access this subtitle label.

➤ Add the following line just before return cell in tableView(_:cellForRowA:t):

cell.detailTextLabel!.text = "\(checklist.countUncheckedItems()) Remaining"

You call the countUncheckedItems() method on the Checklist object and put the count into a new string that you display using the detailTextLabel.

As usual, you use \(…) to do the string interpolation. Notice that you can even call methods inside interpolated strings. Sweet!

Force unwrapping

To put text into the cell’s labels, you wrote:

cell.textLabel!.text = someString
cell.detailTextLabel!.text = anotherString

The ! is necessary because textLabel and detailTextLabel are optionals.

The textLabel property is only present on table view cells that use one of the built-in cell styles; it is nil on custom cell designs. Likewise, not all of the cell styles have a detail label and detailTextLabel will be nil in those cases.

Here you’re using the “subtitle” cell style, which is guaranteed to have both labels. Because these optionals will never be nil for a “subtitle” cell, you can use ! to force unwrap them. This turns the optional into an actual object that you can use.

Be careful with this, though… using ! on an optional that is nil will crash your app immediately.

You could also have written the above code as:

if let label = cell.textLabel {
  label.text = someString
}
if let label = cell.detailTextLabel {
  label.text = anotherString
}

That is safer — no chance of crashing here — but also a bit more cumbersome. Writing ! was just more convenient in this case.

➤ Run the app. For each checklist it will now show how many items still remain unchecked.

The cells now have a subtitle label
The cells now have a subtitle label

Update the unchecked item count on changes

One problem: The to-do count never changes. If you toggle a checkmark on or off, or add new items, the “to do” count remains the same. That’s because you create these table view cells once and never update their labels — try it out

Exercise: Think of all the situations that will cause this “still to do” count to change.

Answer:

  • The user toggles a checkmark on an item. When the checkmark is set, the count goes down. When the checkmark gets removed, the count goes up again.
  • The user adds a new item. New items don’t have their checkmark set, so adding a new item should increment the count.
  • The user deletes an item. The count should go down but only if that item had no checkmark.

These changes all happen in the ChecklistViewController but the “still to do” label is shown in the AllListsViewController.

So, how do you let the All Lists View Controller know about this?

If you thought, “That’s easy, let’s use a delegate!”, then you’re starting to get the hang of this. You could make a new ChecklistViewControllerDelegate protocol that sends messages when the following things happen:

  • The user toggles a checkmark on an item
  • The user adds a new item
  • The user deletes an item

But what would the delegate — which would be AllListsViewController — do in response? It would simply set some new text on the cell’s detailTextLabel in all cases.

The delegate approach sounds good, but you’re going to cheat and not use a delegate at all :] There is a simpler solution, and a smart programmer always picks the simplest way to solve a problem.

➤ Go to AllListsViewController.swift and add the viewWillAppear() method to do the following:

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  tableView.reloadData()
}

Don’t confuse this method with viewDidAppear(). The difference is in the verb: will versus did. viewWillAppear() is called before viewDidAppear(), when the view is about to become visible but the animation hasn’t started yet. viewDidAppear() is called after the view is visible on the screen and the animation has completed. There may be half a second or so difference between them as the animation takes place.

The iOS API often does this: there is a “will” method that is invoked before something happens and a “did” method that is invoked after that something happens. Sometimes you need to do things before, sometimes after, and having two methods gives you the ability to choose whichever situation works best for you.

API (ay-pee-eye) stands for Application Programming Interface. When people say “the iOS API” they mean all the frameworks, objects, protocols and functions that are provided by iOS that you as a programmer can use to write apps.

The iOS API consists of everything from UIKit, Foundation, Core Graphics, and so on. Likewise, when people talk about “the Facebook API” or “the Google API”, they mean the services that these companies provide that allow you to write apps for those platforms.

Here, viewWillAppear() tells the table view to reload its entire contents. That will cause tableView(_:cellForRowAt:) to be called again for every visible row.

When you tap the back button on the ChecklistViewController’s navigation bar, the AllListsViewController screen will slide back into view. Just before that happens, viewWillAppear() is called. Thanks to the call to tableView.reloadData() the app will update all of the table cells, including the detailTextLabels.

Reloading all of the cells may seem like overkill, but in this situation you can easily get away with it. It’s unlikely the All Lists screen will contain many rows (say, less than 100) and only about 14 visible cells, so reloading them is quite fast. And it saves you the work of having to create yet another delegate.

Sometimes a delegate is the best solution; sometimes you just reload the entire table :]

➤ Run the app and test that it works!

Display a completion message when all items are done

Exercise: Change the label to read “All Done!” when there are no more to-do items left to check.

Answer: Change the relevant code in tableView(_:cellForRowAt:) to:

let count = checklist.countUncheckedItems()
cell.detailTextLabel!.text = count == 0 ? "All Done" : "\(count) Remaining"

You put the count into a local constant because you will refer to it more than once. Calculating the count once and storing it into a temporary constant is more optimal than doing the same calculation twice.

But what about the second line of code? It has something new/interesting going on, right?

It’s actually just a simpler way to do an if...else block. The condition ? If true : else construct is known as a ternary conditional operator — if the first part (the bit before the ?) evaluates to true, then the result of the expression would be the item after the ?. Otherwise, the result is the item after the :. It can be very handy in a lot of places to write simpler, more succinct code.

The same thing could have been done with an if...else block but that would have taken five lines. Personally, I prefer to use the ternary operator where possible.

Display an indicator when there are no items in a list

Exercise: Now update the label to say “No Items” when the list is empty.

Answer:

let count = checklist.countUncheckedItems()
if checklist.items.count == 0 {
  cell.detailTextLabel!.text = "(No Items)"
} else {
  cell.detailTextLabel!.text = count == 0 ? "All Done" : "\(count) Remaining"
}

Just looking at the result of countUncheckedItems() is not enough. If this returns 0, you don’t know whether that means all items are checked off or if the list has no items at all. You also need to look at the total number of items in the checklist, with checklist.items.count.

You could have done the setting of the text as two nested ternary operators as well, but sometimes, it’s better to write code that’s clear rather than succinct :]

The text in the detail label changes depending on how many items are checked off
The text in the detail label changes depending on how many items are checked off

Little details like these matter – they make your app more fun to use. Ask yourself, what would make you feel better about having done your chores, the rather bland message “0 Remaining” or the joyous exclamation “All Done!”?

Functional Programming

Swift is primarily an object-oriented language. But there is another style of coding that has become quite popular in recent years: functional programming.

The term “functional” means that programs can be expressed purely in terms of mathematical functions that transform data.

Unlike the methods and functions in Swift, these mathematical functions are not allowed to have “side effects”. For any given inputs, a function should always produce the same output. Methods are much less strict.

Even though Swift is not a purely functional language, it does let you use certain functional programming techniques in your apps. They can really make your code a lot shorter.

For example, let’s look at countUncheckedItems() again:

func countUncheckedItems() -> Int {
  var count = 0
  for item in items where !item.checked {
    count += 1
  }
  return count
}

That’s quite a bit of code for something that’s fairly simple. You can actually write this in a single line of code:

func countUncheckedItems() -> Int {
  return items.reduce(0) { 
    cnt,item in cnt + (item.checked ? 0 : 1) 
  }
}

reduce() is a method that looks at each item in the array and performs the code in the { } block. Initially, the cnt variable contains the value 0, but after each item it is incremented by either 0 or 1, depending on whether the item was checked — that check is done using our new friend, the ternary operator.

When reduce() is done, its return value is the total count of unchecked items.

You don’t have to remember any of this for now, but it’s pretty cool to see that Swift allows you to express this kind of algorithm very succinctly.

Sort the lists

Another thing you often need to do with lists is sort them in some particular order.

Let’s sort the list of checklists by name. Currently when you add a new checklist it is always appended to the end of the table, regardless of alphabetical order.

When do you do the sorting?

Before we figure out how to sort an array, let’s think about when you need to perform this sort:

  • When a new checklist is added
  • When a checklist is renamed

There is no need to re-sort when a checklist is deleted because that doesn’t have any impact on the order of the other objects.

Currently you handle these two situations in AllListsViewController’s implementation of didFinishAdding and didFinishEditing.

➤ Change these methods to the following:

func listDetailViewController(
  _ controller: ListDetailViewController, 
  didFinishAdding checklist: Checklist
) {
  dataModel.lists.append(checklist)
  dataModel.sortChecklists()    
  tableView.reloadData()
  navigationController?.popViewController(animated: true)
}

func listDetailViewController(
  _ controller: ListDetailViewController, 
  didFinishEditing checklist: Checklist
) {
  dataModel.sortChecklists()
  tableView.reloadData()
  navigationController?.popViewController(animated: true)
}

You were able to remove a bunch of code from both methods because you now always do reloadData() on the table view.

It is no longer necessary to insert the new row manually, or to update the cell’s textLabel. Instead you simply call tableView.reloadData() to refresh the entire table’s contents after you’ve sorted the data.

Again, you can get away with this because the table will only hold a handful of rows. If this table had hundreds of rows, a more advanced approach might be necessary — you could figure out where the new or renamed Checklist object should be inserted and just update that row.

The sorting algorithm

The sortChecklists() method on DataModel is new and you still need to add it. But before that, we need to have a short discussion about how sorting works.

When you sort a list of items, the app will compare the items one-by-one to figure out what the proper order is. But what does it mean to compare two Checklist objects?

In Checklists we obviously want to sort them by name, but we need some way to tell the app that’s what we mean.

➤ Add the following method to DataModel.swift:

func sortChecklists() {
  lists.sort { list1, list2 in
    return list1.name.localizedStandardCompare(list2.name) == .orderedAscending
  }
}

Here you tell the lists array that the Checklists it contains should be sorted using some specific logic.

Note that while sort() is a method, you don’t have the brackets after the method because you are using a trailing closure to do the actual work of sorting. You can tell it’s a closure by the { } brackets around the sorting code:

lists.sort { /* the sorting code goes here */ }

You’ve briefly seen closures with the alert box in the Bull’s Eye app. They wrap a piece of source code into an anonymous, inline method.

The purpose of the closure is to determine whether one Checklist object comes before another, based on our rules for sorting.

The sort algorithm will repeatedly ask one Checklist object from the list how it compares to the other Checklist objects using the logic from the closure, and then shuffle them around until the array is sorted.

This allows sort() to sort the contents of the array in any order you desire. If you wanted to sort on other criteria, all you’d have to do is change the logic inside the closure.

The actual sorting code is this:

list1.name.localizedStandardCompare(list2.name) == .orderedAscending

To compare these two Checklist objects, you’re only looking at their names.

The localizedStandardCompare(_:) method compares the two name strings while ignoring lowercase vs. uppercase (so “a” and “A” are considered equal) and taking into consideration the rules of the current locale.

A locale is an object that knows about country and language-specific rules. Sorting in German may be different than sorting in English, for example.

That’s all you have to do to sort the array: call sort() and give it a closure with the logic that compares two Checklist objects.

➤ Just to make sure the existing lists are also sorted in the right order, you should also call sortChecklists() when the plist file is loaded:

func loadChecklists() {
    . . .
    lists = try decoder.decode([Checklist].self, from: data)
    sortChecklists()       // Add this
  } catch {
    ...
}

➤ Run the app and add some new checklists. Change their names and notice that the list is always sorted alphabetically.

New checklists are always sorted alphabetically
New checklists are always sorted alphabetically

Add icons

Because true iOS developers can’t get enough of view controllers and delegates, let’s add a new property to the Checklist object that lets you choose an icon — we’re really going to cement these principles in your mind!

When you’re done, the Add/Edit Checklist screen will look like this:

You can assign an icon to a checklist
You can assign an icon to a checklist

You are going to add a row to the Add/Edit Checklist screen that opens a new screen for picking an icon. This icon picker is a new view controller and you will show it by pushing it on to the navigation stack, just like your previous view controllers.

Add the icons to the project

The Resources folder for the book contains a folder named Checklist Icons with a selection of PNG images that depict different categories.

The various checklist icon images
The various checklist icon images

➤ Add the images from this folder to the asset catalog. Select Assets.xcassets in the project navigator, click the + button at the bottom and choose Import…

Importing new images into the asset catalog
Importing new images into the asset catalog

Navigate to the Checklist Icons folder and select all the files inside:

Selecting the image files to import
Selecting the image files to import

Note: Make sure to select the actual image files, not the folder.

Click Open to import the images. The asset catalog should now look like this:

The asset catalog after importing the checklist icons
The asset catalog after importing the checklist icons

Each image comes with a 2x version for Retina devices and a 3x version for the Retina HD devices.

As I pointed out previously, you don’t need low-resolution 1x graphics anymore. All iPhone, iPad, and iPod touch devices that can run iOS 14 have Retina 2x or 3x screens.

Update the data model

➤ Add the following property to Checklist.swift:

var iconName = ""

The iconName variable holds the name of the icon image.

The above code initializes iconName to have no icon set by default. But what if you actually wanted to create new Checklist objects with a default icon?

It’s very easy to implement a default icon. Say, you want all new checklists to have the “Appointments” icon — then change the above line to this:

var iconName = "Appointments"

And that’s all you need to do :]

Display the icon

At this point, you just want to see that you can make an icon — any icon — show up in the table view. When that works, you can worry about letting the user pick their own icons. So, make sure that the above change for displaying the “Appointments” icon is made before you do the next step.

➤ Change tableView(_:cellForRowAt:) in AllListsViewController.swift to put the icon into the table view cell:

override func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  . . .

  cell.imageView!.image = UIImage(named: checklist.iconName)
  return cell
}

Cells using the standard .subtitle cell style come with a built-in UIImageView on the left. You can simply pass it an image and it will be displayed automatically. Easy peasy.

Note: When you run the app, you will not see any of your previously saved checklist items. Can you guess why? The addition of iconName changed the Checklist object and the previously saved information for the object is no longer valid. So, the decoder will run into issues when trying to decode the previously saved file and so, you will end up with no saved items. Sorry.

➤ Run the app, create a few checklists and now each of them should have an alarm clock icon.

The checklists have an icon
The checklists have an icon

The default icon

Now that you know it works, you can change Checklist to give each Checklist object an icon named “No Icon” by default.

➤ In Checklist.swift, change the iconName declaration to:

var iconName = "No Icon"

The “No Icon” image is a transparent PNG image with the same dimensions as the other icons. Using a transparent image is necessary to make all the checklists line up properly, even if they have no icon.

If you were to set iconName to an empty string instead, the image view in the table view cell would remain empty and the text would align with the left margin of the screen. That looks bad when other cells do have icons:

Using an empty image to properly align the text labels (right)
Using an empty image to properly align the text labels (right)

The icon picker class

Now, let’s create the icon picker screen.

➤ Add a new Swift file to the project. Name it IconPickerViewController.

➤ Replace the contents of IconPickerViewController.swift with:

import UIKit

protocol IconPickerViewControllerDelegate: class {
  func iconPicker(
    _ picker: IconPickerViewController, 
    didPick iconName: String)
}

class IconPickerViewController: UITableViewController {
  weak var delegate: IconPickerViewControllerDelegate?
}

This defines the IconPickerViewController object, which is a table view controller, and a delegate protocol that it uses to communicate with other objects in the app.

➤ Add a constant (inside the class implementation) to hold the array of icons:

let icons = [ 
  "No Icon", "Appointments", "Birthdays", "Chores", 
  "Drinks", "Folder", "Groceries", "Inbox", "Photos", "Trips" 
]

This is an array that contains a list of icon names. These strings are both the text you will show on the screen and the name of the PNG file inside the asset catalog.

The icons array is the data model for this table view. Note that it is a non-mutable array — it is defined with let and arrays are “value” types — because the user cannot add new icons or delete icons from the available list.

This new view controller is a UITableViewController, so you have to implement the data source methods for the table view.

➤ Add the following method to the source file:

// MARK: - Table View Delegates
override func tableView(
  _ tableView: UITableView, 
  numberOfRowsInSection section: Int
) -> Int {
  return icons.count
}

This simply returns the number of icons in the array.

➤ Next, add this method:

override func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
    withIdentifier: "IconCell", 
    for: indexPath)
  let iconName = icons[indexPath.row]
  cell.textLabel!.text = iconName
  cell.imageView!.image = UIImage(named: iconName)
  return cell
}

Here, you obtain a table view cell and give it a title and an image. You will design this cell in the storyboard momentarily. It will be a prototype cell with the “default” cell style, or “Basic” as it is called in Interface Builder. Cells with this style already contain a text label and an image view, which is very convenient.

The icon picker storyboard changes

➤ Open the storyboard. Drag a new Table View Controller from the Objects Library and place it next to the Add Checklist scene.

➤ In the Identity inspector, change the class of this new table view controller to IconPickerViewController.

➤ Select the prototype cell and set its Style to Basic and its (re-use) Identifier to IconCell.

That takes care of the design for the icon picker. Now you need to have some place to call it from. To do this, you will add a new row to the Add Checklist screen.

➤ Go to the Add Checklist View Controller and add a new section to the table view. You can do this by changing the Sections value in the Attributes inspector for the table view from 1 to 2. This will duplicate the existing section.

➤ Delete the Text Field from the cel in the new section; you don’t need it.

➤ Add a Label to this cell and change its text to Icon.

➤ Set the cell’s Accessory to Disclosure Indicator. That adds a gray chevron.

➤ Add an Image View to the right of the cell. Make it 36 × 36 points big – use the Size inspector for this.

➤ Once the Label and Image View are sized and positioned, add width, height, top, right, and bottom Auto Layout constraints to the Image View using the Add New Constraints menu available via the icon at the bottom of the canvas.

Adding constraints to the Image View
Adding constraints to the Image View

What you want to happen is that the image view stays glued to the right edge of the screen, always at the same distance from the disclosure indicator. When the view controller grows or shrinks to fit the iPhone screen, the image view should move along with it.

The image view should now look like this:

The Image View with the constraints
The Image View with the constraints

Make sure the bars representing the constraints are blue. If they are orange or red you may have forgotten something in the Add New Constraints menu. Either try again or use the Editor ▸ Resolve Auto Layout Issues ▸ Update Frames menu item.

The most important constraint is the one on the right. This tells UIKit that the right-hand side of the image view should always stick to the right-hand edge of the table view cell’s content view.

In other words, no matter how wide or narrow the screen is, the image view will always have the same location relative to the disclosure indicator.

The other constraints — top, bottom, width, and height — were necessary only because all views must always have enough constraints to determine their position and size. You could have left out the bottom one, but here you need it because the whole cell size is determined based on the image view height and its top and bottom spacing.

➤ Add left and right constraints — where the right constraint is 8 points instead of whatever value you would get by default — to the Label via the Add New Constraints menu. Finally, vertically center the Label to the Image View by Control-dragging from the Label to the Image View in the Document Outline and selecting Center Vertically from the pop up menu.

The Image View with the constraints
The Image View with the constraints

➤ To verify that your changes do the right thing you don’t necessarily need to run the app in the simulator. Use the View as: panel at the bottom to switch between the different iPhone models right inside Interface Builder. If your constraints are correct, then the icon should always be in the right place.

➤ Use the Assistant Editor to add an outlet property for the image to ListDetailViewController.swift and name it iconImage.

That completes the designs for both screens — you can now connect them via a segue.

Control-drag from the “Icon” table view cell to the Icon Picker View Controller and add a segue of type Selection Segue – Show. Make sure you’re dragging from the Table View Cell, not its Content View or any of the other subviews. If you are unable to do this accurately from the scene, remember that you can also Control-drag from the Document Outline.

➤ Give the segue the identifier PickIcon.

➤ Thanks to the segue, the new view controller has been given a navigation bar. Double-click the navigation bar center, where the title should be, and set the title to Choose Icon.

This part of the storyboard should now look like this:

The Icon Picker view controller in the storyboard
The Icon Picker view controller in the storyboard

Display the icon picker

➤ In ListDetailViewController.swift, change the willSelectRowAt table view delegate method to:

override func tableView(
  _ tableView: UITableView, 
  willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
  return indexPath.section == 1 ? indexPath : nil
}

Without this change you cannot tap the “Icon” cell to trigger the segue.

Previously this method always returned nil, which meant tapping on rows was not possible. Now, however, you want to allow the user to tap the Icon cell, so this method should return the index-path for that cell.

Because the Icon cell is the only row in the second section, you only have to check indexPath.section. There is no need to check the row number. Users still can’t select the cell with the text field from section 0.

➤ Run the app and verify that there is now an Icon row in the Add/Edit Checklist screen. Tapping it will open the Choose Icon screen and show a list of icons.

The icon picker screen
The icon picker screen

Handle icon selection

You can press the back button to go back but selecting an icon doesn’t do anything yet. It just colors the row gray but doesn’t put the icon into the checklist.

To make this work, you have to hook up the icon picker to the Add/Edit Checklist screen through its own delegate protocol.

➤ First, add an instance variable in ListDetailViewController.swift:

var iconName = "Folder"

You use this variable to keep track of the chosen icon name.

Even though the Checklist object now has an iconName property, you cannot keep track of the chosen icon in the Checklist object for the simple reason that you may not always have a Checklist object, i.e. when the user is adding a new checklist.

So, you’ll store the icon name in a temporary variable and copy that into the Checklist’s iconName property at the right time.

You should initialize the iconName variable with something reasonable. Let’s go with the folder icon. This is only necessary for new Checklists, which get the Folder icon by default.

➤ Update viewDidLoad() to the following:

override func viewDidLoad() {
  . . .
  if let checklist = checklistToEdit {
    . . .
    iconName = checklist.iconName              // add this
  }
  iconImage.image = UIImage(named: iconName)   // add this
}

This has two new lines: If the checklistToEdit optional is not nil, then you copy the Checklist object’s icon name into the iconName instance variable. You also load the icon’s image file into a new UIImage object and set it as the cell’s image so it shows up in the Icon row.

Earlier you created a push segue named “PickIcon”. You still need to implement prepare(for:sender:) in order to tell the IconPickerViewController that this screen is now its delegate.

➤ First, add the name of that protocol to the class line in ListDetailViewController.swift:

class ListDetailViewController: UITableViewController, UITextFieldDelegate, IconPickerViewControllerDelegate {

➤ Next, add the implementation of the method from that delegate protocol:

// MARK: - Icon Picker View Controller Delegate
func iconPicker(
  _ picker: IconPickerViewController, 
  didPick iconName: String
) {
  self.iconName = iconName
  iconImage.image = UIImage(named: iconName)
  navigationController?.popViewController(animated: true)
}

This puts the name of the chosen icon into the iconName variable to remember it, and also updates the image view with the new image.

After you do all that, you use popViewController(animated:) to “pop” the Icon Picker View Controller off the navigation stack.

Recall that navigationController is an optional property of the view controller, so you need to use ? (or !) to access the actual UINavigationController object.

➤ Now, add the following method to ListDetailViewController.swift:

// MARK: - Navigation
override func prepare(
  for segue: UIStoryboardSegue, 
  sender: Any?
) {
  if segue.identifier == "PickIcon" {
    let controller = segue.destination as! IconPickerViewController
    controller.delegate = self
  }
}

This code should have no big surprises for you.

➤ Change the done() action so that it puts the chosen icon name into the Checklist object when the user closes the screen:

@IBAction func done() {
  if let checklist = checklistToEdit {
    checklist.name = textField.text!
    checklist.iconName = iconName                  // add this
    delegate?.listDetailViewController(
      self, 
      didFinishEditing: checklist)
  } else {
    let checklist = Checklist(name: textField.text!)
    checklist.iconName = iconName                  // add this
    delegate?.listDetailViewController(
      self, 
      didFinishAdding: checklist)
  }
}

Finally, you must change IconPickerViewController to actually call the delegate method when a row is tapped.

➤ Add the following method to the bottom of IconPickerViewController.swift:

override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  if let delegate = delegate {
    let iconName = icons[indexPath.row]
    delegate.iconPicker(self, didPick: iconName)
  }
}

And that’s it. You can now set icons on the Checklist objects.

To recap, you:

  • Added a new view controller object.
  • Designed its user interface in the storyboard editor.
  • Hooked it up to the Add/Edit Checklist screen using a segue and a delegate.

Those are the basic steps you need to take with any new screen that you add.

➤ Run the app to try it out.

You can now give each list its own icon
You can now give each list its own icon

Achievement unlocked: users can pick icons!

Code refactoring

There’s still a small improvement you can make to the code. In done(), you currently do this:

let checklist = Checklist(name: textField.text!)
checklist.iconName = iconName

Setting the icon name can be considered part of the initialization of Checklist, so it would be nice if you could pass the icon name to the Checklist initializer. And you can :]

➤ Switch to Checklist.swift and modify the init method as follows:

init(name: String, iconName: String = "No Icon") {
  self.name = name
  self.iconName = iconName
  super.init()
}

The modified init method looks almost the same as the previous one except for taking a new iconName parameter and assigning it to the object’s iconName property.

But what is the = "No Icon" bit after the second parameter? That’s called a default parameter value. When you specify a default parameter value for a method, when the method is called, you can omit the parameters with default values and the method call would still work, but the default values would be used for the parameters that were omitted. Nifty, huh?

➤ In ListDetailViewController.swift’s done() method, replace the code that creates the new Checklist object with this and remove the old line set the iconName property:

let checklist = Checklist(name: textField.text!, iconName: iconName)

➤ Build the app to verify it still works.

Exercise: Give ChecklistItem an init(text:) method that is used instead of the parameter-less init(). Or how about an init(text:checked:) method?

Make the app look good

For Checklists, you’re going to keep things simple as far as fancying up the graphics goes. The standard look of navigation controllers and table views is perfectly adequate, although a little bland. In the next apps you’ll see how you can customize the look of these UI elements.

Change the tint color

Even though this app uses the stock visuals, there is a simple trick to give the app its own personality: changing the tint color.

The tint color is what UIKit uses to indicate that things, such as buttons, can be interacted with. The default tint color is a medium blue.

The buttons all use the same tint color
The buttons all use the same tint color

Changing the tint color is pretty easy.

➤ Open the storyboard and go to the File inspector (the first tab). Make sure you select a scene on the storyboard, otherwise you might not see the setting you need for the next step.

➤ Show the dropdown for Global Tint, click Custom… to open the color picker, and choose Red: 4, Green: 169, Blue: 235. That makes the tint color a lighter shade of blue.

Changing the Global Tint color for the storyboard
Changing the Global Tint color for the storyboard

Tip: If the color picker only shows a black & white bar, then click the dropdown at the top that says Gray Scale Slider and change it to RGB Sliders.

Set the color of the checkmark

It would also look nice if the checkmark wasn’t black but used the tint color too.

➤ To make that happen, select the checkmark label in the storyboard, switch to the Attributes inspector and change the Color setting to the same color as the global tint color.

➤ Run the app. It already looks a lot more interesting:

The tint color makes the app less plain looking
The tint color makes the app less plain looking

Add app icons

No app is complete without an icon. The Resources folder for this app contains a folder named Icon with the app icon image in various sizes. Notice that it uses the same blue as the tint color.

➤ Add these icons to the asset catalog – Assets.xcassets. Recall that icons go into the AppIcon section. Simply drag them from the Finder into the slots.

The app icons in the asset catalog
The app icons in the asset catalog

Set the launch image

Apps should also have a launch image or launch file. Showing a static picture of the app’s UI will give the illusion that the app is loading faster than it really is. It’s all smoke and mirrors :]

The Xcode template includes the file LaunchScreen.storyboard that is used as the launch file. With some effort you could make this look like the initial screen of the app, but there’s an easier solution.

➤ Open the Project Settings screen. In the General tab, scroll down to the App Icons and Launch Images section.

➤ In the Launch Screen File box, press the arrow and select Main.storyboard.

Changing the launch screen file
Changing the launch screen file

This tells the app you’ll be using the design from the storyboard as the launch file.

Upon startup, the app finds the initial view controller and converts it into a static launch image. For this app that is the All Lists View Controller inside its navigation controller.

➤ Delete LaunchScreen.storyboard from the project.

➤ From the Product menu choose Clean Build Folder. It’s also a good idea to delete the app from the Simulator just so it no longer has any copies of the old launch file lying around — hold down on the icon until it starts to wiggle, just like on a real iPhone.

➤ Run the app. Just before the real UI appears you should briefly see the following launch screen:

The empty launch screen
The empty launch screen

The launch screen simply has a navigation bar and an empty table view. This gives the illusion the app’s UI has already been loaded, though in reality, the data hasn’t been filled in yet.

Using a proper launch screen makes the app look more professional – and faster!

For many apps, you can simply use the main storyboard as the launch file, making it a no-brainer to add.

Test on all iOS devices

The app should run without major problems on all current iOS devices, from the smallest (iPhone SE) to the largest (iPad Pro). Table view controllers are very flexible and will automatically resize to fit the screen, no matter how large or small. Give it a try in the different Simulators!

Of course, there’s a bit of a gap between should and does — so make sure to test on all the different device types to make sure that nothing was missed :]

But if all your testing turns up nothing amiss, then you should be good to go!

You can find the project for the app up to this point under 19-UI-improvements in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.