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.
Set up authentication
To access the secure ArcGIS location services used in this tutorial, you must implement API key authentication or user authentication using an ArcGIS Location Platform or an ArcGIS Online account.
You can implement API key authentication or user authentication in this tutorial. Compare the differences below:
API key authentication
- Users are not required to sign in.
- Requires creating an API key credential with the correct privileges.
- API keys are long-lived access tokens.
- Service usage is billed to the API key owner/developer.
- Simplest authentication method to implement.
- Recommended approach for new ArcGIS developers.
Learn more in API key authentication.
User authentication
- Users are required to sign in with an ArcGIS account.
- User accounts must have privilege to access the ArcGIS services used in application.
- Requires creating OAuth credentials.
- Application uses a redirect URL and client ID.
- Service usage is billed to the organization of the user signed into the application.
Learn more in User authentication.
Create a new API key access token with privileges to access the secure resources used in this tutorial.
-
Complete the Create an API key tutorial and create an API key with the following privilege(s):
- Privileges
- Location services > Basemaps
- Location services > Geocoding
- Privileges
-
Copy and paste the API Key access token into a safe location. It will be used in a later step.
Develop or Download
To complete this tutorial you have 2 options:
Option 1: Develop the code
To start the tutorial, complete the Display a map tutorial. This creates a map to display the Santa Monica Mountains in California using the topographic basemap from the ArcGIS basemap styles service. You can choose to implement either API key authentication or user authentication.
Continue with the following instructions search for places of interest, such as hotels, cafes, and gas stations using the ArcGIS geocoding service. First, you need to set the develop credentials in your app so that you can access the ArcGIS geocoding service.
Set developer credentials
To allow your app users to access ArcGIS location services, pass the developer credentials that you created in the Set up authentication step to the application's ArcGISEnvironment
.
Pass your API Key access token to the ArcGISEnvironment
.
-
In the Project Navigator, click MainApp.swift.
-
Set the
ArcGIS
property with your API key access token.Environment.api Key MainApp.swiftUse dark colors for code blocks ArcGISEnvironment.apiKey = APIKey("<#YOUR-ACCESS-TOKEN#>")
Best Practice: The access token is stored directly in the code as a convenience for this tutorial. In a production environment we do not recommend that you store it directly in source code.
Update the map
-
In Xcode, in the Project Navigator, click ContentView.swift.
-
Create a private extension of
Content
and make a private class namedView Model
of typeObservable
. Add aObject @
variable of theState Object Model
to theContent
. See the programming patterns page for more information on how to manage states.View ContentView.swiftUse dark colors for code blocks 19 20 22 23 24 25 26 27 28 29 30Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. struct ContentView: View { @StateObject private var model = Model() @State private var map = { let map = Map(basemapStyle: .arcGISTopographic) map.initialViewpoint = Viewpoint(latitude: 34.02700, longitude: -118.80500, scale: 72_000) return map }() } private extension ContentView { private class Model: ObservableObject { } }
-
Create a
GraphicsOverlay
namedgraphics
in theOverlay Model
class. A graphics overlay is a container for graphics.A graphics overlay is a container for graphics. It is used with a map view to display graphics on a map. You can add more than one graphics overlay to a map view. Graphics overlays are displayed on top of all the other layers.
ContentView.swiftUse dark colors for code blocks 135 136 138 139Add line. private class Model: ObservableObject { let graphicsOverlay = GraphicsOverlay() }
-
Add the graphics overlay to the map view, wrap the map view inside a
MapViewReader
, and expose theMapViewProxy
class in its closure.Map
provides operations that can be performed on the map view, such as 'identify'. For more information see Perform GeoView operations.View Proxy ContentView.swiftUse dark colors for code blocks 39 40 17 18Add line. Add line. Add line. Add line. Add line. var body: some View { MapViewReader { mapViewProxy in MapView(map: map, graphicsOverlays: [model.graphicsOverlay]) } }
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.
-
In the Model, create a
LocatorTask
property namedlocator
based on the Geocoding service.A locator task is used to convert an address to a point (geocode) or vice-versa (reverse geocode). An address includes any type of information that distinguishes a place. A locator involves finding matching locations for a given address. Reverse-geocoding is the opposite and finds the closest address for a given point.
ContentView.swiftUse dark colors for code blocks 109 110 111 112 113 114 118 119 120 121Add line. Add line. Add line. private extension ContentView { private class Model: ObservableObject { let graphicsOverlay = GraphicsOverlay() let locator = LocatorTask( url: URL(string: "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer")! ) } }
-
To support the geocode operation, create an
enum
namedCategory
in theContent
extension. Provide aView String
named "label" and aUI
named "color". Each category is searched using itsColor label
and is distinguished on the map using its associatedcolor
.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.
ContentView.swiftUse dark colors for code blocks 109 110 134 135 136 137 138 139 140 141 142 143 144 145Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. private extension ContentView { enum Category: CaseIterable, Equatable { case coffeeShop, gasStation, food, hotel, parksOutdoors var label: String { switch self { case .coffeeShop: return "Coffee shop" case .gasStation: return "Gas station" case .food: return "Food" case .hotel: return "Hotel" case .parksOutdoors: return "Parks and Outdoors" } } var color: UIColor { switch self { case .coffeeShop: return .brown case .gasStation: return .orange case .food: return .purple case .hotel: return .blue case .parksOutdoors: return .green } } } private class Model: ObservableObject { let graphicsOverlay = GraphicsOverlay() let locator = LocatorTask( url: URL(string: "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer")! ) } }
-
In the
Content
struct, create a private variable namedView geo
of typeView Extent Envelope
with the@
property wrapper. This will be used to define the search location.State ContentView.swiftUse dark colors for code blocks 19 20 21 22 24 25 26 27 28 29 30 31 32 33 34 35 36 8 9 10 11 12Add line. struct ContentView: View { @StateObject private var model = Model() @State private var geoViewExtent: Envelope? @State private var map = { let map = Map(basemapStyle: .arcGISTopographic) map.initialViewpoint = Viewpoint(latitude: 34.02700, longitude: -118.80500, scale: 72_000) return map }() var body: some View { MapViewReader { mapViewProxy in MapView(map: map, graphicsOverlays: [model.graphicsOverlay]) } } }
-
In the
body
, add theon
method to the map view. Set theVisible Area Changed(perform :) geo
variable to the new visible area's extent.View Extent ContentView.swiftUse dark colors for code blocks 39 40 41 42 43 44 48 49 50 51Add line. Add line. Add line. var body: some View { MapViewReader { mapViewProxy in MapView(map: map, graphicsOverlays: [model.graphicsOverlay]) .onVisibleAreaChanged { newVisibleArea in geoViewExtent = newVisibleArea.extent } } }
-
In the
Model
, create a private, asynchronous method calledfind
to perform the geocode search operation. The method takes a parameter of typePlaces(for Category :search Point :) Category
that you created in the previous step to indicate which category of places to search for and aPoint
that acts as the preferred search location.ContentView.swiftUse dark colors for code blocks 135 136 137 138 139 140 141 142 131 132Add line. Add line. Add line. private class Model: ObservableObject { let graphicsOverlay = GraphicsOverlay() let locator = LocatorTask( url: URL(string: "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer")! ) func findPlaces(forCategory category: Category, searchPoint: Point? = nil) async { } }
-
Clear the previous results by removing all graphics from the graphics overlay. Create and configure new
Geocode
. Populate them with theParameters search
parameter as the search location and add result attribute names.Point ContentView.swiftUse dark colors for code blocks 143 144 149 135Add line. Add line. Add line. Add line. func findPlaces(forCategory category: Category, searchPoint: Point? = nil) async { graphicsOverlay.removeAllGraphics() let geocodeParameters = GeocodeParameters() geocodeParameters.preferredSearchLocation = searchPoint geocodeParameters.addResultAttributeNames(["Place_addr", "PlaceName"]) }
-
Perform the search query using
geocode(for
. Pass in the category'sSearch Text :using :) label
and the geocode parameters.ContentView.swiftUse dark colors for code blocks 143 144 145 146 147 148 149 156 157Add line. Add line. Add line. Add line. Add line. Add line. func findPlaces(forCategory category: Category, searchPoint: Point? = nil) async { graphicsOverlay.removeAllGraphics() let geocodeParameters = GeocodeParameters() geocodeParameters.preferredSearchLocation = searchPoint geocodeParameters.addResultAttributeNames(["Place_addr", "PlaceName"]) do { let geocodeResults = try await locator.geocode(forSearchText: category.label, using: geocodeParameters) } catch { print(error) } }
-
Create graphics for each of the results and add them to the graphics overlay.
Populate the
graphics
withOverlay SimpleMarkerSymbol
s representing each place returned in the search results. This is very similar to the Add a point, line, and polygon tutorial.ContentView.swiftUse dark colors for code blocks 143 144 145 146 147 148 149 150 151 152 167 168 169 170 171 172Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. func findPlaces(forCategory category: Category, searchPoint: Point? = nil) async { graphicsOverlay.removeAllGraphics() let geocodeParameters = GeocodeParameters() geocodeParameters.preferredSearchLocation = searchPoint geocodeParameters.addResultAttributeNames(["Place_addr", "PlaceName"]) do { let geocodeResults = try await locator.geocode(forSearchText: category.label, using: geocodeParameters) if !geocodeResults.isEmpty { let placeSymbol = SimpleMarkerSymbol( style: .circle, color: category.color, size: 10 ) placeSymbol.outline = SimpleLineSymbol( style: .solid, color: .white, width: 2 ) let graphics = geocodeResults.map { Graphic(geometry: $0.displayLocation, attributes: $0.attributes, symbol: placeSymbol) } graphicsOverlay.addGraphics(graphics) } } catch { print(error) } }
Add a category picker
You will add a Picker 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.
-
In the
Content
struct, add a variable of typeView Category
with the@
property wrapper and give it a default value ofState coffee
. This will indicate the currently selected category.Shop ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 26 27 28 29 30 31Add line. struct ContentView: View { @StateObject private var model = Model() @State private var geoViewExtent: Envelope? @State private var selectedCategory: Category = .coffeeShop @State private var map = { let map = Map(basemapStyle: .arcGISTopographic) map.initialViewpoint = Viewpoint(latitude: 34.02700, longitude: -118.80500, scale: 72_000) return map }()
-
In the
Content
View body
, add atoolbar
view modifier to the map view that places aToolbar
at the bottom of the view where thePicker
will be contained.ContentView.swiftUse dark colors for code blocks 39 40 41 42 43 44 45 46 47 48 25 26 27 28Add line. Add line. Add line. Add line. Add line. var body: some View { MapViewReader { mapViewProxy in MapView(map: map, graphicsOverlays: [model.graphicsOverlay]) .onVisibleAreaChanged { newVisibleArea in geoViewExtent = newVisibleArea.extent } .toolbar { ToolbarItemGroup(placement: .bottomBar) { } } } }
-
Add a
Picker
to the toolbar and label it "Choose a category". Set the selection to$selected
. This will iterate throughCategory .all
ofCases Category
to populate the Picker with all the category labels. Add the.labels
modifier.Hidden ContentView.swiftUse dark colors for code blocks 16 17 18 25 26 27Add line. Add line. Add line. Add line. Add line. Add line. .toolbar { ToolbarItemGroup(placement: .bottomBar) { Picker("Choose a category", selection: $selectedCategory) { ForEach(Category.allCases, id: \.self) { category in Text(category.label) } } .labelsHidden() } }
-
Lastly, add a
.task
modifier to thePicker
that calls the model'sfind
function. Pass inPlaces(for Category :search Point :) selected
and theCategory geo
. This will initiate a geocode search when a category is selected.View Extent?.center ContentView.swiftUse dark colors for code blocks 16 17 18 19 20 21 22 23 24 25 29 30 31Add line. Add line. Add line. .toolbar { ToolbarItemGroup(placement: .bottomBar) { Picker("Choose a category", selection: $selectedCategory) { ForEach(Category.allCases, id: \.self) { category in Text(category.label) } } .labelsHidden() .task(id: selectedCategory) { await model.findPlaces(forCategory: selectedCategory, searchPoint: geoViewExtent?.center) } } }
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.
-
In the
Content
struct, add objects to track the map and screen locations. CreateView Point
andCG
variables with thePoint @
property wrappers. Name themState map
andLocation tap
, respectively.Location ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 29 30 31 32 33 34Add line. Add line. struct ContentView: View { @StateObject private var model = Model() @State private var geoViewExtent: Envelope? @State private var selectedCategory: Category = .coffeeShop @State private var tapLocation: CGPoint? @State private var mapLocation: Point? @State private var map = { let map = Map(basemapStyle: .arcGISTopographic) map.initialViewpoint = Viewpoint(latitude: 34.02700, longitude: -118.80500, scale: 72_000) return map }()
-
Add objects to support the callout. Create
Callout
andPlacement String
variables with the@
property wrapper. Name themState callout
andPlacement callout
respectively.Text ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 27 28 29 32 33 34 35 36 37Add line. Add line. struct ContentView: View { @StateObject private var model = Model() @State private var geoViewExtent: Envelope? @State private var selectedCategory: Category = .coffeeShop @State private var tapLocation: CGPoint? @State private var mapLocation: Point? @State private var calloutPlacement: CalloutPlacement? @State private var calloutText: String? @State private var map = { let map = Map(basemapStyle: .arcGISTopographic) map.initialViewpoint = Viewpoint(latitude: 34.02700, longitude: -118.80500, scale: 72_000) return map }()
-
In the
body
, add a.callout
modifier to the map view. Pass in$callout
as the placement parameter. In the closure, create aPlacement Text
object using thecallout
and provide a defaultText String
in case it is nil.ContentView.swiftUse dark colors for code blocks 39 40 41 42 43 44 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75Add line. Add line. Add line. Add line. Add line. Add line. var body: some View { MapViewReader { mapViewProxy in MapView(map: map, graphicsOverlays: [model.graphicsOverlay]) .callout(placement: $calloutPlacement.animation(.default.speed(2))) { _ in Text(calloutText ?? "No address found.") .font(.callout) .padding(8) .frame(maxWidth: 350) } .onVisibleAreaChanged { newVisibleArea in geoViewExtent = newVisibleArea.extent } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Picker("Choose a category", selection: $selectedCategory) { ForEach(Category.allCases, id: \.self) { category in Text(category.label) } } .labelsHidden() .task(id: selectedCategory) { await model.findPlaces(forCategory: selectedCategory, searchPoint: geoViewExtent?.center) } } } } }
-
Add the
on
method to the map view and setSingle Tap Gesture(perform :) map
andLocation tap
.Location ContentView.swiftUse dark colors for code blocks 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80Add line. Add line. Add line. Add line. var body: some View { MapViewReader { mapViewProxy in MapView(map: map, graphicsOverlays: [model.graphicsOverlay]) .callout(placement: $calloutPlacement.animation(.default.speed(2))) { _ in Text(calloutText ?? "No address found.") .font(.callout) .padding(8) .frame(maxWidth: 350) } .onVisibleAreaChanged { newVisibleArea in geoViewExtent = newVisibleArea.extent } .onSingleTapGesture { screenPoint, mapPoint in tapLocation = screenPoint mapLocation = mapPoint } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Picker("Choose a category", selection: $selectedCategory) { ForEach(Category.allCases, id: \.self) { category in Text(category.label) } } .labelsHidden() .task(id: selectedCategory) { await model.findPlaces(forCategory: selectedCategory, searchPoint: geoViewExtent?.center) } } } } }
-
Add a
.task
modifier to the map view, passing intap
as the idefntifier. Ensure that the location objects are not nil.Location ContentView.swiftUse dark colors for code blocks 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85Add line. Add line. Add line. Add line. var body: some View { MapViewReader { mapViewProxy in MapView(map: map, graphicsOverlays: [model.graphicsOverlay]) .callout(placement: $calloutPlacement.animation(.default.speed(2))) { _ in Text(calloutText ?? "No address found.") .font(.callout) .padding(8) .frame(maxWidth: 350) } .onVisibleAreaChanged { newVisibleArea in geoViewExtent = newVisibleArea.extent } .onSingleTapGesture { screenPoint, mapPoint in tapLocation = screenPoint mapLocation = mapPoint } .task(id: tapLocation) { guard let tapLocation, let mapLocation else { return } } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Picker("Choose a category", selection: $selectedCategory) { ForEach(Category.allCases, id: \.self) { category in Text(category.label) } } .labelsHidden() .task(id: selectedCategory) { await model.findPlaces(forCategory: selectedCategory, searchPoint: geoViewExtent?.center) } } } } }
-
Perform
identify(on
on the map view proxy to identify the graphics at the:screen Point :tolerance :return Popups Only :maximum Results :) tap
.Location ContentView.swiftUse dark colors for code blocks 61 62 63 74 75Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. .task(id: tapLocation) { guard let tapLocation, let mapLocation else { return } do { let identifyResult = try await mapViewProxy.identify( on: model.graphicsOverlay, screenPoint: tapLocation, tolerance: 12 ) } catch { print(error) } }
-
Lastly, assign the
callout
andText callout
variables with with attributes from the first graphic of the identify results. This change in state will trigger the callout to be displayed.Placement ContentView.swiftUse dark colors for code blocks 61 62 63 64 65 66 67 68 69 70 79 80 81 82 83 84Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. .task(id: tapLocation) { guard let tapLocation, let mapLocation else { return } do { let identifyResult = try await mapViewProxy.identify( on: model.graphicsOverlay, screenPoint: tapLocation, tolerance: 12 ) if let graphic = identifyResult.graphics.first { let placeName = graphic.attributes["PlaceName"] as? String ?? "Unknown" let placeAddress = graphic.attributes["Place_addr"] as? String ?? "no address provided" calloutText = "\(placeName)\n\(placeAddress)" calloutPlacement = .location(mapLocation) } else { calloutPlacement = nil } } catch { print(error) } }
Run the solution
Press Command + R to run the app.
If you are using the Xcode simulator your system must meet these minimum requirements: macOS 14 (Sonoma), Xcode 16, iOS 18. If you are using a physical device, then refer to the system requirements.
When the app opens, use the picker to search different categories of places in the Malibu area near Los Angeles, California. You can tap one of the places and see its name and address.
Alternatively, you can download the tutorial solution, as follows.
Option 2: Download the solution
-
Click the
Download solution
link under Solution and unzip the file to a location on your machine. -
Open the
.xcodeproj
file in Xcode.
Since the downloaded solution does not contain authentication credentials, you must add the developer credentials that you created in the set up authentication section.
Set developer credentials in the solution
To allow your app users to access ArcGIS location services, pass the developer credentials that you created in the Set up authentication step to the application's ArcGISEnvironment
.
Pass your API Key access token to the ArcGISEnvironment
.
-
In the Project Navigator, click MainApp.swift.
-
Set the
Authentication
toMode .api
.Key MainApp.swiftUse dark colors for code blocks // Change the `AuthenticationMode` to `.apiKey` if your application uses API key authentication. private var authenticationMode: AuthenticationMode { .apiKey }
-
Set the
api
property with your API key access token.Key MainApp.swiftUse dark colors for code blocks // Please enter an API key access token if your application uses API key authentication. private let apiKey = APIKey("YOUR-ACCESS-TOKEN")
Best Practice: The access token is stored directly in the code as a convenience for this tutorial. In a production environment we do not recommend that you store it directly in source code.
Run the solution
Press Command + R to run the app.
If you are using the Xcode simulator your system must meet these minimum requirements: macOS 14 (Sonoma), Xcode 16, iOS 18. If you are using a physical device, then refer to the system requirements.
When the app opens, use the picker to search different categories of places in the Malibu area near Los Angeles, California. You can tap one of the places and see its name and address.
What's next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: