NnReminderKit is a Swift package designed to simplify the scheduling and management of local notifications, including countdown, calendar-based, and location-based reminders. It provides a clean, SwiftUI-friendly API for handling permissions, scheduling, canceling, and loading pending reminders.
- Features
- Installation
- Usage
- UI Test Helpers
- Architecture Notes
- Documentation
- About This Project
- Contributing
- License
- Request and handle notification permissions with SwiftUI view modifiers.
- Schedule and cancel countdown (one-time) reminders.
- Schedule and cancel calendar-based (recurring) weekday reminders.
- Schedule daily repeating reminders or one-time reminders at specific times.
- Schedule and manage location-based reminders.
- Load all pending reminders with detailed metadata.
- Clean abstraction for unit testing and previewing reminder behavior.
- UI test helper for dismissing notification permission alerts.
Add the following dependency to your Package.swift:
.package(url: "https://github.com/nikolainobadi/NnReminderKit", from: "1.4.0")Add NnReminderKit to your app target:
.product(name: "NnReminderKit", package: "NnReminderKit")Add NnReminderUITestHelpers to your UI test target:
.product(name: "NnReminderUITestHelpers", package: "NnReminderKit")Use .optionalNotificationPermissionsRequest when notifications enhance but aren't essential for your app. This modifier requests permissions first, then shows content regardless of the user's decision:
struct ContentView: View {
@State private var notificationsGranted = false
var body: some View {
YourMainContent()
.optionalNotificationPermissionsRequest(
permissionGranted: $notificationsGranted,
options: [.alert, .badge, .sound],
detailView: { requestPermission in
VStack {
Text("Enable notifications to get reminders")
Button("Enable Notifications", action: requestPermission)
Button("Skip") {
// User can skip - will show content with notificationsGranted = false
requestPermission() // This will set permission to denied
}
}
}
)
.onChange(of: notificationsGranted) { _, granted in
print("Notifications \(granted ? "enabled" : "disabled")")
}
}
}Use .requiredNotificationPermissionsRequest when notifications are essential for core functionality:
ReminderAppContent()
.requiredNotificationPermissionsRequest(
options: [.alert, .badge, .sound],
detailView: { requestPermission in
VStack {
Text("Notifications are required for this app to function")
Button("Enable Notifications", action: requestPermission)
}
},
deniedView: { settingsURL in
VStack {
Text("Notifications are disabled. Please enable them in settings.")
ShowNotificationSettingsButton {
Text("Open Settings")
}
}
}
)Use the ShowNotificationSettingsButton component anywhere in your app:
ShowNotificationSettingsButton() // Uses default "Open Settings" text
// Or with custom content:
ShowNotificationSettingsButton {
Label("Notification Settings", systemImage: "gear")
}let reminderManager = NnReminderManager()
Task {
let granted = await reminderManager.requestAuthPermission(options: [.alert, .badge, .sound])
print("Permissions granted: \(granted)")
}let countdownReminder = CountdownReminder(
id: "water_reminder",
title: "Drink Water",
message: "Stay hydrated!",
repeating: false,
timeInterval: 3600
)
Task {
try await reminderManager.scheduleCountdownReminder(countdownReminder)
}let reminderTime = Date.createReminderTime(hour: 8, minute: 30)
let eveningReminder = Date.createReminderTime(hour: 17, minute: 0)let reminder = WeekdayReminder(
id: "morning_reminder",
title: "Morning Reminder",
message: "Start your day!",
time: Date.createReminderTime(hour: 8, minute: 30),
repeating: true,
daysOfWeek: [.monday, .wednesday, .friday]
)
try await reminderManager.scheduleWeekdayReminder(reminder)let mondayReminder = WeekdayReminder(
id: "monday_reminder",
title: "Workout",
message: "Time for your Monday workout!",
time: Date.createReminderTime(hour: 7, minute: 0),
repeating: true,
daysOfWeek: [.monday]
)
let weekendReminder = WeekdayReminder(
id: "weekend_reminder",
title: "Stretch",
message: "Weekend stretch reminder!",
time: Date.createReminderTime(hour: 8, minute: 0),
repeating: true,
daysOfWeek: [.saturday, .sunday]
)
try await reminderManager.scheduleWeekdayReminder(mondayReminder)
try await reminderManager.scheduleWeekdayReminder(weekendReminder)To create a reminder that fires every day at the same time, use WeekdayReminder with an empty daysOfWeek array. This creates a single notification that repeats daily.
Using the convenience factory method:
let dailyReminder = WeekdayReminder.daily(
title: "Daily Standup",
message: "Time for the daily meeting",
time: Date.createReminderTime(hour: 9, minute: 0)
)
try await reminderManager.scheduleWeekdayReminder(dailyReminder)One-time reminder (fires once at the next occurrence):
let oneTimeReminder = WeekdayReminder.oneTime(
title: "Important Meeting",
message: "Don't forget!",
time: Date.createReminderTime(hour: 14, minute: 30)
)
try await reminderManager.scheduleWeekdayReminder(oneTimeReminder)Using the standard initializer:
let dailyReminder = WeekdayReminder(
id: UUID(),
title: "Exercise",
message: "Time to workout",
time: Date.createReminderTime(hour: 7, minute: 0),
repeating: true,
daysOfWeek: [] // Empty array = daily reminder
)
try await reminderManager.scheduleWeekdayReminder(dailyReminder)let locationRegion = LocationRegion(
latitude: 37.7749,
longitude: -122.4194,
radius: 200,
notifyOnEntry: true,
notifyOnExit: false
)
let locationReminder = LocationReminder(
id: UUID(),
title: "Arrived at the Park",
message: "Don't forget to stretch!",
locationRegion: locationRegion,
repeats: false
)
try await reminderManager.scheduleLocationReminder(locationReminder)await reminderManager.cancelCountdownReminder(countdownReminder)
await reminderManager.cancelWeekdayReminder(calendarReminder)
await reminderManager.cancelLocationReminder(locationReminder)
await reminderManager.cancelReminders(identifiers: [idList])
await reminderManager.cancelAllReminders()Task {
// Load by reminder type
let countdownReminders = await reminderManager.loadAllCountdownReminders()
let weekdayReminders = await reminderManager.loadAllWeekdayReminders()
let locationReminders = await reminderManager.loadAllLocationReminders()
// Load specific weekday reminder categories
let dailyReminders = await reminderManager.loadAllDailyReminders() // Empty days, repeating
let oneTimeReminders = await reminderManager.loadAllOneTimeReminders() // Empty days, not repeating
let weeklyReminders = await reminderManager.loadAllWeeklyReminders() // Specific days
}The NnReminderUITestHelpers library provides a convenient extension for handling the iOS notification permission alert in UI tests.
import NnReminderUITestHelpers
let app = XCUIApplication()
app.launch()
// Allow notifications (default)
app.handleNotificationPermissionAlert()
// Explicitly allow
app.handleNotificationPermissionAlert(.allow)
// Deny notifications
app.handleNotificationPermissionAlert(.deny)NnReminderKit is organized around a central NnReminderManager class that abstracts UNUserNotificationCenter interactions. It supports three primary reminder types:
CountdownReminderfor time-interval-based alertsWeekdayReminderfor calendar-based repetition, including:- Weekly reminders on specific weekdays
- Daily repeating reminders (empty
daysOfWeekarray) - One-time reminders at specific times
LocationReminderfor geofenced alerts
The permission handling is implemented using a SwiftUI-first approach with composable modifiers.
NnReminderKit was built to reduce the boilerplate involved in managing local notifications on Apple platforms. The native APIs can be verbose and error-prone, especially when dealing with multiple types of reminders. This library was designed to offer a clean, testable, and SwiftUI-friendly abstraction for modern apps.
Contributions, feedback, and feature requests are welcome! To contribute:
- Fork the repository
- Open an issue or discussion to propose changes
- Submit a pull request with your updates
NnReminderKit is available under the MIT license.