What can we help you with?
EventKit Calendar Sync
EventKit Calendar Sync
Instructions
Apple EventKit (iOS/macOS)
Authorization
- 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)
- Add Info.plist keys:
NSCalendarsUsageDescription— explain why the app needs calendar accessNSCalendarsFullAccessUsageDescription(iOS 17+) — required for full access
- 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
- 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
- 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)
- Update events:
- For single instances of recurring events, use
.thisEventspan - For all future occurrences, use
.futureEvents - Always re-fetch the event before updating to avoid stale data
- Delete events:
- Use
.thisEventor.futureEventsspan for recurring events - Prompt user for span choice when deleting a recurring event instance
Recurring Events
- 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]
- Supported patterns: daily, weekly, monthly (by day of month or day of week), yearly
- Exception handling: individual occurrences can be modified without affecting the series
Change Observation
- Register for change notifications:
NotificationCenter.default.addObserver(
self, selector: #selector(calendarChanged),
name: .EKEventStoreChanged, object: store
)
- On change notification: re-fetch visible date range and diff against cached data
- Debounce: changes may fire rapidly during sync; debounce by 500ms
Google Calendar OAuth Integration
- 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) orhttps://www.googleapis.com/auth/calendar(read/write)
- 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
- API endpoints:
- List calendars:
GET /calendar/v3/users/me/calendarList - List events:
GET /calendar/v3/calendars/{calendarId}/events - Always use
syncTokenfor incremental sync after initial full fetch - Use
pageTokenfor paginated responses
- Sync strategy:
- First sync: full fetch with
timeMinandtimeMax - Subsequent syncs: use
syncTokenfrom previous response - If
syncTokenis invalid (410 response), perform full re-sync
Outlook / Microsoft Graph Integration
- OAuth 2.0 via MSAL:
- Register in Azure AD app registrations
- Request scope:
Calendars.ReadWrite - Use MSAL library for token management
- API endpoints:
- List calendars:
GET /me/calendars - List events:
GET /me/calendarView?startDateTime={}&endDateTime={} - Use
deltaLinkfor incremental sync (equivalent to Google’s syncToken)
- Data mapping: Microsoft Graph uses different field names; normalize to a common model
Cross-Platform Data Normalization
- 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
}
- Source-specific mapping:
- Apple EventKit:
EKEvent→CalendarEvent - Google: Calendar API JSON →
CalendarEvent - Outlook: Microsoft Graph JSON →
CalendarEvent
- Timezone normalization: always store in UTC internally; convert to local time for display
Conflict Resolution
- Detection: compare
lastModifiedtimestamps when the same event exists in multiple sources - 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)
- 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
