When I initialize the map, it shows both my location and the destination (forward geocoded from a string) but it does not draw directions between them.
Here is my code :
#import "EventDetailMapViewController.h"
@interface EventDetailMapViewController ()
@property (nonatomic,strong) MKMapItem *destination;
@end
@implementation EventDetailMapViewController
CLPlacemark *thePlacemark;
MKRoute *routeDetails;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
_mapView.showsUserLocation = YES;
self.navigationController.toolbarHidden = NO;
_mapView.delegate = self;
[self getRoute];
}
- (void)addAnnotation:(CLPlacemark *)placemark {
MKPointAnnotation *point = [[MKPointAnnotation alloc] init];
point.coordinate = CLLocationCoordinate2DMake(placemark.location.coordinate.latitude, placemark.location.coordinate.longitude);
point.title = [placemark.addressDictionary objectForKey:@"Street"];
point.subtitle = [placemark.addressDictionary objectForKey:@"City"];
[self.mapView addAnnotation:point];
}
-(void)showRoute:(MKDirectionsResponse *)response{
for (MKRoute *route in response.routes)
{
[_mapView
addOverlay:route.polyline level:MKOverlayLevelAboveRoads];
for (MKRouteStep *step in route.steps){
NSLog(@"%@",step.instructions);
}
}
}
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
// If it's the user location, just return nil.
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
// Handle any custom annotations.
if ([annotation isKindOfClass:[MKPointAnnotation class]]) {
// Try to dequeue an existing pin view first.
MKPinAnnotationView *pinView = (MKPinAnnotationView*)[self.mapView dequeueReusableAnnotationViewWithIdentifier:@"CustomPinAnnotationView"];
if (!pinView)
{
// If an existing pin view was not available, create one.
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"CustomPinAnnotationView"];
pinView.canShowCallout = YES;
} else {
pinView.annotation = annotation;
}
return pinView;
}
return nil;
}
-(void)getRoute {
[self getLocationFromString];
MKDirectionsRequest *directionsRequest = [[MKDirectionsRequest alloc] init];
MKPlacemark *placemark = [[MKPlacemark alloc] initWithPlacemark:thePlacemark];
[directionsRequest setSource:[MKMapItem mapItemForCurrentLocation]];
[directionsRequest setDestination:[[MKMapItem alloc] initWithPlacemark:placemark]];
directionsRequest.transportType = MKDirectionsTransportTypeAutomobile;
MKDirections *directions = [[MKDirections alloc] initWithRequest:directionsRequest];
[directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {
if (error) {
NSLog(@"Error %@", error.description);
} else {
routeDetails = response.routes.lastObject;
[self.mapView addOverlay:routeDetails.polyline];
}
}];
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
MKPolylineRenderer * routeLineRenderer = [[MKPolylineRenderer alloc] initWithPolyline:routeDetails.polyline];
routeLineRenderer.strokeColor = [UIColor redColor];
routeLineRenderer.lineWidth = 5;
return routeLineRenderer;
}
- (IBAction)changeMapType:(id)sender {
if (_mapView.mapType == MKMapTypeStandard)
_mapView.mapType = MKMapTypeSatellite;
else
_mapView.mapType = MKMapTypeStandard;
}
-(void)getLocationFromString{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:self.agendaEntry.address completionHandler:^(NSArray *placemarks, NSError *error) {
if (error) {
NSLog(@"%@", error);
} else {
thePlacemark = [placemarks lastObject];
float spanX = 1.00725;
float spanY = 1.00725;
MKCoordinateRegion region;
region.center.latitude = thePlacemark.location.coordinate.latitude;
region.center.longitude = thePlacemark.location.coordinate.longitude;
region.span = MKCoordinateSpanMake(spanX, spanY);
[self.mapView setRegion:region animated:YES];
[self addAnnotation:thePlacemark];
}
}];
}
@end
I'm relatively new to this so some of this is probably wrong. What I want is when I push a button (in another view) the "agendaEntry" gets passed on the segue. It contains a string with an address, this address gets forward-geocoded and the map displays directions/routes from the user's location to the address in that string.
I don't know how to make it show driving/walking directions or to make it show multiple routes. But for starters, it would be great if it would work.
The reason it doesn't draw directions is because the directions request is being made before the destination placemark (
thePlacemark
) is actually set by thegeocodeAddressString
completion handler block.Note what the documentation says about
geocodeAddressString:completionHandler:
:So even though
getLocationFromString
(which callsgeocodeAddressString
) is called before the directions request, execution returns and continues ingetRoute
immediately after thegeocodeAddressString
is initiated (but not completed).Therefore,
thePlacemark
is stillnil
whengetRoute
sets updirectionsRequest
and you're probably getting an error logged such as "directions not available".To account for this, you can move the call to the directions-request-set-up code to inside the
geocodeAddressString
completion handler block (after the add-annotation code).There are three things to change for this:
viewDidLoad
, do[self getLocationFromString];
instead of[self getRoute];
.getRoute
, remove the call togetLocationFromString
.getLocationFromString
, in the completion handler block, after[self addAnnotation:thePlacemark];
, do[self getRoute];
.Regarding showing multiple routes, first you'll need to actually request alternate routes (default is
NO
):The other part of the problem you may have experienced is due to this line in
rendererForOverlay
:It's creating a polyline renderer using an external instance variable instead of the
overlay
parameter provided to the delegate method. This basically means the delegate method will only work for that one specific overlay (routeDetails.polyline
) and since there's no control over when exactly the delegate method will be called by the map view, you can't reliably setrouteDetails.polyline
from outside the delegate method to be sure it points to the right overlay. Each alternate route is a separate polyline and the map view will make separate calls torendererForOverlay
for each one.Instead, create the polyline renderer using the
overlay
parameter (and first check ifoverlay
is anMKPolyline
):Then in
getRoute
, instead of this:call the
showRoute
method you already have which adds all the routes: