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 using your ArcGIS Location Platform or ArcGIS Online account.
-
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 MainApp.swift.
-
In the Editor, set the
ArcGISEnvironment.apiKey
property on theArcGIS
with your copied access token.Environment MainApp.swiftUse dark colors for code blocks init() { ArcGISEnvironment.apiKey = APIKey("<#YOUR-ACCESS-TOKEN#>") }
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) } }
-
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: