Streaming is available in most browsers,
and in the Developer app.
-
Migrate your app to Swift 6
Experience Swift 6 migration in action as we update an existing sample app. Learn how to migrate incrementally, module by module, and how the compiler helps you identify code that's at risk of data races. Discover different techniques for ensuring clear isolation boundaries and eliminating concurrent access to shared mutable state.
Chapters
- 0:00 - Introduction
- 0:33 - The Coffee Tracker app
- 0:45 - Review the refactor from WWDC21
- 3:20 - Swift 6 and data-race safety
- 4:40 - Swift 6 migration in practice
- 7:26 - The strategy
- 8:53 - Adopting concurrency features
- 11:05 - Enabling complete checking in the watch extension
- 13:05 - Shared mutable state in global variables
- 17:04 - Shared mutable state in global instances and functions
- 19:29 - Delegate callbacks and concurrency
- 23:40 - Guaranteeing data-race safety with code you don’t maintain
- 25:51 - Enabling the Swift 6 language mode in the watch extension
- 26:35 - Moving on to CoffeeKit
- 27:24 - Enabling complete checking in CoffeeKit
- 27:47 - Common patterns and an incremental strategy
- 29:55 - Global variables in CoffeeKit
- 31:05 - Sending an array between actors
- 33:53 - What if you can’t mark something as Sendable?
- 35:23 - Enabling the Swift 6 language mode in CoffeeKit
- 35:59 - Adding a new feature with guaranteed data-race safety
- 40:43 - Wrap up and the Swift 6 migration guide
Resources
Related Videos
WWDC21
-
Download
Hi, I'm Ben from the Swift team, and in this video, I’m going to walk you through enabling the Swift 6 language mode in an existing application. We’ll see how Swift 6 helps protect you against possible race conditions, and look at some techniques for introducing this change gradually into your app, as well as how to deal with interactions with frameworks that aren’t yet aware of Swift’s concurrency guarantees. I'm going to be using a simple app that tracks coffee consumption throughout the day, as well as a complication to show your current caffeine level on a watch face. When we first introduced Swift concurrency at WWDC 2021, I walked you through how to adopt Swift’s new concurrency model in this app. In that talk, you see how a seemingly clean app architecture sometimes hides the hidden complexity of concurrency. If you look at the views and models, everything is well organized. But delving into how concurrency is managed shows a different picture. The original app had 3 concurrent queues on which code could be executing. Work in the UI, and on the model, was done on the main queue. The app also had a dispatch queue for doing work in the background. And finally, certain callbacks into completion handlers, like those returning results from HealthKit, were done on arbitrary queues. So while the types were organized neatly, the way concurrency was organized throughout the app was not such a clear story.
Which queue was executing code, and why, weaved through the types in a way that was not clear in various places.
By adopting Swift concurrency, we went from this ad-hoc concurrency architecture. To something that looked like this. The UI views and models were set to run on what’s called the main actor, while background operations were performed on dedicated actors. The actors communicated between each other using thread-safe value types, using Swift’s async/await feature. When we were done, the concurrency architecture was as clear and easy to describe as the type architecture. But there was one wrinkle. When doing this refactoring to improve the architecture, there was still a lot of responsibility on me, the programmer, to avoid data races. I followed all the guidelines, and used value types to communicate between my actors, but I didn’t have to. I could have taken a reference type, like a class, and passed it, for example, from one actor to another. Reference types allow shared mutable state to be passed around and when you do that, you can break the mutual exclusion the actors provide by allowing them to both access shared state simultaneously. So, if I sent an instance of a class from one actor to another, it’s still possible to get data races that could cause my program to crash, or worse, to corrupt user data. This brings us to is the benefit of Swift 6.
The Swift 6 language mode introduces full enforcement of data isolation. The compiler will prevent this kind of accidental sharing of state between tasks and actors, allowing you to carry out refactoring, or add new functionality to your app, without worrying that you’re introducing new concurrency bugs.
The Swift 6 language mode is opting for both existing and new projects. Adopting it can significantly improve the quality of your app by catching mistakes in concurrent code at compile time. It can be especially useful if you are experiencing hard-to-reproduce crashes, and want to go through and methodically eliminate the risk of data races. And if you're actively working on integrating more concurrency to improve responsiveness and performance, adopting Swift 6 mode can ensure that those changes don't risk introducing new data races.
If you maintain a public Swift package, we encourage you to adopt Swift 6 as soon as possible to help your users who want to migrate their code bases, too. They will benefit from building on top of dependencies that have also adopted Swift 6. Everyone can follow along on the adoption of Swift 6 in popular packages on swiftpackageindex.com. Today, we’re going to see how this works in practice. We’re going to take our CoffeeTracker application, and enable Swift’s data isolation. We’ll do this step-by-step, and look at some of the guidance the compiler gives us, about where we need to make changes to allow Swift to guarantee that CoffeeTracker is free of any data races. Now, I don’t believe that my app actually contains any data races today. And chances are, the same is going to be true of your code, too. You might have already shaken out most of the data races in your existing code through years of refinement, bug reports, using the main thread checker, thread sanitizer, and so on. The real value of data race safety is protecting against bugs in new code that you write, either adding new features, or refactoring existing code to take better advantage of concurrency. Data race safety allows you to leverage concurrency in your app without fear of introducing new data races that you'll later have to hunt down or put in a speculative fix for, because you can't reproduce a crash. Since we last saw it, our coffee tracking app has really taken off, and we’ve expanded the team to start adding new features.
As part of that we’ve factored some of the app into a new framework, CoffeeKit, and some of the code now lives in there. The team is keen to start adding new features to the app, but before we do that, we’re going to update it to Swift 6 so we know we’re not going to be introducing new concurrency bugs when we do that. Now, I’ve just downloaded Xcode 16 and opened it up. This means I have the new Swift 6 compiler and the latest SDK for watchOS 11, let’s try and build the app.
And it builds just fine. No need for any updates. But that’s not because our app was free of any possibly data races. It’s just that we haven’t enabled the Swift 6 language mode yet. Just like with previous releases, Swift 6 has a source compatibility guarantee. Aside from very minor changes, your app should always build with the new compiler.
Now that we know our app builds with the latest Xcode, we want to take the next step of enabling Swift 6 mode, which brings full data-isolation enforcement. Now, in preparation for this, you could go through and try to do MainActor and Sendable audits before enabling any of the compiler diagnostics. But this is missing out on the benefits that the new Swift compiler brings you. The compiler diagnostics will guide you towards the places that need fixing. Think of it like a pair programmer that's pointing out potential bugs in your code. This helps add some structure to the migration process. We’re going to follow a step-by-step process where we migrate each one of the targets in our code. With each target we’re going to follow these steps: First, we’ll enable complete concurrency checking. This is a per-module setting that leaves your project in Swift 5 mode, but enables warnings for all the code that would fail with Swift 6’s enforced data isolation. We’re going to go through, resolving all of these warnings for that target.
Once that is done, then we enable Swift 6 mode. This locks in all of our changes, and prevents any future refactorings from regressing to an unsafe state. We then move onto the next target and repeat the process. Finally, once that is enabled, we might want to go back and do some whole-app refactoring, perhaps to undo some unsafe opt outs by altering the architecture, or perform some refactoring you spotted that would make the code nicer. One bit of advice about refactoring: try and resist the temptation to blend together both significant refactoring, and enabling data race safety. Try to do one at a time. If you try to do both at once, you’ll probably find it too much change at once, and have to backtrack.
In this talk, we’re going to focus only on the step of enabling Swift 6 in an app that we already refactored previously to use Swift concurrency. So we start with enabling complete checking. What does complete checking enable? If you’ve already been using Swift Concurrency in your app, you’ve probably seen warnings or errors from the Swift compiler about concurrency issues that came up as you adopted Swift’s concurrency features. For example, here we’re working on adding a new delegate, that’s going to receive a callback we’re adding for CoffeeKit to tell me when my caffeine levels are worryingly low. This delegate is going to publish a value back to my SwiftUI view, so I want it to be isolated to the Main Actor. So I can add an @MainActor to the top, requiring all its methods and property accesses to be made on the main thread. When I do this, though, I get an error lower down in the code, on the implementation of the protocol.
It’s telling me that a “Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement”. This CaffeineThresholdDelegate protocol now makes no guarantees about how it’s going to be called. It’s inside CoffeeKit and that hasn’t been updated to Swift 6 yet. But here I’m conforming my Recaffeinater type to it, and I just_constrained it to the main actor. Its methods are going to run on the main actor, so they can’t just conform to a protocol that isn’t always guaranteed to be called on the main actor. We’ll come back to this issue and resolve it shortly. But this is an example of an error that the swift compiler generates because you opted a type into checking that it was being called on the main actor. If you watched the previous talk, adopting Swift concurrency in Coffee Tracker, you will have seen these issues coming up as concurrency was being adopted in different places in the app.
Enabling strict checking in a target’s build settings opts your entire module into being checked for possible race conditions. Let’s enable that now, and see what happens. Data isolation in Swift is enabled on a per-target basis, and in my app, I have two key targets, the WatchKit extension, where my UI layer lives, and CoffeeKit, a framework where the business logic for tracking the caffeine and saving it to HealthKit lives.
I’m going to start by enabling complete checking on the watch extension. There are two reasons for this. First, it’s often the simpler place to enable concurrency checking. Much of the UI layer will run on the main thread, and will be using APIs, like SwiftUI or UIKit, that are themselves guaranteed to perform operations on the main thread. The other reason is, when enabling strict concurrency you’re often going to be working with other modules that haven’t yet been updated for Swift concurrency. Maybe you’re using a C library that will never be updated. Or maybe it’s a framework or package module that will get updated to Swift 6, just hasn’t been yet. And that, of course, includes our own CoffeeKit framework. We’ll see why going top down like this helps once we get started. So, as a first step, let’s go to our extension, and go to its settings. And we’re going to search for the Swift concurrency checking setting and we’re going to set it to Complete checking.
Once we do this, the compiler is going to start emitting warnings about code it can’t confirm is concurrency safe. These are just going to be warnings, our project will still build and run. So let’s try building.
And, we see a few more warnings, in addition to the one we introduced earlier. Let’s take a look at them.
Now, the first issue we can see is one of the most common issues to deal with in Swift 6, referring to this “logger” variable.
We see a logger instance declared as a global variable. Global variables are a source of shared mutable state, every bit of code in your program, no matter what thread it runs on, is able to read and write to this same variable. So this can be a really easy source of data races, and we need to make it safe. We have a few options as to how. In fact if we expand the issue, we see the compiler is recommending some.
First option is the easiest, we just make it read only. Logger is a Sendable type, and that means that if it’s declared as a let variable, it can’t cause data races when used from multiple threads. So switching this var to a let. And rebuilding.
Makes that issue go away. Now, this is the right fix, but there were other options here.
Suppose I don’t want it to be immutable, let’s say I want to update the value later. So I want to keep it as a var not a let.
Another option is to tie this global variable to a global actor. Here, I’m in my UI layer, so maybe all my logging is going to happen on the main actor. So I can annotate this global variable with @MainActor, and yes, all my logging use is from the main actor so that also eliminates the warnings.
Finally, sometimes I have some other external mechanism protecting this variable, that the compiler can’t enforce. Perhaps I’m guarding all access using a dispatch queue. In that case, I can use the nonisolated(unsafe) keyword.
Like other uses of the word “unsafe” in Swift, this puts the burden on you to ensure safety for this variable. This should be a last resort, it’s best to use Swift’s compile time guarantees instead. But the compiler can’t know about everything, so nonisolated(unsafe) is available as an option for you in those cases. This might be an example of something where I want to come back later and refactor this code, maybe move this variable into an actor so the compiler can verify it’s being used safely. But for now, I could mark is as nonisolated(unsafe) and move on to the next warning. This is not one of those cases, so I’m going to go back to declaring this variable as a let, that’s the best option. Now you might wonder about this initializer here, that initializes the global. When does that get run? Isn’t that important to know to understand thread safety? Global variables in Swift are initialized lazily. The value is initialized on first use, the first time CoffeeTracker logs something. This is a really important difference when compared to C and Objective-C. In those languages, global variables are initialized on startup. And this can be really bad for launch times. Swift’s lazy initialization avoids these problems, allowing your app to get to a useable state faster. But lazy initialization also can introduce races: what if two threads try to log for the first time with this global variable simultaneously? Could they create two loggers? Well don’t worry — in Swift, global variables are guaranteed to be created atomically. If two threads tried to access the Logger simultaneously for the first time, only one would initialize it, while the other one blocked waiting. OK, that’s resolved that issue, let’s look at the next ones.
Here we have some code accessing the shared instance of WKApplication. This global instance method is an example of one that’s isolated to the main actor.
The first note here is pointing out that calls to actor-isolated state are implicitly asynchronous, that is, if this were an async function, you could use await to access this global variable on the main actor. This function isn’t asynchronous, so we’d have to mark it async, or start a new task. But the compiler is offering us another fix, we can just put this function, on the main actor.
Since this is a free function, not a method on our view, it’s not defaulting to be on the main actor. Let’s apply the fixit, and now this method is isolated to the main actor, and if we try and build, it succeeds.
To see why this worked, let’s have a quick look at the callers.
We can see it’s called two times, both from methods that I know are on the main actor. If this function was being called anywhere not on the main actor, I would have got a compiler error telling me so, and I could go look at that caller and see what context it was calling from. One is a SwiftUI view. And the other is in the implementation of WKApplicationDelegate. Let’s go look at that.
If we option-click WKApplicationDelegate, we can see it’s a protocol that’s tied to the main actor. This is a guarantee that it will only be called on the main actor, either by the WatchKit framework, or once you enable Swift 6 mode, by your code.
Many delegates, and other protocols like SwiftUI views, that are designed to operate only on the main actor, have been annotated like this, especially in the latest SDKs that come with Xcode 16. Most importantly, this includes the SwiftUI View protocol. You will probably find if you previously enabled strict concurrency checking, you had to add more main actor annotations than are necessary with the new SDKs, and that you might be able to remove some of those annotations. Now, let’s talk a bit about delegate callbacks and concurrency. You probably already know that whenever you receive a callback, from a delegate or a completion handler, you always have to first understand what the concurrency guarantees are on that callback. Some callbacks have a guarantee, they might say in their documentation that all callbacks will always be on the main thread. A lot of UI frameworks give this guarantee, and it’s one of the reasons we aren’t getting that many warnings in our work on this view layer of our watch extension, when we’re marking things as main actor. On the other hand, some delegates do the opposite, they make no guarantees on how they’re called back, saying it will be on some arbitrary thread or queue. This makes a lot of sense for callbacks that are more likely to be coming into the back end of an app. The callbacks received by CoffeeTracker from HealthKit are more like this. In those cases, the user needs to redispatch onto the right queue or actor or do their work in a thread safe way. The trouble with this approach, where each delegate has its own rules that are captured only in documentation, is that this puts a lot of burden on you, the user, to do the right thing. You need to think about where you will be when you’re called back, and where you need to be to do the next part of your logic. And if you forget to check, or forget to redispatch to the right place, you can easily get a data race. What’s worse, let’s say that a callback is already in place and working, and it happened to always be on the main queue but that wasn’t always guaranteed. Then later, some changes happen in that framework, and now it’s coming in on a different queue. But your UI layer was relying on it being on the main queue. What we’re missing here is local reasoning — guarantees that when I’m working in my UI layer, I can't easily be broken by changes happening to the code elsewhere in my app, like them changing the queue they do work on.
Swift concurrency tackles this problem, by making these guarantees, or lack of guarantees, explicit. If a callback doesn’t specify how it’s called back, it’s considered non-isolated, and it can’t access data that requires a certain isolation. On the other hand, if a callback does provide an isolation guarantee, say that it will always be called back on the main actor, then it can annotate the delegate protocol or callback as always being on the main actor, and the receiver of the callback can rely on that guarantee. With that said, let’s go back to that first warning we saw, where our delegate type on the main actor couldn’t conform to a nonisolated protocol. We have a couple of options here, which the compiler is offering. The first one is to declare the method nonisolated. That means that despite this being a method on a main actor isolated type, this specific method will not be isolated to the main actor. This is the route to go for for callbacks that intentionally make no promises about where they call you back. Of course, this being a view, I probably immediately want to turn around and do some work on the main actor. If I don’t do that, I’ll get a new error when compiling this code, because I’m accessing properties on the view that are protected by the main actor.
I could fix this by starting a task on the main actor.
In this case though, I know this callback is supposed to be on the main actor, I know it’s coming from my model type inside CoffeeKit that is itself running on the main actor.
One option, if I maintain the whole codebase, is just to go fix that right now. I can jump to definition and here I see the protocol inside CoffeeKit. I could annotate it with @MainActor here, to guarantee it’s going to get called on the main actor. But sometimes you don’t maintain the whole codebase like this. Maybe another team maintains CoffeeKit, or it’s a package or framework you rely on maintained by someone else. Let’s assume this were the case here, and go back to our delegate implementation.
Now, I know that this method is getting called on the main actor, I just checked the code, or maybe I read it in some documentation for this delegate.
When you know for certain that a call will be on a particular actor, there is a method you can use to tell the compiler this, it’s called assumeIsolated. So instead of the code starting a task, I can write MainActor.assumeIsolated. This doesn’t start a new task to async onto the main actor. It just tells Swift that this code is already running on the main actor.
Note that it’s still perfectly possible for some part of the code to call this function from not the main actor. This is the same as with Swift functions today that assume they’re being called from the main thread. A good way to protect against this is to add an assertion inside the function that you are indeed on the main actor and that’s what assumeIsolated does. If this function is ever not called from the main actor, it will trap and your program will stop running. Trapping isn’t something you want, but it’s better than a race condition that could corrupt user's data.
Now this pattern of conforming to a delegate protocol that assumes its called on the main actor, is common enough that there’s a shorthand for it. So let me undo my changes here, and instead, on the protocol conformance, I write @preconcurrency.
That does everything I wrote out manually, it assumes it’s being called on the actor this view is isolated to, and it’ll trap if it isn’t.
Now that we’ve eliminated all our concurrency warnings, we’re ready to enable Swift 6 in this target. So we go to settings. And search this time for Swift Language Mode.
And we set that to Swift 6.
Compile, and it builds without any errors and warnings. I’ve now locked in full data isolation checking in my extension, and any future changes I make here will have full data isolation checking from the compiler, ensuring I don’t accidentally introduce data races. OK,now that the extension is in Swift 6 mode, let’s turn our attention to the CoffeeKit target. Now that we’re working on this target, let’s go and add that @MainActor annotation to our delegate protocol. So let’s find it. Add the main actor annotation, rebuild, and we get a new warning.
The warning is back in the extension, and it’s on that @preconcurrency attribute we just added.
Now that the compiler can see that this protocol is guaranteed to be on the main actor, the compiler is warning that that pre concurrency attribute is no longer needed. So we can remove it.
OK. with that issue resolved, we can follow the same routine as before, enabling complete concurrency checking. We go to the project settings, and enable it for the CoffeeKit target.
And we build.
Now, this time we see more warnings - 11 of them. That’s quite a few warnings for a fairly simple project.
This is going to be a common experience. You enable complete concurrency checking in your project, and your project generates hundreds or even thousands of warnings. And when that happens, you might worry what you’ve signed up for. It’s important at this point not to panic. It’s common for a large number of warnings to all stem from a few issues. And many of these issues are often quick to fix.
So, when first cleaning up concurrency warnings, a flurry of simple changes, putting a method onto the main actor, or changing a global to be immutable, can then cut these warnings down very quickly. It’s a good strategy when you first turn on strict checking to go looking for these quick wins and act on them, reducing the warnings with the simplest fixes at first. It’s also good to look out for issues, that are at the root of a very large number of issues. Sometimes a single line change can resolve hundreds of knock-on issues. If you’ve tried out complete checking mode with the previous versions of Xcode, it’s also worth trying with the latest Xcode beta. The newer SDKs include more annotations to help with migration. For example, all SwiftUI views are now tied to the main actor, which means you no longer need to add MainActor annotations to your view types yourself. In fact, you might find you can remove some of those annotations as they’re already are being inferred.
Once that’s done, you may find yourself with a much smaller number of warnings that are harder to tackle. The other thing to remember is you don’t have to address all these issues in one sitting. If you have to ship a release, or go work on some more pressing change, you can go back into settings and turn strict checking back off. All the changes you made to reduce those warnings will be valid improvements to the code base that you can keep and check in, even if you then go back to minimal checking for a while. You can return to that setting later when you’re ready, and tackle them then. In our case, once we start looking at the warnings, we see a pattern we’ve seen before. Several of these global variables are marked as vars, but I think these are all constants, or don’t need to be mutable, like the logger we saw before. We can resolve all of these fairly quickly. By the way this is a great time to try out your multi-line edit skills.
So I just highlight the var, then cmd-option-E selects all of them, change them to lets.
Build.
And that eliminates these warnings.
Now, I don’t want to give you the impression I’m making this look easier than it is. This is just a sample project, in a real project, there would be more warnings. But our experience, putting real full-scale projects is that this is a common experience, lots of easy wins, and then a few harder issues to tackle. Let’s take a look at the last errors. They are being caused by us passing around arrays of drinks between different actors. For example, this first one is saying that sending self.currentDrinks to this save method may cause data races. Note that save is a method on another actor. CoffeeData is on the main actor, it needs to be because it’s a SwiftUI ObservableObject.
But save is on another actor, it’s on this CoffeeDataStore actor that does saving and loading from disk in the background. And if we go back to the warning, you can see that we’re sending to save the same array of drinks we’re keeping in the model as isolated to the main actor.
If Drink were a reference type, we’d be setting up a potential data race where both the main actor and the storing actor could have access to shared mutable state simultaneously. To address this, let’s take a look at Drink. If we go to the definition, we can see that it’s a struct, with a few immutable properties, all of which are value types. Based on this, it clearly can be made Sendable, and then it would be perfectly fine to store the drinks in one actor, and then send that same array off to another actor.
Now, if this had been an internal type, Swift would just automatically consider this type Sendable for you. But this is a public type, we share it outside CoffeeKit with our CoffeTracker extension.
Swift doesn’t infer sendability for public types automatically for you. It doesn’t do this because marking a type sendable is a guarantee to your clients. This type contains no mutable state today, but maybe I want to change that in future, I don’t want to lock in sendability prematurely. For this reason, Swift requires you to explicitly add Sendable conformances on public types.
Now in this case, I’m happy to do that. Incidentally, this is an example of a single line change I can make that eliminates multiple warnings at once: three different places that were needing the Drink type to be Sendable. In a large project, it might not be three, it could be dozens of warnings in multiple projects. So, let’s go ahead and mark this type Sendable.
We can recompile.
And we see that there’s another type here that isn’t sendable.
Now, it happens this is just an enum, so I can go and mark it sendable, too. But what if it wasn’t, what if it was an Objective-C type, maybe one that could never be sendable because it stores mutable state in a reference type. At this point you might need to make some choices about safety. One option is to reason about that type, and decide, even though it is a mutable reference type, it’s safe because it was a fresh copy, maybe generated by NSCopying. You might decide that even though it’s not sendable, you could protect that class, maybe make the variable private say, and store it in your sendable type anyway. To do that, you could again use the nonisolated(unsafe) keyword.
If you do that, then the Drink type is now compiling with the Sendable annotation. Now, I know that isn’t necessary, so instead, I’m going to undo that, and instead go to DrinkType, and mark it as Sendable, too.
Now that we’ve eliminated all our concurrency warnings in CoffeeKit, we’re ready to enable Swift 6 mode in this target. So again we go to settings.
And search this time for Swift Language Mode.
And we set that to Swift 6.
Compile.
And it builds. At this point the whole of our CoffeeTracker app is protected by Swift concurrency.
Finally, now that we’re protected, let’s look at adding a new feature to CoffeeTracker. Our users want to start tracking their location when they have their coffee, so that they can mine that data for key insights into their caffeine habits. So let’s look at adding CoreLocation into our app. So, let’s go to the addDrink method in CoffeeKit. And just before we add our drink, we’re going to use CoreLocation to fetch the user’s current location. CoreLocation has an async sequence that streams current location, that fits very well with Swift concurrency, and we can use it here to loop over the location results until we have the right level of accuracy, at which point we can assign the location to our caffeine sample. You’d probably want to add a timeout to this code, which you could do with Swift’s structured concurrency and cancellation. There’s just one issue with this approach.
We’d have to raise CoffeeTracker’s minimum deployment target. And we’re not quite ready for that, we still have some users who don’t want to update to watchOS 10, but do really want to track where they’re drinking their coffee. So instead, we’re going to need to use the older CoreLocation APIs based on delegate callbacks. These predate Swift concurrency, so they’ll be a little trickier to use. The really nice thing about the approach we see here is that this code looks like regular synchronous code. We ask for our location, loop over the incoming stream of location updates, until we get one that’s accurate enough, all within this function where we’re adding a new drink to our array. The delegate API, by contrast, will require us to store away some state, wait for the delegate method to fire, and then from there, continue with saving our drink value with a location. So it’s worth trying to raise your deployment target to take advantage of these new APIs.
But let’s say we don’t want to do that. So instead, let’s remove this new code, and we’re going to go down to the bottom of this file, and create a CoreLocation delegate object.
So here I have the basic implementation of a delegate class that can receive location updates from CoreLocation. This part of CoreLocation predates Swift concurrency. So I have to do more work to make sure I stick to the rules. Unlike all the other callbacks we’ve seen in this talk, this CLLocation delegate is a bit different. It does not have a static guarantee of what thread it will be called back on.
Up until now, we’ve talked about delegates always called on the main thread, or delegates called on an arbitrary thread. If you look at the documentation for CoreLocationManager, you’ll see that the guarantee it gives is that the thread this delegate is called on is determined by the thread on which you created the CLManager. That’s a dynamic property that Swift cannot automatically enforce for you without some help.
In our case, we’re using this information from the main actor, in our model type. So, the simplest path would be to ensure this delegate is going to be on the main thread, too. It’s simple to get back into a mode where Swift will help us to enforce that. We put this type entirely on the main actor.
That means the location manager will be created on the main thread, here in the initializer. And so the delegate will also come in on the main actor.
Of course, once we do that, we get the familiar error from the compiler telling us this delegate callback isn’t isolated to the main actor. We’ve already seen the pattern for how to deal with this.
We mark the delegate as nonisolated.
And then wrap the code that must run on the main actor in a MainActor.assumeIsolated call.
And my build has succeeded. We’ve now sourcing current location in our app, still building in Swift 6. This was a quick tour of some of the techniques for migrating your app to the Swift 6 language mode. There are many scenarios we haven’t covered today, but we have more resources available for you. You can start by watching the previous session, which looked at using Swift's concurrency features to modernize an existing app. Much of the path for migrating this code to Swift 6 was made easier by some of the refactoring covered in that talk. Based on lessons from previous migrations, the Swift 6 Language Mode is designed for incremental migration across the Swift ecosystem. So data races can be eliminated one code change at a time. We've only scratched the surface of techniques for bridging between static and dynamic data race safety as we take on this transition as a community. You can find a guide with all of these strategies and more at Swift.org/migration. We hope you find these resources useful as you work on your own code. Thanks for watching!
-
-
9:08 - Recaffeinater and CaffeineThresholdDelegate
//Define Recaffeinator class class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //Add protocol to notify if caffeine level is dangerously low extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
9:26 - Add @MainActor to isolate the Recaffeinator
//Isolate the Recaffeinater class to the main actor @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 }
-
9:38 - Warning in the protocol implementation
//warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
9:59 - Understanding why the warning is there
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
12:59 - A warning on the logger variable
//var 'logger' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
13:38 - Option 1: Convert 'logger' to a 'let' constant
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:20 - Option 2: Isolate 'logger' it to the main actor
//Option 2: Annotate 'logger' with '@MainActor' if property should only be accessed from the main actor @MainActor var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:58 - Option 3: Mark it nonisolated(unsafe)
//Option 3: Disable concurrency-safety checks if accesses are protected by an external synchronization mechanism nonisolated(unsafe) var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
15:43 - The right answer
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
17:03 - scheduleBackgroundRefreshTasks() has two warnings
func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() //warning: Call to main actor-isolated class method 'shared()' in a synchronous nonisolated context // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { //warning: Call to main actor-isolated instance method 'scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:)' in a synchronous nonisolated context error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
17:57 - Annotate function with @MainActor
@MainActor func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
22:15 - Revisiting the Recaffeinater
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not //warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
22:26 - Option 1: Mark function as nonisolated
//error: Main actor-isolated property 'minimumCaffeine' can not be referenced from a non-isolated context nonisolated public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
23:07 - Option 1b: Wrap functionality in a Task
nonisolated public func caffeineLevel(at level: Double) { Task { @MainActor in if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
23:34 - Option 1c: Explore options to update the protocol
public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
24:15 - Option 1d: Instead of wrapping it in a Task, use `MainActor.assumeisolated`
nonisolated public func caffeineLevel(at level: Double) { MainActor.assumeIsolated { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
25:21 - `@preconcurrency` as a shorthand for assumeIsolated
extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
26:42 - Add `@MainActor` to the delegate protocol in CoffeeKit
@MainActor public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
26:50 - A new warning
//warning: @preconcurrency attribute on conformance to 'CaffeineThresholdDelegate' has no effect extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
27:09 - Remove @preconcurrency
extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
29:56 - Global variables in CoffeeKit are marked as `var`
//warning: Var 'hkLogger' is not concurrency-safe because it is non-isolated global shared mutable state private var hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. //warning: Var 'anchorKey' is not concurrency-safe because it is non-isolated global shared mutable state private var anchorKey = "anchorKey" // The HealthKit store. // warning: Var 'store' is not concurrency-safe because it is non-isolated global shared mutable state private var store = HKHealthStore() // warning: Var 'isAvailable' is not concurrency-safe because it is non-isolated global shared mutable state private var isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. // warning: Var 'caffeineType' is not concurrency-safe because it is non-isolated global shared mutable state private var caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! // warning: Var 'types' is not concurrency-safe because it is non-isolated global shared mutable state private var types: Set<HKSampleType> = [caffeineType] // Milligram units. // warning: Var 'miligrams' is not concurrency-safe because it is non-isolated global shared mutable state internal var miligrams = HKUnit.gramUnit(with: .milli)
-
30:19 - Change all global variables to `let`
private let hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. private let anchorKey = "anchorKey" // The HealthKit store. private let store = HKHealthStore() private let isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. private let caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! private let types: Set<HKSampleType> = [caffeineType] // Milligram units. internal let miligrams = HKUnit.gramUnit(with: .milli)
-
30:38 - Warning 1: Sending arrays in `drinksUpdated()`
// warning: Sending 'self.currentDrinks' risks causing data races // Sending main actor-isolated 'self.currentDrinks' to actor-isolated instance method 'save' risks causing data races between actor-isolated and main actor-isolated uses await store.save(currentDrinks)
-
32:04 - Looking at Drink struct
// The record of a single drink. public struct Drink: Hashable, Codable { // The amount of caffeine in the drink. public let mgCaffeine: Double // The date when the drink was consumed. public let date: Date // A globally unique identifier for the drink. public let uuid: UUID public let type: DrinkType? public var latitude, longitude: Double? // The drink initializer. public init(type: DrinkType, onDate date: Date, uuid: UUID = UUID()) { self.mgCaffeine = type.mgCaffeinePerServing self.date = date self.uuid = uuid self.type = type } internal init(from sample: HKQuantitySample) { self.mgCaffeine = sample.quantity.doubleValue(for: miligrams) self.date = sample.startDate self.uuid = sample.uuid self.type = nil } // Calculate the amount of caffeine remaining at the provided time, // based on a 5-hour half life. public func caffeineRemaining(at targetDate: Date) -> Double { // Calculate the number of half-life time periods (5-hour increments) let intervals = targetDate.timeIntervalSince(date) / (60.0 * 60.0 * 5.0) return mgCaffeine * pow(0.5, intervals) } }
-
33:29 - Mark `Drink` struct as Sendable
// The record of a single drink. public struct Drink: Hashable, Codable, Sendable { //... }
-
33:35 - Another type that isn't Sendable
// warning: Stored property 'type' of 'Sendable'-conforming struct 'Drink' has non-sendable type 'DrinkType?' public let type: DrinkType?
-
34:28 - Using nonisolated(unsafe)
nonisolated(unsafe) public let type: DrinkType?
-
34:45 - Undo that change
public let type: DrinkType?
-
35:04 - Change DrinkType to be Sendable
// Define the types of drinks supported by Coffee Tracker. public enum DrinkType: Int, CaseIterable, Identifiable, Codable, Sendable { //... }
-
36:35 - CoreLocation using AsyncSequence
//Create a new drink to add to the array. var drink = Drink(type: type, onDate: date) do { //error: 'CLLocationUpdate' is only available in watchOS 10.0 or newer for try await update in CLLocationUpdate.liveUpdates() { guard let coord = update.location else { logger.info( "Update received but no location, \(update.location)") break } drink.latitude = coord.coordinate.latitude drink.longitude = coord.coordinate.longitude } catch { }
-
38:10 - Create a CoffeeLocationDelegate
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
39:32 - Put the CoffeeLocationDelegate on the main actor
@MainActor class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() // This CLLocationManager will be initialized on the main thread manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } // error: Main actor-isolated instance method 'locationManager_:didUpdateLocations:)' cannot be used to satisfy nonisolated protocol requirement func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
40:06 - Update the locationManager function
nonisolated func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { MainActor.assumeIsolated { self.location = locations. last } }
-
-
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.