Swift By Rahul

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.

iOS App States and Background Execution Active Inactive Background Suspended In Background state: - Limited execution time - BGTaskScheduler for deferrable work App State Note

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:

  1. 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.
  2. 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.expirationHandler to 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:

  1. Run your app on a device or simulator.
  2. Send the app to the background (press Home button).
  3. In Xcode, go to Debug > Simulate Background Tasks.
  4. 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.
Comparison of BGAppRefreshTask and BGProcessingTask BGAppRefreshTask BGProcessingTask Purpose: Keep content fresh, quick updates. Duration: Short (approx. 30 seconds). Dependencies: No specific requirements. User Control: Affected by "Background App Refresh" setting. Use Cases: Fetching latest news, updating social feed. Frequency: System-determined, often. Purpose: Long-running, deferrable maintenance. Duration: Longer (several minutes). Dependencies: Can require network, external power. User Control: Not directly affected by "Background App Refresh". Use Cases: Database cleanup, large file sync, ML model updates. Frequency: System-determined, less frequent.

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!