#はじめに
前回の記事の続きになります。前回はカレンダー作製に必要な基本的な考え方について説明しました。
- 閏年の確認
- 曜日の計算
- 週の数の計算
今回はこれらを元に実装していきたいと思います。
#本稿を読むにあたって必要なスキル
下記のリポジトリに全コードを掲載しています。本稿ではその全コードを説明致しません。従って以下の知識を有していることを前提とさせていただきます。
- Swiftプログラミングの基本的な知識
- storyboardの使い方
#環境
- XCode Version 10.2.1
- Swift 5.0
#リポジトリ
下記のアドレスに実装コードがあります。ダウンロードなどご自由にお使いください。
https://github.com/ynakaDream/Calender-Application-Swift
#Storyboard
storyboard上でUINavigationControllerとUICollectionViewを用いて上図のように作製してください。今回は曜日と日付との間と上下の日付間に下線を入れたいためUIView(スーパービュー)の背景色に白以外の色を選択し,UICollectionViewの背景色は透明,cellは白色を選択してください。下線に関してはCalendarViewController.swift内のUICollectionViewDelegateFlowLayoutのところをご覧ください。
#実装コード
CalendarControllerView.swiftとCalendarUseCase.swiftのコードを掲載します。そのほかのコードはGithubをご参照ください。
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
}
}
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の
requestForCalendar?.requestNumberOfWeeks(request: date)
requestForCalendar?.requestDateManager(request: date)
によって週の数の取得requestNumberOfWeeks()とcellに表示する日付の取得requestDateManager()がCalendarControllerに要求され,CalendarControllerはその要求事項をCalendarUseCaseに伝えます。
週の数は
func numberOfWeeks(year: Int, month: Int) {
let weeks = numberOfWeeks(year, month)
responseForCalendar?.responseNumberOfWeeks(response: weeks)
}
で取得されます。これに関しては前回の記事を参照していただくとわかると思います。
一方,cellに表示する日にちは
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)になります。
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を引き算します。
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()に渡され,次月または先月の年月が取得されます。
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