做爲一個程序員
,當你開發的app
愈來愈多的時候,或者當你瀏覽一些app
的時候,你會發現不少模塊實現的功能是同樣的。而做爲開發者
而言,就更加註意這些功能同樣的東西了,由於你會發現這個項目中的某個模塊徹底可使用之前作項目時封裝的一些功能模塊,這樣你會無比的開心。而後去尋找之前封裝的東西,簡單的導入和引用就解決了一個功能模塊。git
日期選擇器
能夠說是一個常常用到的控件了,只是形式各不相同而已。因此爲了知足項目的需求我決定本身研究一下日曆控件
的實現方法。程序員
老規矩,先上圖github
工程目錄結構數組
EngineeringDocuments
:工程頭文件
,pch
,類目
,base文件
等。controller
:控制器(YZXSelectDateViewController),日報
,月報
,年報
,自定義
等視圖
,都是添加到該控制器的view
上。Model
:用於緩存
和處理
數據。
開始日期
和結束日期
計算兩日期之間全部的年份
和月份
數組。YZXDateModel
中。月份
的具體信息。(其實應該放在YZXMonthModel
中,可能當時腦子抽筋了...)Views
:各類view
,用於初始化完整的日曆控件
。
YZXCalendarHelper
:整個工程的manager
(應該放到EngineeringDocuments
目錄下的😅,Demo中已修改),能夠設置一些基本信息,如:日曆
的開始時間
和結束時間
,一些經常使用的NSDateFormatter
等。YZXWeekMenuView
:日報
中UICollectionView-Section
展現星期
。YZXDaysMenuView
:日報
中展現具體的日期
。YZXCalendarView
:YZXWeekMenuView
和YZXDaysMenuView
,組成完整的日曆
。YZXCalendarDelegate
:選擇日期後的回調``代理
。DateSelection
:月報
,年報
及其對應的其餘視圖
。collectionView
:日曆控件
主要用UICollectionView
來實現界面的搭建的,因此該文件夾
中都是一些cell
,header
等。下面將詳細介紹一下主要
文件
的做用緩存
YZXCalendarHelper(manager)app
YZXCalendarHelper
中主要提供了一下日曆控件
相關的設置,好比開始日期
和結束日期
,一些枚舉
,還有一些經常使用的NSDateFormatter
和日期
的比較方法等,方便設置日曆控件
,並減小重複代碼。具體的實現方法,將在使用到的時候介紹。佈局
日報
和自定義日期
YZXWeekMenuView字體
初始化一個NSDateFormatter
,使時區
和區域語言
和NSCalendar
相同,而後經過NSDateFormatter
的實例方法veryShortWeekdaySymbols
獲取到周符號(S,M,T,W...)
,而後遍歷佈局,將週末字體設置爲紅色。ui
- (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];
}];
複製代碼
YZXDaysMenuViewatom
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)**