目的
カレンダー連携の際、参考になる記事がほぼなかったため自分のメモ用
使用ライブラリ
・EventKit
まず最初に
本記事での実装方法
Calendar, Event毎にidentify
が与えられているため、それを利用してなんやかんや行う。
流れ
カレンダーという枠(箱?)を作成し(既存のカレンダーを使用しても良い)
その中にイベントを作成していく。
削除の時はその逆で、箱の中を空っぽにする。(カレンダー自体の削除は行わない。後述)
わかりにくい仕様1
・カレンダーアプリ > 画面下部「カレンダー」
遷移後「ICLOUD」もしくは「IPHONE」というSectionがある。
これは
・設定 > AppleID(Profileみたいなやつ) > iCloud > カレンダー
にてICLOUDを使用しているかの有無によって名称が異なる。
わかりにくい仕様2
例えとして「ICLOUD」で説明
・「ICLOUD」にはDefaultですでにカレンダーが存在している
・「ICLOUD」には必ず1つのカレンダーが存在しなければならない
・Defaultカレンダーは削除可能となっている。
もしカレンダー連携アプリを作成し、こちらから3つカレンダーを登録したとして
全件削除機能を付けたとする。
ユーザーがなにもしていなれば、全件削除が可能となるが
全件削除前にDefaultカレンダーを削除されていた場合、
全件削除しようと試みた最後の1つが削除されずに残る結果となる。
そのため本記事では中身を削除し、カレンダー自体はユーザーに削除してもらうという実装内容として記載する。
削除するときにDefaultっぽいカレンダーを作成して
それ以外を削除みたいなことをすれば、全件削除もどきは作成出来る。
Model作成
class CalendarModel {
let eventStore = EKEventStore()
let userID = "testさん"
init() { }
}
CreateCalendar
上記で記載したようにiCloudの使用設定により「ICLOUD」もしくは「IPHONE」と異なるタイプになるため
どちらにカレンダーを作成出来るか確認する。
var source = eventStore.sources.filter { $0.sourceType == .calDAV && $0.title == "iCloud" }.first
if source == nil {
source = eventStore.sources.filter{ $0.sourceType == .local }.first
}
guard let _ = source else { return nil }
カレンダーのPropertyをいじりたければここで付けておく
calendar.title = "テストカレンダー"
calendar.cgColor = UIColor.green.cgColor
// 上で作成したsourceを入れる
calendar.source = source
まとめると
private func createCalendar(title: String) -> EKCalendar? {
let calendar = EKCalendar(for: .event, eventStore: eventStore)
var source = eventStore.sources.filter { $0.sourceType == .calDAV && $0.title == "iCloud" }.first
if source == nil {
source = eventStore.sources.filter{ $0.sourceType == .local }.first
}
guard let _ = source else { return nil }
calendar.title = title
calendar.cgColor = UIColor.green.cgColor
calendar.source = source
do {
try eventStore.saveCalendar(calendar, commit: true)
} catch let error as NSError {
print(error)
}
return calendar
}
保存する型
user毎にカレンダーを作成することを見越して
synchronizationInfo = [userID: [calendarIdentify: [eventIdentifies]]]
分けても全然OK、めんどくさかったので1つの変数で管理
内訳は
key:"calendar", value: "event情報複数"
という辞書をさらにValueとして
key: "userID", value: "上の辞書"
struct UserStatus {
static var synchronizationInfo = [String: [Any]]
}
saveEvent
作成したCalendarもしくは既存CalendarにEventを保存する。
var synchronizationInfo = UserStatus.synchronizationInfo
let calenders = eventStore.calendars(for: .event)
var calender: EKCalendar?
// identifiersDic = key:calendar, value:Events の辞書
if let identifiersDic = synchronizationInfo[userID] as? [String : [Any]],
let calendarIdentifier = identifiersDic.keys.first {
// ローカル保存したカレンダー識別子がすでに存在しているか確認
calender = calenders.filter { $0.calendarIdentifier == calendarIdentifier }.first
}
if calender == nil {
// 存在していなかったらこのユーザー専用のカレンダーを作成(上のCreateCalendarにて作成したメソッド呼び出し)
calender = self.createCalendar(title: userID) ?? self.eventStore.defaultCalendarForNewEvents
}
guard let calenderIdentifier = calender?.calendarIdentifier else { return }
Event保存するカレンダーが用意できたらEventを保存
var eventIdentifiers = [String]()
var errorMessage: NSError?
models.forEach { model in
let event = EKEvent(eventStore: self.eventStore)
// ここでもカレンダーのpropertyは変更可能
event.calendar = calender
event.calendar.title = loginName
event.title = "イベント1つ目"
event.startDate = Date()
event.endDate = Date()
event.isAllDay = true
event.notes = "これはテストで作成されたカレンダーです"
do {
try self.eventStore.save(event, span: .thisEvent)
// イベント保存に成功したら作成してあるArrayに格納
eventIdentifiers.append(event.eventIdentifier)
} catch let error as NSError {
errorMessage = error
}
}
保存が終わったら、ローカルにデータを保存
synchronizationInfo[userID] = [calenderIdentifier : eventIdentifiers]
UserStatus.synchronizationInfo = synchronizationInfo
Dispatchを使用してまとめると
private func saveEvent() {
let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "Save_Calendar")
var synchronizationInfo = UserStatus.synchronizationInfo
let calenders = eventStore.calendars(for: .event)
var calender: EKCalendar?
if let identifiersDic = synchronizationInfo[userID] as? [String : [Any]],
let calendarIdentifier = identifiersDic.keys.first {
calender = calenders.filter { $0.calendarIdentifier == calendarIdentifier }.first
}
if calender == nil {
calender = self.createCalendar(title: userID) ?? self.eventStore.defaultCalendarForNewEvents
}
guard let calenderIdentifier = calender?.calendarIdentifier else { return aHandler(nil, nil) }
var eventIdentifiers = [String]()
var errorMessage: NSError?
dispatchQueue.async(group: dispatchGroup) {
models.forEach { model in
dispatchGroup.enter()
let event = EKEvent(eventStore: self.eventStore)
event.calendar = calender
event.calendar.title = loginName
event.title = "イベント1つ目"
event.startDate = Date()
event.endDate = Date()
event.isAllDay = true
event.notes = "これはテストで作成されたカレンダーです"
do {
defer { dispatchGroup.leave() }
try self.eventStore.save(event, span: .thisEvent)
eventIdentifiers.append(event.eventIdentifier)
} catch let error as NSError {
errorMessage = error
}
}
dispatchGroup.notify(queue: .global()) {
synchronizationInfo[userID] = [calenderIdentifier : eventIdentifiers]
UserStatus.synchronizationInfo = synchronizationInfo
}
}
ReadCalendar
カレンダーの識別子を引数にこれで検索するだけ、Eventも同様
private func readCalendar(calendarIdentifier: String) -> EKCalendar? {
return self.eventStore.calendar(withIdentifier: calendarIdentifier)
}
RemoveEvents
削除対象にするCalendarのEventをローカルデータから取り出す
var synchronizationInfo = UserStatus.synchronizationInfo
guard let identifiersDic = synchronizationInfo[userID] as? [String:Any] else { return }
guard let calendarIdentifier = identifiersDic.keys.first else { return }
guard var eventIdentifiers = identifiersDic[calendarIdentifier] else { return }
取り出したEventを削除
var errorIdentifiers = [String]()
var errorMessage: NSError?
if let eventIdentifiers = eventIdentifiers as? [String] {
eventIdentifiers.forEach { identifiers in
if var event = self.eventStore.event(withIdentifier: identifiers) {
do {
try self.eventStore.remove(event, span: .thisEvent)
} catch let error as NSError {
errorIdentifiers.append(identifiers)
errorMessage = error
}
}
}
}
//念の為ValueはArrayじゃなかった時も
if let identifiers = eventIdentifiers as? String {
if let event = self.eventStore.event(withIdentifier: identifiers) {
do {
try self.eventStore.remove(event, span: .thisEvent)
} catch let error as NSError {
errorIdentifiers.append(identifiers)
errorMessage = error
}
}
}
結果を保存
// errorがなければ空に、あればそれを入れてerrorがでたEventだけは残す
eventIdentifiers = []
UserStatus.synchronizationInfo[userID] = errorIdentifiers.isEmpty ? [calendarIdentifier: eventIdentifiers] : [calendarIdentifier: errorIdentifiers]
まとめると
private func removeEvent() {
let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "Remove_Calendar")
var synchronizationInfo = UserStatus.synchronizationInfo
guard let identifiersDic = synchronizationInfo[userID] as? [String:Any] else { return }
guard let calendarIdentifier = identifiersDic.keys.first else { return }
guard var eventIdentifiers = identifiersDic[calendarIdentifier] else { return }
var errorIdentifiers = [String]()
var errorMessage: NSError?
if let eventIdentifiers = eventIdentifiers as? [String] {
eventIdentifiers.forEach { identifiers in
if var event = self.eventStore.event(withIdentifier: identifiers) {
do {
try self.eventStore.remove(event, span: .thisEvent)
} catch let error as NSError {
errorIdentifiers.append(identifiers)
errorMessage = error
}
}
}
}
if let identifiers = eventIdentifiers as? String {
if let event = self.eventStore.event(withIdentifier: identifiers) {
do {
try self.eventStore.remove(event, span: .thisEvent)
} catch let error as NSError {
errorIdentifiers.append(identifiers)
errorMessage = error
}
}
}
dispatchGroup.notify(queue: .global()) {
eventIdentifiers = []
UserStatus.synchronizationInfo[userID] = errorIdentifiers.isEmpty ? [calendarIdentifier: eventIdentifiers] : [calendarIdentifier: errorIdentifiers]
}
}
まとめ
・カレンダーアクセス承認関係は、記事が豊富だと思いますので割愛してます。
・userID(testさん)
という固定値で記事を書いてますが 実際利用する場合はremoveEvent()
をremoveEvent(targetID: String)
のように変更してuserID
をtargetID
変更する予定で書いています。
・saveEvent
を呼ぶ際は一度removeEvent
を呼び、中身を空にしてsaveEvent
でイベントを保存するように使用しています。
・ざっくりと雛形のような感じで記載したため、コピペで動かない可能性があります。
その際はコメントいただけると幸いです