(Swift) Custom Calendar in your APP 在你的 APP 中自定義日曆

目標:自定義屬於你的日曆

新增 UIButton UILabel 在 StoryBoard 中

UILabel : Font->System Style->Semibold Size-> 26

UIButton : Image->chevron.right chevron.left

使用 StackView 將其包住,並設定 StackView 的 Auto Layout

UILabelContent Hugging Priority Horizontal 設為 249

新增 7 個 UILabel Sun Mon Tue Wed Thur Fri Sat ,並使用 StackView 將其包住,最後一樣設定 StackView 的 Auto Layout

StackView : Axis-> Horizontal Alignment->Fill Distribution->Fill Equally Spacing->0

新增 CollectionView 到 StoryBoard 中,並設定 Auto Layout

從 CollectionView 拉到上方的 StackView Vertical Spacing

從 CollectionView 拉到上方的 View,並勾選 Leading Space to Safe Area Trailing Space to Safe Area Bottom Space to Safe Area

將 CollectionView 中 Horizontal Vertical 的 Safe Area Equals 都設為 0

left(before) right(after)

新增一個 UIButton 到 Collection View Cell 中,並設定 UIButton 的 Auto Layout

從 UIButton 拉到 Collection View Cell 中,勾選 Center Horizontally in Container Center Vertically in Container

新增一個 Cocoa Touch Class

Subclass of : UICollectionViewCell

Class : CalenderCollectionViewCell

將 Collection View Cell 的 Class 改為 CalenderCollectionViewCell

接下來在 CalenderCollectionViewCell 中新增以下程式碼

class CalenderCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var dayOfMonth: UIButton!
}

拉畫面中所需要的 @IBOutlet@IBAction

import UIKit

class ViewController: UIViewController {


@IBOutlet weak var collectionView: UICollectionView!
@IBOutlet weak var monthLabel: UILabel!



override func viewDidLoad() {
super.viewDidLoad()


}

@IBAction func nextMonth(_ sender: UIButton) {

}

@IBAction func previousMonth(_ sender: UIButton) {

}

}

點選 Collection View Cell 中的 UIButton,並從 Automatic 中,點選 CalenderCollectionViewCell

@IBOutlet weak var dayOfMonth: UIButton! 拉 到左方的 Button 中

並設定 Collection View Cell 的 Identifier 成 calCell

在 ViewController 中新增一些所需的變數

import UIKit

class ViewController: UIViewController {

...

var now = Date()
let dateformatter = DateFormatter()
var totalSquares = [String]()

...

新增以下 Function 在 ViewController 中

class ViewController: UIViewController {

...

func setCellsView() {
let width = (collectionView.frame.size.width - 2) / 8
let height = (collectionView.frame.size.height - 2) / 10

let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.itemSize = CGSize(width: width, height: height)
}

新增 UICollectionViewDelegate UICollectionViewDataSource 到 ViewController 中

extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
<#code#>
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
<#code#>
}
}

接下來新增必要的程式碼在 func collectionView

extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
totalSquares.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "calCell", for: indexPath) as! CalenderCollectionViewCell
cell.dayOfMonth.setTitle(totalSquares[indexPath.item], for: .normal)
return cell
}

}

設定 CollectionView 的 delegate dataSource,可以從 CollectionView 拉到 ViewController

創建一個 Swift File CalendarHelper

CalendarHelper中新增以下的程式碼

import Foundation
import UIKit


class CalendarHelper
{
let calendar = Calendar.current

let dateFormatter = DateFormatter()


func plusMonth(date: Date) -> Date
{
return calendar.date(byAdding: .month, value: 1, to: date)!
}


func minusMonth(date: Date) -> Date
{
return calendar.date(byAdding: .month, value: -1, to: date)!
}


func monthString(date: Date) -> String
{
dateFormatter.dateFormat = "LLLL"
return dateFormatter.string(from: date)
}


func yearString(date: Date) -> String
{
dateFormatter.dateFormat = "yyyy"
return dateFormatter.string(from: date)
}

func yearandmonth(date: Date) -> String
{
dateFormatter.dateFormat = "yyyy/MM"

return dateFormatter.string(from: date)
}


func daysInMonth(date: Date) -> Int
{
let range = calendar.range(of: .day, in: .month, for: date)!
return range.count
}


func dayOfMonth(date: Date) -> Int
{
let components = calendar.dateComponents([.day], from: date)
return components.day!
}


func firstOfMonth(date: Date) -> Date
{
let components = calendar.dateComponents([.year, .month], from: date)
return calendar.date(from: components)!
}


func weekDay(date: Date) -> Int
{
let components = calendar.dateComponents([.weekday], from: date)
return components.weekday! - 1
}

}

在 ViewController 中新增 function setMonthView ,再將 setCellsView() setMonthView 放到 viewDidLoad()

class ViewController: UIViewController {


override func viewDidLoad() {
super.viewDidLoad()

setCellsView()
setMonthView()

}

...

func setMonthView() {
totalSquares.removeAll()

let daysInMonth = CalendarHelper().daysInMonth(date: now)
let firstDayOfMonth = CalendarHelper().firstOfMonth(date: now)
let startingSpaces = CalendarHelper().weekDay(date: firstDayOfMonth)

var count: Int = 1

while(count <= 42)
{
if(count <= startingSpaces || count - startingSpaces > daysInMonth)
{
totalSquares.append("")
}
else
{
totalSquares.append(String(count - startingSpaces))
}
count += 1
}

let text = CalendarHelper().monthString(date: now)
+ " " + CalendarHelper().yearString(date: now)

monthLabel.text = text
}

到這邊就可以先來執行程式測試看看,會發現顯示的數字沒有對齊

點選 CollectionView ,將 Estimate 改為 None ,並將 Min Spacing For Cells For Lines 都改為 2,即可修正

最後在 nextMonth previousMonth 新增以下程式碼,即可完成月份的切換

    @IBAction func nextMonth(_ sender: UIButton) {
now = CalendarHelper().plusMonth(date: now)
setMonthView()
collectionView.reloadData()

}

@IBAction func previousMonth(_ sender: UIButton) {
now = CalendarHelper().minusMonth(date: now)
setMonthView()
collectionView.reloadData()

}

讓今日日期的 Button 在初始畫面被選取

要讓本日的日期,在初始畫面先被選取,可以在 collectionView 新增以下程式碼

    
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "calCell", for: indexPath) as! CalenderCollectionViewCell
cell.dayOfMonth.setTitle(totalSquares[indexPath.item], for: .normal)

// 判斷是否為今日日期
// 若為今日日期 button 為選取模式
dateformatter.dateFormat = "dd"
// 若小於 10 必須補 0
if Int(totalSquares[indexPath.item]) ?? 0 < 10 {
if "0\(totalSquares[indexPath.item])" == dateformatter.string(from: now) {
cell.dayOfMonth.isSelected = true
}
}else if totalSquares[indexPath.item] == dateformatter.string(from: now) {
cell.dayOfMonth.isSelected = true
}

return cell
}

執行後,本日的日期已經被預設選取,但是無法切換不同日期或是取消點選

給定 Button 點擊事件

新增 @objc func touchButton(sender: UIButton) 與下列程式碼

    
@objc func touchButton(sender: UIButton) {
sender.isSelected.toggle()
}


func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "calCell", for: indexPath) as! CalenderCollectionViewCell
cell.dayOfMonth.setTitle(totalSquares[indexPath.item], for: .normal)

...

cell.dayOfMonth.addTarget(self, action: #selector(touchButton(sender: )), for: .touchUpInside)

return cell
}

雖然可以取消點擊還有改變其他按鈕了,但是通常使用者只會點選一個日期,並希望點選的日期做出反應,且空白的 Button 也能選取

新增 var allButton = [UIButton]() 保存所有的 Button

import UIKit

class ViewController: UIViewController {

...
var allButton = [UIButton]()

新增 func closealltoggle(_ button: [UIButton]) 用來關閉所有 Button 選取

import UIKit

class ViewController: UIViewController {

...
// 預設所有 button 都未選取
func closealltoggle(_ button: [UIButton]) {
for i in button {
i.isSelected = false
}
}

再次修改 @objc func touchButton(sender: UIButton)

     @objc func touchButton(sender: UIButton) {
// 關閉所有 Button 選取
closealltoggle(allButton)
// 判斷是否為空的 Button
if sender.titleLabel?.text == "Button" {
sender.isSelected = false
sender.isEnabled = false
}else {
sender.isSelected.toggle()
// 取得點選日期
print("\(CalendarHelper().yearandmonth(date: now))/\(sender.titleLabel?.text! ?? "")")
}

// 取得點選日期
var selectDay = "\(CalendarHelper().yearandmonth(date: now))/\(sender.titleLabel?.text! ?? "")"

// 判斷日期數字是否小於 10,若小於 10 必須補 0
if Int(sender.titleLabel?.text ?? "") ?? 0 < 10 {
selectDay = "\(CalendarHelper().yearandmonth(date: now))/0\(sender.titleLabel?.text! ?? "")"
}

// 將 now 日期改為 所選的日期
dateformatter.dateFormat = "yyyy/MM/dd"
now = dateformatter.date(from: selectDay)!

}

新增 預防點選到空白按鈕造成閃退

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "calCell", for: indexPath) as! CalenderCollectionViewCell
cell.dayOfMonth.setTitle(totalSquares[indexPath.item], for: .normal)


// 判斷 button 上面的字是否為空白
// 若為空白就無法點選
if totalSquares[indexPath.item] == "" {
cell.dayOfMonth.isEnabled = false
}else {
cell.dayOfMonth.isEnabled = true
}

...

預防 切換月份時,產生多個被選取的 Button,在 nextMonth previousMonth 都新增 closealltoggle(allButton)

    @IBAction func nextMonth(_ sender: UIButton) {
now = CalendarHelper().plusMonth(date: now)
setMonthView()
closealltoggle(allButton)
collectionView.reloadData()

}

@IBAction func previousMonth(_ sender: UIButton) {
now = CalendarHelper().minusMonth(date: now)
setMonthView()
closealltoggle(allButton)
collectionView.reloadData()

}

GitHub

Reference

--

--