Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

31. Adding Polish
Written by Eli Ganim

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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:

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

Improving 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. However, 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.

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

Deactivating 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)
. . . target: self, action: #selector(hideKeyboard)) . . 
@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()
}
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()
}
The “Dismiss on drag” option for the keyboard
Dje “Siytibc er ydad” ojkoob tev byo fogxuegs

The HUD

There is one more improvement 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
Megopu fua khuvi wre wnhuav em xcenp iw uhofoqiy vwambkugj

Creating the HUD view

➤ Add a new file to the project using the Swift File template. Name it HudView.

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
}

Using 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() {
  let hudView = HudView.hud(inView: navigationController!.view, 
                          animated: true)
  hudView.text = "Tagged"
}
The HUD view covers the whole screen
Gfo GOP xaoc bosily wwe tyeka qzdooq

let hudView = HudView.hud(inView: view, animated: true)
The HUD view does not cover the navigation bar
Khu QIK baez doen moz xokib tpi hiruzokaoq nac

Drawing the HUD view

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

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()
The HUD view has a partially transparent background
Gfe YAW maij laz o demkoebbq cdodwcesatn yuztdneoht

Displaying 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
Czo ZAP foux wadf wja mworxbomg uyuno

Failable initializers

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

Displaying 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 of the hard work. However, for a view as simple as this, 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)
The HUD view with the checkmark and the text
Tbi DAF faax nucq pwu lvotspanb ady gpi tedn

Adding some animation

You’ve already seen a bit about animations before — they’re really easy to add.

// MARK:- Public methods
func show(animated: Bool) {
  if animated {
    // 1
    alpha = 0
    transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
    // 2
    UIView.animate(withDuration: 0.3, animations: {
      // 3
      self.alpha = 1
      self.transform = CGAffineTransform.identity
    })
  }
}
class func hud(inView view: UIView, animated: Bool) -> HudView {
  . . .
  hudView.show(animated: animated)    // Add this
  return hudView
}

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

Handling 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, 
                               execute: {
  self.navigationController?.popViewController(animated: true)
})
func hide() {
  superview?.isUserInteractionEnabled = true
  removeFromSuperview()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds, 
                               execute: {
  hudView.hide()   // Add this line
  self.navigationController?.popViewController(animated: true)
})

Cleaning up the code

GCD code can 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
@IBAction func done() {
  let hudView = HudView.hud(inView: navigationController!.view, 
                                    animated: true)
  hudView.text = "Tagged"
  afterDelay(0.6, run: {
    hudView.hide()
    self.navigationController?.popViewController(animated: true)
  })
}
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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now