With iOS 26, Apple deprecated CLGeocoder — the API developers have relied on since Core Location’s earliest days to turn a coordinate into a readable place: city, region, nearby point of interest. The replacement lives in MapKit: MKReverseGeocodingRequest.1
Let’s take one coordinate as an example, about 20 metres (60 feet) away from Cypress Mountain Resort’s main cabin, located in the city of West Vancouver, BC in Canada:
49.396840, -123.204116, located 20 metres from Cypress Mountain Resort, in West Vancouver, BC, Canada.Below is the code I ran both reverse-geocoding processes, side by side:
let location = CLLocation(latitude: 49.396840, longitude: -123.204116)
func geocoderLookup() async -> String {
let coder = CLGeocoder()
if let placemarks = try? await coder.reverseGeocodeLocation(location)
{
let output = placemarks.map({ mark in
var output = ""
output += (mark.areasOfInterest ?? []).joined(separator: " | ") + "\n"
output += (mark.subLocality ?? "") + "\n"
output += (mark.locality ?? "") + "\n"
output += (mark.subAdministrativeArea ?? "") + "\n"
output += (mark.administrativeArea ?? "") + "\n"
output += (mark.country ?? "") + "\n"
return output
}).joined(separator: "\n\n")
return output
} else {
return "Error"
}
}
func mapkitLookup() async -> String {
let request = MKReverseGeocodingRequest(location: location)
let items = (try? await request?.mapItems) ?? []
let output = items.map({ item in
var output = ""
output += "\(item.pointOfInterestCategory?.rawValue ?? "NA"): \(item.name ?? "No Name")" + "\n"
output += (item.address?.fullAddress ?? "") + "\n"
output += (item.address?.shortAddress ?? "") + "\n"
output += (item.addressRepresentations?.cityName ?? "") + "\n"
output += (item.addressRepresentations?.regionName ?? "") + "\n"
output += (item.addressRepresentations?.cityWithContext(.full) ?? "") + "\n"
output += (item.addressRepresentations?.cityWithContext(.short) ?? "") + "\n"
output += (item.addressRepresentations?.cityWithContext(.automatic) ?? "") + "\n"
output += (item.addressRepresentations?.fullAddress(includingRegion: true, singleLine: true) ?? "") + "\n"
return output
}).joined(separator: "\n\n")
return output
}
Here are the results, side by side:
| CLGeocoder | MapKit | |
|---|---|---|
| Point of Interest | a) Cypress Mountain b) Cypress Provincial Park |
Cypress Mountain |
| Neighbourhood | Cypress Park | 🫥 N/A |
| City | ✅ West Vancouver | ❌ Bowen Island |
| Area | ✅ Metro Vancouver | 🫥 N/A |
| Province | ✅ BC | ⚠️ (city), BC |
| Country | ✅ Canada | ✅ Canada |
| Postal Code | ❌ V0N 1G2 | ❌ V0N 1G2 |
First, about correctness
The most obvious points from the table above:
- Both APIs got the country right — it’s Canada.
- Both APIs got the postal code wrong — it’s a postal code located in Bowen Island, not in West Vancouver.
On all other fronts, CLGeocoder produces the correct results: The neighbourhood/sublocality of Cypress Mountain, the point of interest that is Cypress Provincial Park, the fact that this is the city of West Vancouver, located in Metro Vancouver, in BC, Canada.
MapKit named the neighbourhood, too, but it doesn’t know anything about it also being a provincial park. It placed the coordinate in the wrong city (the municipality of Bowen Island is an island across Howe Sound2, with no relationship to the mountain behind West Vancouver whatsoever), and it has no knowledge of the metropolitan area.
That’s not a formatting difference or a naming preference between two reasonable answers. It’s just wrong.
Then, beyond correctness
Beyond correctness, MapKit’s AddressRepresentations also don’t get me clean information that I can work with:
-
I cannot get separate names of the city or of the province (Americans: read state). All I could get is a city name on its own (Portland) or the readable city name with disambiguation (Portland, OR).
-
I simply cannot get neighbourhood and metropolitan information at all. Those are available as
subLocalityandsubAdministrativeAreafrom aCLPlacemarkobject. While as a Canadian, I don’t care about metropolitan as much since it has no impact on elections, sub-administrative area is more meaningful to Americans because this is where you would find your county names — the level between a city and a state. -
Not demonstrated above, I cannot find waterbodies — available in CLPlacemark in
.oceanand.inlandWater, respectively. -
CLPlacemark contains as many qualifying points of interests as it seems able. MapKit, however, doesn’t even return the provincial park as a POI.
So the MapKit replacement is not only inaccurate, it contains less information and is harder to work with.
A replacement that (still) isn’t ready
I don’t doubt Apple will get MapKit’s reverse geocoding sorted out eventually — it clearly has all the right data, since it named the point of interest correctly. But “eventually” isn’t “now,” and CLGeocoder having a deprecation warning on it doesn’t change that its output is the one you can trust today. Until MapKit stops making mistakes like this, I’m not switching, warning or not.
-
CLGeocoder’s documentation has a coy deprecation message of two words: “Use MapKit”. There’s no link to the docs of MapKit, and there is no example. The deprecation message is not even a complete sentence with a period. My cynical read is that whoever wrote this two-word deprecation message was too embarrassed to even write up a full sentence, or to furnish the deprecation with an example. ↩
-
Just for the reference, here are Bowen Island and West Vancouver on the same map: