28

I want to auto-complete the address for the user as same as what google api provides in this link:

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

How can i implement the same functionality using apple map kit?

I have tried to use the Geo Coder, i wrote this for example:

@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)
            }
        }
    }
}

However the results are very disappointing.

Any Apple APIs available to implement such functionality, or should i head for google api ?

Thank you

Axel Guilmin
  • 11,454
  • 9
  • 54
  • 64
Mostafa
  • 1,522
  • 2
  • 18
  • 34

5 Answers5

92

Update - I've created a simple example project here using Swift 3 as the original answer was written in Swift 2.

In iOS 9.3 a new class called MKLocalSearchCompleter was introduced, this allows the creation of an autocomplete solution, you simply pass in the queryFragment as below:

var searchCompleter = MKLocalSearchCompleter()
searchCompleter.delegate = self
var searchResults = [MKLocalSearchCompletion]()

searchCompleter.queryFragment = searchField.text!

Then handle the results of the query using the MKLocalSearchCompleterDelegate:

extension SearchViewController: MKLocalSearchCompleterDelegate {

    func completerDidUpdateResults(completer: MKLocalSearchCompleter) {
        searchResults = completer.results
        searchResultsTableView.reloadData()
    } 

    func completer(completer: MKLocalSearchCompleter, didFailWithError error: NSError) {
        // handle error
    }
}

And display the address results in an appropriate format:

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
}

You can then use a MKLocalCompletion object to instantiate a MKLocalSearch.Request, thus gaining access to the MKPlacemark and all other useful data:

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
    }
}
Ever Uribe
  • 639
  • 6
  • 13
George McDonnell
  • 1,445
  • 1
  • 16
  • 21
14

Swift 5 + Combine + (Optionally) SwiftUI solution

There seem to be a number of comments on other solutions wanting a version compatible with more recent versions of Swift. Plus, It seems likely that (as I did), people will need a SwiftUI solution as well.

This builds on previous suggestions, but uses Combine to monitor the input, debounce it, and then provide results through a Publisher.

The MapSearch ObservableObject is easily used in SwiftUI (example provided), but could also be used in non-SwiftUI situations as well.

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 interface, including mapped locations


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))
    }
}

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • NICE!!! I'm on day 50 of #100DaysOfSwiftUI and this was the perfect addition. https://www.hackingwithswift.com/books/ios-swiftui/checking-for-a-valid-address – Mark Petereit Jul 18 '21 at 20:47
  • This is fantastic and way better than having to pay for Google's API. However, How would you rewrite this so that if you have a few TextFields like (address, city, state, zip) you can start typing your address in the address field and then select your address and have it autocomplete the other textfields? I don't need the detail view or to display a map at all, just the autocomplete for the form. I need each TextField to bind to it's own variable (i.e. $address, $city, $state, and $zip) instead of having a searchfield that binds to mapsearch. Thanks! – kittonian Jan 03 '22 at 19:15
  • @kittonian That sounds like a good project for you to take on and my answer should provide a good foundation for whatever your specific needs are. If you get stuck, you could start a question here for what the problem(s) are that you encounter. – jnpdx Jan 03 '22 at 19:24
  • Thanks jnpdx. I just posted my question. – kittonian Jan 03 '22 at 21:26
  • Here's the link: https://stackoverflow.com/questions/70571615/swiftui-using-mapkit-for-address-auto-complete – kittonian Jan 03 '22 at 23:10
  • The docs for MKLocalSearchCompleter say "The completer object waits a short amount of time before initiating new searches" so the debouncing and eliminating duplicates in the Combine code should be unnecessary. (https://developer.apple.com/documentation/mapkit/mklocalsearchcompleter/1452555-queryfragment) – jsadler Jan 06 '22 at 22:48
  • @jsadler Search for "MKLocalSearchCompleter rate limit" and you'll see that people do in fact run into issues. I think that debouncing is not necessarily unnecessary here. – jnpdx Jan 06 '22 at 23:24
4

My answer is fully based on @George McDonnell's. I hope it helps to guys who has troubles with implementing of the last one.

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)
    }
}
Gogi
  • 41
  • 3
1

SAMPLE PROJECT FOR THIS ISSUE CAN BE DOWNLOAD FROM HERE

In this sample project this issue is achieved through MKLocalSearchRequest and MapKit.

It is showing autocomplete places just like google places API and can place the Annotation point on Apple's map (not on Google map, I hope that is only you are looking for.)

However it does not show as accurate result as you can get from Google Places API. As the problem is that Geocoding database is not obviously complete and Apple is not the company who leads this field - Google is.

Attaching some screenshots of the sample app, so you can see if it is useful for your requirement or not.

Apple's autocomplete view.

Plotting the annotation point on Apple's map.

Hope this is what you are looking for!

Apekshit
  • 767
  • 2
  • 13
  • 27
1

Simple Solution - SwiftUI

How: searchText is linked to Textfield, when Textfield changes, searchText is queried (compared) against worldwide addresses.

The query's completion triggers completerDidUpdateResults which updates the SearchThis.swift list with those results (addresses).

SearchThis.swift (SwiftUI)

import SwiftUI
import Foundation

struct SearchThis : View {
    @StateObject var searchModel = SearchModel()
    
    var body: some View {
        VStack {
            TextField("Type Here", text: $searchModel.searchText)
                .onChange(of: searchModel.searchText) { newValue in
                    searchModel.completer.queryFragment = searchModel.searchText
                }
            List(searchModel.locationResult, id: \.self) { results in
                Button(results.title) {print("hi")}
            }
        }
    }
}

struct SearchThis_Previews: PreviewProvider {
    static var previews: some View {
        SearchThis()
    }
}

SearchModel.swift (Class)

import MapKit

class SearchModel: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
    @Published var searchText = ""
    @Published var locationResult: [MKLocalSearchCompletion] = []
    
    
    let completer = MKLocalSearchCompleter()
    
    override init() {
        super.init()
        completer.delegate = self
    }

    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        locationResult = completer.results
    }

    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        print(error.localizedDescription)
    }
}
Hao
  • 159
  • 1
  • 4