Unexpected UIApplication behavior under Swift Concurrency

Hello,

Calling UIApplication from a non-main-actor context triggers Main Thread Checker at runtime.

Code sample:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            let someInstance = SomeClass(
                uiApplication: UIApplication.shared,
                someApplication: SomeApplication()
            )
            await someInstance.someMethod()
        }
    }
}

final class SomeClass {
    let uiApplication: UIApplication
    let someApplication: SomeApplication

    init(uiApplication: UIApplication, someApplication: SomeApplication) {
        self.uiApplication = uiApplication
        self.someApplication = someApplication
    }

    func someMethod() async {
        try? await uiApplication.setAlternateIconName("")
        try? await someApplication.setAlternateIconName("")
    }
}

@MainActor class SomeApplication: UIResponder {
    open func setAlternateIconName(_ alternateIconName: String?) async throws {
        precondition(Thread.isMainThread)
    }
}

In this example SomeApplication acts as expected and setAlternateIconName is called from the main actor.
On the other hand, the UIApplication call:

  • Raises MTC warning
  • Raises “value of non-sendable type“ warning for the String parameter with concurrency checking level Complete

Could someone help me to understand why it behaves like this? Thanks

1 Like

Before Xcode 26 / Swift 6.2, your someMethod() async runs on the default executor (it does NOT inherit the @MainActor isolation of the Task), which means setAlternateIconName runs off the main queue. If you compile in Swift 6 mode you'll l likely see some errors from the compiler when you try to do that.

With Xcode 26 / Swift 6.2 and the "Approachable Concurrency" build setting, you can invert that behavior and make async methods with no declared isolation, like someMethod(), be executed on the caller's executor, rather than the default. That would make your current usage safe.

I suggest you start by building in Swift 6 mode so you have the full suite of concurrency tools enabled with maximum compiler checking, then experiment with the newer Swift 6.2 features to see if they help.

1 Like

SomeClass is apparently not isolated to the main actor. Thus, someMethod isn’t either.

If this is all that SomeClass does, you can simply isolate it to the main actor:

@MainActor
class SomeClass {…}

Then its properties and methods will be isolated to the main actor, too.

Needless to say, if you were using Swift 6.2’s “default isolation” build setting of “MainActor`, it would do this for you.

your someMethod() async runs on the default executor (it does NOT inherit the @MainActor isolation of the Task), which means setAlternateIconName runs off the main queue

SomeClass is apparently not isolated to the main actor. Thus, someMethod isn’t either.

Yes, that’s the point, I mentioned it in the description:

Calling UIApplication from a non-main-actor context

Let me try to explain it a bit more.

We have non-isolated SomeClass. And we have two main @MainActor-isolated application classes

// UIKit
@MainActor open class UIApplication : UIResponder { ... }
// Custom
@MainActor class SomeApplication: UIResponder { ... }

What I expect:

When calling @MainActor-annotated class from non-isolated context the execution will switch to the main actor inside setAlternateIconName. And this is how it works for my custom SomeApplication. Precondition always succeeds.

But for UIApplication:

  • @MainActor annotation was ignored (?) and the code was executed from a bg queue
  • Concurrency warning shows “String is not sendable“ warning. It doesn’t make any sense. String IS Sendable.
1 Like

Turn on Swift 6 mode and you'll likely get the appropriate errors. You're running into limitations of the Swift 5 mode with whatever upcoming features you have enabled. What version of Swift are you running?

1 Like

I was testing using Xcode 26 with Swift 5 mode, all upcoming features enabled

Enabling Swift 6 just promotes the warning to an error

Looks to be a region-based isolation bug, I can replicate it in Xcode 26 in a project in Swift 6 mode. Oddly, if you break the String into a separate value, the error changes:

Sending 'string._bridgeToObjectiveC.some' risks causing data races

Perhaps something strange with how the API is imported from Obj-C.

2 Likes

Even more fun:

func someMethod() async {
  nonisolated(unsafe) let string = ""
  // ⚠️ 'nonisolated(unsafe)' is unnecessary for a constant with 'Sendable' type 'String', consider removing it
  try? await uiApplication.setAlternateIconName(string) // 🛑 Sending 'string._bridgeToObjectiveC.some' risks causing data races
  try? await someApplication.setAlternateIconName(string)
}

Really strange.

1 Like

Ultimately this seems to be a bug in the Obj-C importer for the async throws version of setAlternateIconName. Calling the version with the error completion builds fine:

await uiApplication.setAlternateIconName(string, completionHandler: nil)

You could make your own extension with a slightly different name to bring back the throwing version.

1 Like

This seems to be happening to any member of UIApplication when accessed from the default executor, even when explicitly annotated with @MainActor.

Yes, something really strange is happening.

Our main issue is that a @MainActor-annotated class isn’t executing on the main thread. But the warning/error message is also misleading, which doesn’t help.

Both issues may be related to Objective-C - Swift concurrency interoperation. I’ll file a bug report.

2 Likes