swiftmapkitmklocalsearch

Use MKLocalSearch to get coordinates


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

Solution

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