最终效果:
请大家忽略图片质量哈,我这软件弄出来的质量不高.最终实现的就是在客户端能够签到功能,使用了以为大神封装的日历类,FSCalendar,附github地址:FSCalendar
我的demo基本就是一个使用FSCalendar的一个样例,但是直接使用中会有一些坑,话不多说,直接上代码
1.首先安装FSCalendar
pod 'FSCalendar', '~> 2.7.9'
2.创建一个新类,导入FSCalendar和系统事件库EventKit
#import "FSCalendar.h"
//用来读取,修改和创建日历上的事件
#import <EventKit/EventKit.h>
3.重写loadView方法,创建FSCalendar
//创建日历类
FSCalendar *calendar = [[FSCalendar alloc] initWithFrame:CGRectMake(0, self.navigationController.navigationBar.frame.size.height, self.view.bounds.size.width - 50, 300)];
calendar.backgroundColor = [UIColor whiteColor];
calendar.dataSource = self;
calendar.delegate = self;
//日历语言为中文
calendar.locale = [NSLocale localeWithLocaleIdentifier:@"zh-CN"];
//允许多选,可以选中多个日期
calendar.allowsMultipleSelection = YES;
//如果值为1,那么周日就在第一列,如果为2,周日就在最后一列
calendar.firstWeekday = 1;
//周一\二\三...或者头部的2017年11月的显示方式
calendar.appearance.caseOptions = FSCalendarCaseOptionsWeekdayUsesSingleUpperCase|FSCalendarCaseOptionsHeaderUsesUpperCase;
[self.view addSubview:calendar];
self.calendar = calendar;
4.根据创建的日历类做初始化设置
#pragma mark - <配置日历>
- (void)calendarConfig{
//创建系统日历类
self.gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
//获取日历要显示的日期范围
NSArray *timeArray = [ViewController getStartTimeAndFinalTime];
//设置最小和最大日期(在最小和最大日期之外的日期不能被选中,日期范围如果大于一个月,则日历可翻动)
self.minimumDate = [self.dateFormatter dateFromString:timeArray[0]];
self.maximumDate = [self.dateFormatter dateFromString:timeArray[1]];
self.calendar.accessibilityIdentifier = @"calendar";
//title显示方式
self.calendar.appearance.headerDateFormat = @"yyyy年MM月";
//关闭字体自适应,设置字体大小\颜色
self.calendar.appearance.adjustsFontSizeToFitContentSize = NO;
self.calendar.appearance.subtitleFont = [UIFont systemFontOfSize:8];
self.calendar.appearance.headerTitleColor = [UIColor whiteColor];
self.calendar.appearance.weekdayTextColor = [UIColor whiteColor];
self.calendar.appearance.selectionColor = [UIColor orangeColor];
//日历头部颜色
self.calendar.calendarHeaderView.backgroundColor = themeColor;
self.calendar.calendarWeekdayView.backgroundColor = themeColor;
}
5.实现FSCalendar数据源方法
#pragma mark - FSCalendarDataSource
//日期范围(最小)
- (NSDate *)minimumDateForCalendar:(FSCalendar *)calendar
{
return self.minimumDate;
}
//日期范围(最大)
- (NSDate *)maximumDateForCalendar:(FSCalendar *)calendar
{
return self.maximumDate;
}
6.(重点)签到逻辑
- (void)viewDidLoad {
[super viewDidLoad];
//日历配置
[self calendarConfig];
//1.加载缓存中的的日期,并选中这些日期
[self getCache];
//2.从网络获取其签到结果,如果发现请求的结果中存在没有被选中,就将其选中,并加载到缓存中
[self getSign];
}
上述两个方法的具体实现大致思路为:
- 当控制器加载完毕后,从缓存获取数据并让日历选中
- 从服务器获取一次该用户的签到结果,检查是否有遗漏(考虑到当用户在其他设备登录时),如果有遗漏添加到缓存中,并选中
- 缓存策略,如果不存缓存的话,每次启动APP后加载签到页面,就要重新网络请求获取签到数据,并选中日期,每次动画都要延时出现不太合适,所以存缓存,缓存可以让签到结果快速加载
具体实现如下
//加载缓存
- (void)getCache{
//从缓存中先把数据取出来
NSString *key = [NSString stringWithFormat:@"arrayDate"];
NSMutableArray *cache = [[NSUserDefaults standardUserDefaults] objectForKey:key];
//允许用户选择,其实是允许系统来选中签到日期
self.calendar.allowsSelection = YES;
self.calendar.allowsMultipleSelection = YES;
if (cache.count) {//如果cache里面有数据
//选中日期,只有不在选中之列的才去选中它
for (NSInteger i = 0; i<cache.count; i++) {
if (![self.calendar.selectedDates containsObject:cache[i]]) {
[self.calendar selectDate:cache[i]];
}
}
}else{//如果cache里面没有数据,说明第一次启动
//创建个可变数组储存进缓存
NSMutableArray *cache = [NSMutableArray array];
[[NSUserDefaults standardUserDefaults] setValue:cache forKey:key];
[[NSUserDefaults standardUserDefaults] synchronize];
}
//选择完毕后关闭可选项,不让用户自己点
self.calendar.allowsSelection = NO;
}
//点击签到按钮的Action
- (void)signInAction{
//假设在这里网络请求签到成功,成功后需要重新请求签到所有结果
if (_count>31) {//此处的判断仅为本demo临时使用,正式使用中可以根据具体情况删除此if else判断
NSLog(@"别点了");
return;
}else if (!_count){
_count = 1;
}
NSString *dateStr = [NSString stringWithFormat:@"2017-11-%ld",_count];
_count++;
[self.signInList addObject:dateStr];
[self getSign];
}
//从网络获取所有签到结果
- (void)getSign{
//配置日期缓存的key
NSString *key = [NSString stringWithFormat:@"arrayDate"];
//在这里假装网络请求所有的签到结果(signInList)成功了
NSLog(@"%@",_signInList);
//获取签到总数量
self.SignCount = _signInList.count;
//常见临时数组dataArrayCache,用于存放签到结果(可能有的人觉得这一步不需要,但是咱们假设的签到结果里面只有纯日期,正式项目中可不一定如此)
NSMutableArray *dataArrayCache = [NSMutableArray array];
if (self.SignCount) {//如果请求的数据有效
for (NSString *dateStr in _signInList) {
//把所有签到数据取出来添加进临时数组
NSDate *date = [self.dateFormatter dateFromString:dateStr];
if(date){
[dataArrayCache addObject:date];
}
}
//用偏好设置保存签到数据到本地缓存
[[NSUserDefaults standardUserDefaults] setValue:dataArrayCache forKey:key];
[[NSUserDefaults standardUserDefaults] synchronize];
//保存后重新加载缓存数据
[self getCache];
}
}
//获取日历范围,让日历出现时就知道该显示哪个月了哪一页了(根据系统时间来获取)
+(NSArray *)getStartTimeAndFinalTime{
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"YYYY-MM-dd"];
NSDate *datenow = [NSDate date];
NSString *currentTimeString = [formatter stringFromDate:datenow];
NSDate *newDate=[formatter dateFromString:currentTimeString];
double interval = 0;
NSDate *firstDate = nil;
NSDate *lastDate = nil;
NSCalendar *calendar = [NSCalendar currentCalendar];
BOOL OK = [calendar rangeOfUnit:NSCalendarUnitMonth startDate:& firstDate interval:&interval forDate:newDate];
if (OK) {
lastDate = [firstDate dateByAddingTimeInterval:interval - 1];
}else {
return @[@"",@""];
}
NSString *firstString = [formatter stringFromDate: firstDate];
NSString *lastString = [formatter stringFromDate: lastDate];
//返回数据为日历要显示的最小日期firstString和最大日期lastString
return @[firstString, lastString];
}
7.关于FSCalendar的代理方法基本不需要实现,因为签到一般不允许用户点击,如果有特殊需求的话,代价可以加上,更多的可以去delegate中寻找需要用的
#pragma mark - FSCalendarDelegate
//手动选中了某个日期(本demo暂时被隐藏)
- (void)calendar:(FSCalendar *)calendar didSelectDate:(NSDate *)date atMonthPosition:(FSCalendarMonthPosition)monthPosition
{
NSLog(@"did select %@",[self.dateFormatter stringFromDate:date]);
}
//当前页被改变,日历翻动时调用(本demo暂时没用到)
- (void)calendarCurrentPageDidChange:(FSCalendar *)calendar
{
NSLog(@"did change page %@",[self.dateFormatter stringFromDate:calendar.currentPage]);
}
8.显示农历:将LunarFormatter拖进项目,FSCalendar的demo中也有,本文demo中也有,主要在数据源方法中使用
//数据源方法,根据是否显示节日和农历
- (NSString *)calendar:(FSCalendar *)calendar subtitleForDate:(NSDate *)date
{
if (self.showsEvents) {//如果要求显示节日
EKEvent *event = [self eventsForDate:date].firstObject;
if (event) {
return event.title;
}
}
if (self.showsLunar) {//如果要求显示农历
return [self.lunarFormatter stringFromDate:date];
}
return nil;
}
//加载节日到日历中
- (void)loadCalendarEvents
{
__weak typeof(self) weakSelf = self;
EKEventStore *store = [[EKEventStore alloc] init];
//请求访问日历
[store requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError *error) {
//允许访问
if(granted) {
NSDate *startDate = self.minimumDate;
NSDate *endDate = self.maximumDate;
NSPredicate *fetchCalendarEvents = [store predicateForEventsWithStartDate:startDate endDate:endDate calendars:nil];
NSArray<EKEvent *> *eventList = [store eventsMatchingPredicate:fetchCalendarEvents];
NSArray<EKEvent *> *events = [eventList filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(EKEvent * _Nullable event, NSDictionary<NSString *,id> * _Nullable bindings) {
return event.calendar.subscribed;
}]];
dispatch_async(dispatch_get_main_queue(), ^{
if (!weakSelf) return;
weakSelf.events = events;
[weakSelf.calendar reloadData];
});
} else {
// Alert
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"权限错误" message:@"获取节日事件需要权限呀大宝贝!" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alertController animated:YES completion:nil];
}
}];
}
//根据日期来显示事件
- (NSArray<EKEvent *> *)eventsForDate:(NSDate *)date
{
NSArray<EKEvent *> *events = [self.cache objectForKey:date];
if ([events isKindOfClass:[NSNull class]]) {
return nil;
}
NSArray<EKEvent *> *filteredEvents = [self.events filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(EKEvent * _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
return [evaluatedObject.occurrenceDate isEqualToDate:date];
}]];
if (filteredEvents.count) {
[self.cache setObject:filteredEvents forKey:date];
} else {
[self.cache setObject:[NSNull null] forKey:date];
}
return filteredEvents;
}
9.最后获取日历权限需要在info.plist文件配置Privacy - Calendars Usage Description获取日历使用权限
10.demo地址:https://github.com/TynnPassBy/TynnSignCalendar,下载完后记得pod install后再启动项目,有任何疑问可以在下方留言,我会尽力帮助大家.附FSCalendar两篇比较使用的文章:http://www.jianshu.com/p/59c5d535a710和http://www.jianshu.com/p/6f1592260d35,感谢~