Use MKLocalSearch to get coordinates

209 views Asked by At

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))
    }
}
2

There are 2 answers

0
Jaxon Steinhower On BEST ANSWER

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)
        }
    }
}
0
JeremyP On

If you have an MKLocalSearchCompletion you can make an MKLocalSearch.Request using it and then perform the search

func getLocations(completion: MKLocalSearchCompletion)
{
    let request = MKLocalSearch.Request(completion: MKLocalSearchCompletion)
    let search = MKLocalSearch(request: request)
    search.start 
    { 
        (response, error) in
        guard let response = response else { /* error! */ }
    
        for item in response.mapItems 
        {
            if let location = item.placemark.location 
            {
                // You have your location
            )
        }
    }
}

The above is not tested but should give you an idea of how it works.