Using iOS 6 Event Kit to Create Date and Location Based Reminders
Previous | Table of Contents | Next |
Working with Apple Maps on the iPhone with MapKit and the MKMapView Class | Accessing the iPhone Camera and Photo Library (iOS 6) |
Learn SwiftUI and take your iOS Development to the Next Level |
iOS 5 introduced the Reminders application, the purpose of which was to allow users to specify events about which they wished to be reminded. Reminders can be specified for a specific date and time, or even to be triggered when the user either arrives at or leaves a specified location. You might, for example, use the Reminders app to remind you to buy milk on your way home when your iPhone detects that are leaving your office. With the introduction of iOS 6, it is now possible to create and manage reminders from within your own applications using the Event Kit Framework.
This chapter will cover some of the basics of calendars and reminders before working step-by-step through the creation of an example application that demonstrates the creation of both date and location based reminders.
An Overview of the Event Kit Framework
The Event Kit Framework consists of a range of classes designed specifically to provide access to the calendar database and to facilitate the management of events, reminders and alarms. In terms of integrating reminders into an iOS application, these classes are EKCalendar, EKEventStore, EKReminder and EKAlarm.
The EKEventStore class provides an interface between applications and the underlying calendar database. The calendar database can, in turn, contain multiple calendars (for example the user may have a work calendar and a personal calendar configured). Each calendar in a database is represented in code in the form of an EKCalendar object. Within each calendar there are events and reminders, each of which is managed in code using the EKEvent and EKReminder classes respectively.
Finally, the EKAlarm class is used to configure alarms to alert the user at a specified point in the future.
The EKEventStore Class
In order to work with reminders in an iOS application, an instance of the EKEventStore class must be created. It is important to note that there is system overhead in requesting access to the calendar database so the call to initialize an EKEventStore object should ideally only be performed once within an application. In some situations, the system will prompt the user to allow the application access to the calendar. As such, the EKEventStore object should only be initialized immediately prior to the point in the code where calendar access is required. A reference to this event store object should then be retained and used for future calendar interaction throughout the lifespan of the application.
An EKEventStore object must request access to the calendar at the point that it is initialized. This request must specify whether access is required for calendar events (EKEntityTypeEvent) or reminders (EKEntityTypeReminder). Whether or not access is granted will depend on the current privacy settings for the application in the Privacy section of the device Settings app. Privacy settings are available for both Calendar and Reminders access. When an application seeks access for the first time, the user will be prompted by the system to allow the application to access either the calendar events or reminders. If the user declines access, the application will need to handle this gracefully. This is achieved via the completion handler of the requestAccessToEntityType: method of the EKEventStore class. The following code excerpt, for example, seeks access to reminders in the calendar database and reports an error in the console log in the event that access was declined:
[_eventStore requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) { if (!granted) NSLog(@"Access to store not granted"); }];
Once access has been accepted or denied by the user, the privacy setting for that application can be viewed and changed within the Privacy section of the Settings application. Figure 64-1, for example, shows that the access for an application named ReminderApp to Reminders on the system is currently disabled.
Figure 64-1
In addition, it is worth noting that the message used by the system to request access to the calendar database can be configured by adding an entry to the Info.plist file for the project. By default a message similar to that illustrated in Figure 64-2 will be displayed.
Figure 64-2
By editing the Info.plist file, for example, and adding a “Privacy – Reminders Usage Description” key and value, the message may be augmented to provide the user with additional information as to why access is required. Once defined, the custom message will appear when access is requested from the user:
Figure 64-3
Accessing Calendars in the Database
As previously stated, the calendar database on an iOS device can contain multiple calendars, both for the events calendar and for reminders. An array of available calendars can be obtained via a call to the calendarsForEntityType: method of the event store object, specifying as an argument whether event or reminder calendars are required. The following code outputs a list, by title, of the reminder calendars configured on the device. Note that each calendar entry in the array is represented by an EKCalendar object:
NSArray *calendars = [_eventStore calendarsForEntityType:EKEntityTypeReminder]; for (EKCalendar *calendar in calendars) { NSLog(@"Calendar = %@", calendar.title); }
The Event Kit framework has the concept of a default calendar for the addition of new reminders and events. This default calendar may be configured by the user within the Settings app on the device (Settings -> Reminders and Settings -> Mail, Contacts, Calendars). These are represented in code by the defaultCalendarForNewReminders and defaultCalendarForNewEvents constants. Whilst some applications may need to let the user choose which calendar to use, these defaults can be useful when selection is not necessary.
Accessing Current Reminders
The reminders currently configured in a database calendar may be accessed using a variety of methods depending on the scope of the search to be performed. The reminders may be filtered to include only those that match a given predicate. Matching results are returned in the form of an array of EKReminder objects. The following code excerpt outputs a list of all reminders from all calendars in the database:
NSArray *calendars = [_eventStore calendarsForEntityType:EKEntityTypeReminder]; for (EKCalendar *calendar in calendars) { NSLog(@"Calendar = %@", calendar.title); }
In addition to the calendarsForEntityType: method, predicates may also be generated using the following additional methods of the EKEventStore class:
- predicateForIncompleteRemindersWithDueDateStarting - Searches for incomplete reminders specified between optional start and end dates.
- predicateForCompletedRemindersWithCompletionDateStarting: - Searches for completed reminders between optional start and end dates.
Creating Reminders
New reminders are added to the calendar by creating new instances of the EKReminder class, configuring the object according to the requirements of the reminder and then adding it to the event store.
The following example code creates a reminder and adds it to the default calendar for new reminder entries:
EKReminder *reminder = [EKReminder reminderWithEventStore:self.eventStore]; reminder.title = @"Go to the store and buy milk"; reminder.calendar = [_eventStore defaultCalendarForNewReminders]; NSError *error = nil; [_eventStore saveReminder:reminder commit:YES error:&error];
The above code creates a new EKReminder object and, in so doing, associates it with the event store before setting the title of the reminder. Next, the reminder is configured so that it will be added to the user’s default reminder calendar before being saved to the event store.
Reminders can be general as in the above example or, as will be demonstrated in the following tutorial, configured to be triggered at a specific date and time, or when the user arrives at or departs from a physical geographical location (a concept known as geofencing).
Creating Alarms
The EKAlarm class can be used to add an alarm to the reminder. Alarms can be specified either using a specific date and time (via a call to alarmWithAbsoluteDate: and passing through an NSDate object) or using a relative time interval (via a call to alarmWithRelativeOffset: passing through an NSTimeInterval value).
Once an EKAlarm object has been created and configured it must be added to the EKReminder object with which the alarm is to be associated. When the specified time arrives, the user will be notified of the reminder with sound, vibration and a notification panel.
Creating the Example Project
Begin by launching Xcode and selecting the options to create a new iPhone iOS application based on the Tabbed Application template. Enter ReminderApp as the product name and class prefix, set the device to iPhone and select the Use Storyboards and Use Automatic Reference Counting options if they are not already selected.
In the remainder of this chapter, an application will be constructed designed to allow the user to add reminders based on either date/time or location factors.
Designing the User Interface for the Date/Time Based Reminder Screen
Upon reviewing the MainStoryboard.storyboard file, it is clear that Xcode has created a template-based tabbed application consisting of Tab Bar Controller and two Views, each of which has its own view controller. For the purposes of this example, the first view will be used to implement a screen whereby the user can create a new date and time based reminder. Within the Storyboard canvas, therefore, locate the Reminder App First View screen and remove the current labels that were added by Xcode. With a clean view, add a Text Field, Date Picker and Button object to the view canvas. Once added, position and configure the user interface layout so that it resembles that of Figure 64-4:
Figure 64-4
Select the Text Field object and display the Assistant Editor using View -> Assistant Editor -> Show Assistant Editor menu option, making sure that it is displaying the content of the ReminderAppFirstViewController.h file. If it is not, click on the file name in the bar at the top of the Assistant Editor panel and select the file from the drop down menu (Figure 64-5):
Figure 64-5
Ctrl-click on the Text Field object in the view and drag the resulting line to the area immediately beneath the @interface directive in the Assistant Editor panel. Upon releasing the line, the configuration panel will appear. Configure the connection as an Outlet named reminderText and click on the Connect button. Repeat this step to add an outlet connection to the Date Picker object named myDatePicker.
Finally, Ctrl-click on the Button object, drag the line to the Assistant Editor and release it beneath the last outlet connection added. In the resulting connection panel, change the connection type to Action and name the action setReminder.
Implementing the Reminder Code
With the user interface view designed, the next step is to implement the code in the view controller for the first view to access the event store and set up the reminder. We will need a place to store the event store object once it has been requested and, as previously discussed, it is recommended that access to the event store be requested only once. As such we will need a location to store the reference once it has been obtained, so select the ReminderAppFirstViewController.m file and add an additional property. This is also an opportune time to import the EventKit framework headers:
#import <UIKit/UIKit.h> #import <EventKit/EventKit.h> @interface ReminderAppFirstViewController : UIViewController @property (strong, nonatomic) EKEventStore *eventStore; @property (strong, nonatomic) IBOutlet UIDatePicker *myDatePicker; @property (strong, nonatomic) IBOutlet UITextField *reminderText; - (IBAction)setReminder:(id)sender; @end
Within the project navigator, select the ReminderAppFirstViewController.m file, locate the setReminder: template stub and add the following code:
- (IBAction)setReminder:(id)sender { if (_eventStore == nil) { _eventStore = [[EKEventStore alloc]init]; [_eventStore requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) { if (!granted) NSLog(@"Access to store not granted"); }]; } if (_eventStore != nil) [self createReminder]; }
The code added to the method verifies that access to the event store has not already been obtained and, in the event that it has not, requests access to the reminder calendars. If access is denied a message is reported to the console. In the event that access is granted, a second method named createReminder: is called. With ReminderAppFirstViewController.m still in the editing panel, implement this method:
-(void)createReminder { EKReminder *reminder = [EKReminder reminderWithEventStore:self.eventStore]; reminder.title = _reminderText.text; reminder.calendar = [_eventStore defaultCalendarForNewReminders]; NSDate *date = [_myDatePicker date]; EKAlarm *alarm = [EKAlarm alarmWithAbsoluteDate:date]; [reminder addAlarm:alarm]; NSError *error = nil; [_eventStore saveReminder:reminder commit:YES error:&error]; if (error) NSLog(@"error = %@", error); }
The createReminder: method creates a new EKReminder object associated with the event store and sets the title property to the content of the Text Field object in the user interface. The code elects the default calendar as the target for the reminder and then creates an EKAlarm object primed with the date value selected by the user in the Date Picker object. The alarm is then added to the reminder which, in turn, is saved in the event store. Errors are output to the console for debugging purposes.
Hiding the Keyboard
Before moving on to the next part of the tutorial, some code will now need to be added to the application so that the keyboard is withdrawn when the user touches the view background.
Select the MainStoryboard.storyboard file and click on the background of the view. Display the Identity Inspector (View -> Utilities -> Show Identity Inspector) and change the object’s class from UIView to UIControl. Display the Assistant Editor, Ctrl-click on the view background and drag to a position beneath the setReminder: action in the Assistant Editor and declare an action connection named hideKeyboard for the Touch Down event.
Within the ReminderAppFirstViewController.m file, implement the code in the hideKeyboard: stub method:
- (IBAction)hideKeyboard:(id)sender { [_reminderText resignFirstResponder]; }
With the time and date based reminder phase of the application completed, the next step is to implement the location based reminder functionality.
Designing Location-based Reminder Screen
The tab controller created on our behalf by Xcode contains a second view that will be used for the location based reminder creation. The goal of this view will be to allow the user to specify a reminder and alarm that will be triggered when the user moves away from the geographical location at which the reminder was created.
Begin by selecting the MainStoryboard.storyboard file and locating the second view. Remove the template labels added by Xcode and design a new user interface using a Button and a Text Field as illustrated in Figure 64-6.
Using the Assistant Editor (taking care to ensure that that editor displays the code for the ReminderAppSecondViewController.h file not the file for the first view controller), establish an outlet connection for the Text Field named locationText. Next, establish an Action connection from the button to a method named setLocationReminder.
Figure 64-6
Finally, add an event store property to the ReminderAppSecondViewController.h file and import the Event Kit header:
#import <UIKit/UIKit.h> #import <EventKit/EventKit.h> @interface ReminderAppSecondViewController : UIViewController @property (strong, nonatomic) EKEventStore *eventStore; @property (strong, nonatomic) IBOutlet UITextField *locationText; - (IBAction)setLocationReminder:(id)sender; @end
Creating a Location-based Reminder
The code for accessing the event store is the same as that for the date/time example. Note that in practice the two view controllers could share the event store, but for now we will work with two store objects within the application. Since the application will need to get the user’s current location, the <CoreLocation/CoreLocation.h> file will need to be imported. The code will require the use of CLLocationManager so a property for the manager instance will also be needed, as will a declaration that the view controller now implements the CLLocationManagerDelegate protocol:
#import <UIKit/UIKit.h> #import <EventKit/EventKit.h> #import <CoreLocation/CoreLocation.h> @interface ReminderAppSecondViewController : UIViewController <CLLocationManagerDelegate> @property (strong, nonatomic) EKEventStore *eventStore; @property (strong, nonatomic) CLLocationManager *manager; @property (strong, nonatomic) IBOutlet UITextField *locationText; - (IBAction)setLocationReminder:(id)sender; @end
All that remains is to implement the code to access the event store and create the alert and reminder. Select the ReminderAppSecondViewController.m file and modify the setLocationReminder: method:
-(void)setLocationReminder:(id)sender { if (_eventStore == nil) { _eventStore = [[EKEventStore alloc]init]; [_eventStore requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) { if (!granted) NSLog(@"Access to store not granted"); }]; } if (_eventStore) { _manager = [[CLLocationManager alloc]init]; _manager.delegate = self; _manager.distanceFilter = kCLDistanceFilterNone; _manager.desiredAccuracy = kCLLocationAccuracyBest; [_manager startUpdatingLocation]; } }
As with the previous example, access to the event store is requested. This time, however, a CLLocationManager instance is created and started up. This will result in a call to the didUpdateLocations: delegate method where the code to obtain the current location and to create the alarm and reminder will need to be implemented:
#pragma mark - #pragma mark CLLocationManagerDelegate -(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { [_manager stopUpdatingLocation]; EKReminder *reminder = [EKReminder reminderWithEventStore:_eventStore]; reminder.title = _locationText.text; reminder.calendar = [_eventStore defaultCalendarForNewReminders]; EKStructuredLocation *location = [EKStructuredLocation locationWithTitle:@"Current Location"]; location.geoLocation = [locations lastObject]; EKAlarm *alarm = [[EKAlarm alloc]init]; alarm.structuredLocation = location; alarm.proximity = EKAlarmProximityLeave; [reminder addAlarm:alarm]; NSError *error = nil; [_eventStore saveReminder:reminder commit:YES error:&error]; if (error) NSLog(@"Failed to set reminder: %@", error); }
Since this code introduces some new concepts a more detailed breakdown is probably warranted. To begin with, the code stops the location manager from sending further updates.
[_manager stopUpdatingLocation];
Next, a new EKReminder instance is created and initialized with the text entered by the user into the Text Field.
EKReminder *reminder = [EKReminder reminderWithEventStore:_eventStore]; reminder.title = _locationText.text;
The default calendar is selected to store the reminder and then an EKStructuredLocation instance created with a location title of “Current Location”. This is the title by which the location will be listed in the Reminders app. The most recent location from the location update is extracted from the end of the locations array (see chapter Getting iPhone Location Information using the iOS 6 Core Location Framework for more details on location awareness) and the coordinates assigned to the EKStructuredLocation object.
reminder.calendar = [_eventStore defaultCalendarForNewReminders]; EKStructuredLocation *location = [EKStructuredLocation locationWithTitle:@"Current Location"]; location.geoLocation = [locations lastObject];
The location is then added to a newly created alarm instance which is subsequently configured to be triggered when the user moves away from the location proximity:
EKAlarm *alarm = [[EKAlarm alloc]init]; alarm.structuredLocation = location; alarm.proximity = EKAlarmProximityLeave;
Finally, the fully configured reminder is saved to the event store.
NSError *error = nil; [_eventStore saveReminder:reminder commit:YES error:&error]; if (error) NSLog(@"Failed to set reminder: %@", error);
Adding the Core Location and Event Kit Frameworks
Whilst the coding is complete, an attempt to compile the application at this point will result in undefined symbols because the Core Location and Event Kit Frameworks have not yet been added to the project. Within the project navigator panel, select the ReminderApp target at the top of the list and in the main panel select the Build Phases tab. In the Link Binary with Libraries category, click on the + button and in the resulting list of libraries search for and add the CoreLocation.framework library. Repeat this step for EventKit.framework.
Testing the Application
Since the application will rely on a default calendar having been designated for reminders, the first step is to make sure this has been configured. Launch the Settings application, scroll down to and then select Reminders, and make sure that a calendar has been assigned to the Default List.
Compile and run the application on a physical iPhone device (reminders do not currently work on the iOS Simulator). Select a time a few minutes into the future, and enter some text onto the first screen before touching the Set Reminder button. Put the app into the background and launch the built-in Reminder app where the new reminder should be listed in the default reminder list. When the designated time arrives the alarm should trigger, displaying the text entered by the user.
Using the tab bar, switch to the second screen, enter a message and touch the “When I Leave Here” button. Once again, switch to the built in Reminders app, locate the new reminder and select it. Listed amongst the reminder settings will be an option entitled “Current Location” with the “When I Leave” option selected as shown in Figure 64 7:
Figure 64-7
When you next leave your current location the alarm should trigger.
Summary
The Event Kit Framework provides a platform for building reminders into iOS application. Reminders can be triggered based on either a date and time or change of geographical location. This chapter has provided an overview of Event Kit based reminders before working through the creation of an example application.
Learn SwiftUI and take your iOS Development to the Next Level |
Previous | Table of Contents | Next |
Working with Apple Maps on the iPhone with MapKit and the MKMapView Class | Accessing the iPhone Camera and Photo Library (iOS 6) |