eventkit-calendar

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

EventKit — Calendar & Reminders

EventKit — 日历与提醒事项

Create, read, and manage calendar events and reminders. Covers authorization, event and reminder CRUD, recurrence rules, alarms, and EventKitUI editors. Targets Swift 6.2 / iOS 26+.
创建、读取并管理日历事件与提醒事项。涵盖权限授权、事件与提醒的CRUD操作、重复规则、提醒闹钟以及EventKitUI编辑器。适配Swift 6.2 / iOS 26+。

Contents

目录

Setup

设置

Info.plist Keys

Info.plist 键

Add the required usage description strings based on what access level you need:
KeyAccess Level
NSCalendarsFullAccessUsageDescription
Read + write events
NSCalendarsWriteOnlyAccessUsageDescription
Write-only events (iOS 17+)
NSRemindersFullAccessUsageDescription
Read + write reminders
For apps also targeting iOS 16 or earlier, also include the legacy
NSCalendarsUsageDescription
/
NSRemindersUsageDescription
keys.
根据所需的访问级别添加必要的使用描述字符串:
访问级别
NSCalendarsFullAccessUsageDescription
读取+写入事件
NSCalendarsWriteOnlyAccessUsageDescription
仅写入事件(iOS 17+)
NSRemindersFullAccessUsageDescription
读取+写入提醒事项
若应用同时适配iOS 16或更早版本,还需添加旧版的
NSCalendarsUsageDescription
/
NSRemindersUsageDescription
键。

Event Store

事件存储

Create a single
EKEventStore
instance and reuse it. Do not mix objects from different event stores.
swift
import EventKit

let eventStore = EKEventStore()
创建单个
EKEventStore
实例并复用,不要混合使用来自不同事件存储的对象。
swift
import EventKit

let eventStore = EKEventStore()

Authorization

权限授权

iOS 17+ introduced granular access levels. Use the modern async methods.
iOS 17+引入了精细化的访问级别,请使用现代异步方法。

Full Access to Events

事件完全访问权限

swift
func requestCalendarAccess() async throws -> Bool {
    let granted = try await eventStore.requestFullAccessToEvents()
    return granted
}
swift
func requestCalendarAccess() async throws -> Bool {
    let granted = try await eventStore.requestFullAccessToEvents()
    return granted
}

Write-Only Access to Events

事件仅写入权限

Use when your app only creates events (e.g., saving a booking) and does not need to read existing events.
swift
func requestWriteAccess() async throws -> Bool {
    let granted = try await eventStore.requestWriteOnlyAccessToEvents()
    return granted
}
当应用仅需创建事件(例如保存预订信息)且无需读取现有事件时使用。
swift
func requestWriteAccess() async throws -> Bool {
    let granted = try await eventStore.requestWriteOnlyAccessToEvents()
    return granted
}

Full Access to Reminders

提醒事项完全访问权限

swift
func requestRemindersAccess() async throws -> Bool {
    let granted = try await eventStore.requestFullAccessToReminders()
    return granted
}
swift
func requestRemindersAccess() async throws -> Bool {
    let granted = try await eventStore.requestFullAccessToReminders()
    return granted
}

Checking Authorization Status

检查授权状态

swift
let status = EKEventStore.authorizationStatus(for: .event)

switch status {
case .notDetermined:
    // Request access
    break
case .fullAccess:
    // Read and write allowed
    break
case .writeOnly:
    // Write-only access granted (iOS 17+)
    break
case .restricted:
    // Parental controls or MDM restriction
    break
case .denied:
    // User denied -- direct to Settings
    break
@unknown default:
    break
}
swift
let status = EKEventStore.authorizationStatus(for: .event)

switch status {
case .notDetermined:
    // 请求权限
    break
case .fullAccess:
    // 允许读取和写入
    break
case .writeOnly:
    // 已授予仅写入权限(iOS 17+)
    break
case .restricted:
    // 受家长控制或MDM限制
    break
case .denied:
    // 用户拒绝 -- 引导至设置页面
    break
@unknown default:
    break
}

Creating Events

创建事件

swift
func createEvent(
    title: String,
    startDate: Date,
    endDate: Date,
    calendar: EKCalendar? = nil
) throws {
    let event = EKEvent(eventStore: eventStore)
    event.title = title
    event.startDate = startDate
    event.endDate = endDate
    event.calendar = calendar ?? eventStore.defaultCalendarForNewEvents

    try eventStore.save(event, span: .thisEvent)
}
swift
func createEvent(
    title: String,
    startDate: Date,
    endDate: Date,
    calendar: EKCalendar? = nil
) throws {
    let event = EKEvent(eventStore: eventStore)
    event.title = title
    event.startDate = startDate
    event.endDate = endDate
    event.calendar = calendar ?? eventStore.defaultCalendarForNewEvents

    try eventStore.save(event, span: .thisEvent)
}

Setting a Specific Calendar

指定特定日历

swift
// List writable calendars
let calendars = eventStore.calendars(for: .event)
    .filter { $0.allowsContentModifications }

// Use the first writable calendar, or the default
let targetCalendar = calendars.first ?? eventStore.defaultCalendarForNewEvents
event.calendar = targetCalendar
swift
// 列出可写入的日历
let calendars = eventStore.calendars(for: .event)
    .filter { $0.allowsContentModifications }

// 使用第一个可写入的日历,或默认日历
let targetCalendar = calendars.first ?? eventStore.defaultCalendarForNewEvents
event.calendar = targetCalendar

Adding Structured Location

添加结构化位置

swift
import CoreLocation

let location = EKStructuredLocation(title: "Apple Park")
location.geoLocation = CLLocation(latitude: 37.3349, longitude: -122.0090)
event.structuredLocation = location
swift
import CoreLocation

let location = EKStructuredLocation(title: "Apple Park")
location.geoLocation = CLLocation(latitude: 37.3349, longitude: -122.0090)
event.structuredLocation = location

Fetching Events

获取事件

Use a date-range predicate to query events. The
events(matching:)
method returns occurrences of recurring events expanded within the range.
swift
func fetchEvents(from start: Date, to end: Date) -> [EKEvent] {
    let predicate = eventStore.predicateForEvents(
        withStart: start,
        end: end,
        calendars: nil  // nil = all calendars
    )
    return eventStore.events(matching: predicate)
        .sorted { $0.startDate < $1.startDate }
}
使用日期范围谓词查询事件。
events(matching:)
方法会返回在该范围内展开的重复事件实例。
swift
func fetchEvents(from start: Date, to end: Date) -> [EKEvent] {
    let predicate = eventStore.predicateForEvents(
        withStart: start,
        end: end,
        calendars: nil  // nil = 所有日历
    )
    return eventStore.events(matching: predicate)
        .sorted { $0.startDate < $1.startDate }
}

Fetching a Single Event by Identifier

通过标识符获取单个事件

swift
if let event = eventStore.event(withIdentifier: savedEventID) {
    print(event.title ?? "No title")
}
swift
if let event = eventStore.event(withIdentifier: savedEventID) {
    print(event.title ?? "无标题")
}

Reminders

提醒事项

Creating a Reminder

创建提醒事项

swift
func createReminder(title: String, dueDate: Date) throws {
    let reminder = EKReminder(eventStore: eventStore)
    reminder.title = title
    reminder.calendar = eventStore.defaultCalendarForNewReminders()

    let dueDateComponents = Calendar.current.dateComponents(
        [.year, .month, .day, .hour, .minute],
        from: dueDate
    )
    reminder.dueDateComponents = dueDateComponents

    try eventStore.save(reminder, commit: true)
}
swift
func createReminder(title: String, dueDate: Date) throws {
    let reminder = EKReminder(eventStore: eventStore)
    reminder.title = title
    reminder.calendar = eventStore.defaultCalendarForNewReminders()

    let dueDateComponents = Calendar.current.dateComponents(
        [.year, .month, .day, .hour, .minute],
        from: dueDate
    )
    reminder.dueDateComponents = dueDateComponents

    try eventStore.save(reminder, commit: true)
}

Fetching Reminders

获取提醒事项

Reminder fetches are asynchronous and return through a completion handler.
swift
func fetchIncompleteReminders() async -> [EKReminder] {
    let predicate = eventStore.predicateForIncompleteReminders(
        withDueDateStarting: nil,
        ending: nil,
        calendars: nil
    )

    return await withCheckedContinuation { continuation in
        eventStore.fetchReminders(matching: predicate) { reminders in
            continuation.resume(returning: reminders ?? [])
        }
    }
}
提醒事项的获取是异步的,通过完成回调返回结果。
swift
func fetchIncompleteReminders() async -> [EKReminder] {
    let predicate = eventStore.predicateForIncompleteReminders(
        withDueDateStarting: nil,
        ending: nil,
        calendars: nil
    )

    return await withCheckedContinuation { continuation in
        eventStore.fetchReminders(matching: predicate) { reminders in
            continuation.resume(returning: reminders ?? [])
        }
    }
}

Completing a Reminder

标记提醒事项为已完成

swift
func completeReminder(_ reminder: EKReminder) throws {
    reminder.isCompleted = true
    try eventStore.save(reminder, commit: true)
}
swift
func completeReminder(_ reminder: EKReminder) throws {
    reminder.isCompleted = true
    try eventStore.save(reminder, commit: true)
}

Recurrence Rules

重复规则

Use
EKRecurrenceRule
to create repeating events or reminders.
使用
EKRecurrenceRule
创建重复事件或提醒事项。

Simple Recurrence

简单重复规则

swift
// Every week, indefinitely
let weeklyRule = EKRecurrenceRule(
    recurrenceWith: .weekly,
    interval: 1,
    end: nil
)
event.addRecurrenceRule(weeklyRule)

// Every 2 weeks, ending after 10 occurrences
let biweeklyRule = EKRecurrenceRule(
    recurrenceWith: .weekly,
    interval: 2,
    end: EKRecurrenceEnd(occurrenceCount: 10)
)

// Monthly, ending on a specific date
let monthlyRule = EKRecurrenceRule(
    recurrenceWith: .monthly,
    interval: 1,
    end: EKRecurrenceEnd(end: endDate)
)
swift
// 每周重复,无结束时间
let weeklyRule = EKRecurrenceRule(
    recurrenceWith: .weekly,
    interval: 1,
    end: nil
)
event.addRecurrenceRule(weeklyRule)

// 每2周重复,重复10次后结束
let biweeklyRule = EKRecurrenceRule(
    recurrenceWith: .weekly,
    interval: 2,
    end: EKRecurrenceEnd(occurrenceCount: 10)
)

// 每月重复,在指定日期结束
let monthlyRule = EKRecurrenceRule(
    recurrenceWith: .monthly,
    interval: 1,
    end: EKRecurrenceEnd(end: endDate)
)

Complex Recurrence

复杂重复规则

swift
// Every Monday and Wednesday
let days = [
    EKRecurrenceDayOfWeek(.monday),
    EKRecurrenceDayOfWeek(.wednesday)
]

let complexRule = EKRecurrenceRule(
    recurrenceWith: .weekly,
    interval: 1,
    daysOfTheWeek: days,
    daysOfTheMonth: nil,
    monthsOfTheYear: nil,
    weeksOfTheYear: nil,
    daysOfTheYear: nil,
    setPositions: nil,
    end: nil
)
event.addRecurrenceRule(complexRule)
swift
// 每周一和周三重复
let days = [
    EKRecurrenceDayOfWeek(.monday),
    EKRecurrenceDayOfWeek(.wednesday)
]

let complexRule = EKRecurrenceRule(
    recurrenceWith: .weekly,
    interval: 1,
    daysOfTheWeek: days,
    daysOfTheMonth: nil,
    monthsOfTheYear: nil,
    weeksOfTheYear: nil,
    daysOfTheYear: nil,
    setPositions: nil,
    end: nil
)
event.addRecurrenceRule(complexRule)

Editing Recurring Events

编辑重复事件

When saving changes to a recurring event, specify the span:
swift
// Change only this occurrence
try eventStore.save(event, span: .thisEvent)

// Change this and all future occurrences
try eventStore.save(event, span: .futureEvents)
保存对重复事件的修改时,需指定范围:
swift
// 仅修改本次事件
try eventStore.save(event, span: .thisEvent)

// 修改本次及所有未来的事件
try eventStore.save(event, span: .futureEvents)

Alarms

提醒闹钟

Attach alarms to events or reminders to trigger notifications.
swift
// 15 minutes before
let alarm = EKAlarm(relativeOffset: -15 * 60)
event.addAlarm(alarm)

// At an absolute date
let absoluteAlarm = EKAlarm(absoluteDate: alertDate)
event.addAlarm(absoluteAlarm)
为事件或提醒事项添加闹钟以触发通知。
swift
// 提前15分钟
let alarm = EKAlarm(relativeOffset: -15 * 60)
event.addAlarm(alarm)

// 在指定绝对时间触发
let absoluteAlarm = EKAlarm(absoluteDate: alertDate)
event.addAlarm(absoluteAlarm)

EventKitUI Controllers

EventKitUI控制器

EKEventEditViewController — Create/Edit Events

EKEventEditViewController — 创建/编辑事件

Present the system event editor for creating or editing events.
swift
import EventKitUI

class EventEditorCoordinator: NSObject, EKEventEditViewDelegate {
    let eventStore = EKEventStore()

    func presentEditor(from viewController: UIViewController) {
        let editor = EKEventEditViewController()
        editor.eventStore = eventStore
        editor.editViewDelegate = self
        viewController.present(editor, animated: true)
    }

    func eventEditViewController(
        _ controller: EKEventEditViewController,
        didCompleteWith action: EKEventEditViewAction
    ) {
        switch action {
        case .saved:
            // Event saved
            break
        case .canceled:
            break
        case .deleted:
            break
        @unknown default:
            break
        }
        controller.dismiss(animated: true)
    }
}
展示系统事件编辑器以创建或编辑事件。
swift
import EventKitUI

class EventEditorCoordinator: NSObject, EKEventEditViewDelegate {
    let eventStore = EKEventStore()

    func presentEditor(from viewController: UIViewController) {
        let editor = EKEventEditViewController()
        editor.eventStore = eventStore
        editor.editViewDelegate = self
        viewController.present(editor, animated: true)
    }

    func eventEditViewController(
        _ controller: EKEventEditViewController,
        didCompleteWith action: EKEventEditViewAction
    ) {
        switch action {
        case .saved:
            // 事件已保存
            break
        case .canceled:
            break
        case .deleted:
            break
        @unknown default:
            break
        }
        controller.dismiss(animated: true)
    }
}

EKEventViewController — View an Event

EKEventViewController — 查看事件

swift
import EventKitUI

let viewer = EKEventViewController()
viewer.event = existingEvent
viewer.allowsEditing = true
navigationController?.pushViewController(viewer, animated: true)
swift
import EventKitUI

let viewer = EKEventViewController()
viewer.event = existingEvent
viewer.allowsEditing = true
navigationController?.pushViewController(viewer, animated: true)

EKCalendarChooser — Select Calendars

EKCalendarChooser — 选择日历

swift
let chooser = EKCalendarChooser(
    selectionStyle: .multiple,
    displayStyle: .allCalendars,
    entityType: .event,
    eventStore: eventStore
)
chooser.showsDoneButton = true
chooser.showsCancelButton = true
chooser.delegate = self
present(UINavigationController(rootViewController: chooser), animated: true)
swift
let chooser = EKCalendarChooser(
    selectionStyle: .multiple,
    displayStyle: .allCalendars,
    entityType: .event,
    eventStore: eventStore
)
chooser.showsDoneButton = true
chooser.showsCancelButton = true
chooser.delegate = self
present(UINavigationController(rootViewController: chooser), animated: true)

Observing Changes

监听变更

Register for
EKEventStoreChanged
notifications to keep your UI in sync when events are modified outside your app (e.g., by the Calendar app or a sync).
swift
NotificationCenter.default.addObserver(
    forName: .EKEventStoreChanged,
    object: eventStore,
    queue: .main
) { [weak self] _ in
    self?.refreshEvents()
}
Always re-fetch events after receiving this notification. Previously fetched
EKEvent
objects may be stale.
注册
EKEventStoreChanged
通知,以便在事件被外部应用(例如日历应用或同步服务)修改时保持UI同步。
swift
NotificationCenter.default.addObserver(
    forName: .EKEventStoreChanged,
    object: eventStore,
    queue: .main
) { [weak self] _ in
    self?.refreshEvents()
}
收到该通知后,请务必重新获取事件。之前获取的
EKEvent
对象可能已过期。

Common Mistakes

常见错误

DON'T: Use the deprecated requestAccess(to:) method

不要:使用已废弃的requestAccess(to:)方法

swift
// WRONG: Deprecated in iOS 17
eventStore.requestAccess(to: .event) { granted, error in }

// CORRECT: Use the granular async methods
let granted = try await eventStore.requestFullAccessToEvents()
swift
// 错误:在iOS 17中已废弃
eventStore.requestAccess(to: .event) { granted, error in }

// 正确:使用精细化的异步方法
let granted = try await eventStore.requestFullAccessToEvents()

DON'T: Save events to a read-only calendar

不要:将事件保存到只读日历

swift
// 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)
swift
// 错误:未检查权限 -- 如果日历为只读会抛出异常
event.calendar = someCalendar
try eventStore.save(event, span: .thisEvent)

// 正确:验证日历是否允许修改
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

不要:创建事件时忽略时区

swift
// 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
swift
// 错误:对于经常出差的用户,事件时间会显示错误
event.startDate = Date()
event.endDate = Date().addingTimeInterval(3600)

// 正确:为特定地点的事件显式设置时区
event.timeZone = TimeZone(identifier: "America/New_York")
event.startDate = startDate
event.endDate = endDate

DON'T: Forget to commit batched saves

不要:批量保存后忘记提交

swift
// 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()
swift
// 错误:变更不会持久化
try eventStore.save(event1, span: .thisEvent, commit: false)
try eventStore.save(event2, span: .thisEvent, commit: false)
// 缺少提交操作!

// 正确:批量操作后提交
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

不要:混合使用来自不同事件存储的EKObjects

swift
// 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)
swift
// 错误:从storeA获取的事件,保存到storeB
let event = storeA.event(withIdentifier: id)!
try storeB.save(event, span: .thisEvent) // 行为未定义

// 正确:全程使用同一个事件存储
let event = eventStore.event(withIdentifier: id)!
try eventStore.save(event, span: .thisEvent)

Review Checklist

审核检查清单

  • Correct
    Info.plist
    usage description keys added for calendars and/or reminders
  • Authorization requested with iOS 17+ granular methods (
    requestFullAccessToEvents
    ,
    requestWriteOnlyAccessToEvents
    ,
    requestFullAccessToReminders
    )
  • Authorization status checked before fetching or saving
  • Single
    EKEventStore
    instance reused across the app
  • Events saved to a writable calendar (
    allowsContentModifications
    checked)
  • Recurring event saves specify correct
    EKSpan
    (
    .thisEvent
    vs
    .futureEvents
    )
  • Batched saves followed by explicit
    commit()
  • EKEventStoreChanged
    notification observed to refresh stale data
  • Timezone set explicitly for location-specific events
  • EKObjects not shared across different event store instances
  • EventKitUI delegates dismiss controllers in completion callbacks
  • 已为日历和/或提醒事项添加正确的
    Info.plist
    使用描述键
  • 使用iOS 17+的精细化方法请求授权(
    requestFullAccessToEvents
    requestWriteOnlyAccessToEvents
    requestFullAccessToReminders
  • 在获取或保存前检查授权状态
  • 应用中复用单个
    EKEventStore
    实例
  • 事件保存到可写入的日历(已检查
    allowsContentModifications
  • 保存重复事件时指定正确的
    EKSpan
    .thisEvent
    .futureEvents
  • 批量保存后显式调用
    commit()
  • 监听
    EKEventStoreChanged
    通知以刷新过期数据
  • 为特定地点的事件显式设置时区
  • 未在不同事件存储实例之间共享EKObjects
  • EventKitUI代理在完成回调中关闭控制器

References

参考资料