Background Tasks and App Refresh on iOS
In the world of mobile applications, a truly great user experience often extends beyond what happens when your app is actively in use. Users expect their data to be fresh, notifications to be timely, and content to be ready the moment they launch an app. Achieving this requires your app to perform work even when it's not in the foreground. This is where iOS background tasks and app refresh come into play.
However, background execution on iOS isn't a free-for-all. Apple's strict resource management policies prioritize battery life and system performance. Developers must work within these constraints, using the right APIs to respectfully and efficiently perform work in the background.
This article will guide you through the modern approach to background tasks using BGTaskScheduler, discuss the nuances of Background App Refresh, and provide practical Swift examples to help you keep your app's content up-to-date and perform necessary maintenance.
Understanding iOS App Lifecycle and Background Execution
Before diving into specific APIs, it's crucial to understand how iOS manages an app's lifecycle:
- Not Running: The app is not launched or has been terminated by the system or user.
- Active: The app is running in the foreground and receiving events.
- Inactive: The app is running in the foreground but not receiving events (e.g., during a phone call).
- Background: The app is running in the background. It still executes code but has limited time. This is where background tasks come in.
- Suspended: The app is in the background but is no longer executing code. The system might terminate it at any time to reclaim resources.
For apps to perform work in the Background state, they need explicit permission and must use specific APIs. Older approaches included traditional background modes (audio, location, VoIP, etc.) for continuous, specific tasks. However, for general data fetching and processing, Apple introduced BGTaskScheduler.
BGTaskScheduler: The Modern Approach for Deferrable Tasks
Introduced in iOS 13, BGTaskScheduler is the recommended framework for scheduling and performing deferrable background tasks. It allows the system to intelligently schedule your tasks when it's most opportune – for instance, when the device is charging, on Wi-Fi, or when system resources are otherwise available. This leads to better battery life and overall system performance.
There are two primary types of tasks you can schedule with BGTaskScheduler:
BGAppRefreshTask: For small, quick data fetches to keep your app's content fresh. The system tries to run these frequently but offers no guarantees. This is tied to the user's "Background App Refresh" setting.BGProcessingTask: For longer-running, more resource-intensive tasks that can be deferred, such as database cleanup, machine learning model updates, or large file uploads/downloads. These can require network connectivity or external power.
Setting Up BGTaskScheduler
To use BGTaskScheduler, you need to configure your app's Info.plist and register your task identifiers.
1. Add Permitted Background Task Scheduler Identifiers to Info.plist
You need to declare the identifiers for your background tasks in your app's Info.plist file. Add a new array key named Permitted background task scheduler identifiers (or BGTaskSchedulerPermittedIdentifiers in XML) and list your unique task identifiers as strings.
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourcompany.yourapp.refresh</string>
<string>com.yourcompany.yourapp.processData</string>
</array>
2. Register Your Tasks
In your AppDelegate's application(_:didFinishLaunchingWithOptions:) or an equivalent entry point (e.g., App struct in SwiftUI), you must register your task handlers with BGTaskScheduler.shared.
import BackgroundTasks
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
let refreshTaskIdentifier = "com.yourcompany.yourapp.refresh"
let processingTaskIdentifier = "com.yourcompany.yourapp.processData"
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ... your other setup ...
registerBackgroundTasks()
return true
}
private func registerBackgroundTasks() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshTaskIdentifier, using: nil) { task in
// Handle app refresh task
self.handleAppRefreshTask(task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: processingTaskIdentifier, using: nil) { task in
// Handle processing task
self.handleProcessingTask(task as! BGProcessingTask)
}
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Schedule tasks when the app goes into the background
scheduleAppRefresh()
scheduleDataProcessing()
}
// ... Task handling methods will go here ...
}
Scheduling a Task
Once registered, you can schedule tasks whenever your app enters the background or at other opportune moments.
Scheduling BGAppRefreshTask
This task is for quick updates.
extension AppDelegate {
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: refreshTaskIdentifier)
// Earliest date the task can run. The system might run it later.
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // Try to run in 15 minutes
do {
try BGTaskScheduler.shared.submit(request)
print("App Refresh task scheduled.")
} catch {
print("Could not schedule app refresh: \(error)")
}
}
}
Scheduling BGProcessingTask
This task is for more intensive work. It offers additional options.
extension AppDelegate {
func scheduleDataProcessing() {
let request = BGProcessingTaskRequest(identifier: processingTaskIdentifier)
request.requiresNetworkConnectivity = true // Only run if network is available
request.requiresExternalPower = false // Can run without external power
request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // Try to run in 1 hour
do {
try BGTaskScheduler.shared.submit(request)
print("Data Processing task scheduled.")
} catch {
print("Could not schedule data processing: \(error)")
}
}
}
Handling a Task
When the system decides to run your scheduled task, the handler closure you registered will be executed.
extension AppDelegate {
func handleAppRefreshTask(_ task: BGAppRefreshTask) {
// Schedule a new refresh task for the future
scheduleAppRefresh()
// Simulate network fetch
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
let fetchOperation = BlockOperation {
print("Performing app refresh task...")
Thread.sleep(forTimeInterval: 5) // Simulate work
print("App refresh task completed.")
}
// Set an expiration handler to gracefully end the task if time runs out
task.expirationHandler = {
operationQueue.cancelAllOperations()
task.setTaskCompleted(success: false)
print("App refresh task expired.")
}
fetchOperation.completionBlock = {
// Mark the task as complete, indicating success or failure
task.setTaskCompleted(success: !fetchOperation.isCancelled)
}
operationQueue.addOperation(fetchOperation)
}
func handleProcessingTask(_ task: BGProcessingTask) {
// Schedule a new processing task
scheduleDataProcessing()
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
let processingOperation = BlockOperation {
print("Performing data processing task...")
Thread.sleep(forTimeInterval: 20) // Simulate longer work
print("Data processing task completed.")
}
task.expirationHandler = {
operationQueue.cancelAllOperations()
task.setTaskCompleted(success: false)
print("Data processing task expired.")
}
processingOperation.completionBlock = {
task.setTaskCompleted(success: !processingOperation.isCancelled)
}
operationQueue.addOperation(processingOperation)
}
}
Key points for task handling:
- Reschedule: Always schedule the next instance of your task at the beginning of your handler.
- Expiration Handler: Set
task.expirationHandlerto cancel any ongoing work and mark the task as failed if the system needs to reclaim resources. Your app has a limited time (typically around 30 seconds for refresh, a few minutes for processing). setTaskCompleted(success:): You must call this when your task finishes, whether successfully or not. If you don't, the system will assume your app crashed and might prevent future background tasks.
Simulating Background Tasks in Xcode
Debugging background tasks can be tricky because they're system-driven. Xcode offers a great way to simulate them:
- Run your app on a device or simulator.
- Send the app to the background (press Home button).
- In Xcode, go to Debug > Simulate Background Tasks.
- Select the task identifier you want to simulate.
This will trigger your registered handler immediately, allowing you to test your logic.
┌───────────────────────────┐
│ App Launches │
│ (AppDelegate:didFinish) │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Register Task Handlers │
│ (BGTaskScheduler.shared) │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ App Enters Background │
│ (applicationDidEnterBack) │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Schedule Task │
│ (BGAppRefreshTaskRequest) │
│ (BGProcessingTaskRequest) │
└───────────┬───────────────┘
│ System intelligently
│ schedules based on
│ device conditions.
▼
┌───────────────────────────┐
│ Task Handler Executes │
│ (Registered closure runs) │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Perform Work (e.g. API) │
│ Set Expiration │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Call setTaskCompleted │
│ (Success or Failure) │
└───────────────────────────┘
Background App Refresh Setting
The Background App Refresh setting, found in Settings > General > Background App Refresh, is crucial for BGAppRefreshTask. If a user disables this setting globally or for your specific app, BGAppRefreshTasks will not be scheduled. BGProcessingTasks are generally not affected by this setting, as they're for more critical, deferrable work.
You can check the status of this setting using UIApplication.shared.backgroundRefreshStatus.
func checkBackgroundRefreshStatus() {
switch UIApplication.shared.backgroundRefreshStatus {
case .available:
print("Background App Refresh is available.")
// You can schedule BGAppRefreshTask here
case .denied:
print("Background App Refresh is denied by the user. Guide them to Settings.")
case .restricted:
print("Background App Refresh is restricted (e.g., parental controls).")
@unknown default:
fatalError("Unknown background refresh status")
}
}
If backgroundRefreshStatus is .denied, you might consider gently guiding your users to enable it if background app refresh is critical for your app's core functionality (e.g., a news app that needs to fetch headlines).
Best Practices and Considerations
- Be Efficient: Background tasks are a privilege. Use minimal CPU, memory, and network resources. Complete your work as quickly as possible.
- Provide Value: Only perform background work that genuinely enhances the user experience. Don't fetch data that's rarely used or can wait until the app is active.
- Error Handling: Design your tasks to be robust against network failures, data corruption, and system interruptions.
- Idempotency: Ensure your background tasks can be run multiple times without causing issues (e.g., duplicate data). The system might run them more or less frequently than you request.
- Testing on Device: While Xcode simulation is great, always test background tasks on a physical device under various conditions (low battery, poor network) to observe real-world behavior.
- Monitoring: Use tools like Instruments (Energy Log) to monitor the impact of your background tasks on battery life.
- No UI Updates: You cannot perform UI updates directly from background tasks. Store data, and let your app update its UI when it becomes active.
Summary
Mastering background tasks is essential for building robust and user-friendly iOS applications. BGTaskScheduler provides a powerful yet respectful way to perform work when your app is not in the foreground. By understanding the distinction between BGAppRefreshTask and BGProcessingTask, and by respecting system resources and user preferences (like the Background App Refresh setting), you can ensure your app delivers a consistently great experience without compromising battery life or system performance. Always remember to be a good citizen in the iOS ecosystem!
Happy Swifting!