mcpField
E
eventkit-calendar

eventkit-calendar

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

关于 eventkit-calendar

面向 Swift 开发者的 EventKit 日历与提醒事项开发指南,讲解授权、数据操作和系统编辑界面。还涵盖重复规则、提醒、变更监听及常见数据一致性问题。

主要功能

  1. 01日历与提醒事项授权
  2. 02事件和提醒事项增删改查
  3. 03重复规则与闹钟设置
  4. 04EventKitUI 系统编辑界面
  5. 05日历变更监听与刷新

适用场景

  1. 01创建带提醒的日历事件
  2. 02按日期范围查询事件
  3. 03创建并完成提醒事项
  4. 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 代理在完成回调中关闭控制器

参考资料

评论

支持 Markdown