| name | eventkit |
| description | EventKit patterns for calendar events, reminders, and the EventKitUI views. Use when integrating calendar or reminder functionality. |
First step: Tell the user: "eventkit skill loaded."
EventKit Development Guide
Patterns for working with calendar events and reminders using EventKit and EventKitUI on Apple platforms.
When This Skill Activates
Use this skill when the user:
- Wants to read, create, or modify calendar events
- Needs to work with reminders (fetch, create, complete)
- Asks about EKEventStore, EKEvent, EKReminder, or EKCalendar
- Wants to present system calendar/reminder UI (EventKitUI)
- Needs recurrence rules, alarms, or availability settings
- Asks about calendar/reminder permissions (iOS 17+ full access model)
- Wants to observe calendar database changes
- Asks about virtual conference providers
Decision Tree: EventKit vs EventKitUI vs CalendarKit
| Framework | Use When |
|---|
| EventKit | Reading/writing events and reminders programmatically |
| EventKitUI | Presenting Apple's built-in event viewing, editing, or calendar chooser UI |
| CalendarKit | Building a fully custom calendar UI (third-party package, not Apple) |
Use EventKit alone for background sync or data queries. Use EventKitUI for standard system UI. Combine both when querying data programmatically and displaying with system UI.
API Availability
| API | iOS | macOS | watchOS | visionOS |
|---|
| EKEventStore | 4.0+ | 10.8+ | -- | 1.0+ |
| EKEvent | 4.0+ | 10.8+ | -- | 1.0+ |
| EKReminder | 6.0+ | 10.8+ | -- | 1.0+ |
| EKEventEditViewController | 4.0+ | -- | -- | -- |
| EKEventViewController | 4.0+ | -- | -- | 1.0+ |
| EKCalendarChooser | 5.0+ | -- | -- | 1.0+ |
| Full Access / Write-Only (iOS 17+) | 17.0+ | 14.0+ | -- | 1.0+ |
| EKVirtualConferenceProvider | 15.0+ | 12.0+ | -- | -- |
EKEventStore: Requesting Access
iOS 17+ Authorization Model
iOS 17 replaced the single calendar permission with two tiers:
| Access Level | Reads Events | Writes Events | Info.plist Key |
|---|
| Full access | Yes | Yes | NSCalendarsFullAccessUsageDescription |
| Write-only | No | Yes | NSCalendarsWriteOnlyAccessUsageDescription |
| Reminders | Yes | Yes | NSRemindersFullAccessUsageDescription |
Requesting Access
import EventKit
let store = EKEventStore()
func requestFullCalendarAccess() async throws -> Bool {
if #available(iOS 17.0, *) {
return try await store.requestFullAccessToEvents()
} else {
return try await store.requestAccess(to: .event)
}
}
@available(iOS 17.0, *)
func requestWriteOnlyAccess() async throws -> Bool {
return try await store.requestWriteOnlyAccessToEvents()
}
func requestReminderAccess() async throws -> Bool {
if #available(iOS 17.0, *) {
return try await store.requestFullAccessToReminders()
} else {
return store.requestAccess(to: .reminder)
}
}
Checking Authorization Status
let status = EKEventStore.authorizationStatus(for: .event)
Reading Calendars
let eventCalendars = store.calendars(for: .event)
let reminderCalendars = store.calendars(for: .reminder)
let iCloudCalendars = eventCalendars.filter { $0.source?.sourceType == .calDAV }
let birthdayCalendar = eventCalendars.first { $0.type == .birthday }
let defaultCalendar = store.defaultCalendarForNewEvents
let defaultReminderList = store.defaultCalendarForNewReminders()
Creating and Modifying Events
let event = EKEvent(eventStore: store)
event.title = "Team Standup"
event.startDate = Date()
event.endDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
event.calendar = store.defaultCalendarForNewEvents
event.location = "Conference Room A"
event.notes = "Weekly sync"
event.availability = .busy
event.isAllDay = false
event.addAlarm(EKAlarm(relativeOffset: -600))
let rule = EKRecurrenceRule(
recurrenceWith: .weekly, interval: 1,
daysOfTheWeek: [.monday, .wednesday, .friday].map { EKRecurrenceDayOfWeek($0) },
daysOfTheMonth: nil, monthsOfTheYear: nil,
weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil,
end: EKRecurrenceEnd(occurrenceCount: 52)
)
event.addRecurrenceRule(rule)
try store.save(event, span: .thisEvent, commit: true)
try store.remove(event, span: .thisEvent, commit: )
Querying Events
let startDate = Calendar.current.startOfDay(for: Date())
let endDate = Calendar.current.date(byAdding: .month, value: 1, to: startDate)!
let predicate = store.predicateForEvents(
withStart: startDate,
end: endDate,
calendars: nil
)
let events = store.events(matching: predicate)
store.enumerateEvents(matching: predicate) { event, stop in
if event.title.contains("Standup") {
stop.pointee = true
}
}
Reminders
let reminder = EKReminder(eventStore: store)
reminder.title = "Buy groceries"
reminder.calendar = store.defaultCalendarForNewReminders()
reminder.priority = 1
reminder.dueDateComponents = Calendar.current.dateComponents(
[.year, .month, .day, .hour, .minute],
from: Date().addingTimeInterval(86400)
)
reminder.addAlarm(EKAlarm(absoluteDate: Date().addingTimeInterval(86400)))
try store.save(reminder, commit: true)
let predicate = store.predicateForIncompleteReminders(
withDueDateStarting: nil,
ending: Date().addingTimeInterval(7 * 86400),
calendars: nil
)
store.fetchReminders(matching: predicate) { reminders in
guard let reminders else { return }
for r in reminders { print(r.title ?? "Untitled") }
}
reminder.isCompleted = true
try store.save(reminder, commit: true)
EventKitUI View Controllers
import EventKitUI
let eventVC = EKEventViewController()
eventVC.event = event
eventVC.allowsEditing = true
eventVC.delegate = self
navigationController?.pushViewController(eventVC, animated: true)
let editVC = EKEventEditViewController()
editVC.eventStore = store
editVC.event = event
editVC.editViewDelegate = self
present(editVC, animated: true)
let chooser = EKCalendarChooser(
selectionStyle: .multiple,
displayStyle: .allCalendars,
entityType: .event,
eventStore: store
)
chooser.showsDoneButton = true
chooser.showsCancelButton = true
chooser.delegate = self
present(UINavigationController(rootViewController: chooser), animated: true)
Delegate callbacks:
func eventViewController(_ controller: EKEventViewController,
didCompleteWith action: EKEventViewAction) {
controller.dismiss(animated: true)
}
func eventEditViewController(_ controller: EKEventEditViewController,
didCompleteWith action: EKEventEditViewAction) {
controller.dismiss(animated: true)
}
func calendarChooserDidFinish(_ calendarChooser: EKCalendarChooser) {
let selected = calendarChooser.selectedCalendars
calendarChooser.dismiss(animated: true)
}
Observing Changes
Listen for EKEventStoreChangedNotification to detect external modifications (other apps, sync). Re-fetch all cached data when received -- object identifiers may have changed.
NotificationCenter.default.addObserver(
self, selector: #selector(storeChanged(_:)),
name: .EKEventStoreChanged, object: store
)
@objc func storeChanged(_ notification: Notification) {
}
Privacy: Info.plist Keys
| Key | Required For |
|---|
NSCalendarsFullAccessUsageDescription | Read + write events (iOS 17+) |
NSCalendarsWriteOnlyAccessUsageDescription | Write events only (iOS 17+) |
NSRemindersFullAccessUsageDescription | Read + write reminders (iOS 17+) |
NSCalendarsUsageDescription | Calendar access (pre-iOS 17) |
NSRemindersUsageDescription | Reminder access (pre-iOS 17) |
For apps supporting both iOS 17+ and older versions, include both old and new keys.
Virtual Conference Provider
Subclass EKVirtualConferenceProvider (iOS 15+) and register it in Info.plist under the EKVirtualConferenceProvider key.
@available(iOS 15.0, *)
class MyConferenceProvider: EKVirtualConferenceProvider {
override func fetchVirtualConference(
for identifier: EKVirtualConferenceDescriptor.Identifier
) async throws -> EKVirtualConference {
let url = EKVirtualConferenceURLDescriptor(
title: "Join Call",
url: URL(string: "https://meet.example.com/\(identifier.rawValue)")!
)
return EKVirtualConference(title: "My App Meeting",
urlDescriptors: [url],
conferenceDetails: "Tap the link to join.")
}
}
Patterns
Good Patterns
class CalendarManager {
static let shared = CalendarManager()
let store = EKEventStore()
}
func addEvent() async throws {
guard try await requestFullCalendarAccess() else { throw CalendarError.accessDenied }
}
for event in events { try store.save(event, span: .thisEvent, commit: false) }
try store.commit()
Bad Patterns
func fetchEvents() { let store = EKEventStore() }
store.predicateForEvents(withStart: .distantPast, end: .distantFuture, calendars: nil)