Category: 04. Robustness & Async

https://cdn3d.iconscout.com/3d/premium/thumb/shield-robust-3d-icon-png-download-12991751.png

  • Swift Memory Management

    Swift Memory Management

    Understand ARC, avoid retain cycles with weak/unowned, and manage closure captures safely.


    Automatic Reference Counting (ARC)

    Classes are reference types.

    Swift uses ARC to automatically track and release class instances when no strong references remain.

    Example

    class Box {
      let name: String
      init(_ n: String) { name = n; print("init \(n)") }
      deinit { print("deinit \(name)") }
    }
    
    do {
      let b = Box("A")
      print("in scope")
    }
    print("after scope")

    This example demonstrates ARC’s automatic deallocation of the Box instance when it goes out of scope.

    Tip: Use weak to avoid strong reference cycles between class instances.


    Strong Reference Cycles

    strong reference cycle occurs when two class instances hold strong references to each other, preventing ARC from deallocating them.

    Mark one side as weak (or unowned when appropriate) to break the cycle.

    Example

    class Person {
      let name: String
      var apartment: Apartment?
      init(name: String) { self.name = name }
      deinit { print("Person deinit") }
    }
    
    class Apartment {
      let unit: String
      weak var tenant: Person? // weak breaks the cycle
      init(unit: String) { self.unit = unit }
      deinit { print("Apartment deinit") }
    }
    
    do {
      var john: Person? = Person(name: "John")
      var unit: Apartment? = Apartment(unit: "4A")
      john!.apartment = unit
      unit!.tenant = john
      john = nil   // Person deinit
      unit = nil   // Apartment deinit
    }

    Declaring tenant as weak breaks the cycle so both objects deallocate when their strong references are set to nil.



    Closures and Capture Lists (weak self)

    Closures capture variables by default.

    When a class stores a closure that references self, use a capture list like [weak self] to avoid retain cycles.

    Example

    class Loader {
      var onComplete: (() -> Void)?
      func load() {
    
    // simulate async completion
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
      guard let self = self else { return }
      print("Finished: \(self)")
      self.onComplete?()
    }
    } deinit { print("Loader deinit") } } do { let loader = Loader() loader.onComplete = { print("done callback") } loader.load() } // loader can be deallocated if nothing else references it
  • Swift Concurrency

    Swift Concurrency

    Write safe, structured async code with async/await, Tasks, actors, and cooperative cancellation.


    Basic GCD (Grand Central Dispatch)

    Use DispatchQueue to perform work asynchronously.

    The example below runs a task on a background queue and waits for it to complete.

    Syntax:

    • DispatchQueue.global().async { ... }
    • DispatchGroup.enter()
    • DispatchGroup.leave()
    • DispatchGroup.wait()

    Example

    import Dispatch
    
    print("Start")
    let group = DispatchGroup()
    group.enter()
    DispatchQueue.global().async {
      print("Background work")
      group.leave()
    }
    group.wait()
    print("Done")

    This example runs work on a background queue and waits for completion using a DispatchGroup.

    Tip: In modern Swift, prefer async/await and Task over GCD for structured concurrency.


    Async/Await with Task

    async/await lets you write asynchronous code that looks like synchronous code.

    Use Task to start concurrent work from synchronous contexts.

    Syntax:

    • func name() async -> T {}
    • await value
    • Task { ... }

    Example

    import Dispatch
    
    func fetchValue() async -> Int { 7 }
    
    print("Start")
    let sem = DispatchSemaphore(value: 0)
    Task {
      let v = await fetchValue()
      print("Got \(v)")
      sem.signal()
    }
    sem.wait()
    print("Done")

    This example starts asynchronous work with Task and awaits a value using async/await.



    async let (Parallel Child Tasks)

    Use async let to start multiple child tasks in parallel and await their results.

    Syntax: async let name = expression() starts a child task; use await when reading the value.

    Example

    import Dispatch
    
    func fetch(_ id: Int) async -> Int { id * 10 }
    
    print("Start")
    let sem = DispatchSemaphore(value: 0)
    Task {
      async let a = fetch(1)
      async let b = fetch(2)
      let total = await (a + b)
      print("Total \(total)")
      sem.signal()
    }
    sem.wait()
    print("Done")

    This example launches two child tasks in parallel and awaits both to compute a total.


    Async/Await with Errors

    Async functions can also throw.

    Combine try with await and handle failures with do/catch.

    Syntax:

    • func name() async throws -> T
    • try await
    • do { ... } catch { ... }

    Example

    import Dispatch
    
    enum FetchError: Error { case bad }
    
    func fetch(_ ok: Bool) async throws -> Int {
      if !ok { throw FetchError.bad }
      return 42
    }
    
    print("Start")
    let sem = DispatchSemaphore(value: 0)
    Task {
      do {
    
    let v = try await fetch(false)
    print("ok \(v)")
    } catch {
    print("error")
    } sem.signal() } sem.wait() print("Done")

    This example shows error handling with try/await and do/catch around an async function.


    Task Groups

    Use withTaskGroup to fan out concurrent child tasks and aggregate their results.

    Syntax: withTaskGroup(of: ReturnType.self) { group in ... } adds tasks and iterates results.

    Example

    import Dispatch
    
    func square(_ n: Int) async -> Int { n * n }
    
    print("Start")
    let sem = DispatchSemaphore(value: 0)
    Task {
      var results: [Int] = []
      await withTaskGroup(of: Int.self) { group in
    
    for n in [1,2,3] {
      group.addTask { await square(n) }
    }
    for await val in group {
      results.append(val)
    }
    } print(results.sorted().map(String.init).joined(separator: ",")) sem.signal() } sem.wait() print("Done")

    This example concurrently computes squares for a list of numbers and aggregates the results from the task group.


    Actors and MainActor

    Actors protect their mutable state from data races by serializing access.

    Use @MainActor to mark code that must run on the main thread (e.g., UI updates).

    Syntax: actor Name { ... } defines an actor; call methods/properties with await from outside.

    MainActor: Annotate types/functions with @MainActor to run on the main thread.

    Example

    import Dispatch
    
    actor SafeCounter { private var value = 0 func increment() { value += 1 } func get() -> Int { value } } let counter = SafeCounter() print("Start") let sem = DispatchSemaphore(value: 0) Task { await withTaskGroup(of: Void.self) { group in
    for _ in 0..<1000 {
      group.addTask { await counter.increment() }
    }
    } print("Final: \(await counter.get())") sem.signal() } sem.wait() print("Done")

    This example uses an actor to protect mutable state and aggregates increments via a task group.

    Tip: Annotate UI-facing APIs with @MainActor to ensure they execute on the main thread.


    Task Cancellation

    Cancel long-running work by calling task.cancel() and checking for cancellation with Task.isCancelled or try Task.checkCancellation().

    Syntax:

    • t.cancel()
    • Task.isCancelled
    • try Task.checkCancellation()

    Example

    import Dispatch
    
    func slowWork() async throws {
      for i in 1...5 {
    
    try await Task.sleep(nanoseconds: 300_000_000) // 0.3s
    try Task.checkCancellation()
    print("Step ", i)
    } } let sem = DispatchSemaphore(value: 0) let t = Task { do { try await slowWork() } catch { print("Cancelled") } sem.signal() } DispatchQueue.global().asyncAfter(deadline: .now() + 0.7) { t.cancel() } sem.wait()
  • Swift Error Handling

    Swift Error Handling

    Throw and catch errors explicitly, or convert failures to optionals with try? when appropriate.


    Throw, Try, Catch

    Functions can throw errors with throw.

    Callers must use try and handle failures with do/catch, or use try? to get an optional result.

    Syntax:

    • func f() throws -> T
    • try f()
    • try? f()
    • do { ... } catch { ... }

    Example

    enum InputError: Error { case negative }
    
    func validate(_ n: Int) throws -> String {
      if n < 0 { throw InputError.negative }
      return "ok: \(n)"
    }
    
    do {
      let result = try validate(-1)
      print(result)
    } catch {
      print("error")
    }
    
    let maybe = try? validate(1)
    print(maybe ?? "nil")

    Tip: Use defer to run cleanup code before exiting the current scope, even when errors are thrown.



    Defer (Cleanup)

    Use defer to ensure cleanup code always runs when a scope exits, even if an error is thrown.

    Example

    enum FileError: Error { case fail }
    
    func work(_ ok: Bool) throws {
      print("start")
      defer { print("cleanup") }
      if !ok { throw FileError.fail }
      print("done")
    }
    
    do { try work(false) } catch { print("error") }