iOS 自定義日曆(日期選擇)控件

前言

做爲一個程序員,當你開發的app愈來愈多的時候,或者當你瀏覽一些app的時候,你會發現不少模塊實現的功能是同樣的。而做爲開發者而言,就更加註意這些功能同樣的東西了,由於你會發現這個項目中的某個模塊徹底可使用之前作項目時封裝的一些功能模塊,這樣你會無比的開心。而後去尋找之前封裝的東西,簡單的導入和引用就解決了一個功能模塊。git

日期選擇器能夠說是一個常常用到的控件了,只是形式各不相同而已。因此爲了知足項目的需求我決定本身研究一下日曆控件的實現方法。程序員

實現 (工程代碼見文末連接)

老規矩,先上圖github

日曆控件

工程目錄結構數組

日曆控件-目錄結構

  • EngineeringDocuments:工程頭文件pch類目base文件等。
  • controller:控制器(YZXSelectDateViewController),日報月報年報自定義視圖,都是添加到該控制器的view上。
  • Model:用於緩存處理數據。
    • YZXDateModel:記錄年份信息,經過設置的開始日期結束日期計算兩日期之間全部的年份月份數組。
    • YZXMonthModel:記錄月份信息,主要用於YZXDateModel中。
    • YZXCalendarModel:記錄月份的具體信息。(其實應該放在YZXMonthModel中,可能當時腦子抽筋了...)
  • Views:各類view,用於初始化完整的日曆控件
    • YZXCalendarHelper:整個工程的manager(應該放到EngineeringDocuments目錄下的😅,Demo中已修改),能夠設置一些基本信息,如:日曆開始時間結束時間,一些經常使用的NSDateFormatter等。
    • YZXWeekMenuView:日報UICollectionView-Section展現星期
    • YZXDaysMenuView:日報中展現具體的日期
    • YZXCalendarView:YZXWeekMenuViewYZXDaysMenuView,組成完整的日曆
    • YZXCalendarDelegate:選擇日期後的回調``代理
    • DateSelection:月報年報及其對應的其餘視圖
    • collectionView:日曆控件主要用UICollectionView來實現界面的搭建的,因此該文件夾中都是一些cellheader等。

下面將詳細介紹一下主要文件的做用緩存

Manager

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 經過傳入的startDateendDate,計算日期間隔之間全部的年份月份天數等信息。

  1. 使用NSCalendar的實例方法components:fromDate:toDate:options:獲得一個NSDateCompoments實例,根據設置的components能夠獲取到對應的年差值月差值日差值等。
  2. 根據獲取到的dateComponents.month``for循環,調用NSCalendar的實例方法dateByAddingComponents:toDate:options:獲取每月的date
  3. 根據NSCalendarrangeOfUnit:inUnit:forDate方法,獲得該月的天數numberOfDaysInMonth(獲得的是一個NSRange.length獲取天數)。
  4. 根據NSCalendarcomponents:fromDate方法,獲取到一個關於weekdayNSDateComponents實例,再經過NSDateComponents實例的weekday方法獲得該月的第一天firstDayInMonth第一個星期第幾天(當前日曆的每一個星期第一天星期日)。
  5. 經過numberOfDaysInMonthfirstDayInMonth計算collectionView對應的月份須要多少行item(一行是一個星期)。
  6. 將對應信息緩存到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.itemfirstDayInMonth,從而將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.selectedArraycount判斷是選擇第幾個時間。
      1. self.selectedArray.count == 0,表示選擇的第一個日期,改變選中cell樣式,並將cell.indexPath添加到self.selectedArray中。最後調用delegate返回數據。
      2. self.selectedArray.count == 1,表示選擇的第二個日期,經過self.selectedArray中的indexPath.sectonindexPath.item判斷第二次選擇和第一次選擇是否相同,若是相同改變cell未選中樣式,移除self.selectedArray中的數據,並調用delegate告知父視圖取消選擇,最後return。若是不相同,將兩次的選擇轉換爲日期,經過NSCalendarcomponents:fromDate:toDate:options:計算兩個日期相差多少天,若是設置了maxChooseNumber最大選擇範圍,當超過範圍直接return,若是未設置或者未超過,則將點擊的NSIndexPath加入self.selectedArray,對數組進行一個排序,而後從新轉換爲日期,經過delegate回傳數據。
      3. 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,記錄其NSIndexPathreloadData刷新。

- (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

YZXWeekMenuViewYZXDaysMenuView組合在一塊兒就組成了一個日曆控件(日期選擇),這裏就很少介紹了。

到這裏,日報自定義日期的功能基本完成了。

月報年報

YZXMonthlyReportView(月報) YZXAnnualReportView(年報)

月報的佈局這裏採用的是兩個UITableView,一個展現年份,一個展現月份年報直接一個UITableView就展現完成了)。對於月報年報的實現對數據源的處理等和日報就同樣了,在這裏就不囉嗦了,具體的能夠去下載Demo看看。

最後

其實日曆控件的樣式有不少方式,就看你想怎樣的了。可是內容的展現都逃不過NSCalendar及其相關的API了,只要瞭解了NSCalendar,再動一下腦子,計算一下具體日期就差很少了。Demo下載**(已適配iPhone X)**

相關文章
相關標籤/搜索