🕊️

[Swift]EventKitでカレンダーを操作する

2022/12/16に公開

EventKitとは

https://developer.apple.com/documentation/eventkit
SwiftUIでは、EventKitフレームワークを使用することで、Apple純正のカレンダー・リマインダーのデータを取得・変更できます。
EventKit UIを使用することでイベントの表示・作成などのインタフェースを簡単にアプリに組み込むこともできます。(この記事では紹介しないです。)
今回は、EventKitを使用して、カレンダーのイベントの取得・作成・変更・削除を行います。

リマインダー編はこちらの記事で紹介しています

https://zenn.dev/ryodeveloper/articles/kame_ga_4_hiki

今回作成するファイル

今回作成するアプリでは、
カレンダーを操作(イベントの取得・作成・変更・削除)するEventManager.swiftと、
取得したイベントを表示するContentView.swiftと、
イベントの追加・変更をするCreateEventView.swiftを作成します。

カレンダーにアクセスする手順

1. Info.plistにKeyとValueの追加

https://developer.apple.com/documentation/bundleresources/information_property_list/nscalendarsusagedescription

Key

Privacy - Calendars Usage Description

Valueには下記のような利用用途を記載します。

カレンダーは、イベントの表示に使用されます。

2. カレンダーを操作するClassを作成

EventManager.swift
import Foundation

class EventManager: ObservableObject {
    
}

3. EventKitをインポート

EventManager.swift
import EventKit

4. カレンダーへのアクセスを要求

EventManager.swift
var store = EKEventStore()
// イベントへの認証ステータスのメッセージ
@Published var statusMessage = ""

init() {
    Task {
        do {
            // カレンダーへのアクセスを要求
            try await store.requestAccess(to: .event)
        } catch {
            print(error.localizedDescription)
        }
        // イベントへの認証ステータス
        let status = EKEventStore.authorizationStatus(for: .event)
        
        switch status {
        case .notDetermined:
            statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
        case .restricted:
            statusMessage = "カレンダーへのアクセスする\n権限がありません。"
        case .denied:
            statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
        case .authorized:
            statusMessage = "カレンダーへのアクセスが\n許可されています。"
        @unknown default:
            statusMessage = "@unknown default"
        }
    }
}

5. ContentViewでEventManagerを使用する

今回は、1つのEventManagerのインスタンスを複数のViewで使用したいため、@EnvironmentObjectを使用しています。

ContentViewを変更します。

ContentView.swift
struct ContentView: View {
    @EnvironmentObject var eventManager: EventManager
    
    var body: some View {
        VStack {
            Text(eventManager.statusMessage)
        }
    }
}
EventKit_EventApp.swift
 @main
 struct EventKit_EventApp: App {
     var body: some Scene {
         WindowGroup {
             ContentView()
+                .environmentObject(EventManager())
         }
     }
 }

6. 完成🎉

ここまでのコード
ContentView.swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var eventManager: EventManager
    
    var body: some View {
        VStack {
            Text(eventManager.statusMessage)
        }
    }
}
EventManager.swift
import Foundation
import EventKit

class EventManager: ObservableObject {
    var store = EKEventStore()
    // イベントへの認証ステータスのメッセージ
    @Published var statusMessage = ""
    
    init() {
        Task {
            do {
                // カレンダーへのアクセスを要求
                try await store.requestAccess(to: .event)
            } catch {
                print(error.localizedDescription)
            }
            // イベントへの認証ステータス
            let status = EKEventStore.authorizationStatus(for: .event)
            
            switch status {
            case .notDetermined:
                statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
            case .restricted:
                statusMessage = "カレンダーへのアクセスする\n権限がありません。"
            case .denied:
                statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
            case .authorized:
                statusMessage = "カレンダーへのアクセスが\n許可されています。"
            @unknown default:
                statusMessage = "@unknown default"
            }
        }
    }
}
EventKit_EventApp.swift
import SwiftUI

@main
struct EventKit_EventApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(EventManager())
        }
    }
}

イベントを表示する手順

1. イベントを取得する関数を作成

https://developer.apple.com/documentation/eventkit/retrieving_events_and_reminders

受け取った日付から、期間内のイベントを取得して、配列に追加する関数を作成します。

EventManager.swift
/// イベントの取得
func fetchEvent() {
    // 開始日コンポーネントの作成
    // 指定した日付の0:00:0
    let start = Calendar.current.startOfDay(for: day)
    // 終了日コンポーネントの作成
    // 指定した日付の23:59:1
    let end = Calendar.current.date(bySettingHour: 23, minute: 59, second: 1, of: start)
    // イベントストアのインスタンスメソッドから述語を作成
    var predicate: NSPredicate? = nil
    if let end {
        predicate = store.predicateForEvents(withStart: start, end: end, calendars: nil)
    }
    // 述語に一致するすべてのイベントを取得
    if let predicate {
        events = store.events(matching: predicate)
    }
}

predicateForEvents(withStart:end:calendars:)

https://developer.apple.com/documentation/eventkit/ekeventstore/1507479-predicateforevents

  • withStart
    指定した日付を含む、取得するイベントの始まりの範囲
    endに日付を指定してwithStartにnilを指定した場合はendより前のイベントを取得する。
  • end
    指定した日付を含まない、取得するイベントの終わりの範囲
    withStartに日付を指定してendにnilを指定した場合はwithStartより後のイベントを取得する。
  • calendars
    イベントを取得するカレンダーの指定
    すべてのカレンダーを取得したい場合は、nilにする。

2. fetchEvent()を呼び出す

カレンダーへのアクセスが許可されていた場合は、先ほど作成したfetchEvent()を呼び出します。

EventManager.swift
 init() {
     // イベントへの認証ステータス
     let status = EKEventStore.authorizationStatus(for: .event)
     
     switch status {
     case .notDetermined:
         statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
         Task {
             do {
                 // カレンダーへのアクセスを要求
                 try await store.requestAccess(to: .event)
             } catch {
                 print(error.localizedDescription)
             }
         }
     case .restricted:
         statusMessage = "カレンダーへのアクセスする\n権限がありません。"
     case .denied:
         statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
     case .authorized:
         statusMessage = "カレンダーへのアクセスが\n許可されています。"
+        fetchEvent()
     @unknown default:
         statusMessage = "@unknown default"
     }
 }

3. イベントを表示

ContentViewを下記のように変更します。
EventManagerの配列にイベントが入っている場合は、イベントの一覧を表示、
EventManagerの配列にイベントが入っていない場合は、認証ステータスが表示されます。

ContentView.swift
struct ContentView: View {
    @EnvironmentObject var eventManager: EventManager
    
    var body: some View {
        if let aEvent = eventManager.events {
            NavigationStack {
                List(aEvent, id: \.eventIdentifier) { event in
                    Text(event.title)
                }
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                            .labelsHidden()
                            .onChange(of: eventManager.day) { newValue in
                                eventManager.fetchEvent()
                            }
                    }
                }
            }
        } else {
            Text(eventManager.statusMessage)
        }
    }
}

4. イベントが変更されたら再描画する

https://developer.apple.com/documentation/eventkit/updating_with_notifications
EventKitで用意されているNotificationを設定することで、
カレンダーデータベースの変更を受け取り、指定した関数を実行させることができます。
今回は、変更を受け取ったらfetchEvent()を実行するようにします。

EventManager.swift
 /// イベントの取得
-func fetchEvent() {
+@objc func fetchEvent() {
     // 開始日コンポーネントの作成
     // 指定した日付の0:00:0
     let start = Calendar.current.startOfDay(for: day)
     // 終了日コンポーネントの作成
     // 指定した日付の23:59:1
     let end = Calendar.current.date(bySettingHour: 23, minute: 59, second: 1, of: start)
     // イベントストアのインスタンスメソッドから述語を作成
     var predicate: NSPredicate? = nil
     if let end {
         predicate = store.predicateForEvents(withStart: start, end: end, calendars: nil)
     }
     // 述語に一致するすべてのイベントを取得
     if let predicate {
         events = store.events(matching: predicate)
     }
 }
EventManager.swift
 switch status {
 case .notDetermined:
     statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
 case .restricted:
     statusMessage = "カレンダーへのアクセスする\n権限がありません。"
 case .denied:
     statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
 case .authorized:
     statusMessage = "カレンダーへのアクセスが\n許可されています。"
     fetchEvent()
+    // カレンダーデータベースの変更を検出したらfetchEvent()を実行する
+    NotificationCenter.default.addObserver(self, selector: #selector(fetchEvent), name: .EKEventStoreChanged, object: store)
 @unknown default:
     statusMessage = "@unknown default"   
 }

NotificationCenter.default.addObserver(selector:name:object:)

https://developer.apple.com/documentation/foundation/notification

  • selector:
    変更を受け取ったら実行したい関数
    #selector(実行したい変数名)

  • name:
    変更を受け取る通知の種類
    今回は、EKEventの変更を受け撮りたいため下記のタイプを指定する。
    .EKEventStoreChanged

  • object:
    受け取る通知を発行するオブジェクト
    今回は、EKEventの変更を受け撮りたいため、アクセスの要求やイベントの取得に使用しているEKEventStore()の変数を指定する。

5. 完成🎉

ここまでのコード
ContentView.swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var eventManager: EventManager
    
    var body: some View {
        if let aEvent = eventManager.events {
            NavigationStack {
                List(aEvent, id: \.eventIdentifier) { event in
                    Text(event.title)
                }
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                            .labelsHidden()
                            .onChange(of: eventManager.day) { newValue in
                                eventManager.fetchEvent()
                            }
                    }
                }
            }
        } else {
            Text(eventManager.statusMessage)
        }
    }
}
EventManager.swift
import Foundation
import EventKit

class EventManager: ObservableObject {
    var store = EKEventStore()
    // イベントへの認証ステータスのメッセージ
    @Published var statusMessage = ""
    // 取得されたevents
    @Published var events: [EKEvent]? = nil
    // 取得したいイベントの日付
    @Published var day = Date()
    
    init() {
        Task {
            do {
                // カレンダーへのアクセスを要求
                try await store.requestAccess(to: .event)
            } catch {
                print(error.localizedDescription)
            }
            // イベントへの認証ステータス
            let status = EKEventStore.authorizationStatus(for: .event)
            
            switch status {
            case .notDetermined:
                statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
            case .restricted:
                statusMessage = "カレンダーへのアクセスする\n権限がありません。"
            case .denied:
                statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
            case .authorized:
                statusMessage = "カレンダーへのアクセスが\n許可されています。"
                fetchEvent()
  // カレンダーデータベースの変更を検出したらfetchEvent()を実行する
                NotificationCenter.default.addObserver(self, selector: #selector(fetchEvent), name: .EKEventStoreChanged, object: store)
            @unknown default:
                statusMessage = "@unknown default"
            }
        }
    }
    
    /// イベントの取得
    @objc func fetchEvent() {
        // 開始日コンポーネントの作成
        // 指定した日付の0:00:0
        let start = Calendar.current.startOfDay(for: day)
        // 終了日コンポーネントの作成
        // 指定した日付の23:59:1
        let end = Calendar.current.date(bySettingHour: 23, minute: 59, second: 1, of: start)
        // イベントストアのインスタンスメソッドから述語を作成
        var predicate: NSPredicate? = nil
        if let end {
            predicate = store.predicateForEvents(withStart: start, end: end, calendars: nil)
        }
        // 述語に一致するすべてのイベントを取得
        if let predicate {
            events = store.events(matching: predicate)
        }
    }
}
EventManager.swift
import SwiftUI

@main
struct EventKit_EventApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(EventManager())
        }
    }
}

イベントを追加する手順

1. イベントを追加する関数を作成

https://developer.apple.com/documentation/eventkit/creating_events_and_reminders

受け取ったタイトルと日時から、イベントを追加する関数を作成します。

EventManager.swift
/// イベントの追加
func createEvent(title: String, startDate: Date, endDate: Date){
    // 新規イベントの作成
    let event = EKEvent(eventStore: store)
    event.title = title
    event.startDate = startDate
    event.endDate = endDate
    // 保存するカレンダー
    // デフォルトカレンダー
    event.calendar = store.defaultCalendarForNewEvents
    do {
        try store.save(event, span: .thisEvent, commit: true)
    } catch {
        print(error.localizedDescription)
    }
}

save(_:span:commit:)

https://developer.apple.com/documentation/eventkit/ekeventstore/1507295-save

  • event
    保存するイベント
  • span
    定期的なイベントの場合、影響範囲を単一のイベントにする場合は、
    .thisEvent
    将来のすべてにする場合は、
    .futureEvents
  • commit
    イベントをすぐに保存するか
    すぐに保存したい場合は
    true
    この引数を省略することも可能。

2. イベントの追加をするViewを作成

CreateEventView.swift
import SwiftUI

struct CreateEventView: View {
    @EnvironmentObject var eventManager: EventManager
    // ContentViewのsheetのフラグ
    @Environment(\.dismiss) var dismiss
    // eventのタイトル
    @State var title = ""
    // eventの開始日時
    @State var start = Date()
    // eventの終了日時
    @State var end = Date()
    
    var body: some View {
        NavigationStack{
            List {
                TextField("タイトル", text: $title)
                DatePicker("開始", selection: $start)
                //in: start...はstartより前を選択できないようにするため
                DatePicker("終了", selection: $end, in: start...)
                    .onChange(of: start) { newValue in
                        // in: start...では、すでに代入済みの値は変更しないため
                        if start > end {
                            end = start
                        }
                    }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("追加") {
                        eventManager.createEvent(title: title, startDate: start, endDate: end)
                        // sheetを閉じる
                        dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("キャンセル", role: .destructive) {
                        // sheetを閉じる
                        dismiss()
                    }
                    .buttonStyle(.borderless)
                }
            }
        }
    }
}

3. CreateEventViewをContentViewから呼び出す

ContentViewのToolbarの+をタップすると、
sheetでCreateEventViewを表示するようにします。

ContentView.swift
 struct ContentView: View {
     @EnvironmentObject var eventManager: EventManager
+    // sheetのフラグ
+    @State var isShowCreateEventView = false
     
     var body: some View {
         if let aEvent = eventManager.events {
             NavigationStack {
                 List(aEvent, id: \.eventIdentifier) { event in
                     Text(event.title)
                 }
+                .sheet(isPresented: $isShowCreateEventView) {
+                        CreateEventView()
+                        .presentationDetents([.medium])
+                }
                 .toolbar {
                     ToolbarItem(placement: .principal) {
                         DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                             .labelsHidden()
                             .onChange(of: eventManager.day) { newValue in
                                 eventManager.fetchEvent()
                             }
                     }
+                    ToolbarItem(placement: .navigationBarTrailing) {
+                        Button {
+                            isShowCreateEventView = true
+                        } label: {
+                            Label("追加", systemImage: "plus")
+                        }
+                    }
                 }
             }
         } else {
             Text(eventManager.statusMessage)
         }
     }
 }

4. 完成🎉

ここまでのコード
ContentView.swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var eventManager: EventManager
    // sheetのフラグ
    @State var isShowCreateEventView = false
    
    var body: some View {
        if let aEvent = eventManager.events {
            NavigationStack {
                List(aEvent, id: \.eventIdentifier) { event in
                    Text(event.title)
                }
                .sheet(isPresented: $isShowCreateEventView) {
                    CreateEventView()
                        .presentationDetents([.medium])
                }
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                            .labelsHidden()
                            .onChange(of: eventManager.day) { newValue in
                                eventManager.fetchEvent()
                            }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            isShowCreateEventView = true
                        } label: {
                            Label("追加", systemImage: "plus")
                        }
                    }
                }
            }
        } else {
            Text(eventManager.statusMessage)
        }
    }
}
CreateEventView.swift
import SwiftUI

struct CreateEventView: View {
    @EnvironmentObject var eventManager: EventManager
    // ContentViewのsheetのフラグ
    @Environment(\.dismiss) var dismiss
    // eventのタイトル
    @State var title = ""
    // eventの開始日時
    @State var start = Date()
    // eventの終了日時
    @State var end = Date()
    
    var body: some View {
        NavigationStack{
            List {
                TextField("タイトル", text: $title)
                DatePicker("開始", selection: $start)
                //in: start...はstartより前を選択できないようにするため
                DatePicker("終了", selection: $end, in: start...)
                    .onChange(of: start) { newValue in
                        // in: start...では、すでに代入済みの値は変更しないため
                        if start > end {
                            end = start
                        }
                    }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("追加") {
                        eventManager.createEvent(title: title, startDate: start, endDate: end)
                        // sheetを閉じる
                        dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("キャンセル", role: .destructive) {
                        // sheetを閉じる
                        dismiss()
                    }
                    .buttonStyle(.borderless)
                }
            }
        }
    }
}
EventManager.swift
import Foundation
import EventKit

class EventManager: ObservableObject {
    var store = EKEventStore()
    // イベントへの認証ステータスのメッセージ
    @Published var statusMessage = ""
    // 取得されたevents
    @Published var events: [EKEvent]? = nil
    // 取得したいイベントの日付
    @Published var day = Date()
    
    init() {
        Task {
            do {
                // カレンダーへのアクセスを要求
                try await store.requestAccess(to: .event)
            } catch {
                print(error.localizedDescription)
            }
            // イベントへの認証ステータス
            let status = EKEventStore.authorizationStatus(for: .event)
            
            switch status {
            case .notDetermined:
                statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
            case .restricted:
                statusMessage = "カレンダーへのアクセスする\n権限がありません。"
            case .denied:
                statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
            case .authorized:
                statusMessage = "カレンダーへのアクセスが\n許可されています。"
                fetchEvent()
  // カレンダーデータベースの変更を検出したらfetchEvent()を実行する
                NotificationCenter.default.addObserver(self, selector: #selector(fetchEvent), name: .EKEventStoreChanged, object: store)
            @unknown default:
                statusMessage = "@unknown default"
            }
        }
    }
    
    /// イベントの取得
    @objc func fetchEvent() {
        // 開始日コンポーネントの作成
        // 指定した日付の0:00:0
        let start = Calendar.current.startOfDay(for: day)
        // 終了日コンポーネントの作成
        // 指定した日付の23:59:1
        let end = Calendar.current.date(bySettingHour: 23, minute: 59, second: 1, of: start)
        // イベントストアのインスタンスメソッドから述語を作成
        var predicate: NSPredicate? = nil
        if let end {
            predicate = store.predicateForEvents(withStart: start, end: end, calendars: nil)
        }
        // 述語に一致するすべてのイベントを取得
        if let predicate {
            events = store.events(matching: predicate)
        }
    }
    
    /// イベントの追加
    func createEvent(title: String, startDate: Date, endDate: Date){
        // 新規イベントの作成
        let event = EKEvent(eventStore: store)
        event.title = title
        event.startDate = startDate
        event.endDate = endDate
        // 保存するカレンダー
        // デフォルトカレンダー
        event.calendar = store.defaultCalendarForNewEvents
        do {
            try store.save(event, span: .thisEvent, commit: true)
        } catch {
            print(error.localizedDescription)
        }
    }
}

EventKit_EventApp.swift
import SwiftUI

@main
struct EventKit_EventApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(EventManager())
        }
    }
}

イベントを変更する手順

1. イベントを変更する関数を作成

https://developer.apple.com/documentation/eventkit/creating_events_and_reminders

受け取ったイベントとタイトルと日時からイベントを変更する関数を作成します。

EventManager.swift
/// イベントの変更
func modifyEvent(event: EKEvent,title: String, startDate: Date, endDate: Date){
    // 変更したいイベントを取得
    event.title = title
    event.startDate = startDate
    event.endDate = endDate
    // 保存するカレンダー
    // デフォルトカレンダー
    event.calendar = store.defaultCalendarForNewEvents
    do {
        try store.save(event, span: .thisEvent, commit: true)
    } catch {
        print(error.localizedDescription)
    }
}

save(_:span:commit:)

https://developer.apple.com/documentation/eventkit/ekeventstore/1507295-save

  • event
    保存するイベント
  • span
    定期的なイベントの場合、影響範囲を単一のイベントにする場合は、
    .thisEvent
    将来のすべてにする場合は、
    .futureEvents
  • commit
    イベントをすぐに保存するか
    すぐに保存したい場合は
    true
    この引数を省略することも可能。

2. EventKitをインポート

CreateEventView.swift
import EventKit

3. CreateEventViewを変更にも対応させる

CreateEventViewでイベントの追加も変更もできるようにしていきます。
CreateEventViewを呼び出す際に引数にイベントを持たせて、
イベントが渡されたた場合は、そのイベントを変更、
nilが渡された場合は追加になるように修正します。

CreateEventView.swift
 struct CreateEventView: View {
     @EnvironmentObject var eventManager: EventManager
     // ContentViewのsheetのフラグ
     @Environment(\.dismiss) var dismiss
+    // 変更したいイベント(nilの場合は新規追加)
+    @Binding var event: EKEvent?
     // eventのタイトル
     @State var title = ""
     // eventの開始日時
     @State var start = Date()
     // eventの終了日時
     @State var end = Date()
     
     var body: some View {
         NavigationStack{
             List {
                 TextField("タイトル", text: $title)
                 DatePicker("開始", selection: $start)
                 //in: start...はstartより前を選択できないようにするため
                 DatePicker("終了", selection: $end, in: start...)
                     .onChange(of: start) { newValue in
                         // in: start...では、すでに代入済みの値は変更しないため
                         if start > end {
                             end = start
                         }
                     }
             }
             .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
-       Button("追加") {
+                    Button(event == nil ? "追加" : "変更") {
+                        if let event {
+                            eventManager.modifyEvent(event: event, title: title, startDate: start, endDate: end)
+                        } else{
                             eventManager.createEvent(title: title, startDate: start, endDate: end)
+                        }
                         // sheetを閉じる
                         dismiss()
                     }
                 }
                 ToolbarItem(placement: .navigationBarLeading) {
                     Button("キャンセル", role: .destructive) {
                         // sheetを閉じる
                         dismiss()
                     }
                     .buttonStyle(.borderless)
                 }
             }
         }
+        .task {
+            if let event {
+                // eventが渡されたら既存の値をセットする(変更の場合)
+                self.title = event.title
+                self.start = event.startDate
+                self.end = event.endDate
+            }
+        }
     }
 }

4. EventKitをインポート

ContentView.swift
import EventKit

5. CreateEventViewをContentViewから呼び出す(変更に対応させる)

イベントのタイトルをタップしたら変更(CreateEventView.swiftに変更したいイベントを渡して開く)
Toolbarの+をタップしたら追加(CreateEventView.swiftにnilを渡して開く)

ContentView.swift
 struct ContentView: View {
     @EnvironmentObject var eventManager: EventManager
     // sheetのフラグ
     @State var isShowCreateEventView = false
+    // 変更したいイベント(追加の場合はnil)
+    @State var event: EKEvent?
     
     var body: some View {
         if let aEvent = eventManager.events {
             NavigationStack {
                 List(aEvent, id: \.eventIdentifier) { event in
-       Text(event.title)
+                    Button(event.title) {
+                        // 変更したいイベントをCreateEventViewに送る
+                        self.event = event
+                        isShowCreateEventView = true
+                    }
                 }
                 .sheet(isPresented: $isShowCreateEventView) {
-       CreateEventView()
+                    CreateEventView(event: $event)
                         .presentationDetents([.medium])
                 }
                 .toolbar {
                     ToolbarItem(placement: .principal) {
                         DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                             .labelsHidden()
                             .onChange(of: eventManager.day) { newValue in
                                 eventManager.fetchEvent()
                             }
                     }
                     ToolbarItem(placement: .navigationBarTrailing) {
                         Button {
+                            // 追加したい場合は、CreateEventViewにイベントを送らない(nilを送る)
+                            event = nil
                             isShowCreateEventView = true
                         } label: {
                             Label("追加", systemImage: "plus")
                         }
                     }
                 }
             }
         } else {
             Text(eventManager.statusMessage)
         }
     }
 }

6. 完成🎉

ここまでのコード
ContentView.swift
import SwiftUI
import EventKit

struct ContentView: View {
    @EnvironmentObject var eventManager: EventManager
    // sheetのフラグ
    @State var isShowCreateEventView = false
    // 変更したいイベント(追加の場合はnil)
    @State var event: EKEvent?
    
    var body: some View {
        if let aEvent = eventManager.events {
            NavigationStack {
                List(aEvent, id: \.eventIdentifier) { event in
                    Button(event.title) {
                        // 変更したいイベントをCreateEventViewに送る
                        self.event = event
                        isShowCreateEventView = true
                    }
                }
                .sheet(isPresented: $isShowCreateEventView) {
                    CreateEventView(event: $event)
                        .presentationDetents([.medium])
                }
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                            .labelsHidden()
                            .onChange(of: eventManager.day) { newValue in
                                eventManager.fetchEvent()
                            }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            // 追加したい場合は、CreateEventViewにイベントを送らない(nilを送る)
                            event = nil
                            isShowCreateEventView = true
                        } label: {
                            Label("追加", systemImage: "plus")
                        }
                    }
                }
            }
        } else {
            Text(eventManager.statusMessage)
        }
    }
}
CreateEventView.swift
import SwiftUI
import EventKit

struct CreateEventView: View {
    @EnvironmentObject var eventManager: EventManager
    // ContentViewのsheetのフラグ
    @Environment(\.dismiss) var dismiss
    // 変更したいイベント(nilの場合は新規追加)
    @Binding var event: EKEvent?
    // eventのタイトル
    @State var title = ""
    // eventの開始日時
    @State var start = Date()
    // eventの終了日時
    @State var end = Date()

    var body: some View {
        NavigationStack{
            List {
                TextField("タイトル", text: $title)
                DatePicker("開始", selection: $start)
                //in: start...はstartより前を選択できないようにするため
                DatePicker("終了", selection: $end, in: start...)
                    .onChange(of: start) { newValue in
                        // in: start...では、すでに代入済みの値は変更しないため
                        if start > end {
                            end = start
                        }
                    }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(event == nil ? "追加" : "変更") {
                        if let event {
                            eventManager.modifyEvent(event: event, title: title, startDate: start, endDate: end)
                        } else{
                            eventManager.createEvent(title: title, startDate: start, endDate: end)
                        }
                        // sheetを閉じる
                        dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("キャンセル", role: .destructive) {
                        // sheetを閉じる
                        dismiss()
                    }
                    .buttonStyle(.borderless)
                }
            }
        }
        .task {
            if let event {
                // eventが渡されたら既存の値をセットする(変更の場合)
                self.title = event.title
                self.start = event.startDate
                self.end = event.endDate
            }
        }
    }
}
EventManager.swift
import Foundation
import EventKit

class EventManager: ObservableObject {
    var store = EKEventStore()
    // イベントへの認証ステータスのメッセージ
    @Published var statusMessage = ""
    // 取得されたevents
    @Published var events: [EKEvent]? = nil
    // 取得したいイベントの日付
    @Published var day = Date()
    
    init() {
        Task {
            do {
                // カレンダーへのアクセスを要求
                try await store.requestAccess(to: .event)
            } catch {
                print(error.localizedDescription)
            }
            // イベントへの認証ステータス
            let status = EKEventStore.authorizationStatus(for: .event)
            
            switch status {
            case .notDetermined:
                statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
            case .restricted:
                statusMessage = "カレンダーへのアクセスする\n権限がありません。"
            case .denied:
                statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
            case .authorized:
                statusMessage = "カレンダーへのアクセスが\n許可されています。"
                fetchEvent()
                // カレンダーデータベースの変更を検出したらfetchEvent()を実行する
                NotificationCenter.default.addObserver(self, selector: #selector(fetchEvent), name: .EKEventStoreChanged, object: store)
            @unknown default:
                statusMessage = "@unknown default"
            }
        }
    }
    
    /// イベントの取得
    @objc func fetchEvent() {
        // 開始日コンポーネントの作成
        // 指定した日付の0:00:0
        let start = Calendar.current.startOfDay(for: day)
        // 終了日コンポーネントの作成
        // 指定した日付の23:59:1
        let end = Calendar.current.date(bySettingHour: 23, minute: 59, second: 1, of: start)
        // イベントストアのインスタンスメソッドから述語を作成
        var predicate: NSPredicate? = nil
        if let end {
            predicate = store.predicateForEvents(withStart: start, end: end, calendars: nil)
        }
        // 述語に一致するすべてのイベントを取得
        if let predicate {
            events = store.events(matching: predicate)
        }
    }
    
    /// イベントの追加
    func createEvent(title: String, startDate: Date, endDate: Date){
        // 新規イベントの作成
        let event = EKEvent(eventStore: store)
        event.title = title
        event.startDate = startDate
        event.endDate = endDate
        // 保存するカレンダー
        // デフォルトカレンダー
        event.calendar = store.defaultCalendarForNewEvents
        do {
            try store.save(event, span: .thisEvent, commit: true)
        } catch {
            print(error.localizedDescription)
        }
    }
    
    /// イベントの変更
    func modifyEvent(event: EKEvent,title: String, startDate: Date, endDate: Date){
        // 変更したいイベントを取得
        event.title = title
        event.startDate = startDate
        event.endDate = endDate
        // 保存するカレンダー
        // デフォルトカレンダー
        event.calendar = store.defaultCalendarForNewEvents
        do {
            try store.save(event, span: .thisEvent, commit: true)
        } catch {
            print(error.localizedDescription)
        }
    }
}
EventKit_EventApp.swift
import SwiftUI

@main
struct EventKit_EventApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(EventManager())
        }
    }
}

イベントを削除する手順

1. イベントを削除する関数を作成

受け取ったイベントを削除する関数を作成します。

EventManager.swift
/// イベントの削除
func deleteEvent(event: EKEvent){
    // 削除したいイベントを取得
    do {
        try store.remove(event, span: .thisEvent, commit: true)
    } catch {
        print(error.localizedDescription)
    }
}

2. ContentViewからイベントを削除できるようにする

イベントのタイトルを長押ししたら削除ボタンを表示するようにします。

ContentView.swift
 struct ContentView: View {
     @EnvironmentObject var eventManager: EventManager
     // sheetのフラグ
     @State var isShowCreateEventView = false
     // 変更するイベント(nilの場合は新規追加)
     @State var event: EKEvent?
     
     var body: some View {
         if let aEvent = eventManager.events {
             NavigationStack {
                 List(aEvent, id: \.eventIdentifier) { event in
                     Button(event.title) {
                         // 変更の場合は、CreateEventViewに変更したいイベントを送る
                         self.event = event
                         isShowCreateEventView = true
                     }
+                    .contextMenu {
+                        Button(role: .destructive) {
+                            eventManager.deleteEvent(event: event)
+                        } label: {
+                            Label("削除", systemImage: "trash")
+                        }
+                    }
                 }
                 .sheet(isPresented: $isShowCreateEventView) {
                     CreateEventView(event: $event)
                         .presentationDetents([.medium])
                 }
                 .toolbar {
                     ToolbarItem(placement: .principal) {
                         DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                             .labelsHidden()
                             .onChange(of: eventManager.day) { newValue in
                                 eventManager.fetchEvent()
                             }
                     }
                     ToolbarItem(placement: .navigationBarTrailing) {
                         Button {
                             // 追加の場合は、CreateEventViewにnilを送る
                             event = nil
                             isShowCreateEventView = true
                         } label: {
                             Label("追加", systemImage: "plus")
                         }
                     }
                 }
             }
         } else {
             Text(eventManager.statusMessage)
         }
     }
 }

3. 完成🎉

ここまでのコード
ContentView.swift
import SwiftUI
import EventKit

struct ContentView: View {
    @EnvironmentObject var eventManager: EventManager
    // sheetのフラグ
    @State var isShowCreateEventView = false
    // 変更したいイベント(追加の場合はnil)
    @State var event: EKEvent?
    
    var body: some View {
        if let aEvent = eventManager.events {
            NavigationStack {
                List(aEvent, id: \.eventIdentifier) { event in
                    Button(event.title) {
                        // 変更したいイベントをCreateEventViewに送る
                        self.event = event
                        isShowCreateEventView = true
                    }
                    .contextMenu {
                        Button(role: .destructive) {
                            eventManager.deleteEvent(event: event)
                        } label: {
                            Label("削除", systemImage: "trash")
                        }
                    }
                    
                }
                .sheet(isPresented: $isShowCreateEventView) {
                    CreateEventView(event: $event)
                        .presentationDetents([.medium])
                }
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        DatePicker("", selection: $eventManager.day, displayedComponents: .date)
                            .labelsHidden()
                            .onChange(of: eventManager.day) { newValue in
                                eventManager.fetchEvent()
                            }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            // 追加したい場合は、CreateEventViewにイベントを送らない(nilを送る)
                            event = nil
                            isShowCreateEventView = true
                        } label: {
                            Label("追加", systemImage: "plus")
                        }
                    }
                }
            }
        } else {
            Text(eventManager.statusMessage)
        }
    }
}
CreateEventView.swift
import SwiftUI
import EventKit

struct CreateEventView: View {
    @EnvironmentObject var eventManager: EventManager
    // ContentViewのsheetのフラグ
    @Environment(\.dismiss) var dismiss
    // 変更したいイベント(nilの場合は新規追加)
    @Binding var event: EKEvent?
    // eventのタイトル
    @State var title = ""
    // eventの開始日時
    @State var start = Date()
    // eventの終了日時
    @State var end = Date()
    
    var body: some View {
        NavigationStack{
            List {
                TextField("タイトル", text: $title)
                DatePicker("開始", selection: $start)
                //in: start...はstartより前を選択できないようにするため
                DatePicker("終了", selection: $end, in: start...)
                    .onChange(of: start) { newValue in
                        // in: start...では、すでに代入済みの値は変更しないため
                        if start > end {
                            end = start
                        }
                    }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(event == nil ? "追加" : "変更") {
                        if let event {
                            eventManager.modifyEvent(event: event, title: title, startDate: start, endDate: end)
                        } else{
                            eventManager.createEvent(title: title, startDate: start, endDate: end)
                        }
                        // sheetを閉じる
                        dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("キャンセル", role: .destructive) {
                        // sheetを閉じる
                        dismiss()
                    }
                    .buttonStyle(.borderless)
                }
            }
        }
        .task {
            if let event {
                // eventが渡されたら既存の値をセットする(変更の場合)
                self.title = event.title
                self.start = event.startDate
                self.end = event.endDate
            }
        }
    }
}
EventManager.swift
import Foundation
import EventKit

class EventManager: ObservableObject {
    var store = EKEventStore()
    // イベントへの認証ステータスのメッセージ
    @Published var statusMessage = ""
    // 取得されたevents
    @Published var events: [EKEvent]? = nil
    // 取得したいイベントの日付
    @Published var day = Date()
    
    init() {
        Task {
            do {
                // カレンダーへのアクセスを要求
                try await store.requestAccess(to: .event)
            } catch {
                print(error.localizedDescription)
            }
            // イベントへの認証ステータス
            let status = EKEventStore.authorizationStatus(for: .event)
            
            switch status {
            case .notDetermined:
                statusMessage = "カレンダーへのアクセスする\n権限が選択されていません。"
            case .restricted:
                statusMessage = "カレンダーへのアクセスする\n権限がありません。"
            case .denied:
                statusMessage = "カレンダーへのアクセスが\n明示的に拒否されています。"
            case .authorized:
                statusMessage = "カレンダーへのアクセスが\n許可されています。"
                fetchEvent()
                // カレンダーデータベースの変更を検出したらfetchEvent()を実行する
                NotificationCenter.default.addObserver(self, selector: #selector(fetchEvent), name: .EKEventStoreChanged, object: store)
            @unknown default:
                statusMessage = "@unknown default"
            }
        }
    }
    
    /// イベントの取得
    @objc func fetchEvent() {
        // 開始日コンポーネントの作成
        // 指定した日付の0:00:0
        let start = Calendar.current.startOfDay(for: day)
        // 終了日コンポーネントの作成
        // 指定した日付の23:59:1
        let end = Calendar.current.date(bySettingHour: 23, minute: 59, second: 1, of: start)
        // イベントストアのインスタンスメソッドから述語を作成
        var predicate: NSPredicate? = nil
        if let end {
            predicate = store.predicateForEvents(withStart: start, end: end, calendars: nil)
        }
        // 述語に一致するすべてのイベントを取得
        if let predicate {
            events = store.events(matching: predicate)
        }
    }
    
    /// イベントの追加
    func createEvent(title: String, startDate: Date, endDate: Date){
        // 新規イベントの作成
        let event = EKEvent(eventStore: store)
        event.title = title
        event.startDate = startDate
        event.endDate = endDate
        // 保存するカレンダー
        // デフォルトカレンダー
        event.calendar = store.defaultCalendarForNewEvents
        do {
            try store.save(event, span: .thisEvent, commit: true)
        } catch {
            print(error.localizedDescription)
        }
    }
    
    /// イベントの変更
    func modifyEvent(event: EKEvent,title: String, startDate: Date, endDate: Date){
        // 変更したいイベントを取得
        event.title = title
        event.startDate = startDate
        event.endDate = endDate
        // 保存するカレンダー
        // デフォルトカレンダー
        event.calendar = store.defaultCalendarForNewEvents
        do {
            try store.save(event, span: .thisEvent, commit: true)
        } catch {
            print(error.localizedDescription)
        }
    }
    
    /// イベントの削除
    func deleteEvent(event: EKEvent){
        // 削除したいイベントを取得
        do {
            try store.remove(event, span: .thisEvent, commit: true)
        } catch {
            print(error.localizedDescription)
        }
    }
}
EventKit_EventApp.swift
import SwiftUI

@main
struct EventKit_EventApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(EventManager())
        }
    }
}

EventKitを使用したサンプルアプリ

この記事で紹介した、

  1. イベントの取得
  2. イベントの追加
  3. イベントの変更
  4. イベントの削除
    上記の機能組み合わせたサンプルをGitHubに公開しました。
    終日の選択や、URLの追加や、カレンダーのカラーでの表示なども実装しています。

https://github.com/kame-08/EventKit-Event

脚注
  1. 2022年12月16日現在
    Xcode14.2およびiOS16.2で検証 ↩︎

Discussion