做为一个程序员
,当你开发的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)**