iOS-使用FSCalendar实现日历签到功能

最终效果:
效果.gif

请大家忽略图片质量哈,我这软件弄出来的质量不高.最终实现的就是在客户端能够签到功能,使用了以为大神封装的日历类,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.(重点)签到逻辑


Simulator Screen Shot - iPhone 8 - 2017-11-28 at 17.04.50.png
- (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中也有,主要在数据源方法中使用


image.png
//数据源方法,根据是否显示节日和农历
- (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/59c5d535a710http://www.jianshu.com/p/6f1592260d35,感谢~

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,811评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 我不是佛教徒,却行走于各大庙宇,我在寻找什么?我在寻找心灵的寄托!我在寻找拯救我的神! 这么多年经历下来,看见的却...
    竺子阅读 119评论 0 0
  • 心累那是没遇到对的风景 身累那是没遇到喜爱的事 身心疲惫那是没人理解你 只要做的决定对的起自己 那就抬高你的脚步大...
    蔷薇花儿落地开阅读 203评论 4 4
  • 2017年9月23日 星期六 天气晴 今天中午去打针。我在打针的时候医生给我打两只手。打好了之后我去跟其他小朋友玩...
    诗雨_137阅读 158评论 0 2