Modern Concurrency: Beyond the Basics

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

Part 2: Concurrent Code

17. Creating a GlobalActor

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: 16. GlobalActor Next episode: 18. Using a 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.

The ImageLoader actor implements an in-memory cache. It manages a dictionary of completed, failed and in-progress downloads, so the server doesn’t get duplicate requests.

Creating a global actor

  • In the Model group, create a new Swift file named ImageDatabase.swift and replace the import statement:
import UIKit

@globalActor actor ImageDatabase {
  static let shared = ImageDatabase()

}
let imageLoader = ImageLoader()
private let storage = DiskStorage()
private var storedImagesIndex = Set<String>()

Creating a safe silo

You’ve introduced two dependencies into your code: ImageLoader and DiskStorage.

🟩@ImageDatabase 🟥class DiskStorage {
Call to global actor 'ImageDatabase'-isolated initializer 'init()' in a synchronous actor-isolated context

Initializing the database actor

  • Change the storage declaration:
private var storage: DiskStorage!  // remember to change = to :
func setUp() async throws {
  storage = await DiskStorage()
  for fileURL in try await storage.persistedFiles() {
    storedImagesIndex.insert(fileURL.lastPathComponent)
  }
}

Writing files to disk

The new cache will need to write images to disk. When you fetch an image, you’ll export it to PNG format and save it.

func store(image: UIImage, forKey key: String) async throws {
  guard let data = image.pngData() else {
    throw "Could not save image \(key)"
  }
}
func store(image: UIImage, forKey key: String) async throws {
  guard let data = image.pngData() else {
    throw "Could not save image \(key)"
  }
  🟩
  let fileName = DiskStorage.fileName(for: key)
  try await storage.write(data, name: fileName)
  🟥
}
func store(image: UIImage, forKey key: String) async throws {
  guard let data = image.pngData() else {
    throw "Could not save image \(key)"
  }
  let fileName = DiskStorage.fileName(for: key)
  try await storage.write(data, name: fileName)
  🟩
  storedImagesIndex.insert(fileName)
  🟥
}
Expression is 'async' but is not marked with 'await'
🟩nonisolated 🟥static func fileName(for path: String) -> String {

Fetching images from disk (or elsewhere)

Now, you need a helper method to fetch an image from the database. If the file is already stored on disk, you’ll fetch it from there. Otherwise, you’ll use ImageLoader to make a request to the server. Add a method:

func image(_ key: String) async throws -> UIImage {

}
func image(_ key: String) async throws -> UIImage {
🟩
  let keys = await imageLoader.cache.keys
🟥
}
func image(_ key: String) async throws -> UIImage {
  let keys = await imageLoader.cache.keys
  🟩
  if keys.contains(key) {
    print("In memory cache.")
    return try await imageLoader.image(key)
  }
  🟥
}
do {
  let fileName = DiskStorage.fileName(for: key)
  if !storedImagesIndex.contains(fileName) {
    throw "Image not persisted."
  }
} catch {

}
do {
  let fileName = DiskStorage.fileName(for: key)
  if !storedImagesIndex.contains(fileName) {
    throw "Image not persisted"
  }
🟩
  let data = try await storage.read(name: fileName)
  guard let image = UIImage(data: data) else {
    throw "Invalid image data."
  }
🟥
} catch {
 
}
🟩
  print("In disk cache.")
  await imageLoader.add(image, forKey: key)
  return image
🟥
} catch {

}
} catch {
🟩
  let image = try await imageLoader.image(key)
  try await store(image: image, forKey: key)
  return image
🟥
}

Purging the cache

  • Add a clear method to ImageDatabase:
func clear() async {
  for name in storedImagesIndex {
    try? await storage.remove(name: name)
  }
  storedImagesIndex.removeAll()
}