eventkit について
EventKit は、Apple プラットフォームの開発者向けに、カレンダーとリマインダーの権限、読み書き、繰り返し設定、システム編集画面を解説する Swift スキルガイドです。アクセス権の種類と OS バージョンごとの扱いも説明します。
主な機能
- 01カレンダーとリマインダーの権限設定
- 02イベントの作成・検索・編集
- 03リマインダーの作成と完了
- 04繰り返しルールとアラーム
- 05システム編集画面と変更監視
活用シーン
- 01期間を指定したカレンダーイベントの取得
- 02繰り返し予定の作成
- 03期限付きリマインダーの追加
- 04外部でのカレンダー変更後に画面を更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill eventkitインストール後、Claude Code で/eventkitを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/eventkit-patterns.md
詳細
EventKit
カレンダーやリマインダーの認可、CRUD、繰り返し、アラーム、システムエディタには EventKit を使用します。
目次
- 利用可能なバージョン
- セットアップ
- 認可
- イベントの作成
- イベントの取得
- リマインダー
- 繰り返しルール
- アラーム
- EventKitUI コントローラー
- 変更の監視
- よくある間違い
- レビュー用チェックリスト
- 参考資料
利用可能なバージョン
- iOS 17 以降: 権限を細かく指定できる全アクセス/書き込み専用リクエストメソッドを使用します。従来の
requestAccess(to:)ではプロンプトが表示されず、エラーがスローされます。システムのイベントエディタでは、アプリにカレンダーへのアクセス権がなくてもイベントを作成できます。iOS 10~16 では、これらの API をガードし、従来のリクエストとNSCalendarsUsageDescription/NSRemindersUsageDescriptionを使用します。EventKitUI ではNSContactsUsageDescriptionも必要になる場合があります。 - iOS 26 以降: 型付きメッセージ
EKEventStore.EventStoreChanged/.changedがガード付きで利用できます。それ以前のシステムではEKEventStoreChangedを使用します。
セットアップ
Info.plist キー
認可で選択したアクセス方法に応じて、使用目的の説明を追加します。セットアップを簡単にするためだけに、必要以上に広いアクセスをリクエストしないでください。
認可なしでシステムエディタを使用する場合、カレンダーの使用目的を示す文字列は不要です。直接書き込むには書き込み専用または全アクセスが必要です。読み取りには全アクセスが必要です。リマインダーで利用できるのは全アクセスのみです。
イベントストア
EKEventStore のインスタンスを 1 つ作成し、再利用します。異なるイベントストアのオブジェクトを混在させないでください。
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)
リマインダーのジオフェンスでは、EKStructuredLocation と .enter/.leave
の近接条件を EKAlarm に設定し、それをリマインダーに追加します。位置情報に基づく
リマインダーの全体的なパターンについては、
references/eventkit-patterns.md を参照してください。
EventKitUI コントローラー
EKEventEditViewController — イベントの作成/編集
イベントを作成または編集するためのシステムイベントエディターを表示します。
Availability に記載されている認証不要の方法では、エディターは 独自のカレンダーアクセス権を持って別プロセスで実行されます。保存内容を確認するために、 閉じたコントローラーを調べてはいけません。別途フルアクセス権がある場合にのみ再取得してください。
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 には、書き込み専用またはフルアクセスのカレンダーアクセス権が必要です。
書き込み専用のアプリでは、選択画面には書き込み可能なカレンダーのみが表示され、
書き込み可能なカレンダーを1つだけ選択できます。
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()
Availability に記載されている互換性フォールバックでのみ使用してください。
してはいけないこと:読み取り専用のカレンダーにイベントを保存する
// 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使用目的の説明キーを追加している - Availability のバージョンごとの区分に従って認証している
- 書き込み専用のカレンダーアクセス権は、イベントの直接作成にのみ使用し、イベントやカレンダーの読み取りには使用していない
- 取得または保存の前に認証ステータスを確認している
- イベントまたはリマインダーを取得する前にフルアクセス権が必要である
- アプリ全体で単一の
EKEventStoreインスタンスを再利用している - イベントを変更可能なカレンダーに保存している(
allowsContentModificationsを確認済み) - 繰り返しイベントの保存で正しい
EKSpan(.thisEventと.futureEventsのいずれか)を指定している - まとめて保存する場合、変更可能なカレンダーを検証し、
commit: falseで変更を準備し、例外をスローするcommit()を呼び出している。失敗時は未保存の状態をreset()し、無効になったすべてのEKObjectを破棄してから、再試行の前に再取得または再構築している - 古くなったデータを更新するために
EKEventStoreChanged通知を監視している - 変更の監視では、Availability に従って従来の通知またはガード付きの型付きメッセージを使用している
- 場所に固有のイベントではタイムゾーンを明示的に設定している
- 異なるイベントストアインスタンス間で EKObjects を共有していない
- EventKitUI のデリゲートが完了コールバック内でコントローラーを閉じている