Skip to content

raykim2414/JMEventKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

10 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

JMEventKit

A modern Swift wrapper for Apple's EventKit framework, designed to simplify reminders and calendar event management in iOS applications.

Swift Platform License

🎯 Features

  • βœ… Simple API: Intuitive, modern async/await interface
  • βœ… SwiftUI Ready: Built-in ObservableObject support with @Published properties
  • βœ… 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

πŸ“‹ Requirements

  • iOS 17.0+ / macOS 14.0+ / watchOS 10.0+
  • Swift 5.9+
  • Xcode 15.0+

πŸ“¦ Installation

Swift Package Manager

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:

  1. File > Add Package Dependencies
  2. Enter: https://github.com/raycsh/JMEventKit.git
  3. Select version and add to your target

πŸš€ Quick Start

1. Add Required Permissions

Add these keys to your Info.plist:

<key>NSRemindersFullAccessUsageDescription</key>
<string>We need access to your reminders to help you track important tasks.</string>

2. Request Permission

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)")
}

3. Create a Reminder

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)")
}

4. Fetch Reminders

do {
    let reminders = try await JMEventKit.shared.fetchReminders()
    for reminder in reminders {
        print("- \(reminder.title ?? "Untitled")")
    }
} catch {
    print("Error fetching reminders: \(error)")
}

5. Complete a Reminder

do {
    try await JMEventKit.shared.completeReminder(reminder)
    print("Reminder completed!")
} catch {
    print("Error completing reminder: \(error)")
}

6. Delete a Reminder

do {
    try await JMEventKit.shared.deleteReminder(reminder)
    print("Reminder deleted!")
} catch {
    print("Error deleting reminder: \(error)")
}

πŸ’» SwiftUI Integration

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)")
        }
    }
}

πŸ“š Advanced Usage

Fetch Completed Reminders

let lastWeek = Date().addingTimeInterval(-7 * 24 * 60 * 60)
let completed = try await JMEventKit.shared.fetchCompletedReminders(
    from: lastWeek,
    to: Date()
)

Update a Reminder

reminder.title = "Updated title"
reminder.notes = "Updated notes"
try await JMEventKit.shared.updateReminder(reminder)

Check Authorization Status

if JMEventKit.shared.isAuthorized() {
    // Proceed with reminder operations
} else {
    // Request authorization
}

Get Default Calendar

if let calendar = JMEventKit.shared.defaultCalendar() {
    print("Default calendar: \(calendar.title)")
}

Create Recurring Reminder

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
)

Create Reminder with Alarms

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]
)

Advanced Filtering

// 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 Reminders

// Search in title and notes
let results = try await JMEventKit.shared.searchReminders(
    query: "grocery",
    includeCompleted: false
)

Create Calendar Event

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"
)

Create All-Day Event

let event = try await JMEventKit.shared.createAllDayEvent(
    title: "Company Holiday",
    date: Date().addingTimeInterval(7 * 24 * 60 * 60)
)

Create Recurring Event

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
)

Fetch Events

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
)

Update Event

event.title = "Updated Meeting Title"
event.location = "Conference Room B"
try await JMEventKit.shared.updateEvent(event)

Delete Event

try await JMEventKit.shared.deleteEvent(event)

πŸ§ͺ Testing

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)
    }
}

πŸ›‘ Error Handling

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 access
  • permissionRestricted - Access restricted by parental controls
  • reminderNotFound - Reminder doesn't exist
  • reminderCreationFailed - Failed to create reminder
  • reminderDeletionFailed - Failed to delete reminder
  • reminderUpdateFailed - Failed to update reminder
  • eventNotFound - Event doesn't exist
  • eventCreationFailed - Failed to create event
  • eventDeletionFailed - Failed to delete event
  • eventUpdateFailed - Failed to update event
  • saveFailed(Error) - Failed to save to event store
  • fetchFailed(Error) - Failed to fetch reminders
  • invalidConfiguration(String) - Invalid configuration
  • calendarNotFound - Default calendar not found
  • unknown(Error) - Unknown error occurred

πŸ—Ί Roadmap

Phase 1: Core Reminders (v0.1.0) - βœ… Complete

  • βœ… Basic reminder CRUD operations
  • βœ… iOS 17+ permission handling
  • βœ… SwiftUI ObservableObject integration
  • βœ… Error handling
  • βœ… Unit tests
  • βœ… Documentation

Phase 2: Advanced Reminders (v0.2.0) - βœ… Complete

  • βœ… Recurring reminders
  • βœ… Reminder alarms
  • βœ… Priority support (color not supported by EventKit API for individual reminders)
  • βœ… Advanced filtering
  • βœ… Search functionality

Phase 3: Calendar Events (v0.3.0) - βœ… Complete

  • βœ… Event creation and management
  • βœ… All-day events
  • βœ… Recurring events
  • βœ… Event attendees (read-only, write requires UI)

Phase 4: Advanced Features (v0.4.0+) - Planned

  • Location-based reminders
  • Batch operations
  • iCloud sync change notifications
  • Advanced recurrence rules (specific days of week, etc.)

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

πŸ“„ License

JMEventKit is available under the MIT license. See the LICENSE file for more info.

πŸ‘¨β€πŸ’» Author

Ray Kim - https://github.com/raykim2414

πŸ™ Acknowledgments

  • 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages