eventkit-calendar について
カレンダーやリマインダー機能を開発する Swift 開発者向けの EventKit ガイドです。権限、データ操作、繰り返し設定、アラーム、システム編集画面、変更監視を扱います。
主な機能
- 01カレンダーとリマインダーの権限設定
- 02イベントとリマインダーの CRUD
- 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 — カレンダーとリマインダー
カレンダーイベントとリマインダーを作成、読み取り、管理します。認証、 イベントとリマインダーの CRUD、繰り返しルール、アラーム、EventKitUI エディターを扱います。 対象は Swift 6.2 / iOS 26+ です。
目次
セットアップ
Info.plist キー
必要なアクセスレベルに応じて、必須の使用目的説明文字列を追加します。
| キー | アクセスレベル |
|---|---|
NSCalendarsFullAccessUsageDescription | イベントの読み取りと書き込み |
NSCalendarsWriteOnlyAccessUsageDescription | イベントの書き込みのみ(iOS 17+) |
NSRemindersFullAccessUsageDescription | リマインダーの読み取りと書き込み |
iOS 16 以前も対象とするアプリでは、従来の
NSCalendarsUsageDescription/NSRemindersUsageDescriptionキーも追加してください。
Event Store
EKEventStore のインスタンスを1つだけ作成し、再利用します。異なるイベントストアのオブジェクトを混在させないでください。
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通知を監視している - 特定の場所に関連するイベントでは、タイムゾーンを明示的に設定している
- 異なるイベントストアのインスタンス間で
EKObjectを共有していない - EventKitUI のデリゲートが完了コールバック内でコントローラーを閉じている
参考資料
- 拡張パターン(SwiftUI ラッパー、述語クエリ、バッチ処理):
references/eventkit-patterns.md - EventKit フレームワーク
- EKEventStore
- EKEvent
- EKReminder
- EKRecurrenceRule
- EKCalendar
- EventKit UI
- EKEventEditViewController
- EKCalendarChooser
- イベントストアへのアクセス
- 繰り返しイベントの作成