Explain how MainActor and Sendable interact
Asked on 2024-07-30
1 search
MainActor and Sendable are two important concepts in Swift that help manage concurrency and ensure data-race safety.
MainActor
The MainActor is a special actor in Swift that ensures certain pieces of code run on the main thread. This is particularly useful for UI updates, which must be performed on the main thread to avoid inconsistencies and crashes. When a method or property is isolated to the MainActor, it means that any access to it must be done on the main thread. This isolation helps prevent data races by serializing access to the shared state.
For example, if you have a method that needs to run on the main thread, you can annotate it with @MainActor:
@MainActor
func updateUI() {
// UI update code
}
Sendable
Sendable is a protocol in Swift that ensures a type can be safely transferred across concurrency domains. A type is considered Sendable if it protects its state from concurrent accesses, either by being immutable or by using synchronization mechanisms like locks.
In Swift 6, the compiler enforces that values shared between concurrency domains must be Sendable. This helps prevent data races by ensuring that only safe types are passed between different threads or actors.
For example, if you have a struct that you want to make Sendable, you can conform it to the Sendable protocol:
struct MyData: Sendable {
let value: Int
}
Interaction between MainActor and Sendable
When you have a type that is isolated to the MainActor and you need to send it to another actor, it must conform to Sendable to ensure data-race safety. If the type is a value type with immutable properties, it can be easily made Sendable. However, if it is a reference type, you need to ensure that it does not introduce shared mutable state that could lead to data races.
For example, consider a scenario where you have a Drink type that is a struct with immutable properties. This type can be made Sendable and safely passed between the MainActor and another actor:
struct Drink: Sendable {
let name: String
let size: Int
}
If Drink were a reference type, you would need to ensure it is thread-safe before making it Sendable.
Example from WWDC
In the session Migrate your app to Swift 6, it is explained how to handle a scenario where a Drink type is stored in one actor and then sent to another actor. By making Drink conform to Sendable, you ensure that it can be safely transferred between actors without introducing data races.
For more details, you can refer to the sessions:
Relevant Sessions

What’s new in Swift
Join us for an update on Swift. We’ll briefly go through a history of Swift over the past decade, and show you how the community has grown through workgroups, expanded the package ecosystem, and increased platform support. We’ll introduce you to a new language mode that achieves data-race safety by default, and a language subset that lets you run Swift on highly constrained systems. We’ll also explore some language updates including noncopyable types, typed throws, and improved C++ interoperability.

A Swift Tour: Explore Swift’s features and design
Learn the essential features and design philosophy of the Swift programming language. We’ll explore how to model data, handle errors, use protocols, write concurrent code, and more while building up a Swift package that has a library, an HTTP server, and a command line client. Whether you’re just beginning your Swift journey or have been with us from the start, this talk will help you get the most out of the language.

Migrate your app to Swift 6
Experience Swift 6 migration in action as we update an existing sample app. Learn how to migrate incrementally, module by module, and how the compiler helps you identify code that’s at risk of data races. Discover different techniques for ensuring clear isolation boundaries and eliminating concurrent access to shared mutable state.
