Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

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: 12 chapters
Show chapters Hide chapters

35. Asynchronous Networking
Written by Eli Ganim

You’ve got your app doing network searches and it’s working well. The synchronous network calls aren’t so bad, are they?

Yes they are, and I’ll show you why! Did you notice that whenever you performed a search, the app became unresponsive? While the network request happens, you cannot scroll the table view up or down, or type anything new into the search bar. The app is completely frozen for a few seconds.

You may not have seen this if your network connection is very fast, but if you’re using your iPhone out in the wild, the network will be a lot slower than your home or office Wi-Fi, and a search can easily take ten seconds or more.

To most users, an app that does not respond is an app that has crashed. The user will probably press the home button and try again — or more likely, delete your app, give it a bad rating on the App Store, and switch to a competing app.

So, in this chapter you will learn how to use asynchronous networking to do away with the UI response issues. You’ll do the following:

  • Extreme synchronous networking: Learn how synchronous networking can affect the performance of your app by dialing up the synchronous networking to the maximum.
  • The activity indicator: Add an activity indicator to show when a search is going on so that the user knows something is happening.
  • Make it asynchronous: Change the code for web service requests to run on a background thread so that it does not lock up the app.

Extreme synchronous networking

Still not convinced of the evils of synchronous networking? Let’s slow down the network connection to pretend the app is running on an iPhone that someone may be using on a bus or in a train, not in the ideal conditions of a fast home or office network. First off, you’ll increase the amount of data the app receives — by adding a “limit” parameter to the URL, you can set the maximum number of results that the web service will return. The default value is 50, the maximum is 200.

➤ Open SearchViewController.swift and in iTunesURL(searchText:), change the line with the web service URL to the following:

let urlString = String(format: 
  "https://itunes.apple.com/search?term=%@&limit=200", 
  encodedText)

You added &limit=200 to the URL. Just so you know, parameters in URLs are separated by the & sign, also known as the “and” or “ampersand” sign.

➤ If you run the app now, the search should be quite a bit slower.

Device Conditions

Still too fast for you to see any app response issues? Then use Device Conditions. This lets you simulate different network conditions such as bad cellphone network, in order to test your iOS apps. In order to activate it you need to connect a device running iOS 13 to your Mac, then

➤ Select Devices and Simulators from the Window menu.

➤ Choose the Devices tab.

➤ Choose your iPhone from the left pane and scroll down to Device Conditions

➤ Under Condition choose Network Link and under Profile choose Very poor network.

➤ Finally, click on Start.

Device Conditions dialog
Device Conditions dialog

➤ Now run the app and search for something. The Device Conditions tool will simulate a slow connection and download the data very slowly.

Tip: If the download still appears very fast, then try searching for some term you haven’t used before; the system may be caching the results from a previous search.

Notice how the app totally doesn’t respond during this time? It feels like something is wrong. Did the app crash or is it still doing something? It’s impossible to tell and very confusing to your users when this happens.

Even worse, if your program is unresponsive for too long, iOS may actually force kill it, in which case it really does crash. You don’t want that to happen!

“Ah,” you say, “let’s show some type of animation to let the user know that the app is communicating with a server. Then at least they will know that the app is busy.”

That sounds like a decent thing to do, so let’s get to it.

The activity indicator

You’ve used a spinning activity indicator before in MyLocations to show the user that the app was busy. Let’s create a new table view cell that you’ll show while the app is querying the iTunes store. It will look like this:

The app shows that it is busy
The app shows that it is busy

The activity indicator table view cell

➤ Create a new, empty nib file. Call it LoadingCell.xib. ➤ Drag a new Table View Cell on to the canvas. Set its width to 375 points and its height to 80 points.

➤ Set the reuse identifier of the cell to LoadingCell and set the Selection attribute to None.

➤ Drag a new Label into the cell. Set the title to Loading… and change the font to System 15. The label’s text color should be 50% opaque black.

➤ Drag a new Activity Indicator View into the cell and put it next to the label. Set its Style to Gray and give it the Tag 100.

The design should look like this:

The design of the LoadingCell nib
The design of the LoadingCell nib

To make this cell work properly on larger screens, you’ll add constraints that keep the label and the activity spinner centered in the cell. The easiest way to do this is to place these two items inside a container view and center that.

➤ Select both the Label and the Activity Indicator View — hold down to select multiple items. From the Xcode menu bar, choose Editor ▸ Embed In ▸ View Without Inset. This puts a white view behind the selected views.

The label and the spinner now sit in a container view
The label and the spinner now sit in a container view

Note: If you’re wondering what the difference is between the Embed In ▸ View and Embed In ▸ View Without Inset options in the Editor menu is, try it and you should see what happens. The first option adds a view which is slightly larger than the items it encloses because it has inset the new view to add some padding around the enclosed items. The second option, the one you used, simply encloses all of the items without any pading.

➤ With this new container view selected, click the Align button and put checkmarks in front of Horizontally in Container and Vertically in Container to make new constraints.

The container view has red constraints
The container view has red constraints

You end up with a number of red constraints. That’s no good; we want to see blue ones. The reason your new constraints are red is that Auto Layout does not know how large this container view should be; you’ve only added constraints for the view’s position, not its size.

To fix this, you’re going to add constraints to the label and activity indicator as well, so that the width and height of the container view are determined by the size of the two things inside it.

This is especially important for later when you’re going to translate the app to another language. If the Loading… text becomes larger or smaller, then so should the container view, in order to stay centered inside the cell.

➤ Select the label and click the Add New Constraints button. Simply pin it to all four sides and press Add 4 Constraints.

➤ Repeat this for the Activity Indicator View. You don’t need to pin it to the left because that constraint already exists — pinning the label added it.

Now the constraints for the label and the activity indicator should be all blue. At this point, the container view may still have orange lines indicating that the constraints are fine but that the view’s frame is not in the proper position. If so, select it and choose Editor ▸ Resolve Auto Layout Issues ▸ Update Frames — under Selected Views. This will move the container view into the position dictated by its constraints.

Cool, you now have a cell that automatically adjusts itself to any size screen.

Using the activity indicator cell

To make this special table view cell appear, you’ll follow the same steps as for the “Nothing Found” cell.

➤ Add the following line to the TableView.CellIdentifiers structure in SearchViewController.swift:

static let loadingCell = "LoadingCell"

➤ And register the nib in viewDidLoad():

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

You now have to come up with some way to let the table view’s data source know that the app is currently in a state of downloading data from the server. The simplest way to do that is to add another boolean flag. If this variable is true, then the app is downloading stuff and the new Loading… cell should be shown; if the variable is false, you show the regular contents of the table view.

➤ Add a new instance variable:

var isLoading = false

➤ Change tableView(_:numberOfRowsInSection:) to:

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

The if isLoading condition returns 1, because you need a row in order to show a cell.

➤ Update tableView(_:cellForRowAt:) as follows:

func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  // New code 
  if isLoading {
    let cell = tableView.dequeueReusableCell(withIdentifier: 
        TableView.CellIdentifiers.loadingCell, for: indexPath)
        
    let spinner = cell.viewWithTag(100) as! 
                  UIActivityIndicatorView
    spinner.startAnimating()
    return cell
  } else 
  // End of new code
  if searchResults.count == 0 {
    . . .

You added an if condition to return an instance of the new Loading… cell. It also looks up the UIActivityIndicatorView by its tag and then tells the spinner to start animating. The rest of the method stays the same.

➤ Change tableView(_:willSelectRowAt:) to:

func tableView(_ tableView: UITableView, 
     willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  if searchResults.count == 0 || isLoading {    // Changed
    return nil
  } else {
    return indexPath
  }
}

You added || isLoading to the if statement. Just like you don’t want users to select the “Nothing Found” cell, you also don’t want them to select the “Loading…” cell, so you return nil in both cases.

There’s only one thing remaining: you should set isLoading to true before you make the HTTP request to the iTunes server, and also reload the table view to make the Loading… cell appear.

➤ Change searchBarSearchButtonClicked(_:) to:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    searchBar.resignFirstResponder()
    // New code
    isLoading = true                    
    tableView.reloadData()
    // End of new code
    . . .
    isLoading = false                     // New code
    tableView.reloadData()
  }
}

Before you do the networking request, you set isLoading to true and reload the table to show the activity indicator.

After the request completes and you have the search results, you set isLoading back to false and reload the table again to show the SearchResult objects.

Makes sense, right? Let’s fire up the app and see this in action!

Testing the new loading cell

➤ Run the app and perform a search. While search is taking place the Loading… cell with the spinning activity indicator should appear…

…or should it?!

The sad truth is that there is no spinner to be seen. And in the unlikely event that it does show up for you, it won’t be spinning — try it with Network Link Conditioner enabled.

➤ To show you why, first change searchBarSearchButtonClicked(_:) as follows:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    searchBar.resignFirstResponder()
    isLoading = true
    tableView.reloadData()
    /*
       . . . the networking code (commented out) . . . 
     */
  }
}

Note that you don’t have to remove anything from the code — simply comment out everything after the first call to tableView.reloadData().

➤ Run the app and do a search. Now the activity spinner does show up!

So at least you know that part of the code is working fine. But with the networking code enabled, the app is not only totally unresponsive to any input from the user, it also doesn’t want to redraw its screen. What’s going on here?

The main thread

The CPU (Central Processing Unit) in older iPhone and iPad models has one core, which means it can only do one thing at a time. More recent models have a CPU with two cores, which allows for a whopping two computations to happen simultaneously. Your Mac may have 4 cores.

With so few cores available, how come modern computers can have many more applications and other processes running at the same time?

To get around the hardware limitation of having only one or two CPU cores, most computers, including the iPhone and iPad, use preemptive multitasking and multithreading to give the illusion that they can do many things at once.

Multitasking is something that happens between different apps. Each app is said to have its own process and each process is given a small portion of each second of CPU time to perform its jobs. Then it is temporarily halted, or pre-empted, and control is given to the next process.

Each process contains one or more threads. Each process is given a bit of CPU time to do its work. The process splits up that time among its threads. Each thread typically performs its own work and is as independent as possible from the other threads within that process.

An app can have multiple threads and the CPU switches between them:

If you go into the Xcode debugger and pause the app, the debugger will show you which threads are currently active and what they were doing before you stopped them.

For the StoreSearch app, there were apparently six threads at the time the following screenshot was taken:

Most of these threads are managed by iOS itself and you don’t have to worry about them. Also, you may see less or more than six threads. However, there is one thread that requires special care: the main thread. In the image above, that is Thread 1.

The main thread is the app’s initial thread and from there all the other threads are spawned. The main thread is responsible for handling user interface events and also for drawing the UI. Most of your app’s activities take place on the main thread. Whenever the user taps a button in your app, it is the main thread that performs your action method.

Because it’s so important, you should be careful not to hold up, or “block,” the main thread. If your action method takes more than a fraction of a second to run, then doing all these computations on the main thread is not a good idea because that would lock up your main thread.

The app becomes unresponsive because the main thread cannot handle any UI events while you’re keeping it busy doing something else — and if the operation takes too long, the app may even be killed by the system.

In StoreSearch, you’re doing a lengthy network operation on the main thread. It could potentially take many seconds, maybe even minutes, to complete.

After you set the isLoading flag to true, you tell the tableView to reload its data so that the user can see the spinning animation. But that never comes to pass. Telling the table view to reload schedules a “redraw” event, but the main thread gets no chance to handle that event as you immediately start the networking operation, keeping the main thread busy for a long time.

This is why the current synchronous approach to doing networking is bad: Never block the main thread. It’s one of the cardinal sins of iOS programming!

Making it asynchronous

To prevent blocking the main thread, any operation that might take a while to complete should be asynchronous. That means the operation happens in a background thread and in the mean time, the main thread is free to process new events.

That is not to say you should create your own thread. If you’ve programmed on other platforms before, you may not think twice about creating new threads, but on iOS that is often not the best solution.

You see, threads are tricky. Not threads per se, but doing things in parallel. There’s no need to go into too much detail here, but generally, you want to avoid the situation where two threads are modifying the same piece of data at the same time. That can lead to very surprising — but not very pleasant — results.

Rather than making your own threads, iOS has several more convenient ways to start background processes. For this app you’ll be using queues and Grand Central Dispatch, or GCD. GCD greatly simplifies tasks that require parallel programming. You’ve already briefly played with GCD in MyLocations, but now you’ll put it to even better use.

In short, GCD has a number of queues with different priorities. To perform a job in the background, you put the job in a closure and then pass that closure to a queue and forget about it. It’s as simple as that.

GCD will get the closures — or “blocks” as it calls them — from the queues one-by-one and perform their code in the background. Exactly how it does that is not important, you’re only guaranteed it happens on a background thread somewhere. Queues are not exactly the same as threads, but they use threads to do their job.

Queues have a list of closures to perform on a background thread
Queues have a list of closures to perform on a background thread

Putting the web request in a background thread

To make the web service requests asynchronous, you’re going to put the networking part from searchBarSearchButtonClicked(_:) into a closure and then place that closure on a medium priority queue.

➤ Change searchBarSearchButtonClicked(_:) as follows:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    . . .
    searchResults = []
    // Replace all code after this with new code below
    // 1
    let queue = DispatchQueue.global()
    let url = self.iTunesURL(searchText: searchBar.text!)
    // 2
    queue.async {
      
      if let data = self.performStoreRequest(with: url) {
        self.searchResults = self.parse(data: data)
        self.searchResults.sort(by: <)
        // 3
        print("DONE!")
        return
      }
    }
  }
}

Here is the new stuff:

  1. This gets a reference to the queue. You’re using a “global” queue, which is a queue provided by the system. You can also create your own queues, but using a standard queue is fine for this app. You also get the URL for your search here, outside the closure.
  2. Once you have the queue, you can dispatch a closure on it — everything between queue.async { and the closing } is the closure. Whatever code in the closure will be put on the queue and be executed asynchronously in the background. After scheduling this closure, the main thread is immediately free to continue. It is no longer blocked.
  3. Inside the closure, you remove the code that reloads the table view after the search is done, as well as the error handling code. For now, this has been replaced by print() statements. There is a good reason for this and we’ll get to it in a second. First let’s try the app again.

➤ Run the app and do a search. The “Loading…” cell should be visible, complete with animating spinner! After a short while you should see the “DONE!” message appear in the Console.

Of course, the Loading… cell sticks around forever because you still haven’t told it to go away.

Putting UI updates on the main thread

The reason you need to remove all the user interface code from the closure — and moved getting the search URL outside the closure — is that UIKit has a rule that UI code should always be performed on the main thread. This is important!

Accessing the same data from multiple threads can create all sorts of misery, so the designers of UIKit decided that changing the UI from other threads would not be allowed. That means you cannot reload the table view from within this closure, because it runs on a queue that is on a background thread, not the main thread.

As it happens, there is also a “main queue” that is associated with the main thread. If you need to do anything on the main thread from a background queue, you can simply create a new closure and schedule the main thread actions on the main queue.

➤ Replace the line in searchBarSearchButtonClicked(_:) that says print("DONE!") with:

DispatchQueue.main.async {
  self.isLoading = false
  self.tableView.reloadData()
}

With DispatchQueue.main.async you can schedule a new closure on the main queue. This new closure sets isLoading back to false and reloads the table view. Note that self is required because this code sits inside a closure.

➤ Try it out. With these changes in place, your networking code no longer occupies the main thread and the app suddenly feels a lot more responsive!

All kinds of queues

When working with GCD queues you will often see this pattern:

let queue = DispatchQueue.global()
queue.async {
  // code that needs to run in the background
  
  DispatchQueue.main.async {
    // update the user interface
  }
}

Basically, while you do your work in a background thread, you still have to switch over to the main thread to do any user interface updates. That’s just the way it is.

There is also queue.sync, without the “a,” which takes the next closure from the queue and performs it in the background, but makes you wait until that closure is done. That can be useful in some cases but most of the time you’ll want to use queue.async. No one likes to wait!

The main thread checker

You read previously that you should not run UI code on a background thread. However, till Xcode 9, there was no easy way to discover UI code running on background threads except by scouring the source code laboriously line-by-line trying to determine what code ran on the main thread and what ran on a background thread.

With Xcode 9, Apple introduced a new diagnostic setting called the Main Thread Checker which would warn you if you had any UI code running on a background thread.

This setting is supposed to be enabled by default, but if it is not, you can enable it quite easily — It’s highly recommended that you have it enabled at all times if possible since it can be quite invaluable.

➤ Click on the scheme dropdown in the Xcode toolbar and select Edit Scheme…

Edit scheme
Edit scheme

➤ Select Run in the left panel, switch to the Diagnostics tab, and make sure Main Thread Checker is checked under Runtime API Checking.

Main Thread Checker setting
Main Thread Checker setting

➤ Now, move the following line from outside the closure:

let url = self.iTunesURL(searchText: searchBar.text!)

To be inside the closure like this:

queue.async {
    let url = self.iTunesURL(searchText: searchBar.text!)
    ...
} 

➤ Run StoreSearch and do a search for an item, you should see something like the following in the Xcode Console:

Main Thread Checker: UI API called on a background thread: -[UISearchBar text]
PID: 12986, TID: 11267540, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:
4   StoreSearch                         0x000000010bccfa75 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_ + 469
5   StoreSearch                         0x000000010bcd0101 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_TA + 17
6   StoreSearch                         0x000000010bcd02bd $SIeg_IeyB_TR + 45
7   libdispatch.dylib                   0x000000010f3a1225 _dispatch_call_block_and_release + 12
8   libdispatch.dylib                   0x000000010f3a22e0 _dispatch_client_callout + 8
9   libdispatch.dylib                   0x000000010f3a4d8a _dispatch_queue_override_invoke + 1028
10  libdispatch.dylib                   0x000000010f3b2daa _dispatch_root_queue_drain + 351
11  libdispatch.dylib                   0x000000010f3b375b _dispatch_worker_thread2 + 130
12  libsystem_pthread.dylib             0x000000010f791169 _pthread_wqthread + 1387
13  libsystem_pthread.dylib             0x000000010f790be9 start_wqthread + 13
2018-07-28 11:39:02.726132+0200 StoreSearch[12986:11267540] [reports] Main Thread Checker: UI API called on a background thread: -[UISearchBar text]
PID: 12986, TID: 11267540, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:
4   StoreSearch                         0x000000010bccfa75 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_ + 469
5   StoreSearch                         0x000000010bcd0101 $S11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_TA + 17
6   StoreSearch                         0x000000010bcd02bd $SIeg_IeyB_TR + 45
7   libdispatch.dylib                   0x000000010f3a1225 _dispatch_call_block_and_release + 12
8   libdispatch.dylib                   0x000000010f3a22e0 _dispatch_client_callout + 8
9   libdispatch.dylib                   0x000000010f3a4d8a _dispatch_queue_override_invoke + 1028
10  libdispatch.dylib                   0x000000010f3b2daa _dispatch_root_queue_drain + 351
11  libdispatch.dylib                   0x000000010f3b375b _dispatch_worker_thread2 + 130
12  libsystem_pthread.dylib             0x000000010f791169 _pthread_wqthread + 1387
13  libsystem_pthread.dylib             0x000000010f790be9 start_wqthread + 13

You might also notice that the Xcode toolbar’s activity view now has a purple icon and that there’s a purple icon on the right corner of the jump bar, where errors are normally displayed.

Purple icons indicating Main Thread Checker issues
Purple icons indicating Main Thread Checker issues

If you click on the icon in the activity view, you will be taken to the Runtime tab of the Issue navigator, where you can click on a listed issue to be taken to the offending line in your source code:

Issue navigator
Issue navigator

And you finally see what the issue is — you access the data from a UI control, the Search Bar, in a background thread. It might be better to do this in the main thread. Since we created this issue to illustrate the background thread checker, the fix is simple, just move the line of code back to where it was originally.

Committing your code

➤ With this important improvement, the app deserves a new version number. So commit the changes and create a tag for v0.2. You will have to do this as two seprate steps — first create a commit with a suitable message, and then create a tag for your latest commit.

You can find the project files for this chaper under 35 – Asynchronous Networking 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.