EventKit Tutorial: Making a Calendar Reminder

Soheil Azarpour

Update 3/11/15: Updated for Xcode 6.2.

In this cookbook-style tutorial, you will learn how to access user reminders programmatically.

If you are not already familiar with reminders, consider reading the Apple documentation of the Reminders app. According to Apple, reminders allow you “to organize your life in to-do lists, complete with due dates and locations.”

To get started, download the starter project.

What are you going to work with?

  • EventKit framework
  • UITableView
  • UITableViewController

Authorization

To work with reminder and calendar events, you need to link against EventKit. You will also need a persistent store to save reminder items. Conveniently, EventKit provides this for you: EKEventStore. An EKEventStore allows you to fetch, create, edit, and delete events from a user’s Calendar database.

Both reminders and calendar data are stored in the Calendar database. Ideally, you will have only one event store for your entire app, and you will instantiate it once. That’s because an EKEventStore object requires a relatively large amount of time to initialize and release. It is very inefficient to initialize and release a separate event store for each event-related task. You should use a single event store and continue using it for as long as your app is running!

Calendar database diagram

Calendar database diagram

You can see that reminders and calendars are stored in the same persistent store, but it’s often referred to as just the “calendar database”. In this tutorial, when you see calendars, that really means calendars and reminders, but since programmers are lazy, we just abbreviate it to calendars :]

Before you can access the user’s Calendar you must request access to use the user’s Calendar database. iOS will prompt the user to allow or deny use of calendar information. The most appropriate time to do this is when the app is about to actually access the reminders database.

In the following modification of RWTableViewController.m from the starter project, viewDidLoad ultimately triggers the access request. Note the first line, @import EventKit;, which causes the app to be linked against EventKit and makes that framework’s classes available for use within RWTableViewController.m.

Open RWTableViewController.m and replace the @interface section with the following:

@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

Then add the following two methods:

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

Finally, call updateAuthorizationStatusToAccessEventStore from viewDidLoad

[self updateAuthorizationStatusToAccessEventStore];

So what’s going on here?

  1. This is just a basic lazy instantiation method for eventStore.
  2. Ask the system what the current authorization status is for the event store, using EKEventStore's the class method authorizationStatusForEntityType:. You pass it EKEntityTypeReminder as the event type, which indicates you seek permission to access reminders. Then evaluate different scenarios.
  3. EKAuthorizationStatusDenied means that the user explicitly denied access to the service for your application EKAuthorizationStatusRestricted means that your app is not authorized to access the service possibly due to active restrictions such as parental controls. In both of the above cases you can’t do anything, so just display an alert view and inform the user.
  4. EKAuthorizationStatusAuthorized means that your app has access and you can read from or write to the database.
  5. EKAuthorizationStatusNotDetermined means that you haven’t requested access yet, or the user hasn’t made a decision yet. To request access you call requestAccessToEntityType:completion: on your instance of EKEventStore.

    Note that the completion handler of requestAccessToEntityType:completion: does not get called on the main queue, but UI calls (such as reloadData) have to be executed on the main thread. The line that starts dispatch_async ensures that reloadData is executed on the main thread.

Build and run the project. You’ll immediately see an alert, asking the user whether he or she wants to give the app access to his or her reminders.

Requesting access

Note: If you are wondering what would happen if user puts your app to the background, goes to Settings > Privacy > Reminders, and revokes your app’s permission, the answer is that iOS will terminate your app! This approach may sound harsh, but it is the reality.

Adding a reminder item

Assuming that the user has granted access to your app, you can add a new reminder item to the database. For a better user experience, you may want to keep your app’s reminders in a separate list. The following modification of RWTableViewController.m demonstrates this approach.

Add a private property to RWTableViewController.m:

@property (strong, nonatomic) EKCalendar *calendar;

Then lazily instantiate that property:

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

If you thought lazy meant “not a lot of work”, well, you’re wrong :] So what does this code do?

  1. First, you look up all available reminder lists.
  2. Then you filter the array by finding all lists called UDo!, and return the first list with that name.
  3. If there aren’t any lists named UDo!, create a new one.
  4. To keep your app’s reminders in a separate list, you need an instance of EKCalendar. You have to instantiate an EKCalendar with a type and event store, and specify its source. The source of a calendar represents the account to which this calendar belongs. For simplicity use the same source that defaultCalendarForNewReminders property on your event store has; defaultCalendarForNewReminders is a convenient readonly property on EKEventStore that returns an EKCalendar object either from the user’s iCloud account or locally from the device based on user’s settings.

Now you need a way to add a new reminder to the event store. Add the following method to 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];
}

Though the code above may seem like it’s a lot, it’s actually pretty straight-forward:

  1. Return early if the user hasn’t given permission to access the event store.
  2. Create the reminder. A reminder item is an instance of EKReminder. At the minimum you have to set two properties on EKReminder: title and calendar.
  3. Store the reminder in the event store and handle the error if this fails
  4. Give the user some feedback in the form of an alert

You have a method to add a reminder, but you still need to call that method. Still in RWTableViewController.m, find the comment // Add the reminder to the store in tableView:cellForRowAtIndexPath:, and replace it with

[self addReminderForToDoItem:object];

Build and run the project. You should be able to add a reminder for to-do items by tapping ‘Add Reminder’ for any to-do item.

Reminder added

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:.

Solution Inside: Solution SelectShow

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.

Where to Go From Here?

You can download the completed project here.

This was just a quick introduction to the reminders and what a typical interaction with the users’ reminders looks like. EKEventStore of course has support for actual calendar events (such as “meeting with Bob every Wednesday at 2:00 PM”), which this tutorial did not discuss, but can be very useful as well.

I hope you enjoyed this cookbook article. If you would like to see more cookbook-style articles like this in the future or if you have any questions, please leave a comment in the discussion below!

Soheil Azarpour

My name is Soheil Azarpour and I'm an engineer, developer, author, creator, husband and father. I enjoy bicycling, boating and playing piano. I live in Manchester, NH. I create iOS apps professionally and independently. You can find me on Twitter, GitHub, Stack Overflow and LinkedIn.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 15 total!

iOS Team

... 32 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!