Skip to main content
< All Topics
Print

EventKit Calendar Sync

name: eventkit-calendar-sync

description: Apple EventKit and CalDAV integration patterns for iOS/macOS calendar sync, including Google Calendar OAuth, Outlook calendar integration, and cross-platform calendar data normalization. Covers EKEventStore authorization, event CRUD operations, recurring event handling, calendar source management, and bi-directional sync conflict resolution. Use when implementing calendar sync features, integrating Apple EventKit, connecting to Google Calendar OAuth, or building cross-platform calendar aggregation.

EventKit Calendar Sync

Instructions

Apple EventKit (iOS/macOS)

Authorization

  1. Request access using the appropriate API for the platform version:

   // iOS 17+ / macOS 14+
   let store = EKEventStore()
   let granted = try await store.requestFullAccessToEvents()

   // iOS 16 and earlier
   let granted = try await store.requestAccess(to: .event)
  1. Add Info.plist keys:
  • NSCalendarsUsageDescription — explain why the app needs calendar access
  • NSCalendarsFullAccessUsageDescription (iOS 17+) — required for full access
  1. Handle authorization states:
  • .fullAccess / .authorized: proceed with calendar operations
  • .denied: show explanation UI with Settings deep-link
  • .restricted: inform user that calendar access is restricted by policy
  • .writeOnly (iOS 17+): can create events but cannot read existing ones

Event CRUD Operations

  1. Fetch events:

   let predicate = store.predicateForEvents(
       withStart: startDate,
       end: endDate,
       calendars: selectedCalendars
   )
   let events = store.events(matching: predicate)
  • Always specify a date range; never fetch unbounded
  • Maximum recommended range: 4 years (EventKit limitation)
  • Filter by calendar source to avoid duplicates from synced accounts
  1. Create events:

   let event = EKEvent(eventStore: store)
   event.title = title
   event.startDate = startDate
   event.endDate = endDate
   event.calendar = targetCalendar
   event.notes = notes
   event.location = location
   try store.save(event, span: .thisEvent)
  1. Update events:
  • For single instances of recurring events, use .thisEvent span
  • For all future occurrences, use .futureEvents
  • Always re-fetch the event before updating to avoid stale data
  1. Delete events:
  • Use .thisEvent or .futureEvents span for recurring events
  • Prompt user for span choice when deleting a recurring event instance

Recurring Events

  1. Create recurrence rules:

   let rule = EKRecurrenceRule(
       recurrenceWith: .weekly,
       interval: 1,
       daysOfTheWeek: [EKRecurrenceDayOfWeek(.monday)],
       daysOfTheMonth: nil,
       monthsOfTheYear: nil,
       weeksOfTheYear: nil,
       daysOfTheYear: nil,
       setPositions: nil,
       end: EKRecurrenceEnd(end: endDate)
   )
   event.recurrenceRules = [rule]
  1. Supported patterns: daily, weekly, monthly (by day of month or day of week), yearly
  2. Exception handling: individual occurrences can be modified without affecting the series

Change Observation

  1. Register for change notifications:

   NotificationCenter.default.addObserver(
       self, selector: #selector(calendarChanged),
       name: .EKEventStoreChanged, object: store
   )
  1. On change notification: re-fetch visible date range and diff against cached data
  2. Debounce: changes may fire rapidly during sync; debounce by 500ms

Google Calendar OAuth Integration

  1. OAuth 2.0 setup:
  • Register app in Google Cloud Console
  • Enable Calendar API
  • Configure OAuth consent screen
  • Request scopes: https://www.googleapis.com/auth/calendar.readonly (read) or https://www.googleapis.com/auth/calendar (read/write)
  1. Token management:
  • Store refresh token in Keychain (iOS/macOS) or secure storage
  • Exchange refresh token for access token before each API call
  • Handle token revocation gracefully
  1. API endpoints:
  • List calendars: GET /calendar/v3/users/me/calendarList
  • List events: GET /calendar/v3/calendars/{calendarId}/events
  • Always use syncToken for incremental sync after initial full fetch
  • Use pageToken for paginated responses
  1. Sync strategy:
  • First sync: full fetch with timeMin and timeMax
  • Subsequent syncs: use syncToken from previous response
  • If syncToken is invalid (410 response), perform full re-sync

Outlook / Microsoft Graph Integration

  1. OAuth 2.0 via MSAL:
  • Register in Azure AD app registrations
  • Request scope: Calendars.ReadWrite
  • Use MSAL library for token management
  1. API endpoints:
  • List calendars: GET /me/calendars
  • List events: GET /me/calendarView?startDateTime={}&endDateTime={}
  • Use deltaLink for incremental sync (equivalent to Google’s syncToken)
  1. Data mapping: Microsoft Graph uses different field names; normalize to a common model

Cross-Platform Data Normalization

  1. Canonical event model:

   CalendarEvent {
     id, externalId, source (apple|google|outlook),
     title, description, location,
     startDate (ISO-8601), endDate (ISO-8601),
     isAllDay, timeZone,
     recurrenceRule (RRULE string),
     calendarId, calendarName, calendarColor,
     attendees: [{ name, email, status }],
     reminders: [{ minutesBefore }],
     lastModified, etag
   }
  1. Source-specific mapping:
  • Apple EventKit: EKEventCalendarEvent
  • Google: Calendar API JSON → CalendarEvent
  • Outlook: Microsoft Graph JSON → CalendarEvent
  1. Timezone normalization: always store in UTC internally; convert to local time for display

Conflict Resolution

  1. Detection: compare lastModified timestamps when the same event exists in multiple sources
  2. Resolution strategy:
  • Last-write-wins (default): most recent modification takes precedence
  • Source-priority: designate a primary calendar source; it always wins
  • User-prompt: show diff and let user choose (for important events)
  1. Merge rules:
  • Title/description changes: last-write-wins
  • Time changes: always prompt user
  • Attendee changes: union of attendee lists
  • Deletion: if deleted in one source, prompt before propagating

Inputs Required

  • Target platforms (iOS, macOS, web, cross-platform)
  • Calendar services to integrate (Apple, Google, Outlook, CalDAV)
  • Read-only or read/write access requirements
  • Sync strategy (one-way import or bi-directional sync)
  • Conflict resolution preference (last-write-wins, source-priority, user-prompt)
  • OAuth credentials for Google/Outlook if applicable

Output Format

Calendar Integration Module Structure


calendar/
  EventKitManager.swift       — Apple EventKit authorization and CRUD
  GoogleCalendarClient.swift   — Google OAuth + Calendar API
  OutlookCalendarClient.swift  — MSAL + Microsoft Graph
  CalendarSyncEngine.swift     — orchestrates sync across sources
  ConflictResolver.swift       — detects and resolves conflicts
  models/
    CalendarEvent.swift         — canonical event model
    CalendarSource.swift        — source configuration
    SyncState.swift             — sync tokens, last sync time

Anti-Patterns

  • Fetching all events without a date range — EventKit and APIs will return massive datasets or error; always constrain
  • Ignoring the EKEventStoreChanged notification — without observing changes, the app shows stale calendar data
  • Storing OAuth tokens in UserDefaults — tokens are secrets; use Keychain on Apple platforms
  • Polling for changes instead of using sync tokens — Google and Outlook provide incremental sync; polling wastes quota and battery
  • Assuming all-day events have no timezone — they do; handle them as date-only (not datetime) to avoid off-by-one errors at timezone boundaries
  • Modifying recurring events without asking about span — always prompt: this event only, this and future, or all events
  • Syncing without conflict detection — bi-directional sync without conflict resolution will overwrite user data
Table of Contents