I’m looking for an authoritative answer on how BGAppRefreshTask behaves after a user force-quits an app (swipes it away in the App Switcher).
My app relies on early-morning background refresh to prepare and schedule notifications based on user-defined thresholds and weather forecasts.
Behavior across devices seems inconsistent, however: sometimes a scheduled background refresh still runs, and other times it appears completely blocked.
Apple’s documentation doesn’t clearly state what should happen, and developer discussions conflict.
Could someone from Apple please clarify:
Will a previously scheduled BGAppRefreshTask run after the user force-quits the app?
If not, is there a recommended alternative for time-sensitive updates that must schedule user alerts?
What is the expected system behavior regarding the predictability of background refresh after a force-quit?
A definitive answer would help ensure the app aligns with intended system behavior.
Thanks!
Processes & Concurrency
RSS for tagDiscover how the operating system manages multiple applications and processes simultaneously, ensuring smooth multitasking performance.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi everyone, could you help us?
We implemented a Flutter library that basically makes a call every x minutes if the app is in the background, but when I generate the version via TestFlight for testing, it doesn't work.
Can you help us understand why?
Below is a more detailed technical description.
Apple Developer Technical Support Request
Subject: BGTaskScheduler / Background Tasks Not Executing in TestFlight - Flutter App with workmanager Plugin
Issue Summary
Background tasks scheduled using BGTaskScheduler are not executing when the app is distributed via TestFlight. The same implementation works correctly when running the app locally via USB/Xcode debugging.
We are developing a Flutter application that needs to perform periodic API calls when the app is in the background. We have followed all documentation and implemented the required configurations, but background tasks are not being executed in the TestFlight build.
App Information
Field
Value
App Version
3.1.15 (Build 311)
iOS Minimum Deployment Target
iOS 15.0
Framework
Flutter
Flutter SDK Version
^3.7.2
Technical Environment
Flutter Dependencies (Background Task Related)
Package
Version
Purpose
workmanager
^0.9.0+3
Main background task scheduler (uses BGTaskScheduler on iOS 13+)
flutter_background_service
^5.0.5
Background service management
flutter_background_service_android
^6.2.4
Android-specific background service
flutter_local_notifications
^19.4.2
Local notifications for background alerts
timezone
^0.10.0
Timezone support for scheduling
Other Relevant Flutter Dependencies
Package
Version
firebase_core
4.0.0
firebase_messaging
(via native Podfile)
sfmc (Salesforce Marketing Cloud)
^9.0.0
geolocator
^14.0.0
permission_handler
^12.0.0+1
Info.plist Configuration
We have added the following configurations to Info.plist:
UIBackgroundModes
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>remote-notification</string>
<string>processing</string>
</array>
### BGTaskSchedulerPermittedIdentifiers
```xml
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>br.com.unidas.apprac.ios.workmanager.carrinho_api_task</string>
<string>br.com.unidas.apprac.ios.workmanager</string>
<string>be.tramckrijter.workmanager.BackgroundTask</string>
</array>
**Note:** We included multiple identifier formats as recommended by the `workmanager` Flutter plugin documentation:
1. `{bundleId}.ios.workmanager.{taskName}` - Custom task identifier
2. `{bundleId}.ios.workmanager` - Default workmanager identifier
3. `be.tramckrijter.workmanager.BackgroundTask` - Plugin's default identifier (as per plugin documentation)
## AppDelegate.swift Configuration
We have configured the `AppDelegate.swift` with the following background processing setup:
```swift
// In application(_:didFinishLaunchingWithOptions:)
// Configuration to enable background processing via WorkManager
// The "processing" mode in UIBackgroundModes allows WorkManager to use BGTaskScheduler (iOS 13+)
// This is required to execute scheduled tasks in background (e.g., API calls)
// Note: User still needs to have Background App Refresh enabled in iOS settings
if UIApplication.shared.backgroundRefreshStatus == .available {
// Allows iOS system to schedule background tasks with minimum interval
UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
}
## WorkManager Implementation (Dart/Flutter)
### Initialization
```dart
/// Initializes WorkManager
static Future<void> initialize() async {
await Workmanager().initialize(callbackDispatcher, isInDebugMode: false);
print('WorkManagerService: WorkManager initialized');
}
### Task Registration
/// Schedules API execution after a specific delay
## Observed Behavior
### Works (Debug/USB Connection)
- When running the app via Xcode/USB debugging
- Background tasks are scheduled and executed as expected
- API calls are made successfully when the app is backgrounded
### Does NOT Work (TestFlight)
- When the app is distributed via TestFlight
- Background tasks appear to be scheduled (no errors in code)
- Tasks are **never executed** when the app is in background
- We have tested with:
- Background App Refresh enabled in iOS Settings
- App used frequently
- Device connected to WiFi and charging
- Waited for extended periods (hours)
## Possible heart points
1. **Are there any additional configurations required for `BGTaskScheduler` to work in TestFlight/Production builds that are not required for debug builds?**
2. **Is the identifier format correct?** We are using:
`br.com.unidas.apprac.ios.workmanager.carrinho_api_task`
- Should it match exactly with the task name registered in code?
3. **Are there any known issues with Flutter's `workmanager` plugin and iOS BGTaskScheduler in production environments?**
4. **Is there any way to verify through logs or system diagnostics if the background tasks are being rejected by the system?**
5. **Could there be any conflict between our other background modes (`location`, `remote-notification`) and `processing`?**
6. **Does the Salesforce Marketing Cloud SDK (SFMC) interfere with BGTaskScheduler operations?**
## Additional Context
- We have verified that `Background App Refresh` is enabled for our app in iOS Settings
- The app has proper entitlements for push notifications and location services
- Firebase, SFMC (Salesforce Marketing Cloud), and other SDKs are properly configured
- The issue is **only** present in TestFlight builds, not in debug/USB-connected builds
## References
- [Apple Documentation - BGTaskScheduler](https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler)
- [Apple Documentation - Choosing Background Strategies](https://developer.apple.com/documentation/backgroundtasks/choosing_background_strategies_for_your_app)
Thank you
Topic:
App & System Services
SubTopic:
Processes & Concurrency
It looks like ExtensionKit (and ExtensionFoundation) is fully available on iOS 26 but there is no mention about this in WWDC.
From my testing, it seems as of beta 1, ExtensionKit allows the app from one dev team to launch extension provided by another dev team. Before we start building on this, can someone from Apple help confirm this is the intentional behavior and not just beta 1 thing?
Hello! We are in the progress of migrating a large Swift 5.10 legacy code base over to use Swift 6.0 with Strict Concurrency checking.
We have already stumbled across a few weird edge cases where the "guaranteed" @MainActor isolation is violated (such as with @objc #selector methods used with NotificationCenter).
However, we recently found a new scenario where our app crashes accessing main actor isolated state on a background thread, and it was surprising that the compiler couldn't warn us.
Minimal reproducible example:
class ViewController: UIViewController {
var isolatedStateString = "Some main actor isolated state"
override func viewDidLoad() {
exampleMethod()
}
/// Note: A `@MainActor` isolated method in a `@MainActor` isolated class.
func exampleMethod() {
testAsyncMethod() { [weak self] in
// !!! Crash !!!
MainActor.assertIsolated()
// This callback inherits @MainActor from the class definition, but it is called on a background thread.
// It is an error to mutate main actor isolated state off the main thread...
self?.isolatedStateString = "Let me mutate my isolated state"
}
}
func testAsyncMethod(completionHandler: (@escaping () -> Void)) {
let group = DispatchGroup()
let queue = DispatchQueue.global()
// The compiler is totally fine with calling this on a background thread.
group.notify(queue: queue) {
completionHandler()
}
// The below code at least gives us a compiler warning to add `@Sendable` to our closure argument, which is helpful.
// DispatchQueue.global().async {
// completionHandler()
// }
}
}
The problem:
In the above code, the completionHandler implementation inherits main actor isolation from the UIViewController class.
However, when we call exampleMethod(), we crash because the completionHandler is called on a background thread via the DispatchGroup.notify(queue:).
If were to instead use DispatchQueue.global().async (snippet at the bottom of the sample), the compiler helpfully warns us that completionHandler must be Sendable.
Unfortunately, DispatchGroup's notify gives us no such compiler warnings. Thus, we crash at runtime.
So my questions are:
Why can't the compiler warn us about a potential problem with DispatchGroup().notify(queue:) like it can with DispatchQueue.global().async?
How can we address this problem in a holistic way in our app, as it's a very simple mistake to make (with very bad consequences) while we migrate off GCD?
I'm sure the broader answer here is "don't mix GCD and Concurrency", but unfortunately that's a little unavoidable as we migrate our large legacy code base! 🙂
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
...
}
2025-02-23_19-53-50.2294_+0000-876d2b8ec083447af883961da90398f00562f781.crash
We are experiencing a crash in our application that only occurs on devices running iOS beta 26. It looks like a Beta problem.
The crash appears to be caused by an excessive number of open File Descriptors.
We identified this after noticing a series of crashes in different parts of the code each time the app was launched. Sometimes it would crash right at the beginning, when trying to load the Firebase plist file.
That’s when we noticed a log message saying “too many open files,” and upon further investigation, we found that an excessive number of File Descriptors were open in our app, right after the didFinishLaunching method of the AppDelegate.
We used the native Darwin library to log information about the FDs and collected the following:
func logFDs() {
var rlim = rlimit()
if getrlimit(RLIMIT_NOFILE, &rlim) == 0 {
print("FD LIMIT: soft: \(rlim.rlim_cur), hard: \(rlim.rlim_max)")
}
// Count open FDs before Firebase
let openFDsBefore = countOpenFileDescriptors()
print("Open file descriptors BEFORE Firebase.configure(): \(openFDsBefore)")
}
private func countOpenFileDescriptors() -> Int {
var count = 0
let maxFD = getdtablesize()
for fd in 0..<maxFD {
if fcntl(fd, F_GETFD) != -1 {
count += 1
}
}
return count
}
With this code, we obtained the following data:
On a device with iOS 26 Beta 1, 2, or 3:
FD LIMIT: soft: 256, hard: 9223372036854775807
Open file descriptors BEFORE Firebase.configure(): 256
On a device with iOS 18:
FD LIMIT: soft: 256, hard: 9223372036854775807
Open file descriptors BEFORE Firebase.configure(): 57
In the case of the device running iOS 26 beta, the app crashes when executing Firebase.configure() because it cannot open the plist file, even though it can be found at the correct path — meaning the OS locates it.
To confirm this was indeed the issue, we used the following code to close FDs before proceeding with Firebase configuration. By placing a breakpoint just before Firebase.configure() and running the following LLDB command:
expr -l c -- for (int fd = 180; fd < 256; fd++) { (int)close(fd); }
This released the FDs, allowing Firebase to proceed with its configuration as expected. However, the app would later crash again after hitting the soft limit of file descriptors once more.
Digging deeper, we used this code to try to identify which FDs were being opened and causing the soft limit to be exceeded:
func checkFDPath() {
var r = rlimit()
if getrlimit(RLIMIT_NOFILE, &r) == 0 {
print("FD LIMIT: soft: \(r.rlim_cur), hard: \(r.rlim_max)")
for fd in 0..<Int32(r.rlim_cur) {
var path = [CChar](repeating: 0, count: Int(PATH_MAX))
if fcntl(fd, F_GETPATH, &path) != -1 {
print(String(cString: path))
}
}
}
}
We ran this command at the very beginning of the didFinishLaunching method in the AppDelegate.
On iOS 26, the log repeatedly showed Cryptexes creating a massive number of FDs, such as:
/dev/null
/dev/ttys000
/dev/ttys000
/private/var/mobile/Containers/Data/Application/AEE414F2-7D6F-44DF-A6D9-92EDD1D2B014/Library/Application Support/DTX_8.191.1.1003.sqlite
/private/var/mobile/Containers/Data/Application/AEE414F2-7D6F-44DF-A6D9-92EDD1D2B014/Library/Caches/KSCrash/MyAppScheme/Data/ConsoleLog.txt
/private/var/mobile/Containers/Data/Application/AEE414F2-7D6F-44DF-A6D9-92EDD1D2B014/Library/HTTPStorages/mybundleId/httpstorages.sqlite
/private/var/mobile/Containers/Data/Application/AEE414F2-7D6F-44DF-A6D9-92EDD1D2B014/Library/HTTPStorages/mybundleId/httpstorages.sqlite-wal
/private/var/mobile/Containers/Data/Application/AEE414F2-7D6F-44DF-A6D9-92EDD1D2B014/Library/HTTPStorages/mybundleId/httpstorages.sqlite-shm
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.01
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.11
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.12
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.13
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.14
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.15
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.16
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.17
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.18
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.19
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.20
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.21
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.22
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.23
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.24
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.25
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.26
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.29
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.30
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.31
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.32
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.36
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.37
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.38
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.39
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.40
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e
…
This repeats itself a lot of times.
…
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.36
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.37
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.38
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.39
/private/preboot/Cryptexes/OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e.40