iOS EventKit日历提醒

EKEventStore

EKEventStore用于应用访问日历或者提醒事件,在iOS中,创建好实例之后,必须使用requestAccessToEntityType:completion:方法访问实体类型。

与一个EKEventStore实例相关联的事件,提醒,日历数据无法在另一个EKEventStore实例中使用,所以你最好创建一个常驻的实例,例如创建一个单例。

在iOS10.0或者之后的版本,你必须在info.plist文件中添加权限请求,否则应用将会崩溃。提醒和日历分别是NSRemindersUsageDescriptionNSCalendarsUsageDescription

在macOS中,使用initWithAccessToEntityTypes:代替默认初始化方法,可以接受的实体类型是EKEntityMaskEventEKEntityMaskReminder

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建实例
EKEventStore *eventStore = [[EKEventStore alloc] init];
self.eventStore = eventStore;

// 获取数据
[self.eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
NSLog(@"授权通过");
// 下一步操作...
} else {
NSLog(@"授权没有通过");
}
}];

获取授权状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 获取授权状态
EKAuthorizationStatus eventStatus = [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent];
switch (eventStatus) {
case EKAuthorizationStatusNotDetermined:// 未进行选择
{
// 弹框 提示授权
@weakify(self)
[self.eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * _Nullable error) {
@strongify(self)
if (granted) {
NSLog(@"授权通过");
// 下一步操作...
} else {
NSLog(@"授权没有通过");
}
}];
}break;
case EKAuthorizationStatusRestricted:// 未授权
{
NSLog(@"未授权");
}break;
case EKAuthorizationStatusDenied:// 拒绝
{
NSLog(@"拒绝");
}break;
case EKAuthorizationStatusAuthorized:// 已授权
{
NSLog(@"已授权");
}break;
default:break;
}

EKSource 日历源

你不需要创建此类的实例; 相反,你从EKEventStore实例中获取EKSource对象。 使用sources属性获取事件存储的所有EKSource对象,并使用此类中的实例方法访问该对象。

EKSource为日历所属的集合的抽象超类。一个EKEventStore可以包含多个EKSource,一个日历源可以包含多个日历对象。

日历的类型:

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSInteger, EKSourceType) {
EKSourceTypeLocal,
EKSourceTypeExchange,
EKSourceTypeCalDAV,//Represents a CalDAV or iCloud source
EKSourceTypeMobileMe,
EKSourceTypeSubscribed,
EKSourceTypeBirthdays
};

真机中的日历源

获取指定的日历源

1
2
3
4
5
6
7
NSArray *sources = self.eventStore.sources;
for (EKSource *source in sources) {
if (source.sourceType == EKSourceTypeCalDAV) {
// 当前source的类型为EKSourceTypeCalDAV,你还可以使用title属性进行筛选
break;
}
}

EKCalendar 日历

  • type 这个属性跟所属于的EKSource实例的type对应
  • calendarIdentifier,我们可以根据这个属性,使用EKEventStore的实例方法来获取指定的EKCalendar对象。需要注意的是,当发生了完全同步之后,这个值是会变化的。

会发生完全同步的几种情况:

  • 在iPhone上,用户手动修改了日历的名字,添加/编辑/删除事件
  • 用户在mac上对iCloud日历进行了一些修改,iOS设备会通知iCloud日历已更改,从而发生同步
  • 第三方应用程序收到日历通知,iOS在后台启动,应用根据通知创建一些日历事件

也就是说,完全同步事件可以随时发生。当发生完全同步时,你可以通过下面的代码获得通知

1
2
3
4
5
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(storeChanged:)
name:EKEventStoreChangedNotification
object:eventStore];

如果需要的话,收到通知之后,重新创建EKCalendar实例是有意义的。

创建添加日历

创建日历之前,我们先要找到它所属于的日历源(当然这取决与你想把它放在哪个日历源)。创建时,我们先要判断是否已经有相同名字的日历存在,如果没有的话,则继续创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (void)checkJYCalendar
{
NSArray *sources = self.eventStore.sources;
for (EKSource *source in sources) {
if (source.sourceType == EKSourceTypeCalDAV) {

NSSet *calendars = [source calendarsForEntityType:EKEntityTypeEvent];
BOOL needAdd = true;

for (EKCalendar *calendar in calendars) {
if ([calendar.title isEqualToString:@"测试日历"]) {
needAdd = false;
_calendar = calendar;
break;
}
}

// 如果没找到日历,则创建一个
if (needAdd) {
EKCalendar *calendar = [EKCalendar calendarForEntityType:EKEntityTypeEvent eventStore:self.eventStore];
calendar.source = source;
calendar.title = @"测试日历";
calendar.CGColor = [UIColor greenColor].CGColor;
NSError *error;
[self.eventStore saveCalendar:calendar commit:YES error:&error];

if (error) {
JYLog(@"创建日历失败 error:%@",error);
} else {
JYLog(@"创建日历成功");
}
_calendar = calendar;
}
break;
}
}
}

获取日历集

可以用下面两种实例方法获取日历数据

1
2
3
4
5
6
// EKEventStore的实例方法
- (NSArray<EKCalendar *> *)calendarsForEntityType:(EKEntityType)entityType NS_AVAILABLE(10_8, 6_0);

// EKSource的实例方法
- (NSSet<EKCalendar *> *)calendarsForEntityType:(EKEntityType)entityType NS_AVAILABLE(10_8, 6_0);

EkEvent 日历事件

代表添加到日历中的事件。使用类方法eventWithEventStore:来创建实例

属性说明

  • eventIdentifier:您可以使用此标识符使用EKEventStore方法eventWithIdentifier:查找事件。
    如果事件的日历发生更改,则其标识符很可能也会更改。
  • allDay:设置是否是全天时间
  • startDate:事件开始时间
  • endDate:事件结束时间
  • structuredLocation:往事件里添加地理信息位置,get方法仅返回地理位置的名称

如果你创建了一个日历事件,并且设置了重复规则:从2019-01-01开始,连续10天,每天9:00-9:30。这就就相当于你创建了10个日历事件!对每天的日历事件来说,9:00即·startDate·,9:30即·endDate·,也不是说2019-01-10 9:30是·endDate·!

事件修改

当你对EKEvent的属性修改完成之后,可以用EKEventStore的方法- (BOOL)saveEvent:(EKEvent *)event span:(EKSpan)span commit:(BOOL)commit error:(NSError **)error进行保存

  • EKSpan有两种类型,EKSpanThisEvent和EKSpanFutureEvents

举个例子,你创建了一个·EkEvent·对象,并且添加到了日历中了。它的规则为:连续10天,每天的早上9:00- 9:30。当你使用·EKEventStore·的方法·eventsMatchingPredicate:·查找时间时,就会得到10个·EKEventStore·对象,如果是事件发生顺序的快慢来编号(1-10),那么对于第一个·EkEvent·对象讲,·EKSpanThisEvent·指的是自己,而·EKSpanFutureEvents·指的是编号为(1-10)的几个对象;对于第二个·EkEvent·对象讲,·EKSpanThisEvent·指的是自己,而·EKSpanFutureEvents·指的是编号为(2-10)的几个对象。总之,最好自己尝试一下看看这到底是什么。

事件刷新

当在其它地方对事件进行了修改之后,你可以收到EKEventStoreChangedNotification的通知,这个时候你可以选择使用实例方法refresh,更新事件的数据

在日历中添加事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
添加事件到日历中
*/
- (BOOL)addEventToCalendarWithTitle:(NSString *)title
location:(NSString *)location
startDate:(NSDate *)start
endDate:(NSDate *)end
alarmArray:(NSArray *)alarmArray
{
EKEvent *newEvent = [EKEvent eventWithEventStore:self.eventStore];

newEvent.title = title;
newEvent.startDate = start;
newEvent.endDate = end;
newEvent.location = location;
newEvent.notes = @"这是备注";
newEvent.allDay = false;
// 重复规则, 具体代码在下面会讲到
EKRecurrenceRule *rule = [self getRecurrenceRule];
// 设置提醒,具体代码在下面会讲到
[newEvent addAlarm:[self getAlarm]];
newEvent.calendar = self.calendar;

NSError *error = nil;
[self.eventStore saveEvent:newEvent span:EKSpanThisEvent commit:YES error:&error];

if (isNull(error)) {
JYLog(@"添加Event成功");
return YES;
} else {
JYLog(@"添加Event失败 error:%@", error);
return NO;
}
}

EKRecurrenceRule重复周期

目前无法直接修改EKRecurrenceRule或其任何属性。 通过创建新的EKRecurrenceRule并设置事件或提醒以使用新创建的重复规则。

一般使用方法下面的方法来创建实例:

1
2
3
4
5
6
7
8
9
- (instancetype)initRecurrenceWithFrequency:(EKRecurrenceFrequency)type
interval:(NSInteger)interval
daysOfTheWeek:(nullable NSArray<EKRecurrenceDayOfWeek *> *)days
daysOfTheMonth:(nullable NSArray<NSNumber *> *)monthDays
monthsOfTheYear:(nullable NSArray<NSNumber *> *)months
weeksOfTheYear:(nullable NSArray<NSNumber *> *)weeksOfTheYear
daysOfTheYear:(nullable NSArray<NSNumber *> *)daysOfTheYear
setPositions:(nullable NSArray<NSNumber *> *)setPositions
end:(nullable EKRecurrenceEnd *)end;
  • type: 重复规则的频率,可以是每天,每周,每周和每年
  • interval: 周期间隔,必须大于0
  • days: 事件发生在一周的哪几天,是一个包含了EKRecurrenceDayOfWeek对象的数组
  • monthDays: 事件发生在一月的哪几天,值必须在131或者-1-31之间,当为-1时,即代表这个月的倒数第一天,这个参数仅当typeEKRecurrenceFrequencyMonthly有效
  • months: 事件发生在一年的哪几月,值必须在112或者-1-12之间,当为-1时,即代表这年的倒数第一个月,这个参数仅当typeEKRecurrenceFrequencyYearly有效
  • weeksOfTheYear: 事件发生在一年的哪几周,值必须在153或者-1-53之间,当为-1时,即代表这年的倒数第一周,这个参数仅当typeEKRecurrenceFrequencyYearly有效
  • daysOfTheYear: 事件发生在一年的哪几天,值必须在1366或者-1-366之间,当为-1时,即代表这年的倒数第一天,这个参数仅当typeEKRecurrenceFrequencyYearly有效
  • setPositions: 额外的重复条件
  • end: 结束规则,为空时即代表永久重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
EKRecurrenceRule *rule = nil;

// 每两天执行一次
rule = [[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyDaily interval:2 end:nil];

// 工作日都执行
rule = [[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyWeekly interval:1 daysOfTheWeek:@[[EKRecurrenceDayOfWeek dayOfWeek:2], [EKRecurrenceDayOfWeek dayOfWeek:3], [EKRecurrenceDayOfWeek dayOfWeek:4], [EKRecurrenceDayOfWeek dayOfWeek:5], [EKRecurrenceDayOfWeek dayOfWeek:6]] daysOfTheMonth:nil monthsOfTheYear:nil weeksOfTheYear:nil daysOfTheYear:nil setPositions:nil end:nil];

// 每两周的周1执行
rule = [[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyWeekly interval:2 daysOfTheWeek:@[[EKRecurrenceDayOfWeek dayOfWeek:2]] daysOfTheMonth:nil monthsOfTheYear:nil weeksOfTheYear:nil daysOfTheYear:nil setPositions:nil end:nil];

// 在每年的第二周以及最后一周的工作执行
rule = [[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyYearly interval:1 daysOfTheWeek:@[[EKRecurrenceDayOfWeek dayOfWeek:2], [EKRecurrenceDayOfWeek dayOfWeek:3], [EKRecurrenceDayOfWeek dayOfWeek:4], [EKRecurrenceDayOfWeek dayOfWeek:5], [EKRecurrenceDayOfWeek dayOfWeek:6]] daysOfTheMonth:nil monthsOfTheYear:nil weeksOfTheYear:nil daysOfTheYear:nil setPositions:@[@2, @-1] end:nil];

EKEvent *event = [[EKEvent alloc] init];
[event addRecurrenceRule:rule];

EKAlarm创建提醒

EKAlarmEvent Kit中代表警告。你可以使用alarmWithAbsoluteDatealarmWithAbsoluteDate来创建实例。

可以用下面的方法来创建实例

1
2
3
4
// 设置绝对时间
+ (EKAlarm *)alarmWithAbsoluteDate:(NSDate *)date;
// 设置相对时间(相对event的start date),单位是秒,设置负值表示事件前提醒,设置正值是事件发生后提醒
+ (EKAlarm *)alarmWithRelativeOffset:(NSTimeInterval)offset;

设置了提醒后,我们打开iOS系统自带的日历App,会发现只会显示2个提醒,看不到多余的提醒.但是实际测试发现全部提醒都可以工作,而且我们可以在Mac的日历程序中看到所有的提醒。

属性说明:

  • structuredLocation:与proximity配合使用,简单来说,就是当你进入或者离开某个范围时,就可以发出提醒
1
2
3
4
EKAlarm *alarm = nil;

// 事件前5分钟提醒
alarm = [EKAlarm alarmWithRelativeOffset:-60. * 5];

结束

目前我对系统的日历讲解就到这啦,受限于个人的技术水平以及没有深度的使用,可能会有点错误的地方,希望指正,以后我发现错误之后也会及时来更新的!

作者

千行

发布于

2019-03-01

更新于

2022-10-21

许可协议

评论