Use apple's search location api from textfield

Is it possible to use apple's search location api from a UItextfield?

Accepted Reply

It's probably crashing because vc.testTxt is nil.


Most of the code in your tableView:didSelectRowAt: method is unnecessary. See below.


Your other view controller with the mapview has the textfield, right? So just make a new delegate method to send the address:


override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 
    let selectedItem = matchingItems[indexPath.row].placemark 
    handleMapSearchDelegate?.dropPinZoomIn(selectedItem) 
    dismiss(animated: true, completion: nil)
    let address = parseAddress(selectedItem)
   handleMapSearchDelegate?.receiveAddress(address)
}


Then, in your other view controller:


func receiveAddress(_ address: String) {
   testTxt.text = address
}


Does this make sense?

Replies

You can use MKLocalSearch to do the actual location search and MKLocalSearchCompleter for search autocomplete.

This hasn't worked for me. I can't find a way to search and display places without using the mapview.

What exactly is your goal? Also, please share any code snippets that are not working.

I have a *form* on a view controller. I'm going to need an address that the user will choose at a later time. Obviously, I let the user write in the address themselves but if the user doesn't know an address I want to save time by letting the search the textbox for that address. However, I dont know if this is possible. So I made another view controller and I'm using apple's search api from mapkit to search for addresses. I want to pull the address that the user has chosen and send it back to a label or textfield.


This is my code:


import UIKit
import MapKit
public class LocationSearchTable: UITableViewController {
   
   
    weak var handleMapSearchDelegate: HandleMapSearch?
    var matchingItems: [MKMapItem] = []
    var mapView: MKMapView?
     var valueToPass:String!
  
   
    func parseAddress(_ selectedItem:MKPlacemark) -> String {
       
        /
        let firstSpace = (selectedItem.subThoroughfare != nil &&
                            selectedItem.thoroughfare != nil) ? " " : ""
       
        /
        let comma = (selectedItem.subThoroughfare != nil || selectedItem.thoroughfare != nil) &&
                    (selectedItem.subAdministrativeArea != nil || selectedItem.administrativeArea != nil) ? ", " : ""
       
        /
        let secondSpace = (selectedItem.subAdministrativeArea != nil &&
                            selectedItem.administrativeArea != nil) ? " " : ""
       
        let addressLine = String(
            format:"%@%@%@%@%@%@%@",
            /
            selectedItem.subThoroughfare ?? "",
            firstSpace,
            /
            selectedItem.thoroughfare ?? "",
            comma,
            /
            selectedItem.locality ?? "",
            secondSpace,
            /
            selectedItem.administrativeArea ?? ""
        )
       
        return addressLine
       
       
    }
   
}
extension LocationSearchTable : UISearchResultsUpdating {
   
    public func updateSearchResults(for searchController: UISearchController) {
        guard let mapView = mapView,
            let searchBarText = searchController.searchBar.text else { return }
       
        let request = MKLocalSearchRequest()
        request.naturalLanguageQuery = searchBarText
        request.region = mapView.region
        let search = MKLocalSearch(request: request)
       
        search.start { response, _ in
            guard let response = response else {
                return
            }
            self.matchingItems = response.mapItems
            self.tableView.reloadData()
        }
       
    }
   
   
   
}
extension LocationSearchTable {
   
    override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return matchingItems.count
    }
   
    override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
        let selectedItem = matchingItems[indexPath.row].placemark
        cell.textLabel?.text = selectedItem.name
        cell.detailTextLabel?.text = parseAddress(selectedItem)
        return cell
    }
   
}
extension LocationSearchTable {
   
   
   
    override public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
       
     print("You selected cell # \(indexPath.row)!")
       
    let indexPath = tableView.indexPathForSelectedRow!
       
    let currentCell = tableView.cellForRow(at: indexPath)! as UITableViewCell
       
        valueToPass = currentCell.textLabel?.text
       
        let storyboard = UIStoryboard(name: "viewController", bundle: nil)
        let controller = storyboard.instantiateViewController(withIdentifier: "idk") as! ViewController
       
        self.present(controller, animated: true, completion: nil)
   
       
    }
   
   
    override public func prepare(for segue: UIStoryboardSegue, sender: Any?) {
           
            let index = segue.destination as! ViewController
           
            index.testLbl.text = valueToPass
           
           
       
    }
   
}

In your updateSearchResults:for: method, does the response closure ever get called? Maybe the MKLocalSearch object is being deinitialized right after you call search.start(). In that case, you need a strong reference to your MKLocalSearch object (i.e. a variable in your view controller).


If not, what else is not working? The rest of the code looks fine to me.

To be honest, I'm not very experienced with swift. I think the problem I am having is getting the value from the cell.


https://i.stack.imgur.com/F0Is9.png


In the link above I want to be able to pull the subtitle, which is the address.


Another, problem I am having is that my project is crashing with an error at: let indexPath = tableView.indexPathForSelectedRow!

because of:

fatal error: unexpectedly found nil while unwrapping an Optional value

Thanks so much for your help and patience.

Thanks for the details! On this forum, more details is always better since we don't know your project like you do. 😉


A few general tips:


1. Never get data from your UI. Always get data from an actual data source. In this case, get the information from your matchingItems array and parseAddress function directly instead of the cell's text.


2. Avoid force-unwrapping optionals if possible. For example:

let indexPath = tableView.indexPathForSelectedRow!

indexPathForSelectedRow is not guaranteed to be non-nil. Why is it nil in this case? Because you call:

tableView.deselectRow(at: indexPath, animated: false)

just before it. Because you deselect the selected row, there is no selected row. Thus, table.indexPathForSelectedRow is nil, and you get a crash if you to use it.


So, in your tableView:didSelectRowAt: method, you need to get the information from the data source itself, not the UI. Something like this:


override public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: false)
    print("You selected cell # \(indexPath.row)!")
    let selectedItem = matchingItems[indexPath.row].placemark
    let storyboard = UIStoryboard(name: "viewController", bundle: nil)
    let controller = storyboard.instantiateViewController(withIdentifier: "idk")
    controller.selectedAddress = parseAddress(selectedItem)
    present(controller, animated: true, completion: nil)
}


In your other view controller, set your label to the selected text:


public var selectedAddress: String?

override public func viewDidLoad() {
   super.viewDidLoad()
   testLbl.text = selectedAddress
}

Hello, So I tweaked my code a little bit because I wasn't getting the value of the cell that I wanted.


So I have a navigation controller that consist of a view controller and a UITableViewController. The view controller contains a mapview and a textfield and the tableviewcontroller searches for the locations.


https://i.stack.imgur.com/BTux8.png


When i choose a location in the table view controller, I would like to be able to store the address in the textfield. I can get the address that I want to print


https://i.stack.imgur.com/BLkRI.png


However whenever I try to assign that address to the textfield, the app crashes with a "fatal error: unexpectedly found nil while unwrapping an Optional value" Does you know why? This is my tableview controller code:



import UIKit
import MapKit
class LocationSearchTable: UITableViewController {
  
  
    weak var handleMapSearchDelegate: HandleMapSearch?
    var matchingItems: [MKMapItem] = []
    var mapView: MKMapView?
  
  
    func parseAddress(_ selectedItem:MKPlacemark) -> String {
      
        /
        let firstSpace = (selectedItem.subThoroughfare != nil &&
                            selectedItem.thoroughfare != nil) ? " " : ""
      
        /
        let comma = (selectedItem.subThoroughfare != nil || selectedItem.thoroughfare != nil) &&
                    (selectedItem.subAdministrativeArea != nil || selectedItem.administrativeArea != nil) ? ", " : ""
      
        /
        let secondSpace = (selectedItem.subAdministrativeArea != nil &&
                            selectedItem.administrativeArea != nil) ? " " : ""
      
        let addressLine = String(
            format:"%@%@%@%@%@%@%@",
            /
            selectedItem.subThoroughfare ?? "",
            firstSpace,
            /
            selectedItem.thoroughfare ?? "",
            comma,
            /
            selectedItem.locality ?? "",
            secondSpace,
            /
            selectedItem.administrativeArea ?? ""
          
        )
      
        return addressLine
    }
  

  
}
extension LocationSearchTable : UISearchResultsUpdating {
  
    func updateSearchResults(for searchController: UISearchController) {
        guard let mapView = mapView,
            let searchBarText = searchController.searchBar.text else { return }
      
        let request = MKLocalSearchRequest()
        request.naturalLanguageQuery = searchBarText
        request.region = mapView.region
        let search = MKLocalSearch(request: request)
      
        search.start { response, _ in
            guard let response = response else {
                return
            }
            self.matchingItems = response.mapItems
            self.tableView.reloadData()
          
         
        }
      
    }
  
}
extension LocationSearchTable {
  
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return matchingItems.count
    }
  
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
        let selectedItem = matchingItems[indexPath.row].placemark
        cell.textLabel?.text = selectedItem.name
        cell.detailTextLabel?.text = parseAddress(selectedItem)
  
      
        return cell
    }
  
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let selectedItem = matchingItems[indexPath.row].placemark
        handleMapSearchDelegate?.dropPinZoomIn(selectedItem)
        dismiss(animated: true, completion: nil)
      
         let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
      
      
         cell.detailTextLabel?.text = parseAddress(selectedItem)
     
     
      
        DispatchQueue.main.async {
        let right = cell.detailTextLabel?.text
        print (right!)
        let vc = ViewController()
        vc.testTxt.text = right!
          
        }
  
}
}


vc.testTxt.text = right! is crashing the app with a nil value

It's probably crashing because vc.testTxt is nil.


Most of the code in your tableView:didSelectRowAt: method is unnecessary. See below.


Your other view controller with the mapview has the textfield, right? So just make a new delegate method to send the address:


override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 
    let selectedItem = matchingItems[indexPath.row].placemark 
    handleMapSearchDelegate?.dropPinZoomIn(selectedItem) 
    dismiss(animated: true, completion: nil)
    let address = parseAddress(selectedItem)
   handleMapSearchDelegate?.receiveAddress(address)
}


Then, in your other view controller:


func receiveAddress(_ address: String) {
   testTxt.text = address
}


Does this make sense?

Yes, this makes a lot more sense! Thanks!


However, the address is not being returne to the textbox, Do I need to do anything in the viewdidload function?

Is the text field connected to an @IBOutlet? Otherwise, what does debugging show?


func receiveAddress(_ address: String) {
    print("textField: \(testTxt)")
    print("text: \(address)")
    testTxt.text = address
}

Yes it is connected to an IBOutlet. It looks like func receive address isn't even ran because nothing I print from that function is printing. My code is:



import UIKit
import MapKit
protocol HandleMapSearch: class {
    func receiveAddress(_ address: String)
}
class ViewController: UIViewController {
   

    @IBOutlet weak var testTxt: UITextField!
   
    var selectedPin: MKPlacemark?
    var resultSearchController: UISearchController!
   
    let locationManager = CLLocationManager()
   
   
   
    @IBOutlet weak var mapView: MKMapView!
   
   
    override func viewDidLoad() {
        super.viewDidLoad()
    
   
       
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        locationManager.requestLocation()
        let locationSearchTable = storyboard!.instantiateViewController(withIdentifier: "LocationSearchTable") as! LocationSearchTable
        resultSearchController = UISearchController(searchResultsController: locationSearchTable)
        resultSearchController.searchResultsUpdater = locationSearchTable
        let searchBar = resultSearchController!.searchBar
        searchBar.sizeToFit()
        searchBar.placeholder = "Search for places"
        navigationItem.titleView = resultSearchController?.searchBar
        resultSearchController.hidesNavigationBarDuringPresentation = false
        resultSearchController.dimsBackgroundDuringPresentation = true
        definesPresentationContext = true
        locationSearchTable.mapView = mapView
   
       
        mapView.isHidden = true
       
    }
   
    func getDirections(){
        guard let selectedPin = selectedPin else { return }
        let mapItem = MKMapItem(placemark: selectedPin)
        let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
        mapItem.openInMaps(launchOptions: launchOptions)
    }
   
    }
extension ViewController : CLLocationManagerDelegate {
   
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if status == .authorizedWhenInUse {
            locationManager.requestLocation()
        }
    }
   
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.first else { return }
        let span = MKCoordinateSpanMake(0.05, 0.05)
        let region = MKCoordinateRegion(center: location.coordinate, span: span)
        mapView.setRegion(region, animated: true)
    }
   
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("error:: \(error)")
    }
   
   
}
extension ViewController: HandleMapSearch {
    func receiveAddress(_ address: String) {
        print("textField: \(testTxt)")
        print("text: \(address)")
        testTxt.text = address
    }
}
extension ViewController : MKMapViewDelegate {
   
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
       
        guard !(annotation is MKUserLocation) else { return nil }
        let reuseId = "pin"
        var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
        if pinView == nil {
            pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
        }
        pinView?.pinTintColor = UIColor.orange
        pinView?.canShowCallout = true
        let smallSquare = CGSize(width: 30, height: 30)
        let button = UIButton(frame: CGRect(origin: CGPoint.zero, size: smallSquare))
        button.setBackgroundImage(UIImage(named: "car"), for: UIControlState())
        button.addTarget(self, action: #selector(ViewController.getDirections), for: .touchUpInside)
        pinView?.leftCalloutAccessoryView = button
       
        return pinView
    }
}

You need to set handleMapSearchDelegate in your map view controller's viewDidLoad.


locationSearchTable.delegate = self

Thanks so much for your help!