7.4 — MapKit & CoreLocation
Opening scenario
The PM wants a “Find My”-style screen: live user location, a few annotations, the ability to draw a route. You start with Map { ... } in SwiftUI and hit a wall the moment the brief grows: clustering 10,000 pins, drawing a polyline, switching to a custom tile source, getting reverse-geocoded street addresses for an annotation. Welcome to MapKit + CoreLocation — twin frameworks deep enough to fill a year, accessible enough that the basics fit in a single chapter.
| Context | What it usually means |
|---|---|
| Reads “Map in SwiftUI” | Has built simple location views |
| Reads “MKMapView” | Has used UIKit MapKit for advanced features |
| Reads “geocoding” | Has converted between coordinates and addresses |
| Reads “CLLocationManager” | Has wrangled the permission dance |
| Reads “region monitoring / CLVisit” | Has built location-aware background features |
Concept → Why → How → Code
Concept
- CoreLocation owns the device’s location: GPS, Wi-Fi/cell triangulation, region monitoring, visit detection, motion-based activity.
- MapKit displays maps and annotations. SwiftUI’s
Mapis a thin layer overMKMapView; for complex needs, drop back to UIKitMKMapViewviaUIViewRepresentable.
Why
Native MapKit is free, integrates with Apple Maps for navigation handoffs, respects user privacy, and offers richer features (3D buildings, Look Around, Maps Server API for search) than most third-party SDKs without leaking user behavior.
How — CoreLocation permission
Add to Info.plist:
NSLocationWhenInUseUsageDescription— required for foreground access.NSLocationAlwaysAndWhenInUseUsageDescription— required if you ever want background.NSLocationTemporaryUsageDescriptionDictionary— for precise-location prompts in iOS 14+.
import CoreLocation
@Observable
@MainActor
final class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var lastLocation: CLLocation?
var authorization: CLAuthorizationStatus = .notDetermined
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
authorization = manager.authorizationStatus
}
func requestWhenInUse() { manager.requestWhenInUseAuthorization() }
func startUpdates() { manager.startUpdatingLocation() }
nonisolated func locationManagerDidChangeAuthorization(_ mgr: CLLocationManager) {
Task { @MainActor in
self.authorization = mgr.authorizationStatus
if self.authorization == .authorizedWhenInUse {
self.startUpdates()
}
}
}
nonisolated func locationManager(_ mgr: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
Task { @MainActor in self.lastLocation = location }
}
nonisolated func locationManager(_ mgr: CLLocationManager, didFailWithError error: Error) {
// Most failures are transient; log and ignore.
}
}
Precise vs reduced accuracy (iOS 14+)
If the user grants only “Approximate Location,” lastLocation is fuzzed to ~1km. For one-off precise reads:
manager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: "PreciseForRouteSearch")
PreciseForRouteSearch must exist as a key in NSLocationTemporaryUsageDescriptionDictionary.
SwiftUI Map (iOS 17+)
import MapKit
import SwiftUI
struct StoresMap: View {
@State private var position: MapCameraPosition = .automatic
let stores: [Store]
let user: CLLocation?
var body: some View {
Map(position: $position) {
UserAnnotation()
ForEach(stores) { store in
Marker(store.name, systemImage: "cup.and.saucer", coordinate: store.coordinate)
.tint(.brown)
}
if let user, let nearest = stores.min(by: { $0.distance(to: user) < $1.distance(to: user) }) {
MapPolyline(coordinates: [user.coordinate, nearest.coordinate])
.stroke(.blue, lineWidth: 4)
}
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapPitchToggle()
}
.safeAreaInset(edge: .bottom) {
StoreListSheet(stores: stores)
}
}
}
MapContentBuilder (the closure DSL) supports Marker, Annotation (custom view), MapCircle, MapPolyline, MapPolygon.
Custom annotation view
Annotation("HQ", coordinate: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.03)) {
VStack(spacing: 0) {
Image(systemName: "building.2.fill")
.padding(8)
.background(.tint.opacity(0.2), in: .circle)
Image(systemName: "arrowtriangle.down.fill")
.offset(y: -4)
}
}
Forward & reverse geocoding
let geocoder = CLGeocoder()
// Address → coordinate
let placemarks = try await geocoder.geocodeAddressString("1 Infinite Loop, Cupertino")
let coordinate = placemarks.first?.location?.coordinate
// Coordinate → address
let placemark = try await geocoder.reverseGeocodeLocation(location).first
let street = placemark?.thoroughfare ?? "—"
CLGeocoder is rate-limited by Apple (~one request per few seconds). For high-volume server-side geocoding, use Apple MapKit Server API or Google/Mapbox.
Region monitoring (geofencing)
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 40.78, longitude: -73.97),
radius: 100,
identifier: "central-park-entrance"
)
region.notifyOnEntry = true
region.notifyOnExit = true
manager.startMonitoring(for: region)
// Delegate
func locationManager(_ mgr: CLLocationManager, didEnterRegion region: CLRegion) {
// Fired even if app is killed (subject to OS budget)
}
iOS caps at 20 monitored regions per app. For more, swap them in/out as the user moves.
Significant location changes (battery-friendly background)
manager.startMonitoringSignificantLocationChanges()
Wakes the app every ~500m of movement. Far cheaper than continuous updates; perfect for crash-resilient location loggers.
CLVisit
manager.startMonitoringVisits()
func locationManager(_ mgr: CLLocationManager, didVisit visit: CLVisit) {
// Triggered when the user arrives/departs from a "significant" place
}
The OS does the inference: dwell time + radius. Lifelogging apps love this.
In the wild
- Apple Maps, Find My, Reminders (location-based) — all use MapKit + CoreLocation.
- Yelp, OpenTable — MapKit for restaurant pins, often layered with custom annotations.
- Strava, Nike Run Club — CoreLocation continuous high-accuracy in foreground, polyline rendering on MapKit.
- Citymapper — uses MapKit basemap with overlay routing computed server-side.
- Find My Friends — region monitoring + significant change for stalker-free location sharing.
Common misconceptions
- “
requestAlwaysAuthorizationworks immediately.” Apple now shows the “Always” prompt only after the user has been on “When In Use” for a while. Plan a two-step UX. - “Background updates work as long as my background mode is on.” iOS will kill your app eventually if it sits in the background only for location. Use significant-change or region monitoring for long-running needs.
- “
CLGeocodercan handle bulk addresses.” It’s rate-limited and intended for occasional UI use. Bulk geocoding belongs on a server. - “MapKit needs an API key.” It doesn’t on iOS. The server-side MapKit JS and MapKit Server API require a JWT, but the iOS framework is free with your Apple Developer Program membership.
- “
Map { }in SwiftUI can do everythingMKMapViewdoes.” SwiftUI’s map gets closer each year but still lacks fine-grained gestures, custom tile overlays, and some legacy delegate hooks. Drop toUIViewRepresentablefor those.
Seasoned engineer’s take
Location is the place teams overspend battery and underspend privacy review. The two best instincts:
- Always ask the question: “What’s the minimum accuracy and frequency that satisfies the feature?” A delivery app’s “ETA updating” feature does not need 1-meter precision every second; reduced accuracy + significant location changes are usually enough.
- The user is right when they grant “Approximate.” Don’t keep nagging for “Precise.” Build the feature to be useful with approximate; offer a one-tap temporary-precise upgrade for the moments that demand it (e.g., turning navigation).
On MapKit specifically: prefer Marker for known SF Symbols, drop to Annotation for custom UI, and don’t try to render 10K annotations in SwiftUI — MKMapView with MKClusterAnnotation still wins at scale.
TIP: Set
manager.pausesLocationUpdatesAutomatically = trueandmanager.activityType = .fitness(or your app’s activity) to let iOS smartly pause updates when the user is stationary. Apps that override these defaults blindly are why battery icons turn yellow.
WARNING: Never log raw CLLocation values to a third-party analytics service. Always bucket to ~1km or coarser. Apple’s privacy reviewers and your DPO will both care.
Interview corner
Junior: “How do you ask for location permission and read the current location?”
Add the
NSLocationWhenInUseUsageDescriptionInfo.plist key, create aCLLocationManager, set its delegate, callrequestWhenInUseAuthorization(), and start updates afterlocationManagerDidChangeAuthorizationconfirms authorized. Readlocations.lastindidUpdateLocations.
Mid: “How would you implement a low-battery geofencing feature for 50 stores in a city?”
iOS caps
startMonitoring(for:)at 20 regions per app. Compute the 20 closest stores to the user’s current significant-change region, monitor those. WhendidExitRegionfires, re-rank and update the monitored set. UsestartMonitoringSignificantLocationChangesinstead of continuous updates so the app wakes every ~500m, not every second.
Senior: “Design the location pipeline for a ride-sharing driver app where the server needs the driver’s position every 5 seconds with sub-10-meter accuracy.”
Foreground, high-accuracy CLLocationManager updates with
desiredAccuracy = kCLLocationAccuracyBest,distanceFilter = kCLDistanceFilterNone. Background: enable Location background mode, useallowsBackgroundLocationUpdates = true. Wrap updates in an actor that batches and sends every 5s rather than every update; gzip the payload. Heartbeat with a server keep-alive over WebSocket; on disconnect, queue updates locally for 60s then drop oldest. Battery hedge: whenCLActivityManagerreports stationary for >2min, drop to significant-change to save power; on motion resume, return to high-accuracy. Privacy: rotate the analytics ID per shift; obfuscate the precise lat/lng of pickup/dropoff in any non-active-trip context.
Red flag: “We just keep continuous location updates running in the background to be safe.”
Battery, App Review rejection, and a privacy violation in one sentence. Demonstrates the candidate doesn’t understand iOS’s background execution budget or the user-facing battery and privacy implications.
Lab preview
Lab 7.1 — Weather + Map app wires this chapter together: user location + MapKit annotations + WeatherKit overlay. You’ll see exactly how CLLocationManager, Map, and WeatherService cooperate in a real screen.
Next: 7.5 — HealthKit