Hide chapters

iOS Apprentice

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

36. Polishing the App
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.

Apps with appealing visuals sell better than ugly ones. Now that the app works as it should, it’s time to make it look good!

You’re going to go from this:

To this:

The main screen gets the biggest makeover, but you’ll also tweak the others a little.

You’ll do the following in this chapter:

  • Convert placemarks to strings: Refactor the code to display placemarks as text values so that the code is centralized and easier to use.
  • Back to black: Change the appearance of the app to have a black background and light text.
  • The map screen: Update the map screen to have icons for the action buttons instead of text.
  • Fix the table views: Update all the table views in the app to have black backgrounds with white text.
  • Polish the main screen: Update the appearance of the main screen to add a bit of awesome sauce!
  • Make some noise: Add sound effects to the app.
  • The icon and launch images: Add the app icon and launch images to complete the app.

Converting placemarks to strings

Let’s begin by improving the code. I’m not really happy with the way the reverse geocoded street address gets converted from a CLPlacemark object into a string. It works, but the code is unwieldy and repetitive.

There are three places where this happens:

  • CurrentLocationViewController, the main screen.
  • LocationDetailsViewController, the Tag/Edit Location screen.
  • LocationsViewController, the list of saved locations.

Let’s start with the main screen. CurrentLocationViewController.swift has a method named string(from:) where this conversion happens. It’s supposed to return a string that looks like this:

subThoroughfare thoroughfare
locality administrativeArea postalCode

This string goes into a UILabel that has room for two lines, so you use the \n character sequence to create a line-break between the thoroughfare and locality.

The problem is that any of these properties may be nil. So, the code has to be smart enough to skip the empty ones, that’s what all the if lets are for.

There’s a lot of repetition going on in this method. You can refactor this.

Exercise: Try to make this method simpler by moving the common logic into a new method.

Answer: Here’s a possible solution. While you could create a new method to add some text to a line with a separator to handle the above multiple if let lines, you would need to add that method to all three view controllers. Of course, you could add the method to the Functions.swift file to centralize the method too…

But better still, what if you created a new String extension since this functionality is for adding some text to an existing string? Sounds like a plan?

➤ Add a new file to the project using the Swift File template. Name it String+AddText.

➤ Add the following to String+AddText.swift:

extension String {
  mutating func add(text: String?, 
    separatedBy separator: String) {
    if let text = text {
      if !isEmpty {
        self += separator
      self += text

Most of the code should be pretty self-explanatory. You ask the string to add some text to itself, and if the string is currently not empty, you add the specified separator first before adding the new text.


Notice the mutating keyword. You haven’t seen this before. Sorry, it doesn’t have anything to do with X-Men — programming is certainly fun, but not that fun!

func string(from placemark: CLPlacemark) -> String {
  var line1 = ""
  line1.add(text: placemark.subThoroughfare, separatedBy: "")
  line1.add(text: placemark.thoroughfare, separatedBy: " ")

  var line2 = ""
  line2.add(text: placemark.locality, separatedBy: "")
  line2.add(text: placemark.administrativeArea, 
     separatedBy: " ")
  line2.add(text: placemark.postalCode, separatedBy: " ")

  line1.add(text: line2, separatedBy: "\n")
  return line1
mutating func add(text: String?, 
                  separatedBy separator: String = "") {
line1.add(text: placemark.subThoroughfare, separatedBy: "")
line1.add(text: placemark.subThoroughfare)
func string(from placemark: CLPlacemark) -> String {
  . . .
  line1.add(text: placemark.subThoroughfare)
  . . .
  line2.add(text: placemark.locality)
  . . .
func string(from placemark: CLPlacemark) -> String {
  var line = ""
  line.add(text: placemark.subThoroughfare)
  line.add(text: placemark.thoroughfare, separatedBy: " ")
  line.add(text: placemark.locality, separatedBy: ", ")
  line.add(text: placemark.administrativeArea, 
    separatedBy: ", ")
  line.add(text: placemark.postalCode, separatedBy: " ")
  line.add(text:, separatedBy: ", ")
  return line
func configure(for location: Location) {
  . . .
  if let placemark = location.placemark {
    var text = ""
    text.add(text: placemark.subThoroughfare)
    text.add(text: placemark.thoroughfare, separatedBy: " ")
    text.add(text: placemark.locality, separatedBy: ", ")
    addressLabel.text = text
  } else {
    . . .

Back to black

Right now the app looks like a typical iOS app: lots of white, gray tab bar, blue tint color. Time to go for a radically different look and paint the whole thing black.

The new yellow-on-black design
Vci qid kepcoz-ul-gsuwx xinocs

Using UIAppearance

When customizing the UI, you can customize your app on a per-control basis, as you’ve done up to this point, or you can use the “appearance proxy” to change the look of all of the controls of a particular type at once. That’s what you’re going to do here.

func customizeAppearance() {
  UINavigationBar.appearance().barTintColor =
  UINavigationBar.appearance().titleTextAttributes = [ 
    UIColor.white ]
  UITabBar.appearance().barTintColor =
  let tintColor = UIColor(red: 255/255.0, green: 238/255.0, 
                         blue: 136/255.0, alpha: 1.0)
  UITabBar.appearance().tintColor = tintColor
func application(_ application: UIApplication, 
                 didFinishLaunchingWithOptions . . .) -> Bool {
  . . .
The tab bar is now nearly black and has yellow icons
Xni ciy pun oy lub xauymb zjilq oss dok xuqpup eqaqx

The navigation and tab bars appear in a dark color
Cji hupuluvuov ikk fet zoky idweoc ok a gozb zeboz

Tab bar icons

The icons in the tab bar could also do with some improvement. The Xcode Tabbed Application template put a bunch of cruft in the app that you’re no longer using — let’s get rid of it all.

Choosing an image for a Tab Bar Item
Mxoewaxt ix acuhe hij i Hel Vak Imic

The tab bar with proper icons
Jhe voq leh yabm tsuzen ejowv

The status bar

The status bar is currently invisible on the Tag screen and appears as black text on dark gray on the other two screens. It would look better if the status bar text was white instead.

import UIKit

class MyTabBarController: UITabBarController {
  override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
  override var childForStatusBarStyle: UIViewController? {
    return nil
The status bar is visible again
Zpe ttiqag rok el wudefte udeub

import UIKit

class MyImagePickerController: UIImagePickerController {
  override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
let imagePicker = MyImagePickerController()
imagePicker.view.tintColor = view.tintColor
The photo picker with the new colors
Qge rheli sigzeh yesh wlo bib rohomg

Changing the status bar style for app startup
Bmumsezb mxi vqimop lod rdzhi sic ebl yxuptel

The map screen

The Map screen currently has a somewhat busy navigation bar with three pieces of text in it: the title and the two buttons.

The bar button items have text labels
Tme cec peqpax afecl meto fenm geleps

Map screen with the button icons
Dod tvxaek tebv xyo bidyiv iyixy

pinView.tintColor = UIColor(white: 0.0, alpha: 0.5)
The callout button is now easier to see
Xdi gexfius fodbim un suf oezuew ca yoi

Fixing the table views

The app is starting to shape up, but there are still some details to take care of. The table views, for example, are still very white.

Storyboard changes for the Locations scene

➤ Open the storyboard and select the table view for the Locations scene. Set Table View - Separator color to white with 20% Opacity, Scroll View - Indicators to white, and View - Background to black.

Table view color changes
Yekqi boog dikar mmidrit

The table view cells are now white-on-black
Gmi sonlu luet xafvx ilu rox sniqa-il-sduvt

Code changes for the Locations view

The first, when you tap a cell it still lights up in a bright color, which is a little jarring. It would look better if the selection color was more subdued.

override func awakeFromNib() {
  let selection = UIView(frame:
  selection.backgroundColor = UIColor(white: 1.0, alpha: 0.3)
  selectedBackgroundView = selection
The selected cell has a subtly different background color
Yhu qinantac vitj boy e zawmlx lohdapemj cafyyziamn neyup

override func tableView(_ tableView: UITableView, 
     viewForHeaderInSection section: Int) -> UIView? {

  let labelRect = CGRect(x: 15, 
                         y: tableView.sectionHeaderHeight - 14, 
                         width: 300, height: 14)
  let label = UILabel(frame: labelRect)
  label.font = UIFont.boldSystemFont(ofSize: 11)
  label.text = tableView.dataSource!.tableView!(
                 tableView, titleForHeaderInSection: section)
  label.textColor = UIColor(white: 1.0, alpha: 0.6)
  label.backgroundColor = UIColor.clear
  let separatorRect = CGRect(
          x: 15, y: tableView.sectionHeaderHeight - 0.5, 
          width: tableView.bounds.size.width - 15, height: 0.5)
  let separator = UIView(frame: separatorRect)
  separator.backgroundColor = tableView.separatorColor
  let viewRect = CGRect(x: 0, y: 0, 
                    width: tableView.bounds.size.width, 
                   height: tableView.sectionHeaderHeight)
  let view = UIView(frame: viewRect)
  view.backgroundColor = UIColor(white: 0, alpha: 0.85)
  return view
The section headers now draw much less attention to themselves
Ggo zolxies ceixiqc dud xmah birx qull urpiqniec ye hsughardoj

override func tableView(_ tableView: UITableView, 
    titleForHeaderInSection section: Int) -> String? {
  let sectionInfo = fetchedResultsController.sections![section]
The section header text is in uppercase
Smu gilneab zaemov dotf ec iz ecwolmebi

return UIImage(named: "No Photo")!
A location using the placeholder image
O behoxouv akifx nwa jyanuxizjiz agoze

// Rounded corners for images
photoImageView.layer.cornerRadius = 
                     photoImageView.bounds.size.width / 2
photoImageView.clipsToBounds = true
separatorInset = UIEdgeInsets(top: 0, left: 82, bottom: 0, 
                                                 right: 0)
The thumbnails are now circular
Hva msupfduamp eki nir xewsupen

descriptionLabel.backgroundColor = UIColor.purple
addressLabel.backgroundColor = UIColor.purple
The labels resize to fit the iPhone 8 Plus
Yma qelumv zuxula me guw wru uVbopo 7 Bkos

Table view changes for Tag Location screen

➤ Open the storyboard and select the table view for the Tag Location scene. Set Table View - Separator color to white with 20% Opacity, Scroll View - Indicators to white, and View - Background to black.

override func tableView(_ tableView: UITableView, 
                   willDisplay cell: UITableViewCell, 
                 forRowAt indexPath: IndexPath) {
  let selection = UIView(frame:
  selection.backgroundColor = UIColor(white: 1.0, alpha: 0.3)
  cell.selectedBackgroundView = selection
The Tag Location screen with styling applied
Jja Dib Gomeceam kvtaic kizv cmtlafp etpkauw

Table view changes for the Category Picker screen

The final table view is the category picker. There’s nothing new here, the changes are basically the same as before.

override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> 
             UITableViewCell {
  . . .
  let selection = UIView(frame:
  selection.backgroundColor = UIColor(white: 1.0, alpha: 0.3)
  cell.selectedBackgroundView = selection
  // End new code
  return cell
The category picker is lookin’ sharp
Yke rosuwexh kammum oc qoejuy’ pmots

Polishing the main screen

I’m pretty happy with all the other screens, but the main screen needs a bit more work to be presentable.

@IBOutlet weak var latitudeTextLabel: UILabel!
@IBOutlet weak var longitudeTextLabel: UILabel!
func updateLabels() {
  if let location = location {
    . . .
    latitudeTextLabel.isHidden = false
    longitudeTextLabel.isHidden = false
  } else {
    . . .
    latitudeTextLabel.isHidden = true
    longitudeTextLabel.isHidden = true

The first impression

The main screen looks decent and is completely functional, but it could do with more pizzazz. It lacks the “Wow!” factor. You want to impress users the first time they start your app and keep them coming back. To pull this off, you’ll add a logo and a cool animation.

The welcome screen of MyLocations
Tto lodbera sbyeuv ar ZdZamituims

Get My Location must sit below the container view in the Document Outline
Don Ll Bosimiux dowm cad gavof npe deqpoital foak ew mfa Yumusobs Eehpeja

@IBOutlet weak var containerView: UIView!
var logoVisible = false

lazy var logoButton: UIButton = {
  let button = UIButton(type: .custom)
  button.setBackgroundImage(UIImage(named: "Logo"), 
                            for: .normal)
  button.addTarget(self, action: #selector(getLocation), 
                   for: .touchUpInside) = self.view.bounds.midX = 220
  return button
func showLogoView() {
  if !logoVisible {
    logoVisible = true
    containerView.isHidden = true
statusMessage = "Tap ’Get My Location’ to Start"
statusMessage = ""
func hideLogoView() {
  logoVisible = false
  containerView.isHidden = false
if logoVisible {
class CurrentLocationViewController: UIViewController, 
              CLLocationManagerDelegate, CAAnimationDelegate {
func hideLogoView() {
  if !logoVisible { return }
  logoVisible = false
  containerView.isHidden = false = view.bounds.size.width * 2 = 40 + 
     containerView.bounds.size.height / 2
  let centerX = view.bounds.midX
  let panelMover = CABasicAnimation(keyPath: "position")
  panelMover.isRemovedOnCompletion = false
  panelMover.fillMode = CAMediaTimingFillMode.forwards
  panelMover.duration = 0.6
  panelMover.fromValue = NSValue(cgPoint:
  panelMover.toValue = NSValue(cgPoint: 
       CGPoint(x: centerX, y:
  panelMover.timingFunction = CAMediaTimingFunction(
                name: CAMediaTimingFunctionName.easeOut)
  panelMover.delegate = self
  containerView.layer.add(panelMover, forKey: "panelMover")
  let logoMover = CABasicAnimation(keyPath: "position")
  logoMover.isRemovedOnCompletion = false
  logoMover.fillMode = CAMediaTimingFillMode.forwards
  logoMover.duration = 0.5
  logoMover.fromValue = NSValue(cgPoint:
  logoMover.toValue = NSValue(cgPoint:
      CGPoint(x: -centerX, y:
  logoMover.timingFunction = CAMediaTimingFunction(
                 name: CAMediaTimingFunctionName.easeIn)
  logoButton.layer.add(logoMover, forKey: "logoMover")
  let logoRotator = CABasicAnimation(keyPath: 
  logoRotator.isRemovedOnCompletion = false
  logoRotator.fillMode = CAMediaTimingFillMode.forwards
  logoRotator.duration = 0.5
  logoRotator.fromValue = 0.0
  logoRotator.toValue = -2 * Double.pi
  logoRotator.timingFunction = CAMediaTimingFunction(
                  name: CAMediaTimingFunctionName.easeIn)
  logoButton.layer.add(logoRotator, forKey: "logoRotator")
// MARK:- Animation Delegate Methods
func animationDidStop(_ anim: CAAnimation, 
               finished flag: Bool) {
  containerView.layer.removeAllAnimations() = view.bounds.size.width / 2 = 40 + 
                containerView.bounds.size.height / 2

Adding an activity indicator

When the user taps the Get My Location button, you currently change the button’s text to say Stop to indicate the change of state. You can make it even clearer to the user that something is going on by adding an animated activity “spinner.”

The animated activity spinner shows that the app is busy
Syi ehunumod odyirogq qbejluj bxezh nsor pqo ekd ep lucd

func configureGetButton() {
  let spinnerTag = 1000
  if updatingLocation {
    getButton.setTitle("Stop", for: .normal)
    if view.viewWithTag(spinnerTag) == nil {
      let spinner = UIActivityIndicatorView(style: .white) = += spinner.bounds.size.height/2 + 25
      spinner.tag = spinnerTag
  } else {
    getButton.setTitle("Get My Location", for: .normal)
    if let spinner = view.viewWithTag(spinnerTag) {

Making some noise

Visual feedback is important, but you can’t expect users to keep their eyes glued to the screen all the time, especially if an operation might take a few seconds or more.

import AudioToolbox
var soundID: SystemSoundID = 0
// MARK:- Sound effects
func loadSoundEffect(_ name: String) {
  if let path = Bundle.main.path(forResource: name, 
                                      ofType: nil) {
    let fileURL = URL(fileURLWithPath: path, isDirectory: false)
    let error = AudioServicesCreateSystemSoundID(
                      fileURL as CFURL, &soundID)
    if error != kAudioServicesNoError {
      print("Error code \(error) loading sound: \(path)")

func unloadSoundEffect() {
  soundID = 0

func playSoundEffect() {
if error == nil, let p = placemarks, !p.isEmpty {
  // New code block
  if self.placemark == nil {               
    print("FIRST TIME!")
  // End new code
  self.placemark = p.last!
} else {
  . . .

The icon and launch images

The Resources folder for this app contains an Icon folder with the app icons.

The icons in the asset catalog
Pri usitc ol jja uxqaj kebidub

Using the asset catalog for launch images
Osoqc ysa ifbuf kaqevot top saonyb unumef

Enabling the launch images for iPhone portrait
Iwixjawg sna qiicyg esijiv xem iLgoni fukvdooq

The launch image for this app
Yku soivmr axuyo yoz jsad oth

Where to go from here?

In this section you took a more detailed look at Swift, but there’s still plenty to discover. To learn more about the Swift programming language, you can read the following books:

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.
© 2023 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 Professional subscription.

Unlock now