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’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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)
}