SwiftUI Views and @MainActor
Tags: swiftui, swift, ios, mobile
Published: 2024-04-17 06:58:59

An increasing number of developers are starting to enable strict concurrency checks in preparation for the arrival of Swift 6. Among the warnings and errors received, a portion relates to SwiftUI views, many of which stem from developers not correctly understanding or using `@MainActor`. This article will discuss the meaning of `@MainActor`, as well as tips and considerations for applying `@MainActor` within SwiftUI views.

---


Don’t miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to [Fatbobman’s Swift Weekly](https://weekly.fatbobman.com) and receive weekly insights and valuable content directly to your inbox.

---

## My PasteButton Stopped Working

Not long ago, in my [Discord](https://discord.com/servers/zhou-zi-de-swiftji-shi-ben-967978112509935657) community, a friend reported that after enabling strict concurrency checks, the compiler threw the following error for `PasteButton`:

```
Call to main actor-isolated initializer 'init(payloadType:onPast:)' in a synchronous nonisolated context
```

![pasteButton-MainActor-error-2024-03-13](https://cdn.fatbobman.com/pasteButton-MainActor-error-2024-03-13.png)

After examining the declaration of `PasteButton`, I asked him whether he placed the view code outside of `body`. Upon receiving a positive response, I advised him to add `@MainActor` to the variable declaration of `PasteButton`, and that solved the problem.

```swift
@MainActor public struct PasteButton : View {
    @MainActor public init(supportedContentTypes: [UTType], payloadAction: @escaping ([NSItemProvider]) -> Void)

    @MainActor public init<T>(payloadType: T.Type, onPaste: @escaping ([T]) -> Void) where T : Transferable
    public typealias Body = some View
}
```

So, where was the issue initially? Why did adding `@MainActor` resolve it?

## What is @MainActor

In Swift's concurrency model, `actor` offers a safe and understandable way to write concurrent code. An `actor` is similar to a class but is specifically designed to address data races and synchronization issues in a concurrent environment.

```swift
actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }
    
    func getValue() async -> Int {
        return value
    }
}

let counter = Counter()
Task {
  await counter.increment()
}
```

The magic of `actor` lies in its ability to serialize access to prevent data races, providing a clear and safe path for concurrent operations. However, this isolation is localized to specific `actor` instances. Swift introduces the concept of `GlobalActor` to extend isolation more broadly.

`GlobalActor` allows us to annotate code across different modules to ensure that these operations are executed in the same serial queue, thereby maintaining the atomicity and consistency of operations.

```swift
@globalActor actor MyActor: GlobalActor {
    static let shared = MyActor()
}

@MyActor
struct A {
  var name: String = "fat"
}

class B {
  var age: Int = 10
}

@MyActor
func printInfo() {
  let a = A()
  let b = B()
  print(a.name, b.age)
}
```

`@MainActor` is a special `GlobalActor` defined by Swift. Its role is to ensure that all code annotated with `@MainActor` executes in the same serial queue, and all this happens on the main thread.

```swift
@globalActor actor MainActor : GlobalActor {
    static let shared: MainActor
}
```

This succinct and powerful feature of `@MainActor` provides a type-safe and integrated way to handle operations that previously relied on `DispatchQueue.main.async` within Swift's concurrency model. It not only simplifies the code and reduces the error rate but also ensures, through compiler protection, that all operations marked with `@MainActor` are safely executed on the main thread.

## The View Protocol and @MainActor

In the world of SwiftUI, a view plays the role of declaratively presenting the application's state on the screen. This naturally leads to the assumption that all code composing a view would execute on the main thread, given that views directly relate to the user interface's presentation.

However, delving into the View protocol reveals a detail: only the `body` property is explicitly marked with `@MainActor`. This discovery means that types conforming to the View protocol are not guaranteed to run entirely on the main thread. Beyond `body`, the compiler does not automatically ensure that other properties or methods execute on the main thread.

```swift
public protocol View {
    associatedtype Body : View
    @ViewBuilder @MainActor var body: Self.Body { get }
}
```

This insight is particularly crucial for understanding the use of SwiftUI's official components like `PasteButton`, which, unlike most other components, is explicitly marked with `@MainActor`. This indicates that `PasteButton` must be used within a context also marked with `@MainActor`, or else the compiler will report an error, indicating that a call to a main actor-isolated initializer is not allowed in a synchronous nonisolated context:

```swift
struct PasteButtonDemo: View {
  var body: some View {
    VStack {
      Text("Hello")
      button
    }
  }
  
  var button: some View {
    PasteButton(payloadType: String.self) { str in // Call to main actor-isolated initializer 'init(payloadType:onPaste:)' in a synchronous nonisolated context
      print(str)
    }
  }
}
```

To resolve this issue, simply marking the `button` variable with `@MainActor` can smoothly pass the compilation, as it ensures that `button` is initialized and used within an appropriate context:

```swift
@MainActor
var button: some View {
  PasteButton(payloadType: String.self) { str in
    print(str)
  }
}
```

> Most SwiftUI components are value types and conform to the Sendable protocol, and they are not explicitly marked as `@MainActor`, thus they do not encounter the specific issues faced by `PasteButton`.

This modification highlights the importance of using `@MainActor` in SwiftUI views and also serves as a reminder to developers that not all code related to views is executed on the main thread by default.

## Applying @MainActor to Views

Some readers might wonder, could directly annotating the `PasteButtonDemo` view type with `@MainActor` fundamentally solve the problem?

Indeed, annotating the entire `PasteButtonDemo` view with `@MainActor` can address the issue at hand. Once annotated with `@MainActor`, the Swift compiler assumes that all properties and methods within the view execute on the main thread, thus obviating the need for a separate annotation for `button`.

```swift
@MainActor
struct PasteButtonDemo: View {
  var body: some View {
    ...
  }
  
  var button: some View {
    PasteButton(payloadType: String.self) { str in
      ...
    }
  }
}
```

This approach also brings other benefits. For example, when building observable objects with the `Observation` framework, to ensure their state updates occur on the main thread, one might annotate the observable objects themselves with `@MainActor`:

```swift
@MainActor
@Observable
class Model {
  var name = "fat"
  var age = 10
}
```

However, attempting to declare this observable object instance in the view with `@State`, as recommended by official documentation, would encounter a compiler warning; this is considered an error in Swift 6.

```swift
struct DemoView: View {
  @State var model = Model() // Main actor-isolated default value in a nonisolated context; this is an error in Swift 6
  var body: some View {
    NameView(model: model)
  }
}

struct NameView: View {
  let model: Model
  var body: some View {
    Text(model.name)
  }
}
```

This issue arises because, by default, a view's implementation is not annotated with `@MainActor`, thus it cannot directly declare types annotated with `@MainActor`. Once `DemoView` is annotated with `@MainActor`, the aforementioned issue is resolved.

To further simplify the process, we could also define a protocol annotated with `@MainActor`, allowing any view that conforms to this protocol to automatically inherit the main thread execution environment:

```swift
@MainActor
protocol MainActorView: View {}
```

Thus, any view that implements the `MainActorView` protocol ensures that all its operations execute on the main thread:

```swift
struct AsyncDemoView: MainActorView {
  var body: some View {
    Text("abc")
      .task {
        await do something()
      }
  }
  
  func doSomething() async {
    print(Thread.isMainThread) // true
  }
}
```

Although annotating view types with `@MainActor` seems like a good solution, it requires all asynchronous methods declared within the view to execute on the main thread, which may not always be desirable. For example:

```swift
@MainActor
struct AsyncDemoView: View {
  var body: some View {
    Text("abc")
      .task {
        await doSomething()
      }
  }
  
  func doSomething() async {
    print(Thread.isMainThread) // true
  }
}
```

If not annotated with `@MainActor`, we could more flexibly annotate properties and methods as needed:

```swift
struct AsyncDemoView: View {
  var body: some View {
    Text("abc")
      .task {
        await doSomething()
      }
  }
  
  func doSomething() async {
    print(Thread.isMainThread) // false
  }
}
```

Therefore, whether to annotate a view type with `@MainActor` depends on the specific application scenario.

## New Uses for @StateObject

With the Observation framework becoming the new standard, the traditional use of `@StateObject` seems to become less prominent. However, it still possesses a special functionality that allows it to remain useful in the era of Observation. As we've discussed before, an `@Observable` object marked with `@MainActor` cannot be directly declared with `@State`—unless the entire view is also annotated with `@MainActor`. But, with `@StateObject`, we can cleverly circumvent this limitation.

Consider the following example, where we can safely introduce an observable object marked with `@MainActor` into the view without having to mark the entire view with `@MainActor`:

```swift
@MainActor
@Observable
class Model: ObservableObject {
  var name = "fat"
  var age = 10
}

struct StateObjectDemo: View {
  @StateObject var model = Model()
  var body: some View {
    VStack {
      NameView(model: model)
      AgeView(model: model)
      Button("update age"){
        model.age = Int.random(in: 0..<100)
      }
    }
  }
}
```

The feasibility of this practice stems from the unique loading mechanism of `@StateObject`. It calls the constructor's closure on the main thread when the view is actually loaded. Moreover, the `wrappedValue` of `@StateObject` is annotated with `@MainActor`, ensuring it can correctly initialize and use types conforming to the `ObservableObject` protocol marked with `@MainActor`.

```swift
@frozen @propertyWrapper public struct StateObject<ObjectType>: SwiftUI.DynamicProperty where ObjectType: Combine.ObservableObject {
    @usableFromInline
    @frozen internal enum Storage {
        case initially(() -> ObjectType)
        case object(SwiftUI.ObservedObject<ObjectType>)
    }

    @usableFromInline
    internal var storage: SwiftUI.StateObject<ObjectType>.Storage
    @inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) {
        storage = .initially(thunk)
    }

    @_Concurrency.MainActor(unsafe) public var wrappedValue: ObjectType {
        get
    }

    @_Concurrency.MainActor(unsafe) public var projectedValue: SwiftUI.ObservedObject<ObjectType>.Wrapper {
        get
    }

    public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
}
```

The main advantage of this method is that it ensures the safety of the observable object's lifecycle while completely preserving its observation logic based on the Observation framework. This allows us to flexibly use observable types marked with `@MainActor` without having to mark the entire view with `@MainActor`. This offers a path that maintains the flexibility of the view without compromising data safety and response logic. Until Apple provides a more definitive solution for running `@State` on the main thread or adjusting view declarations, this approach serves as a practical and effective temporary strategy.

> In the current version of SwiftUI (prior to Swift 6), when developers declare state within a view using `@StateObject`, the Swift compiler implicitly treats the entire view as being annotated with `@MainActor`. This implicit inference behavior can easily lead to misunderstandings among developers. With the official adoption of the [SE-401](https://github.com/apple/swift-evolution/blob/main/proposals/0401-remove-property-wrapper-isolation.md) proposal, starting from Swift 6, such implicit inference will no longer be permitted.

## Conclusion

After enabling strict concurrency checks, many developers might feel confused and overwhelmed by a slew of warnings and errors. Some may resort to modifying code based on these prompts to eliminate errors. However, the fundamental purpose of introducing a new concurrency model into projects goes far beyond "deceiving" the compiler. In reality, developers should deeply understand Swift's concurrency model and reassess their code on a more macro level to discover higher-quality, safer resolution strategies, rather than simply addressing symptoms. This approach not only enhances the quality and maintainability of the code but also helps developers navigate the world of Swift's concurrency programming with greater confidence and ease.

> The original article was published on my blog [Fatbobman's Blog](https://fatbobman.com/en/).