Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Win a Let's visionOS 2024 conference ticket. Join for free

MainActor usage in Swift explained to dispatch to the main thread

MainActor is a new attribute introduced in Swift 5.5 as a global actor providing an executor which performs its tasks on the main thread. When building apps, it’s essential to perform UI updating tasks on the main thread, which can sometimes be challenging when using several background threads. Using the @MainActor attribute will help ensure your UI is always updated on the main thread.

If you’re new to Actors in Swift, I recommend reading my article Actors in Swift: how to use and prevent data races. Global actors act similar to actors, and I won’t go into much detail on how actors work in this post.

What is a MainActor?

A MainActor is a globally unique actor who performs his tasks on the main thread. You can use it for properties, methods, instances, and closures to perform tasks on the main thread. Proposal SE-0316 Global Actors introduced the main actor as an example of a global actor, inheriting the GlobalActor protocol.

Understanding Global Actors

Before we dive into how to use the MainActor in your code, it’s important to understand the concept of global actors. You can see Global Actors as singletons: only one instance exists. We can define a global actor as follows:

@globalActor
actor SwiftLeeActor {
    static let shared = SwiftLeeActor()
}

The shared property is a requirement of the GlobalActor protocol and ensures having a globally unique actor instance. Once defined, you can use the global actor throughout your project, just like you would with other actors:

@SwiftLeeActor
final class SwiftLeeFetcher {
    // ..
}

Anywhere you use the global actor attribute, you’ll ensure synchronization through the shared actor instance to ensure mutually exclusive access to declarations. The outcome is similar to actors in general, as explained in Actors in Swift: how to use and prevent data races.

The underlying @MainActor implementation is similar to our custom-defined @SwiftLeeActor:

@globalActor
final actor MainActor: GlobalActor {
    static let shared: MainActor
}

It’s available by default and defined inside the concurrency framework. In other words, you can start using this global actor immediately and mark your code to be executed on the main thread by synchronizing via this global actor.

How to use MainActor in Swift?

You can use a global actor with properties, methods, closures, and instances. For example, we could add the @MainActor attribute to a view model to perform all tasks on the main thread:

@MainActor
final class HomeViewModel {
    // ..
}

Using nonisolated, we ensure methods without the main thread requirement perform as fast as possible by not waiting for the main thread to become available. You can only annotate a class with a global actor if it has no superclass, the superclass is annotated with the same global actor, or the superclass is NSObject. A subclass of a global-actor-annotated class must be isolated to the same global actor.

In other cases, we might want to define individual properties with a global actor:

final class HomeViewModel {
    
    @MainActor var images: [UIImage] = []

}

Marking the images property with the @MainActor property ensures that it can only be updated from the main thread:

The MainActor attribute requirements are enforced by the compiler.
The compiler enforces the MainActor attribute requirements.

You can mark individual methods with the attribute as well:

@MainActor func updateViews() {
    // Perform UI updates..
}

And you can even mark closures to perform on the main thread:

func updateData(completion: @MainActor @escaping () -> ()) {
    Task {
        await someHeavyBackgroundOperation()
        await completion()
    }
}

Although in this case, you should rewrite the updateData method to an async variant without needing a completion closure.

Using the main actor directly

The MainActor in Swift comes with an extension to use the actor directly:

extension MainActor {

    /// Execute the given body closure on the main actor.
    public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}

This allows us to use the MainActor directly from within methods, even if we didn’t define any of its body using the global actor attribute:

Task {
    await someHeavyBackgroundOperation()
    await MainActor.run {
        // Perform UI updates
    }
}

In other words, there’s no need to use DispatchQueue.main.async anymore. However, I do recommend using the global attribute to restrict any access to the main thread. Without the global actor attribute, anyone could forget to use MainActor.run, potentially leading to UI updates taking place on a background thread.

When should I use the MainActor attribute?

Before Swift 5.5, you might have defined many dispatch statements to ensure tasks are running on the main thread. An example could look as follows:

func fetchImage(for url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        guard let data, let image = UIImage(data: data) else {
            DispatchQueue.main.async {
                completion(.failure(ImageFetchingError.imageDecodingFailed))
            }
            return
        }

        DispatchQueue.main.async {
            completion(.success(image))
        }
    }.resume()
}

In the above example, you’re sure a dispatch is needed to return the image to the main thread. We have to perform dispatches in several places, resulting in code clutter with several closures.

Sometimes, we might even dispatch to the main queue while already on the main thread. Such a case would result in an extra dispatch that you could’ve skipped. By rewriting your code to use async/await and the main actor, you allow optimizations only to dispatch if needed.

In those cases, isolating properties, methods, instances, or closures to the main actor ensures tasks perform on the main thread. Ideally, we would rewrite the above example as follows:

@MainActor
func fetchImage(for url: URL) async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: url)
    guard let image = UIImage(data: data) else {
        throw ImageFetchingError.imageDecodingFailed
    }
    return image
}

The @MainActor attribute ensures the logic executes on the main thread while the network request is still performed on the background queue. Dispatching to the main actor only takes place if needed to ensure the best performance possible.

Continuing your journey into Swift Concurrency

The concurrency changes are more than just actors and include other features that you can benefit from in your code. So, while at it, why not dive into other concurrency features?

Conclusion

Global actors are a great addition to actors in Swift. It allows us to reuse common actors like @MainActor and perform UI tasks in a performant manner, as the compiler can optimize our code internally. You can use Global actors on properties, methods, instances, and closures, to ensure synchronized access.

If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!

 

Do you know everything about Concurrency?

You don't need to master everything, but staying informed is crucial. Join our community of 17,905 developers and stay ahead of the curve:


Featured SwiftLee Jobs

Find your next Swift career step at world-class companies with impressive apps by joining the SwiftLee Talent Collective. I'll match engineers in my collective with exciting app development companies. SwiftLee Jobs