(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
將 UILabel
的 Content 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
新增一個 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()
}