【Swift UIKit】EventKitで標準カレンダーと連携させる方法!取得/保存/削除
この記事からわかること
- SwiftのUIKitでEventKitを使ってAppleのカレンダーと連携する方法
- イベントを読み込み/保存/削除するには?
- EKEventやEKEventStoreクラスの使い方
- テーブルビューへの組み込み方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
SwiftのEventKitを導入してApple公式のカレンダーアプリとの連携方法をまとめていきます。
EventKitフレームワークとは?
EventKitフレームワークとはiOSアプリ開発において、Apple公式のカレンダーやリマインダーアプリへの連携や操作を可能にできるユーザーインターフェースを提供しているフレームワークです。
Xcodeには既に導入されているフレームワークなのでimport
を記述することで簡単に導入することが可能です。
import EventKit
info.plistへのキー追加
EventKitを用いることでデバイスにインストールされているカレンダーアプリに、開発したアプリからアクセスすることができますが、その前に開発するアプリのinfo.plistへのキー:NSCalendarsUsageDescriptionの追加が必要になります。NSCalendarsUsageDescription
とキーに入力するとPrivacy - Calendars Usage Description
と自動変換されます。
キーを追加していないとアプリがクラッシュしてしまうので注意してください。またカレンダー意外のキーは以下の通りになります。
- カレンダー:NSCalendarsUsageDescription
- リマインダー:NSRemindersUsageDescription
- 連絡先:NSContactsUsageDescription
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) {
}
}
クラスには必要となるイベントストアインスタンスとイベントを保持するプロパティ、各メソッドを用意しておきます。
イベントデータの取得
- カレンダーを識別
- 絞り込む条件を構築
- 条件に合ったイベントを取得
コード
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
メソッドを使用します。
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
クラスの持つプロパティからイベントの開始日や終了日、その日に紐づいている予定のタイトルなどを取得することができます。
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? // メモ
}
アプリからイベントデータを保存する
- 保存するイベントデータの構築
- タイトルや日付と追加するカレンダーを指定
- 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プロパティ
イベントが構築できたらEKEventStore
のsave
メソッドを使って保存します。引数span
には指定したイベントが単一であればthisEvent
、繰り返すイベントであればfutureEvents
を指定します。
イベントを削除する
イベントの削除は後述するテーブルビューに表示させている状態からスワイプアクションで消せるようにメソッドを作っていきます。削除するロジックだけで見ると以下の通りです。
- 削除したいイベント(EKEvent)を取得
- 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メソッドと同様の引数を持っています。
テーブルビューに表示させてみる
これでカレンダーのイベントを取得、保存、削除できるようになったのでテーブルビューに当てはめて表示させていきます。
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
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。