EventKit
Use EventKit for calendar and reminder authorization, CRUD, recurrence, alarms,
and system editors.
Contents
Workflow
- Determine whether the app needs event write-only access, full event access, or reminder access and declare the matching usage descriptions.
- Retain one
EKEventStore, request the narrowest authorization, and branch on current status.
- Create or fetch objects from that store, choose writable calendars, and make timezone/recurrence semantics explicit.
- Save or batch changes with the intended commit policy and surface recoverable errors.
- Observe store changes and verify denied/restricted access, recurrence edits, timezone changes, and external modifications.
Route by Task
- Read core implementation details for authorization, events, reminders, recurrence, alarms, EventKitUI, and change observation.
- Read extended EventKit patterns for SwiftUI wrappers, predicates, batch operations, and advanced recurrence workflows.
Core Decisions
- Use current full-access/write-only authorization APIs rather than legacy generic access calls.
- Never mix
EKObject instances from different event stores.
- Check calendar mutability before save and preserve explicit timezone intent.
- Decide whether recurring edits affect one occurrence or the future span before saving.
Common Mistakes
DON'T: Use legacy requestAccess(to:) on current systems
// WRONG: Legacy request API on current systems
eventStore.requestAccess(to: .event) { granted, error in }
// CORRECT: Use the granular async methods
let granted = try await eventStore.requestFullAccessToEvents()
Keep it only in the compatibility fallback from Availability.
DON'T: Save events to a read-only calendar
// WRONG: No check -- will throw if calendar is read-only
event.calendar = someCalendar
try eventStore.save(event, span: .thisEvent)
// CORRECT: Verify the calendar allows modifications
guard someCalendar.allowsContentModifications else {
event.calendar = eventStore.defaultCalendarForNewEvents
return
}
event.calendar = someCalendar
try eventStore.save(event, span: .thisEvent)
DON'T: Ignore timezone when creating events
// WRONG: Event appears at wrong time for traveling users
event.startDate = Date()
event.endDate = Date().addingTimeInterval(3600)
// CORRECT: Set the timezone explicitly for location-specific events
event.timeZone = TimeZone(identifier: "America/New_York")
event.startDate = startDate
event.endDate = endDate
DON'T: Forget to commit batched saves
// WRONG: Changes never persisted
try eventStore.save(event1, span: .thisEvent, commit: false)
try eventStore.save(event2, span: .thisEvent, commit: false)
// Missing commit!
// CORRECT: Commit after batching
try eventStore.save(event1, span: .thisEvent, commit: false)
try eventStore.save(event2, span: .thisEvent, commit: false)
try eventStore.commit()
DON'T: Mix EKObjects from different event stores
// WRONG: Event fetched from storeA, saved to storeB
let event = storeA.event(withIdentifier: id)!
try storeB.save(event, span: .thisEvent) // Undefined behavior
// CORRECT: Use the same store throughout
let event = eventStore.event(withIdentifier: id)!
try eventStore.save(event, span: .thisEvent)
Review Checklist
References
1---2name: eventkit3description: Creates, reads, edits, and presents calendar events or reminders with EventKit and EventKitUI. Use for authorization, event/reminder CRUD, calendars, recurrence, alarms, change observation, event editors, viewers, calendar choosers, or SwiftUI wrappers.4---56# EventKit78Use EventKit for calendar and reminder authorization, CRUD, recurrence, alarms,9and system editors.1011## Contents1213- [Workflow](#workflow)14- [Route by Task](#route-by-task)15- [Core Decisions](#core-decisions)16- [Common Mistakes](#common-mistakes)17- [Review Checklist](#review-checklist)18- [References](#references)1920## Workflow21221. Determine whether the app needs event write-only access, full event access, or reminder access and declare the matching usage descriptions.232. Retain one `EKEventStore`, request the narrowest authorization, and branch on current status.243. Create or fetch objects from that store, choose writable calendars, and make timezone/recurrence semantics explicit.254. Save or batch changes with the intended commit policy and surface recoverable errors.265. Observe store changes and verify denied/restricted access, recurrence edits, timezone changes, and external modifications.2728## Route by Task2930- Read [core implementation details](references/core-implementation.md) for authorization, events, reminders, recurrence, alarms, EventKitUI, and change observation.31- Read [extended EventKit patterns](references/eventkit-patterns.md) for SwiftUI wrappers, predicates, batch operations, and advanced recurrence workflows.3233## Core Decisions3435- Use current full-access/write-only authorization APIs rather than legacy generic access calls.36- Never mix `EKObject` instances from different event stores.37- Check calendar mutability before save and preserve explicit timezone intent.38- Decide whether recurring edits affect one occurrence or the future span before saving.3940## Common Mistakes4142### DON'T: Use legacy requestAccess(to:) on current systems4344```swift45// WRONG: Legacy request API on current systems46eventStore.requestAccess(to: .event) { granted, error in }4748// CORRECT: Use the granular async methods49let granted = try await eventStore.requestFullAccessToEvents()50```5152Keep it only in the compatibility fallback from [Availability](#availability).5354### DON'T: Save events to a read-only calendar5556```swift57// WRONG: No check -- will throw if calendar is read-only58event.calendar = someCalendar59try eventStore.save(event, span: .thisEvent)6061// CORRECT: Verify the calendar allows modifications62guard someCalendar.allowsContentModifications else {63 event.calendar = eventStore.defaultCalendarForNewEvents64 return65}66event.calendar = someCalendar67try eventStore.save(event, span: .thisEvent)68```6970### DON'T: Ignore timezone when creating events7172```swift73// WRONG: Event appears at wrong time for traveling users74event.startDate = Date()75event.endDate = Date().addingTimeInterval(3600)7677// CORRECT: Set the timezone explicitly for location-specific events78event.timeZone = TimeZone(identifier: "America/New_York")79event.startDate = startDate80event.endDate = endDate81```8283### DON'T: Forget to commit batched saves8485```swift86// WRONG: Changes never persisted87try eventStore.save(event1, span: .thisEvent, commit: false)88try eventStore.save(event2, span: .thisEvent, commit: false)89// Missing commit!9091// CORRECT: Commit after batching92try eventStore.save(event1, span: .thisEvent, commit: false)93try eventStore.save(event2, span: .thisEvent, commit: false)94try eventStore.commit()95```9697### DON'T: Mix EKObjects from different event stores9899```swift100// WRONG: Event fetched from storeA, saved to storeB101let event = storeA.event(withIdentifier: id)!102try storeB.save(event, span: .thisEvent) // Undefined behavior103104// CORRECT: Use the same store throughout105let event = eventStore.event(withIdentifier: id)!106try eventStore.save(event, span: .thisEvent)107```108109## Review Checklist110111- [ ] Correct `Info.plist` usage description keys added for calendars and/or reminders112- [ ] Authorization follows the version split in [Availability](#availability)113- [ ] Write-only calendar access used only for direct event creation, not event/calendar reads114- [ ] Authorization status checked before fetching or saving115- [ ] Full access required before any event or reminder fetch116- [ ] Single `EKEventStore` instance reused across the app117- [ ] Events saved to a writable calendar (`allowsContentModifications` checked)118- [ ] Recurring event saves specify correct `EKSpan` (`.thisEvent` vs `.futureEvents`)119- [ ] Batched saves validate writable calendars, stage with `commit: false`,120 call throwing `commit()`, and on failure `reset()` unsaved state, discard121 every invalidated `EKObject`, then refetch or reconstruct before retry122- [ ] `EKEventStoreChanged` notification observed to refresh stale data123- [ ] Change observation uses the classic notification or guarded typed message per [Availability](#availability)124- [ ] Timezone set explicitly for location-specific events125- [ ] EKObjects not shared across different event store instances126- [ ] EventKitUI delegates dismiss controllers in completion callbacks127128## References129130- Extended patterns (SwiftUI wrappers, predicate queries, batch operations): [references/eventkit-patterns.md](references/eventkit-patterns.md)131- [EventKit framework](https://sosumi.ai/documentation/eventkit)132- [EKEventStore](https://sosumi.ai/documentation/eventkit/ekeventstore)133- [EKEvent](https://sosumi.ai/documentation/eventkit/ekevent)134- [EKReminder](https://sosumi.ai/documentation/eventkit/ekreminder)135- [EKRecurrenceRule](https://sosumi.ai/documentation/eventkit/ekrecurrencerule)136- [EKCalendar](https://sosumi.ai/documentation/eventkit/ekcalendar)137- [EventKit UI](https://sosumi.ai/documentation/eventkitui)138- [EKEventEditViewController](https://sosumi.ai/documentation/eventkitui/ekeventeditviewcontroller)139- [EKCalendarChooser](https://sosumi.ai/documentation/eventkitui/ekcalendarchooser)140- [Accessing the event store](https://sosumi.ai/documentation/eventkit/accessing-the-event-store)141- [Creating a recurring event](https://sosumi.ai/documentation/eventkit/creating-a-recurring-event)142- [Core implementation details](references/core-implementation.md) -- setup, API wiring, and focused implementation recipes moved out of the entrypoint.