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

4,518 阅读6分钟

最终效果:
效果.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地址:github.com/TynnPassBy/… install后再启动项目,有任何疑问可以在下方留言,我会尽力帮助大家.

  • 附FSCalendar两篇比较实用的文章:

www.jianshu.com/p/59c5d535a…

www.jianshu.com/p/6f1592260…

感谢~