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.

Transcript: 15. Writing Safe Concurrent Code With Actors

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.

You’ll use an actor that fetches the digital emoji assets from the course server and caches them in memory.

  • Continue with your project from the previous episode or open the starter project. Then open the file ImageLoader.swift in the Model group:
enum DownloadState {
  case inProgress(Task<UIImage, Error>)
  case completed(UIImage)
  case failed
}
private(set) var cache: [String: DownloadState] = [:]

This actor manages a cache dictionary to store the inProgress downloads, the images already downloaded into memory and any attempted downloads where the server returned an error.

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

Actor methods can directly mutate cache, so the add(...) method just sets the value for the given asset key to .completed(image).

The image(...) method fetches a single image from memory or from the server.

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)

If the asset isn’t in the cache, the method downloads it from the server in a detached task.

cache[serverPath] = .inProgress(download)

Once the task is ready, the method adds it to cache as an inProgress value. If the same asset appears in the feed again, the app won’t download it a second time. Instead, it will wait for the ongoing download task to complete and return the fetched result.

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

And finally, it handles the result of the download. It waits for the download task to complete, then calls add to add the image to the in-memory cache and return it.

If the task throws, it updates cache with a failure value for this asset before re-throwing the error.

func clear() {
  cache.removeAll()
}

This third method clears the in-memory cache for debugging purposes. That’s the ImageLoader actor. Now, you need to share it with all the views.

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.

To use it as an environment object, though, it must conform to ObservableObject, even though it doesn’t have any published properties. In ImageLoader.swift, add an ObservableObject conformance:

actor ImageLoader: ObservableObject

No complaints from the compiler, so move on. * Open AppMain.swift. Instantiate ImageLoader and pass it to ListView() as an environment object:

.environmentObject(ImageLoader())

Now, you can use ImageLoader from any view where you need images. * Open the Views group. ThumbImage, displays a single asset in the image feed, so this is certainly a place where you’ll need ImageLoader.

Open ThumbImage.swift and add an imageLoader property:

@EnvironmentObject var imageLoader: ImageLoader

This line initializes the injected image loader. You’ll use it to fetch the asset image. In the view body, add a task right after overlay(...):

.task {
  guard let image = try? await imageLoader.image(file.url) else {
    overlay = "camera.metering.unknown"
    return
  }
  updateImage(image)
}

So, when the thumbnail view appears onscreen, you call imageLoader.image(_:) to fetch the image from the cache or from the server. If the image method fails, you set an overlay for the thumbnail to show the user that the image load failed. If the image method returned an image, you update the view image.

Build and run. At last, you can enjoy some cool emoji art. If you see a few ? images, that’s because the server omits a few, to simulate missing images.

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.

When you look at Xcode’s output console, you’ll initially see some download logs like these:

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

Scroll all the way to the bottom of the feed, and back up again. Download logs stop appearing in the console, even if you keep scrolling up and down. Once you’ve downloaded all the assets, you only fetch images from memory!

Next, you’ll add code to DetailsView.swift to display a larger version of a selected asset.

Add the same imageLoader environment object property:

@EnvironmentObject var imageLoader: ImageLoader

Add a task modifier just below the existing foregroundColor modifier of the ZStack:

.task {
  image = try? await imageLoader.image(file.url)
}

Build and run. Tap an image and enjoy the details preview.

Congratulations, the EmojiArt online catalog app is complete, for now. However, when you quit the app and run it again, it needs to fetch the images from the server all over again. They don’t persist on the device. In the next episodes, you’ll learn about GlobalActor, then create a custom global actor to upgrade your app with a persistent, on-disk cache.