iOS標準カレンダー連携(CRUD部分)

## 目的
カレンダー連携の際、参考になる記事がほぼなかったため自分のメモ用

## 使用ライブラリ
・EventKit

## まず最初に

### 本記事での実装方法
Calendar, Event毎に`identify`が与えられているため、それを利用してなんやかんや行う。

### 流れ
カレンダーという枠(箱?)を作成し(既存のカレンダーを使用しても良い)
その中にイベントを作成していく。
削除の時はその逆で、箱の中を空っぽにする。(カレンダー自体の削除は行わない。後述)

### わかりにくい仕様1
・カレンダーアプリ > 画面下部「カレンダー」
遷移後「ICLOUD」もしくは「IPHONE」というSectionがある。

これは
・設定 > AppleID(Profileみたいなやつ) > iCloud > カレンダー
にてICLOUDを使用しているかの有無によって名称が異なる。

### わかりにくい仕様2
例えとして「ICLOUD」で説明
・「ICLOUD」にはDefaultですでにカレンダーが存在している
・「ICLOUD」には必ず１つのカレンダーが存在しなければならない
・Defaultカレンダーは削除可能となっている。

もしカレンダー連携アプリを作成し、こちらから３つカレンダーを登録したとして
全件削除機能を付けたとする。

ユーザーがなにもしていなれば、全件削除が可能となるが
全件削除前にDefaultカレンダーを削除されていた場合、
全件削除しようと試みた最後の1つが削除されずに残る結果となる。

そのため本記事では中身を削除し、カレンダー自体はユーザーに削除してもらうという実装内容として記載する。

> 削除するときにDefaultっぽいカレンダーを作成して
それ以外を削除みたいなことをすれば、全件削除もどきは作成出来る。

## Model作成
```swift
class CalendarModel {
    let eventStore = EKEventStore()
    let userID = "testさん"
    
    init() { }
}
```

## CreateCalendar
上記で記載したようにiCloudの使用設定により「ICLOUD」もしくは「IPHONE」と異なるタイプになるため
どちらにカレンダーを作成出来るか確認する。

```swift
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をいじりたければここで付けておく

```swift
calendar.title = "テストカレンダー"
calendar.cgColor = UIColor.green.cgColor
// 上で作成したsourceを入れる
calendar.source = source
```

まとめると

```swift:CalendarModel
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: "上の辞書"`

```swift:UserStatus
struct UserStatus {
    static var synchronizationInfo = [String: [Any]]
}
```

## saveEvent
作成したCalendarもしくは既存CalendarにEventを保存する。

```swift
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を保存

```swift
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 = "イベント１つ目"
    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
    }
}
```

保存が終わったら、ローカルにデータを保存

```swift
synchronizationInfo[userID] = [calenderIdentifier : eventIdentifiers]
UserStatus.synchronizationInfo = synchronizationInfo
```

Dispatchを使用してまとめると

```swift:CalendarModel
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 = "イベント１つ目"
        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も同様

```swift:CalendarModel 
private func readCalendar(calendarIdentifier: String) -> EKCalendar? {
    return self.eventStore.calendar(withIdentifier: calendarIdentifier)
}
```

## RemoveEvents
削除対象にするCalendarのEventをローカルデータから取り出す

```swift
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を削除
 
```swift
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
        }
    }
}
```

結果を保存

```swift
// errorがなければ空に、あればそれを入れてerrorがでたEventだけは残す
eventIdentifiers = []
UserStatus.synchronizationInfo[userID] = errorIdentifiers.isEmpty ? [calendarIdentifier: eventIdentifiers] : [calendarIdentifier: errorIdentifiers]
```

まとめると

```swift
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`でイベントを保存するように使用しています。

・ざっくりと雛形のような感じで記載したため、コピペで動かない可能性があります。
その際はコメントいただけると幸いです