BGTaskScheduler crashes on iOS 18.4

I've been seeing a high number of BGTaskScheduler related crashes, all of them coming from iOS 18.4. I've encountered this myself once on launch upon installing my app, but haven't been able to reproduce it since, even after doing multiple relaunches and reinstalls. Crash report attached at the bottom of this post.

I am not even able to symbolicate the reports despite having the archive on my MacBook:

Does anyone know if this is an iOS 18.4 bug or am I doing something wrong when scheduling the task? Below is my code for scheduling the background task on the view that appears when my app launches:

.onChange(of: scenePhase) { newPhase in
    if newPhase == .active {
        #if !os(macOS)
        let request = BGAppRefreshTaskRequest(identifier: "notifications")
        request.earliestBeginDate = Calendar.current.date(byAdding: .hour, value: 3, to: Date())
        do {
            try BGTaskScheduler.shared.submit(request)
            Logger.notifications.log("Background task scheduled. Earliest begin date: \(request.earliestBeginDate?.description ?? "nil", privacy: .public)")
        } catch let error {
            // print("Scheduling Error \(error.localizedDescription)")
            Logger.notifications.error("Error scheduling background task: \(error.localizedDescription, privacy: .public)")
        }
        #endif
...
}

Answered by DTS Engineer in 826562022

Submitted a bug report: FB16595418

Looking the data over, I think this is bug on our side, as the crash is actually coming from SwiftUI's background task integration, not your own code. It's possible there is a timing issue between your usage and SwiftUI, but that would still mean that SwiftUI changed "something" that altered the timing of activity.

When do you call "BGTaskScheduler.register(forTaskWithIdentifier:using:launchHandler:)"? That's the one behavior you have control over which could be a factor in this crash.

Having said all that, please replicate the issue a few time, collect a sysdiagnose, upload it to your bug, and then let me know here when all of that is done. The sysdiagnose should clarify exactly what's going wrong.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Our engineering teams need to investigate this issue, as this might indicate an issue with iOS 18.4 which is still in Beta.

We'd greatly appreciate it if you could open a bug report, include crash logs and sample code or models that reproduce the issue, and post the FB number here once you do.

Bug Reporting: How and Why? has tips on creating a successful bug report.

Same here. Happens only on iOS 18.4

Submitted a bug report: FB16595418

Same. Submitted logs in FB16610879

Submitted a bug report: FB16595418

Looking the data over, I think this is bug on our side, as the crash is actually coming from SwiftUI's background task integration, not your own code. It's possible there is a timing issue between your usage and SwiftUI, but that would still mean that SwiftUI changed "something" that altered the timing of activity.

When do you call "BGTaskScheduler.register(forTaskWithIdentifier:using:launchHandler:)"? That's the one behavior you have control over which could be a factor in this crash.

Having said all that, please replicate the issue a few time, collect a sysdiagnose, upload it to your bug, and then let me know here when all of that is done. The sysdiagnose should clarify exactly what's going wrong.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

There's no usage of BGTaskScheduler.register anywhere in my app. Am I supposed to call this method at some point?

I use the background tasks API in two places currently:

  1. Calling BGTaskScheduler.shared.submit during .onChange(of: scenePhase) when newPhase == .active, on the view that appears when my app is launched. It schedules the background task whenever my app is brought to the foreground.

  2. .backgroundTask(.appRefresh("notifications")) attached to my WindowGroup. I attached the code snippet of this in my initial bug report.

I have just managed to reproduce the crash after quitting and relaunching the app about 100 times. The bug report has been updated with the sysdiagnose and a copy of the crash report.

Also happening here. I use .backgroundTask modifier on WindowGroup. Only an issue with 18.4.

There's no usage of BGTaskScheduler.register anywhere in my app. Am I supposed to call this method at some point?

Yes, very much so. Looking thing over a bit more closely, I think your app may basically have been working by "accident". What you're supposed to do is:

Failing to do so SHOULD crash your app with exactly this crash:

Fatal Exception: NSInternallnconsistencyException
All launch handlers must be registered before application finishes launching

With all that background, there are two more points:

1) Why you didn't crash before

My guess is that the generic "notifications" ID was actually being registered and used by something else in the system.

 let request = BGAppRefreshTaskRequest(identifier: "notifications")
 

Using an short ID like that was a mistake in our part and correcting that mistake would have created the crash you're currently seeing.

2) Your code isn't actually doing anything (sort of).

All this call does:

try BGTaskScheduler.shared.submit(request)

...is tell the system "I'd like to run this work at some point in the future, so let me know when I should run". The way that work is actually supposed to run is through the block you setup in "register(forTaskWithIdentifier:using:launchHandler:)", which we'll call when it's time for your to actually do that work.

The "sort of" part is that it's possible this was working because this is specifically for app refresh. If the old background app refresh delegate was in place, it's likely that it fired as well, masking the issue.

In any case, if you set everything up properly (see above), then I think this crash will go away.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for the response. I am still a bit confused however. My understanding is that the .backgroundTask modifier is the replacement for BGTaskScheduler.register in SwiftUI. And the code that runs during the background task is part of that modifier. As per the WWDC22 video Efficiency awaits: Background tasks in SwiftUI, it is used to register the handler.

Also worth noting that the video uses a short ID similar to what I did.

If I take out the modifier, the app crashes with this message:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'No launch handler registered for task with identifier notifications'

Which indicates that the modifier is indeed used for registering the handler.

There's no difference to this behaviour whether I use the short ID notifications or the reverse DNS com.ip18.SubManager.notifications. If the modifier is not there then the app crashes.

Interestingly when I simply killed and ran my app from Xcode multiple times, I eventually came across the All launch handlers must be registered before application finishes launching crash in one of those launches. Which is probably what the crash reports are about.

Attached a symbolicated crash report from one of those Xcode sessions to FB16595418. If I'm understanding this right, it looks like BGTaskScheduler registerForTaskWithIdentifier is already being called internally by the modifier.

Thread 2 Crashed:
0   libsystem_kernel.dylib        	       0x1ef16f1dc __pthread_kill + 8
1   libsystem_pthread.dylib       	       0x2281a3b40 pthread_kill + 268
2   libsystem_c.dylib             	       0x1a5f9c234 __abort + 132
3   libsystem_c.dylib             	       0x1a5f9c1b0 abort + 136
4   libc++abi.dylib               	       0x2280cd5a0 abort_message + 132
5   libc++abi.dylib               	       0x2280bbf10 demangling_terminate_handler() + 344
6   libobjc.A.dylib               	       0x19b560e48 _objc_terminate() + 156
7   libc++abi.dylib               	       0x2280cc8b4 std::__terminate(void (*)()) + 16
8   libc++abi.dylib               	       0x2280cfe1c __cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 88
9   libc++abi.dylib               	       0x2280cfdc4 __cxa_throw + 92
10  libobjc.A.dylib               	       0x19b55ee74 objc_exception_throw + 448
11  Foundation                    	       0x19d3a6990 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 288
12  BackgroundTasks               	       0x236e9d6ec -[BGTaskScheduler _unsafe_registerForTaskWithIdentifier:usingQueue:launchHandler:] + 448
13  BackgroundTasks               	       0x236e9d4ec -[BGTaskScheduler registerForTaskWithIdentifier:usingQueue:launchHandler:] + 128
14  SwiftUI                       	       0x1a2b77b4c -[BGTaskSchedulerProxy registerForTaskWithIdentifier:launchHandler:] + 220
15  SwiftUI                       	       0x1a34a8028 closure #1 in AppRefreshBackgroundTask.register() + 344
16  SwiftUI                       	       0x1a27cbe15 <deduplicated_symbol> + 1
17  SwiftUI                       	       0x1a293c239 <deduplicated_symbol> + 1
18  SwiftUI                       	       0x1a27ac829 <deduplicated_symbol> + 1
19  libswift_Concurrency.dylib    	       0x1a9adbcc9 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1

My understanding is that the .backgroundTask modifier is the replacement for BGTaskScheduler.register in SwiftUI.

Sigh... yes, I'd forgotten about that API.

If I'm understanding this right, it looks like BGTaskScheduler registerForTaskWithIdentifier is already being called internally by the modifier.

Yes. More specifically, "_unsafe_registerForTaskWithIdentifier" does two different checks when it runs:

  1. It checks how far the app is into the launch process and, if it's to late in that process, it crashes with "All launch handlers must be registered before application finishes launching".

  2. It checks if the ID has already been registered and, if it has, it crashes with "Launch handler for task with identifier <identifier> has already been registered".

...and you're obviously crashing on #1. Moving to here:

Interestingly when I simply killed and ran my app from Xcode multiple times, I eventually came across the All launch handlers must be registered before application finishes launching crash in one of those launches. Which is probably what the crash reports are about.

Based on the evidence at hand, I suspect the issue is that SwiftUI is sometimes registering the crash later than it should, triggering #1. That leads to the most direct solution (assuming you won't want to wait for a bug fix), which would be to stop using ".backgroundTask" and directly use BGTaskScheduler.register.

Also, as a quick clarification here:

.backgroundTask modifier is the replacement for BGTaskScheduler.register in SwiftUI.

Thinking of it as a replacement is somewhat misleading. SwiftUI's architecture is actually trying to provide a unified entry point across multiple frameworks for use cases that are particularly relevant to the interface. It does support BGAppRefreshTask, but it does NOT support the other task types (BGProcessingTask and BGHealthResearchTaskRequest) and, more to the point, all of other task types are totally unrelated to the BackgroundTask framework.

The reason for this is that it isn't actually trying to provide a true replacement. It support BGAppRefreshTask because the primary use case for BGAppRefreshTask is to update the user interface so that it remains current. That's the same reason it DOESN'T support BGProcessingTask. BGProcessingTask is intended to be used for work which requires extended runtime ("several minutes"), which is exactly the kind of work that wouldn't/shouldn't be directly tied to the user interface. Similarly, if your app refresh work is primarily tied to the "backend" of your app, it can be better to use the original register(forTaskWithIdentifier:using:launchHandler:) API instead of artificially creating a connection in SwiftUI that you wouldn't otherwise need.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I have a similar issue. In my case, it crashes when submitting the request ( BGTaskScheduler.shared.submit(request) ) And it's still valid with the last iOS update: 18.4 (22E5216h) Our task identifiers are long and in the reverse-domain style. It seems, that the new bata has some multithreading issue with the task registration.

I have the same issue, it's very intermittent but more users are experiencing this crash.

If we were to use the temporary fix to call BGTaskScheduler.shared.register(forTaskWithIdentifier:) directly, where would we use this in the SwiftUI app lifecycle? Is it in the .init of the App?

Edit: I tried this and it still crashes.

If we were to use the temporary fix to call BGTaskScheduler.shared.register(forTaskWithIdentifier:) directly, where would we use this in the SwiftUI app lifecycle? Is it in the .init of the App?

Theoretically init would probably work, however, my actual recommendation would be that you use UIApplicationDelegateAdaptor and then implement application(_:didFinishLaunchingWithOptions:) and call it there. The core issue here comes down to timing issues within SwiftUI's lifecycle, so I think it's worth being certain that any workaround cannot have similar timing issues.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

@DTS Engineer I ran some tests here that I think maybe highlights what's going on:

  1. I set a symbolic breakpoint on -[BGTaskScheduler registerForTaskWithIdentifier:usingQueue:launchHandler:]
  2. I edited my app scheme to "Wait for the executable to be launched"
  3. I run the app from Xcode.
  4. I trigger the app to be launched from a home screen widget (AudioPlaybackIntent)

Here, on iOS 18.3 and earlier, at the time the app is launched, the symbolic breakpoint is triggered. On 18.4 beta, the symbolic breakpoint is not triggered.

  1. Now, foreground the app. On iOS 18.4 beta the symbolic breakpoint is triggered now, but because the app was already launched (in the background) the assert kicks in.

I hope you guys are already on track to fix this issue (but it's worrying that we've now seen 4 betas without a fix). I don't think widgets are the only way to trigger this issue. I've seen it happen on the first launch right after installing a new build from TestFlight. Maybe app pre-warming could be another path to trigger the launch without tasks getting registered, but that's just speculation.

__

Jonas Salling

With my app force closed from multitasking I tried running my app's shortcuts and control centre buttons. I was able to make it crash almost every time if the app was swiped away from multitasking.

This however only happens with the .backgroundTask modifier in iOS 18.4. I wasn't able to reproduce the crash with BGTaskScheduler.shared.register(forTaskWithIdentifier:). So my question is, if I move over to using the old way, do I have to make my identifier reverse DNS? Or can I keep using the existing notifications identifier that I already have?

Does anyone know if this is an iOS 18.4 bug

This is a bug on our side but, if you haven't already, please file your own bug on this and post the bug number back here.

This however only happens with the .backgroundTask modifier in iOS 18.4. I wasn't able to reproduce the crash with BGTaskScheduler.shared.register(forTaskWithIdentifier:).

Yes. The problem is caused by SwiftUI calling register far later than it should, not by BGTaskScheduler itself. I'll also note that this is not simply a stability issue, as task cannot be run unless "register" has been called.

So my question is, if I move over to using the old way, do I have to make my identifier reverse DNS?

Please use the reverse DNS notation.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

hi ther i have the same issue,backgroundtask modfier is not works in swiftui;and i use appdelegate then,thre issues is i cannot register the bgtask ,in register closure it not works,but my schedule is successful though it finally that pending tasks is 0 count,means : it tells me scheduled success,but not in fact.idk how to handle backgroundtask now in swiftui,i have tested the ios17runtime,same issure. can u guys fix it as soon as posssible? thank you a lot.

This bug made it into the release candidate. Very disappointed!

Now, to move over to the old way completely I need to switch the task identifier to the reverse DNS. But the problem is:

  • The SwiftUI .backgroundTask modifier can't be there at all otherwise the crash will persist
  • Not having the modifier means the old identifier won't be registered and thus if the old background task runs when the user updates but doesn't open the app, it will crash as the launch handler hasn't been registered

How can I migrate to the old method without causing any crashes?

The SwiftUI .backgroundTask modifier can't be there at all otherwise the crash will persist

No, that's not correct. What complicates the conversation here is we're using the term "background task" for two different APIs which then partially overlap. Those are:

  1. SwiftUIs "backgroundTask" API, which is provides of standard code pattern for a number of unrelated APIs that can run in the background.

  2. The BackgroundTask framework, more specifically BGAppRefreshTask.

The crash here is specifically caused by how SwiftUI is registering BGAppRefreshTask's, however, that also mean that it should only effect "appRefresh", not the other task types.

Not having the modifier means the old identifier won't be registered and thus if the old background task runs when the user updates but doesn't open the app, it will crash as the launch handler hasn't been registered

I don't think this is an issue either. Internally, the BackgroundTask framework has a built in enforcement system which requires:

  1. Your app must declare the identifiers it's going to use in it's Info.plist.

  2. Your app has to register a handler for every identifier it declares before launch completes.

SwiftUI bypasses the first check (which is why you don't have to declare IDs in your Info.plist) which also avoids the second crash.

Note that what's crashing here is slightly different than the check above. The issue isn't that SwiftUI failed to register the handler, it's that it tried to register a handler AFTER launch has finished.

Now, I believe there actually is another bug lurking. Jonas Salling posted this analysis (which is entirely correct) in an earlier post:

  1. I set a symbolic breakpoint on -[BGTaskScheduler registerForTaskWithIdentifier:usingQueue:launchHandler:]
  2. I edited my app scheme to "Wait for the executable to be launched"
  3. I run the app from Xcode.
  4. I trigger the app to be launched from a home screen widget (AudioPlaybackIntent)
  5. Now, foreground the app. On iOS 18.4 beta the symbolic breakpoint is triggered now, but because the app was already launched (in the background) the assert kicks in.

Looking at that event chain, if you were to trigger a app refresh task immediately after 4 (but before the app was foregrounded at #5), I believe one of two things would happen:

  • You'd crash with the same crash this whole thread is about. SwiftUI triggers a similar app initialization sequence as foregrounding, which then crashes for exactly the same reason.

  • Nothing happens. Your task does not fire (because it isn't registered) but you don't crash (because the enforcement mechanism doesn't apply to SwiftUI's usage).

I'm not sure which of those case would apply, but I don't think you'll crash for failing to register.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

So we have to say that .backgroundTask(.appRefresh(...)) {... } can't be there at all and we have to migrate because of this accepted bug. It's still very sad and disapointing!

That seems to be the replacement for .backgroundTask(.appRefresh(...))

func registerAppRefresh(
    _ taskID: String,
    handler: @escaping () async -> ()
) { 
    BGTaskScheduler.shared.register(
        forTaskWithIdentifier: taskID,
        using: nil
    ) { bgTask in
        let task = Task {
            await handler()
            bgTask.setTaskCompleted(success: true)
        }
        bgTask.expirationHandler = {
            task.cancel()
        }
    }
}

// use taskID and action from .backgroundTask(.appRefresh(taskID), action: action)
registerAppRefresh(<taskID>,  action)

That seems to be the replacement for .backgroundTask(.appRefresh(...))

That's most of it, however, that's not the entire solution. That code setup up the code path which will execute when background app refresh occurs, however, it doesn't actually tell the system that you WANT background app refresh to occur. To do that, you need to:

Finally, you have to make sure you register handler "early" in the launch process. Theoretically that could be attached to your scene objects, however, my recommendation would be that you implement a UIApplicationDelegate and call this in applicationDidFinishLaunching. The reason this bug exists at all is because of timing issues in the exact details of scene creation and so I'd want any workaround to be guaranteed to avoid any possibility of failure.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

please reply my pre words: Kevin Elliott DTS Engineer, CoreOS/Hardware

Having just moved from the previous method to the "new and improved" view modifier method, I have to say I am little less than impressed with Apple on this. We have an engineer on this thread. Where is the hold up? Are we supposed to spend time undoing the work we did? How does this reflect on "new and improved" features going forward.

BGTaskScheduler crashes on iOS 18.4
 
 
Q