35.
Asynchronous Networking
Written by Fahim Farook
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.
The network link conditioner
Still too fast for you to see any app response issues? Then use the Network Link Conditioner. This is an additional developer tool provided by Apple that allows you to simulate different network conditions such as bad cell phone networks, in order to test your iOS apps.
But first, before you can use it, you probably have to install the Network Link Conditioner since that is not something that is installed by default either as part of macOS or as part of your Xcode installation.
➤ Select Open Developer Tool ▸ More Developer Tools… from the Xcode menu.
This should open the More Downloads for Apple Developers web page in your default browser — you might be asked to login to the Apple Developer portal first since this is a resource which is only available to registered Apple developers.
➤ If you opened the site via the Xcode menu, then it should open as shown in the screenshot, pre-set to search for “for Xcode”. You should get a list of different downloads including ones for “Additional Tools”. Select the most recent download for Additional Tools based on Release Date, download it, open the DMG file, switch to the Hardware folder on the DMG, and then double-click Network Link Conditioner.prefPane to install it.
Note: If you already have the Network Link Conditioner installed, you’ll get an error about the Network Link Conditioner being installed with macOS and that it can’t be replaced.
If you do see this error, go to your System Preferences, right click on the existing Network Link Conditioner and select to remove the preference pane option. You should be able to install the new version after that without any issues.
The Network Link Conditioner is now available to you to use as a System Preferences panel option.
➤ If you want to find it later, open System Preferences on your Mac and locate Network Link Conditioner — it should be in the last section of items, at the very bottom.
Let’s simulate a really slow connection.
➤ Click on Manage Profiles and create a new profile by clicking the plus button on the bottom left. Add the following settings:
- Name: Very Slow Connection
- Downlink Bandwidth: 48 Kbps
- Downlink Packets Dropped: 0 %
- Downlink Delay: 5000 ms, i.e. 1 second
Press OK to add this profile and return to the main screen. Make sure this new profile is selected and flick the switch to ON to start the Network Link Conditioner.
➤ Now run the app and search for something. The Network Link Conditioner tool will delay the HTTP request by 5 seconds in order to simulate a slow connection, and then downloads 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.
Note: While the Network Link Conditioner has worked reliably for previous iOS versions, my results with the latest iOS from the betas up to GM has been very spotty. It would sometimes work flawlessly, and sometimes it would not work at all. I don’t know whether this is due to changes in the Network Link Conditioner or changes to the iOS simulator itself or both.
But if you do experience these issues, know that this is just issues with the Network Link Conditioner itself. Just turning the Network Link Conditioner off should restore normal functionality.
Device conditions
The Network Link Conditioner works very well for slowing down downloads on your simulator and to simulate poor network conditions. But it doesn’t work for actual devices. Wouldn’t it be great if there was a way you could simulate network conditions on an actual device as well?
Actually, there is a way.
➤ Get your iPhone, iPad or iPod touch device and connect it to your Mac.
➤ Select Window ▸ Devices and Simulators from the Xcode menu.
➤ Select the Devices tab and select your phone from the available list of device.
➤ Scroll down to Device Conditions section at the bottom of the screen.
➤ Under Condition select Network Link.
➤ Select the network profile you want to use from the Profile dropdown.
➤ Click the Start button.
You can also enable the Network Link Conditioner from your device itself. Go to Settings ▸ Developer ▸ Network Link Conditioner.
Note: If you do not see the Developer option in the iOS Settings app, you might need to connect your iPhone to your computer via a USB cable, and launch the Xcode Devices and Simulators window so that your device is recognized by Xcode as a developer device.
Now you can create poor network conditions either on simulator or device!
Using these tools to test whether your app can deal with real-world network conditions is a must! Not every user has the luxury of broadband…
The activity indicator
Did you notice how the app totally doesn’t respond during a search operation? 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.”
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 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. Set the label’s text color to the ArtistName color asset.
➤ Drag a new Activity Indicator View into the cell and put it next to the label. Make sure its Style is set to Medium and give it a Tag of 100.
The design should look like this:
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 ▸ Stack View. This puts a white view behind the selected views.
This puts both views in a specialized container view known as stack view which can be used to display multiple views either horizontally as a row or vertically as a column. You’ll learn more about stack views later on.
➤ With the stack view selected, click the Align button and put checkmarks in front of Horizontally in Container and Vertically in Container to make new constraints.
Cool, you now have a cell that automatically adjusts itself to any size screen.
Use 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!
Test 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 the oldest iPhone and iPad models had one core, which meant it can only do one thing at a time. Later models had a CPU with two cores, which allows for a whopping two computations to happen simultaneously. The latest Apple mobile CPUs have six cores.
With limited cores available, how can modern computers have many more applications and other processes — I count 287 active processes on my Mac right now — running at the same time?
To get around the hardware limitation of having only a few 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. I just mentioned that 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 nine 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 nine 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!
Make 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. I won’t 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.
Put 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:
-
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.
-
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. -
Inside the closure, I have removed 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.
Put UI updates on the main thread
The reason I removed 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
I mentioned previously that you should not run UI code on a background thread. However, till just a few years ago, 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.
Then 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 — I would suggest 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…
➤ Select Run in the left panel, switch to the Diagnostics tab, and make sure Main Thread Checker is checked under Runtime API Checking.
➤ Now, move the following line from outside the closure in searchBarSearchButtonClicked(_:):
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: 92439, TID: 13807148, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:
4 StoreSearch 0x000000010785e823 $s11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_ + 291
5 StoreSearch 0x000000010785eec8 $sIeg_IeyB_TR + 40
6 libdispatch.dylib 0x0000000107a39034 _dispatch_call_block_and_release + 12
. . .
You might also notice that Xcode’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.
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:
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 :]
Commit your code
➤ I think 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 separate 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 chapter under 35-Asynchronous-networking in the Source Code folder.