user2242550
user2242550

Reputation:

MapView with local search

I've a question about doing a local search in Apple maps which I have already implemented into my app. The app should show the user where are "supermarkets" around him. My mapView also shows the user's current location but I don't really know how I should do this with the results of the market-locations. Maybe Apple provides a solution/advice for this. Thanks in advance.

The results (supermarkets) should be displayed as in this picture

The results (supermarkets) should be displayed as in this picture.

UPDATE: The function works perfectly but is it also possible to make these pins clickable to show an info-sheet about this location as in the pictures below?

enter image description here enter image description here

UPDATE 2: I'm so fascinated from this map-implemention, so I wonder if it's also possible to get infos about your current location (as in the screenshot below). enter image description here

Upvotes: 1

Views: 3203

Answers (1)

Rob
Rob

Reputation: 438477

In iOS 6.1 and later, you can use MKLocalSearch, adding annotations for each of the MKMapItem objects you find.

MKLocalSearchRequest *request = [[MKLocalSearchRequest alloc] init];
request.naturalLanguageQuery = @"supermarket";
request.region = mapView.region;

MKLocalSearch *search = [[MKLocalSearch alloc] initWithRequest:request];

[search startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
    for (MKMapItem *item in response.mapItems)
    {
        MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
        annotation.coordinate = item.placemark.coordinate;
        annotation.title      = item.name;
        annotation.subtitle   = item.placemark.title;
        [mapView addAnnotation:annotation];
    }
}];

If you want the right and left accessories on your callouts, you should implement a viewForAnnotation that adds those accessories (and of course, for this to work, you have to define your controller to be the delegate for your MKMapView):

typedef enum : NSInteger
{
    kCallOutAccessoryRight = 1,
    kCallOutAccessoryLeft
} CallOutAccessoryType;

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    if ([annotation isKindOfClass:[MKUserLocation class]])
        return nil;

    static NSString *identifier = @"customAnnotationView";

    MKAnnotationView *annotationView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];

    if (annotationView == nil)
    {
        annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
        annotationView.canShowCallout = YES;

        annotationView.rightCalloutAccessoryView     = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
        annotationView.rightCalloutAccessoryView.tag = kCallOutAccessoryRight;

        annotationView.leftCalloutAccessoryView      = ...; // whatever you want for the left button
        annotationView.leftCalloutAccessoryView.tag  = kCallOutAccessoryLeft;
    }
    else
    {
        annotationView.annotation = annotation;
    }

    return annotationView;
}

And, you presumably want to respond to the user tapping on those callouts:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
    if (control.tag == kCallOutAccessoryRight)
    {
        NSLog(@"Present info sheet for %@ here", [view.annotation title]);
    }
    else if (control.tag == kCallOutAccessoryLeft)
    {
        NSLog(@"Do whatever you want if left accessory tapped");
    }
}

You asked how to present the user directions. Yes, you can do that by passing it a MKMapItem to openMapsWithItems. But that presumes that you save the MKMapItem from the local search. To do this, you're creating a custom annotation. For example:

//  MapItemAnnotation.h

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MapItemAnnotation : NSObject <MKAnnotation>

@property (nonatomic, strong, readonly) MKMapItem *item;

- (id)initWithMapItem:(MKMapItem *)item;
- (NSString *)title;
- (NSString *)subtitle;
- (CLLocationCoordinate2D)coordinate;

@end

And

//  MapItemAnnotation.m

#import "MapItemAnnotation.h"

@implementation MapItemAnnotation

- (id)initWithMapItem:(MKMapItem *)item
{
    self = [super init];
    if (self) {
        _item = item;
    }
    return self;
}

- (NSString *)title
{
    return _item.name;
}

- (NSString *)subtitle
{
    return _item.placemark.title;
}

- (CLLocationCoordinate2D)coordinate
{
    return _item.placemark.coordinate;
}

@end

Having done that, your adding the annotation to your maps is simplified a bit:

[search startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
    for (MKMapItem *item in response.mapItems)
    {
        MapItemAnnotation *annotation = [[MapItemAnnotation alloc] initWithMapItem:item];
        [mapView addAnnotation:annotation];
    }
}];

But, now, your left callout accessory can easily initiate directions in Apple Maps:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
    if (control.tag == kCallOutAccessoryRight)
    {
        NSLog(@"Present info sheet for %@ here", [view.annotation title]);
    }
    else if (control.tag == kCallOutAccessoryLeft)
    {
        // request directions from Apple Maps

        MapItemAnnotation *annotation = view.annotation;
        NSAssert([annotation isKindOfClass:[MapItemAnnotation class]], @"Annotation should be MapItemAnnotation: %@", annotation);

        [annotation.item openInMapsWithLaunchOptions:@{MKLaunchOptionsMapCenterKey      : [NSValue valueWithMKCoordinate:mapView.region.center],
                                                       MKLaunchOptionsMapSpanKey        : [NSValue valueWithMKCoordinateSpan:mapView.region.span],
                                                       MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving}];
    }
}

Regarding updating the user location description, you can define a didUpdateUserLocation method (i.e. the map view calls this MKMapViewDelegate method every time the user's location changes). It looks like you want to update the subtitle for the user's location:

- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
    [self updateSubtitleForUserLocation:userLocation];
}

That would call this method that would do the reverse geocode and update the subtitle accordingly:

- (void)updateSubtitleForUserLocation:(MKUserLocation *)userLocation
{
    if ([self.geocoder isGeocoding])
        [self.geocoder cancelGeocode];

    [self.geocoder reverseGeocodeLocation:userLocation.location completionHandler:^(NSArray *placemarks, NSError *error) {
        MKPlacemark *placemark = placemarks[0];
        userLocation.subtitle = placemark.name;
    }];
}

Clearly, you need a class property:

@property (nonatomic, strong) CLGeocoder *geocoder;

And you need to instantiate that, e.g. viewDidLoad could:

self.geocoder = [[CLGeocoder alloc] init];

I might refine this a bit to only do a reverse geocode if the user's location has changed by more than x meters (you don't want to do too many reverse geocode requests; you'll kill the user's battery), but hopefully this illustrates the idea.

Upvotes: 2

Related Questions