前言
前陣子因為工作需求需要做一個可以多選日期的日曆,由於iOS原生沒有支援多選,所以後來找到了一個第三方套件,也就是今天的主角:FSCalendar,他可以客製化你想要的日曆,有興趣的話可以先去逛逛他的github: https://github.com/WenchaoD/FSCalendar。
目標
需求是做到像以下畫面一樣,點選兩個日期會自動選取中間的日期,再點一下選擇到的日期則會將選擇到的日期全部取消,選擇到未選擇的日期則會focus在該日期。
實作
首先要使用FSCalendar當然必須先導入這個第三方套件,不管是用CocoaPods、SPM或是其他方式都可以,要怎麼導入就不再贅述了,讓我們直接進入主題吧。
在導入後我們直接在VC裡面去實例化FSCalendar,由於我們之後還需要加上按鈕在日曆上,所以多寫了一個calenderView放在外層,並把我們的日曆加到calenderView裡面。
internal var calendar: FSCalendar = FSCalendar()
let calenderView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.clipsToBounds = true
return view
}()
private func initLayouts() {
view.addSubview(calenderView)
calenderView.snp.makeConstraints { make in
make.centerX.centerY.equalTo(view)
make.leading.trailing.equalTo(view).inset(20)
make.height.equalTo(350)
}
calenderView.addSubview(calendar)
calendar.snp.makeConstraints { make in
make.leading.trailing.top.equalTo(calenderView)
make.height.equalTo(300)
}
}
畫面看起來會像這樣:
這時候去點擊日期就會發現其實這已經滿足了我們想要多選日期的需求,但我想要的是當我選擇兩個日期後可以自動選擇中間的日期,所以我們還必須做點調整,但在那之前,前面有提到說我們可以客製化這個日曆,所以我們要先來調整一下我們的日曆樣式。
我這邊寫了initCalendarView的方法來初始化日曆的外觀,可以依照自己的喜好調整裡面的參數。
private func initCalendarView() {
calendar.dataSource = self
calendar.delegate = self
// 設置日曆背景為白色
calendar.backgroundColor = .white
// 隱藏月份標題旁邊的淡化月份,將它們的透明度設為0
calendar.appearance.headerMinimumDissolvedAlpha = 0
// 設置日曆標題的日期格式為 "MMM yyyy",例如 "Sep 2024"
calendar.appearance.headerDateFormat = "MMM yyyy"
// 設置日曆上週一至週日的文字顏色為藍色
calendar.appearance.weekdayTextColor = .blue
// 設置日曆標題(例如 "Sep 2024")的文字顏色為藍色
calendar.appearance.headerTitleColor = .blue
// 設置選中日期的背景顏色為藍色
calendar.appearance.selectionColor = .blue
// 設置今天的日期背景顏色為白色
calendar.appearance.todayColor = .white
// 設置日曆的標題使用大寫字母
calendar.appearance.caseOptions = .headerUsesCapitalized
// 設置今天日期的文字顏色為黑色
calendar.appearance.titleTodayColor = .black
}
在初始化日曆時可以看到我們順便將他的delegate及dataSource設成自己,可以透過服從這些protocol去拿到didSelect及didDeselect,好讓我們可以在日期被點擊時去做相對應的處理。
在處理日期的點擊事件之前,我們還需要兩個參數來幫助我們知道目前的選取狀態:
//使用者是否已選取完2天
private var isDoneSelected: Bool = false
//使用者選擇到的日期
private var selectedDates: [Date] = []
接著進入日期的點擊事件處理:
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
if self.isDoneSelected {
let allSelectedDates = calendar.selectedDates
selectedDates.removeAll()
self.isDoneSelected = false
DispatchQueue.main.asyncAfter(deadline: .now()) { [weak self] in
guard let wSelf = self else {return}
for date in allSelectedDates {
calendar.deselect(date)
}
calendar.select(date)
wSelf.selectedDates.append(date)
}
return
}
selectedDates.append(date)
selectedDates.sort()
selectDaysBetweenTwoDays()
if monthPosition == .previous || monthPosition == .next {
calendar.setCurrentPage(date, animated: true)
}
}
程式碼說明:
在點擊未選擇的日期時會進入到calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition)的方法,一開始會先判斷用戶是否已選擇完兩天(isDoneSelected = true),是的話代表我們要把前面選擇到的所有日期清空,並把isDoneSelected設成false,接著再把用戶選到的新日期加入陣列內。
如果還沒選擇完兩天的話(isDoneSelected = false),代表用戶選的可能是第一個日期或第二個日期,這時候就要把用戶選擇到的日期加入陣列內並做排序,以便後續處理,接著會去呼叫selectDaysBetweenTwoDays這個方法,他是用來將兩個日期之間的日期也變成選擇狀態的方法,只有在選擇了兩個日期才會執行裡面的程式碼,實作內容如下:
func selectDaysBetweenTwoDays() {
if selectedDates.count != 2 { return }
if let startDate = selectedDates.first, let endDate = selectedDates.last {
var currentDate = startDate
//選擇起訖日中間的日期
while currentDate <= endDate {
// 增加一天
self.calendar.select(currentDate)
currentDate = currentCalendar.date(byAdding: .day, value: 1, to: currentDate)!
}
selectedDates.sort()
self.isDoneSelected = true
}
}
程式碼說明:
首先會先判斷selectedDates內的數量是否為兩個,不是的話我們就直接return不處理,接著宣告一個currentDate,每跑一次迴圈都會加一天,直到currentDate等於endDate才會終止迴圈,這樣就可以達到在日曆上選取中間的日期的效果。
最後是點擊已選擇到的日期的事件處理:
func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
if self.isDoneSelected {
let selected = calendar.selectedDates
for date in selected {
calendar.deselect(date)
}
selectedDates.removeAll()
self.isDoneSelected = false
return
}
self.selectedDates.removeAll()
}
點擊到已選擇的日期會觸發calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition)的方法,在裡面也是先去判斷是否已選擇兩天,是的話就把UI上選擇到的日期清空,不是的話代表他目前只選到一個日期,直接把該日期remove掉即可。
結語
這次主要著重在點擊兩個日期會自動選取中間日期的實作,一些UI或是點擊事件就沒有特別說明,如果想看完整的程式碼歡迎到我的Github:https://github.com/Sheng-Ping-Wang/MultipleSelectionCalendar/tree/main