EventKit Tutorial: Making a Calendar Reminder

Learn how to make a calendar reminder in iOS quickly and easily! By Soheil Azarpour.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Fetching Reminders

While your app is authorized to access the user’s Calendar database, you can fetch either a set of reminders by using predicates or fetch a particular reminder by using its unique identifier, if you have one. The following modification of RWTableViewController.m demonstrates the predicate approach.

First, add another property:

@property (copy, nonatomic) NSArray *reminders;

Create 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];
      });
    }];
  }
}

So what does fetchReminders do?

  1. predicateForRemindersInCalendars: returns a predicate for the calendars that are passed to this method. In your case, it’s just self.calendar.
  2. Here you fetch all the reminders that match the predicate you created in the previous step.
  3. In the completion block you can store the returned array of reminders in a private property.
  4. After all reminders have been fetched, reload the table view (on the main queue, of course!).

Next, add a call to fetchReminders in viewDidLoad, so that when the view is loaded for the first time, all reminders are being loaded immediately. Also register for EKEventStoreChangedNotification in viewDidLoad:

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

  [self fetchReminders];
  [[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(fetchReminders)
    name:EKEventStoreChangedNotification object:nil];
  
  // the rest of the code...
}

And, as a good citizen, you remove yourself as an observer as well of course:

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

The EKEventStoreChangedNotification notification is posted whenever changes are made to the calendar database, for example when a reminder is added, deleted, etc. When you receive this notification, you should refetch all EKReminder objects you have because they are considered stale.

Now that you can fetch reminders, it’s time to hide the “Add Reminder” button for to-do items from the table view when a reminder for that to-do item was added.

To do so, create a new method called itemHasReminder::

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

Then modify tableView:cellForRowAtIndexPath: to the following:

- (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: remains largely the same, except you wrap it in a conditional statement that checks whether an item already has a reminder and if it does, it doesn’t show the “Add Reminder” button.

Build and run, and add reminders for a few of your to-do items. You’ll see that to-do items that you add reminders for don’t show the “Add Reminder” button, and because of the notification observing, any newly added reminders will make the button for the corresponding to-do item disappear.

Add Reminder button conditionally hidden

You can download the answer here at GitHub.

Deleting a reminder

This is the easiest task so far. The approach here is to modify RWTableViewController.m so that whenever a row gets deleted from the to-do list, any matching reminders are also deleted.

Add deleteReminderForToDoItem: and implement it as follows:

- (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. Find the matching EKReminder(s) for the item that was passed in.
  2. Check if there are any matching reminders
  3. Loop over all the matching reminders, and remove them from the event store using removeReminder:commit:error:.
  4. Commit the changes made to the event store. If you have more than one EKReminder to delete, it is better not to commit them one by one. Rather, delete them all and then commit once at the end. This rule also applies when adding new events to the store.

You call this method from tableView:commitEditingStyle:forRowAtIndexPath:. Find that method and replace its implementation:

- (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];
}

Build and run the project. Now you can delete a user’s reminders from the Calendar database by swiping over a table cell and clicking delete. Of course, this removes the to-do item completely, not just the reminder. When you remove a few reminders, and build and run again, you’ll see the “Add Reminder” button show up again, indicating that that to-do item does not have a reminder yet.

Deleting to-do item

Reminder deleted

Setting a completion and due date

Sometimes it’s useful for reminders to have completion and due dates. Setting a completion date for a reminder is really easy: you just mark the reminder as completed and the completion date gets set for you.

Open RWTableViewController.m again, and implement 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"];
}

This code should be pretty self-explanatory. EKReminder has a property completed that you can use to mark a reminder as completed. As soon as you call setCompleted:YES, the completion date is set automatically for you. If you setComplete:NO, completion date will be nilled out.

Build and run. Now you can mark a reminder as completed by simply tapping on a to-do item. Reminder-enabled to-do items will have a checkmark that changes from gray to green based on the completion status of the corresponding reminder.

Completing reminders

The checkmarks only show up when you actually tap an item, but what about reminders that existed before running the app? That’s a little exercise for you. The solution can of course be found below, but try to figure it out yourself first. Only then do you really learn something! Hint: you only have to change tableView:cellForRowAtIndexPath:.

[spoiler title=”Solution”]

- (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;

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title matches %@", object];
    NSArray *reminders = [self.reminders filteredArrayUsingPredicate:predicate];
    EKReminder *reminder = [reminders firstObject];
    cell.imageView.image = (reminder.isCompleted) ? [UIImage imageNamed:@"checkmarkOn"] : [UIImage imageNamed:@"checkmarkOff"];
  }
  
  return cell;
}

Complete and incomplete reminders
[/spoiler]

You can download the answer here at GitHub.

Setting the due date can be a little bit tricky. Unless you know the exact date and time, you probably need to do some date math. For the purpose of this tutorial, assume you want to set a default due date for all reminders in your app. The default due date is tomorrow at 4:00 P.M.

Date math can be complicated considering leap years, daylight savings, user locale etc. NSCalendar and NSDateComponents are your best bet at such time. Not coincidentally, EKReminder has a property called dueDateComponents, whose type is NSDateComponents.

The starter project came with dateComponentsForDefaultDueDate, which we’re going to leverage in addReminderForToDoItem: by adding a line after reminder.calendar = self.calendar:

reminder.dueDateComponents = [self dateComponentsForDefaultDueDate];

Now all that’s left is showing this due date somewhere. For that, modify tableView:cellForRowAtIndexPath: once again, and add the following code at the end of the else part of the conditional:

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];
}

This just takes the dueDateComponents from all to-do items that have a reminder, and convert it into an actual due date which is shown underneath the to-do items. Hit build and run and you’ll see something like this:

Reminder due date

Note: Any previously added reminders don’t have due dates yet, which is why they don’t show a due date. Add a new reminder and you’ll see that it will have a due date set at 4PM tomorrow.

And that’s it. You now have an almost fully functioning to-do app that has access to the user’s calendar and reminders.