iOS Concepts8 min readMay 30, 2026

Mastering iOS Background Execution: Keep Your Apps Alive

Understanding and implementing background execution is crucial for building robust and responsive iOS applications. This guide will walk you through the various techniques available to ensure your app can perform necessary operations even when it's not in the foreground, enhancing user experience and data freshness.

Mastering iOS Background Execution: Keep Your Apps Alive

Introduction to iOS Background Execution

Modern iOS applications often need to perform tasks even when the user isn't actively interacting with them. This could involve fetching new data, processing information, synchronizing content, or completing long-running operations. iOS provides several mechanisms for background execution, each designed for specific use cases and with different limitations imposed by the system to preserve battery life and device performance.

Historically, iOS was very restrictive about background activity. However, over the years, Apple has introduced more sophisticated APIs that allow developers to achieve a balance between functionality and system resource conservation. It's essential to choose the correct background mode for your app's requirements to ensure reliability, avoid rejections from the App Store, and provide a seamless user experience. Misusing background capabilities can lead to your app being terminated or battery drain complaints.

Before diving into specific techniques, it's important to understand the concept of the app lifecycle and how background states fit in. When your app moves from the active state to the background, it has a limited amount of time to finish any ongoing tasks. If you need more time or want to perform operations at specific intervals, you'll need to leverage one of the specialized background execution modes.

Ephemeral Background Tasks with beginBackgroundTask(withName:expirationHandler:)

When your app moves to the background, the system grants it a short grace period (typically around 30 seconds) to clean up or complete any active work. If you have an operation that might exceed this standard grace period, you can request additional execution time using beginBackgroundTask(withName:expirationHandler:) from UIApplication. This is particularly useful for finishing network requests, saving user data, or performing other short-lived operations that started while the app was active.

It's crucial to call endBackgroundTask(_:) once your task is complete or if the expiration handler is invoked. Failing to do so will cause your app to be terminated by the system, as it will be seen as consuming resources indefinitely.

Let's look at an example where you might need to upload a large file when the user presses a 'Save' button, and you want to ensure the upload finishes even if the user switches to another app. This method is available since iOS 4.0 and macOS 10.9 (for app delegates).

swift
import UIKit

class BackgroundTaskExample:
    private var backgroundTask: UIBackgroundTaskIdentifier = .invalid

    func performLongRunningOperation() {
        print("Starting long-running operation...")

        // Register a background task
        backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "MyDataUploadTask") {
            // This block is called if the system needs to reclaim time
            self.endBackgroundTask()
            print("Background task expired before completion.")
        }

        // Simulate a network request or data processing
        DispatchQueue.global().asyncAfter(deadline: .now() + 20) { // Simulate 20 seconds of work
            // Ensure we are not trying to do UI updates on background thread
            DispatchQueue.main.async {
                print("Long-running operation completed!")
                self.endBackgroundTask()
            }
        }
    }

    private func endBackgroundTask() {
        if backgroundTask != .invalid {
            UIApplication.shared.endBackgroundTask(backgroundTask)
            backgroundTask = .invalid
            print("Background task ended.")
        }
    }
}

Background Fetch for Periodic Content Updates

Background Fetch (introduced in iOS 7.0) allows your app to periodically wake up in the background to download new content. This is ideal for apps like news readers, social media feeds, or weather apps that need to present up-to-date information to the user when they next launch the app. The system intelligently schedules fetch operations based on usage patterns, network conditions, and power considerations.

To enable Background Fetch, you must first enable the 'Background Modes' capability in your Xcode project settings and check 'Background Fetch'. Then, you can set the minimum fetch interval. The system will try to respect this interval but may adjust it based on its internal heuristics.

Upon a background fetch event, your app's application(_:performFetchWithCompletionHandler:) method (or a scene delegate equivalent) will be called. You have a limited time (again, around 30 seconds) to perform your data fetching and call the completion handler to indicate whether new data was available.

Even with background fetch enabled, if the user doesn't interact with your app for a long time, the system may stop scheduling background fetches to conserve resources. Interaction with the app will naturally re-enable this functionality.

swift
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        // Set the minimum background fetch interval
        // Use .minimum to let the system determine the best interval
        UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
        return true
    }

    func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        print("Background Fetch initiated.")

        // Simulate a network request
        DispatchQueue.global().asyncAfter(deadline: .now() + 5) { // Simulate 5 seconds of fetching
            let newDataAvailable = Bool.random() // Simulate whether new data was found

            if newDataAvailable {
                print("New data fetched in background.")
                // Process and save your new data here
                completionHandler(.newData)
            } else {
                print("No new data fetched in background.")
                completionHandler(.noData)
            }
        }
    }

    // ... other AppDelegate methods
}

Background App Refresh with URLSession Background Sessions

For larger downloads or uploads that need to continue reliably even if your app is suspended or terminated, URLSession background sessions (available since iOS 7.0) are your best friend. Unlike regular URLSession tasks that are tied to your app's process, background sessions are managed by the system. This means that if your app is terminated while a download is in progress, the system continues the transfer and can re-launch your app or notify it upon completion.

To use background sessions, you configure a URLSession with a unique identifier and set isDiscretionary to true if you want the system to optimize transferring when network conditions are ideal and battery is good. When a background transfer completes or requires authentication, your app's delegate (specifically application(_:handleEventsForBackgroundURLSession:completionHandler:)) will be called, allowing you to process the result.

This approach is ideal for downloading large media files, syncing cloud storage, or updating app resources that don't need immediate user interaction. It's more robust than relying on short-lived background tasks for network operations.

swift
import UIKit

class BackgroundDownloadManager: NSObject, URLSessionDelegate, URLSessionDownloadDelegate {

    static let shared = BackgroundDownloadManager()
    private var backgroundSession: URLSession!
    private var completionHandlers = [String: () -> Void]()

    override init() {
        super.init()
        let config = URLSessionConfiguration.background(withIdentifier: "com.yourapp.backgrounddownload")
        config.isDiscretionary = true // Let the system decide when to transfer for optimal performance
        config.sessionSendsLaunchEvents = true // App will be relaunched if needed
        backgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }

    func startDownload(from url: URL) {
        let downloadTask = backgroundSession.downloadTask(with: url)
        downloadTask.resume()
        print("Started background download for: \(url.lastPathComponent)")
    }

    // MARK: - URLSessionDownloadDelegate

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print("Download finished to: \(location.lastPathComponent)")
        // Move the downloaded file from the temporary location to app's desired directory
        // Example: let destinationURL = getAppDocumentsDirectory().appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
        // try? FileManager.default.moveItem(at: location, to: destinationURL)
    }

    // MARK: - URLSessionDelegate

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            print("Download task completed with error: \(error.localizedDescription)")
        } else {
            print("Download task completed successfully.")
        }
    }

    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        print("All background session events have been delivered.")
        // Call the stored completion handler for the AppDelegate
        DispatchQueue.main.async {
            self.completionHandlers[session.configuration.identifier ?? ""]?()
            self.completionHandlers.removeValue(forKey: session.configuration.identifier ?? "")
        }
    }

    // Method to be called from AppDelegate's handleEventsForBackgroundURLSession
    func addCompletionHandler(identifier: String, handler: @escaping () -> Void) {
        completionHandlers[identifier] = handler
    }
}

// In your AppDelegate.swift
// func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
//     BackgroundDownloadManager.shared.addCompletionHandler(identifier: identifier, handler: completionHandler)
// }

Utilizing BGTaskScheduler for Modern Background Processing

Introduced in iOS 13.0, BGTaskScheduler is Apple's modern framework for scheduling background tasks. It offers a more structured and robust way to perform background work compared to previous methods. BGTaskScheduler allows you to schedule two types of tasks:

  1. Background Refresh Task (BGAppRefreshTask): For short, periodic content updates, similar to Background Fetch but with more control and better system management. You can specify conditions like network availability.
  2. Background Processing Task (BGProcessingTask): For longer-running, deferred tasks like database cleanups, machine learning model updates, or large file uploads, which can be performed when the device is idle and charging. You can specify exact conditions such as network connection type, battery level, and device state.

To use BGTaskScheduler, you must declare the background task identifiers in your Info.plist under the Permitted background task scheduler identifiers array. You then register your tasks at app launch and schedule them when appropriate. The system will handle when to execute them based on its heuristics and your specified conditions.

BGTaskScheduler offers significantly improved battery efficiency and reliability for background work compared to older techniques, making it the preferred choice for new applications and when modernizing existing ones.

swift
import BackgroundTasks
import UIKit

class BGTaskSchedulerExample {

    static let appRefreshTaskIdentifier = "com.yourapp.apprefresh"
    static let dataProcessingTaskIdentifier = "com.yourapp.dataprocessing"

    func registerBackgroundTasks() {
        // Register background app refresh task
        BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTaskSchedulerExample.appRefreshTaskIdentifier, using: nil) { task in
            // Perform your app refresh directly here
            self.handleAppRefreshTask(task as! BGAppRefreshTask)
        }

        // Register background processing task
        BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTaskSchedulerExample.dataProcessingTaskIdentifier, using: nil) { task in
            // Perform your data processing directly here
            self.handleProcessingTask(task as! BGProcessingTask)
        }
    }

    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: BGTaskSchedulerExample.appRefreshTaskIdentifier)
        // Optional: define when the earliest the task can be run
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // Not sooner than 15 minutes from now

        do {
            try BGTaskScheduler.shared.submit(request)
            print("App refresh task scheduled.")
        } catch {
            print("Could not schedule app refresh task: \(error)")
        }
    }

    func scheduleProcessingTask() {
        let request = BGProcessingTaskRequest(identifier: BGTaskSchedulerExample.dataProcessingTaskIdentifier)
        request.requiresNetworkConnectivity = true // Only run when network is available
        request.requiresExternalPower = true       // Only run when device is charging
        request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // Not sooner than 30 minutes

        do {
            try BGTaskScheduler.shared.submit(request)
            print("Processing task scheduled.")
        } catch {
            print("Could not schedule processing task: \(error)")
        }
    }

    private func handleAppRefreshTask(_ task: BGAppRefreshTask) {
        print("Handling app refresh task...")

        // Schedule a new refresh task for the future
        scheduleAppRefresh()

        // Simulate fetching new data
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        operationQueue.addOperation {
            print("Performing actual app refresh...")
            Thread.sleep(forTimeInterval: 5)
            print("App refresh done.")

            // Mark the task as complete
            task.setTaskCompleted(success: true)
        }

        // Provide a handler to be called if the system needs to preempt the task
        task.expirationHandler = {
            operationQueue.cancelAllOperations()
            print("App refresh task expired.")
            task.setTaskCompleted(success: false)
        }
    }

    private func handleProcessingTask(_ task: BGProcessingTask) {
        print("Handling processing task...")

        // Schedule a new processing task for the future
        scheduleProcessingTask()

        // Simulate long-running processing
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        operationQueue.addOperation {
            print("Performing actual processing...")
            Thread.sleep(forTimeInterval: 20) // Simulate a longer task
            print("Processing done.")

            // Mark the task as complete
            task.setTaskCompleted(success: true)
        }

        // Provide a handler to be called if the system needs to preempt the task
        task.expirationHandler = {
            operationQueue.cancelAllOperations()
            print("Processing task expired.")
            task.setTaskCompleted(success: false)
        }
    }
}

/*
// In your AppDelegate `didFinishLaunchingWithOptions`:
let bgTaskScheduler = BGTaskSchedulerExample()
bgTaskScheduler.registerBackgroundTasks()

// In your AppDelegate `applicationDidEnterBackground` (or equivalent for SceneDelegate):
bgTaskScheduler.scheduleAppRefresh()
bgTaskScheduler.scheduleProcessingTask()
*/

Other Background Execution Capabilities

Beyond general-purpose background execution, iOS offers specialized background modes for specific app types. If your app falls into one of these categories, you can declare the corresponding 'Background Mode' capability in your Info.plist (or Xcode capabilities tab) for extended background privileges:

  • Audio, AirPlay, and Picture in Picture: For apps that play audio or video content while in the background.
  • Location Updates: For navigation apps or fitness trackers that need continuous access to location data.
  • VoIP (Voice over IP): For communication apps that need to maintain persistent network connections to receive incoming calls.
  • Newsstand (Deprecated): For newspaper and magazine apps (replaced by more general methods).
  • External Accessory Communication: For apps communicating with specific Bluetooth accessories.
  • Uses Bluetooth LE accessories: For apps that need to communicate with Bluetooth Low Energy devices.
  • NFC Tag Reading: For apps that need to read NFC tags while inactive.

These modes grant your app significantly more background time and resources but must be used judiciously and for their intended purpose. Misuse can lead to App Store rejection. Always ensure that the background mode you select genuinely aligns with your app's core functionality.

For example, a fitness app using startUpdatingLocation() needs the 'Location Updates' background mode to keep recording the user's path while the app is in the background. Without it, location updates would cease shortly after the app goes inactive.

Best Practices for Background Execution

To ensure your app is well-behaved, battery-friendly, and provides a great user experience, follow these best practices when implementing background execution:

  • Choose the Right Tool: Don't try to force a beginBackgroundTask to do what BGTaskScheduler or URLSession background sessions are designed for. Select the API that best fits your task's nature and duration.
  • Minimize Work: Only perform essential work in the background. Avoid UI updates, heavy computations, or anything that can severely impact battery life or device performance.
  • Be Timely: Complete your background tasks as quickly as possible. The system can terminate your app if it exceeds allowed time limits.
  • Respect System Conditions: Leverage conditions provided by BGTaskScheduler (like requiresExternalPower or requiresNetworkConnectivity) to allow the system to schedule tasks optimally.
  • Handle Expiration: Always implement and properly handle expiration handlers for beginBackgroundTask and BGTask instances. Fail to do so will result in app crashes.
  • Test Thoroughly: Test your background execution logic under various conditions, including low battery, no network, and app termination, to ensure reliability.
  • Monitor Performance: Use Xcode's Instruments (especially Energy Log) to monitor your app's energy consumption and CPU usage in the background. Aim for minimal impact.

Frequently Asked Questions

Why is my iOS app being terminated in the background?
Your app is likely being terminated because it's exceeding its allotted background execution time without properly indicating completion. This can happen if you start a background task with `beginBackgroundTask(withName:expirationHandler:)` but fail to call `endBackgroundTask(_:)` when the work is done, or if your background fetch/processing task doesn't call its completion handler or `setTaskCompleted(success:)` in time (typically around 30 seconds).
Which background execution method should I use for periodic data updates?
For modern iOS apps (iOS 13.0+), you should use `BGTaskScheduler` with a `BGAppRefreshTask`. It provides more flexibility and better system management compared to the older `Background Fetch` API. You can specify network conditions and schedule when the earliest the task can run. If you need to support older iOS versions, `Background Fetch` is still a viable option.
Can I perform a long-running computation, like processing a large image, in the background?
For truly long-running and resource-intensive computations, `BGProcessingTask` from `BGTaskScheduler` (iOS 13.0+) is the recommended approach. You can specify conditions like `requiresExternalPower` and `requiresNetworkConnectivity` to allow the system to execute the task when the device is idle or charging, minimizing impact on the user. For very specific, critical use cases, sometimes audio or location background modes are 'abused' by playing silent audio or constantly updating location, but these are generally not recommended due to App Store review implications and battery drain.
#iOS#Background Execution#Multitasking#App Lifecycle#Swift