--- name: axiom-mapkit-ref description: MapKit API reference — SwiftUI Map, MKMapView, Marker, Annotation, MKLocalSearch, MKDirections, Look Around, MKMapSnapshotter, clustering, overlays, GeoToolbox PlaceDescriptor, geocoding license: MIT metadata: version: "1.0.0" last-updated: "2026-02-26" --- # MapKit API Reference Complete MapKit API reference for iOS development. Covers both SwiftUI Map (iOS 17+) and MKMapView (UIKit). ## Related Skills - `axiom-mapkit` — Decision trees, anti-patterns, pressure scenarios - `axiom-mapkit-diag` — Symptom-based troubleshooting --- ## Part 1: Modern API Overview | Feature | SwiftUI Map (iOS 17+) | MKMapView | |---|---|---| | Declaration | `Map(position:) { content }` | `MKMapView()` | | Camera control | `MapCameraPosition` binding | `setRegion(_:animated:)` | | Annotations | `Marker`, `Annotation` in content | `addAnnotation(_:)` + delegate | | Overlays | `MapCircle`, `MapPolyline`, `MapPolygon` | `addOverlay(_:)` + renderer delegate | | User location | `UserAnnotation()` | `showsUserLocation = true` | | Selection | `.mapSelection($selection)` | delegate `didSelect` | | Controls | `.mapControls { }` | `showsCompass`, `showsScale` | | Interaction modes | `.mapInteractionModes([])` | delegate methods | | Clustering | Built-in via `.mapItemClusteringIdentifier` | `MKClusterAnnotation` | --- ## Part 2: SwiftUI Map API ### Basic Map ```swift @State private var cameraPosition: MapCameraPosition = .automatic Map(position: $cameraPosition) { Marker("Home", coordinate: homeCoord) Annotation("Custom", coordinate: coord) { Image(systemName: "star.fill") .foregroundStyle(.yellow) .padding(4) .background(.blue, in: Circle()) } UserAnnotation() MapCircle(center: coord, radius: 500) .foregroundStyle(.blue.opacity(0.3)) MapPolyline(coordinates: routeCoords) .stroke(.blue, lineWidth: 3) } .mapStyle(.standard(elevation: .realistic)) .mapControls { MapUserLocationButton() MapCompass() MapScaleView() } ``` ### MapCameraPosition Controls where the camera is positioned: ```swift // System manages camera to show all content .automatic // Specific region .region(MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) )) // Specific camera with pitch and heading .camera(MapCamera( centerCoordinate: coordinate, distance: 1000, // meters from center heading: 90, // degrees from north pitch: 60 // degrees from vertical (0 = top-down) )) // Follow user location .userLocation(followsHeading: true, fallback: .automatic) // Show specific item .item(mapItem) // Show specific rect .rect(MKMapRect(...)) ``` #### Programmatic Camera Changes ```swift // Animate to new position withAnimation { cameraPosition = .region(newRegion) } // Keyframe animation (iOS 17+) Map(position: $cameraPosition) .mapCameraKeyframeAnimator(trigger: flyToTrigger) { initialCamera in KeyframeTrack(\.centerCoordinate) { LinearKeyframe(destination, duration: 2.0) } KeyframeTrack(\.distance) { CubicKeyframe(5000, duration: 1.0) CubicKeyframe(1000, duration: 1.0) } } ``` ### Map Selection ```swift @State private var selectedItem: MKMapItem? Map(position: $cameraPosition, selection: $selectedItem) { ForEach(mapItems, id: \.self) { item in Marker(item: item) } } .onChange(of: selectedItem) { _, newItem in if let newItem { // Handle selection } } ``` ### Camera Change Callback ```swift Map(position: $cameraPosition) { ... } .onMapCameraChange { context in // context.region — visible MKCoordinateRegion // context.camera — current MapCamera // context.rect — visible MKMapRect fetchAnnotations(in: context.region) } .onMapCameraChange(frequency: .continuous) { context in // Called during gesture (not just at end) } ``` ### Map Styles ```swift .mapStyle(.standard) // Default .mapStyle(.standard(elevation: .realistic)) // 3D buildings .mapStyle(.standard(emphasis: .muted)) // Muted colors .mapStyle(.standard(pointsOfInterest: .including([.restaurant, .cafe]))) .mapStyle(.imagery) // Satellite .mapStyle(.imagery(elevation: .realistic)) // 3D satellite .mapStyle(.hybrid) // Satellite + labels .mapStyle(.hybrid(elevation: .realistic)) // 3D hybrid ``` ### Interaction Modes ```swift // Allow all interactions (default) .mapInteractionModes(.all) // Read-only map (no interaction) .mapInteractionModes([]) // Pan only, no zoom .mapInteractionModes([.pan]) // Pan and zoom, no rotate/pitch .mapInteractionModes([.pan, .zoom]) ``` --- ## Part 3: Map Content ### Marker System-styled map marker with callout: ```swift // Basic marker Marker("Coffee Shop", coordinate: coord) // With system image Marker("Coffee Shop", systemImage: "cup.and.saucer.fill", coordinate: coord) // With monogram (2 characters max) Marker("Coffee Shop", monogram: Text("CS"), coordinate: coord) // Color Marker("Coffee Shop", coordinate: coord) .tint(.brown) // From MKMapItem Marker(item: mapItem) ``` ### Annotation Fully custom view at a coordinate: ```swift Annotation("Custom Pin", coordinate: coord) { VStack { Image(systemName: "mappin.circle.fill") .font(.title) .foregroundStyle(.red) Text("Here") .font(.caption) } } // Anchor point (default is bottom center) Annotation("Pin", coordinate: coord, anchor: .center) { Circle() .fill(.blue) .frame(width: 20, height: 20) } ``` ### UserAnnotation Current user location indicator: ```swift UserAnnotation() // Custom appearance UserAnnotation(anchor: .center) { Image(systemName: "location.circle.fill") .foregroundStyle(.blue) } ``` ### Shape Overlays ```swift // Circle MapCircle(center: coord, radius: 1000) // radius in meters .foregroundStyle(.blue.opacity(0.2)) .stroke(.blue, lineWidth: 2) // Polygon MapPolygon(coordinates: polygonCoords) .foregroundStyle(.green.opacity(0.3)) .stroke(.green, lineWidth: 2) // Polyline MapPolyline(coordinates: routeCoords) .stroke(.blue, lineWidth: 4) // From MKRoute MapPolyline(route.polyline) .stroke(.blue, lineWidth: 5) ``` ### Clustering ```swift ForEach(locations) { location in Marker(location.name, coordinate: location.coordinate) .tag(location.id) } .mapItemClusteringIdentifier("locations") ``` --- ## Part 4: MKMapView Lifecycle and Delegates ### Creating MKMapView in SwiftUI ```swift struct MapViewWrapper: UIViewRepresentable { @Binding var region: MKCoordinateRegion let annotations: [MKAnnotation] func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator mapView.showsUserLocation = true mapView.register( MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "marker" ) return mapView } func updateUIView(_ mapView: MKMapView, context: Context) { // Guard against infinite loops if !regionsAreEqual(mapView.region, region) { mapView.setRegion(region, animated: true) } // Diff annotations instead of removing all let current = Set(mapView.annotations.compactMap { $0 as? MyAnnotation }) let desired = Set(annotations.compactMap { $0 as? MyAnnotation }) let toAdd = desired.subtracting(current) let toRemove = current.subtracting(desired) mapView.addAnnotations(Array(toAdd)) mapView.removeAnnotations(Array(toRemove)) } func makeCoordinator() -> Coordinator { Coordinator(self) } static func dismantleUIView(_ mapView: MKMapView, coordinator: Coordinator) { mapView.removeAnnotations(mapView.annotations) mapView.removeOverlays(mapView.overlays) } } ``` ### Key MKMapViewDelegate Methods ```swift class Coordinator: NSObject, MKMapViewDelegate { var parent: MapViewWrapper init(_ parent: MapViewWrapper) { self.parent = parent } // Annotation view customization func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { guard !(annotation is MKUserLocation) else { return nil } // Use default for user let view = mapView.dequeueReusableAnnotationView( withIdentifier: "marker", for: annotation ) as! MKMarkerAnnotationView view.markerTintColor = .systemRed view.glyphImage = UIImage(systemName: "mappin") view.clusteringIdentifier = "poi" view.canShowCallout = true return view } // Overlay rendering func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let circle = overlay as? MKCircle { let renderer = MKCircleRenderer(circle: circle) renderer.fillColor = UIColor.systemBlue.withAlphaComponent(0.2) renderer.strokeColor = .systemBlue renderer.lineWidth = 2 return renderer } if let polyline = overlay as? MKPolyline { let renderer = MKPolylineRenderer(polyline: polyline) renderer.strokeColor = .systemBlue renderer.lineWidth = 4 return renderer } if let polygon = overlay as? MKPolygon { let renderer = MKPolygonRenderer(polygon: polygon) renderer.fillColor = UIColor.systemGreen.withAlphaComponent(0.3) renderer.strokeColor = .systemGreen renderer.lineWidth = 2 return renderer } return MKOverlayRenderer(overlay: overlay) } // Region change tracking func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { parent.region = mapView.region } // Annotation selection func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) { // Handle tap } // Cluster annotation func mapView( _ mapView: MKMapView, clusterAnnotationForMemberAnnotations memberAnnotations: [MKAnnotation] ) -> MKClusterAnnotation { MKClusterAnnotation(memberAnnotations: memberAnnotations) } } ``` --- ## Part 5: Annotation Types and Customization ### MKMarkerAnnotationView (iOS 11+) Balloon-shaped marker with glyph: ```swift let view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "marker") view.markerTintColor = .systemPurple view.glyphImage = UIImage(systemName: "star.fill") view.glyphText = "A" // Text glyph (overrides image) view.displayPriority = .required // Always visible view.clusteringIdentifier = "category" // Enable clustering view.canShowCallout = true view.titleVisibility = .adaptive // Show title based on space view.subtitleVisibility = .hidden ``` ### MKAnnotationView Fully custom annotation view: ```swift let view = MKAnnotationView(annotation: annotation, reuseIdentifier: "custom") view.image = UIImage(named: "custom-pin") view.centerOffset = CGPoint(x: 0, y: -view.image!.size.height / 2) view.canShowCallout = true view.leftCalloutAccessoryView = UIImageView(image: thumbnail) view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure) ``` ### Custom Callout ```swift func mapView( _ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl ) { guard let annotation = view.annotation as? MyAnnotation else { return } // Navigate to detail view } ``` ### Annotation View Reuse Always use `dequeueReusableAnnotationView(withIdentifier:for:)`: ```swift // Register in makeUIView (once) mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "marker") // Dequeue in delegate (every time) func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { let view = mapView.dequeueReusableAnnotationView(withIdentifier: "marker", for: annotation) // Configure... return view } ``` Without reuse: 1000 annotations = 1000 views in memory. With reuse: ~20-30 views recycled as user scrolls. --- ## Part 6: MKLocalSearch and MKLocalSearchCompleter ### MKLocalSearchCompleter — Real-Time Autocomplete ```swift let completer = MKLocalSearchCompleter() completer.delegate = self completer.resultTypes = [.pointOfInterest, .address] completer.region = visibleMapRegion // Bias results to visible area // Update on each keystroke completer.queryFragment = "coffee" // Delegate receives results func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { let results = completer.results // [MKLocalSearchCompletion] for result in results { // result.title — "Starbucks" // result.subtitle — "123 Main St, San Francisco, CA" // result.titleHighlightRanges — Ranges matching query } } func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { // Network error, rate limit, etc. } ``` ### MKLocalSearch — Full Search ```swift // From autocomplete completion let request = MKLocalSearch.Request(completion: selectedCompletion) // From natural language let request = MKLocalSearch.Request() request.naturalLanguageQuery = "coffee shops" request.region = mapRegion // Bias results request.resultTypes = .pointOfInterest // Filter type request.pointOfInterestFilter = MKPointOfInterestFilter( including: [.cafe, .restaurant] ) let search = MKLocalSearch(request: request) let response = try await search.start() for item in response.mapItems { // item.name — "Starbucks" // item.placemark — MKPlacemark with address // item.placemark.coordinate — CLLocationCoordinate2D // item.phoneNumber — optional phone // item.url — optional website // item.pointOfInterestCategory — .cafe, .restaurant, etc. } ``` ### Result Types ```swift // Filter what kind of results to return request.resultTypes = .address // Street addresses only request.resultTypes = .pointOfInterest // Businesses, landmarks request.resultTypes = .physicalFeature // Mountains, lakes, parks request.resultTypes = .query // Suggested search queries (iOS 18+) request.resultTypes = [.pointOfInterest, .address] // Multiple types ``` ### Rate Limiting - `MKLocalSearchCompleter` handles its own throttling — safe to call on every keystroke - `MKLocalSearch` — Apple rate-limits these; don't fire more than ~1/second - If rate-limited, you'll get an error in the completion handler - Reuse `MKLocalSearchCompleter` instances — don't create new ones per query --- ## Part 7: MKDirections and MKRoute ### Calculate Directions ```swift let request = MKDirections.Request() request.source = MKMapItem.forCurrentLocation() request.destination = destinationMapItem request.transportType = .automobile request.requestsAlternateRoutes = true // Get multiple routes let directions = MKDirections(request: request) let response = try await directions.calculate() for route in response.routes { route.polyline // MKPolyline — display on map route.expectedTravelTime // TimeInterval in seconds route.distance // CLLocationDistance in meters route.name // "I-280 S" — route name route.advisoryNotices // [String] — warnings route.steps // [MKRoute.Step] — turn-by-turn } ``` ### Transport Types ```swift .automobile // Driving directions .walking // Pedestrian directions .transit // Public transit (where available) .any // All modes ``` ### ETA Only (Faster) ```swift let directions = MKDirections(request: request) let eta = try await directions.calculateETA() eta.expectedTravelTime // TimeInterval eta.distance // CLLocationDistance eta.expectedArrivalDate // Date eta.expectedDepartureDate // Date eta.transportType // MKDirectionsTransportType ``` ### Turn-by-Turn Steps ```swift for step in route.steps { step.instructions // "Turn right onto Main St" step.distance // CLLocationDistance in meters step.polyline // MKPolyline for this step's segment step.transportType // May change for transit routes step.notice // Optional advisory } ``` --- ## Part 8: Look Around ### Check Availability ```swift let request = MKLookAroundSceneRequest(coordinate: coordinate) do { let scene = try await request.scene // scene is non-nil — Look Around available at this coordinate } catch { // Look Around not available here } ``` ### SwiftUI ```swift @State private var lookAroundScene: MKLookAroundScene? LookAroundPreview(scene: $lookAroundScene) .frame(height: 200) // Load scene func loadLookAround(for coordinate: CLLocationCoordinate2D) async { let request = MKLookAroundSceneRequest(coordinate: coordinate) lookAroundScene = try? await request.scene } ``` ### UIKit ```swift let controller = MKLookAroundViewController(scene: scene) // Present modally or embed as child view controller ``` ### Static Snapshot ```swift let snapshotter = MKLookAroundSnapshotter(scene: scene, options: .init()) let snapshot = try await snapshotter.snapshot let image = snapshot.image // UIImage ``` --- ## Part 9: Overlays and Renderers ### Adding Overlays (MKMapView) ```swift // Circle let circle = MKCircle(center: coordinate, radius: 1000) mapView.addOverlay(circle) // Polygon let polygon = MKPolygon(coordinates: &coords, count: coords.count) mapView.addOverlay(polygon) // Polyline let polyline = MKPolyline(coordinates: &coords, count: coords.count) mapView.addOverlay(polyline, level: .aboveRoads) // Custom tile overlay let template = "https://tile.example.com/{z}/{x}/{y}.png" let tileOverlay = MKTileOverlay(urlTemplate: template) tileOverlay.canReplaceMapContent = true // Hides Apple Maps base layer mapView.addOverlay(tileOverlay, level: .aboveLabels) ``` ### Renderer Delegate ```swift func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { switch overlay { case let circle as MKCircle: let renderer = MKCircleRenderer(circle: circle) renderer.fillColor = UIColor.systemBlue.withAlphaComponent(0.2) renderer.strokeColor = .systemBlue renderer.lineWidth = 2 return renderer case let polyline as MKPolyline: let renderer = MKPolylineRenderer(polyline: polyline) renderer.strokeColor = .systemBlue renderer.lineWidth = 4 return renderer case let polygon as MKPolygon: let renderer = MKPolygonRenderer(polygon: polygon) renderer.fillColor = UIColor.systemGreen.withAlphaComponent(0.3) renderer.strokeColor = .systemGreen renderer.lineWidth = 2 return renderer case let tile as MKTileOverlay: return MKTileOverlayRenderer(tileOverlay: tile) default: return MKOverlayRenderer(overlay: overlay) } } ``` ### Overlay Levels ```swift mapView.addOverlay(overlay, level: .aboveRoads) // Above roads, below labels mapView.addOverlay(overlay, level: .aboveLabels) // Above everything ``` ### Gradient Polyline ```swift let renderer = MKGradientPolylineRenderer(polyline: polyline) renderer.setColors([.green, .yellow, .red], locations: [0.0, 0.5, 1.0]) renderer.lineWidth = 6 ``` --- ## Part 10: Map Snapshots Generate static map images for sharing, thumbnails, or offline display: ```swift let options = MKMapSnapshotter.Options() options.region = MKCoordinateRegion( center: coordinate, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) ) options.size = CGSize(width: 300, height: 200) options.scale = UIScreen.main.scale // Retina support options.mapType = .standard options.showsBuildings = true options.pointOfInterestFilter = .excludingAll // Clean map let snapshotter = MKMapSnapshotter(options: options) let snapshot = try await snapshotter.start() let image = snapshot.image // Draw custom annotations on snapshot UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale) image.draw(at: .zero) let pinImage = UIImage(systemName: "mappin.circle.fill")! let point = snapshot.point(for: coordinate) pinImage.draw(at: CGPoint( x: point.x - pinImage.size.width / 2, y: point.y - pinImage.size.height )) let finalImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() ``` #### Snapshot Coordinate Conversion ```swift // Convert coordinate to point in snapshot image let point = snapshot.point(for: coordinate) // Check if coordinate is in snapshot bounds let isVisible = CGRect(origin: .zero, size: snapshot.image.size).contains(point) ``` --- ## Part 11: iOS Version Feature Matrix | Feature | iOS Version | |---|---| | MKMapView | 3.0+ | | MKLocalSearch | 6.1+ | | MKDirections | 7.0+ | | MKMarkerAnnotationView | 11.0+ | | MKMapSnapshotter | 7.0+ | | MKLookAroundSceneRequest | 16.0+ | | LookAroundPreview (SwiftUI) | 17.0+ | | SwiftUI Map (content builder) | 17.0+ | | MapCameraPosition | 17.0+ | | .mapSelection | 17.0+ | | .mapCameraKeyframeAnimator | 17.0+ | | .onMapCameraChange | 17.0+ | | MapUserLocationButton | 17.0+ | | MapCompass | 17.0+ | | MapScaleView | 17.0+ | | .mapInteractionModes | 17.0+ | | MKLocalSearch.ResultType.query | 18.0+ | | GeoToolbox / PlaceDescriptor | 26.0+ | | MKGeocodingRequest | 26.0+ | | MKReverseGeocodingRequest | 26.0+ | | MKAddress | 26.0+ | --- ## Part 12: GeoToolbox and Geocoding ### GeoToolbox Framework `GeoToolbox` provides `PlaceDescriptor` — a standardized representation of physical locations that works across MapKit and third-party mapping services. ```swift import GeoToolbox // From address let fountain = PlaceDescriptor( representations: [.address("121-122 James's St \n Dublin 8 \n D08 ET27 \n Ireland")], commonName: "Obelisk Fountain" ) // From coordinates let tower = PlaceDescriptor( representations: [.coordinate(CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945))], commonName: "Eiffel Tower" ) // Multiple representations let statue = PlaceDescriptor( representations: [ .coordinate(CLLocationCoordinate2D(latitude: 40.6892, longitude: -74.0445)), .address("Liberty Island, New York, NY 10004, United States") ], commonName: "Statue of Liberty" ) // From MKMapItem let descriptor = PlaceDescriptor(item: mapItem) // Returns optional ``` ### PlaceRepresentation Enum representing a place using common mapping concepts: | Case | Usage | |---|---| | `.coordinate(CLLocationCoordinate2D)` | Latitude/longitude | | `.address(String)` | Full address string | Convenience accessors on `PlaceDescriptor`: ```swift descriptor.coordinate // CLLocationCoordinate2D? descriptor.address // String? descriptor.commonName // String? ``` ### SupportingPlaceRepresentation Proprietary identifiers for places from different mapping services: ```swift let place = PlaceDescriptor( representations: [.coordinate(CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278))], commonName: "London Eye", supportingRepresentations: [ .serviceIdentifiers([ "com.apple.maps": "AppleMapsID123", "com.google.maps": "GoogleMapsID456" ]) ] ) // Retrieve a specific service identifier let appleID = place.serviceIdentifier(for: "com.apple.maps") ``` ### MKGeocodingRequest — Forward Geocoding Convert an address string to map items (address to coordinates): ```swift guard let request = MKGeocodingRequest(addressString: "1 Apple Park Way, Cupertino, CA") else { return } let mapItems = try await request.mapItems ``` ### MKReverseGeocodingRequest — Reverse Geocoding Convert coordinates to map items (coordinates to address): ```swift let location = CLLocation(latitude: 37.3349, longitude: -122.0090) guard let request = MKReverseGeocodingRequest(location: location) else { return } let mapItems = try await request.mapItems ``` ### MKAddress Structured address type used when creating `MKMapItem` from a `PlaceDescriptor`: ```swift if let coordinate = descriptor.coordinate { let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) let address = MKAddress() let mapItem = MKMapItem(location: location, address: address) } ``` ### Geocoding vs MKLocalSearch | Need | Use | |---|---| | Address string to coordinates | `MKGeocodingRequest` | | Coordinates to address | `MKReverseGeocodingRequest` | | Natural language place search | `MKLocalSearch` | | Autocomplete suggestions | `MKLocalSearchCompleter` | | Cross-service place identifiers | `PlaceDescriptor` with `SupportingPlaceRepresentation` | --- ## Resources **WWDC**: 2023-10043, 2024-10094 **Docs**: /mapkit, /mapkit/map, /mapkit/mklocalsearch, /mapkit/mkdirections, /geotoolbox, /geotoolbox/placedescriptor, /mapkit/mkgeocodingrequest, /mapkit/mkreversegeocodingrequest, /mapkit/mkaddress **Skills**: mapkit, mapkit-diag, core-location-ref