How to Add Gesture Recognizers, Annotations, and Routes in iOS Using MapKit

Add gesture recognizers, annotations, polylines, and driving routes to Apple Maps in iOS using MapKit with UILongPressGestureRecognizer, MKPointAnnotation, MKPolyline, and MKDirections.

Detecting Map Touches with UILongPressGestureRecognizer

A UILongPressGestureRecognizer detects when a user presses and holds on an MKMapView. Use it to place pins without conflicting with the map's built-in tap and zoom gestures.

Add the gesture recognizer in viewDidLoad:

let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didLongPressMap(_:)))
longPressRecognizer.minimumPressDuration = 0.3
mapView.addGestureRecognizer(longPressRecognizer)

The minimumPressDuration of 0.3 seconds balances responsiveness with accidental trigger prevention. Always check the gesture state in the handler to avoid duplicate actions.

@objc func didLongPressMap(_ sender: UILongPressGestureRecognizer) {
    guard sender.state == .ended else { return }
    let touchPoint = sender.location(in: mapView)
    let coordinate = mapView.convert(touchPoint, toCoordinateFrom: mapView)
    addAnnotation(at: coordinate)
}

Placing Annotations with MKPointAnnotation

An MKPointAnnotation represents a pinned location on the map. Create one by setting its coordinate property and adding it to the MKMapView.

func addAnnotation(at coordinate: CLLocationCoordinate2D) {
    let annotation = MKPointAnnotation()
    annotation.coordinate = coordinate
    annotation.title = "Pinned Location"
    mapView.addAnnotation(annotation)
}

Each annotation stores its geographic position as a CLLocationCoordinate2D containing latitude and longitude. The title and subtitle properties display in the callout bubble when the user taps the annotation.

Animating Annotation Drops

Annotations appear instantly by default. To add a drop animation, implement the MKMapViewDelegate method mapView(_:didAdd:).

extension MapViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        for view in views {
            let finalFrame = view.frame
            view.frame = finalFrame.offsetBy(dx: 0, dy: -600)
            UIView.animate(withDuration: 0.4) {
                view.frame = finalFrame
            }
        }
    }
}

Set mapView.delegate = self in viewDidLoad to enable delegate callbacks.

Drawing Polylines with MKPolyline

MKPolyline connects multiple annotations with straight line segments. Build an array of coordinates and pass it to the MKPolyline constructor.

func drawPolyline() {
    mapView.removeOverlays(mapView.overlays)
    var coordinates = annotations.map { $0.coordinate }
    let polyline = MKPolyline(coordinates: &coordinates, count: coordinates.count)
    mapView.addOverlay(polyline)
}

An MKPolyline is an overlay object that stores coordinate data. It does not render anything until you provide an MKOverlayRenderer.

Rendering with MKPolylineRenderer

Implement mapView(_:rendererFor:) to return an MKPolylineRenderer configured with your desired stroke style.

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    if let polylineOverlay = overlay as? MKPolyline {
        let renderer = MKPolylineRenderer(overlay: polylineOverlay)
        renderer.strokeColor = UIColor.systemBlue
        renderer.lineWidth = 4.0
        renderer.alpha = 0.7
        return renderer
    }
    return MKOverlayRenderer(overlay: overlay)
}

MKPolylineRenderer is a subclass of MKOverlayRenderer. It draws the line on the map using the strokeColor, lineWidth, and alpha properties you specify.

Auto-Zooming to the Polyline

After adding a polyline, adjust the map region to show the full path.

let mapRect = mapView.mapRectThatFits(
    polyline.boundingMapRect,
    edgePadding: UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50)
)
mapView.setRegion(MKCoordinateRegion(mapRect), animated: true)

The mapRectThatFits method calculates the optimal visible area. The edgePadding inset prevents the route from touching screen edges.

Drawing Driving Routes with MKDirections

MKDirections fetches turn-by-turn driving or walking routes from Apple's servers. Create an MKDirections.Request with a source and destination MKMapItem, then call calculate.

func requestRoute(from startCoordinate: CLLocationCoordinate2D, to endCoordinate: CLLocationCoordinate2D) {
    let startPlacemark = MKPlacemark(coordinate: startCoordinate)
    let endPlacemark = MKPlacemark(coordinate: endCoordinate)
    let request = MKDirections.Request()
    request.source = MKMapItem(placemark: startPlacemark)
    request.destination = MKMapItem(placemark: endPlacemark)
    request.transportType = .automobile

    let directions = MKDirections(request: request)
    directions.calculate { response, error in
        guard let route = response?.routes.first else {
            if let error = error { print("MKDirections error: (error.localizedDescription)") }
            return
        }
        self.mapView.addOverlay(route.polyline, level: .aboveRoads)
    }
}

Convert each coordinate to an MKPlacemark, wrap it in an MKMapItem, and configure the request's transportType. The calculate method runs asynchronously. Its completion handler receives an MKDirections.Response containing an array of MKRoute objects. Each MKRoute has a polyline property ready for overlay rendering.

Routing Multiple Annotations Sequentially

To draw a route through multiple pinned locations, call the routing function for each consecutive pair.

func routeThroughAllAnnotations() {
    mapView.removeOverlays(mapView.overlays)
    guard annotations.count > 1 else { return }
    for i in 0..<annotations.count - 1 {
        requestRoute(from: annotations[i].coordinate, to: annotations[i + 1].coordinate)
    }
}

Removing All Map Content

Provide a clear action that removes annotations and overlays from the map.

func clearMap() {
    mapView.removeOverlays(mapView.overlays)
    mapView.removeAnnotations(annotations)
    annotations.removeAll()
}

Handling Multiple Route Transport Types

MKDirections.Request supports multiple transportType values. Use .automobile for driving routes. Use .walking for pedestrian routes. Use .transit for public transit directions.

request.transportType = .walking

Each transport type returns different route data. Walking routes use pedestrian paths. Transit routes include public transportation lines. Automobile routes use road networks.

Best Practices for MapKit Development

Check gesture recognizer states in every handler. Process actions only in the .ended state to prevent duplicate pins.

Call mapRectThatFits after adding overlays. The map does not automatically zoom to show new content.

Handle MKDirections errors with user-facing alerts. Network requests can fail due to connectivity or server issues.

Keep overlay data and rendering separate. MKOverlay objects store coordinate data. MKOverlayRenderer subclasses handle visual presentation.

Test on physical devices. Map features behave differently on simulators, especially route calculations and location services.

Summary

This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.

Frequently Asked Questions

Can I test this on the iOS Simulator? Some features require a physical device with a compatible processor. The article notes specific sections where the Simulator is sufficient and where a real device is required.

What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.

All articles