mcpField
E
eventkit

eventkit

github.com/dpearson2699開発1.2k3.2k 回インストール13 週間前に更新
リポジトリ

eventkit について

EventKit は、Apple プラットフォームの開発者向けに、カレンダーとリマインダーの権限、読み書き、繰り返し設定、システム編集画面を解説する Swift スキルガイドです。アクセス権の種類と OS バージョンごとの扱いも説明します。

主な機能

  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

カレンダーやリマインダーの認可、CRUD、繰り返し、アラーム、システムエディタには EventKit を使用します。

目次

利用可能なバージョン

  • 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 のデリゲートが完了コールバック内でコントローラーを閉じている

参考資料

コメント