Configure player

Close

WWDC Index does not host video files

If you have access to video files, you can configure a URL pattern to be used in a video player.

URL pattern

preview

Use any of these variables in your URL pattern, the pattern is stored in your browsers' local storage.

$id
ID of session: meet-with-apple-276
$eventId
ID of event: meet-with-apple
$eventContentId
ID of session without event part: 276
$eventShortId
Shortened ID of event: meet-with-apple
$year
Year of session: 2026
$extension
Extension of original filename: mp4
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: ...

Meet with Apple • Session 276

Q&A: Swift concurrency

2026-04-13 • iOS, iPadOS, macOS, tvOS, visionOS, watchOS • 1:03:49

Join us online for a live deep dive into Swift concurrency with Apple engineers. Ask questions, gain insights from teams within Apple who have adopted concurrency in their own APIs, and learn best practices for migrating to or adopting Swift concurrency in your apps and libraries. Please note that you‘ll need a free Apple Developer Forums account to participate in the Q&A.

Speakers: Angelica Bato, Holly Borla, Jeremy Schonfeld, Sima Nerush, Allan Shortlidge

Open in Apple Developer site

Transcript

This transcript was generated using Whisper, it may have transcription errors.

[Angelica]

Hello, my name is Angelica. I'm a technology evangelist at Apple, and I'm so excited to be joined today by an incredible panel from Engineering who are here to answer your questions. Today, we'll be talking about all things Swift concurrency. Now most apps can start by running all of their code on the main thread. But if you do too much work in the main thread, it can cause UI to hang.

You can use concurrency in your app to improve responsiveness, opening up the main thread for UI updates. You can also offload expensive computation to the background to run concurrently, improving the overall user experience. But concurrent programming is hard because sharing memory between multiple tasks is prone to mistakes that lead to unpredictable behavior.

Swift's concurrency model is designed to make concurrent code easier to write correctly. It makes the introduction of concurrency explicit and identifies what data is shared across concurrent tasks. It leverages this information to identify potential data races at compile time so you can introduce concurrency as you need it without fear of creating hard-to-fix data races. Swift concurrency provides tools like actors and tasks for expressing these kinds of concurrent operations.

That's where we'll kick off today's Q&A. Now, I see lots of questions already trickling in. And as we know, Swift Concurrency can cover a broad set of topics. While we'd love to be able to answer all of your questions, we likely won't be able to or we'll be here all night. So we're going to focus today's discussion on questions that will help the broader audience.

Panel introduction

Before we get started, let's get to know who we have on our panel. Starting from my left, Holly.

[Holly]

Hi, my name is Holly. I work on the Swift compiler. I've contributed a lot to the language design and compiler implementation concurrency with a particular focus on the data race safety diagnostics that you see in the Swift 6 language mode.

– Jeremy?

[Jeremy]

Hi everyone, my name is Jeremy. I work on the Foundation framework and the Swift standard libraries. And I focused on adopting concurrency features throughout Foundation.

– Awesome, next we have Sima.

[Sima]

Hi, I'm Sima. I work in the SwiftUI team and I've actually directly contributed to Swift's concurrency model through the open source project. And on my team, part of my work is auditing and annotating our APIs with Swift concurrency annotations and also adopting some of the Swift concurrency features internally in our framework.

– Awesome, and last but not least, Allan.

[Allan]

Hi, I'm Allan. I also work on the Swift compiler and I work very closely with framework teams at Apple and I also was a framework developer at Apple previously.

[Angelica]

Awesome, we have a great set of panelists here. While we let these questions trickle in a little bit more, as well as give you all some time to upvote the ones that you want us to answer first, let's get started with a more general question.

Swift Concurrency Data Race Safety Misconceptions

Now, with the introduction of Swift 6 and data race safety, a lot of folks, a lot of developers are looking specifically at their current concurrent programming and looking into migrating to Swift concurrency and have a lot of questions, a lot of misconceptions. What would you say are the biggest misconceptions of Swift concurrency as it relates to data race safety? Holly, do you want to take this?

[Holly]

Yeah, I think there's two very related transitions that you can take your code base through that are often people think about them as one step when I really encourage you to think about them as two. One is modernizing your code to use Swift's native concurrency features like changing your APIs to use async/await and so on. And then separately, there's enabling complete concurrency checking, migrating to the Swift 6 language mode to get that static data race safety checking.

And I recommend thinking about those as two different steps. And you can do them in either order, depending on what's best for your code base. But it's really difficult to get to a point where you're addressing all of those diagnostics and getting that safety enabled in your project, while also potentially drastically changing the way that your code is modeled in the way that your code behaves.

And it's much easier to separate those so that you can change the way your code behaves and continue to test that, or you can enable those diagnostics, make use of unsafe opt-outs like preconcurrency and like nonisolated(unsafe) to at least express what's true of your code today. And then those are things that you can then go and audit later when you do want to take the modernization step.

So that's something that I frequently see that I think is a misconception that I encourage people to think about as distinct steps.

[Angelica]

Yeah, everyone wants to go all in right away. I'm curious, Jeremy, Sima, do you have any insight, like your experience updating your APIs with Swift Concurrency? Did you feel that same kind of urge to try to do a large migration versus being able to do it in the iterative stuff?

[Jeremy]

Yeah, definitely.

When taking a look at the Foundation framework and figuring out how we can adopt some of these features, I first tried to enable some of the diagnostics and sort of see where we stood. And at first it might be a little overwhelming when you're faced with a large wall of diagnostics because you haven't quite expressed your intent to the compiler yet. But I found as I sort of worked through Foundation, there were sort of discrete areas that we could take it upgrading incrementally and in pieces.

And that at first, although it may be daunting to try and accomplish such a lofty goal, even if you just make a little bit of progress by defining something that you know to be true via new compiler annotations, it might be able to provide you some safety areas and fixing something somewhere might not just fix some of the diagnostics but might actually allow you to take a big chunk of them and resolve them so you know if you build with Xcode and you build with the new features enabled and you see a lot of warnings and errors don't let that be your reason to not start adopting today.

[Angelica]

The other thing that I love hearing too is you could turn it on and then turn it back off when you start iterating on those diagnostics and those issues doesn't necessarily need to be like a full — "one fell swoop" is the phrase. Sima?

[Sima]

Yeah, definitely in SwiftUI we had a lot of similar story with adopting some of the strict concurrency tracking and iteratively approaching that.

The one advice that I have is maybe try to first turn on just Swift 5 and strict concurrency tracking to at least see what are the kinds of errors that I will get once I turn on Swift 6. And so that would give you kind of like an overview of what is there to kind of address. But also I think kind of what Holly said, it doesn't necessarily imply you have to like rethink how your code works.

A lot of times it's like you may already have some other mechanism in place, like you might have like a lock or something like that. And so then Swift also gives some of those maybe a little bit unsafe annotations you can apply. sometimes to unblock yourself and move forward iteratively. And then once that's done, you can go back to those places and audit them and see if Swift provides any modern tool, like a mutex or something else that gives you that static concurrency checking. So yeah.

[Angelica]

So great to get insights, especially from frameworks.

As maybe developers don't know this, but a lot of the framework engineers here really behave and plan things like they're maintaining a library that everyone else might be doing for their packages or what they're sharing with developers. So the process of migrating was very much similar. You follow the migration guide just like developers are doing out there as well. All right. So let's jump into some great questions because we have a lot of them trickling in.

Task { @MainActor in } vs MainActor.run

First let's take a look at... Ooh, this one has 31 upvotes. This is an exciting one. And this is going to be fun. This is all code speak. Is Task { @MainActor in … } preferred over task, insert code to run in the background, await MainActor.run { … }? Which one's preferred?

[Holly]

Yeah, so I can answer this one.

The difference between those two things is Task { @MainActor in … } will run the entirety of the task body on the main actor. You know, of course, some operations in there might be isolated to something else, and the task can switch between different isolation domains depending on what the code calls. But like, for example, if you have just some synchronous code within the task and you want all of that to happen on the main actor, then I would write the @MainActor annotation on, you know, the task's closure signature.

If you just have a specific operation, like a couple lines of code within the task that you want to run on the main actor, but everything else you want to run in the same context that you created the task in, which is the default isolation behavior of the task. That's a case where you can use something like MainActor.run { … }, or you can just factor that code out into a main actor isolated function and call that from within the task.

nonisolated vs nonisolated(nonsending)

What's the difference between nonisolated and nonisolated(nonsending), and why was the latter introduced? Allan, is this something that you might be able to cover?

[Allan]

So nonisolated means that, you know, a declaration has no preference for isolation. It doesn't have a fixed isolation. It can be used in multiple isolations.

nonisolated(nonsending) is about asynchronous functions, right? There's a behavior that was originally implemented for asynchronous functions in Swift concurrency where asynchronous functions would move to the concurrent thread pool, the shared executor, by default. And we found that that wasn't the right trade-off. As we learned more, as people were adopting Swift concurrency.

And so nonisolated(nonsending), which is now the default behavior, if you opt in using the approachable concurrency settings, that's the new behavior where your asynchronous task or your asynchronous function won't leave the isolation that it was called from right away until there's a explicit reason to basically.

MainActor model with heavy I/O

When marking a view model with @MainActor, how should we handle a method inside it that performs heavy I/O processing? How do you process outside of MainActor and main thread?

[Holly]

Yeah, if you have a particular operation on any main actor isolated type and you want just that operation to be offloaded, you can annotate just that method with a different isolation.

So for example, if you just want to offload it to the concurrent thread pool, you can now use the @concurrent attribute is the right explicit spelling for that. The compiler will prevent you from directly accessing any main actor mutable state that's on that view model or whatever your main actor isolated type is to make sure that you're not accessing that state at the same time as some other operation on that type is accessing that mutable state from the main actor.

[Angelica]

Take a look at that also. A lot of references from last year's WWDC. We covered a lot of the uses of @concurrent there as well.

Task from MainActor

The next question that we have, when a task is created from @MainActor, will that block the main thread and cause UI hitches? So a task being created from @MainActor. Allan, or maybe Sima, having worked on a UI framework, I feel like you probably encountered this often.

[Sima]

I think that a task that's created on a main actor, I think there's still a suspension point that the compiler creates.

And so while that work is being performed, I think the main thread can still kind of work on the UI updates to not cause the hitches. And I think that task machinery in general kind of tries to schedule your work on the concurrent thread pool to not block excessively. And so that's one of the nice kind of behaviors that tasks provide. So, yeah.

[Holly]

I think it depends on what the task does. So if you create a task that's isolated to the main actor and that task starts running on the main actor, if you're doing some super expensive work that takes a very long time, then that might block the main actor and you might notice that and you might want to profile that code in instruments and then decide to either insert some suspension points or offload pieces of that expensive task off the main actor.

So I really recommend that you don't worry about proactively offloading everything you can from the main actor. There are some things that are totally fine to run on the main actor, but if you notice, you know, hangs in your app and you identify that piece of code through instruments or your profiling tool of choice, that's really when you should then go in and make targeted changes to move that expensive work off.

Best practice to cancel a Task

Another task-related question, what is the best practice to cancel a Task? Anyone have any insights on this?

[Sima]

I can. I think the practice I've been using and maybe some people here can also kind of add to that, but I think storing a task on the type and then just calling cancel on that is good if you're trying to kind of granularly manage the lifetime of that task. But I'd be curious if there's any other ways.

Yeah.

[Allan]

I mean, that sounds right to me. Any task that you want to be able to cancel, you're going to need to track in some way. And so you need to hang on to the handle. So yeah, it's pretty simple though.

[Angelica]

All right. Awesome. All right, next is something related to @concurrent. When should I reach for @concurrent instead of nonisolated async?

Is there a one sentence rule? Allan, do you have one sentence for this?

– Sorry, can you repeat the question?

Yeah. When should I reach for @concurrent instead of nonisolated async?

[Allan]

Okay, so I mean @concurrent is going to be run on the shared thread pool in the background guaranteed.

nonisolated async is that going to be run guaranteed...?

[Holly]

It depends on your upcoming feature settings. So as Allan described earlier, there was @concurrent. Right, the one sentence explanation is @concurrent, you should always use @concurrent if you want your async function to be offloaded to the concurrent thread pool.

It is an explicit spelling for a behavior that used to be implicit for async functions that as Allan described were changing because we found that wasn't the best trade off and it's not the best default for async functions. So you should always use concurrent now when you want that behavior.

And with nonisolated async, it depends on whether you have the approachable concurrency upcoming features enabled in your Xcode project, which that is the default for new Xcode projects. If you don't have it enabled, I recommend enabling those settings because it will give you a set of defaults that are a bit easier to use with the Data Race Safety Diagnostics.

Possible to have too many actors?

Is it possible to have too many actors? Is there an intrinsic limit? I hear Allan struggling. Do you want to take?

[Allan]

I mean, there's no — well, there's probably an intrinsic limit, you know, depending on how much memory you have. But definitely I would I would think about limiting how many different actors you have in your program, because generally speaking you want to have just a few isolation domains in a program to think about concurrent programming is hard like we said earlier and and really most of your code should be synchronous unless you have a reason to introduce concurrency and a reason to introduce different isolation domains every actor is a different isolation domain and if they need to work together and that creates complexity in your program so you really want to think carefully when you factor something out into an actor about whether it's really appropriate for you to be adding the complexity of needing to access that asynchronously from other parts of your program.

[Angelica]

I think that's a very common question also. It's like, when do you actually use an actor? And it's specifically if you could define that it is always going to need its own isolated domain and the entirety of it has a functionality that requires you to maintain that.

I think on the slide I had a couple of examples of network caching or accessing the database. Those are common things that often you already know is gonna be in an isolated domain anyway.

SwiftUI .task modifier cancellation

Could a task start by the .task { } modifier in the SwiftUI view-- oh. Hold on, could a task started by the task modifier in the SwiftUI view be cancelled if the view is refreshed by state change?

[Sima]

So the task modifier in SwiftUI is going to be, we're going to cancel your task when your view disappears.

And state changes usually don't lead to your view kind of fully being destroyed by SwiftUI. State works in a way where when the state change happens, where you evaluate your view's body, but we don't actually like destroy the view. The one thing that you may see this behavior in is like if you have like an if statement in your view's body, and then like one branch of that if statement becomes true, and then there was a view in the other branch, and that had a task in that.

And so then once the if flips, we would actually cancel that task because that other view, the other branch of the statement is destroyed. So you can also think about it as like onDisappear, it's a modifier. And so the task modifier manages the cancellation kind of with the same timeline as the onDisappear modifier if you've used that before.

What does nonisolated mean?

Intuitively, what does the nonisolated keyword mean, especially after the recent changes?

[Allan]

Well, it hasn't really changed necessarily, but it is defaulted in some cases now. nonisolated just is a statement of your intent to not require any particular isolation for a type or a function. When you annotate a type @MainActor, you're stating that you only want this to be used from the main actor.

nonisolated is kind of removing all restriction. you can use it from any isolation domain. And that's really flexible and powerful, and it's often the right choice for code in libraries where you don't know where the app is going to want to use your library types. And it can also be appropriate even in your app for just any kind of like low level model that doesn't really have any inherent needs to be isolated to the main actor or some other isolation domain.

[Holly]

And that's part of what motivated the behavior change for nonisolated with async functions.

When you have something that's marked as nonisolated, you can use it from anywhere. If you have a method that's nonisolated and you call it from somewhere, it's gonna run in the isolation domain that you called it from, that used to not be the behavior for async functions, but now it is. So now there's like that consistent meaning of, okay, when I see nonisolated, it means I can use it from anywhere. And when I call these methods, they're going to run wherever I call them from.

Task timeouts

Are there plans to introduce tasks with timeouts, like a Grand Central Dispatch Group timeout, or something to run after a certain period of time, like the DispatchQueue.main.asyncAfter?

[Holly]

There's actually a proposal that's under review right now that adds this exact API to task. I think the naming is under debate right now.

I think right now it's called withDeadline, but it is meant to provide that exact behavior where you run a task and if it exceeds a certain amount of time the task will be cancelled. That behavior is also a point that's been getting a lot of feedback in review.

If you're interested in checking that out you can head over to the Swift forums and check out that Swift evolution proposal and if you have any feedback feel free to you know comment on the review thread on the forums if you have any feedback about the proposed behavior of it.

Default Actor Isolation

For a new app target, should I adopt MainActor default isolation? Does your answer change for libraries?

[Sima]

Yeah, I can take that one. So in Xcode 26, all new app targets actually use main actor isolation by default. And I, as a person from the SwiftUI team, I think that's an absolutely amazing choice for app targets because in our UI framework, for example, view is already on the main actor. And a lot.

And if you wrote a SwiftUI view and then you might have like model type as well you might have noticed it's actually convenient to also put in the main actor and now with the main actor by default turned on you don't have to do that and it just makes it so much easier to write UI code and reason about it and you don't have to like think about synchronizing your state because now everything is in the main actor but when it comes to libraries I think my answer would change because also like depending on your what your library does but I think if you're trying to be performant then concurrency actually allows you to optimize a lot of the behavior in your code by parallelize in a lot of operations and I think libraries could make use of that in some cases and if you find yourself kind of seeing that oh like your operation is like taken too long and you would like to can split it into and then yeah like it would make sense to introduce more concurrency into it, especially your library code. I mean, and the same applies for app targets as well.

You start in the main actor, and then once you see, you know, maybe a potential for a performance optimization, or you like profiled it, and you see like the main thread being like blocked on some expensive operation in your model, then you would want to consider kind of adding those @concurrent annotations and moving things off the main actor.

[Holly]

I also think a lot of library APIs, general purpose library APIs, they're not really specific to any particular isolation domain. Most of Foundation is not isolated and clients of Foundation can choose where they want to make use of those APIs.

[Jeremy]

Yeah. For example, with Foundation, most of our APIs aren't specific to any isolation, so most of our APIs are not isolated. We do have a few that are specific, like for example, the UndoManager, which is a very heavily UI focused thing and is annotated with the main actor.

For Foundation, it makes sense for us to keep most things nonisolated by default and add that main actor isolation when it comes to our UI related things. But I could see we're working on an app, it's almost the inverse where most of your things are on the main actor and you want to potentially offload from there. Yeah.

Task cancellation in deinit

If a class launches a long-running task and calls task.cancel in deinit, does the task actually stop executing? And is its memory released immediately after cancellation? Also, are tasks automatically canceled when their owning scope is deallocated? So let's start with, does the task actually stop executing when you call task.cancel?

[Holly]

Okay, I didn't follow the entire code setup there. But the way that cancellation works in Swift concurrency land is cancellation is cooperative. So depending on what the task is doing, that operation needs to check for cancellation. And usually the course of action there is to, for like some library method or whatever, to support cancellation is to throw a cancellation error.

So if you're running some long synchronous code and someone cancels the task handle, that synchronous code isn't going to get interrupted at any moment. And that's actually an important behavior because that work might need to be doing some cleanup operation that must happen, like, atomically without being, you know, interrupted by something like task cancellation.

So, no, the task does not necessarily immediately stop running because of that. But as soon as you hit a piece of code that handles cancellation and maybe throws a cancellation error, that's when, you know, it'll return back, usually by throwing such an error. There was another question about whether or not tasks get cancelled when When the owning scope is deallocated?

When the owning scope is deallocated.

[Allan]

If a class has a task handle like we were talking about earlier if you want to track a task to cancel it You need to store it and as a property maybe no, it's not automatically cancelled just because that...

[Sima]

That's one of the gotchas with like if you opt into kind of managing your tasks lifetime, I think is that you don't like you always have to remember to cancel it. And that's I think it's actually good that you're, you know, being explicit about your tasks lifetime.

Task swallowing errors

So we have one about task swallowing errors. A task does not require handling of thrown errors, which can lead to silently ignored failures. Are there plans to require handling of errors from a task?

[Holly]

Actually just posted an acceptance on the forums this week for a proposal that addresses that exact pain point.

So there now will be in Swift 6.4 a new diagnostic in exactly that case where you have a task and you have not handled the errors within it. Now there are some different ways to handle errors within a task either in the task body you can handle any errors that are thrown or if the task itself can throw then you can and you want to get a handle to the task. you can actually wait until you get the tasks value in order to handle any potential errors that were thrown. So those are two different ways that you can actually handle task errors. But yes, you will now get diagnostics with the acceptance of that proposal.

So again, if you're interested head over to you know, either the Swift forums or swift.org/swift-evolution and you can check out that proposed behavior that was accepted this week. That's the reason I was so excited to get to this question.

Best practice asynchronous requests

What's the best practice for asynchronous requests in response to a button tap? Kicking off a one-off task or exposing a synchronous method from your model that kicks off a task internally?

[Sima]

I can take that. The general recommendation, although I think you can structure your code in both ways, but I would prefer kind of moving that asynchronous code to your model and then the model kind of responding back with a synchronous output to your view so that your view stays like responsive and the model actually handles all of that asynchronous work and then the benefit of that is you can also test that asynchronous work outside of your view and that will bring kind of greater benefits to your entire codebase.

Nonsendable NSObject

How do you handle legacy NSObject types which are nonsendable to work with @Sendable closures? Allan, you're thinking?

[Allan]

I'm trying to think if there's a general recommendation I can give that's--

– It could be specific to the NSObject type?

I don't have a specific recommendation for the NSObject type specifically.

[Holly]

I think it very much depends on the situation of the code.

So, you know, if you actually have a class that inherits from NSObject, and your class doesn't add any state, neither does NSObject itself. So it is possible that the type that you have there, if it doesn't have any state or it protects its mutable state, then you should just mark that as unchecked sendable because you're managing that state yourself.

If you do really have mutable state in that class, I don't recommend capturing that inside of a sendable closure. I do think that now with the region-based isolation feature set, which was new with the Swift 6 compiler. There are other tools that you can use when you need to pass a closure over an isolation domain, but you don't necessarily need to like be able to call that closure multiple times, potentially in parallel.

You can instead use the sending modifier, which does allow you to pass a closure to another isolation domain. And this is actually what the task initializer or all of the task creation methods use because that closure will go to a different isolation domain depending on where you want to run that task. But it's only called once. It's only sent over an isolation domain one time. So that allows you to capture nonsendable state, like classes with mutable state, classes that inherit from NSObject, as long as you don't use them, again, from the original isolation domain where you formed the closure.

So depending on the pattern of your code, there might be different solutions there. Like Allan said, I don't think there's general piece of guiding advice for all classes that inherit from NSObject. But there are a variety of tools that you can use depending on what exactly the concurrency guarantees of your code are.

[Jeremy]

One more additional tool that we've used in Foundation is, you know, oftentimes when you're dealing with something that might inherit from NSObject and it has some mutable state that you can't necessarily pass between isolation domains if the, you know, if it doesn't work out.

Many times in our code we've noticed that, you know, while we don't necessarily need that entire NSObject, there might be pieces of it, pieces of it that we do want to be able to share. And in Swift, oftentimes you might have an NSObject that in the end ends up having like a string property or integer properties, which are of sendable types. And so it's also possible while maybe you can't necessarily share the whole NSObject between the isolation domains, you can pull out the pieces that you do need to be able to share them as long as you don't need to worry about mutations.

And that has helped us with not needing to make everything sendable. We can instead just pick out the pieces that really do need to be shared, make those sendable, and let the legacy code that encapsulates all of it still be nonsendable.

[Sima]

I also want to add to just specifically dealing with @Sendable closures and kind of capturing the state. Oftentimes it's very helpful to use closures capture list to like only capture the sendable pieces of your state without kind of trying to send over the entire containing type that may not be sendable.

[Angelica]

Really great techniques. Hopefully that'll provide some insight, but definitely take a look also, documentation, go to forums. There's often a lot of different tactics or suggestions happening in conversation there. So check out the forums. All right. Let's move on to... Ah, you mentioned testing earlier, Sima. What is the best practice to write a unit test for async/await code?

[Holly]

I think you can use Swift testing and make your test method async. And you can test it there. And you can also, if you need that method to run with a particular isolation, you can actually mark the test itself or the entire test case type with @MainActor or wherever you need that test to run.

Use case for Task.detached

All right, let's take a look at the next question on Task.detached. What is the best use case for Task.detached?

[Holly]

I've seen a lot of questions recently about when to use Task.detached versus when to use Task { @concurrent in … } because now those two things have very similar behaviors.

The main difference is that the plain task initializer and you have some isolation that you specify inherit certain things from the surrounding context. Obviously if you've explicitly specified an isolation, it doesn't inherit isolation, but but it does inherit the priority from the surrounding context, whereas Task.detached is completely detached.

Nothing from the surrounding context is inherited by that task. And so if you want something that's completely detached from the surrounding context, use task.detached. If you just want to control the isolation, but you want those other things to be inherited, use Task { @concurrent in … }.

Best practice for spawning many Tasks

What's the recommended pattern when processing many, say, thousands of files and not just spawn a task for every item? Any recommendations for that?

[Allan]

Profile first, like you said, because you never really want to just theorize about what might improve your performance the most. Experiment, profile, see what's working. But I could imagine that spawning a task for every file might not be the right design. Maybe you instead want to have some kind of batching logic.

But you could you could try the trivial, you know, the most straightforward approach first and see how it works out. Before you do something more complicated. This one's related to a SwiftUI view. So, Sima, this might be for you.

Cancel URLSession request from SwiftUI

An async task from a SwiftUI view that calls a shared @Observable service.

Say if the user dismisses the view before the task finishes, what's the way to cancel it so the URLSession request is actually dropped, not just abandoned, and no UI state mutation happens post dismiss?

[Sima]

So, this is a really good question. in SwiftUI's .task { } modifier. And as we've talked before about kind of needing that handle to cancel the task, SwiftUI doesn't currently expose any way to kind of give you that handle so you can cancel the task owned by SwiftUI.

But I think a way I would do it just because task is effectively just onAppear that starts a task, maybe there is a way to somehow structure your code so that you actually own that task. And then within the onAppear of your SwiftUI view, you can start it. And then when the view, or I forgot, what was the condition when you want to cancel it?

– When the user dismisses the view before the task is finished.

Before the task is finished. So, yeah, so as long as you have the task handle, then you should be able to just call cancel yourself. Although now that I hear the dismiss, I'm not sure, like I think you should just be able to use the task modifier as well because we would cancel the task spawned in there when the view is dismissed.

So it just depends on what your expectation is, but there's always a way for you to also just use your own instance of task that's not owned by SwiftUI if you really need to do that. Have granular.

Exposing data from actors to SwiftUI

What are Apple's recommended best practices for exposing data from actors in SwiftUI? Creating a proxy data broker between the actors and UI adds a lot of boilerplate and requires my own notification mechanism when actor data changes, I believe. Do you have any recommended best practices? So exposing data from actors in SwiftUI.

[Sima]

It would depend on how that proxy object looks like, but I think as we've also discussed, and as Allan also brought up before, introducing actors into your code does add that extra synchronization points where you have to await on certain pieces and then you to access the actor state from SwiftUI's main actor isolation, you'd have to introduce a layer that does that synchronization.

So I think it's pretty much inevitable that certain layer has to be introduced to synchronize that state. I think it would be helpful to maybe hear more about the specifics of what is the boilerplate that's been involved here. So yeah, I wish I knew more kind of details.

[Angelica]

And then notification mechanism that's required, things like that. Yeah, definitely.

Hopefully we'll be able to continue conversations, perhaps in the forums, to understand a little bit more about this.

[Sima]

Or maybe file a feedback that's with your project, because I think it would be very interesting for me also to look at, to see what is your setup, and maybe is there room for improvement there that we could improve to make that easier.

Async code in defer

This is another question from a developer. Any plans to support defers in async contexts?

[Holly]

What a great question. Another proposal that I think I was the review manager for. I don't remember. But anyway, it's an accepted proposal for Swift 6.4 that just lifts the limitation on async code in defer blocks. There's no special syntax in a defer block. You can just call async methods and it just works. So yes, another recently accepted proposal.

[Angelica]

Loving these questions that basically point you to the evolution, to Swift evolution, being like, it's there.

[Holly]

There's a dashboard on swift.org. You can actually filter it by implemented proposals for Swift 6.4. If anyone's curious to see all of the, you know, the entire list of proposals that is upcoming for Swift 6.4, That's there on swift.org.

Swift 5 to Swift 6 migration

If we are currently using Swift 5, have approachable concurrency set to no, default actor isolation is not isolated, and strict concurrency checking is minimal, what is the best first steps for us to move to Swift 6 concurrency?

[Holly]

I think the step that I would recommend first is enabling those approachable concurrency features.

There is some migration tooling that you can use at the command line and the Swift migration guide covers how to do this to help you in the case where you are relying on that old behavior for async functions if that's something you're using extensively in your project. But I think and there's a straightforward way to just maintain that behavior it's just to write concurrent on those async functions. So there are very mechanical ways for you to migrate to those upcoming features without actually changing the behavior of your code.

But I think those approachable concurrency features are meant to make it so that there's some data race safety errors that you just don't have to deal with. And that was part of the purpose of those is to make the defaults such that there's less concurrency without you having explicitly introduced it yourself.

But after that, you can either change your complete concurrency checking from minimal to complete, or if you'd like to go on a feature by feature basis in the Xcode build settings, there's a section with specific upcoming features where you can enable specific categories of the data safety diagnostics. For example, static variables are generally unsafe if they're not isolated, and they are actually variables that you can mutate from anywhere.

You can enable just those diagnostics, deal with all your static variables, and then move on to the next category. So if you prefer to work that way, where you are dealing entirely with the same kind of problem and maybe there's like three different solutions and you go through those ones and knock them out and then you can enable that and move on to the next category of errors.

Jeremy, you mentioned that like you had an experience, you know, you turned them all on. What was your approach?

[Jeremy]

Yeah, you know, I think I had that experience where we turned them all on at first, there was a lot of diagnostics and it was a little overwhelming, but like you mentioned, being able to sort of, now in Foundation, we've started adopting some of those upcoming feature flags.

I don't think we've officially enabled Swift 6 in all of our targets yet, but going on first a target by target basis where some of our targets that contain simpler code, we were able to enable more of the features because we just didn't have as many of the issues that we needed to work through. And in others, we've enabled some of those upcoming feature flags and it's been super simple to be able to update those either in your Xcode project or for us in Swift package manifest, you can easily enable them.

And it's really easy for us to be able to enable the diagnostics that we as a team understand and we've decided on our approach for these, whereas some of the other ones where we haven't quite decided on our approach and needs a little more thinking, we can leave those turned off for the moment.

[Sima]

It was very similar for SwiftUI adoption as well. I think what also helps when you see like a huge swarm of those warnings in your project is once you kind of start to look at some of them you kind of see some patterns and then kind of thinking about a more general solution to those identified patterns actually helps iterate faster because then you kind of apply that solution like in multiple places and you see like so many of those warnings just go away because like you know you've marked something with the main actor or like you added the missing like sendable annotation and now suddenly a lot of things just work all the callsites of that code are now concurrency safe. So that was also a pattern we used to incrementally turn on those features.

[Angelica]

Yeah. Calling back to, I think, at the beginning of this Q&A when we talked about misconceptions, I think the assumption is that you have to refactor everything to a proper concurrency model when realistically looking at those diagnostics, maybe breaking it down by feature, allows you to look at it iteratively, focus on very specific things with patterns that usually have only one or two fixes really.

And then you could really think about once you have all of that and your current architecture of app actually works with the native Swift concurrency, then you can start thinking about how do you express it? How do you actually look at the architecture of your app and start breaking it down to very specific isolation domains or breaking down your models into separate properties that have different concurrency stories basically.

Does MainActor guarantee main thread?

Does MainActor guarantee main thread? If not, when does it use another thread? This is a great one.

[Allan]

On our systems, MainActor is main thread for sure. I think you could imagine an environment that Swift compiles for where MainActor might technically be some other thread. There are programs that are not apps where that might make sense, but for most intents and purposes, MainActor means main thread.

[Angelica]

Are there any other than on a different system, are there any other places where it would not be on the main thread?

[Holly]

So there are some cases where you can have something annotated as main actor where it is possible to call from off the main actor in the context where the call site doesn't have strict concurrency checking enabled. I think the case where people see this most frequently is when you are interoperating with C or Objective-C, C++.

So obviously those languages don't have static data race safety. So it is possible to get a data race if you're calling something that's main actor isolated from one of those contexts. But Swift does also have tools to insert dynamic checking if you really want to be able to catch cases like that at runtime.

Best practice async function during app termination

What's the best practice to handle async shutdown functions during app termination? Like an OS lifecycle method such as applicationWillTerminate. These methods are synchronous and they expect you to wrap up work there. Is it safe to use a semaphore in that scenario or are there better alternatives?

[Allan]

Generally speaking, you don't want to use semaphores to bridge asynchronous code to synchronous code because it's likely to result in deadlock. If you block on an asynchronous method using a semaphore and the concurrent pool blocks on that, it's quite likely you can use up all the resources and suddenly nothing can make forward progress. So I don't recommend that.

The situation that you describe yourself in is tough. that you're sort of in the situation where you've been given a synchronous callback, it's the only interface you have, trying to refactor as much code as possible that you need to handle in that context so that it is synchronous itself is probably the best approach. Obviously that can be really difficult in some situations, but yeah, I would not recommend using a semaphore to solve that problem.

What is an Executor?

What is an executor? Is that exposed to us in Swift? I hear this a lot from developers. Anyone have any insight of how do you define an executor?

[Holly]

Sure. An executor is what manages the operations that run on an actor. Not just an actor, there's also task executors as well. These are concepts that are exposed through types in the concurrency library. You can make use of them, you don't have to. Actors have default executors. What that default executor does is it manages scheduling tasks or more precisely the synchronous pieces of a task called a job.

And actors have what is effectively a priority queue of jobs that need to run on the actor from various tasks and the executor handles scheduling and running those. By default, that happens on the concurrent thread pool. You also can implement your own custom executors by conforming to a protocol and implementing whatever scheduling you need for your actor.

And then there's a method that you can provide on an actor that creates an instance of your custom executor type if you want to control that behavior. But again, the defaults are there so that you only need to drop down into that behavior if you need really like fine-grained control over how those tasks are being scheduled or running, or maybe you wanna manage your own custom thread pool.

That's something that I've seen people do in the services land. So yeah, those tools are there, they are available to you to use, but there are also sensible defaults in the case where you don't need that kind of control. It's available, but you don't have to use them.

Migrate from GCD and operation queue to Swift concurrency

What does a healthy migration path from GCD and operation queue to Swift concurrency look like in a large existing app? I kind of want to hand this to Jeremy because I'm sure you've experienced this yourself.

[Jeremy]

Yeah. So, you know, on Foundation, we've experienced a lot of these types of migrations where we're moving from things that use either GCD or operation queue or run loops or, you know, lots of various things. And, you know, I think a healthy migration, first and foremost, is an incremental migration.

I think trying to, as Holly mentioned earlier, change the architecture of your app all at the same time while you adopt these features is not necessarily something that will prove easy to do nor easy to have potentially other members of your team review if you're working with a team of other developers on apps like this.

So I think an incremental migration where you're sort of looking at the architecture of your app that exists today. You know, for example, when I was looking at Foundation and how we can adopt some concurrency attributes in Foundation, the first thing I looked at is, okay, what are types or functionality that we have in Foundation that are, you know, the implementation is known, it's easily expressed using the compiler tools that we have today, where it's trivial to just add a few annotations to express things to the compiler, and it works.

And then from there, once you have sort of the base layer of, okay, you've expressed the things that are easy to do, now let's start working on some of the harder things, It helps you work at it a piece at a time so that you're not trying to fix both just a simple missing sendable annotation while also figuring out how to re-architect some other things.

And I think we found in some cases it was simple enough to add annotations like, for example, I mentioned the UndoManager, which is heavily UI related. We simply made that the main actor bound because that made sense for the UndoManager. Other types, we simply added a sendable annotation because they were simple Swift value types and it was easy to work with.

But there are other things that you might need to consider potentially thinking about a different way to do it. For example, Notification Center, which is something that wasn't, strictly speaking, easy to work with with async/await, but we re-envisioned how Notification Center can work using new APIs. We tried to structure it in such a way that you can use new async/await APIs and it can still integrate with older legacy APIs using some of the unsafe escape patches to cross the border between them.

So I think, you know, sort of working in that incremental approach where you start with the easy things and potentially enable some of the warnings that you were able to completely resolve at the beginning to make sure you don't accidentally backpedal on some of those. And then take a look at your architecture and try and decide, okay, now how do I map some of the more complex concepts into Swift concurrency? And you know, it takes time, you know, Foundation, we're still in the process of adopting some of these features in our framework as, you know, new compiler features come around, as people propose new suggestions on the Swift forums.

Over time as you work on it, we found that you're able to sort of piece by piece get to a place where now a lot of the potentially complex interactions between what was previously threads or queues or run loops is now much easier to understand when you're reading that code because it's all nicely expressed in your Swift code instead of being either a documentation comment that you might forget about or some other note left somewhere.

[Angelica]

Yeah, I'm curious. I mean, it sounds like you approached it using that iterative process. A lot of that, like when you started re-architecting or like rethinking about how these notification center, for example, how you made these APIs or remodeled it, maybe that's the wrong word, but like did that work once you started trying to figure out and really being explicit about how it's expressed to the compiler?

Did that kind of benefit any other new features or new things that you're building in or the current concurrent work that you're still working on?

[Jeremy]

Yeah. I mean, I think there's a lot of that, a little bit of an upfront burden of trying to decide, okay, how are we going to rethink something that was heavily operation queue based? Or how are we going to rethink something that was heavily GCD based in the Swift concurrency world?

And while it takes time to come up with solutions that might work better, once you have things that integrate very nicely with Swift concurrency, we found that as we adopt new features, it's much nicer to be able to more easily build on top of them because now that we have some base layer architecture that works with Swift concurrency, especially in apps that use Foundation, it's much more easy to integrate that with new SwiftUI features that might take advantage of Swift concurrency or other APIs.

And so, yeah, definitely as we've adopted some of these more base architectures, as we've adopted Swift concurrency throughout those, they start to fit more nicely within the other async/await and other concurrency APIs that you see coming from the Apple SDK or as well as other packages as the ecosystem slowly moves into a world where we're able to express these things well in our code.

– Awesome.

[Sima]

Just one small point to add. I also would recommend prioritizing the areas where you are writing new code, areas of your code base where you're not actively, you know, it's in maintenance mode. Maybe you fix a bug in it here and there, but it's code that's been around for a long time. Lots of concurrency bugs have been shaken out over the years. It's okay to not dive in and go and rewrite that whole sale to modernize it or whatever.

I think getting to a point where you can write new code using Swift's concurrency features and expressing the new code that you write with static data race safety guarantees is the most important goal to get to. And then all your legacy code that you have can be revisited over time. And when you have a particular motivation to go and do that work.

Capture weak self in Tasks

Does it make sense to use weak self-capture in tasks?

[Allan]

It really depends on what you're trying to do. It's definitely -- I've seen that it's very popular to do so. People tend to want to sort of optimize this kind of thing where they're worried about capturing reference to self and keeping it alive for too long. And if you're worried about keeping it alive for too long, you might use weak self to make sure that the task is not what's keeping it alive for too long.

But you really need to understand the, what are you trying to accomplish in that task? It might be appropriate for that task to keep self alive until it's finished because you want to complete that work regardless of whether everything else in the system has released its references to self. It really depends on the kind of work.

You know, maybe a task kicked off in a view, you don't want the view to be held onto by the task for too long after it disappears. That might be a good example of a place to use weak self. I don't know if you have opinions on whether that's an anti-pattern or not, but like, it really depends, as with most things.

[Holly]

A place to be careful of is infinitely running tasks. - Yes. Which then, you know, If there's no end to the task, then the task is going to be the thing that keeps self around.

And I think that's also the case where you can inadvertently get into reference cycles if you're also storing a handle to that task on self. But as long as you don't have that, then it might be appropriate to use that.

Isolated conformance

"I have a generic function that takes a codable object T as a parameter, makes a network call to retrieve an array of those objects, and then returns an array of T. This will generate an error in Swift 6 about conformance that it must be on main thread. What's the best way to avoid this?" This is a very specific use case.

So generic function that takes codable object t as a parameter, makes a network call, retrieves an array of those objects, and returns an array of T. And this will generate an error.

[Sima]

Isolated conformances can maybe help. I heard there was an error about main actor conformance not being-- Yeah.

[Angelica]

It's saying that the error in Swift 6 is saying that the conformance must be on the main thread.

[Jeremy]

Yeah, I suspect that it might be isolated conformance is coming into play here. If, you know, depending on what the features are of the target you're building, you know, if you have the main actor isolation by default enabled, it might be requiring that the conformance decodable that you pass is on the main thread or that the conformance might be main thread, but main actor, but you are passing it to something that doesn't require that. So I'd say to check out some of the documentation that we have, the sendable meta type protocol might be something that you might be a good annotation to add to this API.

Depending on whether or not it's the conformance, that's the problem where the value is being returned, either like sendable meta type on the type that you're passing in or perhaps using the sending keyword on the values that you're returning if you're just fetching them from the network, handing them off to the caller and never touching them again.

I suspect some of those keywords might help you express the fact that you are taking a conformance to Codable and perhaps sending it to a different isolation to perform the network request or taking the values that you've created from that background isolation and sending them back to the calling isolation.

In one of those directions, I suspect one of those keywords is likely something that would help resolve that.

[Angelica]

I think the context here is actually really, really valuable. As we mentioned earlier, Feedback Assistant, giving a sample, understanding that context, I think will help us understand exactly what type of recommendation to give and check out the forums and docs.

Wrap up

So I have one more question to wrap up. We've talked a lot about evolution proposals that are coming up, as well as the Foundation and SwiftUI's adoption. A lot of the changes in Swift concurrency have been largely because of the feedback and contributions of the community and working with Engineering here, as well as developers out there. But becoming a contributor sometimes can feel daunting. What would you recommend to people who want to get involved? Sima, you mentioned that you contributed to the concurrency model.

And this was prior to you becoming, coming to Apple, right?

[Sima]

Yeah, I actually was involved in the Swift's open source project as an outside external contributor. And I would really like to recommend the Swift Mentorship Program. It's a program that the Swift contributor experience workgroup runs every year. And I think usually it opens like early, early summer. And the program kind of asks for your interests.

Are you interested in contributing to one of the libraries like Foundation or maybe the compiler or maybe documentation and depending on your kind of interests, you get assigned a mentor and they kind of help you guide, get you set up, get you kind of some advice on your first contribution depending on your interests.

And that was the program that originally kind of helped me a lot, make my first contributions and get involved in the Swift community. But there's also more ways to get involved and maybe Holly could speak to that.

[Holly]

Yeah, there are lots of different kinds of contributors and contributing code is not the only way to be a contributor to the Swift project.

My favorite kinds of feedback are just questions about concurrency diagnostics. I know they can be difficult to understand and it can be difficult to, given a diagnostic, figure out exactly what action you need to take in your code depending on your setup and your concurrency guarantees of your code.

So something that I find really valuable as someone who's looking to constantly improve those diagnostics is questions about them and showing like a snippet of code where you experience one of these diagnostics and explaining how you understood it and where you might be confused or what things you tried that didn't resolve the diagnostic and then participating in a discussion where people are reasoning through, here's what the diagnostic means, are the ways that I think we can fix that. And sometimes that results in diagnostic improvements. Sometimes that results in other compiler contributions where we might be able to eliminate if it's a case where it's a false positive data safety diagnostic.

So even something as simple as either asking a question yourself or reading through and answering somebody else's question is a really great way to get involved and help not only develop your own understanding of the concurrency model, but also helping other people understand thinking about it as well.

[Angelica]

All right. Well, that's all the time we have. I want to thank our panelists, Holly, Jeremy, Sima, Allan, for answering our questions today and to our team behind the scenes who worked hard to review all of your questions today. Thank you so much. To all of those who are watching, I want to especially thank you all for joining us and taking time out of your day to join us in this discussion on Swift concurrency.

If you didn't get your question answered, don't hesitate to join the in the conversation in forums, in swift.org. And of course, reach out through Feedback Assistant, as we mentioned, if you encounter any issues or want to ask a very specific question.