This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
Cannot really think of a map app without the search functionality!
With MKLocalSearch, there are two types of searches we can perform
- Search based on keyword (natural language query), and
- Search by specifying points of interest categories interested (This is also how you can get all MKMapItem available within the region!)
Each approach has its own use cases and let’s check those out in this article!
You can find the full code we used in this article at the end. Feel free to copy and paste it into your project, and try it out by yourself as you read through!
Let’s get started!
Set Up
Let’s just add some simple code to start up.
private struct DemoView: View {
static let aspen = MapCameraPosition.camera(MapCamera(
centerCoordinate: CLLocationCoordinate2D(latitude: 39.1911, longitude: -106.817535),
distance: 2000,
heading: 0,
pitch: 0
))
@State private var position: MapCameraPosition = Self.aspen
@State private var region: MKCoordinateRegion? = Self.aspen.region
@State private var selection: MapSelection<Int>?
@State private var searchResultItems: [MKMapItem] = []
var body: some View {
Map(position: $position, selection: $selection) {
ForEach(searchResultItems, id: \.self) { item in
Marker(item: item)
}
}
.onMapCameraChange { context in
self.region = context.region
}
}
}
All we have here is a Map with selection enabled and update the region onMapCameraChange so that we can make sure that the region we are performing the MKLocalSearch on later is actually the region displayed.
Search using Keywords
Here are the two of the main scenarios for performing searches using the natural language query.
- Find a specific location, for example, Tokyo
- Find all matches for a given keyword. Let’s say coffee shops.
Find Specific Location
Let’s first check out the use case of looking for a specific location using MKLocalSearch.
For example, here is what we will have if we are searching for Los Angeles.
private func findLA() async -> MKMapItem? {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "Los Angeles"
request.addressFilter = .includingAll
let search = MKLocalSearch(request: request)
let response = try? await search.start()
return response?.mapItems.first
}
Everything is pretty straight forward except for couple things I would like to point out here.
First of all, the addressFilter. This is a newly added feature from WWDC24 that allows us to set which address options to include or exclude in search results.
For example, if we are passing in a postal code as our naturalLanguageQuery and we only want the results to be match by postal code, we can set it like following.
request.naturalLanguageQuery = "100-0000" //postal code for tokyo
request.addressFilter = MKAddressFilter (
including: [.postalCode]
)
To find out all the options available for MKAddressFilter , please check out MKAddressFilter.Options.
Second, if the search fails, ie: we are not able to get any matches based on what we specify, the response will be nil and we will get an operation couldn’t be completed. (MKErrorDomain error 4.) error (if we do and catch).
Let’s test our search function out by adding the following Button to our view.
Button(action: {
Task {
if let la = await findLA() {
self.position = .region(MKCoordinateRegion(center: la.placemark.coordinate, latitudinalMeters: 500, longitudinalMeters: 500))
}
}
}, label: {
Text("Find LA and GO!")
.foregroundStyle(.white)
.padding(.all, 8)
.background(RoundedRectangle(cornerRadius: 4).fill(.gray))
})
Yes, we are teleported to Los Angeles from Aspen in 1 second (or probably even less)!
Find Matches Based on Keyword
The reason that I have separated out this from the use case above is that I would like to share with you the usage of couple other parameters we can set in our request.
Same as above, let’s first check out an example of searching for ski resorts and then take a more detailed look at what we have done.
private func findSkiResorts(in region: MKCoordinateRegion) async -> [MKMapItem] {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "ski resort"
request.region = region
request.regionPriority = .default
request.pointOfInterestFilter = MKPointOfInterestFilter(including: [.skiing])
let search = MKLocalSearch(request: request)
let response = try? await search.start()
return response?.mapItems ?? []
}
Two additional parameters we have specified here.
First of all, we have regionPriority. This is another beta feature from WWDC24 that allows us to set the importance of the configured region. If we don’t want ANY results outside the specified region, we can set the value to required.
Another parameter we have set here is the pointOfInterestFilter, a filter that lists point-of-interest categories to include or exclude in search results. If we want all types of point of interest to be included, we can either leave the parameter out or we can set request.pointOfInterestFilter = .includingAll.
Again, another button to test it out.
Button(action: {
Task {
if let region = self.region {
let resorts = await findSkiResorts(in: region)
self.searchResultItems = resorts
}
}
}, label: {
Text("Find Ski resorts")
.foregroundStyle(.white)
.padding(.all, 8)
.background(RoundedRectangle(cornerRadius: 4).fill(.gray))
})
Search By Points of Interest
First of all, you might wondering cannot we just use what we have above and left out the naturalLanguageQuery? Unfortunately, nope…You will get an operation couldn’t be completed. (MKErrorDomain error 4.) error (at least I did).
To search based on point of interest categories without providing a natural language query, we will need to create our MKLocalSearch using a MKLocalPointsOfInterestRequest.
For example, to find all restaurants, here is what we have.
private func findRestaurants(in region: MKCoordinateRegion) async -> [MKMapItem] {
let request = MKLocalPointsOfInterestRequest(coordinateRegion: region)
request.pointOfInterestFilter = MKPointOfInterestFilter(including: [.restaurant])
let search = MKLocalSearch(request: request)
let response = try? await search.start()
return response?.mapItems ?? []
}
One thing to keep in mind while using MKLocalPointsOfInterestRequest is that the search result is strictly within the region specified. That is similar behavior to setting request.regionPriority = .required in the previous section.
Button(action: {
Task {
if let region = self.region {
let restaurants = await findRestaurants(in: region)
self.searchResultItems = restaurants
}
}
}, label: {
Text("Find Restaurants")
.foregroundStyle(.white)
.padding(.all, 8)
.background(RoundedRectangle(cornerRadius: 4).fill(.gray))
})
I personally only use MKLocalPointsOfInterestRequest instead of the one above(ie: MKLocalSearch.Request) when I want to get ALL MKMapItems of all types of MKPointOfInterest within the region because in this case, I will not have any keyword I can pass in to the naturalLanguageQuery.
Wrap Up
Just putting together the code we have above, here we go!
import SwiftUI
import MapKit
private struct DemoView: View {
static let aspen = MapCameraPosition.camera(MapCamera(
centerCoordinate: CLLocationCoordinate2D(latitude: 39.1911, longitude: -106.817535),
distance: 2000,
heading: 0,
pitch: 0
))
@State private var position: MapCameraPosition = Self.aspen
@State private var region: MKCoordinateRegion? = Self.aspen.region
@State private var selection: MapSelection<Int>?
@State private var searchResultItems: [MKMapItem] = []
var body: some View {
Map(position: $position, selection: $selection) {
ForEach(searchResultItems, id: \.self) { item in
Marker(item: item)
}
}
.onMapCameraChange { context in
self.region = context.region
}
.overlay(alignment: .topLeading, content: {
VStack(spacing: 16) {
Button(action: {
Task {
if let la = await findLA() {
self.position = .region(MKCoordinateRegion(center: la.placemark.coordinate, latitudinalMeters: 500, longitudinalMeters: 500))
}
}
}, label: {
Text("Find LA and GO!")
.foregroundStyle(.white)
.padding(.all, 8)
.background(RoundedRectangle(cornerRadius: 4).fill(.gray))
})
.frame(maxWidth: .infinity, alignment: .leading)
Button(action: {
Task {
if let region = self.region {
let resorts = await findSkiResorts(in: region)
self.searchResultItems = resorts
}
}
}, label: {
Text("Find Ski resorts")
.foregroundStyle(.white)
.padding(.all, 8)
.background(RoundedRectangle(cornerRadius: 4).fill(.gray))
})
.frame(maxWidth: .infinity, alignment: .leading)
Button(action: {
Task {
if let region = self.region {
let restaurants = await findRestaurants(in: region)
self.searchResultItems = restaurants
}
}
}, label: {
Text("Find Restaurants")
.foregroundStyle(.white)
.padding(.all, 8)
.background(RoundedRectangle(cornerRadius: 4).fill(.gray))
})
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.all, 8)
})
}
private func findLA() async -> MKMapItem? {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "Los Angeles"
request.addressFilter = .includingAll
let search = MKLocalSearch(request: request)
let response = try? await search.start()
// print(response?.mapItems)
return response?.mapItems.first
}
private func findSkiResorts(in region: MKCoordinateRegion) async -> [MKMapItem] {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "ski resort"
request.region = region
request.regionPriority = .default
request.pointOfInterestFilter = MKPointOfInterestFilter(including: [.skiing])
let search = MKLocalSearch(request: request)
let response = try? await search.start()
// print(response?.mapItems.first)
return response?.mapItems ?? []
}
private func findRestaurants(in region: MKCoordinateRegion) async -> [MKMapItem] {
let request = MKLocalPointsOfInterestRequest(coordinateRegion: region)
request.pointOfInterestFilter = MKPointOfInterestFilter(including: [.restaurant])
let search = MKLocalSearch(request: request)
let response = try? await search.start()
// print(response?.mapItems.first)
return response?.mapItems ?? []
}
}
Thank you for reading!
That’s all I have for this article!
Happy local searching!
SwiftUI+MapKit: Taking Our Map to the Next Level Using MKLocalSearch was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
Itsuki | Sciencx (2024-08-25T15:16:37+00:00) SwiftUI+MapKit: Taking Our Map to the Next Level Using MKLocalSearch. Retrieved from https://www.scien.cx/2024/08/25/swiftuimapkit-taking-our-map-to-the-next-level-using-mklocalsearch/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.