Skip to content

Latest commit

 

History

History
191 lines (137 loc) · 10.3 KB

File metadata and controls

191 lines (137 loc) · 10.3 KB

iOS 26+ BGContinuedProcessingTask integration

Availability: iOS 26.0+ / iPadOS 26.0+ Apple reference: Performing long-running tasks on iOS and iPadOS (WWDC25 session 227).

Why this exists

On iOS, the foreground service historically relied on UIApplication.beginBackgroundTask, which gives roughly 30 seconds of execution time after the user backgrounds the app. For user-initiated, long-running work (uploads, exports, on-device ML, downloads), that window is often too short and the task gets suspended mid-flight.

BGContinuedProcessingTask, introduced in iOS 26, is built for exactly this case:

  • The system shows progress UI (with a cancel affordance) on the user's behalf.
  • The task keeps running when the app moves to the background.
  • There is no fixed time limit — instead, the system watches the progress you report and expires the task if progress stalls.

This plugin supports BGContinuedProcessingTask as an opt-in layer on top of the regular foreground service. If you don't configure it, nothing changes. If you do, the plugin submits the task automatically when you call startService and completes it when you call stopService.

Setup checklist

1. Enable the Background processing capability

Open your app target in Xcode → Signing & Capabilities+ Capability → add Background Modes (if not already present) and check Background processing. This writes the background-processing entry into your entitlements file.

2. Declare the task identifier in Info.plist

Add your task identifier to the BGTaskSchedulerPermittedIdentifiers array in ios/Runner/Info.plist (next to any identifier you already use, e.g. for BGAppRefreshTask):

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
  <string>com.pravera.flutter_foreground_task.refresh</string>
  <string>com.example.app.upload</string>
</array>

Wildcards are supported as a suffix when you need per-invocation identifiers:

<string>com.example.app.upload.*</string>

With the above wildcard, identifiers like com.example.app.upload.recording_2025_06_12 are all permitted, so you can submit one continued processing task per user-visible piece of work.

The plugin validates the identifier you pass against this list at submission time and logs (without crashing) if the identifier is not permitted.

3. Configure the Dart side

Pass IOSContinuedProcessingTaskOptions to IOSNotificationOptions when initialising the foreground task:

FlutterForegroundTask.init(
  androidNotificationOptions: AndroidNotificationOptions(
    channelId: 'my_channel',
    channelName: 'Background tasks',
    channelImportance: NotificationChannelImportance.LOW,
    priority: NotificationPriority.LOW,
  ),
  iosNotificationOptions: const IOSNotificationOptions(
    showNotification: true,
    playSound: false,
    continuedProcessingTask: IOSContinuedProcessingTaskOptions(
      identifier: 'com.example.app.upload',
      title: 'Uploading recording',
      subtitle: 'recording_2025_06_12.m4a',
      submissionStrategy: IOSContinuedProcessingSubmissionStrategy.fail,
    ),
  ),
  foregroundTaskOptions: ForegroundTaskOptions(
    eventAction: ForegroundTaskEventAction.repeat(5000),
    allowWakeLock: true,
  ),
);

4. Start the service from a user gesture

Critical: Apple requires BGContinuedProcessingTaskRequest.submit to be invoked in direct response to a user action. Call FlutterForegroundTask.startService(...) synchronously from the button tap or equivalent gesture. Scheduling the call through Future.delayed, Timer, or navigation transitions breaks this contract and the submission will fail.

ElevatedButton(
  onPressed: () async {
    await FlutterForegroundTask.startService(
      notificationTitle: 'Uploading recording',
      notificationText: 'Your recording will finish uploading in the background.',
      callback: startCallback,
    );
  },
  child: const Text('Upload'),
),

5. Report progress from your TaskHandler

The system will expire the task if you stop reporting progress. From inside your TaskHandler, call updateIOSContinuedProcessingTaskProgress as your work advances:

class UploadHandler extends TaskHandler {
  double _progress = 0.0;

  @override
  void onRepeatEvent(DateTime timestamp) async {
    _progress = min(1.0, _progress + 0.05);
    await FlutterForegroundTask.updateIOSContinuedProcessingTaskProgress(
      progress: _progress,
    );

    if (_progress >= 1.0) {
      FlutterForegroundTask.stopService();
    }
  }
}

You can call the method from anywhere in the Dart isolate (it's an async method). Values outside 0.0...1.0 are clamped before being sent across the method channel.

6. Stop the service when the work finishes

Calling FlutterForegroundTask.stopService() invokes setTaskCompleted(success: true) on the continued processing task automatically. You don't need to call anything else.

If the system or the user cancels the task (via the progress UI), the plugin invokes the same service-stop path internally so your TaskHandler.onDestroy is called and the service is torn down cleanly.

API reference

  • IOSNotificationOptions.continuedProcessingTask — nullable; when null the plugin uses the legacy background execution model and all calls below are no-ops.
  • IOSContinuedProcessingTaskOptions(identifier, title, subtitle?, submissionStrategy) — configuration for the submitted task.
  • IOSContinuedProcessingSubmissionStrategyqueue (default) or fail. fail returns an error immediately if the task can't start right away.
  • FlutterForegroundTask.updateIOSContinuedProcessingTaskProgress({required double progress}) — push progress [0.0, 1.0] to the system.
  • FlutterForegroundTask.isIOSContinuedProcessingTaskSupported — future that resolves true on iOS 26+, false on older iOS and on Android.

Xcode version requirement

BGContinuedProcessingTask only exists in the iOS 26 SDK, which ships with Xcode 26+ (Swift 6.2+). If you configure IOSContinuedProcessingTaskOptions but build with an older Xcode, the plugin will raise a fatal error at runtime with a message explaining the mismatch. This is intentional — a silent no-op would make the feature appear broken and be difficult to diagnose.

If you need to support building with both old and new Xcode versions from the same codebase, guard the option behind a runtime check:

iosNotificationOptions: IOSNotificationOptions(
  showNotification: true,
  playSound: false,
  continuedProcessingTask:
      await FlutterForegroundTask.isIOSContinuedProcessingTaskSupported
          ? IOSContinuedProcessingTaskOptions(
              identifier: 'com.example.app.upload',
              title: 'Uploading recording',
            )
          : null,
),

Behavior on unsupported platforms

Platform Behavior
Android Options are ignored. Dart APIs are no-ops.
iOS, built with Xcode < 26 Fatal error if continuedProcessingTask is set. Remove the option or upgrade Xcode.
iOS < 26, built with Xcode 26+ Options are ignored. Dart APIs are no-ops.
iOS 26+, built with Xcode 26+, without options No continued processing task. Dart APIs are no-ops.
iOS 26+, built with Xcode 26+, with options BGContinuedProcessingTask is submitted alongside the service.

On Android and on iOS 26+ built with Xcode 26+ but without options, the plugin falls back to the existing behavior. You cannot safely set continuedProcessingTask unconditionally when building with an older Xcode — either upgrade or guard the option as shown above.

Troubleshooting

"The identifier is not declared in BGTaskSchedulerPermittedIdentifiers"

The plugin logs this warning and skips submitting the task. Double-check ios/Runner/Info.plist — the identifier must match exactly, or the wildcard prefix must cover it (com.example.app.upload.* covers com.example.app.upload.anything).

The task expires mid-work

The system expires BGContinuedProcessingTask instances that stop reporting progress. Ensure updateIOSContinuedProcessingTaskProgress is called at every meaningful step — per chunk for uploads, per frame for rendering, per sample for DSP. A good rule of thumb is at least once every 5 seconds while the task is active.

The system shows no progress UI

BGContinuedProcessingTask UI is presented by iOS and only when the app is in the background. While the app is foregrounded, the system UI is suppressed because your app is already communicating progress to the user.

Fatal error: "IOSContinuedProcessingTaskOptions was configured, but this binary was compiled with a version of Xcode that does not include the iOS 26 SDK"

You configured continuedProcessingTask in IOSNotificationOptions but built the app with Xcode 15, 16, or another version that predates the iOS 26 SDK. Either:

  1. Upgrade to Xcode 26+, or
  2. Remove or guard the continuedProcessingTask option so it is null on unsupported toolchains (see Xcode version requirement above).

The submission raises an "unauthenticated" or "background task not permitted" error

  • Verify the Background processing capability is enabled.
  • Verify the identifier is listed in BGTaskSchedulerPermittedIdentifiers.
  • Verify startService runs synchronously from an explicit user gesture. Asynchronous gaps (e.g. a Future.delayed, a navigation transition, or waiting on a permission dialog) break Apple's user-initiated requirement.

Relationship to the existing BGAppRefreshTask

The plugin already registers a BGAppRefreshTask with identifier com.pravera.flutter_foreground_task.refresh for periodic background refreshes. That task is separate from the continued processing task introduced here and both can coexist in the same app. Keep the refresh identifier in BGTaskSchedulerPermittedIdentifiers alongside any continued processing identifiers.