Sendable and Swift 6 strict concurrency
You will buildBoarding, a new project left with Xcode's own settings, moved to the Swift 6 language mode the way Apple recommends: Strict Concurrency Checking set to Complete while still in Swift 5 mode, the one warning it finds fixed, and then Swift 6 — the move Trips made at once on lesson 17, taken a step at a time.
TimeAbout 45 minutes
WhyLesson 38's actor kept its own state safe. The compiler can check more than that: whenever a value crosses from one actor or task to another, whether it is safe to share. A type that is safe to share is Sendable. In the Swift 6 language mode these checks are errors, and a whole class of bug — two threads changing the same thing at once — stops compiling. Trips has been built that way since lesson 17. But Xcode 27 still makes new projects in Swift 5 mode, where most of the checks are off, and most projects you will meet at work started there. Moving one across is a standard interview question, so in this lesson a new project makes the move.
Trips sits this lesson out. It moved to Swift 6 on lesson 17, in one step, and has nothing left to fix — that is what the words in it are for. This lesson's project is a new one, made the way Xcode makes every project, and left that way.
Open Xcode and choose File ▸ New ▸ Project… (⇧⌘N). If you are looking at the Welcome to Xcode window instead, its New Project menu offers the same choices.
Xcode 27 shows a short list of project kinds. Click Choose Template.
A sheet titled Choose a template for your new project appears. In the row of platforms at the top, click iOS. Under Application, select App. Click Next.
Fill in the options. Product Name: Boarding. Team: leave it as it is — the simulator does not need one. Organization Identifier: the one you normally use, such as com.yourname. Interface: SwiftUI. Language: Swift. Testing System: None. Storage: None. Click Next.
Choose the folder to keep the project in — your Development folder, for example. Tick Create Git repository on my Mac or not, as you prefer. Click Create.
Xcode opens ContentView.swift. The project has two Swift files. BoardingApp.swift holds the type marked @main, which is where the program starts. ContentView.swift is the screen it shows, and so far it is whatever the template wrote:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}Run it before changing anything. In the toolbar, open the run destination menu and pick an iPhone simulator — iPhone 17, for example. Choose Product ▸ Run (⌘R). Xcode builds the app, and Device Hub opens a small window with the screen of the simulated iPhone. After a moment it shows this:

Look at its concurrency settings before changing anything. In the Project navigator, select the project — Boarding, at the top. In the editor, under TARGETS, select Boarding, open Build Settings, click All, and search for each of these:
| Setting | Boarding, as Xcode made it | Trips, since lesson 17 |
|---|---|---|
| Swift Language Version | Swift 5 | Swift 6 |
| Default Actor Isolation | MainActor | nonisolated |
| Approachable Concurrency | Yes | No |
| Strict Concurrency Checking | Minimal | always Complete, in Swift 6 |
Boarding will draw lesson 25's boarding-pass shape. In Finder, open your Trips project's folder, and drag Ticket.swift from its inner Trips folder into Boarding's Project navigator, below ContentView.swift. In the sheet that appears, tick Copy items if needed, check that Boarding is ticked under Add to targets, and click Finish. Then draw it. In ContentView.swift:
struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() Text("BOARDING PASS") .font(.headline) .padding(.horizontal, 48) .padding(.vertical, 32) .background(Ticket().fill(.yellow.opacity(0.35))) }}struct ContentView: View {
var body: some View {
Text("BOARDING PASS")
.font(.headline)
.padding(.horizontal, 48)
.padding(.vertical, 32)
.background(Ticket().fill(.yellow.opacity(0.35)))
}
}Run the app (⌘R):

Apple's advice for the move, in its article on adopting Swift 6, is to go in two steps: "you can increase the level of concurrency checking by changing the Strict Concurrency Checking build setting from Minimal to Complete," and then "To upgrade your project to the Swift 6 language mode, open your build settings, and choose Swift Compiler - Language > Swift Language Version." The first step shows every problem as a warning while the project still builds; the second makes them errors. Xcode describes the setting itself this way: "Enables strict concurrency checking to produce warnings for possible data races. This is always 'complete' when in the Swift 6 language mode and produces errors instead of warnings."
In Boarding's Build Settings, type Strict Concurrency in the search field. Set Strict Concurrency Checking to Complete:
Build setting Strict Concurrency Checking: Complete SWIFT_STRICT_CONCURRENCY = complete
Run the app (⌘R). It runs as before:

And Xcode puts one yellow warning on the line that declares Ticket:
conformance of 'Ticket' to protocol 'Shape' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode
The same file builds in Trips, in Swift 6 mode, without a word. The difference is not the shape; it is the settings. A project that has grown up in Swift 5 mode usually has more warnings than one, and they come in a few kinds; the lesson's exercise is to keep a tally of the kinds, because the same few fixes clear most of them.
SwiftUI declares the protocol like this, in the iOS 27 SDK: nonisolated public protocol Shape : Sendable, Animatable, View, and its requirement nonisolated func path(in rect: CGRect) -> Path. A shape must be Sendable, and its path(in:) may be called from any thread — SwiftUI is free to compute paths away from the main actor. But in a project whose Default Actor Isolation is MainActor, Ticket belongs to the main actor, and so does its path(in:). A main-actor method cannot stand in for one that any thread may call: the conformance "crosses into main actor-isolated code".
A ticket is geometry — a notch size and some arithmetic — with no state that belongs on the main actor, so the struct should say that it belongs to none. In Ticket.swift:
import SwiftUI/// A ticket: a rectangle with a round notch bitten out of each side.@Animatablestruct Ticket: Shape {nonisolated struct Ticket: Shape { var notch: CGFloat = 14 func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.minX, y: rect.minY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY - notch)) path.addArc(center: CGPoint(x: rect.maxX, y: rect.midY), radius: notch, startAngle: .degrees(270), endAngle: .degrees(90), clockwise: true) path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) path.addLine(to: CGPoint(x: rect.minX, y: rect.midY + notch)) path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: notch, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true) path.closeSubpath() return path }}import SwiftUI
/// A ticket: a rectangle with a round notch bitten out of each side.
@Animatable
nonisolated struct Ticket: Shape {
var notch: CGFloat = 14
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY - notch))
path.addArc(center: CGPoint(x: rect.maxX, y: rect.midY), radius: notch,
startAngle: .degrees(270), endAngle: .degrees(90), clockwise: true)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY + notch))
path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: notch,
startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
path.closeSubpath()
return path
}
}Run the app. This time the build has nothing to say:

Marking only the method, nonisolated func path(in:), also works — the compiler accepts either. Marking the whole struct says more plainly what it is.
With no warnings left, switch the language mode. Apple's sample for the move: "After the warnings are resolved, change the Swift Language Version in Build settings to Swift 6." In the same Build Settings, type Swift Language Version in the search field, and choose Swift 6:
Build setting Swift Language Version: Swift 6 SWIFT_VERSION = 6.0
Run the app:

It builds without a warning and runs as before. The difference is in what it refuses from now on: the data race in the first edge case below compiles without a word in Swift 5 mode, and here it cannot be built at all. Strict Concurrency Checking no longer matters — Xcode's description of it says it "is always 'complete' when in the Swift 6 language mode".
That is Apple's move, and where most projects stop. Trips went two settings further on lesson 17: Default Actor Isolation set to nonisolated, and Approachable Concurrency off. Neither is needed for Swift 6. What they change is who decides where code runs: with them, no setting puts a type on the main actor or keeps an async function with its caller, so every type and every async function in Trips says it in its own words. That is why the Ticket you copied had no nonisolated in it — in Trips it was never the main actor's — and why Trips' store says @MainActor where Boarding's views are the main actor's by default.
The flash cards and edge cases below are compiled as Boarding started: a new project's settings, in the language mode each one names.
Ticket shape.Ticket made nonisolated, because Shape requires a conformance any thread may use — and why Trips, with no default actor, never needed it.Next, back to Trips: AsyncSequence and AsyncStream — values that arrive over time, and turning a callback-based API into one you can for await over.
Stuck, or want to compare with your own? This is the project as it stands at the end of this lesson, built and run before it was put here.
Boarding.xcodeproj in Xcode.17 cards for this lesson, each labelled with its topic. 9 ask how you would write something; the other 8 are trick questions, written to catch a misunderstanding. Answer in your head first, then tap the card.
: Sendable. A struct whose stored properties are all Sendable can conform, and in Swift 6 mode the compiler checks it. It printed Seattle 1.
Sendable honestly.final, and only let properties of Sendable types: nothing in it can change, so any thread may read it. A var makes Swift 6 refuse the conformance (a trick card shows it). It printed Bergen.
A Mutex from the Synchronization framework (iOS 18 and later), held in a let: the class is Sendable with no @unchecked, because the mutex is. A thousand tasks, 1000. An actor does the same with await instead of a lock.
Build setting Strict Concurrency Checking → Complete; Xcode then passes -enable-upcoming-feature StrictConcurrency. This race, silent before, now says: "passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure; this is an error in the Swift 6 language mode" Fix the warnings, then set Swift Language Version to Swift 6.
Sendable to an actor, once and for good.A sending parameter: the caller gives the value up, so it may cross even though it is not Sendable. Use it again after the call and Swift 6 refuses (an edge case). It printed handed over.
nonisolated(unsafe). The compiler stops checking that variable — the safety is now yours. A last resort, for state that a lock or a single thread really does protect.
Sendable from an actor to its caller.A sending result: the actor promises the value is not kept anywhere it can reach, so the caller may have it. Without sending, Swift 6 refuses the same method. It printed Bergen.
nonisolated let. Without it the constant belongs to the main actor, like everything else in a new Xcode project, and a nonisolated function cannot read it in Swift 6 mode. It printed true.
[String: any Sendable]: each value may be of any type, as long as it is Sendable. [String: Any] is refused. It printed kept.
Sendable. It is passed where Sendable is required, in Swift 6 mode. Does it compile?Yes: "kept". Inside one module, Swift infers Sendable for a struct or enum whose stored properties are all Sendable. Mark it yourself when it is public, or to have the compiler hold you to it.
NSMutableArray. Is it still Sendable?No: "cannot find type 'NSMutableArray' in scope" One non-Sendable property spoils the whole value: copying the struct copies a reference to the same mutable array.
final class declared Sendable has one var. Does Swift 6 accept it?No: "stored property 'city' of 'Sendable'-conforming class 'Place' is mutable" A class is shared by reference, so a Sendable class may hold only constants of Sendable types — or protect its state itself, with a lock, as @unchecked Sendable (or a Mutex).
[String: Any] dictionary — the shape of decoded JSON in older code — is passed where Sendable is required, in Swift 6 mode. Does it compile?No: "type 'Any' does not conform to the 'Sendable' protocol" Any can hold anything, including a mutable class, so the compiler cannot vouch for it. Say what the values must be: [String: any Sendable] (a card shows it).
@MainActor class with a var is captured by a detached task, in Swift 6 mode. Does it compile?Yes: "Trips". A class isolated to a global actor is Sendable: its state can be reached only on that actor, so sharing the reference is safe. The detached task needs await to read it — that hop is the protection. It is why SwiftUI models on the main actor can be handed to tasks freely.
let limit = 10, is read by a nonisolated function. Swift 6 mode: does it compile?No: "main actor-isolated let 'limit' can not be referenced from a nonisolated context" A new Xcode project puts every declaration on the main actor unless it says otherwise — constants included. Write nonisolated let limit = 10 (a card shows it); an Int that never changes is safe anywhere.
Draft — a class that is not Sendable — and a method returns it. Can the caller have it, in Swift 6 mode?No: "non-Sendable 'Draft'-typed result can not be returned from actor-isolated instance method 'current()' to main actor-isolated context" An actor may keep values that are not Sendable — it protects them — but it cannot hand out one it still holds: the caller and the actor would share it. A value made fresh for the caller can go, with a sending result (a card shows it).
var. In Swift 6 mode, is that an error or a warning?Only a warning, even in Swift 6 mode: "stored property 'attempts' of 'Sendable'-conforming class 'Failure' is mutable". Every error is Sendable — SE-0302 made Error inherit from it, because a thrown error can carry state out of an actor — and planned that such errors "will be downgraded to warnings in Swift < 6". Xcode 27 still warns in Swift 6 mode too. Do not wait for it to become an error: make the property a let, or the error a struct.
10 questions of the kind an interviewer uses to find out whether you really know this — the corners, the traps, and the things that changed recently. Each has its source. Wherever the answer is what some code does, that code was compiled and run, and what you see under “Show the answer” is what it really did. Say your answer aloud before you open it.
A thousand child tasks each add 1 to a plain class's var. What does the compiler say in a new project's Swift 5 mode, with Strict Concurrency Checking set to Complete, and in Swift 6 mode?
Nothing; a warning; an error. In Swift 5 mode with the minimal checking new projects get, it compiles without a word. With Complete:
Xcode warns
passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure; this is an error in the Swift 6 language modeIn Swift 6 mode the same message is an error:
Xcode reports
passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure"Sending" is the key word: each child task's closure is sent to run concurrently, and it carries counter, a reference to the same mutable object, into a thousand of them. An actor, a Mutex, or giving each task its own value fixes it.
Where this comes from: Adopting strict concurrency in Swift 6 apps · SE-0414: Region based isolation · The Swift book: Concurrency — Compiled with Xcode 27 three ways: as a new project's settings set it, with Complete checking, and in Swift 6 mode.
In Swift 6 mode, a Draft — a class that is not Sendable — is captured by a detached task. In one version nothing touches it afterwards; in the other, the next line changes it. Which compile?
Only the first. It runs — "Seattle!" — because nothing else can reach draft once the task has it. The second does not compile:
Xcode reports
sending value of non-Sendable type '@concurrent () async -> String' risks causing data racesSwift 6 does not demand that everything crossing be Sendable; it tracks regions of values that can reach each other (SE-0414), and lets a region move to another task as long as the code it leaves never touches it again. That is why most code that merely passes data along compiles unchanged.
Where this comes from: SE-0414: Region based isolation · Adopting strict concurrency in Swift 6 apps — Compiled and run with Xcode 27, in Swift 6 mode.
A Draft is passed to an actor's method, and the caller changes it on the next line. Swift 6 mode: does it compile?
It does not compile. Xcode reports
sending 'draft' risks causing data races
No: "sending 'draft' risks causing data races"
Inside the actor, draft could be read while the caller writes it — two threads, one object. Make Draft a Sendable struct (copying it ends the sharing), pass only the values the actor needs, or, if the caller is truly done with it, mark the parameter sending (a flash card shows it).
Where this comes from: SE-0414: Region based isolation · The Swift book: Concurrency — Compiled with Xcode 27, in Swift 6 mode.
Trip is a struct, but it holds a Photo — a class with a var. A trip is handed to an actor and still used afterwards. Swift 6 mode?
It does not compile. Xcode reports
sending 'trip' risks causing data races
It does not compile: "sending 'trip' risks causing data races"
Being a struct is not enough. Copying trip copies the reference to the same Photo, so the archive and the caller would share one mutable object. A value type is Sendable only if everything it holds is — which is why lesson 32 kept Trip's properties to strings, numbers and dates.
Where this comes from: Sendable · SE-0414: Region based isolation — Compiled with Xcode 27, in Swift 6 mode.
Ten child tasks each run count += 1 on a local var count. Swift 6 mode?
It does not compile. Xcode reports
passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure
No: "passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure"
A local var captured by tasks that run at the same time is shared state like any other. Return a value from each child and add them up as they arrive — for await one in group { count += one } — and the counting happens in one place.
Where this comes from: SE-0414: Region based isolation · TaskGroup — Compiled with Xcode 27, in Swift 6 mode.
In a new Xcode project, var visits = 0 at the top level of a file, and a nonisolated function that adds to it. What does Swift 5 mode say — and Swift 6?
It compiles, with a warning
main actor-isolated var 'visits' can not be mutated from a nonisolated context
A warning even in Swift 5 mode: "main actor-isolated var 'visits' can not be mutated from a nonisolated context" — and in Swift 6, the same words as an error.
Global variables are the classic source of data races, and a new Xcode project makes each one belong to the main actor, like everything else in it. Code that runs elsewhere must reach it through the main actor. The usual fixes: make the function @MainActor too, or put the state in an actor; if it never changes, make it a nonisolated let — a plain let is still the main actor's, as a flash card shows.
Where this comes from: SE-0466: Default actor isolation · Adopting strict concurrency in Swift 6 apps — Compiled with Xcode 27, in Swift 5 mode and in Swift 6 mode.
A class that many tasks share is declared @unchecked Sendable — but has no lock. Swift 6 mode: does it compile?
It compiles: "it compiled, and it ran" — and it is the same data race the compiler refused a moment ago without @unchecked.
@unchecked Sendable and nonisolated(unsafe) switch the checking off for one type or one variable; the compiler takes your word. They exist for code that really does protect itself — a lock, a Mutex, a queue — and that the compiler cannot see into. In a code review, each one is a question: what protects this?
Where this comes from: Sendable · Adopting strict concurrency in Swift 6 apps — Compiled and run with Xcode 27, in Swift 6 mode.
An actor's method takes _ draft: sending Draft. The caller hands a draft over — and then changes it. Swift 6 mode?
It does not compile. Xcode reports
sending 'draft' risks causing data races
No: "sending 'draft' risks causing data races"
sending is a promise the caller makes: this value goes, and I will not touch it again. The compiler holds the caller to it. Without the last line, the same code compiles — a flash card runs it.
Where this comes from: SE-0430: sending parameter and result values · SE-0414: Region based isolation — Compiled with Xcode 27, in Swift 6 mode.
Weather is a Sendable struct. The cache actor hands one to two screens at once. Can the screens interfere with each other?
No — each gets its own copy: "mine: 70.0 theirs: 56.0 the cache's: 56.0"
This is what makes value types the easy road to Sendable: crossing makes a copy, and a copy has no one to race with. A class crossing the same way would be one object seen from three places.
Where this comes from: Sendable · The Swift book: Concurrency — Run with Xcode 27, in Swift 6 mode.
This lesson's warning came from Ticket, a Shape in a new Xcode project. Which smaller fix also works — and why does the whole struct get nonisolated on the page?
Marking just path(in:) nonisolated also compiles in Swift 6 mode, and the shape works: "100.0".
Shape declares path(in:) as nonisolated, so it is the method that must be callable from anywhere. The page marks the whole struct because nothing in a ticket belongs to the main actor — it is geometry — and saying so once covers whatever is added later. Both are honest; the narrow fix is the one to reach for when a type also has state that does belong on the main actor.
Where this comes from: Shape · SE-0470: Global-actor isolated conformances — Compiled and run with Xcode 27, in Swift 6 mode.