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

# はじめに
[Life is Tech !](https://life-is-tech.com/index.php)というところで中高生(メンバー)に対してiPhoneアプリプログラミングコースでSwiftを教えているメンターのふみっちです。

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


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

さて、12月に入って寒くなってきましたね。12月といえば、クリスマスや冬休み。冬休みといえば、12月ではありませんがお正月。カレンダーの予定もきっと何かしら埋まっているのではないでしょうか。そこで、今回はカレンダーの予定を一覧で表示してみたいと思います。予定がないと思った方も安心してください。現在から一年後までの予定を取得してみますので。また、途中で分からなくなった方も記事の中で[途中経過](https://qiita.com/fummicc1-sub/private/a1f551abc5b5fecb773f#%E7%8F%BE%E5%9C%A8%E3%81%BE%E3%81%A7%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AF%E3%81%93%E3%81%A1%E3%82%89)を貼り付けていたり、GitHubのリポジトリも[下の方に](https://qiita.com/fummicc1-sub/private/a1f551abc5b5fecb773f#%E5%AE%8C%E6%88%90%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA)貼ってあるので是非参考に！

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

# 環境
- Swift 4.2
- Xcode 10.1
- MasOS Mojave 10.14.1 

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

![giphy.gif](https://qiita-image-store.s3.amazonaws.com/0/300444/9c09e625-c7b8-bc5c-fa1c-717a7dd03d98.gif)

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

# 使うフレームワーク
`UIKit`
`EventKit`

# プロジェクト作成を作成しましょう
 `Create a new Xcode Project` → `Simple 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() // これをクラスの中に記述。
```

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

次に`NSCalendarsUsageDescription`を`Info.plist`ファイルに記述して純正カレンダーをなぜこのアプリで使用するかをユーザーに提示してあげましょう。
<img width="1021" alt="スクリーンショット 2018-11-26 17.55.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/300444/21fad119-45f3-7240-7ddd-0da2fd1764d5.png">
下から２つ目の`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("アクセスが拒否されました。")
            }
        }
    }
}
```

次に`checkAuth`を`viewDidLoad`のなかで呼びます。

```ViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    checkAuth()
}
```

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

![Simulator Screen Shot - iPhone XR - 2018-11-26 at 18.20.19.png](https://qiita-image-store.s3.amazonaws.com/0/300444/9a27be74-379b-cee4-1a34-9e85773b14c9.png)

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

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

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

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

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

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

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

```ViewController.swift
// クラスの中に定義。
func getEventsInOneYear() {
}
```

次に`DateComponents`使用します。
`DateComponents`は`Calendar`クラスと密接な関係があり２つを用いることで`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`型で簡単に作成できた。
}
```

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

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

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

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


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

```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 {
        // ここでは１つ１つのセルの装飾をしていくよ！
    }
}
```

`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() //ここを追加
}
```

最後に`viewDidLoad`で`getEventsInOneMonth`を呼んであげましょう。


```ViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    table.dataSource = self
    checkAuth()
    getEventsInOneYear()
}
```

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

# `Main.storyBoard`の設定をする。

必要な部品は
`UITableView`１つです。
`Cell`の`Identifier`はコードでは`"Cell"`にしているのでそこだけ注意してもらえばあとは自由にしてもらって大丈夫です！

ちなみに僕のは

<img width="284" alt="スクリーンショット 2018-11-26 19.31.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/300444/4e746f3d-9056-2ae6-ad5f-dd1d3fdd7dba.png">

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

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

![Simulator Screen Shot - iPhone XR - 2018-11-26 at 19.33.12.png](https://qiita-image-store.s3.amazonaws.com/0/300444/05164131-8b23-8d43-1c28-3464f0e6c67c.png)

# もう少し頑張りたい編　（おまけ）

## やっておいてほしいこと
`UITableViewCell`のカスタムクラスを作ること&&`Main.storyboard`でセルのクラスをカスタムクラスに設定すること

## やること
先ほどのプロジェクトを引き続き使っていきます。
実は`EKEvent`は`title`以外にもカレンダーの予定の情報を持っています。
<img width="1012" alt="スクリーンショット 2018-11-27 23.37.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/300444/58354929-8baf-5c81-5bb3-4a7f9401834f.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
}
```

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

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

という風に一つのプロパティ`title`をセルに渡していたのですが、`startDate`や`endDate`と渡すプロパティが多くなってくるとどうも個人的にスッキリしません。セルが持つ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:00`や`0時00分`などの日時を表示する形式のことです！

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

```swift
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の変更

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

<img width="1440" alt="スクリーンショット 2018-11-28 0.09.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/300444/55e55f05-b51d-6536-7163-0f2cb71b99ad.png">

# 実行

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

![giphy.gif](https://qiita-image-store.s3.amazonaws.com/0/300444/9c09e625-c7b8-bc5c-fa1c-717a7dd03d98.gif)

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

# 完成リポジトリ
[FetchCalendarEvent](https://github.com/fummicc1-sub/FetchCalendarEvent)

# 最後に
12月になって年の終わりを感じますね。
カレンダーの予定は埋まっていましたか？

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

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

最後まで読んでいただきありがとうございました！
ふみっちでした〜。