Find places

Learn how to search for businesses, administrative boundaries, and geographic locations with the geocoding service.

This tutorial shows you how to search for businesses, administrative boundaries, and geographic locations with the geocoding service. You will use a UI to allow users to select a category of places, for example, coffee shops or gas stations, and click on the map to find candidates that match. The places found will be displayed in the map.

The following are required for this tutorial:

  1. An ArcGIS account to access your API keys. If you don't have an account, sign up for free.
  2. Your system meets the system requirements.
  3. The ArcGIS Runtime API for iOS is installed.

Steps

Add a category picker

  1. Open main.storyboard and select the Map View. In the Object library select a Picker View and drag it over the View containing the Map View such that it becomes a child of the View.

    • Adjust the height of the Picker View to approximately 70 pixels and is anchored to the bottom of the View.

    • Adjust the height of the Map View anchor to the top of the Picker View.

    • Set the data source outlet to the view controller.

    • Set the delegate outlet to the view controller.

    • Control-drag the picker view to the view controller to define a new referencing outlet. Name the outlet categoryPicker. Your ViewController.swift should have the following code in it:

     
    @IBOutlet weak var categoryPicker: UIPickerView!
  2. In the ViewController.swift file, update the class declaration to add the Picker View delegate and data source interfaces. Also add a touch delegate for the map view:

    The AGSGeoViewTouchDelegate is a protocol you adopt to be notified about touch events on the map view. You use this to identify places on the map and display their attributes.

     
    class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, AGSGeoViewTouchDelegate {
  3. Add a member variable to hold the available place categories to search for. This is used to populate a picker view allowing the user to select a category. Each category is identified with a different color graphic on the map. Declare an array named categories and initialize it with the available categories and colors. The code should appear as follows:

    This tutorial uses category filtering to provide accurate search results based on pre-determined place categories. Feel free to modify this list to your specific requirements.

    This technique uses typealias to define the structure of each element of the array.

      
    private typealias Category = (title: String, color: UIColor)
    private let categories: [Category] = [("Coffee shop", .brown), ("Gas station", .orange), ("Food", .cyan), ("Hotel", .blue), ("Neighborhood", .black), ("Parks and Outdoors", .green)]

Set up the LocatorTask

  1. Add member variables to support the search operation. This includes creating a locatorTask that is used each time a new search is requested. cancelableGeocodeTask is a means to cancel a search in progress if another one is requested. graphicsOverlay displays search results on the map. AttributeKeys defines which attributes in the search results you are interested in. isNavigatingObserver is a key value observer protocol to monitor map navigation, and when navigation stops, perform a new search using the updated visible area of the map.

    To learn more about AGSLocatorTask, go to the iOS API Reference.

    Use a GraphicsOverlay to display temporary, non-persistent information on a map in its own layer. Visit the display point line and polygon graphics tutorial to learn more about graphics and graphics overlays.

              
    private let locatorTask = AGSLocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!)
    private var cancelableGeocodeTask: AGSCancelable?
    
    private let graphicsOverlay = AGSGraphicsOverlay()
    private var isNavigatingObserver: NSKeyValueObservation?
    
    private struct AttributeKeys {
        static let placeAddress = "Place_addr"
        static let placeName = "PlaceName"
    }
    
  2. Create a new member function to perform the geocode search operation. The new function takes a single parameter to indicate which category of places to search for using the Category type alias you created in a previous step. Verify the map has a valid visible area, otherwise a search should not be attempted.

    The search is based on the center point of the visible area of the map. It's possible this may be invalid if the map did not finish loading or completed rendering in the view. In that case you should not perform the search. This is not an issue with the geocoding service, it's just how we built this particular tutorial.

       
    private func findPlaces(forCategory category: Category) {
        guard let visibleArea = mapView.visibleArea else { return }
    }
    
  3. Add to the findPlaces method a means to clear results from a prior search and to cancel a search if a prior request is still in progress.

       
        mapView.callout.dismiss()
        graphicsOverlay.graphics.removeAllObjects()
        cancelableGeocodeTask?.cancel()
    
  4. Configure the parameters for the search task. Add to the findPlaces method an AGSGeocodeParameters object and fill it in with the search location, number of results to return, and the result attributes your app is interested in.

    Find out more about geocoding output attributes in the REST API documentation and the iOS API Reference.

        
        let geocodeParameters = AGSGeocodeParameters()
        geocodeParameters.preferredSearchLocation = visibleArea.extent.center
        geocodeParameters.maxResults = 25
        geocodeParameters.resultAttributeNames.append(contentsOf: [AttributeKeys.placeAddress, AttributeKeys.placeName])
  5. Perform the search by using the locatorTask with the category title to search for and the geocode parameters created in the prior step. Performing a geocode search is an asynchronous operation. When the locator task completes, verify the operation performed successfully and you received results. Iterate over the results and represent each located place as a simple marker symbol on the map. Each graphic symbol also holds the place attributes so you can recall them when the user taps the graphic.

    Since the closure is called with a weak reference to self, if the class has not deallocated you need to check and maintain a strong reference to self for the duration of the closure with strongSelf.

    Populate the graphicsOverlay with simple marker symbols representing each place returned in the search results. This is very similar to the display point line and polygon graphics tutorial.

                         
        cancelableGeocodeTask = locatorTask.geocode(withSearchText: category.title, parameters: geocodeParameters) { [weak self] (results: [AGSGeocodeResult]?, error: Error?) -> Void in
    
            guard let strongSelf = self else { return }
    
            guard error == nil else {
                print("geocode error", error!.localizedDescription)
                return
            }
    
            guard let results = results, results.count > 0 else {
                print("No places found for category", category.title)
                return
            }
    
            for result in results {
                let placeSymbol = AGSSimpleMarkerSymbol(style: .circle, color: category.color, size: 10.0)
                placeSymbol.outline = AGSSimpleLineSymbol(style: .solid, color: .white, width: 2)
                let graphic = AGSGraphic(geometry: result.displayLocation, symbol: placeSymbol, attributes: result.attributes as [String : AnyObject]?)
                strongSelf.graphicsOverlay.graphics.add(graphic)
            }
        }
    
  6. Create a new method to read the current selection in the UIPickerView and call the findPlaces method with the selected category.

    It is good coding practice to encapsulate different operations into small and separate methods. This way the concern with getting the user's category selection is separate from searching for a particular category. This practice gives your code flexibility.

          
    private func findPlacesForCategoryPickerSelection() {
        let categoryIndex = categoryPicker.selectedRow(inComponent: 0)
        guard categoryIndex < categories.count else { return }
        let category = categories[categoryIndex]
        findPlaces(forCategory: category)
    }
    
  7. Create a new method to show the place attributes in a callout when the user taps a place on the map.

    A callout is a property of the map view. A callout can be used to display information on a map with a title and a detail string. Here you are mapping the attributes returned from the place search to the title and detail properties of the callout.

          
    private func showCalloutForGraphic(_ graphic:AGSGraphic, tapLocation:AGSPoint) {
        self.mapView.callout.title = graphic.attributes["PlaceName"] as? String ?? "Unknown"
        self.mapView.callout.detail = graphic.attributes["Place_addr"] as? String ?? "no address provided"
        self.mapView.callout.isAccessoryButtonHidden = true
        self.mapView.callout.show(for: graphic, tapLocation: tapLocation, animated: true)
    }
    

Add the data source and delegate protocol methods

  1. Add the UIPickerView data source interface methods to map requests to the categories array:

    Add these new methods at the bottom of ViewController, before the closing } of ViewController.

    UIPickerView is a standard UIKit component. Learn more at developer.apple.com.

           
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return categories.count
    }
    
  2. Add the UIPickerView delegate interface methods. This will map a request for a specific item to an element in the categories array. Once the user selects an item, perform a search for places matching the selected category.

    Add these new methods after the data source methods added in the prior step.

           
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return categories[row].title
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        findPlaces(forCategory: categories[row])
    }
    

Call identify when the user touches a result on the map

  1. Add a new method to respond to the GeoViewTouchDelegate protocol. When the user taps the map you want to use the identify operation to determine if there is a graphic symbol at that location corresponding to a place on the map. It is possible identify can return more than one result, in which case you only want to show the first result found.

    This method is called via the AGSGeoViewTouchDelegate protocol you set on the ViewController. When the user taps the map you are given the screen point and the corresponding map location.

    If a callout is open from a prior search, close it.

    Call identify on the map view to identify which graphics objects in the graphics overlay correspond with the screen point indicated by the user. If this identify operation returns results, call your showCalloutForGraphic method with the first graphic result identified and the corresponding map location.

                 
    func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
    
        self.mapView.callout.dismiss()
        self.mapView.identify(self.graphicsOverlay, screenPoint: screenPoint, tolerance: 10, returnPopupsOnly: false, maximumResults: 2) { (result: AGSIdentifyGraphicsOverlayResult) -> Void in
            guard result.error == nil else {
                print(result.error!)
                return
            }
            if let graphic = result.graphics.first {
                self.showCalloutForGraphic(graphic, tapLocation: mapPoint)
            }
        }
    }
    

Put it all together

  1. Update the setupMap method to add the touch delegate and the graphics overlay to the map view.

         
        mapView.map = AGSMap(basemapType: .navigationVector, latitude: 34.09042, longitude: -118.71511, levelOfDetail: 10)
    
        // *** ADD ***
        mapView.touchDelegate = self
        mapView.graphicsOverlays.add(graphicsOverlay)
    
  2. When the map finishes navigating to a new extent, perform a new search using the updated visible area. Add a key/value observer on the map view to listen for a change in isNavigating. Once map navigation completes, call findPlacesForCategoryPickerSelection.

            
        isNavigatingObserver = mapView.observe(\.isNavigating, options:[]) { (mapView, _) in
    
            guard !mapView.isNavigating else { return }
    
            DispatchQueue.main.async { [weak self] in
                self?.findPlacesForCategoryPickerSelection()
            }
        }
    
  3. When the map view loads the first time, use the viewpointChangedHandler to perform a search on the selected category. This way when the map loads and the picker view displays a category, there are search results to match.

    Once the search is preformed the first time, you no longer need this handler and you disable it.

          
        mapView.viewpointChangedHandler = { [weak self] () -> Void in
            DispatchQueue.main.async {
                self?.findPlacesForCategoryPickerSelection()
                self?.mapView.viewpointChangedHandler = nil
            }
        }
    
  4. Now press Command-R to run the app in the iOS Simulator.

    Other ways to run the project in Xcode:

    • In Xcode's app menu, select Product > Run.
    • Pressing the Run button at the top-left of the Xcode project window.

Congratulations, you're done!

Your map should load and center on the Malibu area near Los Angeles, California, with coffee shop locations displayed. You can touch one of the coffee shops and see it's place name and address. Use the picker to select different place categories to search for. Compare your solution with our completed solution project.