I'm implementing search in my MKMapView and I've faced two problems:
When I perform search, location appears in the result and map moves to the found location only after I start to move to the destination. This happens, when the search results are out of the view bounds. If they are inside of the map view bounds or near them it's fine.
It "hops" all the time from one search result to another or to the user's location. I don't expect such behaviour from it.
I've tried several things and I suppose, that the problem is in: didAddAnnotationViews:
[self.locationManager stopUpdatingLocation];
MKAnnotationView *annotationView = [views objectAtIndex:0];
NSLog(#"_Here_ %#", [views description]);
id<MKAnnotation> mp = [annotationView annotation];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance([mp coordinate], 250, 250);
[mv setRegion:region animated:YES];
[self.mapView selectAnnotation:mp animated:YES];
Though, I also thought that the problem is in didUpdateToLocation, so I disable updating after the first pin is drop (by search or by tap):
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation {
double miles = 0.3;
double scalingFactor =
ABS( cos(2 * M_PI * newLocation.coordinate.latitude /360.0) );
MKCoordinateSpan span;
span.latitudeDelta = miles/69.0;
span.longitudeDelta = miles/( scalingFactor*69.0 );
MKCoordinateRegion region;
region.span = span;
region.center = newLocation.coordinate;
[self.mapView setRegion:region animated:YES];
self.mapView.showsUserLocation = YES;
}
Finally, search method:
-(void)searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
{
MKLocalSearchRequest *searchRequest = [[MKLocalSearchRequest alloc] init];
[searchRequest setNaturalLanguageQuery:theSearchBar.text];
searchRequest.region = MKCoordinateRegionMakeWithDistance(self.mapView.userLocation.coordinate, 1000, 1000);
MKLocalSearch *localSearch = [[MKLocalSearch alloc] initWithRequest:searchRequest];
[localSearch startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
if (!error) {
NSMutableArray *annotations = [NSMutableArray array];
[response.mapItems enumerateObjectsUsingBlock:^(MKMapItem *item, NSUInteger idx, BOOL *stop) {
for (id<MKAnnotation>annotation in self.mapView.annotations)
{
if (annotation.coordinate.latitude == item.placemark.coordinate.latitude &&
annotation.coordinate.longitude == item.placemark.coordinate.longitude)
{
return;
}
}
MKPointAnnotation *addAnnotation = [[MKPointAnnotation alloc] init];
addAnnotation.title = [item.placemark.addressDictionary objectForKey:#"Street"];
addAnnotation.coordinate = item.placemark.coordinate;
[annotations addObject:addAnnotation];
}];
for (id<MKAnnotation>annotation in self.mapView.annotations) {
[self.mapView removeAnnotation:annotation];
}
[self.mapView addAnnotations:annotations];
} else {
NSLog(#"Search Request Error: %#", [error localizedDescription]);
}
}];
//Hide the keyboard.
[self.searchBar resignFirstResponder];
}
My aim is to create a MapView, where user can pin the location by tap or via search and, obviously, see the search result.
For the first problem:
When I perform search, location appears in the result and map moves to
the found location only after I start to move to the destination. This
happens, when the search results are out of the view bounds. If they
are inside of the map view bounds or near them it's fine.
This happens because you are moving the map to the annotations found (at least the first one) in the didAddAnnotationViews delegate method.
But that delegate method is only called when an annotation is in the visible area. If an annotation is added to the map but it's not in the visible area (yet), viewForAnnotation won't get called and therefore didAddAnnotationViews won't get called.
Then, when you manually move the map, the annotations that were added start coming into the visible area and then the delegate method gets called and suddenly the map jumps to one of those annotations.
Don't call setRegion inside the didAddAnnotationViews delegate method.
Sometimes, doing so can also cause an endless cycle of viewForAnnotation and didAddAnnotationViews calls because when the region is changed, it causes other annotations to come into view that weren't previously, so viewForAnnotation gets called and then didAddAnnotationViews gets called, and so on.
Instead, set the region right after you call addAnnotations: (or better, just call showAnnotations:) in the searchBarSearchButtonClicked: method.
I would also remove the call to stopUpdatingLocation from didAddAnnotationViews. You probably don't even need the location manager at all if you set the map's showsUserLocation to YES.
For the second problem:
It "hops" all the time from one search result to another or to the
user's location. I don't expect such behaviour from it.
This is also partly due to calling setRegion in didAddAnnotationViews but also because setRegion is called in didUpdateToLocation.
So for the reason described for the first problem, the two delegate methods and the user's manual movements are fighting with each other and the map ends up hopping around.
Don't call setRegion in the didUpdateToLocation method (or, call it once by keeping track in a BOOL whether you've already zoomed to the user location or not).
Not affecting the behavior, but setting showsUserLocation to YES in the didUpdateToLocation doesn't make sense. Why not set this in viewDidLoad or turn it on in the storyboard/xib?
Also, there's no need to calculate the region span manually like that (it's better to let the MapKit do that work for you). Just convert the miles to meters and call MKCoordinateRegionMakeWithDistance.
Related
So I want to display where my app's user walked on a MKMapView, I collect datas with the following code :
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation {
// calc. distance walked
CLLocationDistance meters = [newLocation distanceFromLocation:oldLocation];
self.totalMetters += meters;
[[self labelDistance] setText:[self formatDistanceIntoString:self.totalMetters]];
// create another annotation
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
annotation.coordinate = newLocation.coordinate;
// Also add to our map so we can remove old values later
[self.locations addObject:annotation];
// Remove values if the array is too big
while (self.locations.count > 100)
{
annotation = [self.locations objectAtIndex:0];
[self.locations removeObjectAtIndex:0];
// Also remove from the map
[self.map removeAnnotation:annotation];
}
Once it's finished, I call my draw method :
[self drawRoute];
Which contains the following :
- (void)drawRoute {
NSLog(#"drawRoute");
NSInteger pointsCount = self.locations.count;
CLLocationCoordinate2D pointsToUse[pointsCount];
for(int i = 0; i < pointsCount; i++) {
MKPointAnnotation *an = [self.locations objectAtIndex:i];
pointsToUse[i] = CLLocationCoordinate2DMake(an.coordinate.latitude,an.coordinate.latitude);
}
MKPolyline *myPolyline = [MKPolyline polylineWithCoordinates:pointsToUse count:pointsCount];
[self.map addOverlay:myPolyline];
}
Finally my mapView delegate :
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay {
NSLog(#"route");
if ([overlay isKindOfClass:MKPolyline.class]) {
MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay];
lineView.strokeColor = [UIColor greenColor];
return lineView;
}
return nil;
}
Obviously my controller is MKMapView Delegate conform
#interface NewWalkViewController : UIViewController <CLLocationManagerDelegate, MKMapViewDelegate>
And the mapView in the Storyboard is linked to the controller (outlet and delegate)
I use the "bike road" debug tool and there is the output :
2014-01-25 20:27:30.132 The walking dog[2963:70b] new location : 37.330435
2014-01-25 20:27:30.133 The walking dog[2963:70b] drawRoute
As I can see the method for drawing the overlay is never called, and I don't have a single clue how to fix it.
The main problem is that in drawRoute, this line is passing latitude for both parameters to CLLocationCoordinate2DMake:
pointsToUse[i] = CLLocationCoordinate2DMake
(an.coordinate.latitude,an.coordinate.latitude);
This results in the line being drawn in a different part of the world than where the actual an.coordinate is. For example, if an.coordinate is 37,-122 (somewhere near San Francisco), the line is being drawn instead at 37,37 (somewhere in southern Turkey).
Since you are not actually positioning the map at the wrong location (you are looking for the line at the "right" location), viewForOverlay is never called because the map only calls it when it's possible that the overlay will be visible.
Change that line to:
pointsToUse[i] = CLLocationCoordinate2DMake
(an.coordinate.latitude,an.coordinate.longitude);
or simply:
pointsToUse[i] = an.coordinate;
As James Frost mentions in the comments, as of iOS 7, you should be using rendererForOverlay instead of viewForOverlay though the map view will still call viewForOverlay in iOS 7 if rendererForOverlay has not been implemented. Though this isn't preventing your overlay from displaying in the current case, you should implement the new delegate method as well as the old one (if the iOS 7 app will also be running on iOS 6 or earlier).
Another important but unrelated issue is that you are unnecessarily creating multiple, overlapping overlays. In drawRoute, since the overlay you are creating includes all the locations, you should remove any existing overlays before adding the new one. Otherwise, the map ends up with an overlay for location 0 to 1, an overlay for location 0 to location 2, an overlay for location 0 to location 3, etc. The previous overlays are not obviously visible since they have the same color and line width. Before the addOverlay line, put:
[self.map removeOverlays:self.map.overlays];
I have an app that loads a tabbarcontroller with 3 tabs. One of them is a mapview. It is set to zoom into the user's location by this code:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
_mapView.showsUserLocation = YES;
CLLocationCoordinate2D zoomLocation;
//IF no city was selected, use userLocation as center
if (!self.cityWasSelected) {
zoomLocation.latitude = self.userLocation.coordinate.latitude;
zoomLocation.longitude = self.userLocation.coordinate.longitude;
CLLocationDistance visibleDistance = 5000; // 5 kilometers
MKCoordinateRegion adjustedRegion = MKCoordinateRegionMakeWithDistance(zoomLocation, visibleDistance, visibleDistance);
[_mapView setRegion:adjustedRegion animated:YES];
} else { //if a city was selected, use that city's value...this is actually the same right now, since self.userLocation is set appropriately elsewhere.
//Set location from selection - forward geocode
zoomLocation.latitude = self.userLocation.coordinate.latitude;
zoomLocation.longitude = self.userLocation.coordinate.longitude;
CLLocationDistance visibleDistance = 5000; // 5 kilometers
MKCoordinateRegion adjustedRegion = MKCoordinateRegionMakeWithDistance(zoomLocation, visibleDistance, visibleDistance);
[_mapView setRegion:adjustedRegion animated:YES];
}
}
The initial tab is a tableviewcontroller for user preferences, the second tab is the mapview and the third is a tableview. When I first tap on the mapview, the map shows the entire world :) If i tap back to the initial tab or the list tab and then return to the mapview, the map is properly centered around my current location.
Why does this happen?
Setting showsUserLocation on the map view will start searching for the user's location. This is an asynchronous operation and you cannot assume that mapView.userLocation will be valid immediately after settings showsUserLocation to YES.
The first time you view appears, you ask the map view to start location. mapView.userLocation probably returns nil. The second time around, the map view has probably finally gotten the user location and loads your map region successfully. This is sheer luck, and you should not rely that you will get location the second time (it may fail, take longer than usual).
When the map view has determined user location, it will call back on a delegate method (MKMapViewDelegate). You need to implement this delegate method like so:
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation {
// We have location, do your logic of setting the map region here.
...
}
Hope this helps.
I have a tab on the RootViewController.m. The tab has 2 buttons. The first button upon click will go to CorpViewcontroller which has the mapView on it. When I click on the first button on the first try, the map is blank with google label on the bottom. I have to click back then click on the button again then the map show up. Is it possible to always show the map on the first button click?
My rootViewController.m to go to the second screen:
[self.navigationController pushViewController:self.corpController animated:YES];
The second screen called corpViewController has the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = #"Set Remote Location";
self.jsonData = [[NSMutableData alloc] init];
mapView.showsUserLocation = YES;
//Setup the double tap gesture for getting the remote location..
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(handleGesture:)];
tgr.numberOfTapsRequired = 2;
tgr.numberOfTouchesRequired = 1;
[mapView addGestureRecognizer:tgr];
mapView.delegate = self;
NSLog(#"viewDidLoad done");
}
- (void)viewWillAppear:(BOOL)animated {
NSLog(#"viewWillAppear");
appDelegate = (NBSAppDelegate *)[[UIApplication sharedApplication] delegate];
double curLat = [appDelegate.curLat doubleValue];
MKUserLocation *userLocation = mapView.userLocation;
double miles = 10.0;
double scalingFactor = ABS( (cos(2 * M_PI * curLat / 360.0) ));
MKCoordinateSpan span;
span.latitudeDelta = miles/69.0;
span.longitudeDelta = miles/(scalingFactor * 69.0);
MKCoordinateRegion region2;
region2.span = span;
region2.center = userLocation.coordinate;
[mapView setRegion:region2 animated:YES];
NSLog(#"viewWillAppear done..");
}
Please Advise.
Thank you
Are you initializing the MapView in the viewDidLoad method in your view controller?
If so, try moving it to the viewDidAppear method. That worked for me.
In viewDidLoad you are setting showsUserLocation to YES and in viewWillAppear, you are zooming into the mapView.userLocation coordinate.
The userLocation property isn't usually ready with a valid coordinate immediately after setting showsUserLocation to YES.
The first time you show the view controller, it's still invalid and you are zooming into the coordinate 0,0.
By the time you show the view controller a second time, the user location has been obtained and the coordinate is valid.
Instead of zooming into the user location in viewWillAppear, do it in the delegate method mapView:didUpdateUserLocation: which the map view calls when it gets a user location update.
In addition, you also probably want to move the mapView.showsUserLocation = YES; to viewWillAppear and in viewWillDisappear, set it to NO. This way, the map view will zoom in to the user location every time the view controller is shown instead of just the first time.
An unrelated point is that to zoom in to a specific distance, it's much easier to use the MKCoordinateRegionMakeWithDistance function instead of trying to convert miles to degrees yourself.
Here's an example of the changes suggested in corpViewController:
- (void)viewWillAppear:(BOOL)animated
{
//move this from viewDidLoad to here...
mapView.showsUserLocation = YES;
}
-(void)viewWillDisappear:(BOOL)animated
{
mapView.showsUserLocation = NO;
}
-(void)mapView:(MKMapView *)mv didUpdateUserLocation:(MKUserLocation *)userLocation
//Changed the **internal** parameter name from mapView to mv
//to avoid a compiler warning about it hiding instance var with same name.
//It's better to use the passed parameter variable anyway.
{
NSLog(#"didUpdateUserLocation");
double miles = 10.0;
//Instead of manually calculating span from miles to degrees,
//use MKCoordinateRegionMakeWithDistance function...
//Just need to convert miles to meters.
CLLocationDistance meters = miles * 1609.344;
MKCoordinateRegion region2 = MKCoordinateRegionMakeWithDistance
(userLocation.coordinate, meters, meters);
[mv setRegion:region2 animated:YES];
}
I'm getting an error in MapView that I don't recognize and can't find documentation on. It looks like this:
CoreAnimation: ignoring exception:
Invalid Region <center:-180.00000000, -180.00000000
span:+2.81462803, +28.12500000>
Obviously the numbers are exclusive to my code right now but I can't figure out what's going on. The MapView runs just fine and all of my annotations show up (and it zooms on the user's location like I have it set). What specifically does this point to?
Thanks.
Here's the method I use to zoom to the user's location. It's a little unorthodox but it's what I've been helped with since I had problems with zooms for a variety of reasons (I can explain if need be, but it's probably not relevant):
- (void)zoomToUserLocation:(MKUserLocation *)userlocation
{
if (!userlocation)
return;
MKCoordinateRegion region;
region.center = userlocation.coordinate;
region.span = MKCoordinateSpanMake(2.0, 2.0);
region = [self.mapView regionThatFits:region];
[self.mapView setRegion:region animated:YES];
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self zoomToUserLocation:self.mapView.userLocation];
}
- (void)mapView:(MKMapView *)theMapView didUpdateUserLocation:(MKUserLocation *)location
{
[self zoomToUserLocation:location];
}
Can't tell where the invalid coordinates are coming from but I suggest adding the following checks to the zoomToUserLocation method.
Just checking if userlocation is nil is not enough. You have to also check if the location property inside userlocation is nil or not. Then, you can use the coordinate property (especially when you're using the coordinates outside the didUpdateUserLocation delegate method).
Also, just checking if coordinate is 0,0 (technically a valid coordinate) is not recommended as the struct will be "zero" if it's never been set or it could even be filled with random data. The Core Location framework's CLLocationCoordinate2DIsValid function is used as the last line of defense to prevent an invalid region.
You could also check the timestamp and horizontalAccuracy if you want.
- (void)zoomToUserLocation:(MKUserLocation *)userlocation
{
if (!userlocation)
return;
if (!userlocation.location)
{
NSLog(#"actual location has not been obtained yet");
return;
}
//optional: check age and/or horizontalAccuracy
//(technically should check if location.timestamp is nil first)
NSTimeInterval locationAgeInSeconds =
[[NSDate date] timeIntervalSinceDate:userlocation.location.timestamp];
if (locationAgeInSeconds > 300) //adjust max age as needed
{
NSLog(#"location data is too old");
return;
}
if (!CLLocationCoordinate2DIsValid(userlocation.coordinate))
{
NSLog(#"userlocation coordinate is invalid");
return;
}
MKCoordinateRegion region;
region.center = userlocation.coordinate;
region.span = MKCoordinateSpanMake(2.0, 2.0);
//region = [self.mapView regionThatFits:region];
//don't need to call regionThatFits explicitly, setRegion will do it
[self.mapView setRegion:region animated:YES];
}
Additionally (possibly unrelated and you may have already done this but), based on a couple of your previous questions related to this, you might want to clear and re-set the map view's delegate in the map view controller's viewWillDisappear and viewWillAppear methods to prevent certain errors:
-(void)viewWillAppear:(BOOL)animated
{
mapView.delegate = self;
}
-(void)viewWillDisappear:(BOOL)animated
{
mapView.delegate = nil;
}
I have found that if you have location services enabled and then display a map view that contains the current user location as an annotation, then disable location services and attempt to use the "location" property of the annotation, the result will be (-180, -180).
Very much a newbie here so please forgive the ignorance. I have spent some time trying to understand what I am missing but cannot figure it out.
My app centers over Washington State when loading but when I try to zoom to the users current location it puts me at latitude 0 longitude 0. If I comment out the "// startup: center over WA" section it centers over the users current location and then goToLocation works fine.
How do I get it to center over Washington State and then zoom to the users current location upon clicking goToLocation?
Thanks!
- (void)viewDidLoad {
[super viewDidLoad];
[self loadOurAnnotations];
[mapView setShowsUserLocation:NO];
// startup: center over WA
CLLocationCoordinate2D defaultCoordinate;
defaultCoordinate.latitude = 47.517201;
defaultCoordinate.longitude = -120.366211;
[mapView setRegion:MKCoordinateRegionMake(defaultCoordinate, MKCoordinateSpanMake(6.8, 6.8)) animated:NO];
}
-(IBAction)goToLocation {
MKUserLocation *myLocation = [mapView userLocation];
CLLocationCoordinate2D coord = [[myLocation location] coordinate];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coord, 350, 350);
[mapView setRegion:region animated:YES];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:1];
[UIView commitAnimations];
}
First, to use userLocation in the MKMapView at all, you have to pass YES to setShowsUserLocation (not NO).
Next thing is that after turning showsUserLocation on, it may take a few seconds or more for the map view to determine the location and set userLocation. Until then, the location will be nil (giving coordinates of 0,0).
To really know when the userLocation is ready (or updated), implement the didUpdateUserLocation delegate method. It's also helpful to implement the didFailToLocateUserWithError method in case of a problem determining the user location.
However, in your case, you could just do the following in the goToLocation method:
MKUserLocation *myLocation = [mapView userLocation];
if (myLocation.location == nil)
{
NSLog(#"user location has not been determined yet");
return;
}
CLLocationCoordinate2D coord = [[myLocation location] coordinate];
MKCoordinateRegion region = ... //rest of the code stays the same
The animation statements at the end of that method don't do anything, by the way.