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

  • EngineeringDocumentsProject:The header file.pch.category.The base fileAnd so on.
  • controller: the controller (YZXSelectDateViewController),Daily..Monthly report.The annual report.The customEtc.view, are added to the controllerviewOn.
  • Model: used toThe cacheandTo deal withThe data.
    • YZXDateModel: Records the year informationStart dateandEnd dateCalculate all the values between two datesyearandinThe array.
    • YZXMonthModel: Records month informationYZXDateModelIn the.
    • YZXCalendarModel:inSpecific information. (It should beYZXMonthModelMaybe I had a brain cramp…)
  • 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 calendartheThe start timeandThe end of time, some common onesNSDateFormatterAnd so on.
    • YZXWeekMenuView:Daily.In theUICollectionView-Sectionshowweek.
    • YZXDaysMenuView:Daily.To show specificThe date of.
    • YZXCalendarView:YZXWeekMenuViewandYZXDaysMenuViewComposed ofThe calendar.
    • YZXCalendarDelegate: After the selected dateCallback agent.
    • DateSelection:Monthly report.The annual reportAnd its corresponding othersview.
    • collectionView:Calendar controlThe main useUICollectionViewTo achieve the construction of the interface, so thefolderSome of themcell.headerAnd 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 timeandThe end of timeTo initialize the interface.
  • delegate: Date select end callback.
  • startDate:Daily.Record the date selected last time.
  • customSelect: Checks whether it isCustom calendarSelect (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.

  1. useNSCalendarInstance method ofcomponents:fromDate:toDate:options:To get aNSDateCompomentsInstance, according to the SettingscomponentsYou can get the correspondingYears of difference.On the difference.Day differenceAnd so on.
  2. Based on what you getDateComponents. The month ` ` for loop, the callNSCalendarInstance method ofdateByAddingComponents:toDate:options:Get the monthly valuedate.
  3. According to theNSCalendartherangeOfUnit:inUnit:forDateMethod to get the days of the monthnumberOfDaysInMonth(You get oneNSRange..lengthDays to obtain).
  4. According to theNSCalendarthecomponents:fromDateMethod to obtain an information aboutweekdaytheNSDateComponentsInstance, and pass againNSDateComponentsThe instanceweekdayMethod to get the month’sThe first day firstDayInMonthisWeek 1theHow many days(Current calendarEvery weektheThe first dayisSunday).
  5. throughnumberOfDaysInMonthandfirstDayInMonthTo calculatecollectionViewThe correspondinginHow many rows are neededitem(a lineA week).
  6. Cache the corresponding information tomodel, 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 selectioncell(Last selectedcell), add a new selection, and setcellStyle, and finally called_delegatemethodsclickCalendarDate:Will choose theThe date ofTo 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 theself.selectedArraythecountJudgment is to choose the number of times.
      1. whenself.selectedArray.count == 0, means selectedThe first date, change selectcellStyle, and willcell.indexPathAdded to theself.selectedArrayIn the. The last calldelegateReturn data.
      2. whenself.selectedArray.count == 1, means selectedSecond dateThrough theself.selectedArrayIn theindexPath.sectonandindexPath.itemDetermine if the second choice is the same as the first choice, ifThe samechangecellforuncheckStyle, removeself.selectedArrayData in and calldelegateinformParent viewDeselect, and finallyreturn. ifNot the same, converts the two choices toThe date ofThrough theNSCalendarthecomponents:fromDate:toDate:options:Calculated twoThe date ofHow many days, if setmaxChooseNumberMaximum selection range whenMore thanThe scope of directreturnIf theIs not setorNot more than, will clickNSIndexPathjoinself.selectedArray, sort the array, and then convert toThe date ofThrough thedelegateReturn data.
      3. whenself.selectedArray.count == 2Said,To chooseTo removeself.selectedArrayAdd the content of this click,reloadDataUpdate the view, calldelegateCallback data.
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 dateThe function is basically complete.

Monthly reportwithThe 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) **