ios-push-and-background-tasks
Skill1 starsChanged 15 days agoMITView on GitHub
Implement push notifications (remote and local) and BackgroundTasks framework scheduling — registration, payload handling, notification actions, and background refresh/processing task budgets. Use when asked to add push notifications, handle a notification tap or action, schedule background work, debug a background task that never runs, or decide between a silent push and a scheduled background task.
In this repository37 other files it gives its agents.
- AGENTS.mdAGENTS.md
- ios-accessibility-auditSkill
- ios-api-contract-migrationSkill
- ios-api-endpointSkill
- ios-app-intents-siriSkill
- ios-bug-triageSkill
- ios-build-configurationSkill
- ios-cloudkit-syncSkill
- ios-combine-patternsSkill
- ios-commit-and-prSkill
- ios-commitSkill
- ios-concurrency-auditSkill
- ios-coreml-apple-intelligenceSkill
- ios-deep-linkingSkill
- ios-dependency-assessmentSkill
- ios-design-fixSkill
- ios-feature-moduleSkill
- ios-knowledge-baseSkill
- ios-latest-os-adoptionSkill
- ios-localizationSkill
- ios-offline-writeSkill
- ios-performance-tuningSkill
- ios-port-from-androidSkill
- ios-port-to-androidSkill
- ios-precommitSkill
- ios-privacy-complianceSkill
- ios-refactorSkill
- ios-release-prepSkill
- ios-sdk-upgradeSkill
- ios-secure-reviewSkill
- ios-storekit-subscriptionsSkill
- ios-swift6-concurrency-migrationSkill
- ios-swiftdataSkill
- ios-swift-testing-migrationSkill
- ios-swiftui-screenSkill
- ios-unit-testsSkill
- ios-widgetkit-live-activitiesSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 73
Always re-submit the next request from inside the handler (if this is a recurring refresh) — a BGAppRefreshTaskRequest is one-shot.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Push notifications and background tasks
Two different systems, often needed together: push notifications tell the app something happened; BackgroundTasks gets CPU time to actually do something about it when the app isn't in the foreground. Neither gives the app a guarantee about when — both are scheduled at the system's discretion, not the app's.
1. Registering for remote notifications
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
guard granted else { return }
await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }- Request authorization at the point the feature needing it is actually relevant (permission priming at launch, before the user has any reason to want notifications, is a common rejection-adjacent pattern and a bad conversion rate even where it's allowed) — same principle as permission requests in
ios-privacy-compliance. - The device token arrives asynchronously via
application(_:didRegisterForRemoteNotificationsWithDeviceToken:)— send it to your backend from there, and handledidFailToRegisterForRemoteNotificationsWithError:(no network, not actually entitled, simulator without a real APNs connection) as a real case, not a silently ignored one. - Re-register and re-send the token on meaningful app state changes (user re-signs-in as a different account, app reinstall) — a token tied to the wrong account silently misdelivers or fails to deliver push, which is much harder to debug after the fact than preventing it.
2. Handling the notification
// Foreground delivery:
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
.banner // or [] to suppress entirely for a notification the app handles itself
}
// Tap / action:
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let payload = response.notification.request.content.userInfo
guard let destination = route(fromPushPayload: payload) else { return }
await navigator.navigate(to: destination)
}- Route a notification tap through the same deep-link resolution as
ios-deep-linking— a push payload is just another source of "here's where the user should end up," not a separate ad hoc navigation path. - A silent push (
content-available: 1, no alert) exists to wake the app briefly to fetch/update state in the background — not to show UI. Keep that work bounded and fast; the OS gives silent pushes a short execution window, and iOS will throttle an app that abuses them for anything resembling polling. - A notification that arrives while the app is already showing the relevant content shouldn't duplicate or re-trigger navigation — check current state before navigating, the same discipline as any other "don't fetch/navigate twice" concern.
3. BackgroundTasks framework
// Register once, early (before applicationDidFinishLaunching returns):
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.widgetapp.refresh", using: nil) { task in
handleRefresh(task: task as! BGAppRefreshTask)
}
func scheduleRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.example.widgetapp.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
func handleRefresh(task: BGAppRefreshTask) {
task.expirationHandler = { /* cancel in-flight work; task is about to time out */ }
let operation = Task {
defer { task.setTaskCompleted(success: true) }
await performRefresh()
}
task.expirationHandler = { operation.cancel() }
}BGAppRefreshTaskis for short, periodic refreshes;BGProcessingTaskis for longer-running work (can require network/power connected) like a bulk sync or cleanup — pick based on the actual work's shape, not availability-driven habit.earliestBeginDateis a floor, not a schedule — the system decides the actual run time based on usage patterns, battery, and background activity budget; a background task that "never runs" in testing is very often working as designed (the system hasn't judged this a good moment), not broken. Debug withe -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"..."]in the LLDB console, or the Xcode scheme's background-task debug trigger, rather than waiting on real-world scheduling during development.- Always re-submit the next request from inside the handler (if this is a recurring refresh) — a
BGAppRefreshTaskRequestis one-shot; forgetting to reschedule after each run is the most common reason "background refresh used to work and now doesn't." - Respect
task.expirationHandler— cancel the in-flight work and callsetTaskCompleted(success:)promptly when it fires, or the system will kill the process entirely rather than iOS politely waiting; an app that gets killed mid-background-task repeatedly is penalized with a worse budget for future background execution. - Register every background task identifier declared in
Info.plist'sBGTaskSchedulerPermittedIdentifiers— an identifier scheduled but never registered (or vice versa) fails silently at the API level, not with an obvious crash.
4. Testing
- Local notifications and their actions can be tested directly via
UNUserNotificationCenterin a simulator/device without any push infrastructure — use this for the handling/routing logic, reserve real APNs sandbox testing for payload delivery itself. - Background task handlers are testable as plain async functions once separated from the
BGTaskSchedulerregistration glue — testperformRefresh()directly with the usual stubs, don't try to unit-test actual OS scheduling behavior. - Cancellation under
expirationHandlerneeds an explicit test: start the work, trigger the handler, assert partial work is actually cancelled rather than continuing to completion unobserved.
Verification
- Notification authorization is requested at a point tied to an actual feature, not speculatively at launch.
- Device-token failures and re-registration-on-account-change are both handled, not just the happy path.
- Notification taps/actions route through the same deep-link resolution as the rest of the app.
- Silent pushes do bounded, fast work only — nothing resembling continuous polling.
- Every
BGTaskSchedulerPermittedIdentifiersentry inInfo.plisthas a matchingregister(forTaskWithIdentifier:)call, and vice versa. - A recurring background task re-submits its next request from inside its own handler.
expirationHandleractually cancels in-flight work and the task reports completion promptly.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

