【Swift UIKit】EventKitで標準カレンダーと連携させる方法!取得/保存/削除

この記事からわかること

  • SwiftUIKitEventKitを使ってAppleカレンダー連携する方法
  • イベント読み込み/保存/削除するには?
  • EKEventEKEventStoreクラス使い方
  • テーブルビューへの組み込み方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

SwiftのEventKitを導入してApple公式のカレンダーアプリとの連携方法をまとめていきます。

EventKitフレームワークとは?

公式リファレンス:EventKitフレームワーク

EventKitフレームワークとはiOSアプリ開発において、Apple公式のカレンダーやリマインダーアプリへの連携や操作を可能にできるユーザーインターフェースを提供しているフレームワークです。

Xcodeには既に導入されているフレームワークなのでimportを記述することで簡単に導入することが可能です。

import EventKit

info.plistへのキー追加

EventKitを用いることでデバイスにインストールされているカレンダーアプリに、開発したアプリからアクセスすることができますが、その前に開発するアプリのinfo.plistへのキー:NSCalendarsUsageDescriptionの追加が必要になります。NSCalendarsUsageDescriptionとキーに入力するとPrivacy - Calendars Usage Descriptionと自動変換されます。

'【Swift UIKit】EventKitでカレンダーを読み込む方法!保存や削除

キーを追加していないとアプリがクラッシュしてしまうので注意してください。またカレンダー意外のキーは以下の通りになります。

EventKitの仕組み:EKEventStore

EventKitではEKEventStore(イベントストア)クラスとしてカレンダーやリマインダーへのアクセスをサポートしています。

class EKEventStore : NSObject

このイベントストアごとにアクセスへの許可から操作などを行います。複数インスタンス化することもできますが、アプリ内では1つのインスタンスを生成し、許可申請や操作をしないとエラーの元になるので注意してください。

let eventStore: EKEventStore = EKEventStore()

requestAccessで許可申請

カレンダーへのアクセスはユーザーの許可がないと参照することはできません。使用するにはインスタンス化したEKEventStoreからrequestAccessメソッドを呼び出してポップアップを表示させユーザーへ許可を申請します。

func authRequest(){
    if EKEventStore.authorizationStatus(for: .event) == .notDetermined{
        eventStore.requestAccess(to: .event, completion: { (granted, error) in
            if granted && error == nil {
                print("許可")
            }
        })
    }
}

authorizationStatusでステータスを取得

許可申請を出す前に現在の許可状態を取得し、未許可であれば申請を出すようにします。authorizationStatusからEKAuthorizationStatus型の現在の許可状態が取得できるのでnotDetermined(未申請)の場合のみ申請を出すようにしておきます。

公式リファレンス:EKAuthorizationStatus

enum EKAuthorizationStatus : Int, @unchecked  Sendable {
    case authorized // 承認済
    case denied // 明示的に拒否
    case notDetermined // 未選択
    case restricted // 未承認
}

今回は独自のEventControllerクラスを用意してEventKitを操作しやすいように定義しておきます。許可申請やイベントの取得などを関数などにまとめて呼び出しやすいようにしておきました。承認されればカレンダーのデータにアクセスできるようになります。

import Foundation
import EventKit
class EventController {
    
    let eventStore: EKEventStore = EKEventStore()
    var events: [EKEvent]?
    
    func authRequest(){
    }
    
    func loadEvents() {
    }
    
    func newEvent(title: String) {
    }
    
    func removeEvent(at indexPath: IndexPath) {
    }
}

クラスには必要となるイベントストアインスタンスとイベントを保持するプロパティ、各メソッドを用意しておきます。

イベントデータの取得

  1. カレンダーを識別
  2. 絞り込む条件を構築
  3. 条件に合ったイベントを取得

コード

func loadEvents() {
    let calendars = eventStore.calendars(for: .event)
    let predicate = eventStore.predicateForEvents(withStart: Calendar.current.date(byAdding: .month, value: -12, to: Date())!, end: Date(), calendars: calendars)
    events = eventStore.events(matching: predicate)
}

calendarsメソッド

既にカレンダーに保存されているイベントデータを取得するにはまずcalendarsメソッドを使用します。

公式リファレンス:calendarsメソッド

func calendars(for entityType: EKEntityType) -> [EKCalendar]

引数には指定したEKEntityTypeによって取得できるデータが代わり、eventにすることでイベントが取得可能なカレンダーを配列形式のEKCalendar型として取得できます。

predicateForEventsメソッド

次に絞り込む条件をpredicateForEventsメソッドで構築します。

公式リファレンス:predicateForEventsメソッド

func predicateForEvents(
    withStart startDate: Date,
    end endDate: Date,
    calendars: [EKCalendar]?
) -> NSPredicate

引数には取得したいイベントの日付範囲を定義できます。Date型で指定できるのでDate()とすれば今日の日付になります。calendarsには検索したいカレンダー配列を渡します

今回はNSCalendarクラスのdate(byAdding:to:options:)メソッドを使用して今日の日付から1年前の日付を定義し、1年前〜今日までの条件を構築しています。

let predicate = eventStore.predicateForEvents(withStart: Calendar.current.date(byAdding: .month, value: -12, to: Date())!, end: Date(), calendars: calendars)

公式リファレンス:date(byAdding:to:options:)

events(matching:)

条件の構築ができたらevents(matching:)メソッドを使って実際にイベントを取得します。

func events(matching predicate: NSPredicate) -> [EKEvent]

引数に先程構築した条件を渡すこと配列形式のEKEvent型としてイベント情報を取得できます。今回はその配列をクラスのプロパティに保持させるようにしています。

events = eventStore.events(matching: predicate)

取得したイベント情報を取得

EKEventクラスの持つプロパティからイベントの開始日や終了日、その日に紐づいている予定のタイトルなどを取得することができます。

公式リファレンス:EKEventクラス

class EKEvent : EKCalendarItem {
    var eventIdentifier: String! // イベントの一意の識別子
    var availability: EKEventAvailability // イベントの空き状況の設定
    var startDate: Date! // イベントの開始日
    var endDate: Date! // イベントの終了日
    var isAllDay: Bool // イベントが終日どうか
    var occurrenceDate: Date! // イベントの元の発生日
    var isDetached: Bool // イベントが繰り返しイベントの切り離されたインスタンスであるかどうか
    var organizer: EKParticipant? // イベントの主催者
    var status: EKEventStatus // イベントステータス
}

EKEventクラスはEKCalendarItemクラスを継承しており、タイトルなどはそのプロパティに定義されています。

class EKCalendarItem : EKObject{
    var calendar: EKCalendar! // カレンダー
    var title: String! // タイトル
    var location: String? // 場所
    var creationDate: Date? // 作成された日付
    var lastModifiedDate: Date? // 変更された日付
    var timeZone: TimeZone? // タイムゾーン
    var url: URL? // URL
    var hasNotes: Bool // メモがあるか
    var notes: String? // メモ
}

アプリからイベントデータを保存する

  1. 保存するイベントデータの構築
  2. タイトルや日付と追加するカレンダーを指定
  3. saveメソッドで保存
func newEvent(title: String) {
    let event = EKEvent(eventStore: eventStore)
    event.title = title
    event.startDate = Date()
    event.endDate = Date()
    event.isAllDay = true
    event.calendar = eventStore.defaultCalendarForNewEvents
    try! eventStore.save(event, span: .thisEvent)
    loadEvents()
} 

カレンダーアプリへ保存するイベントはEKEvent型で保存するのでインスタンスを生成し、各プロパティに値を格納しておきます。追加するカレンダーはイベントストアのdefaultCalendarForNewEventsプロパティから取得できる設定済みのデフォルトカレンダーを渡します。

defaultCalendarForNewEventsプロパティ

イベントが構築できたらEKEventStoresaveメソッドを使って保存します。引数spanには指定したイベントが単一であればthisEvent繰り返すイベントであればfutureEventsを指定します。

公式リファレンス:saveメソッド

イベントを削除する

イベントの削除は後述するテーブルビューに表示させている状態からスワイプアクションで消せるようにメソッドを作っていきます。削除するロジックだけで見ると以下の通りです。

  1. 削除したいイベント(EKEvent)を取得
  2. removeメソッドで削除
func removeEvent(at indexPath: IndexPath) {
    guard let events = events else { return }
    let event = events[indexPath.row]
    try! eventStore.remove(event, span: .thisEvent)
    loadEvents()
}

removeメソッドもsaveメソッドと同様の引数を持っています。

テーブルビューに表示させてみる

これでカレンダーのイベントを取得、保存、削除できるようになったのでテーブルビューに当てはめて表示させていきます。

'【Swift UIKit】EventKitでカレンダーを読み込む方法!保存や削除

import Foundation
import EventKit

class EventController {
    
    let eventStore: EKEventStore = EKEventStore()
    var events: [EKEvent]?
    
    
    init(){
        authRequest()
        self.loadEvents()
    }
    
    func authRequest(){
        if EKEventStore.authorizationStatus(for: .event) == .notDetermined{
            eventStore.requestAccess(to: .event, completion: { (granted, error) in
                if granted && error == nil {
                    print("許可")
                }
            })
        }
    }
    
    func loadEvents() {
        let calendars = eventStore.calendars(for: .event)
        let predicate = eventStore.predicateForEvents(withStart: Calendar.current.date(byAdding: .month, value: -3, to: Date())!, end: Date(), calendars: calendars)
        events = eventStore.events(matching: predicate)
    }
    
    func newEvent(title: String) {
        let event = EKEvent(eventStore: eventStore)
        event.title = title
        event.startDate = Date()
        event.endDate = Date()
        event.isAllDay = true
        event.calendar = eventStore.defaultCalendarForNewEvents
        try! eventStore.save(event, span: .thisEvent)
        
        loadEvents()
    }
    
    func removeEvent(at indexPath: IndexPath) {
        guard let events = events else { return }
        let event = events[indexPath.row]
        try! eventStore.remove(event, span: .thisEvent)
        
        loadEvents()
    }
}

import UIKit

class EventViewController: UIViewController ,UITableViewDataSource,UITableViewDelegate {
    let eventController = EventController()
    
    @IBOutlet  var textField:UITextField!
    @IBOutlet  var entryBtn:UIButton!
    @IBOutlet  var tableView:UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        entryBtn.addTarget(self, action: #selector(self.entryBtnTapped), for: .touchUpInside)
    }
    
    @objc  func entryBtnTapped(){
        if textField.hasText {
            eventController.newEvent(title: textField.text!)
        }
        tableView.reloadData()
        
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return eventController.events?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: "myCell")
        cell.textLabel!.text = eventController.events![indexPath.row].title
        return cell
        
    }
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let destructiveAction = UIContextualAction(style: .destructive, title: "") { (action, view, completionHandler) in
            self.eventController.removeEvent(at: indexPath)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            completionHandler(true)
        }
        destructiveAction.image = UIImage(systemName: "trash.fill")
        let configuration = UISwipeActionsConfiguration(actions: [destructiveAction])
        return configuration
    }
    
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index