使用 EventKit 和 EventKitUI 创建、读取及管理日历日程与提醒事项。适用于向用户日历添加日程、创建提醒事项、设置重复规则、申请日历或提醒事项权限、弹出日程编辑器、选择日历、处理闹钟/提醒、监听日历变动,或使用 EKEventStore、EKEvent、EKReminder、EKCalendar、EKRecurrenceRule、EKEventEditViewController、EKCalendarChooser 以及 EventKitUI 视图等场景。
EventKit
使用 EventKit 处理日历与提醒事项的权限授权、增删改查 (CRUD)、重复规则、闹钟提醒及系统编辑器集成。
目录
可用性
- iOS 17+: 请使用更细粒度的全量(Full)/仅写入(Write-Only)请求方法;旧版
requestAccess(to:)不再弹出提示且会直接抛出异常。系统级的日程编辑器支持在应用未获取日历权限的情况下直接创建日程。对于 iOS 10–16,需对新 API 进行条件检查,并使用旧版请求配合NSCalendarsUsageDescription/NSRemindersUsageDescription;EventKitUI 可能还需要配置NSContactsUsageDescription。 - iOS 26+: 类型化的
EKEventStore.EventStoreChanged/.changed消息需要在条件检查后使用。对于更早期的系统,请继续使用EKEventStoreChanged。
配置说明
Info.plist 键值配置
请根据在权限授权中选择的访问路径,添加对应的权限描述文本。切勿单纯为了简化配置流程而申请超出需求的权限。
免授权的系统编辑器路径无需添加日历使用描述。直接写入操作需要仅写入或全量权限;读取操作必须拥有全量权限。提醒事项(Reminders)仅支持全量权限。
事件库 (Event Store)
在应用中只创建一个 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)
}
指定特定日历
// 列出可写入的日历
let calendars = eventStore.calendars(for: .event)
.filter { $0.allowsContentModifications }
// 使用首个可写入的日历,或使用默认日历
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
查询日程
在完成权限授权中的全量访问校验后,使用日期范围谓词(Predicate)来查询日程。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 表示所有日历
)
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)
}
查询提醒事项
提醒事项的查询是异步执行的,会通过 completion handler 回调返回结果。
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 来创建重复日程或提醒事项。
基础重复规则
// 每周重复,无限期
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)
)
复杂重复规则
// 每周一和周三
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)
编辑重复日程
保存对重复日程的修改时,需指定影响范围(span):
// 仅修改本次实例
try eventStore.save(event, span: .thisEvent)
// 修改本次及后续所有实例
try eventStore.save(event, span: .futureEvents)
闹钟与提醒
为日程或提醒事项绑定闹钟以触发通知。
// 提前 15 分钟
let alarm = EKAlarm(relativeOffset: -15 * 60)
event.addAlarm(alarm)
// 指定绝对时间
let absoluteAlarm = EKAlarm(absoluteDate: alertDate)
event.addAlarm(absoluteAlarm)
对于基于地理围栏的提醒事项,需要在 EKAlarm 上设置 EKStructuredLocation 以及 .enter / .leave 的到达/离开触发条件,再将其添加到提醒事项中。详见 references/eventkit-patterns.md 了解基于位置提醒的完整模式。
EventKitUI 控制器
EKEventEditViewController — 创建/编辑日程
弹出系统级日程编辑器以创建或编辑日程。
在可用性中提及的免授权路径下,该编辑器在独立进程中运行并拥有自身的日历访问权限。关闭视图后,切勿通过检查已 Dismiss 的控制器来获取保存结果;如需确认保存内容,必须在拥有独立全量权限的条件下重新查询。
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 — 查看日程
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 通知,以便在日程被应用外部(例如系统“日历”App 或后台同步)修改时同步更新 UI。
NotificationCenter.default.addObserver(
forName: .EKEventStoreChanged,
object: eventStore,
queue: .main
) { [weak self] _ in
self?.refreshEvents()
}
收到此通知后务必重新查询日程。先前获取的 EKEvent、EKReminder 以及 EKCalendar 对象可能已失效过时。该通知会在主线程(Main Actor)派发。
常见误区
错误做法:在当前系统中使用旧版 requestAccess(to:)
// 错误:在当前系统使用旧版请求 API
eventStore.requestAccess(to: .event) { granted, error in }
// 正确:使用更细粒度的异步方法
let granted = try await eventStore.requestFullAccessToEvents()
仅在可用性提到的向下兼容降级代码中保留该方法。
错误做法:将日程保存至只读日历
// 错误:未经校验——若日历只读将抛出异常
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)
错误做法:创建日程时忽略时区
// 错误:跨时区用户查看到的日程时间会出现偏差




