eventkit-calendar by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill eventkit-calendar创建、读取和管理日历事件与提醒事项。涵盖授权、事件和提醒事项的增删改查、重复规则、闹钟以及 EventKitUI 编辑器。目标版本为 Swift 6.2 / iOS 26+。
根据你需要的访问级别添加必需的使用说明字符串:
| 键 | 访问级别 |
|---|---|
NSCalendarsFullAccessUsageDescription | 读取 + 写入事件 |
NSCalendarsWriteOnlyAccessUsageDescription | 仅写入事件 (iOS 17+) |
NSRemindersFullAccessUsageDescription |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 读取 + 写入提醒事项 |
对于同时支持 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:
// 请求访问权限
break
case .fullAccess:
// 允许读取和写入
break
case .writeOnly:
// 已授予仅写入访问权限 (iOS 17+)
break
case .restricted:
// 家长控制或 MDM 限制
break
case .denied:
// 用户已拒绝 —— 引导至设置
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)
}
// 列出可写日历
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
使用日期范围谓词来查询事件。events(matching:) 方法返回在指定范围内展开的重复事件的发生实例。
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)
}
提醒事项的获取是异步的,并通过完成处理程序返回。
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)
保存对重复事件的更改时,请指定范围:
// 仅更改此实例
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)
呈现系统事件编辑器以创建或编辑事件。
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)
}
}
import EventKitUI
let viewer = EKEventViewController()
viewer.event = existingEvent
viewer.allowsEditing = true
navigationController?.pushViewController(viewer, animated: true)
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 通知,以便当事件在你的应用外部(例如,由日历应用或同步)被修改时,保持你的用户界面同步。
NotificationCenter.default.addObserver(
forName: .EKEventStoreChanged,
object: eventStore,
queue: .main
) { [weak self] _ in
self?.refreshEvents()
}
收到此通知后,务必重新获取事件。先前获取的 EKEvent 对象可能已过时。
// 错误:在 iOS 17 中已弃用
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)
// 错误:对于旅行的用户,事件会出现在错误的时间
event.startDate = Date()
event.endDate = Date().addingTimeInterval(3600)
// 正确:为特定地点的事件明确设置时区
event.timeZone = TimeZone(identifier: "America/New_York")
event.startDate = startDate
event.endDate = endDate
// 错误:更改从未持久化
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()
// 错误:从 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)
Info.plist 使用说明键requestFullAccessToEvents, requestWriteOnlyAccessToEvents, requestFullAccessToReminders)EKEventStore 实例allowsContentModifications)EKSpan (.thisEvent 与 .futureEvents)commit()EKEventStoreChanged 通知以刷新过时数据references/eventkit-patterns.md每周安装数
332
代码仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex329
cursor326
github-copilot326
amp326
cline326
kimi-cli326
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+.
Add the required usage description strings based on what access level you need:
| Key | Access 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/NSRemindersUsageDescriptionkeys.
Create a single EKEventStore instance and reuse it. Do not mix objects from different event stores.
import EventKit
let eventStore = EKEventStore()
iOS 17+ introduced granular access levels. Use the modern async methods.
func requestCalendarAccess() async throws -> Bool {
let granted = try await eventStore.requestFullAccessToEvents()
return granted
}
Use when your app only creates events (e.g., saving a booking) and does not need to read existing events.
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
Use a date-range predicate to query events. The events(matching:) method returns occurrences of recurring events expanded within the range.
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)
}
Reminder fetches are asynchronous and return through a 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)
}
Use EKRecurrenceRule to create repeating events or reminders.
// 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)
When saving changes to a recurring event, specify the span:
// Change only this occurrence
try eventStore.save(event, span: .thisEvent)
// Change this and all future occurrences
try eventStore.save(event, span: .futureEvents)
Attach alarms to events or reminders to trigger notifications.
// 15 minutes before
let alarm = EKAlarm(relativeOffset: -15 * 60)
event.addAlarm(alarm)
// At an absolute date
let absoluteAlarm = EKAlarm(absoluteDate: alertDate)
event.addAlarm(absoluteAlarm)
Present the system event editor for creating or editing events.
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)
}
}
import EventKitUI
let viewer = EKEventViewController()
viewer.event = existingEvent
viewer.allowsEditing = true
navigationController?.pushViewController(viewer, animated: true)
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)
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).
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.
// 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()
// 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 usage description keys added for calendars and/or remindersrequestFullAccessToEvents, requestWriteOnlyAccessToEvents, requestFullAccessToReminders)EKEventStore instance reused across the appallowsContentModifications checked)EKSpan (.thisEvent vs .futureEvents)references/eventkit-patterns.mdWeekly Installs
332
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex329
cursor326
github-copilot326
amp326
cline326
kimi-cli326
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
commit()EKEventStoreChanged notification observed to refresh stale data