Contents

Using background tasks

Handle scheduled update tasks in the background, and respond to background system interactions including Siri intents and incoming Bluetooth messages.

Overview

watchOS defines background tasks for specific features that your app may use, such as acquiring snapshots or handing Siri intents. All background tasks follow the same general procedure; first, the system wakes your app in the background. If your app is suspended, the system resumes execution in the background. If the system has purged your app from memory, your app launches in the background. For more information on app states and transitions, see Life cycles.

When the system wakes your app, there are two ways it can respond to the task:

  • Use SwiftUI’s backgroundTask(_:action:) modifier to respond to the tasks you’re interested in.

  • Use an app delegate and implement the delegate’s handle(_:) method. This method must respond to all the background tasks generated by the system.

Respond to background tasks in SwiftUI

You can use SwiftUI’s backgroundTask(_:action:) method to define a closure for a specific type of background task. The system executes this closure when your app receives a matching task.

import SwiftUI

@main
struct MyApp_Watch_AppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .backgroundTask(.appRefresh("My_App_Updates")) { context in
            // Perform the background task here.
        }
    }
}

The system gives your app a few seconds of background execution time to execute the closure you provided. Complete the task as quickly as possible.

The SwiftUI background tasks provide several advantages over the WatchKit background tasks:

  • The system automatically sets the task as complete when your closure returns.

  • You only need to handle the background tasks relevant to your app.

  • For appRefresh(_:) tasks, you can specify an identifier for each type of refresh task your app supports. You can then handle tasks with different identifiers separately.

To specify a particular identifier, pass a String as the userInfo parameter for scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:).

WKApplication.shared()
    .scheduleBackgroundRefresh(
        withPreferredDate: Date.init(timeIntervalSinceNow: 15.0 * 60.0),
        userInfo: "MY_FIRST_UPDATE" as NSSecureCoding & NSObjectProtocol) { error in
            if error != nil {
                // Handle the scheduling error.
                fatalError("*** An error occurred while scheduling the background refresh task. ***")
            }

            print("*** Scheduled! ***")
        }

You can then use appRefresh(_:) to specify a task for each identifier you want to handle, and use appRefresh for tasks that match any other background refresh tasks.

import SwiftUI

@main
struct MyApp_Watch_AppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .backgroundTask(.appRefresh("MY_FIRST_IDENTIFIER")) { context in
            // Handle tasks with the "MY_FIRST_IDENTIFIER" identifier.
        }
        .backgroundTask(.appRefresh("MY_SECOND_IDENTIFIER")) { context in
            // Handle tasks with the "MY_SECOND_IDENTIFIER" identifier.
        }
        .backgroundTask(.appRefresh) { context in
            // Handles any other app refresh tasks.
        }
    }
}

Implement the app delegate’s handle method

SwiftUI Background tasks are the preferred way to handle background tasks and interactions. However, if your app uses an app delegate, and you aren’t ready to migrate to SwiftUI background tasks, you can use an app delegate to handle your background tasks. This approach requires you to handle and manually mark all incoming tasks as complete. To use an app delegate, add a WKApplicationDelegate to your app and implement its handle(_:) method. For more information about using an application delegate, see Building a watchOS app.

The system calls your app delegate’s handle(_:) method whenever it receives a background task. The system then passes a set that contains one or more background tasks. In response, your app needs to perform the following actions:

  1. For each task in the set, the app checks the task type and performs the appropriate action. If you schedule multiple tasks, provide user information to identify each task’s intended purpose. When the system schedules a task, it automatically sets the userInfo property to nil.

  2. After the task completes, the app calls the task’s setTaskCompletedWithSnapshot(_:) method. The system suspends the app again as soon as all background tasks finish. The system gives your app only a few seconds of background execution time. Complete the background task and call setTaskCompletedWithSnapshot(_:) as quickly as possible.

Because background tasks happen asynchronously, the system may call your handle(_:) method with multiple tasks. It can also call handle(_:) while you’re already handling other background tasks, or even while your app is running in the foreground. If watchOS calls your handle(_:) method while your app is in the foreground, you can ignore the tasks, call setTaskCompletedWithSnapshot(_:), and return.

If you implement your delegate’s handle(_:) method, you receive all the background tasks, both those that you schedule and those that the system schedules. Some of your app’s other behaviors also change:

For some background tasks, the system calls other delegate methods or completion handlers in addition to calling handle(_:). The call to your delegate’s handle(_:) method occurs before any other methods or handlers. To process these multistep procedures, first save the task. Then, after you finish processing all the delegate method calls, call the task’s setTaskCompletedWithSnapshot(_:) method.

Request a background task

Your app can request to run in the background by calling scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:).

WKApplication.shared()
    .scheduleBackgroundRefresh(
        withPreferredDate: Date.init(timeIntervalSinceNow: 15.0 * 60.0),
        userInfo: nil { error in
            if error != nil {
                // Handle the scheduling error.
                fatalError("*** An error occurred while scheduling the background refresh task. ***")
            }

            print("*** Scheduled! ***")
        }

For the system to allocate background execution time to your app, your app must have a complication on the active watch face. If the system gives your app background execution time, it attempts to trigger your background task no earlier than the time specified by the withPreferredDate parameter; however, depending on the watch’s state, the system can defer or throttle your tasks. Therefore, your app might not wake up exactly at the specified time.

When designing your app, don’t expect the system to trigger every background task. Design a fallback mechanism so your app behaves correctly even when throttling occurs. For example, even if your app never receives any background tasks, it can still update its state whenever the user launches it in the foreground.

Budget your app’s background time

The delivery of background tasks and the allocation of background execution time are completely up to the system. When making a watchOS app, schedule background updates based on these limits, and prepare for background task throttling. Understanding the limits helps you better design your app’s background update strategy.

To prevent background tasks from using too many system resources or excessively draining the battery, watchOS controls how often and how long background tasks can run. When scheduling background tasks, the system may:

  • Give each app an individual allotment of background execution time. The system only triggers an app’s background tasks when it has time remaining in its budget.

  • Throttle background tasks when system resources are tight. Even if individual apps have the budget for tasks, the system doesn’t trigger background tasks when the device’s battery is low or the system conditions are poor.

  • Throttle background execution when the user is performing high-priority activities, such as exercising or navigating.

When the system triggers a background task, it wakes your app and gives it a small amount of time to run in the background. You need to complete the task as quickly as possible. If the available background time expires before you finish processing the task, the system may terminate your app, triggering an EXC_CRASH (SIGKILL) crash. For more information, see Understanding the exception types in a crash report.

When using BackgroundTask instances that may run long, use a withTaskCancellationHandler(operation:onCancel:) function to detect when the system cancels the background task. Use this function to clean up your background task and prepare for your app to become suspended.

.backgroundTask(.appRefresh) { context in
    // Handles any other app refresh tasks.
    await withTaskCancellationHandler {
        // Handle the background refresh task.
    } onCancel: {
        // Clean up and prepare to become suspended.
    }
}

When using WKRefreshBackgroundTask, assign an expirationHandler to the task. The system then calls the expirationHandler when your task is running out of time. You can use this handler to clean up your tasks.

task.expirationHandler =  {
    // Clean up the background task and prepare
    // for the system to suspend your app.
}

For example, you could try to download data synchronously during a background refresh task. Then, in the expiration handler, convert the download to a background URL session if necessary. This lets your URLSession continue downloading the requested content, even after your app suspends.

Handle Siri Intents

To handle Siri intents, the system wakes your app in the background and executes your handler. Then it calls your app delegate’s handle(_:completionHandler:) method. When using a WKIntentDidRunRefreshBackgroundTask task, you need to defer calling setTaskCompletedWithSnapshot(_:) until after you finish handling the intent. Be sure to process the intent using the following steps:

  1. The system executes your handler using a WKIntentDidRunRefreshBackgroundTask task. Save the task so you can set it as complete later.

  2. The system then calls your delegate’s handle(_:completionHandler:) method, passing an intent and completion handler. In this method, your app processes the intent and calls its completion handler.

  3. Finally, mark the task as complete by calling its setTaskCompletedWithSnapshot(_:) method.

Manage background URL sessions

When you schedule a background URLSession, the system can wake your app in the background to handle a variety of events, including authorization requests, errors, and a completed download. The system then calls the relevant methods on your session delegate.

When using a WKURLSessionRefreshBackgroundTask task, you need to defer calling setTaskCompletedWithSnapshot(_:) until after you finish handling the call to the session delegate. For example, to handle a basic download task, use the following steps:

  1. The system calls your delegate’s handle(_:) method, passing a WKURLSessionRefreshBackgroundTask task. In this method, your app needs to save the task so you can access it later.

  2. The system then calls your session delegate’s urlSession(_:downloadTask:didFinishDownloadingTo:) method. In this method, your app needs to move the downloaded data to a permanent location.

  3. Finally, the system calls your session delegate’s urlSession(_:task:didCompleteWithError:) method. In this method, your app needs to mark the task as complete by calling the setTaskCompletedWithSnapshot(_:) method on the background task that you saved in step 1.

For more complex interactions, the system may call other session delegate methods, such as urlSession(_:task:didReceive:completionHandler:), to handle authentication. These follow the same basic pattern: You handle the delegate call, and then call setTaskCompletedWithSnapshot(_:) to end the background task.

Communicate using Watch Connectivity

The system can wake your app in the background to handle communication using Watch Connectivity. It calls your background task handler and then calls the relevant methods on your doc://com.apple.documentation/documentation/watchconnectivity/wcsessiondelegate.

When using a WKWatchConnectivityRefreshBackgroundTask task, you need to defer calling setTaskCompletedWithSnapshot(_:) until after you finish handling the call to the session delegate. For example, to handle a basic download task, use the following steps:

  1. The system calls your delegate’s handle(_:) method, passing a WKWatchConnectivityRefreshBackgroundTask task. In this method, your app needs to save the task so you can access it later.

  2. Then the system calls methods on your doc://com.apple.documentation/documentation/watchconnectivity/wcsessiondelegate, based on the type of data that the paired iPhone sends. Your app processes the incoming data in these delegate methods.

  3. Your app can use the current session’s hasContentPending method to determine whether you still have any pending data.

  4. After you’ve processed all the incoming data, call the setTaskCompletedWithSnapshot(_:) method on the background task you saved in step 1.

Communicate over Bluetooth

Your apps can connect to Bluetooth peripherals and communicate with those devices in the background. To perform any background Bluetooth tasks, add the bluetooth-central value to the UIBackgroundModes array in the Info tab of your Xcode project settings or in your app’s Info.plist file. In Xcode’s property list editor, add “App communicates using Core Bluetooth” to the “Required background modes” array.

Typically, your app scans for the Bluetooth peripheral while running in the foreground. You can then perform the initial connection and pair the devices if necessary.

In watchOS 8 or later, you can reconnect with the Bluetooth peripheral and fetch additional data using a WKApplicationRefreshBackgroundTask, BackgroundTask, or WKExtendedRuntimeSession. In watchOS 9 or later, the system can retain your connection to the peripheral, even after suspending your app; therefore, you may not need to reconnect when your app wakes.

In watchOS 9 or later, your app can also receive WKBluetoothAlertRefreshBackgroundTask or bluetoothAlert tasks to handle timely alerts in the background. To receive timely alerts, your peripheral must use Generic Attribute Profile (GATT) transactions. Call setNotifyValue(_:for:) to enable notifications for the specified characteristic. Then, any change to the peripheral’s characteristic wakes your app and executes your handler. Use your handler to reconnect to the peripheral and handle the alert.

In watchOS 9 or later, your app can also scan for Bluetooth peripherals from the background. Background scanning shares a budget with the timely alert tasks, so your app can only perform a few background scans per day.

When your app receives a timely alert and your budget has only one Bluetooth alert task remaining, the system raises a CBError.Code.leGattNearBackgroundNotificationLimit error. If you exceed the budget, the system raises a CBError.Code.leGattExceededBackgroundNotificationLimit error, and your app doesn’t receive any timely alerts and can’t perform any background scans until additional background budget is available.

See Also

Background tasks