Actor is a type introduced in Swift 5.5+ that solves the problem of data races at the language level. Unlike manual locks and DispatchQueue queues, an actor automatically isolates its state and synchronizes access to it. This means that two threads cannot simultaneously modify the same property of an actor type, eliminating race conditions without additional effort from the developer. The mechanism is based on the concept of actor isolation, where the compiler tracks access to actor properties and methods. According to Apple, 2025, the actor model is a fundamental part of Swift's concurrency system.
Key Takeaways
Actor is a reference type, similar to a class, but with automatic protection against data races. It was introduced in Swift 5.5 as part of the concurrency system alongside async/await and Task. An actor guarantees that its mutable state will never be simultaneously read or written from different threads without explicit synchronization.
To declare an actor, use the actor keyword followed by curly braces with its members. Syntactically, an actor resembles a class, but its behavior differs radically.
actor BankAccount {
private var balance: Double
init(initialBalance: Double) {
self.balance = initialBalance
}
func deposit(amount: Double) {
balance += amount
}
func getBalance() -> Double {
return balance
}
}
The compiler automatically isolates all actor properties and methods so they are only accessible within the actor context. When trying to access balance from outside the actor, the compiler will produce an error if the call is not marked as async.
Data isolation is the key concept of an actor. An actor guarantees mutually exclusive access to its state through the actor executor mechanism. Each actor has its own executor that processes all accesses to its isolated members sequentially.
When code outside an actor calls its method, the call is placed in the actor executor queue. The actor executes only one task at a time, guaranteeing the absence of races. If two threads call deposit simultaneously, the second call waits for the first to complete.
let account = BankAccount(initialBalance: 1000.0)
// Async call — required from outside actor
await account.deposit(amount: 500.0)
let currentBalance = await account.getBalance()
Every call to an actor method requires await because the actor may be busy with another task. This is not a bug but a conscious design that prevents races. Swift implicitly makes actor property getters and setters async, so reading a property also requires await.
Actor and class are both reference types, but their behavior in a multithreaded environment differs dramatically. A class does not provide any automatic protection against races, whereas an actor embeds it at the compiler level through the type system.
| Characteristic | Actor | Class |
|---|---|---|
| Race protection | Automatic, at the compiler level | Requires manual synchronization |
| Property access | Only via await from outside | Direct, without synchronization |
| Inheritance | Only from other actors | Standard class-based |
| Protocol conformance | Can conform to protocols | Standard |
| Performance | Low overhead with isolation | Faster without synchronization |
An actor can only inherit from another actor and cannot inherit from a class. This is intentional because a class lacks the actor isolation mechanism, and mixing types would break safety guarantees.
actor SavingsAccount: BankAccount {
func applyInterest(rate: Double) {
let interest = balance * rate
balance += interest
}
}
Asynchronous calls are the mechanism for interacting with an actor from external code. Since an actor isolates its state, any access to its members from outside requires await. This allows Swift to guarantee that the calling code does not block the thread and the actor can process other requests.
In addition to declared actor types, Swift supports global actors — the @MainActor attribute, which marks classes, properties, or methods as executable on the main thread. This is especially useful for UI code in iOS applications.
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
func updateTitle() {
// This code is guaranteed to run on the main thread
title = "New Title"
}
}
Using @MainActor eliminates the need to manually call DispatchQueue.main.async, making the code cleaner and safer. The compiler ensures that switching to the main thread happens correctly.
Nonisolated is a keyword that allows marking a method or computed property of an actor as not isolated. Such members do not have access to the actor-isolated state but can be called without await from outside the actor.
Nonisolated methods are useful for computations that do not depend on the actor's mutable state. For example, the formatBalance method does not access balance directly but only formats the passed value — such a method is safe to make nonisolated.
actor BankAccount {
private var balance: Double = 0
nonisolated func formatBalance(amount: Double) -> String {
return "$\(amount)"
}
}
Nonisolated members execute synchronously and do not require await. However, they cannot read actor-isolated properties directly. If a nonisolated method needs a value from the actor, it must be passed as a parameter.
Reentrancy is a mechanism that allows re-entry into an actor while waiting for an asynchronous call. Without reentrancy, an actor could deadlock forever if one of its methods waited for another which in turn waited for the first.
When code inside an actor executes await, the actor suspends the current task and can process another queued task. After await completes, the task resumes. This prevents deadlocks but requires caution: the actor state between await points may change.
actor DataProcessor {
var cache: [Int: String] = [:]
func process(id: Int) async -> String {
if let cached = cache[id] {
return cached
}
// await — reentrancy point
let result = await fetchData(id: id)
// Cache may have changed after await — recheck
cache[id] = result
return result
}
}
Developers must account for reentrancy and check the actor state after await points. A typical mistake is assuming that actor isolation persists across asynchronous suspension points. In practice, between await and the next statement, the state may differ from what was expected.
Frequently Asked Questions
Actor automatically isolates its state from data races, requiring await for external access. A class does not provide such protection — the developer is responsible for synchronization via locks or queues. An actor only inherits from an actor, a class from a class.
Yes, an actor can inherit from another actor. The subclass gets all isolated properties and methods of the parent. An actor cannot inherit from a class because classes lack the actor isolation mechanism at the compiler level.
An actor-isolated context is a code area where direct access to the actor's mutable state is allowed. Inside actor methods marked as isolated (by default), you can read and write properties without await. The compiler checks isolation boundaries.
Data from an actor is passed through async methods that return Sendable types, or through nonisolated methods that accept values as parameters. You can also create an async property that returns a snapshot of the actor's state as a Sendable structure.
Yes, an actor can conform to protocols. If a protocol contains isolated requirements (actor-isolated), they automatically become actor-isolated. For async methods in protocols, you can specify that they must be called on a specific actor using the isolated marker.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also