/mapkit-animated-overlays

iOS MapKit Animated Overlays

Primary LanguageObjective-CMIT LicenseMIT

MapKit Animated Overlays

--

Introduction

In response to a Stack Overflow question, Draw polygon overlay by free hand using MKMapView on iOS, I wanted to share some code I've used in the past. The basic idea would be to recognize touches, draw MKPolyLine as touches move, and when done, replace MKPolyline with MKPolygon when all done.

For example, I'm assuming that you have a button that toggles between "start drawing polygon" and "done drawing polygon", which might do something like:

- (IBAction)didTouchUpInsideDrawButton:(id)sender
{
    if (!self.isDrawingPolygon)
    {
        // We're starting the drawing of our polyline/polygon, so
        // let's initialize everything
        
        self.isDrawingPolygon = YES;
        
        self.coordinates = [NSMutableArray array];
        
        // turn off map interaction so touches can fall through to
        // here
        
        self.mapView.userInteractionEnabled = NO;
        
        // change our "draw overlay" button to "done"
        
        [self.drawOverlayButton setTitle:@"Done"];
    }
    else
    {
        // We're finishing the drawing of our polyline, so let's
        // clean up, remove polyline, and add polygon
        
        self.isDrawingPolygon = NO;
        
        // let's restore map interaction
        
        self.mapView.userInteractionEnabled = YES;
        
        // and, if we drew more than two points, let's draw our polygon
        
        NSInteger numberOfPoints = [self.coordinates count];

        if (numberOfPoints > 2)
        {
            CLLocationCoordinate2D points[numberOfPoints];
            for (NSInteger i = 0; i < numberOfPoints; i++)
                points[i] = [self.coordinates[i] MKCoordinateValue];
            [self.mapView addOverlay:[MKPolygon polygonWithCoordinates:points count:numberOfPoints]];
        }
        
        // and if we had a previous polyline, let's remove it
        
        if (self.polyLine)
            [self.mapView removeOverlay:self.polyLine];
        
        // change our "draw overlay" button to "done"
        
        [self.drawOverlayButton setTitle:@"Draw overlay"];
        
        // remove our old point annotation
        
        if (self.previousAnnotation)
            [self.mapView removeAnnotation:self.previousAnnotation];
    }
}

And we'd need some properties to keep track of the polygon that we're drawing:

@property (nonatomic, strong) NSMutableArray *coordinates;
@property (nonatomic, weak) MKPolyline *polyLine; // used during the drawing of the polyline, but discarded once the MKPolygon is added
@property (nonatomic, weak) MKPointAnnotation *previousAnnotation; // if you're using other annotations, you might want to use a custom annotation type here
@property (nonatomic) BOOL isDrawingPolygon;

You then would make sure that the underlying view recognizes touches:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (!self.isDrawingPolygon)
        return;
    
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];

    [self addCoordinate:coordinate replaceLastObject:NO];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (!self.isDrawingPolygon)
        return;
    
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];
    
    [self addCoordinate:coordinate replaceLastObject:YES];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (!self.isDrawingPolygon)
        return;
    
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];
    
    [self addCoordinate:coordinate replaceLastObject:YES];

    // detect if this coordinate is close enough to starting
    // coordinate to qualify as closing the polygon

    if ([self isClosingPolygonWithCoordinate:coordinate])
        [self didTouchUpInsideDrawButton:nil];
}

That might use an addCoordinate method that either adds a line segment (or replaces the last line segment) and shows an annotation for where the user currently tapped, like so:

- (void)addCoordinate:(CLLocationCoordinate2D)coordinate replaceLastObject:(BOOL)replaceLast
{
    if (replaceLast && [self.coordinates count] > 0)
        [self.coordinates removeLastObject];
    
    [self.coordinates addObject:[NSValue valueWithMKCoordinate:coordinate]];
    
    NSInteger numberOfPoints = [self.coordinates count];
    
    // if we have more than one point, then we're drawing a line segment
    
    if (numberOfPoints > 1)
    {
        MKPolyline *oldPolyLine = self.polyLine;
        CLLocationCoordinate2D points[numberOfPoints];
        for (NSInteger i = 0; i < numberOfPoints; i++)
            points[i] = [self.coordinates[i] MKCoordinateValue];
        MKPolyline *newPolyLine = [MKPolyline polylineWithCoordinates:points count:numberOfPoints];
        [self.mapView addOverlay:newPolyLine];
        
        self.polyLine = newPolyLine;
        
        // note, remove old polyline _after_ adding new one, to avoid flickering effect
        
        if (oldPolyLine)
            [self.mapView removeOverlay:oldPolyLine];
        
    }
    
    // let's draw an annotation where we tapped
    
    MKPointAnnotation *newAnnotation = [[MKPointAnnotation alloc] init];
    newAnnotation.coordinate = coordinate;
    [self.mapView addAnnotation:newAnnotation];
    
    // if we had an annotation for the previous location, remove it
    
    if (self.previousAnnotation)
        [self.mapView removeAnnotation:self.previousAnnotation];
    
    // and save this annotation for future reference
    
    self.previousAnnotation = newAnnotation;
}

The touchesEnded also checks to see if the user closed the polygon, so here's the method that does that check, converting the current coordinate and the first coordinate, calculating the distances between them on the screen:

- (BOOL)isClosingPolygonWithCoordinate:(CLLocationCoordinate2D)coordinate
{
    if ([self.coordinates count] > 2)
    {
        CLLocationCoordinate2D startCoordinate = [self.coordinates[0] MKCoordinateValue];
        CGPoint start = [self.mapView convertCoordinate:startCoordinate
                                          toPointToView:self.mapView];
        CGPoint end = [self.mapView convertCoordinate:coordinate
                                        toPointToView:self.mapView];
        CGFloat xDiff = end.x - start.x;
        CGFloat yDiff = end.y - start.y;
        CGFloat distance = sqrtf(xDiff * xDiff + yDiff * yDiff);
        if (distance < 30.0)
        {
            [self.coordinates removeLastObject];
            return YES;
        }
    }
    
    return NO;
}

Finally, make sure you have your view controller set up as the delegate for the map view and that you have a viewForOverlay that will draw the polyline and polygon:

#pragma mark - MKMapViewDelegate

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
    MKOverlayPathView *overlayPathView;
    
    if ([overlay isKindOfClass:[MKPolygon class]])
    {
        overlayPathView = [[MKPolygonView alloc] initWithPolygon:(MKPolygon*)overlay];
        
        overlayPathView.fillColor = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
        overlayPathView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.7];
        overlayPathView.lineWidth = 3;
        
        return overlayPathView;
    }

    else if ([overlay isKindOfClass:[MKPolyline class]])
    {
        overlayPathView = [[MKPolylineView alloc] initWithPolyline:(MKPolyline *)overlay];
        
        overlayPathView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.7];
        overlayPathView.lineWidth = 3;
        
        return overlayPathView;
    }
    
    return nil;
}

Likewise, since we're showing an annotation as to where the user tapped on the screen, let's define viewForAnnotation to show that annotation:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    if ([annotation isKindOfClass:[MKUserLocation class]])
        return nil;
    
    static NSString * const annotationIdentifier = @"CustomAnnotation";
    
    MKAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
    
    if (annotationView)
    {
        annotationView.annotation = annotation;
    }
    else
    {
        annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
        annotationView.image = [UIImage imageNamed:@"annotation.png"];
    }
    
    annotationView.canShowCallout = NO;
    
    return annotationView;
}

There are all sorts of refinements you might want (e.g. add draggable custom MKAnnotation annotations for the vertices so that you can drag and drop them, updating your MKPolygon overlays in the process; save the coordinate arrays in your own model structure; etc.) but I'll leave that to you.

But this should show you enough about animating the drawing of a MKPolyline overlay to get you going.


This was developed using Xcode 4.6.2 for devices running iOS 6.1 or later (though the concepts are applicable to earlier versions of iOS).


If you have any questions, do not hesitate to post an issue here on Github:

Rob Ryan

29 April 2013