[EventKit][SwiftUI] Reminder へのアクセス方法

EventKit

     
⌛️ 3 min.
EventKit を使って、Apple の Reminder アプリのデータにアクセスする方法を解説します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Ventura 13.4 Beta
  • Xcode 14.3
  • iOS 16.4 beta

Reminderアプリ

Reminder アプリは、iOS / macOS に付属してくる以下のアプリです。

ReminderApp

いわゆる TODO 項目を管理するためのアプリです。

Reminder にプログラムからアクセスする

Reminder アプリに登録した TODO 項目へ プログラム的にアクセスしていきます。

EventKit

Reminder のデータへアクセスするために、EventKit を使用します。

通常、EventKit はカレンダーに登録した要素へのアクセスに使用されますが、Reminder へのアクセスにも EventKit が使用されます。

Apple のドキュメントは、こちら

EventKit を使って、Reminder にアクセス

EventKit を使用して、Reminder へアクセスするためには、事前準備が必要となります。

準備

EventKit を使って、データへアクセスするためには、事前にユーザーからの許諾を取る必要があります。
# Calendar へのアクセスと同じです。

以下の記事を参考に、Info.plist を設定し、requestAccess して、ユーザーからアクセスの許諾を得ます。

EventKit [iOS][macOS][EventKit] EventKit のセットアップ async/await 対応板

Calendarへのアクセスとの相違点は、アクセス要求の対象が、.event ではなく、.reminder となることです。

let access = try await store.requestAccess(to: .reminder)

EKReminder 取得

Reminder の要素は、EKReminder という class です。

Apple のドキュメントは、こちら

EKCalendarItem を 継承している class で、Reminder 要素として、完了しているかを表す isCompleted 等が追加されています。

completion handler経由で取得

EKEvent は、enumerateEvents で取得しましたが、EKReminder は、fetchReminders で取得します。

let predicate: NSPredicate = store.predicateForReminders(in: nil)
store.fetchReminders(matching: predicate, completion: { reminders in
    // reminders: [EKReminder]?
    guard let reminders = reminders else { return }
    for reminder in reminders {
      // process reminder
    }
})

上記のように、取得する EKReminder の条件を predicate で与えると、該当する EKReminder は、completion に [EKReminder] として渡されてきます。

2023.4 時点では、いわゆる async/await 対応した API は提供されていません。

async/await で取得する

せっかくなので(?)、async/await で取得できるように拡張してみます。

enumerateEvents では、1つづつ順番に EKCalendarItem を取得していたために、AsyncStream を使っていましたが、EKReminder の API は、まとめて [EKReminder]? を返してくる仕様です。
ですので、AsyncStream ではなく、単純に withCheckedContinuation を使って async/await への拡張を行います。

extension EKEventStore {
    public func reminder(predicate: NSPredicate) async -> [EKReminder]? {
        return await withCheckedContinuation({ continuation in
            self.fetchReminders(matching: predicate) { reminders in
                continuation.resume(returning: reminders)
            }
        })
    }
}

SwiftUI で、EKReminder を表示

せっかく取得したので、EKReminder を表示するアプリを作ります。

2つの class で構成されています。
EventKitStore: ViewModel です。
ContentView: View です。

ViewModel(EventKitStore)

EventKitStore ではこれまでに説明したコードを 1つの class にまとめています。

アクセス許諾されているかを表す accessToEvent というプロパティを作成し、EKEventStore.authorizationStatus の返り値を保存しています。
この プロパティが true になったときに、EKEventStore から EKReminder を取得します。

取得した EKReminder は、reminders という変数に保存しています。直接 UI 表示に使用しているので、@Published と @MainActor を付与しています。

class EventKitStore: ObservableObject {
    let store: EKEventStore
    var accessToEvent: Bool = false {
        didSet {
            if accessToEvent {
                Task { await fetchReminder() }
            }
        }
    }

    @MainActor
    @Published var reminders: [EKReminder] = []

    init() {
        store = EKEventStore()

        if EKEventStore.authorizationStatus(for: .event) != .authorized {
            Task {
                do {
                    self.accessToEvent = try await store.requestAccess(to: .reminder)
                } catch {
                    print("\(error)")
                }
           }
        } else {
            accessToEvent = true
        }
    }

    func fetchReminder() async {
        let predicate: NSPredicate = store.predicateForReminders(in: nil)
        let passedReminders = await store.reminder(predicate: predicate)
        guard let check = passedReminders else { return }
        Task { @MainActor in
            self.reminders = check
        }
    }
}

extension EKEventStore {
    public func reminder(predicate: NSPredicate) async -> [EKReminder]? {
        return await withCheckedContinuation({ continuation in
            self.fetchReminders(matching: predicate) { reminders in
                continuation.resume(returning: reminders)
            }
        })
    }
}

View(ContentView)

View は、単純に ViewModel が持っている [EKReminder] をリスト表示しています。

取得がうまくいっているか確認したかったので、EKReminder の数を タイトル代わりに表示しています。
次に、取得できた EKReminder のそれぞれの title をリスト表示しています。

なお、EKReminder は、識別用に calendarItemIdentifier というプロパティを持っているので、SwiftUI の List 要素の id にはそのプロパティを指定しています。

struct ContentView: View {
    @StateObject var store = EventKitStore()
    var body: some View {
        VStack {
            Text("# of reminder \(store.reminders.count)")
                .font(.title)
            List {
                ForEach(store.reminders, id: \.calendarItemIdentifier) { reminder in
                    Text(reminder.title)
                }
            }
        }
        .padding()
    }
}

アプリ外観

iOS app
macOS app

なお、EventKit は、iOS / macOS の両方に存在するので、同じコードで iOS / macOS の両方で動作します。(まさしく、マルチプラットフォームです)
# EventKit は、iOS / macOS / watchOS 向けに用意されています。

上記の画像も、iOS / macOS どちらも同じ アカウント上で動作させているので、表示される EKReminder も同じものになっています。

まとめ

Reminder 要素取得の方法

Reminder 要素取得の方法
  • EventKit を使用する
  • 使用許諾をリクエストするのは EKEvent と同様
  • 取得するための API は、async/await 対応されていない
  • async/await 対応は、withCheckedContinuation を使う
  • EventKit は、iOS/macOS 両対応しているので、マルチプラットフォームしやすい

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です