mcpField
E
eventkit-calendar

eventkit-calendar

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

eventkit-calendar について

カレンダーやリマインダー機能を開発する Swift 開発者向けの EventKit ガイドです。権限、データ操作、繰り返し設定、アラーム、システム編集画面、変更監視を扱います。

主な機能

  1. 01カレンダーとリマインダーの権限設定
  2. 02イベントとリマインダーの CRUD
  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 — カレンダーとリマインダー

カレンダーイベントとリマインダーを作成、読み取り、管理します。認証、 イベントとリマインダーの 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 のデリゲートが完了コールバック内でコントローラーを閉じている

参考資料

コメント