[Swift]EventKitでカレンダーを操作する
EventKitとは
EventKit UIを使用することでイベントの表示・作成などのインタフェースを簡単にアプリに組み込むこともできます。(この記事では紹介しないです。)
今回は、EventKitを使用して、カレンダーのイベントの取得・作成・変更・削除を行います。
リマインダー編はこちらの記事で紹介しています
今回作成するファイル
今回作成するアプリでは、
カレンダーを操作(イベントの取得・作成・変更・削除)するEventManager.swiftと、
取得したイベントを表示するContentView.swiftと、
イベントの追加・変更をするCreateEventView.swiftを作成します。
カレンダーにアクセスする手順
1. Info.plistにKeyとValueの追加
Key
Privacy - Calendars Usage Description
Valueには下記のような利用用途を記載します。
カレンダーは、イベントの表示に使用されます。
2. カレンダーを操作するClassを作成
import Foundation
class EventManager: ObservableObject {
}
3. EventKitをインポート
import EventKit
4. カレンダーへのアクセスを要求
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を変更します。
struct ContentView: View {
@EnvironmentObject var eventManager: EventManager
var body: some View {
VStack {
Text(eventManager.statusMessage)
}
}
}
@main
struct EventKit_EventApp: App {
var body: some Scene {
WindowGroup {
ContentView()
+ .environmentObject(EventManager())
}
}
}
6. 完成🎉
ここまでのコード
import SwiftUI
struct ContentView: View {
@EnvironmentObject var eventManager: EventManager
var body: some View {
VStack {
Text(eventManager.statusMessage)
}
}
}
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"
}
}
}
}
import SwiftUI
@main
struct EventKit_EventApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(EventManager())
}
}
}
イベントを表示する手順
1. イベントを取得する関数を作成
受け取った日付から、期間内のイベントを取得して、配列に追加する関数を作成します。
/// イベントの取得
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:)
- withStart
指定した日付を含む、取得するイベントの始まりの範囲
endに日付を指定してwithStartにnilを指定した場合はendより前のイベントを取得する。 - end
指定した日付を含まない、取得するイベントの終わりの範囲
withStartに日付を指定してendにnilを指定した場合はwithStartより後のイベントを取得する。 - calendars
イベントを取得するカレンダーの指定
すべてのカレンダーを取得したい場合は、nilにする。
2. fetchEvent()を呼び出す
カレンダーへのアクセスが許可されていた場合は、先ほど作成したfetchEvent()を呼び出します。
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の配列にイベントが入っていない場合は、認証ステータスが表示されます。
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. イベントが変更されたら再描画する
カレンダーデータベースの変更を受け取り、指定した関数を実行させることができます。
今回は、変更を受け取ったらfetchEvent()を実行するようにします。
/// イベントの取得
-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)
}
}
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:)
-
selector:
変更を受け取ったら実行したい関数
#selector(実行したい変数名)
-
name:
変更を受け取る通知の種類
今回は、EKEventの変更を受け撮りたいため下記のタイプを指定する。
.EKEventStoreChanged
- object:
受け取る通知を発行するオブジェクト
今回は、EKEventの変更を受け撮りたいため、アクセスの要求やイベントの取得に使用しているEKEventStore()
の変数を指定する。
5. 完成🎉
ここまでのコード
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)
}
}
}
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)
}
}
}
import SwiftUI
@main
struct EventKit_EventApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(EventManager())
}
}
}
イベントを追加する手順
1. イベントを追加する関数を作成
受け取ったタイトルと日時から、イベントを追加する関数を作成します。
/// イベントの追加
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:)
- event
保存するイベント - span
定期的なイベントの場合、影響範囲を単一のイベントにする場合は、
.thisEvent
将来のすべてにする場合は、
.futureEvents
- commit
イベントをすぐに保存するか
すぐに保存したい場合は
true
この引数を省略することも可能。
2. イベントの追加をするViewを作成
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を表示するようにします。
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. 完成🎉
ここまでのコード
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)
}
}
}
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)
}
}
}
}
}
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)
}
}
}
import SwiftUI
@main
struct EventKit_EventApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(EventManager())
}
}
}
イベントを変更する手順
1. イベントを変更する関数を作成
受け取ったイベントとタイトルと日時からイベントを変更する関数を作成します。
/// イベントの変更
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:)
- event
保存するイベント - span
定期的なイベントの場合、影響範囲を単一のイベントにする場合は、
.thisEvent
将来のすべてにする場合は、
.futureEvents
- commit
イベントをすぐに保存するか
すぐに保存したい場合は
true
この引数を省略することも可能。
2. EventKitをインポート
import EventKit
3. CreateEventViewを変更にも対応させる
CreateEventViewでイベントの追加も変更もできるようにしていきます。
CreateEventViewを呼び出す際に引数にイベントを持たせて、
イベントが渡されたた場合は、そのイベントを変更、
nilが渡された場合は追加になるように修正します。
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をインポート
import EventKit
5. CreateEventViewをContentViewから呼び出す(変更に対応させる)
イベントのタイトルをタップしたら変更(CreateEventView.swiftに変更したいイベントを渡して開く)
Toolbarの+をタップしたら追加(CreateEventView.swiftにnilを渡して開く)
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. 完成🎉
ここまでのコード
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)
}
}
}
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
}
}
}
}
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)
}
}
}
import SwiftUI
@main
struct EventKit_EventApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(EventManager())
}
}
}
イベントを削除する手順
1. イベントを削除する関数を作成
受け取ったイベントを削除する関数を作成します。
/// イベントの削除
func deleteEvent(event: EKEvent){
// 削除したいイベントを取得
do {
try store.remove(event, span: .thisEvent, commit: true)
} catch {
print(error.localizedDescription)
}
}
2. ContentViewからイベントを削除できるようにする
イベントのタイトルを長押ししたら削除ボタンを表示するようにします。
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. 完成🎉
ここまでのコード
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)
}
}
}
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
}
}
}
}
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)
}
}
}
import SwiftUI
@main
struct EventKit_EventApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(EventManager())
}
}
}
EventKitを使用したサンプルアプリ
この記事で紹介した、
- イベントの取得
- イベントの追加
- イベントの変更
- イベントの削除
上記の機能組み合わせたサンプルをGitHubに公開しました。
終日の選択や、URLの追加や、カレンダーのカラーでの表示なども実装しています。
-
2022年12月16日現在
Xcode14.2およびiOS16.2で検証 ↩︎
Discussion