Skip to content

Commit

Permalink
feat!: iOS major enhancements BGAppRefreshTask, BGProcessingTask, beg…
Browse files Browse the repository at this point in the history
…inBackgroundTask, printScheduledTasks (#511)

* fix:Update Workmanager iOS because no callback in Background on iOS real device, added 30sec BGAppRefresh, Updated example #396

* added permissionhandler an requests for iOS
added alert and MaterialApp to workmanager when no iOS permissions activated

* fixed errormessage on xcode

* feat:Added check for background refresh permissions #441

* text to display task event dates (show prefs) added.

* fixed workmanager iOS Part
fixed BGProcessing
fixed inputdata in callback on task
clarified timings

* fixed warning dead code and ! check

* improved Task description (hints)

* Update README.md

* Update README.md

* Update README.md

* Format readme iOS examples

* Improve code documentation

* Cleanups in SwiftWorkmanagerPlugin.swift
* Use logInfo instead of prints and NSLog
* Log unnecessary logs only in debug mode
* Remove unnecessary logs
* Remove isInitalized flag in SwiftWorkmanagerPlugin which was not set to true anywhere

* * iOS, Rename registeriOSBackgroundProcessingTask to a generic name registerProcessingTask to be consistent with rest of the plugin and possible future Android implementation
* iOS, Rename wrongly named startOnOffTask to startOneOffTask

* * Cleanup code to make it more close to original plugin so that change size is reduced and it will make it easy to review
* Change new task identifier to be consistent with existing ones e.g. instead of app.workmanager... use be.tramckrijte...
* Documentation update
* Remove unnecessary logs, comments etc which were added in PRs which were not merged, and cleanup unnecessary code
* Revert using a custom log helper OS file to use the plugins existing shared prefs
* Bump example flutter sdk to < 4 instead of < 3

* Add task identifiers to iOS AppRefresh and ProcessingTask so that user can define task names instead of using hardcoded names

* * iOS AppRefresh task interval should be 15 minutes
* Documentation update

* Initialize should not auto open App settings if background refresh permission is not assigned.
Initialize should return result

* Continue work on task identifiers for iOS AppRefresh and ProcessingTask.
* Temporarily commented old iOS background fetch

* Fix extra commas on iOS

* New iOS feature printScheduledTasks to print details of un-executed scheduled tasks. To be used during development/debugging.
Format readme to improve readability

* iOS Periodic and processing tasks will be immediately scheduled, instead of waiting for App to go to background. Since doing on backgrounding will keep on changing earliest begin date.
* Add printScheduledTasks to example app
* Format example code

* Option to set frequency for iOS periodic tasks in AppDelegate.swift
* Add initialDelay support for Workmanager.registerProcessingTask
* Remove unnecessary WorkmanagerPlugin.registerBGProcessingTask calls from AppDelegate.swift
* Cleanup unused params from Workmanager.registerProcessingTask
* Update readme and iOS setup as per new iOS developments
* Create migration steps for iOS Workmanager.registerOneOffTask to Workmanager.registerProcessingTask

* Update iOS docs

* TODO for cleanups later

---------

Co-authored-by: Lars Huth <[email protected]>
Co-authored-by: xunreal75 <[email protected]>
Co-authored-by: Ioseph Magno <[email protected]>
Co-authored-by: delfme <[email protected]>
  • Loading branch information
5 people authored Apr 8, 2024
1 parent 370df0c commit b783000
Show file tree
Hide file tree
Showing 15 changed files with 922 additions and 140 deletions.
28 changes: 23 additions & 5 deletions IOS_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This plugin is compatible with **Swift 4.2** and up. Make sure you are using **X
> ⚠️ BGTaskScheduler is similar to Background Fetch described below and brings a similar set of constraints. Most notably, there are no guarantees when the background task will be run. Excerpt from the documentation:
>
> Schedule a processing task request to ask that the system launch your app when conditions are favorable for battery life to handle deferrable, longer-running processing, such as syncing, database maintenance, or similar tasks. The system will attempt to fulfill this request to the best of its ability within the next two days as long as the user has used your app within the past week.
>
> Workmanager BGTaskScheduler methods `registerOneOffTask`, `registerPeriodicTask`, and `registerProcessingTask` are only available on iOS 13+
![Screenshot of Background Fetch Capabilities tab in Xcode ](.art/ios_background_mode_background_processing.png)

Expand All @@ -19,6 +21,9 @@ This will add the **UIBackgroundModes** key to your project's `Info.plist`:
<key>UIBackgroundModes</key>
<array>
<string>processing</string>

<!-- If you need periodic tasks in iOS 13+ you need to enable Background Fetch as well -->
<string>fetch</string>
</array>
```

Expand All @@ -31,16 +36,25 @@ import workmanager

``` swift
// In AppDelegate.application method
WorkmanagerPlugin.registerTask(withIdentifier: "task-identifier")
WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "task-identifier")

// Register a periodic task in iOS 13+
WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60))
```

- Info.plist
``` xml
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>task-identifier</string>
</array>
<array>
<string>task-identifier</string>

<!-- Register a periodic task in iOS 13+ -->
<string>be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh</string>
</array>
```
> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval`
methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version.
For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app)

And will set the correct *SystemCapabilities* for your target in the `project.pbxproj` file:

Expand All @@ -64,7 +78,11 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWith

## Enabling Background Fetch

> ⚠️ Background fetch is one supported way to do background work on iOS with work manager: **Periodic tasks** are available on Android only for now! (see #109)
> ⚠️ Background fetch is one supported way to do background work on iOS with work manager. Note that this API is deprecated starting iOS 13, however it still works on iOS 13+ as of writing this article
> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval`
methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version.
For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app)

Background fetching is very different compared to Android's Background Jobs.
In order for your app to support Background Fetch, you have to add the *Background Modes* capability in Xcode for your app's Target and check *Background fetch*:
Expand Down
106 changes: 100 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ void callbackDispatcher() {
```

Android tasks are identified using their `taskName`.
iOS tasks are identitied using their `taskIdentifier`.
iOS tasks are identified using their `taskIdentifier`.

However, there is an exception for iOS background fetch: `Workmanager.iOSBackgroundTask`, a constant for iOS background fetch task.

Expand All @@ -93,25 +93,119 @@ Refer to the example app for a successful, retrying and a failed task.

# iOS specific setup and note

iOS supports **One off tasks** with a few basic constraints:
Initialize Workmanager only once.
Background app refresh can only be tested on a real device, it cannot be tested on a simulator.

### Migrate to 0.6.x
Version 0.6.x of this plugin has some breaking changes for iOS:
- Workmanager.registerOneOffTask was previously using iOS **BGProcessingTask**, now it will be an immediate run task which will continue in the background if user leaves the App. Since the previous solution meant the one off task will only run if the device is idle and as often experienced only when device is charging, in practice it means somewhere at night, or not at all during that day, because **BGProcessingTask** is meant for long running tasks. The new solution makes it more in line with Android except it does not support **initialDelay**
- If you need the old behavior you can use the new iOS only method `Workmanager.registerProcessingTask`:
1. Replace `Workmanager().registerOneOffTask` with `Workmanager().registerProcessingTask` in your App
1. Replace `WorkmanagerPlugin.registerTask` with `WorkmanagerPlugin.registerBGProcessingTask` in `AppDelegate.swift`
- Workmanager.registerOneOffTask does not support **initialDelay**
- Workmanager.registerOneOffTask now supports **inputData** which was always returning null in the previous solution
- Workmanager.registerOneOffTask now does NOT require `WorkmanagerPlugin.registerTask` call in `AppDelegate.swift` hence remove the call

### One off tasks
iOS supports **One off tasks** only on iOS 13+ with a few basic constraints:

`registerOneOffTask` starts immediately. It might run for only 30 seconds due to iOS restrictions.

```dart
Workmanager().registerOneOffTask(
"task-identifier",
simpleTaskKey, // Ignored on iOS
initialDelay: Duration(minutes: 30),
initialDelay: Duration(minutes: 30), // Ignored on iOS
inputData: ... // fully supported
);
```

### Periodic tasks
iOS supports two types of **Periodic tasks**:
- On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](./IOS_SETUP.md), even though the API is
deprecated by iOS it still works on iOS 13+ as of writing this article

- `registerPeriodicTask` is only supported on iOS 13+, it might run for only 30 seconds due to iOS restrictions, but doesn't start immediately, rather iOS will schedule it as per user's App usage pattern.

> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval`
methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version.
For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app)

To use `registerPeriodicTask` first register the task in `Info.plist` and `AppDelegate.swift` [iOS Setup](./IOS_SETUP.md). Unlike Android, for iOS you have to set the frequency in `AppDelegate.swift`. The frequency is not guaranteed rather iOS will schedule it as per user's App usage pattern, iOS might take a few days to learn usage pattern. In reality frequency just means do not repeat the task before x seconds/minutes. If frequency is not provided it will default to 15 minutes.

```objc
// Register a periodic task with 20 minutes frequency. The frequency is in seconds.
WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60))
```

Then schedule the task from your App
```dart
const iOSBackgroundAppRefresh = "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh";
Workmanager().registerPeriodicTask(
iOSBackgroundAppRefresh,
iOSBackgroundAppRefresh,
initialDelay: Duration(seconds: 10),
frequency: Duration(hours: 1), // Ignored on iOS, rather set in AppDelegate.swift
inputData: ... // Not supported
);
```

For more information see [BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask)

### Processing tasks
iOS supports **Processing tasks** only on iOS 13+ which can run for more than 30 seconds.

`registerProcessingTask` is a long running one off background task, currently only for iOS. It can be run for more than 30 seconds but doesn't start immediately, rather iOS might schedule it when device is idle and charging.
Processing tasks are for long processes like data processing and app maintenance. Processing tasks can run for minutes, but the system can interrupt these.
iOS might terminate any running background processing tasks when the user starts using the device.
For more information see [BGProcessingTask](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtask)

```dart
const iOSBackgroundProcessingTask = "be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask";
Workmanager().registerProcessingTask(
iOSBackgroundProcessingTask,
iOSBackgroundProcessingTask,
initialDelay: Duration(minutes: 2),
constraints: Constraints(
// connected or metered mark the task as requiring internet
// Connected or metered mark the task as requiring internet
networkType: NetworkType.connected,
// require external power
// Require external power
requiresCharging: true,
),
inputData: ... // fully supported
);
```

### Background App Refresh permission
On iOS user can disable `Background App Refresh` permission anytime, hence background tasks can only run if user has granted the permission.
With `Workmanager.checkBackgroundRefreshPermission` you can check whether background app refresh is enabled. If it is not enabled you might ask
the user to enable it in app settings.

```dart
if (Platform.isIOS) {
final hasPermission = await Workmanager().checkBackgroundRefreshPermission();
if (hasPermission != BackgroundRefreshPermissionState.available){
// Inform the user that background app refresh is disabled
}
}
```

For more information see the [BGTaskScheduler documentation](https://developer.apple.com/documentation/backgroundtasks).

### Print scheduled tasks
On iOS you can print scheduled tasks using `Workmanager.printScheduledTasks`

It prints task details to console. To be used during development/debugging.
Currently only supported on iOS and only on iOS 13+.

```dart
if (Platform.isIOS) {
Workmanager().printScheduledTasks();
// Prints: [BGTaskScheduler] Task Identifier: iOSBackgroundAppRefresh earliestBeginDate: 2023.10.10 PM 11:10:12
// Or: [BGTaskScheduler] There are no scheduled tasks
}
```


# Customisation (Android)

Not every `Android WorkManager` feature is ported.
Expand Down
6 changes: 3 additions & 3 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_VERSION = 4.2;
Expand Down Expand Up @@ -577,7 +577,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -628,7 +628,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
Expand Down
16 changes: 9 additions & 7 deletions example/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import workmanager
GeneratedPluginRegistrant.register(with: registry)
}

WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.taskId")
WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleTask")
WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.rescheduledTask")
WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.failedTask")
WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleDelayedTask")
WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodicTask")
WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask")
WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.taskId")
WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.rescheduledTask")
WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleDelayedTask")
WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask")

// When this task is scheduled from dart it will run with minimum 20 minute frequency. The
// frequency is not guaranteed rather iOS will schedule it as per user's App usage pattern.
// If frequency is not provided it will default to 15 minutes
WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60))

return super.application(application, didFinishLaunchingWithOptions: launchOptions)

Expand Down
2 changes: 2 additions & 0 deletions example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<string>be.tramckrijte.workmanagerExample.simpleDelayedTask</string>
<string>be.tramckrijte.workmanagerExample.simplePeriodicTask</string>
<string>be.tramckrijte.workmanagerExample.simplePeriodic1HourTask</string>
<string>be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh</string>
<string>be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
Expand Down
Loading

2 comments on commit b783000

@kyle-is-loading-please-wait

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this commit be pulled into main and committed to pub.dev soon? It appears multiple bugs have been issued which this commit resolves, and users are targeting it instead of the pub.dev as a workaround.

See: #551

@lisovyk
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am also interested on the status here – waiting for it to be proper to start migration to workmanager...

Please sign in to comment.