はじめに
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
で一覧表示してみるという記事です。
こんな感じのアプリを作ってみようと思います!
使うフレームワーク
UIKit
EventKit
プロジェクト作成を作成しましょう
Create a new Xcode Project
→ Simple View Application
を選択してプロジェクトを作成するところまでやってください。
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
のプロパティを宣言しましょう。
下のように宣言します。同時にインスタンスの生成も行います!
var eventStore = EKEventStore() // これをクラスの中に記述。
こやつが純正カレンダーのデータを持っています。
次にNSCalendarsUsageDescription
をInfo.plist
ファイルに記述して純正カレンダーをなぜこのアプリで使用するかをユーザーに提示してあげましょう。
下から2つ目のKey
を追加しました。Value
は純正カレンダーを利用する理由を書いておくといいと思います。
## 純正カレンダーへのアクセスをユーザーに求めてみよう
まずは今現在、純正カレンダーにアクセスできるのかを取得します。
それを確かめるための関数を定義します。
今回はアクセス権限を確認するメソッドなのでcheckAuth
という名前にします。
// クラスの中に定義。
func checkAuth() {
}
現在のアクセス権限の状態を取得します。
func checkAuth() {
//現在のアクセス権限の状態を取得
let status = EKEventStore.authorizationStatus(for: EKEntityType.event)
}
次に、アクセス権限がまだならリクエストを送ります。その他の場合では今回は特に何もしません。
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
のなかで呼びます。
override func viewDidLoad() {
super.viewDidLoad()
checkAuth()
}
一度実行してみましょう!
このような画面が出れば成功です!
次は直近一年のカレンダーのイベントを取得してみましょう。
純正カレンダーのイベントを取得。
まずはカレンダーを作成します。
let calendar = Calendar.current // クラスの中に記述。
このCalendar
クラスは日時を操作・管理するクラスです。
今日から一年後までのイベントを取得するメソッドを定義します。
// クラスの中に定義。
func getEventsInOneYear() {
}
次にDateComponents
使用します。
DateComponents
はCalendar
クラスと密接な関係があり2つを用いることでDate
型のプロパティを操作できます。
func getEventsInOneYear() {
var componentsOneYearDelay = DateComponents()
componentsOneYearDelay.year = 1 // 今の時刻から1年進めるので1を代入
let endDate = calendar.date(byAdding: componentsOneYearDelay, to: Date()) // 一年後の日付が`Date`型で作成できた。 Date()は現在の日付を表す。
}
byAdding
でどのくらい時間を進めるのか(減少も可能)
to
で何に対して時間を進めるのか
を決定します。
これで一年先の日付が作成できました。あとは現在の日付も変数で宣言しておきましょう。
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)
この行を追加するのですが、定数predicate
はNSPredicate
という型に属していて指定した条件と照らし合わせてマッチするものだけを探してくれるものです。
今回は範囲の最初と最後を指定して現在から一年後までのイベントを取得しています。
あとは指定した範囲でデータを取ってきます。
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) //ここを追加してます!
}
また、取って来たイベントのデータを格納するための配列を他のブロックの中でも使いたいのでプロパティとして宣言しておきます。
現在までのコードはこちら。
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
を宣言。
class ViewController: UIViewController {
@IBOutlet var table: UITableView!
}
dataSource
もろもろを設定。
このときに、プロトコルの実装はクラスと切り分けて記述すると見やすくなるという利点があるので以下のようにextension
を使用します!
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
のメソッドに処理を追加していきます。
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
に追加しておきます。
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
を呼んであげましょう。
override func viewDidLoad() {
super.viewDidLoad()
table.dataSource = self
checkAuth()
getEventsInOneYear()
}
これでひとまずViewController
の設定は完了です!
Main.storyBoard
の設定をする。
必要な部品は
UITableView
1つです。
Cell
のIdentifier
はコードでは"Cell"
にしているのでそこだけ注意してもらえばあとは自由にしてもらって大丈夫です!
ちなみに僕のは
単純ですけど、こんな感じにしてみました!
あとは関連付けをして完成です!
もう少し頑張りたい編 (おまけ)
やっておいてほしいこと
UITableViewCell
のカスタムクラスを作ること&&Main.storyboard
でセルのクラスをカスタムクラスに設定すること
やること
先ほどのプロジェクトを引き続き使っていきます。
実はEKEvent
はtitle
以外にもカレンダーの予定の情報を持っています。
ここら辺がユーザー的に一覧で表示されると喜ぶデータだと思うので、title
以外にも表示してみましょう!!!
ということで、フェーズ2始めていきましょう。
CalendarTableViewCell
(カスタムセル)を編集
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
の一部のメソッドを以下のように編集します。
//このメソッドを以下のように編集!
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
も以下のように編集!
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)
}
}
少したくさん書きましたがこんな感じです。
解説をすると
var event: EKEvent! {
didSet {
titileLabel.text = event.title
dateLabel.text = formatToString(date: event.startDate!) + "~" + formatToString(date: event.endDate!)
}
}
まず、この部分のdidSet
とは変数に値が代入し終わったら自動的に呼ばれる機能のことです。
次に、
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
をセルに渡していたのですが、startDate
やendDate
と渡すプロパティが多くなってくるとどうも個人的にスッキリしません。セルが持つUIの設定はセルのクラスの中で行なって、ここではセルに必要なデータを渡すだけにしておきたいところです。
そこで、event
ごと渡すことで、セルの中ではUIの更新をビューコントローラの中では**データの受け渡し
**とやることが分けられるのでスッキリするのではないでしょうか。
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)
メソッドを読んだときの結果が変わってきます。
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の変更
僕はこんな感じにしてみました。
実行
リンクをタップすると無事、カレンダーの予定を確認できましたでしょうか。
これで本当に終了です。本当にお疲れ様でした!!
完成リポジトリ
最後に
12月になって年の終わりを感じますね。
カレンダーの予定は埋まっていましたか?
僕は埋まってなかったです。。。
次回は、Web系メンターのコバトンさんです!どんな内容かは明日にならないとわかりませんがWebでなにかということなので乞うご期待!
最後まで読んでいただきありがとうございました!
ふみっちでした〜。