Swift Concurrency Notes

In this article, I will share my insights about Donny Wals's presentation in iOS Conf SG 2023.

@MainActor

struct MyView: View {
 @StateObject var vm = MyViewModel()
 
 var body: some View {
   Button {
     Task {
        await performSomeWork()
      }
    } label: {
      Text("Test")
      }
   }
}
func someVerySlowOperation() async {
   // this takes a while...
} 
 
await MainActor.run {
  await someVerySlowOperation()
}
class MyViewModel: ObservableObject {
  // this will always run on the Global executor
   func performSomeWork() async {
      
    }
}
class MyViewModel: ObservableObject {
  // this will always run on the Main actor / thread
  @MainActor func performSomeWork() async {
 
  }
}
@MainActor
class MyViewModel: ObservableObject {
  // this will _not_ run on the Main actor / thread
  nonisolated func performSomeWork() async {
  
  }
}
struct ContentView: View {
  @StateObject var myViewModel = MyViewModel()
 
var body: some View {
  Button("Test") {
   	Task { 
      await someExpensiveOperation()
    }
   }
 }
 
**Where does this function work?** 
	// Since usage of @StateObject, @ObservedObject and @EnvironmentObject properties
  // Struct gains implicit MainActor behaviour. 
  // So, the answer is this function also work on **Main actor / thread**
private func someExpensiveOperation() async {
  
 }
}

Recap:

🌟 Key rule: Functions run on the global executor unless otherwise specified.

🌟 Mark method or enclosing object as @MainActor to enforce main actor

🌟 Use nonisolated to opt out MainActor isolation

🌟 SwiftUI views with ObservableObjects are implicitly @MainActor.


Task

In Swift Concurrency, a Task represents a unit of work. There are two types of tasks: unstructured and detached. Unstructured tasks inherit parts of their creation context, but they are not child tasks of their creation context. Detached tasks, on the other hand, do not inherit anything. Both types of tasks can work in parallel with other tasks and do not interact with other tasks.

🌟 Unstructured tasks inherits parts of their creation context.

🌟 Unstructured tasks are not child tasks of their creation context.

🌟 Detached tasks inherit nothing.

🌟 Both tasks are their own islands of concurrency. They can work in parallel other with concurrent tasks. They don’t interact with other tasks which means only interact within their own islands.


Structured concurrency relates to tasks and their children

Example

In this example, withTaskGroup is used to create a new task group. Inside the task group, a new child task is spawned to run the longRunningTask function. The for await loop is used to wait for all child tasks to finish and collect their results. The result of the task group is the sum of the results of all child tasks. This is an example of structured concurrency because the parent task (the task group) cannot finish until all its child tasks have finished.

// Define a function that simulates a long-running task
func longRunningTask() async -> Int {
    print("Starting long running task...")
    await Task.sleep(2 * 1_000_000_000)  // Sleep for 2 seconds
    print("Long running task finished.")
    return 42
}
 
// Define a function that uses structured concurrency to run the long-running task
func runTask() async {
    let result = await withTaskGroup(of: Int.self) { group -> Int in
        // Spawn a new child task
        group.spawn {
            await longRunningTask()
        }
 
        // Wait for all child tasks to finish and collect the results
        var total = 0
        for await result in group {
            total += result
        }
        return total
    }
    print("Result: \(result)")
}
 
// Run the task
Task {
    await runTask()
}

Example

// safe because whole class (including both property and method) is actor-isolated; `Task {…}` will be actor isolated, too
@MainActor
class SafeActorIsolatedClassExample {
    var foo = Foo(bar: "baz")
    
    func thisIsSafe() {
        Task {
            foo.bar = "qux"
        }
    }
}
 
// safe because both property and method are actor-isolated to same global actor; `Task {…}` will be actor isolated, too
class SafeActorIsolatedPropertyAndMethodExample {
    @MainActor var foo = Foo(bar: "baz")
    
    @MainActor func thisIsSafe() {
        Task {
            foo.bar = "qux"
        }
    }
}
 
// safe because `UIViewController` is actor-isolated to `@MainActor`, 
// and therefore behaves like `SafeActorIsolatedClassExample`
// @available(iOS 2.0, *)
// @MainActor open class UIViewController :
 
class SafeViewController: UIViewController {
    var foo = Foo(bar: "baz")
    
    func thisIsSafe() {
        Task {
            foo.bar = "qux"
        }
    }
}

Sources:

Your Brain 🧠 on Swift Concurrency - iOS Conf SG 2023 (opens in a new tab) Mutating a mutable Struct within a Swift Task (opens in a new tab)

Sign up for my newsletter