Building location-aware apps has never been easier—and I mean that sincerely. With MapKit for SwiftUI in iOS 26, you get a fully declarative way to embed interactive maps, drop markers and annotations, draw routes, and tap into the brand-new GeoToolbox framework. No more UIKit delegates or coordinator boilerplate.
This guide walks you through everything from your very first map view to the newest iOS 26 goodies—PlaceDescriptor, MKReverseGeocodingRequest, cycling directions, and Look Around. There's working code at every step, so you can follow along in Xcode.
Adding a Basic Map to Your SwiftUI App
Getting a map on screen is almost comically simple. Import MapKit, drop a Map view into your body, and you're done:
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
Map()
}
}
That's it. Two imports and one line of actual code. You get a full-screen, interactive map with pinch-to-zoom, rotation, and panning—all for free.
Controlling the Camera Position
Of course, most real apps need the map focused on a specific spot. That's where MapCameraPosition comes in. You can set an initial position or (more commonly) bind it for two-way tracking:
struct CameraMapView: View {
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
)
var body: some View {
Map(position: $position)
}
}
Because position is a binding, you can move the camera programmatically from anywhere—a search bar, a list selection, even a button tap:
Button("Go to Tokyo") {
position = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 35.6762, longitude: 139.6503),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)
)
}
Tracking Camera Changes
Want to react when the user pans or zooms? The onMapCameraChange modifier has you covered:
Map(position: $position)
.onMapCameraChange { context in
let center = context.region.center
print("New center: \(center.latitude), \(center.longitude)")
}
Setting Camera Bounds
If you need to keep users from scrolling to the other side of the planet (it happens), apply MapCameraBounds:
Map(position: $position, bounds: MapCameraBounds(
centerCoordinateBounds: MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
),
minimumDistance: 500,
maximumDistance: 50000
))
Adding Markers
Markers are those balloon-shaped pins you know from Apple Maps. They're the quickest way to highlight a location on your map:
struct MarkerMapView: View {
@State private var position: MapCameraPosition = .automatic
var body: some View {
Map(position: $position) {
Marker("Golden Gate Bridge",
systemImage: "bridge.fill",
coordinate: CLLocationCoordinate2D(latitude: 37.8199, longitude: -122.4783))
.tint(.orange)
Marker("Alcatraz Island",
coordinate: CLLocationCoordinate2D(latitude: 37.8267, longitude: -122.4230))
.tint(.red)
}
}
}
One nice touch: when you use .automatic for the camera position, MapKit frames the map so all your markers are visible. No manual region math needed.
Custom Annotations with SwiftUI Views
Sometimes the standard marker balloon just doesn't cut it. Maybe you want a branded icon, a live data badge, or something more creative. That's what Annotation is for—it lets you drop any SwiftUI view directly onto the map:
Map(position: $position) {
Annotation("Apple Park",
coordinate: CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)) {
VStack(spacing: 4) {
Image(systemName: "apple.logo")
.font(.title)
.foregroundStyle(.white)
.padding(8)
.background(.blue.gradient, in: Circle())
Text("Apple Park")
.font(.caption2)
.bold()
}
}
}
Honestly, this is one of my favorite parts of the SwiftUI map API. Animations, gestures, state-driven content—it all works, rendered live right on the map surface.
Map Overlays: Polylines, Polygons, and Circles
SwiftUI's Map view supports three overlay types through the MapContent protocol: MapPolyline, MapPolygon, and MapCircle. No delegate methods. No coordinator. Just declarative code.
Drawing a Polyline
let routeCoordinates = [
CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
CLLocationCoordinate2D(latitude: 37.7849, longitude: -122.4094),
CLLocationCoordinate2D(latitude: 37.7949, longitude: -122.4294),
CLLocationCoordinate2D(latitude: 37.8049, longitude: -122.4194)
]
Map {
MapPolyline(coordinates: routeCoordinates)
.stroke(.blue, lineWidth: 4)
}
Drawing a Polygon and Circle
Map {
MapPolygon(coordinates: parkBoundaryCoordinates)
.foregroundStyle(.green.opacity(0.3))
.stroke(.green, lineWidth: 2)
MapCircle(center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
radius: 500)
.foregroundStyle(.blue.opacity(0.2))
.stroke(.blue, lineWidth: 1)
}
Displaying Directions and Routes
This is where things get really practical. MapKit can calculate routes between locations and render them as polylines. You use MKDirections to request a route, then pass it straight to MapPolyline:
struct DirectionsMapView: View {
@State private var route: MKRoute?
@State private var position: MapCameraPosition = .automatic
let start = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
let end = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)
var body: some View {
Map(position: $position) {
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
Marker("Start", coordinate: start).tint(.green)
Marker("End", coordinate: end).tint(.red)
}
.task {
await calculateRoute()
}
}
func calculateRoute() async {
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: start))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: end))
request.transportType = .automobile
let directions = MKDirections(request: request)
if let response = try? await directions.calculate() {
route = response.routes.first
}
}
}
The key thing here: passing an MKRoute directly to MapPolyline means MapKit handles all the coordinate decoding. One line replaces the old delegate-based overlay rendering. That's a massive win.
Map Controls and Styles
MapKit ships with built-in controls you can add via the mapControls modifier:
Map(position: $position) {
// markers and overlays
}
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
MapPitchToggle()
}
And you can change the map's visual appearance with mapStyle. There are some really nice options here:
Map(position: $position)
.mapStyle(.imagery(elevation: .realistic)) // satellite with 3D terrain
Map(position: $position)
.mapStyle(.hybrid(elevation: .realistic, pointsOfInterest: .including([.restaurant, .cafe])))
Map(position: $position)
.mapStyle(.standard(emphasis: .muted)) // subdued colors for data overlays
The .muted emphasis is particularly useful when you're layering your own data on top of the map and don't want Apple's styling to compete with it.
Look Around Integration
Look Around is Apple's answer to street-level imagery, and it's surprisingly easy to embed in SwiftUI with LookAroundPreview:
struct LookAroundView: View {
@State private var scene: MKLookAroundScene?
let coordinate: CLLocationCoordinate2D
var body: some View {
VStack {
if let scene {
LookAroundPreview(scene: scene, allowsNavigation: true)
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 12))
} else {
ContentUnavailableView("No Look Around Available",
systemImage: "eye.slash",
description: Text("Street-level imagery isn't available here."))
}
}
.task {
await fetchScene()
}
}
func fetchScene() async {
let request = MKLookAroundSceneRequest(coordinate: coordinate)
scene = try? await request.scene
}
}
Set allowsNavigation to true so users can move freely through the imagery, or false to lock them at the specified coordinate. For a details screen, I'd usually go with true—it's a much more engaging experience.
New in iOS 26: GeoToolbox and PlaceDescriptor
So, let's dive into the big new stuff. iOS 26 introduces the GeoToolbox framework along with a new type called PlaceDescriptor. This is the modern way to reference places in MapKit, especially when you don't already have a MapKit Place ID on hand.
PlaceDescriptor lets you describe a place using structured info—a name, coordinates, or an address—and MapKit matches it against Apple Maps data to return a rich MKMapItem complete with phone numbers, websites, categories, and branded iconography.
import GeoToolbox
import MapKit
func findPlace() async throws -> MKMapItem {
let coordinates = CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945)
let descriptor = PlaceDescriptor(
representations: [.coordinate(coordinates)],
commonName: "Eiffel Tower"
)
let request = MKMapItemRequest(placeDescriptor: descriptor)
return try await request.mapItem
}
Quick note: the commonName should be a well-known public name—"Eiffel Tower", "Sydney Opera House", "Central Park". Private names like "Mom's House" won't help MapKit find anything useful.
Using PlaceDescriptor with Multiple Representations
You can supply multiple representations to increase the chance of a successful match. Think of it as giving MapKit more clues to work with:
let descriptor = PlaceDescriptor(
representations: [
.coordinate(CLLocationCoordinate2D(latitude: 40.7484, longitude: -73.9857)),
.address(AddressRepresentation(
street: "20 W 34th St",
city: "New York",
state: "NY",
country: "US"
))
],
commonName: "Empire State Building"
)
If a MapKit identifier is present, MKMapItemRequest tries that first. If it fails, it falls back to coordinates, addresses, or names. Pretty smart failover behavior.
Displaying the Result on a Map
struct PlaceDescriptorMapView: View {
@State private var mapItem: MKMapItem?
@State private var position: MapCameraPosition = .automatic
var body: some View {
Map(position: $position) {
if let mapItem {
Marker(item: mapItem)
}
}
.task {
do {
mapItem = try await findPlace()
} catch {
print("Failed to find place: \(error)")
}
}
}
}
When you pass the resolved MKMapItem to a Marker, it automatically picks up Apple Maps' branded name, colors, and iconography for that place. No extra styling work on your end.
New in iOS 26: Replacing CLGeocoder with MKReverseGeocodingRequest
Here's something you should know: CLGeocoder is deprecated in iOS 26. The replacement is MKReverseGeocodingRequest, and honestly, the new API is nicer to work with because it integrates directly with the MapKit place system:
import MapKit
import CoreLocation
func reverseGeocode(location: CLLocation) async throws -> MKMapItem {
guard let request = MKReverseGeocodingRequest(location: location) else {
throw GeocodingError.invalidCoordinates
}
return try await request.mapItem
}
enum GeocodingError: Error {
case invalidCoordinates
}
// Usage
let location = CLLocation(latitude: 51.5074, longitude: -0.1278)
let mapItem = try await reverseGeocode(location: location)
print(mapItem.name ?? "Unknown") // "Big Ben" or nearby place
print(mapItem.url ?? "No URL") // website if available
print(mapItem.phoneNumber ?? "N/A") // phone number if available
The initializer returns an optional—pass invalid coordinates and you get nil instead of a runtime crash. The returned MKMapItem includes all the rich data MapKit has for that location, which is way more than you ever got from CLPlacemark.
New in iOS 26: Cycling Directions
This one's been a long time coming. iOS 26 adds cycling as a transport type for directions, giving your users bike-friendly routes that account for bike lanes, elevation, and cycling-specific road access:
func calculateCyclingRoute(from start: CLLocationCoordinate2D,
to end: CLLocationCoordinate2D) async -> MKRoute? {
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: start))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: end))
request.transportType = .cycling
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
return response?.routes.first
}
You render cycling routes the same way as automobile routes using MapPolyline(route:). A nice pattern is to combine this with walking and driving routes so users can compare transport options right in your app.
Working with User Location
To show the user's current location on the map, add a UserAnnotation inside the map content and request location authorization:
struct UserLocationMapView: View {
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
var body: some View {
Map(position: $position) {
UserAnnotation()
}
.mapControls {
MapUserLocationButton()
}
}
}
Don't forget to add the NSLocationWhenInUseUsageDescription key to your Info.plist with a clear explanation of why your app needs location access. Without it, the authorization prompt won't show up and the location dot just silently won't appear. (I've been burned by this more than once.)
Selecting Map Items
MapKit in SwiftUI has built-in selection support. Just bind a state variable to track which marker the user tapped:
struct SelectableMapView: View {
@State private var position: MapCameraPosition = .automatic
@State private var selectedItem: MKMapItem?
let locations: [PointOfInterest]
var body: some View {
Map(position: $position, selection: $selectedItem) {
ForEach(locations) { poi in
Marker(poi.name, coordinate: poi.coordinate)
.tag(poi.mapItem)
}
}
.sheet(item: $selectedItem) { item in
PlaceDetailView(mapItem: item)
}
}
}
When the user taps a marker, selectedItem updates and you can present a detail sheet, navigate to another view, or update any related UI. Simple and reactive—exactly how SwiftUI should feel.
Putting It All Together
Let's wrap things up with a practical example that pulls together markers, route drawing, Look Around, and the new PlaceDescriptor API into a single view:
import SwiftUI
import MapKit
import GeoToolbox
struct CityExplorerView: View {
@State private var position: MapCameraPosition = .automatic
@State private var route: MKRoute?
@State private var lookAroundScene: MKLookAroundScene?
@State private var selectedPlace: MKMapItem?
@State private var places: [MKMapItem] = []
let cityCenter = CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522)
var body: some View {
VStack(spacing: 0) {
Map(position: $position, selection: $selectedPlace) {
ForEach(places, id: \.self) { place in
Marker(item: place)
}
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 4)
}
UserAnnotation()
}
.mapControls {
MapUserLocationButton()
MapCompass()
}
.mapStyle(.standard(pointsOfInterest: .including([.museum, .landmark])))
.onChange(of: selectedPlace) { _, newPlace in
guard let coordinate = newPlace?.placemark.coordinate else { return }
Task { await fetchLookAround(at: coordinate) }
}
if let lookAroundScene {
LookAroundPreview(scene: lookAroundScene, allowsNavigation: true)
.frame(height: 200)
}
}
.task {
await loadPlaces()
}
}
func loadPlaces() async {
let landmarks = [
("Eiffel Tower", 48.8584, 2.2945),
("Louvre Museum", 48.8606, 2.3376),
("Notre-Dame", 48.8530, 2.3499)
]
for (name, lat, lon) in landmarks {
let descriptor = PlaceDescriptor(
representations: [.coordinate(CLLocationCoordinate2D(latitude: lat, longitude: lon))],
commonName: name
)
let request = MKMapItemRequest(placeDescriptor: descriptor)
if let item = try? await request.mapItem {
places.append(item)
}
}
}
func fetchLookAround(at coordinate: CLLocationCoordinate2D) async {
let request = MKLookAroundSceneRequest(coordinate: coordinate)
lookAroundScene = try? await request.scene
}
}
Frequently Asked Questions
Do I still need UIViewRepresentable to use MKMapView in SwiftUI?
For most use cases, no. The SwiftUI Map view handles markers, annotations, overlays, directions, camera control, and Look Around natively. You only need to bridge to MKMapView via UIViewRepresentable for advanced stuff like custom tile overlays, precise overlay z-ordering, or satellite-only configurations that aren't yet exposed in the SwiftUI API.
What replaced CLGeocoder in iOS 26?
CLGeocoder is deprecated. Use MKMapItemRequest with a PlaceDescriptor for forward geocoding (finding a place from a name or address) and MKReverseGeocodingRequest for reverse geocoding (turning coordinates into a place). Both return rich MKMapItem objects with significantly more data than the old CLPlacemark results.
Can I request cycling directions in MapKit before iOS 26?
Nope. The .cycling transport type for MKDirections.Request is new in iOS 26. On earlier versions, you're limited to .automobile, .walking, or .transit. If your app supports older iOS versions, make sure to check availability before requesting cycling routes.
How do I handle the case where Look Around isn't available?
Not every location has Look Around coverage. MKLookAroundSceneRequest returns nil when no imagery exists for that spot. Always handle this gracefully—show a placeholder or a static map image instead of leaving a blank gap. The code example in the Look Around section above uses ContentUnavailableView for exactly this purpose.
What is the GeoToolbox framework and when should I use it?
GeoToolbox is a new framework in iOS 26 that provides PlaceDescriptor—a structured way to describe places using names, coordinates, and addresses. Use it when you have place data from external sources (a database, API, or user input) and need to resolve it into a rich MKMapItem with Apple Maps data like phone numbers, websites, and branded icons. It essentially replaces the old pattern of manual geocoding followed by manual map item creation.