What is the correct way to call UIApplication.shared.beginBackgroundTask no on the main thread?

This is my newbie implementation for beginBackgroundTask


@MainActor
class BackgroundTaskManager {
    var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid

    func startBackgroundTask()  {
        backgroundTaskID = UIApplication.shared.beginBackgroundTask {
            print("Background task about to expire with \(UIApplication.shared.backgroundTimeRemaining)")
            UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
        }
        print("Started background task with \(UIApplication.shared.backgroundTimeRemaining)")
    }
    
    func endBackgroundTask() {
        UIApplication.shared.endBackgroundTask(backgroundTaskID)
    }
}

When I search online, I see that apple has commented on their forum that these methods in fact does not need to be called from the main thread. But if I were to remove @MainActor and just make this class an actor instead, I get these warnings:

Main actor-isolated class property 'shared' can not be referenced on a nonisolated actor instance; this is an error in the Swift 6 language mode

Actor-isolated property 'backgroundTaskID' can not be referenced from the main actor; this is an error in the Swift 6 language mode

Ideally, I dont want to isolate to main, as that's an extra dependencies for my background jobs.

You can make both methods async and get instance of the UIApplication:

func startBackgroundTask() async  {
    let app = await UIApplication.shared
    backgroundTaskID = app.beginBackgroundTask { /* ... */ }
}

Some of APIs in UIKit and related parts are questionable at the moment (another example is UIDevice which got a few topics here as well). And this is really a bit of a complication here to just call an API from the background. I think over time this will get an improvement, but right now you can use a workaround.

It's just a difficulties bridging old (largely ObjC still I guess) systems. I don't think it is anything to blame at all, just a transition case.

1 Like

What's that?

A kind of a mess created by good intentions. Not sure if the term already exists, or I just coined it. the tm mark was intended as a joke.

nomnom wrote:

I see that apple has commented on their forum that these
methods in fact does not need to be called from the main
thread.

It’s more official than that. In the Objective-C header these methods are decorated with NS_SWIFT_NONISOLATED, which is why you’re able to call them from non-main-actor contexts.

vns wrote:

You can make both methods async and get instance of the
UIApplication:

That’ll work, but it requires bouncing to the main thread, which undermines the purpose of making this API thread safe.

I kinda like this approach:

actor BackgroundTaskManager {

    @MainActor
    init() {
        self.app = UIApplication.shared
    }
    
    let app: UIApplication
    
    func runJob(name: String) {
        let t = self.app.beginBackgroundTask(withName: name) {
            … handle expiration …
        }
        defer {
            if t != .invalid {
                self.app.endBackgroundTask(t)
            }
        }
        … do something …
    }
}

Of course the expiry handler is main actor isolated, courtesy of the NS_SWIFT_UI_ACTOR decoration in the header, which is another tricky complication )-:

Honestly, I’m not a fan of these APIs [1] and I encourage you to file an enhancement request for a better option.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] Although I’m even less of a fan of the ProcessInfo API that app extensions have to use. I talk about this in UIApplication Background Task Notes.

1 Like

That’s a clever way to handle this, thanks for sharing! Might be useful in other cases as well.

1 Like