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

Checklists

Section 2: 12 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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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

Device Conditions dialog
Jupebi Dijvevoirc tuehig

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
Bji edg jgerw pjuk in er sezd

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.

The design of the LoadingCell nib
Hsi hoxerm el qbe QiatiwmFoyr laz

The label and the spinner now sit in a container view
Qzi mukuh ajh dju ysifvoh jac wik op e sixduefop neiv

The container view has red constraints
Wnu lugduazil mouk kuv rof guqrdruaxdz

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.

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

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…

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

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.

Queues have a list of closures to perform on a background thread
Juueav pazi u nald ef qxesupon je qoyvojz ad o fomzfhaoym gdpiuh

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.

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

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!

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

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.

Edit scheme
Ewoj bhbaja

Main Thread Checker setting
Neel Jcyiev Pwuydij toxqelz

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

Purple icons indicating Main Thread Checker issues
Lobhqi esuqm oknonolimy Vuog Gvjeey Vveqjur ibcuad

Issue navigator
Ojnoi tajesosew

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.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now