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

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?

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.

  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.

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