I have some code below that searches for cities in the world. Only the city name and country is returned. Is there any way I can tweak the code to also get the coordinates of each result? I know I can use a geocoder to get the coordinates but I was hoping there's a simpler way since I'm already using MKLocalSearch.
class CitySearchViewModel: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
@Published var searchQuery: String = ""
@Published var searchResults: [CityResult] = []
private var searchCompleter: MKLocalSearchCompleter!
override init() {
super.init()
searchCompleter = MKLocalSearchCompleter()
searchCompleter.delegate = self
searchCompleter.resultTypes = .address
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
let results = getCityList(results: completer.results)
let final = Array(Set(results))
DispatchQueue.main.async {
self.searchResults = final
}
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { }
func performSearch() {
searchCompleter.queryFragment = searchQuery
}
struct CityResult: Hashable {
var city: String
var country: String
}
private func getCityList(results: [MKLocalSearchCompletion]) -> [CityResult] {
var searchResults: [CityResult] = []
for result in results {
let titleComponents = result.title.components(separatedBy: ", ")
let subtitleComponents = result.subtitle.components(separatedBy: ", ")
buildCityTypeA(titleComponents, subtitleComponents) { place in
if !place.city.isEmpty && !place.country.isEmpty {
searchResults.append(CityResult(city: place.city, country: place.country))
}
}
buildCityTypeB(titleComponents, subtitleComponents) { place in
if !place.city.isEmpty && !place.country.isEmpty {
searchResults.append(CityResult(city: place.city, country: place.country))
}
}
}
return searchResults
}
private func buildCityTypeA(_ title: [String], _ subtitle: [String], _ completion: @escaping ((city: String, country: String)) -> Void) {
var city: String = ""
var country: String = ""
if title.count > 1 && subtitle.count >= 1 {
city = title.first!
country = subtitle.count == 1 && subtitle[0] != "" ? subtitle.first! : title.last!
}
completion((city, country))
}
private func buildCityTypeB(_ title: [String], _ subtitle: [String], _ completion: @escaping ((city: String, country: String)) -> Void) {
var city: String = ""
var country: String = ""
if title.count >= 1 && subtitle.count == 1 {
city = title.first!
country = subtitle.last!
}
completion((city, country))
}
}
Thanks to JermeyP I was able to compose this complete solution based on his code. The solution below retrieves cities that match a search string along with their corresponding country and coordinates.
import SwiftUI
import MapKit
struct ContentMapView: View {
@ObservedObject private var viewModel = CitySearchViewModel()
var body: some View {
VStack {
TextField("search", text: $viewModel.searchQuery)
.onSubmit {
viewModel.performSearch()
}
List(viewModel.searchResults, id: \.self) { result in
Text("\(result.city), \(result.country), \(result.latitude), \(result.longitude)")
}
}
}
}
class CitySearchViewModel: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
@Published var searchQuery: String = ""
@Published var searchResults: [CityResult] = []
private var searchCompleter: MKLocalSearchCompleter!
override init() {
super.init()
searchCompleter = MKLocalSearchCompleter()
searchCompleter.delegate = self
searchCompleter.resultTypes = .address
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
getCityList(results: completer.results) { cityResults in
DispatchQueue.main.async {
self.searchResults = cityResults
}
}
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { }
func performSearch() {
searchCompleter.queryFragment = searchQuery
}
struct CityResult: Hashable {
var city: String
var country: String
var latitude: Double
var longitude: Double
}
private func getCityList(results: [MKLocalSearchCompletion], completion: @escaping ([CityResult]) -> Void) {
var searchResults: [CityResult] = []
let dispatchGroup = DispatchGroup()
for result in results {
dispatchGroup.enter()
let request = MKLocalSearch.Request(completion: result)
let search = MKLocalSearch(request: request)
search.start { (response, error) in
defer {
dispatchGroup.leave()
}
guard let response = response else { return }
for item in response.mapItems {
if let location = item.placemark.location {
let city = item.placemark.locality ?? ""
var country = item.placemark.country ?? ""
if country.isEmpty {
country = item.placemark.countryCode ?? ""
}
if !city.isEmpty {
let cityResult = CityResult(city: city, country: country, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
searchResults.append(cityResult)
}
}
}
}
}
dispatchGroup.notify(queue: .main) {
completion(searchResults)
}
}
}