目的

カレンダーの機能を作ってほしいという依頼があり、

うげええ、面倒な作業だぁっっ

と思いつつ、実はカレンダー機能作ったこと無いな。

ということで、
iOSアプリのカレンダー機能を作成する際のメモ


なぜ面倒そうな気がするのか

なぜ、作ったこと無いのに、面倒そうなイメージをすでに持っているかというと、
カレンダーの機能を想定すれば容易です。

  • 閏年の計算
  • 祝日の計算
  • 日にちと曜日の関係性の計算
  • 前月と翌月を計算

など多くの計算の実施などが理由で、作るのが億劫になります。


サンプルコードを利用させていただく

他の人も面倒な機能と思っている方多いと思われ、
偉大な先人により、素晴らしいライブラリやサンプルが結構多く作られています。

例えば以下です。
GitHub FSCalendar



GitHub Calender-Application-Swift

上のライブラリはかなり有名でクオリティ高いもののようです。
ただ、2019年9月現在で、更新は1年前で止まってますし、issueも多く。
下のサンプルについては、シンプルなものですが、カレンダーの機能としては十分ですし、プログラム製作のノウハウを学べます。
それぞれ、MITライセンスなので、仕様記載が必要です。

下のサンプルコードを利用すれば十分とおもったのですが、一つ肝心な、祝日の掲載機能が搭載されていませんでした。


ゼロから作る

他に祝日の機能も持ったライブラリがないか調べようとしましたが、、
まぁ、そもそも自分のアプリのいち機能を
ライブラリやサンプルコードでがっつり固めるのは怖いと思うのですがいかがでしょうか?

なので、上のサンプルコードの考え方などは参考にしつつ、自分で制作したいと思います。

また、祝日についてですが、
ものすごくありがたいことに 、swift用のサンプルコードがありますので、それを用います。
GitHub handMadeCalendarOfSwift
上記はライセンス制限がなく、自由に使えそうです。


サンプルコード

コード全部をガッツリ載せたり、GitHubに置いたりはしませんが、
以下に抜粋して載せていこうと思います。
次ゼロから作る場合でもすぐに動作するレベルかと。

動作

コードの前にまず挙動を載せておきます。


左がiPhoneアプリで右に比較のためMacのカレンダーアプリを載せています。

一応1年前後で調べましたが、日付や祝日まわりを問題なく表示できていました。
あくまで表示だけですし、今回は前後の月は表示しないので、計算しませんでした。


定数で持っておいた方が良いもの

自分のアプリは以下の3つとなりました。

//週の数
private let weekNumber = 7

//曜日ラベル
private let weekLabel = ["日", "月", "火", "水", "木", "金", "土"]

//その月がもつ日数をDictionary型で
private let monthDay = [1:31, 2:28, 3:31, 4:30, 5:31, 6:30, 7:31, 8:31, 9:30, 10:31, 11:30, 12:31]
    



現在時刻取得ロジック

現在の年・月・日を取得
現在時刻は他にも使うので、これだけで別記事作ったほうが良いですね。

let calendar = Calendar.current
targetYear = calendar.component(.year, from: Date())
targetMonth = calendar.component(.month, from: Date())
targetDay = calendar.component(.day, from: Date())



基本構成

構成としてはCollectionViewで作りました。

collectionView.delegate = self
collectionView.dataSource = self



numberOfSections

セクションは2つにします。
1つ目は曜日記載のカレンダー上の部分
2つ目は日にちの部分

func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 2
}



numberOfItemsInSection

セル数は最大で37必要と思いました。
月ごとに計算してもいいかもしれません。

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    if section == 0 {
        return weekNumber
    }
    //1日が土曜日の場合で31日月の場合(6週ある月)、7x5+2マスが必要
    return 37
}



cellForItemAt

セルの記載部分です。
addSubviewしているので、最初にremoveFromSuperviewしてます。
セルに日付を記載するかどうかはフラグで管理してます。
閏年チェックもここで随時していますが、これは月切り替え時でも良いのかも

土日のチェックはまぁいいとして、、祝日ですが、
上であげたサンプルコードのjudgeJapaneseHolidayクラスをそのまま持ってきています。
本当に感謝で、これがないと製作がかなりキツイですね。

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    
    for subView in cell.contentView.subviews {
        subView.removeFromSuperview()
    }
    
    let dayLabel = UILabel(frame: cell.contentView.frame)
    dayLabel.textAlignment = .center
    if indexPath.section == 0 {
        dayLabel.text = weekLabel[indexPath.row]
        dayLabel.textColor = #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1)
    } else {
        //flagDayStart = trueでセルに日付記載。他の場合は無地
        if dayOfWeek == indexPath.row {
            flagDayStart = true
        }
        
        //閏年チェック
        var maxDay = monthDay[targetMonth] ?? 1
        if targetMonth == 2 && ((targetYear % 4 == 0 && targetYear % 100 != 0) || targetYear % 400 == 0) {
            maxDay = 29
        }
        if maxDay <= indexPath.row - dayOfWeek {
            flagDayStart = false
        }
        
        if flagDayStart {
            dayLabel.text = String(dayCount)
            if (dayOfWeek + dayCount) % weekNumber == 0 ||
                (dayOfWeek + dayCount) % weekNumber == 1 ||
                holidayObject.judgeJapaneseHoliday(year: targetYear, month: targetMonth, day: dayCount) {
                dayLabel.backgroundColor = #colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1)
            }
            dayCount += 1
        }
    }
    
    cell.contentView.addSubview(dayLabel)

    return cell
}



月切り替え部分

簡単な処理ですが、月切り替えの部分も載せます。
前月はこの逆ですね。。

@IBAction func moveNextMonth(_ sender: Any) {
    targetMonth += 1
    if 12 < targetMonth {
        targetMonth = 1
        targetYear += 1
    }
    labelMonth.text = String(targetMonth)
    //configureについては下で書いているツェラーの公式の処理を行っている。
    configure()
    collectionView.reloadData()
}



閏年チェック

閏年チェックは閏年の意外な事実がわかって、面白かったです。
処理としては結構有名なようですね。

閏年の判定としては

  • その年が4で割れるが、100で割れない場合は閏年。
  • その年が4でも100でも400でも割れる場合は閏年。

ということで

if (targetYear % 4 == 0 && targetYear % 100 != 0) || targetYear % 400 == 0 {
    return true
} else {
    return false
}

とするのが良いようです。


日付と曜日の紐付け

ツェラーの公式というのを利用するらしい。
コチラも全くの無知でしたが、有名な公式みたいです。

// dayOfWeek -> 0:日曜 ~ 6:土曜
var checkYear = targetYear
var checkMonth = targetMonth
if checkMonth <= 2 {
    checkMonth += 12
    checkYear  -= 1
}
let leap = checkYear + checkYear / 4 - checkYear / 100 + checkYear / 400
dayOfWeek = (leap + (13 * checkMonth + 8) / 5 + 1) % 7


以上が、カレンダー機能のメモです。