Imported from Yar177/appleNativeSkills (
apple-system-integration/SKILL.md). Install upstream withnpx skills add Yar177/appleNativeSkills --skill apple-system-integration. Copyright stays with the author.
Apple System Integration
Connect an app to the rest of the system: the user's calendars and reminders, notifications, Siri/Shortcuts, background execution, and widgets/Live Activities. Permission-first, async/await-native, current through iOS 18 / macOS 15.
Contents
- Capability Router
- The Permission-First Rule
- EventKit
- User Notifications
- App Intents (Siri / Shortcuts)
- Background Execution
- Widgets & Live Activities
- Info.plist & Entitlements Quick Reference
- Common Mistakes
- Review Checklist
- References
Capability Router
| You want to... | Use | Key API |
|---|---|---|
| Read/write the user's calendar events | EventKit | EKEventStore, EKEvent |
| Read/write reminders | EventKit | EKReminder |
| Repeating events/reminders | EventKit | EKRecurrenceRule |
| Schedule a local reminder/alert | User Notifications | UNUserNotificationCenter |
| Fire at a wall-clock time / repeating | User Notifications | UNCalendarNotificationTrigger |
| Fire after a delay | User Notifications | UNTimeIntervalNotificationTrigger |
| Actionable / categorized notifications | User Notifications | UNNotificationAction, UNNotificationCategory |
| Expose an action to Siri / Shortcuts | App Intents | AppIntent, AppShortcutsProvider |
| Pass typed app objects to intents | App Intents | AppEntity, @Parameter |
| Periodic background refresh | Background Tasks | BGAppRefreshTaskRequest |
| Longer background maintenance | Background Tasks | BGProcessingTaskRequest |
| Big download/upload that survives suspension | Background URLSession | URLSessionConfiguration.background |
| Home/Lock Screen widget | WidgetKit | TimelineProvider, Widget |
| Interactive widget (button/toggle) | WidgetKit + App Intents | Button(intent:) |
| Live, ongoing status (Dynamic Island) | ActivityKit | Activity, ActivityAttributes |
Routing principles:
- System scheduling beats your own timers. A
UNCalendarNotificationTriggersurvives app suspension; an in-appTimerdoes not. - App Intents over the legacy SiriKit/Intents framework for new Siri/Shortcuts work - no Siri entitlement required.
- EventKit for the user's real calendar; User Notifications for your app's reminders. Don't create calendar events just to get an alert.
- Background execution is best-effort and rate-limited - never assume a task runs at a precise time.
The Permission-First Rule
Every capability here is gated by user trust. Get this wrong and the API silently no-ops or the app is rejected.
- Add the usage string to Info.plist before you call the API (missing strings = guaranteed crash on iOS).
- Request at the moment of need, with context, not at launch.
- Re-check status every time - users revoke access in Settings.
- Degrade gracefully when denied: explain, offer Settings, keep the rest of the app working.
| Capability | Info.plist key |
|---|---|
| Calendar (full) | NSCalendarsFullAccessUsageDescription |
| Calendar (write-only) | NSCalendarsWriteOnlyAccessUsageDescription |
| Reminders | NSRemindersFullAccessUsageDescription |
| Notifications | (no plist key; runtime authorization prompt) |
EventKit
Access the user's calendars and reminders. iOS 17 / macOS 14 split authorization into write-only and full access - request the least you need.
import EventKit
let store = EKEventStore()
// Write-only is enough to ADD events without reading the user's calendar.
let granted = try await store.requestWriteOnlyAccessToEvents()
// Full access only if you must READ existing events/reminders:
// let granted = try await store.requestFullAccessToEvents()
let event = EKEvent(eventStore: store)
event.title = "Focus block"
event.startDate = start
event.endDate = end
event.calendar = store.defaultCalendarForNewEvents
try store.save(event, span: .thisEvent)
Rules:
- Prefer
requestWriteOnlyAccessToEvents()for add-only features; onlyrequestFullAccessToEvents()when you genuinely read the calendar. - Map the iOS 17 statuses (
.fullAccess,.writeOnly,.denied,.notDetermined,.restricted) - the old.authorizedis deprecated. - Run
events(matching:)off the main actor (it's synchronous and can be slow). - Store an event's
eventIdentifierto update/delete later; never duplicate.
See references/eventkit.md for reminders, recurrence
(EKRecurrenceRule), predicates/queries, editing/deleting, and change
notifications.
User Notifications
Schedule your app's local alerts (and handle push). Survives suspension; the right tool for "remind me at 5pm".
import UserNotifications
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
let content = UNMutableNotificationContent()
content.title = "Daily review"
content.sound = .default
var date = DateComponents(); date.hour = 17; date.minute = 0
let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)
let request = UNNotificationRequest(identifier: "daily-review", content: content, trigger: trigger)
try await center.add(request)
Rules:
- Use a stable identifier so re-scheduling replaces rather than duplicates;
reconcile with
removePendingNotificationRequests(withIdentifiers:). - iOS caps 64 pending requests per app - schedule a rolling window, not hundreds.
- Set a
UNUserNotificationCenterDelegateto show notifications in-foreground and handle taps/actions. UNCalendarNotificationTriggerfor wall-clock/repeating;UNTimeIntervalNotificationTriggerfor delays (min 60s if repeating).
See references/notifications.md for actions and categories, foreground presentation, rich attachments, badge management, the delegate, and push (APNs) basics.
App Intents (Siri / Shortcuts)
Expose app actions to Siri, Shortcuts, Spotlight, and widgets with no Siri
entitlement. Define an AppIntent; surface it via AppShortcutsProvider.
import AppIntents
struct AddTaskIntent: AppIntent {
static let title: LocalizedStringResource = "Add Task"
static var openAppWhenRun: Bool { false }
@Parameter(title: "Title") var taskTitle: String
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> {
let id = TaskStore.shared.add(title: taskTitle)
return .result(value: id)
}
}
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(intent: AddTaskIntent(), phrases: ["Add a task in \(.applicationName)"])
}
}
Rules:
- An intent that runs headless (
openAppWhenRun = false) cannot touch the SwiftUI environment - build a dedicatedModelContainer/data path for it. - Use
AppEntity+@Parameterto pass typed objects and offer pickers. - Keep
perform()fast and idempotent; throw to report failure to Siri/Shortcuts.
See references/app-intents.md for entities and queries, parameter resolution/disambiguation, app shortcuts and phrases, interactive snippets/dialog, and widget-driven intents.
Background Execution
Run work while suspended - best-effort, system-scheduled, rate-limited. Never assume precise timing.
import BackgroundTasks
// 1. Register at launch (also list the identifier in Info.plist BGTaskSchedulerPermittedIdentifiers).
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.refresh", using: nil) { task in
handleRefresh(task as! BGAppRefreshTask)
}
// 2. Schedule a request.
func scheduleRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
// 3. Handle: set an expiration handler, finish promptly, reschedule.
func handleRefresh(_ task: BGAppRefreshTask) {
scheduleRefresh() // chain the next one
let work = Task { await refreshData(); task.setTaskCompleted(success: true) }
task.expirationHandler = { work.cancel(); task.setTaskCompleted(success: false) }
}
Rules:
- Register every identifier in
BGTaskSchedulerPermittedIdentifiersand callregisterbeforeapplication(didFinishLaunching)returns. - Always set an
expirationHandlerand callsetTaskCompleted- or the system throttles you. BGAppRefreshTaskRequestfor short, frequent updates;BGProcessingTaskRequestfor longer/maintenance (oftenrequiresExternalPower).- For large transfers use a background
URLSession, not a BG task.
See references/background-tasks.md for processing
tasks, background URLSession, testing with the debugger, budget/throttling, and
BGContinuedProcessingTask.
Widgets & Live Activities
Surface glanceable content and live status. WidgetKit drives Home/Lock Screen widgets; ActivityKit drives Live Activities and the Dynamic Island.
import WidgetKit
import SwiftUI
struct NextTaskWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "NextTask", provider: Provider()) { entry in
NextTaskView(entry: entry)
}
}
}
// Interactive: Button(intent: CompleteTaskIntent(id: entry.id)) { ... }
// Refresh: WidgetCenter.shared.reloadTimelines(ofKind: "NextTask")
Rules:
- Share data with the widget via an App Group container; widgets can't read the app's private store.
- Drive interactivity with App Intents (
Button(intent:)/Toggle(intent:)), not custom URLs. - Live Activities require
NSSupportsLiveActivities = YES; update viaactivity.update(...)and end withactivity.end(...). - Timelines are budgeted - don't request second-by-second reloads.
See references/widgets-live-activities.md
for TimelineProvider, App Group setup, interactive widgets, Live Activity
lifecycle, and the Dynamic Island. For deep WidgetKit layout/animation, defer to a
dedicated widgetkit skill if one is installed.
Info.plist & Entitlements Quick Reference
| Feature | Info.plist / entitlement |
|---|---|
| Calendar full / write-only | NSCalendarsFullAccessUsageDescription / NSCalendarsWriteOnlyAccessUsageDescription |
| Reminders | NSRemindersFullAccessUsageDescription |
| Background tasks | BGTaskSchedulerPermittedIdentifiers + UIBackgroundModes (fetch/processing) |
| Background URLSession | UIBackgroundModes not required; uses background config |
| Live Activities | NSSupportsLiveActivities = YES |
| Widgets / shared data | App Group entitlement (group.com.app) |
| Push notifications | Push Notifications capability + APNs |
Common Mistakes
- Missing Info.plist usage string before calling an API - hard crash on iOS.
- Requesting full calendar access when write-only suffices (privacy review risk, worse approval rate).
- Treating old EventKit statuses (
.authorized) as the only success - missing.writeOnly/.fullAccesson iOS 17+. - Using an in-app
Timerfor reminders - dies on suspension; use a notification trigger. - Duplicate notifications from non-stable identifiers / not reconciling pending requests; or exceeding the 64-pending cap.
- Forgetting the foreground delegate - notifications silently don't show while the app is open.
- App Intent touching the SwiftUI environment when running headless - build a separate data path.
- Background task not registered in Info.plist / before launch finishes, or no
expirationHandler/setTaskCompleted- the system throttles you. - Assuming background tasks run on time - they're best-effort.
- Widget reading the app's private store instead of an App Group container.
Review Checklist
- Every capability has its Info.plist usage string / entitlement.
- Permissions requested at point of need; status re-checked; denial handled.
- EventKit uses the least access (write-only when possible) and maps iOS 17 statuses; queries run off the main actor; identifiers reused for edits.
- Notifications use stable identifiers, reconcile pending requests, stay under 64, set a delegate for foreground/taps.
- App Intents are fast, idempotent, headless-safe; shortcuts have natural phrases.
- Background tasks registered in Info.plist and before launch returns; set expiration + completion; reschedule; large transfers use background URLSession.
- Widgets share data via App Group; interactivity via App Intents; Live Activities declared and ended properly; timeline reloads are reasonable.
- Concurrency correct:
@MainActorwhere touching UI/SwiftData main context; typesSendable.
References
- references/eventkit.md -- events & reminders CRUD, iOS 17 authorization, recurrence, queries, change notifications.
- references/notifications.md -- authorization, triggers, actions/categories, foreground presentation, attachments, badges, delegate, push.
- references/app-intents.md -- intents, entities/queries, parameters, app shortcuts/phrases, snippets, widget intents.
- references/background-tasks.md -- app-refresh vs processing tasks, scheduling, expiration, background URLSession, testing, throttling.
- references/widgets-live-activities.md -- TimelineProvider, App Groups, interactive widgets, ActivityKit lifecycle, Dynamic Island.
Apple documentation:
