mcpField
E
eventkit

eventkit

github.com/dpearson2699开发1.2k3.2k 次安装13 周前更新
代码仓库

关于 eventkit

EventKit 是面向 Apple 平台开发者的 Swift 技能指南,介绍日历和提醒事项的授权、读写、重复规则及系统编辑界面。内容特别说明不同访问级别和系统版本的处理方式。

主要功能

  1. 01日历与提醒事项授权
  2. 02创建、查询和编辑事件
  3. 03创建与完成提醒事项
  4. 04重复规则和闹钟设置
  5. 05系统编辑器及变更观察

适用场景

  1. 01按日期范围读取日历事件
  2. 02创建带重复规则的日程
  3. 03添加带截止日期的提醒事项
  4. 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 委托在完成回调中关闭控制器

参考资料

评论

支持 Markdown