Remind Me At – geofenced reminder tutorial, part 2

Remind Me At
We’re continuing development of Remind Me At, solution for very specific problem – create geofenced reminder by tapping on a map.

We’ve designed our map, added code to show existing reminders, code to put a new pin on the map. Now it is time to review communication with EventKit. We’re using approach similar to iRemember.

Singleton class for management reminders.

typedef void (^RMReminderFetchCompletionBlock)(NSArray *reminders);
typedef void (^RMReminderAddCompletionBlock)(EKReminder *reminder);
typedef void (^RMReminderOperationCompletionBlock)(BOOL result);

extern NSString * const RMReminderManagerAccessGrantedNotification;
extern NSString * const RMReminderManagerReminderUpdatedNotification;

@interface RMReminderManager : NSObject

// singleton

+(RMReminderManager*)defaultManager;

@property (nonatomic, strong, readonly) EKEventStore *store;

// Access to reminders

@property (atomic, readonly) BOOL accessGranted;
-(void)requestAccess;
-(void)requestAccessAndWait;

// Verification methods

-(BOOL)isCalendarIdentifierValid:(NSString*)calendarIdentifier;

// Fetch and add reminders

-(void)fetchGeofencedRemindersWithCompletion:(RMReminderFetchCompletionBlock)completionBlock;

-(void)addReminderWithAnnotation:(RMGeofencedReminderAnnotation*)annotation
        inCalendarWithIdentifier:(NSString*)calendarIdentifier
                      completion:(RMReminderAddCompletionBlock)completionBlock;

-(void)saveReminder:(EKReminder*)reminder 
     withCompletion:(RMReminderOperationCompletionBlock)completionBlock;

-(void)removeReminder:(EKReminder*)reminder 
       withCompletion:(RMReminderOperationCompletionBlock)completionBlock;

// Sources, calendars


-(EKCalendar*)defaultReminderCalendar;
-(EKCalendar*)calendarWithIdentifier:(NSString*)identifier;

@end

Most of things are identical and simple. Let’s focus on fetch process. Our code should get all reminders from all calendars which are incomplete and have geofenced alarms set.

-(void)fetchGeofencedRemindersWithCompletion:(RMReminderFetchCompletionBlock)completionBlock
{
    if (self.accessGranted)
    {
        NSPredicate *predicate = [self.store predicateForIncompleteRemindersWithDueDateStarting:nil
                                                                                         ending:nil
                                                                                      calendars:nil];
        
        if (completionBlock)
        {
            RMReminderFetchCompletionBlock completion = [completionBlock copy];
            
            [self.store fetchRemindersMatchingPredicate:predicate 
                                             completion:^(NSArray * reminders) {
                NSMutableArray *array = [NSMutableArray array];
                for (EKReminder *reminder in reminders)
                {
                    BOOL hasGeofencedAlarm = NO;
                    for (EKAlarm *alarm in reminder.alarms)
                    {
                        if ((alarm.structuredLocation) &&
                            ((alarm.proximity == EKAlarmProximityLeave) || 
                             (alarm.proximity == EKAlarmProximityEnter)))
                        {
                            hasGeofencedAlarm = YES;
                            break;
                        }
                    }
                    if (hasGeofencedAlarm)
                    {
                        [array addObject:reminder];
                    }
                }
                
                completion(array);
            }];
        }
    }
    else if (completionBlock)
    {
        completionBlock(nil);
    }
}

There are several things I want to mention here.

Code checks that completion block is not NULL. If it is NULL, then no need to run any code – results will not be seen by anyone.

Next – code is creating copy of the completion block. This is required since we’re doing asynchronous call. Block itself is allocated in stack, and we have to move it to heap (yes, those memory management things; basically, this means that block is valid only until caller function is running, so we need to make a copy of it).

One more thing to mention – reminders are queried with a NSPredicate returned by one of the EKStore methods. However, we can’t combine that predicate with regular ones (for example, to query only those reminders that have alarms with location set). So, we have to fetch all incomplete reminders and then iterate through those checking that reminder has an alarm with location.

Geofenced alarm could be triggered by entering or leaving specific place. That’s defined in EKAlarm‘s proximity property.

Now let’s look how reminders are created.

-(void)addReminderWithAnnotation:(RMGeofencedReminderAnnotation*)annotation
        inCalendarWithIdentifier:(NSString*)calendarIdentifier
                      completion:(RMReminderAddCompletionBlock)completionBlock;
{
    if (self.accessGranted)
    {
        RMReminderAddCompletionBlock completion = [completionBlock copy];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            EKReminder *reminder = [EKReminder reminderWithEventStore:self.store];
            reminder.calendar = [self.store calendarWithIdentifier:calendarIdentifier];
            reminder.title = annotation.title;
            
            EKAlarm *alarm = [[EKAlarm alloc] init];
            alarm.proximity = annotation.proximity;
            alarm.structuredLocation = [EKStructuredLocation locationWithTitle:annotation.subtitle];
            alarm.structuredLocation.geoLocation = [[CLLocation alloc] initWithLatitude:annotation.coordinate.latitude
                                                                              longitude:annotation.coordinate.longitude];
            alarm.structuredLocation.radius = 0.0f;
            
            reminder.alarms = @[ alarm ];
            
            NSError * __autoreleasing error;
            
            if (![self.store saveReminder:reminder commit:YES error:&error])
            {
                NSLog(@"failure saving reminder: %@", error);
            }
            if (completion)
            {
                completion(reminder);
            }
            [[NSNotificationCenter defaultCenter] postNotificationName:RMReminderManagerReminderUpdatedNotification
                                                                object:self];
        });
    }
    else if (completionBlock)
    {
        completionBlock(nil);
    }
}

Everything is pretty easy. We’re using our intermediate class RMGeofencedReminderAnnotation to pass information (title, subtitle, coordinate, proximity). Our RMAddReminderViewController will out these details before calling this method, we’ll talk about it in next post.

We’re also posting custom notification RMReminderManagerReminderUpdatedNotification to signal RMMapViewController to update map.

That’s it for today. Next time we’ll see how view controllers for reminders are created, how to use standard calendar chooser and how to unwind view controllers.

For more information, consult Apple documentation – Calendar and Reminders Programming Guide, Event Kit Framework Reference.
Sources of Remind Me At app are available on GitHub (use freely under terms of MIT License).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s