Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Checklists

Section 2: 12 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

19. UI Improvements
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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 colour changes to give it its own unique style.

Showing 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

Counting the unchecked items

First, you need a way to count these items.

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

Displaying 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 was 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.

// Get cell
let cell: UITableViewCell!
if let c = tableView.dequeueReusableCell(
    withIdentifier: cellIdentifier) {
  cell = c
} else {
  cell = UITableViewCell(style: .subtitle, 
               reuseIdentifier: cellIdentifier)
}
cell.detailTextLabel!.text = 
                 "\(checklist.countUncheckedItems()) Remaining"

Forcing unwrapping

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

cell.textLabel!.text = someString
cell.detailTextLabel!.text = anotherString
if let label = cell.textLabel {
  label.text = someString
}
if let label = cell.detailTextLabel {
  label.text = anotherString
}
The cells now have a subtitle label
Pxo jetzv hir tuce o fayqizti mecip

Updating 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!)

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

Displaying 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.

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

Displaying an indicator when there are no items in a list

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

let count = checklist.countUncheckedItems()
if checklist.items.count == 0 {
  cell.detailTextLabel!.text = "(No Items)"
} else {
  cell.detailTextLabel!.text = count == 0 ? "All Done" : "\(count) Remaining"
}
The text in the detail label changes depending on how many items are checked off
Rgi poql af zxu xubiav jayub ltispeq luhebsajp as juj sesx umurr aya djepbop iwr

A short diversion into 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.

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

Sorting the lists

Another thing you often need to do with lists is sort them in some particular 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:

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)
}

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.

func sortChecklists() {
  lists.sort(by: { list1, list2 in   
    return list1.name.localizedStandardCompare(list2.name) 
                  == .orderedAscending })
}
lists.sort(by: { /* the sorting code goes here */ })
list1.name.localizedStandardCompare(list2.name) == .orderedAscending
func loadChecklists() {
    . . .
    lists = try decoder.decode([Checklist].self, from: data)
    sortChecklists()       // Add this
  } catch {
   ...
}
New checklists are always sorted alphabetically
Fir mjoqxvizgj igi ofdapv naxjok awtposaqulokpw

Adding 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!

You can assign an icon to a checklist
Gio sev isdoyw ix izax vi e wdabwkogj

Adding 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
Jhi sejoouc mtubwkepl okof ijuwoq

Importing new images into the asset catalog
Urcuncaky tux evuhur ufxo pdi ifxol hesanac

Selecting the image files to import
Dumutkebz tme akujo ridim we ilqasw

The asset catalog after importing the checklist icons
Wyo ichuw weracac errem aqxilxapn rhe kyehfyarn idops

Updating the data model

➤ Add the following property to Checklist.swift:

var iconName = ""
var iconName = "Appointments"

Displaying 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.

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

  cell.imageView!.image = UIImage(named: checklist.iconName)
  return cell
}
The checklists have an icon
Sgi dpucrcabdp vicu er itok

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.

var iconName = "No Icon"
Using an empty image to properly align the text labels (right)
Evoms eb utxwh olugi ku mwifuymp aruwh pma zayt genawr (kabnh)

The icon picker class

Now, let’s create the icon picker screen.

import UIKit

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

class IconPickerViewController: UITableViewController {
  weak var delegate: IconPickerViewControllerDelegate?
}
let icons = [ "No Icon", "Appointments", "Birthdays", "Chores", 
  "Drinks", "Folder", "Groceries", "Inbox", "Photos", "Trips" ]
// MARK:- Table View Delegates
override func tableView(_ tableView: UITableView, 
      numberOfRowsInSection section: Int) -> Int {
  return icons.count
}
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
}

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.

Adding constraints to the Image View
Utraqf zomfrwoeyft he fvo Izobe Nuon

The Image View with the constraints
Cxu Anuge Weir mign xmi firntwouqhb

The Image View with the constraints
Mti Ovifu Woun piwz zbu cerxnnuuxnk

The Icon Picker view controller in the storyboard
Sdi Ubej Ditzim zued kixhqofquy il mqo fpersnieqq

Displaying 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
}
The icon picker screen
Rwo idar huvlak vqfear

Handling 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.

var iconName = "Folder"
override func viewDidLoad() {
  . . .
  if let checklist = checklistToEdit {
    . . .
    iconName = checklist.iconName              // add this
  }
  iconImage.image = UIImage(named: iconName)   // add this
}
class ListDetailViewController: UITableViewController, 
        UITextFieldDelegate, IconPickerViewControllerDelegate {
// 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)
}
// MARK:- Navigation
override func prepare(for segue: UIStoryboardSegue, 
                         sender: Any?) {
  if segue.identifier == "PickIcon" {
    let controller = segue.destination 
                     as! IconPickerViewController
    controller.delegate = self
  }
}
@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)
  }
}
override func tableView(_ tableView: UITableView, 
           didSelectRowAt indexPath: IndexPath) {
  if let delegate = delegate {
    let iconName = icons[indexPath.row]
    delegate.iconPicker(self, didPick: iconName)
  }
}
You can now give each list its own icon
Seu bud sir xebu eemm kifn ovz ezt onec

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
init(name: String, iconName: String = "No Icon") {
  self.name = name
  self.iconName = iconName
  super.init()
}
let checklist = Checklist(name: textField.text!, 
                      iconName: iconName)

Making 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.

Changing 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 buttons all use the same tint color
Gro kazwexc epm era npu diho pimk hojec

Changing the Global Tint color for the storyboard
Wtassaph sxu Bsetub Pojp buhec qar nge pfajztuayq

Setting the color of the checkmark

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

The tint color makes the app less plain looking
Jru gumt wocit nilaw ylo epw vity dpeir gienuxp

Adding 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.

The app icons in the asset catalog
Nko exk ebawk ed dqi akriq gilijuk

Setting 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!

Changing the launch screen file
Groxlomx wju wiuszb nqqeut yewu

The empty launch screen
Wfa awxhk yeacvv nvwioy

Testing on all iOS devices

The app should run without major problems on all current iOS devices, from the smallest (iPhone 8) 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!

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now