3

I am using MKLocalSearch to allow users to search for their city. However, when I try the code I only receive results in the US. Also I receive a lot of results that include stores etc. I added the filters in func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) to get rid of results that are not cities. This works but I feel like this may fail for certain places. I tried adding the code below but that didn't work. I'd appreciate any help.

let worldRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 0, longitude: 0),
                                       span: MKCoordinateSpan(latitudeDelta: 180, longitudeDelta: 360))
self.searchCompleter.region = worldRegion
import Foundation
import Combine
import MapKit

class LocationService: NSObject, ObservableObject {

    enum LocationStatus: Equatable {
        case idle
        case noResults
        case isSearching
        case error(String)
        case result
    }

    @Published var queryFragment: String = ""
    @Published private(set) var status: LocationStatus = .idle
    @Published private(set) var searchResults: [MKLocalSearchCompletion] = []

    private var queryCancellable: AnyCancellable?
    private let searchCompleter: MKLocalSearchCompleter!

    init(searchCompleter: MKLocalSearchCompleter = MKLocalSearchCompleter()) {
        self.searchCompleter = searchCompleter
        super.init()
        self.searchCompleter.delegate = self
        
        queryCancellable = $queryFragment
            .receive(on: DispatchQueue.main)
            .debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
            .sink(receiveValue: { fragment in
                self.status = .isSearching
                if !fragment.isEmpty {
                    self.searchCompleter.queryFragment = fragment
                } else {
                    self.status = .idle
                    self.searchResults = []
                }
        })
    }
}

extension LocationService: MKLocalSearchCompleterDelegate {
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        self.searchResults = completer.results.filter { result in
            if !result.title.contains(",") {
                return false
            }
            if result.title.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil {
                return false
            }
            if result.subtitle.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil {
                return false
            }
            return true
        }
        self.status = completer.results.isEmpty ? .noResults : .result
    }

    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        self.status = .error(error.localizedDescription)
    }
}
import SwiftUI
import MapKit

struct locView: View {
    @StateObject var locationService = LocationService()
    var body: some View {
        VStack {
            Form {
                Section(header: Text("Location Search")) {
                    ZStack(alignment: .trailing) {
                        TextField("Search", text: $locationService.queryFragment)
                        if locationService.status == .isSearching {
                            Image(systemName: "clock")
                                .foregroundColor(Color.gray)
                        }
                    }
                }
                Section(header: Text("Results")) {
                    List {
                        Group { () -> AnyView in
                            switch locationService.status {
                            case .noResults: return AnyView(Text("No Results"))
                            case .error(let description): return AnyView(Text("Error: \(description)"))
                            default: return AnyView(EmptyView())
                            }
                        }.foregroundColor(Color.gray)

                        ForEach(locationService.searchResults, id: \.self) { completionResult in
                            Text(completionResult.title)
                        }
                    }
                }
            }
        }
    }
}
HangarRash
  • 7,314
  • 5
  • 5
  • 32
ahmed
  • 341
  • 2
  • 9
  • @Rob no I have not tried both of those. I looked into them and Im not quite sure how to implement them within my given code. Maybe you can demonstrate the approach you were thinking about. CLGeocoder won't work because from my understanding it spits out only one location. I would like all results matching the search to return. Basically if the user types in Munich, they should see Munich Germany and Munich the city in the US. Id appreciate an example though, thanks. – ahmed Aug 05 '23 at 05:59
  • @Rob how do I specify the result type? I tried doing "self.searchResults = .address" in the init but that doesn't work and I get errors. – ahmed Aug 05 '23 at 06:19
  • @Rob thanks, also when I search Munich I only get Munich ND. When I search anything actually I only get US results. Do you have any idea why this is. Also adding in the line you just provided doesn't do anything to the search results, would you explain what its for please. – ahmed Aug 05 '23 at 06:25
  • @Rob the code I have above is what I am using so If you are using the exact same code then maybe I have something wrong in my setup or scheme. If you are doing something different than what I have what is it. I understand your thinking on the .address now, it does help narrow down the search for simply cities but there is still some un wanted results though. Thanks anyways. Let me know how you have it all set up though. – ahmed Aug 05 '23 at 06:38
  • @Rob thanks, seemed like the filter was taking it out. I don't think MKLocalSearch is what I need. It has too many random locations that pop up. Im looking for a way to allow users to look up their city and where the search results look like "city, state (if applicable), country". Id appreciate if you can point me in the right direction. – ahmed Aug 05 '23 at 07:10

1 Answers1

0

A few observations:

  1. To exclude searching points of interests, from things like stores, you can specify that the completer should only search addresses (e.g. a resultTypes of .address):

    searchCompleter.resultTypes = .address
    

    Alternatively, you can explicitly exclude points of interest with a pointsOfInterestFilter of .excludingAll:

    searchCompleter.pointOfInterestFilter = .excludingAll
    
  2. Your completerDidUpdateResults(_:) implementation is filtering results (perhaps an adaptation of Search for Place Names (City/Town) Using MKLocalSearchCompleter, which assumes the title for a city must include a comma). E.g., search for “munich” and you will return two results “Munich, ND”/“United States” and “Munich”/“Bavaria, Germany”. Because you are looking for a comma in the title, the latter result will subsequently be filtered out.

    This illustrates how brittle it can be to attempt to look at string characteristics to determine whether a title/subtitle combination represents a city or not.

  3. You can contemplate using MKLocalSearch or CLGeocoder, but while those both return arrays of results, I find that their results are more narrow than MKLocalSearchCompleter. In the “munich” search example, both of these only return the city in Germany.

Bottom line, MapKit and CoreLocation do not appear to offer a simple city search capability. I understand that Google Maps API does (but apparently also requires that if you are showing the results on an actual map, you only show it on a Google map, not MapKit’s).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thanks everything works just fine until I add "searchCompleter.pointOfInterestFilter = .excludingAll" then I get the error " did fail with error: Error Domain=kCLErrorDomain Code=0 "(null)"" – ahmed Aug 05 '23 at 18:43
  • Yeah, it looks like that is unnecessary. Either `resultTypes` or `pointsOfInterestFilter`, but not both. – Rob Aug 05 '23 at 18:56