Learn how to download and display an offline map for a user-defined geographical area of a web map.
Offline maps allow users to continue working when network connectivity is poor or lost. If a web map is enabled for offline use, a user can request that ArcGIS generates an offline map for a specified geographic area of interest.
In this tutorial, you will download an offline map for an area of interest from the web map of the stormwater network within Naperville, IL, USA . You can then use this offline map without a network connection.
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 to create and view web maps. Use the Map Viewer to identify the web map item ID. This item ID will be used later in the tutorial.
-
Go to the Naperville water network in the Map Viewer in ArcGIS Online. This web map displays a stormwater network within Naperville, Illinois, USA.
-
Make a note of the item ID at the end of the browser's URL.
The item ID should be 5a030a31e42841a89914bd7c5ecf4d8f.
Display the web map
You can display a web map using the web map's item ID. Create an Map 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 Online, and the web map'sitem. TheID portalis used to create anItem Mapthat is displayed in the app'sMapView.ContentView.swiftUse dark colors for code blocks 103 104 105 106 108 109 110Change line @State private var map = Map( item: PortalItem( portal: .arcGISOnline(connection: .anonymous), id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")! ) ) -
Create a private class named
Modelof typeObservableand add aObject @variable of theState Object Modelto theContent. Make theView Modelthe@. See the programming patterns page for more information on how to manage states.Main Actor ContentView.swiftUse dark colors for code blocks 16 17 18 23 24 25 27 28 29 30 31 32 33 34 35Add line. Add line. Add line. Add line. Add line. import SwiftUI import ArcGIS @MainActor private 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@variable namedPublished offlineof typeMap Mapto store the output of the downloaded map.ContentView.swiftUse dark colors for code blocks 19 20 21 23 24Add line. @MainActor private class Model: ObservableObject { @Published private(set) var offlineMap: Map! } -
In the
Content, modify the map view to display the model's offline map or the online map. IfView offlinehas no value, then the map view should display the online map.Map ContentView.swiftUse dark colors for code blocks 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 113 114 115 116Change line struct 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 map to take offline using either an Envelope 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
Content, wrap the map view inside aView MapViewReaderand expose theMapViewProxyin its closure. Name itmap.View Mapprovides operations that can be performed on the map view, such asView Proxy envelope(from. For more information see Perform GeoView operations.View Rect :) ContentView.swiftUse dark colors for code blocks 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 117 118 119 120Add line. Add line. Add line. Add line. Add line. struct 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 in MapView(map: model.offlineMap ?? map) } } } -
Wrap the map view reader inside a
Geometryand expose theReader Geometryin its closure. Name itProxy geometry.Geometryprovides access to the size and coordinate space (for anchor resolution) of the views enclosed in the geometry reader.Proxy ContentView.swiftUse dark colors for code blocks 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 121 122 123 124Add line. 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 = Map( item: PortalItem( portal: .arcGISOnline(connection: .anonymous), id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")! ) ) var body: some View { GeometryReader { geometry in MapViewReader { mapView in MapView(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.swiftUse dark colors for code blocks 112 113 114 115 116 117 118 119 126 127 128 129 130 131Add line. Add line. Add line. Add line. Add line. Add line. var body: some View { GeometryReader { geometry in MapViewReader { mapView in MapView(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 for a specified area of interest using an asynchronous task. When complete, it will provide the offline map that can be displayed in a map view.
-
In the
Model, create a@Published GenerateOfflineMapJobvariable calledgenerateand aOffline Map Job OfflineMapTaskvariable calledoffline. These objects will contain the job and task needed to perform the download function. Learn more about Tasks and jobs.Map Task ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 26 27Add line. Add line. @MainActor private class Model: ObservableObject { @Published private(set) var offlineMap: Map! @Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob! private var offlineMapTask: OfflineMapTask! } -
Create a
@Boolean variable calledPublished isand set it to true. Create a variable calledGenerate Disabled isand set it to false. These variables will be used to determine if the download button is enabled and if the completion alert is present.Showing Alert ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 29 6Add line. Add line. @MainActor private 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 namedtemporary. This property will generate a uniqueDirectory URL URLat which to store the offline map on the device.ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 27 28 29 39 40Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. @MainActor private 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 private let temporaryDirectory: URL = { // swiftlint:disable:next force_try return 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.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 43 44Add line. Add line. Add line. @MainActor private 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 private let temporaryDirectory: URL = { // swiftlint:disable:next force_try return 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
initialize. This function loads the online map and creates aOffline Map Task(online Map :) OfflineMapTaskfrom it. Set theisBoolean toGenerate Disabled false.ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 53 54Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. @MainActor private 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 private let temporaryDirectory: URL = { // swiftlint:disable:next force_try return 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
Content, call theView initializefunction in aOffline Map Task(online Map :) .taskmodifier, passing in the onlinemap. The task's function is called as the map view appears.ContentView.swiftUse dark colors for code blocks 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135Add line. Add line. Add line. struct 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 in MapViewReader { mapView in MapView(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 calledmake. The function returnsGenerate Offline Map Parameters(area Of Interest :) GenerateOfflineMapParameters, created usingofflineand the area of interest.Map Task ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 62 63Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. @MainActor private 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 private let temporaryDirectory: URL = { // swiftlint:disable:next force_try return 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
generate. This function is called when the user initiates the download. Set theOffline Map(extent :) isBoolean toGenerate Disabled trueand create default parameters for the given extent. If the parameters fail to create, set theisBoolean toGenerate Disabled false.ContentView.swiftUse dark colors for code blocks 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 64 65Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. @MainActor private 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 private let temporaryDirectory: URL = { // swiftlint:disable:next force_try return 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 = true guard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else { isGenerateDisabled = false return } } } -
Create an offline map job using
makeand start the job.Generate Offline Map Job(parameters :download Directory :overrides :) ContentView.swiftUse dark colors for code blocks 63 64 65 66 67 68 69 75 69Add line. Add line. Add line. Add line. Add line. func generateOfflineMap(extent: Envelope) async { isGenerateDisabled = true guard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else { isGenerateDisabled = false return } generateOfflineMapJob = offlineMapTask.makeGenerateOfflineMapJob( parameters: parameters, downloadDirectory: temporaryDirectory ) generateOfflineMapJob.start() } -
In a
deferclosure, ensure the job does not continue running by setting it toniland set theisvalue according to whether or notGenerate Disabled offlineis instantiated.Map ContentView.swiftUse dark colors for code blocks 63 64 65 66 67 68 69 70 71 72 73 74 75 85 86Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. func generateOfflineMap(extent: Envelope) async { isGenerateDisabled = true guard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else { isGenerateDisabled = false return } generateOfflineMapJob = offlineMapTask.makeGenerateOfflineMapJob( parameters: parameters, downloadDirectory: temporaryDirectory ) generateOfflineMapJob.start() defer { generateOfflineMapJob = nil isGenerateDisabled = 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 theofflineproperty. UseMap EnvelopeBuilderto 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.swiftUse dark colors for code blocks 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 88 89 90 91 92 93Add line. Add line. Add line. Add line. Add line. Add line. func generateOfflineMap(extent: Envelope) async { isGenerateDisabled = true guard let parameters = await makeGenerateOfflineMapParameters(areaOfInterest: extent) else { isGenerateDisabled = false return } generateOfflineMapJob = offlineMapTask.makeGenerateOfflineMapJob( parameters: parameters, downloadDirectory: temporaryDirectory ) generateOfflineMapJob.start() defer { generateOfflineMapJob = nil isGenerateDisabled = offlineMap != nil } do { let output = try await generateOfflineMapJob.output offlineMap = output.offlineMap let 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 Progress to display the download progress, and use alert( to indicate when the download is complete.
-
In the
Content, add aView @variable calledState isand set it toGenerating Offline Map false. This Boolean indicates whether an offline map is currently being generated.ContentView.swiftUse dark colors for code blocks 97 98 99 100 102 103 104 105 106 107 108 109 110Add line. 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")! ) ) -
Add an
.interactionmodifier to the map view. WhenModes isisGenerating Offline Map true, the user should not be able to interact. Otherwise, allow the user to pan and zoom.ContentView.swiftUse dark colors for code blocks 112 113 114 115 116 117 118 119 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137Add line. var body: some View { GeometryReader { geometry in MapViewReader { mapView in MapView(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. IfisisGenerating Offline Map true, create aProgressto trackView model.generate. Customize the progress view style, frame, and other attributes as required.Offline Map Job?.progress ContentView.swiftUse dark colors for code blocks 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 147 148 149 150 151 152Add 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. var body: some View { GeometryReader { geometry in MapViewReader { mapView in MapView(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(modifier to the map view. Present an alert to indicate that the offline map has finished generating. Use the model's_:is Presented :presenting :actions :message :) isproperty to determine when to present the alert.Showing Alert ContentView.swiftUse dark colors for code blocks 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 153 154 155 156 157 158Add line. Add line. Add line. Add line. Add line. var body: some View { GeometryReader { geometry in MapViewReader { mapView in MapView(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, theisproperty is set toGenerating Offline Map true. The button is disabled if the offline map has been generated (model.is) or if the offline map is being generated (Generated Disabled == true is).Generating Offline Map == true ContentView.swiftUse dark colors for code blocks 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 165 166 167 168 169 170Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. Add line. var body: some View { GeometryReader { geometry in MapViewReader { mapView in MapView(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 usingisas its identifier. WhenGenerating Offline Map isisGenerating Offline Map true, create a frame from thegeometryusingframe(in coordinate. Use the frame to create an envelope in the map view usingSpace :) envelope(view. Pass the resulting envelope into the model'sRect :) generatefunction and setOffline Map(extent :) isto false.Generating Offline Map ContentView.swiftUse dark colors for code blocks 154 155 156 157 158 159 160 161 162 177 178 179Add 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. .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 } } }
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 of the Naperville water network in the map view and a Download Map Area button embedded within the bottom toolbar.
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 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
- Privileges
-
Copy and paste the API key access token into a safe location. It will be used in a later step.
Set developer credentials in the solution
To allow your app users to access ArcGIS location services, use the developer credentials that you created in the Set up authentication step to authenticate requests for resources.
Pass your API Key access token to the ArcGISEnvironment.
-
In the Project Navigator, click MainApp.swift.
-
Set the
AuthenticationtoMode .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
apiproperty 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. 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 of the Naperville water network in the map view and a Download Map Area button embedded within the bottom toolbar.
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: