Sendable Protocol Fundamentals

Sendable is a marker protocol that says "this type is safe to share across concurrency boundaries." Swift 6 enforces this strictly.
Follow along with the code: iOS-Practice on GitHub
Why Sendable Exists
When you pass data to a Task or actor, it crosses a concurrency boundary. If that data is mutable and shared, you get data races.
actor DataStore {
func save(_ settings: UserSettings) {
// If UserSettings is a class with mutable properties,
// another thread could modify it while we're saving!
}
}
Sendable is the compiler's way of ensuring this can't happen.
What's Automatically Sendable
Value types with Sendable properties:
struct Point: Sendable {
let x: Double
let y: Double
}
struct User: Sendable {
let id: UUID
let name: String
let email: String
}
The compiler auto-synthesizes Sendable for structs/enums with all Sendable properties.
Actors:
actor Counter: Sendable { // Always Sendable
var count = 0
}
Actors are inherently safe—that's their purpose.
Immutable classes:
final class Config: Sendable {
let apiKey: String
let environment: String
init(apiKey: String, environment: String) {
self.apiKey = apiKey
self.environment = environment
}
}
final + only let properties = Sendable.
What's NOT Sendable
Mutable classes:
class UserSettings { // NOT Sendable
var theme: String = "light"
var fontSize: Int = 14
}
Multiple threads could modify theme simultaneously.
Classes with mutable reference properties:
class Container: Sendable { // ERROR
let items: NSMutableArray // NSMutableArray isn't Sendable
}
@Sendable Closures
Closures that cross concurrency boundaries must be @Sendable:
Task { @Sendable in
// This closure is Sendable
await actor.doWork()
}
func process(_ action: @Sendable () async -> Void) async {
await action()
}
Sendable closures can't capture mutable variables:
var count = 0
Task {
count += 1 // ERROR: Mutation of captured var in Sendable closure
}
Making Types Sendable
Option 1: Use struct
// Instead of class, use struct
struct UserData: Sendable {
let id: UUID
var name: String // Structs copy, so mutation is safe
}
Option 2: Make class immutable
final class ImmutableUser: Sendable {
let id: UUID
let name: String
init(id: UUID, name: String) {
self.id = id
self.name = name
}
}
Option 3: @unchecked Sendable (you guarantee safety)
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
}
Common Sendable Types
| Type | Sendable? |
|---|---|
| Int, Double, Bool | Yes |
| String | Yes |
| Array, Dictionary (of Sendable) | Yes |
| Struct (all props Sendable) | Yes |
| Enum (all payloads Sendable) | Yes |
| Actor | Yes |
| Class (mutable) | No |
| NSObject subclasses | Usually no |
Interview Tip
Sendable is foundational to Swift concurrency safety. When discussing actors or async/await, mention: "Data passed across actor boundaries must be Sendable, either value types or carefully designed reference types."