使用 EventKit 與 EventKitUI 建立、讀取及管理行事曆事件與提醒事項。適用於新增事件至使用者的行事曆、建立提醒事項、設定重複規則、請求行事曆或提醒事項存取權限、顯示事件編輯器、選擇行事曆、處理鬧鈴與警示、監聽行事曆變更,或使用 EKEventStore、EKEvent、EKReminder、EKCalendar、EKRecurrenceRule、EKEventEditViewController、EKCalendarChooser 或 EventKitUI 視圖等情境。
EventKit
使用 EventKit 處理行事曆與提醒事項的權限授權、CRUD(新增、讀取、更新、刪除)、重複規則、鬧鈴警示,以及系統編輯器。
目錄
可用性與版本支援
- iOS 17+: 請使用細粒度的完整存取(Full Access)或僅限寫入(Write-Only)請求方法;舊有的
requestAccess(to:)不再跳出提示並會拋出錯誤(throws)。系統事件編輯器可以在應用程式沒有行事曆存取權限的情況下建立事件。針對 iOS 10–16,請對新 API 進行版本檢查,使用舊版請求搭配NSCalendarsUsageDescription/NSRemindersUsageDescription;EventKitUI 可能還需要NSContactsUsageDescription。 - iOS 26+: 具型別的
EKEventStore.EventStoreChanged/.changed訊息已可用(需透過版本檢查 Guard 防護)。在較舊的系統版本中仍請保留EKEventStoreChanged。
初始設定
Info.plist 鍵值
請根據在權限要求中選擇的存取路徑新增對應的用途說明(Usage Description)。切勿僅為了簡化設定流程而要求超出範圍的權限。
無需授權的系統編輯器路徑不需要設定行事曆用途說明。直接寫入需要「僅限寫入」或「完整存取」權限;讀取則需要「完整存取」權限。提醒事項僅支援「完整存取」權限。
Event Store(事件存儲)
請建立單一 EKEventStore 實例並重複使用它。切勿混用來自不同 Event Store 的物件。
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:) 方法會返回指定區間內展開的重複事件。事件的 Predicate 時間跨度最多限制為 4 年,且 events(matching:) 與 enumerateEvents(matching:using:) 皆為同步執行,僅會返回已提交(committed)的事件。
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)
鬧鈴與警示
在事件或提醒事項附加鬧鈴(Alarm)以觸發通知。
// 15 分鐘前
let alarm = EKAlarm(relativeOffset: -15 * 60)
event.addAlarm(alarm)
// 指定絕對時間
let absoluteAlarm = EKAlarm(absoluteDate: alertDate)
event.addAlarm(absoluteAlarm)
若要設定提醒事項的地理圍籬(Geofence),請在 EKAlarm 上設定 EKStructuredLocation 與 .enter / .leave 接近條件,然後將其新增至提醒事項。完整的基於位置提醒事項範例請參閱 references/eventkit-patterns.md。
EventKitUI 控制器
EKEventEditViewController — 建立/編輯事件
顯示用於建立或編輯事件的系統事件編輯器。
在可用性與版本支援所述的免授權路徑下,編輯器會在獨立程序(Out of Process)中運作,並擁有自身的行事曆存取權限。切勿在關閉控制器後直接檢查該控制器以確認儲存內容;請一律透過獨立的完整存取權限重新查詢。
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 需要僅限寫入或完整行事曆存取權限。在僅寫入存取的 App 中,選擇器僅會顯示可寫入的行事曆,且只允許選擇單一可寫入行事曆。
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 物件可能會過期(Stale)。該通知會在 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)
切勿:建立事件時忽略時區
// 錯誤:使用者跨時區旅行時,事件時間將會顯示錯誤
<!-- truncated for translation batch; full body continues in source -->




