Leave a rating/review
Notes: 14. Actor
This episode displays the Xcode 14 documentation for Sendable — it’s more extensive than Xcode 13’s.
For the rest of this course, you’ll work with EmojiArt, an app that lets you browse an online catalog of digital emoji art. It reads the feed of current works of art from the server, verifies the digital signature of the images and displays them onscreen.
This app runs a lot of tasks in parallel, and there’s a lot of potential for data races when multiple concurrent tasks are adding downloaded images to a collection or updating progress values.
Open the starter project, then build and run.
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
For now, ignore this warning about updating the UI from background threads. You’ll fix it shortly.
Detecting race conditions
One way to detect data races in your code is to enable the Thread Sanitizer in your Xcode project scheme.
Click the scheme selector in Xcode’s toolbar and select Edit scheme…:
In the scheme editing window, click Run, then select the Diagnostics tab. Check the Thread Sanitizer checkbox then close this window.
When you rebuild the project, Xcode adds some extra checks into your app. At runtime, these check whether your code concurrently mutates any data. Build and run.
The app UI looks the same as before. Xcode, however, has a new purple runtime warning: It found a Swift access race. Your warning message might mention a data race.
If Thread Sanitizer detects a data race, your code will eventually crash in production.
Using actors to protect shared mutable state
To protect verifiedCount from concurrent access, you’ll convert EmojiArtModel from a class to an actor. Actors have a lot of typical class behavior, like by-reference semantics, so the change won’t be too complex.
In the Model group, open EmojiArtModel.swift and replace the class keyword with actor:
actor EmojiArtModel: ObservableObject // don't press return: this auto-selects Actor
This changes the type of the model to an actor. Your shared state is now safe! Actors don’t magically solve concurrent access. The compiler now follows the rules for actors and finds issues in code that used to compile.
The compiler suggests how you should change your code to make it work safely in a concurrent context. But more importantly, when you use an actor, the compiler protects you against creating unsafe thread accesses in your code.
Now, follow Xcode’s suggestions to make the existing code thread-safe. This error says:
"Actor-isolated property 'verifiedCount' can not be mutated from a Sendable closure".
You’ll learn about Sendable soon. For now, just know you get the error because you can’t update the actor state from “outside” its direct scope. All code that isn’t confined to the serial executor of the actor is “outside” access. That means it includes calls from other types and concurrent tasks — like your TaskGroup, in this case.
Let’s take a closer look at the Actor protocol.
actor Counter {
private var count = 0
func increment() {
count += 1
}
}
The actor type is one of the concurrency-related improvements introduced in Swift 5.5. actor is a programming type just like its peers: enum, struct, class and so on. And it’s a reference type, like class.
An actor in Swift can safely access and mutate its own state. A special type called a serial executor, which the runtime manages, synchronizes all calls to the actor’s members. The serial executor, like a serial dispatch queue in GCD, executes tasks one after another. By doing this, it protects the actor’s state from concurrent access.
Access to the actor from other types is automatically performed asynchronously and scheduled on the actor’s serial executor. This is called the state isolation layer, which ensures that all state mutation is thread-safe.
Using actors to protect shared mutable state (continued)
Now, back to Xcode: To overcome the verifiedCount issue, you’ll extract the code to increment verifiedCount into a method, then call it asynchronously. This allows the actor to serialize the calls to that method.
Above verifyImages(), add a new method:
private func increaseVerifiedCount() {
verifiedCount += 1
}
You can call this method synchronously from “inside” the actor’s direct scope, but the compiler will enforce asynchronous access from “outside” of it.
Now, replace self.verifiedCount += 1 in verifyImages():
await self.increaseVerifiedCount()
This new code makes calls to increaseVerifiedCount() serially, making sure you mutate your shared state safely.
But there are still a lot of compiler errors. Now that imageFeed is part of your EmojiArtModel actor, you can’t access that property on the main actor. SwiftUI runs on the main actor and can’t read the feed anymore. You’ll fix that next.
Sharing data across actors
You mostly use imageFeed to drive the app’s UI, so it makes sense to place this property on the main actor. But how can you share it between the main actor and EmojiArtModel?
Well, you can use the @MainActor attribute to “move” imageFeed to execute on the main actor, while the property itself remains inside EmojiArtModel.
In EmojiArtModel, locate imageFeed and assign it to the main actor:
@Published @MainActor private(set) var imageFeed: [ImageFile] = []
By moving imageFeed from the EmojiArtModel serial executor to the main actor, imageFeed is now accessible from the main thread.
Fixing the other errors
Next is the error on the line that calls imageFeed.forEach { ... }. To access the actor, you need to call imageFeed.forEach { ... } asynchronously.
Easy — just await it:
await imageFeed.forEach { file in
To fix the errors in loadImages(), wrap imageFeed.removeAll() in your old friend await MainActor.run:
await MainActor.run {
imageFeed.removeAll()
}
And do the same for imageFeed = list:
await MainActor.run {
imageFeed = list
}
You run the two calls asynchronously on the main actor, where it’s safe to update the UI.
Just one more error to fix… Quick open [Shift-Command-O] LoadingView: The final error is on the line, right at the end, that calculates the value of progress:
Actor-isolated property 'verifiedCount' can not be referenced from the main actor
Wrap it in a Task and await it:
Task {
progress = await Double(model.verifiedCount) /
Double(model.imageFeed.count)
}
Congratulations, you’ve followed Xcode’s guidance to fix all the unsafe code. Build and run again.
This time, no purple warnings!
You’ve worked through designing your first actor type and experienced some actor-specific compiler behavior. But there’s one topic you skipped over: What is the Sendable type that those compiler errors mentioned? Coming up next.
Sendable is a protocol that indicates that a given value is safe to use in concurrent code.
Open Window/Developer Documentation and look up Sendable. I’m using Xcode 14. Its Sendable documentation is more extensive than Xcode 13’s.
A type whose values can safely be passed across concurrency domains by copying.
Scroll down to the Inherited By section: The Actor protocol is Sendable, so actor instances are safe to use in concurrent code. No surprise.
Scroll down to the Conforming Types section: Many types are Sendable by default: Async types, value types like Array, Dictionary, Bool, Double, Int and others are all safe to use in concurrent code.
Value types are safe because value semantics prevent you from accidentally mutating a shared reference to the same object.
Classes are generally not Sendable because their by-reference semantics allow you to mutate the same instance in memory.
The Sendable protocol has no requirements — you really only use it to annotate types that you know are safe to use across threads.
Once you add Sendable conformance to one of your types, the compiler will automatically limit it in various ways to help you ensure its thread safety. For example, it’ll ask you to make a class final or make class properties immutable. Let’s take a closer look at how this works in the Task type…
init(
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async -> Success
)
You use the @Sendable attribute to require thread-safe values in your code.
For example, the Task type creates an asynchronous task that could unsafely mutate shared state, so the Task.init(...) declaration requires that the operation closure is Sendable.
addTask(...) also requires a Sendable closure:
mutating func addTask(
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async -> ChildTaskResult
)
Therefore, the best practice in your own code is to require that any closures you run asynchronously be @Sendable, and that any values you use in asynchronous code adhere to the Sendable protocol.
And if your struct or class is thread-safe, you should also add Sendable conformance so other concurrent code can use it safely.
Making safe methods nonisolated
Now that you’ve moved imageFeed off your custom actor and onto MainActor, the methods that work with the feed don’t actually work with your actor’s shared state directly.
In EmojiArtModel, loadImages() and downloadImage(_:) don’t have any state to protect anymore. Therefore, they don’t need the actor behavior at all.
When methods like that are safe, you can mark them with the nonisolated keyword: This speeds up the runtime by removing the safety harness around them.
Mark loadImages() as nonisolated:
nonisolated func loadImages() async throws
And also downloadImage(_:)
nonisolated func downloadImage(_ image: ImageFile) async throws -> Data
Now, Xcode treats these two methods like normal class methods instead of actor methods.
In the next episode, you’ll use a more complex actor — an image cache.