Discover Swift concurrency in action: Follow along as we update an existing sample app. Get real-world experience with async/await, actors, and continuations. We'll also explore techniques for migrating existing code to Swift concurrency over time.
To get the most out of this code-along, we recommend first watching “Meet async/await in Swift” and “Protect mutable state with Swift actors” from WWDC21.
Note: To create an async task in Xcode 13 beta 3 and later, use the Task initializer instead.
♪ Bass music playing ♪ ♪ Ben Cohen: Hi, I'm Ben from the Swift team, and in this video, I'm going to walk through porting an existing application over to use Swift's new concurrency features. We'll see how these new features help you write clearer asynchronous code, and protect against possible race conditions, as well as look at some techniques for gradually moving your code to this new way of operating. I'm going to be using an app called Coffee Tracker, and it's based on a talk from WWDC 2020 about creating and updating watch complications. This is a simple app that lets you track all the coffee you've drunk today, as well as complication to show your current caffeine level on a watch face. It's a nice example to use for our purposes because although it's a small app, it shows lots of different things we want to think about, including how concurrency works with SwiftUI, delegate callbacks from the watch SDK, some IO, and interfacing with asynchronous APIs in Apple's SDK. Now let's take a quick tour of the app. It's roughly broken up into three layers. First, there's the UI layer. This is mostly SwiftUI views, but in this we can also consider things like the complication data source as part of the UI layer. Next there's the model layer that comprises a couple of simple value types to represent caffeine drinks, as well as well as a model type called Coffee model. Now, this what you might call the UI Model. That is, the place where you hold data for display by the UI layer. It's an observable object that feeds our SwiftUI view, and all of the updates on it will need to be made on the main thread. I'm referring to it as a UI model because it might not be the full model of all of your application's data. It might just be a projection of your data model or a subset of it; just what you need to display on the UI at this moment. Finally, we have what you can think of as the back-end layer: processing that happens, probably in the background, to populate your model or talk to the world outside your app. In our case, that's represented by this HealthKitController type that manages communication to HealthKit to save and load the user's caffeine intake. Now, before we start looking at code, let's talk about how concurrency is managed in the app. This architecture looks fairly clean, but when we layer on how concurrency is handled, we get a much messier picture. This app is essentially broken up into three concurrent queues on which code can be executing. Work in the UI and on the model is done on the main queue. The app also has a dispatch queue for doing work in the background. And finally, certain callbacks into completion handlers -- like those returning results from HealthKit -- are done on arbitrary queues. This is a pretty common situation. What seems like a simple application architecture ignores a lot of the hidden complexity in how it handles concurrency. Now, for a quick spoiler. In adopting Swift concurrency, we're going to go from this ad hoc concurrency architecture to something that will look like this. We're going to put our UI views and model on what's called the main actor. We're going to create new actors to operate in the background, and these types will pass thread-safe values between each other using the async/await feature. When we're done, the concurrency architecture should be as clear and easy to describe as the type architecture. Now we've used a few terms here you might not be familiar with, like async/await and actors, and we'll explain them briefly as we use them in code. But for a more in-depth explanation of these features, there are several other talks available that go into much more detail. So now we've seen the overall architecture, let's dive into the code. So here we have a few different types. We have some of the SwiftUI View layers as well as the extension delegate for showing information on the watch face. And then we have a simple model type for modeling drinks. And the UI model, CoffeeData, that aggregates an array of them and publishes to SwiftUI. Finally, we have the HealthKitController. And it's in this layer that I'm going to get started introducing some of Swift's new concurrency features to our app. The HealthKitController contains a few different calls to the HealthKit SDK that take completion handlers. Let's start by looking at this controller's save operation. So hit Control-6, and that brings up the list of functions in this file, and we're going to go to the save operation. Now, before we even get into the new concurrency features, let's talk a bit about thread safety in Swift today. This code accesses some variables like isAvailable and store. Now, it looks like we're only reading these variables in this function. Is that safe? Well no, not if other code could be writing to them at the same time. To know whether or not this code is thread-safe, I need more information than just looking at this function. There's no use of dispatch queues or locks in it, so whatever makes this code thread-safe -- assuming it is -- must be elsewhere. Maybe the caller serializes all calls to save through a queue. Or maybe the app is constructed in some way that means it's OK. But I can't know this just by looking at this function. Being able to do that -- to look at this function and know certain things without having to go look at other parts of my program -- is what we call local reasoning, and it's a really important goal for Swift. For example, Swift's emphasis on value types is about local reasoning. Unlike a reference type, you don't have to worry that a value type you've been passed is being mutated elsewhere in your program. A lot of the language features being introduced for concurrency in Swift 5.5 are about giving you more opportunity to reason locally about your code. Now, as it happens, this function is entirely thread-safe, but I figured that out myself; the compiler isn't helping me spot these issues. So, down here we have a call to the HealthKit SDK to save a caffeineSample into the user's health data. And this takes a completion handler, and that completion handler takes two values: success or error. If the operation succeeds, there's no error; the error will be nil. This means we need to remember to check the status, and then, when appropriate, unwrap the optional error down here. Now, this isn't normally how we handle errors in Swift. This would be much better as a method that can throw on failure. But that approach doesn't work with completion handlers. But now, with async methods, we can have asynchronous functions that can throw. This HealthKit save method now has an async equivalent that does just that, so let's switch over to using it. To do that, we remove the completion handler, and we write await in front of our new asynchronous method. This await reminds us that it's an async function, and the code may suspend at this point and allow other code to run. We'll come back to why this is important later. OK. So now that we've done that, the compiler is telling us that -- as I mentioned -- this code can now throw. And this is a big benefit of async functions; they can throw. There's no more having to remember to check the optional error. So to resolve this, we add the try, and I just want to handle the error right here. So we wrap this in a do-catch block. We don't need this guard or force unwrap anymore. And we can rearrange our code to keep the happy path all together, with the successful logging right underneath the call to save. Notice that save no longer returns a value. Returning success/failure was really duplicative with the error, so our new function only either throws or it's succeeded.
Now that we've added the try-catch, we're getting one more error from the compiler. We're calling an asynchronous function, but we're calling it from within a synchronous function. This does not work. Asynchronous functions have a capability that synchronous functions don't have: the ability to give up control of the thread they're running on while they await. To do this, they have a separate way of handling their stack frame, and this isn't compatible with synchronous functions. Now one option is to make this synchronous function into an async function. So let's do that. And now, this file compiles. But the whole project doesn't compile yet. Making this function async has pushed up the problem a level. Here in my data model, I'm now getting that same compiler error because this function isn't async. Now I could keep going up the chain, but for now, let's see another technique to keep the changes localized. To call my async function, I'm going to spin off a new asynchronous task... ...and that will be allowed to call async functions. This async task is very similar to calling async on the global dispatch queue. You can't return a value from it back to the outer function because the block executes concurrently. So whatever you do within the detached closure needs to be self-contained. In this case, we're just calling save, which doesn't return a value, so that's fine. You also need to be careful that you're not touching global state that might get mutated simultaneously from other threads. This is where it was relevant that save was completely thread-safe, otherwise we might be accidentally introducing new race conditions by adding this asynchronous task. Now that we've put it inside an async task, our awaited function compiles, and we've finished our first use of async/await in our app. We can compile and run it. Let's do another, and this time, let's look at some other techniques when migrating to async. So let's go to the requestAuthorization function. Unlike save, this function takes a completion handler. I'm going to create a second version of the function to become async while keeping the completionHandler version. That way other parts of our code that call it with a completion handler can keep on working. I can do this easily by using the "Create Async Alternative" refactoring action. That's available in the Code Action menu, which bring up with Command-Shift-A. And I choose the option to add the async alternative. So this has added a second version of the function from the original code, and it's replaced the original completion handler code with code that creates a new async task and then just calls into the new async version of the function. Notice the refactoring action has added a deprecation warning. This is going to help guide me to parts of my code that could benefit from refactoring to call the new async version instead. Now, let's undo and go back for a moment to that original completionHandler version. Inside this requestAuthorization function, the callback can happen on an arbitrary thread. So you need to know that the code in it is thread-safe. But I don't think it is. This assignment here to isAuthorized, might happen simultaneously with other code reading this value on other threads. And there's another example of a lack of local reasoning in this code after it. This completionHandler is called, but I have no idea whether that code is thread safe. I'd have to go and look at the call sites to this function to know if that completion handler code is thread-safe. Now, let's redo again to see the refactored version. Now remember, a new async task also runs on an arbitrary thread. So this forwarding version has similar problems to the completion handler version. We haven't made our code any safer quite yet, but we haven't made it any worse either. We'll fix this soon by introducing actors into our code. But for now, we should note that just because we've converted this function to async does not mean we're free from race conditions. In fact, you should be aware of the risk of introducing new race conditions into your code if you only perform refactoring to introduce async functions. Now let's look at this new async function. The refactoring action has already converted the completion handler to a call to the new async version of requestAuthorization. But converting this function to async has highlighted something interesting. Here, when we're using a completion handler technique, we had a return without calling the completion handler. And this was probably a bug. The caller would have been left hanging. But with async functions, you must return a value, so this bug is actually impossible. So we can replace this entry here with a return of false to indicate it's failed. And just like before, the new async version of requestAuthorization doesn't return a success boolean; it just either succeeds or throws. So we need to just return true on the happy path and false in case of failure. And now, our whole project compiles because the old code elsewhere can still call the completion handler version that's just forwarding on to the async version. And we're left with these deprecation warnings telling us about future refactorings we can make. OK, one more async conversion: the function to load data from HealthKit. This one is a little bit trickier. We're going to start just like last time, creating a stub for the old code to call. Then moving to the async version, incidentally, this one takes an optional completion handler with an async function that would be analogous to a discardable result. And then we start going through, replacing all of the completion handler code. But once we get here, we hit a snag, and it has to do with how the HealthKit query API arranged. The completion handler is on the query type here. But really, what we want to await is the execution of the query down here. Incidentally, this hopping up and down around the function is another thing that async/await is great at resolving. So, what I want to do is create a single async function that both creates the query and then executes it. We're going to do that now using a technique called a continuation. So let's go up here, and we're going to create a helper function. Now, I could do all of this inside the existing function but it might get a bit messy, so I like to keep it as a separate helper function. This function is going to query HealthKit. It'll be async, so we can await it, and it'll throw because the query operation can fail. And the function is going to return the useful values that I get back normally through the completion handler down here. So now I'm going to go and take my querying code just down to here, and I'm going to move it up into my helper function. And I'm also going to take the execution of the query from down the bottom. Now, I need to somehow invert this code so that it can await the completion handler and return these values back out of my async function. And that's where we use a continuation.
So at the start of this function, we're going to return the result of awaiting a withCheckedThrowingContinunation call. That's going to take a block that takes a continuation that we'll use to resume. And then I'm going to take all of the code that I want to execute and move it up into that continuation. Then we use the continuation to replace our completion handlers... ...by throwing the error, in the case of failure, or... resuming returning...
...these useful values.
Now that we have this awaitable function, we can use it in our original code. So we just assign the results of awaiting our new query. And I want to handle the error, so I put it inside a do-catch block. And inside the catch, I think we actually want to pull down this code from the helper function. So just log our failure. And then we return false. And then we take the remainder of the code and move it up into the happy path.
Finally, we need to address this closure down here that's still using the completion handler. Here, we're using DispatchQueue.main.async to get back onto the main thread. But we've ditched our completion handler, so there's no way of relaying this information back to our caller using that form. We need an alternative. To resolve this, we're going to make our first use of actors. In Swift's concurrency model, there is a global actor called the main actor that coordinates all operations on the main thread. We can replace our dispatch main.async with a call to MainActor's run function. This takes a block of code to run on the MainActor. run is an async function, so we need to await it. Awaiting is necessary because this function may need to suspend until the main thread is ready to process this operation. But because we await it, we can remove our completionHandler use, and instead, once this block completes, we can just return true.
Next, the compiler is giving me an error about a captured variable. Now this is a new error that only occurs inside asynchronous functions. Because closures in Swift capture variables by reference, when you capture mutable variables -- in this case, our newDrinks array -- you create the possibility for shared mutable state, and that can be the source of race conditions. So when doing this, you need to ensure you're making an immutable copy first. One way to do this is to add newDrinks to the closure's capture list. And this will automatically make an immutable copy that can be used within the closure. But often it's better to just avoid this problem by not having mutable variables in the first place. Here, we can do this by changing our newDrinks. It's written this way because of the optionality of the completionHandler. But if we instead turn it into a let, and then in the else path, just assign an empty value... ...the issue is resolved. Since value types declared with let are immutable, it's completely safe to share them across closures. Let's keep talking about the main actor by taking a look at this function that needs to run on the main thread. So we can bring up the Actions menu and Jump to Definition. And at the top of this function, there's something that's a really great idea: an assert that the function is correctly running on the main thread. If you ever made a mistake and called this function without wrapping it in a dispatch async to the main thread, you'd get an error in your debug builds, and you should definitely adopt this practice in some of your existing code. But this approach does have some limitations. You might forget to put an assert everywhere it's needed, and you can't assert on access to stored properties, or at least not without lots of boilerplate. It's much better if the compiler can enforce some of these rules for you, so you can't make mistakes like this at all. And that's how we use the main actor. I can annotate functions with @MainActor.
And that requires that the caller switch over to the MainActor before the function is run. Now that I've done this, I can remove the assertion because the compiler won't let this function be called anywhere other than on the main thread. We can prove it by going back to the call site and moving one of the calls outside of our MainActor.run. And you'll see the compiler tells us no, we can't call that from here because we're not on the MainActor. Here's a way to think about this feature: it's a lot like optional values. We used to have values like pointers and had to remember check for nil, but it was easy to forget, and it's much better to let the compiler make sure this check always happens along with some language syntactic sugar to make it easier. Here, we're doing a similar thing, only instead of enforcing nil checks, it's enforcing what actor you're running on. Now that we've put this function on the MainActor, we don't, strictly speaking, need this MainActor.run anymore. If you're outside of an actor, you can always run functions on that actor by awaiting them. And in fact, that's what the compiler is telling us here. So we could accept the fix-it and await updateModel. Here, we're using await on a synchronous function; updateModel is synchronous. But the await indicates that the function we're in may need to suspend to get itself onto the MainActor. Think of this as similar to making a DispatchQueue.sync call. Except with await, your function suspends instead of blocking and then resumes after the call to the main thread is completed. So, we don't need it here anymore, but this MainActor.run technique is still important for another reason. At each await, your function may suspend and other code might run. That's the point of awaiting; to let other code run instead of blocking a thread. In this case, we've only got one function to await, so it doesn't matter and we don't need this MainActor.run technique. But sometimes you do want to run multiple calls on the main thread. For example, if you're working on UI updates, such as updating entries in a table view, you might not want the main run loop to turn in between the operations you perform. In that case, you would want to use MainActor.run to group together multiple calls to the main actor to ensure that each ran without any possible suspensions in between. So now, we're now using the main actor to protect the code that needs to run on the main thread. But what about other code in this class, in particular, the code that mutates local variables, like the query anchor that we saw earlier? How can we guarantee those are free from race conditions? One way would be to just put the entire of HealthKitController on the main actor. If I write @MainActor here on the class instead of individual methods, that will protect every method and property of this type, coordinated on the main thread. And for a simple application like this, that would probably be an OK choice. But that also seems a little bit wrong. This HealthKitController is really the back end of the app; it seems unnecessary to be doing all of this work on the main thread. We want to leave that thread free to do UI-focused activities. So instead, we can change this class itself to be an actor. Unlike the main actor, which is a global actor, this actor type can be instantiated multiple times. In my project, I'm still only going to create one of them, but there are many other uses of actors where you might instantiate multiple copies of the same actor. For example, you might make each room in a chat server be its own actor. So now that we've made this class into an actor, let's see what the compiler says. OK. So we're getting some compilation errors. Now let's take a pause here and talk about compiler errors. These errors are guiding you towards the places in your code you need to update when you're migrating code to the new concurrency model. When you get these errors, make sure you understand what they're telling you. Resist the temptation to mash the fix-it button when you're not sure how or why it'll fix the issue. One thing to be wary of is getting into a cascade of errors. Sometimes you'll make a change -- like converting a class to be an actor like we just did or making a method async -- and it'll generate some compiler errors. So you go to the site of those errors, and it's tempting to make more changes to fix those errors, like making that method async or putting it on the main actor. The trouble is that this can lead to even more errors, and quickly you can feel overwhelmed. Instead, use techniques like we're using here in this walk-through, and try and keep the change isolated and done one step at a time with your project compiling and running in between. Add shims to allow your old code to keep working, even though you might end up deleting them later. That way, you can gradually move out from a point, tidying up the code as you go. Incidentally, what I did here is first convert the HealthKitController's methods to async and then make it into an actor. And I find it works out easiest if you do it that way around, rather than starting with the actor conversion. OK, so let's look at these errors. They're down here on the function that we put on the main actor, and this makes sense. In this function, we're touching a stored property of our new HealthKitController actor, the model property. The actor protects its state and it won't let functions not on the actor access that state, such as this stored property. Now, looking at this function, it looks like the only state on the actor it touches is the model object. Everything else is passed in as function arguments. Now, to me, this actually suggests that this function belongs on the model; that the model here ought to be self. So let's do that. We just need to cut this method out of the HealthKitController, move over to the CoffeeData model, and paste it in. This is going to be a public method that we call from the HealthKitController. And we just need to go through and replace any reference to the model because that's going to be self. Finally, we need to go back to the call site... ...where instead we call this method on our model. So now the HealthKitController compiles, and I'm getting a new set of errors from other files. So here we're calling into those completion handler shims that we added earlier. Now, these functions actually don't need isolation in the actor because all they do is create a task that awaits a call into the actor. They don't touch any other parts of the actor's state. So we can mark them as what's called nonisolated. This tells the compiler they aren't a part of the actor's protected state, so they can be called directly from other parts of the code. So we need to mark this one as nonisolated. And now, our project compiles. Note that the compiler will check that this nonisolated claim is true. If I were to try and access some state of the actor -- like if I try and print out the authorization status -- I'll get a compilation error. So now, I've completed my work converting the HealthKitController into an actor that protects its internal state from race conditions. Next, lets follow these deprecation warning breadcrumbs into the next place that I want to work on, which is our CoffeeData model type. Now this class implements ObservableObject, and it has an @Published property. Any updates to properties that are published to a SwiftUI View must be done on the main thread, so this class is probably a good candidate to put on the main actor. But there's also this background dispatch queue here for doing some work in the background. Let's have a look at where it's used.
And it's just in two functions: load and save. And that makes sense; you probably don't want to do your loading and saving on the main thread. When you see a pattern like this -- where a queue is used to coordinate some specific activities, but the rest of a class needs to be on the main thread -- that's a sign that you want to factor that background code out into a separate actor. So let's do that now.
So let's go to the top of this file and create a new private actor called CoffeeDataStore. And let's open CoffeeData in another editor window. And we're going to start moving code over into this new actor. So first, we'll give it a logger of its own.
Just update that. And this will help us know when we're using our new actor when we run the code. Next, instead of this background dispatch queue, we're going to use an instance of our new actor. And so first, let's go to the save operation, and let's just take this and move it over into the actor. So cut from here. And then let's just compile and see what issues we hit. OK, so first, currentDrinks. Now, this was a property of the model type before we moved this method out into the actor. So how can we access it now? Well, the way that actors relay information is they pass values between each other. So we should have this function take currentDrinks as an argument... ...and the model will pass it in to the function call on the actor. So that resolves that. Next, savedValue. Now, this is a copy of the values that were last saved to avoid saving unnecessarily when nothing's changed. This value is mutated by both the save function and the load function, so it definitely needs protecting by the actor. So let's just move it over into the actor.
OK, next. This property dataURL, that's actually just used by the load and save operations, so again, we can just move this over into the actor.
OK, last issues to resolve. Now, here we're getting errors, and if we look, it appears that there's a closure that's capturing some state from the actor, so we need to fix that. Now, why is there a closure here? Well, if we look down, it's because the same piece of code is used in two places. And this actually turns out to be an issue the compiler has flagged that's pretty interesting. What this code is doing is checking if the watch extension is currently running in the background. And the idea is, if it's running in the background already, then don't go onto the background queue; just stay onto the main thread and perform the task synchronously. But this doesn't seem right. You should never block the main thread to perform a synchronous IO operation, even when your app is running in the background. Why does the app do this? Well, we can trace it back to where the save operation is being called. It's being called from a didSet down here on the currentDrinks property. And that fires so that whenever the property is assigned, it saves the new values. Now, didSets are super convenient, but they can be a little too tempting. Let's look at all of the callers of the currentDrinks property. If we drill all the way down here, we can find that the save operation is synchronous because the way it's going to be called originates here in the WatchKit extension's handler for background tasks. Now this handle API has a contract. You're supposed to do all your work, and then call this setTaskCompletedWithSnapshot method. And you must guarantee all your work is done when you call this because your watch app will be suspended at that point. You can't have some I/O operation, like our save operation, still running when you say that you're done. This is a perfect example of how asynchrony forces global reasoning throughout your code. Let's visualize what's happening here. We start in handle(backgroundTasks:) which calls the load from HealthKit function. This takes a completion handler. But then we switch to updateModel(), which is performed synchronously, and so synchronously calls the didSet, which synchronously saves. Once this is done, the completion handler is called and that notifies WatchKit that it's all done. It's the synchronous parts that force us to perform synchronous I/O on the main thread. How can we fix this? To fix it with completion handlers, you'd have to update each currently synchronous method to now take a completion handler. But you can't do that with a didSet; it doesn't take arguments, it just fires automatically when you update the property. So first, let's go to the published property, and we'll make it a private(set). Then let's take the didSet operation... ...and move this logic into a new drinksUpdated function. We'll make this function async. And in it, we need to await a call to our new actor save operation... ...into which we pass the new currentDrinks values. Then, we need to go to wherever currentDrinks is updated and ensure we call drinksUpdated afterwards. There's one final thing to note here. It's important that this operation -- that makes a copy of currentDrinks, mutates it, and then writes it back -- happens as one atomic operation. This is why the await keyword is so vital a clue; it indicates that at this point in the code, this operation may suspend, and other operations -- operations that may also update currentDrinks -- could run. So we need to make sure our update of fetching the value, mutating it, and storing it back is complete before calling any awaitable operations. So that's it! We can now go back to our save operation and complete the refactor...
...by changing this actor to do all of the operations in the background.
Finally, let's look at the load operation. Let's bring up the side editor again. Now here, the logic is split between the code that needs to run in the background and the code that needs to run back on the main thread. So we'll lift just this top part of the function -- just down to here -- and move it into the actor.
Now in doing this, we notice another possible race condition. savedValues here was being mutated on the main queue, but if you remember the save operation, it was read and written on the background queue. Now, as it happens, the way the app was constructed, the load only ever happened on startup, so this was fine. But again, that's relying on global reasoning, and it's the kind of assumption that can break in subtle ways when you make future changes. It's much better to let the actor ensure the program will always be correct. So let's remove this manual key management, fix up the indentation...
...and just like save, we need a way to pass back the loaded values, which we do just be returning a value from the load function. So let's hop back to the load operation on the model type. We've moved this code, so we can just delete it.
And what we need to do here is load our data from the I/O actor. And while we're here, we can clean up these deprecated methods. And that means we need to make this load asynchronous... ...which means in turn that we need this load to be called from a new async task. But at this point, if we just used an async task, we're potentially introducing a new race condition. Remember that outside of an actor, creating a new async task runs on an arbitrary thread. We shouldn't mutate shared state, like currentDrinks, from an arbitrary thread. One way to resolve this is to put this task on the main actor, which we could do like this. But there's a much better way to do this, which involves moving this whole model type onto the main actor. The way we do this is to go to our CoffeeModel definition and add @MainActor to our model type. By putting the model on the main actor, we're now guaranteeing that all access to CoffeeData's properties are going to be made from the main thread. And this is good because, as we noted earlier, it's an ObservableObject with a published property, and properties published to SwiftUI must only be updated on the main thread. It also means any calls to async from the actor will also run on the actor. Now you may have noticed that protecting this type with the main actor didn't lead to any compilation errors, unlike earlier when we moved our HealthKitController into an actor. That's because the places we are calling into our model are things like SwiftUI views. For example, here's the DrinkListView. It displays a list of buttons, and then when you click on one of them, it calls this addDrink method, and that updates the coffeeData model. But this DrinkListView is itself on the MainActor. And so its methods can call into my model type on the main actor without needing any awaits. What is it that determines that this SwiftUI View is on the main actor? Well, it's inferred from the use of EnvironmentObject here. Any SwiftUI View that accesses shared state -- such as environment objects or an observed object -- will always be on the main actor. Elsewhere, we're also accessing our model from this extension delegate call that we saw earlier. Since this extension delegate is guaranteed to always be called on the main thread, it's been annotated by WatchKit as running on the main actor, so it can call into our model type directly down here. Finally, now that we're here, let's refactor this method to get rid of this deprecated completion handler usage. We can instead wrap this in another async task. And then, once the HealthKit load returns, we know all of our work is completed without any completion handlers or without having blocked the main thread, and we can call setTaskCompletedWithSnapshot. We now have this nice, structured, top-down approach to waiting for an asynchronous operation before completing any more work. Incidentally, this structured approach to concurrency is another really important part of Swift's concurrency feature. To learn more, watch the associated talk, which covers how you can take advantage of this feature to structure more complex examples, such as waiting on multiple asynchronous operations to complete before continuing. If, while you watch this talk, you wondered exactly how some of these new features work, check out our under-the-hood talk that explores some of the technology in detail. So let's recap. We've taken some code that had sound type architecture but complex concurrency architecture that had some hidden race conditions that were really hard to spot. And with the help of the new concurrency features, we've rearchitected it so that the concurrency and type architecture are nicely aligned. And the compiler helped us find some hidden potential race conditions along the way. There's lots more to Swift 5.5 that we haven't covered, like structured concurrency with task groups, asynchronous sequences, and some great new asynchronous APIs in the SDK. There's also a few more refactorings that we didn't do in this project that you might want to try yourself. The best way to learn these techniques is to try them out in your own apps, so have fun and enjoy these cleaner, safer ways to code. ♪
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.