iOS + MKMapView user touch based drawing iOS + MKMapView user touch based drawing ios ios

iOS + MKMapView user touch based drawing


I have an app that basically does this. I have a map view, with a toolbar at the top of the screen. When you press a button on that toolbar, you are now in a mode where you can swipe your finger across the map. The start and end of the swipe will represent the corners of a rectangle. The app will draw a translucent blue rectangle overlay to show the area you've selected. When you lift your finger, the rectangular selection is complete, and the app begins a search for locations in my database.

I do not handle circles, but I think you could do something similar, where you have two selection modes (rectangular, or circular). In the circular selection mode, the swipe start and end points could represent circle center, and edge (radius). Or, the two ends of a diameter line. I'll leave that part to you.

Implementation

First, I define a transparent overlay layer, that handles selection (OverlaySelectionView.h):

#import <QuartzCore/QuartzCore.h>#import <MapKit/MapKit.h>@protocol OverlaySelectionViewDelegate// callback when user finishes selecting map region- (void) areaSelected: (CGRect)screenArea;@end@interface OverlaySelectionView : UIView {@private        UIView* dragArea;    CGRect dragAreaBounds;    id<OverlaySelectionViewDelegate> delegate;}@property (nonatomic, assign) id<OverlaySelectionViewDelegate> delegate;@end

and OverlaySelectionView.m:

#import "OverlaySelectionView.h"@interface OverlaySelectionView()@property (nonatomic, retain) UIView* dragArea;@end@implementation OverlaySelectionView@synthesize dragArea;@synthesize delegate;- (void) initialize {    dragAreaBounds = CGRectMake(0, 0, 0, 0);    self.userInteractionEnabled = YES;    self.multipleTouchEnabled = NO;    self.backgroundColor = [UIColor clearColor];    self.opaque = NO;    self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;}- (id) initWithCoder: (NSCoder*) coder {    self = [super initWithCoder: coder];    if (self != nil) {        [self initialize];    }    return self;}- (id) initWithFrame: (CGRect) frame {    self = [super initWithFrame: frame];    if (self != nil) {        [self initialize];    }    return self;}- (void)drawRect:(CGRect)rect {    // do nothing}#pragma mark - Touch handling- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {    UITouch* touch = [[event allTouches] anyObject];    dragAreaBounds.origin = [touch locationInView:self];}- (void)handleTouch:(UIEvent *)event {    UITouch* touch = [[event allTouches] anyObject];    CGPoint location = [touch locationInView:self];    dragAreaBounds.size.height = location.y - dragAreaBounds.origin.y;    dragAreaBounds.size.width = location.x - dragAreaBounds.origin.x;    if (self.dragArea == nil) {        UIView* area = [[UIView alloc] initWithFrame: dragAreaBounds];        area.backgroundColor = [UIColor blueColor];        area.opaque = NO;        area.alpha = 0.3f;        area.userInteractionEnabled = NO;        self.dragArea = area;        [self addSubview: self.dragArea];        [dragArea release];    } else {        self.dragArea.frame = dragAreaBounds;    }}- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {    [self handleTouch: event];}- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {    [self handleTouch: event];    if (self.delegate != nil) {        [delegate areaSelected: dragAreaBounds];    }    [self initialize];}- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {    [self initialize];    [self.dragArea removeFromSuperview];    self.dragArea = nil;}#pragma mark -- (void) dealloc {    [dragArea release];    [super dealloc];}@end

Then I have a class that implements the protocol defined above (MapViewController.h):

#import "OverlaySelectionView.h"typedef struct {    CLLocationDegrees minLatitude;    CLLocationDegrees maxLatitude;    CLLocationDegrees minLongitude;    CLLocationDegrees maxLongitude;} LocationBounds;@interface MapViewController : UIViewController<MKMapViewDelegate, OverlaySelectionViewDelegate> {    LocationBounds searchBounds;    UIBarButtonItem* areaButton;

And in my MapViewController.m, the areaSelected method is where I perform the conversion of touch coordinates to geographic coordinates with convertPoint:toCoordinateFromView: :

#pragma mark - OverlaySelectionViewDelegate- (void) areaSelected: (CGRect)screenArea{           self.areaButton.style = UIBarButtonItemStyleBordered;    self.areaButton.title = @"Area";    CGPoint point = screenArea.origin;    // we must account for upper nav bar height!    point.y -= 44;    CLLocationCoordinate2D upperLeft = [mapView convertPoint: point toCoordinateFromView: mapView];    point.x += screenArea.size.width;    CLLocationCoordinate2D upperRight = [mapView convertPoint: point toCoordinateFromView: mapView];    point.x -= screenArea.size.width;    point.y += screenArea.size.height;    CLLocationCoordinate2D lowerLeft = [mapView convertPoint: point toCoordinateFromView: mapView];    point.x += screenArea.size.width;    CLLocationCoordinate2D lowerRight = [mapView convertPoint: point toCoordinateFromView: mapView];    searchBounds.minLatitude = MIN(lowerLeft.latitude, lowerRight.latitude);    searchBounds.minLongitude = MIN(upperLeft.longitude, lowerLeft.longitude);    searchBounds.maxLatitude = MAX(upperLeft.latitude, upperRight.latitude);    searchBounds.maxLongitude = MAX(upperRight.longitude, lowerRight.longitude);    // TODO: comment out to keep search rectangle on screen    [[self.view.subviews lastObject] removeFromSuperview];    [self performSelectorInBackground: @selector(lookupHistoryByArea) withObject: nil];}// this action is triggered when user selects the Area button to start selecting area// TODO: connect this to areaButton yourself (I did it in Interface Builder)- (IBAction) selectArea: (id) sender{    PoliteAlertView* message = [[PoliteAlertView alloc] initWithTitle: @"Information"                                                              message: @"Select an area to search by dragging your finger across the map"                                                             delegate: self                                                              keyName: @"swipe_msg_read"                                                    cancelButtonTitle: @"Ok"                                                    otherButtonTitles: nil];    [message show];    [message release];    OverlaySelectionView* overlay = [[OverlaySelectionView alloc] initWithFrame: self.view.frame];    overlay.delegate = self;    [self.view addSubview: overlay];    [overlay release];    self.areaButton.style = UIBarButtonItemStyleDone;    self.areaButton.title = @"Swipe";}

You'll notice that my MapViewController has a property, areaButton. That's a button on my toolbar, which normally says Area. After the user presses it, they are in area selection mode at which point, the button label changes to say Swipe to remind them to swipe (maybe not the best UI, but that's what I have).

Also notice that when the user presses Area to enter area selection mode, I show them an alert that tells them that they need to swipe. Since this is probably only a reminder they need to see once, I have used my own PoliteAlertView, which is a custom UIAlertView that users can suppress (don't show the alert again).

My lookupHistoryByArea is just a method that searches my database for locations, by the saved searchBounds (in the background), and then plots new overlays on the map at the found locations. This will obviously be different for your app.

Limitations

  • Since this is for letting the user select approximate areas, I did not consider geographic precision to be critical. It doesn't sound like it should be in your app, either. Thus, I just draw rectangles with 90 degree angles, not accounting for earth curvature, etc. For areas of just a few miles, this should be fine.

  • I had to make some assumptions about your phrase touch based drawing. I decided that both the easiest way to implement the app, and the easiest for a touchscreen user to use, was to simply define the area with one single swipe. Drawing a rectangle with touches would require 4 swipes instead of one, introduce the complexity of non-closed rectangles, yield sloppy shapes, and probably not get the user what they even wanted. So, I tried to keep the UI simple. If you really want the user drawing on the map, see this related answer which does that.

  • This app was written before ARC, and not changed for ARC.

  • In my app, I actually do use mutex locking for some variables accessed on the main (UI) thread, and in the background (search) thread. I took that code out for this example. Depending on how your database search works, and how you choose to run the search (GCD, etc.), you should make sure to audit your own thread-safety.


ViewController.h

#import <UIKit/UIKit.h>@interface ViewController : UIViewController@end

ViewController.m

#import "ViewController.h"#import <MapKit/MapKit.h>@interface ViewController () <MKMapViewDelegate>@property (weak, nonatomic) IBOutlet MKMapView *mapView;@property (nonatomic, weak) MKPolyline *polyLine;@property (nonatomic, strong) NSMutableArray *coordinates;@property (weak, nonatomic) IBOutlet UIButton *drawPolygonButton;@property (nonatomic) BOOL isDrawingPolygon;@end@implementation ViewController@synthesize coordinates = _coordinates;- (NSMutableArray*)coordinates{    if(_coordinates == nil) _coordinates = [[NSMutableArray alloc] init];    return _coordinates;}- (void)viewDidLoad{    [super viewDidLoad];}- (void)didReceiveMemoryWarning{    [super didReceiveMemoryWarning];}- (IBAction)didTouchUpInsideDrawButton:(UIButton*)sender{    if(self.isDrawingPolygon == NO) {        self.isDrawingPolygon = YES;        [self.drawPolygonButton setTitle:@"done" forState:UIControlStateNormal];        [self.coordinates removeAllObjects];        self.mapView.userInteractionEnabled = NO;    } else {        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]];        }        if (self.polyLine)            [self.mapView removeOverlay:self.polyLine];        self.isDrawingPolygon = NO;        [self.drawPolygonButton setTitle:@"draw" forState:UIControlStateNormal];        self.mapView.userInteractionEnabled = YES;    }}- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{    if (self.isDrawingPolygon == NO)        return;    UITouch *touch = [touches anyObject];    CGPoint location = [touch locationInView:self.mapView];    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];    [self addCoordinate:coordinate];}- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{    if (self.isDrawingPolygon == NO)        return;    UITouch *touch = [touches anyObject];    CGPoint location = [touch locationInView:self.mapView];    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];    [self addCoordinate:coordinate];}- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{    if (self.isDrawingPolygon == NO)        return;    UITouch *touch = [touches anyObject];    CGPoint location = [touch locationInView:self.mapView];    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];    [self addCoordinate:coordinate];    [self didTouchUpInsideDrawButton:nil];}- (void)addCoordinate:(CLLocationCoordinate2D)coordinate{    [self.coordinates addObject:[NSValue valueWithMKCoordinate:coordinate]];    NSInteger numberOfPoints = [self.coordinates count];    if (numberOfPoints > 2) {        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;        if (oldPolyLine) {            [self.mapView removeOverlay:oldPolyLine];        }    }}#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;}- (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.alpha = 0.5;    }    annotationView.canShowCallout = NO;    return annotationView;}@end

or You can find here the entire project :https://github.com/tazihosniomar/MapKitDrawing

i hope it will help you.


this is my way how I convert the touches to CLLocation on the MKMapView.

it works with the the Google Maps and the Apple Maps as well:

- (void)viewDidLoad {    // ...    // ... where the _customMapView is a MKMapView object;    // find the gesture recogniser of the map    UIGestureRecognizer *_factoryDoubleTapGesture = nil;    NSArray *_gestureRecognizersArray = [_customMapView gestureRecognizers];    for (UIGestureRecognizer *_tempRecogniser in _gestureRecognizersArray) {        if ([_tempRecogniser isKindOfClass:[UITapGestureRecognizer class]]) {            if ([(UITapGestureRecognizer *)_tempRecogniser numberOfTapsRequired] == 2) {                _factoryDoubleTapGesture = _tempRecogniser;                break;            }        }    }    // my tap gesture recogniser    UITapGestureRecognizer *_locationTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(mapLocationTouchedUpInside:)];    if (_factoryDoubleTapGesture) [_locationTapGesture requireGestureRecognizerToFail:_factoryDoubleTapGesture];    [_customMapView addGestureRecognizer:_locationTapGesture];    // ...}

and...

- (void)mapLocationTouchedUpInside:(UITapGestureRecognizer *)sender {    CGPoint _tapPoint = [sender locationInView:_customMapView];    CLLocationCoordinate2D _coordinates = [_customMapView convertPoint:_tapPoint toCoordinateFromView:_customMapView];    // ... do whatever you'd like with the coordinates}