Modern Concurrency: Beyond the Basics

Oct 20 2022 Swift 5.5, iOS 15, Xcode 13.4

Part 2: Concurrent Code

14. Actor

Episode complete

Play next episode

Next
Save for later
About this episode
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 13. Using TaskGroup Next episode: 15. Writing Safe Concurrent Code With Actors

This episode displays the Xcode 14 documentation for Sendable — it’s more extensive than Xcode 13’s.

Heads up... You've reached locked video content where the transcript will be shown as obfuscated text.

You can unlock the rest of this video course, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.

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.

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.

Detecting race conditions

One way to detect data races in your code is to enable the Thread Sanitizer in your Xcode project scheme.

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.

actor EmojiArtModel: ObservableObject  // don't press return: this auto-selects Actor
"Actor-isolated property 'verifiedCount' can not be mutated from a Sendable closure".
actor Counter {
  private var count = 0

  func increment() {
    count += 1
  }
}

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.

private func increaseVerifiedCount() {
  verifiedCount += 1
}
await self.increaseVerifiedCount()

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?

@Published @MainActor private(set) var imageFeed: [ImageFile] = []

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.

await imageFeed.forEach { file in
await MainActor.run {
  imageFeed.removeAll()
}
await MainActor.run {
  imageFeed = list
}
Actor-isolated property 'verifiedCount' can not be referenced from the main actor
Task {
  progress = await Double(model.verifiedCount) /
    Double(model.imageFeed.count)
}
A type whose values can safely be passed across concurrency domains by copying.
init(
  priority: TaskPriority? = nil, 
  operation: @escaping @Sendable () async -> Success
)
mutating func addTask(
  priority: TaskPriority? = nil, 
  operation: @escaping @Sendable () async -> ChildTaskResult
)

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.

nonisolated func loadImages() async throws
nonisolated func downloadImage(_ image: ImageFile) async throws -> Data