美文网首页iOS开发资料收集区iOS开发iOS学习开发
iOS开发——做日历,看我就够了

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

作者: 那么你就是我的 | 来源:发表于2019-05-18 15:42 被阅读3次

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

    日历.gif

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

    一、使用方法:


    初始化及实现代理方法.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)日历中日期处理工具类;
    (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.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];
        
        // 回传日期
        [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;
        }
    
    }
    
    #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];
    }
    
    

    相关文章

      网友评论

        本文标题:iOS开发——做日历,看我就够了

        本文链接:https://www.haomeiwen.com/subject/shzzaqtx.html