I am trying to get it so that when you rotate an iOS 7 map that the annotations rotate along with the camera heading. Imagine I had pin annotations that must point to North at all times.
This would seem simple at first, there should be a MKMapViewDelegate for getting the camera rotation but there isn't.
I've tried using the map delegates to then query the map view's camera.heading object but firstly these delegates only seem to be called once before and once after a rotation gesture:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
I also tried using KVO on the camera.heading object but this doesn't work and the camera object seems to be some kind of proxy object that only updates once the rotation gesture is complete.
My most successful method so far is to add a rotation gesture recogniser to calculate a rotation delta and use this with the camera heading reported at the beginning of the region change delegate. This works to a point but in OS 7 you can 'flick' your rotation gesture and it adds velocity which I can't seem to track. Is there any way to track the camera heading in real-time?
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
heading = self.mapView.camera.heading;
}
- (void)rotationHandler:(UIRotationGestureRecognizer *)gesture
{
if(gesture.state == UIGestureRecognizerStateChanged) {
CGFloat headingDelta = (gesture.rotation * (180.0/M_PI) );
headingDelta = fmod(headingDelta, 360.0);
CGFloat newHeading = heading - headingDelta;
[self updateCompassesWithHeading:actualHeading];
}
}
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
[self updateCompassesWithHeading:self.mapView.camera.heading];
}
Apple does not give real time updates to any map information unfortunately. Your best bet is to set up a CADisplayLink and update whatever you need to when it changes. Something like this.
#property (nonatomic) CLLocationDirection *previousHeading;
#property (nonatomic, strong) CADisplayLink *displayLink;
- (void)setUpDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkFired:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)displayLinkFired:(id)sender
{
double difference = ABS(self.previousHeading - self.mapView.camera.heading);
if (difference < .001)
return;
self.previousHeading = self.mapView.camera.heading;
[self updateCompassesWithHeading:self.previousHeading];
}
Related
How do we follow the user in maps. I want to have the blue dot (user location) be in the center of the map, But I also what to allow the user to zoom in and zoom out and then after a couple seconds zoom in back in the user location.
My Educated Guess for the Solution: We detect if the user is zooming in or out, after three seconds of no zooming in or out detection, we starting follow the user :). Your HELP would be awesome :)
This code zoom in the user location but doesn't delay for zoom in and out:
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation {
MKCoordinateRegion userLocation = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 1500.0, 1500.0); [mapView setRegion:userLocation animated:YES];
}
A quick look in the docs reveals the magic.
Set the userTrackingMode of your map to MKUserTrackingModeFollow.
See here.
Update:
Since you've updated your question, here's the new answer.
To recenter the map to the user location i would recommend to write a simple helper Method:
- (void)recenterUserLocation:(BOOL)animated{
MKCoordinateSpan zoomedSpan = MKCoordinateSpanMake(1000, 1000);
MKCoordinateRegion userRegion = MKCoordinateRegionMake(self.mapView.userLocation.coordinate, zoomedSpan);
[self.mapView setRegion:userRegion animated:animated];
}
And now you should call it after a short delay if user has stopped moving the map. You can do this in the regionDidChange delegate method of the mapView.
But you can get problems if you don't lock the reset-method if the user changes the region multiple times before it really resets the map. So it would be wise to make a flag if it is possible to recenter the map. Like a property BOOL canRecenter.
Init it with YES and update the recenterUserLocation method to:
- (void)recenterUserLocation:(BOOL)animated{
MKCoordinateSpan zoomedSpan = MKCoordinateSpanMake(1000, 1000);
MKCoordinateRegion userRegion = MKCoordinateRegionMake(self.mapView.userLocation.coordinate, zoomedSpan);
[self.mapView setRegion:userRegion animated:animated];
self.canRecenter = YES;
}
Now you can call it safely after the user has moved the map in any way with a small delay:
- (void)mapView:(MKMapView *)mMapView regionDidChangeAnimated:(BOOL)animated{
if (self.canRecenter){
self.canRecenter = NO;
[self performSelector:#selector(recenterUserLocation:) withObject:#(animated) afterDelay:3];
}
}
I had the same problem. I guessed:
If the user drag the map, he wants to stay on that position.
If the user do nothing or reset to show current location, I need to follow the user.
I added a reset button to show the current user location like this:
On the reset button clicked, changed the needToCenterMap to TRUE
Added a drag gesture recognizer on map
// Map drag handler
UIPanGestureRecognizer* panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didDragMap:)];
- (void)didDragMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
NSLog(#"Map drag ended");
self.needToCenterMap = FALSE;
}
}
Followed the user on map depending on needToCenterMap flag
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
if (self.needToCenterMap == TRUE)
[mapView setCenterCoordinate:userLocation.location.coordinate animated:YES];
}
I made a little example to show how you can delegate this job to the Map SDK.
Of course you could listen to the Location change but MKUserTrackingModeFollow automatically does this for you, so just a single line of code
#import <MapKit/MapKit.h>
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
MKMapView *mapView = [[MKMapView alloc] initWithFrame:self.view.frame];
//Always center the dot and zoom in to an apropriate zoom level when position changes
[mapView setUserTrackingMode:MKUserTrackingModeFollow];
//don't let the user drag around the the map -> just zooming enabled
[mapView setScrollEnabled:NO];
[self.view addSubview:mapView];
}
Then the app looks like this:
For more information just read the Apple Documentation:
http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MKMapView_Class/MKMapView/MKMapView.html
This shell do the trick: mkMapview.showsUserLocation = YES;
I have created one Google map view using MK Mapkit and i have annotated pins on different locations now i want to calculate number of pins on visible rect on map view also on when i zoom the map view?
Thanx in advance.
The MKMapView annotationsInMapRect: method will give you the set of annotations in a given map rect.
To get the ones currently visible, pass it the map view's visibleMapRect property.
To detect what annotations are visible after a zoom in, zoom out, or pan, call that method in the regionDidChangeAnimated delegate method:
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
NSSet *annSet = [mapView annotationsInMapRect:mapView.visibleMapRect];
NSLog(#"regionDidChangeAnimated: annSet count = %d", annSet.count);
}
No. of pin can be counted by the following delegate method
-(MKAnnotationView *)mapView:(MKMapView *)mapView1 viewForAnnotation:(id )annotation
{
Count ++
NSLog(#"Count : %d", count);
}
Assuming you have a ViewController with mapView as a subview.
- (void)someMethod
{
NSArray *visibleAnnotations = [[self.mapView annotationsInMapRect:self.mapView.visibleMapRect] allObjects];
NSUInteger VisibleAnnotationCounts = visibleAnnotations.count;
...
}
You can also add to a proper MKMapView Delegate Method
{
...
NSArray *visibleAnnotations = [[mapView annotationsInMapRect:mapView.visibleMapRect] allObjects];
NSUInteger VisibleAnnotationCounts = visibleAnnotations.count;
...
}
I have created a custom MKAnnotationView for User Location:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MKUserLocation class]]) {
if (navStatus == NavStatusHeadingEnabled) {
if ([annotation isKindOfClass:[MKUserLocation class]]) {
locationView = [[CustomLocationView alloc] initWithAnnotation:annotation reuseIdentifier:#"locationIdentifier"];
return locationView;
}
}
return nil;
}
CustomLocationView.h
- (id)initWithAnnotation:(id <MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self != nil)
{
self.backgroundColor = [UIColor clearColor];
blueDot = [UIImage imageNamed:#"userLocationDot.png"].CGImage;
CGImageRetain(blueDot);
CGPoint blueDotCenter = CGPointMake((self.frame.size.width - (CGImageGetWidth(blueDot) / 2)) / 2, (self.frame.size.height - (CGImageGetHeight(blueDot) / 2)) / 2);
blueDotLayer = [CALayer layer];
blueDotLayer.frame = CGRectMake(blueDotCenter.x, blueDotCenter.y , CGImageGetWidth(blueDot) / 2, CGImageGetHeight(blueDot) / 2);
blueDotLayer.contents = (id)blueDot;
blueDotLayer.shadowOpacity = 0.4;
blueDotLayer.shadowColor = [UIColor blackColor].CGColor;
blueDotLayer.shadowOffset = CGSizeMake(0.4, 0.3);
blueDotLayer.shadowRadius = 1.0f;
[self.layer insertSublayer:blueDotLayer above:self.layer];
}
return self;
}
- (void)setAnnotation:(id <MKAnnotation>)annotation
{
[super setAnnotation:annotation];
[self setNeedsDisplay];
}
- (void)dealloc {
[blueDotLayer release];
[super dealloc];
}
The problem is it just stays on the same place and not moving like the blue dot.
What I am doing wrong?
Thanks
Bill.
I ran into this problem just now as well. I'm not sure if this is expected behavior or not, but for whatever reason it is up to us to move our custom MKUserLocation annotation views.
A naive solution is
- (void) locationController: (LocationController *) locationController
didUpdateToLocation: (CLLocation *) location
{
[[self mapView] setShowsUserLocation:NO];
[[self mapView] setShowsUserLocation:YES];
}
But this makes the current location annotation jump around the screen which I found undesirable.
Better yet is to keep a reference to the custom annotation view as an ivar in your view controller and then do:
- (void) locationController: (LocationController *) locationController
didUpdateToLocation: (CLLocation *) location
{
CGPoint newCenterPoint = [[self mapView] convertCoordinate:[location coordinate] toPointToView:[[self customAnnotationView] superview]];
newCenterPoint.x += [[self customAnnotationView] centerOffset].x;
newCenterPoint.y += [[self customAnnotationView] centerOffset].y;
[UIView animateWithDuration:0.3f animations:^{
[[self customAnnotationView] setCenter:newCenterPoint];
}];
}
This is good except when you change the zoom level the annotation stays where it was relative to the map view's rect and then animates to the correct location only after the zoom or pan is complete. Best to follow Apple's lead and make the current location annotation disappear and reappear during region changes:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
[[self mapView] setShowsUserLocation:NO];
}
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
[[self mapView] setShowsUserLocation:YES];
}
Bill,
You need a CLLocationManager that has been initialized with an applicable desiredAccuracy and distanceFilter and then implement the applicable delegate methods, and set your own code as the delegate on the locationManager instance.
At a minimum you should have the following method which the Location Manager will call once it has determined the current location with in the desiredAccuracy and then again whenever the distanceFilter has been met.
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation ;
Cheers,
Mack
I just found an answer for this problem. My code is in Monotouch, but it should be easy to re-do the same in ObjC.
to show customized image over default MKUserLocation we need to add a subview on top of the original one. to do this, override DidAddAnnotationViews in MKMapView delegate
void DidAddAnnotationViews (object sender, MKMapViewAnnotationEventArgs e)
{
MKAnnotationView v = mapView.ViewForAnnotation(mapView.UserLocation);
if(v != null)
{
if(v.Subviews.Count() == 0)
{
UIImageView iv = new UIImageView(new RectangleF(0,0, 22, 22));
iv.Image = UIImage.FromFile("res/pins/Yhere.png");
v.AddSubview(iv);
v.BringSubviewToFront(iv);
}
}
}
This gives custom image moving on top of blue dot. more over, user tracking and location updates features works perfectly and you still can see blue circles that indicate location accuracy.
Have fun customizing MKUserLocation!
Here is the answer from Apple Developer Technical Support
I just finished talking to the MapKit engineers and they confirmed that this is a bug in iOS.
My test app is experiencing the same problem.
I don't know if this was fixed in iOS 6, the answer is for the iOS 5.
I'm using MapKit to display the user's location relative to pins around them. I'd like to be able to mimic the functionality that Maps provides via the crosshair button in the lower left-hand corner of the screen. I'm already aware that MapKit provides a CLLocation object with the user's location via MKUserLocation, I just wanted to seek advice on how I should keep focus on that location. My initial inclination was to use an NSTimer to center the map on that coordinate every 500ms or so.
Is there a better way to do this? Is there something built in to MapKit that I'm missing that will accomplish this?
Thanks so much,
Brendan
If you're on IOS5+ this is VERY easy. Just change the "userTrackingMode" using code such as:
[_mapView setUserTrackingMode:MKUserTrackingModeFollow animated:YES];
This will smoothly follow the users current location. If you drag the map it will even set the tracking mode back to MKUserTrackingModeNone which is usually the behaviour you want.
It's really simple to have the map update the user location automatically just like the google maps. Simply set showsUserLocation to YES
self.mapView.showsUserLocation = YES
...and then implement the MKMapViewDelegate to re-center the map when the location is updated.
-(void) mapView:(MKMapView *)mapView
didUpdateUserLocation:(MKUserLocation *)userLocation
{
if( isTracking )
{
pendingRegionChange = YES;
[self.mapView setCenterCoordinate: userLocation.location.coordinate
animated: YES];
}
}
And to allow the user to zoom & pan without stealing the view back to the current location...
-(void) mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
if( isTracking && ! pendingRegionChange )
{
isTracking = NO;
[trackingButton setImage: [UIImage imageNamed: #"Location.png"]
forState: UIControlStateNormal];
}
pendingRegionChange = NO;
}
-(IBAction) trackingPressed
{
pendingRegionChange = YES;
isTracking = YES;
[mapView setCenterCoordinate: mapView.userLocation.coordinate
animated: YES];
[trackingButton setImage: [UIImage imageNamed: #"Location-Tracking.png"]
forState: UIControlStateNormal];
}
I think that I would actually use the CoreLocation CLLocationManager and use its delegate method locationManager:didUpdateToLocation:fromLocation:.
This way, you don't have the overhead of an NSTimer, and it only updates when there's a new location available.
You can pull the longitude and latitude from the CLLocation object sent to the locationManager:didUpdateToLocation:fromLocation: method and pass it to the map view.
I go with Jacob Relkin's answer. This tutorial provides a step-by-step procedure of using CoreLocation in an iPhone app. Hope this helps you.
All the Best.
I'm working on a MKMapView with the usual colored pin as the location points. I would like to be able to have the callout displayed without touching the pin.
How should I do that? Calling setSelected:YES on the annotationview did nothing. I'm thinking of simulate a touch on the pin but I'm not sure how to go about it.
But there is a catch to get benvolioT's solution to work, the code
for (id<MKAnnotation> currentAnnotation in mapView.annotations) {
if ([currentAnnotation isEqual:annotationToSelect]) {
[mapView selectAnnotation:currentAnnotation animated:FALSE];
}
}
should be called from - (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView, and nowhere else.
The sequence in which the various methods like viewWillAppear, viewDidAppear of UIViewController and the - (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView is called is different between the first time the map is loaded with one particular location and the subsequent times the map is displayed with the same location. This is a bit tricky.
Ok, here's the solution to this problem.
To display the callout use MKMapView's selectAnnotation:animated method.
Assuming that you want the last annotation view to be selected, you can put the code below:
[mapView selectAnnotation:[[mapView annotations] lastObject] animated:YES];
in the delegate below:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
{
//Here
[mapView selectAnnotation:[[mapView annotations] lastObject] animated:YES];
}
Ok, to successfully add the Callout you need to call selectAnnotation:animated after all the annotation views have been added, using the delegate's didAddAnnotationViews:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views{
for (id<MKAnnotation> currentAnnotation in mapView.annotations) {
if ([currentAnnotation isEqual: annotationToSelect]) {
[mapView selectAnnotation:currentAnnotation animated:YES];
}
}
}
After trying a variety of answers to this thread, I finally came up with this. It works very reliably, I have yet to see it fail:
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views;
{
for(id<MKAnnotation> currentAnnotation in aMapView.annotations)
{
if([currentAnnotation isEqual:annotationToSelect])
{
NSLog(#"Yay!");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_current_queue(), ^
{
[aMapView selectAnnotation:currentAnnotation animated:YES];
});
}
}
}
The block is used to delay slightly, as without it the callout may not be shown correctly.
This does not work for me. I suspect a bug in the MapKit API.
See this link for details of someone else for who this is not working:
http://www.iphonedevsdk.com/forum/iphone-sdk-development/19740-trigger-mkannotationview-callout-bubble.html#post110447
--edit--
Okay after screwing with this for a while, here is what I've been able to make work:
for (id<MKAnnotation> currentAnnotation in mapView.annotations) {
if ([currentAnnotation isEqual:annotationToSelect]) {
[mapView selectAnnotation:currentAnnotation animated:FALSE];
}
}
Note, this requires implementing - (BOOL)isEqual:(id)anObject for your class that implements the MKAnnotation protocol.
If you just want to open the callout for the last annotation you added, try this, works for me.
[mapView selectAnnotation:[[mapView annotations] lastObject] animated:YES];
The problem with calling selectAnnotation from - (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView is that, as the name implies, this event is only triggered once your MapView loads initially, so you won't be able to trigger the annotation's callout if you add it after the MapView has finished loading.
The problem with calling it from - (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views is that your annotation may not be on-screen when selectAnnotation is called which would cause it to have no effect. Even if you center your MapView's region to the annotation's coordinate before adding the annotation, the slight delay it takes to set the MapView's region is enough for selectAnnotation to be called before the annotation is visible on-screen, especially if you animate setRegion.
Some people have solved this issue by calling selectAnnotation after a delay as such:
-(void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
[self performSelector:#selector(selectLastAnnotation)
withObject:nil afterDelay:1];
}
-(void)selectLastAnnotation {
[myMapView selectAnnotation:
[[myMapView annotations] lastObject] animated:YES];
}
But even then you may get weird results since it may take more than one second for the annotation to appear on-screen depending on various factors like the distance between your previous MapView's region and the new one or your Internet connection speed.
I decided to make the call from - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated instead since it ensures the annotation is actually on-screen (assuming you set your MapView's region to your annotation's coordinate) because this event is triggered after setRegion (and its animation) has finished. However, regionDidChangeAnimated is triggered whenever your MapView's region changes, including when the user just pans around the map so you have to make sure you have a condition to properly identify when is the right time to trigger the annotation's callout.
Here's how I did it:
MKPointAnnotation *myAnnotationWithCallout;
- (void)someMethod {
MKPointAnnotation *myAnnotation = [[MKPointAnnotation alloc] init];
[myAnnotation setCoordinate: someCoordinate];
[myAnnotation setTitle: someTitle];
MKCoordinateRegion someRegion =
MKCoordinateRegionMakeWithDistance (someCoordinate, zoomLevel, zoomLevel);
myAnnotationWithCallout = myAnnotation;
[myMapView setRegion: someRegion animated: YES];
[myMapView addAnnotation: myAnnotation];
}
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
if (myAnnotationWithCallout)
{
[mapView selectAnnotation: myAnnotationWithCallout animated:YES];
myAnnotationWithCallout = nil;
}
}
That way your annotation is guaranteed to be on-screen at the moment selectAnnotation is called, and the if (myAnnotationWithCallout) part ensures no region setting other than the one in - (void)someMethod will trigger the callout.
I read the API carefully and finally I found the problem:
If the specified annotation is not onscreen, and therefore does not have an associated annotation view, this method has no effect.
So you can wait some time (for example, 3 seconds) and then perform this action. Then it works.
Due to something like the code shown by benvolioT, that I suspect exists in the system, when I used selectAnnotation:animation: method, it did not show the callOut, I guessed that the reason was because it was already selected and it was avoiding from asking the MapView to redraw the callOut on the map using the annotation title and subtitle.
So, the solution was simply to deselect it first and to re-select it.
E.g: First, I needed to do this in Apple's touchMoved method (i.e. how to drag an AnnotationView) to hide the callOut. (Simply using annotation.canShowAnnotation = NO alone does not work, since I suspect that it needs redrawing. The deselectAnnotaiton causes the necessary action. Also, deselecting alone did not do that trick, the callOut disappeared only once and got redrawn straight away. This was the hint that it got reselected automatically).
annotationView.canShowAnnotation = NO;
[mapView deselectAnnotation:annotation animated:YES];
Then, simply using the code below in touchEnded method did not bring back the callOut (The annotation has been automatically selected by the system by that time, and presumably the redrawing of the callOut never occrrs):
annotationView.canShowAnnotation = YES;
[mapView selectAnnotation:annotation animated:YES];
The solution was:
annotationView.canShowAnnotation = YES;
[mapView deselectAnnotation:annotation animated:YES];
[mapView selectAnnotation:annotation animated:YES];
This simply bought back the callOut, presumably it re-initiated the process of redrawing the callOut by the mapView.
Strictly speaking, I should detect whether the annotation is the current annotation or not (selected, which I know it is) and whether the callOut is actually showing or not (which I don't know) and decide to redraw it accordingly, that would be better. I, however, have not found the callOut detection method yet and trying to do so myself is just a little bit unnecessary at this stage.
Steve Shi's response made it clear to me that selectAnnotation has to be called from mapViewDidFinishLoadingMap method. Unfortunately i cannot vote up but i want to say thanks here.
Just add [mapView selectAnnotation:point animated:YES];
Resetting the annotations also will bring the callout to front.
[mapView removeAnnotation: currentMarker];
[mapView addAnnotation:currentMarker];