Skip to content
macOSDeep Dive Published Updated 8 min readViews unavailable

EventKit on macOS: Event Store Access, Recurrence, and Change Recovery

Build resilient EventKit features with least-privilege access, stable calendar queries, recurrence-aware edits, stale-object recovery, and safe batch commits.

EventKit is the supported API boundary for reading and writing a person’s Calendar and Reminders data. It is not a direct handle to Calendar’s database. The event store owns the interaction with accounts and calendars, while the app owns access requests, query scope, object lifetime, and decisions about what to do when the system changes the data.

Production code must distinguish write-only event access from full event access. Write-only access lets a feature create events without reading the user’s existing calendar, including events the app itself created. Reading requires full access. Reminders use a separate full-access request. Ask for only the level needed by the visible feature, and keep UI actions disabled or explanatory until the corresponding authorization state is known.

Keep the event store alive for its objects

Create a long-lived EKEventStore owner for the feature or application subsystem. Event, reminder, calendar, and source objects are associated with that store. Releasing the store before the objects that depend on it can produce errors. A short-lived function that creates a store, returns EKEvent objects, and discards the store has unclear ownership and makes later saves or refreshes difficult to reason about.

For a feature that needs to read the calendar, request full event access before fetching. Put NSCalendarsFullAccessUsageDescription in the app’s Info.plist; event creation without read access uses NSCalendarsWriteOnlyAccessUsageDescription. Sandboxed macOS apps also need the Calendar personal-information entitlement. An app that only needs to create a new event should request write-only access rather than asking to inspect all calendars. If the product also targets iOS or Mac Catalyst, EventKitUI’s chooser and editor controllers may avoid unnecessary broad browsing. Those controllers are not listed for native macOS, so an AppKit app must not assume they are available there.

import EventKit
import Foundation

final class CalendarAccess {
    private let store = EKEventStore()

    func requestFullEventAccess() async throws -> Bool {
        try await store.requestFullAccessToEvents()
    }

    // Schedule this synchronous fetch away from the main thread.
    func events(from start: Date, through end: Date) -> [EKEvent] {
        let predicate = store.predicateForEvents(
            withStart: start,
            end: end,
            calendars: nil
        )
        return store.events(matching: predicate)
    }
}

The request method shown is the modern full-access API. Gate APIs against the deployment targets your application supports and handle an error or a denied result separately. Do not call a permission prompt every time a view appears. Check authorization status first, explain the feature’s need in context, and provide a Settings or reduced-functionality path if access is unavailable.

Scope queries and time carefully

An event predicate takes a start and end date and an optional calendar set. Narrowing the range and calendar list avoids loading more information than the view needs. Sort results intentionally for presentation. A month view may need events that overlap the visible date interval, while a reminders list uses a different query model and may have completion state to consider.

EventKit dates are absolute instants, while calendar views express civil dates in a time zone. Build day boundaries with Calendar and its time zone instead of assuming a day is always 86,400 seconds. Daylight-saving transitions can make local days shorter or longer. For recurring events, query the intended display window and test events near midnight, timezone changes, all-day boundaries, and repeated or skipped local times.

When editing a recurring event, choose the correct span. Updating one occurrence, this and future occurrences, or the entire series have different user-visible meanings. EKEvent recurrence rules describe repetition, but they do not replace the need to understand the requested edit scope. Make the UI explicit before applying an operation that changes multiple occurrences.

Event identifiers can be useful for retrieving an object later, but should not be treated as immutable across every account change or event-store rebuild. Persist only the identifier and minimum metadata your feature needs, handle failed lookups, and give the user a recovery path if a calendar or event no longer exists. A title plus date is not a guaranteed unique key.

Treat change notifications as invalidation

The event-store-changed notification does not provide an item-by-item change log. Apple documents that previously accessed event and reminder objects are stale after the notification. Refetch the objects needed by the visible feature, or refresh an event only when the documented refresh method confirms it remains valid. Avoid applying stale objects from a cache after sync, edits in Calendar, or account updates.

Coalesce repeated change notifications into one refresh task. Fetch the new snapshot, associate results with the query generation that produced them, and publish only if that generation remains current. If the user changes the selected calendar while an older fetch is in flight, discard the obsolete results instead of replacing the newer screen.

Do not interpret a change notification as proof that a particular event was added or deleted by a specific actor. It indicates the store changed; the app must query again. This makes the notification useful as an invalidation signal, not an audit log.

Save edits transactionally

Create and edit EventKit objects through their documented APIs. Avoid direct writes to Calendar’s database or attempts to infer its internal storage format. When performing several related changes, stage them with commit disabled and commit once after the logical operation is complete. If one staged operation fails, reset the store before attempting a later commit; the official documentation warns that a partially staged batch can cause subsequent commits to fail until the changes are rolled back. Reset discards all unsaved changes and invalidates objects created or fetched from that store, so reacquire events, calendars, and predicates before continuing.

For a single operation, immediate commit is often simpler. For larger batches, present an explicit summary before changing multiple events and report whether the commit succeeded. Do not update your local UI as if a save is durable until the save call succeeds. If the app keeps its own database, use an idempotent reconciliation strategy because the user’s Calendar may change independently.

An event saved with write-only authorization may be placed into a calendar chosen by the person or system policy, and the app may not be able to read it back. Do not promise that the new event will appear in an app-owned list when the permission mode does not allow that read. Preserve a local draft or success receipt only if the product’s behavior clearly distinguishes it from a verified Calendar readback.

Failure handling and observability

Handle denied and restricted authorization, an empty calendar list, account removal, stale events, invalid time ranges, unavailable calendars, and save failures. An empty result is not the same as a fetch error. Model those outcomes separately so the interface does not claim the user has no events when the real issue is access or account state.

Avoid logging event titles, notes, attendee addresses, location, or reminder text. Useful diagnostics can record query range, calendar count, operation type, authorization state, and error domain/code without collecting personal content. Keep data retention proportional to the feature and revalidate authorization before accessing cached data after user settings change.

For reminders, build a distinct flow rather than assuming event query methods apply. Request full reminder access, fetch the reminder entity with its own predicate, and handle completion state and due dates. EventKit unifies access to related data but event and reminder authorization and semantics are not interchangeable.

Verification plan

Test first-run permission request, denial, later authorization change, write-only event creation, full-access reads, reminder-only use, multiple calendars, recurring events, all-day events, DST boundaries, imported account data, external Calendar edits, stale object refresh, batch rollback, and save errors. Verify that a view refresh does not duplicate notification observers or issue redundant concurrent fetches.

For an edit workflow, assert the selected occurrence span, destination calendar, start and end instants, time zone display, and post-save result. Measure query latency and memory on large calendars. Test with real user-controlled calendars only in a dedicated test account, and ensure diagnostics never export personal event content.

EventKit provides a supported route into the user’s calendar store, not ownership of that data. A durable macOS integration keeps the store alive, asks for the minimum access, treats notifications as invalidation, handles recurrence and time zones explicitly, and commits changes with an honest recovery path.

For reminders, avoid reusing event-specific assumptions about duration and recurrence. A reminder can have a due date without a meaningful end time, can be incomplete or completed, and can belong to a list whose availability changes. Use the reminder APIs and predicates for the requested list scope. A user-facing “today” view should define whether overdue incomplete reminders remain visible and how a local date boundary is computed.

If an application offers its own event editor, preserve user intent separately from the EKEvent object. Validate required fields before saving, identify the selected calendar, and show the actual save error rather than silently falling back to another calendar. After an external change notification, refresh the object and re-check the fields the user was editing. If they changed, present a conflict decision instead of overwriting an unrelated update.

The completion state of a permission request should be stored as current authorization state, not as a permanent “prompted” Boolean. The user can change access in System Settings. Re-evaluate status when the app returns to the foreground or a feature is opened, and make the no-access path useful without constructing predicates that require protected data.

Related:

Sources:

Comments