Swift Actors Demo from Network to UI

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

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

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

Unlock now

Swift Actor Demo From Network to UI

This section guides you through building a small iOS app that demonstrates modern Swift concurrency using actors. We’ll tackle common problems like data races and redundant network requests, building a safe and efficient image loading system.

The “Why”: Solving Data Races with Actors

Before we code, let’s understand the problem. In concurrent programming, a data race occurs when multiple threads try to access and change the same piece of data at the same time, leading to unpredictable and incorrect results. Imagine two cashiers trying to update the same inventory count on paper simultaneously—the final number would be chaos!

Key Terms You’ll See:

  1. actor: A type that protects its internal state from concurrent access.
  2. await: The keyword you use to call an actor’s methods from the outside. It tells your code to pause if necessary until the actor is free to respond.
  3. @MainActor: A special global actor that ensures code runs on the main UI thread, which is required for all UI updates.
  4. @globalActor: A pattern for creating your own app-wide, shared actor to protect a resource like the file system.

Project Overview

You’ll create:


import Foundation

actor ImageCache {
  private var inMemory: [URL: Data] = [:]
  private var inFlight: [URL: Task<Data, Error>] = [:]

  func data(for url: URL) async throws -> Data {
    if let d = inMemory[url] { return d }
    if let t = inFlight[url] { return try await t.value }

    let t = Task { [url] () async throws -> Data in
      let (data, resp) = try await URLSession.shared.data(from: url)
      guard (resp as? HTTPURLResponse)?.statusCode == 200 else {
        throw URLError(.badServerResponse)
      }
      return data
    }
    inFlight[url] = t
    defer { inFlight[url] = nil }

    let data = try await t.value
    inMemory[url] = data
    return data
  }
}

import Foundation

@globalActor
actor DiskActor: GlobalActor {
  static let shared = DiskActor()
}

@DiskActor
enum ImageDisk {
  static func url(for key: String) -> URL {
    FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
      .appendingPathComponent("\(key).img")
  }
  static func read(for key: String) -> Data? {
    try? Data(contentsOf: url(for: key))
  }
  static func write(_ data: Data, for key: String) {
    try? data.write(to: url(for: key), options: .atomic)
  }
}

import Foundation

extension ImageCache {
  func dataWithDisk(for url: URL) async throws -> Data {
    let key = (url.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? "img")
    if let disk = await ImageDisk.read(for: key) {
      inMemory[url] = disk
      return disk
    }
    let data = try await data(for: url)
    await ImageDisk.write(data, for: key)
    return data
  }
}


import SwiftUI
import Observation

struct ContentView: View {
  @Environment(ArticleImageVM.self) private var vm
  private let url = URL(string: "https://httpbin.org/image/png")!

  var body: some View {
    VStack(spacing: 16) {
      if let img = vm.image {
        img.resizable().scaledToFit().frame(maxHeight: 240)
      } else {
        ProgressView("Loading…")
      }

      Toggle("Use Disk Cache", isOn: Binding(
        get: { vm.useDisk },
        set: { vm.useDisk = $0 }
      ))
      .padding(.horizontal)

      HStack {
        Button("Reload") { vm.load(from: url) }
        Button("Clear Image") { vm.image = nil }
      }
    }
    .padding()
    .task { vm.load(from: url) }
  }
}

import SwiftUI
import Observation

@main
struct ActorsDemoApp: App {
  @State private var vm = ArticleImageVM()

  var body: some Scene {
    WindowGroup {
      ContentView()
        .environment(vm) // inject Observation model into environment
    }
  }
}

Playground Smoke Test (Optional)

What this code does: A minimal, console-based demo to prove the core concept of actor-based safety. The SafeTicketOffice simulates selling a limited number of tickets (just one). We then try to buy two tickets at the same time using async let.

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

actor Counter {
  private var value = 0
  func increment() { value += 1 }
  func get() -> Int { value }
}

actor SafeTicketOffice {
  private var available = 1
  func buy() async throws {
    guard available > 0 else { throw NSError(domain: "soldout", code: 0) }
    available -= 1
    do {
      try await Task.sleep(nanoseconds: 100_000_000) // simulate work
    } catch {
      available += 1 // Important: restore state if task is cancelled
      throw error
    }
  }
  func remaining() -> Int { return available }
}

let counter = Counter()
let office = SafeTicketOffice()

Task {
  await counter.increment()
  print("Counter =", await counter.get())

  async let first: Void = { try? await office.buy() }()
  async let second: Void = { try? await office.buy() }()
  _ = await (first, second)

  print("Tickets left =", await office.remaining()) // Should print 0
  PlaygroundPage.current.needsIndefiniteExecution = false
}
See forum comments
Download course materials from Github
Previous: A Guide to Swift Actors Next: Conclusion