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
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 web 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() } } }
-
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
map
variable. Provide the web map's item ID.The code creates an
PortalItem
using thePortal
that references ArcGIS Online, and the web map'sitem
. TheI D portal
is used to create anItem Map
that is displayed in the app'sMapView
.ContentView.swiftUse dark colors for code blocks Change line @State private var map = Map( item: PortalItem( portal: .arcGISOnline(connection: .anonymous), id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")! ) )
-
Create a private class named
Model
of typeObservable
and add aObject @State
variable of theObject Model
to theContent
. Make theView Model
the@Main
. See the programming patterns page for more information on how to manage states.Actor ContentView.swiftUse dark colors for code blocks Add 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@Published
variable namedoffline
of typeMap Map
to store the output of the downloaded map.ContentView.swiftUse dark colors for code blocks Add 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 offline
has no value, then the map view should display the online map.Map ContentView.swiftUse dark colors for code blocks Change 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 MapViewReader
and expose theMapViewProxy
in its closure. Name itmap
.View Map
provides 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 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 { MapViewReader { mapView in MapView(map: model.offlineMap ?? map) } } }
-
Wrap the map view reader inside a
Geometry
and expose theReader Geometry
in its closure. Name itProxy geometry
.Geometry
provides 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 Add 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
.overlay
modifier to the map view. The overlay contains a red rectangle that encompasses an area to be downloaded.ContentView.swiftUse dark colors for code blocks 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) .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
GenerateOfflineMapJob
variable calledgenerate
and aOffline M a p Job OfflineMapTask
variable calledoffline
. These objects will contain the job and task needed to perform the download function. Learn more about Tasks and jobs.M a p Task ContentView.swiftUse dark colors for code blocks 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! }
-
Create a
@Published
Boolean variable calledi
and set it to true. Create a variable calleds Generate Disabled i
and set it to false. These variables will be used to determine if the download button is enabled and if the completion alert is present.s Showing Alert ContentView.swiftUse dark colors for code blocks 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 }
-
Create a private
URL
property namedtemporary
. This property will generate a uniqueDirectory URL URL
at which to store the offline map on the device.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. @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
deinit
function to remove the temporary directory after the app closes.ContentView.swiftUse dark colors for code blocks 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) } }
-
Create an asynchronous function called
initialize
. This function loads the online map and creates aOffline M a p Task(online M a p: ) OfflineMapTask
from it. Set thei
Boolean tos Generate Disabled false
.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. @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 initialize
function in aOffline M a p Task(online M a p: ) .task
modifier, passing in the onlinemap
. The task's function is called as the map view appears.ContentView.swiftUse dark colors for code blocks 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) .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 M a p Parameters(area Of Interest: ) GenerateOfflineMapParameters
, created usingoffline
and the area of interest.M a p Task ContentView.swiftUse dark colors for code blocks 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) } } 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: ) i
Boolean tos Generate Disabled true
and create default parameters for the given extent. If the parameters fail to create, set thei
Boolean tos Generate Disabled false
.ContentView.swiftUse dark colors for code blocks 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) } } 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
make
and start the job.Generate Offline M a p Job(parameters: download Directory: overrides: ) ContentView.swiftUse dark colors for code blocks 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() }
-
In a
defer
closure, ensure the job does not continue running by setting it tonil
and set thei
value according to whether or nots Generate Disabled offline
is instantiated.Map 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. 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
do
closure, wait for theGenerateOfflineMapJob
to produce an output that contains the downloaded offline map. Assign the output to theoffline
property. UseMap EnvelopeBuilder
to 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 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 { 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 @State
variable calledi
and set it tos Generating Offline Map false
. This Boolean indicates whether an offline map is currently being generated.ContentView.swiftUse dark colors for code blocks Add 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
.interaction
modifier to the map view. WhenModes i
iss Generating 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 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) } } } }
-
Add an
.overlay
modifier to the map view. Ifi
iss Generating Offline Map true
, create aProgress
to trackView model.generate
. Customize the progress view style, frame, and other attributes as required.Offline M a p Job?.progress 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. 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: i s Presented: presenting: actions: message: ) i
property to determine when to present the alert.s Showing Alert ContentView.swiftUse dark colors for code blocks 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 } } } } }
-
Add a
Button
labeled "Download Map Area" to a toolbar at the bottom of the map view. When you click on the button, thei
property is set tos Generating 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 i
).s Generating Offline Map == true 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. 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
.task
modifier to the button usingi
as its identifier. Whens Generating Offline Map i
iss Generating Offline Map true
, create a frame from thegeometry
usingframe(in coordinate
. Use the frame to create an envelope in the map view usingSpace: ) envelope(view
. Pass the resulting envelope into the model'sRect: ) generate
function and setOffline Map(extent: ) i
to false.s Generating Offline Map 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. .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 } } }
-
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.
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: