关于 eventkit-calendar
面向 Swift 开发者的 EventKit 日历与提醒事项开发指南,讲解授权、数据操作和系统编辑界面。还涵盖重复规则、提醒、变更监听及常见数据一致性问题。
主要功能
- 01日历与提醒事项授权
- 02事件和提醒事项增删改查
- 03重复规则与闹钟设置
- 04EventKitUI 系统编辑界面
- 05日历变更监听与刷新
适用场景
- 01创建带提醒的日历事件
- 02按日期范围查询事件
- 03创建并完成提醒事项
- 04编辑重复事件并选择影响范围
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill eventkit-calendar安装后在 Claude Code 中输入/eventkit-calendar确认已加载。
附带文件references/eventkit-patterns.md
详细介绍
EventKit — 日历与提醒事项
创建、读取和管理日历事件与提醒事项。涵盖授权、事件和提醒事项的增删改查、重复规则、闹钟以及 EventKitUI 编辑器。 适用于 Swift 6.2 / iOS 26+。
目录
设置
Info.plist 键
根据所需的访问级别,添加相应的使用说明字符串:
| 键 | 访问级别 |
|---|---|
NSCalendarsFullAccessUsageDescription | 读取和写入事件 |
NSCalendarsWriteOnlyAccessUsageDescription | 仅写入事件(iOS 17+) |
NSRemindersFullAccessUsageDescription | 读取和写入提醒事项 |
如果应用还面向 iOS 16 或更早版本,请同时包含旧版
NSCalendarsUsageDescription/NSRemindersUsageDescription键。
事件存储
创建一个 EKEventStore 实例并重复使用。不要混用来自不同事件存储的对象。
import EventKit
let eventStore = EKEventStore()
授权
iOS 17+ 引入了细粒度访问级别。请使用现代异步方法。
事件的完全访问权限
func requestCalendarAccess() async throws -> Bool {
let granted = try await eventStore.requestFullAccessToEvents()
return granted
}
事件的仅写入权限
当应用只需创建事件(例如保存预订),不需要读取现有事件时使用。
func requestWriteAccess() async throws -> Bool {
let granted = try await eventStore.requestWriteOnlyAccessToEvents()
return granted
}
提醒事项的完全访问权限
func requestRemindersAccess() async throws -> Bool {
let granted = try await eventStore.requestFullAccessToReminders()
return granted
}
检查授权状态
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
}
创建事件
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:) 方法会返回该范围内展开后的重复事件实例。
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)
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 — 选择日历
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 对象可能已过期。
常见错误
不要:使用已弃用的 requestAccess(to:) 方法
// WRONG: Deprecated in iOS 17
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使用说明键 - 使用 iOS 17+ 的细粒度方法请求授权(
requestFullAccessToEvents、requestWriteOnlyAccessToEvents、requestFullAccessToReminders) - 在获取或保存之前检查授权状态
- 在整个应用中重复使用同一个
EKEventStore实例 - 将事件保存到可写日历(检查
allowsContentModifications) - 保存重复事件时指定正确的
EKSpan(.thisEvent与.futureEvents) - 批量保存后显式调用
commit() - 监听
EKEventStoreChanged通知以刷新过期数据 - 为特定地点的事件显式设置时区
- 不在不同的事件存储实例之间共享 EKObjects
- EventKitUI 代理在完成回调中关闭控制器
参考资料
- 扩展模式(SwiftUI 包装器、谓词查询、批量操作):
references/eventkit-patterns.md - EventKit 框架
- EKEventStore
- EKEvent
- EKReminder
- EKRecurrenceRule
- EKCalendar
- EventKit UI
- EKEventEditViewController
- EKCalendarChooser
- 访问事件存储
- 创建重复事件
评论
支持 Markdown