Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

32. Search Bar
Written by Fahim Farook

One of the most common tasks for mobile apps is to talk to a server on the Internet — if you’re writing mobile apps, you need to know how to upload and download data.

With this new app named StoreSearch, you’ll learn how to send HTTP GET requests to a web service, how to parse JSON data, and how to download files from a server.

You’re going to build an app that lets you search the iTunes store. Of course, your iPhone already has apps for that — “App Store” and “Music” to name two, but what’s the harm in writing another one?

Apple has made a web service available for searching the entire iTunes store and you’ll be using that to learn about networking.

The finished app will look like this:

The finished StoreSearch app
The finished StoreSearch app

You will add search capability to your old friend, the table view. There is an animated pop-up with extra information when you tap an item in the table. And when you flip the iPhone over to landscape, the layout of the app completely changes to show the search results in a different way.

With the last app, you dipped your toe in the dark mode pool. Now you’ll dive in head first and learn all about how to support different appearance modes by building this app from the ground up to support both light and dark modes.

There will also be an iPad version of the app with a custom UI for the iPad:

The app on the iPad
The app on the iPad

StoreSearch fills in the missing pieces and rounds off the knowledge you have gained from developing the previous apps. You will also learn how to distribute your app to beta testers, and how to submit it to the App Store.

In this chapter, you will do the following:

  • Create the project: Create a new project for your new app. Set up version control using Git.
  • Create the UI: Create the user interface for StoreSearch.
  • Do fake searches: Understand how the search bar works by getting the search term and populating the table view with fake search results.
  • Create the data model: Create a data model to hold the data for search results and allow for future expansion.
  • No data found: Handle “no data” situations when doing a search.

There’s a lot of work ahead, so let’s get started!

Create the project

Fire up Xcode and make a new project. Choose the App template and fill in the options as follows:

  • Product Name: StoreSearch
  • Team: Default value
  • Organization Identifier: com.yourname
  • Interface: Storyboard
  • Language: Swift
  • Use Core Data, Include Tests: leave these unchecked

When you save the project Xcode gives you the option to create a Git repository. You’ve ignored this option thus far, but now you should make sure it is enabled:

Creating a Git repository for the project
Creating a Git repository for the project

If you don’t see this option, click the Options button at the bottom-left of the dialog.

Git and version control

Git is a version control system — it allows you to make snapshots of your work so you can always go back later and see a history of the changes made to the project. Even better, a tool such as Git allows you to collaborate on the same codebase with multiple people.

Imagine the chaos if two programmers changed the same source file at the same time. It’s possible that your changes could accidentally be overwritten by a colleague’s. I once had a job where I had to shout down the hall to another programmer, “Are you using file X?” just so we wouldn’t be destroying each other’s work.

With a version control system such as Git, each programmer can work independently on the same files, without fear of undoing the work of another. Git is smart enough to automatically merge in all of the changes, and if there are any conflicting edits, it will let you resolve them manually.

Git is not the only version control system out there, but it’s the most popular one for iOS. A lot of iOS developers share their source code on GitHub (github.com), a free collaboration site that uses Git as its engine. Xcode has built-in support for Git.

For StoreSearch, you will use some basic Git functionality. Even if you work alone and don’t have to worry about other programmers messing up your code, it still makes sense to use it. After all, you might be the one messing up your own code, and with Git, you’ll always have a way to go back to your old — and working! — version of the code.

The first screen

The first screen in StoreSearch will have a table view with a search bar — let’s create the view controller for that screen.

➤ In the Project navigator, select ViewController.swift, move your cursor over the ViewController class name and right-click to show the context menu. Select Refactor ▸ Rename… from the menu and rename the class (and associated files and storyboard references) to SearchViewController.

➤ Run the app to make sure everything works. You should see a white screen with the status bar at the top.

Test dark mode

Since we are building the app for both appearance modes from the ground up, we should test each screen for both appearance modes each time we do any testing.

➤ Select Features ▸ Toggle Appearance from the Simulator menu to switch your simulator to dark mode. You should now see a black screen with white status bar text. It’s looking good.

You can use the Toggle Appearance menu option again to switch your simulator back to light mode.

Since we’ll be doing a lot of dark mode testing, there’s another way to switch the app’s appearance, as well as some other settings, via Xcode directly.

When you have the app running via Xcode, click on the Environment Overrides button in the debug toolbar at the bottom of the Xcode editor window.

The Xcode environment overrides
The Xcode environment overrides

Initially, each section in the menu that pops up will be disabled, but you can use the relevant switch to enable the section.

Once enabled, you can change the settings in that particular section to change how your app looks on-the-fly. So, for example, you can enable the switch for the Appearance section and then click on either the Light or Dark options to switch your app between dark and light modes.

It’s good to know different ways to test your app under different environment conditions and to actually test for all these conditions. You never know what particular set of conditions surfaces a hard to replicate bug.

Git version control

Notice that the project navigator now shows M and R icons next to some of the filenames in the list:

Xcode shows the files that are modified
Xcode shows the files that are modified

If you don’t see these icons, then choose the Source Control ▸ Refresh File Status option from the Xcode menu bar. If that gives an error message or still doesn’t work, simply restart Xcode. That’s a good tip in general: if Xcode is acting weird, restart it.

An M means the file has been modified since the last commit and an R means this is a file that has been renamed.

So what is a commit?

When you use a version control system such as Git, you’re supposed to make a snapshot every so often. Usually you’ll do that after you’ve added a new feature to your app or when you’ve fixed a bug, or whenever you feel like you’ve made changes that you want to keep. That is called a commit.

When you created the project, Xcode made the initial commit. You can see that in the Project History window.

➤ Select the Source Control navigator from the Navigator pane and switch to the Repositories tab to see the project history:

The history of commits for this project
The history of commits for this project

Note: Your Git history might not always look the same as mine in the screenshots since mine might have additional commits than what I tell you to make in the book. Don’t worry about that. You can always make your own commits – once you learn how — at any point and not rely solely on the instructions in the book.

➤ Let’s commit the change you just made. From the Xcode menu select Source Control ▸ Commit…

This opens a new window that shows in detail what changes you made. This a good time to quickly review the code changes, just to make sure you’re not committing anything you didn’t intend to:

Xcode shows the changes you’ve made since the last commit
Xcode shows the changes you’ve made since the last commit

The text view at the bottom of the window — the one which says “Enter commit message here” — is where you mention what changed in this particular commit. It’s always a good idea to write a short but clear reason for the commit here. Having a good description will help you later to find specific commits in your project’s history.

➤ Write: Rename ViewController to SearchViewController as the commit message.

➤ Press the Commit 5 Files button – the number will vary, in your case it’ll probably be 3 files. You’ll see that in the Project navigator the M and R icons are gone – at least until you make your next change.

The Source Control navigator should now show two commits.

Your commit is listed in the project history
Your commit is listed in the project history

If you double-click a particular commit, Xcode will show you the changes for that commit. You’ll be doing commits on a regular basis and by the end of the book you’ll be a pro at it :]

Create the UI

StoreSearch still doesn’t do much yet. In this section, you’ll build the UI to look like this — a search bar on top of a table view:

The app with a search bar and table view
The app with a search bar and table view

Even though this screen uses the familiar table view, it is not a table view controller but a regular UIViewController — check the class definition in SearchViewController.swift, if you are not sure.

You are not required to use a UITableViewController as the base class for your view controller just because you have a table view in your UI. For this app I will show you how to do that.

UITableViewController vs. UIViewController

So what exactly is the difference between a table view controller and a regular view controller?

First off, UITableViewController is a subclass of UIViewController — it can do everything that a regular view controller can. However, it is optimized for use with table views and has some cool extra features.

For example, when a table cell contains a text field, tapping that text field will bring up the on-screen keyboard. UITableViewController automatically scrolls the cells out of the way of the keyboard so you can always see what you’re typing.

You don’t get that behavior for free with a plain UIViewController — if you want that feature, you’ll have to program it yourself.

UITableViewController does have a big restriction: its main view must be a UITableView that takes up the entire screen space, except for a possible navigation bar at the top, and a toolbar or tab bar at the bottom.

If your screen consists of just a UITableView, then it makes sense to make it a UITableViewController. But if you want to have other views (or controls) as well, the more basic UIViewController is the option to go with.

That’s the reason you’re not using a UITableViewController in this app. Beside the table view, the app has another view, a UISearchBar. It is possible to put the search bar inside the table view as a special header view, or have the search bar appear as part of the navigation bar, but for this app you will have it sitting above the table view.

Set up the storyboard

➤ Open the storyboard and use the Interface Builder toolbar to switch to the iPhone SE (2nd generation). It doesn’t really matter which iPhone model you choose here, but the iPhone SE makes it easiest to follow along with this book.

➤ Also set the Appearance to Dark Appearance. Again, it doesn’t really matter which appearance you use, but you know from the past few apps that light mode (which is the default) works fine generally. So, using dark mode allows you to immediately see any visual/color issues that might be there for specifically for dark mode.

➤ Drag a new Table Viewnot a Table View Controller — into the existing view controller.

➤ Make the Table View as big as the main view (375 by 667 points) and then use the Add New Constraints menu at the bottom to attach the Table View to the edges of the screen so that left=0, top=0, right=0, bottom=0.

Remember to uncheck Constrain to margins, if it is checked. Each screen has 16-point margins on the left and right, but you can change their size. When “Constrain to margins” is enabled you’re pinning to these margins. That’s no good here; you want to pin the Table View to the edge of the screen instead.

Creating constraints to pin the Table View
Creating constraints to pin the Table View

This pins the Table View to the edges of its superview. Now the table will always fill up the entire screen, regardless of the size of the device screen.

➤ From the Objects Library, drag a Search Bar on to the Document Outline so that it is placed just below the Table View.

The reason you drag the Search Bar on to the Document Outline is that if you drop the Search Bar on to the main view, it is likely to be placed inside the Table View rather than outside.

You want the Search Bar to sit on the same level as the Table View in the Document Outline:

Search Bar must be below of Table View (left), not inside (right)
Search Bar must be below of Table View (left), not inside (right)

If you did put the Search Bar inside the Table View, you can pick it up in the Document Outline and drag it below the Table View.

➤ Pin the Search Bar so that top=0, left=0, and right=0 — 3 constraints in total.

The constraints for the Search Bar
The constraints for the Search Bar

You don’t need to pin the bottom of the Search Bar or give it a height constraint. Search Bars have an intrinsic height of 51 points.

➤ In the Attributes inspector for the Search Bar, change the Placeholder text to App name, artist, song, album, e-book.

At this point, the view controller’s design should look like this:

The search view controller with Search Bar and Table View
The search view controller with Search Bar and Table View

Connect to outlets

You know what’s coming next — connecting the Search Bar and the Table View to outlets on the view controller.

➤ Add the following outlets to SearchViewController.swift:

@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var tableView: UITableView!

➤ Switch back to the storyboard and connect the Search Bar and the Table View to their respective outlets — Control-drag from the view controller to the object that you want to connect.

➤ Run the app to make sure that everything works correctly.

Note: You might have noticed that when you ran the app, the appearance is Light — unless you’d already switched the appearance on the simulator, of course. If this confuses you, note that though you have the appearance set to Dark on Interface Builder, that does not mean that the app will run in Dark mode. Instead, the app will show whichever Appearance that is set on your device or simulator.

So if you want to see the app in dark mode when you run it, you’d have to switch the device or simulator to dark mode explicitly.

Do fake searches

Before you implement the iTunes store searching, it’s good to understand how the UISearchBar component works.

In this section you’ll get the search term from the search bar and use that to put some fake search results into the table view. Once you’ve got that working, you can build in the web service. Baby steps!

➤ Run the app. If you tap the search bar, the on-screen keyboard will appear — if you’re on the simulator, you may need to press ⌘K to bring up the keyboard, and Shift+⌘K to allow typing from your Mac keyboard.

However, it won’t do anything when you type in a search term and tap the Search button.

Listening to the search bar is done — how else? — with a delegate. Let’s put this delegate code into an extension.

Add a search bar delegate

➤ Add the following to the bottom of SearchViewController.swift, after the final closing bracket:

// MARK: - Search Bar Delegate
extension SearchViewController: UISearchBarDelegate {
  func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    print("The search text is: '\(searchBar.text!)'")
  }
}

Recall that you can use extensions to organize your source code. By putting all the UISearchBarDelegate stuff into its own extension, you keep it together in one place and out of the way of the rest of the code.

The UISearchBarDelegate protocol has a method searchBarSearchButtonClicked(_:) that is invoked when the user taps the Search button on the keyboard. You will implement this method to put some fake data into the table. Later, you’ll make this method send a network request to the iTunes store to find songs, movies and e-books that match the search text that the user typed, but let’s not do too many new things at once!

At the moment, all the new code does is to output the search term from the search bar to the Xcode Console.

Tip: I always put strings between single quotes when I use print(). That way you can easily see whether there are any trailing or leading spaces in the string. Also note that searchBar.text is an optional, so we need to unwrap it. It will never actually return nil, so a ! will do just fine.

➤ In the storyboard, Control-drag from the Search Bar to Search View Controller, or the yellow circle at the top. Connect to delegate.

➤ Run the app, type something in the search bar and press the Search button. The Xcode Debug pane should now print the text you typed.

Show fake results

➤ Add the following new (and empty) extension to SearchViewController.swift:

// MARK: - Table View Delegate
extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
}

The above extension will handle all the table view related delegate methods. You could certainly have added them as two separate extensions if you liked, but I prefer to keep all the table view delegate related code in one place.

Adding the UITableViewDataSource and UITableViewDelegate protocols wasn’t necessary for the previous apps because you used a UITableViewController in each case. UITableViewController already conforms to these protocols under the hood.

SearchViewController however, is a regular view controller and therefore you have to hook up the data source and delegate protocols yourself.

➤ Xcode should complain at this point that your code does not conform to the UITableViewDataSource protocol. Modify the extension as follows to add the minimum code you need for the moment:

extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
  func tableView(
    _ tableView: UITableView, 
    numberOfRowsInSection section: Int
  ) -> Int {
    return 0
  }
  
  func tableView(
    _ tableView: UITableView,
    cellForRowAt indexPath: IndexPath
  ) -> UITableViewCell {
    return UITableViewCell()
  }
}

This simply tells the table view that it has no rows yet. Soon you’ll give it some fake data to display, but for now you just want to be able to compile the code without errors.

Often you can declare as conforming to a protocol without implementing any of its methods — for example, this works fine for UISearchBarDelegate. A protocol can have optional and required methods. If you forget a required method, you’ll generally see Xcode complain, like you did above.

➤ In the storyboard, Control-drag from the Table View to Search View Controller. Connect to dataSource. Repeat to connect to delegate.

In case you’re wondering how you connected something to a delegate property in Search View Controller twice — first the Search Bar, and then the Table View — the way Interface Builder presents this is a little misleading: the delegate outlet is not from SearchViewController, but belongs to the thing that you Control-dragged from. So you connected the SearchViewController to the delegate outlet on the Search Bar and also to the delegate (and dataSource) outlets on the Table View:

The connections from Search View Controller to the other objects
The connections from Search View Controller to the other objects

➤ Build and run the app to make sure everything still works.

Note: Did you notice a difference between these data source methods and the ones from the previous apps? Look closely…

Answer: They don’t have the override keyword.

In the previous apps, override was necessary because you were dealing with a subclass of UITableViewController, which already provides its own version of the tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:) methods.

In those apps, you were “overriding” or replacing those methods with your own versions, hence the need for the override keyword.

Here, however, your base class is not a table view controller but a regular UIViewController. Such a view controller doesn’t have any table view methods yet, so you’re not overriding anything here.

As you know by now, a table view needs some kind of data model. Let’s start with a simple Array.

➤ Add an instance variable for the array — this goes inside the class brackets, not in any of the extensions:

var searchResults = [String]()

The search bar delegate method will put some fake data into this array and then display it using the table.

➤ Replace the searchBarSearchButtonClicked(_:) method with:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  searchResults = []
  for i in 0...2 {
    searchResults.append(
      String(
        format: "Fake Result %d for '%@'", i, searchBar.text!
      )
    )
  }
  tableView.reloadData()
}

Here the notation [] means you instantiate a new String array and replace the contents of searchResults property with it. This is done each time the user performs a search. If there was already a previous array of results, then that is thrown away and deallocated. You could also have written searchResults = [String]() to do the same thing.

You add a string with some text into the array. Just for fun, that is repeated 3 times so your data model will have three rows in it.

When you write for i in 0...2, it creates a loop that repeats three times because the closed range 0...2 contains the numbers 0, 1, and 2. Note that this is different from the half-open range 0..<2, which only contains 0 and 1. You could also have written 1...3 but as you’ve discovered by now, programmers like to start counting at 0 :]

You’ve seen format strings before. The format specifier %d is a placeholder for integer numbers. Likewise, %f is for floating-point numbers. The placeholder %@ is for all other kinds of objects, such as strings.

The last statement in the method reloads the table view to make the new rows visible, which means you have to adapt the data source methods to read from this array as well.

➤ Replace the methods in the table view delegate extension with the following:

func tableView(
  _ tableView: UITableView, 
  numberOfRowsInSection section: Int
) -> Int {
  return searchResults.count
}

func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  let cellIdentifier = "SearchResultCell"
  
  var cell = tableView.dequeueReusableCell(
    withIdentifier: cellIdentifier) 
  if cell == nil {
    cell = UITableViewCell(
      style: .default, reuseIdentifier: cellIdentifier)
  }
  cell.textLabel!.text = searchResults[indexPath.row]
  return cell
}

All of the above code should be pretty familiar to you by now. You simply return the number of rows to display based on the contents of the searchResults array and you create a UITableViewCell by hand to display the table rows.

➤ Run the app. If you search for anything, a few fake results get added to the data model and are shown in the table.

Search for something else and the table view updates with new fake results.

The app shows fake results when you search
The app shows fake results when you search

UI Improvements

There are some issues with the current implementation of the app though – some are immediately evident, while others are a bit more subtle.

Table view content insets

The first thing you might have noticed: the first row of the Table View is hidden beneath the Search Bar.

That’s not so strange because you put the Search Bar on top of the table, obscuring part of the table view below.

You could fix this in several different ways:

  1. Change the table view’s top layout constraint to match the search bar’s bottom edge.
  2. Make the Search Bar partially translucent to let the contents of the table cells shine through.
  3. Use the table view’s content inset attribute to allow for the area covered by the search bar.

You will go with option #3. Unfortunately, the content inset attribute is unavailable via Interface Builder. So, this has to be done from code.

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

tableView.contentInset = UIEdgeInsets(top: 51, left: 0, bottom: 0, right: 0)

This tells the table view to add a 51-point margin at the top to account for the Search Bar.

Now the first row will always be visible, and when you scroll the table view, the cells still go under the search bar. Nice.

Don’t forget to switch appearance from Light to Dark (or vice versa) to make sure that your UI changes work correctly for both appearance modes.

Dismiss keyboard on search

It’s not very nice that the keyboard stays on screen after you press the Search button. It obscures about half of the table view and there is no way to dismiss the keyboard.

➤ Add the following line to the top of searchBarSearchButtonClicked(_:):

searchBar.resignFirstResponder()

This tells the UISearchBar that it should no longer listen for keyboard input. As a result, the keyboard will hide itself until you tap on the search bar again.

You can also configure the table view to dismiss the keyboard with a gesture.

➤ In the storyboard, select the Table View. Go to the Attributes inspector and set Scroll View - Keyboard to Dismiss interactively.

Extend search bar to status area

The search bar has a slightly jarring line above it to separate it from the status area. Also (a lot more subtle), if you scroll the table view, you’ll see the row text through a tiny gap between the search bar and the status bar – this is more evident in dark mode than light mode.

It would look a lot better if the status bar area was unified with the search bar. There’s a delegate method for UINavigationBar and UISearchBar items which allows the item to indicate its top position.

➤ Add the following method to the SearchBarDelegate extension:

func position(for bar: UIBarPositioning) -> UIBarPosition {
  return .topAttached
}

Now the app looks way better:

The search bar is “attached” to the top of the screen
The search bar is “attached” to the top of the screen

Since you’ve made a UI change, don’t forget to check for Dark appearance as well. In this particular case, there isn’t anything you need to handle.

If you are keen-eyed, you might notice that there’s now slightly more space above the first row of the table view and the search bar — this is due to the merging of the status bar and the search bar and the elimination of the separator line between the two. That additional space is about 4 points.

If you want, you can adjust the table view content inset by that amount to make things look just right again. I leave that to you as an exercise :]

The API documentation

If you were to look in the API documentation for UISearchBarDelegate you wouldn’t find the position(for:) method that you used above.

Instead, it is part of the UIBarPositioningDelegate protocol, which the UISearchBarDelegate protocol extends — like classes, protocols can inherit from other protocols.

Xcode comes with a big library of documentation for developing iOS apps. Basically everything you need to know is in here. Learn to use the Xcode documentation browser — it will become your best friend!

There are a few ways to get to the documentation for an item in Xcode. There is Quick Help, which shows info about the item under the text cursor:

Simply have the Quick Help inspector open and it will show context-sensitive help. Put the text cursor on the item you want to know more about and the inspector will provide a summary. You can click any of the blue text links in the summary to jump to the full documentation.

You can also get pop-up help. Hold down the Option (Alt) key and hover over the item that you want to learn more about. Then click the mouse:

And of course, there is the full-fledged documentation window. You can access it from the Help menu, under Developer Documentation. Use the bar at the top to search for the item that you want to know more about:

Create the data model

So far you’ve added String objects to the searchResults array, but that’s a bit limited. The search results that you’ll get back from the iTunes store include the product name, the name of the artist, a link to an image, the purchase price, and much more.

You can’t fit all of that in a single string, so let’s create a new class to hold this data.

The SearchResult class

➤ Add a new file to the project using the Swift File template. Name the new class SearchResult.

➤ Add the following to SearchResult.swift:

class SearchResult {
  var name = ""
  var artistName = ""
}

This adds two properties to the new SearchResult class. You’ll add several others in a bit.

In SearchViewController you need to modify the searchResults array to hold instances of SearchResult.

➤ In SearchViewController.swift, change the declaration of the property:

var searchResults = [SearchResult]()

➤ Next, change the for in loop in the search bar delegate method to:

for i in 0...2 {
  let searchResult = SearchResult()
  searchResult.name = String(format: "Fake Result %d for", i)
  searchResult.artistName = searchBar.text!
  searchResults.append(searchResult)
}

This creates an instance of the SearchResult object and simply puts some fake text into its name and artistName properties. Again, you do this in a loop because just having one search result by itself is a bit sad.

➤ At this point, tableView(_:cellForRowAt:) still expects the array to contain strings. So, update that method:

func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  . . .  
  if cell == nil {
    cell = UITableViewCell(style: .subtitle,           // change
                           reuseIdentifier: cellIdentifier)
  }
  // Replace all the code below this point
  let searchResult = searchResults[indexPath.row]
  cell.textLabel!.text = searchResult.name  
  cell.detailTextLabel!.text = searchResult.artistName
  return cell
}

Instead of a regular table view cell, the code now uses a “subtitle” cell style. You put the contents of the artistName property into the subtitle text label.

➤ Run the app; it should look like this:

Fake results in a subtitle cell
Fake results in a subtitle cell

No results found

When you add search functionality to your apps, you have to handle the following situations:

  1. The user did not perform a search yet.

  2. The user performed the search and received one or more results. That’s what happens in the current version of the app: for every search you’ll get back a handful of SearchResult objects.

  3. The user performed the search and there were no results. It’s usually a good idea to explicitly tell the user there were no results. If you display nothing at all, the user may wonder whether the search was actually performed or not.

Even though the app doesn’t do any actual searching yet, there is no reason why you cannot fake the last scenario as well.

Handle not getting any results

In defense of good taste, the app will return 0 results when a user searches for “justin bieber”, just so you know the app can handle this kind of situation.

➤ In searchBarSearchButtonClicked(_:), put the following if statement around the for in loop:

. . .
if searchBar.text! != "justin bieber" {
  for i in 0...2 {
    . . .
  }
}
. . .

The change here is pretty simple — you’ve added an if statement that prevents the creation of any SearchResult objects if the text is equal to "justin bieber".

➤ Run the app and do a search for “justin bieber” — note the all lowercase. The table should remain empty.

At this point, you don’t know if the search failed, or if there were no results. You can improve the user experience by showing the text “(Nothing found)” instead, so the user knows beyond a shadow of a doubt that there were no search results.

➤ Change the last part of tableView(_:cellForRowAt:) to:

if cell == nil {
  . . .
}
// New code
if searchResults.count == 0 {
  cell.textLabel!.text = "(Nothing found)"  
  cell.detailTextLabel!.text = ""
} else {
  let searchResult = searchResults[indexPath.row]
  cell.textLabel!.text = searchResult.name
  cell.detailTextLabel!.text = searchResult.artistName
}
// End of new code
return cell

That alone is not enough. When there is nothing in the array, searchResults.count is 0, right? But that also means that numberOfRowsInSection will return 0 and the table view will stay empty — this “Nothing found” row will never show up.

➤ Change tableView(_:numberOfRowsInSection:) to:

func tableView(
  _ tableView: UITableView,
  numberOfRowsInSection section: Int
) -> Int {
  if searchResults.count == 0 {
    return 1
  } else {
    return searchResults.count
  }
}

Now, if there are no results, the method returns 1, for the row with the text “(Nothing Found)”. This works because both numberOfRowsInSection and cellForRowAt check for this special situation.

➤ Try it out:

One can hope…
One can hope…

Handle no results when app starts

Unfortunately, the text “Nothing found” also appears initially when the user has not searched for anything yet. That’s just silly.

The problem is that you have no way to distinguish between “not searched yet” and “nothing found”. Right now, you can only tell whether the searchResults array is empty, but not what caused this.

Exercise: How would you solve this little problem?

There are two obvious solutions that come to mind:

  • Change searchResults to an optional. If it is nil, i.e. it has no value, then the user hasn’t searched yet. That’s different from the case where the user did search and no matches were found.

  • Use a separate boolean variable to keep track of whether a search has been done yet or not.

It may be tempting to choose the optional, but it’s best to avoid optionals if you can. They complicate the logic, they can cause the app to crash if you don’t unwrap them properly, and they require if let statements everywhere. Optionals certainly have their uses, but here they are not really necessary.

So, we’ll opt for the boolean. But do feel free to come back and try the optional on your own, and compare the differences. It’ll be a great exercise!

➤ Still in SearchViewController.swift, add a new instance variable:

var hasSearched = false

➤ In the search bar delegate method, set this variable to true. It doesn’t really matter where you do this, as long as it happens before the table view is reloaded.

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  . . .
  hasSearched = true      // Add this line
  tableView.reloadData()
}

➤ And finally, change tableView(_:numberOfRowsInSection:) to look at the value of this new variable:

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

Now, the table view remains empty until you first search for something. Try it out! Later on, you’ll see a much better way to handle this using an enum and it will blow your mind!

Selection handling

One more thing, if you currently tap on a row it will become selected and stay selected.

➤ To fix that, add the following methods to the table view delegate extension:

func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  tableView.deselectRow(at: indexPath, animated: true)
}
  
func tableView(
  _ tableView: UITableView, 
  willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
  if searchResults.count == 0 {
    return nil
  } else {
    return indexPath
  }
}

The tableView(_:didSelectRowAt:) method will simply deselect the row with an animation, while willSelectRowAt makes sure that you can only select rows when you have actual search results.

If you tap on the (Nothing Found) row now, you will notice that it is not selected at all. Actually, the row may still turn gray if you press down on it for a short while. That happens because you did not change the selectionStyle property of the cell. You’ll fix that in a bit.

➤ This is a good time to commit your changes. Go to Source Control ▸ Commit… — or press the ⌘+Option+C keyboard shortcut.

Make sure all the modified files are selected/checked in the list on the left, review your changes, and type a good commit message — something like “Add a search bar and table view. The search puts fake results in the table for now”. Press the Commit button to finish.

Note: It is customary to write commit messages in the present tense. That’s why I wrote “Add a search bar” instead of “Added a search bar”.

The app isn’t very impressive yet, but you’ve laid the foundation for what is to come. You have a search bar and know how to take action when the user presses the Search button. The app also has a simple data model that consists of an array with SearchResult objects, and it can display these search results in a table view for both Light and Dark appearances.

You can find the project files for this chapter under 32-Search-bar 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.