How to create, remove and manage geofence reminders in iOS programmatically with Xcode

I’ve been a fan of geofencing since way back in the days of Where’s Tim. I spoke at the University of Kansas back in 2006 about how exciting it would be to one day have location aware checklists.  Now, with mobile devices running iOS 6, we can take advantage of the reminders API to add and remove location-based items. This saves us from writing a ton of code to manage our own geofencing framework.

I use this feature in Christian Radio Locator to allow users to set alerts when they enter into listening range of a particular station.

To add a reminder, we make sure the device is running iOS 6 or greater, then ask the user for permission to access their reminders. We create a new reminder and set the title and location. Then the distance range of when to alert the user. This will be specific to the situation. For a reminder triggered by Target, you’d want it to be only a 100 meters or so to avoid drive-by triggering. For a “call my wife when I’m getting close to home” situation, the range would be 2000 or 3000 meters. If everything is configured correctly, the API will add the reminder to the system and hand back an eventID, which should be store internally in the application to allow the user to remove the reminder and to avoid entering duplicates.

To remove a reminder, pass in the eventID and tell the API to remove it from the system. There seems to be a five to ten second delay, which makes debugging difficult. We can also query the reminders to display them in our app and to make sure the user hasn’t checked any off from Reminders.app.

Connecting to Event store

Here is the code to get the Event store. The store object will process all the add and remove messages. As the code indicates, the store object is cached locally once is has been successfully retrieved. There is also an unknown delay between requesting the store and the system asking the user for permission. That is why the providedAccess ivar is set via a block.

- (EKEventStore*) getEventStore{

    if (eventStore) {

        return eventStore;

    }

    

    EKEventStore *store = [[EKEventStore alloc] init];

    __block BOOL providedAccess = YES;

    BOOL atLeastIOS6 = [[[UIDevice currentDevice] systemVersion] floatValue] >= 6.0;

    if (!atLeastIOS6) {

        providedStoreAccess = NO;

        return nil;

    }

    [store requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) {

        providedAccess = granted;

    }];

    [self setProvidedStoreAccess:providedAccess];

    [self setEventStore:store];

    return store;

}

Adding location-based reminder to store

Now that we have the store, we can add a location-based reminder that will use the built-in geofencing. This will also display the outlined GPS symbol on the users Status Bar so they know they have a location-based reminder. Note that we are storing the eventID in NSUserDefaults so we can retrieve the details about it later.

- (BOOL) addReminder{

    //make sure there is a store to get to

    EKEventStore *store = [self getEventStore];

    if (!store) {

        return NO;

    }

    

    //make sure the user provided access to the store

    if (!providedStoreAccess) {

        return NO;

    }

    EKReminder *reminder = [EKReminder reminderWithEventStore:store];

    [reminder setTitle:[NSString stringWithFormat:@"Listen to %@ (%@)",station.ownedBy,station.frequency]];

    [reminder setNotes:[NSString stringWithFormat:@"%@ %i mile range",station.city, (int)[station.range doubleValue]]];

    //create the geofence alarm

    EKAlarm *enterAlarm = [[EKAlarm alloc] init];

    [enterAlarm setProximity:EKAlarmProximityEnter];

    EKStructuredLocation *enterLocation = [EKStructuredLocation locationWithTitle:[NSString stringWithFormat:@"%@ (%@)",station.ownedBy,station.frequency]];

    CLLocationDegrees lat = [station.lat doubleValue];

    CLLocationDegrees lng = [station.lng doubleValue];

    CLLocation *radioLocation = [[CLLocationalloc] initWithLatitude:lat longitude:lng];

    [enterLocation setGeoLocation:radioLocation];

    //convert our range from miles to meters then set the radius

    //the alarm will go off when the user enters this radius

    [enterLocation setRadius:[station.range doubleValue] * 1609];

    [enterAlarm setStructuredLocation:enterLocation];

    [reminder addAlarm:enterAlarm];

    [reminder setCalendar:[store defaultCalendarForNewReminders]];

 

    NSError *err;

    [store saveReminder:reminder commit:YES error:&err];

    [self setEventId:reminder.calendarItemIdentifier];

    

    //store the event id so we can track if the user

    //has a geofence reminder for this station

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSDictionary *reminders = [defaults objectForKey:@"reminders"];

    NSMutableArray *latlng = [NSMutableArray arrayWithArray:reminders.allKeys];

    NSMutableArray *itemIds = [NSMutableArray arrayWithArray:reminders.allValues];

    

    [latlng addObject:[NSString stringWithFormat:@"%@%@",station.lat,station.lng]];

    [itemIds addObject:eventId];

    

    NSDictionary *newReminders = [NSDictionary dictionaryWithObjects:itemIds forKeys:latlng];

    [defaults setObject:newReminders forKey:@"reminders"];

    [defaults synchronize];

    

    return YES;

}

Removing reminders

If the user wishes to remove or check off the event, they can do so in reminders.app, or we can do it programatically.

- (BOOL) removeReminder {

    if (eventId.length == 0) {

        return NO;

    }

    EKEventStore *store = [self getEventStore];

    if (!store) {

        return NO;

    }

    if (!providedStoreAccess) {

        return NO;

    }

    

    EKReminder *reminder = (EKReminder *)[store calendarItemWithIdentifier:eventId];

 

    NSError *err;

    [store removeReminder:reminder commit:YES error:&err];

    

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSDictionary *reminders = [defaults objectForKey:@"reminders"];

    NSMutableArray *latlng = [NSMutableArray arrayWithArray:reminders.allKeys];

    NSMutableArray *itemIds = [NSMutableArray arrayWithArray:reminders.allValues];

    

    [latlng removeObject:[NSString stringWithFormat:@"%@%@",station.lat,station.lng]];

    [itemIds removeObject:eventId];

    

    NSDictionary *newReminders = [NSDictionary dictionaryWithObjects:itemIds forKeys:latlng];

    [defaults setObject:newReminders forKey:@"reminders"];

    [defaults synchronize];

    

    [self setEventId:@""];

    returnYES;

    

}

Staying in sync with Reminders.app

We want to be able to display to the user the reminders that our app has set for them. We also need to make sure that we stay in sync with the Event store in case the user checked off or deleted a reminder that our app created.

- (void) setReminderCellDisplay{

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    

    NSDictionary *reminders = [defaults objectForKey:@"reminders"];

    

    //check if the user has any stored reminders to display to them

    if (reminders.count == 0) {

        return;

    }

    

    EKEventStore *store = [self getEventStore];

    if (!store) {

        return;

    }

    if (!providedStoreAccess) {

        return;

    }

    NSMutableArray *remindersToRemove = [[NSMutableArray alloc] init];

    for (NSString *key in reminders) {

        //making sure that no events have been removed outside of this application

        //if so, we are going to remove it from the collection and not mark the reminder cell

        BOOL skip = NO;

        NSString *eventIdFromReminders = [reminders valueForKey:key];

        EKReminder *reminder = (EKReminder *)[store calendarItemWithIdentifier:eventIdFromReminders];

        if (!reminder || reminder.completed) {

            [remindersToRemove addObject:eventIdFromReminders];

            skip = YES;

            NSLog(@”Need to remove:%@”,eventIdFromReminders);

        }

        //this station does have a reminder associated with it

        if ([[NSString stringWithFormat:@"%@%@",station.lat,station.lng] isEqualToString:key] && !skip) {

            [reminderCell setAccessoryType:UITableViewCellAccessoryCheckmark];

            [self setEventId:eventIdFromReminders];

        }

    }

    //nothing to remove…carry on

    if (remindersToRemove.count == 0) {

        return;

    }

    

    NSMutableArray *latlng = [NSMutableArray arrayWithArray:reminders.allKeys];

    NSMutableArray *itemIds = [NSMutableArray arrayWithArray:reminders.allValues];

    

    //we have a reminder that we need to get rid of

    for (NSString *eventIdToRemove in remindersToRemove) {

        NSUInteger index = [itemIds indexOfObject:eventIdToRemove];

        [latlng removeObjectAtIndex:index];

        [itemIds removeObjectAtIndex:index];

        NSLog(@”Removed:%@”,eventIdToRemove);

    }

    

    NSDictionary *newReminders = [NSDictionary dictionaryWithObjects:itemIds forKeys:latlng];

    [defaults setObject:newReminders forKey:@"reminders"];

    [defaults synchronize];

}

Summary

This blog post showed how to create and manage location-based reminders for iPhones and iPads. Geofencing is a powerful tool that we can use to make our lives more productive…or to make sure we don’t miss any of our favorite radio stations.

5 thoughts on “How to create, remove and manage geofence reminders in iOS programmatically with Xcode

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>