Home iOS & Swift Tutorials

Background Modes Tutorial: Getting Started

In this tutorial, you’ll create an app that uses audio playback, location updates, critical tasks, and background fetch to learn about the most common background modes.


  • Swift 5.5, iOS 15, Xcode 13
Update note: Chuck Krutsinger updated this tutorial for Swift 5.5, iOS 15 and Xcode 13. Chris Wagner wrote the original.

Apple started allowing apps to do work in the background in 2010 with the release of iOS 4 and has evolved and improved background modes since then. iOS restricts the use of background operations to improve user experience and extend battery life. Your app can run in the background for specific use cases, including: playing audio, updating location and fetching the latest content from a server.

If your task doesn’t fall into one of the permitted categories, backgrounding may not be for you. You could face one of those dreaded App Store rejections if you try to game the system with background modes that operate beyond the scopes of their purposes. Consider yourself warned!

In this background modes tutorial, you’ll learn about four things your apps can do in the background:

  • Play audio: Allow the app to continue playing audio in the background.
  • Receive location updates: Enable the app to receive location changes in the background.
  • Complete finite-length critical tasks: Enable the app continue to complete a critical task after moving to the background.
  • Background Fetch: Perform background updates on a schedule determined by iOS.

Getting Started

Download the project files at the top or bottom of the tutorial by clicking the Download Materials button. The user interface and core logic are already built.

Before digging in, here’s a quick overview of the basic background modes available in iOS:

  • Audio, AirPlay, and Picture in Picture: Play audio and video while the app is in the background.
  • Location Updates: Continue to receive location updates when in the background.
  • Voice over IP: Send and receive voice over the internet.
  • External accessory communication: Communicate via the lightning port with external accessories.
  • Using Bluetooth LE accessories: Communicate with Bluetooth LE accessories while in the background.
  • Acting as a Bluetooth LE accessory: Allow the app to serve Bluetooth LE information to an accessory.
  • Background fetch: Perform data refreshes.
  • Remote notitifications: Send and receive remote notifications.
  • Background processing: Perform longer critical processes.

You’re going to add four of the above modes to the sample app – audio, location, background processing and background fetches. If you’re only interested in some of these modes, feel free to skip around and just play with the modes that interest you.

Note: For the full effect, you should follow along on a real device. In Simulator, the app might appear to run in the background when you forget a step. Then when you switch to a real device, it may not work at all.

Before you can run the project on a physical device, you must set your development team, as shown here:

Select your development team

Build and run the sample project to get a feel for Sleepless, an app that never rests because it does things in the background. There are four tabs — one to cover each mode:

First look at Sleepless sample app

The first capability you’re going to add is background audio.

Playing Audio

Build and run Sleepless on a physical device. Navigate to the audio tab, play the music, and then put the app in the background by returning to the home screen. The music will stop playing.

Open AudioModel.swift.

The app makes use of AVQueuePlayer to queue up songs and play them in sequence. The model observes the player’s currentItem value to provide updates to the view.

Giving Credit Where Credit Is Due

The starter project includes audio files from incompetech.com, a popular royalty-free music website. You can use the music for free with atttribution. All three songs are by Kevin MacLeod:

Thanks for the wonderful music, Kevin!

Note: Check out Execution States for Apps on Apple’s UIKit documentation to learn more about the active state and others.

Testing Audio in the Background

Why does the music stop when the app enters the background? Well, a crucial piece is missing!

Most background modes won’t work unless you enable specific capabilities that indicate that the app wants to run code in the background. The exception is critical task completion, which any app can perform.

When activated, the audio background mode tells iOS to continue playing audio even though the app is in the background. That’s right — the audio background mode is virtually automatic. You just have to activate it.

Return to Xcode and do the following:

  1. Click the project in the Project navigator.
  2. Select SleeplessApp target.
  3. Select the Signing & Capabilities tab.
  4. Click on the + symbol to add a capability.

Next, double-click Background Modes to add this capability. Expand the Background Modes capability, then check the Audio, AirPlay, and Picture in Picture box to enable background audio.

Add audio background mode to project

Build and run the app on a physical device. Start the music as before, then leave the app. This time the audio will continue. It’s that simple!

SleeplessApp playing music in the background

Next, you’ll use the Location updates background mode to continue to receive location updates even when the app is in the background.

Receiving Location Updates

First, build and run the app. Choose the Location tab and tap Start. Nothing happens yet because you’re missing some important steps. You’ll change that now.

Enabling Location Updates

Open LocationModel.swift. This is the code that provides location data to the LocationView. You’re going to make a simple change to init(). Replace following two lines:

  mgr.allowsBackgroundLocationUpdates = false


 mgr.allowsBackgroundLocationUpdates = true

The first line requests location updates even when the app is not in use. The second requests updates even in the background.

Go back to the Signing & Capabilities screen and check the box for Location updates to let iOS know that your app wants to receive location updates while in the background.

Enable location updates background mode

In addition to checking this box, iOS requires that you set a key in your Info.plist explaining to the user why you need background updates. If you don’t include this, location requests will silently fail.

Open Info.plist and add the keys for Privacy — Location Always and When In Use Usage Description and Privacy — Location When In Use Usage Description. Then type The app will show your location on a map as the value for both keys.

Usage descriptions for location updates

Now, build and run! Switch to the Location tab and tap Start.

When it loads the first time, you’ll see the message that you wrote into your location privacy reason.

Tap Allow while using app and take a walk outside or around your building — try not to get too distracted catching Pokemons.

Permission to track location

Location updates should start to appear. If they don’t, send the app to the background again to trigger the Always prompt for location tracking. You can also use the Settings app to enable always tracking by the Sleepless app in the Privacy ▸ Location Services ▸ Sleepless settings.

If you send the app to the background, you’ll still see location updates occurring in the console.

After a while, you should see something like this:

Location updates as pins on map

Testing Location Mode in the Background

If you exit the app, you should see the app update the location in your console log. Open it again to see all the pins on the map showing the places you went during your walk.

If you’re using the simulator, you can use it to simulate movement, too! Check out the Features ▸ Location menu:

Simulator options for location tracking

Easy-peasy, right? On to the third tab and the third background mode!

Completing Critical Tasks Upon Moving to the Background

The next background mode is officially called Extending Your App’s Background Execution Time. What a mouthful. Task Completion is a bit easier to say!

Technically, this is not a background mode at all. You don’t have to declare that your app uses it in Capabilities. It’s an API that lets you run arbitrary code for a finite period when your app is in the background, giving you more time to finish critical tasks like saving data.

When to Use Task Completion

A valid use case of the Completion background mode is to complete some critical task, such as saving the user’s input or posting a transaction. There are many possibilities.

As the code is arbitrary, you can use this API to do pretty much anything: perform lengthy calculations, apply filters to images, render a complicated 3D mesh — whatever! Your imagination is the limit, as long as you keep in mind that you only get some time, not unlimited time. In a few moments, you’ll set up a lengthy calculation to run in the background, so you can see how this API works.

iOS determines how much time you get after your app moves to the background. There are no guarantees on the time you’re granted, but you can always check UIApplication.shared.backgroundTimeRemaining. This will tell you how much time you have left.

The general, observation-based consensus is that you get about 30 seconds. Again, there are no guarantees, and the API documentation doesn’t even give an estimate — so don’t rely on this number. You might get five minutes or five seconds, so your app needs to be prepared for an interruption. iOS will signal you with a callback when your time is about up.

Setting Up a Completion Task

Here’s a common task that every computer science student should be familiar with: Calculating numbers in the Fibonacci Sequence. The twist here is that you’ll calculate these numbers after the app moves to the background!

Open CompleteTaskModel.swift and take a look at what’s there already. As it stands, this view will calculate Fibonacci numbers sequentially and display the result.

If you were to suspend the app on an actual device right now, the calculations would stop and pick up where they were once the app became active again. Your task is to create a background task so the calculation can keep running until iOS says, “Time’s up!”

You first need to add the following to CompleteTaskModel:

var backgroundTask: UIBackgroundTaskIdentifier = .invalid

This property identifies the task request to run in the background.

Next add the following method to CompleteTaskModel just before resetCalcuation():

func registerBackgroundTask() {
  backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
    print("iOS has signaled time has expired")

registerBackgroundTask() tells iOS that you need more time to complete whatever it is that you’re doing when the app moves to the background. The value returned is an identifier for this task so that you can tell iOS when you are done. After this call, if your app moves to the background, it will still get CPU time until you call endBackgroundTask(_:).

Well, some amount of CPU time, at least.

Ending the Completion Task

If you don’t call endBackgroundTask(_:) after a period of time in the background, iOS will call the closure defined when you called beginBackgroundTask(expirationHandler:). This gives you a chance to stop executing code.

So it’s a good idea to call endBackgroundTask(_:) to tell the system that you’re done. If you don’t call it and continue to execute code after this block runs, iOS will terminate your app!

Add this method just below registerBackgroundTask():

func endBackgroundTaskIfActive() {
  let isBackgroundTaskActive = backgroundTask != .invalid
  if isBackgroundTaskActive {
    print("Background task ended.")
    backgroundTask = .invalid

This will end the background task if it is actively registered and reset its ID to invalid.

Registering and Ending Background Tasks

Now, for the important part: updating onChangeOfScenePhase(_:) to register and end the background task depending on whether the app is moving to the background or active state.

Replace the two case statements with the following:

case .background:
  let isTimerRunning = updateTimer != nil
  let isTaskUnregistered = backgroundTask == .invalid

  if isTimerRunning && isTaskUnregistered {
case .active:

When changing to background state, this registers the task when it’s running but not registered. When changing to active state, it’ll end the background task.

In beginPauseTask(), add this line right after updateTimer = nil:


Now, when the user stops calculations, you call endBackgroundTask(_:) to indicate to iOS that you don’t need any extra CPU time.

NOTE: It’s important to call endBackgroundTask(_:) every time you call beginBackgroundTask(expirationHandler:). If you call beginBackgroundTask(expirationHandler:) twice and only call endBackgroundTask(_:) for one of the tasks, you’re still going to get CPU time until you call endBackgroundTask(_:) a second time with the second background task’s identifier.

Build and run, then switch to the third tab.

Start calculating fibonacci sequence

Tap Play and watch the app calculate those sweet Fibonacci values. Send the app to the background, but watch the output in Xcode’s console. Your app should continue updating the numbers while the time remaining goes down.

In most cases, this time will start with 30 and go down to five seconds. If you wait for the time to expire when you reach five seconds — or whatever value you see — iOS invokes the expiration block.

Your app should stop generating output soon afterward. Then, if you go back to the app, the timer should start firing again, and the Fibonacci madness will continue.

Switch between active and background to see how you get an additional block of time with each switch.

On to the final topic for this background modes tutorial: background fetch.

Background Fetch

Background fetch was introduced in iOS 7. It lets your app appear up-to-date while minimizing the impact on battery life. Starting with iOS 13, Apple introduced a new background task scheduler API that offers significant improvements.

Suppose, for example, you were implementing a news feed in your app. Prior to background fetch, you would refresh the feed each time the app launches.

Sadly, users would see old headlines for several seconds while the refresh occurs. And you know some would try to tap a story only to see it vanish and be replaced by an unrelated story. Not a good look.

Wouldn’t it be better if the latest headlines were magically there when users open your app? This is the ability background fetch gives you.

When enabled, the system leverages usage patterns to determine when to fire off a background fetch. For example, if a user opens your news app at 9 a.m. most days, a background fetch will likely occur sometime before 9 a.m. The system decides the best time to issue a background fetch, and for this reason, it’s unsuitable for critical updates.

Understanding Background Fetch

Background fetch is controlled by BGTaskScheduler, a sophisticated system for balancing all the factors that affect user experience, such as performance, usage patterns, battery life and more.

Background fetch usually involves fetching information from an external source like a network service. For the purposes of this background modes tutorial, you’ll fetch the current time and won’t use the network.

In order to implement background fetch, you’ll need to do these tasks — but don’t do them yet:

  • Check the box Background fetch in the Background Modes of your app’s Capabilities.
  • Add an identifier to Info.plist for your refresh task.
  • Invoke BGTaskScheduler.register(forTaskWithIdentifier:using:launchHandler:) in your app delegate to handle the background fetch.
  • Create a BGAppRefreshTaskRequest with an earliestBeginDate for when to execute.
  • Submit the request using BGTaskScheduler.submit(_:).

Similar to background completion tasks, you have a short but indefinite timeframe to do a background fetch. The consensus figure is a maximum of 30 seconds, but plan for less. If you need to download large resources as part of the fetch, use URLSession‘s background transfer service.

Implementing Background Fetch

Time to get started. First, the easy part: Check the Background fetch mode capability under Signing & Capabilities.

Check the background fetch mode

Next, open Info.plist and tap + to add a new identifier.

Scroll down and select Permitted background task scheduler identifiers. Expand the item, then tap + next to that new identifier to add an entry.

Type com.mycompany.myapp.task.refresh for the value of the identifier.

Note: In your real projects, you would use your company’s URL in reverse as the root of the identifier, adding your app name and descriptive elements such as task.refresh. You could define multiple kinds of refresh tasks, each with its own identifier.

Add permitted background task identifier

Next, you need an AppDelegate class because iOS expects you to register your fetch task during application(_:didFinishLaunchingWithOptions:).

In the App folder, add a new Swift file named AppDelegate.swift. Then replace the existing code with the following:

import UIKit
import BackgroundTasks

class AppDelegate: UIResponder, UIApplicationDelegate {
  static var dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .long
    return formatter

  var window: UIWindow?

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    return true

This code defines a date formatter for the refresh timestamp. It also includes an empty method for application(_:didFinishLaunchingWithOptions:), which is where you’ll register the background fetch task.

Now add the following function to AppDelegate:

func refresh() {
  // to simulate a refresh, just update the last refresh date
  // to current date/time
  let formattedDate = Self.dateFormatter.string(from: Date())
    forKey: UserDefaultsKeys.lastRefreshDateKey)
  print("refresh occurred")

This function is the simulation of a refresh.

In an app you create, you might fetch data from the network. For this tutorial, you’ll save a formatted timestamp to UserDefaults to show the time of the refresh execution.

Still in AppDelegate.swift, add the following function to AppDelegate:

func scheduleAppRefresh() {
  let request = BGAppRefreshTaskRequest(
    identifier: AppConstants.backgroundTaskIdentifier)
  request.earliestBeginDate = Date(timeIntervalSinceNow: 1 * 60)
  do {
    try BGTaskScheduler.shared.submit(request)
    print("background refresh scheduled")
  } catch {
    print("Couldn't schedule app refresh \(error.localizedDescription)")

Here you create a BGAppRefreshTaskRequest then assign an earliestBeginDate one minute from the present time. Then you submit the request using BGTaskScheduler.submit(_:).

Now, replace the body of application(_:didFinishLaunchingWithOptions:) with:

  forTaskWithIdentifier: AppConstants.backgroundTaskIdentifier,
  using: nil) { task in
    self.refresh() // 1
    task.setTaskCompleted(success: true) // 2
    self.scheduleAppRefresh() // 3

return true

When iOS finishes launching the app, this code registers the task with the task scheduler and schedules the first refresh. The task itself, when executed, will:

  1. Perform the refresh.
  2. Mark the task as successfully completed.
  3. Schedule the next refresh.

Now you need to connect your AppDelegate to AppMain. Open AppMain.swift. Add this line just before the body:

@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

That is all you need for iOS to invoke the AppDelegate.

Build and run the app on a physical device. Check the Xcode console for a message to confirm the background refresh is scheduled.

Testing Background Fetch

One way to test background fetch is to sit around and wait for the system to decide to do it. But you might be sitting for a long time waiting for that to happen.

iOS makes no guarantees about when your refresh will execute. The system uses a variety of factors to decide when to execute, such app usage patterns, battery charge and others. Fortunately, Xcode gives you a way to trigger a background fetch with a debugger command.

Open RefreshView.swift and set a breakpoint at print("moved to background").

Then send the app to the background and Xcode should break at your new breakpoint. At the lldb prompt, type (or, since it’s quite complicated, copy and paste!) the following command:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.mycompany.myapp.task.refresh"]

This will instruct the debugger to execute the background refresh immediately.

Resume execution of the app. The console should display refresh occurred and then background refresh scheduled. Each refresh schedules another for the future. You may also see debug messages from the background task scheduler indicating its activities.

Next, reopen the app. The Refresh tab will display the time and date when the refresh occurred.

If you leave the app on your device and check over the next few days, you’ll see the timestamp updates from time to time. iOS invokes the refresh according to its calculations of when the best time is to refresh.

Where to Go From Here?

To compare results, download the completed project files by clicking on the Download Materials button at the top or bottom of the tutorial.

For long-running background tasks that need many minutes to complete, learn more about background processing tasks. Background processing tasks are similar to background fetch but intended for more rigorous tasks such as data processing and maintenance.

There are also two excellent WWDC presentations related to background modes:

  1. Advances in App Background Execution: Discusses recent improvements to background processing options.
  2. Background Execution Demystified: Will help you get a deeper understanding of the different background modes.

Finally, you can read about all of the background execution modes at Configuring Background Execution Modes.

We hope you enjoyed this tutorial on background modes. If you have any questions or comments, please join the forum discussion below!




More like this