Watch this episode for free — or access the full course as a raywenderlich.com Professional Subscriber. Learn more here
Beginning Networking with URLSession
Part 1: Introduction to URLSession & Concurrency Introduction to Modern Concurrency
— iOS & Swift

Lesson Complete

Play Next Lesson
Next

Introduction to Modern Concurrency

Introduction to the modern concurrency features of Swift.

Contributors

URLSession is designed to access networks beyond your user’s device. Doing so takes time; whether to upload data or respond to a chat message. That’s time the user can be doing other things.

This is where concurrency comes into play. Now, if you already have experience with concurrency and the modern concurrency features of Swift, then by all means, feel free to skip this episode, but if you are new to it then I definitely recommend you to follow along.

In a nutshell, concurrency means doing multiple things at once. When you write your code,

that code can be translated into a single path execution. You may have lots of objects communicating with each other but if you just follow the path of execution, you’ll see that it follows a linear path. This is evident when you set a breakpoint in the debugger and then step through each line of code.

You can think of this path of execution as a thread. In fact, you write all of your code in what is known as the main thread. This is the thread that runs your code but also the code that manages the user interface as well.

As you add threads to your code, you actually have more paths of execution. This means you can do multiple things at once. One thread can be responding to user input and another thread could be download files across the network.

A thread runs on a cpu core. The more cores that a device has,

the more threads can run at the same time. If there are more threads than resources to run them,

then the cpu will switch between them. One moment a thread is trucking along drawing a circle on the screen,

and then next moment it is asleep. Moments later it is awake unaware that it was ever asleep. Computers process information so fast that you can’t see the processing.

Writing synchronous code, or code that runs one step at a time, on the main thread is the most straightforward way to write code. all lines of code you write are performed on the main thread unless you, or the API you are using, do otherwise.

In this scenario if some function call or operation takes a fair bit of time to complete then your program’s interface might stop responding until this finished. Unresponsive, “choppy”, or “laggy” apps are poor user experience and should be avoided.

The alternative to this is to perform your time-consuming tasks asynchronously, or concurrently on multiple background threads, in order to keep the rest of your app running smoothly.

Swift has always supported concurrency by using classes like NSThread or Operation Queues, or libraries like Grand Central Dispatch (or GCD for short).

While great at what they do (and still useful if you need lower-level access or control over your concurrent code) they aren’t as integrated into the Swift language as its newer, more modern concurrency features are, nor are they as easy and straightforward to use in comparison.

The cool thing about Swift’s concurrency features is that you don’t need to worry about managing threads or queues on your own, it’s all taken care of for you. How about taking a look at Swift’s concurrency features?

To get started, open this episode’s Starter playground. The first thing you’ll do is create an unstructured Task, or an object that can help encapsulate a concurrent piece of work. Add the following code:

Task {
  print("Doing some work on a task.")
}

print("Doing work on the main actor (main thread).")

Task takes a trailing closure containing the work to perform. In this case you print a message inside the task, and you also print a message outside of the task. This outer print statement is being performed on the main actor, or main thread. In Swift concurrency, the term main actor is often used in place of main thread. But what is an actor?

From the Swift Programming Language guide we have that actors are a reference type like classes. Unlike classes, however, actors allow only one task to access their mutable state at the same time, making it safe for code on multiple tasks to interact with the same instance of an actor.

The MainActor is a globally-unique actor that performs tasks exclusively on the main thread. Expand upon the previous example by replacing the code with the following:

Task {
  print("This is first.")
    
  let sum = (1...100000).reduce(0, +)
  print("This is second: 1 + 2 + 3 ... 100 = \(sum)")
}

print("This is last.")

Run your code and look at the results. Not what you expecting? Or perhaps it was.

This is the main challenge of concurrent programming. The order of operations can change depending on many factors like how fast the device’s processor is, how intensive the task is, or how the operating system’s scheduler schedules your tasks.

What if you want to cancel a task? Once again, replace your code with the following:

let task = Task {
  print("This is first.")
    
  let sum = (1...100000).reduce(0, +)
  try Task.checkCancellation()
  print("This is second: 1 + 2 + 3 ... 100 = \(sum)")
}

print("This is last.")
task.cancel()

This code creates a Task with some work in it, and it gets stored in the task variable. It then calls cancel() on the task in order to cancel it. One important change to the task itself is the try Task.checkCancellation() statement. It checks a Boolean flag, Task.isCancelled, and throws an error that causes the task to unwind in case it was cancelled.

Since you cancel the task in this scenario, the last print statement inside your task never executes.

The important takeaway in this example is that you need to do a bit of extra work, using checkCancellation() in order to tell your code when and how cancellation should work.

Suspending a task is also possible for when you want some time to pass between operations. As an example, this can be useful if you want to show a view in your app or game for a few seconds and then dismiss it. Replace your code with the following:

Task {
  print("Starting the task.")
  try await Task.sleep(nanoseconds: 1_000_000_000)
  print("Finishing the task.")
}

Two new keywords are being used in this task when compared to the previous ones. try, that indicates the function call to sleep can fail, and await, that indicates the sleep operation is performed asynchronously and can suspend and resume execution later.

For now you’ve been performing these operations on their own in your playground, but what if you want to move this task’s functionality into a…function? Replace the code in your playground with the following:

func performTask() {
  print("Starting the task in a function.")
  try await Task.sleep(nanoseconds: 1_000_000_000)
  print("Finishing the task in a function.")
}

You get a couple of errors. Functions that can throw an error need to be marked with throws, so update your function to indicate that:

func performTask() throws {

The second error is because your function in its current form does not support concurrency, but the sleep call does and you await for its execution. To fix the issue, add the async keyword before the throws:

func performTask() async throws {

Next, and this code to asynchronously call your function:

Task {
  try await performTask()
}

You might have noticed some similarity between throwing functions and async functions. You need indicate either (or both) in the declaration and then, when called, use try await. This is an intentional pattern to keep things consistent between your function declarations and how you call them.

Tasks are pretty cool, as you have seen, but how about we jump a few steps and download some JSON from the internet? In the next example you’ll asynchronously download and decode all of the learning domains on the site. This will entail: 1. Asynchronously downloading data from a URL. 2. Decoding the data from JSON into your own Swift types.

Replace the code in your playground with the following:

func fetchDomains() async throws -> [Domain] {
  []
}

You declare a function that indicates it might take some time to perform, and thus can suspend, (indicated by the async keyword), and that it can fail (indicated by the throws keyword). If successful this function will return a list of Domain values.

The starter playground contains the Domains.swift file that has created Swift types to match the JSON response and its hierarchy. Next, replace the contents of your function with the following:

func fetchDomains() async throws -> [Domain] {
  let url = URL(string: "https://api.raywenderlich.com/api/domains")!
  let (data, _) = try await URLSession.shared.data(from: url)
    
  return try JSONDecoder().decode(Domains.self, from: data).data
}

With just a few keywords sprinkled across your code you get asynchronous functionality. Looking at what each line of code inside the function does:

  1. First, you create the url that you want to download data from. As this is a trusted URL we know about, and to simplify the code for this example, its force unwrapped.

  2. Next, you get your first taste of URLSession in order to download the data from the URL and receive its response. Since this method is asynchronous in the URLSession API you call it with await. This will free up your program to do other things while it waits for this network operation to complete. Since this call can also throw errors you mark it with try. This call returns a tuple containing the data and response, since the response is not going to be used you ignore it with the underscore.

  3. Finally, you use JSONDecoder to decode the data that was returned from the URL and then grab the list of domains stored in the data property.

If you are coming from the world of GCD or queues, this code might look surprisingly simple and clean in comparison. Add the following code after your function declaration in order to test it:

Task {
  do {
    let domains = try await fetchDomains()
    for (index, domain) in domains.enumerated() {
      let attr = domain.attributes
      print("\(index + 1)) \(attr.name): \(attr.description) - \(attr.level)")
    }
  } catch {
    print(error)
  }
}

This should look familiar to you, but let’s run through things once more: 1. You create a Task context that you can await, or use to perform asynchronous calls. 2. Next up you create a do-catch block to try and catch any potential errors. 3. Inside the do block you perform the actual download. The await keyword recognizes that the task can suspend while other things are happening. 4. With your domains downloaded, you print out the list of learning domains.

And there you have it. You’ve now had an introduction to Swift’s modern concurrency features and also seen a brief example of how to work with URLSession. In the next episode you’ll continue looking at Swift’s modern concurrency features. See ya there! :)

Reviews

Comments