Chapters

Hide chapters

UIKit Apprentice

Third Edition · iOS 18 · Swift 5.10 · Xcode 16

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

26. Adding Polish
Written by Fahim Farook

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

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

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

Unlock now

Your Tag Location screen is now functional but it looks a little basic and could do with some polish. It’s the small details that will make your apps a delight to use and stand out from the competition.

In this chapter, you will learn the following:

  • Improve the user experience: How to improve the user experience by adding tiny tweaks to your app which gives it some polish.
  • Add a HUD: How to add a HUD (Heads Up Display) to your app to provide a quick, animated status update.
  • Handle the navigation: How to continue the navigation flow after displaying the HUD.

Improve the user experience

Take a look at the design of the cell with the Description text view:

There is a margin between the text view and the cell border
There is a margin between the text view and the cell border

There is a margin between the text view and the cell border, but because the background of both the cell and the text view are white, the user cannot see where the text view begins or ends.

It is possible to tap on the cell but be just outside the text view area. That is annoying when you want to start typing: you think that you’re tapping in the text view, but the keyboard doesn’t appear.

There is no feedback to the user that they’re actually tapping outside the text view, and they will think your app is broken. In my opinion, deservedly so.

Keyboard activation for cells

You’ll have to make the app a little more forgiving. When the user taps anywhere inside that first cell, the text view should activate, even if the tap wasn’t on the text view itself.

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

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

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

Unlock now
// MARK: - Table View Delegates
override func tableView(
  _ tableView: UITableView, 
  willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
  if indexPath.section == 0 || indexPath.section == 1 {
    return indexPath
  } else {
    return nil
  }
}

override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  if indexPath.section == 0 && indexPath.row == 0 {
    descriptionTextView.becomeFirstResponder()
  }
}

Deactivate the keyboard

It would be nice if the keyboard disappeared after you tapped anywhere else on the screen. As it happens, that is not so hard to implement.

// Hide keyboard
let gestureRecognizer = UITapGestureRecognizer(
  target: self, 
  action: #selector(hideKeyboard))
gestureRecognizer.cancelsTouchesInView = false
tableView.addGestureRecognizer(gestureRecognizer)
. . . action: #selector(hideKeyboard)) . . .

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

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

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

Unlock now
@objc func hideKeyboard(
  _ gestureRecognizer: UIGestureRecognizer
) {
  let point = gestureRecognizer.location(in: tableView)
  let indexPath = tableView.indexPathForRow(at: point)

  if indexPath != nil && indexPath!.section == 0 && 
  indexPath!.row == 0 {
    return
  }
  descriptionTextView.resignFirstResponder()
}

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

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

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

Unlock now
if indexPath == nil || !(indexPath!.section == 0 && 
  indexPath!.row == 0) {
  descriptionTextView.resignFirstResponder()
}
if let indexPath = indexPath {
  if indexPath.section != 0 && indexPath.row != 0 {
    descriptionTextView.resignFirstResponder()
  }
} else {
  descriptionTextView.resignFirstResponder()
}

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

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

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

Unlock now

The HUD

There is one more improvement I wish to make to this screen, just to add a little spice. When you tap the Done button to close the screen, the app will show a quick animation to let you know it successfully saved the location:

Before you close the screen it shows an animated checkmark
Toqavo lua bdoyi rbi bdfuon ag bkukj em eqocimay vfagqfexd

Create the HUD view

➤ Add a new file to the project using the New Empty File option and name it HudView.

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

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

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

Unlock now
import UIKit

class HudView: UIView {
  var text = ""

  class func hud(
    inView view: UIView, 
    animated: Bool
  ) -> HudView {
    let hudView = HudView(frame: view.bounds)
    hudView.isOpaque = false

    view.addSubview(hudView)
    view.isUserInteractionEnabled = false

    hudView.backgroundColor = UIColor(
      red: 1, 
      green: 0, 
      blue: 0, 
      alpha: 0.5)
    return hudView
  }
}
let hudView = HudView()
let hudView = HudView.hud(inView: parentView, animated: true)
class func hud(inView view: UIView, animated: Bool) -> HudView {
  let hudView = HudView(frame: view.bounds)
  . . .
  return hudView
}

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

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

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

Unlock now

Use the HUD view

Let’s add the code to call this funky new HUD, so that you can see it in action.

@IBAction func done() {
  guard let mainView = navigationController?.parent?.view 
  	else { return }
  let hudView = HudView.hud(inView: mainView, animated: true)
  hudView.text = "Tagged"
}
var mainView: UIView
if let view = navigationController?.parent?.view {
  mainView = view
} else {
  return
}

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

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

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

Unlock now
The HUD view covers the whole screen
Gve LEK giah parefr dwo lqohu rrbeev

let hudView = HudView.hud(inView: view, animated: true)

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

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

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

Unlock now
The HUD view does not cover the full screen
Nzu ZIB puad guab fuf miweg byu vizl bkzeoq

Draw the HUD view

➤ Remove the backgroundColor line from the hud(inView:animated:) method.

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

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

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

Unlock now
override func draw(_ rect: CGRect) {
  let boxWidth: CGFloat = 96
  let boxHeight: CGFloat = 96

  let boxRect = CGRect(
    x: round((bounds.size.width - boxWidth) / 2),
    y: round((bounds.size.height - boxHeight) / 2),
    width: boxWidth,
    height: boxHeight)

  let roundedRect = UIBezierPath(
    roundedRect: boxRect, 
    cornerRadius: 10)
  UIColor(white: 0.3, alpha: 0.8).setFill()
  roundedRect.fill()
}
let boxWidth: CGFloat = 96
let boxHeight: CGFloat = 96
let boxRect = CGRect(
  x: round((bounds.size.width - boxWidth) / 2),
  y: round((bounds.size.height - boxHeight) / 2),
  width: boxWidth,
  height: boxHeight)
let roundedRect = UIBezierPath(roundedRect: boxRect, cornerRadius: 10)
UIColor(white: 0.3, alpha: 0.8).setFill()
roundedRect.fill()

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

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

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

Unlock now
The HUD view has a partially transparent background
Fhi RAL beuk mer e posvaevxk jzixpfolutp fabmlreaxr

Display the HUD checkmark

➤ The Resources folder for the book has two files in the Hud Images folder, Checkmark@2x.png and Checkmark@3x.png. Add these files to the asset catalog, Assets.xcassets.

// Draw checkmark
if let image = UIImage(named: "Checkmark") {
  let imagePoint = CGPoint(
    x: center.x - round(image.size.width / 2),
    y: center.y - round(image.size.height / 2) - boxHeight / 8)
  image.draw(at: imagePoint)
}
The HUD view with the checkmark image
Gxe GEZ voon kopq hno gcehsxukx epigu

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

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

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

Unlock now

Failable initializers

To create the UIImage you used if let to unwrap the resulting object. That’s because UIImage(named:) is a failable initializer.

guard let image = UIImage(named: "Checkmark") else { return }
let imagePoint = CGPoint(
  x: center.x - round(image.size.width / 2), 
  y: center.y - round(image.size.height / 2) - boxHeight / 8)
image.draw(at: imagePoint)

Display the HUD text

Usually, to display text in your own view, you’d add a UILabel object as a subview and let UILabel do all the hard work. However, for a view as simple as the HUD, you can also do your own text drawing.

// Draw the text
let attribs = [ 
    NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),
    NSAttributedString.Key.foregroundColor: UIColor.white
]

let textSize = text.size(withAttributes: attribs)

let textPoint = CGPoint(
  x: center.x - round(textSize.width / 2),
  y: center.y - round(textSize.height / 2) + boxHeight / 4)

text.draw(at: textPoint, withAttributes: attribs)

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

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

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

Unlock now
The HUD view with the checkmark and the text
Fri BEC poid buzd qyo lmoypjadp ixp kci gixg

Add some animation

OK, you’ve now got a rounded box with a checkmark, but it’s still far from spectacular. Time to liven it up a little with some animation!

// MARK: - Helper methods
func show(animated: Bool) {
  if animated {
    // 1
    alpha = 0
    transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
    // 2
    UIView.animate(withDuration: 0.3) {
      // 3
      self.alpha = 1
      self.transform = CGAffineTransform.identity
    }
  }
}

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

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

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

Unlock now
class func hud(inView view: UIView, animated: Bool) -> HudView {
  . . .
  hudView.show(animated: animated)    // Add this
  return hudView
}

Improve the animation

You can actually do one better. iOS has something called “spring” animations, which bounce up and down and are much more visually interesting than the plain old version of animations. Using them is very simple.

UIView.animate(
  withDuration: 0.3, 
  delay: 0, 
  usingSpringWithDamping: 0.7, 
  initialSpringVelocity: 0.5,
  options: [], 
  animations: {
    self.alpha = 1
    self.transform = CGAffineTransform.identity
  }, completion: nil) 

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

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

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

Unlock now

Handle the navigation

Back to LocationDetailsViewController… You still need to close the screen when the user taps Done.

let delayInSeconds = 0.6
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) {
  self.navigationController?.popViewController(animated: true)
}

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

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

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

Unlock now
func hide() {
  superview?.isUserInteractionEnabled = true
  removeFromSuperview()
}

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

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

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

Unlock now
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) {
  hudView.hide()   // Add this line
  self.navigationController?.popViewController(animated: true)
}

Clean up the code

I don’t know about you, but I find this GCD stuff to be a bit messy. So let’s clean up the code and make it easier to understand.

import Foundation

func afterDelay(_ seconds: Double, run: @escaping () -> Void) {
  DispatchQueue.main.asyncAfter(
    deadline: .now() + seconds, 
    execute: run)
}
(parameter list) -> return type

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

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

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

Unlock now
@IBAction func done() {
  ...
  hudView.text = "Tagged"
  afterDelay(0.6) {
    hudView.hide()
    self.navigationController?.popViewController(animated: true)
  }
}
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.
© 2025 Kodeco Inc.

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

Unlock now