preface
As a programmer, as you develop more and more apps, or as you browse through some apps, you will find that many modules do the same thing. As a developer, pay more attention to the same things, because you will be very happy to find that one of the modules in the project can use some of the modules encapsulated in the previous project. Then look for things that were previously wrapped, and a simple import and reference solves a functional module.
Date pickers can be said to be a frequently used control, but in different forms. So in order to meet the needs of the project I decided to study the implementation of calendar controls.
implementation(See link at the end of the article for project code)
Same old rule. Picture first
Project directory structure
EngineeringDocuments
Project:The header file
.pch
.category
.The base file
And so on.controller
: the controller (YZXSelectDateViewController),Daily.
.Monthly report
.The annual report
.The custom
Etc.view
, are added to the controllerview
On.Model
: used toThe cache
andTo deal with
The data.- YZXDateModel: Records the year information
Start date
andEnd date
Calculate all the values between two datesyear
andin
The array. - YZXMonthModel: Records month information
YZXDateModel
In the. - YZXCalendarModel:
in
Specific information. (It should beYZXMonthModel
Maybe I had a brain cramp…)
- YZXDateModel: Records the year information
Views
: all kinds ofview
, used to initialize completeCalendar control
.YZXCalendarHelper
: of the whole projectmanager
(Should be placedEngineeringDocuments
😅 (modified in Demo), you can set some basic information, such as:The calendar
theThe start time
andThe end of time
, some common onesNSDateFormatter
And so on.YZXWeekMenuView
:Daily.
In theUICollectionView-Section
showweek
.YZXDaysMenuView
:Daily.
To show specificThe date of
.YZXCalendarView
:YZXWeekMenuView
andYZXDaysMenuView
Composed ofThe calendar
.YZXCalendarDelegate
: After the selected dateCallback agent
.DateSelection
:Monthly report
.The annual report
And its corresponding othersview
.collectionView
:Calendar control
The main useUICollectionView
To achieve the construction of the interface, so thefolder
Some of themcell
.header
And so on.
The purpose of the main file is described in detail below
Manager
YZXCalendarHelper (manager)
YZXCalendarHelper provides the following calendar control-related Settings, such as start and end dates, enumerations, and some common NSDateFormatter and date comparison methods. The specific implementation method will be introduced when used.
Daily.
andCustom date
YZXWeekMenuView
Initialize a NSDateFormatter, make the same time zone and regional language and NSCalendar, then through NSDateFormatter instance methods veryShortWeekdaySymbols get week symbols (S, M, T, W… , and then iterate through the layout and set the weekend font to red.
- (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];
}];
Copy the code
YZXDaysMenuView
YZXDaysMenuView.h
Frame frame@param startDateString Start time of the calendar (date format: YYYY MM month DD day) @param endDateString End time of the calendar (date format: YYYY MM month DD day) Yyyy Year MM month DD day) @return self */
- (instancetype)initWithFrame:(CGRect)frame
withStartDateString:(NSString *)startDateString
endDateString:(NSString *)endDateString;
// Click the callback proxy
@property (nonatomic.weak) id<YZXCalendarDelegate> delegate;
// Calendar option
@property (nonatomic.copy) NSString *startDate;
// Check whether it is a custom selection (select the date segment)
@property (nonatomic.assign) BOOL customSelect;
// Custom calendar (select two time ranges)
@property (nonatomic.copy) NSArray *dateArray;
// Customize calendar to control the maximum span of selectable dates
@property (nonatomic.assign) NSInteger maxChooseNumber;
Copy the code
initWithFrame:withStartDateString:endDateString:
: according to theThe start time
andThe end of time
To initialize the interface.delegate
: Date select end callback.startDate
:Daily.
Record the date selected last time.customSelect
: Checks whether it isCustom calendar
Select (select the date segment).dateArray
:Custom calendar
, records the last selected date segment.maxChooseNumber
:Custom calendar
, sets the maximum span of an optional date segment.
YZXDaysMenuView.m
Private properties section:
// The interface implemented by collectionView
@property (nonatomic.strong) UICollectionView *collectionView;
/ / collectionView data
@property (nonatomic.copy) NSArray <YZXCalendarModel *> *collectionViewData;
//manager
@property (nonatomic.strong) YZXCalendarHelper *calendarHelper;
/ / data
@property (nonatomic.strong) YZXCalendarModel *model;
// The cell used to record clicks
@property (nonatomic.strong) NSMutableArray <NSIndexPath *> *selectedArray;
Copy the code
Key code implementation part:
YZXCalendarModel Calculates all the years, months, days and other information between the date intervals using the incoming startDate and endDate.
- use
NSCalendar
Instance method ofcomponents:fromDate:toDate:options:
To get aNSDateCompoments
Instance, according to the Settingscomponents
You can get the correspondingYears of difference
.On the difference
.Day difference
And so on. - Based on what you get
DateComponents. The month ` ` for loop
, the callNSCalendar
Instance method ofdateByAddingComponents:toDate:options:
Get the monthly valuedate
. - According to the
NSCalendar
therangeOfUnit:inUnit:forDate
Method to get the days of the monthnumberOfDaysInMonth
(You get oneNSRange
..length
Days to obtain). - According to the
NSCalendar
thecomponents:fromDate
Method to obtain an information aboutweekday
theNSDateComponents
Instance, and pass againNSDateComponents
The instanceweekday
Method to get the month’sThe first day firstDayInMonth
isWeek 1
theHow many days
(Current calendarEvery week
theThe first day
isSunday
). - through
numberOfDaysInMonth
andfirstDayInMonth
To calculatecollectionView
The correspondingin
How many rows are neededitem
(a lineA week
). - Cache the corresponding information to
model
, and return oneThe model an array
.
- (NSArray<YZXCalendarModel *> *)achieveCalendarModelWithData:(NSDate *)startDate toDate:(NSDate *)endDate
{
NSMutableArray *modelArray = [NSMutableArray array];
NSDateFormatter *formatter = [YZXCalendarHelper helper].yearAndMonthFormatter;
// Determine how many months the given month is from the current month
NSDateComponents *components = [YZXCalendarHelper.helper.calendar components:NSCalendarUnitMonth fromDate:startDate toDate:endDate options:NSCalendarWrapComponents];
// Loop to get all month information from the given month up to the current month
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];
// Gets the number of days of the month represented by this section
NSRange daysOfMonth = [YZXCalendarHelper.helper.calendar rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:headerDate];
NSUInteger numberOfDaysInMonth = daysOfMonth.length;
// The first day of the month represented by this section is the first day of the first week.
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];
}
Copy the code
UI layout: So I’m going to go through the collectionView, and I’m going to set the section to be the month, the item to be the day, and the number of items to be the number of rows that I got in that month sectionRow*7, And you need to compare indexPath. Item with firstDayInMonth, so that the text on the item is set to the corresponding date, and determine the date today, so that the text is set to today, and the date beyond today is set to not optional.
// Set cell.day from the first day of each month
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;
}
/ / today
if ([YZXCalendarHelper.helper determineWhetherForTodayWithIndexPaht:indexPath model:model] == YZXDateEqualToToday) {
self.day.text = @ "today";
self.day.textColor = CustomRedColor;
}else if ([YZXCalendarHelper.helper determineWhetherForTodayWithIndexPaht:indexPath model:model] == YZXDateLaterThanToday) {// Determine if the date exceeds today
self.day.textColor = [UIColor grayColor];
self.userInteractionEnabled = NO;
}
Copy the code
Determine the date corresponding to item and the relationship between today: YZXCalendarHelper
- (YZXDateWithTodayType)determineWhetherForTodayWithIndexPaht:(NSIndexPath *)indexPath
model:(YZXCalendarModel *)model
{
/ / today
NSDateFormatter *formatter = self.yearMonthAndDayFormatter;
// Gets the number of days represented on the current cell
NSString *dayString = [NSString stringWithFormat:@ "% @ % ld day",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 {
returnYZXDateEarlierThanToday; }}return NO;
}
Copy the code
Click to select events:
- Daily (optional, not custom) Remove the default selection
cell
(Last selectedcell
), add a new selection, and setcell
Style, and finally called_delegate
methodsclickCalendarDate:
Will choose theThe date of
To return.
// Remove the selected cell
[self.selectedArray removeAllObjects];
// Record the currently clicked button
[self.selectedArray addObject:indexPath];
// Set the style of the clicked cell
[self p_changeTheSelectedCellStyleWithIndexPath:indexPath];
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarDate:)]) {
NSString *dateString = [NSString stringWithFormat:@ "% @ % 02 d day".self.collectionViewData[indexPath.section].headerTitle,indexPath.item - (self.collectionViewData[indexPath.section].firstDayOfTheMonth - 2)];
[_delegate clickCalendarDate:dateString];
}
Copy the code
- Custom selection (multiple selection)
- According to the
self.selectedArray
thecount
Judgment is to choose the number of times.- when
self.selectedArray.count == 0
, means selectedThe first date, change selectcell
Style, and willcell.indexPath
Added to theself.selectedArray
In the. The last calldelegate
Return data. - when
self.selectedArray.count == 1
, means selectedSecond dateThrough theself.selectedArray
In theindexPath.secton
andindexPath.item
Determine if the second choice is the same as the first choice, ifThe samechangecell
foruncheck
Style, removeself.selectedArray
Data in and calldelegate
informParent view
Deselect, and finallyreturn
. ifNot the same, converts the two choices toThe date of
Through theNSCalendar
thecomponents:fromDate:toDate:options:
Calculated twoThe date of
How many days, if setmaxChooseNumber
Maximum selection range whenMore than
The scope of directreturn
If theIs not set
orNot more than
, will clickNSIndexPath
joinself.selectedArray
, sort the array, and then convert toThe date of
Through thedelegate
Return data. - when
self.selectedArray.count == 2
Said,To chooseTo removeself.selectedArray
Add the content of this click,reloadData
Update the view, calldelegate
Callback data.
- when
- According to the
switch (self.selectedArray.count) {
case 0:// Select the first time
{
// Set the style of the clicked cell
[self p_changeTheSelectedCellStyleWithIndexPath:indexPath];
// Record the current cell click
[self.selectedArray addObject:[NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section]];
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
NSString *startString = [NSString stringWithFormat:@ "% @ % 02 ld day".self.collectionViewData[indexPath.section].headerTitle,indexPath.item - (self.collectionViewData[indexPath.section].firstDayOfTheMonth - 2)];
[_delegate clickCalendarWithStartDate:startString andEndDate:nil]; }}break;
case 1:// Select the second time
{
// If the second selection is the same as the first selection, the selection is cancelled
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:@ "% @ % 02 d day".self.collectionViewData[self.selectedArray.firstObject.section].headerTitle,self.selectedArray.firstObject.item - (self.collectionViewData[self.selectedArray.firstObject.section].firstDayOfTheMonth - 2)];
NSString *endDate = [NSString stringWithFormat:@ "% @ % 02 d day".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];
// Determine if the selected time range is out of range when maxChooseNumber is set
if (self.maxChooseNumber) {
if (labs(components.day) > self.maxChooseNumber - 1) {
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
[_delegate clickCalendarWithStartDate:startDate andEndDate:@"error"];
}
return; }}// Record the current cell click
[self.selectedArray addObject:[NSIndexPath indexPathForRow:indexPath.item inSection:indexPath.section]];
// Sort selectedArray, small first, big last
[self p_sortingTheSelectedArray];
// Re-determine the start and end times after sorting
startDate = [NSString stringWithFormat:@ "% @ % 02 ld day".self.collectionViewData[self.selectedArray.firstObject.section].headerTitle,self.selectedArray.firstObject.item - (self.collectionViewData[self.selectedArray.firstObject.section].firstDayOfTheMonth - 2)];
endDate = [NSString stringWithFormat:@ "% @ % 02 ld day".self.collectionViewData[self.selectedArray.lastObject.section].headerTitle,self.selectedArray.lastObject.item - (self.collectionViewData[self.selectedArray.lastObject.section].firstDayOfTheMonth - 2)];
// When the time is selected, refresh the interface
[self.collectionView reloadData];
// The proxy returns data
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) { [_delegate clickCalendarWithStartDate:startDate andEndDate:endDate]; }}break;
case 2:// Re-select
{
// When re-selecting, the previously clicked cell is restored to clicked state and all objects in the array are removed
[self.selectedArray removeAllObjects];
// Record the current cell click
[self.selectedArray addObject:indexPath];
[self.collectionView reloadData];
//
if (_delegate && [_delegate respondsToSelector:@selector(clickCalendarWithStartDate:andEndDate:)]) {
NSString *startString = [NSString stringWithFormat:@ "% @ % 02 ld day".self.collectionViewData[indexPath.section].headerTitle,indexPath.item - (self.collectionViewData[indexPath.section].firstDayOfTheMonth - 2)];
[_delegate clickCalendarWithStartDate:startString andEndDate:nil]; }}break;
default:
break;
}
Copy the code
Setting interface events:
If headerTitle is the same as the date passed in, get the section, and then calculate the corresponding item by firstDayOfTheMonth, get the corresponding NSIndexPath, record its NSIndexPath, ReloadData refresh.
- (void)setStartDate:(NSString *)startDate
{
_startDate = startDate;
if(! _startDate) {return;
}
// When a time is passed in, find its indexPath information and display it in the 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;
}
// When two times are passed, find the indexPath information and display it in the 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]; }Copy the code
YZXCalendarView
YZXWeekMenuView and YZXDaysMenuView are combined to form a calendar control (date selection) that I won’t cover here.
Here,Daily.
andCustom date
The function is basically complete.
Monthly report
withThe annual report
YZXMonthlyReportView
The layout of the monthly report uses two UITableViews, one for the year and one for the month (the annual report is displayed directly in a UITableView). For the implementation of monthly reports and annual reports on the processing of data sources and daily is the same, here is not wordy, specific can go to download Demo.
The last
In fact, the calendar control style has many ways, depending on how you want to. However, NSCalendar and its associated apis are no exception to the content presentation, as long as you understand NSCalendar, a little brain, calculate the specific date is almost. Demo download ** (already available for iPhone X) **