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
The following are required for this tutorial:
- An ArcGIS account to access your API keys. If you don't have an account, sign up for free.
- Your system meets the system requirements.
Steps
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. -
If you downloaded the solution project, set your API key.
An API Key enables access to services, web maps, and web scenes hosted in ArcGIS Online.
-
Go to your developer dashboard to get your API key. For these tutorials, use your default API key. It is scoped to include all of the services demonstrated in the tutorials.
- In Xcode, in the Project Navigator, click MainApp.swift.
- In the Editor, set the
ArcGISEnvironment.apiKey
property on theArcGISEnvironment
with your API key.
MainApp.swiftUse dark colors for code blocks import SwiftUI import ArcGIS @main struct MainApp: App { init() { ArcGISEnvironment.apiKey = APIKey("<#your-API-key#>") } var body: some SwiftUI.Scene { WindowGroup { ContentView() .ignoresSafeArea() } } }
-
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 @State
variable of theObject Model
to theContent
. See the programming patterns page for more information on how to manage states.View ContentView.swiftUse dark colors for code blocks Add 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 Add 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 Add 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
using the Geocoding service URL.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 Add 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 aUIColor
named "color". Each category is searched using itslabel
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 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. 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@State
property wrapper. This will be used to define the search location.ContentView.swiftUse dark colors for code blocks Add 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 theo
method to the map view. Set then Visible Area Changed(perform: ) geo
variable to the new visible area's extent.View Extent ContentView.swiftUse dark colors for code blocks 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 } } }
-
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 Add 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 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"]) }
-
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 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) } 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 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. 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@State
property wrapper and give it a default value ofcoffee
. This will indicate the currently selected category.Shop ContentView.swiftUse dark colors for code blocks Add 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 Add 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 Add 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 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() .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
andCGPoint
variables with the@State
property wrappers. Name themmap
andLocation tap
, respectively.Location ContentView.swiftUse dark colors for code blocks Add 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@State
property wrapper. Name themcallout
andPlacement callout
respectively.Text ContentView.swiftUse dark colors for code blocks Add 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 Add 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
o
method to the map view and setn Single T a p Gesture(perform: ) map
andLocation tap
.Location ContentView.swiftUse dark colors for code blocks 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 } .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 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 } .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 thescreen Point: tolerance: return Popups Only: maximum Results: ) tap
.Location ContentView.swiftUse dark colors for code blocks Add 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 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 ) 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 Monterey 12.5, Xcode 15, iOS 17. 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: