iOS開發(fā)——做日歷,看我就夠了

大家可以先看一下最終的效果(gif圖制作的不太好,更加詳細(xì)的效果下載demo查看):

日歷.gif

在這里,先闡述一下我封裝這個(gè)日歷demo的緣由吧:項(xiàng)目中之前用到的日歷,是在網(wǎng)上隨便down了一個(gè)demo,弄到項(xiàng)目中就用了,不僅邏輯復(fù)雜,而且代碼風(fēng)格也不太好,代碼嵌套層次過多,把沒必抽出來的抽出來,功能也不太完整,使用起來偶爾還會出現(xiàn)偏移不正確的bug,居然還用了2000+行代碼。下個(gè)版本我的需求中又用到了日歷,實(shí)在是難以忍受,遂決定自己重新封裝一個(gè)完美的。我自己寫的這個(gè)demo,我大概看了下,也就500行左右,思路非常清晰,你們?nèi)绻陧?xiàng)目中使用的話,完全可以根據(jù)自己實(shí)際需求,根據(jù)我的代碼思路以及里面的工具類寫一個(gè)自己的日歷封裝類(回到今天的事件處理等功能都在代碼里面,此文章沒有展示,詳情請查看源碼)。查看demo源碼及更加詳細(xì)的注釋,下載鏈接:https://github.com/fashion98/FSCalendarDemo,點(diǎn)顆小??哦,在此先謝過了。有任何疑問可以下方評論或者直接簡信我,如果沒能及時(shí)回復(fù),請加我QQ:870587568或者微信:18712941007。一起交流,一起進(jìn)步!go~ go~ go~

一、使用方法:


初始化及實(shí)現(xiàn)代理方法.png

二、日歷設(shè)計(jì)思路:
背景是一個(gè)大的scrollView,contentSize的width是屏幕的寬度*3,設(shè)置整頁滑動,每一頁上放置一個(gè)collectionView,代碼中,起名依次為:collectionViewL、collectionViewM、collectionViewR,即:左側(cè)、中間、右側(cè)collectionView。collectionViewM就是我們當(dāng)前看到的當(dāng)月日歷,collectionViewL為上月日歷,collectionViewR為下月日歷。初始化數(shù)據(jù)源,刷新三個(gè)collectionView。當(dāng)左右滑動,在結(jié)束減速的時(shí)候,重新設(shè)置三個(gè)collectionView對應(yīng)的數(shù)據(jù)源,然后直接改變scrollView的contentOffset為CGPointMake(self.bounds.size.width, offsetY),這樣就能實(shí)現(xiàn)無限滑動日歷了。

三、難點(diǎn):
(1)上面無限滑動日歷的整體思路;
(2)每個(gè)月有28、29、30、31天,如果選中31天,左右滑動后,那個(gè)月可能沒有31天,造成顯示不正常;
(3)上滑(單行顯示)后,左右滑動需要展示那個(gè)月選中的一天,偏移量需要重新計(jì)算;
(4)FSCalendarDateModel中NSMutableArray<FSCalendarDayModel *> *monthModelList 值的處理。

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


文件類介紹.png

(1)用來初始化日歷的類;
(2)日歷繪制的類;
(3)日歷頭的類(日、一、二、三、四、五、六這一條view);
(4)FSCalendarScrollView類中collectionView的collectionViewCell;
(5)collectionView的數(shù)據(jù)源model(大model);
(6)collectionViewCell每個(gè)item對應(yīng)的model(小model);
(7)日歷中日期處理工具類:


日期處理工具類接口文件.png

(8)日歷中,顏色、字體等宏定義頭文件。

五、思路有了,我覺得最重要的就是設(shè)置數(shù)據(jù)源了,只要有數(shù)據(jù)源,設(shè)置數(shù)據(jù)繪制界面,和最簡單的collectionView沒什么兩樣。下面附上設(shè)置數(shù)據(jù)源的核心代碼:

- (void)dealData {
    
    self.monthModelList = [NSMutableArray array];
    
    NSDateFormatter *dateFormatter = [NSDateFormatter new];
    dateFormatter.dateFormat = @"yyyy-MM-dd";
    
    // 當(dāng)前月上月末尾的幾天
    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];
    }
    
    // 當(dāng)前月所有
    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;// 左側(cè)collectionView
@property (nonatomic, strong) UICollectionView *collectionViewM;// 中間collectionView
@property (nonatomic, strong) UICollectionView *collectionViewR;// 右側(cè)collectionView

@property (nonatomic, strong) NSMutableArray<FSCalendarDateModel *> *monthArray;// 數(shù)據(jù)源array

@end

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

- (NSMutableArray *)monthArray {
    
    if (_monthArray == nil) {
        
        _monthArray = [NSMutableArray arrayWithCapacity:3];
        
        NSDate *previousMonthDate = [self.currentMonthDate previousMonthDate];// 上個(gè)月的今天
        NSDate *nextMonthDate = [self.currentMonthDate nextMonthDate];// 下個(gè)月的今天
        
        // 添加上月、當(dāng)前月、下月 數(shù)據(jù)
        [_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];
        
        // 初始化三個(gè)collectionView
        [self setupCollectionViews];
        
        // 默認(rèn)選中當(dāng)前日期,回傳當(dāng)前日期
        [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;
    
    // 遍歷創(chuàng)建3個(gè)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的數(shù)據(jù)及控件屬性等
- (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;// 一個(gè)月的第一天是星期幾
    NSInteger totalDays = monthInfo.totalDays;// 一個(gè)月的總天數(shù)
    
    // 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) {// 當(dāng)前月所有日期
        
        if (index == 1) {
            
            // 假如當(dāng)前選中了31日,左滑或右滑 那個(gè)月沒有31日,則需要選中那個(gè)月的最后一天
            self.currentDateNumber = self.currentDateNumber > totalDays ? totalDays : self.currentDateNumber;

            if (self.currentDateNumber+firstWeekday-1 == indexPath.row) { //當(dāng)前選中日期
                
                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)) { //當(dāng)前日期

                cell.currentSelectView.layer.borderColor = Color_currentSelectView_Border_CurrentDay.CGColor;
                cell.currentSelectView.layer.borderWidth = 1;
            }
            
            BOOL isHaving = NO;// pointsArray 中是否包含當(dāng)前日期
            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;
    
    // 記錄當(dāng)前選中的日期number
    self.currentDateNumber = [monthInfo.monthModelList[indexPath.row].solarDateString integerValue];
    
    if (indexPath.row < firstWeekday) {// 點(diǎn)擊當(dāng)前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) {// 點(diǎn)擊當(dāng)前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) {
            
            // 移動完成后,重新設(shè)置數(shù)據(jù)源
            [self scrollViewDidEndDecelerating:self];
        }
    }];
}

// 回到今天
- (void)refreshToCurrentDate {
    
    // 只需要置為nil,用到的時(shí)候就會自動重新初始化
    self.monthArray = nil;
    
    // 設(shè)置currentMonthDate 及 currentDateNumber
    self.currentMonthDate = [NSDate date];
    self.currentDateNumber = [self.currentMonthDate dateDay];
    
    // 刷新collectionViews
    [self reloadCollectionViews];

    // 回到今天,需要重新設(shè)置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];
        
        // 數(shù)組中最左邊的月份現(xiàn)在作為中間的月份,中間的作為右邊的月份,新的左邊的需要重新獲取
        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];
        
        // 數(shù)組中最右邊的月份現(xiàn)在作為中間的月份,中間的作為左邊的月份,新的右邊的需要重新獲取
        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 ---- 設(shè)置scrollView的偏移量 ----
- (void)setScrollViewContentOffset {
    
    CGFloat offsetY = 0;
    if (self.isShowSingle) {
        
        // 假如當(dāng)前選中了31日,左滑或右滑 那個(gè)月沒有31日,則需要選中那個(gè)月的最后一天
        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 先刷新數(shù)據(jù)
    [_collectionViewL reloadData]; // 最后兩邊的 collectionView 也刷新數(shù)據(jù)
    [_collectionViewR reloadData];
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 之前跟輝哥有鏈接,談到工作,談到對象。 輝哥說的一個(gè)詞特別觸動我,未來可期。 來滬好多年,一開始有那么三到五年的規(guī)...
    范琳琳123閱讀 251評論 1 1
  • 一、 Softmax函數(shù)與多元邏輯回歸 為了之后更深入地討論神經(jīng)網(wǎng)絡(luò),本節(jié)將介紹在這個(gè)領(lǐng)域里很重要的soft...
    城市中迷途小書童閱讀 718評論 0 0
  • 在云臺山旅游的時(shí)候,看到人站在樹底下,不停的捋樹上的葉子。還未走近,便有一股濃郁的臭味撲面而來,我仔細(xì)一聞,覺得熟...
    微吟相呷閱讀 700評論 0 0
  • 鷓鴣天 家在山東泗水邊, 出游且向水山間。 閑來船棹行穿月, 倦罷云中臥看山。 春風(fēng)綠,朔風(fēng)殘。 不妨隨處...
    老頑童_ba08閱讀 456評論 0 8

友情鏈接更多精彩內(nèi)容