【问题标题】:How to implement auto-complete for address using Apple Map Kit如何使用 Apple Map Kit 实现地址自动完成
【发布时间】:2016-01-27 14:47:14
【问题描述】:

我想自动完成用户的地址,就像 google api 在此链接中提供的一样:

https://developers.google.com/maps/documentation/javascript/places-autocomplete?hl=en

如何使用苹果地图套件实现相同的功能?

我曾尝试使用 Geo Coder,例如:

@IBAction func SubmitGeoCode(sender: AnyObject) {

    let address = "1 Mart"
    let coder = CLGeocoder()

    coder.geocodeAddressString(address) { (placemarks, error) -> Void in

        for placemark in placemarks! {

            let lines = placemark.addressDictionary?["FormattedAddressLines"] as? [String]

            for addressline in lines! {
                print(addressline)
            }
        }
    }
}

但是结果非常令人失望。

任何可用于实现此类功能的 Apple API,还是我应该使用 google api?

谢谢

【问题讨论】:

    标签: ios swift geocoding


    【解决方案1】:

    更新 - 我使用 Swift 3 创建了一个简单的示例项目 here,因为原始答案是用 Swift 2 编写的。

    在 iOS 9.3 中引入了一个名为 MKLocalSearchCompleter 的新类,这允许创建自动完成解决方案,您只需传入如下 queryFragment:

    var searchCompleter = MKLocalSearchCompleter()
    searchCompleter.delegate = self
    var searchResults = [MKLocalSearchCompletion]()
    
    searchCompleter.queryFragment = searchField.text!
    

    然后使用MKLocalSearchCompleterDelegate处理查询结果:

    extension SearchViewController: MKLocalSearchCompleterDelegate {
    
        func completerDidUpdateResults(completer: MKLocalSearchCompleter) {
            searchResults = completer.results
            searchResultsTableView.reloadData()
        } 
    
        func completer(completer: MKLocalSearchCompleter, didFailWithError error: NSError) {
            // handle error
        }
    }
    

    并以适当的格式显示地址结果:

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let searchResult = searchResults[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        cell.textLabel?.text = searchResult.title
        cell.detailTextLabel?.text = searchResult.subtitle
        return cell
    }
    

    然后您可以使用MKLocalCompletion 对象来实例化MKLocalSearch.Request,从而获得对MKPlacemark 和所有其他有用数据的访问权限:

    let searchRequest = MKLocalSearch.Request(completion: completion!)
    let search = MKLocalSearch(request: searchRequest)
    search.startWithCompletionHandler { (response, error) in
        if error == nil {
            let coordinate = response?.mapItems[0].placemark.coordinate
        }
    }
    

    【讨论】:

    • 在什么情况下它不起作用?你得到什么结果?
    • 它如何无缝转换为 Objective C 真是太棒了
    • 如何在选定的地方放置图钉??
    • 我不确定我是否理解这个问题,你能解释一下你所说的 pin 是什么意思吗?
    • 这很漂亮@GeorgeMcDonnell 非常感谢。我一直在寻找这样的东西来代替 Google Places Autocomplete API 仅仅因为他们的一些 T&C 问题。这非常适合我的应用程序..
    【解决方案2】:

    Swift 5 + Combine +(可选)SwiftUI 解决方案

    在其他解决方案上似乎有许多 cmets 想要一个与最新版本的 Swift 兼容的版本。另外,似乎(就像我一样)人们也需要 SwiftUI 解决方案。

    这建立在之前的建议之上,但使用 Combine 来监控输入、消除抖动,然后通过 Publisher 提供结果。

    MapSearchObservableObject 很容易在 SwiftUI 中使用(提供示例),但也可以在非 SwiftUI 情况下使用。

    MapSearch ObservableObject

    import SwiftUI
    import Combine
    import MapKit
    
    class MapSearch : NSObject, ObservableObject {
        @Published var locationResults : [MKLocalSearchCompletion] = []
        @Published var searchTerm = ""
        
        private var cancellables : Set<AnyCancellable> = []
        
        private var searchCompleter = MKLocalSearchCompleter()
        private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
        
        override init() {
            super.init()
            searchCompleter.delegate = self
            
            $searchTerm
                .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
                .removeDuplicates()
                .flatMap({ (currentSearchTerm) in
                    self.searchTermToResults(searchTerm: currentSearchTerm)
                })
                .sink(receiveCompletion: { (completion) in
                    //handle error
                }, receiveValue: { (results) in
                    self.locationResults = results
                })
                .store(in: &cancellables)
        }
        
        func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> {
            Future { promise in
                self.searchCompleter.queryFragment = searchTerm
                self.currentPromise = promise
            }
        }
    }
    
    extension MapSearch : MKLocalSearchCompleterDelegate {
        func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
                currentPromise?(.success(completer.results))
            }
        
        func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
            //could deal with the error here, but beware that it will finish the Combine publisher stream
            //currentPromise?(.failure(error))
        }
    }
    

    SwiftUI 界面,包括地图位置

    
    struct ContentView: View {
        @StateObject private var mapSearch = MapSearch()
        
        var body: some View {
            NavigationView {
                Form {
                    Section {
                        TextField("Address", text: $mapSearch.searchTerm)
                    }
                    Section {
                        ForEach(mapSearch.locationResults, id: \.self) { location in
                            NavigationLink(destination: Detail(locationResult: location)) {
                                VStack(alignment: .leading) {
                                    Text(location.title)
                                    Text(location.subtitle)
                                        .font(.system(.caption))
                                }
                            }
                        }
                    }
                }.navigationTitle(Text("Address search"))
            }
        }
    }
    
    class DetailViewModel : ObservableObject {
        @Published var isLoading = true
        @Published private var coordinate : CLLocationCoordinate2D?
        @Published var region: MKCoordinateRegion = MKCoordinateRegion()
        
        var coordinateForMap : CLLocationCoordinate2D {
            coordinate ?? CLLocationCoordinate2D()
        }
        
        func reconcileLocation(location: MKLocalSearchCompletion) {
            let searchRequest = MKLocalSearch.Request(completion: location)
            let search = MKLocalSearch(request: searchRequest)
            search.start { (response, error) in
                if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate {
                    self.coordinate = coordinate
                    self.region = MKCoordinateRegion(center: coordinate, span: MKCoordinateSpan(latitudeDelta: 0.03, longitudeDelta: 0.03))
                    self.isLoading = false
                }
            }
        }
        
        func clear() {
            isLoading = true
        }
    }
    
    struct Detail : View {
        var locationResult : MKLocalSearchCompletion
        @StateObject private var viewModel = DetailViewModel()
        
        struct Marker: Identifiable {
            let id = UUID()
            var location: MapMarker
        }
        
        var body: some View {
            Group {
                if viewModel.isLoading {
                    Text("Loading...")
                } else {
                    Map(coordinateRegion: $viewModel.region,
                        annotationItems: [Marker(location: MapMarker(coordinate: viewModel.coordinateForMap))]) { (marker) in
                        marker.location
                    }
                }
            }.onAppear {
                viewModel.reconcileLocation(location: locationResult)
            }.onDisappear {
                viewModel.clear()
            }
            .navigationTitle(Text(locationResult.title))
        }
    }
    
    

    【讨论】:

    • 不错!!!我在#100DaysOfSwiftUI 的第 50 天,这是完美的补充。 hackingwithswift.com/books/ios-swiftui/…
    • 这太棒了,而且比为 Google 的 API 付费要好得多。但是,您将如何重写它,以便如果您有一些文本字段,例如(地址、城市、州、邮编),您可以开始在地址字段中输入您的地址,然后选择您的地址并让它自动完成其他文本字段?我根本不需要详细视图或显示地图,只需要表单的自动完成。我需要每个 TextField 绑定到它自己的变量(即 $address、$city、$state 和 $zip),而不是绑定到 mapsearch 的搜索字段。谢谢!
    • @kittonian 这听起来像是一个适合您的好项目,我的回答应该为您的特定需求提供良好的基础。如果遇到困难,您可以在此处提出问题,了解您遇到的问题。
    • 感谢 jnpdx。我刚刚发布了我的问题。
    【解决方案3】:

    我的回答完全基于@George McDonnell 的回答。我希望它对执行最后一个有困难的人有所帮助。

    import UIKit
    import MapKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var searchBar: UISearchBar!
        @IBOutlet weak var tableVIew: UITableView!
    
        //create a completer
        lazy var searchCompleter: MKLocalSearchCompleter = {
            let sC = MKLocalSearchCompleter()
            sC.delegate = self
            return sC
        }()
    
        var searchSource: [String]?
    }
    
    extension ViewController: UISearchBarDelegate {
        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            //change searchCompleter depends on searchBar's text
            if !searchText.isEmpty {
                searchCompleter.queryFragment = searchText
            }
        }
    }
    
    extension ViewController: UITableViewDelegate, UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return searchSource?.count ?? 0
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            //I've created SearchCell beforehand; it might be your cell type
            let cell = self.tableVIew.dequeueReusableCell(withIdentifier: "SearchCell", for: indexPath) as! SearchCell
    
            cell.label.text = self.searchSource?[indexPath.row]
    //            + " " + searchResult.subtitle
    
            return cell
        }
    }
    
    extension ViewController: MKLocalSearchCompleterDelegate {
        func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
            //get result, transform it to our needs and fill our dataSource
            self.searchSource = completer.results.map { $0.title }
            DispatchQueue.main.async {
                self.tableVIew.reloadData()
            }
        }
    
        func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
            //handle the error
            print(error.localizedDescription)
        }
    }
    

    【讨论】:

    • 考虑为答案添加更多上下文,而不仅仅是发布代码。即使是 cmets 形式的上下文也会很有用。
    • 感谢您的评论。我添加了一些上下文 cmets。
    • 如何在选定的位置添加图钉??
    • 您应该在委托的方法中处理选定的位置。
    【解决方案4】:

    此问题的示例项目可以从HERE下载

    在这个示例项目中,这个问题是通过 MKLocalSearchRequest 和 MapKit 实现的。

    它像谷歌地方 API 一样显示自动完成的地方,并且可以将注释点放置在 Apple 的地图上(不是在谷歌地图上,我希望只有你在寻找。)

    但是,它显示的结果不如您从 Google Places API 获得的准确结果。问题在于地理编码数据库显然并不完整,而且苹果不是领导该领域的公司——谷歌才是。

    附上一些示例应用的屏幕截图,以便您查看它是否对您的要求有用。

    希望这就是你要找的东西!

    【讨论】:

    • 无法在 swift 4 中使用该项目..给出许多错误..如果可能的话更新它。
    • 你能不能为swift 4更新这个我正需要这个@Apekshit
    猜你喜欢
    • 2010-10-03
    • 2011-07-19
    • 1970-01-01
    • 1970-01-01
    • 2013-10-24
    • 2017-02-07
    • 2021-02-15
    • 2023-03-12
    相关资源
    最近更新 更多