All skills
dpearson2699 avatar

/background-processing

@45c9085

Schedule and execute background work on iOS using BGTaskScheduler. Use when registering BGAppRefreshTask for short background fetches, BGProcessingTask for long-running maintenance, BGContinuedProcessingTask (iOS 26+) for foreground-started work that continues in background, background URLSession downloads, or background push notifications. Covers Info.plist configuration, expiration handling, task completion, and debugging with simulated launches.

Use this Skill: https://skilld.dev/gh/dpearson2699/swift-ios-skills/background-processing

This session only. Nothing lands on disk.

referencesbackground-task-patterns.md

≈3.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Background Task Patterns — Extended Reference

Overflow reference for the background-processing skill. Contains debugging tips, advanced background URLSession patterns, background push best practices, and SwiftUI integration patterns.

Contents

Debugging Background Tasks

Simulating Task Launches in Xcode

Use the LLDB console to trigger tasks instantly during development. The app must be running on a device in the debugger with a breakpoint hit or paused.

These are Apple-documented private functions for development only. Do not include references to them in App Store-submitted code.

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.app.refresh"]

For processing tasks:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.app.db-cleanup"]

To simulate early termination (expiration):

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.example.app.refresh"]

Verifying Pending Tasks

Check what tasks are currently scheduled:

BGTaskScheduler.shared.getPendingTaskRequests { requests in
    for request in requests {
        print("Pending: \(request.identifier), earliest: \(String(describing: request.earliestBeginDate))")
    }
}

Common Debugging Issues

Symptom Cause Fix
Task never fires Identifier not in Info.plist Add to BGTaskSchedulerPermittedIdentifiers
Task never fires Background modes not enabled Enable fetch and/or processing in capabilities
Task never fires on device Background App Refresh disabled or Low Power Mode active Check UIApplication.shared.backgroundRefreshStatus; Low Power Mode reduces background runtime
.notPermitted error Identifier mismatch Verify exact string match between code and plist
.unavailable error Running in extension BGTaskScheduler not available in app extensions
.tooManyPendingTaskRequests More than 1 refresh task or 10 processing tasks scheduled in total Cancel old requests before submitting new ones

Submitting an unexecuted task request with the same identifier replaces the previous request.

Advanced BGProcessingTask Patterns

Conditional Requirements

Use requiresExternalPower and requiresNetworkConnectivity to ensure the system only launches your task when conditions are met:

func scheduleSyncTask() {
    let request = BGProcessingTaskRequest(
        identifier: "com.example.app.full-sync"
    )
    // Only run when charging and connected to network
    request.requiresExternalPower = true
    request.requiresNetworkConnectivity = true
    // Don't run before 2 AM
    var components = DateComponents()
    components.hour = 2
    if let twoAM = Calendar.current.nextDate(
        after: Date(),
        matching: components,
        matchingPolicy: .nextTime
    ) {
        request.earliestBeginDate = twoAM
    }

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("Failed to schedule sync: \(error)")
    }
}

Incremental Work with Checkpointing

Design tasks to save progress so they can resume if terminated:

func handleMigration(task: BGProcessingTask) {
    let work = Task {
        let lastProcessed = UserDefaults.standard.integer(
            forKey: "migrationLastIndex"
        )
        let items = try await loadItems()

        for (index, item) in items.dropFirst(lastProcessed).enumerated() {
            try Task.checkCancellation()
            try await migrate(item)
            // Checkpoint progress
            UserDefaults.standard.set(
                lastProcessed + index + 1,
                forKey: "migrationLastIndex"
            )
        }

        task.setTaskCompleted(success: true)
    }

    task.expirationHandler = {
        work.cancel()
        // Progress is saved -- next launch picks up where we left off
        task.setTaskCompleted(success: false)
    }
}

Background URLSession — Extended Patterns

Configuration Best Practices

let config = URLSessionConfiguration.background(
    withIdentifier: "com.example.app.background-transfer"
)

// isDiscretionary = true: system picks optimal time (WiFi, power)
// Use for non-urgent transfers
config.isDiscretionary = true

// sessionSendsLaunchEvents = true: app relaunched when transfer completes
config.sessionSendsLaunchEvents = true

// Set reasonable timeouts
config.timeoutIntervalForResource = 60 * 60 * 24 * 7  // 7 days

// Allow cellular (default is true)
config.allowsCellularAccess = true

Upload with Background Session

func uploadFile(at fileURL: URL) {
    var request = URLRequest(url: URL(string: "https://api.example.com/upload")!)
    request.httpMethod = "POST"
    let uploadTask = session.uploadTask(with: request, fromFile: fileURL)
    uploadTask.resume()
}

Important: Background sessions only support uploadTask(with:fromFile:) and downloadTask(with:). Data tasks, uploadTask(with:from:) (Data), and closure/async-based tasks are not supported.

Handling App Relaunch

When the system completes a background transfer and your app is not running, it relaunches the app. You must:

  1. Store the completion handler from application(_:handleEventsForBackgroundURLSession:completionHandler:)
  2. Recreate the URLSession with the same identifier
  3. Call the stored completion handler in urlSessionDidFinishEvents
// AppDelegate
func application(
    _ application: UIApplication,
    handleEventsForBackgroundURLSession identifier: String,
    completionHandler: @escaping () -> Void
) {
    // Recreating session with the same identifier reconnects to the transfer
    _ = DownloadManager.shared.session  // trigger lazy init
    DownloadManager.shared.completionHandler = completionHandler
}

// In your URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    Task { @MainActor in
        self.completionHandler?()
        self.completionHandler = nil
    }
}

Handling Download Errors and Retries

func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    didCompleteWithError error: (any Error)?
) {
    guard let error else { return }  // Success handled in didFinishDownloadingTo

    let nsError = error as NSError

    // Check if download can be resumed
    if let resumeData = nsError.userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
        // Store resumeData and retry later
        let downloadTask = session.downloadTask(withResumeData: resumeData)
        downloadTask.resume()
        return
    }

    // Non-resumable error -- retry from scratch or notify user
    if nsError.code == NSURLErrorNetworkConnectionLost {
        // Re-enqueue the download
        if let url = task.originalRequest?.url {
            let newTask = session.downloadTask(with: url)
            newTask.resume()
        }
    }
}

Progress Tracking

func urlSession(
    _ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64,
    totalBytesExpectedToWrite: Int64
) {
    let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
    // Update UI on main actor if app is in foreground
    Task { @MainActor in
        DownloadProgressStore.shared.update(
            taskID: downloadTask.taskIdentifier,
            progress: progress
        )
    }
}

Background Push — Extended Patterns

Push Payload Requirements

The content-available: 1 flag is required. You can include custom data:

{
    "aps": {
        "content-available": 1
    },
    "type": "new-message",
    "conversation-id": "abc-123"
}

Do not include alert, badge, or sound if you only want a silent push. Including visual notification keys changes the push behavior.

Send background notification requests with APNs headers:

apns-push-type: background
apns-priority: 5

Rate Limiting

Apple throttles background push delivery. Guidelines:

  • Delivery is low priority and not guaranteed.
  • Do not send more than two or three background notifications per hour.
  • The system may hold only the newest background notification and discard older held notifications.
  • If the user force-quits the app, background pushes stop until the next manual launch.
  • Use apns-priority: 5; high-priority pushes are for user-visible notifications, not silent refresh.

Handling Push with Async Work

func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler:
        @escaping (UIBackgroundFetchResult) -> Void
) {
    guard let type = userInfo["type"] as? String else {
        completionHandler(.noData)
        return
    }

    Task {
        do {
            switch type {
            case "new-message":
                let conversationID = userInfo["conversation-id"] as? String
                let fetched = try await MessageService.shared
                    .fetchMessages(for: conversationID)
                completionHandler(fetched ? .newData : .noData)

            case "config-update":
                try await ConfigService.shared.refreshConfig()
                completionHandler(.newData)

            default:
                completionHandler(.noData)
            }
        } catch {
            completionHandler(.failed)
        }
    }
}

Important: You have approximately 30 seconds to call completionHandler. Failure to do so causes the system to penalize your app's background push budget.

SwiftUI BackgroundTask Modifier

SwiftUI provides a .backgroundTask modifier as an alternative to manual BGTaskScheduler registration:

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
            .backgroundTask(.appRefresh("com.example.app.refresh")) {
                await refreshFeed()
                // Schedule the next one
                scheduleAppRefresh()
            }
    }
}

This is a SwiftUI handler for matching background tasks. You still need the Info.plist identifiers, background modes, scheduling, and cancellation-safe work patterns.

BGContinuedProcessingTask — Extended Patterns

Checking Supported Resources

Before requesting GPU or other resources, verify the device supports them and enable Background GPU Access (com.apple.developer.background-tasks.continued-processing.gpu) for GPU work:

let supported = BGTaskScheduler.supportedResources
if supported.contains(.gpu) {
    request.requiredResources = .gpu
}

Submission Strategies

BGContinuedProcessingTaskRequest.SubmissionStrategy controls behavior when the system cannot run the task immediately:

Strategy Behavior
.queue Task is queued and starts as soon as possible
.fail Submission fails immediately if can't run now

Use .fail when the work is only relevant in the current moment (e.g., a user is waiting). Use .queue for work that can start whenever the system allows.

Cancellation by the User

The system shows a Live Activity for continued processing tasks. The user can cancel the task from there. Handle this in your expiration handler:

task.expirationHandler = {
    // Clean up partial work
    cleanupPartialExport()
    task.setTaskCompleted(success: false)
}

Progress Reporting

The system uses your Progress object to decide termination priority. Tasks with no progress updates are terminated first under resource pressure:

// Report fine-grained progress
let progress = task.progress
progress.totalUnitCount = Int64(totalItems)

for (index, item) in items.enumerated() {
    try Task.checkCancellation()
    await process(item)
    progress.completedUnitCount = Int64(index + 1)
}

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill is a collection of educational resources and code snippets for iOS developers implementing background tasks. It covers standard Apple frameworks like BackgroundTasks and URLSession. No malicious patterns, obfuscation, or unauthorized data access were found.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer6mo

    1/2 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 45c9085. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Steadyupdated 3 months ago
  • ios
  • swift
  • background-tasks
  • bgtaskscheduler
  • urlsession
  • push-notifications
  • scheduling
  • maintenance

README badge

README badge for dpearson2699/swift-ios-skills/background-processing

Registers and executes background work on iOS using BGTaskScheduler, background URLSession, and silent push notifications. Covers BGAppRefreshTask for short fetches, BGProcessingTask for long maintenance jobs, BGContinuedProcessingTask for foreground-started work that continues in background, and includes Info.plist configuration, expiration handling, and debugging patterns.

Generated from the current SKILL.md.

Does this skill cover all iOS background task types?
Yes. It covers BGAppRefreshTask for short fetches, BGProcessingTask for long-running maintenance, BGContinuedProcessingTask (iOS 26+) for foreground-started work, background URLSession downloads, and silent push notifications.
What Info.plist configuration is required?
Every task identifier must be declared under BGTaskSchedulerPermittedIdentifiers, and the corresponding UIBackgroundModes (fetch, processing) must be enabled in target capabilities.
When should I register task handlers?
Register handlers before app launch completes—in AppDelegate's didFinishLaunchingWithOptions for UIKit, or in the App initializer for SwiftUI.
What happens if I don't call setTaskCompleted?
The system penalizes future scheduling of that task type. You must call setTaskCompleted on every code path, including the expirationHandler.
Does BGContinuedProcessingTask work on iOS 25 and earlier?
No. BGContinuedProcessingTask requires iOS 26+. Use BGAppRefreshTask or BGProcessingTask for earlier versions.

Generated from the current SKILL.md. These answers refresh after source changes.