SwiftUI Background Tasks in iOS 26: The Complete Guide to BGAppRefreshTask, BGProcessingTask, and BGContinuedProcessingTask

A complete iOS 26 guide to BackgroundTasks in SwiftUI: schedule app refresh, run long processing tasks, and use the new BGContinuedProcessingTask to finish foreground work after the user leaves. With Swift 6 code and LLDB testing.

SwiftUI Background Tasks iOS 26: Guide 2026

Background execution on iOS used to be a black box. You scheduled a task, hoped the system would actually run it, and shrugged when it didn't. iOS 26 reshapes that model with BGContinuedProcessingTask — a third task type that bridges foreground work into the background with a visible system UI. Combined with the SwiftUI .backgroundTask scene modifier and the older BGAppRefreshTask and BGProcessingTask requests, you now have three sharp tools instead of one blunt one.

So, let's dive in. This guide walks through every task type with working Swift 6 code, the exact Info.plist and capability setup, idiomatic SwiftUI integration, and the LLDB tricks you need to actually test background tasks on a real device. By the end, you'll know which task type fits each use case, how to keep the system happy enough to run your code at all, and how to debug the (many) cases where it doesn't.

The three background task types at a glance

Before iOS 26, the BackgroundTasks framework gave you two request types: a short app refresh task for fetching data, and a longer processing task for heavy work that could wait until the device was idle. iOS 26 adds a user-initiated continuation task. Here's how they stack up:

Task typeDurationTriggerTypical use case
BGAppRefreshTask~30 secondsSystem-scheduled, opportunisticQuick data fetches, feed refreshes, badge updates
BGProcessingTaskMinutes, while device is idleSystem-scheduled, requires idle deviceDatabase migrations, ML model updates, large file processing
BGContinuedProcessingTaskUntil completion or system stopUser-initiated in the foreground, continues in backgroundVideo exports, large uploads, archive compression

The mental shift in iOS 26 is that BGContinuedProcessingTask isn't opportunistic at all. The user explicitly taps a button (export, upload, compress), and the system commits to letting that work finish, displaying progress UI even after the app backgrounds. You aren't begging the scheduler for time anymore — you're handing off in-flight work.

Honestly, that's the part I've been waiting on for years.

One-time project setup

None of the code below runs without two pieces of project configuration. Get these wrong and your task handlers will silently never fire (ask me how I know).

1. Enable the Background Modes capability

In Xcode, select your target, open Signing & Capabilities, click + Capability, and add Background Modes. Check the modes that match the task types you plan to use:

  • Background fetch — required for BGAppRefreshTask.
  • Background processing — required for BGProcessingTask and BGContinuedProcessingTask.

2. Declare permitted identifiers in Info.plist

iOS will reject any task identifier that isn't pre-declared. Add the BGTaskSchedulerPermittedIdentifiers array to your Info.plist (Xcode 16+ shows this under the target's Info tab, or you can edit the source file directly):

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.swiftcrafted.example.refresh</string>
    <string>com.swiftcrafted.example.cleanup</string>
    <string>com.swiftcrafted.example.export</string>
</array>

Use reverse-DNS identifiers, and keep them stable. They survive across launches and are how the system routes a scheduled task back to your handler.

BGAppRefreshTask: short, frequent updates

BGAppRefreshTask gives you roughly 30 seconds of CPU time, usually while the device is plugged in or otherwise convenient for iOS to wake your app. It's perfect for keeping a feed warm, refreshing tokens, or updating a widget's timeline.

SwiftUI integration with the .backgroundTask modifier

SwiftUI exposes a backgroundTask(_:action:) scene modifier that registers and handles the task in one place — no AppDelegate required. This is the idiomatic iOS 26 pattern:

import SwiftUI
import BackgroundTasks

@main
struct FeedApp: App {
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { _, newPhase in
            if newPhase == .background {
                scheduleAppRefresh()
            }
        }
        .backgroundTask(.appRefresh("com.swiftcrafted.example.refresh")) {
            await refreshFeed()
        }
    }

    private func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(
            identifier: "com.swiftcrafted.example.refresh"
        )
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        try? BGTaskScheduler.shared.submit(request)
    }

    private func refreshFeed() async {
        do {
            let articles = try await FeedClient.shared.latest()
            try await ArticleStore.shared.save(articles)
        } catch {
            // Surface the error to your logging/telemetry pipeline.
        }
    }
}

Why submit every time the app backgrounds

BGTaskScheduler holds at most one pending request per identifier. Submitting again from scenePhase == .background is the canonical pattern: it refreshes the earliestBeginDate, recovers from a missed previous submission, and costs essentially nothing if a request is already queued.

One thing to keep in mind: earliestBeginDate is a floor, not a deadline. The system may wait hours longer based on usage patterns, battery, and thermal state. Don't treat it as a promise.

BGProcessingTask: long, opportunistic work

BGProcessingTask trades immediacy for runtime. iOS only runs it while the device is idle (typically overnight, on charger), but gives your code minutes instead of seconds. The moment the user picks up the phone, iOS will signal expiration — and you must stop within a few seconds.

import BackgroundTasks

func scheduleNightlyCleanup() {
    let request = BGProcessingTaskRequest(
        identifier: "com.swiftcrafted.example.cleanup"
    )
    request.requiresNetworkConnectivity = true
    request.requiresExternalPower = true
    request.earliestBeginDate = nextRunAfterMidnight()

    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("Cleanup schedule failed: \(error)")
    }
}

private func nextRunAfterMidnight() -> Date {
    let calendar = Calendar.current
    let tomorrow = calendar.date(byAdding: .day, value: 1, to: Date())!
    return calendar.startOfDay(for: tomorrow)
}

Wire the handler with the SwiftUI scene modifier:

.backgroundTask(.appRefresh("com.swiftcrafted.example.cleanup")) {
    await CleanupService.shared.run()
}

Inside the handler, you must cooperate with the Task cancellation protocol. When the system signals expiration, SwiftUI cancels the surrounding Task, and your async work should bail out gracefully:

actor CleanupService {
    static let shared = CleanupService()

    func run() async {
        for batch in await pendingBatches() {
            if Task.isCancelled { break }
            await purge(batch)
        }
    }
}

BGContinuedProcessingTask: the iOS 26 game-changer

This is the API that genuinely changes how you ship features like video export or large uploads. The flow looks like this:

  1. The user taps an explicit action in your UI (e.g. "Export 4K video").
  2. Your code starts the work in the foreground and submits a BGContinuedProcessingTaskRequest.
  3. The user switches apps or locks the device. iOS keeps your task alive, showing a system progress UI that the user can tap to return to your app.
  4. You report progress to the system; when the task finishes, the UI auto-dismisses.

Submitting a continued processing request

import BackgroundTasks

func startExport(for asset: VideoAsset) {
    let request = BGContinuedProcessingTaskRequest(
        identifier: "com.swiftcrafted.example.export",
        title: "Exporting \(asset.name)",
        subtitle: "4K \u{2022} H.265"
    )
    request.strategy = .fail // or .queue to wait if too many are running

    do {
        try BGTaskScheduler.shared.submit(request)
        Task { await ExportEngine.shared.run(asset) }
    } catch BGTaskScheduler.Error.tooManyPendingTaskRequests {
        showAlert("Too many exports already in progress.")
    } catch {
        showAlert("Could not start export: \(error.localizedDescription)")
    }
}

The title and subtitle strings show up in the system progress UI, so localize them like any other user-facing string. The strategy property is new in iOS 26: .fail rejects the request when the per-app limit is hit, while .queue waits its turn.

Handling the task and reporting progress

.backgroundTask(.continuedProcessing("com.swiftcrafted.example.export")) { task in
    let progress = Progress(totalUnitCount: 100)
    task.progress = progress

    await ExportEngine.shared.export(
        onProgress: { fraction in
            progress.completedUnitCount = Int64(fraction * 100)
        }
    )
}

Two things worth pointing out:

  • The closure receives the BGContinuedProcessingTask instance, so you can assign a Progress object that drives the visible system UI.
  • You don't need a separate setTaskCompleted(success:) call — when the async closure returns, the task completes. That's it.

If the system needs to terminate your task early (thermal pressure, system shutdown, whatever), the closure's surrounding Task is cancelled. Always check Task.isCancelled inside long loops, and persist enough state to resume on the next launch.

Coordinating with URLSession background uploads

For pure upload/download work, a URLSession background configuration is still the right tool. It survives termination and is handled by nsurlsessiond, not your process. SwiftUI ties it together with another backgroundTask variant:

.backgroundTask(.urlSession("com.swiftcrafted.example.upload")) {
    await UploadCoordinator.shared.handleEvents(for: "com.swiftcrafted.example.upload")
}

Pair this with a URLSessionConfiguration.background(withIdentifier:) session that uses isDiscretionary = true for non-urgent transfers, and let iOS pick an optimal moment (Wi-Fi, power, off-peak).

Battery transparency in iOS 26

iOS 26 surfaces per-app background battery usage in Settings > Battery, and users can disable background activity for individual apps from that screen. The practical implication? Avoid scheduling speculative work. Tasks that fail, run hot, or do nothing useful are now visible to users — and they will turn you off.

Two habits keep you in good standing:

  1. Always set earliestBeginDate based on actual data freshness needs, not "as soon as possible."
  2. Profile your handler with Instruments' Time Profiler on a real device. Background CPU time counts double against battery perception because it's invisible to the user.

Testing background tasks (the only way that works)

Two unavoidable facts:

  • The simulator does not run scheduled background tasks. You must test on a physical device.
  • The system makes no guarantees about when your task will run. Waiting for "natural" execution can take days.

Thankfully, Apple ships two private LLDB helpers for development use:

# Force a scheduled task to launch right now
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.swiftcrafted.example.refresh"]

# Simulate the system expiring your task mid-flight
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.swiftcrafted.example.refresh"]

The typical workflow:

  1. Run on a real device attached to Xcode.
  2. Background the app (press the home indicator).
  3. Pause the debugger and paste the simulate-launch command in the LLDB console.
  4. Resume — your handler runs immediately.

You can also inspect the scheduler queue at any point:

Task {
    let pending = await BGTaskScheduler.shared.pendingTaskRequests()
    for request in pending {
        print(request.identifier, request.earliestBeginDate ?? "no floor")
    }
}

Common failure modes

  • "No identifier registered" crash on launch. The task identifier in your code doesn't match BGTaskSchedulerPermittedIdentifiers. Identifiers are case-sensitive and must match exactly — one stray uppercase letter and you're done.
  • Task never runs in production. Often caused by users who force-quit the app. iOS treats force-quit as "the user doesn't want this app to do anything" and stops scheduling. There's no fix; document it and design around it.
  • Handler times out at 30 seconds. You used BGAppRefreshTask for work that needed BGProcessingTask. The fix is structural: split the workload or switch task types.
  • Expiration cancels mid-write. Wrap any database or file-system commits in a transaction, and check Task.isCancelled before opening it. Don't start writes you can't finish in a few seconds.

FAQ

What's the difference between BGAppRefreshTask and BGProcessingTask?

BGAppRefreshTask is short (around 30 seconds), runs frequently, and isn't gated on device idleness — it's for keeping data fresh. BGProcessingTask can run for minutes, but only when the device is idle, and you can require external power or network connectivity. Use the first for quick fetches, the second for heavy or maintenance work.

Why isn't my background task running on the simulator?

The simulator doesn't support BGTaskScheduler execution. You have to run on a real device. During development, use the _simulateLaunchForTaskWithIdentifier: LLDB command while attached to Xcode to force the task to run on demand.

Can BGContinuedProcessingTask start without user interaction?

No. BGContinuedProcessingTask is explicitly user-initiated — it must be submitted in direct response to a user action (a tap, a swipe). The system surfaces UI to the user about the running task, so silent or speculative use just isn't allowed.

How often does iOS actually run BGAppRefreshTask?

There's no guarantee. iOS schedules refreshes based on how often the user opens your app, battery state, time of day, network reachability, and thermal conditions. Apps used multiple times a day may see several refreshes per day; rarely-used apps may see one a week — or none if the user force-quit recently.

Does the new BGContinuedProcessingTask work on iPadOS and macOS?

Yes on iPadOS 26 — the API was introduced in parallel for both platforms. On macOS the same task type isn't needed, because Mac apps already run unrestricted in the background; use a regular Task and let the user quit the app to stop it.

Should I use BackgroundTasks or URLSession background uploads for large transfers?

For pure file transfer, prefer a URLSessionConfiguration.background session — it runs in nsurlsessiond and survives even if your app is terminated. Use BGContinuedProcessingTask when the work involves CPU processing alongside the transfer (encoding, encryption, compression) that needs your app's code to run.

Wrapping up

The iOS 26 BackgroundTasks framework finally has the right shape for the three real-world problems iOS apps face: keep data fresh, run heavy work overnight, and finish what the user started. The SwiftUI .backgroundTask scene modifier ties them all together without an AppDelegate, and the new BGContinuedProcessingTask is the first time iOS has given third-party developers a first-class story for foreground-to-background continuation.

Set up your identifiers, pick the right task type, cooperate with cancellation, and test on a real device with the LLDB helpers. The system will do the rest — most of the time.

About the Author Tomasz Wojcik

Tomasz is a Krakow-based iOS engineer with 11 years of Swift experience. He spent four years at Revolut on the Wealth team, where he rewrote the trading charts in SwiftUI and shaved 40% off cold-start time by lazy-loading the analytics SDK. Before Revolut he was at Allegro, Poland's largest e-commerce platform, on the Seller Center iOS team. His specialty is iOS performance work: Instruments deep-dives, memory-graph debugging, and figuring out why your scroll view drops frames only on iPhone SE 2nd-gen. He has contributed patches to swift-syntax and writes a quarterly newsletter for iOS engineers that covers under-discussed APIs like BackgroundTasks and NSFileCoordinator. Tomasz holds the iOS App Development with Swift certification from Apple and occasionally runs paid workshops on Swift concurrency for in-house engineering teams in Europe.