Reliable iOS Background Processing and Push Notifications
Executing tasks when your app is in the background is one of the most restricted processes on iOS. Apple aggressively limits background execution to protect the device’s battery life and user privacy. If your app attempts to run unauthorized background calculations, the OS will terminate it immediately.
In this post, we explain how to configure the BackgroundTasks framework and handle background push notifications reliably in modern Swift.
The BackgroundTasks API: Fetch and Processing
iOS provides two main categories of background tasks:
- Background App Refresh (
BGAppRefreshTask): Used for quick updates (e.g., downloading the latest weather feed or social posts). Limit execution to 30 seconds. - Background Processing (
BGProcessingTask): Used for heavy calculations or data synchronization (e.g., database cleanups or ML model syncs). This mode can run for several minutes when the device is charging.
Registering Tasks at App Launch
You must register background tasks before applicationDidFinishLaunching returns. Use the BGTaskScheduler API:
import BackgroundTasks
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Register refresh task
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.portfolio.app.refresh",
using: nil
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
}
Implementing and Scheduling Tasks
When handling a task, ensure you set an expiration handler. If the task runs out of time, you must stop all processing cleanly to avoid system penalties:
extension AppDelegate {
func handleAppRefresh(task: BGAppRefreshTask) {
// Reschedule the next task
scheduleNextRefresh()
let queue = OperationQueue()
task.expirationHandler = {
// Cancel operations if system runs out of time
queue.cancelAllOperations()
}
let refreshOperation = BlockOperation {
// Run network update sync
print("Background sync complete.")
}
refreshOperation.completionBlock = {
task.setTaskCompleted(success: !refreshOperation.isCancelled)
}
queue.addOperation(refreshOperation)
}
func scheduleNextRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.portfolio.app.refresh")
// Request launch at least 15 minutes in the future
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule background refresh: \(error)")
}
}
}
Handling Background Push Notifications
To wake your app up silently when a push notification arrives, send a payload containing content-available: 1 from your server:
{
"aps": {
"content-available": 1
},
"data": {
"update-type": "sync-feed"
}
}
On iOS, handle this silent push using the background notification delegate callback:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]
) async -> UIBackgroundFetchResult {
if let updateType = userInfo["update-type"] as? String, updateType == "sync-feed" {
do {
try await performLocalDataSync()
return .newData
} catch {
return .failed
}
}
return .noData
}
Key Guidelines
- Always submit tasks on a background thread: Submitting tasks on the main thread can cause UI stutter.
- Never rely on exact times: iOS determines when to run background tasks based on user behavior (e.g., if the user opens the app every day at 8 AM, the OS will run tasks at 7:45 AM).
- Test with Emulator CLI Commands: You don’t have to wait 15 minutes to test your tasks. Use Xcode debugger commands to force trigger background refreshes on the simulator.