I have an MKMapView that's supposed to track the user's location using a custom view (not the blue dot). In order to substitute this view for the blue dot, I return it thusly:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if (annotation == [mapView userLocation])
{
return userLocationView;
}
}
To initialize tracking, I call
[mapView setShowsUserLocation: YES];
[mapView setUserTrackingMode: MKUserTrackingModeFollow animated: NO];
[mapView setDelegate: self];
As would be expected, -mapView:didUpdateUserLocation: gets called once when the app loads. Unfortunately, it's never called again unless I change -mapView:viewForAnnotation: to have the following implementation:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if (annotation == [mapView userLocation])
{
return nil;
}
}
With these changes, the map loads the blue dot as the indicator of the user's location, and -mapView:didUpdateUserLocation: gets called frequently, as would be expected.
Is there some sort of mutual exclusivity for tracking users' locations and have a custom user location view? How can I make both happen?
Source
This project demonstrates this issue. https://dl.dropbox.com/u/2338382/MapKitFuckery.zip
Bug
This is most likely a bug, which I've filed as a radar. In the interim, the accepted answer should prove sufficient. However, it bears noting that I had to give up entirely on [mapView userLocation] and [mapView showsUserLocation], in favor of simply a custom annotation and the CLLocationManager.
Instead of relying on the map view's location updates, start a CLLocationManager, set its delegate and wait for -locationManager:didUpdateToLocation:fromLocation: (in iOS 5 and lower) or -locationManager:didUpdateLocations: (iOS 6). You will get much more reliable and plentiful information than using the map view's delegate methods. You probably know the way to do this, but here it is:
#import <CoreLocation/CoreLocation.h>
- (void)viewWillAppear:(BOOL)animated
{
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
[self.locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
[self.locationManager startUpdatingLocation];
}
// Deprecated in iOS 6
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
// Check the age of the newLocation isn't too long ago using newLocation.timestamp
// Set the map dot using newLocation.coordinate
// Set an MKCircle to represent accuracy using newLocation.horizontalAccuracy
}
I had a look at the delegate calls that come in to the mapView's delegate, and returning anything other than nil stops calls to -mapView:didUpdateUserLocation:, like you said. Here are the calls in the order they arrive:
- (void)mapViewWillStartLocatingUser:(MKMapView *)mapView
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id < MKAnnotation >)annotation
- (void)mapViewWillStartLoadingMap:(MKMapView *)mapView
- (void)mapView:(MKMapView *)mapView didFailToLocateUserWithError:(NSError *)error
Presumably the MKUserLocation object, not the MKMapView is the object responsible for calling the delegate with update calls. If you check the status of showsUserLocation and mapView.userLocation, they both look fine:
NSLog(#"%d %#", mapView.showsUserLocation, mapView.userLocation);
returns 1 and a non-nil object (1 <MKUserLocation: 0x1e02e580>). Maybe the mapView queries its userLocation object to get the current location, then sends it to the delegate. If that object has gone, it won't work.
It's a bit strange, but like I said, you'll get better updates from a CLLocationManager's updates.
An old question that I would like to answer my own way. I was having an issue somewhat similar. I just needed to make a web service call, passing the user location as a GET parameter, when my MapView's ViewController/Screen was loaded and user location retrieved by the mapView. It made sense then to call the web service within the delegate method didUpdateUserLocation. I didn't notice the wrong behaviour first because things seemed to work properly but then for some reasons sometimes upon opening the mapView screen, the user blue dot would show but the map would not "zoom in" and neither didUpdateUserLocation was called nor my inner web service call obviously. After a few seconds of staring at the screen, the map would "zoom in" and the didUpdateUserLocation/web service was called. Some small glitch I thought, not a big deal. Now my detail-oriented developer's mind was still frustrated and after a few weeks of thinking this over, I decided to take action on this. Stackoverflow didn't give me the answer straight away but pointed me towards the right direction nonetheless. And here was the culprit: Sequence of calls! Maddening but made total sense once I figured things out. I knew that in order to see the blue dot and get the user location I had to tell the mapView to do so. So being an Interface Builder lover, I set things up properly for my mapView in IB, that is I checked the box: "User Location", easy. Then carefully reading the mapView documentation I realised that my ViewController needed to conform to MKMapViewDelegate, done deal. As I said, things seemed to work ok, the blue dot would show right away but sometimes the "zoom in" of the map would take a few seconds... well my iPhone is already 3 years old, things are getting slow, I'd deal with the sluggishness... Then I read this stack overflow post (https://stackoverflow.com/a/37407955) and things became clear. In my case, since using IB, here was the sequence of calls:
User Location checkbox checked in IB
In viewDidLoad, call: mapView.delegate = self
Whereas, here is how the sequence of calls should have been:
User Location checkbox NOT checked in IB (This is important)
mapView.delegate = self
mapView.showsUserLocation = true
And this changes EVERYTHING. Instead of having the mapView zooming in sometimes right away and sometimes after a few seconds, the mapView now zooms in right away when the screen opens, and the didUpdateUserLocation/web service IS called.
Now a little more on the why, it still "sometimes" worked. This is simply due to the fact that the location of my iPhone was sometimes updated right after the map screen was loaded and sometimes not :-). I have to say that besides the stackoverflow post I mentioned above, what helped me too was testing my app in the Simulator since I needed to use a gpx file with obviously static location coordinate, the non predictable behaviour I described was then systematic.
So as much as I love Interface Builder, I might have to reconsider how unconditional that love is.
PS: I know this post is old and my answer not entirely related to the issue #Patrick Perini had but I wish it helps others and that it might answer #Patrick Perini's conclusion regarding the fact that a Bug was at fault when it's not. Thanks for reading this blog post :-)
Related
I'm calling
[locationManager requestWhenInUseAuthorization];
on a screen that shows an MKMapView (showsUserLocation = YES). Which seems weird in the first place (Apple should handle this for MKMapView automatically, but XCode was complaining when I didn't do it).
So I get the alert view that says the application wants to use your location, but then the alert view disappears on its own.
Why does the alert view disappear on its own?
Only thing I can think of is that I am calling requestWhenInUseAuthorization in the initWithCoder method. I'm only doing that because I think I saw the Xcode complaint when calling it from viewDidLoad.
You're probably being ARC'd. Make sure that you still have a reference to your CLLocationManager. You can easily do this by making it a property.
ARC stands for Automatic Reference Counting. In an ARC-enabled project (and unless you are working on something really old or you turned it off on purpose, your project is an ARC-enabled project) you need to keep references to objects that you'll use later on. CLLocationManager doesn't return a singleton so you need to keep a reference to it in your classes that care. Something like this:
#property (strong, nonatomic) CLLocationManager *locationManager
See Apple's ARC documentation for details. (And thanks Falko for hunting down the deep link to that.)
I'm putting Gobe's comment inline in case you didn't scroll to read it.
For Swift: instead of creating a local scope locationManager object,
let it as a property of your classes that care, like private let
locationManager = CLLocationManager() and then use it normally as
self.locationManager.requestWhenInUseAuthorization()
I've been having an issue with having multiple MKMapViews in my iOS application.
We are currently using a TabBarController for basic navigation. The issue comes up when a MKMapView's annotation's button segues to another view that has a button that leads to another MKMapView. The first MKMapView works fine with annotations and functionality, but the second MKMapView won't add annotations.
I believe the class is linked to the StoryBoard's layout fine since it triggers the viewDidLoad function upon the segue. When I step through the viewDidLoad function it reaches the addAnnotation function call, but the annotations do not get added to the map.
I know the post "Multiple map views in same app" covers a similar issue, but the poster didn't seem too friendly and didn't get any answers due to that.
Please let me know what you think, if you need more information, or if you've implemented multiple MKMapViews in your iOS project. Thanks!
Look closely at your setup of delegates & IBOutlets to make sure each view is pointing to the right mapview. Then make sure each function uses the parameters it was given, e.g.
- (MKAnnotationView *)mapView:(MKMapView *)aMapView viewForAnnotation:(id < MKAnnotation >)annotation
{
//Only use "aMapView here not "mapView"
}
I am trying to use the mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated method to limit the region of my mapview but whenever I make a call to my mapview's region ([self.userMapView setRegion:regionLimit] ), it gives me a EXC_BAD_ACCESS error.
This error is thrown no matter what property I try to call from my mapView. I have my mapview set to the mapview with a proper outlet, and I was even able to call the set region in the viewDidLoad message.
How can I keep these properties from throwing the bad access error? If no can answer, can anyone direct me to a resource explaining how to debug something like this? Thanks.
See here for his to debug EXC_BAD_ACCESS errors and common mistakes that cause them. In your case, I'll bet your property is long dead and recycled, or was never initialized in the first place.
I have some UI that I need to redraw based on changes to an MKMapView when the user pans or zooms the map.
Currently I am using a move event gesture recogniser and MKMapViewDelegate regionDidChangeAnimated messages to redraw my dependant UI. This is 90% of what I need.
The events I am missing are from the point the user lifts their finger (no more move events) to when the MKMapViewDelegate regionDidChangeAnimated message is fired. During that period the map slowly pans to a halt and my dependant UI is stuck with map tile content that is out of synch.
Is there a lower level API that will notify me when UIView (in this case MKMapView) content is redrawn?
Update
I tried creating a proxy MKMapView subclass that forwarded drawRect calls onto my supplied delegate. I get the first draw event but none of the subsequent ones, so this doesn't help with my predicament.
IIRC, MKMapView is unfortunately not KVO-compliant for #"region".
You might hack you way setting up an NSTimer in regionWillChangeAnimated, using it to refresh you UI during the scroll/pan/etc, and discarding it in regionDidChangeAnimated. Not quite elegant though, and it may not suit your needs if you need to be really fast.
Alternatively, you may look for the UIScrollView in MKMapView's subviews, and observe its transform.
(I haven't tested any of these.)
In any case, I doubt that monitoring redraw events on a MKMapView will be of any use : MKMapView uses CATiledLayer to perform its drawing asynchronously, so you can't even be sure when it's done.
Edit : This apparently does not work with iOS 6. Of course, this should not really come as a surprise. However, as far as I know, the delegate methods behave the same, so the OP's problem is still real. I haven't looked for an updated solution.
Hate to post THESE kind of solutions, but.
MKMapView has many subview in itself.
In it's subviews hierarchy there's an view with class MKTiledView, which have TiledLayer as layer.
So, actually, you can't resolve notifications of rendering in "normal" way.
Tiled layer renders it's contents by constantly calling -drawLayer:inContext: method of it's delegate, which MKTiledView is. Those calls can be performed simultaneosly in different threads.
You're not receiving norifications(updates) from MKMapView because it isn't updating itself. Only underlying contents of it are updating.
So. There's always better solution exists.
My solution depends on view hierarchy and method's swizzling.
It's up to you, to use it or not.
Creating category-method in which we will post "update notifications" to custom view that need to be updated
#implementation UIView (Custom)
- (void)drawCustomLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
NSLog(#"Need to draw custom layer :%# in context %#, Thread: %#", layer, ctx, [NSThread currentThread]);
// Calling old method
[self drawCustomLayer:layer inContext:ctx];
}
#end
// Exchanging method implementation of MKTiledView to our custom implementation
#import <objc/runtime.h>
Class tileViewclass = NSClassFromString(#"MKMapTileView");
Class viewClass = NSClassFromString(#"UIView");
SEL originalSelector = #selector(drawLayer:inContext:);
SEL newSelector = #selector(drawCustomLayer:inContext:);
Method origMethod = class_getInstanceMethod(tileViewclass, originalSelector);
Method newMethod = class_getInstanceMethod(viewClass, newSelector);
method_exchangeImplementations(origMethod, newMethod);
Still looking for better solution.
MKMapView has many subviews that redraws. It is very hard to find which view or layer drawed...
Alternatively you could try to find some of MKMapView properties are changed. You can do this with Key Value Observing (KVO) mechanics. http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html
Eg. (properties can be changed to whatever you need)
[myMapView addObserver:self forKeyPath:#"region" options:NSKeyValueObservingOptionNew context:nil];
[myMapView addObserver:self forKeyPath:#"centerCoordinate" options:NSKeyValueObservingOptionNew context:nil];
And you should implement observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// Do something that you want with keyPath;
}
Whenever your mapView has new values for each property you defined, this method will be fired.
I am creating a walking tour and am using mapkit. I have the map loading and am using custom icons as pins and loading my gps coordinates from a plist file. My callouts are working fine. My problem is that I would like the right callout button to load different information on another screen for each stop (picture of stop and an MP3). Would I load a different view controller for each stop on the tour? If so what code would I add to this following to have a view controller named Detailcontroller load?
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
NSLog(#"I've been tapped");
}
If I am way off and someone could point me in the right direction it would be greatly appreciated. I apologize for my wording as I am really new to coding/app development.
I would hope and expect that you'd use the same kind of view controller for each stop, but set up with different data. When I've done something like this, my annotations have been the data objects for each point of interest on the map, so I'd do this:
-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
StopInfo *stopInfo = view.annotation;
StopInfoDetailController *detailController = [[StopInfoDetailController alloc] initWithNibName:nil bundle:nil];
detailController.stopInfo = stopInfo;
[mapView.navigationController pushViewController:detailController animated:YES];
[detailController release];
}
In this case, StopInfo would be the class of the annotation objects. It should contain the information that the detail controller needs to do it's thing: MP3 file name, stop location, stop image file name, stop description, etc.
Note that you might actually have several different kinds of annotations on your map. Maybe some are stops on your walking tour while others are points of interest that aren't on the tour, food vendors, bathrooms, etc. In that case you might want to use different view controllers for each type of anntation, so you'd look at the annotation object to figure out what kind of view controller to instantiate.