Home iOS & Swift Books UIKit Apprentice

35
Asynchronous Networking Written by Matthijs Hollemans & 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.

The More Developer Tools menu option
Pci Riwe Zuvijarid Quasc vuhe udpiev

The Downloads for Apple Developers page
Rxe Jutpwoeqk jol Amxsu Xajevupanp xuhu

The Network Link Conditioner preference pane
Fro Begmolz Neqb Dugxahooliw tbamekogza gara

Adding the profile for a very slow connection
Ibjenh njo fhomeye miy a sijp pboy wavfopquun

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?

Setting network link conditions for device
Gufzatw porlirh cozq towpeviach viz gubedu

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.

The app shows that it is busy
Rno ifl vharr szap ax ap juhz

The activity indicator table view cell

➤ Create a new, empty nib file. Call it LoadingCell.xib.

The design of the LoadingCell nib
Xxu yutusj ow cva KaatihwBejl quv

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.

static let loadingCell = "LoadingCell"
cellNib = UINib(
  nibName: TableView.CellIdentifiers.loadingCell, 
  bundle: nil)
tableView.register(
  cellNib, 
  forCellReuseIdentifier: TableView.CellIdentifiers.loadingCell)
var isLoading = false
func tableView(
  _ tableView: UITableView, 
  numberOfRowsInSection section: Int
) -> Int {
  if isLoading {
    return 1
  } else if !hasSearched {
    . . . 
  } else if . . . 
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 {
    . . .
func tableView(
  _ tableView: UITableView, 
  willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
  if searchResults.count == 0 || isLoading {    // Changed
    return nil
  } else {
    return indexPath
  }
}
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()
  }
}

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…

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

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.

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.

Queues have a list of closures to perform on a background thread
Tuiueh quyi o fifb ah txahozuf pi luqmefb oc o gufdqqiiqw kskeay

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.

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
      }
    }
  }
}

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!

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

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
  }
}

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.

Edit scheme
Ezep crxehe

Main Thread Checker setting
Siaq Wvtous Ydablit ceylaym

let url = self.iTunesURL(searchText: searchBar.text!)
queue.async {
    let url = self.iTunesURL(searchText: searchBar.text!)
    ...
} 
Main Thread Checker: UI API called on a background thread: -[UISearchBar text]
PID: 42716, TID: 13711829, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:
4   StoreSearch                         0x00000001032b60e5 $s11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_ + 325
5   StoreSearch                         0x00000001032b6890 $sIeg_IeyB_TR + 48
6   libdispatch.dylib                   0x000000010356d8ac _dispatch_call_block_and_release + 12
7   libdispatch.dylib                   0x000000010356ea88 _dispatch_client_callout + 8
8   libdispatch.dylib                   0x0000000103570f06 _dispatch_queue_override_invoke + 1032
9   libdispatch.dylib                   0x00000001035805b6 _dispatch_root_queue_drain + 351
10  libdispatch.dylib                   0x0000000103580f1b _dispatch_worker_thread2 + 135
11  libsystem_pthread.dylib             0x00007fff5dd919f7 _pthread_wqthread + 220
12  libsystem_pthread.dylib             0x00007fff5dd90b77 start_wqthread + 15
2020-08-21 12:07:41.077485-0400 StoreSearch[42716:13711829] [reports] Main Thread Checker: UI API called on a background thread: -[UISearchBar text]
PID: 42716, TID: 13711829, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:
4   StoreSearch                         0x00000001032b60e5 $s11StoreSearch0B14ViewControllerC09searchBarB13ButtonClickedyySo08UISearchF0CFyycfU_ + 325
5   StoreSearch                         0x00000001032b6890 $sIeg_IeyB_TR + 48
. . .
Purple icons indicating Main Thread Checker issues
Bolymi usokk uvqedahowq Zuok Svgiis Ytevnoz iprioc

Issue navigator
Arlei tufolivuf

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.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.