Cookbook: 制作一个日历提醒事项


原文地址:http://www.raywenderlich.com/64513/cookbook-making-calendar-reminder
泰然翻译组:takalard。校对:glory。

在这个cookbook-style教程中,你将学会知道如何程序化的获取用户提醒事项。

如果你事先对提醒事项熟悉的话,考虑阅读 Apple documentation
苹果文档关于提醒事项的部分。通过苹果,备忘录允许你“组织一个生活中即将要做的事情的列表及完成的时间日期和地点”。

从下载 starter project.
开始吧。

如何开始?

  • EventKit framework

  • UITableView

  • UITableViewController

授权

为了让提醒事项和日历事件能工作起来,你需要依赖于EventKit。你将也需要一个持久化的存储来保存备忘录项。因此,EventKit为你提供了这个:EKEventStore。一个EKEventStore允许你从用户日历数据库中更新、创建、编辑和删除事件。

提醒事项和日历数据都存储在日历数据库。在理想情况下,你整个应用将只有一个事件存储器,而且你只能实例化其一次,那就是EKEventStore对象需要一个相对比较长的时间去初始化和释放的原因。为每个事件相关的任务去初始化和释放每一个单独分离的事件存储器是极无效率的一件事情,因此,你需要一个单独的事件存储器保证到你应用能运行多久,其就能工作到多久!

你可以看到提醒事项和日历都存储在同一个持久化存储器中,这就是常常指的“日历数据库”。在这个教程中,当你所看到日历,也就意味着日历和提醒事项。但你须知程序员其实都是懒惰的,我们将二者都缩写为日历:]

在你能够获取到用户日历之前你必须获取到用户的日历数据库。iOS将会提醒用户允许或者拒绝日历信息的使用,大多数情况下都是当应用即将要用到日历数据库的时候提醒用户。

在下面的starter project中的RWTableViewController.m的修改部分,viewDidLoad最后触发了获取请求。在注释的第一行,@import EventKit;,使得应用必须依赖于EventKit以及使得框架类可以在RWTableViewController.m中使用。

打开RWTableViewController.m然后用以下替换@interface部分:

@import EventKit;

@interface RWTableViewController ()

// The database with calendar events and reminders
@property (strong, nonatomic) EKEventStore *eventStore;

// Indicates whether app has access to event store.
@property (nonatomic) BOOL isAccessToEventStoreGranted;

// The data source for the table view
@property (strong, nonatomic) NSMutableArray *todoItems;

@end

然后加入下面两个方法:

// 1
- (EKEventStore *)eventStore {
    if (!_eventStore) {
        _eventStore = [[EKEventStore alloc] init];
    }
    return _eventStore;
}

- (void)updateAuthorizationStatusToAccessEventStore {
    // 2  
    EKAuthorizationStatus authorizationStatus = [EKEventStore authorizationStatusForEntityType:EKEntityTypeReminder];

    switch (authorizationStatus) {
    // 3
    case EKAuthorizationStatusDenied:
    case EKAuthorizationStatusRestricted: {
        self.isAccessToEventStoreGranted = NO;
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Access Denied"
        message:@"This app doesn't have access to your Reminders." delegate:nil
    cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];
        [alertView show];
        [self.tableView reloadData];
    break;
    }

    // 4
    case EKAuthorizationStatusAuthorized:
        self.isAccessToEventStoreGranted = YES;
        [self.tableView reloadData];
    break;

    // 5  
    case EKAuthorizationStatusNotDetermined: {
        __weak RWTableViewController *weakSelf = self;
        [self.eventStore requestAccessToEntityType:EKEntityTypeReminder
                                  completion:^(BOOL granted, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            weakSelf.isAccessToEventStoreGranted = granted;
            [weakSelf.tableView reloadData];
            });
        }];
    break;
    }
    }
}

最后从viewDidLoad中调用updateAuthorizationStatusToAccessEventStore

[self updateAuthorizationStatusToAccessEventStore];

因此这里将会发生什么情况呢?

  1. 这仅仅是eventStore的一个基本简单化的实例化。

  2. 使用EKEventStore's的类方法authorizationStatusForEntityType:,询问系统现在的事件存储器的授权状况。你将EKEntityTypeReminder转换成你获取提醒事项时提示你需要允许的事件类型,然后再评估不同的场景。

  3. EKAuthorizationStatusDenied的意思是用户明确的拒绝了你应用的服务请求;EKAuthorizationStatusRestricted的意思是可能由于活动的限制,如父类的控制而使得你的应用没有获取服务的授权。在上面的两种情况下你不能做任何事情,而只能仅仅是显示一个alert view和通知用户。

  4. EKAuthorizationStatusAuthorized意思是你的应用有获取数据库的授权,然后你可以对数据库进行读或写操作。

  5. EKAuthorizationStatusNotDetermined意思是你仍然没有通过授权或者你的用户仍然没有做出觉得。请求授权的话可以在你一旦实例化EKEventStore时调用requestAccessToEntityType:completion:方法来进行。需要说明的是requestAccessToEntityType:completion:完成句柄不会在主队列中调用,但是UI调用(如reloadData)却只能在主线程中执行。起始于dispatch_async的这行封装了reloadData,会在主线程执行。

编译运行工程,你将会立即看到一个警告,询问用户是否要给备忘录一个授权。

注释:如果你想知道当你将应用放在后台时将会发生什么,去设置>隐私>提醒事项唤醒你应用的授权,答案是iOS会立即终止你的应用!这个听上去是很残酷的,但这就是现实。

加入一个提醒事项项

假定用户已经授权你的应用,你可以加入一个提醒事项项到数据库中。为了更好的用户体验,你也许想让你应用的提醒事项保存在一个单独的列表中。下面RWTableViewController.m的修改部分演示了这种方法。
RWTableViewController.m加入一个私有属性:

@property (strong, nonatomic) EKCalendar *calendar;

简单的实例化该属性:

- (EKCalendar *)calendar {
    if (!_calendar) {

    // 1
    NSArray *calendars = [self.eventStore calendarsForEntityType:EKEntityTypeReminder];

    // 2
    NSString *calendarTitle = @"UDo!";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title matches %@", calendarTitle];
    NSArray *filtered = [calendars filteredArrayUsingPredicate:predicate];

    if ([filtered count]) {
        _calendar = [filtered firstObject];
    } else {

        // 3
        _calendar = [EKCalendar calendarForEntityType:EKEntityTypeReminder eventStore:self.eventStore];
        _calendar.title = @"UDo!";
        _calendar.source = self.eventStore.defaultCalendarForNewReminders.source;

        // 4
        NSError *calendarErr = nil;
        BOOL calendarSuccess = [self.eventStore saveCalendar:_calendar commit:YES error:&calendarErr];
        if (!calendarSuccess) {
            // Handle error
        }
    }
    }
    return _calendar;
}

如果你只是简单的想“不是很多工作”,好的,那么就错了 :] 这部分代码所做何事?

  1. 首先你得找出所有可以获取到的提醒事项列表。

  2. 然后你需要筛选所有名为UDo!的列表数组,然后返回该名字的第一个列表。

  3. 如果没有任何名叫UDo!的列表,那么创建一个新的。

  4. 为了保证你的提醒事项在一个单独的列表中,你需要一个EKCalendar实例化。你不得不通过指定一个类型、事件存储器以及指定的源来实例化一个EKCalendar。一个日历源代表了一个归属于其帐号的日历。对于同一个源简单的使用就是你事件存储器已经存在的defaultCalendarForNewReminders属性;defaultCalendarForNewReminders是EKEventStore属性上的一个实用的只读属性,不管是从用户的iCloud帐号还是基于用户本地设置的设备,都返回的是一个EKCalendar对象。

现在你需要一个方法去添加一个提醒事项到事件存储器。加入以下函数到RWTableViewController.m

- (void)addReminderForToDoItem:(NSString *)item {
    // 1
    if (!self.isAccessToEventStoreGranted)
        return;

    // 2
    EKReminder *reminder = [EKReminder reminderWithEventStore:self.eventStore];
    reminder.title = item;
    reminder.calendar = self.calendar;

    // 3
    NSError *error = nil;
    BOOL success = [self.eventStore saveReminder:reminder commit:YES error:&error];
    if (!success) {
        // Handle error.
    }

    // 4
    NSString *message = (success) ? @"Reminder was successfully added!" : @"Failed to add reminder!";
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:message delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Dismiss", nil];
    [alertView show];
}

虽然上面看似很多的代码,但其实际上是相当的言简意赅:

  1. 如果用户没有获取到该事件存储器的授权会首先返回。

  2. 创建一个提醒事项。一个提醒事项项是一个EKReminder实例化。最基础的你必须设置EKReminder的两个属性:title和calendar。

  3. 存储该提醒事项到事件存储器中,如果有错误则处理错误。

  4. 用一个提醒(alert)的形式给用户一些反馈。

你已经有一个方法增加一个提醒事项,但你仍需要调用哪个方法。仍然在RWTableViewController.m中找到注释// Add the reminder to the store in tableView:cellForRowAtIndexPath:,然后用如下代替:
[self addReminderForToDoItem:object];

编译并运行工程,你将可以通过点击任何一个需要做的单项的'Add Reminder'来添加一个提醒事项。

取得提醒事项

当你的应用已经取得一个用户的日历数据库的授权,如果以你一个提醒事项,你即可以通过使用声明来取得设定的提醒事项,也可以通过使用唯一标识符来取得一个特定的提醒事项。下面RWTableViewController.m中的修改部分演示了该过程:
首先,加入另外一个属性:

@property (copy, nonatomic) NSArray *reminders;

创建 fetchReminders:

- (void)fetchReminders {  
    if (self.isAccessToEventStoreGranted) {    
    // 1
    NSPredicate *predicate =
  [self.eventStore predicateForRemindersInCalendars:@[self.calendar]];

    // 2
    [self.eventStore fetchRemindersMatchingPredicate:predicate completion:^(NSArray *reminders) 
    {      
        // 3      
        self.reminders = reminders;      
        dispatch_async(dispatch_get_main_queue(), ^{
            // 4
            [self.tableView reloadData];
        });
    }];
    }
}

fetchReminders所做何事?

  1. predicateForRemindersInC
    alendars: 通过这个方法返回一个日历的声明,在你的例子中,就是self.calendar。

  2. 这里可以取得匹配前一步骤中创建的所有提醒事项的声明。

  3. 在这个完成的模块中,你可以将返回的提醒事项数组存储在一个私有属性中。

  4. 在取出所有的提醒事项后,重新加载列表视图(当然,这是在主线程中操作)。

接下来,在viewDidLoad中加入对fetchReminders的调用,所以当视图在第一时间加载后,所有的提醒事项已经被立即加载。也要在viewDidLoad中注册EKEventStoreChangedNotification:

- (void)viewDidLoad {
    // some code...

    [self fetchReminders];
    [[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(fetchReminders) name:EKEventStoreChangedNotification object:nil];

    // the rest of the code...
}

然后,作为一个好的程序员,你当然要尽可能的你自己的观察者:

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

EKEventStoreChangedNotification通知投递在任何日历数据库有变化时,例如一个提醒事项被添加、删除等。当你接受到该通知时,你需要取出所有你有的EKReminder对象,因为他们被认为是已过时的。

既然你可以取出提醒事项,当你在列表视图中增加一项需要做的单项时,是时候该在列表视图中隐藏该单项的“Add Reminder”按钮。

为了这样做,创建一个新的方法itemHasReminder:

- (BOOL)itemHasReminder:(NSString *)item {
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title matches %@", item];
    NSArray *filtered = [self.reminders filteredArrayUsingPredicate:predicate];
    return (self.isAccessToEventStoreGranted && [filtered count]);
}

tableView:cellForRowAtIndexPath:的修改如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *kIdentifier = @"Cell Identifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kIdentifier forIndexPath:indexPath];

    // Update cell content from data source.
    NSString *object = self.todoItems[indexPath.row];
    cell.backgroundColor = [UIColor whiteColor];
    cell.textLabel.text = object;

    if (![self itemHasReminder:object]) {
    // Add a button as accessory view that says 'Add Reminder'.
        UIButton *addReminderButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        addReminderButton.frame = CGRectMake(0.0, 0.0, 100.0, 30.0);
        [addReminderButton setTitle:@"Add Reminder" forState:UIControlStateNormal];

        [addReminderButton addActionblock:^(UIButton *sender) {
            [self addReminderForToDoItem:object];
        } forControlEvents:UIControlEventTouchUpInside];

        cell.accessoryView = addReminderButton;
    } else {
        cell.accessoryView = nil;
    }

    return cell;
}

tableView:cellForRowAtIndexPath: 保留了大部分相同的,除非你将其包含在一个条件表达式中,该表达式是检查是否一个单项已经有一个提醒事项以及如果其已经执行了,那么其不会显示“Add Reminder” 按钮。

编译并运行,为一小部分你需要做的事件项添加提醒事项。你将可以看到你已经添加了提醒事项的事件项没有显示“Add Reminder” 按钮,因为提醒事项的通知正在观察,任何新的添加了提醒事项的事件项对应的按钮都会消失。

你可以下载answer here at GitHub

删除一个提醒事项

这是目前最为简单的任务。修改RWTableViewController.m中的方法,以便当一个事件项列删除了,任何与之对应的提醒事项也被删除。

加入deleteReminderForToDoItem:然后实现如下:

- (void)deleteReminderForToDoItem:(NSString *)item {
    // 1
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title matches %@", item];
    NSArray *results = [self.reminders filteredArrayUsingPredicate:predicate];

    // 2
    if ([results count]) {
        [results enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            NSError *error = nil;
            // 3
            BOOL success = [self.eventStore removeReminder:obj commit:NO error:&error];
            if (!success) {
                // Handle delete error
            }
        }];

        // 4
        NSError *commitErr = nil;
        BOOL success = [self.eventStore commit:&commitErr];
        if (!success) {
            // Handle commit error.
        }
    }
}
  1. 找到已过时的EKReminder(s);

  2. 检查是否有任何提醒事项;

  3. 遍历所有匹配条件的提醒事项,使用removeReminder:commit:error:从事件存储器中移除它们;

  4. 提交变化到事件存储器。如果你有不止一个EKReminder需要删除,好的做法是不要一个一个的提交,而是全部删除,在最后一次性提交。这个也适用于增加新的事件到存储器中。

从tableView:commitEditingStyle:forRowAtIndexPath:中调用这个方法。找到该方法并用如下实现替换:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    NSString *todoItem = self.todoItems[indexPath.row];

    // Remove to-do item.
    [self.todoItems removeObject:todoItem];
    [self deleteReminderForToDoItem:todoItem];

    [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}

编译并运行工程。现在你可以通过手指滑动一个列表单元,然后点击删除按钮来从日历数据库中删除一个用户的提醒事项。当然,这是完全移除事件项,而不仅限于提醒事项。当你移除一部分提醒事项,再次编译并运行,你将看到 “Add Reminder” 再次重现,也就意味着事件项已经没有了对应的提醒事项。


设置一个完成及预定日期

有的时候对应一个提醒事项设置一个完成预计的日期是非常有用的。对一个提醒事项设置一个预定日期是相当的简单:你只需要标记一个提醒事项为完成,然后预定日期是你设置的。

再次打开RWTableViewController.m并实现 tableView:didSelectRowAtIndexPath::

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

  UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];



  NSString *toDoItem = self.todoItems[indexPath.row];

  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title matches %@", toDoItem];



  // Assume there are no duplicates...

  NSArray *results = [self.reminders filteredArrayUsingPredicate:predicate];

  EKReminder *reminder = [results firstObject];

  reminder.completed = !reminder.isCompleted;



  NSError *error;

  [self.eventStore saveReminder:reminder commit:YES error:&error];

  if (error) {

// Handle error

  }



  cell.imageView.image = (reminder.isCompleted) ? [UIImage imageNamed:@"checkmarkOn"] : [UIImage imageNamed:@"checkmarkOff"];

}

这代码相当的容易自我理解。EKReminder有一个completed属性,你可以使用它标记一个提醒事项为完成。一旦你调用setCompleted:YES,完成日期自动的为你设置了。如果你调用setComplete:NO,完成日期将为空。

编译并运行。现在你可以简单的通过点击事件项来标记一个提醒事项为完成,提醒事项为enabled的事件项将有一个标记符,该标记符基于对应的提醒事项的完成状态从灰变到绿。

这个标记符只在当你实际点击一个事件项的时候显示,但是在运行应用前提醒事项就已经存在那会是什么情况呢?这是为你设置的一个小小的练习。解决方法当然可在下面找到,但希望你自己第一时间尝试着指出来。只有这样你才真正的学到了一些东西。提示:你只要改变tableView:cellForRowAtIndexPath:。

解决方案里的措施

你可以下载 answer here at GitHub

设置预定日期可以小小的谨慎下。除非你知道准确的日期和时间,你或许需要做一些日期数学。这个教程的目的,假设你想为你应用的所有提醒事项设置一个默认的预定日期。这个默认的预定日期是明日4:00 P.M

日期数学可以复杂的考虑下闰年、夏令时、用户本地化等。那种时间下,NSCalendar和NSDateComponents是你最好的选择. 不是巧合的是, EKReminder有一个名dueDateComponents的属性,其类型是 NSDateComponents.

starter project使用的是dateComponentsForDefaultDueDate,通过增加一行在reminder.calendar = self.calendar后,作用于addReminderForToDoItem:

reminder.dueDateComponents = [self dateComponentsForDefaultDueDate];

现在所有的事情都停留在在某个地方显示预定日期。因此,再次修改tableView:cellForRowAtIndexPath:,在else条件部分的末端增加下列代码:

if (reminder.dueDateComponents) {

  NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

  NSDate *dueDate = [calendar dateFromComponents:reminder.dueDateComponents];

  cell.detailTextLabel.text = [NSDateFormatter localizedStringFromDate:dueDate dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];

}

这里仅仅是从所有拥有提醒事项的事件项中取出dueDateComponents,然后将其转化成一个显示在事件项下面的实际预定日期。点击编译并运行,你将看到如图所示:

注释:所有之前增加的提醒事项仍没有预定日期,这就是为什么他们没有显示预定日期。增加一个新的提醒事项然后你会看到它将会有一个明日4PM的预定日期。

就这个样子。你现在有一个可以获取用户日历和提醒事项的几乎全功能事件应用。

从哪儿开始

你可以下载 completed project here

这里仅仅是对提醒事项的一个快速介绍,并且用户提醒事项看上更像是一个典型的互动。当然EKEventStore支持实际的日历事件(如“与Bob在每周三2:00 PM见面”),这一点,这个教程没有讨论,但是也是相当的有用的。

我希望你喜欢这篇cookbook文章。如果你希望在将来看到更多的类似于这样的cookbook-style的文章,或者你有任何问题,请在下面讨论区留言!

标签: iOS

?>