Managing Calendars and Reminders with the Event Kit API - Using the APIs - Learning iCloud Data Management (2014)

Learning iCloud Data Management (2014)

Part II: Using the APIs

5. Managing Calendars and Reminders with the Event Kit API

iCloud manages calendars and events, synchronizing them across devices just as it does with contact information. This is data that your users have entered, even if that entry is indirect, such as with data detectors in an email message or via an invitation. It is data that your users have already seen, although they may have forgotten specific details (that’s the reason for calendars and reminders).

Both calendars and reminders are managed by the Event Kit framework, which provides access to the Calendar database that is built into OS X and iOS. If iCloud is enabled, the Calendar database is what is synchronized. It is the responsibility of apps on the various devices and different operating systems to manipulate the database data.

Just as is the case with AddressBook, there is a framework, EventKitUI, that provides view controllers for the iOS devices. EventKitUI provides a standard interface for users regardless of what app they’re using. In this chapter, however, you’ll see how to use your own interface. The code you’ll use works on both iOS and OS X.


Structuring Events and To-Do Item Data

The synchronization of the Calendar database can extend to Exchange and CalDAV because it is based on calendar standards. There are extensions to the standards that provide additional functionality, and you can provide your own extensions. Finding the right way to organize calendar data has been a long task for the software industry. Standard calendar formats have been around for many, many years (and there is a multitude of such standards).

If you go back to the datebooks that people used in precomputer days (and that many people still use), you can see that people often entered events or appointments (the terms are basically interchangeable) as well as to-do items. There is a benefit to entering to-do items in a separate format because even if they are tied to a specific date, they are often done before or after that date, whereas events are tied to a definite date and time as well as, often, to other people. Keeping a separate list of to-do items means that you don’t have to carry unfinished to-do items forward from date to date, but it also means that to find out the appointments and to-do items for a specific date requires you to look in two places.

This has been addressed in the Event Kit framework with two classes. The main class is EKCalendarItem. It has a subclass: EKReminder. This means that reminders and events sometimes behave the same way (for example, both can be assigned to calendars), and sometimes they observe slightly different behavior. The distinction between the two classes is clear at the user interface level even though there is some shared code underneath.

The distinction between to-do items and appointments is clear and logical, but when you come right down to it, the answer to the question “What do I have to do today?” shouldn’t require consulting two lists (appointments and to-do items). Perhaps by designing for the most complex needs of large organizations, we have wound up with tools that make it awkward to note that tomorrow you have to buy cat food and have lunch with Earnest at noon. As it stands now, on OS X and iOS, you use the Calendar database to manage both types of data, but the interface and functionality are provided separately by Calendar and Reminder apps.


Exploring the Event Class Hierarchy

The interaction between events and reminders is reflected in the relative complexity of the class hierarchy. This background information shows you what happens behind the scenes.

There is a new subclass of NSObject: EKObject. It is an abstract superclass of all Event Kit classes. As you can see, most of the methods of this class have to do with synchronization: the heart of iCloud. There are five methods and no properties. All Event Kit classes inherit these methods:

Image hasChanges: Returns a BOOL value if this object or any objects it contains has changes.

Image isNew: Returns a BOOL letting you know if it has ever been saved.

Image refresh: This method also returns a BOOL value. It accesses the copy of this event in the store and refreshes the object’s data with the current store values except for the values that have been changed locally. Thus, your object will contain the latest values from the data store along with your changes. The result value is YES if all goes well. If the object has been deleted from the store, it returns NO.

Image reset: Gets rid of local changes to the object and restores it to the value in the store. With regard to your changes, refresh is nondestructive and reset is destructive.

Image rollback: Gets rid of any local changes and sets the object to the values it had when it was first fetched. If the object in the store has changed since the first fetch, reset will incorporate them into your local object, and rollback will not.

Setting OS X Permissions

When you set up an OS X project that will use calendars, you need to set it up using the new Capabilities tab in Xcode 5. After you create the project, go to Capabilities and turn on Contacts, Location, and Calendar, as shown in Figure 5.1. (Strictly speaking, Location isn’t essential at this point, but it often is used.) Note that you don’t have to turn iCloud on to get the benefits of synchronization. Once you have access to Calendar and Contacts, your choices in Settings (iOS) or System Preferences (OS X) control iCloud’s access to Calendar and Contacts. You don’t need to adjust capabilities on iOS for this project.

Image

Figure 5.1 Setting up capabilities for your OS X project

Working with the Calendar Database

The Calendar database stores both events (appointments) and reminders (to-dos). Although the syntax is different (and despite that there are differences between OS X and iOS), the overall architecture or design pattern is the same:

Image Events and reminders are stored in the Calendar database. From your code, you access it as a data store: EKEventStore. The overhead in creating this object is significant, so you generally keep it around while your app is running. Get rid of it after you are finished with all your Event Kit processing.

Image Once you have the data store, you request access to specific types of entities within the Calendar database using EKEntityTypeEvent and EKEntityTypeReminder.

Image At this point, you can create a new event or reminder.

Image You can also search for an existing event or reminder.

Image Having created or found an event or reminder, you can set or modify its properties.

Image When you are finished working with the store and its calendar data, you must commit all unsaved data in the store. (If you use Core Data, this process will be familiar to you.)

In the sections that follow, the code for each of these steps is provided for OS X and for iOS. On iOS, you’ll create a reminder, and on OS X, you’ll create an event. In both cases, you’ll pick up text from the interface just as you did in Chapter 4, “Working with the AddressBook API for Contacts.” As you’ll see, the iOS/reminder code is very similar to the OS X/event code. The few differences are pointed out in the chapter. You will start from the code as of the end of Chapter 4.

Allocating and Getting Access to the Event Store

You allocate and init a variable in your app for the event store. By convention, it is called store, but you can call it anything you want. If a view controller on iOS is associated with all your event handling, this is typically done in the viewDidLoad method. If events are used throughout the app, you might want to call it from application:didFinishLaunchingWithOptions: in the app delegate. On OS X, you may want to call it from your window controller’s windowDidLoad method or, for a more pervasive structure, you might call it fromapplicationDidFinishLaunching: in the app delegate.

In the code snippets that follow, the event store is assumed to be a property of a view controller (iOS), a window controller (OS X), or the app delegate (on either). It is named eventStore.

Listing 5.1 shows how the event store can be declared in a class declaration. On OS X, this code could be placed at the top of AppDelegate.h. On iOS, it might be at the top of your view controller interface file. Note the import statements at the top of the file. You also need to add theEventKit framework to your project from the General tab. The code in Listing 5.1 also declares a new reminder button that will create a reminder from the text in userTextView.

Listing 5.1 Declaring the Event Store on iOS


#import <UIKit/UIKit.h> //1
#import <EventKit/EventKit.h> //2

@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextView *userTextView;
@property (nonatomic, strong) EKEventStore *eventStore; //3

@end


1 This is the Cocoa Touch framework that you almost always start with.

2 Add the EventKit headers. Remember to add the EventKit framework
in the General tab of your project.

3 This is the event store object for your class.

On OS X, the corresponding code may be placed at the top of AppDelegate.h, as you see in Listing 5.2.

Listing 5.2 Declaring the Event Store on OS X


#import <Cocoa/Cocoa.h> //1
#import <EventKit/EventKit.h> //2

@interface AppDelegate : NSObject <NSApplicationDelegate>

@property (assign) IBOutlet NSWindow *window;
@property (nonatomic, strong) EKEventStore *eventStore; //3
@property (nonatomic, strong) EKCalendar *defaultCalendar; //4

@end


1 This brings the Cocoa headers into the class.

2 This is the import of EventKit headers. Remember to add the EventKit framework in the General tab of your project.

3 This is your class’s event store object.

4 This is the default calendar for reminders. You will need it to create new events or reminders, but you can always get it from the event store. In this version, it’s stored in a public property. In the iOS version, it is obtained as needed from the event store.

This is the code to access the event store object for iOS:

EKEventStore *self.eventStore = [[EKEventStore alloc]init];

Once you have your local store, you can then request further access to it. (Before iOS 6, you did not need to request this access.)

Listing 5.3 shows the code for iOS 6 or later and OS X Mavericks (10.9) or later.

Listing 5.3 Requesting Access to Events on iOS 6 or Later and OS X Mavericks (10.9) or Later


self.eventStore = [[EKEventStore alloc] init]; //1

[self.eventStore requestAccessToEntityType: EKEntityTypeEvent
completion:^(BOOL granted, NSError *error) //2
{
if (!granted) { //3
// handle error
}
}];


1 Allocate and init a new event store.

2 This is the request for access to events.

3 This is the block for the completion handler.

Note that the code shown in Listing 5.3 is slightly different in OS X Mavericks (10.9) than in previous versions. It has been tweaked to make it identical to the iOS version in Mavericks.

Similarly, Listing 5.4 shows the allocation and access requests for reminders on iOS 6 or later and OS X Mavericks (10.9) or later.

Listing 5.4 Requesting Access to Reminders on iOS 6 or Later and OS X Mavericks (10.9) or Later


[self.eventStore requestAccessToEntityType: EKEntityTypeReminder
completion:^(BOOL granted, NSError *error) //2
{
if (!granted) {
//handle error}
}];


2 This is the only difference between Listing 5.3 and Listing 5.4—the constant specifying events (EKEntityTypeEvent in Listing 5.3) or the constant specifying reminders (EKEntityTypeReminder in Listing 5.4).

Creating a New Event or Reminder

The syntax for creating a new event or reminder is parallel in OS X and iOS. You use a class method to create the new object; you pass in your event store. You get the new event or reminder back, and you can then set its values. The values are not saved until you commit the changes.

As noted previously, the code in this chapter builds on the code from the end of Chapter 4.

Listing 5.5 shows the code to implement the button that will create a new event. This code can be used on iOS or OS X provided that you choose either line three (for OS X) or line four (for iOS).

Listing 5.5 Create a New Event in an Event Store


- (IBAction)tapEventButton:(id)sender {
EKEvent *newEvent = [EKEvent eventWithEventStore: _eventStore]; //1
newEvent.title = @"Test Event"; //2
newEvent.notes = [[_userTextView textStorage] string]; //3
newEvent.notes = userTextView.text; //4
newEvent.calendar = [_eventStore defaultCalendarForNewEvents]; //5
newEvent.startDate = [NSDate date]; //6
newEvent.endDate = [NSDate date]; //7
NSError *error = nil; //8
[_eventStore saveEvent: newEvent span:EKSpanThisEvent //9
commit:YES error:&error];
}


1 This is where you create a new event.

2 This sets the title.

3 This sets the notes from an iOS UITextView. Use only line 3 or line 4.

4 This sets the notes from an OS X NSTextView. Use only line 3 or line 4.

5 This sets the calendar for the new event. As you will see in Listing 5.6, you can also get it from the event store rather than storing it in a property.

6, 7 Events must have a start and end date (date and time in an NSDate). Without a start and end date (called an NSSpan), you will get an error.

8 Create a variable for the error message

9 Save the event.

Listing 5.6 shows the code to implement the button that will create a new reminder. This code can be used on iOS or OS X provided that you choose either line three (OS X) or line four (iOS).

Listing 5.6 Creating a New Reminder in an Event Store


#pragma mark - Create Reminder Button
- (IBAction)tapReminderButton:(id)sender {
EKReminder *newReminder =
[EKReminder reminderWithEventStore: _eventStore];

newReminder.title = @"Test";
newReminder.notes = [[_userTextView textStorage] string];
newReminder.notes = userTextView.text;
newReminder.calendar = [_eventStore defaultCalendarForNewReminders];
NSError *error = nil;
[_eventStore saveReminder: newReminder commit:YES error: &error];
}


Every instance of EKEvent or EKReminder has a unique identifier. That unique identifier is assigned when the object is created. If you want to be able to retrieve this specific item in the future, you can store it (see the following section for searching). Remember that reminders are subclasses of calendar items, so the calendarItem-Identifier property is available for both reminders and calendar items. This is a legacy of the class structure described in the sidebar at the beginning of the chapter. If you want to break all existing reminders and calendars, you can come up with a more streamlined class structure, but that’s not an option in most cases.

Here is the unique identifier property for both EKEvent and EKReminder instances:

@property(nonatomic, readonly)
NSString *calendarItemIdentifier;

As soon as you receive the new event or reminder back from the class method that creates it, you can access this unique identifier. However, until you commit the store, the object won’t be saved and the unique identifier will do you no good in the future. Note that this is not an argument for immediately committing the store: that may unnecessarily slow down your app. It is, however, an argument for making certain that you do commit the object (particularly before your app is suspended on iOS), and it’s a reminder of something to check if, during testing, you discover that your app is sometimes losing new events and reminders.

Searching for an Event or Reminder

There are two ways to search for events or reminders (and, because reminders are a subclass of events, in this chapter both will be referred to as events—which they are).

If you have an event identifier, you can use it to retrieve the event. Although identifiers are unique, in the case of recurring events, there can be multiple objects returned from this method. Only the first occurrence is returned. Here is the method of the store to call:

- (EKEvent *)eventWithIdentifier:(NSString *)identifier

You can also search based on dates in a predicate. Predicates are widely used in databases to select the records for inclusion (or exclusion) in a query. In SQL-speak, predicates are very roughly the WHERE clause. You use them with events just as you use them elsewhere in Cocoa and Cocoa Touch (they are particularly important in Core Data).

You create a predicate using a method of an event store as follows:

- (NSPredicate *)predicateForEventsWithStartDate:(NSDate *)startDate
endDate:(NSDate *)endDate
calendars:(NSArray *)calendars

You use the predicate to get an array of matching objects as in this code:

- (NSArray *)eventsMatchingPredicate:(NSPredicate *)predicate

You can also use the predicate to drive an enumerator, which executes a block for each object returned, as shown in Listing 5.7.

Listing 5.7 Enumerating Events with a Predicate and Block


- (void)enumerateEventsMatchingPredicate:(NSPredicate *)predicate
usingBlock:(EKEventSearchCallback)block


Thus, you have three ways of getting events:

Image You can retrieve them using the unique identifier. You get the first of any recurring events in this case. The unique identifier is found either when you first create an event or if you take it from one of the events returned with a predicate.

Image You can get all of the matching events in an array. It is common to immediately sort that array.

Image You can enumerate through the matching events running a block on each one.

These are all standard Cocoa operations, so they are not described further here.

Setting or Modifying Properties

The properties for calendar items and reminders fall into two groups:

Image Simple items

Image Groups of items

Table 5.1 shows the simple items and their types. With very few exceptions (the properties with types beginning EK), these are standard Cocoa types, so they are not described in detail here.

Image

Table 5.1 Simple Properties of Calendar Items

The groups of items are shown in Table 5.2. Unless otherwise noted, they return an array and a BOOL indicating if there are any of the items.

Image

Table 5.2 Group Properties of Calendar Items

In addition, there are methods to add and remove recurrence rules and alarms.

Finally, the properties for EKReminder objects beyond those for calendar items are shown in Table 5.3.

Image

Table 5.3 Simple Properties of Reminders

completed and completionDate work together. Setting completed to YES sets completionDate to today, and setting it to NO sets completionDate to nil. Likewise, setting completionDate to a value (or nil) sets completed accordingly.

The NSDateComponents class represents a remarkably flexible and powerful way of specifying dates along with calendars that refine them. All fields of the object are optional. If you are not familiar with this class, it’s worth looking at the class reference. They were one of the most admired features of NeXTSTEP (and then OPENSTEP and now OS X and iOS).

Committing Changes

As noted previously, your changes to reminders and events are not saved until you commit the changes to the store. When you commit a save or remove action, the changes are propagated to the event’s calendar, which may be on another computer or environment (CalDAV or Exchange, for example). These changes do not wait for a sync process via iCloud.

You must always ask the user’s permission before you modify the Calendar database. You have the option of committing changes as you make them or batching the updates for a single commit. The remainder of this section shows the methods you will use. The reminder methods are shown first because they are simpler than the event methods.

The commit parameter is a BOOL indicating whether the commit should be performed now or should wait for a batch commit later on.

To save a reminder, use the code in Listing 5.8.

Listing 5.8 Saving a Reminder


- (BOOL)saveReminder:(EKReminder *)reminder
commit:(BOOL)commit
error:(NSError **)error


To remove a reminder, use the code in Listing 5.9.

Listing 5.9 Removing a Reminder


- (BOOL)removeReminder:(EKReminder *)reminder
commit:(BOOL)commit
error:(NSError **)error


Events are a bit more complicated because they can recur. Thus, there is an added span parameter, which takes one of two constants: EKSpanThisEvent or EKSpanFutureEvents. The companion methods are discussed next.

To save an event, use the code in Listing 5.10.

Listing 5.10 Saving an Event


- (BOOL)saveEvent:(EKEvent *)event
span:(EKSpan)span
commit:(BOOL)commit
error:(NSError **)error


To remove an event, use the code in Listing 5.11.

Listing 5.11 Removing an Event


- (BOOL)removeEvent:(EKEvent *)event
span:(EKSpan)span
commit:(BOOL)commit
error:(NSError **)error


Finally, if you have uncommitted changes, make certain to commit them all to the store before you are finished with it:

- (BOOL)commit:(NSError **)error

Adding a Reminder to the App on iOS

This is the full code for you to use to add a reminder by tapping a button on iOS. In Chapter 4, you moved the contents of the text view into a new email message. Now, you can add a Reminder button to that app to move the text from the text view into a new reminder. The next section shows you the code to implement an Event button on OS X. Remember that you can mix and match the code—take the relevant parts of this code to implement adding a reminder on iOS or the relevant parts of the following code to implement adding an event on OS X. The listings earlier in the chapter focus on the key lines for each operating system as well as for events and appointments.

To prepare, add the EventKit framework to your project in the Linked Frameworks and Library section of the General tab. At the top of ViewController.h, add the following line:

#import <EventKit/EventKit.h>

You’ll also need the Reachability.h from the sample code on developer.apple.com, as described in Chapter 4.

You’ll also need an event store, so add this code to your view controller’s interface:

@property (nonatomic, strong) EKEventStore *eventStore;

Begin by declaring the store as shown in Listing 5.12. This code can go conveniently into ViewDidLoad of your view controller.

Listing 5.12 Declaring the Store on iOS


#import "ViewController.h"
#import "Reachability.h"

#import <MessageUI/MessageUI.h>
#import <MessageUI/MFMailComposeViewController.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.eventStore = [[EKEventStore alloc] init];

[self.eventStore requestAccessToEntityType: EKEntityTypeReminder
completion:^(BOOL granted, NSError *error)
{
if (!granted) {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle: @"Error"
message: @"Can't access reminders"
delegate: nil
cancelButtonTitle: @"OK"
otherButtonTitles: nil];
[alert show];
}
}];
}


Now that the store is declared, you need to create it and gain access to it, as shown in Listing 5.13.

Listing 5.13 Creating the Store on iOS


#import "ViewController.h"
#import "Reachability.h"

#import <MessageUI/MessageUI.h>
#import <MessageUI/MFMailComposeViewController.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_eventStore = [[EKEventStore alloc] init]; //1

[_eventStore requestAccessToEntityType: EKEntityTypeReminder
completion:^(BOOL granted, NSError *error) //2
{
if (!granted) { //3
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"Error"
message: @"Can't access reminders"
delegate: nil
cancelButtonTitle: @"OK"
otherButtonTitles: nil];
[alert show];
}
}];

}


1 Create the event store.

2 Request access.

3 If access is not granted, warn the user. Note that this is a placeholder type of message.

Create a reminder button and wire it up to a new method in the same way that you wired up the Email button in Chapter 4. Listing 5.14 shows what you need to do. The only choice that you have to make is whether to move the contents of the text view to notes or the reminder title. This is the same issue you confront with email. Because a text field is more limited than a text view, a text field is more appropriate for an email subject or a reminder title than a text view, which is more appropriate for the body of an email or a note.

Listing 5.14 Creating the Add Reminder Button on iOS


# #pragma mark - Create Reminder Button
- (IBAction)tapReminderButton:(id)sender {
EKReminder *newReminder =
[EKReminder reminderWithEventStore: _eventStore]; //1

newReminder.title = @"Test"; //2
newReminder.notes = _userTextView.text; //3
newReminder.calendar = [_eventStore defaultCalendarForNewReminders]; //4
NSError *error = nil;
[_eventStore saveReminder: newReminder commit:YES error: &error]; //5
}


1 This creates the new reminder in your store.

2 This sets the title.

3 This sets the notes.

4 This sets the calendar from the event store.

5 This saves and commits the reminder.

Adding an Event to the App on OS X

This code builds on the code from the end of Chapter 4 or the code you have just created to implement a button that adds an event to the app. Listing 5.15 shows the declaration of the store. It can go into AppDelegate.h or a window controller. It just needs to be available before any access to the store is needed.

Listing 5.15 Declaring the Store on OS X


#import <Cocoa/Cocoa.h> //1
#import <EventKit/EventKit.h> //2

@interface AppDelegate : NSObject <NSApplicationDelegate>

@property (assign) IBOutlet NSWindow *window;
@property (unsafe_unretained) IBOutlet NSTextView *userTextView;
@property (nonatomic, strong) EKEventStore *eventStore; //3
@property (nonatomic, strong) EKCalendar *defaultCalendar; //4

- (IBAction)tapButton:(id)sender;
- (IBAction)tapEventButton:(id)sender; //5
@end


1 This line imports the basic Cocoa headers.

2 This imports the EventKit headers. Make sure to add it to your frameworks in the General tab.

3 This line adds the event store property.

4 This is the default calendar property. You can also get it at runtime from the event store.

5 This is the new action for the new button.

In Listing 5.16, you see the code to create the store.

Listing 5.16 Creating the Store on OS X


- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
self.eventStore = [[EKEventStore alloc] init];
[self.eventStore requestAccessToEntityType: EKEntityTypeEvent
completion:^(BOOL granted, NSError *error) {
if (!granted) {
// Handle error
}
}];
}
@end


Finally, you provide the code for a button to actually do the work of adding the event, as shown in Listing 5.17.

Listing 5.17 Supporting the Button


- (IBAction)tapEventButton:(id)sender {
EKEvent *newEvent = [EKEvent eventWithEventStore: self.eventStore]; //1
newEvent.title = @"Test Event"; //2
newEvent.notes = [[_userTextView textStorage] string];
newEvent.calendar = [_eventStore defaultCalendarForNewEvents]; //3
newEvent.startDate = [NSDate date]; //4
newEvent.endDate = [NSDate date];
NSError *error = nil; //5
[_eventStore saveEvent: newEvent span:EKSpanThisEvent
commit:YES error:&error]; //6
}
@end


1 This line creates the event store.

2 This line and the following line set the title and notes.

3 This gets the default calendar for new events from the event store.

4 This line and the following line set the span of the event.

5 This line creates an error instance and sets it to nil.

6 This line creates the event. If an error occurs, it is returned in a new error object.

Chapter Summary

Your interaction with events and reminders is quite similar to your interaction with email. In both cases, you’re facilitating access to the user’s data that is synchronized through iCloud with various devices and, possibly, external servers such as Exchange. In Chapter 4, you saw how to use theMFMailComposeViewController class on iOS to provide your user interface. There is a similar interface for events available to you on iOS: EventKitUI.framework. In this chapter, you saw how to work directly with the Event Kit, so this code will work on both iOS and OS X.

Exercises

1. Move the code to iOS (and a real device). Add the EventKitUI framework to provide a graphical user interface for managing events. Use it to look at and edit your event. You might want to look at SimpleEKDemo from the sample code on developer.apple.com.

2. Instead of using separate buttons for mail and reminders in your interface, explore other interface possibilities. For example, on iPad, you might use an action sheet.

3. Get used to testing iCloud. That means working with multiple devices (and a single Apple ID). Create, view, edit, and delete data across the devices. After your first experiments, you might want to prepare a standard protocol so that you can run through it as needed. Vary it by reversing or randomizing the steps where it’s possible so that you catch errors.

4. On iOS devices, turn Airplane mode on and off as part of your testing. On your Mac(s), turn WiFi on and off and unplug Ethernet. You want to make certain that your code works properly, and you want to give iCloud a chance to catch conflicts.