LoginSignup
3
1

More than 3 years have passed since last update.

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

Posted at

目的

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

使用ライブラリ

・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

まとめると

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

UserStatus
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を使用してまとめると

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 = "イベント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も同様

CalendarModel
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)のように変更してuserIDtargetID変更する予定で書いています。

saveEventを呼ぶ際は一度removeEventを呼び、中身を空にしてsaveEventでイベントを保存するように使用しています。

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1