前言
作為一個程式設計師
,當你開發的app
越來越多的時候,或者當你瀏覽一些app
的時候,你會發現很多模組實現的功能是一樣的。而作為開發者
而言,就更加註意這些功能一樣的東西了,因為你會發現這個專案中的某個模組完全可以使用以前做專案時封裝的一些功能模組,這樣你會無比的開心。然後去尋找以前封裝的東西,簡單的匯入和引用就解決了一個功能模組。
日期選擇器
可以說是一個經常用到的控制元件了,只是形式各不相同而已。所以為了滿足專案的需求我決定自己研究一下日曆控制元件
的實現方法。
實現 (工程程式碼見文末連結)
老規矩,先上圖
工程目錄結構
EngineeringDocuments
:工程標頭檔案
,pch
,類目
,base檔案
等。controller
:控制器(YZXSelectDateViewController),日報
,月報
,年報
,自定義
等檢視
,都是新增到該控制器的view
上。Model
:用於快取
和處理
資料。- YZXDateModel:記錄年份資訊,通過設定的
開始日期
和結束日期
計算兩日期之間所有的年份
和月份
陣列。 - YZXMonthModel:記錄月份資訊,主要用於
YZXDateModel
中。 - YZXCalendarModel:記錄
月份
的具體資訊。(其實應該放在YZXMonthModel
中,可能當時腦子抽筋了...)
- YZXDateModel:記錄年份資訊,通過設定的
Views
:各種view
,用於初始化完整的日曆控制元件
。YZXCalendarHelper
:整個工程的manager
(應該放到EngineeringDocuments
目錄下的?,Demo中已修改),可以設定一些基本資訊,如:日曆
的開始時間
和結束時間
,一些常用的NSDateFormatter
等。YZXWeekMenuView
:日報
中UICollectionView-Section
展示星期
。YZXDaysMenuView
:日報
中展示具體的日期
。YZXCalendarView
:YZXWeekMenuView
和YZXDaysMenuView
,組成完整的日曆
。YZXCalendarDelegate
:選擇日期後的回撥``代理
。DateSelection
:月報
,年報
及其對應的其他檢視
。collectionView
:日曆控制元件
主要用UICollectionView
來實現介面的搭建的,所以該資料夾
中都是一些cell
,header
等。
下面將詳細介紹一下主要
檔案
的作用
Manager
YZXCalendarHelper(manager)
YZXCalendarHelper
中主要提供了一下日曆控制元件
相關的設定,比如開始日期
和結束日期
,一些列舉
,還有一些常用的NSDateFormatter
和日期
的比較方法等,方便設定日曆控制元件
,並減少重複程式碼。具體的實現方法,將在使用到的時候介紹。
日報
和自定義日期
YZXWeekMenuView
初始化一個NSDateFormatter
,使時區
和區域語言
和NSCalendar
相同,然後通過NSDateFormatter
的例項方法veryShortWeekdaySymbols
獲取到周符號(S,M,T,W...)
,然後遍歷佈局,將週末字型設定為紅色。
- (NSDateFormatter *)createDateFormatter
{
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.timeZone = self.calendarHelper.calendar.timeZone;
dateFormatter.locale = self.calendarHelper.calendar.locale;
return dateFormatter;
}
NSDateFormatter *formatter = [self createDateFormatter];
NSMutableArray *days = [[formatter veryShortWeekdaySymbols] mutableCopy];
[days enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
UILabel *weekdayLabel = [[UILabel alloc] initWithFrame:CGRectMake(self.bounds.size.width / 7.f * idx, 0, self.bounds.size.width / 7.f, self.bounds.size.height - lineView_height)];
weekdayLabel.text = obj;
weekdayLabel.font = [UIFont systemFontOfSize:10.0];
weekdayLabel.textAlignment = NSTextAlignmentCenter;
weekdayLabel.textColor = CustomBlackColor;
if (idx == 0 || idx == 6) {
weekdayLabel.textColor = CustomRedColor;
}
[self addSubview:weekdayLabel];
}];
複製程式碼
YZXDaysMenuView
YZXDaysMenuView.h
/**
自定義初始化
@param frame frame
@param startDateString 日曆的開始時間(日期格式:yyyy年MM月dd日)
@param endDateString 日曆的結束時間(日期格式:yyyy年MM月dd日)
@return self
*/
- (instancetype)initWithFrame:(CGRect)frame
withStartDateString:(NSString *)startDateString
endDateString:(NSString *)endDateString;
//點選回撥代理
@property (nonatomic, weak) id<YZXCalendarDelegate> delegate;
//日曆單選
@property (nonatomic, copy) NSString *startDate;
//判斷是否為自定義選擇(選擇日期段)
@property (nonatomic, assign) BOOL customSelect;
//自定義日曆(可選擇兩個時間的範圍)
@property (nonatomic, copy) NSArray *dateArray;
//自定義日曆,控制可選擇的日期的最大跨度
@property (nonatomic, assign) NSInteger maxChooseNumber;
複製程式碼
initWithFrame:withStartDateString:endDateString:
:根據開始時間
和結束時間
,初始化介面。delegate
:日期選擇結束回撥。startDate
:日報
單選時,用於記錄上次所選日期。customSelect
:判斷是否為自定義日曆
選擇(選擇日期段)。dateArray
:自定義日曆
時,記錄上次選擇的日期段。maxChooseNumber
:自定義日曆
,設定可選擇日期段的最大跨度。
YZXDaysMenuView.m
私有屬性部分:
//使用的collectionView實現的介面
@property (nonatomic, strong) UICollectionView *collectionView;
//collectionView資料
@property (nonatomic, copy) NSArray <YZXCalendarModel *> *collectionViewData;
//manager
@property (nonatomic, strong) YZXCalendarHelper *calendarHelper;
//資料
@property (nonatomic, strong) YZXCalendarModel *model;
//用於記錄點選的cell
@property (nonatomic, strong) NSMutableArray <NSIndexPath *> *selectedArray;
複製程式碼
關鍵程式碼實現部分:
獲取資料來源:YZXCalendarModel
通過傳入的startDate
和endDate
,計算日期間隔之間所有的年份
,月份
,天數
等資訊。
- 使用
NSCalendar
的例項方法components:fromDate:toDate:options:
得到一個NSDateCompoments
例項,根據設定的components
可以獲取到對應的年差值
,月差值
,日差值
等。 - 根據獲取到的
dateComponents.month``for迴圈
,呼叫NSCalendar
的例項方法dateByAddingComponents:toDate:options:
獲取每個月的date
。 - 根據
NSCalendar
的rangeOfUnit:inUnit:forDate
方法,得到該月的天數numberOfDaysInMonth
(得到的是一個NSRange
,.length
獲取天數)。 - 根據
NSCalendar
的components:fromDate
方法,獲取到一個關於weekday
的NSDateComponents
例項,再通過NSDateComponents
例項的weekday
方法得到該月的第一天firstDayInMonth
是第一個星期
的第幾天
(當前日曆的每個星期
的第一天
是星期日
)。 - 通過
numberOfDaysInMonth
和firstDayInMonth
計算collectionView
對應的月份
需要多少行item
(一行是一個星期
)。 - 將對應資訊快取到
model
中,然後返回一個model陣列
。
- (NSArray<YZXCalendarModel *> *)achieveCalendarModelWithData:(NSDate *)startDate toDate:(NSDate *)endDate
{
NSMutableArray *modelArray = [NSMutableArray array];
NSDateFormatter *formatter = [YZXCalendarHelper helper].yearAndMonthFormatter;
//判斷所給年月距離當前年月有多少個月
NSDateComponents *components = [YZXCalendarHelper.helper.calendar components:NSCalendarUnitMonth fromDate:startDate toDate:endDate options:NSCalendarWrapComponents];
//迴圈遍歷得到從給定年月一直到當前年月的所有年月資訊
for (NSInteger i = 0; i<=components.month; i++) {
NSDateComponents *monthComponents = [[NSDateComponents alloc] init];
monthComponents.month = i;
NSDate *headerDate = [YZXCalendarHelper.helper.calendar dateByAddingComponents:monthComponents toDate:startDate options:0];
NSString *headerTitle = [formatter stringFromDate:headerDate];
//獲取此section所表示月份的天數
NSRange daysOfMonth = [YZXCalendarHelper.helper.calendar rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:headerDate];
NSUInteger numberOfDaysInMonth = daysOfMonth.length;
//獲取此section所表示月份的第一天是第一個星期的第幾天(當前日曆的每個星期的第一天是星期日)
NSDateComponents *comps = [YZXCalendarHelper.helper.calendar components:NSCalendarUnitWeekday fromDate:headerDate];
NSInteger firstDayInMonth = [comps weekday];
NSInteger sectionRow = ((numberOfDaysInMonth + firstDayInMonth - 1) % 7 == 0) ? ((numberOfDaysInMonth + firstDayInMonth - 1) / 7) : ((numberOfDaysInMonth + firstDayInMonth - 1) / 7 + 1);
YZXCalendarModel *model = [[YZXCalendarModel alloc] init];
model.numberOfDaysOfTheMonth = numberOfDaysInMonth;
model.firstDayOfTheMonth = firstDayInMonth;
model.headerTitle = headerTitle;
model.sectionRow = sectionRow;
[modelArray addObject:model];
}
return [modelArray copy];
}
複製程式碼
UI介面佈局:
佈局我是通過collectionView
,設定section
表示月
,item
表示日
,item
的個數為之前獲取的當月
行數sectionRow
*7,並且你需要比較indexPath.item
與firstDayInMonth
,從而將item
上的text
設定為對應的日期
,並判斷今天
的日期,將text
設定為今天
,超過今天
的日期設定為不可選
。
//從每月的第一天開始設定cell.day的值
if (indexPath.item >= firstDayInMonth - 1 && indexPath.item <= firstDayInMonth + model.numberOfDaysOfTheMonth - 2) {
self.day.text = [NSString stringWithFormat:@"%ld",indexPath.item - (firstDayInMonth - 2)];
self.userInteractionEnabled = YES;
}else {
self.day.text = @"";
self.userInteractionEnabled = NO;
}
//今天
if ([YZXCalendarHelper.helper determineWhetherForTodayWithIndexPaht:indexPath model:model] == YZXDateEqualToToday) {
self.day.text = @"今天";
self.day.textColor = CustomRedColor;
}else if ([YZXCalendarHelper.helper determineWhetherForTodayWithIndexPaht:indexPath model:model] == YZXDateLaterThanToday) {//判斷日期是否超過今天
self.day.textColor = [UIColor grayColor];
self.userInteractionEnabled = NO;
}
複製程式碼
判斷item
對應的日期
和今天
的關係:YZXCalendarHelper
- (YZXDateWithTodayType)determineWhetherForTodayWithIndexPaht:(NSIndexPath *)indexPath
model:(YZXCalendarModel *)model
{
//今天
NSDateFormatter *formatter = self.yearMonthAndDayFormatter;
//獲取當前cell上表示的天數
NSString *dayString = [NSString stringWithFormat:@"%@%ld日",model.headerTitle,indexPath.item - (model.firstDayOfTheMonth - 2)];
NSDate *dayDate = [formatter dateFromString:dayString];
if (dayDate) {
if ([YZXCalendarHelper.helper date:[NSDate date] isTheSameDateThan:dayDate]) {
return YZXDateEqualToToday;
}else if ([dayDate compare:[NSDate date]] == NSOrderedDescending) {
return YZXDateLaterThanToday;
}else {
return YZXDateEarlierThanToday;
}
}
return NO;
}
複製程式碼
點選選擇事件:
- 日報(單選,非自定義)
移除預設選中
cell
(上次選中cell
),再新增新的選擇,並設定cell
樣式,最後呼叫_delegate
方法clickCalendarDate:
將選擇的日期
返回。
//移除已選中cell
[self.selectedArray removeAllObjects];
//記錄當前點選的按鈕
[self.selectedArray addObject:indexPath];
//設定點選的cell的樣式
[self p_changeTheSelectedCellStyleWithIndexPath:indexPath];
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarDate:)]) {
NSString *dateString = [NSString stringWithFormat:@"%@%02d日",self.collectionViewData[indexPath.section].headerTitle,indexPath.item - (self.collectionViewData[indexPath.section].firstDayOfTheMonth - 2)];
[_delegate clickCalendarDate:dateString];
}
複製程式碼
- 自定義選擇(多選)
- 根據
self.selectedArray
的count
判斷是選擇第幾個時間。- 當
self.selectedArray.count == 0
,表示選擇的第一個日期,改變選中cell
樣式,並將cell.indexPath
新增到self.selectedArray
中。最後呼叫delegate
返回資料。 - 當
self.selectedArray.count == 1
,表示選擇的第二個日期,通過self.selectedArray
中的indexPath.secton
和indexPath.item
判斷第二次選擇和第一次選擇是否相同,如果相同改變cell
為未選中
樣式,移除self.selectedArray
中的資料,並呼叫delegate
告知父檢視
取消選擇,最後return
。如果不相同,將兩次的選擇轉換為日期
,通過NSCalendar
的components:fromDate:toDate:options:
計算兩個日期
相差多少天,如果設定了maxChooseNumber
最大選擇範圍,當超過
範圍直接return
,如果未設定
或者未超過
,則將點選的NSIndexPath
加入self.selectedArray
,對陣列進行一個排序,然後重新轉換為日期
,通過delegate
回傳資料。 - 當
self.selectedArray.count == 2
,表示重新選擇,移除self.selectedArray
中所有的內容,新增此次點選內容,reloadData
更新檢視,呼叫delegate
回撥資料。
- 當
- 根據
switch (self.selectedArray.count) {
case 0://選擇第一個時間
{
//設定點選的cell的樣式
[self p_changeTheSelectedCellStyleWithIndexPath:indexPath];
//記錄當前點選的cell
[self.selectedArray addObject:[NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section]];
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
NSString *startString = [NSString stringWithFormat:@"%@%02ld日",self.collectionViewData[indexPath.section].headerTitle,indexPath.item - (self.collectionViewData[indexPath.section].firstDayOfTheMonth - 2)];
[_delegate clickCalendarWithStartDate:startString andEndDate:nil];
}
}
break;
case 1://選擇第二個時間
{
//如果第二次的選擇和第一次的選擇一樣,則表示取消選擇
if (self.selectedArray.firstObject.section == indexPath.section && self.selectedArray.firstObject.item == indexPath.item) {
[self p_recoveryIsNotSelectedWithIndexPath:self.selectedArray.firstObject];
[self.selectedArray removeAllObjects];
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
[_delegate clickCalendarWithStartDate:nil andEndDate:nil];
}
return;
}
NSString *startDate = [NSString stringWithFormat:@"%@%02d日",self.collectionViewData[self.selectedArray.firstObject.section].headerTitle,self.selectedArray.firstObject.item - (self.collectionViewData[self.selectedArray.firstObject.section].firstDayOfTheMonth - 2)];
NSString *endDate = [NSString stringWithFormat:@"%@%02d日",self.collectionViewData[indexPath.section].headerTitle,indexPath.item - (self.collectionViewData[indexPath.section].firstDayOfTheMonth - 2)];
YZXCalendarHelper *helper = [YZXCalendarHelper helper];
NSDateComponents *components = [helper.calendar components:NSCalendarUnitDay fromDate:[helper.yearMonthAndDayFormatter dateFromString:startDate] toDate:[helper.yearMonthAndDayFormatter dateFromString:endDate] options:0];
//當設定了maxChooseNumber時判斷選擇的時間段是否超出範圍
if (self.maxChooseNumber) {
if (labs(components.day) > self.maxChooseNumber - 1) {
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
[_delegate clickCalendarWithStartDate:startDate andEndDate:@"error"];
}
return;
}
}
//記錄當前點選的cell
[self.selectedArray addObject:[NSIndexPath indexPathForRow:indexPath.item inSection:indexPath.section]];
//對selectedArray進行排序,小的在前,大的在後
[self p_sortingTheSelectedArray];
//排序之後重新確定開始和結束時間
startDate = [NSString stringWithFormat:@"%@%02ld日",self.collectionViewData[self.selectedArray.firstObject.section].headerTitle,self.selectedArray.firstObject.item - (self.collectionViewData[self.selectedArray.firstObject.section].firstDayOfTheMonth - 2)];
endDate = [NSString stringWithFormat:@"%@%02ld日",self.collectionViewData[self.selectedArray.lastObject.section].headerTitle,self.selectedArray.lastObject.item - (self.collectionViewData[self.selectedArray.lastObject.section].firstDayOfTheMonth - 2)];
//時間選擇完畢,重新整理介面
[self.collectionView reloadData];
//代理返回資料
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
[_delegate clickCalendarWithStartDate:startDate andEndDate:endDate];
}
}
break;
case 2://重新選擇
{
//重新選擇時,將之前點選的cell恢復成為點選狀態,並移除陣列中所有物件
[self.selectedArray removeAllObjects];
//記錄當前點選的cell
[self.selectedArray addObject:indexPath];
[self.collectionView reloadData];
//
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
NSString *startString = [NSString stringWithFormat:@"%@%02ld日",self.collectionViewData[indexPath.section].headerTitle,indexPath.item - (self.collectionViewData[indexPath.section].firstDayOfTheMonth - 2)];
[_delegate clickCalendarWithStartDate:startString andEndDate:nil];
}
}
break;
default:
break;
}
複製程式碼
設定介面事件:
通過傳入的日期
,遍歷資料來源
,當headerTitle
和傳入日期相同時,獲取section
,再通過firstDayOfTheMonth
計算對應的item
,獲取到對應的NSIndexPath
,記錄其NSIndexPath
,reloadData
重新整理。
- (void)setStartDate:(NSString *)startDate
{
_startDate = startDate;
if (!_startDate) {
return;
}
//傳入一個時間時,查詢其indexPath資訊,用在collectionView上展現
[self.collectionViewData enumerateObjectsUsingBlock:^(YZXCalendarModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj.headerTitle isEqualToString:[_startDate substringWithRange:NSMakeRange(0, 8)]]) {
NSInteger day = [_startDate substringWithRange:NSMakeRange(8, 2)].integerValue;
[self.selectedArray addObject:[NSIndexPath indexPathForItem:(day + obj.firstDayOfTheMonth - 2) inSection:idx]];
[_collectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:(self.collectionViewData[idx].sectionRow * 7 - 1) inSection:idx] animated:YES scrollPosition:UICollectionViewScrollPositionBottom];
*stop = YES;
}
}];
[_collectionView reloadData];
}
- (void)setDateArray:(NSArray *)dateArray
{
_dateArray = dateArray;
if (!_dateArray) {
return;
}
//傳入兩個時間時,查詢其indexPath資訊,用在collectionView上展現
[self.collectionViewData enumerateObjectsUsingBlock:^(YZXCalendarModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj.headerTitle isEqualToString:[_dateArray.firstObject substringWithRange:NSMakeRange(0, 8)]]) {
NSInteger day = [_dateArray.firstObject substringWithRange:NSMakeRange(8, 2)].integerValue;
[self.selectedArray addObject:[NSIndexPath indexPathForItem:(day + obj.firstDayOfTheMonth - 2) inSection:idx]];
}
if ([obj.headerTitle isEqualToString:[_dateArray.lastObject substringWithRange:NSMakeRange(0, 8)]]) {
NSInteger day = [_dateArray.lastObject substringWithRange:NSMakeRange(8, 2)].integerValue;
[self.selectedArray addObject:[NSIndexPath indexPathForItem:(day + obj.firstDayOfTheMonth - 2) inSection:idx]];
[_collectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:(self.collectionViewData[idx].sectionRow * 7 - 1) inSection:idx] animated:YES scrollPosition:UICollectionViewScrollPositionBottom];
}
}];
[_collectionView reloadData];
}
複製程式碼
YZXCalendarView
將YZXWeekMenuView
和YZXDaysMenuView
組合在一起就組成了一個日曆控制元件(日期選擇)
,這裡就不多介紹了。
到這裡,日報
和自定義日期
的功能基本完成了。
月報
與年報
YZXMonthlyReportView(月報) YZXAnnualReportView(年報)
月報
的佈局這裡採用的是兩個UITableView
,一個展示年份
,一個展示月份
(年報
直接一個UITableView
就展示完成了)。對於月報
和年報
的實現對資料來源
的處理等和日報
就一樣了,在這裡就不囉嗦了,具體的可以去下載Demo看看。
最後
其實日曆控制元件
的樣式有很多方式,就看你想怎樣的了。但是內容的展示都逃不過NSCalendar
及其相關的API
了,只要瞭解了NSCalendar
,再動一下腦子,計算一下具體日期
就差不多了。Demo下載**(已適配iPhone X)**