Detecting touches on MKOverlay in iOS7 (MKOverlayRenderer)

Viewed 8510

I have an MKMapView with possibly hundreds of polygons drawn. Using MKPolygon and MKPolygonRenderer as one is suppose to on iOS7.

What I need is a way of acting upon the user touching one of the polygons. They represent an area on the map with a certain population density for example. On iOS6 the MKOverlays were drawn as MKOverlayViews so touch detection was more straightforward. Now using renderers I don't really see how this is suppose to be done.

I'm not sure this will help or is even relevant but as a reference I'll post some code:

This adds all the MKOverlays to the MKMapView using mapData.

-(void)drawPolygons{
    self.polygonsInfo = [NSMutableDictionary dictionary];
    NSArray *polygons = [self.mapData valueForKeyPath:@"polygons"];

    for(NSDictionary *polygonInfo in polygons){
        NSArray *polygonPoints = [polygonInfo objectForKey:@"boundary"];
        int numberOfPoints = [polygonPoints count];

        CLLocationCoordinate2D *coordinates = malloc(numberOfPoints * sizeof(CLLocationCoordinate2D));
        for (int i = 0; i < numberOfPoints; i++){
            NSDictionary *pointInfo = [polygonPoints objectAtIndex:i];

            CLLocationCoordinate2D point;
            point.latitude = [[pointInfo objectForKey:@"lat"] floatValue];
            point.longitude = [[pointInfo objectForKey:@"long"] floatValue];

            coordinates[i] = point;
        }

        MKPolygon *polygon = [MKPolygon polygonWithCoordinates:coordinates count:numberOfPoints];
        polygon.title = [polygonInfo objectForKey:@"name"];
        free(coordinates);
        [self.mapView addOverlay:polygon];
        [self.polygonsInfo setObject:polygonInfo forKey:polygon.title]; // Saving this element information, indexed by title, for later use on mapview delegate method
    }
}

Then there is the delegate method for returning a MKOverlayRenderer for each MKOverlay:

-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay{
    /* ... */

    MKPolygon *polygon = (MKPolygon*) overlay;
    NSDictionary *polygonInfo = [self.polygonsInfo objectForKey:polygon.title]; // Retrieving element info by element title
    NSDictionary *colorInfo = [polygonInfo objectForKey:@"color"];

    MKPolygonRenderer *polygonRenderer = [[MKPolygonRenderer alloc] initWithPolygon:polygon];

    polygonRenderer.fillColor = [UIColor colorWithRed:[[colorInfo objectForKey:@"red"] floatValue]
                                               green:[[colorInfo objectForKey:@"green"] floatValue]
                                                blue:[[colorInfo objectForKey:@"blue"] floatValue]
                                               alpha:[[polygonInfo objectForKey:@"opacity"] floatValue]];

    return polygonRenderer;

    /* ... */
}
8 Answers

Based on @davidrynn answer i've accomplished a more dynamic and updated result.

Swift 5

Subclass MKMapView:

public class MapView: MKMapView {

public var mapViewProtocol: MapViewProtocol?

public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    if let touch = touches.first {

        if touch.tapCount == 1 {

            let touchLocation: CGPoint = touch.location(in: self)
            let locationCoordinate: CLLocationCoordinate2D = self.convert(touchLocation, toCoordinateFrom: self)

            var mkCircleList: [MKCircle] = self.overlays.compactMap { $0 as? MKCircle }
            mkCircleList = mkCircleList.filter { $0.contains(locationCoordinate) }
            if !mkCircleList.isEmpty {

                self.mapViewProtocol?.didTapMKCircles(self, mkCircleList)
            }

            var mkMultiPolygonList: [MKMultiPolygon] = self.overlays.compactMap { $0 as? MKMultiPolygon }
            mkMultiPolygonList = mkMultiPolygonList.filter { $0.contains(locationCoordinate) }
            if !mkMultiPolygonList.isEmpty {

                self.mapViewProtocol?.didTapMKMultiPolygons(self, mkMultiPolygonList)
            }

            var mkPolygonList: [MKPolygon] = self.overlays.compactMap { $0 as? MKPolygon }
            mkPolygonList = mkPolygonList.filter { $0.contains(locationCoordinate) }
            if !mkPolygonList.isEmpty {

                self.mapViewProtocol?.didTapMKPolygons(self, mkPolygonList)
            }

            var mkMultiPolylineList: [MKMultiPolyline] = self.overlays.compactMap { $0 as? MKMultiPolyline }
            mkMultiPolylineList = mkMultiPolylineList.filter { $0.contains(locationCoordinate) }
            if !mkMultiPolylineList.isEmpty {

                self.mapViewProtocol?.didTapMKMultiPolylines(self, mkMultiPolylineList)
            }

            var mkPolylineList: [MKPolyline] = self.overlays.compactMap { $0 as? MKPolyline }
            mkPolylineList = mkPolylineList.filter { $0.contains(locationCoordinate) }
            if !mkPolylineList.isEmpty {

                self.mapViewProtocol?.didTapMKPolylines(self, mkPolylineList)
            }

            //TODO
            //var mkTileOverlayList: [MKTileOverlay] = self.overlays.compactMap { $0 as? MKTileOverlay }
            //mkTileOverlayList = mkTileOverlayList.filter { $0.contains(locationCoordinate) }


            self.mapViewProtocol?.didTapMap(self, locationCoordinate)
        }
    }

    super.touchesEnded(touches, with: event)
}

}

After that i created multiple extensions for each mkOverlay type:

MKKCircle

import Foundation
import MapKit

extension MKCircle {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKCircleRenderer(circle: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

MKMultiPolygon

import Foundation
import MapKit

@available(iOS 13.0, *)
extension MKMultiPolygon {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        return self.polygons.filter { $0.contains(coordinate2D) }.isEmpty ? false : true
    }
}

MKMultiPolyline

    import Foundation
import MapKit

@available(iOS 13.0, *)
extension MKMultiPolyline {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        return self.polylines.filter { $0.contains(coordinate2D) }.isEmpty ? false : true
    }
}

MKPolygon

import Foundation
import MapKit

extension MKPolygon {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKPolygonRenderer(polygon: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate2D)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

MKPolyline

import Foundation
import MapKit

extension MKPolyline {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKPolylineRenderer(polyline: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate2D)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

And finally create and implement the protocol:

    public protocol MapViewProtocol {

    func didTapMKPolygons(_ mapView: MKMapView, _ mkPolygons: [MKPolygon])

    func didTapMKCircles(_ mapView: MKMapView, _ mkCircles: [MKCircle])

    func didTapMKPolylines(_ mapView: MKMapView, _ mkPolylines: [MKPolyline])

    func didTapMKMultiPolygons(_ mapView: MKMapView, _ mkMultiPolygons: [MKMultiPolygon])

    func didTapMKMultiPolylines(_ mapView: MKMapView, _ mkMultiPolylines: [MKMultiPolyline])

    func didTapMap(_ mapView: MKMapView, _ clLocationCoordinate2D: CLLocationCoordinate2D)
}
Related