Post

Access MainActor Synchronously Safely

Use with caution: Avoid accessing MainActor synchronously unless absolutely necessary.

In rare cases—particularly when integrating with legacy code—you may need to synchronously access MainActor-isolated code. While this approach should be avoided when possible, Swift does provide a safe mechanism for doing so when absolutely necessary.

Here’s how you can accomplish that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
extension MainActor {

    /// Executes a closure synchronously on the `MainActor`.
    ///
    /// This method allows synchronous access to `MainActor`-isolated code.
    ///
    /// - Parameter operation: A closure that runs on the `MainActor`.
    /// - Returns: The value returned by `operation`.
    /// - Throws: Rethrows any error thrown by `operation`.
    ///
    /// - Warning: Synchronous access to actors should be avoided when possible.
    ///   This is intended for exceptional cases, such as bridging with legacy APIs.
    public static func sync<R: Sendable>(operation: @MainActor () throws -> R) rethrows -> R {
        if Thread.isMainThread {
            // This is valid as per SE-0424:
            // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0424-custom-isolation-checking-for-serialexecutor.md
            //
            // Being able to assert isolation for non-task code this way is important enough
            // that the Swift runtime actually already has a special case for it:
            // even if the current thread is not running a task, isolation checking will succeed
            // if the target actor is the MainActor and the current thread is the main thread.
            try MainActor.assumeIsolated(operation)
        } else {
            // As per https://developer.apple.com/documentation/swift/mainactor
            // MainActor is a singleton actor whose executor is equivalent to the main dispatch queue.
            // The compiler would also yell at you if you use non-main queue.
            // Therefore, this code is valid.
            //
            // - Note: You cannot do `DispatchQueue.main.sync` if you're already on main thread,
            // because that will cause a deadlock. Hence, the `isMainThread` check.
            try DispatchQueue.main.sync(execute: operation)
        }
    }
}
This post is licensed under CC BY 4.0 by the author.