NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    EventKitでカレンダーへイベントを追加する方法【Swift】

    記事の概要

    今回はEventKitを使用してiPhoneの標準カレンダーへ予定やTODOを追加してみたいと思います。
    記事の流れとしては以下です。

    • EventKitとは
    • カレンダーへアクセスすることの許可を要求する
    • カレンダーへ追加する
    • サンプル

    EventKitとは

    developer.apple.com

    EventKitはカレンダーやリマインダーのイベントを作成、表示、編集を行う際に使用するフレームワークです。
    今回はこのフレームワークを使用して既存のカレンダーにイベントを追加してみたいと思います!
    ちなみにEventKitUIと言うフレームワークもありますが、こちらはカレンダーのイベントやリマインダーを表示、選択、編集するためのインターフェイスを表示する際に使用します。

    実装の流れ

    早速実装していきたいと思います。
    簡単に調べてみると流れとしては以下でできそうです。

    • info.plistにカレンダーへアクセスする為の設定を行う
    • カレンダーへアクセスする許可を要求
    • カレンダーへ予定を追加する処理
    • 画面の作成(サンプルコード)

    info.plistで設定する

    KeyはPrivacy - Calendars Usage Descriptionを選択し、Valueには適当に"カレンダーにイベントを追加するため使用します。"と書いておきます。

    リマインダーや連絡帳にアクセスしたい場合は他にKeyを追加する必要があります。
    その場合はコチラを参照ください。

    カレンダーへアクセスすることの許可を要求する

    お次はカレンダーへアクセスしても良いかユーザーに許可いただくための要求処理を作成していきます。

    func requestAccess() async {
        do {
            try await eventStore.requestAccess(to: .event)
        } catch {
            print(error.localizedDescription)
        }
    }
    

    とりあえずこのメソッド叩けばこんな感じでカレンダーへのアクセス許可を要求するアラートを表示させることができます。

    このままでは最初に許可を要求した際に得られなければ設定から変更しない限り許可を得られないので状態を取得するメソッドを追加してみます。

    func getAuthorizationStatus() -> Bool {
        let status = EKEventStore.authorizationStatus(for: .event)
        switch status {
        case .notDetermined:
            return false
        case .restricted:
            return false
        case .denied:
            return false
        case .authorized:
            return true
        @unknown default:
            return false
        }
    }
    

    それぞれ何を指しているのかは以下の表を参考にしていただければと思いますが、上記のメソッドでは許可を得られている時はtrueを、それ以外はfalseを返します。

    Status 内容
    authorized アプリはサービスへのアクセスを許可されています。
    denied ユーザーは、アプリのサービスへのアクセスを明示的に拒否しました。
    notDetermined ユーザーは、アプリがサービスにアクセスできるかどうかについてまだ選択していません。
    restricted アプリはサービスへのアクセスを許可されていません。

    これで状態を監視することはできますが、notDetermined以外ではアラートを表示しません。
    そこでUIApplication.shared.openを使用して許可されていない場合は設定画面へ遷移させてみます。
    全体としては以下の様に作成して試してみます。

    import SwiftUI
    import EventKitUI
    
    struct ContentView: View {
        private let eventStore = EventStore()
        var body: some View {
            VStack {
                Button {
                    Task {
                        await eventStore.requestAccess()
                    }
                } label: {
                    Text("許可を要求!!")
                }
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    
    class EventStore {
        private let eventStore: EKEventStore
        
        init() {
            eventStore = EKEventStore()
        }
        
        func getAuthorizationStatus() -> Bool {
            let status = EKEventStore.authorizationStatus(for: .event)
            switch status {
            case .notDetermined:
                return false
            case .restricted:
                if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                }
                return false
            case .denied:
                if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                }
                return false
            case .authorized:
                return true
            @unknown default:
                return false
            }
        }
        @MainActor
        func requestAccess() async {
            if getAuthorizationStatus() {
                //許可されている時の処理
                return
            } else {
                do {
                    try await eventStore.requestAccess(to: .event)
                } catch {
                    print(error.localizedDescription)
                }
            }
        }
    }
    

    これで状態によって以下の処理を行うことができます。

    • 初めて許可を要求 → 許可を要求するアラートが表示される
    • 明治的に許可されていないor設定のトグルボタンより許可を取り消している → 設定画面へ遷移し、アクセス許可を促す
    • 許可されている → 許可状態で行いたい処理をする

    ※設定画面への遷移前にアラートなどで遷移するかユーザーへ許可を取った方が親切だと思いますがその辺りは試作なので割愛しています。

    カレンダーへ予定を追加する

    とりあえず今回はカレンダーへイベントを追加するだけなので最小の実装をおこなっていきたいと思います。
    以下カレンダーに追加するメソッドです。

    func addEvent(startDate: Date, stopDate: Date, title: String) {
        let defauletCalendar = eventStore.defaultCalendarForNewEvents
        let event = EKEvent(eventStore: eventStore)
        event.title = title
        event.startDate = startDate
        event.endDate = stopDate
        event.calendar = defauletCalendar
        do {
            try eventStore.save(event, span: .thisEvent)
        } catch {
            print(error.localizedDescription)
        }
    }
    

    引数にイベントのタイトルと開始日時、終了日時を渡すことでカレンダーに追加してくれるメソッドになります。
    定義はevent.で行なっていますが、その他にも沢山プロパティが用意されているので是非興味ある人は以下のdocumentみてください!
    (イベントやリマインダーに関する詳細のdocument)

    サンプル全体

    上記までのコードを使用してタイトルと時間を指定してカレンダーに追加するサンプルを作ってみました!

    画面サンプル

    struct ContentView: View {
        private let eventStore = EventStore()
        @State private var eventTitle = ""
        @State private var startDate = Date()
        @State private var endDate = Date()
        var body: some View {
            NavigationView {
                List {
                    HStack {
                        Text("Event Title:")
                        TextField("勤務時間", text: $eventTitle)
                    }
                    VStack(alignment: .leading) {
                        Text("開始日時")
                        DatePicker("開始日時", selection: $startDate)
                            .datePickerStyle(.wheel)
                    }
                    VStack(alignment: .leading) {
                        Text("終了日時")
                        DatePicker("終了日時", selection: $endDate)
                            .datePickerStyle(.wheel)
                    }
                    HStack {
                        Spacer()
                        Button {
                            Task {
                                await eventStore.addEvent(startDate: startDate, endDate: endDate, title: eventTitle)
                                eventTitle.removeAll()
                            }
                        } label: {
                            Text("カレンダーに追加")
                        }
                        .buttonStyle(.borderedProminent)
                        Spacer()
                    }
                }
                .task {
                    await eventStore.requestAccess()
                }
                .navigationTitle("EventKit Demo")
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    機能サンプル

    class EventStore {
        private let eventStore: EKEventStore
        
        init() {
            eventStore = EKEventStore()
        }
        
        func getAuthorizationStatus() -> Bool {
            let status = EKEventStore.authorizationStatus(for: .event)
            switch status {
            case .notDetermined:
                return false
            case .restricted:
                if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                }
                return false
            case .denied:
                if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                }
                return false
            case .authorized:
                return true
            @unknown default:
                return false
            }
        }
        
        func requestAccess() async {
            do {
                try await eventStore.requestAccess(to: .event)
            } catch {
                print(error.localizedDescription)
            }
        }
        
        @MainActor
        func addEvent(startDate: Date, endDate: Date, title: String) async {
            if getAuthorizationStatus() {
                let defauletCalendar = eventStore.defaultCalendarForNewEvents
                let event = EKEvent(eventStore: eventStore)
                event.title = title
                event.startDate = startDate
                event.endDate = endDate
                event.calendar = defauletCalendar
                do {
                    try eventStore.save(event, span: .thisEvent)
                } catch {
                    print(error.localizedDescription)
                }
            } else {
                await requestAccess()
            }
        }
    }
    

    最後に

    今回はカレンダーのみ試してみましたが、今後はリマインダー機能なども試してみようと思います!
    また今年になってdocumentが整理されみやすくなったのがすごくありがたいです!
    以上です!この記事が誰かの役に立てば幸いです。

    執筆者岡優志

    iOSエンジニア
    iOSを専門とし、モバイルアプリの開発を行なっています。

    Twitter