Reliable iOS Background Processing and Push Notifications

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:

  1. Background App Refresh (BGAppRefreshTask): Used for quick updates (e.g., downloading the latest weather feed or social posts). Limit execution to 30 seconds.
  2. 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.