Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

38. Custom Table Cells
Written by Eli Ganim

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.

Adding assets

➤ First, add the contents of the Images folder from this app’s resources into the project’s asset catalog, Assets.xcassets.

Imported images in the asset catalog
Imported images in the asset catalog

Each of the images comes in two versions: 2x and 3x. There are no low-resolution 1x devices that can run the latest version of iOS. So there’s no point in including 1x images.

Adding 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 will be used 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 8 dimensions. As usual, you’ll design for this 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:

The Table View Cell in the Objects Library
The Table View Cell in the Objects Library

➤ 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 8 screen.

The cell now looks like this:

An empty table view cell
An empty table view cell

Note: Sometimes, you might have a blue bounding rectangle for the cell which is slightly offset from the actual cell’s location. This is an Interface Builder bug. If this happens to you, simply switch to some other file and then switch back to the SearchResultCell.xib — all should be well at this point.

➤ Drag an Image View and two Labels into the cell, 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 ▸ Show 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:220, Height:22.

➤ Set the Text for the second label to Artist Name, Font to System 15, Color to black with 50% opacity, X:84, Y:44, Width:220, Height:18. .

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.

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

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. That placeholder is part of the image files you just added to the project.

➤ Select the Image View. In the Attributes inspector, set Image to Placeholder.

The cell design should now look like this:

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

You’re not done yet. The design for the cell is only 320 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.

Setting up Auto Layout constraints

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 to the top and left sides of the cell. Also give it Width and Height constraints so that its size is always fixed at 60 by 60 points:

The constraints for the Image View
The constraints for the Image View

➤ Click Add 4 Constraints to actually add the constraints.

➤ Select the Name label and again use the Add New Constraints menu. Uncheck Constrain to margins and select the top, left, and right pins (but not the bottom one):

The constraints for the Name label
The constraints for the Name label

➤ Click Add 3 Constraints.

➤ Finally, pin the Artist Name label to the left, top, right and bottom — again without constraining to margins — as above by adding 4 new constraints.

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

Registering 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 cell = tableView.dequeueReusableCell(
             withIdentifier: "SearchResultCell", 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.

Adding 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 weak var nameLabel: UILabel!
@IBOutlet weak var artistNameLabel: UILabel!
@IBOutlet weak 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.

Using custom table view cell in app

➤ In SearchViewController.swift, change cellForRowAt to:

func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {

  let cell = tableView.dequeueReusableCell(withIdentifier: 
             "SearchResultCell", for: indexPath) 
             as! SearchResultCell
  if searchResults.count == 0 {
    cell.nameLabel.text = "(Nothing found)"
    cell.artistNameLabel.text = ""
  } else {
    let searchResult = searchResults[indexPath.row]
    cell.nameLabel.text = searchResult.name
    cell.artistNameLabel.text = searchResult.artistName
  }
  return cell
}

Notice the change in the first 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!

There are a few more things to improve. Notice that you’ve been using the string literal “SearchResultCell” in a few different places? It’s generally better to create a constant for such occasions.

Using a constant for table cell identifier

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, replace the string “SearchResultCell” with TableView.CellIdentifiers.searchResultCell.

For example, viewDidLoad() will now look like this:

override func viewDidLoad() {
  . . .
  let cellNib = UINib(nibName: 
      TableView.CellIdentifiers.searchResultCell, bundle: nil)
  tableView.register(cellNib, forCellReuseIdentifier: 
                     TableView.CellIdentifiers.searchResultCell)
}

The other change is in tableView(_:cellForRowAt:).

➤ Run the app to make sure everything still works.

A new “No results” cell

Remember our friend Justin Bieber? Searching for him now looks like 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. Make the text color 50% opaque black and the font 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.

➤ Center the label in the cell, using the blue guides to snap it exactly to the center.

It should look like this:

Design of the Nothing Found cell
Design of the Nothing Found cell

In order to keep the text centered on all devices, you can use the Auto Layout Align menu:

Creating the alignment constraints
Creating the alignment constraints

➤ Choose Horizontally in Container and Vertically in Container and click Add 2 Constraints.

The constraints should look like this:

The constraints for the label
The constraints for the label

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?

This is actually something new in Xcode 10 — 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. You can use the message “Use custom cells for search results.”

Changing the look of the app

The app too looks quite gray and dull. Let’s cheer it up a little by giving it more vibrant colors.

➤ Add the following method to AppDelegate.swift:

// MARK:- Helper Methods
func customizeAppearance() {
  let barTintColor = UIColor(red: 20/255, green: 160/255, 
                            blue: 160/255, alpha: 1)
  UISearchBar.appearance().barTintColor = barTintColor
}

This changes the appearance of the UISearchBar — in fact, it changes all search bars in the application. You only have one, but if you had several then this changes the whole lot in one fell swoop.

The UIColor(red:green:blue:alpha:) method makes a new UIColor object based on the RGB and alpha color components that you specify.

Many painting programs let you pick RGB values going from 0 to 255 so that’s the range of color values that many programmers are accustomed to thinking in. The UIColor initializer, however, accepts values between 0.0 and 1.0, so you have to divide these numbers by 255 to scale them down to that range.

➤ Call this new method from application(_:didFinishLaunchingWithOptions:):

func application(_ application: UIApplication, 
     didFinishLaunchingWithOptions launchOptions: 
     [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  customizeAppearance()  // Add this line
  return true
}

➤ Run the app and notice the difference:

The search bar in the new teal-colored theme
The search bar in the new teal-colored theme

The search bar is bluish-green, but still slightly translucent. The overall tint color is now a dark shade of green instead of the default blue — you can currently only see the tint color in the text field’s cursor but it will become more obvious later on.

The role of App Delegate

The poor AppDelegate is often abused. People give it too many responsibilities. Really, there isn’t that much for the app delegate to do.

It gets a number of callbacks about the state of the app — whether the app is about to be closed, for example — and handling those events should be its primary responsibility. The app delegate also owns the main window and the top-level view controller. Other than that, it shouldn’t do much.

Some developers use the app delegate as their data model. That is just bad design. You should really have a separate class — or several — for that. Others make the app delegate their main control hub. Wrong again! Put that stuff in your top-level view controller.

If you ever see the following type of thing in someone’s source code, it’s a pretty good indication that the application delegate is being used the wrong way:

let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.someProperty = . . .

This happens when an object wants to get something from the app delegate. It works but it’s not good architecture.

In my opinion, it’s better to design your code the other way around: the app delegate may do a certain amount of initialization, but then it gives any data model objects to the root view controller, and hands over control. The root view controller passes these data model objects to any other controller that needs them, and so on.

This is also called dependency injection. This principle was described in Chapter 32 in the “Pass the context” section for the MyLocations app.

Changing the row selection color

Currently, tapping a row highlights it in gray. This doesn’t go so well with the teal-colored theme. So, you’ll give the row selection the same bluish-green tint.

As you learned 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(red: 20/255, 
      green: 160/255, blue: 160/255, alpha: 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.

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

The selection color is now green
The selection color is now green

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

Keep in mind that for the 2x slots you need to use the image with twice the size in pixels. For example, you drag the Icon-152.png file into iPad App 76pt, 2x. For 3x you need to multiply the image size by 3.

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

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

Tagging commits

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

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, which is available for free on the Mac App Store.

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.

Like a detective, the debugger lets you dig through the evidence after the damage has been done, 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.

Indexing 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:

The debugger points at the line that crashed
The debugger points at the line that crashed

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 and 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) $R1 = 3

This means the value of indexPath.row is 3 and the type is Int — you can ignore the $R1 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.

Hopefully 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 weak var searchBar2: UISearchBar!

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

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: signal SIGABRT.”

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

*** Terminating app due to uncaught exception ’NSUnknownKeyException’, reason: ’[<StoreSearch.SearchViewController 0x7fb83ec09bf0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key searchBar2.’
*** First throw call stack:
(
	0   CoreFoundation         0x0000000111da1c7b __exceptionPreprocess + 171
  . . . 

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 AppDelegate?
Crash in AppDelegate?

That also isn’t very useful. Xcode says the app crashed in AppDelegate, but that’s not really true.

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:

You cannot look inside the source code of system libraries
You cannot look inside the source code of system libraries

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 the 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 your app topic under Xcode’s Help ▸ Xcode Help menu option.

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

➤ Now run the app again. It will still crash, but 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 [UIViewController _loadViewFromNibNamed:bundle:]. 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 on AppDelegate — not very useful! — add an Exception Breakpoint to get more info.

  • If the app crashes with a SIGABRT 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.

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 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 chaper under 38 – 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.