iOS高可控性日历基础组件-SKCalendarView的使用和实现思路的分享

简述

SKCalendarView是一个高可控性的日历基础组件,为了提高应用的自由度,默认只提供了日历部分的视图封装,但不涵盖切换月份按钮、年月分显示等非关键性控件,但请不要担心,SKCalendarView为你提供了多样性的API,你可以很轻松的拿到这些信息去展示在你自己的自定义控件中,以及对当前UI的修改:如:替换日历主题图片、节假日或特殊日期的日历背景、各种现实颜色等等。不仅如此,SKCalendarView还为你封装了公历农历节假日以及中国24节气的核心算法,即使你觉得默认的视图并不合胃口,也可以直接快速的利用这套算法创造出一个全新的日历控件。最后,SKCalendarView还提供了一些简单的切换动画,如果你不喜欢它,可以忽略掉,用自己的,这里完全不会受到任何限制。如果觉得还不错,点个star吧~

效果图

最新更新:

  • 发布了0.0.3版本

  • 对日历算法部分进行优化,完全解决线程卡顿问题

  • 修复了指定日期查寻在显示上的若干问题

一.如何使用

1.如何开始

1.从GitHub上Clone-->SKCalendarView, 然后查看Demo (由于使用cocoaPods管理,请打开xcworkspace工程进行查看)

2.在项目中使用SKCalendarView,直接将目录下的SKCalendarView文件夹拷贝到工程中,或在podfile文件中添加pod 'SKCalendarView'

3.SKCalendarView的默认视图基于Masonry布局,如果需要使用, 请确保你的工程里已存在Masonry,下载地址

4.如果遇到其它问题,欢迎提交issues,我会及时回复

2.使用方法

头文件导入

#import "SKConstant.h"

继承SKCalendarView

@property (nonatomic, strong) SKCalendarView * calendarView;

日历设置

_calendarView.calendarTodayTitleColor = [UIColor redColor];// 今天标题字体颜色
_calendarView.calendarTodayTitle = @"今日";// 今天下标题
_calendarView.dateColor = [UIColor orangeColor];// 今天日期数字背景颜色
_calendarView.calendarTodayColor = [UIColor whiteColor];// 今天日期字体颜色
_calendarView.dayoffInWeekColor = [UIColor redColor];
_calendarView.springColor = [UIColor colorWithRed:48 / 255.0 green:200 / 255.0 blue:104 / 255.0 alpha:1];// 春季节气颜色
_calendarView.summerColor = [UIColor colorWithRed:18 / 255.0 green:96 / 255.0 blue:0 alpha:8];// 夏季节气颜色
_calendarView.autumnColor = [UIColor colorWithRed:232 / 255.0 green:195 / 255.0 blue:0 / 255.0 alpha:1];// 秋季节气颜色
_calendarView.winterColor = [UIColor colorWithRed:77 / 255.0 green:161 / 255.0 blue:255 / 255.0 alpha:1];// 冬季节气颜色
_calendarView.holidayColor = [UIColor redColor];//节日字体颜色
self.lastMonth = _calendarView.lastMonth;// 获取上个月的月份
self.nextMonth = _calendarView.nextMonth;// 获取下个月的月份

翻页动画

[SKCalendarAnimationManage animationWithView:self.calendarView andEffect:SK_ANIMATION_REVEAL isNext:YES];

获取农历年

self.chineseYearLabel.text = [NSString stringWithFormat:@"%@年", self.calendarView.chineseYear];// 农历年

获取农历月日

self.chineseMonthAndDayLabel.text = [NSString stringWithFormat:@"%@%@", self.calendarView.chineseMonth, getNoneNil(self.calendarView.chineseCalendarDay[row])];

获取公历年/月

self.yearLabel.text = [NSString stringWithFormat:@"%@年%@月", @(self.calendarView.year), @(self.calendarView.month)];// 公历年

获取节日/节气

self.holidayLabel.text = [self.calendarView getHolidayAndSolarTermsWithChineseDay:getNoneNil(self.calendarView.chineseCalendarDay[row])];

查询指定日期

[self.calendarView checkCalendarWithAppointDate:[NSDate date]];

日历UI配置

@property (nonatomic, strong) UIColor * weekBackgroundColor;// 周的背景颜色
@property (nonatomic, strong) UIColor * normalInWeekColor;// 周(除双休日外)字体颜色
@property (nonatomic, strong) UIColor * dayoffInWeekColor;// 双休日字体颜色
@property (nonatomic, strong) UIColor * calendarTodayColor;// 本日日期字体颜色
@property (nonatomic, strong) UIColor * dateColor;// 日期小背景颜色
@property (nonatomic, strong) UIImage * dateIcon;// 日期图片
@property (nonatomic, strong) UIColor * holidayBackgroundColor;// 节日背景颜色
@property (nonatomic, strong) UIColor * solarTeromBackgroundColor;// 节气背景颜色
@property (nonatomic, strong) UIColor * dateBackgroundColor;// 日期背景颜色(非节日&节气)
@property (nonatomic, strong) UIImage * dateBackgroundIcon;// 日期背景图片
@property (nonatomic, strong) NSString * calendarTodayTitle;// 本日日期标题
@property (nonatomic, strong) UIColor * calendarTodayTitleColor;// 本日日期标题字体颜色
@property (nonatomic, strong) UIColor * calendarTitleColor;// 日期标题字体颜色
@property (nonatomic, strong) UIColor * holidayColor;// 节日标题字体颜色
@property (nonatomic, strong) UIColor * springColor;// 春季节气颜色
@property (nonatomic, strong) UIColor * summerColor;// 夏季节气颜色
@property (nonatomic, strong) UIColor * autumnColor;// 秋季节气颜色
@property (nonatomic, strong) UIColor * winterColor;// 冬季节气颜色
@property (nonatomic, assign) BOOL enableClickEffect;// 开启点击效果
@property (nonatomic, assign) BOOL enableDateRoundCorner;// 开启日期圆角

获取点击到的日期

注意:这里需要先遵循<SKCalendarViewDelegate>代理协议

- (void)selectDateWithRow:(NSUInteger)row
```

# 二.如何实现


## 1.设计思路
- 总体上SKCalendarView仍然才去模块化思路,主要分为三个部分`View`(视图)、`Animation`(动画)以及`Algorithm`(算法)

- `View`主要负责处理外部对UI的配置信息、日历核心部分的展示、UI的刷新、效果的处理和界面控件的创建和布局约束等

- `Animation`主要负责日历翻页时的动画效果及点击日期的动画效果的处理

- `Algorithm`是整个`SKCalendarView`最核心的部分,负责了`公历`、`农历`、`节假日`以及`中国24节气`的核心算法,以及对日期查询的处理反馈

## 2.功能实现


### 2.1布局

#### 思路
 我们先要搞清楚日历是什么。所谓日历,就是一年当中12个月份的日期展示,每个月当中的日期数量由28~31天不等,这里指的是公历, 而农历当中每个月最多30天,虽然在计算方法上是有很大差别,但好在当代日历都是以公历为展示基准,所以只需要考虑公历的每月天数。

 因为要考虑到展示上的美观性,一般都是采用正方形来展示,由于一周是固定的7天,所以我们日历的横向子控件数量也必须为7。但是这样问题就来了,由于需要考虑到与日期上方的周时间相对应,并且除了2月没有哪个月是的天数的7的倍数,也就做不到整除而导致无法形成正方形布局,所以我们不能直接用和月份天数相等的子控件数量来展示我们的日历,经过思考,我决定采取填充数据的方式来达到正方形展示的目的:
 - 首先规划整体子控件数量,由于横向固定是7,那么纵向就由最多的一个月31天算,31 / 7 ≈ 4.4, 既然超过了4行,那么我们就放5行吧: 5 x 7 = 35,子控件放置35个如何?但经过尝试后,发现这并不可取:因为我们这里理想状态下的31天是以这个月的第一天恰好是周日 (周日为公历一周的开始) 为前提条件的,那么显然在现实生活里并不可能每个月都恰好第一天都是周日,所以,我们就需要考虑到`需要显示的这个月的第一天是周几`这个问题,众所周知,一周有7天的时间,那么每个月的第一天就有7种可能。做最多的打算,假设这个月总共有31天,而第一天恰好是周六,那么在这个月的1日这一天之前就有6天是没有日期的,结合我们之前计算的数量加上周六前的6天: 35 + 6 = 41,子控件放41个又如何呢?当然是不行了,因为需要正方形的日历,所以至少要成为7的倍数,最接近这个倍数的值就是我们要的答案:42.

#### 实现
- 在基础控件的布局上,我们采取最简便的方式:周和日期我们分别使用了`weekCollectionView`、`calendarCollectionView`这两个`UICollectionView`来完成

- 而月份的背景数字`monthBackgroundLabel`作为最上面一层采用的是`UILabel`,在设置了其`size`和`weight`后,效果就如同背景图一样
```objectivec
// 周
    UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc] init];
    layout.scrollDirection = UICollectionViewScrollDirectionVertical;
    self.weekCollectionView = [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:layout];
    [self addSubview:self.weekCollectionView];
    self.weekCollectionView.backgroundColor = [UIColor whiteColor];
    self.weekCollectionView.delegate = self;
    self.weekCollectionView.dataSource = self;
    [self.weekCollectionView registerClass:[SKWeekCollectionViewCell class] forCellWithReuseIdentifier:@"Week"];
    [self.weekCollectionView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self);
        make.left.equalTo(self);
        make.right.equalTo(self);
        
        make.height.mas_offset(self.frame.size.height / 7.5);
        make.height.mas_greaterThanOrEqualTo(40).priorityHigh();
    }];

    // 日期
    UICollectionViewFlowLayout * dateLayout = [[UICollectionViewFlowLayout alloc] init];
    dateLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
    self.calendarCollectionView = [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:dateLayout];
    [self addSubview:self.calendarCollectionView];
    self.calendarCollectionView.backgroundColor = [UIColor whiteColor];
    self.calendarCollectionView.delegate = self;
    self.calendarCollectionView.dataSource = self;
    [self.calendarCollectionView registerClass:[SKCalendarCollectionViewCell class] forCellWithReuseIdentifier:@"Calendar"];
    [self.calendarCollectionView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.weekCollectionView.mas_bottom);
        make.left.equalTo(self);
        make.right.equalTo(self);
        make.bottom.equalTo(self);
    }];
    
    // 背景月份
    self.monthBackgroundLabel = [UILabel new];
    [self addSubview:self.monthBackgroundLabel];
    self.monthBackgroundLabel.textColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:200 / 2550.f];
    self.monthBackgroundLabel.font = [UIFont systemFontOfSize:150.0f weight:120.f];
    self.monthBackgroundLabel.textAlignment = NSTextAlignmentCenter;
    [self.monthBackgroundLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self).with.insets(UIEdgeInsetsMake(0, 0, 0, 0));
    }];
```

#### 对日历高度的控制
- 由于不同的月份的第一天所处的周时间不同,导致日历的`有效日期` (有日期显示的) 行数不固定,如:当本月第一天为周日时,最多只占35个子控件位数,而我们一开始设置的子控件数量值是42,这样一来就会空出一行的空白出来,这是很不美观的。所以日历的高度对于我们来说就是一个把控的值,如何来保证可以根据每个月的天数来控制日历的高度呢,在`SKCalendarView`中采取了以下的办法:
```objectivec
if (self.calendarManage.isIncreaseHeight == YES) {// 根据isIncreaseHeight来判断是否需要更改高度
            [self.calendarCollectionView mas_updateConstraints:^(MASConstraintMaker *make) {
                make.height.mas_offset(6 * (self.frame.size.height / 7.5));
            }];
            return 42;
            
        } else {
            if (self.calendarCollectionView.frame.size.height > 218) {
                [self.calendarCollectionView mas_updateConstraints:^(MASConstraintMaker *make) {
                    make.height.mas_offset(5 * (self.frame.size.height / 7.5));
                }];
            }
            return 35;
        }
```

#### 日期点击效果的处理
- 在`SKCalendarCollectionViewCell`的内部,我们将`enableClickEffect`(是否开启点击效果)为YES的状态设为开启效果,并调用动画管理类`SKCalendarAnimationManage`的方法
```objectivec
[SKCalendarAnimationManage clickEffectAnimationForView:self.baseView];
```


### 2.2 日历算法

##### 这一部分算法是整个SKCalendarView最核心的部分

- `SKCalendarManage`以单例的模式封装了`SKCalendarView`全部的核心算法

- 主要难点在于对个别不定期节日,如复活节的日期的计算等,以及24节气和农历的计算,推荐阅读[《算法:计算中国农历》](http://blog.csdn.net/orbit/article/details/9337377)

- 查看所选日期所处的月份:

```objectivec
#pragma mark - 查看所选日期所处的月份
- (void)checkThisMonthRecordFromToday:(NSDate *)today
{
    if (isEmpty(today)) {// 如果没有日期,默认今天
        today = [NSDate date];
    }
    
    [self calculationThisMonthDays:today];// 计算本月天数
    [self calculationThisMonthFirstDayInWeek:today];// 计算本月第一天是周几
}
```

- 计算本月天数

```objectivec
#pragma mark - 计算本月天数
- (void)calculationThisMonthDays:(NSDate *)days
{
    NSCalendar * calendar = [NSCalendar currentCalendar];
    if (isEmpty(days)) {
        days = [NSDate date];
    }
    NSRange range = [calendar rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:days];
    self.days = range.length;// 保存天数
}
```


- 计算本月第一天是周几

```objectivec
#pragma mark - 计算本月第一天是周几
- (void)calculationThisMonthFirstDayInWeek:(NSDate *)date;
{
    if (isEmpty(date)) {
        date = [NSDate date];
    }
    NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSDateComponents * comps = [[NSDateComponents alloc] init];
    NSDateComponents * theComps = [[NSDateComponents alloc] init];
    NSInteger unitFlags = NSCalendarUnitDay | NSCalendarUnitWeekday | NSCalendarUnitMonth | NSCalendarUnitYear;
    comps = [calendar components:unitFlags fromDate:date];
    theComps = [calendar components:unitFlags fromDate:[NSDate date]];
    self.theMonth = [theComps month];// 本月的月份
    NSUInteger day = [comps day];// 是本月第几天
    self.todayInMonth = day;
    if (day > 1) {// 如果不是本月第一天
        // 将日期推算到本月第一天
        NSInteger hours = (day - 1) * -24;
        date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:date];
    }
    comps = [calendar components:unitFlags fromDate:date];
    self.dayInWeek = [comps weekday];// 是周几
    self.year = [comps year];// 公历年
    self.month = [comps month];// 公里月

    [self creatcalendarArrayWithDate:date];// 创建日历数组
}
```


- 创建日历数组(公历、农历)

这里的算法还有优化的必要,如果有朋友可以指点一二,不胜感激

```objectivec
#pragma mark - 创建日历数组
- (void)creatcalendarArrayWithDate:(NSDate *)date
{
    self.calendarDate = [NSMutableArray new];
    self.chineseCalendarDate = [NSMutableArray new];
    self.chineseCalendarDay = [NSMutableArray new];
    for (NSInteger j = 0; j < 42; j ++) {// 创建空占位数组
        [self.calendarDate addObject:@""];
        [self.chineseCalendarDate addObject:@""];
        [self.chineseCalendarDay addObject:@""];
    }
    // 向前推算日期到本月第一天
    NSDate * firstDay = date;
    self.todayInMonth = self.todayInMonth + self.dayInWeek - 2;// 计算在本月日历上所处的位置
    switch (self.dayInWeek) {// 根据本月第一天是周几,来确定之后的日期替换空占位
        case 1:// 周日
            for (NSInteger i = 1; i <= self.days; i ++) {
                [self.calendarDate replaceObjectAtIndex:i - 1 withObject:@(i)];// 替换公历日期
                for (NSInteger j = 1; j <= self.days; j ++) {// 公历日期
                    // 向后推算至本月末
                    NSInteger hours = (j - 1) * 24;
                    NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:firstDay];
                    NSString * chineseDay = [self calculationChinaCalendarWithDate:date dispalyHoliday:YES];
                    [self.chineseCalendarDate replaceObjectAtIndex:j - 1 withObject:chineseDay];// 替换农历日期
                    NSString * noHoliday = [self calculationChinaCalendarWithDate:date dispalyHoliday:NO];
                    [self.chineseCalendarDay replaceObjectAtIndex:j - 1 withObject:noHoliday];// 替换纯农历日期(无节假日)
                }
            }
            self.isIncreaseHeight = NO;
            break;
            
        case 2:// 周一
            for (NSInteger i = 1; i <= self.days + 1; i ++) {
                if (i >= 2) {
                    [self.calendarDate replaceObjectAtIndex:i - 1 withObject:@(i - 1)];
                    for (NSInteger j = 1; j <= self.days + 1; j ++) {
                        if (j >= 2) {
                            // 向后推算至本月末
                            NSInteger hours = (j - 2) * 24;
                            NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:firstDay];
                            NSString * chineseDay = [self calculationChinaCalendarWithDate:date dispalyHoliday:YES];
                            [self.chineseCalendarDate replaceObjectAtIndex:j - 1 withObject:chineseDay];// 替换农历日期
                            NSString * noHoliday = [self calculationChinaCalendarWithDate:date dispalyHoliday:NO];
                            [self.chineseCalendarDay replaceObjectAtIndex:j - 1 withObject:noHoliday];
                        }
                    }

                }
            }
            self.isIncreaseHeight = NO;
            break;
            
        case 3:// 周二
            for (NSInteger i = 1; i <= self.days + 2; i ++) {
                if (i >= 3) {
                    [self.calendarDate replaceObjectAtIndex:i - 1 withObject:@(i - 2)];
                    for (NSInteger j = 1; j <= self.days + 2; j ++) {
                        if (j >= 3) {
                            // 向后推算至本月末
                            NSInteger hours = (j - 3) * 24;
                            NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:firstDay];
                            NSString * chineseDay = [self calculationChinaCalendarWithDate:date dispalyHoliday:YES];
                            [self.chineseCalendarDate replaceObjectAtIndex:j - 1 withObject:chineseDay];// 替换农历日期
                            NSString * noHoliday = [self calculationChinaCalendarWithDate:date dispalyHoliday:NO];
                            [self.chineseCalendarDay replaceObjectAtIndex:j - 1 withObject:noHoliday];
                        }
                    }

                }
            }
            self.isIncreaseHeight = NO;
            break;
            
        case 4:// 周三
            for (NSInteger i = 1; i <= self.days + 3; i ++) {
                if (i >= 4) {
                    [self.calendarDate replaceObjectAtIndex:i - 1 withObject:@(i - 3)];
                    for (NSInteger j = 1; j <= self.days + 3; j ++) {
                        if (j >= 4) {
                            // 向后推算至本月末
                            NSInteger hours = (j - 4) * 24;
                            NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:firstDay];
                            NSString * chineseDay = [self calculationChinaCalendarWithDate:date dispalyHoliday:YES];
                            [self.chineseCalendarDate replaceObjectAtIndex:j - 1 withObject:chineseDay];// 替换农历日期
                            NSString * noHoliday = [self calculationChinaCalendarWithDate:date dispalyHoliday:NO];
                            [self.chineseCalendarDay replaceObjectAtIndex:j - 1 withObject:noHoliday];
                        }
                    }

                }
            }
            self.isIncreaseHeight = NO;
            break;
            
        case 5:// 周四
            for (NSInteger i = 1; i <= self.days + 4; i ++) {
                if (i >= 5) {
                    [self.calendarDate replaceObjectAtIndex:i - 1 withObject:@(i - 4)];
                    for (NSInteger j = 1; j <= self.days + 4; j ++) {
                        if (j >= 5) {
                            // 向后推算至本月末
                            NSInteger hours = (j - 5) * 24;
                            NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:firstDay];
                            NSString * chineseDay = [self calculationChinaCalendarWithDate:date dispalyHoliday:YES];
                            [self.chineseCalendarDate replaceObjectAtIndex:j - 1 withObject:chineseDay];// 替换农历日期
                            NSString * noHoliday = [self calculationChinaCalendarWithDate:date dispalyHoliday:NO];
                            [self.chineseCalendarDay replaceObjectAtIndex:j - 1 withObject:noHoliday];
                        }
                    }

                }
            }
            self.isIncreaseHeight = NO;
            break;
            
        case 6:// 周五
            for (NSInteger i = 1; i <= self.days + 5; i ++) {
                if (i >= 6) {
                    [self.calendarDate replaceObjectAtIndex:i - 1 withObject:@(i - 5)];
                    for (NSInteger j = 1; j <= self.days + 5; j ++) {
                        if (j >= 6) {
                            // 向后推算至本月末
                            NSInteger hours = (j - 6) * 24;
                            NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:firstDay];
                            NSString * chineseDay = [self calculationChinaCalendarWithDate:date dispalyHoliday:YES];
                            [self.chineseCalendarDate replaceObjectAtIndex:j - 1 withObject:chineseDay];// 替换农历日期
                            NSString * noHoliday = [self calculationChinaCalendarWithDate:date dispalyHoliday:NO];
                            [self.chineseCalendarDay replaceObjectAtIndex:j - 1 withObject:noHoliday];
                        }
                    }

                }
            }
            if (self.days == 31) {// 是否为大月
                self.isIncreaseHeight = YES;
            } else {
                self.isIncreaseHeight = NO;
            }
            break;
            
        case 7:// 周六
            for (NSInteger i = 1; i <= self.days + 6; i ++) {
                if (i >= 7) {
                    [self.calendarDate replaceObjectAtIndex:i - 1 withObject:@(i - 6)];
                    for (NSInteger j = 1; j <= self.days + 6; j ++) {
                        if (j >= 7) {
                            // 向后推算至本月末
                            NSInteger hours = (j - 7) * 24;
                            NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:firstDay];
                            NSString * chineseDay = [self calculationChinaCalendarWithDate:date dispalyHoliday:YES];
                            [self.chineseCalendarDate replaceObjectAtIndex:j - 1 withObject:chineseDay];// 替换农历日期
                            NSString * noHoliday = [self calculationChinaCalendarWithDate:date dispalyHoliday:NO];
                            [self.chineseCalendarDay replaceObjectAtIndex:j - 1 withObject:noHoliday];
                        }
                    }

                }
            }
            self.isIncreaseHeight = YES;
            break;
    }
}
```


- 计算农历日期

由于农历、节假日都是在同一个位置展示,就放到了一个函数里

1.复活节采用了Meeus/Jones/Butcher算法
2.二十四节气采用了积日日计算公式F = 365.242 * (y – 1900) + 6.2 + 15.22 *x - 1.9 * sin(0.262 * x)
####探讨:
这个函数当中24节气的算法在执行当中由于需要对积日进行计算,就需要处理1900-1-0这个基准日的日期转换,由于stringFromDate方法过于耗时,会导致一定的线程卡顿,目前我是将这24个节气根据月份分开来执行,然后使用单例NSDateFormatter来解决这个问题

```objectivec
- (NSDateFormatter *)dateFormatter
{
    if (!_dateFormatter) {
        _dateFormatter = [[NSDateFormatter alloc] init];
        [_dateFormatter setDateFormat:@"yyyy-MM-dd"];
    }
    return _dateFormatter;
}

- (NSDateFormatter *)strDateFormatter
{
    if (!_strDateFormatter) {
        _strDateFormatter = [[NSDateFormatter alloc] init];
        [_strDateFormatter setDateFormat:@"MM-dd"];
    }
    return _strDateFormatter;
}

- (NSDate *)baseDate
{
    if (!_baseDate) {
        _baseDate = [self.dateFormatter dateFromString:@"1900-1-1"];
    }
    return _baseDate;
}
```

```objectivec
#pragma mark - 计算农历日期
- (NSString *)calculationChinaCalendarWithDate:(NSDate *)date dispalyHoliday:(BOOL)display
{
    if (isEmpty(date)) {
        return nil;
    }
    NSArray * chineseYears = @[@"甲子", @"乙丑", @"丙寅", @"丁卯", @"戊辰", @"己巳", @"庚午", @"辛未", @"壬申", @"癸酉", @"甲戌", @"乙亥", @"丙子", @"丁丑", @"戊寅", @"己卯", @"庚辰", @"辛己", @"壬午", @"癸未", @"甲申", @"乙酉", @"丙戌", @"丁亥", @"戊子", @"己丑", @"庚寅", @"辛卯", @"壬辰", @"癸巳", @"甲午", @"乙未", @"丙申", @"丁酉", @"戊戌", @"己亥", @"庚子", @"辛丑", @"壬寅", @"癸丑", @"甲辰", @"乙巳", @"丙午", @"丁未", @"戊申", @"己酉", @"庚戌", @"辛亥", @"壬子", @"癸丑", @"甲寅", @"乙卯", @"丙辰", @"丁巳", @"戊午", @"己未", @"庚申", @"辛酉", @"壬戌", @"癸亥"];
    NSArray * chineseMonths = @[@"正月", @"二月", @"三月", @"四月", @"五月", @"六月", @"七月", @"八月",
                                @"九月", @"十月", @"冬月", @"腊月"];
    NSArray * chineseDays = @[@"初一", @"初二", @"初三", @"初四", @"初五", @"初六", @"初七", @"初八", @"初九", @"初十", @"十一", @"十二", @"十三", @"十四", @"十五", @"十六", @"十七", @"十八", @"十九", @"廿十", @"廿一", @"廿二", @"廿三", @"廿四", @"廿五", @"廿六", @"廿七", @"廿八", @"廿九", @"三十"];
    
    NSCalendar * localeCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierChinese];
    
    unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth |  NSCalendarUnitDay;
    
    NSDateComponents * localeComp = [localeCalendar components:unitFlags fromDate:date];
    
    self.chineseYear = [chineseYears objectAtIndex:localeComp.year - 1];
    NSString * m_str = [chineseMonths objectAtIndex:localeComp.month - 1];
    self.chineseMonth = m_str;
    NSString * d_str = [chineseDays objectAtIndex:localeComp.day - 1];

    NSString * chineseCal_str = d_str;
    
    // 农历节日
    if([chineseMonths containsObject:m_str] && [d_str isEqualToString:@"初一"]) {
        chineseCal_str = m_str;
        if ([m_str isEqualToString:@"正月"] && [d_str isEqualToString:@"初一"]) {
            chineseCal_str = @"春节";
        } else{
            chineseCal_str = @"初一";
        }
    } else if ([m_str isEqualToString:@"正月"] && [d_str isEqualToString:@"十五"]) {
        chineseCal_str = @"元宵节";
    } else if ([m_str isEqualToString:@"五月"] && [d_str isEqualToString:@"初五"]) {
        chineseCal_str = @"端午节";
    } else if ([m_str isEqualToString:@"七月"] && [d_str isEqualToString:@"初七"]) {
        chineseCal_str = @"七夕";
    } else if ([m_str isEqualToString:@"七月"] && [d_str isEqualToString:@"十五"]) {
        chineseCal_str = @"中元节";
    } else if ([m_str isEqualToString:@"八月"] && [d_str isEqualToString:@"十五"]) {
        chineseCal_str = @"中秋节";
    } else if ([m_str isEqualToString:@"九月"] && [d_str isEqualToString:@"初九"]) {
        chineseCal_str = @"重阳节";
    } else if ([m_str isEqualToString:@"腊月"] && [d_str isEqualToString:@"初八"]) {
        chineseCal_str = @"腊八节";
    } else if ([m_str isEqualToString:@"腊月"] && [d_str isEqualToString:@"廿三"]) {
        chineseCal_str = @"小年";
    } else if ([m_str isEqualToString:@"腊月"] && [d_str isEqualToString:@"三十"]) {
        chineseCal_str = @"除夕";
    }
    // 公历节日
    NSDictionary * Holidays = @{@"01-01":@"元旦",
                                @"02-14":@"情人节",
                                @"03-08":@"妇女节",
                                @"03-12":@"植树节",
                                @"04-01":@"愚人节",
                                @"05-01":@"劳动节",
                                @"05-04":@"青年节",
                                @"06-01":@"儿童节",
                                @"07-01":@"建党节",
                                @"08-01":@"建军节",
                                @"09-10":@"教师节",
                                @"10-01":@"国庆节",
                                @"12-24":@"平安夜",
                                @"12-25":@"圣诞节"};
    
//    NSDateFormatter * dateFormatt= [[NSDateFormatter alloc] init];
//    [dateFormatt setDateFormat:@"MM-dd"];
    NSString * nowStr = [self.strDateFormatter stringFromDate:date];
    // 复活节, Meeus/Jones/Butcher算法
    NSUInteger a = self.year % 19;
    NSUInteger b = self.year / 100;
    NSUInteger c = self.year % 100;
    NSUInteger d = b / 4;
    NSUInteger e = b % 4;
    NSUInteger f = (b + 8) / 25;
    NSUInteger g = (b - f + 1) / 3;
    NSUInteger h = (19 * a + b - d - g + 15) % 30;
    NSUInteger i = c / 4;
    NSUInteger k = c % 4;
    NSUInteger l = (32 + (2 * e) + (2 * i) - h - k) % 7;
    NSUInteger m = (a + (11 * h) + (22 * l)) / 451;
    NSUInteger theMonth = (h + l - (7 * m) + 114) / 31;
    NSUInteger day = ((h + l - (7 * m) + 114) % 31)+ 1;
    NSString * easter = [NSString stringWithFormat:@"0%@-%@", @(theMonth), @(day)];
    if ([easter isEqualToString:nowStr]) {
        chineseCal_str = @"复活节";
    }
    
    NSArray * array = [Holidays allKeys];
    if([array containsObject:nowStr]) {
        chineseCal_str = [Holidays objectForKey:nowStr];
    }
    
    // 公历礼拜节日
    NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSDateComponents * comps = [[NSDateComponents alloc] init];
    NSInteger unit = NSCalendarUnitDay | NSCalendarUnitWeekday | NSCalendarUnitMonth | NSCalendarUnitYear;
    comps = [calendar components:unit fromDate:date];
    NSUInteger month = [comps month];
    NSUInteger dayInMonth = [comps day];
    switch (month) {
        case 5:
            if (dayInMonth == 14) {
                chineseCal_str = @"母亲节";
            }
            break;
        case 6:
            if (dayInMonth == 21) {
                chineseCal_str = @"父亲节";
            }
            break;
        case 11:
            if (dayInMonth == 26) {
                chineseCal_str = @"感恩节";
            }
            break;

            
        default:
            break;
    }
    
    // 二十四节气, 将节气按月份拆开计算,否则由于计算积日所需日期转换stringFromDate方法过于耗时将会造成线程卡顿
    NSString * solarTerms = @"";
    switch (self.month) {// 过滤月份
        case 1:
            for (NSInteger i = 0; i < 2; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 0:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"小寒";
                        }
                        break;
                    case 1:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"大寒";
                        }
                        break;
                }
            }
            break;
        case 2:
            for (NSInteger i = 2; i < 4; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 2:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"立春";
                        }
                        break;
                    case 3:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"雨水";
                        }
                        break;
                }
            }
            break;
        case 3:
            for (NSInteger i = 4; i < 6; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 4:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"惊蛰";
                        }
                        break;
                    case 5:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"春分";
                        }
                        break;
                }
            }
            break;
        case 4:
            for (NSInteger i = 6; i < 8; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 6:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"清明";
                        }
                        break;
                    case 7:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"谷雨";
                        }
                        break;
                }
            }
            break;
        case 5:
            for (NSInteger i = 8; i < 10; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 8:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"立夏";
                        }
                        break;
                    case 9:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"小满";
                        }
                        break;
                }
            }
            break;
        case 6:
            for (NSInteger i = 10; i < 12; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 10:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"芒种";
                        }
                        break;
                    case 11:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"夏至";
                        }
                }
            }
            break;
        case 7:
            for (NSInteger i = 12; i < 14; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 12:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"小暑";
                        }
                        break;
                    case 13:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"大暑";
                        }
                    break;                }
            }
            break;
        case 8:
            for (NSInteger i = 14; i < 16; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 14:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"立秋";
                        }
                        break;
                    case 15:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"处暑";
                        }
                        break;
                }
            }
            break;
        case 9:
            for (NSInteger i = 16; i < 18; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 16:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"白露";
                        }
                        break;
                    case 17:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"秋分";
                        }
                        break;
                }
            }
            break;
        case 10:
            for (NSInteger i = 18; i < 20; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 18:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"寒露";
                        }
                        break;
                    case 19:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"霜降";
                        }
                        break;
                }
            }
            break;
        case 11:
            for (NSInteger i = 20; i < 22; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 20:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"立冬";
                        }
                        break;
                    case 21:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"小雪";
                        }
                    break;
                }
            }
            break;
        case 12:
            for (NSInteger i = 22; i < 24; i ++) {
                solarTerms = [self calculationSolarTermsWithYear:self.year solarTermsIndex:i];
                switch (i) {
                    case 22:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"大雪";
                        }
                        break;
                    case 23:
                        if ([solarTerms isEqualToString:nowStr]) {
                            chineseCal_str = @"冬至";
                        }
                        break;
                }
            }
            break;
    }
    
    if (display == YES) {// 需要显示假期&节日
        return chineseCal_str;
    }
    return d_str;
}
```


- 计算24节气的具体日期

这里的计算是整个线程里最耗时的地方,昨天用instruments查看这里的执行,竟然有8000x,我想最可能到这这个的原因就是dateFromString这里了,在我做了一些优化调整后,虽然已经不卡顿了,但不知道有什么更好的解决方案吗?

```objectivec
#pragma mark - 计算二十四节气的具体日期
/**
 * @param year 年份
 * @param index 节气索引,0代表小寒,1代表大寒,其它节气按照顺序类推
 */
- (NSString *)calculationSolarTermsWithYear:(NSUInteger)year solarTermsIndex:(NSUInteger)index
{
    NSString * solarTerms = @"";
    CGFloat base = 365.242 * (year - 1900) + 6.2 + (15.22 * index) - (1.9 * sinf(0.262 * index));// 计算积日
    NSInteger hours = (base - 1) * 24;// 由于基准日为1900年1月0日,所以这里需要-1
    NSDate * date = [NSDate dateWithTimeInterval:hours * 60 * 60 sinceDate:self.baseDate];
   
    solarTerms = [self.strDateFormatter stringFromDate:date];
    
    
    return solarTerms;
}```

### 2.3 动画
- 动画方面主要就是两个方面,`翻页动画`和`点击效果`


- 翻页动画

```objectivec
+ (void)animationWithView:(UIView *)view andEffect:(SK_ANIMATION)effect isNext:(BOOL)next
{
    CATransition * transition = [CATransition animation];
    if (next == YES) {// 向下翻页
        switch (effect) {
            case SK_ANIMATION_REVEAL:
                transition.type = @"pageUnCurl";
                transition.subtype = kCATransitionFromLeft;
                break;
            case SK_ANIMATION_RIPPLE:
                transition.type = @"rippleEffect";
                transition.subtype = kCATransitionFromLeft;
                break;
            case SK_ANIMATION_SUCK:
                transition.type = @"suckEffect";
                transition.subtype = kCATransitionFromLeft;
                break;
        }
    } else {
        switch (effect) {
            case SK_ANIMATION_REVEAL:
                transition.type = @"pageCurl";
                transition.subtype = kCATransitionFromLeft;
                
                break;
            case SK_ANIMATION_RIPPLE:
                transition.type = @"rippleEffect";
                transition.subtype = kCATransitionFromRight;
                break;
            case SK_ANIMATION_SUCK:
                transition.type = @"suckEffect";
                transition.subtype = kCATransitionFromRight;
                break;
        }
    }
    transition.duration = 0.5;
    [view.layer addAnimation:transition forKey:nil];
    
}
```


- 点击效果

```objectivec
+ (void)clickEffectAnimationForView:(UIView *)view
{
    CABasicAnimation * scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    scaleAnimation.fromValue = [NSNumber numberWithFloat:1.3];
    scaleAnimation.toValue = [NSNumber numberWithFloat:0.7];
    scaleAnimation.duration = 0.1;
    scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [view.layer addAnimation:scaleAnimation forKey:nil];
}
```



##### 好了,以上就是本次内容的分享,如果能帮到你,我很开心,欢迎在文章下面留言,在文中提到的关于算法上的优化,希望能够得到大神的指点


# 感谢你花时间阅读以上内容, 如果这个项目能够帮助到你,记得告诉我

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

推荐阅读更多精彩内容