3

I'm trying to build a simple weather app and I'm stuck when trying to implement the search city function. I managed to implement the search function to return a list of CLPlacemarks, however, this includes a lot of points of interest (e.g. restaurants, street names..) which make the results very messy. Is there a way to limit the results to only cities with that name? Here's the code I have:

func updateSearchResults(for searchController: UISearchController) {

    var searchText = searchController.searchBar.text
    
    request.naturalLanguageQuery = searchText
    localSearch = MKLocalSearch(request: request)
    localSearch?.start { (searchResponse, _) in
        guard let items = searchResponse?.mapItems else {
            return
        }
        self.placemarks = [CLPlacemark]()
        for pm in items {
            self.placemarks.append(pm.placemark)
        }
    }
}
  • 1
    You can add the following line to your request; `request.resultTypes = .address`. That sets your result only to adresses. –  Jul 02 '20 at 19:15

3 Answers3

4

Tested using Combine and SwiftUI (Swift 5.3)

This is not the perfect solution but more or less you get only cities and countries:

MKLocalSearchCompleter settings:

searchCompleter = MKLocalSearchCompleter()    
searchCompleter.delegate = self
searchCompleter.region = MKCoordinateRegion(.world)
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])

Code to add on delegate methods:

func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    
    let searchResults = self.getCityList(results: completer.results)
    
    print(searchResults)
}

func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
    
    print(error.localizedDescription)
}

Get the cities:

func getCityList(results: [MKLocalSearchCompletion]) -> [(city: String, country: String)]{
    
    var searchResults: [(city: String, country: String)] = []
    
    for result in results {
        
        let titleComponents = result.title.components(separatedBy: ", ")
        let subtitleComponents = result.subtitle.components(separatedBy: ", ")
        
        buildCityTypeA(titleComponents, subtitleComponents){place in
            
            if place.city != "" && place.country != ""{
                
                searchResults.append(place)
            }
        }
        
        buildCityTypeB(titleComponents, subtitleComponents){place in
            
            if place.city != "" && place.country != ""{
                
                searchResults.append(place)
            }
        }
    }
    
    return searchResults
}

You can get two types of cities:

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

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

Matthew Rodríguez
  • 429
  • 1
  • 3
  • 7
0

I'm doing the same thing as a research project. I've come close but there are some frustrating aspects to this.

Here is the set up

private var completer: MKLocalSearchCompleter = MKLocalSearchCompleter()

// triggers the search-as-you-type
//
func searchFor(term: String) {
    completer.delegate = self
    completer.region = MKCoordinateRegion(.world)
    completer.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll
    completer.queryFragment = term
}

I set the region to search the world, then removed all of the points of interest.

In the completer function, I filtered out anything that does not have a comma. The results tend to be City, State or Region, Country (with Country being optional).

let results = completer.results.filter { result in
    guard result.title.contains(",") else { return false }
    return true
}
self.searchTerms = results.map { $0.title }

This works well for anything in the United States. But try entering "Paris" or "London" and you get "Paris, TX" because Paris, in France, is just "Paris" for some reason.

However, if you examine the .subtitle property, you'll find that Paris, in France, has the subtitle, "France" (why this is not consistent is beyond me). So I adjusted by completer function as follows:

func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    let results = completer.results.filter { result in
        guard result.title.contains(",") || !result.subtitle.isEmpty else { return false }
        guard !result.subtitle.contains("Nearby") else { return false }
        return true
    }
    self.searchTerms = results.map { $0.title + ($0.subtitle.isEmpty ? "" : ", " + $0.subtitle) }
}

This is not pretty but it works fairly well

P. Ent
  • 1,654
  • 1
  • 12
  • 22
0

I was looking for a way to pick cities around the world through a search. After considering APIs, building actual data search of top cities, showing search results using Search Controller etc this ended up the quickest working solution:

What worked okay for me was using CoreLocation only assuming you just want the closest city/GPS (Lat Lng) coordinate to what the user searched or typed in.

For a user entry like "Washington, DC" into a Text Field you can pass that String into a geocodeAddressString function:

 @IBAction func entryFieldAction(_ sender: UITextField) {
        
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            
            
            let city = sender.text ?? " "
            
 
            if (city == " " || city == "") {
                
                return
            }
            
           
            self.getCoordinate(addressString: city) { coordinate, place, error in
             
                        
                    let lat = coordinate.latitude
                    let lng = coordinate.longitude

                    let city = place?.locality ?? ""
                    let province = place?.administrativeArea ?? ""
                    let country = place?.country ?? ""
                    
                 
                    print(city)
                    // Washington, DC
                    print(lat)
                    // 38.8954381
                    print(lng)
                    //-77.0312812
                    print(city)
                    //Washington
                    print(province)
                    // DC
                    print(country)
                    // United States
             
             
            }
                
             
        }
        
        
    }


  func getCoordinate( addressString : String,
            completionHandler: @escaping(CLLocationCoordinate2D, CLPlacemark?, NSError?) -> Void ) {
        let geocoder = CLGeocoder()
        geocoder.geocodeAddressString(addressString) { (placemarks, error) in
            if error == nil {
                
                if let placemark = placemarks?[0] {
                    
                    let location = placemark.location!
                        
                    completionHandler(location.coordinate, placemark, nil)
                   
                    return
                }
            }
                
            completionHandler(kCLLocationCoordinate2DInvalid, nil, error as NSError?)
        }
    }

So this will take whatever String the user typed in and try it's best to produce a single result for city, state/province, country and also a GPS (Lat Lng) coordinate. It seems to be pretty good since it can handle all kinds of user entries as far formatting, city, address, spelling.

Would be curious to hear of other/better ways of doing this.

Nick Khotenko
  • 427
  • 4
  • 13