#はじめに
カレンダーに関するアプリは数多くあります。しかしながらそれを自分でいちから作ろうとすると意外と参考になるサイトが少ないと感じました。カレンダーを作製してみようとしている人に役に立てればと思い本稿を作成しました。実際に作ってみると結構難しかったのですが,その反面,プログラムのスキル向上には非常に役に立ちます。
今回,カレンダーを作製する際,基本的には日付の取得のみCalendar()を用い,それ以外(例えば,閏年や曜日の計算など)は全て自作しました。UIに関してはUINavigationControllerとUICollectionViewを用いました。本カレンダーアプリを起動すると,今日の日にちはピンク色で表示され,次月(先月)ボタンをタップすると次月(先月)のカレンダーが表示されます。
本稿ではカレンダーを作製するのに必要なロジックに焦点を置いてます。実装に関しては続編で説明させて頂きます。
#本稿を読むにあたって必要なスキル
下記のリポジトリに全コードを掲載しています。本稿ではその全コードを説明致しません。従って以下の知識を有していることを前提とさせていただきます。
- Swiftプログラミングの基本的な知識
#環境
- XCode Version 10.2.1
- Swift 5.0
#リポジトリ
下記のアドレスに実装コードがあります。ダウンロードなどご自由にお使いください。
https://github.com/ynakaDream/Calender-Application-Swift
本カレンダーアプリを作製する上でのアーキテクチャを示します。あくまで一例ですのでこうする必然性はありません。むしろ別の良い方法があれば教えて頂ければと思います。なお,このアーキテクチャに関する詳細な説明は行いません。以下に概要のみ説明します。
###Presentation層
アプリの外観に関する部分の責務を担います。
- CalendarViewController: ビューの表示に関する処理を担います
- CalendarController: CalendarViewControllerからの指示を受け取りDomain層の各機能を呼び出します
- CalendarPresenter: Domain層から送られてきた結果をCalendarViewControllerに適した形式に変換してから渡すことを担います
###Domain層
ビジネスロジックに関する責務を担います。
- CalendarUseCase: カレンダーに関する計算を担います
- DateItems: 日付の取得を行います
#カレンダーを作製するのに必要なロジック
ここからが本稿のメインになります。この章の内容とSwiftのプログラミングのスキルがあればカレンダーを作製することができると思います。実装に関しては本稿の続編で説明します。それでは,カレンダーの作製に欠かせないロジックをコードとともに説明します。なお,本稿では日曜日始まりのカレンダーを前提に説明します。
### 閏年の確認
その年が閏年である条件は
条件⑴ 400で割り切れる
条件⑵ 4で割り切れる且つ100で割り切れない
上記の条件⑴または条件⑵を満たせば閏年になります。これをコードで書けば以下のようになります。
let isLeapYear = { (year: Int) in year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) }
引数year(年)を代入したとき,isLeapYear=trueの時は閏年,falseの時は平年を意味します。クロージャで記述していますが,メソッドで定義しても特に問題ありません。メソッドの場合は下記のようになります。
func isLeapYear(year: Int) -> Int {
return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}
### 曜日の計算
曜日の計算にはツェラーの公式(Zeller's congruence)を用います。
この式を用いることで年月日から曜日を求めることができます。数式をみるとややこしそうに感じますが実際は結構簡単です。床関数はxを超えないx以下の最大整数を表し,Swiftでは「/」に当たります。(x)mod(n)はxをnで割った時の剰余を表し,Swiftでは「%」になります。従って,ツェラーの公式をコードで書けば以下になります。
let zellerCongruence = { (year: Int, month: Int, day: Int) in (year + year/4 - year/100 + year/400 + (13 * month + 8)/5 + day) % 7 }
引数であるyear(年),month(月),day(日)を代入すると曜日が求められます。計算結果は0〜6の整数値のいづれかで,0が日曜日を表し,6が土曜日に当たります。ただし,monthが1月と2月の場合のみそれぞれに12を加え,前年の年として計算します。例えば,2019年1月場合は2018年13月として計算します。これをコードで書くと以下のようになります。
// year, month, dayは引数
if month == 1 || month == 2 {
year -= 1
month += 12
}
以上をまとめると曜日の計算は以下のようなコードになります。
func dayOfWeek(_ year: Int, _ month: Int, _ day: Int) -> Int {
var year = year
var month = month
if month == 1 || month == 2 {
year -= 1
month += 12
}
return zellerCongruence(year, month, day)
}
例えば,2019年5月20日の曜日を求めたければ
let dayOfweek = dayOfWeek(2019, 5, 20)
print(dayOfweek) // 1(月曜日)
で求めることができます。
### 週の数の計算
本稿のCalendarアプリはUICollectionViewを用いています(上図参照)。Section0は曜日を表し,そのCell数は日曜日から土曜日までの7つになります。一方,Section1は日付部分で,そのCell数は週の数に曜日の日数の7を掛ければ求める事ができますが,週の数は年月によって変わります。例えば,2019年5月の週の数は5つに対し,6月は6つになります。従って,5月のCell数は5✖️7=35であるのに対し,6月は6✖️7=42になります。
それでは週の数を求めてみましょう。週の数は必ず4〜6のいずれかになります。ではその条件を見てみましょう。
####週の数が4つの場合
この条件を満たすのは一つしかありません。それは平年の2月でかつ初日が日曜日で始まる時のみです。平年の2月は28日間しかありません。従って2月1日が日曜日から始まる時のみ28/7=4で週の数は4つになります。コードは以下のようになります。
func conditionFourWeeks(_ year: Int, _ month: Int) -> Bool {
let firstDayOfWeek = dayOfWeek(year, month, 1)
return !isLeapYear(year) && month == 2 && (firstDayOfWeek == 0)
}
!isLeapYear(year)は平年の場合はtrue(!が付いているので),dayOfWeek(year, month, 1)==0で初日が日曜日を表すので,month==2と併せてそれらの条件を満たせばconditionFourWeeks(_ year: Int)はtrueを返します。
####週の数が6つの場合
この条件を満たすのは,月の日数が30日間でかつ初日が土曜日の場合と,月の日数が31日間で初日が金曜日か土曜日の場合に限られます。コードは以下になります。
func conditionSixWeeks(_ year: Int, _ month: Int) -> Bool {
let firstDayOfWeek = dayOfWeek(year, month, 1)
let days = numberOfDays(year, month)
return (firstDayOfWeek == 6 && days == 30) || (firstDayOfWeek >= 5 && days == 31)
}
####週の数が5つの場合
これは週の数が4かつ6でない場合は,週の数は5になります。
####まとめると
以上をまとめると,週の数を求めるコードは以下になります。
func numberOfWeeks(_ year: Int, _ month: Int) -> Int {
if conditionFourWeeks(year, month) {
return 4
} else if conditionSixWeeks(year, month) {
return 6
} else {
return 5
}
}
Cellの数はこのnumberOfWeeks(_ year: Int, _ month: Int)で得られた整数値に7を掛ければ得られます。
### 日にちの取得
これはSwiftのCalendar()を用います。
struct Request {
var year: Int
var month: Int
var day: Int
init() {
let calendar = Calendar(identifier: .gregorian)
let date = calendar.dateComponents([.year, .month, .day], from: Date())
year = date.year!
month = date.month!
day = date.day!
}
}
インスタンス生成時に自動的に日にちを取得をするようにしています。構造体を用いていますが,必ずしもこうする必要はありません。
#最後に
今回はカレンダー作製に必要なロジックに焦点を当て説明しました。本稿の内容とUICollectionViewを組み合わせればカレンダーを作製することができます。
本稿に関して間違いやご意見があれば連絡ください。またコードに関して,より良いアイデアがあれば教えていただけると助かります。参考にさせていただきます。
ここまで読んでいただきありがとうございました。
#続編の記事
Swiftによるカレンダーアプリの作製【2/2】
#参考文献
【Swift】Calendarアプリを作る
https://qiita.com/sakuran/items/3c2c9f22cbcbf4aff731