Chapters

Hide chapters

UIKit Apprentice

First Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

33. Custom Table Cells
Written by Matthijs Hollemans & Fahim Farook

Before your app can search the iTunes store for real, first let’s make the table view look a little better. Appearance does matter when it comes to apps!

Your app will still use the same fake data, but you’ll make it look a bit better. This is what you’ll have by the end of this chapter:

The app with better looks
The app with better looks

In the process, you will learn the following:

  • Custom table cells and nibs: How to create, configure and use a custom table cell via nib file.
  • Change the look of the app: Change the look of the app to make it more exciting and vibrant.
  • Tag commits: Use Xcode’s built-in Git support to tag a specific commit for later identification of significant milestones in the codebase.
  • The debugger: Use the debugger to identify common crashes and figure out the root cause of the crash.

Custom table cells and nibs

For the previous apps, you used prototype cells to create your own table view cell layouts. That works great, but there’s another way. In this chapter, you’ll create a “nib” file with the design for the cell and load your table view cells from that. The principle is very similar to prototype cells.

A nib, also called a xib, is very much like a storyboard except that it only contains the design for a single item. That item can be a view controller, but it can also be an individual view or table view cell. A nib is really nothing more than a container for a “freeze dried” object that you can edit in Interface Builder.

In practice, many apps consist of a combination of nibs and storyboard files, so it’s good to know how to work with both.

Add a nib file

➤ Add a new file to the project. Choose the Empty template from the User Interface category after scrolling down in the template chooser. This will create a new empty nib.

Adding an empty nib to the project
Adding an empty nib to the project

➤ Click Next and save the new file as SearchResultCell.

Open SearchResultCell.xib and you will see an empty canvas.

Xib or nib?

I’ve been calling it a nib but the file extension is .xib. So what is the difference? In practice, these terms are used interchangeably. Technically speaking, a xib file is compiled into a nib file that is put into your application bundle. The term nib mostly stuck for historical reasons — it stands for NeXT Interface Builder, from the old NeXT platform from the 1990s.

You can consider the terms “xib file” and “nib file” to be equivalent. The preferred term seems to be nib, so that is what I will be using from now on. This won’t be the last time computer terminology is confusing, ambiguous or inconsistent. The world of programming is full of jargon.

➤ Use the View as: panel to switch to iPhone SE (2n Generation) and Dark appearance. As usual, we’ll design for the smaller device but use Auto Layout to make the user interface adapt to larger devices/screens.

➤ From the Objects Library, drag a new Table View Cell on to the canvas.

➤ Select the new Table View Cell and go to the Size inspector. Type 80 in the Height field (not Row Height). Make sure Width is 375, the width of the iPhone SE screen.

➤ Drag an Image View and two Labels into the cell so that the cell now looks like this:

The design of the cell
The design of the cell

Note: If you get blue rectangles around each item like above — or would like to get the rectangles to see the full bounds of each item — then use the Editor ▸ Canvas ▸ Bounds Rectangles menu item to toggle the bounds rectangles on/off.

➤ Position the Image View at X:16, Y:10, Width:60, Height:60.

➤ Set the Text of the first label to Name, Font to System 18, X:84, Y:16, Width:275, Height:22.

➤ Set the Text for the second label to Artist Name, Font to System 15, X:84, Y:44, Width:275, Height:18.

➤ The Table View Cell itself needs to have a reuse identifier. You can set this in the Attributes inspector to SearchResultCell.

As you can see, editing a nib is just like editing a storyboard. The difference is that the canvas is a lot smaller because you’re only editing a single table view cell, not an entire view controller.

Set up Auto Layout constraints

The design for the cell is only 375 points wide but there are iOS devices with screens wider than that. The cell itself will resize to accommodate those larger screens, but the labels won’t, potentially causing their text to be cut off. You’ll have to add some Auto Layout constraints to make the labels resize along with the cell.

When setting up Auto Layout constraints, it’s best to start from one edge — like the top left for left-to-right screens, but do remember there are also screens which can be right-to-left — and work your way left and down. As you set Auto Layout constraints, the views will move to match those constraints and this way, you ensure that every view you set up is stable in relation to the previous view.

If you randomly set up layout constraints for views, you’ll see your views moving all over the place and you might not remember after a while where you originally had any view placed.

➤ Select the Image View and open the Add New Constraints menu. Uncheck Constrain to margins and pin the Image View so that top=10 and left=16. Also set Width=60 and Height=60 so that the image is always fixed at 60 by 60 points.

➤ Select the Name label and again use the Add New Constraints menu. Uncheck Constrain to margins and set top=16, left=8, and right=16.

➤ Pin the Artist Name label so that left=8, top=8, right=16 and bottom=18 — again without constraining to margins.

➤ Finally, select the Name label and set its Vertical Compression Resistance Priority to 751 in the Size Inspector.

The last setting is to let Auto Layout figure out which label has priority in displaying its content if both Name and Artist Name labels had too much content to fit within the available space. Here, we declared that the item name has higher priority.

Symbol images

The image view will hold the artwork for the found item, such as an album cover, book cover, or an app icon. It may take a few seconds for these images to be loaded, so until then, it’s a good idea to show a placeholder image.

Till now we’ve added images to the Asset Catalog from the bundled resources and used those images. However, there is another way.

➤ Select the Image View. In the Attributes inspector, type “square” into the Image field. You should get a dropdown with a list of suggestions:

List of image suggestions
List of image suggestions

You haven’t added any images to the project yet! So where did these images come from?

Xcode comes bundled with what are known as Symbol Images. These are vector images which can adapt to different sizes and different contexts depending on what’s required. For example, while the images listed are all black and white, you can actually set a color of your preference to each image if you wanted. Is that nifty, or what?

Apple has a handy SF Symbols app — which you can download from the design resources page at developer.apple.com — which allows you to browse all the available symbol images and search for the exact one that would suit your requirements. Once you find the right image, you can simply use it’s name in the Interface Builder as we did above to set that image to be displayed in your app.

➤ Select the square image — the first one shown in the image above — from the dropdown.

You should now have a blue square displaying as your image:

The cell now uses a symbol image
The cell now uses a symbol image

Wait a minute! Blue? We don’t want blue!

Easy enough to fix, actually.

➤ Select a nice grey color from the Attributes Inspector’s View - Tint dropdown – something like System Gray Color which displays fine in both Light and Dark modes.

Of course, if you prefer, you can set a different color too – totally your call. But do test the color out in both Light and Dark modes to make sure that it displays fine in all appearance modes.

What’s that? You think the gray stroke is too wide? No problem!

➤ Select Thin (or Ultra Light) from the Symbol Configuration - Weight dropdown in the Attributes Inspector.

The cell design should now look like this:

The cell design with placeholder symbol image
The cell design with placeholder symbol image

Looking good, right? Again, don’t forget to test for Light appearance if you’re designing for Dark or vice versa.

Setting colors

There’s one more change to make – the text color for the Artist Name label. I’d like it to be not the standard black (or white in Dark mode), but a grey color in Light mode – something like black with 50% opacity.

But that won’t look good against the black background in Dark mode. Try it and see.

What do we do?

We could, of course, use our (new) old friend System Gray Color since we know that it displays fine in any appearance. But what if we wanted our own custom colors for each appearance?

There’s another way that you briefly learnt about in the last app. Yes, color assets!

➤ Open the Asset Catalog, Assets.xcassets, and click the + button at the bottom of the Xcode window and select Color Set.

This adds a new named color asset to the asset catalog.

➤ Name the color ArtistName.

Notice that the asset has two color values – Any Appearance and Dark Appearance. The Any Appearance color value would be used for both Light appearance and for devices where there is no appearance support.

If you wanted to differentiate colors even further and have a special color for Any Appearance and a separate color for Light Appearance, then you can use the Appearance dropdown in the Attributes Inspector to select “Any, Light, Dark” to get three options for your color value.

We are fine with just the two default options we got.

➤ Select Any Appearance and change the Input Method dropdown (under the Color section) in the Attributes Inspector to 8-bit (0-255).

This gives you the color inputs that you’re used to from the previous apps where you specify the red, green, and blue values as an integer in the 0-255 range. If you prefer using the Color Panel, you can also tap the Show Color Panel button below the inputs to select your color directly via the Color Panel.

➤ Set the color values to red=0, green=0, blue=0, opacity=50 to get the black with 50% opacity color that we wanted.

➤ Next, open SearchResultCell.xib, select the Artist Name label, and open the Label - Color dropdown. There’s a section in the dropdown called Named Colors and it should have the ArtistName asset that you added just now. Select it.

The Artist Name label now has the color asset that you set up. Switch between the Light and Dark appearances and make sure that the label displays correctly for all appearances.

You’ll notice that under Dark appearance, the Artist Name still displays as white text.

Exercise: We didn’t set up a color for the Dark appearance, so how did the Artist Name get to white text under Dark appearance?

Answer: The ArtistName color asset had a default value set for both Any Appearance and Dark Appearance. That default value was white. That’s why we still get white text under Dark appearance.

So, let’s change the color for Dark Appearance.

➤ Select the asset catalog, select the ArtistName color, and then select the Dark Appearance option. Set the color values to red=255, green=255, blue=255, opacity=50 to set the color to white with 50% opacity.

➤ Switch back to SearchResultCell.xib and check how the Artist Name label displays under all appearance options.

That looks more like it, right?

That concludes the design for this cell. Now you have to tell the app to use this nib.

Register nib file for use in code

➤ In SearchViewController.swift, add these lines to the end of viewDidLoad():

let cellNib = UINib(nibName: "SearchResultCell", bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: "SearchResultCell")

The UINib class is used to load nibs. Here, you tell it to load the nib you just created — note that you don’t specify the .xib file extension. Then you ask the table view to register this nib for the reuse identifier “SearchResultCell”.

From now on, when you call dequeueReusableCell(withIdentifier:) for the identifier “SearchResultCell”, UITableView will automatically make a new cell from the nib — or reuse an existing cell if one is available, of course. And that’s all you need to do.

➤ In tableView(_:cellForRowAt:) change this bit of code:

let cellIdentifier = "SearchResultCell"

var cell: UITableViewCell! = tableView.dequeueReusableCell(
  withIdentifier: cellIdentifier)
if cell == nil {
  cell = UITableViewCell(
    style: .subtitle, reuseIdentifier: cellIdentifier)
}

So that the final method looks like this:

func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  let cellIdentifier = "SearchResultCell"
  let cell = tableView.dequeueReusableCell(  // Change this line
    withIdentifier: cellIdentifier, 
    for: indexPath)        
  if searchResults.count == 0 {
    . . .
  } else {
    . . .
  }
  return cell
}

You were able to replace a chunk of code with just one statement. Now, it’s almost exactly like using prototype cells, except that you have to create your own nib object and you need to register it with the table view beforehand.

Note: The call to dequeueReusableCell(withIdentifier:) now takes a second parameter, for:, that takes an IndexPath value. This variant of the dequeue method lets the table view be a bit smarter, but it only works when you have registered a nib with the table view — or when you use a prototype cell.

➤ Run the app and do a (fake) search. Yikes, the app crashes.

Exercise. Any ideas why?

Answer: Because you made your own custom cell design, you cannot use the textLabel and detailTextLabel properties of UITableViewCell.

Every table view cell — even a custom cell that you load from a nib — has a few labels and an image view of its own, but you should only employ these when you’re using one of the standard cell styles: .default, .subtitle, etc. If you use them on custom cells, then these built-in labels get in the way of your own labels.

In this case, you shouldn’t use textLabel and detailTextLabel to put text into the cell — you need to make your own properties for your labels.

Where do you put these properties? In a new class, of course. You’re going to make a new class named SearchResultCell which extends UITableViewCell and has properties — and logic — for displaying the search results in this app.

Add a custom UITableViewCell subclass

➤ Add a new file to the project using the Cocoa Touch Class template. Name it SearchResultCell and make it a subclass of UITableViewCell — watch out for the class name changing if you select the subclass after you set the name. “Also create XIB file” should be unchecked as you already have one.

This creates the Swift file to accompany the nib file you created earlier.

➤ Open SearchResultCell.xib and select the Table View Cell — make sure you select the actual Table View Cell object, not its Content View.

➤ In the Identity inspector, change its class from “UITableViewCell” to SearchResultCell.

You do this to tell the nib that the top-level view object it contains is no longer a UITableViewCell but your own SearchResultCell subclass. From now on, whenever you call dequeueReusableCell(), the table view will return an object of type SearchResultCell.

➤ Add the following outlet properties to SearchResultCell.swift:

@IBOutlet var nameLabel: UILabel!
@IBOutlet var artistNameLabel: UILabel!
@IBOutlet var artworkImageView: UIImageView!

➤ Hook these outlets up to the respective labels and image view in the nib. It is easiest to do this from the Connections inspector for SearchResultCell:

Connect the labels and image view to Search Result Cell
Connect the labels and image view to Search Result Cell

You can also open the Assistant editor and Control-drag from the labels and image view to their respective outlet definitions. If you’ve used nib files before you might be tempted to connect the outlets to File’s Owner but that won’t work in this case; they must be connected to the table view cell.

Now that this is all set up, you can tell the SearchViewController to use these new SearchResultCell objects.

Use custom table view cell in app

➤ In SearchViewController.swift, change cellForRowAt to:

func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  let cellIdentifier = "SearchResultCell"
  let cell = tableView.dequeueReusableCell(
    withIdentifier: cellIdentifier, 
    for: indexPath) as! SearchResultCell                  // Change this
  if searchResults.count == 0 {
    cell.nameLabel.text = "(Nothing found)"               // Change this
    cell.artistNameLabel.text = ""                        // Change this
  } else {
    let searchResult = searchResults[indexPath.row]
    cell.nameLabel.text = searchResult.name               // Change this
    cell.artistNameLabel.text = searchResult.artistName   // Change this
  }
  return cell
}

Notice the first changed line. Previously this returned a UITableViewCell object, but now that you’ve changed the class name in the nib, you’re guaranteed to always receive a SearchResultCell — you still need to cast it with as!, though.

Given that cell, you can put the name and artist name from the search result into the proper labels. You’re now using the cell’s nameLabel and artistNameLabel outlets instead of textLabel and detailTextLabel. You also no longer need to write ! to unwrap because the outlets are implicitly unwrapped optionals.

➤ Run the app and it should look something like this:

Much better!
Much better!

Use a constant for table cell identifier

Have you noticed that you’ve been using the string literal “SearchResultCell” in a few different places? It’s generally better to create a single constant for such occasions.

Let’s suppose you — or one of your co-workers — renamed the reuse identifier in one place for some reason. Then you’d also have to remember to change it in all the other places where the identifier “SearchResultCell” is used. It’s better to limit those changes to one single spot by using a symbolic name instead.

➤ Add the following to SearchViewController.swift, somewhere within the class definition:

struct TableView {
  struct CellIdentifiers {
    static let searchResultCell = "SearchResultCell"
  }
}

This defines a new struct, TableView, containing a secondary struct named CellIdentifiers which contains a constant named searchResultCell with the value “SearchResultCell”.

Should you want to change this value, then you only have to do it here and any code that uses TableView.CellIdentifiers.searchResultCell will be automatically updated.

There is another reason for using a symbolic name rather than the actual value: it gives extra meaning. Just seeing the text “SearchResultCell” says less about its intended purpose than the symbol TableView.CellIdentifiers.searchResultCell.

Note: Putting symbolic constants as static let members inside a struct — or a series of structs — is a common trick in Swift. A static value can be used without an instance so you don’t need to instantiate TableView.CellIdentifiers before you can use it — like you would need to do with a class.

It’s allowed in Swift to place a struct inside a class, which permits different classes to all have their own TableView.CellIdentifier structs. This wouldn’t work if you placed the struct outside the class — then you’d have multiple structs with the same name in the global namespace, which is not allowed.

➤ In SearchViewController.swift, search for and replace the string “SearchResultCell” with TableView.CellIdentifiers.searchResultCell.

You’d have to do this in viewDidLoad() and in tableView(_:cellForRowAt:) — in the latter, you would have to remove the local constant and replace that instead with the new value.

➤ Run the app to make sure everything still works.

A new “No results” cell

Remember our friend Justin Bieber? Searching for him now results in this:

The Nothing Found label now looks like this
The Nothing Found label now looks like this

That’s not very pretty — not to mention slightly off. It would be nicer if you gave this its own look. That’s not too hard: you can simply make another nib for it.

➤ Add another nib file to the project. Again this will be an Empty nib. Name it NothingFoundCell.xib.

➤ Drag a new Table View Cell on to the canvas. Set its Width to 375, its Height to 80 and give it the reuse identifier NothingFoundCell.

➤ Drag a Label into the cell and give it the text Nothing Found. Set the text color to the ArtistName color asset and the font to System 15.

➤ Use Editor ▸ Size to Fit Content to make the label fit the text exactly — you may have to deselect and select the label again to enable the menu option.

➤ Choose Horizontally in Container and Vertically in Container from the Auto Layout Align popup.

The cell should now look like this:

The Nothing Found cell
The Nothing Found cell

One more thing to fix. Remember that in willSelectRowAt you return nil if there are no search results to prevent the row from being selected? Well, if you are persistent enough you can still make the row appear gray as if it were selected.

For some reason, UIKit draws the selected background if you press down on the cell for long enough, even though this doesn’t count as a real selection. To prevent this, you have to tell the cell not to use a selection color.

➤ Select the cell itself. In the Attributes inspector, set Selection to None. Now tapping or holding down on the Nothing Found row will no longer show any sort of selection.

You don’t have to make a UITableViewCell subclass for this cell because there is no text to change or properties to set. All you need to do is register this nib with the table view.

➤ Add a new reuse identifier to the struct in SearchViewController.swift:

struct TableView {
    struct CellIdentifiers {
      static let searchResultCell = "SearchResultCell"
      static let nothingFoundCell = "NothingFoundCell"    // New
    }
}

➤ Add these lines to viewDidLoad(), below the other code registering the nib:

cellNib = UINib(nibName: TableView.CellIdentifiers.nothingFoundCell, bundle: nil)
tableView.register(
  cellNib, 
  forCellReuseIdentifier: TableView.CellIdentifiers.nothingFoundCell)

This also requires you to change let cellNib two lines up to var because you’re re-using the cellNib local variable.

➤ And finally, change tableView(_:cellForRowAt:) to:

func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  if searchResults.count == 0 {
    return tableView.dequeueReusableCell(
      withIdentifier: TableView.CellIdentifiers.nothingFoundCell, 
      for: indexPath)
  } else {
    let cell = tableView.dequeueReusableCell(withIdentifier:
      TableView.CellIdentifiers.searchResultCell, 
      for: indexPath) as! SearchResultCell

    let searchResult = searchResults[indexPath.row]
    cell.nameLabel.text = searchResult.name
    cell.artistNameLabel.text = searchResult.artistName
    return cell
  }
}

The logic here has been restructured a little. You only make a SearchResultCell if there are actually any results. If the array is empty, you’ll simply dequeue the cell for the nothingFoundCell identifier and return it since there is nothing to configure for that cell.

➤ Run the app. The search results for Justin Bieber now look like this:

The new Nothing Found cell in action
The new Nothing Found cell in action

Also try it out on larger screen devices. The label should always be centered in the cell.

Sweet. It has been a while since your last commit, so this seems like a good time to secure your work.

Source Control Changes

But before you commit your changes, take a look at SearchViewController.swift in your editor view. You might notice some blue lines along the gutter like this:

Source control change indicator in editor view
Source control change indicator in editor view

Whatever could those blue lines mean?

Those blue lines appear in projects which have source control enabled and they indicate the changes made by the developer since the last commit.

But it goes beyond that, if you work with other developers and somebody else made a change to the file you are working on and committed their change to Git, Xcode will even show these pending changes so that you are aware of changes made by somebody else that might impact the work you’re doing. Very handy!

➤ Commit the changes to the repository. I used the message “Use custom cells for search results.”

Change the look of the app

The app currently looks a little bland. Let’s cheer it up a little by giving it more vibrant colors.

To start with, let’s change the search bar color and the tint color used throughout the app. And since we want to support Light and Dark appearances, let’s set these colors via color assets.

To start with, we’ll set just a single color for both appearances and then once we know how that color looks, we can change to suit a particular appearance, if we need to.

➤ Open the Asset Catalog and select the AccentColor which is added to it by default. As you learnt with the previous app, this color can be used to specify the global tint color for your whole app.

The AccentColor currently has no color value set. You can tell by looking at the Color - Content setting in the Attributes Inspector because it is set to “None”.

➤ Change Color - Content to sRGB, and the panel below will get new controls. Set Input Method to 8-bit (0-255) and then set the color to red=10, green=80, blue=80.

This sets the global tint to a dark green.

➤ Click the + button at the bottom of the window and select Color Set to create a new color set. Name it SearchBar.

➤ Change the Appearances dropdown for the new color set in the Attributes Inspector to Any. This will change your available color choices to one.

➤ Set the color (as you did earlier) to red=180, green=240, blue=210, which is a lighter shade of green.

➤ Switch to the storyboard, select the Search Bar and set its Bar Tint to the SearchBar color asset.

That’s all you need to do change the colors.

But does these new colors work for all appearances?

➤ Run the app and notice the difference:

The new theme in action
The new theme in action

The new color looks fine for Light Appearance but it’s a bit too bright for Dark mode and the status bar text is hard to read. Let’s change that.

➤ Go to the Asset Catalog, select the SearchBar color asset, and change the Appearances (in the Attributes Inspector) to Any, Dark. Now you get two color instances to work with.

➤ Set the color for Dark Appearance to red=10, green=120, blue=100.

➤ Run the app again and check for both appearance modes.

The updated theme in action
The updated theme in action

That looks much better, doesn’t it?

You can’t see the change you made to AccentColor much except in the search bar’s cursor, but it will become more obvious later on. Then you can decide if you want to change that to fit the Dark appearance or not.

Change the row selection color

Currently, tapping a row highlights it in gray. This doesn’t go so well with the new color theme. So, you’ll give the row selection the same color tint.

As you learnt with MyLocations, that’s very easy to do because all table view cells have a selectedBackgroundView property. The view from that property is placed on top of the cell’s background, but below the other content, when the cell is selected.

➤ Add the following code to awakeFromNib() in SearchResultCell.swift:

override func awakeFromNib() {
  super.awakeFromNib()
  // New code below
  let selectedView = UIView(frame: CGRect.zero)
  selectedView.backgroundColor = UIColor(named: "SearchBar")?.withAlphaComponent(0.5)
  selectedBackgroundView = selectedView
}

The awakeFromNib() method is called after the cell object has been loaded from the nib but before the cell is added to the table view. You can use this method to do additional work to prepare the object for use. That’s perfect for creating the view with the selection color.

Why don’t you do that in an init method, such as init?(coder)? To be fair, in this case you could. But it’s worth noting that awakeFromNib() is called some time after init?(coder) and also after the objects from the nib have been connected to their outlets.

For example, in init?(coder) the nameLabel and artistNameLabel outlets will still be nil but in awakeFromNib() they will be properly hooked up to their UILabel objects. So, if you wanted to do something with those outlets in code, you’d need to do that in awakeFromNib(), not in init?(coder).

That’s why awakeFromNib() is the ideal place for this kind of thing — it’s similar to how you use viewDidLoad() in a view controller.

Don’t forget to first call super.awakeFromNib() — it is required. If you forget, then the superclass UITableViewCell — or any of the other superclasses — may not get a chance to initialize themselves.

Tip: It’s always a good idea to call super.methodName(…) in methods that you’re overriding — such as viewDidLoad(), viewWillAppear(), awakeFromNib(), and so on — unless the documentation says otherwise.

The code itself should hold no surprises except for the creation of the color. As you can see, you can initialize color assets by simply using their name from the asset catalog. How handy is that?

Now, you can simply set up the whole color scheme that you want to use for your app as color assets, set their values for any appearance you want to support and then be able to simply instantiate any of those colors by using the name you assigned to the color. No more guessing at what a color might be based on RGB values!

Also, as you can see from the code, you can modify the originally defined color by adjusting the transparency using withAlphaComponent. Here, you’ve made the color semi-transparent so that it isn’t as prominent as the search bar color.

You know why you have the question mark before the call to withAlphaComponent, right? Because the named color might not exist, or you might have made a mistake spelling the name correctly, and so the color is not guaranteed to be a valid value. If the system can’t find the color based on the name, it returns a nil value.

When you run the app, do a search and tap a row, it should look like this:

The new selection color in action
The new selection color in action

Add app icons

While you’re at it, you might as well give the app an icon.

➤ Open the asset catalog (Assets.xcassets) and select the AppIcon group.

➤ Drag the images from the Icon folder from the Resources folder into the matching slots.

All the icons in the asset catalog
All the icons in the asset catalog

➤ Run the app and notice that it now has a nice new icon:

The app icon
The app icon

Show keyboard on app launch

One final user interface tweak I’d like to make is that the keyboard should be immediately visible when you start the app so the user can start typing right away.

➤ Add the following line to viewDidLoad() in SearchViewController.swift:

searchBar.becomeFirstResponder()

As you are aware from the Checklists app, becomeFirstResponder() will give searchBar the “focus” and show the keyboard. Anything you type will end up in the search bar.

➤ Try it out and commit your changes. You styled the search bar and added app icons.

Tag commits

If you look through the various commits you’ve made so far, you’ll notice a bunch of strange numbers, such as “ba03dbb”:

The commits listed in the history window have weird numbers
The commits listed in the history window have weird numbers

Those are internal numbers — known as the hash — that Git uses to uniquely identify commits. Such numbers aren’t very memorable, or useful, for us humans, so Git also allows you to “tag” a certain commit with a more friendly label.

➤ Tagging a commit in Xcode is as simple a selecting the commit in the Source Control navigator view, right-clicking to get the context menu and selecting the Tag option.

Tagging a commit in Xcode
Tagging a commit in Xcode

➤ Enter “v0.1” as the Tag, and an optional message describing what this particular tag encompasses. Then click Create to create the tag.

You can see the new tag in the Source Control navigator view:

The new tag in Xcode
The new tag in Xcode

Xcode works quite well with Git, but you might want more power to do complex Git operations. If you do, you’ll probably need to learn how to use the Terminal or get a tool such as SourceTree (sourcetreeapp.com), which is available for free.

The debugger

Xcode has a built-in debugger. Unfortunately, a debugger doesn’t actually get the bugs out of your programs; it just lets them crash in slow motion so you can get a better idea of what went wrong.

The debugger lets you dig through the evidence after the damage has been done — like a CSI unit member or a detective —, in order to find the scoundrel who did it. Thanks to the debugger, you don’t have to stumble in the dark with no idea what just happened. Instead, you can use it to quickly pinpoint what went wrong and where. Once you know those two things, figuring out why it went wrong becomes a lot easier.

Index out of range bug

Let’s introduce a bug into the app so that it crashes — knowing what to do when your app crashes is very important.

➤ Change SearchViewController.swift’s numberOfRowsInSection method to:

func tableView(
  _ tableView: UITableView,
  numberOfRowsInSection section: Int
) -> Int {
  if !hasSearched {
    . . .
  } else if searchResults.count == 0 {
    . . .
  } else {
    return searchResults.count + 1  // This line changes
  }
}

➤ Now run the app and search for something. The app crashes and the Xcode window changes to something like this:

The Xcode debugger appears when the app crashes
The Xcode debugger appears when the app crashes

The crash is: Thread 1: Fatal error: Index out of range. Sounds nasty!

According to the error message, the index that was used to access some array is larger than the number of items inside the array. In other words, the index is “out of range”. That is a common error with arrays and you’re likely to make this mistake more than once in your programming career.

Now that you know what went wrong, the big question is: where did it go wrong? You may have many calls to array[index] in your app, and you don’t want to have to dig through the entire code to find the culprit.

Thankfully, you have the debugger to help you out. In the source code editor it already points out the offending line as you can see in the screenshot above — apparently its line 99.

Important: This line isn’t necessarily the cause of the crash — after all, you didn’t change anything in this method — but it is where the crash happens. From here you can trace backwards to the cause.

The array is searchResults and the index is given by indexPath.row. It would be great to get some insight into the row number in the index path – there are several ways to do this.

The one we’ll look at here is to use the debugger’s command line interface, like a hacker whiz kid from the movies :]

➤ In the Xcode Console, after the (lldb) prompt, type p indexPath.row and press enter:

Printing the value of indexPath.row
Printing the value of indexPath.row

The output should be something like:

(Int) $R0 = 3

This means the value of indexPath.row is 3 and the type is Int — you can ignore the $R0 bit.

Let’s also find out how many items are in the array.

➤ Type p searchResults and press enter. If you use the auto complete functionality, do note that both searchResult — without the “s” at the end — and searchResults are choices. Be sure to select the correct one.:

Printing the searchResults array
Printing the searchResults array

The output shows an array with three items.

You can now reason about the problem: the table view is asking for a cell for the fourth row — i.e. the one at index 3 — but apparently there are only three rows in the data model — rows 0 through 2.

The table view knows how many rows there are from the value that is returned from numberOfRowsInSection, so maybe that method is returning the wrong number of rows? That is indeed the cause, of course, as you intentionally introduced the bug in that method.

I hope this illustrates how you should deal with crashes: first find out where the crash happens and what the actual error is, then reason your way backwards until you find the cause.

Storyboard outlet bug

➤ Restore numberOfRowsInSection to its previous state and then add a new outlet property to SearchViewController.swift:

@IBOutlet var searchBar2: UISearchBar!

➤ Open the storyboard and Control-drag from Search View Controller to the Search Bar. Select searchBar2 from the popup.

Now the search bar is also connected to this new searchBar2 outlet — it’s perfectly fine for an object to be connected to more than one outlet at a time.

➤ Delete the searchBar2 outlet property from SearchViewController.swift in the source code, not the storyboard.

This is a dirty trick on my part to create another crash. The storyboard contains a connection to a property that no longer exists. If you think this a convoluted example, then wait until you make this mistake in one of your own apps. It happens more often than you may think!

➤ Run the app and it immediately crashes. The crash is “Thread 1: “[<StoreSearch.SearchViewController 0x7fc7d690a6e0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key searchBar2.””.

Scrolling up the Xcode Console output in the Debug pane you should come across:

2020-08-20 08:13:39.945773-0400 StoreSearch[1038:12267770] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<StoreSearch.SearchViewController 0x7fc7d690a6e0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key searchBar2.'
*** First throw call stack:
  . . . 

The first part of this message is very important: it tells you that the app was terminated because of an “NSUnknownKeyException”. On some platforms, exceptions are a commonly used error handling mechanism, but on iOS this is always a fatal error and the app is forced to halt.

The bit that should pique your interest is this:

this class is not key value coding-compliant for the key searchBar2

Hmm, that is a bit cryptic. It does mention searchBar2 but what does “key value-coding compliant” mean? I’ve seen this error enough times to know what is wrong, but if you’re new to this game, a message like that isn’t very enlightening.

So let’s see where Xcode thinks the crash happened:

Crash in system library
Crash in system library

That also isn’t very useful. Xcode says the app crashed happened in a system library but we know that isn’t the root cause.

Xcode goes through the call stack until it finds a method that it has source code for and that’s the one it shows. The call stack is the list of methods that have been called most recently. You can see it on the left of the Debugger window.

➤ Click the left-most icon at the bottom of the Debug navigator to see more info.

A more detailed call stack
A more detailed call stack

The method at the top, __pthread_kill, was the last method that was called — it’s actually a function, not a method. It got called from pthread_kill, which was called from abort, which was called from abort_message, and so on, all the way back to the main function, which is the entry point of the app and the very first function that was called when the app started.

All of the methods and functions that are listed in this call stack are from system libraries, which is why they are grayed out. If you click on one, you’ll get a bunch of unintelligible assembly code like you saw in the previous screenshot.

Clearly, this approach is not getting you anywhere. However, there is another thing you can try — set an Exception Breakpoint.

A breakpoint is a special marker in your code that will pause app execution and launch the debugger.

When your app hits a breakpoint, the app will pause at that exact spot. Then you can use the debugger to step line-by-line through your code in order to run it in slow motion. That can be a handy tool if you really cannot figure out why something crashes.

You’re not going to step through code in this book, but you can read more about it in the Debugging section of Apple’s developer support site: developer.apple.com/support/debugging. Or, you can check the Debug code topic under Xcode’s Help ▸ Xcode Help menu option — if the help window is blank except for a few links, click on Show topics to see the list of topics.

You are going to set a special breakpoint that is triggered whenever a fatal exception occurs. This will halt the program just as it is about to crash, which should give you more insight into what is going on.

➤ Switch to the Breakpoint navigator and click the + button at the bottom to add an Exception Breakpoint:

Adding an Exception Breakpoint
Adding an Exception Breakpoint

This will add a new breakpoint:

After adding the Exception Breakpoint
After adding the Exception Breakpoint

➤ Run the app again. It will still crash, but now Xcode shows a lot more info:

Xcode now halts the app at the point the exception occurs
Xcode now halts the app at the point the exception occurs

There are many more methods in the call stack now. Let’s see if we can find some clues as to what is going on.

What catches my attention is the call to something called [UINib instantiateWithOwner:options:]. That’s a pretty good hint that this error occurs when loading a nib file, or the storyboard in this case.

Using these hints and clues, and the somewhat cryptic error message that you got without the Exception Breakpoint, you can usually figure out what is making your app crash.

In this case, we’ve established that the app crashes when it’s loading the storyboard, and the error message mentioned “searchBar2”. Put two and two together and you’ve got your answer.

A quick peek in the source code confirms that the searchBar2 outlet no longer exists in the view controller but the storyboard still refers to it.

➤ Open the storyboard and in the Connections inspector disconnect Search View Controller from searchBar2 to fix the crash. That’s another bug squashed!

Note: Enabling the Exception Breakpoint means that you no longer get a useful error message in the Console if the app crashes — the breakpoint stops the app just before the exception happens. If sometime later during development your app crashes on another bug, you may want to disable this breakpoint to actually see the error message. You can do that from the Breakpoint navigator by simply selecting the breakpoint and clicking on the dark blue arrow. If the arrow goes from dark blue to a pale blue, it is disabled.

To summarize:

  • If your app crashes while running in Xcode, the Xcode debugger will often show you an error message and where in the code the crash happened.

  • If Xcode thinks the crash happened in a system library — not very useful! — add an Exception Breakpoint to get more info.

  • If the app crashes but there is no error message in the Console, disable any Exception Breakpoints you may have and make the app crash again. Alternatively, click the Continue program execution button from the debugger toolbar a few times. That will also show the error message… eventually.

  • An EXC_BAD_ACCESS error usually means something went wrong with your memory management. An object may have been “released” one time too many or not “retained” enough. With Swift these problems are mostly a thing of the past because the compiler will usually make sure to do the right thing. However, it’s still possible to mess up if you’re talking to Objective-C code or low-level APIs.

  • EXC_BREAKPOINT is not an error. The app has stopped on a breakpoint, the blue arrow points at the line where the app is paused. You set breakpoints to pause your app at specific places in the code, so you can examine the state of the app inside the debugger. The “Continue program execution” button resumes the app.

    It is fairly common for this to happen because you accidentally clicked on the gutter of the source code editor and set a breakpoint when you didn’t mean to.

This should help you get to the bottom of most of your crashes!

The build log

If you’re wondering what Xcode actually does when it builds your app, then take a peek at the Report navigator. It’s the last tab in the navigator pane.

The Report navigator keeps track of your builds and debug sessions so you can look back at what happened. It even remembers the debug output of previous runs of the app.

Make sure All Messages – at the top of the log window – is selected. To get more information about a particular log item, select the item and click the little detail icon that appears on the right. The line will expand and you’ll see exactly which commands Xcode executed and what the result was.

Should you run into some weird compilation problem, then this is the place for troubleshooting. Besides, it’s interesting to see what Xcode is up to from time to time.

You can find the project files for this chapter under 33-Custom-table-cells in the Source Code folder.

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.