Learn how to download and display an offline map

Offline maps
In this tutorial, you will download an offline map
Prerequisites
Before starting this tutorial:
-
You need an ArcGIS Location Platform or ArcGIS Online account.
-
Your system meets the system requirements.
Develop or Download
You have two options for completing this tutorial:
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
Open an Xcode project
- Open the
.xcodeprojproject you created by completing the Display a map tutorial. - Continue with the following instructions to download and display an offline map for a user-defined geographical area of a web map.
Get the web map item ID
You can use ArcGIS tools
-
Go to the Naperville water network in the Map Viewer
Map Viewer is a browser-based mapping tool that can view, create, and save web maps. It can also perform mapping, visualization, and spatial analysis operations. in ArcGIS OnlineArcGIS Online is a GIS mapping, analytics, data hosting, and content management software as a service (SaaS) product. It includes applications, tools, APIs, and location services for users and developers. It is subscription-based and requires an ArcGIS Online account. . This web mapA web map is a map stored as a JSON object that defines properties such as the basemap layer, data layers, layer styles, and pop-up styles. Its JSON structure is defined by the web map specification. displays a stormwater network within Naperville, Illinois, USA. -
Make a note of the item ID
An item ID is a unique identifier representing a single item stored, managed, and accessed in a portal, such as a web map, hosted layer, or file. at the end of the browser’s URL.The item ID should be 5a030a31e42841a89914bd7c5ecf4d8f.
Display the web map
You can display a web mapMap from the web map’s PortalItem, and display it in your app’s MapView.
-
In Xcode, in the Project Navigator, click ContentView.swift.
-
In the editor, modify the
mapvariable. Provide the web map’s item ID.The code creates an
PortalItemusing thePortalthat references ArcGIS OnlineArcGIS Online is a GIS mapping, analytics, data hosting, and content management software as a service (SaaS) product. It includes applications, tools, APIs, and location services for users and developers. It is subscription-based and requires an ArcGIS Online account. , and the web map’sitemID. TheportalItemis used to create anMapthat is displayed in the app’sMapView.ContentView.swift@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!)) -
Create a private class named
Modelof typeObservableObjectand add a@StateObjectvariable of theModelto theContentView. Make theModelthe@MainActor. See the programming patterns page for more information on how to manage states.ContentView.swiftimport SwiftUIimport ArcGIS@MainActorprivate class Model: ObservableObject {}struct ContentView: View {@StateObject private var model = Model()@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!)) -
In the
Model, create a@Publishedvariable namedofflineMapof typeMapto store the output of the downloaded map.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!} -
In the
ContentView, modify the map view to display the model’s offline map or the online map. IfofflineMaphas no value, then the map view should display the online map.ContentView.swiftstruct ContentView: View {@StateObject private var model = Model()@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!))var body: some View {MapView(map: model.offlineMap ?? map)}}
Specify an area of the web map to take offline
Specify an area of the web mapEnvelope or a Polygon. Use views to obtain data about the map view and an overlay to indicate the area on the map to be downloaded.
-
In the
ContentView, wrap the map view inside aMapViewReaderand expose theMapViewProxyin its closure. Name itmapView.MapViewProxyprovides operations that can be performed on the map view, such asenvelope(fromViewRect:). For more information see Perform GeoView operations.ContentView.swiftstruct ContentView: View {@StateObject private var model = Model()@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!))var body: some View {MapViewReader { mapView inMapView(map: model.offlineMap ?? map)}}} -
Wrap the map view reader inside a
GeometryReaderand expose theGeometryProxyin its closure. Name itgeometry.GeometryProxyprovides access to the size and coordinate space (for anchor resolution) of the views enclosed in the geometry reader.ContentView.swiftstruct ContentView: View {@StateObject private var model = Model()@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!))var body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map)}}}} -
Add an
.overlaymodifier to the map view. The overlay contains a red rectangle that encompasses an area to be downloaded.ContentView.swiftvar body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map).overlay {Rectangle().stroke(.red, lineWidth: 2).padding(EdgeInsets(top: 60, leading: 20, bottom: 100, trailing: 20)).opacity(model.offlineMap == nil ? 1 : 0)}}}}
Download and display the offline map
Generate and download an offline map
-
In the
Model, create a@PublishedGenerateOfflineMapJobvariable calledgenerateOfflineMapJoband aOfflineMapTaskvariable calledofflineMapTask. These objects will contain the job and task needed to perform the download function. Learn more about Tasks and jobs.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!} -
Create a
@PublishedBoolean variable calledisGenerateDisabledand set it to true. Create a variable calledisShowingAlertand set it to false. These variables will be used to determine if the download button is enabled and if the completion alert is present.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!@Published private(set) var isGenerateDisabled = true@Published var isShowingAlert = false} -
Create a private
URLproperty namedtemporaryDirectoryURL. This property will generate a uniqueURLat which to store the offline map on the device.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!@Published private(set) var isGenerateDisabled = true@Published var isShowingAlert = falseprivate let temporaryDirectory: URL = {// swiftlint:disable:next force_tryreturn try! FileManager.default.url(for: .itemReplacementDirectory,in: .userDomainMask,appropriateFor: FileManager.default.temporaryDirectory,create: true)}()} -
Create a
deinitfunction to remove the temporary directory after the app closes.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!@Published private(set) var isGenerateDisabled = true@Published var isShowingAlert = falseprivate let temporaryDirectory: URL = {// swiftlint:disable:next force_tryreturn try! FileManager.default.url(for: .itemReplacementDirectory,in: .userDomainMask,appropriateFor: FileManager.default.temporaryDirectory,create: true)}()deinit {try? FileManager.default.removeItem(at: temporaryDirectory)}} -
Create an asynchronous function called
initializeOfflineMapTask(onlineMap:). This function loads the online map and creates aOfflineMapTaskfrom it. Set theisGenerateDisabledBoolean tofalse.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!@Published private(set) var isGenerateDisabled = true@Published var isShowingAlert = falseprivate let temporaryDirectory: URL = {// swiftlint:disable:next force_tryreturn try! FileManager.default.url(for: .itemReplacementDirectory,in: .userDomainMask,appropriateFor: FileManager.default.temporaryDirectory,create: true)}()deinit {try? FileManager.default.removeItem(at: temporaryDirectory)}func initializeOfflineMapTask(onlineMap: Map) async {do {try await onlineMap.load()offlineMapTask = OfflineMapTask(onlineMap: onlineMap)isGenerateDisabled = false} catch {print(error)}}} -
In the
ContentView, call theinitializeOfflineMapTask(onlineMap:)function in a.taskmodifier, passing in the onlinemap. The task’s function is called as the map view appears.ContentView.swiftstruct ContentView: View {@StateObject private var model = Model()@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!))var body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map).task {await model.initializeOfflineMapTask(onlineMap: map)}.overlay {Rectangle().stroke(.red, lineWidth: 2).padding(EdgeInsets(top: 60, leading: 20, bottom: 100, trailing: 20)).opacity(model.offlineMap == nil ? 1 : 0)}}}}} -
In the
Model, create an asynchronous function calledmakeGenerateOfflineMapParameters(areaOfInterest:). The function returnsGenerateOfflineMapParameters, created usingofflineMapTaskand the area of interest.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!@Published private(set) var isGenerateDisabled = true@Published var isShowingAlert = falseprivate let temporaryDirectory: URL = {// swiftlint:disable:next force_tryreturn try! FileManager.default.url(for: .itemReplacementDirectory,in: .userDomainMask,appropriateFor: FileManager.default.temporaryDirectory,create: true)}()deinit {try? FileManager.default.removeItem(at: temporaryDirectory)}func initializeOfflineMapTask(onlineMap: Map) async {do {try await onlineMap.load()offlineMapTask = OfflineMapTask(onlineMap: onlineMap)isGenerateDisabled = false} catch {print(error)}}private func makeGenerateOfflineMapParameters(areaOfInterest: Envelope) async -> GenerateOfflineMapParameters? {do {return try await offlineMapTask.makeDefaultGenerateOfflineMapParameters(areaOfInterest: areaOfInterest)} catch {print(error)return nil}}} -
Create an asynchronous function called
generateOfflineMap(extent:). This function is called when the user initiates the download. Set theisGenerateDisabledBoolean totrueand create default parameters for the given extent. If the parameters fail to create, set theisGenerateDisabledBoolean tofalse.ContentView.swift@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!@Published private(set) var isGenerateDisabled = true@Published var isShowingAlert = falseprivate let temporaryDirectory: URL = {// swiftlint:disable:next force_tryreturn try! FileManager.default.url(for: .itemReplacementDirectory,in: .userDomainMask,appropriateFor: FileManager.default.temporaryDirectory,create: true)}()deinit {try? FileManager.default.removeItem(at: temporaryDirectory)}func initializeOfflineMapTask(onlineMap: Map) async {do {try await onlineMap.load()offlineMapTask = OfflineMapTask(onlineMap: onlineMap)isGenerateDisabled = false} catch {print(error)}}private func makeGenerateOfflineMapParameters(areaOfInterest: Envelope) async -> GenerateOfflineMapParameters? {do {return try await offlineMapTask.makeDefaultGenerateOfflineMapParameters(areaOfInterest: areaOfInterest)} catch {print(error)return nil}}func generateOfflineMap(extent: Envelope) async {isGenerateDisabled = trueguard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else {isGenerateDisabled = falsereturn}}} -
Create an offline map job using
makeGenerateOfflineMapJob(parameters:downloadDirectory:overrides:)and start the job.ContentView.swiftfunc generateOfflineMap(extent: Envelope) async {isGenerateDisabled = trueguard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else {isGenerateDisabled = falsereturn}generateOfflineMapJob = offlineMapTask.makeGenerateOfflineMapJob(parameters: parameters,downloadDirectory: temporaryDirectory)generateOfflineMapJob.start()} -
In a
deferclosure, ensure the job does not continue running by setting it toniland set theisGenerateDisabledvalue according to whether or notofflineMapis instantiated.ContentView.swiftfunc generateOfflineMap(extent: Envelope) async {isGenerateDisabled = trueguard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else {isGenerateDisabled = falsereturn}generateOfflineMapJob = offlineMapTask.makeGenerateOfflineMapJob(parameters: parameters,downloadDirectory: temporaryDirectory)generateOfflineMapJob.start()defer {generateOfflineMapJob = nilisGenerateDisabled = offlineMap != nil}do {} catch {print(error)}} -
In the
doclosure, wait for theGenerateOfflineMapJobto produce an output that contains the downloaded offline map. Assign the output to theofflineMapproperty. UseEnvelopeBuilderto expand the extent by0.8. Set the offline map’s initial viewpoint to this new envelope. Indicate that an alert is shown to the user.ContentView.swiftfunc generateOfflineMap(extent: Envelope) async {isGenerateDisabled = trueguard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else {isGenerateDisabled = falsereturn}generateOfflineMapJob = offlineMapTask.makeGenerateOfflineMapJob(parameters: parameters,downloadDirectory: temporaryDirectory)generateOfflineMapJob.start()defer {generateOfflineMapJob = nilisGenerateDisabled = offlineMap != nil}do {let output = try await generateOfflineMapJob.outputofflineMap = output.offlineMaplet builder = EnvelopeBuilder(envelope: extent)builder.expand(by: 0.8)offlineMap.initialViewpoint = Viewpoint(boundingGeometry: builder.toGeometry())isShowingAlert = true} catch {print(error)}}
Setup the UI
Add a button to initiate the download, a ProgressView to display the download progress, and use alert(_:isPresented:presenting:actions:message:) to indicate when the download is complete.
-
In the
ContentView, add a@Statevariable calledisGeneratingOfflineMapand set it tofalse. This Boolean indicates whether an offline map is currently being generated.ContentView.swiftstruct ContentView: View {@StateObject private var model = Model()@State private var isGeneratingOfflineMap = false@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!)) -
Add an
.interactionModesmodifier to the map view. WhenisGeneratingOfflineMapistrue, the user should not be able to interact. Otherwise, allow the user to pan and zoom.ContentView.swiftvar body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map).interactionModes(isGeneratingOfflineMap ? [] : [.pan, .zoom]).task {await model.initializeOfflineMapTask(onlineMap: map)}.overlay {Rectangle().stroke(.red, lineWidth: 2).padding(EdgeInsets(top: 60, leading: 20, bottom: 100, trailing: 20)).opacity(model.offlineMap == nil ? 1 : 0)}}}} -
Add an
.overlaymodifier to the map view. IfisGeneratingOfflineMapistrue, create aProgressViewto trackmodel.generateOfflineMapJob?.progress. Customize the progress view style, frame, and other attributes as required.ContentView.swiftvar body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map).interactionModes(isGeneratingOfflineMap ? [] : [.pan, .zoom]).task {await model.initializeOfflineMapTask(onlineMap: map)}.overlay {Rectangle().stroke(.red, lineWidth: 2).padding(EdgeInsets(top: 60, leading: 20, bottom: 100, trailing: 20)).opacity(model.offlineMap == nil ? 1 : 0)}.overlay {if isGeneratingOfflineMap,let progress = model.generateOfflineMapJob?.progress {VStack(spacing: 16) {ProgressView(progress).progressViewStyle(.linear).frame(maxWidth: 200)}.padding().background(.regularMaterial).clipShape(RoundedRectangle(cornerRadius: 15)).shadow(radius: 3)}}}}} -
Add an
.alert(_:isPresented:presenting:actions:message:)modifier to the map view. Present an alert to indicate that the offline map has finished generating. Use the model’sisShowingAlertproperty to determine when to present the alert.ContentView.swiftvar body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map).interactionModes(isGeneratingOfflineMap ? [] : [.pan, .zoom]).task {await model.initializeOfflineMapTask(onlineMap: map)}.overlay {Rectangle().stroke(.red, lineWidth: 2).padding(EdgeInsets(top: 60, leading: 20, bottom: 100, trailing: 20)).opacity(model.offlineMap == nil ? 1 : 0)}.overlay {if isGeneratingOfflineMap,let progress = model.generateOfflineMapJob?.progress {VStack(spacing: 16) {ProgressView(progress).progressViewStyle(.linear).frame(maxWidth: 200)}.padding().background(.regularMaterial).clipShape(RoundedRectangle(cornerRadius: 15)).shadow(radius: 3)}}.alert("Offline map generated", isPresented: $model.isShowingAlert) {Button("Done") {model.isShowingAlert = false}}}}} -
Add a
Buttonlabeled “Download Map Area” to a toolbar at the bottom of the map view. When you click on the button, theisGeneratingOfflineMapproperty is set totrue. The button is disabled if the offline map has been generated (model.isGeneratedDisabled == true) or if the offline map is being generated (isGeneratingOfflineMap == true).ContentView.swiftvar body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map).interactionModes(isGeneratingOfflineMap ? [] : [.pan, .zoom]).task {await model.initializeOfflineMapTask(onlineMap: map)}.overlay {Rectangle().stroke(.red, lineWidth: 2).padding(EdgeInsets(top: 60, leading: 20, bottom: 100, trailing: 20)).opacity(model.offlineMap == nil ? 1 : 0)}.overlay {if isGeneratingOfflineMap,let progress = model.generateOfflineMapJob?.progress {VStack(spacing: 16) {ProgressView(progress).progressViewStyle(.linear).frame(maxWidth: 200)}.padding().background(.regularMaterial).clipShape(RoundedRectangle(cornerRadius: 15)).shadow(radius: 3)}}.alert("Offline map generated", isPresented: $model.isShowingAlert) {Button("Done") {model.isShowingAlert = false}}.toolbar {ToolbarItem(placement: .bottomBar) {Button("Download Map Area") {isGeneratingOfflineMap = true}.disabled(model.isGenerateDisabled || isGeneratingOfflineMap)}}}}} -
Add a
.taskmodifier to the button usingisGeneratingOfflineMapas its identifier. WhenisGeneratingOfflineMapistrue, create a frame from thegeometryusingframe(in coordinateSpace:). Use the frame to create an envelope in the map view usingenvelope(viewRect:). Pass the resulting envelope into the model’sgenerateOfflineMap(extent:)function and setisGeneratingOfflineMapto false.ContentView.swift153 collapsed lines// Copyright 2024 Esri//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.import SwiftUIimport ArcGIS@MainActorprivate class Model: ObservableObject {@Published private(set) var offlineMap: Map!@Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!private var offlineMapTask: OfflineMapTask!@Published private(set) var isGenerateDisabled = true@Published var isShowingAlert = falseprivate let temporaryDirectory: URL = {// swiftlint:disable:next force_tryreturn try! FileManager.default.url(for: .itemReplacementDirectory,in: .userDomainMask,appropriateFor: FileManager.default.temporaryDirectory,create: true)}()deinit {try? FileManager.default.removeItem(at: temporaryDirectory)}func initializeOfflineMapTask(onlineMap: Map) async {do {try await onlineMap.load()offlineMapTask = OfflineMapTask(onlineMap: onlineMap)isGenerateDisabled = false} catch {print(error)}}private func makeGenerateOfflineMapParameters(areaOfInterest: Envelope) async -> GenerateOfflineMapParameters? {do {return try await offlineMapTask.makeDefaultGenerateOfflineMapParameters(areaOfInterest: areaOfInterest)} catch {print(error)return nil}}func generateOfflineMap(extent: Envelope) async {isGenerateDisabled = trueguard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else {isGenerateDisabled = falsereturn}generateOfflineMapJob = offlineMapTask.makeGenerateOfflineMapJob(parameters: parameters,downloadDirectory: temporaryDirectory)generateOfflineMapJob.start()defer {generateOfflineMapJob = nilisGenerateDisabled = offlineMap != nil}do {let output = try await generateOfflineMapJob.outputofflineMap = output.offlineMaplet builder = EnvelopeBuilder(envelope: extent)builder.expand(by: 0.8)offlineMap.initialViewpoint = Viewpoint(boundingGeometry: builder.toGeometry())isShowingAlert = true} catch {print(error)}}}struct ContentView: View {@StateObject private var model = Model()@State private var isGeneratingOfflineMap = false@State private var map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous),id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!))var body: some View {GeometryReader { geometry inMapViewReader { mapView inMapView(map: model.offlineMap ?? map).interactionModes(isGeneratingOfflineMap ? [] : [.pan, .zoom]).task {await model.initializeOfflineMapTask(onlineMap: map)}.overlay {Rectangle().stroke(.red, lineWidth: 2).padding(EdgeInsets(top: 60, leading: 20, bottom: 100, trailing: 20)).opacity(model.offlineMap == nil ? 1 : 0)}.overlay {if isGeneratingOfflineMap,let progress = model.generateOfflineMapJob?.progress {VStack(spacing: 16) {ProgressView(progress).progressViewStyle(.linear).frame(maxWidth: 200)}.padding().background(.regularMaterial).clipShape(RoundedRectangle(cornerRadius: 15)).shadow(radius: 3)}}.alert("Offline map generated", isPresented: $model.isShowingAlert) {Button("Done") {model.isShowingAlert = false}}.toolbar {ToolbarItem(placement: .bottomBar) {Button("Download Map Area") {isGeneratingOfflineMap = true}.disabled(model.isGenerateDisabled || isGeneratingOfflineMap).task(id: isGeneratingOfflineMap) {guard isGeneratingOfflineMap else { return }let viewRect = geometry.frame(in: .local).inset(by: UIEdgeInsets(top: 60,left: geometry.safeAreaInsets.leading + 20,bottom: 100,right: -geometry.safeAreaInsets.trailing + 20))guard let extent = mapView.envelope(fromViewRect: viewRect) else { return }await model.generateOfflineMap(extent: extent)isGeneratingOfflineMap = false}}}8 collapsed lines}}}}
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.
You should see a web map
Tap the Download Map Area button to download the visible area of the web map, offline. Once the download is complete, you will be able to pinch, drag, and double-tap the map view to explore this offline map.
Alternatively, you can download the tutorial solution, as follows.
Option 2: Download the solution
-
Click the
Download solutionlink under Solution and unzip the file to a location on your machine. -
Open the
.xcodeprojfile in Xcode.
Since the downloaded solution does not contain authentication credentials, you must first set up authentication to create credentials, and then add the developer credentials to the solution.
Set up authentication
To access the secure ArcGIS location services
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
API key credentials are an item that contains the parameters used to create and manage long-lived access tokens for API key authentication. They are a type of developer credential. with the correct privileges. - API keys
An API key is a long-lived access token created using API key credentials. They are valid for up to one year and are typically embedded directly into client applications. 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
An ArcGIS account is an identity with a user type and set of privileges that can access specific ArcGIS products, tools, APIs, services, and resources. The main account types that can be used for development are an ArcGIS Location Platform account, ArcGIS Online account, and ArcGIS Enterprise account. ArcGIS Location Platform and ArcGIS Online accounts are also associated with a subscription. . - User accounts must have privilege
Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. to access the ArcGIS servicesA service, also known as an ArcGIS service, is software that supports an ArcGIS REST API and provides geospatial functionality or data. A service can be hosted by Esri or in ArcGIS Enterprise. used in application. - Requires creating OAuth credentials
OAuth credentials are an item that contains parameters required to implement user authentication or app authentication, including a .client_id,client_secret, and redirect URIs. They are a type of developer credential. - 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.
To complete this tutorial, click on the tab in the switcher below for your authentication type of choice, either API key authentication or User authentication.
Create a new API key access token
-
Complete the Create an API key tutorial and create an API key with the following privilege(s)
Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. :- Privileges
- Location services > Basemaps
- Privileges
-
Copy and paste the API key access token into a safe location. It will be used in a later step.
Create new OAuth credentials to access the secure resources used in this tutorial.
-
Complete the Create OAuth credentials for user authentication tutorial to obtain a Client ID and Redirect URL.
A
Client IDuniquely identifies your app on the authenticating server. If the server cannot find an app with the provided Client ID, it will not proceed with authentication.The
Redirect URL(also referred to as a callback url) is used to identify a response from the authenticating server when the system returns control back to your app after an OAuth login. Since it does not necessarily represent a valid endpoint that a user could navigate to, the redirect URL can use a custom scheme, such asmy-app://auth. It is important to make sure the redirect URL used in your app’s code matches a redirect URL configured on the authenticating server. -
Copy and paste the Client ID and Redirect URL into a safe location. They will be used in a later step.
All users that access this application need account privileges
Set developer credentials in the solution
To allow your app users to access ArcGIS location services
Pass your API Key access token to the ArcGISEnvironment.
-
In the Project Navigator, click MainApp.swift.
-
Set the
AuthenticationModeto.apiKey.MainApp.swift// Change the `AuthenticationMode` to `.apiKey` if your application uses API key authentication.private var authenticationMode: AuthenticationMode { .apiKey } -
Set the
apiKeyproperty with your API key access token.MainApp.swift31 collapsed lines// Copyright 2022 Esri//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.import SwiftUIimport ArcGISimport ArcGISToolkit@mainstruct MainApp: App {// The authentication mode.private enum AuthenticationMode {case apiKeycase user}// Change the `AuthenticationMode` to `.apiKey` if your application uses API key authentication.private var authenticationMode: AuthenticationMode { .apiKey }// Please enter an API key access token if your application uses API key authentication.private let apiKey = APIKey("<#YOUR-ACCESS-TOKEN#>")43 collapsed lines// Setup an `Authenticator` with OAuth configuration if your application uses OAuth credentials.@ObservedObject var authenticator = Authenticator(oAuthUserConfigurations: [OAuthUserConfiguration(// Please enter OAuth credentials for user authentication.portalURL: URL(string: "<#YOUR-PORTAL-URL#>")!,clientID: "<#YOUR-CLIENT-ID#>",redirectURL: URL(string: "<#YOUR-REDIRECT-URL#>")!)])func setAuthentication() {switch authenticationMode {case .apiKey:ArcGISEnvironment.apiKey = apiKeycase .user:ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = authenticator}}init() {setAuthentication()}var body: some SwiftUI.Scene {WindowGroup {ContentView().authenticator(authenticator).ignoresSafeArea()}}}
Best Practice: The access token is stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.
Use the Authenticator toolkit component to manage your OAuth credentialsclient_id, client_secret, and redirect URIs. They are a type of developer credential. ArcGISEnvironment.
-
In the Project Navigator, click MainApp.swift.
-
Set the
AuthenticationModeto.user.MainApp.swift// Change the `AuthenticationMode` to `.user` if your application uses OAuth credentials.private var authenticationMode: AuthenticationMode { .user } -
Set your
portalURL,clientIDandredirectURLvalues.MainApp.swift36 collapsed lines// Copyright 2022 Esri//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.import SwiftUIimport ArcGISimport ArcGISToolkit@mainstruct MainApp: App {// The authentication mode.private enum AuthenticationMode {case apiKeycase user}// Change the `AuthenticationMode` to `.user` if your application uses OAuth credentials.private var authenticationMode: AuthenticationMode { .apiKey }// Please enter an API key access token if your application uses API key authentication.private let apiKey = APIKey("<#YOUR-ACCESS-TOKEN#>")// Setup an `Authenticator` with OAuth configuration if your application uses OAuth credentials.@ObservedObject var authenticator = Authenticator(oAuthUserConfigurations: [OAuthUserConfiguration(// Please enter OAuth credentials for user authentication.portalURL: URL(string: "<#YOUR-PORTAL-URL#>")!,clientID: "<#YOUR-CLIENT-ID#>",redirectURL: URL(string: "<#YOUR-REDIRECT-URL#>")!)])28 collapsed linesfunc setAuthentication() {switch authenticationMode {case .apiKey:ArcGISEnvironment.apiKey = apiKeycase .user:ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = authenticator}}init() {setAuthentication()}var body: some SwiftUI.Scene {WindowGroup {ContentView().authenticator(authenticator).ignoresSafeArea()}}}
Best Practice: The OAuth credentials are stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.
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.
You should see a web map
Tap the Download Map Area button to download the visible area of the web map, offline. Once the download is complete, you will be able to pinch, drag, and double-tap the map view to explore this offline map.
What’s next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: