Loading...
Loading...
Implement, review, or debug push notifications in iOS/macOS apps — local notifications, remote (APNs) notifications, rich notifications, notification actions, silent pushes, and notification service/content extensions. Use when working with UNUserNotificationCenter, registering for remote notifications, handling notification payloads, setting up notification categories and actions, creating rich notification content, or debugging notification delivery. Trigger for any task involving alerts, badges, sounds, background pushes, or user notification permissions in Swift apps.
npx skill4agent add dpearson2699/swift-ios-skills push-notificationsUserNotificationsimport UserNotifications
@MainActor
func requestNotificationPermission() async -> Bool {
let center = UNUserNotificationCenter.current()
do {
let granted = try await center.requestAuthorization(
options: [.alert, .sound, .badge]
)
return granted
} catch {
print("Authorization request failed: \(error)")
return false
}
}@MainActor
func checkNotificationStatus() async -> UNAuthorizationStatus {
let settings = await UNUserNotificationCenter.current().notificationSettings()
return settings.authorizationStatus
// .notDetermined, .denied, .authorized, .provisional, .ephemeral
}// Delivers silently -- no permission prompt shown to the user
try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])// Requires com.apple.developer.usernotifications.critical-alerts entitlement
try await center.requestAuthorization(
options: [.alert, .sound, .badge, .criticalAlert]
)@MainActor
func openNotificationSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}
// Usage: show a button/banner explaining why notifications matter,
// then call openNotificationSettings() on tap.UIApplicationDelegateAdaptor@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("APNs token: \(token)")
// Send token to your server
Task { await TokenService.shared.upload(token: token) }
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("APNs registration failed: \(error.localizedDescription)")
// Simulator always fails -- this is expected during development
}
}@MainActor
func registerForPush() async {
let granted = await requestNotificationPermission()
guard granted else { return }
UIApplication.shared.registerForRemoteNotifications()
}didRegisterForRemoteNotificationsWithDeviceTokenregisterForRemoteNotifications()let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.subtitle = "Time to move"
content.body = "You have a scheduled workout in 15 minutes."
content.sound = .default
content.badge = 1
content.userInfo = ["workoutId": "abc123"]
content.threadIdentifier = "workouts" // groups in notification center// Fire after a time interval (minimum 60 seconds for repeating)
let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)
// Fire at a specific date/time
var dateComponents = DateComponents()
dateComponents.hour = 8
dateComponents.minute = 30
let calendarTrigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents, repeats: true // daily at 8:30 AM
)
// Fire when entering a geographic region
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01),
radius: 100,
identifier: "gym"
)
region.notifyOnEntry = true
region.notifyOnExit = false
let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
// Requires "When In Use" location permission at minimumlet request = UNNotificationRequest(
identifier: "workout-reminder-abc123",
content: content,
trigger: timeTrigger
)
let center = UNUserNotificationCenter.current()
try await center.add(request)
// Remove specific pending notifications
center.removePendingNotificationRequests(withIdentifiers: ["workout-reminder-abc123"])
// Remove all pending
center.removeAllPendingNotificationRequests()
// Remove delivered notifications from notification center
center.removeDeliveredNotifications(withIdentifiers: ["workout-reminder-abc123"])
center.removeAllDeliveredNotifications()
// List all pending requests
let pending = await center.pendingNotificationRequests(){
"aps": {
"alert": {
"title": "New Message",
"subtitle": "From Alice",
"body": "Hey, are you free for lunch?"
},
"badge": 3,
"sound": "default",
"thread-id": "chat-alice",
"category": "MESSAGE_CATEGORY"
},
"messageId": "msg-789",
"senderId": "user-alice"
}content-available: 1{
"aps": {
"content-available": 1
},
"updateType": "new-data"
}func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
guard let updateType = userInfo["updateType"] as? String else {
return .noData
}
do {
try await DataSyncService.shared.sync(trigger: updateType)
return .newData
} catch {
return .failed
}
}mutable-content: 1{
"aps": {
"alert": { "title": "Photo", "body": "Alice sent a photo" },
"mutable-content": 1
},
"imageUrl": "https://example.com/photo.jpg"
}{
"aps": {
"alert": {
"title-loc-key": "NEW_MESSAGE_TITLE",
"loc-key": "NEW_MESSAGE_BODY",
"loc-args": ["Alice"]
}
}
}application(_:didFinishLaunchingWithOptions:)App.init@MainActor
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
static let shared = NotificationDelegate()
// Called when notification arrives while app is in FOREGROUND
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
// Return which presentation elements to show
// Without this, foreground notifications are silently suppressed
return [.banner, .sound, .badge]
}
// Called when user TAPS the notification
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
let userInfo = response.notification.request.content.userInfo
let actionIdentifier = response.actionIdentifier
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// User tapped the notification body
await handleNotificationTap(userInfo: userInfo)
case UNNotificationDismissActionIdentifier:
// User dismissed the notification
break
default:
// Custom action button tapped
await handleCustomAction(actionIdentifier, userInfo: userInfo)
}
}
}@Observable@Observable @MainActor
final class DeepLinkRouter {
var pendingDestination: AppDestination?
}
// In NotificationDelegate:
func handleNotificationTap(userInfo: [AnyHashable: Any]) async {
guard let id = userInfo["messageId"] as? String else { return }
DeepLinkRouter.shared.pendingDestination = .chat(id: id)
}
// In SwiftUI -- observe and consume:
.onChange(of: router.pendingDestination) { _, destination in
if let destination {
path.append(destination)
router.pendingDestination = nil
}
}references/notification-patterns.mdfunc registerNotificationCategories() {
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY_ACTION",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type a reply..."
)
let likeAction = UNNotificationAction(
identifier: "LIKE_ACTION",
title: "Like",
options: []
)
let deleteAction = UNNotificationAction(
identifier: "DELETE_ACTION",
title: "Delete",
options: [.destructive, .authenticationRequired]
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [replyAction, likeAction, deleteAction],
intentIdentifiers: [],
options: [.customDismissAction] // fires didReceive on dismiss too
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}func handleCustomAction(_ identifier: String, userInfo: [AnyHashable: Any]) async {
switch identifier {
case "REPLY_ACTION":
// response is UNTextInputNotificationResponse for text input actions
break
case "LIKE_ACTION":
guard let messageId = userInfo["messageId"] as? String else { return }
await MessageService.shared.likeMessage(id: messageId)
case "DELETE_ACTION":
guard let messageId = userInfo["messageId"] as? String else { return }
await MessageService.shared.deleteMessage(id: messageId)
default:
break
}
}.authenticationRequired.destructive.foregroundthreadIdentifierthread-idcontent.threadIdentifier = "chat-alice" // all messages from Alice group together
content.summaryArgument = "Alice"
content.summaryArgumentCount = 3 // "3 more notifications from Alice"let category = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [replyAction],
intentIdentifiers: [],
categorySummaryFormat: "%u more messages from %@",
options: []
)requestAuthorizationregisterForRemoteNotifications()String(data: deviceToken, encoding: .utf8)deviceToken.map { String(format: "%02x", $0) }.joined()mutable-content: 1willPresentwillPresent.banner.sound.badgeUNUserNotificationCenter.current().delegate.onAppearApp.initapplication(_:didFinishLaunchingWithOptions:)UIApplication.shared.registerForRemoteNotifications()UIApplicationDelegateAdaptordidFailToRegisterForRemoteNotificationsWithErrordidRegisterForRemoteNotificationsWithDeviceTokenString(data:encoding:)UNUserNotificationCenterDelegateApp.initapplication(_:didFinishLaunching:)willPresentdidReceiveUIApplicationDelegateAdaptorthreadIdentifierreferences/notification-patterns.mdreferences/rich-notifications.md