Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Diagnose and fix Swift Concurrency issues: async/await, actor isolation, Sendable, and Swift 6 migration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/sendable.md
1# Sendable23Use this when:45- A value or reference type must cross an isolation boundary safely.6- You are resolving "non-Sendable type" compiler diagnostics.7- You need to decide between value types, `@unchecked Sendable`, actors, or region-based isolation.89Skip this file if:1011- The issue is about which actor should own the state. Use `actors.md`.12- The issue is about how async functions execute. Use `threading.md`.1314Jump to:1516- Isolation Domains17- Value Types (Structs, Enums)18- Reference Types (Classes)19- Functions and Closures (@Sendable)20- @unchecked Sendable21- Region-Based Isolation / `sending`22- Global Variables23- Decision Tree2425## What is Sendable?2627`Sendable` indicates a type is safe to share across isolation domains (actors, tasks, threads). The compiler verifies thread-safety at compile time.2829```swift30public protocol Sendable {}31```3233Empty protocol, but triggers compiler verification of thread-safety.3435> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.1: Explaining the concept of Sendable in Swift](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)3637## Isolation Domains3839Three types of isolation in Swift Concurrency:4041### 1. Nonisolated (default)4243No concurrency restrictions, but can't modify isolated state:4445```swift46func computeValue(a: Int, b: Int) -> Int {47return a + b48}49```5051### 2. Actor-isolated5253Dedicated isolation domain with serialized access:5455```swift56actor Library {57var books: [String] = []5859func addBook(_ title: String) {60books.append(title)61}62}6364// External access requires await65await library.addBook("Swift Concurrency")66```6768### 3. Global actor-isolated6970Shared isolation domain across types:7172```swift73@MainActor74func updateUI() {75// Runs on main thread76}77```7879## Data Races vs Race Conditions8081### Data Race8283Multiple threads access shared mutable state, at least one writes, without synchronization:8485```swift86// ⚠️ Data race87var counter = 088DispatchQueue.global().async { counter += 1 }89DispatchQueue.global().async { counter += 1 }90```9192**Detection**: Enable Thread Sanitizer in scheme settings.9394**Prevention**: Use actors or Sendable types:9596```swift97actor Counter {98private var value = 099100func increment() {101value += 1102}103}104```105106### Race Condition107108Timing-dependent behavior leading to unpredictable results:109110```swift111let counter = Counter()112113for _ in 1...10 {114Task { await counter.increment() }115}116117// May print inconsistent values118print(await counter.getValue())119```120121**Key difference**: Swift Concurrency prevents data races but not race conditions. You must still ensure proper sequencing.122123> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.2: Understanding Data Races vs. Race Conditions: Key Differences Explained](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)124125## Value Types (Structs, Enums)126127### Implicit conformance128129Non-public structs/enums with Sendable members:130131```swift132// Implicitly Sendable133struct Person {134var name: String135}136```137138### Explicit conformance required139140Public types need explicit declaration:141142```swift143public struct Person: Sendable {144var name: String145}146```147148**Why**: Compiler can't verify internal details of public types across modules.149150### Frozen types151152Public frozen types can be implicitly Sendable:153154```swift155@frozen156public struct Point: Sendable {157public var x: Double158public var y: Double159}160```161162### All members must be Sendable163164```swift165public struct Person: Sendable {166var name: String167var hometown: Location // Must also be Sendable168}169170public struct Location: Sendable {171var name: String172}173```174175> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.3: Conforming your code to the Sendable protocol](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)176177### Copy-on-write makes mutability safe178179```swift180public struct Person: Sendable {181var name: String // Mutable but safe due to COW182}183```184185Each mutation creates a copy, preventing concurrent access to same instance.186187> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.4: Sendable and Value Types](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)188189## Reference Types (Classes)190191### Requirements for Sendable classes192193Must be:1941. `final` (no inheritance)1952. Immutable stored properties only1963. All properties Sendable1974. No superclass or `NSObject` only198199```swift200final class User: Sendable {201let name: String202let id: Int203204init(name: String, id: Int) {205self.name = name206self.id = id207}208}209```210211### Why non-final classes can't be Sendable212213Child classes could introduce unsafe mutability:214215```swift216// Can't be Sendable217class Purchaser {218func purchase() { }219}220221// Could introduce data races222class GamePurchaser: Purchaser {223var credits: Int = 0 // Mutable!224}225```226227### Actor isolation makes classes Sendable228229```swift230@MainActor231class ViewModel {232var data: [Item] = [] // Safe due to actor isolation233}234// Implicitly Sendable235```236237### Composition over inheritance238239```swift240final class Purchaser: Sendable {241func purchase() { }242}243244final class GamePurchaser {245let purchaser: Purchaser = Purchaser()246// Handle credits separately247}248```249250> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.5: Sendable and Reference Types](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)251252## Functions and Closures (@Sendable)253254Mark functions/closures that cross isolation domains:255256```swift257actor ContactsStore {258func removeAll(_ shouldRemove: @Sendable (Contact) -> Bool) async {259contacts.removeAll { shouldRemove($0) }260}261}262```263264### Captured values must be Sendable265266```swift267let query = "search"268269// ✅ Immutable capture270store.filter { contact in271contact.name.contains(query)272}273274var query = "search"275276// ❌ Mutable capture277store.filter { contact in278contact.name.contains(query) // Error279}280```281282### Capture lists for mutable values283284```swift285var query = "search"286287// ✅ Capture immutable snapshot288store.filter { [query] contact in289contact.name.contains(query)290}291```292293> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.6: Using @Sendable with closures](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)294295## @unchecked Sendable296297**Use as last resort.** Tells compiler to skip verification—you guarantee thread-safety.298299### When to use300301Manual locking mechanisms the compiler can't verify:302303```swift304final class Cache: @unchecked Sendable {305private let lock = NSLock()306private var items: [String: Data] = [:]307308func get(_ key: String) -> Data? {309lock.lock()310defer { lock.unlock() }311return items[key]312}313314func set(_ key: String, value: Data) {315lock.lock()316defer { lock.unlock() }317items[key] = value318}319}320```321322### Risks323324- No compile-time safety325- Easy to introduce data races326- Must manually ensure all access uses lock327328```swift329final class Cache: @unchecked Sendable {330private let lock = NSLock()331private var items: [String: Data] = [:]332333// ⚠️ Forgot lock - data race!334var count: Int {335items.count336}337}338```339340**Better**: Use actor instead:341342```swift343actor Cache {344private var items: [String: Data] = [:]345346var count: Int { items.count }347348func get(_ key: String) -> Data? {349items[key]350}351352func set(_ key: String, value: Data) {353items[key] = value354}355}356```357358> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.7: Using @unchecked Sendable](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)359360## Region-Based Isolation361362Compiler allows non-Sendable types in same scope:363364```swift365class Article {366var title: String367init(title: String) { self.title = title }368}369370func check() {371let article = Article(title: "Swift")372373Task {374print(article.title) // ✅ OK - same region375}376}377```378379**Why**: No mutation after transfer, so no data race risk.380381### Breaks when accessed after transfer382383```swift384func check() {385let article = Article(title: "Swift")386387Task {388print(article.title)389}390391print(article.title) // ❌ Error - accessed after transfer392}393```394395## The sending Keyword396397Enforces ownership transfer for non-Sendable types:398399### Parameter values400401```swift402actor Logger {403func log(article: Article) {404print(article.title)405}406}407408func printTitle(article: sending Article) async {409let logger = Logger()410await logger.log(article: article)411}412413// Usage414let article = Article(title: "Swift")415await printTitle(article: article)416// article no longer accessible here417```418419### Return values420421```swift422@SomeActor423func createArticle(title: String) -> sending Article {424return Article(title: title)425}426```427428Transfers ownership to caller's region.429430> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.8: Understanding region-based isolation and the sending keyword](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)431432## Global Variables433434Must be concurrency-safe since accessible from any context.435436### Problem437438```swift439class ImageCache {440static var shared = ImageCache() // ⚠️ Not concurrency-safe441}442```443444### Solution 1: Actor isolation445446```swift447@MainActor448class ImageCache {449static var shared = ImageCache()450}451```452453### Solution 2: Immutable + Sendable454455```swift456final class ImageCache: Sendable {457static let shared = ImageCache()458}459```460461### Solution 3: nonisolated(unsafe)462463**Last resort** - you guarantee safety:464465```swift466struct APIProvider: Sendable {467nonisolated(unsafe) static private(set) var shared: APIProvider!468469static func configure(apiURL: URL) {470shared = APIProvider(apiURL: apiURL)471}472}473```474475Use `private(set)` to limit mutation points.476477> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.9: Concurrency-safe global variables](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)478479## Custom Locks + Sendable480481### Legacy code with locks482483```swift484final class BankAccount: @unchecked Sendable {485private var balance: Int = 0486private let lock = NSLock()487488func deposit(amount: Int) {489lock.lock()490balance += amount491lock.unlock()492}493494func getBalance() -> Int {495lock.lock()496defer { lock.unlock() }497return balance498}499}500```501502### Migration strategy503504**New code**: Use actors505506**Existing code**:5071. If isolated and small scope → migrate to actor5082. If widely used → use `@unchecked Sendable`, file migration ticket509510```swift511// Better: Migrate to actor512actor BankAccount {513private var balance: Int = 0514515func deposit(amount: Int) {516balance += amount517}518519func getBalance() -> Int {520balance521}522}523```524525> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.10: Combining Sendable with custom Locks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)526527## Decision Tree528529```530Need to share type across isolation domains?531├─ Value type (struct/enum)?532│ ├─ Public? → Add explicit Sendable533│ └─ Internal? → Implicit Sendable (if members Sendable)534│535├─ Reference type (class)?536│ ├─ Can be final + immutable? → Sendable537│ ├─ Needs mutation?538│ │ ├─ Can use actor? → Use actor (automatic Sendable)539│ │ ├─ Main thread only? → @MainActor540│ │ └─ Has custom lock? → @unchecked Sendable (temporary)541│ └─ Can be struct instead? → Refactor to struct542│543└─ Function/closure? → @Sendable attribute544```545546## Common Patterns547548### Restructure to avoid non-Sendable dependencies549550```swift551// Instead of storing non-Sendable type552public struct Person: Sendable {553var hometown: String // Just the name554555init(hometown: Location) {556self.hometown = hometown.name557}558}559```560561### Prefer actors for mutable state562563```swift564// Instead of @unchecked Sendable with locks565actor Cache {566private var items: [String: Data] = [:]567568func get(_ key: String) -> Data? {569items[key]570}571}572```573574### Use @MainActor for UI-bound types575576```swift577@MainActor578class ViewModel: ObservableObject {579@Published var items: [Item] = []580}581```582583## Best Practices5845851. **Prefer value types** - structs/enums are easier to make Sendable5862. **Use actors for mutable state** - automatic thread-safety5873. **Avoid @unchecked Sendable** - use only for proven thread-safe code5884. **Mark public types explicitly** - don't rely on implicit conformance5895. **Ensure all members Sendable** - one non-Sendable breaks the chain5906. **Use @MainActor for UI types** - simple isolation for view models5917. **Capture immutably** - use capture lists for mutable variables5928. **Test with Thread Sanitizer** - catches runtime data races5939. **File migration tickets** - track @unchecked Sendable usage594595## Further Learning596597For migration strategies, real-world examples, and actor patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).598599