Searching for points of interest in MapKit with SwiftUI
Learn how to use MapKit to perform a search for points of interest and display them on a map.
The power of MapKit comes to light when searching for points of interest. Performing a search for a specific type of location is made simple by the combination of the MKLocalSearch
and MKMapItem
objects.
In this short tutorial, you will learn how to use MKLocalSearch
to search for points of interest using MapKit based on a search query and a region and display them on a Map view.
Before we start
To go through this tutorial you need a basic understanding of SwiftUI and the Swift Language. You don’t need any particular assets to go through it.
Start by creating a new app project with Xcode.
As a starting point, edit your ContentView
view to add the following:
import SwiftUI
// 1.
import MapKit
struct ContentView: View {
// 2.
let garage = CLLocationCoordinate2D(
latitude: 40.83657722488077,
longitude: 14.306896671048852
)
// 3. View properties
@State private var searchQuery: String = ""
var body: some View {
NavigationStack {
// 4.
Map {
Marker("Garage", coordinate: garage)
}
// 5. Map modifiers
.mapStyle(.hybrid(elevation: .realistic))
// 6. Search modifiers
.searchable(text: $searchQuery, prompt: "Locations")
.onSubmit(of: .search) {
// Add the search action here
}
// 7. Navigation modifiers
.navigationTitle("Search")
}
}
}
- Import the MapKit frameworks
- Create a local variable that will be used as a reference point for our search. In other scenarios, you could use the current user location
- Create a local state property that will store the search query to be used to perform the search later on
- Replace the body content with a
NavigationStack
with aMap
view inside. In theMap
view create aMarker
for our reference coordinate. - Set up the map style (you can choose any style of your preference here)
- Set up the search modifiers to be used to trigger the search later on
- Assign a navigation title to our view
With our starting point set up, we can start implementing the search functionality of our view.
Step 1 - Performing a search with MKLocalSearch
In this step, we will create the method responsible for using the user input to perform a local search using MapKit. The results will then be stored on a local property that will be used to display the results on the Map view in future steps.
struct ContentView: View {
let garage = CLLocationCoordinate2D(latitude: 40.83657722488077, longitude: 14.306896671048852)
@State private var searchQuery: String = ""
// 1.
@State private var searchResults: [MKMapItem] = []
var body: some View {
...
}
// 2.
private func search(for query: String) {
// 3.
let request = MKLocalSearch.Request()
// 4.
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = MKCoordinateRegion(
center: garage,
span: MKCoordinateSpan(
latitudeDelta: 0.0125,
longitudeDelta: 0.0125
)
)
// 5.
Task {
// 6.
let search = MKLocalSearch(request: request)
// 7.
let response = try? await search.start()
// 8.
searchResults = response?.mapItems ?? []
}
}
}
- Create a property called
searchResults
which will be responsible for storing an array ofMKMapItem
objects and initialize it as an empty array - Create a function that will receive the search query as a parameter to perform the search
- Create a new
MKLocalSearch.Request
object to be used for searching for map locations based on a natural language string - Set up the search properties:
naturalLanguageQuery
: the query to be used to perform the searchresultTypes
: the types of items to include in the search resultsregion
: the map region where to perform the search
- Since the search is performed asynchronously, create a
Task
object to perform it - Create a
MKLocalSearch
object based on the request object created previously - Start the search and store the resulting response
- If the response successfully returns a list of map items, update the search results property accordingly
Step 2 - Presenting the search results on the map
Now that we have the methods to perform the search set, we need to:
- Trigger the search
- Present the results on the map
NavigationStack {
Map {
Marker("Garage", coordinate: garage)
// 2.
ForEach(searchResults, id: \\.self) { result in
Marker(item: result)
}
}
/// Map modifiers
.mapStyle(.hybrid(elevation: .realistic))
/// Search modifiers
.searchable(text: $searchQuery, prompt: "Locations")
.onSubmit(of: .search) {
// 1.
self.search(for: searchQuery)
}
/// Navigation modifiers
.navigationTitle("Searching")
}
- Call the search method when the user submits the input in the search text field
- Present a
Marker
for each of the results
Now we can already perform a search and see the results on the map!
But at the moment the search is always being performed by taking a pre-determined area of the map as the region to be used. Let’s allow the search to be performed in the current region the user is looking at.
Step 3 - Searching on the visible region
To configure the search to happen in the map's currently visible area, we need to track its camera position and use that information in our search request.
struct ContentView: View {
let garage = CLLocationCoordinate2D(
latitude: 40.83657722488077,
longitude: 14.306896671048852
)
/// View properties
@State private var searchQuery: String = ""
@State private var searchResults: [MKMapItem] = []
// 1.
@State private var position: MapCameraPosition = .automatic
// 2.
@State private var visibleRegion: MKCoordinateRegion?
var body: some View {
NavigationStack {
// 3.
Map(position: $position) {
Marker("Garage", coordinate: garage)
ForEach(searchResults, id: \\.self) { result in
Marker(item: result)
}
}
/// Map modifiers
.mapStyle(.hybrid(elevation: .realistic))
// 4.
.onMapCameraChange { context in
self.visibleRegion = context.region
}
/// Search modifiers
.searchable(text: $searchQuery, prompt: "Locations")
.onSubmit(of: .search) {
self.search(for: searchQuery)
}
/// Navigation modifiers
.navigationTitle("Search")
}
}
private func search(for query: String) {
...
}
}
- Create a property called
position
of typeMapCameraPosition
to be used to update the current map position based on the results of the search - Create a property called
visibleRegion
of typeMKCoordinateRegion
to keep track of the currently visible map area to be used when searching - Bind the
Map
view position to theposition
property - We use the
onMapCameraChange(frequency:_:)
modifier to update the visible region to be used on the search every time the user moves the map around
Now let’s update the search method to use the visible region of the map as the region to be used to perform the search.
private func search(for query: String) {
// 1.
let defaultRegion = MKCoordinateRegion(
center: garage,
span: MKCoordinateSpan(
latitudeDelta: 0.0125,
longitudeDelta: 0.0125
)
)
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
// 2.
request.region = visibleRegion ?? defaultRegion
print("Visible region: \\(visibleRegion.debugDescription)")
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
// 3.
self.position = .region(request.region)
}
}
- Create a property to be the default region used on the search in case the visible region is nil
- Assign the currently visible region of the map to the search request
- Add a line of code to update the current map position, so it is the same region of the performed search once the search is completed
With this step complete, now the region where the search is performed is no longer fixed to a specific area but is defined by the currently visible area of the map.
Step 4 - Selecting an item on the map
If the user interacts with the markers on the map nothing happens. To add interactivity to them is simple.
The first step is to create a property called selectedResult
of the type MKMapItem
which will be responsible for storing the currently selected map item.
struct ContentView: View {
let garage = CLLocationCoordinate2D(latitude: 40.83657722488077, longitude: 14.306896671048852)
@State private var searchQuery: String = ""
@State private var searchResults: [MKMapItem] = []
@State private var position: MapCameraPosition = .automatic
@State private var visibleRegion: MKCoordinateRegion?
// 1.
@State private var selectedResult: MKMapItem?
...
}
Then bind the selectedResult
property to the map with its view initializer.
Map(position: $position, selection: $selectedResult) {
...
}
With just these two small changes, now when the user selects a marker on the map it does a little scale animation as visual feedback for the selection.
Final Result
Now you have a view that presents a map and allows the user to search for points of interest based on a natural language query with a search bar. Here is what the app should look like:
Here is the complete code of the ContentView
created during this tutorial:
import SwiftUI
import MapKit
struct ContentView: View {
/// View properties
let garage = CLLocationCoordinate2D(
latitude: 40.83657722488077,
longitude: 14.306896671048852
)
/// Search properties
@State private var searchQuery: String = ""
@State private var searchResults: [MKMapItem] = []
/// Map properties
@State private var position: MapCameraPosition = .automatic
@State private var visibleRegion: MKCoordinateRegion?
@State private var selectedResult: MKMapItem?
var body: some View {
NavigationStack {
Map(position: $position, selection: $selectedResult) {
/// Reference point
Marker("Garage", coordinate: garage)
/// Search results on the map
ForEach(searchResults, id: \\.self) { result in
Marker(item: result)
}
}
/// Map modifiers
.mapStyle(.hybrid(elevation: .realistic))
.onMapCameraChange { context in
self.visibleRegion = context.region
}
/// Search modifiers
.searchable(text: $searchQuery, prompt: "Locations")
.onSubmit(of: .search) {
self.search(for: searchQuery)
}
/// Navigation modifiers
.navigationTitle("Search")
}
}
/// Search method
private func search(for query: String) {
let defaultRegion = MKCoordinateRegion(
center: garage,
span: MKCoordinateSpan(
latitudeDelta: 0.0125,
longitudeDelta: 0.0125
)
)
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = visibleRegion ?? defaultRegion
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
position = .region(request.region)
}
}
}