Learn how to search for places of interest, such as hotels, cafes, and gas stations using the geocoding service.
Geocoding is the process of transforming an address or place name to a location on the earth's surface. A geocoding service allows you to quickly find places that meet specific criteria.
In this tutorial, you use a picklist in the user interface to select a category of places, for example, coffee shops or gas stations. You locate all the places that match this category by accessing a geocoding service. The places are displayed on the map so that you can click on them to get further information.
Prerequisites
Before starting this tutorial:
-
You need an ArcGIS Location Platform or ArcGIS Online account.
-
Your system meets the system requirements.
Steps
Get an access token
You need an access token to use the location services used in this tutorial.
-
Go to the Create an API key tutorial to obtain an access token.
-
Ensure that the following privileges are enabled: Location services > Basemaps > Basemap styles service and Location services > Geocoding.
-
Copy the access token as it will be used in the next step.
To learn more about other ways to get an access token, go to Types of authentication.
Open the Xcode project
-
To start the tutorial, complete the Display a map tutorial or download and unzip the solution.
-
Open the
.xcodeproj
file in Xcode. -
In Xcode, in the Project Navigator, click AppDelegate.swift.
-
In the editor, set the
API
property on theKey AGS
with your access token.ArcGIS Runtime Environment AppDelegate.swiftUse dark colors for code blocks func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { AGSArcGISRuntimeEnvironment.apiKey = "YOUR_ACCESS_TOKEN" return true }
Add a category picker
You will add a Picker View to the user interface to show categories of places to find, for example, coffee shops or gas stations. Each category will be displayed with a different color on the map.
-
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 anchor it 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
category
.Picker
UI
is a standard UIKit component. Learn more at developer.apple.com.Picker View ViewController.swiftUse dark colors for code blocks @IBOutlet weak var categoryPicker: UIPickerView!
-
-
In the
View
file, update the class declaration to add the Picker View delegate and data source protocols. Also add the touch delegate protocol (Controller.swift AGS
) for the map view:Geo View Touch Delegate The AGSGeoViewTouchDelegate is a protocol you adopt to be notified about touch events on the map view. You will use this to identify places on the map and display their attributes.
ViewController.swiftUse dark colors for code blocks class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, AGSGeoViewTouchDelegate {
-
Add an array of the place categories to search for. Use this to populate a Picker View to allow 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.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.ViewController.swiftUse dark colors for code blocks 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
A locator task is used to search for places using a geocoding service. Results from this search contain the place location and additional information (attributes). Create the locator task along with any variables and methods needed to perform the search and display the results.
-
Add the following variables to support the search operation.
locator
is used each time a new search is requestedTask cancelable
is a means to cancel a search that is in progressGeocode Task graphics
displays the search results on the mapOverlay is
is a key-value observer protocol to monitor map navigation. When navigation stops, a new search is performed using the updated visible area of the map.Navigating Observer Attribute
defines which attributes in the search results you are interested inKeys
See AGSLocatorTask for more information.
Use a AGSGraphicsOverlay to display temporary, non-persistent information on a map in its own layer. Visit the Add a point, line, and polygon tutorial to learn more about graphics and graphics overlays.
ViewController.swiftUse dark colors for code blocks private let locatorTask = AGSLocatorTask(url: URL(string: "https://geocode-api.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" }
-
Create a method called
find
to perform the geocode search operation. The method takes a single parameter to indicate which category of places to search for. This uses thePlaces 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 map must be loaded and layers rendered in the view in order for the
visible
property to be valid.Area ViewController.swiftUse dark colors for code blocks private func findPlaces(forCategory category: Category) { guard let visibleArea = mapView.visibleArea else { return } }
-
Add code to clear the results from a prior search and cancel a search if it is still in progress.
ViewController.swiftUse dark colors for code blocks private func findPlaces(forCategory category: Category) { guard let visibleArea = mapView.visibleArea else { return } // Clean up anything from the prior search. mapView.callout.dismiss() graphicsOverlay.graphics.removeAllObjects() cancelableGeocodeTask?.cancel() }
-
Configure the geocode parameters. Add an AGSGeocodeParameters object to the
find
method. Populate it with the search location, the number of results to return, and the result attributes you want to show.Places ViewController.swiftUse dark colors for code blocks private func findPlaces(forCategory category: Category) { guard let visibleArea = mapView.visibleArea else { return } // Clean up anything from the prior search. mapView.callout.dismiss() graphicsOverlay.graphics.removeAllObjects() cancelableGeocodeTask?.cancel() // Configure parameters for the search task. let geocodeParameters = AGSGeocodeParameters() geocodeParameters.preferredSearchLocation = visibleArea.extent.center geocodeParameters.maxResults = 25 geocodeParameters.resultAttributeNames.append(contentsOf: [AttributeKeys.placeAddress, AttributeKeys.placeName]) }
-
Call the
geocode
method on thelocator
using the category title and the geocode parameters. When the asynchronous geocode operation completes, verify that it completed successfully and that results were returned. Iterate over the results and display each location as a graphic with a simple marker symbol. Assign the result's attributes to the graphic.Task Populate the
graphics
with simple marker symbols representing each place returned in the search results. This is very similar to the Add a point, line, and polygon tutorial.Overlay ViewController.swiftUse dark colors for code blocks private func findPlaces(forCategory category: Category) { guard let visibleArea = mapView.visibleArea else { return } // Clean up anything from the prior search. mapView.callout.dismiss() graphicsOverlay.graphics.removeAllObjects() cancelableGeocodeTask?.cancel() // Configure parameters for the search task. let geocodeParameters = AGSGeocodeParameters() geocodeParameters.preferredSearchLocation = visibleArea.extent.center geocodeParameters.maxResults = 25 geocodeParameters.resultAttributeNames.append(contentsOf: [AttributeKeys.placeAddress, AttributeKeys.placeName]) cancelableGeocodeTask = locatorTask.geocode(withSearchText: category.title, parameters: geocodeParameters) { [weak self] (results: [AGSGeocodeResult]?, error: Error?) -> Void 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 } // Represent each located place as a dot on the map. Each graphic symbol also gets the place attributes so we can // show them later when the user taps the graphic. 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]?) self.graphicsOverlay.graphics.add(graphic) } } }
-
Create a method to read the Picker View's selected category and call the
find
method.Places 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.
ViewController.swiftUse dark colors for code blocks // Get the current selection in the picker and find places for that category. private func findPlacesForCategoryPickerSelection() { let categoryIndex = categoryPicker.selectedRow(inComponent: 0) guard categoryIndex < categories.count else { return } let category = categories[categoryIndex] findPlaces(forCategory: category) }
Add the data source and delegate protocol methods
Delegate interface methods are used to map a request for a specific row in the Picker View to an element in the categories array. When the user selects a row, a search will be performed using the matching category.
-
Add the
UI
data source interface methods to map requests to thePicker View categories
array:Add these new methods at the bottom of
View
, before the closingController }
ofView
.Controller ViewController.swiftUse dark colors for code blocks func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return categories.count }
-
Add the
UI
delegate interface methods. This will map a request for a specific row to an element in thePicker View categories
array. Once the user selects an row, a geocode will be performed for the places matching the selected category.Add these new methods after the data source methods added in the prior step.
ViewController.swiftUse dark colors for code blocks 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]) }
Show information about a tapped location in the map
An identify operation can be used to get information about a geoelement (such as a graphic) at a location where the user has tapped on the map. A callout can be used to display this information.
-
Add a method to respond to the AGSGeoViewTouchDelegate protocol. When the user taps the map, use the
identify
operation to determine if there is a result graphic at that location. Anidentify
operation can return more than one result but you will show the first one.This method is called via the
AGS
protocol you set on theGeo View Touch Delegate View
. When the user taps the map you are given the screen point and the corresponding map location.Controller 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 yourshow
method with the first graphic result identified and the corresponding map location.Callout For Graphic ViewController.swiftUse dark colors for code blocks func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // Dismiss the callout if already visible. self.mapView.callout.dismiss() // Identify graphics at the tapped location. 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 { // Show a callout for the first graphic in the array. self.showCalloutForGraphic(graphic, tapLocation: mapPoint) } } }
-
Create a method to assign the location's attributes to the
map
. Show the callout when the user taps a graphic on the map.View.callout An
AGS
is a property of the AGSMapView. The location's name and address are passed to the title and detail string of theCallout AGS
so they can be displayed when the user taps on a place.Callout ViewController.swiftUse dark colors for code blocks // When a graphic in the graphics layer is tapped, use a callout to dislay its attributes. 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) }
Put it all together
Load the basemap, search for coffee shops in the visible area (using the first category in the Picker View), and display their location on the map.
-
Update the
setup
method to display theMap .arc
basemap style. Change theGIS Navigation AGS
constructor to center the map around Malibu, California.Viewpoint ViewController.swiftUse dark colors for code blocks private func setupMap() { let map = AGSMap( basemapStyle: .arcGISNavigation ) mapView.map = map mapView.setViewpoint( AGSViewpoint( latitude: 34.09042, longitude: -118.71511, scale: 500_000 ) ) }
-
Add a touch delegate so that the app responds when the user taps on the map. Provide a graphics overlay to display the places on the map.
ViewController.swiftUse dark colors for code blocks private func setupMap() { let map = AGSMap( basemapStyle: .arcGISNavigation ) mapView.map = map mapView.setViewpoint( AGSViewpoint( latitude: 34.09042, longitude: -118.71511, scale: 500_000 ) ) mapView.touchDelegate = self mapView.graphicsOverlays.add(graphicsOverlay) }
-
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
is
. Once map navigation completes, callNavigating find
.Places For Category Picker Selection ViewController.swiftUse dark colors for code blocks private func setupMap() { let map = AGSMap( basemapStyle: .arcGISNavigation ) mapView.map = map mapView.setViewpoint( AGSViewpoint( latitude: 34.09042, longitude: -118.71511, scale: 500_000 ) ) mapView.touchDelegate = self mapView.graphicsOverlays.add(graphicsOverlay) // When the map finishes navigating to a new extent perform a new search using the updated visible area. isNavigatingObserver = mapView.observe(\.isNavigating, options:[]) { (mapView, _) in guard !mapView.isNavigating else { return } // Update results if the map view extent has moved. DispatchQueue.main.async { [weak self] in self?.findPlacesForCategoryPickerSelection() } } }
-
When the map view loads the first time, use the
viewpoint
to perform a search on the selected category. The matching places are displayed on the map.Changed Handler Once the search is performed the first time, you no longer need this handler and can disable it.
ViewController.swiftUse dark colors for code blocks private func setupMap() { let map = AGSMap( basemapStyle: .arcGISNavigation ) mapView.map = map mapView.setViewpoint( AGSViewpoint( latitude: 34.09042, longitude: -118.71511, scale: 500_000 ) ) mapView.touchDelegate = self mapView.graphicsOverlays.add(graphicsOverlay) // When the map finishes navigating to a new extent perform a new search using the updated visible area. isNavigatingObserver = mapView.observe(\.isNavigating, options:[]) { (mapView, _) in guard !mapView.isNavigating else { return } // Update results if the map view extent has moved. DispatchQueue.main.async { [weak self] in self?.findPlacesForCategoryPickerSelection() } } // After the view loads, perform a search on the selected item in the picker. Once this completes // remove this handler. mapView.viewpointChangedHandler = { [weak self] () -> Void in DispatchQueue.main.async { self?.findPlacesForCategoryPickerSelection() self?.mapView.viewpointChangedHandler = nil } } }
-
Press Command + R to run the app.
If you are using the Xcode simulator your system must meet these minimum requirements: macOS Big Sur 11.3, Xcode 13, iOS 13. If you are using a physical device, then refer to the system requirements.
When the app opens, your map displays coffee shop locations for the Malibu area near Los Angeles, California. You can touch one of the coffee shops and see its place name and address. Use the picklist to search different categories of places.
What's next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: