MKUserTrackingModeFollowWithHeading, zoom - ios

I use MKUserTrackingModeFollowWithHeading so that the map follows the user location and rotates. Problem is zoom level is not sticking. here is my code
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.locMapView.delegate = self;
[self.locMapView setUserTrackingMode:MKUserTrackingModeFollowWithHeading animated:YES];
}
-(void) viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
MKMapPoint pt = MKMapPointForCoordinate(_locMapView.userLocation.coordinate);
double w = MKMapPointsPerMeterAtLatitude(_locMapView.userLocation.coordinate.latitude) * 500;
MKMapRect zoomRect = MKMapRectMake(pt.x - w/2.0, pt.y - w/2.0, w, w);
[_locMapView setVisibleMapRect:zoomRect animated:YES];
}
I tried to set zoom level in viewDidLoad also, not much luck.
How can i make map to stick to zoom level when UserTrackingMode is set to MKUserTrackingModeFollowWithHeading ?

Here's the trick I did to resolve this problem: Set mapView.userTrackingMode = MKUserTrackingModeNone before zoom in/out then set = MKUserTrackingModeFollowWithHeading.
#property (assign, nonatomic) MKUserTrackingMode lastTrackingMode;
- (IBAction)zoomInButtonPressed:(id)sender {
self.mapView.userTrackingMode = MKUserTrackingModeNone;
MKCoordinateRegion newRegion = MKCoordinateRegionMake(self.mapView.region.center,MKCoordinateSpanMake(self.mapView.region.span.latitudeDelta*0.5, self.mapView.region.span.longitudeDelta*0.5));
[self.mapView setRegion:newRegion];
self.mapView.userTrackingMode = self.lastTrackingMode;
}
- (IBAction)zoomOutButtonPressed:(id)sender {
self.mapView.userTrackingMode = MKUserTrackingModeNone;
// Zoom-out code goes here
self.mapView.userTrackingMode = self.lastTrackingMode;
}

Related

How to make a MKMapView come in from the top

I have a normal MKMap view with a basic MKAnnoation what i want to accomplish is that my annotation is in the UK and the MKCoordinateSpan is 0.05 but i want the MKMapView start from a view of the whole UK and zoom in to the annotation.
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
/* set here the region from where map will be zooming; */
[self.mapView setRegion:MKCoordinateRegionForMapRect(MKMapRectWorld) animated:NO];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
/* set here your end point of zooming */
CLLocationCoordinate2D zoomLocation;
zoomLocation.latitude = 39.281516;
zoomLocation.longitude= -76.580806;
[self.mapView setCenterCoordinate:zoomLocation animated:YES];
CLLocationDistance locationDistance = [self locationDistance:0.5];
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(zoomLocation, locationDistance, locationDistance);
[self.mapView setRegion:viewRegion animated:YES];
}
- (CLLocationDistance )locationDistance:(CGFloat )value {
return 1609.344 *value;
}

Pan gesture interferes with scroll

I have three view controllers that are part of a UIScrollView. I want to be able to swipe between the three, although one of the view controllers has a UIPanGestureRecognizer. I use this pan gesture recognizer to allow the user to drag their finger up and down to increase and decrease the height of a rectangular UIView. Therefore, this UIPanGestureRecognizer only really needs to know about the upwards/downwards panning, and the scroll view can use the horizontal panning.
An example of this, is like the home screen; you can swipe left or right, but also swipe down to get spotlight. I want this kind of mechanism.
This is my code for the pan:
- (void)pan:(UIPanGestureRecognizer *)aPan; // When pan guesture is recognised
{
CGPoint location = [aPan locationInView:self.view]; // Location of finger on screen
CGRect secondRect = CGRectMake(210.0, 45.0, 70.0, 325.0); // Rectangles of maximimum bar area
CGRect minuteRect = CGRectMake(125.0, 45.0, 70.0, 325.0);
CGRect hourRect = CGRectMake(41.0, 45.0, 70.0, 325.0);
if (CGRectContainsPoint(secondRect, location)) { // If finger is inside the 'second' rectangle
CGPoint currentPoint = [aPan locationInView:self.view];
currentPoint.y -= 80; // Make sure animation doesn't go outside the bars' rectangle
if (currentPoint.y < 0) {
currentPoint.y = 0;
}
else if (currentPoint.y > 239) {
currentPoint.y = 239;
}
currentPoint.y = 239.0 - currentPoint.y;
CGFloat pointy = currentPoint.y - fmod(currentPoint.y, 4.0);
[UIView animateWithDuration:0.01f // Animate the bars to rise as the finger moves up and down
animations:^{
CGRect oldFrame = secondBar.frame;
secondBar.frame = CGRectMake(oldFrame.origin.x, (oldFrame.origin.y - (pointy - secondBar.frame.size.height)), oldFrame.size.width, (pointy));
}];
CGFloat result = secondBar.frame.size.height - fmod(secondBar.frame.size.height, 4.0);
secondInt = (result / 4.0); // Update labels with new time
self->secondLabel.text = [NSString stringWithFormat:#"%02d", secondInt];
}
The code is basically repeated for three separate rectangular UIViews.
If anyone can tell me how to get the homescreen-style panning/swiping into my app, that would be great!!
Alright, here is the short answer:
You have to use UIGestureRecognizer's method -requireGestureRecognizerToFail:.
And here is the long answer:
You have to make the pan gesture recognizer of your scroll view to succeed only if the pan gesture recognizer of TimerViewController fails. However that gesture (TimerViewController's gesture) should only succeed if the initial movement is vertical. If it is horizontal it should fail.
To accomplish this we have to subclass UIPanGestureRecognizer and modify it to fit those needs.
Here is what you have to do:
Disregard ALL the changes you made from my previous answer
Add VerticalPanGestureRecognizer to your project.
Modify TimerViewController as shown.
Modify ScrollViewController as shown.
VerticalPanGestureRecognizer.h
#import <UIKit/UIKit.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface VerticalPanGestureRecognizer : UIPanGestureRecognizer
#end
VerticalPanGestureRecognizer.m
#import "VerticalPanGestureRecognizer.h"
#interface VerticalPanGestureRecognizer ()
#property (nonatomic, assign) CGPoint origLoc;
#end
#implementation VerticalPanGestureRecognizer
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.origLoc = [[touches anyObject] locationInView:self.view.superview];
[super touchesBegan:touches withEvent:event];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.state == UIGestureRecognizerStatePossible) {
CGPoint loc = [[touches anyObject] locationInView:self.view.superview];
CGFloat deltaX = fabs(loc.x - self.origLoc.x);
CGFloat deltaY = fabs(loc.y - self.origLoc.y);
if (deltaY < deltaX)
self.state = UIGestureRecognizerStateFailed;
}
[super touchesMoved:touches withEvent:event];
}
#end
TimerViewController.h
// Your imports here
#interface TimerViewController : UIViewController
{
// Your ivars here
}
// Add the following property
#property (nonatomic, strong) UIPanGestureRecognizer *pan;
// Your methods here
#end
TimerViewController.m
#import "TimerViewController.h"
#import "VerticalPanGestureRecognizer.h"
#implementation TimerViewController
#synthesize pan = _pan;
// prefersStatusBarHidden method here
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil // Initialise view controller
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Instantiate the pan gesture as "VerticalPanGestureRecognizer"
self.pan = [[VerticalPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)]; // Create recogniser for a pan guesture
self.pan.maximumNumberOfTouches = self.pan.minimumNumberOfTouches = 1;
[self.view addGestureRecognizer:self.pan];
}
return self;
}
// The rest of your code here
#end
ScrollViewController.m
- (void)viewDidLoad
{
// Your code here
TimerViewController *tvc = [[TimerViewController alloc]init];
CGRect frame = tvc.view.frame;
frame.origin.x = 320;
tvc.view.frame = frame;
// Add the following line
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:tvc.pan];
[self addChildViewController:tvc];
[self.scrollView addSubview:tvc.view];
[tvc didMoveToParentViewController:self];
// More code here
}
This new approach works perfectly. I tested it.
Let me know if you have more questions.
Cheers!
UPDATE
To answer the question you posted on the comments, here is what you have to do:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
BarsViewController *bvc = [[BarsViewController alloc]init];
[self addChildViewController:bvc];
[self.scrollView addSubview:bvc.view];
[bvc didMoveToParentViewController:self];
TimerViewController *tvc = [[TimerViewController alloc]init];
CGRect frame = tvc.view.frame;
frame.origin.x = 320;
tvc.view.frame = frame;
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:tvc.pan];
[self addChildViewController:tvc];
[self.scrollView addSubview:tvc.view];
[tvc didMoveToParentViewController:self];
StopwatchViewController *svc = [[StopwatchViewController alloc] init];
frame = svc.view.frame;
frame.origin.x = 320*2;
svc.view.frame = frame;
[self addChildViewController:svc];
[self.scrollView addSubview:svc.view];
[svc didMoveToParentViewController:self];
self.scrollView.contentSize = CGSizeMake(320*3, self.view.frame.size.height);
self.scrollView.pagingEnabled = YES;
[self.scrollView setShowsHorizontalScrollIndicator:NO];
}
Again, I tested it and it's working. You just have to add the gesture recognizer for the bars

Same MKOverlayView delegate called in one UIView but not another. What's missing?

My app tracks GPS movement as a MKPolyline routepath on a MKMapView as an MKOverlayRenderer in the HomeVC, saves the data, and displays it later, as a saved routepath a few VCs deeper, on DisplayVC. I can confirm that the data is identical to the original data on the second VC, and the proper routeBounds are used when the map is shown, but the OverlayRenderer is never called on the second VC. Why not? I'm thinking delegate problems, but I can't find anything wrong.
Both homeVC.h
#interface homeVC : UIViewController <CLLocationManagerDelegate, MKMapViewDelegate> {
and displayVC.h are the same, except for the name:
#interface displayVC : UIViewController <CLLocationManagerDelegate, MKMapViewDelegate> {
CLLocationManager *locationManager;
// the data representing the route points
MKPolyline* _routePath;
// the view we create for the line on the map
MKPolylineView* _routePathVw;
// the rect that bounds the loaded points
MKMapRect _routeBounds;
}
#property (nonatomic, weak) IBOutlet MKMapView *mapView;
#end
And both homeVC.m and displayVC.m are set up the same:
- (void)viewDidLoad {
[super viewDidLoad];
// Add the Map
[_mapView setDelegate:self];
_mapView.mapType = MKMapTypeStandard;
}
Lots of good-working code here. Then,
-(void) buildRoute {
CLLocationCoordinate2D thisCoord;
int i = [arrayLa count] - 1; // keep growing the array size
MKMapPoint *tmpArr = realloc(pointArr, sizeof(CLLocationCoordinate2D)*(arrayLa.count));
pointArr = tmpArr;
thisCoord.latitude = [[arrayLa objectAtIndex:i] floatValue];
thisCoord.longitude = [[arrayLo objectAtIndex:i] floatValue];
MKMapPoint point = MKMapPointForCoordinate(thisCoord);
pointArr[i] = point;
// Reset Map View Boundaries
if( point.x > ne_Pt.x - 500 ) ne_Pt.x = point.x + 1000;
if( point.y > ne_Pt.y - 500 ) ne_Pt.y = point.y + 1000;
if( point.x < sw_Pt.x + 500 ) sw_Pt.x = point.x - 1000;
if( point.y < sw_Pt.y + 500 ) sw_Pt.y = point.y - 1000;
// create the polyline based on the C-array of map Points
_routePath = [MKPolyline polylineWithPoints:pointArr count:arrayLa.count];
_routeBounds = MKMapRectMake(sw_Pt.x, sw_Pt.y, ne_Pt.x-sw_Pt.x, ne_Pt.y-sw_Pt.y);
// add the routePath overlay to the map, if it isn't empty
if (recState == REC && _routePath != nil) {
// zoom in on the route with the fresh bounding box, routeBounds
[self zoomInOnRoute];
[_mapView addOverlay:_routePath];
}
}
-(void) zoomInOnRoute {
[_mapView setVisibleMapRect:_routeBounds];
}
#pragma mark MKMapViewDelegate
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
if ([overlay isKindOfClass:[MKPolyline class]]) {
MKPolyline *route = overlay;
MKPolylineRenderer *routeRenderer = [[MKPolylineRenderer alloc] initWithPolyline:route];
routeRenderer.lineWidth = 3;
routeRenderer.strokeColor = [UIColor redColor];
return routeRenderer;
}
else return nil;
}
Can anyone help solve my problem?
Thanks!
It does look like a delegate issue. Have you tried putting a breakpoint on the addOverLay call just in case the 'if' is skipping it?
I do something similar and all works fine using MKOverlay and MKOverlayRender (based on the apple Breadcrumbs sample app but updated). The app displays a route that the user can save to CoreData. They can select from a table of saved routes and the route is rendered using MKOverlayRenderer.
Set the delegate
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[self.mapView setDelegate:self];
}
Create a MKOverlay and add to mapView
- (void)createRoute2
{
// Some CoreData stuff. Iterate through map points and create an overlay.
// Order by date ascending so we draw in sequential order
NSSortDescriptor *timeStampDescriptor = [[NSSortDescriptor alloc] initWithKey:#"pointDate" ascending:YES];
NSArray *sortDescriptors = #[timeStampDescriptor];
NSArray *routePts = [[self.selectedRoute mapDetail] sortedArrayUsingDescriptors:sortDescriptors];
// A long is a bit excessive just use an int is fine ( BUT might be a huge number of route points)
long nbrPts = routePts.count;
if (nbrPts < 2){
return;
}
CLLocationCoordinate2D mapPointLoc;
MKMapRect updateRect; // The map area
// Init the route
// FtfmapDetail is a managed object holding lat long (and other stuff)
FtfMapDetail *currentPt = (FtfMapDetail *)[routePts objectAtIndex:0];
mapPointLoc = CLLocationCoordinate2DMake([currentPt.latitude floatValue], [currentPt.longitude floatValue]);
// self.route is a subclassed MapOverlay
if (!self.route)
{
// BGSMapOverlay is a subclassed MapOverlay
self.route = [[BGSMapOverlay alloc] initWithCenterCoordinate:mapPointLoc];
[self.mapView addOverlay:self.route];
}
// Add subsequent points. Kick off the for loop at int position 1 not 0
for (int i=1; i <nbrPts; i++){
currentPt = (FtfMapDetail *)[routePts objectAtIndex:i];
mapPointLoc = CLLocationCoordinate2DMake([currentPt.latitude floatValue], [currentPt.longitude floatValue]);
// AddCoordinate is a method in MKOverlay subclass that returns a bounding MKMaprect for all points in MkOverlay
updateRect = [self.route addCoordinate:mapPointLoc];
}
MKCoordinateRegion region = MKCoordinateRegionForMapRect(self.route.boundingMapRectCompleteRoute);
[self.mapView setRegion:region animated:YES];
}
And make sure you add the delegate method
#pragma mark - MKMapView delegate
// self.routeViewRenderer is a sub-classed MKOverlayRendered (based on the Breadcrumbs app from apple CrumbPathView subclassed MKOverlayView)
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay{
if (!self.routeViewRenderer)
{
_routeViewRenderer = [[BGSMKOverlayRender alloc] initWithOverlay:overlay];
}
return self.routeViewRenderer;
}
Or it could be that you are not sending any coordinates to the MkPolyLine. In the following snippet if I uncomment the "NSArray *routeCoordinates = [[NSArray alloc]init];" to send a Nil array to the MKPolyLine then the code will run but the delegate doesn't get called. If routeCoordinates contains points then delegate is called and route displayed.
-(void)buildRouteOverlays
{
for (int i=0; i< _routeHeaders.count;i++)
{
_selectedRoute = (FtfMaps*) [_routeHeaders objectAtIndex:i];
NSLog(#"DEBUG route date : %#", _selectedRoute.dateMap);
NSArray *routeCoordinates = [self arrayRoutePointCoordinates];
// if a nil array is produce then MapOverlayRenerder is not called - nothing to render
// Test this by uncommenting:
// NSArray *routeCoordinates = [[NSArray alloc]init];
NSLog(#"DEBUG number of point in Route : %lu",(unsigned long)routeCoordinates.count);
// Just a quick test only process the first route
if (i==0){
MKPolyline *routePolyLine = [self polyLineFromArray:routeCoordinates];
[self.mapView addOverlay:routePolyLine];
}
}
}
-(MKPolyline*)polyLineFromArray:(NSArray*)routePoints
{
NSInteger pointsCount = routePoints.count;
CLLocationCoordinate2D pointsToUse[pointsCount];
for(int i = 0; i < pointsCount; i++) {
FtfMapDetail *mapPt = (FtfMapDetail *) [routePoints objectAtIndex:i];
pointsToUse[i] = CLLocationCoordinate2DMake([mapPt.latitude doubleValue], [mapPt.longitude doubleValue]);
}
MKPolyline *myPolyline = [MKPolyline polylineWithCoordinates:pointsToUse count:pointsCount];
return myPolyline;
}
#pragma mark MKMapViewDelegate
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
if ([overlay isKindOfClass:[MKPolyline class]]) {
MKPolyline *route = overlay;
MKPolylineRenderer *routeRenderer = [[MKPolylineRenderer alloc] initWithPolyline:route];
routeRenderer.lineWidth = 3;
routeRenderer.strokeColor = [UIColor redColor];
return routeRenderer;
}
else return nil;
}

Rotating rectangle around circumference of a circle (iOS)?

I am trying to rotate the rectangle around the circle. So far after putting together some code I found in various places (mainly here: https://stackoverflow.com/a/4657476/861181) , I am able to rotate rectangle around it's center axis.
How can I make it rotate around the circle?
Here is what I have:
OverlaySelectionView.h
#import <QuartzCore/QuartzCore.h>
#interface OverlaySelectionView : UIView {
#private
UIView* dragArea;
CGRect dragAreaBounds;
UIView* vectorArea;
UITouch *currentTouch;
CGPoint touchLocationpoint;
CGPoint PrevioustouchLocationpoint;
}
#property CGRect vectorBounds;
#end
OverlaySelectionView.m
#import "OverlaySelectionView.h"
#interface OverlaySelectionView()
#property (nonatomic, retain) UIView* vectorArea;
#end
#implementation OverlaySelectionView
#synthesize vectorArea, vectorBounds;
#synthesize delegate;
- (void) initialize {
self.userInteractionEnabled = YES;
self.multipleTouchEnabled = NO;
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(rotateVector:)];
panRecognizer.maximumNumberOfTouches = 1;
[self addGestureRecognizer:panRecognizer];
}
- (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 {
if (vectorBounds.origin.x){
UIView* area = [[UIView alloc] initWithFrame: vectorBounds];
area.backgroundColor = [UIColor grayColor];
area.opaque = YES;
area.userInteractionEnabled = NO;
vectorArea = area;
[self addSubview: vectorArea];
}
}
- (void)rotateVector: (UIPanGestureRecognizer *)panRecognizer{
if (touchLocationpoint.x){
PrevioustouchLocationpoint = touchLocationpoint;
}
if ([panRecognizer numberOfTouches] >= 1){
touchLocationpoint = [panRecognizer locationOfTouch:0 inView:self];
}
CGPoint origin;
origin.x=240;
origin.y=160;
CGPoint previousDifference = [self vectorFromPoint:origin toPoint:PrevioustouchLocationpoint];
CGAffineTransform newTransform =CGAffineTransformScale(vectorArea.transform, 1, 1);
CGFloat previousRotation = atan2(previousDifference.y, previousDifference.x);
CGPoint currentDifference = [self vectorFromPoint:origin toPoint:touchLocationpoint];
CGFloat currentRotation = atan2(currentDifference.y, currentDifference.x);
CGFloat newAngle = currentRotation- previousRotation;
newTransform = CGAffineTransformRotate(newTransform, newAngle);
[self animateView:vectorArea toPosition:newTransform];
}
-(CGPoint)vectorFromPoint:(CGPoint)firstPoint toPoint:(CGPoint)secondPoint
{
CGPoint result;
CGFloat x = secondPoint.x-firstPoint.x;
CGFloat y = secondPoint.y-firstPoint.y;
result = CGPointMake(x, y);
return result;
}
-(void)animateView:(UIView *)theView toPosition:(CGAffineTransform) newTransform
{
[UIView setAnimationsEnabled:YES];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.0750];
vectorArea.transform = newTransform;
[UIView commitAnimations];
}
#end
here is attempt to clarify. I am creating the rectangle from a coordinates on a map. Here is the function that creates that rectangle in the main view. Essentially it is the middle of the screen:
overlay is the view created with the above code.
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
if (!circle){
circle = [MKCircle circleWithCenterCoordinate: userLocation.coordinate radius:100];
[mainMapView addOverlay:circle];
CGPoint centerPoint = [mapView convertCoordinate:userLocation.coordinate toPointToView:self.view];
CGPoint upPoint = CGPointMake(centerPoint.x, centerPoint.y - 100);
overlay = [[OverlaySelectionView alloc] initWithFrame: self.view.frame];
overlay.vectorBounds = CGRectMake(upPoint.x, upPoint.y, 30, 100);
[self.view addSubview: overlay];
}
}
Here is the sketch of what I am trying to achieve:
Introduction
A rotation is always done around (0,0).
What you already know:
To rotate around the center of the rectangle you translate the rect to origin, rotate and translate back.
Now for your question:
to rotate around a center point of a circle, simply move the center of the rectangle such that the circle is at (0,0) then rotate, and move back.
start positioning the rectangle at 12 o clock, with the center line at 12.
1) as explained you always rotate around 0,0, so move the center of the circle to 0,0
CGAffineTransform trans1 = CGAffineTransformTranslation(-circ.x, -circ.y);
2) rotate by angle
CGAffineTransform transRot = CGAffineTransformRotation(angle); // or -angle try out.
3) Move back
CGAffineTransform transBack = CGAffineTransformTranslation(circ.x, circ.y);
Concat these 3 rotation matrices to one combibed matrix, and apply it to the rectangle.
CGAffineTransformation tCombo = CGAffineTransformConcat(trans1, transRot);
tCombo = CGTransformationConcat(tCombo, transback);
Apply
rectangle.transform = tCombo;
You probably should also read the chapter about Transformation matrices in Quartz docu.
This code is written with a text editor only, so expect slighly different function names.

iOS + MKMapView user touch based drawing

I have searched a lot for this question, but none of them seem to do exactly what I want.
A lot of tutorials show me how to add lines and polygons in code, but not with freehand drawing.
The question is the following one:
I am building a real estate application. If the user is on the MKMapView it has the ability to draw a rectangle/circle/... around a certain area where he/she wants to buy/rent a house. Then I need to display the results that correspond within the area the user has selected.
Currently I have a UIView on top of my MKMapView where I do some custom drawing, is there a way to translate points to coordinates from that or ..? Or is this completely not the way this is done ? I have also heard about MKMapOverlayView, etc .. but am not exactly sure how to use this.
Can anybody point me in the right direction or does he have some sample code or a tutorial that can help me accomplish what I am in need for?
Thanks
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
}
Try MKOverlayPathView. The problem in denoting a region by drawing a path on an MKMapView is, unless you know the zoom scale you don't know much. So you have to track that.

Resources