如何使用 Apple Map Kit 实现地址自动完成
How to implement auto-complete for address using Apple Map Kit
我想为用户自动完成地址,就像 google api 在此 link 中提供的一样:
https://developers.google.com/maps/documentation/javascript/places-autocomplete?hl=en
如何使用 apple map kit 实现相同的功能?
我试过使用 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?
谢谢
更新 - 我使用 Swift 3 创建了一个简单的示例项目 here,因为原始答案写在 Swift 2.
在 iOS 9.3 中引入了一个名为 MKLocalSearchCompleter
的新 class,这允许创建自动完成解决方案,您只需按如下所示传入 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
}
}
我的回答完全基于@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 { [=10=].title }
DispatchQueue.main.async {
self.tableVIew.reloadData()
}
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//handle the error
print(error.localizedDescription)
}
}
SAMPLE PROJECT FOR THIS ISSUE CAN BE DOWNLOAD FROM HERE
在这个示例项目中,这个问题是通过 MKLocalSearchRequest 和 MapKit 实现的。
它显示自动完成的地方就像 google 地方 API 并且可以在 Apple 的地图上放置注释点(而不是在 Google 地图上,我希望只有你在看对于。)
但是,它显示的结果不如您从 Google 个地方 API 获得的结果准确。由于问题是地理编码数据库显然不完整而且 Apple 不是领导该领域的公司 - Google 是。
附上示例应用程序的一些屏幕截图,以便您查看它是否对您的要求有用。
Hope this is what you are looking for!
Swift 5 + 合并 + (可选)SwiftUI 解决方案
似乎有许多关于其他解决方案的评论希望与 Swift 的更新版本兼容。另外,人们似乎(就像我一样)也需要 SwiftUI 解决方案。
这建立在之前的建议之上,但使用 Combine 来监视输入、去抖动,然后通过 Publisher 提供结果。
MapSearch
ObservableObject
很容易用在 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))
}
}
我想为用户自动完成地址,就像 google api 在此 link 中提供的一样:
https://developers.google.com/maps/documentation/javascript/places-autocomplete?hl=en
如何使用 apple map kit 实现相同的功能?
我试过使用 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?
谢谢
更新 - 我使用 Swift 3 创建了一个简单的示例项目 here,因为原始答案写在 Swift 2.
在 iOS 9.3 中引入了一个名为 MKLocalSearchCompleter
的新 class,这允许创建自动完成解决方案,您只需按如下所示传入 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
}
}
我的回答完全基于@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 { [=10=].title }
DispatchQueue.main.async {
self.tableVIew.reloadData()
}
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//handle the error
print(error.localizedDescription)
}
}
SAMPLE PROJECT FOR THIS ISSUE CAN BE DOWNLOAD FROM HERE
在这个示例项目中,这个问题是通过 MKLocalSearchRequest 和 MapKit 实现的。
它显示自动完成的地方就像 google 地方 API 并且可以在 Apple 的地图上放置注释点(而不是在 Google 地图上,我希望只有你在看对于。)
但是,它显示的结果不如您从 Google 个地方 API 获得的结果准确。由于问题是地理编码数据库显然不完整而且 Apple 不是领导该领域的公司 - Google 是。
附上示例应用程序的一些屏幕截图,以便您查看它是否对您的要求有用。
Hope this is what you are looking for!
Swift 5 + 合并 + (可选)SwiftUI 解决方案
似乎有许多关于其他解决方案的评论希望与 Swift 的更新版本兼容。另外,人们似乎(就像我一样)也需要 SwiftUI 解决方案。
这建立在之前的建议之上,但使用 Combine 来监视输入、去抖动,然后通过 Publisher 提供结果。
MapSearch
ObservableObject
很容易用在 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))
}
}