Grand Central Dispatch Tutorial for Swift 5: Part 2/2

Learn all about multithreading, dispatch queues, and concurrency in the second part of this Swift 5 tutorial on Grand Central Dispatch. By David Piper.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Testing Asynchronous Code

This might sound like a crazy idea, but did you know that Xcode has testing functionality? :] Writing and running tests is important when building complex relationships in code.

Xcode tests are all contained in subclasses of XCTestCase. They're any method whose signature begins with test. Tests run on the main thread, so you can assume that every test happens in a serial manner.

As soon as a given test method completes, Xcode considers the test to have finished and moves on to the next test. This means that any asynchronous code from the previous test will continue to run while the next test is running.

Networking code is usually asynchronous, since you don't want to block the main thread while performing a network fetch. That, coupled with the fact that tests finish when the test method finishes, can make it hard to test networking code.

Take a brief look at how you can use semaphores to test asynchronous code.

Using Semaphores

Semaphores are an old-school threading concept introduced to the world by the ever-so-humble Edsger W. Dijkstra. Semaphores are a complex topic because they build upon the intricacies of operating system functions.

If you want to learn more about semaphores, check out this detailed discussion on semaphore theory. If you're the academic type, you may also want to check out Dining Philosophers Problem, which is a classic software development problem that uses semaphores.

Open GooglyPuffTests.swift and replace the code inside downloadImageURL(withString:) with the following:

let url = try XCTUnwrap(URL(string: urlString))

// 1
let semaphore = DispatchSemaphore(value: 0)
_ = DownloadPhoto(url: url) { _, error in
  if let error = error {
    XCTFail("\(urlString) failed. \(error.localizedDescription)")
  }

  // 2
  semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)

// 3
if semaphore.wait(timeout: timeout) == .timedOut {
  XCTFail("\(urlString) timed out")
} 

Here's how the semaphore works in the code above:

  1. You create a semaphore and set its start value. This represents the number of things that can access the semaphore without needing to increment it. Another name for incrementing a semaphore is signaling it.
  2. You signal the semaphore in the completion closure. This increments its count and signals that the semaphore is available to other resources.
  3. You wait on the semaphore with a given timeout. This call blocks the current thread until you signal the semaphore. A non-zero return code from this function means that the timeout period expired. In this case, the test fails because the network should not take more than 10 seconds to return — a fair point!

Run your tests by selecting Product ▸ Test from the menu or using Command-U, if you have the default key bindings. They should all succeed:

Screenshot of Xcode showing GooglyPuffTests. All tests succeeded.

Disable your connection and run the tests again. If you're running on a device, put it in airplane mode. If you're running on the simulator, turn off your connection. The tests complete with a fail result after 10 seconds. It worked!

Screenshot of Xcode showing GooglyPuffTests. All tests failed. An error alert shows that the internet connection appears to be offline.

These are rather trivial tests. But if you're working with a server team, they can prevent finger-pointing of who is to blame for the latest network issue.

Note: When you implement asynchronous tests in your code, look at XCTWaiter first before going down to these low-level APIs. XCTWaiter's APIs are much nicer and provide a lot of powerful technology for asynchronous testing.

Using Dispatch Sources

Dispatch sources are a particularly interesting feature of GCD. You can use a dispatch source to monitor for some type of event. Events can include Unix signals, file descriptors, Mach ports, VFS Nodes and other obscure stuff.

When setting up a dispatch source, you tell it what type of events you want to check. You also need to define the dispatch queue on which its event handler block should execute. You then assign an event handler to the dispatch source.

Upon creation, dispatch sources start off in a suspended state. This allows you to perform any extra configuration required — such as setting up the event handler. After configuring your dispatch source, you must resume it to start processing events.

In this tutorial, you'll get a small taste of working with dispatch sources by using it in a rather peculiar way: to monitor when your app goes into debug mode.

Open PhotoCollectionViewController.swift and add the following just below the backgroundImageOpacity global property declaration:

// 1
#if DEBUG

  // 2
  var signal: DispatchSourceSignal?

  // 3
  private let setupSignalHandlerFor = { (_ object: AnyObject) in
    let queue = DispatchQueue.main

    // 4
    signal =
      DispatchSource.makeSignalSource(signal: SIGSTOP, queue: queue)
        
    // 5
    signal?.setEventHandler {
      print("Hi, I am: \(object.description ?? "")")
    }

    // 6
    signal?.resume()
  }
#endif

The code is a little involved, so walk through it step-by-step:

  1. You compile this code only in debug mode to prevent "interested parties" from gaining a lot of insight into your app. :] Add -D DEBUG under Project Settings ▸ Build Settings ▸ Swift Compiler - Custom Flags ▸ Other Swift Flags ▸ Debug. It should be set already in the starter project.
  2. Declare signal variable of type DispatchSourceSignal for use in monitoring Unix signals.
  3. Create a block assigned to the setupSignalHandlerFor global property. You use it for one-time setup of your dispatch source.
  4. Here, you set up signal. You're interested in monitoring the SIGSTOP Unix signal. The main queue hands received events — you'll discover why shortly.
  5. Next, you register an event handler closure that's invoked whenever you receive the SIGSTOP signal. Your handler prints a message that includes the class description.
  6. All sources start off in the suspended state by default. Here, you tell the dispatch source to resume so it can start monitoring events.

Add the following code to viewDidLoad() below the call to super.viewDidLoad():

#if DEBUG
  setupSignalHandlerFor(self)
#endif

This code invokes the dispatch source's initialization code.

Build and run the app. Pause the program execution and resume the app immediately by pressing the Pause then Play buttons in Xcode's debugger:

Screenshot of Xcode. The pause execution button is highlighted.

Check out the console. Here's what you'll see:

Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7fbf0af08a10>

Your app is now debugging-aware! That's pretty awesome, but how would you use this in real life?

You could use this to debug an object and display data whenever you resume the app. You could also give your app custom security logic to protect itself (or the user's data) when malicious attackers attach a debugger to your app.

An interesting idea is to use this approach as a stack trace tool to find the object you want to manipulate in the debugger.

Time to flex some GCD muscles.

Cool bird with sunglasses

Think about that situation for a second. When you stop the debugger out of the blue, you're almost never on the desired stack frame. Now you can stop the debugger at any time and have code execute at your desired location. This is very useful if you want to execute code at a point in your app that's tedious to access from the debugger. Try it out!

Put a breakpoint on the print() statement inside the setupSignalHandlerFor block that you just added.

Pause in the debugger, then start again. The app will hit the breakpoint you added. You're now deep in the depths of your PhotoCollectionViewController method. Now you can access the instance of PhotoCollectionViewController to your heart's content. Pretty handy!

Note: If you haven't already noticed which threads are which in the debugger, take a look at them now. The main thread will always be the first thread. It's followed by libdispatch, the coordinator for GCD, as the second thread. After that, the thread count and remaining threads depend on what the hardware was doing when the app hit the breakpoint.

In the debugger console, type the following:

expr object.navigationItem.prompt = "WOOT!"

The Xcode debugger can sometimes be uncooperative. You might get this message:

error: use of unresolved identifier 'self'

If you do, you have to do it the hard way to work around a bug in LLDB. First, take note of the address of object in the debug area:

po object

Then, manually cast the value to the type you want by running the following commands in the debugger, replacing 0xHEXADDRESS with the outputted address:

expr let $vc = unsafeBitCast(0xHEXADDRESS, to: GooglyPuff.PhotoCollectionViewController.self)
expr $vc.navigationItem.prompt = "WOOT!"

If this doesn't work, lucky you – you encountered another bug in LLDB! In that case, you may have to try building and running the app again.

Once you've run this command successfully, resume execution of the app. You'll see the following:

The initial view controller, but this time the navigation bar shows the text WOOT!.

With this method, you can make updates to the UI, inquire about the properties of a class and even execute methods — all while not having to restart the app to get into that special workflow state. Pretty neat.