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’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

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