本文字数:4281

预计阅读时间:29分钟

背景

笔者五一之前补班的时候,闹钟没响,早上差点迟到了。笔者闹钟设置的是周一到周五,因为iPhone没有法定节假日和补休的设置。。。。笔者就想要解决这个痛点,梦想着,要是做出来了,发布到商店,从此走上人生巅峰,赢取白。。。。

YY过后,回过头来,接着调研,法定节假日闹钟的实现,笔者查找了很多资料,发现不用做梦了。首先iOS程序添加闹钟到时钟APP是不允许的。。。其次,iOS也没有法定节假日的判断。。。。所以不用YY了。但是笔者还真找到了iOS自定义闹钟 —— 中国法定节假日(升级版)这个,通过快捷指令自定义闹钟,可以实现过滤法定节假日。原理是:设置闹钟,然后通过快捷指令的自动执行,每天在闹钟时间前,通过订阅的别人维护的日历或者自己本地维护日历,判断当天是否是节假日,然后决定当天的闹钟是否打开、关闭。真的要赞一个,优秀。

虽然笔者的发财梦夭折了。。。但笔者想到了另一个办法,iOS程序不能直接添加闹钟,但确是可以直接添加日历提醒的,比如预约直播或者预约抢购,其原理都是添加事件到日历中,然后在指定的时间,弹出来日历提醒去做什么。那是否能用日历提醒来实现,法定工作日的提醒呢。。。比如每个工作日提醒打卡;或者只针对节假日补班提醒,每个补班前天晚上提醒设置闹钟。

实现

iPhone 添加日历提醒的实现很简单,难的地方还是在于国内法定节假日的判断,怎么能过滤掉法定节假日,实现真正纯工作日的时候提醒?

第一步,先创建周一到周五的重复事件

笔者又调研了一番,发现日历提醒中有一个EKRecurrenceRule的规则选项,是否能用这个来实现呢?

EKRecurrenceRule是什么?

官方解释:

A class that describes the recurrence pattern for a recurring event.

笔者理解:重复事件的重复规则。简单的说,就是定义一个重复规则,比如每周重复、每天重复、每隔几天重复类似的,然后按照这个规则添加事件。

看到这个,笔者的心凉了半截,重复的规则,对于国内法定节假日来说。。。。除了五一、国庆、元旦之外,农历的节日重复的规则找不到。。。怎么办?笔者寻思着都到这一步了,就先做个周一到周五的,也算是需求完成了半个,工作日的那部分完成了,剩下的那部分过滤法定节假日和补休,慢慢看,又不是不用????

先来看设置每周一到周五的循环日历事件

添加日历事件 添加日历事件的步骤如下:

  1. 获取读写日历权限

  2. 创建单独的日历

  3. 生成周一到周五的规则

  4. 根据标题、地址、规则和时间生成日历事件

  5. 添加事件到日历 判断生成的事件是否已经添加,已添加则不操作,没添加则添加

下面一步步来看:

  1. 获取读写日历权限

    首先需要在plist中添加Privacy - Calendars Usage Description权限,然后使用下面代码申请权限

    lazy fileprivate var store = EKEventStore()// MARK: utils
    // 申请日历权限
    func requestEventAuth(_ callback:((Bool) -> Void)?) {store.requestAccess(to: EKEntityType.event) { granted, error incallback?(granted)}
    }
    
  2. 创建单独的日历

    用于保证不和其它日历冲突,而且不显示或者移除时方便,建议每个自定义日历事件的都单独定义一个日历。

    听起来有些绕,打开iPhone,打开日历,然后点击底部中间的日历按钮,就能看到自己的所有日历。看图如下,"自定义的事项日历"即是笔者自定义的日历,笔者所添加的日历事件都会在这个日历中,如果不想要看到这些事件,可以直接把前面的勾选去除,日历中就不会显示自定义的日历事件了。或者想要删除这个日历中的所有事件时,只需要把这个日历删掉即可,不需要一条条事件删除,点击右边的提示按钮,然后滑动到最下方就有删除日历的按钮。

    Ps:默默的吐槽,不知道为啥预约抢购和预约直播提醒的,不单独建一个日历。。。。笔者预约了之后感觉烦,每次都得手动去删除事件

    创建日历的代码如下,注意calendar的source的设置,source设置为什么,最后添加的日历会显示在哪个地方

     // 创建新的日历func createNewCalendar() {guard let calendarId = MKCalendarReminderUtil.userDefaultsSaveStr(kCustomCalendarId),let _ = store.calendar(withIdentifier: calendarId) else { // 说明本地已经创建了当前日历return}let calendar = EKCalendar(for: EKEntityType.event, eventStore: store)for item in store.sources {if item.title == "iCloud" || item.title == "Default" {calendar.source = itembreak}}calendar.title = "自定义的事项日历" // 自定义日历标题calendar.cgColor = UIColor.systemPurple.cgColor // 自定义日历颜色do {try store.saveCalendar(calendar, commit: true)MKCalendarReminderUtil.saveToUserDefaults(kCustomCalendarId, valueStr: calendar.calendarIdentifier)}catch {print(error)}}
  3. 生成周一到周五的规则

    使用EKRecurrenceRule生成每周一到周五重复的规则。EKRecurrenceRule的使用如下,其中EKRecurrenceRule(recurrenceWith:interval:daysOfTheWeek:daysOfTheMonth:monthsOfTheYear:weeksOfTheYear:daysOfTheYear:setPositions:end:)初始化方法各参数意义如下:recurrenceWith:

      • EKRecurrenceFrequency,代表重复频率,可设置:按天、周、月、年的重复频率?

    • interval:

      • Int, 代表重复间隔,每个多久重复,不能为0

    • daysOfTheWeek:

      • [EKRecurrenceDayOfWeek],每周哪几天重复,设置之后,除了按天的重复频率外,都可以生效

    • daysOfTheMonth:

      • [number],number取值1-31,也可以为负数,负数说明是从月底开始,比如-1是该月最后一天。只有在设置了按月重复频率下生效

    • monthsOfTheYear:

      • [number],number取值1-12,只有在设置了按年重复频率下生效

    • weeksOfTheYear:

      • [number],number取值1-53,也可以为负数,负数说明是从年底开始。只有在设置了按年重复频率下生效

    • daysOfTheYear:

      • [number],number取值1-366,也可以为负数,负数说明是从年底开始。只有在设置了按年重复频率下生效

    • setPositions:

      • [number],number取值1-366,也可以为负数,负值表示反向计算,过滤其它规则的过滤器,在设置了daysOfTheWeek,daysOfTheMonth,monthsOfTheYear,weeksOfTheYear,daysOfTheYear 之后有效

    • end:

      • EKRecurrenceEnd, 重复截止日期

       // 生成重复的规则func generateEKRecurrenceRule() -> EKRecurrenceRule {let monday = EKRecurrenceDayOfWeek(EKWeekday.monday)let tuesday = EKRecurrenceDayOfWeek(EKWeekday.tuesday)let wednesday = EKRecurrenceDayOfWeek(EKWeekday.wednesday)let thursday = EKRecurrenceDayOfWeek(EKWeekday.thursday)let friday = EKRecurrenceDayOfWeek(EKWeekday.friday)// 设置按重复频率为按周重复,重复间隔为每周都重复,一周中的周一、周二、周三、周四、周五重复let rule = EKRecurrenceRule(recurrenceWith: EKRecurrenceFrequency.weekly,interval: 1,daysOfTheWeek: [monday, tuesday, wednesday, thursday, friday],daysOfTheMonth: nil,monthsOfTheYear: nil,weeksOfTheYear: nil,daysOfTheYear: nil,setPositions: nil,end: nil)return rule}
  • 根据标题、地址、备注、规则和时间生成日历事件

    生成日历事件时,要注意事件的持续时间,以及是否添加闹钟提示。这个闹钟提示不是通常意义的闹钟,是日程提醒,比如设置了事件的闹钟提示,在达到闹钟提醒时间后,会提醒响铃,且在通知栏弹出。

    // 生成日历事件
    func generateEvent(_ title: String?, location: String?, notes: String?, timeStr: String?) -> EKEvent {let event = EKEvent(eventStore: store)event.title = titleevent.location = locationevent.notes = notes// 事件的时间if let date = Date.date(from: timeStr, formatterStr: "yyyy-MM-dd HH:mm:ss") {// 开始let startDate = Date(timeInterval: 0, since: date)// 结束let endDate = Date(timeInterval: 60, since: date)// 日历提醒持续时间event.startDate = startDateevent.endDate = endDateevent.isAllDay = false}else {// 全天提醒event.isAllDay = true}// 添加重复规则let recurrenceRule = generateEKRecurrenceRule()event.addRecurrenceRule(recurrenceRule)// 添加闹钟结合(开始前多少秒)若为正则是开始后多少秒。let alarm = EKAlarm(relativeOffset: 0)event.addAlarm(alarm)if let calendarId = MKCalendarReminderUtil.userDefaultsSaveStr(kCustomCalendarId),let calendar = store.calendar(withIdentifier: calendarId) {event.calendar = calendar}return event
    }
    
  • 添加事件到日历

    添加时,需要判断生成的事件是否已经添加,已添加则不操作,没添加则添加。添加成功后,把事件ID存储起来,避免重复添加同一个事件

        // 添加事件到日历
    func addEvent(_ title: String?, location: String?, notes: String?, timeStr: String, eventKey: String) {requestEventAuth { [weak self] granted inif granted {// 先创建日历self?.createNewCalendar()// 判断事件是否存在if let eventId = MKCalendarReminderUtil.userDefaultsSaveStr(eventKey),let _ = self?.store.event(withIdentifier: eventId) {// 事件已添加return}else {if let event = self?.generateEvent(title, location: location, notes: notes, timeStr: timeStr) {do {try self?.store.save(event, span: EKSpan.thisEvent, commit: true)//添加成功后需要保存日历关键字// 保存在沙盒,避免重复添加等其他判断MKCalendarReminderUtil.saveToUserDefaults(eventKey, valueStr: event.eventIdentifier)}catch {print(error)}}}}}
    }
    
  • 从外部使用下面代码调用

    let date = Date.beijingDate()
    let timeStr = Date.string(from: date, formatterStr: "yyyy-MM-dd HH:mm:ss")
    MKCalendarReminderUtil.util.addEvent("自定义标题", location: "上海东方明珠", notes: "记得拍照打卡", timeStr: timeStr!, eventKey: "自定义标题")
    

    会先弹出授权访问日历的提示框,点击允许后,成功添加到日历,然后去日历中可以看到,

    • 日历中从当天开始的,每周一至周五都有事件存在

    • 点开具体的日期,可以看到当天日期的所有事件,点击添加的事件

    • 可以看到事件的标题、地址、持续时间、重复频率、所属日历以及备注

    至此,笔者成功添加了周一到周五重复提醒的事件,已经算是完成了一半,勉强能用,就是遇到节假日时,补班、调休的时候会错误提醒。还有一半,即怎么把节假日的逻辑加入到事件中?

    第二步,添加法定节假日逻辑

    笔者一直想的是添加法定节假日的逻辑,一开始其实就陷入了误区,一直想的是,是否有一个规则,按照这个规则,能自动过滤掉节假日和添加补班,然后生成重复日历事件。然而并没有这样的规则存在。

    参考快捷指令节假日闹钟的实现,笔者就想到了另一种方式,如果没有直接节假日的规则,那能否分两步走?第一步先创建周一到周五的固定重复逻辑;第二步,从某个地方获取到节假日和补班信息,然后根据信息,在第一步的基础上,“多退少补”,即属于节假日的周一至周五的事件移除,属于补班的没有日历事件的则添加事件。

    那这种方案是否可行呢?实践出真知!

    步骤如下:

    1. 获取节假日和补班信息 从哪里能获取到节假日和补班信息呢?笔者去网上查找了一番,最终看到了有两个合适的订阅来源holiday-cn和节假日 API,

      对于笔者来说,holiday-cn已满足,故而笔者选用了holiday-cn。当然如果公司支持,也可以在公司服务端维护一份节假日信息,能保证各端统一。甚至也可以维护在客户端一份本地json,等下一年的节假日信息出来后,再更新客户端本地的。

      返回节假日JSON格式如下,name是节假日名称,date是节假日日期,isOffDay代表是否是休息,比如2021-09-18是中秋节的补班

      {"$schema":"https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/schema.json","$id":"https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/2021.json","year":2021,"papers":["http://www.gov.cn/zhengce/content/2020-11/25/content_5564127.htm"],"days":[...{"name":"中秋节","date":"2021-09-18","isOffDay":false},{"name":"中秋节","date":"2021-09-19","isOffDay":true},{"name":"中秋节","date":"2021-09-20","isOffDay":true},{"name":"中秋节","date":"2021-09-21","isOffDay":true},{"name":"国庆节","date":"2021-09-26","isOffDay":false},{"name":"国庆节","date":"2021-10-01","isOffDay":true},{"name":"国庆节","date":"2021-10-02","isOffDay":true},{"name":"国庆节","date":"2021-10-03","isOffDay":true},{"name":"国庆节","date":"2021-10-04","isOffDay":true},{"name":"国庆节","date":"2021-10-05","isOffDay":true},{"name":"国庆节","date":"2021-10-06","isOffDay":true},{"name":"国庆节","date":"2021-10-07","isOffDay":true},{"name":"国庆节","date":"2021-10-09","isOffDay":false}]}
      

      代码如下

      fileprivate func filterHolidayInfo(with title: String?, location: String?, notes: String?, timeStr: String, eventKey: String) {guard let url = URL(string: "https://natescarlet.coding.net/p/github/d/holiday-cn/git/raw/master/2021.json") else {return}let task = URLSession.shaGold.dataTask(with: url) { [weak self] (data, response, error) inguard let data = data else { return }do {if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary,let days = jsonResult["days"] as? [NSDictionary] {// 过滤节假日self?.handleHolidayInfo(with: days, title: title, location: location, notes: notes, timeStr: timeStr, eventKey: eventKey)}} catch {print(error)}}task.resume()
      }
      
    • holiday-cn:自动每日抓取国务院公告,返回节假日和补班信息

    • 节假日 API:是由私人维护的API,支持多种API接口访问,传入月份、传入日期、传入年份等等

  • “多退少补”

    即属于节假日的周一至周五的事件移除,属于补班的没有日历事件的则添加事件。这里需要判断,某天日期是否有当前的事件。

    这个地方需要注意两点:

    1. 之前生成事件generateEvent的方法中,有事件添加重复规则的逻辑,需要把这个逻辑从通用的generateEvent方法中抽出来。放到addEvent方法的save之前。

    2. 补班的日期,不能直接使用传入日期,要从传入日期中获取时分秒,然后拼接上补班的日期,作为要设置的日期

    // 判断某天,是否有指定的事件
    fileprivate func eventExist(on tdate: Date?, eventKey: String) -> EKEvent? {var resultEvent: EKEvent?guard let date = tdate else {return resultEvent}let endDate = date.addingTimeInterval(TimeInterval(24 * 60 * 60))guard let calendarId = MKCalendarReminderUtil.userDefaultsSaveStr(kCustomCalendarId),let eventId = MKCalendarReminderUtil.userDefaultsSaveStr(eventKey),let calendar = store.calendar(withIdentifier: calendarId) else {return resultEvent}let pGoldicate = store.pGoldicateForEvents(withStart: date, end: endDate, calendars: [calendar])let events = store.events(matching: pGoldicate)for event in events {if event.eventIdentifier == eventId {resultEvent = eventbreak}}return resultEvent
    }// "多退少补"
    fileprivate func handleHolidayInfo(with days: [NSDictionary], title: String?, location: String?, notes: String?, timeStr: String, eventKey: String) {for dayDic in days {if let dayStr = dayDic["date"] as? String,let date = Date.date(from: dayStr, formatterStr: "yyyy-MM-dd"), // 日期let isOffDay = dayDic["isOffDay"] as? Bool { // 是否上班let interval = date.timeIntervalSince(date)// 1. 判断获取到的日期小于当前日期,说明是以前的日期,不处理// 2. 判断日期大于等于当前日期后,判断是否休息,判断日期那天是否有要添加的事件,// 3. 休息,有事件,则移除事件// 4. 未休息,无事件,则添加事件if interval < 0 {continue}else {if let targetEvent = eventExist(on: date, eventKey: eventKey) {// 事件存在if isOffDay { // 休息日do {try store.remove(targetEvent, span: EKSpan.thisEvent)} catch {print(error)}}}else {// 事件不存在if !isOffDay { // 非休息日,即要补班var targetDateStr = dayStrif let lastComponentStr = timeStr.components(separatedBy: " ").last {targetDateStr = String(format: "%@ %@", dayStr, lastComponentStr)}let event = generateEvent(title, location: location, notes: notes, timeStr: targetDateStr)do {try store.save(event, span: EKSpan.thisEvent)} catch {print(error)}}}}}}
    }

    运行调试代码,调用代码中修改标题和内容用以区分之前的事件,成败在此一举,哈哈哈,binggo,完美:

    let date = Date.beijingDate()
    let timeStr = Date.string(from: date, formatterStr: "yyyy-MM-dd HH:mm:ss")
    MKCalendarReminderUtil.util.addEvent("自定义标题2", location: "上海东方明珠2", notes: "记得拍照打卡2", timeStr: timeStr!, eventKey: "自定义标题2", filterHoliday: true)
    

    最终结果如下,可以看到,9月18日、9月26日、10月9日的补班都已经添加了提醒事件;且周一到周五中放假的日期没有提醒事件。Perfect,此处当有掌声,[手动狗头],哈哈哈

    代码地址:MKReminderUtil

    总结:


    通过这种方式,生成的日历提醒,还需要考虑一点,就是节假日数据有更新的时候,如何更新?

    笔者的个人看法是,在自己服务端维护一套节假日数据比较好,返回节假日数据时,也返回对应版本号。请求了之后,根据version对比,如果节假日数据没有更新,则无需做任何操作,如果有更新,则根据更新的数据默默的把明年的日历也创建了即可。

    引用

    • Creating a Recurring Event

    • ios – 如何从日历中获取所有事件(Swift)

    • holiday-cn

    • 节假日 API

    本期赠书

    《iOS面试一战到底》

    张益珲 著

    从内容上讲,图书是一本专门面向提升面试技巧的工具书,每一章都可以作为一个独立的专题模块。你也可以将本书作为一本工具书,一本专注于提升iOS核心开发能力的进阶教程。在日常开发中查询某些知识点的用法,或者在技术面试前夕进行突击训练。书中的所有范例都提供了源代码参考,并且本书每一章的结尾都提供了一些面试场景,以供读者进行练习。

    活动参与方式:留言点赞数前五名的同学各获赠书一本

    获奖公布时间及位置:7月1日头条推送文末

    特别提醒:兑奖截止至7月8日,请参与读者及时兑奖~

    P.S.如果不能留言,微信首页-下拉-长按删除小程序-“小互动”,然后重新进入留言板即可留言。

    也许你还想看

    (▼点击文章标题或封面查看)

    正经分析iOS包大小优化

    2021-05-27

    【文末有惊喜!】如何让iOS推送播放语音?

    2021-05-13

    干货:图像の二值图

    2021-04-15

    包教包会:设计一套完整日志系统

    2021-05-20

    从YYModel源码分析JSON解析原理

    2021-06-03

【文末有惊喜!】iOS日历攻略:提醒调休并过滤法定节假日相关推荐

  1. 服务器进系统黑屏只有鼠标怎么办,【干货】开机进入系统桌面后只有一个鼠标(黑屏)怎么办(文末有惊喜)...

    原标题:[干货]开机进入系统桌面后只有一个鼠标(黑屏)怎么办(文末有惊喜) 由于现在7代处理器笔记本不支持win10以下版本的系统,用户对系统不熟悉且习惯使用某60安全卫士及某管家,但是这两款所谓的& ...

  2. 【粉丝专享福利】联合6大博主送出18本ChatGPT扫盲教程实体书,文末有惊喜

    文末一口气赠书18本, 这次就让你high个够. 人工智能技术的发展已经逐渐改变了我们的生活和工作方式,其中,语言模型技术是近年来关注度很高的一个领域.在这个领域,ChatGPT是一个备受瞩目的产品, ...

  3. 【文末有惊喜!】Hive SQL血缘关系解析与应用

    本文字数:7860字 预计阅读时间:20分钟 + 1 研究背景 随着企业信息化和业务的发展,数据资产日益庞大,数据仓库构建越来越复杂,在数仓构建的过程中,常遇到数据溯源困难,数据模型修改导致业务分析困 ...

  4. 四川大学计算机学院研究生宿舍,注意!这些高校全日制研究生也不提供住宿!(有些高校宿舍简直不堪入目,文末有惊喜!)...

    滴滴滴~我来啊 你们成为社畜以后才会知道,除了住在家里,毕竟学校的宿舍是最便宜的了,学校一般一年的住宿费都在1000-1500左右,但是你工作以后租房,像北京上海这些一线城市,你租房一个月可能得花上这 ...

  5. IOS面试攻略(1.0)

    来自:伊甸网 @ 看到这个关键字,我们就应该想到,这是Object-C对C语言的扩展,例如@interface XXX. @interface 声明类 @implementation 实现类 @pro ...

  6. iOS面试攻略,你必须拥有

    还在面试的时候感觉自己像一只无头苍蝇么?本文为大家整理了一系列iOS面试题,其中包括一些Objective-C的关键字和概念,少编也祝各位马到功成. @ 看到这个关键字,我们就应该想到,这是Objec ...

  7. 中国知网论文查重算法和修改攻略

    现在高校对于硕士和博士论文采用的检测系统,是由知网开发的.但该软件的具体算法,判定标准,以前一直不清楚, 本文是从知网内部工作人员哪里拿到的,揭示了知网反抄袭检测系统的算法,如何判定论文是抄袭,以及如 ...

  8. 文末有惊喜 | 开通微信公众号留言功能,只需3步!

    本篇推文共计800个字,阅读时间约1分钟. 微信公众号留言功能 在早期是都可以去开通获取的 直到2018年3月份 公众号留言功能被官方关闭 所以之后新注册的公众号都不再有留言功能 2018年3月公众号 ...

  9. 兄弟俩畅游Tomcat城市的SpringMVC科技园区(文末有惊喜)

    原创: 编程新说李新杰 编程新说 今天 Tomcat城市 Tomcat这座城市的历史相当悠久了,经历过几次大的变迁后,呈现出非常明显的地域特征. 从城市往西走,过了城乡结合部以后,可以说是满目疮痍.一 ...

最新文章

  1. 拖着3个箱子,跨越太平洋,求学美帝 那一年我19岁
  2. php 企业号文本消息推送,Python如何实现微信企业号文本消息推送功能的示例
  3. 阿里云物模型层功能分析
  4. 爬虫学习笔记(九)—— Scrapy框架(四):FormRequest、日志
  5. WinSock服务程序
  6. 汽车电子专业知识篇(三)-双目视觉三大应用视觉深度、标定、立体匹配
  7. c语言用宏定义常量_使用宏定义常量以在C的数组声明中使用
  8. web全栈架构师所需技术栈_统一架构–一种构建全栈应用程序的简单方法
  9. body里写注释 postman_是时候扔掉 Postman 了,试试 IntelliJ IDEA 自带的高能神器!
  10. django本地安装mysql_Ununtu 15.04 安装MySql(Django连接Mysql)
  11. Ubuntu安装SublimeText3
  12. 随机矩阵(stochastic matrix)与 PageRank
  13. 程序员必备算法——算法相关链接总结
  14. TOPcoder准备
  15. SpringSecurity简单集成
  16. Spring前一次定时任务没执行完,下次任务是否会执行
  17. 使用新版Mendeley自动插入参考文献,并修改得到GB/T 7714-2005格式
  18. MPLS中的标签信息库LIB和标签转发信息库LFIB + RIB/FIB + ARP/FDB + CAM/TCAM
  19. 面向中国企业关系抽取的双向门控递归单元神经网络
  20. 网络传输介质有哪几种

热门文章

  1. adf被打开_ADF格式文件 如何打开ADF文件 ADF是什么格式的文件 用什么打开 - The X Online Tools...
  2. Python根据字幕文件自动给视频添加字幕(通用版)
  3. 【语音控制ROS】PocketPhinx语音包的使用<三>
  4. 帧率(FPS)计算的六种方法总结
  5. 字符串的拼接需要间隔符的时候
  6. linux sh文件执行情况,Linux下SH执行
  7. 认证授权那点事儿 —— OAuth 2.0
  8. matlab元胞带索引的数组,Matlab-元胞数组的索引
  9. CSS性能优化的几个技巧
  10. 财务数据填报怎样做?用这个报表工具轻松搞定!_光点科技