Edited at

iOSアプリカレンダーの予定を表示するアプリを作ってみる 【UITableView × EventKit】


はじめに

Life is Tech !というところで中高生(メンバー)に対してiPhoneアプリプログラミングコースでSwiftを教えているメンターのふみっちです。

実は記事を書く経験はあまりしたことがなくて、今回の記事は先輩のメンターからアドバイスをいただいた部分もあって完成した記事となってます。アドバイスくれた先輩方ありがとうございました。

昨日のアドベンドカレンダーではニーザさんがReact Nativeでのモバイル開発を紹介していましたが今回はSwiftでアプリを作っていこうと思います!ちなみに僕は焼肉より寿司派です。(笑)

さて、12月に入って寒くなってきましたね。12月といえば、クリスマスや冬休み。冬休みといえば、12月ではありませんがお正月。カレンダーの予定もきっと何かしら埋まっているのではないでしょうか。そこで、今回はカレンダーの予定を一覧で表示してみたいと思います。予定がないと思った方も安心してください。現在から一年後までの予定を取得してみますので。また、途中で分からなくなった方も記事の中で途中経過を貼り付けていたり、GitHubのリポジトリも下の方に貼ってあるので是非参考に!

それでは始めていきましょう!


環境


  • Swift 4.2

  • Xcode 10.1

  • MasOS Mojave 10.14.1


やること

EventKitを用いてiOS純正アプリであるカレンダー(以下、純正カレンダー)の中の予定をUITabelViewで一覧表示してみるという記事です。

giphy.gif

こんな感じのアプリを作ってみようと思います!


使うフレームワーク

UIKit

EventKit


プロジェクト作成を作成しましょう

Create a new Xcode ProjectSimple View Applicationを選択してプロジェクトを作成するところまでやってください。


ViewController.swiftを編集しよう。


ViewController.swift

import UIKit

import EventKit // ここを追加

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}

}


まずはEKEventStoreのプロパティを宣言しましょう。

下のように宣言します。同時にインスタンスの生成も行います!


ViewController.swift

var eventStore = EKEventStore() // これをクラスの中に記述。


こやつが純正カレンダーのデータを持っています。

次にNSCalendarsUsageDescriptionInfo.plistファイルに記述して純正カレンダーをなぜこのアプリで使用するかをユーザーに提示してあげましょう。

スクリーンショット 2018-11-26 17.55.34.png

下から2つ目のKeyを追加しました。Valueは純正カレンダーを利用する理由を書いておくといいと思います。


 純正カレンダーへのアクセスをユーザーに求めてみよう

まずは今現在、純正カレンダーにアクセスできるのかを取得します。

それを確かめるための関数を定義します。

今回はアクセス権限を確認するメソッドなのでcheckAuthという名前にします。


ViewController.swift

// クラスの中に定義。

func checkAuth() {
}

現在のアクセス権限の状態を取得します。


ViewController.swift

func checkAuth() {

//現在のアクセス権限の状態を取得
let status = EKEventStore.authorizationStatus(for: EKEntityType.event)
}

次に、アクセス権限がまだならリクエストを送ります。その他の場合では今回は特に何もしません。


ViewController.swift

func checkAuth() {

//現在のアクセス権限の状態を取得
let status = EKEventStore.authorizationStatus(for: EKEntityType.event)

if status == .authorized { // もし権限がすでにあったら
print("アクセスできます!!")
}else if status == .notDetermined {
// アクセス権限のアラートを送る。
eventStore.requestAccess(to: EKEntityType.event) { (granted, error) in
if granted { // 許可されたら
print("アクセス可能になりました。")
}else { // 拒否されたら
print("アクセスが拒否されました。")
}
}
}
}


次にcheckAuthviewDidLoadのなかで呼びます。


ViewController.swift

override func viewDidLoad() {

super.viewDidLoad()
checkAuth()
}

一度実行してみましょう!

Simulator Screen Shot - iPhone XR - 2018-11-26 at 18.20.19.png

このような画面が出れば成功です!

次は直近一年のカレンダーのイベントを取得してみましょう。


純正カレンダーのイベントを取得。

まずはカレンダーを作成します。


ViewController.swift

let calendar = Calendar.current // クラスの中に記述。


このCalendarクラスは日時を操作・管理するクラスです。

今日から一年後までのイベントを取得するメソッドを定義します。


ViewController.swift

// クラスの中に定義。

func getEventsInOneYear() {
}

次にDateComponents使用します。

DateComponentsCalendarクラスと密接な関係があり2つを用いることでDate型のプロパティを操作できます。


ViewController.swift

func getEventsInOneYear() {

var componentsOneYearDelay = DateComponents()
componentsOneYearDelay.year = 1 // 今の時刻から1年進めるので1を代入
let endDate = calendar.date(byAdding: componentsOneYearDelay, to: Date()) // 一年後の日付が`Date`型で作成できた。 Date()は現在の日付を表す。
}

byAddingでどのくらい時間を進めるのか(減少も可能)

toで何に対して時間を進めるのか

を決定します。

これで一年先の日付が作成できました。あとは現在の日付も変数で宣言しておきましょう。


ViewController.swift

func getEventsInOneYear() {

var componentsOneYearDelay = DateComponents()
componentsOneYearDelay.year = 1
let startDate = Date()
let endDate = calendar.date(byAdding: componentsOneYearDelay, to: Date()) // 一ヶ月後の日付が`Date`型で簡単に作成できた。
}

次に、上の2つの日付の範囲で純正カレンダーからイベントを取得します。

let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)

この行を追加するのですが、定数predicateNSPredicateという型に属していて指定した条件と照らし合わせてマッチするものだけを探してくれるものです。

今回は範囲の最初と最後を指定して現在から一年後までのイベントを取得しています。

あとは指定した範囲でデータを取ってきます。


ViewController.swift

func getEventsInOneYear() {

var componentsOneYearDelay = DateComponents()
componentsOneYearDelay.year = 1
let startDate = Date()
let endDate = calendar.date(byAdding: componentsOneYearDelay, to: Date())!
let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)

let eventArray = eventStore.events(matching: predicate) //ここを追加してます!
}


また、取って来たイベントのデータを格納するための配列を他のブロックの中でも使いたいのでプロパティとして宣言しておきます。


現在までのコードはこちら。


ViewController.swift

import UIKit

import EventKit

class ViewController: UIViewController {

var eventStore = EKEventStore()
let calendar = Calendar.current
var eventArray: [EKEvent] = [] //ここを追加!!

override func viewDidLoad() {
super.viewDidLoad()
checkAuth()
}

func checkAuth() {
//現在のアクセス権限の状態を取得
let status = EKEventStore.authorizationStatus(for: EKEntityType.event)

if status == .authorized { // もし権限がすでにあったら
print("アクセスできます!!")
}else if status == .notDetermined {
// アクセス権限のアラートを送る。
eventStore.requestAccess(to: EKEntityType.event) { (granted, error) in
if granted { // 許可されたら
print("アクセス可能になりました。")
}else { // 拒否されたら
print("アクセスが拒否されました。")
}
}
}
}

func getEventsInOneYear() {
var componentsOneYearDelay = DateComponents()
componentsOneYearDelay.year = 1 // 今の時刻から1年進めるので1を代入
let startDate = Date()
let endDate = calendar.date(byAdding: componentsOneYearDelay, to: Date())! // 一年後の日付が`Date`型で簡単に作成できた。 Date()は現在の日付を表す。
let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)
eventArray = eventStore.events(matching: predicate) // ここを変更!
}

}



UITableViewを用いてデータを表示していく。

UITableViewを宣言。


ViewController.swift

class ViewController: UIViewController {

@IBOutlet var table: UITableView!
}

dataSourceもろもろを設定。

このときに、プロトコルの実装はクラスと切り分けて記述すると見やすくなるという利点があるので以下のようにextensionを使用します!


ViewController.swift

class ViewController: UIViewController { // ここではクラスしか継承しない。

@IBOutlet var table: UITableView!

override func viewDidLoad() {
super.viewDidLoad()
table.dataSource = self
}
}

extension ViewController: UITableViewDataSource {

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// ここでは表示する合計のセルの個数を戻り値として指定するよ。
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// ここでは1つ1つのセルの装飾をしていくよ!
}
}


dataSourceのメソッドに処理を追加していきます。


ViewController.swift

extension ViewController: UITableViewDataSource {

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventArray.count // 配列の数だけセルを用意する。
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = eventArray[indexPath.row].title
return cell
}
}


次に、純正カレンダーのイベントの取得が終わった時にtableをリロードする処理をgetEventsInOneYearに追加しておきます。


ViewController.swift

func getEventsInOneYear() {

var componentsOneYearDelay = DateComponents()
componentsOneYearDelay.year = 1
let startDate = Date()
let endDate = calendar.date(byAdding: componentsOneYearDelay, to: Date())!
let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)
eventArray = eventStore.events(matching: predicate)

table.reloadData() //ここを追加
}


最後にviewDidLoadgetEventsInOneMonthを呼んであげましょう。


ViewController.swift

override func viewDidLoad() {

super.viewDidLoad()
table.dataSource = self
checkAuth()
getEventsInOneYear()
}

これでひとまずViewControllerの設定は完了です!


Main.storyBoardの設定をする。

必要な部品は

UITableView1つです。

CellIdentifierはコードでは"Cell"にしているのでそこだけ注意してもらえばあとは自由にしてもらって大丈夫です!

ちなみに僕のは

スクリーンショット 2018-11-26 19.31.50.png

単純ですけど、こんな感じにしてみました!

あとは関連付けをして完成です!

Simulator Screen Shot - iPhone XR - 2018-11-26 at 19.33.12.png


もう少し頑張りたい編 (おまけ)


やっておいてほしいこと

UITableViewCellのカスタムクラスを作ること&&Main.storyboardでセルのクラスをカスタムクラスに設定すること


やること

先ほどのプロジェクトを引き続き使っていきます。

実はEKEventtitle以外にもカレンダーの予定の情報を持っています。

スクリーンショット 2018-11-27 23.37.53.png

ここら辺がユーザー的に一覧で表示されると喜ぶデータだと思うので、title以外にも表示してみましょう!!!

ということで、フェーズ2始めていきましょう。


CalendarTableViewCell(カスタムセル)を編集


CalendarTableViewCell.swift

import UIKit

import EventKit

class CalendarTableViewCell: UITableViewCell {

@IBOutlet var titileLabel: UILabel! // イベントのタイトルを表示するラベル
@IBOutlet var dateLabel: UILabel! // イベントの日時を表示するラベル

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}

//あとで使います!
@IBAction func tappedURLButton() {
}

//あとで使います! Date型をString型に変換しているメソッドです。
func formatToString(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
return dateFormatter.string(from: date)
}

}


こんな感じでカスタムセルを編集してください!

特に説明はしませんがもし分からないところがあれば教えてください!


書き方をスマートに!

上で予想がついたかと思いますが、カレンダーのURLスキームや作成日時をこれから取得します。それに伴いViewController.swiftの一部のメソッドを以下のように編集します。


ViewController.swift


//このメソッドを以下のように編集!
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "Cell") as! CalendarTableViewCell
cell.event = eventArray[indexPath.row]
return cell
}

CalendarTableViewCell.swiftも以下のように編集!


CalendarTableViewCell.swift

import UIKit

import EventKit

class CalendarTableViewCell: UITableViewCell {

@IBOutlet var titileLabel: UILabel!
@IBOutlet var dateLabel: UILabel!

var event: EKEvent! {
didSet {
titileLabel.text = event.title
dateLabel.text = formatToString(date: event.startDate!) + "~" + formatToString(date: event.endDate!)
}
}

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}

@IBAction func tappedURLButton() {
let interval = event.startDate!.timeIntervalSinceReferenceDate
let url = URL(string: "calshow:\(interval)")!
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

func formatToString(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
return dateFormatter.string(from: date)
}

}


少したくさん書きましたがこんな感じです。

解説をすると


CalendarTableViewCell.swift

var event: EKEvent! {

didSet {
titileLabel.text = event.title
dateLabel.text = formatToString(date: event.startDate!) + "~" + formatToString(date: event.endDate!)
}
}

まず、この部分のdidSetとは変数に値が代入し終わったら自動的に呼ばれる機能のことです。

次に、


ViewController.swift

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = table.dequeueReusableCell(withIdentifier: "Cell") as! CalendarTableViewCell
cell.event = eventArray[indexPath.row]
return cell
}

この部分では、先ほどまでは

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = table.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = eventArray[indexPath.row].title
return cell
}

という風に一つのプロパティtitleをセルに渡していたのですが、startDateendDateと渡すプロパティが多くなってくるとどうも個人的にスッキリしません。セルが持つUIの設定はセルのクラスの中で行なって、ここではセルに必要なデータを渡すだけにしておきたいところです。

そこで、eventごと渡すことで、セルの中ではUIの更新をビューコントローラの中ではデータの受け渡しとやることが分けられるのでスッキリするのではないでしょうか。



CalendarTableViewCell.swift

func formatToString(date: Date) -> String {

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
return dateFormatter.string(from: date)
}

DateFormatterとは0:00:000時00分などの日時を表示する形式のことです!

DateFormatterを用いるとdateFormatter.string(from: date)メソッドを読んだときの結果が変わってきます。

let dateFormatter = DateFormatter()

dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
print(dateFormatter.string(from: date))

let secondDateFormatter = DateFormatter()
secondDateFormatter.dateFormat = "MM/dd HH:mm"
print(secondDateFormatter.string(from: date))

2018/11/29 00:00

2018/11/29 23:59
2018/11/30 14:45
2018/11/30 16:15
2019/01/20 00:00
2019/01/20 23:59
2019/06/15 00:00
2019/06/15 23:59

11/29 00:00
11/29 23:59
11/30 14:45
11/30 16:15
01/20 00:00
01/20 23:59
06/15 00:00
06/15 23:59

結果が違うのがわかったでしょうか。DateFormatterは指定したフォーマットで日付を文字列に変換してくれます。


UIの変更

僕はこんな感じにしてみました。

スクリーンショット 2018-11-28 0.09.15.png


実行

リンクをタップすると無事、カレンダーの予定を確認できましたでしょうか。

giphy.gif

これで本当に終了です。本当にお疲れ様でした!!


完成リポジトリ

FetchCalendarEvent


最後に

12月になって年の終わりを感じますね。

カレンダーの予定は埋まっていましたか?

僕は埋まってなかったです。。。

次回は、Web系メンターのコバトンさんです!どんな内容かは明日にならないとわかりませんがWebでなにかということなので乞うご期待!

最後まで読んでいただきありがとうございました!

ふみっちでした〜。