Background Tasks help apps respond to system events and keep time-sensitive data up to date. Learn how you can use the SwiftUI Background Tasks API to handle tasks succinctly. We'll show you how to use Swift Concurrency to handle network responses, background refresh, and more — all while preserving performance and power.
♪ Mellow instrumental hip-hop music ♪ ♪ Welcome to "Efficiency awaits: Background Tasks in SwiftUI." I'm John Gallagher, an engineer on the watchOS Frameworks team.
In this talk, we'll learn about a new SwiftUI API for handling background tasks using Swift Concurrency and in a consistent way across all of Apple's platforms.
We'll begin by describing a sample app called Stormy, an app for taking photos of the sky on stormy days which makes use of background tasks.
Then, we'll dive into how the app uses background tasks and how background tasks work under the hood.
Next, we'll learn how to handle those background tasks using a new API in SwiftUI.
And finally, we'll review how the API uses Swift Concurrency to make handling background tasks easier than ever.
The new API is shared across watchOS, iOS, tvOS, Mac Catalyst, and Widgets, including iOS apps running on the Mac, which means the concepts and patterns you learn handling background tasks for one platform can apply to work you do on others.
Utilizing Swift Concurrency, the new API reduces the need for deeply nested completion handlers and callbacks as well as much of the mutable state which was frequently a side effect.
Swift Concurrency's native task cancellation helps applications gracefully complete tasks in a timely way to avoid being quit in the background by the system.
For people who like to keep their head in the clouds, we're going to build an app called Stormy that will remind users to take photos of the sky when it's stormy outside.
The app will show a notification at noon on stormy days requesting that the user takes a picture of the sky.
When the user taps the notification, they'll take a photo of the sky to upload to their profile for future admiration.
We're going to upload this photo in the background.
The app will send another notification when the upload has finished.
Let's dive in to how background tasks can let us do this.
In this diagram, we'll examine at a high level how the notification will only get sent on stormy days utilizing background tasks.
We'll represent foreground application runtime with the bar on the left, background application runtime with the bar in the middle, and the system is represented on the right.
When our app is first launched to the foreground by the user, we can take our first opportunity to schedule a background app refresh task for noon.
Then, when the user leaves our app and the app is suspended, the system will know to wake our application again in the background at the time we scheduled.
We scheduled our task for noon, so that's when system will wake the app in the background and send a background app refresh task.
With this background runtime, we need to figure out whether it is stormy outside, and if it is, send a notification to the user.
To start, we'll make a network request to a weather service to check the current weather.
With the URLSession scheduled for background, the application can suspend and wait for the network request to complete.
When the background network request for weather data completes, our application will be given background runtime again with a new URLSession background task.
With the results of the weather data request in hand, our application knows whether it is stormy outside and can choose whether to send a notification prompting the user to take a photo of the sky.
Now that our work for that URLSession task is done, the system can once again suspend the application.
Let's dive into the details of a single background task and see how it works.
To do that, we're going to take a closer look at the lifecycle of a single app refresh background task.
Let's zoom in here a little bit.
First, the system will wake our application and send it an app refresh background task.
Then, still in the background, we make a network request to check whether it is stormy outside.
Ideally, our network request completes within the allotted background runtime our application has for app refresh.
When we get the network response, we'd like to post the notification immediately.
With the notification posted, we've completed everything we needed to do during app refresh and the system can suspend the application again.
But what about when our network request for weather data doesn't complete in time? If an app is running low on background runtime for the current task, the system signals the app that the time is running low, giving us a chance to handle this situation gracefully.
If applications do not signal that they've completed their background work before runtime expires, the application may be quit by the system and throttled for future background task requests.
In this case, we should make sure that our network request is a background network request, which will allow us to complete our app refresh task immediately and get woken again for additional background runtime when the network request completes.
With our background URLSession scheduled, the system can suspend the application again.
Now, let's dive into how the BackgroundTask API in SwiftUI can help us build Stormy.
To begin, we'll need a basic application.
Then, we'll write a function to schedule background app refresh for noon tomorrow.
First, we create a date representing noon tomorrow.
Then, we create a background app refresh request with an earliest begin date of noon tomorrow and submit it to the scheduler.
This is what tells the system to wake our application tomorrow at noon.
We'll want to call this function when the user first opens the application and requests daily storm notifications at noon.
We can register a handler corresponding to the background task we scheduled by using the new background task scene modifier.
When the app receives a background task, any blocks registered with this modifier that match the background task received are run.
In this case, we used the appRefresh task type which can be scheduled in advance to provide our application with a limited amount of runtime in the background at a desired date.
Using the same identifier for the request and the handler in the background task modifier lets the system identify which handler to call when the corresponding task is received by your application.
In order for us to be sure that we are scheduled again for tomorrow, we'll start our background task by calling the scheduleAppRefresh function we just wrote to schedule background runtime again for tomorrow at noon.
Now that our background runtime at noon is recurring, we make our network request to check whether it is stormy outside and wait for the result using the await Swift keyword.
Then, if our network request has returned and it is indeed stormy outside, we await sending the notification to the user prompting them to upload a photo of the sky.
When the body of our closure returns, the underlying background task assigned to our application by the system is implicitly marked as complete, and the system can suspend our application again.
Here, using Swift Concurrency has let do potentially long-running operations in our background task without the need for an explicit callback for when the work has completed.
Many APIs across Apple Platforms, such as adding a notification, already support Swift Concurrency for asynchronous operations.
Here, the notifyForPhoto async function can be implemented in a straightforward way using the asynchronous addNotification method found on UserNotificationCenter.
Let's dive in to how Swift Concurrency and async/await do some heavy lifting for us and make it easier than ever to handle background tasks.
Let's write the asynchronous isStormy function that we've been referencing.
This async function is going to need to make a network request checking the weather outside.
To start, we'll get the shared URLSession and instantiate a request for weather data.
URLSession has adopted Swift Concurrency and has a method for downloading data from the network that can be awaited from async contexts.
With the network response in hand, we can read the weather data and return our result.
But what about when our application can't complete the network request before our runtime expires? Recall that in this case, we wanted to make sure that we had set up our URLSession as a background session and to ensure that it will send launch events to our application using a URLSession background task.
Back to our code.
We had used the shared URLSession.
Instead, we should create a URLSession from a background configuration with the sessionSendsLaunchEvents property set to true.
This tells the system that some network requests should run even when the app is suspended and to then wake the app for a URLSession background task when that request completes.
Note that this is especially important on watchOS as all network requests made by apps running in the background on watchOS must be requested through background URLSessions.
We're not quite done though.
Recall that when our background task runtime is expiring, the system will cancel the async task that is running the closure provided to the background task modifier.
This means that the network request made here will also be cancelled when our background runtime is expiring.
To respond to and handle that cancellation, we can use the withTaskCancellationHandler function built in to Swift Concurrency.
Instead of awaiting the result directly, we place our download into a withTaskCancellationHandler call and await this as well.
The first block passed to withTaskCancellationHandler is the async procedure we'd like to run and await.
The second onCancel trailing closure is code that will run when the task is cancelled.
Here, when the immediate network request is cancelled due to our runtime expiring, we promote the network request to a background download task, on which we can call resume, triggering the background download that will persist even when our app is suspended.
This code is not making the underlying network request twice as we're using the same URLSession to back both, and URLSession will deduplicate any in-process requests under the hood.
Finally, we need to ensure that our application is set up to handle a launch from a background URLSession.
We can use the background task modifier again, but this time with the URLSession task type.
By using the same identifier for the background URLSession configuration we made earlier, we can ensure this block is only called when that specific URLSession produces a background task.
We've taken a dive into the new unified SwiftUI API for handling background tasks and discovered how Swift Concurrency makes it easier than ever for us to manage task completion and expiration.
For more information about Swift Concurrency, check out the "Meet Async/await in Swift" talk from WWDC 2021.
And to learn more about concurrency in SwiftUI, we recommend "Discover Concurrency in SwiftUI," also from WWDC 2021.
Thanks for watching "Efficiency awaits: Background Tasks in SwiftUI." ♪
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.