WWDC Quick Look đź’“ By SwiftGGTeam
Go further with MapKit

Go further with MapKit

Watch original video

Highlight

In 2025 MapKit ships PlaceDescriptor, moves geocoding from CoreLocation into MapKit, adds cycling directions, and brings Look Around to MapKit JS and watchOS.


Core content

MapKit has had an awkward problem. Your place data comes from a CRM, your own backend, or a third-party service, but MapKit only recognizes the Place ID it shipped last year. Without a Place ID you cannot reach Apple Maps’ rich data — name, icon, hours, address. Developers had two choices: run an MKLocalSearch first to map the third-party data back to a MapKit Place ID (which can return several results and still needs a human to pick), or skip the rich data and draw a bare Marker on the map.

This session at WWDC25 removes that glue layer. Apple ships PlaceDescriptor in a new framework called GeoToolbox. You describe a place with a coordinate, an address, or a service identifier — or several at once — and hand it to MKMapItemRequest to get back the matching MKMapItem. At the same time, CLGeocoder is deprecated. Geocoding moves to MKReverseGeocodingRequest and MKGeocodingRequest, and the returned MKMapItem carries the new MKAddress and MKAddressRepresentations so you no longer hand-format address strings. Cycling joins the list of transportType values, more than 20 MapKit APIs land on watchOS for the first time, and Look Around comes to MapKit JS. One session covers every key piece of Apple Maps’ cross-platform story.


Details

PlaceDescriptor: use what you already have to reach Apple Maps’ rich data

The most common case: you only have a coordinate and a name, but you want Apple Maps’ rich data. Drawing a Marker from a coordinate in SwiftUI Map is easy, but it is an empty Marker with no Apple Maps data behind it (04:49):

// Putting Marker on the Map with a coordinate

let annaLiviaCoordinates = CLLocationCoordinate2D(
    latitude: 53.347673,
    longitude: -6.290198
)
var body: some View {
    Map {
       Marker(
            "Anna Livia Fountain",
            coordinate: annaLiviaCoordinates
        )
    }
}

Key points:

  • CLLocationCoordinate2D is just a latitude/longitude pair. It carries nothing tying it to Apple Maps data.
  • The pin Marker draws from this coordinate uses the string you pass as its name. It has no Apple Maps icon, no color, and no rich data on tap.

PlaceDescriptor packages what you have into a structured description (05:07):

// Creating and resolving a PlaceDescriptor with coordinate PlaceRepresentation

import GeoToolbox
import MapKit

let annaLiviaCoordinates = CLLocationCoordinate2D(
    latitude: 53.347673,
    longitude: -6.290198
)
let annaLiviaDescriptor =  PlaceDescriptor(
    representations: [.coordinate(annaLiviaCoordinates)],
    commonName: "Anna Livia Fountain"
)

let request = MKMapItemRequest(placeDescriptor: annaLiviaDescriptor)
do {
    annaLiviaMapItem = try await request.mapItem
} catch {
    print("Error resolving placeDescriptor: \(error)")
}

Key points:

  • import GeoToolbox pulls in the new framework where PlaceDescriptor is defined.
  • The representations array is ordered from most precise to least, and can mix .coordinate, .address, and .deviceLocation.
  • commonName is the public name of the place (such as Guggenheim Museum). Do not put private user data in it (such as “Mom’s house”). It does not help MapKit find the place on its own and is mainly for display.
  • MKMapItemRequest(placeDescriptor:) turns the descriptor into a request, and try await request.mapItem returns a single MKMapItem asynchronously — unlike MKLocalSearch, this returns one best match.

An address alone works too (05:56):

let address = "121-122 James's St, Dublin 8"
let descriptor =  PlaceDescriptor(
    representations: [.address(address)],
    commonName: "Obelisk Fountain"
)
let request = MKMapItemRequest(placeDescriptor: descriptor)
obeliskFountain = try await request.mapItem

Key points:

  • .address(_:) uses a full postal address as the representation. The more complete the address, the better the match.
  • The call shape is the same as the coordinate version. Only the representation type changes — which shows that the representations array is the core abstraction of PlaceDescriptor.

For cross-service cases, supportingRepresentations can carry identifiers from several services at once (06:45):

let identifiers = ["com.apple.MapKit" : "ICBB5FD7684CE949"]
let annaLiviaDescriptor =  PlaceDescriptor(
    representations: [.coordinate(annaLiviaCoordinates)],
    commonName: "Anna Livia Fountain",
    supportingRepresentations: [.serviceIdentifiers(identifiers)]
)

Key points:

  • .serviceIdentifiers(_:) takes a dictionary. The key is a service’s bundle identifier and the value is that service’s ID for the place.
  • If the dictionary contains com.apple.MapKit, MKMapItemRequest uses it first; if that fails it falls back to the coordinate or address in representations.
  • This matters for App Intents: when you hand a PlaceDescriptor to another app, that app may not use MapKit, but it can find an ID it knows in the dictionary.

Geocoding moves to MapKit

CLGeocoder is deprecated in iOS 26, and CLPlacemark is soft-deprecated (10:07). Reverse geocoding becomes (10:45):

import MapKit

let millCreekCoordinates = CLLocation(latitude: 39.042617, longitude: -94.587526)
if let request = MKReverseGeocodingRequest(location: millCreekCoordinates) {
    do {
        let mapItems = try await request.mapItems
        millCreekMapItem = mapItems.first
    } catch {
        print("Error reverse geocoding location: \(error)")
    }
}

Key points:

  • The MKReverseGeocodingRequest(location:) initializer is failable — pass an invalid coordinate and it returns nil instead of throwing at runtime.
  • request.mapItems returns an array. The docs say it almost always has one entry, so .first is enough.
  • The returned MKMapItem only carries address-point information, not POI rich data. Do not expect it to tell you this is a coffee shop; it tells you which street and number.

For forward geocoding, MKGeocodingRequest turns an address into an MKMapItem in one step (13:50):

let request = MKGeocodingRequest(
    addressString: "1 Ferry Building, San Francisco"
)
mapItem = try await request?.mapItems.first

The returned MKMapItem has an address: MKAddress? with fullAddress and shortAddress string properties. Its addressRepresentations: MKAddressRepresentations? offers more display strategies, such as fullAddress(includingRegion: false) to drop the country, or cityWithContext which decides on its own whether to add the state or the country (Los Angeles, California vs. Paris, France). MapKit picks the form by looking at the address itself and the device’s locale.

Cycling routes plus Look Around across platforms

Cycling needs only a different transportType (16:06):

let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = selectedItem
request.transportType = .cycling
let directions = MKDirections(request: request)
let response = try await directions.calculate()
returnedRoutes = response.routes

Key points:

  • .cycling is a new transport type, on the same level as .walking and .automobile.
  • Cycling routes use small streets and greenways that driving cannot, and skip roads that are not cycling-friendly.
  • The same request shape works in MapKit JS through mapkit.Directions.Transport.Cycling.
  • The watchOS 26 SDK adds 20+ MapKit APIs, so cycling routes can finally be computed on the watch itself.

Look Around in MapKit JS (18:10):

const placeLookup = new mapkit.PlaceLookup();
const place = await new Promise(
    resolve => placeLookup.getPlace(
        "IBE1F65094A7A13B1",
        (error, result) => resolve(result)
    )
);

const lookAround = new mapkit.LookAround(
    document.getElementById("container"),
    place,
    options
);

Key points:

  • mapkit.PlaceLookup().getPlace(id, callback) fetches a place object by Place ID, callback-style.
  • new mapkit.LookAround(container, place, options) injects an interactive street view into the given DOM element.
  • options supports openDialog (load straight into full screen), showsDialogControl (a button to enter and leave full screen), and showsCloseControl (a close button).
  • For a static preview, use mapkit.LookAroundPreview. It does not drag, and tapping it opens the full-screen interactive view.

Takeaways

  • What to do: switch every page in your app that hand-rolls “coordinate / address + custom Marker” over to PlaceDescriptor.

    • Why it pays off: today your Marker has no Apple Maps icon, no hours, no phone number, no website link. Tapping it shows the user nothing. Resolve an MKMapItem through MKMapItemRequest(placeDescriptor:) and draw the Marker from that, and all of this data appears for free. Add .mapItemDetailSelectionAccessory(.callout) and you get the official Place Card on tap.
    • How to start: find every spot in the app that uses Marker(_:, coordinate:) or Marker(_:, location:), and add a data-layer method that turns (coordinate, name) into a PlaceDescriptor and resolves it to an MKMapItem. Try it on one low-risk page first and compare user dwell time before and after.
  • What to do: plan the CLGeocoder -> MKReverseGeocodingRequest migration, especially for Chinese addresses.

    • Why it pays off: CLGeocoder is deprecated and a future iOS will remove it; the migration cost is manageable now. The more practical reason is that MKAddressRepresentations is far cleaner than hand-stitching CLPlacemark strings. A case like “in a list of same-province addresses, drop the province” used to take a pile of if-else; now .fullAddress(includingRegion: false) does it in one line.
    • How to start: pick a page where the user sees the address directly (a shipping address on an order, a check-in record), run the old and new APIs side by side, and compare the order of province/city/district and the simplified-vs-traditional rendering for Chinese addresses. If the differences are acceptable, roll it out.
  • What to do: add cycling routes as a quick win.

    • Why it pays off: the API shape is almost the same as driving — change one line to transportType = .cycling — but the experience for cycling users improves a lot. Cycling routes take small roads, avoid unsafe segments, and produce more accurate ETAs. MapKit on Apple Watch picked up the same ability, and looking at a watch is more natural than pulling out a phone while riding.
    • How to start: add a Cycling tab to your existing route planning UI; on watchOS 26, build a small standalone widget that shows “distance + ETA for the next leg”.
  • What to do: replace Google Street View iframes with MapKit JS Look Around.

    • Why it pays off: real-estate, travel, and dining web projects have been locked into Street View for years, and the UI style does not match the brand. Look Around offers a 360-degree interactive view, takes options like openDialog, and exposes event callbacks so you can plug in your own loading animation and error UI.
    • How to start: use mapkit.LookAroundPreview for a static preview on the listing card, then switch to a full-screen LookAround on tap. Listen for the error event so you have a fallback UI ready (many regions still have no Look Around coverage).

  • What’s new in MapKit — A full list of MapKit updates in iOS 26, including PlaceDescriptor, Geocoding, and cycling routes.
  • What’s new in watchOS — watchOS 26 adds 20+ MapKit APIs; cycling routes and on-device map navigation are natively available on the watch for the first time.
  • What’s new in watchOS 26 — Developer-facing updates around watchOS 26, covering maps, fitness, and workout scenarios.
  • What’s new in Wallet — Updates to how Wallet interacts with locations and venues, paired with the unified URL scheme used by MapKit Place Card.

Comments

GitHub Issues · utterances