Overview

You will learn: how to search for coffee shops, gas stations, restaurants, and other places around a location.

The ArcGIS World Geocoding Service can find addresses and places, convert addresses to coordinates, and perform batch geocoding. If you want to create an application that searches for places such as coffee shops, gas stations, or restaurants, you can use a Locator with the ArcGIS World Geocoding Service and the geocodeWithSearchText method. The method requires a GeocodeParameters object that contains the location (point) and the search category (e.g., "Coffee Shop") to search for. You can add the returned results to a map, create a route, or integrate them further into your application. Visit the documentation to learn more about finding places and the capabilities of the geocoding service.

In this lab you will build an app to search for different places around the Santa Monica Mountains area.

Before you begin

You must have previously installed the ArcGIS Runtime SDK for iOS and set up your development environment. Please review the install and set up instructions if you have not done this.

Reuse the starter project

In a new or empty project folder, make a copy of the Create a starter app or download and unzip the project solution.

  • Open the .xcodeproj file in Xcode.

  • Run the project and verify the project builds and the map displays in the device simulator.

  • If you like, rename the project to find-places.

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:

    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:

    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.

    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.

    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.

        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.

        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.

    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.

    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:

    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.

    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.

    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.

        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.

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.

Challenge

Explore more categories

The World Geocoding service can find many different types of places. Explore the Level 1, Level 2, and Level 3 Categories and add them to search for additional places that you are interested in.

Improve the UI

In this lab, you display categories to search for with a persistent picker control constrained to the bottom of the map. Can you come up with a better way?

Use your current location

Instead of performing the search at a prescribed location, use the device's current location. Review the lab Display and track your location to learn more about how to do this.