Remind Me At – geofenced reminder tutorial, part 1

Remind Me At
Recently, we’ve created iRemember – shopping list app that uses EventKit framework. Now we’ll talk about some more of its features.

But first, let’s discuss our goal. With Reminders app in iOS we now can create geofenced reminders – reminders that fire when you enter or leave specific area. For example, you can be reminded to call home when you’re leaving your office. This really helps in some cases. The only thing that bothers me – you have to enter address manually or select it from address book. You can’t just point a place on a map as a location for reminder.

I’ve even filed a radar (rdar://13440874, thanks to @Jury for the “RADAR OR GTFO” policy). But for now we’ll solve this issue by creating a new app – Remind Me At.

What will we cover in this tutorial?

  • EventKit and EventKitUI frameworks – iOS SDK gives you a controller to choose calendars;
  • MapKit framework for displaying map and our objects on it;
  • briefly, we’ll talk about CoreLocation framework covering geocoding;
  • and for very strange reason, we’ll even use AddressBookUI framework;
  • we’ll also look into unwind (or, exit) segues and ways of not using them.


First, let’s check out our interface. It is quite simple.
Remind Me At Storyboard
We have a view controller with MKMapView and two UITableViewController‘s for showing and adding reminders. Both of those are using static cells approach, which is quite convenient in this case.

So, we’ll start with the map. RMMapViewController displays existing reminders with location alarm and allows to create new in any place on the map.

How does MKMapView know where to show our information?
We add and remove annotations by using -addAnnotation:, -addAnnotations:, -removeAnnotation:, -removeAnnotations: Annotations are objects adopting MKAnnotation protocol. It is very simple, so we’ll create a concrete class for our annotations.

@interface RMGeofencedReminderAnnotation : NSObject <MKAnnotation>

-(instancetype)initWithCoordiate:(CLLocationCoordinate2D)coordinate 
                           title:(NSString*)title 
                        subtitle:(NSString*)subtitle;

@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;

@property (nonatomic, assign) EKAlarmProximity proximity;
@property (nonatomic, assign) BOOL isFetched;
@property (nonatomic, strong) EKReminder *reminder;

@end

This class allows us to store information about any reminder with location. Properties coordinate, title and subtitle are parts of MKAnnotation protocol, others will be used by our code.

Notice the instancetype return type of designated initializer. This is one of new Objective-C features. It tells compiler and Xcode exact class being returned by method. In fact, for initializers Xcode automatically convert id return type to instancetype, however, it is still a good pattern to use instancetype in initializers and factory methods (like if we wanted to create +reminderAnnotationWithCoordinate:title:subtitle: method).

How does MKMapView display our annotations on a map?
Let’s now assume that we have those fetched. Once we’ve set annotations, we need to implement a MKMapViewDelegate method.

-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    if ([annotation isKindOfClass:[RMGeofencedReminderAnnotation class]])
    {
        static NSString *reminderAnnotationIdentifier = @"ReminderAnnotation";
        MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:reminderAnnotationIdentifier];
        if (!annotationView)
        {
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:reminderAnnotationIdentifier];
            annotationView.canShowCallout = YES;
        }
        else
        {
            annotationView.annotation = annotation;
        }
        RMGeofencedReminderAnnotation *geofencedReminder = (RMGeofencedReminderAnnotation *)annotation;
        annotationView.pinColor = geofencedReminder.isFetched ? MKPinAnnotationColorGreen : MKPinAnnotationColorRed;
        annotationView.rightCalloutAccessoryView =  geofencedReminder.isFetched ? [UIButton buttonWithType:UIButtonTypeDetailDisclosure] :
                                                                                  [UIButton buttonWithType:UIButtonTypeContactAdd];
        annotationView.animatesDrop = !geofencedReminder.isFetched;
        
        return annotationView;
    }
    else
    {
        return nil;
    }
}

We’re creating MKPinAnnotationView to display reminders. We’re also checking that annotation passed is of our class. This check is required because there is another special annotation on the map – user’s current location. And if we return nil for that annotation, MKMapView will use it’s default implementation which is fine for us.

MKMapView uses view-queue mechanism similar to UITableView‘s. So, we’re trying to dequeue a view, and create new if there are no views in queue.

Part of MKPinAnnotationView configuration is to use green color for existing reminders and red color for the pin which user just placed on the map. Also we’re showing “add” button for new reminder and “detail disclosure” button for existing reminders on pin call-outs (gray panel which is shown when user taps on a pin). And last difference is newly added pins are “dropped” to map with nice animation, while existing reminders are just shown.

We’re using isFetched property of RMGeofencedReminderAnnotation to distinguish between fetched reminders and newly added. We’ll return to this in reminder fetching code.

Notice that we’re not setting target/action for button. Taps on this button should be handled by another MKMapViewDelegate method.

-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view 
                     calloutAccessoryControlTapped:(UIControl *)control
{
    if ([view.annotation isKindOfClass:[RMGeofencedReminderAnnotation class]])
    {
        RMGeofencedReminderAnnotation *annotation = (RMGeofencedReminderAnnotation*)view.annotation;
        if (annotation.isFetched)
        {
            [self performSegueWithIdentifier:@"ShowReminder" sender:view];
        }
        else
        {
            [self performSegueWithIdentifier:@"AddReminder" sender:view];
        }
    }
}

Implementation is rather simple. When user taps on a button, we either performing ShowReminder segue, if reminder was fetched; or AddReminder segue for newly added pins.

Let’s now see how new pin is added. We’ve added UILongPressGestureRecognizer to RMMapViewController in storyboard and connected it to MKMapView. Default settings are fine, pin is dropped after half a second (this is considered as “Began” state of gesture).

-(IBAction)longPress:(UILongPressGestureRecognizer*)recognizer
{
    if (recognizer.state == UIGestureRecognizerStateBegan)
    {
        CLLocationCoordinate2D coordinate = [self.mapView convertPoint:[recognizer locationInView:self.mapView] 
                                                  toCoordinateFromView:self.mapView];
        if (self.addedReminderAnnotation)
        {
            [self.mapView removeAnnotation:self.addedReminderAnnotation];
        }
        self.addedReminderAnnotation = [[RMGeofencedReminderAnnotation alloc] initWithCoordiate:coordinate
                                                                                title:NSLocalizedString(@"New reminder here", @"New reminder placeholder")
                                                                             subtitle:nil];
        [self.mapView addAnnotation:self.addedReminderAnnotation];
        CLLocation *location = [[CLLocation alloc] initWithLatitude:coordinate.latitude 
                                                          longitude:coordinate.longitude];
        [self.geocoger reverseGeocodeLocation:location 
                            completionHandler:^(NSArray *placemarks, NSError *error) {
            if ((placemarks) && (!error))
            {
                CLPlacemark *placemark = [placemarks lastObject];
                self.addedReminderAnnotation.subtitle = ABCreateStringWithAddressDictionary(placemark.addressDictionary, NO);
            }
        }];
    }
}

Implementation again is quite simple:

  1. We’re getting geographical coordinate of the gesture;
  2. Then we’re removing previous “new” pin;
  3. We’re creating annotation object with new coordinate;
  4. We’re adding it to MKMapView;
  5. We’re launching reverse geocoding process to add address description.

For reverse geocoding we’re using CLGeocoder object. -reverseGeocodeLocation:completionHandler: method runs asynchronously and returns array of CLPlacemark objects. We’re taking last one.

And then we’re using ABCreateStringWithAddressDictionary function to create string from structured address. Strangely, this function does only exist in AddressBookUI framework.

But that’s all for now. Next time we’ll focus on reminder fetching/creation (which is very similar to approach used in iRemember).

Consult Apple’s documentation: Location awareness programming, MKMapView reference, CLGeocoder 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