Modern Concurrency: Beyond the Basics

Oct 20 2022 · Swift 5.5, iOS 15, Xcode 13.4

Part 2: Concurrent Code

15. Writing Safe Concurrent Code With Actors

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 14. Actor Next episode: 16. GlobalActor

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

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

In the previous episode, you converted EmojiArtModel from a class to an actor, to protect its verifiedCount property from data races.

A more complex actor

In this episode, you’ll mix actors, tasks and async/await to solve one of the eternal problems in programming: image caching.

enum DownloadState {
  case inProgress(Task<UIImage, Error>)
  case completed(UIImage)
  case failed
}
private(set) var cache: [String: DownloadState] = [:]

Filling the cache

ImageLoader has methods to add images to the cache, start a new download and clear the cache.

func add(_ image: UIImage, forKey key: String) {
  cache[key] = .completed(image)
}
let download: Task<UIImage, Error> = Task.detached {
  guard let url = URL(string: "http://localhost:8080".appending(serverPath))
  else {
    throw "Could not create the download URL"
  }
  print("Download: \(url.absoluteString)")
  let data = try await URLSession.shared.data(from: url).0
  return try resize(data, to: CGSize(width: 200, height: 200))
}

cache[serverPath] = .inProgress(download)
cache[serverPath] = .inProgress(download)
do {
  let result = try await download.value
  add(result, forKey: serverPath)
  return result
} catch {
  cache[serverPath] = .failed
  throw error
}
func clear() {
  cache.removeAll()
}

Sharing ImageLoader with views

Since you’ll use ImageLoader in a few different views, you need to inject it directly into the SwiftUI environment, so you can easily access it throughout your view hierarchy.

actor ImageLoader: ObservableObject
.environmentObject(ImageLoader())
@EnvironmentObject var imageLoader: ImageLoader
.task {
  guard let image = try? await imageLoader.image(file.url) else {
    overlay = "camera.metering.unknown"
    return
  }
  updateImage(image)
}

Using the cached assets

The server image feed returns some duplicate assets so you can play around with the scenario of getting an already-cached asset and displaying it.

Download: http://localhost:8080/gallery/image?11
Download: http://localhost:8080/gallery/image?16
Download: http://localhost:8080/gallery/image?23
Download: http://localhost:8080/gallery/image?26
@EnvironmentObject var imageLoader: ImageLoader
.task {
  image = try? await imageLoader.image(file.url)
}