Overview
MapKit has three search paths: MKLocalSearch for place and point-of-interest queries, MKLocalSearchCompleter for autocomplete suggestions, and geocoding requests that convert between addresses and coordinates. Results are MKMapItem values that feed Marker(item:) (mapkit) and the camera (mapkit-camera).
Scope MKLocalSearch to the visible region and result type
Set region to the map’s current region, restrict resultTypes, and add a pointOfInterestFilter for category screens. On iOS 18 and later, regionPriority = .required rejects results outside the region, while .default treats the region as a ranking hint.
func search(_ query: String, in region: MKCoordinateRegion) async throws -> [MKMapItem] {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = region
request.resultTypes = [.pointOfInterest, .address]
request.pointOfInterestFilter = MKPointOfInterestFilter(including: [.restaurant, .cafe])
return try await MKLocalSearch(request: request).start().mapItems
}Refresh the region from .onMapCameraChange(frequency: .onEnd) instead of hard-coding one.
Drive autocomplete with MKLocalSearchCompleter
The completer produces suggestion strings as queryFragment changes. Implement MKLocalSearchCompleterDelegate on an @Observable model and publish completer.results, clearing them in completer(_:didFailWithError:).
@Observable final class SearchModel: NSObject, MKLocalSearchCompleterDelegate {
var completions: [MKLocalSearchCompletion] = []
private let completer = MKLocalSearchCompleter()
override init() {
super.init()
completer.delegate = self
completer.resultTypes = [.pointOfInterest, .query]
}
func update(_ text: String, region: MKCoordinateRegion) {
completer.region = region
completer.queryFragment = text
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
completions = completer.results
}
}Resolve a completion before showing it
An MKLocalSearchCompletion is display text, not a coordinate. Run MKLocalSearch(request: .init(completion: completion)).start() and use the first mapItem before placing a marker or moving the camera. Show a progress indicator for the round trip.
Cancel the previous search when the query changes
Keep the active MKLocalSearch and call cancel() before starting the next, so a slow response cannot overwrite a newer one. Clear the results when the query is empty.
Geocode with MapKit requests instead of CLGeocoder
CLGeocoder is deprecated in iOS 26 and CLPlacemark is soft-deprecated. Use MKGeocodingRequest(addressString:) for address to coordinate and MKReverseGeocodingRequest(location:) for coordinate to address; both return MKMapItem values from mapItems. Both initializers are failable (the reverse one returns nil for an invalid location), so unwrap before awaiting.
let item = try await MKGeocodingRequest(addressString: "1 Ferry Building, San Francisco")?.mapItems.firstMKMapItem.placemark is deprecated in iOS 26 in favor of location, address, and addressRepresentations. MKAddress and MKAddressRepresentations give formatted display strings (fullAddress, shortAddress, cityWithContext), not structured postal fields, so store the fields you need yourself. Cache geocoding results in swiftdata when you resolve the same addresses repeatedly; each request is a network call. Check the availability of the MapKit geocoding types against your deployment target, and keep CLGeocoder behind an availability check for older versions.
Resolve external places with PlaceDescriptor
The GeoToolbox framework’s PlaceDescriptor describes a place from a coordinate, an address, or service identifiers, for data that has no MapKit place ID (a CRM record, a third-party API). Resolve it with MKMapItemRequest(placeDescriptor:), and convert back with PlaceDescriptor(mapItem:).