我有一个表单,用户可以在其中输入地址。虽然他们总是可以手动输入,但我也想为他们提供一个简单的自动填写解决方案,这样他们就可以开始键入地址,然后点击列表中的正确地址,并自动填充各个字段。
我开始使用jnpdx的Swift5解决方案-https://stackoverflow.com/a/67131376/11053343
然而,有两个问题我似乎无法解决:
-
我需要的结果仅限于美国(不仅是美国大陆,还有整个美国,包括阿拉斯加、夏威夷和波多黎各(。我知道MKCoordinateRegion是如何处理中心点和缩放展开的,但它似乎对地址搜索的结果不起作用。
-
结果的返回只提供了一个标题和副标题,在这里我需要实际提取所有的个人地址信息并填充我的变量(即地址、城市、州、zip和zip文本(。如果用户有一个apt或套件编号,他们就会自己填写。我的想法是创建一个在点击按钮时运行的函数,以便根据用户的选择分配变量,但我不知道如何提取所需的各种信息。苹果的文档和往常一样糟糕,我还没有找到任何解释如何做到这一点的教程。
这是最新的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
由于没有人回应,我和我的朋友托尔斯泰花了很多时间来找出解决方案,我想我会把它发布给其他可能感兴趣的人。托尔斯泰为Mac写了一个版本,而我写了这里显示的iOS版本。
鉴于谷歌对其API的使用收费,而苹果则不然,该解决方案为您提供了表单的地址自动完成功能。请记住,它并不总是完美的,因为我们要感谢苹果和他们的地图。同样,你必须将地址转换为坐标,然后将其转换为占位符,这意味着当从完成列表中点击时,可能会有一些地址发生变化。对于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。这里有一个精简版供参考。
测试无线
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()
}
}
如果有人想知道如何生成全局结果,请更改以下代码:
self.locationResults = results.filter{$0.subtitle.contains("United States")}
地址结构文件:
self.locationResults = results