ios-push-notifications/SKILL.md
Implement push notifications with APNs, handle notification delivery, and create rich notifications. Use when setting up push notifications, debugging APNs, creating notification extensions, or handling notification actions. Triggers on push notification, APNs, notification, remote notification, UNUserNotificationCenter, rich notification, notification extension.
npx skillsauth add abanoub-ashraf/manus-skills-import ios-push-notificationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a push notifications specialist for iOS. When this skill activates, help implement robust notification handling.
In Xcode:
import UserNotifications
@Observable
class NotificationManager {
var isAuthorized = false
var deviceToken: String?
func requestAuthorization() async throws {
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [
.alert,
.badge,
.sound,
.provisional, // Deliver quietly without asking
.criticalAlert // Requires Apple approval
])
await MainActor.run {
isAuthorized = granted
}
if granted {
await registerForRemoteNotifications()
}
}
@MainActor
private func registerForRemoteNotifications() {
UIApplication.shared.registerForRemoteNotifications()
}
func checkAuthorizationStatus() async -> UNAuthorizationStatus {
let settings = await UNUserNotificationCenter.current().notificationSettings()
return settings.authorizationStatus
}
}
// In AppDelegate or App struct
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("Device Token: \(token)")
// Send to your server
Task {
try await APIClient.shared.registerDeviceToken(token)
}
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register: \(error)")
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
// Called when notification received while app is in foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
let userInfo = notification.request.content.userInfo
// Process notification data
await processNotificationPayload(userInfo)
// Choose how to present
return [.banner, .badge, .sound]
}
// Called when user taps 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
await handleNotificationTap(userInfo)
case UNNotificationDismissActionIdentifier:
// User dismissed the notification
break
case "REPLY_ACTION":
// Custom action
if let textResponse = response as? UNTextInputNotificationResponse {
await handleReply(textResponse.userText, userInfo: userInfo)
}
default:
await handleCustomAction(actionIdentifier, userInfo: userInfo)
}
}
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
UNUserNotificationCenter.current().delegate = appDelegate
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
func setupNotificationCategories() {
// Reply action with text input
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY_ACTION",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type your reply..."
)
// Like action
let likeAction = UNNotificationAction(
identifier: "LIKE_ACTION",
title: "❤️ Like",
options: []
)
// View action (opens app)
let viewAction = UNNotificationAction(
identifier: "VIEW_ACTION",
title: "View",
options: .foreground
)
// Delete action (destructive)
let deleteAction = UNNotificationAction(
identifier: "DELETE_ACTION",
title: "Delete",
options: .destructive
)
// Message category
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE",
actions: [replyAction, likeAction],
intentIdentifiers: [],
options: .customDismissAction
)
// Post category
let postCategory = UNNotificationCategory(
identifier: "POST",
actions: [viewAction, likeAction, deleteAction],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([
messageCategory,
postCategory
])
}
{
"aps": {
"alert": {
"title": "New Message",
"subtitle": "From John",
"body": "Hey, how are you?"
},
"badge": 5,
"sound": "default",
"category": "MESSAGE",
"thread-id": "chat-123",
"mutable-content": 1
},
"messageId": "abc123",
"senderId": "user456",
"chatId": "chat-123"
}
{
"aps": {
"content-available": 1
},
"dataToSync": "new-content"
}
{
"aps": {
"alert": {
"title": "Photo from John",
"body": "Check out this sunset!"
},
"mutable-content": 1,
"category": "PHOTO"
},
"mediaUrl": "https://example.com/photo.jpg",
"mediaType": "image"
}
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
guard let bestAttemptContent = bestAttemptContent else {
contentHandler(request.content)
return
}
// Modify notification content
Task {
await modifyNotification(bestAttemptContent)
contentHandler(bestAttemptContent)
}
}
private func modifyNotification(_ content: UNMutableNotificationContent) async {
// Download and attach media
if let urlString = content.userInfo["mediaUrl"] as? String,
let url = URL(string: urlString) {
do {
let attachment = try await downloadAttachment(from: url)
content.attachments = [attachment]
} catch {
print("Failed to download attachment: \(error)")
}
}
// Decrypt end-to-end encrypted message
if let encryptedBody = content.userInfo["encryptedBody"] as? String {
content.body = decrypt(encryptedBody) ?? content.body
}
}
private func downloadAttachment(from url: URL) async throws -> UNNotificationAttachment {
let (data, _) = try await URLSession.shared.data(from: url)
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent(UUID().uuidString + ".jpg")
try data.write(to: fileURL)
return try UNNotificationAttachment(identifier: UUID().uuidString, url: fileURL)
}
override func serviceExtensionTimeWillExpire() {
// Deliver best attempt before timeout (30 seconds)
if let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
// NotificationViewController.swift
import UIKit
import UserNotifications
import UserNotificationsUI
class NotificationViewController: UIViewController, UNNotificationContentExtension {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
func didReceive(_ notification: UNNotification) {
let content = notification.request.content
titleLabel.text = content.title
if let attachment = content.attachments.first,
attachment.url.startAccessingSecurityScopedResource() {
imageView.image = UIImage(contentsOfFile: attachment.url.path)
attachment.url.stopAccessingSecurityScopedResource()
}
}
func didReceive(
_ response: UNNotificationResponse,
completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void
) {
if response.actionIdentifier == "LIKE_ACTION" {
// Handle like
completion(.doNotDismiss)
} else {
completion(.dismissAndForwardAction)
}
}
}
func scheduleLocalNotification() async throws {
let content = UNMutableNotificationContent()
content.title = "Reminder"
content.body = "Don't forget to check your tasks!"
content.sound = .default
content.badge = 1
// Time-based trigger
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 3600, // 1 hour
repeats: false
)
// Calendar-based trigger
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let calendarTrigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents,
repeats: true // Daily at 9 AM
)
// Location-based trigger
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
radius: 100,
identifier: "office"
)
region.notifyOnEntry = true
let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
}
@MainActor
func updateBadgeCount(_ count: Int) {
UNUserNotificationCenter.current().setBadgeCount(count)
}
@MainActor
func clearBadge() {
UNUserNotificationCenter.current().setBadgeCount(0)
}
# Send test notification (requires APNs key)
curl -v \
--header "authorization: bearer $JWT_TOKEN" \
--header "apns-topic: com.company.app" \
--header "apns-push-type: alert" \
--data '{"aps":{"alert":"Test"}}' \
https://api.push.apple.com/3/device/$DEVICE_TOKEN
| Issue | Solution | |-------|----------| | No token received | Check capabilities, provisioning profile | | Notifications not delivered | Verify token sent to server, check APNs certificate | | Background not working | Add Background Modes capability | | Extension not called | Add mutable-content: 1 to payload |
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes