Previous episode: 01. Introduction
Next episode: 03. More Modern Concurrency
Get immediate access to this and 4,000+ other videos and books.
Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and
4,000+ professional videos in a single subscription, it's simply the best investment you can make in
your development career.
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're new to it, then I definitely recommend you 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 execution path. You may have lots of objects communicating with each other, but if you're just following 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 downloading 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 search between them. One moment, a thread is trucking along, drawing a circle on the screen, and the 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're 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 finishes. Unresponsive, choppy, or laggy apps are a 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. Run your code. "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 to a mutable state at the same time, making it safe for code in multiple tasks to interact with the same instance of an actor. The main actor 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. The expected output here is that you got first, last, and then second, meaning the statements executed out of order if looked at from top to bottom. Run your code and look at the results. Not what you were expecting, perhaps? 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. Run your code. 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 canceled. 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. Run your code. Two new keywords are being used in this task, when compared to the previous one: "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. 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. 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." Next, add this code to asynchronously call your function. And run your code. You might have noticed some similarity between throwing functions and async functions. You need to 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 ahead, 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 asynchronously downloading data from a URL, and decoding the data from JSON into your own Swift types. Replace the code in your playground with the following. 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. With just a few keywords sprinkled across your code, you get asynchronous functionality. Looking at what each line of code inside the function does, 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, it's force-unwrapped. 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 the 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. Finally, 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're 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. Run your code. This should look familiar to you, but let's run through things once more. You create a task context that you can await, or use to perform asynchronous calls. Next up, you create a do-catch block to try and catch any potential errors. Inside the "do" block, you perform the actual download. The "await" keyword recognizes that the task can suspend while other things are happening. 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 you there.
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development. Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.