A modern Swift wrapper for Apple's EventKit framework, designed to simplify reminders and calendar event management in iOS applications.
- β Simple API: Intuitive, modern async/await interface
- β
SwiftUI Ready: Built-in
ObservableObjectsupport with@Publishedproperties - β Type-Safe: Leverages Swift's type system for safer code
- β iOS 17+ Compatible: Handles new permission model automatically
- β Well-Tested: Comprehensive unit test coverage with mock support
- β Protocol-Based: Designed for testability with dependency injection
- β Zero Dependencies: Uses only Apple frameworks
- iOS 17.0+ / macOS 14.0+ / watchOS 10.0+
- Swift 5.9+
- Xcode 15.0+
Add JMEventKit to your project via Swift Package Manager:
dependencies: [
.package(url: "https://github.com/raycsh/JMEventKit.git", from: "0.1.0")
]Or in Xcode:
- File > Add Package Dependencies
- Enter:
https://github.com/raycsh/JMEventKit.git - Select version and add to your target
Add these keys to your Info.plist:
<key>NSRemindersFullAccessUsageDescription</key>
<string>We need access to your reminders to help you track important tasks.</string>import JMEventKit
// Configure with your app name
JMEventKit.shared.configure(appName: "My App")
// Request permission
do {
let granted = try await JMEventKit.shared.requestReminderAuthorization()
if granted {
print("Permission granted!")
}
} catch {
print("Error requesting permission: \(error)")
}do {
let reminder = try await JMEventKit.shared.createReminder(
title: "Buy groceries",
notes: "Milk, eggs, bread",
dueDate: Date().addingTimeInterval(3600), // 1 hour from now
priority: 5
)
print("Created reminder: \(reminder.title ?? "")")
} catch {
print("Error creating reminder: \(error)")
}do {
let reminders = try await JMEventKit.shared.fetchReminders()
for reminder in reminders {
print("- \(reminder.title ?? "Untitled")")
}
} catch {
print("Error fetching reminders: \(error)")
}do {
try await JMEventKit.shared.completeReminder(reminder)
print("Reminder completed!")
} catch {
print("Error completing reminder: \(error)")
}do {
try await JMEventKit.shared.deleteReminder(reminder)
print("Reminder deleted!")
} catch {
print("Error deleting reminder: \(error)")
}JMEventKit works seamlessly with SwiftUI using @StateObject:
import SwiftUI
import JMEventKit
struct RemindersView: View {
@StateObject private var eventKit = JMEventKit.shared
var body: some View {
List {
ForEach(eventKit.reminders, id: \.calendarItemIdentifier) { reminder in
HStack {
Text(reminder.title ?? "Untitled")
Spacer()
if reminder.isCompleted {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
}
}
}
}
.task {
await requestPermissionAndFetch()
}
.refreshable {
try? await eventKit.fetchReminders()
}
.overlay {
if eventKit.isFetching {
ProgressView()
}
}
}
private func requestPermissionAndFetch() async {
do {
_ = try await eventKit.requestReminderAuthorization()
try await eventKit.fetchReminders()
} catch {
print("Error: \(error)")
}
}
}let lastWeek = Date().addingTimeInterval(-7 * 24 * 60 * 60)
let completed = try await JMEventKit.shared.fetchCompletedReminders(
from: lastWeek,
to: Date()
)reminder.title = "Updated title"
reminder.notes = "Updated notes"
try await JMEventKit.shared.updateReminder(reminder)if JMEventKit.shared.isAuthorized() {
// Proceed with reminder operations
} else {
// Request authorization
}if let calendar = JMEventKit.shared.defaultCalendar() {
print("Default calendar: \(calendar.title)")
}let reminder = try await JMEventKit.shared.createRecurringReminder(
title: "Take vitamins",
startDate: Date(),
frequency: .daily,
interval: 1,
endDate: Date().addingTimeInterval(30 * 24 * 60 * 60) // 30 days
)let alarm1 = EKAlarm(relativeOffset: -3600) // 1 hour before
let alarm2 = EKAlarm(relativeOffset: -300) // 5 minutes before
let reminder = try await JMEventKit.shared.createReminder(
title: "Important meeting",
dueDate: Date().addingTimeInterval(7200),
alarms: [alarm1, alarm2]
)// Fetch high-priority reminders due this week
let weekFromNow = Date().addingTimeInterval(7 * 24 * 60 * 60)
let highPriorityReminders = try await JMEventKit.shared.fetchIncompleteReminders(
priority: 1,
from: Date(),
to: weekFromNow
)// Search in title and notes
let results = try await JMEventKit.shared.searchReminders(
query: "grocery",
includeCompleted: false
)let startDate = Date().addingTimeInterval(3600)
let endDate = startDate.addingTimeInterval(3600) // 1 hour duration
let event = try await JMEventKit.shared.createEvent(
title: "Team Meeting",
startDate: startDate,
endDate: endDate,
location: "Conference Room A",
notes: "Discuss Q4 goals"
)let event = try await JMEventKit.shared.createAllDayEvent(
title: "Company Holiday",
date: Date().addingTimeInterval(7 * 24 * 60 * 60)
)let event = try await JMEventKit.shared.createRecurringEvent(
title: "Weekly Team Standup",
startDate: Date(),
endDate: Date().addingTimeInterval(1800), // 30 minutes
frequency: .weekly,
interval: 1,
recurrenceEnd: Date().addingTimeInterval(90 * 24 * 60 * 60) // 90 days
)let startDate = Date()
let endDate = Date().addingTimeInterval(7 * 24 * 60 * 60) // Next 7 days
let events = try await JMEventKit.shared.fetchEvents(
from: startDate,
to: endDate
)event.title = "Updated Meeting Title"
event.location = "Conference Room B"
try await JMEventKit.shared.updateEvent(event)try await JMEventKit.shared.deleteEvent(event)JMEventKit is designed with testability in mind. Use the EventStoreProtocol to inject mock implementations:
import XCTest
@testable import JMEventKit
class MyTests: XCTestCase {
func testReminderCreation() async throws {
let mockStore = MockEventStore()
let eventKit = JMEventKit(mockEventStore: mockStore)
let reminder = try await eventKit.createReminder(title: "Test")
XCTAssertEqual(reminder.title, "Test")
XCTAssertTrue(mockStore.saveCalled)
}
}JMEventKit provides detailed error types via JMEventKitError:
do {
try await JMEventKit.shared.createReminder(title: "Test")
} catch JMEventKitError.permissionDenied {
print("Permission denied. Please enable in Settings.")
} catch JMEventKitError.permissionRestricted {
print("Access restricted by device settings.")
} catch JMEventKitError.calendarNotFound {
print("Default calendar not found.")
} catch {
print("Unexpected error: \(error)")
}Available error types:
permissionDenied- User denied accesspermissionRestricted- Access restricted by parental controlsreminderNotFound- Reminder doesn't existreminderCreationFailed- Failed to create reminderreminderDeletionFailed- Failed to delete reminderreminderUpdateFailed- Failed to update remindereventNotFound- Event doesn't existeventCreationFailed- Failed to create eventeventDeletionFailed- Failed to delete eventeventUpdateFailed- Failed to update eventsaveFailed(Error)- Failed to save to event storefetchFailed(Error)- Failed to fetch remindersinvalidConfiguration(String)- Invalid configurationcalendarNotFound- Default calendar not foundunknown(Error)- Unknown error occurred
- β Basic reminder CRUD operations
- β iOS 17+ permission handling
- β SwiftUI ObservableObject integration
- β Error handling
- β Unit tests
- β Documentation
- β Recurring reminders
- β Reminder alarms
- β Priority support (color not supported by EventKit API for individual reminders)
- β Advanced filtering
- β Search functionality
- β Event creation and management
- β All-day events
- β Recurring events
- β Event attendees (read-only, write requires UI)
- Location-based reminders
- Batch operations
- iCloud sync change notifications
- Advanced recurrence rules (specific days of week, etc.)
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
JMEventKit is available under the MIT license. See the LICENSE file for more info.
Ray Kim - https://github.com/raykim2414
- Inspired by Shift
- Built with β€οΈ for the iOS development community
Note: This library wraps Apple's EventKit framework. Some EventKit objects (like EKReminder) are not fully Sendable-compliant in Swift 6. This is expected and will be resolved as Apple updates their frameworks.