post Image
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でなにかということなので乞うご期待!

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


『 Swift 』Article List