This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
916 lines
25 KiB
Markdown
916 lines
25 KiB
Markdown
---
|
|
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
|