【问题标题】:SwiftUI Using MapKit for Address Auto CompleteSwiftUI 使用 MapKit 进行地址自动完成
【发布时间】:2022-01-03 21:25:40
【问题描述】:

我有一个表单,用户可以在其中输入他们的地址。虽然他们总是可以手动输入,但我也想为他们提供一个简单的自动完成解决方案,这样他们就可以开始输入他们的地址,然后从列表中点击正确的地址并让它自动填充各个字段。

我开始使用 jnpdx 的 Swift5 解决方案 - https://stackoverflow.com/a/67131376/11053343

但是,有两个问题我似乎无法解决:

  1. 我需要将结果仅限于美国(不仅仅是美国大陆,而是整个美国,包括阿拉斯加、夏威夷和波多黎各)。我知道 MKCoordinateRegion 如何处理中心点和缩放传播,但它似乎不适用于地址搜索的结果。

  2. 返回的结果仅提供标题和副标题,我需要在其中实际提取所有单独的地址信息并填充我的变量(即地址、城市、州、zip 和 zip 分机)。如果用户有一个 apt 或 suite 号码,他们会自己填写。我的想法是创建一个在点击按钮时运行的函数,以便根据用户的选择分配变量,但我不知道如何提取所需的各种信息。 Apple 的文档和往常一样糟糕,我还没有找到任何解释如何做到这一点的教程。

这是最新的 SwiftUI 和 XCode (ios15+)。

我创建了一个用于测试的虚拟表单。这是我所拥有的:

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
        searchCompleter.region = MKCoordinateRegion()
        searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
        
        $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) {
        //currentPromise?(.failure(error))
    }
}

struct MapKit_Interface: View {

        @StateObject private var mapSearch = MapSearch()
        @State private var address = ""
        @State private var addrNum = ""
        @State private var city = ""
        @State private var state = ""
        @State private var zip = ""
        @State private var zipExt = ""
        
        var body: some View {

                List {
                    Section {
                        TextField("Search", text: $mapSearch.searchTerm)

                        ForEach(mapSearch.locationResults, id: \.self) { location in
                            Button {
                                // Function code goes here
                            } label: {
                                VStack(alignment: .leading) {
                                    Text(location.title)
                                        .foregroundColor(Color.white)
                                    Text(location.subtitle)
                                        .font(.system(.caption))
                                        .foregroundColor(Color.white)
                                }
                        } // End Label
                        } // End ForEach
                        } // End Section

                        Section {
                        
                        TextField("Address", text: $address)
                        TextField("Apt/Suite", text: $addrNum)
                        TextField("City", text: $city)
                        TextField("State", text: $state)
                        TextField("Zip", text: $zip)
                        TextField("Zip-Ext", text: $zipExt)
                        
                    } // End Section
                } // End List

        } // End var Body
    } // End Struct

【问题讨论】:

    标签: swiftui mapkit mkcoordinateregion mklocalsearch


    【解决方案1】:

    由于没有人回复,我和我的朋友 Tolstoy 花了很多时间找出解决方案,我想我会把它发布给任何可能感兴趣的人。 Tolstoy 为 Mac 编写了一个版本,而我编写了这里显示的 iOS 版本。

    鉴于 Google 如何对其 API 的使用收费而 Apple 没有,此解决方案为您提供了表单的地址自动完成功能。请记住,它并不总是完美的,因为我们感谢 Apple 和他们的地图。同样,您必须将地址转换为坐标,然后将其转换为地标,这意味着当从完成列表中点击时,有些地址可能会发生变化。对于 99.9% 的用户来说,这可能不是问题,但我想我会提到它。

    在撰写本文时,我正在使用 XCode 13.2.1 和适用于 iOS 15 的 SwiftUI。

    我用两个 Swift 文件组织了它。一个保存类/结构(AddrStruct.swift),另一个是应用程序中的实际视图。

    AddrStruct.swift

    import SwiftUI
    import Combine
    import MapKit
    import CoreLocation
    
    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
            searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
            
            $searchTerm
                .debounce(for: .seconds(0.2), scheduler: RunLoop.main)
                .removeDuplicates()
                .flatMap({ (currentSearchTerm) in
                    self.searchTermToResults(searchTerm: currentSearchTerm)
                })
                .sink(receiveCompletion: { (completion) in
                    //handle error
                }, receiveValue: { (results) in
                    self.locationResults = results.filter { $0.subtitle.contains("United States") } // This parses the subtitle to show only results that have United States as the country. You could change this text to be Germany or Brazil and only show results from those countries.
                })
                .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))
        }
    }
    
    struct ReversedGeoLocation {
        let streetNumber: String    // eg. 1
        let streetName: String      // eg. Infinite Loop
        let city: String            // eg. Cupertino
        let state: String           // eg. CA
        let zipCode: String         // eg. 95014
        let country: String         // eg. United States
        let isoCountryCode: String  // eg. US
    
        var formattedAddress: String {
            return """
            \(streetNumber) \(streetName),
            \(city), \(state) \(zipCode)
            \(country)
            """
        }
    
        // Handle optionals as needed
        init(with placemark: CLPlacemark) {
            self.streetName     = placemark.thoroughfare ?? ""
            self.streetNumber   = placemark.subThoroughfare ?? ""
            self.city           = placemark.locality ?? ""
            self.state          = placemark.administrativeArea ?? ""
            self.zipCode        = placemark.postalCode ?? ""
            self.country        = placemark.country ?? ""
            self.isoCountryCode = placemark.isoCountryCode ?? ""
        }
    }
    

    出于测试目的,我调用了我的主视图文件 Test.swift。这是一个精简版供参考。

    Test.swift

    import SwiftUI
    import Combine
    import CoreLocation
    import MapKit
    
    struct Test: View {
        @StateObject private var mapSearch = MapSearch()
    
        func reverseGeo(location: MKLocalSearchCompletion) {
            let searchRequest = MKLocalSearch.Request(completion: location)
            let search = MKLocalSearch(request: searchRequest)
            var coordinateK : CLLocationCoordinate2D?
            search.start { (response, error) in
            if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate {
                coordinateK = coordinate
            }
    
            if let c = coordinateK {
                let location = CLLocation(latitude: c.latitude, longitude: c.longitude)
                CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
    
                guard let placemark = placemarks?.first else {
                    let errorString = error?.localizedDescription ?? "Unexpected Error"
                    print("Unable to reverse geocode the given location. Error: \(errorString)")
                    return
                }
    
                let reversedGeoLocation = ReversedGeoLocation(with: placemark)
    
                address = "\(reversedGeoLocation.streetNumber) \(reversedGeoLocation.streetName)"
                city = "\(reversedGeoLocation.city)"
                state = "\(reversedGeoLocation.state)"
                zip = "\(reversedGeoLocation.zipCode)"
                mapSearch.searchTerm = address
                isFocused = false
    
                    }
                }
            }
        }
    
        // Form Variables
    
        @FocusState private var isFocused: Bool
    
        @State private var btnHover = false
        @State private var isBtnActive = false
    
        @State private var address = ""
        @State private var city = ""
        @State private var state = ""
        @State private var zip = ""
    
    // Main UI
    
        var body: some View {
    
                VStack {
                    List {
                        Section {
                            Text("Start typing your street address and you will see a list of possible matches.")
                        } // End Section
                        
                        Section {
                            TextField("Address", text: $mapSearch.searchTerm)
    
    // Show auto-complete results
                            if address != mapSearch.searchTerm && isFocused == false {
                            ForEach(mapSearch.locationResults, id: \.self) { location in
                                Button {
                                    reverseGeo(location: location)
                                } label: {
                                    VStack(alignment: .leading) {
                                        Text(location.title)
                                            .foregroundColor(Color.white)
                                        Text(location.subtitle)
                                            .font(.system(.caption))
                                            .foregroundColor(Color.white)
                                    }
                            } // End Label
                            } // End ForEach
                            } // End if
    // End show auto-complete results
    
                            TextField("City", text: $city)
                            TextField("State", text: $state)
                            TextField("Zip", text: $zip)
    
                        } // End Section
                        .listRowSeparator(.visible)
    
                } // End List
    
                } // End Main VStack
    
        } // End Var Body
    
    } // End Struct
    
    struct Test_Previews: PreviewProvider {
        static var previews: some View {
            Test()
        }
    }
    

    【讨论】:

    • 有用!谢谢。
    【解决方案2】:

    如果有人想知道如何生成全局结果,请更改以下代码:

    self.locationResults = results.filter{$0.subtitle.contains("United States")}
    

    地址结构文件中的这个:

    self.locationResults = results
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-10-24
      • 2021-02-15
      • 2023-03-12
      • 2020-09-15
      • 2016-08-06
      • 1970-01-01
      • 2022-06-14
      相关资源
      最近更新 更多