Using NSFetchedResultsController with an MKMapView

If you’re familiar with Core Data, you’ve most likely encountered an NSFetchedResultsController at some point, and if so, you’ve doubtless linked it to a UITableView. A challenge I ran across recently was to not only list data from Core Data in a UITableView, but to list that same data in an MKMapView. One thing I quickly remembered: MKMapView’s don’t use index paths (NSIndexPath) like UITableViews do. 

I’m going to walk through how I utilized an NSFetchedResultsController and its delegate methods to easily implement it into an MKMapView. I also picked up a neat trick with the MKAnnotation protocol (from Paul Hegarty’s CS193p course in iTunes U) that helped make things easier to understand and implement. 

While looking around for examples of others who have done this, I came across a wonderful article on just the subject. My implementation was a little different, so I felt compelled to write about it, but I did not want to pass on linking to the original article: http://flowandgo.blogspot.com/2011/08/having-fun-with-nsfetchedresultscontrol.html.

Let me start with the MKAnnotation. MKAnnotation is a protocol used by MKMapView to guarantee that any object adhering to it will provide the proper information for the map to display the annotations with proper titles, subtitles, and on the proper coordinate. In my app I already had a Core Data entity with properties for latitude and longitude, so I simply made my human-editable NSManagedObject subclass (in this case, “FamilyMember”. Hint: use MOGenerator. You’re welcome) adhere to the MKAnnotation protocol, and provided the following three methods in its private implementation:

@implementation FamilyMember
- (NSString)title {
    return self.name;
}
- (NSString *)subtitle {
    return self.address;
}
- (CLLocationCoordinate2D)coordinate {
    CLLocationCoordinate2D coord;
    coord.latitude = [self.latitude doubleValue]; // or self.latitudeValue à la MOGen
    coord.longitude = [self.longitude doubleValue];
    return coord;
}
@end

I like doing the whole annotation bit this way, as it saves creating another class just to strictly be the annotation, and then alloc/init'ing a bunch of them throughout the app lifecycle. Having it directly in the entity saves time as the entities are already in an array inside the NSFetchedResultsController, and also reduces code.

OK. Now that your entity is officially also an MKAnnotation object, it will be easier to work with the MKMapView delegate methods. But first, let’s add the NSFetchedResultsController. Open your view controller’s .m file which has your mapView in it, import <CoreData/CoreData.h>, and add this code:


#import <CoreData/CoreData.h>
@interface your_class () <MKMapViewDelegate, NSFetchedResultsControllerDelegate>
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
...any others...
@end

@implementation your_class 
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    switch (type) {
        case NSFetchedResultsChangeInsert:
            [self fetchedResultsChangeInsert:anObject];
            break;
        case NSFetchedResultsChangeDelete:
            [self fetchedResultsChangeDelete:anObject];
            break;
        case NSFetchedResultsChangeUpdate:
            [self fetchedResultsChangeUpdate:anObject];
            break;
        case NSFetchedResultsChangeMove:
            // do nothing
            break;
            
        default:
            break;
    }
}

- (void)fetchedResultsChangeInsert:(FamilyMember *)familyMember
{
    [self.mapView addAnnotation:familyMember];
}

- (void)fetchedResultsChangeDelete:(FamilyMember *)familyMember
{
    [self.mapView removeAnnotation:familyMember];
}

- (void)fetchedResultsChangeUpdate:(FamilyMember *)familyMember
{
    [self fetchedResultsChangeDelete:familyMember];
    [self fetchedResultsChangeInsert:familyMember];
}
@end

The first two methods just simply start the activity indicator in the status bar to show something is happening; they are not necessary. The last three methods are convenience methods. The controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is called to let you handle how you want to handle the object in question. We simply use a switch() command to check what the change type was, and then handle it accordingly. As you can see, with just one line of code per operation (okay, two for Update), you’re altering the map with whatever familyMember object was updated/inserted/deleted (Move doesn’t really apply). These methods are called when the NSFetchedResultsController observes changes in the entities you specify, which we’ll do here. Still in the same file, add the following:


- (NSFetchedResultsController *)fetchedResultsController {
    if (_fetchedResultsController) return _fetchedResultsController;
    
    NSFetchedResultsController *frc = nil;
    
    if (self.managedDocument) {  // using UIManagedDocument for Core Data storage
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"FamilyMember"];
        NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
        [request setSortDescriptors:@[sortDescriptor]];
        NSManagedObjectContext *moc = self.managedDocument.managedObjectContext;
        frc = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:nil cacheName:nil];
        [frc setDelegate:self];
        
        _fetchedResultsController = frc;
        
        NSError *error = nil;
        [_fetchedResultsController performFetch:&error];
        
        [self configureAnnotations];
    }
    
    return _fetchedResultsController;
}

- (void)configureAnnotations
{
    [self.mapView removeAnnotations:self.mapView.annotations];
    [self.mapView addAnnotations:[self.fetchedResultsController fetchedObjects]];
}

// called from AppDelegate, to set the UIMD, and set up the NSFRC
- (void)setManagedDocument:(UIManagedDocument *)managedDocument
{
    _managedDocument = managedDocument;
    [self fetchedResultsController];
}

Breaking this down, we have a custom getter for our _fetchedResultsController property, which returns it immediately if it already exists, or creates one with a fetch request and performs a fetch. The -configureAnnotations method just removes and re-adds the annotations from the _fetchedResultsController’s “fetchedObjects” property, which is an NSArray of the entity you specified (in our case, FamilyMember). The -setManagedDocument: method is there so I can immediately call the getter for the NSFetchedResultsController, since now it has an NSManagedObjectContext from which to fetch objects.

Almost there.  That should be it for the NSFetchedResultsController. Now, for the MKMapView delegate methods. Still in the same file, enter the following:


- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation
{
    MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"MyPin"];
    if (!annotationView) {
        annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"MyPin"];
        annotationView.canShowCallout = YES;
        annotationView.animatesDrop = YES;
    }
    
    annotationView.annotation = annotation;
    
    return annotationView;
}

That’s it! Because we set the FamilyMember entity to be a conformer of the MKAnnotation protocol, the “annotation” in this method is the FamilyMember object itself, and it will now place a pin on the map where the .coordinate property tells it to go. 

I hope this helps you, and you find it helpful for any project you’re working on!

Twitter: @bjmillerltd

App.net: @bjmiller