关于 eventkit
EventKit 是面向 Apple 平台开发者的 Swift 技能指南,介绍日历和提醒事项的授权、读写、重复规则及系统编辑界面。内容特别说明不同访问级别和系统版本的处理方式。
主要功能
- 01日历与提醒事项授权
- 02创建、查询和编辑事件
- 03创建与完成提醒事项
- 04重复规则和闹钟设置
- 05系统编辑器及变更观察
适用场景
- 01按日期范围读取日历事件
- 02创建带重复规则的日程
- 03添加带截止日期的提醒事项
- 04在外部日历变更后刷新界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill eventkit安装后在 Claude Code 中输入/eventkit确认已加载。
附带文件evals/evals.jsonreferences/eventkit-patterns.md
详细介绍
EventKit
使用 EventKit 处理日历和提醒事项的授权、增删改查、重复规则、提醒以及系统编辑器。
目录
可用性
- iOS 17+: 使用细分的完全访问/仅写入请求方法;旧版
requestAccess(to:)不再弹出授权提示,并会抛出错误。系统事件编辑器无需应用拥有日历访问权限即可创建事件。对于 iOS 10–16,请使用条件判断保护这些 API,并使用旧版请求方法以及NSCalendarsUsageDescription/NSRemindersUsageDescription;EventKitUI 也可能需要NSContactsUsageDescription。 - iOS 26+: 添加条件判断后,可使用类型化的
EKEventStore.EventStoreChanged/.changed消息。对于更早的系统,请保留EKEventStoreChanged。
设置
Info.plist 键
根据授权中选择的访问方式,添加相应的用途说明。不要仅为简化设置流程而请求更广泛的访问权限。
无需授权的系统编辑器方式不需要日历用途说明字符串。 直接写入需要仅写入或完全访问权限;读取需要完全访问权限。提醒事项仅支持完全访问权限。
事件存储
创建一个 EKEventStore 实例并重复使用。不要混用来自不同事件存储的对象。
import EventKit
let eventStore = EKEventStore()
授权
请求与功能相符的最小访问权限。采用可用性中对应版本的请求方式。
| 键 | 访问级别 |
|---|---|
NSCalendarsFullAccessUsageDescription | 读取和写入事件 |
NSCalendarsWriteOnlyAccessUsageDescription | 仅直接创建事件 |
NSRemindersFullAccessUsageDescription | 读取和写入提醒事项 |
事件完全访问权限
当应用需要读取、编辑、删除或获取日历事件时,调用 try await eventStore.requestFullAccessToEvents()。
事件仅写入访问权限
适用于应用只创建事件(例如保存预订),而无需读取现有事件的情况。
在执行不使用 EKEventEditViewController 的 EventKit 直接写入操作之前,调用 try await eventStore.requestWriteOnlyAccessToEvents()。
仅写入访问权限可以创建事件,但无法获取日历或事件,包括由应用创建的事件。若之后需要查询、验证、修改或同步,请使用完全访问权限。
提醒事项完全访问权限
在读取、创建、编辑或删除提醒事项之前,调用 try await eventStore.requestFullAccessToReminders()。
检查授权状态
在操作前,使用 EKEventStore.authorizationStatus(for: .event) 或 .reminder。处理 .notDetermined、.fullAccess、.writeOnly、.restricted、.denied 和 @unknown default;只有 .fullAccess 支持读取事件/提醒事项。
创建事件
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)
}
设置特定日历
// 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
添加结构化位置
import CoreLocation
let location = EKStructuredLocation(title: "Apple Park")
location.geoLocation = CLLocation(latitude: 37.3349, longitude: -122.0090)
event.structuredLocation = location
获取事件
通过授权中的完全访问权限检查后,使用日期范围谓词查询事件。events(matching:) 方法会返回该范围内展开的重复事件实例。事件谓词的跨度上限为 4 年,而且 events(matching:)/enumerateEvents(matching:using:) 都是同步方法,只会返回已提交的事件。
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 }
}
按标识符获取单个事件
if let event = eventStore.event(withIdentifier: savedEventID) {
print(event.title ?? "No title")
}
提醒事项
创建提醒事项
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)
}
获取提醒事项
提醒事项的获取操作是异步的,结果会通过完成处理程序返回。
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 ?? [])
}
}
}
完成提醒事项
func completeReminder(_ reminder: EKReminder) throws {
reminder.isCompleted = true
try eventStore.save(reminder, commit: true)
}
重复规则
使用 EKRecurrenceRule 创建重复事件或提醒事项。
简单重复
// 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)
)
复杂重复
// 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)
编辑重复事件
保存对重复事件的更改时,请指定范围:
// Change only this occurrence
try eventStore.save(event, span: .thisEvent)
// Change this and all future occurrences
try eventStore.save(event, span: .futureEvents)
闹钟
将闹钟附加到事件,或将提醒附加到提醒事项,以触发通知。
// 15 minutes before
let alarm = EKAlarm(relativeOffset: -15 * 60)
event.addAlarm(alarm)
// At an absolute date
let absoluteAlarm = EKAlarm(absoluteDate: alertDate)
event.addAlarm(absoluteAlarm)
对于提醒事项地理围栏,请在 EKAlarm 上设置一个 EKStructuredLocation 和 .enter / .leave 接近方式,然后将其添加到提醒事项中。完整的位置提醒模式请参阅
references/eventkit-patterns.md。
EventKitUI 控制器
EKEventEditViewController — 创建/编辑事件
显示系统事件编辑器,用于创建或编辑事件。
在可用性所述的免授权路径中,编辑器会在进程外运行,并使用自己的日历访问权限。请勿通过检查已关闭的控制器来了解保存了什么;只有拥有单独的完整访问权限时,才能重新获取数据。
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)
}
}
EKEventViewController — 查看事件
import EventKitUI
let viewer = EKEventViewController()
viewer.event = existingEvent
viewer.allowsEditing = true
navigationController?.pushViewController(viewer, animated: true)
EKCalendarChooser — 选择日历
EKCalendarChooser 需要仅写入或完整日历访问权限。在仅写入的应用中,选择器只会显示可写日历,并且只允许选择一个可写日历。
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)
观察更改
注册 EKEventStoreChanged 通知,以便在事件于应用外部被修改时(例如,通过日历应用或同步)保持 UI 同步。
NotificationCenter.default.addObserver(
forName: .EKEventStoreChanged,
object: eventStore,
queue: .main
) { [weak self] _ in
self?.refreshEvents()
}
收到此通知后,务必重新获取事件。之前获取的 EKEvent、EKReminder 和 EKCalendar 对象可能已过时。此通知会在主线程执行器上发布。
常见错误
不要:在当前系统上使用旧版 requestAccess(to:)
// 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()
仅在可用性所述的兼容性回退路径中保留它。
不要:将事件保存到只读日历
// 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)
不要:创建事件时忽略时区
// 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
不要:忘记提交批量保存
// 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()
不要:混用来自不同事件存储区的 EKObjects
// 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)
审查清单
- 为日历和/或提醒事项添加了正确的
Info.plist使用说明键 - 授权遵循可用性中的版本划分
- 仅将日历仅写入访问权限用于直接创建事件,不用于读取事件/日历
- 获取或保存数据前检查授权状态
- 获取任何事件或提醒事项前都需要完整访问权限
- 在整个应用中重复使用单个
EKEventStore实例 - 将事件保存到可写日历(已检查
allowsContentModifications) - 保存重复事件时指定正确的
EKSpan(.thisEvent与.futureEvents) - 批量保存时验证日历可写,使用
commit: false暂存,调用会抛错的commit();若失败,则使用reset()清除未保存状态,丢弃所有已失效的EKObject,然后重新获取或重建对象再重试 - 观察
EKEventStoreChanged通知以刷新过时数据 - 更改观察根据可用性使用经典通知或受条件保护的类型化消息
- 为特定地点的事件显式设置时区
- 不在不同事件存储区实例之间共享 EKObjects
- EventKitUI 委托在完成回调中关闭控制器