A type-safe, async/await Swift wrapper around Apple's UserNotifications framework. Schedule local notifications, handle user responses, and manage permissions with a clean, modern API.
- Async/Await — Modern concurrency throughout
- Thread-Safe — Fully
Sendable-compliant - Fluent Builder API — Chain methods to build notification content
- Multiple Triggers — Time interval, calendar, and location (iOS)
- Repeating Schedules — Daily, weekly, custom intervals
- Interactive Actions — Categories with buttons and text input
- Deep Linking — Built-in deep link support via
userInfo - Response Streaming —
AsyncStreamfor handling user interactions - Foreground Presentation — Control in-app notification display
| Platform | Minimum Version |
|---|---|
| iOS | 15.0 |
| macOS | 12.0 |
| Swift | 6.2 |
Add NotificationKit to your Package.swift:
dependencies: [
.package(url: "https://github.com/user/NotificationKit.git", from: "1.0.0")
]Then add it as a dependency to your target:
.target(
name: "MyApp",
dependencies: ["NotificationKit"]
)Or in Xcode: File > Add Package Dependencies and paste the repository URL.
import NotificationKit
// 1. Request permission
let granted = try await NotificationKit.shared.requestPermission()
// 2. Create content
let content = NotificationContent(title: "Hello", body: "Your first notification!")
.withSound(.default)
// 3. Schedule
try await NotificationKit.shared.schedule(
id: "hello",
content: content,
trigger: .timeInterval(5)
)// Default options (alert, sound, badge)
let granted = try await NotificationKit.shared.requestPermission()
// Custom options
let granted = try await NotificationKit.shared.requestPermission(
options: [.alert, .sound, .badge, .criticalAlert]
)
// Check current status
let permission = await NotificationKit.shared.currentPermission()
switch permission {
case .authorized: print("Authorized")
case .denied: print("Denied")
case .notDetermined: print("Not yet asked")
case .provisional: print("Provisional")
}Use the fluent builder API to configure notifications:
let content = NotificationContent(title: "Meeting Reminder", body: "Standup in 5 minutes")
.withSubtitle("Engineering Team")
.withSound(.default)
.withBadge(1)
.withCategory("MEETING")
.withDeepLink(URL(string: "myapp://meetings/123")!)
.withUserInfo(key: "priority", value: "high")
.withThreadIdentifier("meetings")// Fire once after 60 seconds
try await NotificationKit.shared.schedule(
id: "reminder",
content: content,
trigger: .timeInterval(60)
)
// Repeat every 2 hours
try await NotificationKit.shared.schedule(
id: "recurring",
content: content,
trigger: .timeInterval(7200, repeats: true)
)var dateComponents = DateComponents()
dateComponents.year = 2026
dateComponents.month = 3
dateComponents.day = 15
dateComponents.hour = 9
dateComponents.minute = 0
try await NotificationKit.shared.schedule(
id: "birthday",
content: content,
trigger: .calendar(dateComponents)
)// Daily at 9:30 AM
try await NotificationKit.shared.schedule(
id: "daily",
content: content,
repeatRule: .daily(hour: 9, minute: 30)
)
// Every Monday at 10:00 AM
try await NotificationKit.shared.schedule(
id: "weekly",
content: content,
repeatRule: .weekly(weekday: 2, hour: 10, minute: 0)
)
// Every 5 minutes
try await NotificationKit.shared.schedule(
id: "interval",
content: content,
repeatRule: .interval(300)
)#if os(iOS)
import CoreLocation
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01),
radius: 100,
identifier: "office"
)
region.notifyOnEntry = true
try await NotificationKit.shared.schedule(
id: "geofence",
content: content,
trigger: .location(region)
)
#endif// Define actions
let reply = NotificationAction(identifier: "REPLY", title: "Reply", options: [.foreground])
let delete = NotificationAction(identifier: "DELETE", title: "Delete", options: [.destructive])
let textInput = NotificationAction.textInput(
identifier: "QUICK_REPLY",
title: "Quick Reply",
buttonTitle: "Send",
placeholder: "Type a message..."
)
// Register a category
let messageCategory = NotificationCategory(
identifier: "MESSAGE",
actions: [reply, delete, textInput]
)
NotificationKit.shared.registerCategories([messageCategory])
// Assign category to content
let content = NotificationContent(title: "New Message", body: "Hey, are you free?")
.withCategory("MESSAGE")Task {
for await response in NotificationKit.shared.responses {
switch response.action {
case .default:
print("User tapped notification: \(response.notificationIdentifier)")
case .dismiss:
print("Notification dismissed")
case .custom(let actionId):
print("Custom action: \(actionId)")
if let text = response.userText {
print("User typed: \(text)")
}
}
// Handle deep links
if let url = response.deepLinkURL {
navigateTo(url)
}
}
}// Show banner and play sound when app is in foreground
NotificationKit.shared.setForegroundPresentation([.banner, .sound])// Cancel a specific notification
NotificationKit.shared.cancel(id: "reminder")
// Cancel all notifications
NotificationKit.shared.cancelAll()do {
try await NotificationKit.shared.schedule(
id: "test",
content: content,
trigger: .timeInterval(-1) // invalid
)
} catch let error as NotificationError {
switch error {
case .permissionDenied:
print("Permission denied")
case .invalidTrigger(let reason):
print("Invalid trigger: \(reason)")
case .schedulingFailed(let reason):
print("Scheduling failed: \(reason)")
case .categoryRegistrationFailed(let reason):
print("Category registration failed: \(reason)")
}
}| Type | Description |
|---|---|
NotificationKit |
Main entry point. Singleton via .shared or create custom instances. |
NotificationContent |
Notification payload with fluent builder methods. |
NotificationTrigger |
.timeInterval, .calendar, .location (iOS). |
NotificationRepeatRule |
.daily, .weekly, .interval, .custom repeat schedules. |
NotificationCategory |
Groups interactive actions for a notification type. |
NotificationAction |
A button or text input action on a notification. |
NotificationResponse |
User interaction data including action, deep link, and text input. |
NotificationPermission |
Permission status: .authorized, .denied, .notDetermined, .provisional. |
NotificationPermissionOptions |
Options to request: .alert, .sound, .badge, .criticalAlert, .provisional. |
NotificationError |
Typed errors for permission, trigger, scheduling, and category failures. |
A full SwiftUI demo app is included in the Example/ directory. It demonstrates:
- Requesting and displaying permission status
- Scheduling simple, rich, and deep-link notifications
- Registering interactive action categories
- Handling notification responses via
AsyncStream - Repeating notifications (interval and daily)
- Cancelling notifications and enabling foreground display
To run it:
cd Example
swift build # or open in XcodeOr open the Example/ folder in Xcode and run the NotificationKitDemo target on a simulator or device.
MIT License. See LICENSE for details.