In this episode, we’ll learn how to conduct a search on the map. Our objective is to achieve something like this:
Therefore, by typing in a text field what we want to search for, the points of interest found will be displayed on the map. In the example, I typed Shop, but you can type anything else.
The Map
Let’s start by looking at the view that contains the map:
import SwiftUI import MapKit struct ContentView: View { @State var position: MapCameraPosition = .userLocation(fallback: .automatic) @State private var searchResults = [SearchResult]() @State private var selectedLocation: SearchResult? @State private var searchText: String = "" var body: some View { VStack { TextField("Search", text: $searchText) .onSubmit { Task { // searchResults = } }.padding() Map(position: $position, selection: $selectedLocation) { ForEach(searchResults) { result in Marker(coordinate: result.location) { Image(systemName: "mappin") Text(result.name) } .tag(result) } } } } }
- The position variable contains the user’s current location.
- searchResults holds the results of the search.
- selectedLocation stores the marker that has been selected (the item chosen from the search results).
- searchText is the text input used for the query.
When the user submits the search string, the search function is called. The map centers on position (which, in this case, is the user’s location), and selectedLocation is updated when a marker is tapped.
Using ForEach, we add all the searched points of interest to the map.
The Search
Now, let’s take a look at the search logic.
First, let’s define the structure used in the search:
struct SearchResult: Identifiable, Hashable { let id = UUID() let location: CLLocationCoordinate2D let name: String static func == (lhs: SearchResult, rhs: SearchResult) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } }
So, we implement the Identifiable protocol to have a unique identifier and the Hashable protocol to enable the use of this type of item as a selection on the Map.
Now, let’s focus on the core of the logic:
func search(with query: String, coordinate: CLLocationCoordinate2D? = nil) async throws -> [SearchResult] { let mapKitRequest = MKLocalSearch.Request() mapKitRequest.naturalLanguageQuery = query mapKitRequest.resultTypes = .pointOfInterest if let coordinate { mapKitRequest.region = .init(.init(origin: .init(coordinate), size: .init(width: 1, height: 1))) } let search = MKLocalSearch(request: mapKitRequest) let response = try await search.start() return response.mapItems.compactMap { mapItem in guard let location = mapItem.placemark.location?.coordinate else { return nil } return .init(location: location, name: mapItem.placemark.name ?? "") } }
This function receives the search string and coordinates for the search location as input (if ‘nil’, the current location is used), and it returns an array of SearchResult.
In the function, we create a request specifying that we use natural language and that our aim is to search for points of interest.
After that, we remove any points of interest with ‘nil’ coordinates and instead create SearchResult entity by adding coordinate and name.
That’s all.