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
Save for later
About this episode
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 16. GlobalActor Next episode: 18. Using a GlobalActor

This video Creating a GlobalActor was last updated on Oct 20 2022

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.

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