Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Overflow reference for the background-processing skill. Contains debugging tips, advanced background URLSession patterns, background push best practices, and SwiftUI integration patterns.
Use the LLDB console to trigger tasks instantly during development. The app must be running in the debugger with a breakpoint hit or paused.
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"]Check what tasks are currently scheduled:
BGTaskScheduler.shared.getPendingTaskRequests { requests in
for request in requests {
print("Pending: \(request.identifier), earliest: \(String(describing: request.earliestBeginDate))")
}
}| 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 | Device in Low Power Mode | Low Power Mode suppresses background tasks |
.notPermitted error | Identifier mismatch | Verify exact string match between code and plist |
.unavailable error | Running in extension | BGTaskScheduler not available in app extensions |
.tooManyPendingTaskRequests | Over 10 pending refresh or 1 processing per identifier | Cancel old requests before submitting new ones |
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)")
}
}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)
}
}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 = truefunc 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.
When the system completes a background transfer and your app is not running, it relaunches the app. You must:
application(_:handleEventsForBackgroundURLSession:completionHandler:)URLSession with the same identifierurlSessionDidFinishEvents// 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
}
}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()
}
}
}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
)
}
}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.
Apple throttles background push delivery. Guidelines:
apns-priority: 10) are delivered immediately but must
only be used when the notification triggers user-visible contentapns-priority: 5) are batched and delivered
opportunisticallyfunc 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 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 convenience wrapper. Under the hood it registers with
BGTaskScheduler. You still need the Info.plist identifiers and background
modes.
Before requesting GPU or other resources, verify the device supports them:
let supported = BGTaskScheduler.shared.supportedResources
if supported.contains(.gpu) {
request.requiredResources = .gpu
}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.
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)
}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)
}skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references