iOS开发——做日历,看我就够了

大家可以先看一下最终的效果(gif图制作的不太好,更加详细的效果下载demo查看):

日历.gif

在这里,先阐述一下我封装这个日历demo的缘由吧:项目中之前用到的日历,是在网上随便down了一个demo,弄到项目中就用了,不仅逻辑复杂,而且代码风格也不太好,代码嵌套层次过多,把没必抽出来的抽出来,功能也不太完整,使用起来偶尔还会出现偏移不正确的bug,居然还用了2000+行代码。下个版本我的需求中又用到了日历,实在是难以忍受,遂决定自己重新封装一个完美的。我自己写的这个demo,我大概看了下,也就500行左右,思路非常清晰,你们如果在项目中使用的话,完全可以根据自己实际需求,根据我的代码思路以及里面的工具类写一个自己的日历封装类(回到今天的事件处理等功能都在代码里面,此文章没有展示,详情请查看源码)。查看demo源码及更加详细的注释,下载链接:https://github.com/fashion98/FSCalendarDemo,点颗小✨✨哦,在此先谢过了。有任何疑问可以下方评论或者直接简信我,如果没能及时回复,请加我QQ:870587568或者微信:18712941007。一起交流,一起进步!go~ go~ go~

一、使用方法:


初始化及实现代理方法.png

二、日历设计思路:
背景是一个大的scrollView,contentSize的width是屏幕的宽度*3,设置整页滑动,每一页上放置一个collectionView,代码中,起名依次为:collectionViewL、collectionViewM、collectionViewR,即:左侧、中间、右侧collectionView。collectionViewM就是我们当前看到的当月日历,collectionViewL为上月日历,collectionViewR为下月日历。初始化数据源,刷新三个collectionView。当左右滑动,在结束减速的时候,重新设置三个collectionView对应的数据源,然后直接改变scrollView的contentOffset为CGPointMake(self.bounds.size.width, offsetY),这样就能实现无限滑动日历了。

三、难点:
(1)上面无限滑动日历的整体思路;
(2)每个月有28、29、30、31天,如果选中31天,左右滑动后,那个月可能没有31天,造成显示不正常;
(3)上滑(单行显示)后,左右滑动需要展示那个月选中的一天,偏移量需要重新计算;
(4)FSCalendarDateModel中NSMutableArray<FSCalendarDayModel *> *monthModelList 值的处理。

四、介绍一下文件的意思吧:


文件类介绍.png

(1)用来初始化日历的类;
(2)日历绘制的类;
(3)日历头的类(日、一、二、三、四、五、六这一条view);
(4)FSCalendarScrollView类中collectionView的collectionViewCell;
(5)collectionView的数据源model(大model);
(6)collectionViewCell每个item对应的model(小model);
(7)日历中日期处理工具类:


日期处理工具类接口文件.png

(8)日历中,颜色、字体等宏定义头文件。

五、思路有了,我觉得最重要的就是设置数据源了,只要有数据源,设置数据绘制界面,和最简单的collectionView没什么两样。下面附上设置数据源的核心代码:

- (void)dealData {
    
    self.monthModelList = [NSMutableArray array];
    
    NSDateFormatter *dateFormatter = [NSDateFormatter new];
    dateFormatter.dateFormat = @"yyyy-MM-dd";
    
    // 当前月上月末尾的几天
    NSInteger previousMonthTotalDays = [self.date previousMonthDate].totalDaysInMonth;
    NSInteger year = self.month==1 ? self.year-1 : self.year;
    NSInteger month = self.month==1 ? 12 : self.month-1;
    
    for (NSInteger i = previousMonthTotalDays-self.firstWeekday+1; i < previousMonthTotalDays+1; i++) {
        NSDate *currentDate = [dateFormatter dateFromString:[NSString stringWithFormat:@"%ld-%ld-%ld", year, month, I]];
        FSCalendarDayModel *dayModel = [FSCalendarDayModel new];
        dayModel.solarDateString = [NSString stringWithFormat:@"%02ld", I];
        dayModel.lunarDateString = currentDate.lunarText;
        [self.monthModelList addObject:dayModel];
    }
    
    // 当前月所有
    for (NSInteger i = 1; i < self.date.totalDaysInMonth+1; i++) {
        NSDate *currentDate = [dateFormatter dateFromString:[NSString stringWithFormat:@"%ld-%ld-%ld", self.year, self.month, I]];
        FSCalendarDayModel *dayModel = [FSCalendarDayModel new];
        dayModel.solarDateString = [NSString stringWithFormat:@"%02ld", I];
        dayModel.lunarDateString = currentDate.lunarText;
        [self.monthModelList addObject:dayModel];
    }
    
    // 下月开始的几天
    NSInteger number = self.firstWeekday+self.totalDays;
    number = 42-number;//number > 35 ? 42-number : 35-number;
    NSInteger year1 = self.month==12 ? self.year+1 : self.year;
    NSInteger month1 = self.month==12 ? 1 : self.month+1;
    for (NSInteger i = 1; i < number+1; i++) {
        NSDate *currentDate = [dateFormatter dateFromString:[NSString stringWithFormat:@"%ld-%ld-%ld", year1, month1, I]];
        FSCalendarDayModel *dayModel = [FSCalendarDayModel new];
        dayModel.solarDateString = [NSString stringWithFormat:@"%02ld", I];
        dayModel.lunarDateString = currentDate.lunarText;
        [self.monthModelList addObject:dayModel];
    }

}

六、scrollView中核心代码:

@interface FSCalendarScrollView() <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>

@property (nonatomic, strong) UICollectionView *collectionViewL;// 左侧collectionView
@property (nonatomic, strong) UICollectionView *collectionViewM;// 中间collectionView
@property (nonatomic, strong) UICollectionView *collectionViewR;// 右侧collectionView

@property (nonatomic, strong) NSMutableArray<FSCalendarDateModel *> *monthArray;// 数据源array

@end

static NSString *const cellIdOfFSCalendarCell = @"FSCalendarCell";
@implementation FSCalendarScrollView

- (NSMutableArray *)monthArray {
    
    if (_monthArray == nil) {
        
        _monthArray = [NSMutableArray arrayWithCapacity:3];
        
        NSDate *previousMonthDate = [self.currentMonthDate previousMonthDate];// 上个月的今天
        NSDate *nextMonthDate = [self.currentMonthDate nextMonthDate];// 下个月的今天
        
        // 添加上月、当前月、下月 数据
        [_monthArray addObject:[[FSCalendarDateModel alloc] initWithDate:previousMonthDate]];
        [_monthArray addObject:[[FSCalendarDateModel alloc] initWithDate:self.currentMonthDate]];
        [_monthArray addObject:[[FSCalendarDateModel alloc] initWithDate:nextMonthDate]];
    }
    
    return _monthArray;
}

// pointsArray set方法
- (void)setPointsArray:(NSMutableArray<NSString *> *)pointsArray {
    
    FSCalendarDateModel *dateModel = self.monthArray[1];
    dateModel.pointsArray = [NSMutableArray arrayWithArray:pointsArray];
}

// pointsArray get方法
- (NSMutableArray<NSString *> *)pointsArray {
    
    FSCalendarDateModel *dateModel = self.monthArray[1];
    return dateModel.pointsArray;
}

- (instancetype)initWithFrame:(CGRect)frame {
    
    if ([super initWithFrame:frame]) {
        
        self.backgroundColor = Color_collectionView_Bg;
        self.showsHorizontalScrollIndicator = NO;
        self.showsVerticalScrollIndicator = NO;
        self.pagingEnabled = YES;
        self.bounces = NO;
        self.delegate = self;
        self.scrollsToTop = NO;
        
        self.contentSize = CGSizeMake(3 * self.bounds.size.width, self.bounds.size.height);
        [self setContentOffset:CGPointMake(self.bounds.size.width, 0.0) animated:NO];
        
        self.currentMonthDate = [NSDate date];
        self.currentDateNumber = [self.currentMonthDate dateDay];
        
        // 初始化三个collectionView
        [self setupCollectionViews];
        
        // 默认选中当前日期,回传当前日期
        [self passDate];
    }
    
    return self;
}

- (void)setupCollectionViews {
    
    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.itemSize = CGSizeMake(self.bounds.size.width / 7.0, self.bounds.size.height / 6.0);
    flowLayout.minimumLineSpacing = 0.0;
    flowLayout.minimumInteritemSpacing = 0.0;
    
    CGFloat selfWidth = self.bounds.size.width;
    CGFloat selfHeight = self.bounds.size.height;
    
    // 遍历创建3个collectionView
    for (int i = 0; i < self.monthArray.count; i++) {
        
        UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(i*selfWidth, 0.0, selfWidth, selfHeight) collectionViewLayout:flowLayout];
        collectionView.delegate = self;
        collectionView.dataSource = self;
        collectionView.bounces = NO;
        collectionView.backgroundColor = Color_collectionView_Bg;
        [collectionView registerClass:[FSCalendarCell class] forCellWithReuseIdentifier:cellIdOfFSCalendarCell];
        [self addSubview:collectionView];
        if (i == 0) {
            self.collectionViewL = collectionView;
        }else if (i == 1) {
            self.collectionViewM = collectionView;
        }else if (i == 2) {
            self.collectionViewR = collectionView;
        }
    }
}

#pragma mark ---- collectionView delegate ----
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {

    return 42; // 7 * 6
}

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    
    return 1;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    FSCalendarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdOfFSCalendarCell forIndexPath:indexPath];
    
    if (collectionView == self.collectionViewL) {
        
        [self layoutCollectionViewsDataWithCell:cell IndexPath:indexPath withDataIndex:0];
    }else if (collectionView == self.collectionViewM) {
        
        [self layoutCollectionViewsDataWithCell:cell IndexPath:indexPath withDataIndex:1];
    }else if (collectionView == self.collectionViewR) {
        
        [self layoutCollectionViewsDataWithCell:cell IndexPath:indexPath withDataIndex:2];
    }
    
    return cell;
}

// 布局各collectionView的数据及控件属性等
- (void)layoutCollectionViewsDataWithCell:(FSCalendarCell *)cell IndexPath:(NSIndexPath *)indexPath withDataIndex:(NSInteger)index {
    
    FSCalendarDateModel *monthInfo = self.monthArray[index];
    FSCalendarDayModel *dayModel = monthInfo.monthModelList[indexPath.row];
    NSInteger firstWeekday = monthInfo.firstWeekday;// 一个月的第一天是星期几
    NSInteger totalDays = monthInfo.totalDays;// 一个月的总天数
    
    // model赋值
    cell.dayModel = dayModel;
    
    if (indexPath.row < firstWeekday) {// 上月末尾的几天
        
        cell.solarDateLabel.textColor = Color_Text_PreviousOrNextMonth;
        cell.lunarDateLabel.textColor = Color_Text_PreviousOrNextMonth;
        cell.pointView.hidden = YES;
        
    }else if (indexPath.row >= firstWeekday && indexPath.row < firstWeekday + totalDays) {// 当前月所有日期
        
        if (index == 1) {
            
            // 假如当前选中了31日,左滑或右滑 那个月没有31日,则需要选中那个月的最后一天
            self.currentDateNumber = self.currentDateNumber > totalDays ? totalDays : self.currentDateNumber;

            if (self.currentDateNumber+firstWeekday-1 == indexPath.row) { //当前选中日期
                
                cell.solarDateLabel.textColor = Color_Text_CurrentMonth_Selected;
                cell.lunarDateLabel.textColor = Color_Text_CurrentMonth_Selected;
                cell.currentSelectView.backgroundColor = Color_currentSelectView_Bg_Selected;
            }else if ((monthInfo.month == [[NSDate date] dateMonth]) && (monthInfo.year == [[NSDate date] dateYear]) && (indexPath.row == [[NSDate date] dateDay] + firstWeekday - 1)) { //当前日期

                cell.currentSelectView.layer.borderColor = Color_currentSelectView_Border_CurrentDay.CGColor;
                cell.currentSelectView.layer.borderWidth = 1;
            }
            
            BOOL isHaving = NO;// pointsArray 中是否包含当前日期
            for (NSString *pointString in self.pointsArray) {
                
                NSDateFormatter *dateF = [[NSDateFormatter alloc] init];
                dateF.dateFormat = @"yyyy-MM-dd";
                NSDate *date = [dateF dateFromString:pointString];
                
                if (date.dateYear == monthInfo.year && date.dateMonth == monthInfo.month && date.dateDay == indexPath.row-firstWeekday+1) {
                    isHaving = YES;
                }
            }
            
            cell.pointView.hidden = !isHaving;
        }
    }else if (indexPath.row >= firstWeekday + totalDays) {// 下月开始的几天
        
        cell.solarDateLabel.textColor = Color_Text_PreviousOrNextMonth;
        cell.lunarDateLabel.textColor = Color_Text_PreviousOrNextMonth;
        cell.pointView.hidden = YES;
    }
}


- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    
    FSCalendarDateModel *monthInfo = self.monthArray[1];
    NSInteger firstWeekday = monthInfo.firstWeekday;
    
    // 记录当前选中的日期number
    self.currentDateNumber = [monthInfo.monthModelList[indexPath.row].solarDateString integerValue];
    
    if (indexPath.row < firstWeekday) {// 点击当前collectionView上月日期,需要移动到上月所在月
        
        [self pushToPreviousMonthOrNextMonthWithPageIndex:0];
    }else if (indexPath.row >= firstWeekday && indexPath.row < firstWeekday + monthInfo.totalDays) {
        
        // 回传日期并刷新界面
        [self passDate];
        [self.collectionViewM reloadData];
    }else if (indexPath.row >= firstWeekday + monthInfo.totalDays) {// 点击当前collectionView下月日期,需要移动到下月所在月
        
        [self pushToPreviousMonthOrNextMonthWithPageIndex:2];
    }
    
}

// 移动到上月或下月
- (void)pushToPreviousMonthOrNextMonthWithPageIndex:(NSInteger)pageIndex {
    
    [UIView animateWithDuration:0.5 animations:^{
        
        self.contentOffset = CGPointMake(self.bounds.size.width*pageIndex, 0.0);
    } completion:^(BOOL finished) {
        
        if (finished) {
            
            // 移动完成后,重新设置数据源
            [self scrollViewDidEndDecelerating:self];
        }
    }];
}

// 回到今天
- (void)refreshToCurrentDate {
    
    // 只需要置为nil,用到的时候就会自动重新初始化
    self.monthArray = nil;
    
    // 设置currentMonthDate 及 currentDateNumber
    self.currentMonthDate = [NSDate date];
    self.currentDateNumber = [self.currentMonthDate dateDay];
    
    // 刷新collectionViews
    [self reloadCollectionViews];

    // 回到今天,需要重新设置scrollView的偏移量
    [self setScrollViewContentOffset];
    
    // 回传日期
    [self passDate];
}

#pragma mark ---- scrollView delegate ----
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    
    if (scrollView.contentOffset.x < self.bounds.size.width) { // 向右滑动
        
        self.currentMonthDate = [self.currentMonthDate previousMonthDate];
        
        // 数组中最左边的月份现在作为中间的月份,中间的作为右边的月份,新的左边的需要重新获取
        FSCalendarDateModel *previousMonthInfo = [[FSCalendarDateModel alloc] initWithDate:[self.currentMonthDate previousMonthDate]];
        FSCalendarDateModel *currentMothInfo = self.monthArray[0];
        FSCalendarDateModel *nextMonthInfo = self.monthArray[1];

        [self.monthArray removeAllObjects];
        [self.monthArray addObject:previousMonthInfo];
        [self.monthArray addObject:currentMothInfo];
        [self.monthArray addObject:nextMonthInfo];
        
        [self setScrollViewContentOffset];
        [self reloadCollectionViews];
        [self passDate];
        
    }else if (scrollView.contentOffset.x > self.bounds.size.width) { // 向左滑动
        
        self.currentMonthDate = [self.currentMonthDate nextMonthDate];
        
        // 数组中最右边的月份现在作为中间的月份,中间的作为左边的月份,新的右边的需要重新获取
        FSCalendarDateModel *previousMonthInfo = self.monthArray[1];
        FSCalendarDateModel *currentMothInfo = self.monthArray[2];
        FSCalendarDateModel *nextMonthInfo = [[FSCalendarDateModel alloc] initWithDate:[self.currentMonthDate nextMonthDate]];
        
        [self.monthArray removeAllObjects];
        [self.monthArray addObject:previousMonthInfo];
        [self.monthArray addObject:currentMothInfo];
        [self.monthArray addObject:nextMonthInfo];
        
        [self setScrollViewContentOffset];
        [self reloadCollectionViews];
        [self passDate];
    }
    else {

        [self setScrollViewContentOffset];
        return;
    }

}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
    if (scrollView.contentOffset.x == kScreenWidth && scrollView.contentOffset.y == 0.00f && self.isShowSingle) {
        
        [self setScrollViewContentOffset];
    }
}

#pragma mark ---- 设置scrollView的偏移量 ----
- (void)setScrollViewContentOffset {
    
    CGFloat offsetY = 0;
    if (self.isShowSingle) {
        
        // 假如当前选中了31日,左滑或右滑 那个月没有31日,则需要选中那个月的最后一天
        self.currentDateNumber = self.currentDateNumber > self.currentMonthDate.totalDaysInMonth ? self.currentMonthDate.totalDaysInMonth : self.currentDateNumber;
        
        NSInteger index = [self.currentMonthDate firstWeekDayInMonth]+self.currentDateNumber;
        NSInteger rows = index%7 == 0 ? index/7-1 : index/7;
        offsetY = rows*(self.frame.size.height/6);
    }
    self.contentOffset = CGPointMake(self.bounds.size.width, offsetY);
}

#pragma mark ---- 回传所选日期 ----
- (void)passDate {
    
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.passDateBlock) {
            self.passDateBlock([self.currentMonthDate otherDayInMonth:self.currentDateNumber]);
        }
    });
}

#pragma mark ---- 刷新collectionViews ----
- (void)reloadCollectionViews {
    
    [_collectionViewM reloadData]; // 中间的 collectionView 先刷新数据
    [_collectionViewL reloadData]; // 最后两边的 collectionView 也刷新数据
    [_collectionViewR reloadData];
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • 之前跟辉哥有链接,谈到工作,谈到对象。 辉哥说的一个词特别触动我,未来可期。 来沪好多年,一开始有那么三到五年的规...
    范琳琳123阅读 190评论 1 1
  • 一、 Softmax函数与多元逻辑回归 为了之后更深入地讨论神经网络,本节将介绍在这个领域里很重要的soft...
    城市中迷途小书童阅读 625评论 0 0
  • 在云台山旅游的时候,看到人站在树底下,不停的捋树上的叶子。还未走近,便有一股浓郁的臭味扑面而来,我仔细一闻,觉得熟...
    微吟相呷阅读 612评论 0 0
  • 鹧鸪天 家在山东泗水边, 出游且向水山间。 闲来船棹行穿月, 倦罢云中卧看山。 春风绿,朔风残。 不妨随处...
    老顽童_ba08阅读 395评论 0 8