MapSearch
MapSearch runs MapKit's on-device keyword search. Two entry points:
MapSearch.locate(options)— one-shot search, returnsMapItem[].MapSearch.createCompleter(options?)— stateful autocomplete, designed for text-input fields. Delivers suggestions via listener callbacks.
Both are pure query APIs and require no system permissions. Coordinates are
returned as MapCoordinate, so results plug straight into <Marker> /
<Map> from the views layer.
Forward / reverse geocoding (address ↔ coordinates) lives on the existing
Location namespace (Location.geocodeAddress, Location.reverseGeocode).
locate — one-shot search
Options
Result — MapItem
MapItem is a top-level opaque class (also returned by MapDirections). Read
fields by name; do not try to serialize the instance directly.
openInMaps(options?) — hand off to Apple Maps
Resolves with true when the system accepted the launch request. The current
app moves to the background while Apple Maps takes over.
JSON.stringify(item)andObject.keys(item)will not return the field dictionary —MapItemis a class with getters, not a plain object. Spread the fields yourself if you need a serializable snapshot.
Geometry helpers
Both delegate to MapUtils.distance / MapUtils.bearing.
MapItem.forCurrentLocation()
Apple's placeholder MapItem for "the device's current location". Synchronous,
no permission prompt, no coordinate fetched locally — Apple Maps interprets the
sentinel when handed to openInMaps().
Returned items satisfy isCurrentLocation === true.
Selecting markers and built-in POIs — <Map selection>
Bind an observable of MapSelectionValue | null to <Map selection>. The
written value is a tagged union:
Branch on value.type to handle each case:
kind is one of "pointOfInterest" / "physicalFeature" / "territory" /
"unknown". pointOfInterestCategory uses the same vocabulary as
MapPointOfInterestCategory (e.g. "restaurant", "cafe") and is null when
the feature has no category.
Markers without a tag are not selectable.
iOS 17 limitation
On iOS 17, only type: "feature" is reported — tapping a tagged <Marker>
does not fire selection. The unified marker/feature selection requires
iOS 18+ (MapSelection<Value>). If your script targets iOS 17 devices, design
around feature taps for POI selection and treat marker selection as iOS 18+ only.
Item selection + Apple's auto detail cards — <Map itemSelection>
iOS 18+ adds a higher-level path: bind <Map itemSelection> to an
Observable<MapItem | null> and pair <Marker item={mapItem}> markers with
Apple's built-in detail card via <Map itemDetailSelectionAccessory> /
<Map featureSelectionAccessory>.
- Tapping
<Marker item>writes that exactMapIteminstance into the observable. Compare with===to find the picked item (the JS side keeps reference identity through MapKit selection). - Apple's auto card pops up automatically:
itemDetailSelectionAccessory— card for tapped item markers (e.g., address, phone, "Directions" button).featureSelectionAccessory— card for tapped Apple-rendered POI labels.
- Style values:
"automatic"(MapKit picks callout vs sheet),"callout","sheet". Passnull(or omit the prop) to disable.
Nested presentation caveat:
"automatic"and"sheet"use a modal sheet presentation (MKPresentableSelectionAccessoryViewController). InsideNavigation.present(...)-style modal contexts the presentation chain conflicts with the parent modal — iOS 18 currently aborts the second tap withAttempt to present ... which is already presenting, and in some cases dismisses the parent modal entirely. Prefer"callout"(inline anchored bubble, no modal presentation) when the map lives inside a presented sheet orNavigation.presentpage.
itemSelection is mutually exclusive with selection: if both are set,
itemSelection wins and string-tag markers do not fire.
iOS 17 limitation
itemSelection, itemDetailSelectionAccessory, and featureSelectionAccessory
are all iOS 18+. On iOS 17 the props are silently ignored — the map renders,
markers display, but tapping does not write to itemSelection and no
Apple-styled card appears.
Cancellation note
locate does not expose a cancellation handle. For typeahead scenarios use
createCompleter instead — repeated locate calls do not deduplicate stale
responses and a fast typist can see results arrive out of order.
createCompleter — autocomplete
Options
Methods
Lifecycle
One completer corresponds to one input field — sharing a completer between
unrelated fields will cross-contaminate results because the underlying
queryFragment is single-valued. Call dispose() when the field unmounts.
Suggestion lifetime
A MapSearchCompletion's id is only valid until the completer issues its
next batch of suggestions. Resolving a stale suggestion rejects with
"unknown completion id". In React-style UIs, store the array of suggestions
in state alongside the user's selection so the chosen suggestion's id is
always paired with the batch it came from.
Combining with <Map>
Hand the whole MapItem to <Marker> and MapKit fills in title, coordinate,
and a POI-category-driven glyph for you:
If you also pass title / systemImage / monogram, the marker reverts to
the default pin (or your specified glyph) and uses your override — auto-glyph
selection only applies when none of those are given. The two forms are mutually
exclusive in the type system: passing both item and coordinate is a
compile-time error.
To fit the map around the result set, use MapUtils.regionFromCoordinates:
Errors
locate rejects when:
queryis missing or empty- The underlying
MKLocalSearchfails (network, no results, etc.)
completer.resolve rejects when:
- The completion id is stale (next batch invalidated it)
- The underlying
MKLocalSearchlookup fails
The completer's listener never receives a synchronous error; on backend failure the listener is called with an empty array.
