LoginSignup
12
20

More than 3 years have passed since last update.

Swiftによるカレンダーアプリの作製【2/2】

Posted at

はじめに

Calendar.gif

前回の記事の続きになります。前回はカレンダー作製に必要な基本的な考え方について説明しました。

  1. 閏年の確認
  2. 曜日の計算
  3. 週の数の計算

今回はこれらを元に実装していきたいと思います。

本稿を読むにあたって必要なスキル

下記のリポジトリに全コードを掲載しています。本稿ではその全コードを説明致しません。従って以下の知識を有していることを前提とさせていただきます。

  • Swiftプログラミングの基本的な知識
  • storyboardの使い方

環境

  • XCode Version 10.2.1
  • Swift 5.0

リポジトリ

下記のアドレスに実装コードがあります。ダウンロードなどご自由にお使いください。
https://github.com/ynakaDream/Calender-Application-Swift

Storyboard

Calendar-storyboard.png

storyboard上でUINavigationControllerとUICollectionViewを用いて上図のように作製してください。今回は曜日と日付との間と上下の日付間に下線を入れたいためUIView(スーパービュー)の背景色に白以外の色を選択し,UICollectionViewの背景色は透明,cellは白色を選択してください。下線に関してはCalendarViewController.swift内のUICollectionViewDelegateFlowLayoutのところをご覧ください。

実装コード

CalendarControllerView.swiftとCalendarUseCase.swiftのコードを掲載します。そのほかのコードはGithubをご参照ください。

CalendarViewController.swift
import UIKit

//MARK:- Protocol
protocol ViewLogic {
    var numberOfWeeks: Int { get set }
    var daysArray: [String]! { get set }
}

//MARK:- UIViewController
class CalendarViewController: UIViewController, ViewLogic {

    //MARK: Properties
    var numberOfWeeks: Int = 0
    var daysArray: [String]!
    private var requestForCalendar: RequestForCalendar?

    private let date = DateItems.ThisMonth.Request()
    private let daysPerWeek = 7
    private var thisYear: Int = 0
    private var thisMonth: Int = 0
    private var today: Int = 0
    private var isToday = true
    private let dayOfWeekLabel = ["日", "月", "火", "水", "木", "金", "土"]
    private var monthCounter = 0

    //MARK: UI Parts
    @IBOutlet weak var collectionView: UICollectionView!
    @IBAction func prevBtn(_ sender: UIBarButtonItem) { prevMonth() }
    @IBAction func nextBtn(_ sender: UIBarButtonItem) { nextMonth() }

    //MARK: Initialize
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        dependencyInjection()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        dependencyInjection()
    }

    //MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        configure()
        settingLabel()
        getToday()
    }

    //MARK: Setting
    private func dependencyInjection() {
        let viewController = self
        let calendarController = CalendarController()
        let calendarPresenter = CalendarPresenter()
        let calendarUseCase = CalendarUseCase()
        viewController.requestForCalendar = calendarController
        calendarController.calendarLogic = calendarUseCase
        calendarUseCase.responseForCalendar = calendarPresenter
        calendarPresenter.viewLogic = viewController
    }

    private func configure() {
        collectionView.dataSource = self
        collectionView.delegate = self
        requestForCalendar?.requestNumberOfWeeks(request: date)
        requestForCalendar?.requestDateManager(request: date)
    }

    private func settingLabel() {
        title = "\(String(date.year))\(String(date.month))月"
    }

    private func getToday() {
        thisYear = date.year
        thisMonth = date.month
        today = date.day
    }

}

//MARK:- Setting Button Items
extension CalendarViewController {

    private func nextMonth() {
        monthCounter += 1
        commonSettingMoveMonth()
    }

    private func prevMonth() {
        monthCounter -= 1
        commonSettingMoveMonth()
    }

    private func commonSettingMoveMonth() {
        daysArray = nil
        let moveDate = DateItems.MoveMonth.Request(monthCounter)
        requestForCalendar?.requestNumberOfWeeks(request: moveDate)
        requestForCalendar?.requestDateManager(request: moveDate)
        title = "\(String(moveDate.year))\(String(moveDate.month))月"
        isToday = thisYear == moveDate.year && thisMonth == moveDate.month ? true : false
        collectionView.reloadData()
    }

}

//MARK:- UICollectionViewDataSource
extension CalendarViewController: UICollectionViewDataSource {

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

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return section == 0 ? 7 : (numberOfWeeks * daysPerWeek)
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        let label = cell.contentView.viewWithTag(1) as! UILabel
        label.backgroundColor = .clear
        dayOfWeekColor(label, indexPath.row, daysPerWeek)
        showDate(indexPath.section, indexPath.row, cell, label)
        return cell
    }

    private func dayOfWeekColor(_ label: UILabel, _ row: Int, _ daysPerWeek: Int) {
        switch row % daysPerWeek {
        case 0: label.textColor = .red
        case 6: label.textColor = .blue
        default: label.textColor = .black
        }
    }

    private func showDate(_ section: Int, _ row: Int, _ cell: UICollectionViewCell, _ label: UILabel) {
        switch section {
        case 0:
            label.text = dayOfWeekLabel[row]
            cell.selectedBackgroundView = nil
        default:
            label.text = daysArray[row]
            let selectedView = UIView()
            selectedView.backgroundColor = .mercury()
            cell.selectedBackgroundView = selectedView
            markToday(label)
        }
    }

    private func markToday(_ label: UILabel) {
        if isToday, today.description == label.text {
            label.backgroundColor = .myLightRed()
        }
    }

}

//MARK:- UICollectionViewDelegateFlowLayout
extension CalendarViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let weekWidth = Int(collectionView.frame.width) / daysPerWeek
        let weekHeight = weekWidth
        let dayWidth = weekWidth
        let dayHeight = (Int(collectionView.frame.height) - weekHeight) / numberOfWeeks
        return indexPath.section == 0 ? CGSize(width: weekWidth, height: weekHeight) : CGSize(width: dayWidth, height: dayHeight)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let surplus = Int(collectionView.frame.width) % daysPerWeek
        let margin = CGFloat(surplus)/2.0
        return UIEdgeInsets(top: 0.0, left: margin, bottom: 1.5, right: margin)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0.5
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

}

CalendarUseCase.swift
import Foundation

protocol CalendarLogic: class {
    func dateManager(year: Int, month: Int)
    func numberOfWeeks(year: Int, month: Int)
}

class CalendarUseCase: CalendarLogic {

    var responseForCalendar: ResponseForCalendar?

    private let daysPerWeek = 7
    private let isLeapYear = { (year: Int) in year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) }
    private let zellerCongruence = { (year: Int, month: Int, day: Int) in (year + year/4 - year/100 + year/400 + (13 * month + 8)/5 + day) % 7 }

    func dateManager(year: Int, month: Int) {
        let firstDayOfWeek = dayOfWeek(year, month, 1)
        let numberOfCells = numberOfWeeks(year, month) * daysPerWeek
        let days = numberOfDays(year, month)
        let daysArray = alignmentOfDays(firstDayOfWeek, numberOfCells, days)
        responseForCalendar?.responseDateManager(response: daysArray)
    }

    func numberOfWeeks(year: Int, month: Int) {
        let weeks = numberOfWeeks(year, month)
        responseForCalendar?.responseNumberOfWeeks(response: weeks)
    }

}

//MARK:- Core Logic
extension CalendarUseCase {

    private 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)
    }

    private func numberOfWeeks(_ year: Int, _ month: Int) -> Int {
        if conditionFourWeeks(year, month) {
            return 4
        } else if conditionSixWeeks(year, month) {
            return 6
        } else {
            return 5
        }
    }

    private func numberOfDays(_ year: Int, _ month: Int) -> Int {
        var monthMaxDay = [1:31, 2:28, 3:31, 4:30, 5:31, 6:30, 7:31, 8:31, 9:30, 10:31, 11:30, 12:31]
        if month == 2, isLeapYear(year) {
            monthMaxDay.updateValue(29, forKey: 2)
        }
        return monthMaxDay[month]!
    }

    private func conditionFourWeeks(_ year: Int, _ month: Int) -> Bool {
        let firstDayOfWeek = dayOfWeek(year, month, 1)
        return !isLeapYear(year) && month == 2 && (firstDayOfWeek == 0)
    }

    private 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)
    }

    private func alignmentOfDays(_ firstDayOfWeek: Int, _ numberOfCells: Int, _ days: Int) -> [String] {
        var daysArray: [String] = []
        var dayCount = 0
        for i in 0 ... numberOfCells {
            let diff = i - firstDayOfWeek
            if diff < 0 || dayCount >= days {
                daysArray.append("")
            } else {
                daysArray.append(String(diff + 1))
                dayCount += 1
            }
        }
        return daysArray
    }

}

ポイント解説

アプリを起動するとCalendarViewControllerの

CalendarViewController.swift(抜粋)
requestForCalendar?.requestNumberOfWeeks(request: date)
requestForCalendar?.requestDateManager(request: date)

によって週の数の取得requestNumberOfWeeks()とcellに表示する日付の取得requestDateManager()がCalendarControllerに要求され,CalendarControllerはその要求事項をCalendarUseCaseに伝えます。

週の数は

CalendarUseCase.swift(抜粋)
    func numberOfWeeks(year: Int, month: Int) {
        let weeks = numberOfWeeks(year, month)
        responseForCalendar?.responseNumberOfWeeks(response: weeks)
    }

で取得されます。これに関しては前回の記事を参照していただくとわかると思います。

一方,cellに表示する日にちは

CalendarUseCase.swift(抜粋)
    func dateManager(year: Int, month: Int) {
        let firstDayOfWeek = dayOfWeek(year, month, 1)
        let numberOfCells = numberOfWeeks(year, month) * daysPerWeek
        let days = numberOfDays(year, month)
        let daysArray = alignmentOfDays(firstDayOfWeek, numberOfCells, days)
        responseForCalendar?.responseDateManager(response: daysArray)
    }

で取得されます。一番のポイントはalignmentOfDays(firstDayOfWeek, numberOfCells, days)になります。

CalendarUseCase.swift(抜粋)
    private func alignmentOfDays(_ firstDayOfWeek: Int, _ numberOfCells: Int, _ days: Int) -> [String] {
        var daysArray: [String] = []
        var dayCount = 0
        for i in 0 ... numberOfCells {
            let diff = i - firstDayOfWeek
            if diff < 0 || dayCount >= days {
                daysArray.append("")
            } else {
                daysArray.append(String(diff + 1))
                dayCount += 1
            }
        }
        return daysArray
    }

このメソッドによって表示される日にちがdaysArrayに格納され,最終的にCalendarViewControllerに送られます。このメソッド部分は(【Swift】Clendarアプリを作る」)を参考にさせていただきました。

例: 2019年5月のカレンダー表示

⑴ 2019年5月1日の曜日を求めfirstDayOfWeekに格納する
-> 水曜日なのでfirstDayOfWeek=3が格納される

⑵ numberOfWeek()で週の数を求め曜日の数daysPerWeekを掛けcell数を求めnumberOfCellsにその値を格納する
-> 週の数は5なのでnumberOfCells=35が格納される

(3) numberOfDays()で5月の日数を取得し,それをdaysに格納する
-> 5月は31日間なのでdays=31が格納される

(4)alignmentOfDays()でどのセルにどの日にちを入れるかを決める
5月1日は水曜日なのでindexPath.row=3の時に"1"を表示させる。つまりindexPath.rowの値とalignmentOfDaysとの差が負の時は空("")を表示させる。日数は31日間なのでそれを越えると同様に空("")を表示させる。

次月と先月の表示

これはmonthCounter=0をはじめに定義してボタンを押すたび,次月の場合は1を加算し,先月の場合は1を引き算します。

CalendarViewController.swift(抜粋)
extension CalendarViewController {

    private func nextMonth() {
        monthCounter += 1
        commonSettingMoveMonth()
    }

    private func prevMonth() {
        monthCounter -= 1
        commonSettingMoveMonth()
    }

    private func commonSettingMoveMonth() {
        daysArray = nil
        let moveDate = DateItems.MoveMonth.Request(monthCounter)
        requestForCalendar?.requestNumberOfWeeks(request: moveDate)
        requestForCalendar?.requestDateManager(request: moveDate)
        title = "\(String(moveDate.year))\(String(moveDate.month))月"
        isToday = thisYear == moveDate.year && thisMonth == moveDate.month ? true : false
        collectionView.reloadData()
    }

}

そのmonthCounterはDateItems.MoveMonth.Request()に渡され,次月または先月の年月が取得されます。

DateItems.swift
enum DateItems {

    enum ThisMonth {
        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!
            }
        }
    }

    enum MoveMonth {
        struct Request {

            var year: Int
            var month: Int

            init(_ monthCounter: Int) {
                let calendar = Calendar(identifier: .gregorian)
                let date = calendar.date(byAdding: .month, value: monthCounter, to: Date())
                let newDate = calendar.dateComponents([.year, .month], from: date!)
                year = newDate.year!
                month = newDate.month!
            }
        }
    }

}

最後に

本稿に関して間違いやご意見があれば連絡ください。またコードに関して,より良いアイデアがあれば教えていただけると助かります。参考にさせていただきます。

ここまで読んでいただきありがとうございました。

前回の記事

Swiftによるカレンダーアプリの作製【1/2】

参考文献

【Swift】Calendarアプリを作る
https://qiita.com/sakuran/items/3c2c9f22cbcbf4aff731

12
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
20