Skip To Content ArcGIS for Developers Sign In Dashboard

Overview

You will learn: how to build an app to search for coffee shops, gas stations, restaurants, and other nearby places.

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 an AGSLocator configured with the ArcGIS World Geocoding Service and the geocodeWithSearchText method. The method requires an AGSGeocodeParameters object that contains a preferred location to focus the search around 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 tutorial you will build an app to search for different places around the Santa Monica Mountains area.

Before you begin

Make sure you have installed the latest version of Xcode.

Reuse the starter project

If you have completed the Create a starter app tutorial, then copy the project into a new empty folder. Otherwise, download and unzip the project solution. Open the .xcodeproj file in Xcode. Run and verify the map displays in the device simulator.

Steps

Create an app configuration file

  1. Add a new swift file named AppConfiguration.swift. Use this file to specify constants that can be used by the app to connect to data and resources. Create a static URL for the ArcGIS World Geocoding Service. Additionally create two static geocode result attribute keys of type String. We'll use these resources in future steps.

    /** ADD **/
    extension URL {
        static let locator = URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!
    }
    
    extension String {
        static let placeAddress = "Place_addr"
        static let placeName = "PlaceName"
    }
    

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!
    

In ViewController.swift, declare that ViewController adopts the UIPickerViewDataSource and UIPickerViewDelegate protocols. Leave the implementation of these protocols empty for now. ::: labs-info Note, Xcode will highlight that you have not yet implemented required methods of the UIPickerViewDataSource protocol. You will implement these required methods in a future step. :::

```swift
/** ADD */
extension ViewController: UIPickerViewDataSource { }
extension ViewController: UIPickerViewDelegate { }
```

Create a Category model

  1. Add a new swift file named Category.swift. Use this file to define a category model to map categories to colors for displaying search results in the map. Create a struct named Category with two properties, title and color.

    /** ADD **/
    struct Category {
        let title: String
        let color: UIColor
    }
    
  2. In Category.swift create a static collection of Category objects. These category objects will be used to populate the picker view with values and to color geocode result graphics in the map.

    /** ADD **/
    extension Category {
        static var all: [Category] {
            [
                Category(title: "Coffee shop", color: .brown),
                Category(title: "Gas station", color: .orange),
                Category(title: "Food", color: .cyan),
                Category(title: "Hotel", color: .blue),
                Category(title: "Neighborhood", color: .black),
                Category(title: "Parks and Outdoors", color: .green)
            ]
        }
    }
    

Show graphics in the map view's callout

  1. Add a new method named showCallout to ViewController to show a callout in the map view when a user taps a place graphic. The method should take a single graphic parameter and show the map view's callout with information provided by the graphic's attributes. You will call this method in a future step.

    /** ADD **/
    private func showCallout(for graphic: AGSGraphic) {
        mapView.callout.title = graphic.attributes[String.placeName] as? String ?? "Unknown"
        mapView.callout.detail = graphic.attributes[String.placeAddress] as? String ?? "no address provided"
        mapView.callout.isAccessoryButtonHidden = true
        mapView.callout.show(
            for: graphic,
            tapLocation: (graphic.geometry as? AGSPoint),
            animated: true
        )
    }
    
  2. Add a new method named hideCallout to ViewController to dismiss the map view's callout when the user requests new information. You will call this method in a future step.

    /** ADD **/
    private func hideCallout() {
        mapView.callout.dismiss()
    }
    

Setup a graphics overlay

  1. Add a placesOverlay member variable to ViewController to support adding graphics to the map. This graphics overlay is used to display graphics derived from geocode results on a map.

    /** ADD **/
    private let placesOverlay = AGSGraphicsOverlay()
    
  2. Update the setupMap method by adding the placesOverlay graphics overlay to the map view.

    /** UPDATE **/
    private func setupMap() {
        mapView.map = AGSMap(
            basemapType: .navigationVector,
            latitude: 34.09042,
            longitude: -118.71511,
            levelOfDetail: 10
        )
        /** ADD **/
        mapView.graphicsOverlays.add(placesOverlay)
    }
    
  3. Add a new method named addPlacesGraphics to ViewController to add place graphics to the map. This method takes two parameters, a Category and a collection of geocode results (AGSGeocodeResult). The method creates a marker symbol using the category color and then iterates the search results to create a graphic for each. You will call this method in a future step.

    /** ADD **/
    private func addPlacesGraphics(_ results: [AGSGeocodeResult], for category: Category) {
        // Build place marker symbol.
        let symbol: AGSSimpleMarkerSymbol = {
            let placeSymbol = AGSSimpleMarkerSymbol(style: .circle, color: category.color, size: 10.0)
            placeSymbol.outline = AGSSimpleLineSymbol(style: .solid, color: .white, width: 2)
            return placeSymbol
        }()
    
        // Represent each place as a dot on the map.
        // Each graphic symbol gets the place attributes and later shows them when the user taps the graphic.
        let places = results.map { (result) in
            AGSGraphic(
                geometry: result.displayLocation,
                symbol: symbol,
                attributes: result.attributes
            )
        }
        placesOverlay.graphics.addObjects(from: places)
    }
    
  4. Add a new method named clearPlacesGraphics to ViewController to remove all places graphics from the places graphics overlay. You will call this method in a future step.

    /** ADD **/
    private func clearPlacesGraphics() {
        placesOverlay.graphics.removeAllObjects()
    }
    

Set up the locator task

  1. Add a locatorTask member variable to ViewController to support the search operation. This locator task is used each time a new search is requested. The AGSLocatorTask object is constructed using the static URL defined in AppConfiguration.

    /** ADD **/
    private let locatorTask = AGSLocatorTask(url: .locator)
    
  2. Add a currentSearch member variable to ViewController to track the current search. This cancelable reference is used to cancel an existing running search operation if a new one is performed instead.

    /** ADD **/
    private var currentSearch: AGSCancelable?
    
  3. Add a new method named findPlaces to perform the geocode search operation. The new method takes a single parameter to indicate which category of places to search for using the Category model you created in a previous step. Verify the map has a valid visible area, otherwise a search should not be attempted.

    /** ADD **/
    private func findPlaces(forCategory category: Category) {
    
        guard let visibleArea = mapView.visibleArea else { return }
    }
    
  4. Clear any results from the previous search. Call hideCallout() to hide the map view's callout if it's shown and clearPlacesGraphics() to remove any graphics from the graphics overlay.

    private func findPlaces(forCategory category: Category) {
    
        guard let visibleArea = mapView.visibleArea else { return }
    
        /** ADD**/
        hideCallout()
        clearPlacesGraphics()
    }
    
  5. Cancel the previous locator geocode task, if there is one and it's still running.

    private func findPlaces(forCategory category: Category) {
    
        guard let visibleArea = mapView.visibleArea else { return }
    
        hideCallout()
        clearPlacesGraphics()
    
        /** ADD **/
        currentSearch?.cancel()
    }
    
  6. Create and configure geocode parameters for the search task using the Category passed to the method and the two geocode result fields to return that are defined in AppConfiguration.

    private func findPlaces(forCategory category: Category) {
    
        guard let visibleArea = mapView.visibleArea else { return }
    
        hideCallout()
        clearPlacesGraphics()
    
        currentSearch?.cancel()
    
        /** ADD **/
        let parameters: AGSGeocodeParameters = {
            let geocodeParameters = AGSGeocodeParameters()
            geocodeParameters.maxResults = 25
            geocodeParameters.resultAttributeNames.append(contentsOf: [.placeAddress, .placeName])
            geocodeParameters.preferredSearchLocation = visibleArea.extent.center
            geocodeParameters.categories = [category.title]
            return geocodeParameters
        }()
    }
    
  7. Perform the search by using the locatorTask with the category title and geocode parameters. Performing a geocode search is an asynchronous operation. When the locator task completes, verify the operation performed successfully and you received results. If you received results, add a place symbol graphic for each result.

    private func findPlaces(forCategory category: Category) {
    
        guard let visibleArea = mapView.visibleArea else { return }
    
        hideCallout()
        clearPlacesGraphics()
    
        currentSearch?.cancel()
    
        let parameters: AGSGeocodeParameters = {
            let geocodeParameters = AGSGeocodeParameters()
            geocodeParameters.maxResults = 25
            geocodeParameters.resultAttributeNames.append(contentsOf: [.placeAddress, .placeName])
            geocodeParameters.preferredSearchLocation = visibleArea.extent.center
            geocodeParameters.categories = [category.title]
            return geocodeParameters
        }()
    
        /** ADD **/
        currentSearch = locatorTask.geocode(withSearchText: "", parameters: parameters) { [weak self] (results, error) in
    
            guard let self = 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
            }
    
            self.addPlacesGraphics(results, for: category)
        }
    }
    

Integrate the picker view

  1. Update the ViewController's UIPickerViewDataSource extension by adding two required methods. These methods drive the configuration of the categories picker view using the static categories defined in Category.swift.

    /** UPDATE **/
    extension ViewController: UIPickerViewDataSource {
    
        /** ADD **/
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            1
        }
    
        /** ADD **/
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            Category.all.count
        }
    }
    
  2. Update the ViewController's UIPickerViewDelegate extension by adding two methods. These methods drive the response to a user's interaction with the categories picker view. The method findPlaces() is called when the user did select a row of the categories picker view.

    /** UPDATE **/
    extension ViewController: UIPickerViewDelegate {
    
        /** ADD **/
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            Category.all[row].title
        }
    
        /** ADD **/
        func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
            findPlaces(forCategory: Category.all[row])
        }
    }
    

Respond to map touch interaction

  1. Update the setupMap method by establishing ViewController as the map view's touch delegate (AGSGeoViewTouchDelegate). This will allow the view controller to respond when a user touches the map view.

    /** UPDATE **/
    private func setupMap() {
        mapView.map = AGSMap(
            basemapType: .navigationVector,
            latitude: 34.09042,
            longitude: -118.71511,
            levelOfDetail: 10
        )
        mapView.graphicsOverlays.add(placesOverlay)
        /** ADD **/
        mapView.touchDelegate = self
    }
    
  2. Extend ViewController to adopt the AGSGeoViewTouchDelegate protocol. Add a touch delegate method that responds to a user tapping the map view by performing an identify operation in the places graphics overlay. This will look for a place graphic near where the user tapped.

    /** ADD **/
    extension ViewController: AGSGeoViewTouchDelegate {
        func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
            hideCallout()
            mapView.identify(placesOverlay, screenPoint: screenPoint, tolerance: 10, returnPopupsOnly: false, maximumResults: 1) { [weak self] (result) in
                guard let self = self else { return }
                if let error = result.error {
                    print(error)
                    return
                }
                else if let graphic = result.graphics.first {
                    self.showCallout(for: graphic)
                }
            }
        }
    }
    

Put it all together

  1. Add a new method named findPlacesForCategoryPickerSelection to find places based on the category selection made in the categories picker view. This method builds a bridge between the picker view UI and the find places search operation.

    /** ADD **/
    private func findPlacesForCategoryPickerSelection() {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            let categoryIndex = self.categoryPicker.selectedRow(inComponent: 0)
            guard categoryIndex < Category.all.count else { return }
            let category = Category.all[categoryIndex]
            self.findPlaces(forCategory: category)
        }
    }
    
  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.

    /** ADD **/
    private var isNavigatingObservation: NSKeyValueObservation?
    
    /** ADD **/
    private func observeChangesInMapViewIsNavigating() {
        // The map view `.isNavigating` property determines if the map view's visible area has changed.
        isNavigatingObservation = mapView.observe(\.isNavigating) { [weak self] (mapView, _) in
            // Find places for a selected category when the map view stops navigating.
            guard !mapView.isNavigating else { return }
            // Update results if the map view extent has moved.
            self?.findPlacesForCategoryPickerSelection()
        }
    }
    
  3. When the map view sets it's visible area for the first time, 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. Add a key/value observer on the map view to notify ViewController that the initial visible area has been set. Once the initial visible area has been set, call findPlacesForCategoryPickerSelection.

    /** ADD **/
    private var visibleAreaObservation: NSKeyValueObservation?
    
    /** ADD **/
    private func observeChangesInMapViewVisibleArea() {
        // Use the map view `.visibleArea` property to observe when the view sets its visible area for the first time.
        visibleAreaObservation = mapView.observe(\.visibleArea) { [weak self] (mapView, _) in
            guard let self = self else { return }
            // When the visible area is set for the first time, kick off the first find places query.
            self.findPlacesForCategoryPickerSelection()
            // Nullify (thus invalidating) the visible area observation because the first find places query is performed.
            self.visibleAreaObservation = nil
        }
    }
    
  4. Run the app. Press Command-R to run the app in the iOS Simulator.

    (Note, as of 100.8 Runtime supports Metal. In order to run your app in a simulator you must meet some minimum requirements. You must be developing on macOS Catalina, using Xcode 11, and simulating iOS 13. If you do not meet these requirements, you should run the app on a physical iOS device.)

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 tutorial, 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 tutorial Display and track your location to learn more about how to do this.