Skip to main content

Swift MainActor

Introduction to MainActor

In Swift's concurrency model, code can run on different threads simultaneously. While this improves performance, it creates challenges when updating the user interface (UI), which must be done on the main thread. The MainActor is Swift's solution to this problem, providing a safe and convenient way to ensure UI updates happen on the main thread.

The MainActor is a global actor that represents the main thread of your application. It was introduced as part of Swift's concurrency features to help developers write safer concurrent code, particularly when dealing with UI updates.

Understanding the Main Thread Problem

Before we dive into MainActor, let's understand why we need it in the first place.

In iOS and macOS development, all UI updates must occur on the main thread. If you try to update UI elements from a background thread, you might encounter unpredictable behavior, crashes, or visual glitches.

Traditionally, developers handled this with code like:

swift
DispatchQueue.main.async {
// Update UI here
self.label.text = "Downloaded"
self.progressView.isHidden = true
}

This approach works but can lead to scattered code and is easy to forget. This is where MainActor comes in as an elegant solution.

What is MainActor?

MainActor is a global actor that ensures certain code runs on the main thread. It's part of Swift's actor system, which helps manage shared mutable state in concurrent programs.

Think of MainActor as a special zone where code is guaranteed to run on the main thread. You can mark functions, properties, and even entire classes with @MainActor to ensure they always execute on the main thread.

Using MainActor in Swift

Basic MainActor Usage

Here's how to use MainActor to ensure a function runs on the main thread:

swift
@MainActor
func updateUI() {
label.text = "Data Loaded"
progressIndicator.stopAnimating()
}

// To call this from an async context
await updateUI()

In this example, the updateUI function is guaranteed to run on the main thread, making it safe for UI updates.

MainActor with Isolated Properties

You can also mark individual properties with @MainActor:

swift
class ViewModel {
@MainActor var isLoading: Bool = false

func startLoading() async {
// Access to isLoading needs to happen on the main thread
await MainActor.run { isLoading = true }

// Or alternatively:
// await setIsLoading(true)
}

@MainActor
func setIsLoading(_ value: Bool) {
isLoading = value
}
}

MainActor for an Entire Class

For view models or controllers that primarily work with UI, you can mark the entire class with @MainActor:

swift
@MainActor
class ProfileViewController: UIViewController {
private let nameLabel = UILabel()
private let imageView = UIImageView()

func updateProfile(with user: User) {
// This whole function runs on the main thread
nameLabel.text = user.name
imageView.image = user.avatar
}

func fetchAndDisplayProfile() async {
do {
let user = try await userService.fetchCurrentUser()
// Since this class is marked with @MainActor,
// UI updates automatically happen on the main thread
updateProfile(with: user)
} catch {
displayError(error)
}
}
}

MainActor vs. DispatchQueue.main

While MainActor and DispatchQueue.main both help execute code on the main thread, they differ in important ways:

  1. Integration with Swift Concurrency: MainActor is designed to work seamlessly with Swift's async/await system.

  2. Compile-time Safety: The compiler helps enforce MainActor isolation, catching potential threading issues at compile time.

  3. Clarity and Intent: Using @MainActor clearly communicates that a function or property is meant to be accessed on the main thread.

Here's a comparison:

swift
// Traditional approach
func updateUITraditional(with text: String) {
DispatchQueue.main.async {
self.label.text = text
}
}

// MainActor approach
@MainActor
func updateUIWithActor(with text: String) {
label.text = text
}

// Usage
// Traditional
updateUITraditional(with: "Hello")

// MainActor - must be called from async context
Task {
await updateUIWithActor(with: "Hello")
}

Practical Example: A Weather App

Let's look at a more complete example of how MainActor can be used in a weather app:

swift
// Service to fetch weather data
class WeatherService {
func fetchWeather(for city: String) async throws -> WeatherData {
let url = URL(string: "https://api.weather.com/\(city)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(WeatherData.self, from: data)
}
}

// View model that handles UI state
@MainActor
class WeatherViewModel {
// These properties are automatically main-actor-isolated
var temperature: String = "--°"
var condition: String = "Unknown"
var isLoading = false
var errorMessage: String?

private let weatherService = WeatherService()

func loadWeather(for city: String) async {
isLoading = true
errorMessage = nil

do {
// This network call happens on a background thread
let weatherData = try await weatherService.fetchWeather(for: city)

// But these UI updates automatically happen on the main thread
// because the class is marked with @MainActor
temperature = "\(weatherData.temperature)°"
condition = weatherData.condition
} catch {
errorMessage = "Failed to load weather: \(error.localizedDescription)"
}

isLoading = false
}
}

// Using the view model in a SwiftUI view
struct WeatherView: View {
@StateObject private var viewModel = WeatherViewModel()
@State private var cityName = "New York"

var body: some View {
VStack {
TextField("City", text: $cityName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()

Button("Get Weather") {
Task {
await viewModel.loadWeather(for: cityName)
}
}

if viewModel.isLoading {
ProgressView()
} else {
Text(viewModel.temperature)
.font(.largeTitle)
Text(viewModel.condition)
.font(.title)
}

if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
}
}
.padding()
}
}

In this example:

  1. Our WeatherViewModel is marked with @MainActor, ensuring all its properties and methods run on the main thread.
  2. The network request in weatherService.fetchWeather runs on a background thread.
  3. When the data comes back, the UI updates automatically happen on the main thread.
  4. No explicit DispatchQueue.main.async calls are needed.

Advanced MainActor Concepts

Switching to MainActor Context

Sometimes you need to run specific code on the main thread from a non-main-actor context:

swift
func processData() async {
let data = await fetchData() // This runs on a background thread

// Switch to main thread for UI updates
await MainActor.run {
updateUI(with: data)
}
}

Nonisolated Functions within MainActor Classes

If you have a method in a @MainActor-marked class that doesn't need to run on the main thread, you can opt out:

swift
@MainActor
class ImageProcessor {
var processedImage: UIImage?

// This does NOT need the main thread
nonisolated func heavyImageProcessing(image: UIImage) -> UIImage {
// Complex processing that can happen on any thread
return processedImage
}

// This updates UI so it runs on the main thread
func displayProcessedImage() {
guard let image = processedImage else { return }
imageView.image = image
}
}

MainActor and Task Priorities

You can combine MainActor with task priorities for better control:

swift
func refreshData() async {
await MainActor.run {
startLoadingIndicator()
}

async let highPriorityData = Task.detached(priority: .high) {
try? await fetchCriticalData()
}

async let lowPriorityData = Task.detached(priority: .low) {
try? await fetchOptionalData()
}

let (critical, optional) = await (highPriorityData.value, lowPriorityData.value)

await MainActor.run {
stopLoadingIndicator()
updateUI(critical: critical, optional: optional)
}
}

Common Pitfalls and Best Practices

Avoiding Deadlocks

Be careful with nested MainActor calls, which might lead to deadlocks:

swift
// This could potentially lead to a deadlock
@MainActor
func problematicFunction() async {
await anotherMainActorFunction() // Already on main thread, waiting for main thread
}

Performance Considerations

Remember that the main thread is crucial for UI responsiveness. Keep heavy work off the main thread:

swift
@MainActor
class GoodViewModel {
var results: [Result] = []

func processDataAndUpdate() async {
// Do heavy work on background thread
let processedData = await processDataInBackground()

// Update UI on main thread
results = processedData
}

// Not isolated to MainActor, can run on any thread
nonisolated func processDataInBackground() async -> [Result] {
// Heavy computation here
return computeResults()
}
}

Testing with MainActor

When testing code that uses MainActor, ensure your tests properly handle the main actor context:

swift
func testViewModel() async {
let viewModel = ViewModel()
// Test runs on TestActor, so we need to switch to MainActor
await MainActor.run {
viewModel.setupInitialState()
}

// Perform background operations
await viewModel.fetchData()

// Verify UI state on MainActor
await MainActor.run {
XCTAssertEqual(viewModel.items.count, 5)
}
}

Summary

The MainActor in Swift provides an elegant solution to a common problem in concurrent programming: ensuring UI updates happen on the main thread. By leveraging Swift's actor system, MainActor makes it easier to write safe concurrent code without littering your codebase with DispatchQueue.main.async calls.

Key takeaways:

  • MainActor ensures code runs on the main thread, making it safe for UI updates
  • You can mark entire classes, individual functions, or properties with @MainActor
  • MainActor integrates seamlessly with Swift's async/await concurrency system
  • It provides compile-time safety for main-thread operations
  • For code that doesn't need the main thread within a @MainActor type, use nonisolated

With MainActor, Swift concurrency becomes more accessible and safer, especially for UI code.

Additional Resources

Exercises

  1. Create a simple app that downloads an image in the background and displays it on the main thread using MainActor.
  2. Refactor an existing app that uses DispatchQueue.main.async to use MainActor instead.
  3. Create a view model that performs heavy data processing in the background while keeping the UI updated on the main thread.
  4. Experiment with marking an entire class as @MainActor and then adding nonisolated methods for background work.
  5. Write tests for a class that uses MainActor to ensure thread safety.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)