iOS开发之EventKit框架的应用 一、关于系统日历和提醒事宜 iOS系统自带的Calendar应用非常强大,用户可以在其中添加日程事件,并且其提供了接口供其他应用进行调用,可以向日历中进行
iOS开发之EventKit框架的应用
一、关于系统日历和提醒事宜
iOS系统自带的Calendar应用非常强大,用户可以在其中添加日程事件,并且其提供了接口供其他应用进行调用,可以向日历中进行事件的读取和写入。
首先,Canlendar应用默认创建了几个类型的日历,用户也可以根据需要创建自定义的日历,如下图:
在上图中,US Holidays、Birthdays、Siri Found in Apps和Calendar是默认创建的几个日历,Custom是自定义的日历,当用户新建日历事件时,需要关联到某个日历,如下:
对于系统的Reminders,其主要作用是提供事件列表,用户可以向事件列表中添加提醒事件,同样,提供默认创建了两个事件列表,用户也可以根据需要自行创建新的事件列表,如下图:
使用日历和提醒事宜这两个应用,可以提高生活工作效率,例如邮件应用通过与日历的交互可以将会议邮件添加到用户的日程中,EventKit框架则是提供了接口与这两个应用进行交互。
二、EventKit框架概览
EventKit核心的用途是配合系统的日历与提醒应用,对事件提醒进行管理,其中核心类即结构如下:
从图中可以看出,重要数据的管理类为EKEventStore,其他类都是用来描述对应的数据,下面会一一介绍。
三、日历事件操作
第三方应用需要操作用户的日历事件,需要获取用户授权,首先需要在info.plist文件中添加如下权限请求字段:
获取当前Calendars应用中定义的日历示例代码如下:
#import "ViewController.h"#import <EventKit/EventKit.h>@interface ViewController ()@property (nonatomic, strong) EKEventStore *eventStore;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; // 初始化 self.eventStore = [[EKEventStore alloc] init]; // 请求权限 [self.eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * _Nullable error) { if (!granted) { NSLog(@"拒绝访问"); } else { // 获取所有日历 NSArray *calendars = [self.eventStore calendarsForEntityType:EKEntityTypeEvent]; for (EKCalendar *calendar in calendars) { NSLog(@"%@", calendar); } } }];}@end
上面方法会获取到所有的日历,也可以通过来源来获取日历,后面会具体介绍。
使用如下代码可以添加新的日历,通常,第三方应用如果要向用户日历中添加事件,可以先添加一个新的日历:
- (void)createNewCalendar { EKCalendar *calendar = [EKCalendar calendarForEntityType:EKEntityTypeEvent eventStore:self.eventStore]; for (EKSource *source in self.eventStore.sources) { if (source.sourceType == EKSourceTypeLocal) { // 设置来源为本地 calendar.source = source; } } calendar.title = @"珲少的事项日历";//自定义日历标题 calendar.CGColor = [UIColor purpleColor].CGColor;//自定义日历颜色 NSError* error; [_eventStore saveCalendar:calendar commit:YES error:&error];}
效果如下图:
日历事件的查询需要构造NSPredicate对象,示例如下:
- (void)queryEvent { for (EKCalendar *cal in [self.eventStore calendarsForEntityType:EKEntityTypeEvent]) { if ([cal.title isEqualToString:@"珲少的事项日历"]) { NSCalendar *calendar = [NSCalendar currentCalendar]; NSDateComponents *oneMonthFromNowComponents = [[NSDateComponents alloc] init]; oneMonthFromNowComponents.month = 1; NSDate *oneMonthFromNow = [calendar dateByAddingComponents:oneMonthFromNowComponents toDate:[NSDate date] options:0]; // 获取从今往后一个月的 NSPredicate*predicate = [self.eventStore predicateForEventsWithStartDate:[NSDate date] endDate:oneMonthFromNow calendars:@[cal]]; NSArray *eventArray = [self.eventStore eventsMatchingPredicate:predicate]; NSLog(@"%@", eventArray); } }}
通过存储的相关接口,也可以对某个事件进行修改,或创建新的事件,示例如下:
- (void)createEvent { EKCalendar *calen = nil; for (EKCalendar *cal in [self.eventStore calendarsForEntityType:EKEntityTypeEvent]) { if ([cal.title isEqualToString:@"珲少的事项日历"]) { calen = cal; } } EKEvent *event = [EKEvent eventWithEventStore:self.eventStore]; event.title = @"从应用创建的事件"; event.startDate = [NSDate date]; event.calendar = calen; NSCalendar *calendar = [NSCalendar currentCalendar]; NSDateComponents *oneMonthFromNowComponents = [[NSDateComponents alloc] init]; oneMonthFromNowComponents.hour += 1; NSDate *endDate = [calendar dateByAddingComponents:oneMonthFromNowComponents toDate:[NSDate date] options:0]; event.endDate = endDate; event.notes = @"备注"; [event setAllDay:NO];//设置全天 //保存事件 NSError *error = nil; [self.eventStore saveEvent:event span:EKSpanThisEvent commit:YES error:&error]; NSLog(@"%@",error);}
下面示例代码用来删除日历事件:
- (void)removeEvent { for (EKCalendar *cal in [self.eventStore calendarsForEntityType:EKEntityTypeEvent]) { if ([cal.title isEqualToString:@"珲少的事项日历"]) { NSCalendar *calendar = [NSCalendar currentCalendar]; NSDateComponents *oneMonthFromNowComponents = [[NSDateComponents alloc] init]; oneMonthFromNowComponents.month = 1; NSDate *oneMonthFromNow = [calendar dateByAddingComponents:oneMonthFromNowComponents toDate:[NSDate date] options:0]; // 获取从今往后一个月的 NSPredicate*predicate = [self.eventStore predicateForEventsWithStartDate:[NSDate date] endDate:oneMonthFromNow calendars:@[cal]]; NSArray *eventArray = [self.eventStore eventsMatchingPredicate:predicate]; [self.eventStore removeEvent:eventArray.firstObject span:EKSpanThisEvent error:nil]; } }}
三、提醒事件
提醒事件的用法和日历事件的用法基本一致,首先在Reminder应用中,每一个列表就是一个日历,下面代码示例了向列表中插入提醒事件的方法:
- (void)createNewReminder { EKReminder *reminder = [EKReminder reminderWithEventStore:self.eventStore]; reminder.title = @"从应用创建的事件"; reminder.notes = @"备注"; for (EKCalendar *cal in [self.eventStore calendarsForEntityType:EKEntityTypeReminder]) { if ([cal.title isEqualToString:@"自定义"]) { reminder.calendar = cal; } } //保存事件 NSError *error = nil; [self.eventStore saveReminder:reminder commit:YES error:&error]; NSLog(@"%@", error);}
同样,对应的也有Reminder的查询,删除等操作。
四、EKEventStore详解
EKEventStore类是EventKit中的核心类,用来对日历和提醒的事件进行操作。其中方法与属性列举如下:
@interface EKEventStore : NSObject// 获取用户授权情况+ (EKAuthorizationStatus)authorizationStatusForEntityType:(EKEntityType)entityType;// 初始化方法- (id)init;// 使用指定的来源创建实例对象- (instancetype)initWithSources:(NSArray<EKSource *> *)sources;// 请求某个类型的事件访问权限- (void)requestAccessToEntityType:(EKEntityType)entityType completion:(EKEventStoreRequestAccessCompletionHandler)completion;// 唯一标识@property(nonatomic, readonly) NSString *eventStoreIdentifier;// 代理源 使用initWithSources方法实例化对应@property (nonatomic, readonly) NSArray<EKSource *> *delegateSources;// 可用的来源@property (nonatomic, readonly) NSArray<EKSource *> *sources;// 通过identifier获取指定的来源对象- (nullable EKSource *)sourceWithIdentifier:(NSString *)identifier;// 通过类型来获取所有的日历对象- (NSArray<EKCalendar *> *)calendarsForEntityType:(EKEntityType)entityType;// 获取默认的日历对象@property(nullable, nonatomic, readonly) EKCalendar *defaultCalendarForNewEvents;- (nullable EKCalendar *)defaultCalendarForNewReminders;// 通过identifier获取日历对象- (nullable EKCalendar *)calendarWithIdentifier:(NSString *)identifier;// 添加一个新的日历,commit参数是都是否立刻提交修改- (BOOL)saveCalendar:(EKCalendar *)calendar commit:(BOOL)commit error:(NSError **)error;// 删除某个日历- (BOOL)removeCalendar:(EKCalendar *)calendar commit:(BOOL)commit error:(NSError **)error;// 根据identifier获取日程对象- (nullable EKCalendarItem *)calendarItemWithIdentifier:(NSString *)identifier;// 获取一组日程对象- (NSArray<EKCalendarItem *> *)calendarItemsWithExternalIdentifier:(NSString *)externalIdentifier;// 新建事件 span用来设置对于周期事件本次设置是否影响未来的事件- (BOOL)saveEvent:(EKEvent *)event span:(EKSpan)span error:(NSError **)error;- (BOOL)saveEvent:(EKEvent *)event span:(EKSpan)span commit:(BOOL)commit error:(NSError **)error;// 移出事件- (BOOL)removeEvent:(EKEvent *)event span:(EKSpan)span error:(NSError **)error;- (BOOL)removeEvent:(EKEvent *)event span:(EKSpan)span commit:(BOOL)commit error:(NSError **)error;// 通过identifier获取事件- (nullable EKEvent *)eventWithIdentifier:(NSString *)identifier;// 使用给定的NSPredicate进行事件查询- (NSArray<EKEvent *> *)eventsMatchingPredicate:(NSPredicate *)predicate;// 使用给定的NSPredicate进行事件枚举- (void)enumerateEventsMatchingPredicate:(NSPredicate *)predicate usingBlock:(EKEventSearchCallback)block;// 构造NSPredicate查询对象- (NSPredicate *)predicateForEventsWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate calendars:(nullable NSArray<EKCalendar *> *)calendars;// 提醒相关// 新建一条提醒- (BOOL)saveReminder:(EKReminder *)reminder commit:(BOOL)commit error:(NSError **)error;// 删除提醒- (BOOL)removeReminder:(EKReminder *)reminder commit:(BOOL)commit error:(NSError **)error;// 进行提醒查询- (id)fetchRemindersMatchingPredicate:(NSPredicate *)predicate completion:(void (^)(NSArray<EKReminder *> * __nullable reminders))completion;// 取消某此查询- (void)cancelFetchRequest:(id)fetchIdentifier;// 构造查询对象- (NSPredicate *)predicateForRemindersInCalendars:(nullable NSArray<EKCalendar *> *)calendars;- (NSPredicate *)predicateForIncompleteRemindersWithDueDateStarting:(nullable NSDate *)startDate ending:(nullable NSDate *)endDate calendars:(nullable NSArray<EKCalendar *> *)calendars;- (NSPredicate *)predicateForCompletedRemindersWithCompletionDateStarting:(nullable NSDate *)startDate ending:(nullable NSDate *)endDate calendars:(nullable NSArray<EKCalendar *> *)calendars;// 将所有的修改一次性进行提交- (BOOL)commit:(NSError **)error;// 重置所有的修改- (void)reset;// 刷新来源- (void)refreshSourcesIfNecessary;@end
EKEntityType用来描述类型,定义如下:
typedef NS_ENUM(NSUInteger, EKEntityType) { EKEntityTypeEvent, // 日历类型 EKEntityTypeReminder // 提醒类型};
EKAuthorizationStatus用来描述用户授权状态:
typedef NS_ENUM(NSInteger, EKAuthorizationStatus) { EKAuthorizationStatusNotDetermined = 0, // 用户尚未选择 EKAuthorizationStatusRestricted, // 应用无权访问 EKAuthorizationStatusDenied, // 用户拒绝授权 EKAuthorizationStatusAuthorized, // 用户授权} ;
EKSpan用来描述所影响事件类型:
typedef NS_ENUM(NSInteger, EKSpan) { EKSpanThisEvent, // 只影响本次事件 EKSpanFutureEvents // 对未来的事件也会产生影响};
五、EKSource类详解
首先,EKSource描述了,例如某些日历是系统默认创建的,用户没有权限进行修改或删除,某些日历是用户自定义创建的,还有些可能来自云端,这个类继承自EKObject类,首先先看EKObject类的定义:
@interface EKObject : NSObject// 数据对象是否有修改@property (nonatomic, readonly) BOOL hasChanges;// 是否是一个新的数据对象(为存储到日历或提醒中)- (BOOL)isNew;// 重置数据 将所有属性清空- (void)reset;// 回滚未提交的操作- (void)rollback;// 刷新数据- (BOOL)refresh;@end
EKSource是EKObject的子类,其中封装属性和方法如下:
@interface EKSource : EKObject// 来源对象的ID 唯一标识@property(nonatomic, readonly) NSString *sourceIdentifier;// 来源对象的类型@property(nonatomic, readonly) EKSourceType sourceType;// 来源对象的标题@property(nonatomic, readonly) NSString *title;// 此来源对象中所包含的日历对象@property(nonatomic, readonly) NSSet<EKCalendar *> *calendars;// 获取此来源对象中某个类型的日历 (日历或提醒)- (NSSet<EKCalendar *> *)calendarsForEntityType:(EKEntityType)entityType;@end
EKSourceType枚举如下:
typedef NS_ENUM(NSInteger, EKSourceType) { EKSourceTypeLocal, // 本地 EKSourceTypeExchange, // 交换 EKSourceTypeCalDAV, // CalDAV或iCloud EKSourceTypeMobileMe, // MobileMe EKSourceTypeSubscribed,// 订阅 EKSourceTypeBirthdays // 生日};
六、EKCalendar类详解
EKCalendar是具体的日历对象,开发者可以对自定义的日历进行读写操作,其也是继承自EKObject对象,解析如下:
@interface EKCalendar : EKObject// 通过制定的EKEventStore对象创建一个新的日历对象+ (EKCalendar*)calendarWithEventStore:(EKEventStore *)eventStore;// 创建某个类型的日历对象 (日历或提醒)+ (EKCalendar *)calendarForEntityType:(EKEntityType)entityType eventStore:(EKEventStore *)eventStore;// 当前日历对象所属于的来源@property(null_unspecified, nonatomic, strong) EKSource *source;// 日历对象的ID@property(nonatomic, readonly) NSString *calendarIdentifier;// 日历对象的标题 会在 日历 应用 或 提醒 应用中显示@property(nonatomic, copy) NSString *title;// 当前日历对象的类型/*typedef NS_ENUM(NSInteger, EKCalendarType) { EKCalendarTypeLocal, EKCalendarTypeCalDAV, EKCalendarTypeExchange, EKCalendarTypeSubscription, EKCalendarTypeBirthday};*/@property(nonatomic, readonly) EKCalendarType type;// 当前日历是否支持编辑@property(nonatomic, readonly) BOOL allowsContentModifications;// 当前日历是否为订阅类型@property(nonatomic, readonly, getter=isSubscribed) BOOL subscribed;// 当前日历对象是否可变@property(nonatomic, readonly, getter=isImmutable) BOOL immutable;// 对应的提示颜色@property(null_unspecified, nonatomic) CGColorRef CGColor;// 支持的事件状态/*typedef NS_OPTIONS(NSUInteger, EKCalendarEventAvailabilityMask) { EKCalendarEventAvailabilityNone = 0, // calendar doesn't support event availability EKCalendarEventAvailabilityBusy = (1 << 0), EKCalendarEventAvailabilityFree = (1 << 1), EKCalendarEventAvailabilityTentative = (1 << 2), EKCalendarEventAvailabilityUnavailable = (1 << 3),};*/@property(nonatomic, readonly) EKCalendarEventAvailabilityMask supportedEventAvailabilities;// 支持的日历类型/*typedef NS_OPTIONS(NSUInteger, EKEntityMask) { EKEntityMaskEvent = (1 << EKEntityTypeEvent), EKEntityMaskReminder = (1 << EKEntityTypeReminder)};*/@property(nonatomic, readonly) EKEntityMask allowedEntityTypes;@end
七、EKEvent与EKReminder
EKEvent与EKReminder类在设计上是平行的两个类,前面的示例代码中也有介绍。EKEvent对应系统日历中的事件,EKReminder对应系统提醒应用中的事件。他们都继承自EKCalendarItem类,EKCalendarItem也是EKObject的子类,这个类解析如下:
@interface EKCalendarItem : EKObject// UUID 事件的唯一标识@property(nonatomic, readonly) NSString *UUID;// 事件所属于的日历@property(nonatomic, strong, null_unspecified) EKCalendar *calendar;// 事件的ID@property(nonatomic, readonly) NSString *calendarItemIdentifier;// 事件的标题@property(nonatomic, copy, null_unspecified) NSString *title;// 地理位置@property(nonatomic, copy, nullable) NSString *location;// 备注信息@property(nonatomic, copy, nullable) NSString *notes;// 网址URL@property(nonatomic, copy, nullable) NSURL *URL;// 最后一次更改时间@property(nonatomic, readonly, nullable) NSDate *lastModifiedDate;// 事件创建的时间@property(nonatomic, readonly, nullable, strong) NSDate *creationDate;// 时区@property(nonatomic, copy, nullable) NSTimeZone *timeZone;// 是否有闹钟提醒@property(nonatomic, readonly) BOOL hasAlarms; // 是否包含重复提醒的规则@property(nonatomic, readonly) BOOL hasRecurrenceRules;// 是否包含参会人信息@property(nonatomic, readonly) BOOL hasAttendees;// 是否包含备注@property(nonatomic, readonly) BOOL hasNotes;// 参会人信息@property(nonatomic, readonly, nullable) NSArray<__kindof EKParticipant *> *attendees;// 闹钟提醒信息@property(nonatomic, copy, nullable) NSArray<EKAlarm *> *alarms;// 添加闹钟规则- (void)addAlarm:(EKAlarm *)alarm;// 删除闹钟规则- (void)removeAlarm:(EKAlarm *)alarm;// 包含的定期规则@property(nonatomic, copy, nullable) NSArray<EKRecurrenceRule *> *recurrenceRules;// 添加定期规则- (void)addRecurrenceRule:(EKRecurrenceRule *)rule;// 删除定期规则- (void)removeRecurrenceRule:(EKRecurrenceRule *)rule;@end
上面类中有引用到一些其他的核心类,如EKParticipant、EKAlarm和EKRecurrenceRule,后面会介绍。
EKEvent是EKCalendarItem的子类,其对应日历应用中的事件,解析如下:
@interface EKEvent : EKCalendarItem// 创建一个新的事件对象+ (EKEvent *)eventWithEventStore:(EKEventStore *)eventStore;// 系统分类的事件ID@property(null_unspecified, nonatomic, readonly) NSString *eventIdentifier;// 是否是一个全天事件@property(nonatomic, getter=isAllDay) BOOL allDay;// 事件开始日期@property(null_unspecified, nonatomic, copy) NSDate *startDate;// 事件结束日期@property(null_unspecified, nonatomic, copy) NSDate *endDate;// 触发型事件 到达某个地理位置后触发@property(nonatomic, copy, nullable) EKStructuredLocation *structuredLocation;// 与另一个事件的开始事件进行比较- (NSComparisonResult)compareStartDateWithEvent:(EKEvent *)other;// 事件的组织者@property(nonatomic, readonly, nullable) EKParticipant *organizer;// 事件行为状态@property(nonatomic) EKEventAvailability availability;// 事件确认状态/*typedef NS_ENUM(NSInteger, EKEventStatus) { EKEventStatusNone = 0, EKEventStatusConfirmed, EKEventStatusTentative, EKEventStatusCanceled,};*/@property(nonatomic, readonly) EKEventStatus status;// 事件是否是独立的@property(nonatomic, readonly) BOOL isDetached;// 事件发生时间@property(null_unspecified, nonatomic, readonly) NSDate *occurrenceDate;// 刷新- (BOOL)refresh;// 对于生日事件 对应的生日联系人ID@property(nonatomic, readonly, nullable) NSString *birthdayContactIdentifier;@end
EKReminder与EKEvent类似,其对应系统的提醒事宜应用中的事件:
@interface EKReminder : EKCalendarItem// 新建提醒事件+ (EKReminder *)reminderWithEventStore:(EKEventStore *)eventStore;// 开始时间@property(nonatomic, copy, nullable) NSDateComponents *startDateComponents;// 发生时间@property(nonatomic, copy, nullable) NSDateComponents *dueDateComponents;// 是否已完成@property(nonatomic, getter=isCompleted) BOOL completed;// 完成时间@property(nonatomic, copy, nullable) NSDate *completionDate;// 优先级 1-4 为高 5 为中 6-9为低@property(nonatomic) NSUInteger priority;@end
八、EKAlarm与EKStructuredLocation
EKAlarm对应闹钟对象,事件可以绑定闹钟提醒,解析如下:
@interface EKAlarm : EKObject// 通过绝对时间创建闹钟+ (EKAlarm *)alarmWithAbsoluteDate:(NSDate *)date;// 通过与现在的相对事件创建闹钟+ (EKAlarm *)alarmWithRelativeOffset:(NSTimeInterval)offset;// 与现在的相对时间@property(nonatomic) NSTimeInterval relativeOffset;// 绝对时间@property(nonatomic, copy, nullable) NSDate *absoluteDate;// 地理位置触发的闹钟,对应的地理位置@property(nonatomic, copy, nullable) EKStructuredLocation *structuredLocation;// 触发方式/*typedef NS_ENUM(NSInteger, EKAlarmProximity) { EKAlarmProximityNone, EKAlarmProximityEnter, // 进入触发条件 EKAlarmProximityLeave // 离开触发条件};*/@property(nonatomic) EKAlarmProximity proximity;// 闹钟类型/*typedef NS_ENUM(NSInteger, EKAlarmType) { EKAlarmTypeDisplay, EKAlarmTypeAudio, EKAlarmTypeProcedure, EKAlarmTypeEmail};*/@property(nonatomic, readonly) EKAlarmType type;// 提醒的邮件地址@property(nonatomic, copy, nullable) NSString *emailAddress;// 提醒的音效名称@property(nonatomic, copy, nullable) NSString *soundName;// 提醒后打开的网页@property(nonatomic, copy, nullable) NSURL *url;@end
EKStructuredLocation用来描述地区区域结构,如下:
@interface EKStructuredLocation : EKObject// 通过标题创建+ (instancetype)locationWithTitle:(NSString *)title;// 通过地图点创建+ (instancetype)locationWithMapItem:(MKMapItem *)mapItem NS_AVAILABLE(10_11, 9_0);// 标题@property(nullable, nonatomic, strong) NSString *title;// 经纬度地理信息@property(nonatomic, strong, nullable) CLLocation *geoLocation;// 半径@property(nonatomic) double radius;@end
九、EKParticipant类详解
EKParticipant类是与事件相关的联系人数据类,解析如下:
@interface EKParticipant : EKObject// 网址@property(nonatomic, readonly) NSURL *URL;// 姓名@property(nonatomic, readonly, nullable) NSString *name;// 状态/*typedef NS_ENUM(NSInteger, EKParticipantStatus) { EKParticipantStatusUnknown, // 未知 EKParticipantStatusPending, // 等待处理 EKParticipantStatusAccepted, // 接收 EKParticipantStatusDeclined, // 拒绝 EKParticipantStatusTentative,// 尝试 EKParticipantStatusDelegated,// 代理 EKParticipantStatusCompleted,// 完成 EKParticipantStatusInProcess // 进行中};*/@property(nonatomic, readonly) EKParticipantStatus participantStatus;// 人员的角色/*typedef NS_ENUM(NSInteger, EKParticipantRole) { EKParticipantRoleUnknown, // 未知 EKParticipantRoleRequired, // 必要的 EKParticipantRoleOptional, // 可选参与的 EKParticipantRoleChair, // 主持人 EKParticipantRoleNonParticipant // 参与者};*/@property(nonatomic, readonly) EKParticipantRole participantRole;// 参与人类型/*typedef NS_ENUM(NSInteger, EKParticipantType) { EKParticipantTypeUnknown, EKParticipantTypePerson, EKParticipantTypeRoom, EKParticipantTypeResource, EKParticipantTypeGroup};*/@property(nonatomic, readonly) EKParticipantType participantType;// 是否是当前用户@property(nonatomic, readonly, getter=isCurrentUser) BOOL currentUser;// 用来查找联系人的NSPredicate对象@property(nonatomic, readonly) NSPredicate *contactPredicate;