Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Extended patterns for MapKit on iOS 17+ with SwiftUI. Import MapKit and
SwiftUI in every file that uses these APIs.
import MapKit
import SwiftUIA production-ready map view with markers, user location, and controls.
struct StoreLocatorMap: View {
let stores: [Store]
@State private var position: MapCameraPosition = .automatic
@State private var selectedStore: Store?
var body: some View {
Map(position: $position, selection: $selectedStore) {
UserAnnotation()
ForEach(stores) { store in
Marker(store.name, systemImage: "storefront",
coordinate: store.coordinate)
.tint(store.isOpen ? .green : .gray)
.tag(store)
}
}
.mapStyle(.standard(pointsOfInterest: .excludingAll))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
MapPitchToggle()
}
.safeAreaInset(edge: .bottom) {
if let store = selectedStore {
StoreDetailCard(store: store)
.padding()
}
}
}
}Conform the data model to Hashable for use with map selection:
struct Store: Identifiable, Hashable {
let id: UUID
let name: String
let coordinate: CLLocationCoordinate2D
let isOpen: Bool
static func == (lhs: Store, rhs: Store) -> Bool { lhs.id == rhs.id }
func hash(into hasher: inout Hasher) { hasher.combine(id) }
}Use Annotation for fully custom SwiftUI content at a coordinate. Prefer
Marker for standard pins because it handles clustering and accessibility
automatically.
Map {
ForEach(friends) { friend in
Annotation(friend.name, coordinate: friend.coordinate, anchor: .bottom) {
VStack(spacing: 0) {
AsyncImage(url: friend.avatarURL) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.gray
}
.frame(width: 40, height: 40)
.clipShape(.circle)
.overlay(Circle().stroke(.white, lineWidth: 2))
Image(systemName: "triangle.fill")
.font(.caption2)
.foregroundStyle(.white)
.rotationEffect(.degrees(180))
.offset(y: -3)
}
}
}
}Annotation(place.name, coordinate: place.coordinate, anchor: .bottom) {
VStack(spacing: 2) {
Text(place.name)
.font(.caption)
.fontWeight(.semibold)
.padding(.horizontal)
.padding(.vertical)
.background(.ultraThinMaterial, in: .capsule)
Image(systemName: "mappin.circle.fill")
.font(.title)
.foregroundStyle(.red)
}
}Wrap position updates in withAnimation for smooth transitions:
func flyTo(_ coordinate: CLLocationCoordinate2D) {
withAnimation(.easeInOut(duration: 1.0)) {
position = .camera(
MapCamera(centerCoordinate: coordinate, distance: 2000,
heading: 0, pitch: 45)
)
}
}func frameAllStores() {
withAnimation {
position = .automatic // Frames all map content
}
}
// Or frame a specific rect
func frameRegion(_ region: MKCoordinateRegion) {
withAnimation {
position = .region(region)
}
}Use onMapCameraChange to observe what the user is looking at:
@State private var visibleRegion: MKCoordinateRegion?
Map(position: $position) { ... }
.onMapCameraChange(frequency: .onEnd) { context in
visibleRegion = context.region
}frequency: .onEnd fires after the user finishes scrolling. Use
.continuous only when you need live tracking (costs more CPU).
Use MKMapItem as the selection type when you want to look up place details:
@State private var selectedItem: MKMapItem?
Map(selection: $selectedItem) {
ForEach(searchResults) { result in
Marker(item: result)
}
}
.onChange(of: selectedItem) { _, newItem in
guard let item = newItem else { return }
Task { await fetchLookAround(for: item) }
}@State private var selectedPlaceID: Place.ID?
Map(selection: $selectedPlaceID) {
ForEach(places) { place in
Marker(place.name, coordinate: place.coordinate)
.tag(place.id)
}
}Full pattern: completer feeds suggestions, selecting a suggestion triggers a
full search that returns MKMapItem results.
@Observable
final class MapSearchService: NSObject, MKLocalSearchCompleterDelegate {
var completions: [MKLocalSearchCompletion] = []
var searchResults: [MKMapItem] = []
var queryFragment: String = "" {
didSet { completer.queryFragment = queryFragment }
}
private let completer: MKLocalSearchCompleter
override init() {
completer = MKLocalSearchCompleter()
super.init()
completer.delegate = self
completer.resultTypes = [.address, .pointOfInterest]
}
// Restrict suggestions to the visible map region
func updateRegion(_ region: MKCoordinateRegion) {
completer.region = region
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
completions = completer.results
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
completions = []
}
func select(_ completion: MKLocalSearchCompletion) async {
let request = MKLocalSearch.Request(completion: completion)
request.resultTypes = [.pointOfInterest, .address]
do {
let response = try await MKLocalSearch(request: request).start()
searchResults = response.mapItems
} catch {
searchResults = []
}
}
}struct MapSearchView: View {
@State private var searchService = MapSearchService()
@State private var searchText = ""
@State private var position: MapCameraPosition = .automatic
var body: some View {
Map(position: $position) {
ForEach(searchService.searchResults, id: \.self) { item in
Marker(item: item)
}
}
.searchable(text: $searchText, prompt: "Search places")
.searchSuggestions {
ForEach(searchService.completions, id: \.self) { completion in
Button {
Task { await searchService.select(completion) }
} label: {
VStack(alignment: .leading) {
Text(completion.title)
Text(completion.subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
.task(id: searchText) {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
searchService.queryFragment = searchText
}
.onMapCameraChange(frequency: .onEnd) { context in
searchService.updateRegion(context.region)
}
}
}Calculate directions and draw the route polyline on the map.
struct DirectionsMapView: View {
let source: MKMapItem
let destination: MKMapItem
@State private var route: MKRoute?
@State private var position: MapCameraPosition = .automatic
@State private var travelTime: String = ""
var body: some View {
Map(position: $position) {
if let route {
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 6)
}
Marker(item: source)
.tint(.green)
Marker(item: destination)
.tint(.red)
}
.overlay(alignment: .top) {
if !travelTime.isEmpty {
Text(travelTime)
.font(.caption)
.padding()
.background(.ultraThinMaterial, in: .capsule)
.padding(.top)
}
}
.task { await calculateRoute() }
}
private func calculateRoute() async {
let request = MKDirections.Request()
request.source = source
request.destination = destination
request.transportType = .automobile
do {
let response = try await MKDirections(request: request).calculate()
route = response.routes.first
if let route {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute]
travelTime = formatter.string(from: route.expectedTravelTime) ?? ""
withAnimation {
position = .rect(route.polyline.boundingMapRect)
}
}
} catch {
print("Directions error: \(error.localizedDescription)")
}
}
}request.requestsAlternateRoutes = true
let response = try await MKDirections(request: request).calculate()
// response.routes contains multiple route options
// Display all routes, highlight the selected one:
ForEach(Array(response.routes.enumerated()), id: \.offset) { index, route in
MapPolyline(route.polyline)
.stroke(index == 0 ? .blue : .gray.opacity(0.5), lineWidth: index == 0 ? 6 : 3)
}Show Apple's street-level imagery for a selected location. Availability depends on region coverage.
struct LookAroundView: View {
let mapItem: MKMapItem
@State private var scene: MKLookAroundScene?
var body: some View {
Group {
if let scene {
LookAroundPreview(scene: .constant(scene))
.frame(height: 200)
.clipShape(.rect(cornerRadius: 12))
} else {
ContentUnavailableView("No Look Around",
systemImage: "eye.slash",
description: Text("Look Around is not available here."))
}
}
.task(id: mapItem) {
scene = nil
let request = MKLookAroundSceneRequest(mapItem: mapItem)
scene = try? await request.scene
}
}
}Map(selection: $selectedItem) { ... }
.overlay(alignment: .bottomTrailing) {
if lookAroundScene != nil {
LookAroundPreview(scene: $lookAroundScene)
.frame(width: 200, height: 130)
.clipShape(.rect(cornerRadius: 10))
.padding()
}
}
.onChange(of: selectedItem) { _, newItem in
guard let item = newItem else {
lookAroundScene = nil
return
}
Task {
let request = MKLookAroundSceneRequest(mapItem: item)
lookAroundScene = try? await request.scene
}
}Generate a static image of a map region. Useful for share sheets, widgets, notifications, or thumbnails.
func generateMapSnapshot(center: CLLocationCoordinate2D,
size: CGSize = CGSize(width: 300, height: 200)) async throws -> UIImage {
let options = MKMapSnapshotter.Options()
options.region = MKCoordinateRegion(
center: center,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
options.size = size
options.mapType = .standard
options.showsBuildings = true
let snapshotter = MKMapSnapshotter(options: options)
let snapshot = try await snapshotter.start()
// Draw a pin at the center
let image = UIGraphicsImageRenderer(size: size).image { context in
snapshot.image.draw(at: .zero)
let point = snapshot.point(for: center)
let pin = UIImage(systemName: "mappin.circle.fill")?
.withTintColor(.red, renderingMode: .alwaysOriginal)
pin?.draw(at: CGPoint(x: point.x - 15, y: point.y - 30),
blendMode: .normal, alpha: 1.0)
}
return image
}MapKit clusters Marker views automatically when they overlap. Control the
clustering priority and displayed count.
Map {
ForEach(allStores) { store in
Marker(store.name, systemImage: "cart", coordinate: store.coordinate)
.annotationTitles(.hidden) // Hide titles at low zoom
}
}
.mapStyle(.standard(pointsOfInterest: .excludingAll))For custom clustering behavior with MKMapView (UIKit interop), set
clusteringIdentifier on MKMarkerAnnotationView. The SwiftUI Map
handles basic clustering automatically.
let item = MKMapItem(placemark: MKPlacemark(coordinate: coordinate))
item.name = "Destination"
item.openInMaps(launchOptions: [
MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving
])func distanceBetween(_ a: CLLocationCoordinate2D,
_ b: CLLocationCoordinate2D) -> CLLocationDistance {
let locA = CLLocation(latitude: a.latitude, longitude: a.longitude)
let locB = CLLocation(latitude: b.latitude, longitude: b.longitude)
return locA.distance(from: locB) // meters
}let formatter = MKDistanceFormatter()
formatter.unitStyle = .abbreviated
let text = formatter.string(fromDistance: 1500) // "0.9 mi" or "1.5 km"Convert an address string to map items with richer data than CLGeocoder:
@available(iOS 26, *)
func geocodeAddresses(_ addresses: [String]) async -> [MKMapItem] {
var items: [MKMapItem] = []
for address in addresses {
let request = MKGeocodingRequest(address: address)
if let mapItems = try? await request.mapItems {
items.append(contentsOf: mapItems)
}
}
return items
}Convert coordinates to map items with MKAddress:
@available(iOS 26, *)
func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async -> MKAddress? {
let location = CLLocation(latitude: coordinate.latitude,
longitude: coordinate.longitude)
guard let request = MKReverseGeocodingRequest(location: location) else {
return nil
}
let mapItems = try? await request.mapItems
return mapItems?.first?.address
}MKAddress provides structured address components. Use
MKAddressRepresentations to format addresses for different contexts:
@available(iOS 26, *)
func formatAddress(_ address: MKAddress) -> String {
// Use address representations for locale-aware formatting
return address.representations.fullAddress()
}Create place references from coordinates when you do not have a Place ID:
@available(iOS 26, *)
import GeoToolbox
let descriptor = PlaceDescriptor(
representations: [.coordinate(myCoordinate)],
commonName: "My Favorite Cafe"
)
let request = MKMapItemRequest(placeDescriptor: descriptor)
let mapItem = try await request.mapItem
// Use mapItem with any MapKit API: Marker(item:), directions, place cards@available(iOS 26, *)
func cyclingRoute(to destination: MKMapItem) async throws -> MKRoute? {
let request = MKDirections.Request()
request.source = .forCurrentLocation()
request.destination = destination
request.transportType = .cycling
let response = try await MKDirections(request: request).calculate()
return response.routes.first
}Show the user's position with the built-in blue dot:
Map(position: $position) {
UserAnnotation() // Blue dot with accuracy ring
// ... other content
}
.mapControls {
MapUserLocationButton() // Button to re-center on user
}UserAnnotation() requires location authorization. If authorization is
denied, the annotation does not appear and no error is thrown.
When embedding a Map inside a ScrollView, disable map gestures that
conflict with scrolling:
ScrollView {
Map(position: $position, interactionModes: []) {
Marker("Location", coordinate: coord)
}
.frame(height: 200)
.clipShape(.rect(cornerRadius: 12))
Text("Details below the map...")
}Use interactionModes: [] for a fully static map thumbnail or
interactionModes: [.zoom] to allow pinch-to-zoom without pan conflicts.
func regionForCoordinates(_ coords: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
guard !coords.isEmpty else {
return MKCoordinateRegion()
}
var minLat = coords[0].latitude
var maxLat = coords[0].latitude
var minLon = coords[0].longitude
var maxLon = coords[0].longitude
for coord in coords {
minLat = min(minLat, coord.latitude)
maxLat = max(maxLat, coord.latitude)
minLon = min(minLon, coord.longitude)
maxLon = max(maxLon, coord.longitude)
}
let center = CLLocationCoordinate2D(
latitude: (minLat + maxLat) / 2,
longitude: (minLon + maxLon) / 2
)
let span = MKCoordinateSpan(
latitudeDelta: (maxLat - minLat) * 1.3, // 30% padding
longitudeDelta: (maxLon - minLon) * 1.3
)
return MKCoordinateRegion(center: center, span: span)
}CLLocationCoordinate2D does not conform to Equatable by default.
Extend it when needed for comparisons:
extension CLLocationCoordinate2D: @retroactive Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
}Note: Use @retroactive (Swift 5.10+) to silence the warning about
conforming types from other modules.
Marker views include built-in VoiceOver support using the title string.
Add .accessibilityLabel for richer descriptions:
Marker(store.name, coordinate: store.coordinate)
.accessibilityLabel("\(store.name), \(store.distanceText) away")Add a concise description of the map purpose:
Map { ... }
.accessibilityElement()
.accessibilityLabel("Store locations map showing \(stores.count) stores")skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references