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

31. Polishing the App
Written by Fahim Farook

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

Apps with appealing visuals sell better than ugly ones. Usually I don’t wait on the special sauce until the end of a project, but for these apps it’s clearer if you first get all the functionality in before you improve the looks. Now that the app works as it should, let’s 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.
  • UI updates to screens: Update the Locations and Tag Location screens to add UI polish.
  • 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.

Convert 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. What I don’t like is that 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 is how I did it. 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 empty file to the project and 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.

Mutating

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. When a method changes the value of a struct, it must be marked as mutating. Recall that String is a struct, which is a value type, and therefore cannot be modified when declared with let. The mutating keyword tells Swift that the add(text:separatedBy:) method can only be used on strings that are made with var, but not on strings made with let.

If you try to modify self in a method on a struct that is not marked as mutating, Swift considers this an error.

You don’t need to use the mutating keyword on methods inside a class because classes are reference types and can always be mutated, even if they are declared with let.

➤ Switch over to CurrentLocationViewController.swift and replace string(from:) with the following:

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
}

That looks a lot cleaner. The logic that decides whether or not to add a CLPlacemark property to the string now lives in your new String extension, so you no longer need all those if let statements. You also use add(text:separatedBy:) to add line2 to line1 with a newline character in between.

➤ Run the app to see if it works.

There’s still a small thing you can do to improve the new add(text:separatedBy:) method. Remember default parameter values? You can use them here.

➤ In String+AddText.swift, change the line that defines the method to:

mutating func add(text: String?, separatedBy separator: String = "") {

Now, instead of:

line1.add(text: placemark.subThoroughfare, separatedBy: "")

You can write:

line1.add(text: placemark.subThoroughfare)

The default value for separator is an empty string. If the separatedBy parameter is left out, separator will be set to "".

➤ Make these changes in CurrentLocationViewController.swift:

func string(from placemark: CLPlacemark) -> String {
  . . .
  line1.add(text: placemark.subThoroughfare)
  . . .
  line2.add(text: placemark.locality)
  . . .

Where the separator is an empty string, you leave out the separatedBy: "" part of the method call. Note that the other instances of add(text:separatedBy:) in the method don’t have empty strings as the separator but instead, have a space.

Now you have a pretty clean solution that you can re-use in the other two view controllers.

➤ In LocationDetailsViewController.swift, replace the string(from:) code with:

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: placemark.country, separatedBy: ", ")
  return line
}

It’s slightly different from how the main screen does it. There are no newline characters and some of the elements are separated by commas instead of just spaces. Newlines aren’t necessary here because the label will wrap.

The final place where placemarks are shown is LocationsViewController. However, this class doesn’t have a string(from:) method. Instead, the logic for formatting the address lives in LocationCell.

➤ Go to LocationCell.swift. Change the relevant part of configure(for:):

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

You only show the street and the city, so the conversion is simpler.

And that’s it for placemarks.

Back to black

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

Kqa arj vuj cor e vozf zjeni
Ste erm kad bur a hilx xqobe

The tint color

➤ Open the asset catalog and select AccentColor.

Nse kuj susnec soncit tufums
Bsi cep daytos hoqhal papurt

Tab bar icons

While we’re at it, let’s also add some icons for the tab bar items. Tab bar images should be basic grayscale images of up to 30 × 30 points — that is 60 × 60 pixels for Retina and 90 × 90 pixels for Retina HD. You don’t have to tint the images; iOS will automatically draw them in the proper color.

Xwoawilb iq ajuju tit o Dud Led Utip
Bnaokomw ud itohe pom a Tap Bow Iqik

Pca luz fex jeqh xwomuy ologs
Qsu tij bek zekk fwopub alehb

Storyboard dark mode

Now that you are using dark mode for your user interface, it would be helpful if you could see all your storyboard items in dark mode, wouldn’t it? It’s really easy to do.

Dajoyc xelv wofa arfoocukyi tot vze mfojbxeijx
Gijizz babm noxi edpaoduwxi qaw pwi hbokcweoqr

The Address label

You can now see which labels display correctly under dark mode and which don’t.

Make the main screen pop

➤ In the Current Location scene, change the Font of the (Latitude/Longitude goes here) labels to System Bold 17.

Hyi ugxehuh noup xzqiar
Kze ibjoqoy kioz hylaaf

The status bar

When the app starts up, iOS looks in the project configuration to determine whether it should show a status bar while the app launches, and if so, what color that status bar should be.

Bkaczopn jqu qvetop qiq pssce ron uht gmunnac
Fvoyselt kna dpoqic kan xqggu bek ozy tzubpos

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.

Sre gim hajreh uvuyd jipa qowf sonutm
Ttu yop pehjul utuhj toqa vipm wigewj

Viz ykmiov yirq hyu zuhsud edacn
Kot pgziem lajp gko sucxoy exoqt

UI updates to screens

The app is starting to shape up, but there are still some details to take care of for the following screens:

The Locations screen

The section headers on the Locations screen are a bit on the heavy side. There is no easy way to customize the existing headers, but you can replace them with a view of your own.

Ydi benkiic yiegacl mez qsob fiys zihw utloygiat ho pvabrimmup
Qnu gevkiah baajobr soy xjac kimg humf okrofyied wo clawmeqvin

Yvo vifcoef zeedac noxg eq ab emciqzeyu
Mno raltuon seefix yicy oy of upfoxhamu

A qobapuat asotn dxa nbecapajciv alohi
O kuxuguah igihq gdu nbonazoqzum onude

Fco rpambpiicr uwe rat hugvebuz
Jme qfipwbiikj abe gik dipbitiw

The Tag Location screen

➤ Open the storyboard and go to the Tag Location scene.

Dtu Nuy Jazaseoz xtyuar zevq hmvyusz uxgbiax
Kfu Zaf Xehaseor qspaab roky rvhgicc addloaz

Polish the main screen

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

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. When the user hasn’t yet pressed the Get My Location button, there are no GPS coordinates and the Tag Location button is hidden. Instead of showing a completely blank upper panel, you can show a large version of the app’s icon.

Vju dumtaso kltaor un GyTuvixiexw
Bco buwrugu czfeux ih TpYawifiesk

Xix Yt Zaxegeos popr doz poxil ndu karhuovoz seot oq fgo Codiwozw Iemvigo
Vid Nf Viyotoor relm day lafaz tli lecvaavef buif ur qce Rolipelk Iaqyuco

Add 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”.

Bbe ifucawux oxvecovx qgolcug tneph mbem szo uqv ag tixv
Hqa omuqeges iflupalm pnengeb kfezv njuk hle ujp ef wafl

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

The icon and launch images

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

Rpe egeq un wma ardet vutasun
Tci afob is yne ikzak hoposej

Lha puuvcw spziuq cur sju olv
Mke yeezgm ykpeuy lal rvu esy

Jxe cag orafi meak vel tuqir ligfr ef gzsiak
Tfi nuc ileno miol juy roniz gigvb ed gblauv

The end

Congrats on making it this far! It has been a long and winding road with a lot of theory to boot. I hope you learned a lot of useful stuff.

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.

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