SwiftUI and Swift 6: Mastering Concurrency with @MainActor

Oleksandr Kaledin
3 min readApr 26, 2024

--

Photo by Carl Heyerdahl on Unsplash

As Swift 6 approaches, many developers implement stricter concurrency checks to enhance code safety and performance. A frequent source of issues in this transition involves the improper use of @MainActor in SwiftUI. This article will demystify @MainActor, providing essential insights and practical examples to integrate it into your SwiftUI applications effectively.

Understanding @MainActor in Swift’s Concurrency Model

In Swift’s concurrency model, an actor is a construct designed to manage a state safely and clearly in concurrent environments. Unlike classes, actors protect against data races by serializing access to their mutable state. Here’s a basic example:


actor Counter {
private var value = 0

func increment() {
value += 1
}

func getValue() async -> Int {
return value
}
}

let counter = Counter()

Task {
await counter.increment()
}

Actors handle their tasks in a controlled, sequential manner, thereby preventing the typical issues seen in concurrent programming like race conditions.

Swift also introduces the concept of `GlobalActor`, which broadens the scope of an actor to cover different modules or components, ensuring that marked operations execute on a specific queue or thread, enhancing consistency and safety.


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

@MyActor
struct Person {
var name: String = "John"
}

@MyActor
func printInfo() {
let person = Person()
print(person.name)
}

@MainActor: Specialized Global Actor for UI Operations

@MainActor is a predefined global actor in Swift that ensures operations are executed on the main thread, which is crucial for UI updates. Before @MainActor, developers typically used `DispatchQueue.main.async` to update UI components safely from background threads. @MainActor simplifies this pattern by ensuring that any code under its mark runs on the main thread, thus integrating thread safety directly into the type system.


@MainActor
class ViewModel {
var data: String = “Initial Data”

func updateData() {
data = “Updated Data”
}
}

Implementing @MainActor in SwiftUI Views

In SwiftUI, while the `body` property of a view is automatically executed on the main thread due to its @MainActor annotation, other properties and methods are not. This can lead to concurrency issues if not handled correctly. For instance, using a component like `PasteButton` in SwiftUI, which requires @MainActor, can result in compilation errors if not used within an @MainActor context.


struct PasteButtonDemo: View {

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

var body: some View {
VStack {
Text("Hello")
button
}
}
}

Best Practices with @MainActor

It’s feasible to apply @MainActor to the entire view to ensure all components are main-thread confined:


@MainActor
struct UserInterfaceView: View {
var body: some View {
// All UI components here are main-thread safe
}
}

However, marking entire views or components with @MainActor might not always be necessary or efficient, particularly for views that perform a mix of heavy computations and UI updates. Instead, it can be more effective to apply @MainActor only to those parts of the code that interact with the UI or require main-thread execution.

@StateObject and @MainActor

The use of @StateObject with @MainActor provides a seamless way to manage observable objects within SwiftUI views. By initializing @StateObject properties within a @MainActor annotated view, developers ensure that all state changes are handled on the main thread, maintaining UI consistency.


@MainActor
class UserSettings: ObservableObject {
@Published var username: String = “User”
}

struct ContentView: View {
@StateObject private var settings = UserSettings()

var body: some View {
Text(settings.username)
}
}

Conclusion

As SwiftUI and Swift’s concurrency model evolve, understanding and correctly implementing @MainActor becomes essential for developing robust, efficient, and safe iOS applications. By carefully applying @MainActor to SwiftUI views and components, developers can ensure that their applications remain responsive and crash-free, leveraging Swift’s powerful concurrency tools to their full extent. This understanding not only enhances code quality but also prepares developers for future advancements in Swift’s concurrency capabilities.

--

--

Oleksandr Kaledin

iOS App Developer with 2+ years' experience. Expert in Swift, Xcode, and tech writing.