Learn how to download and display an offline map An offline map is a map area and its data content downloaded from an offline-enabled web map for use in offline applications built with ArcGIS Maps SDKs for Native Apps. Learn more for a user-defined geographical area of a web map A 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. Learn more .

display offline map on demand

Offline maps An offline map is a map area and its data content downloaded from an offline-enabled web map for use in offline applications built with ArcGIS Maps SDKs for Native Apps. Learn more allow users to continue working when network connectivity is poor or lost. If a web map A 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. Learn more 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 An offline map is a map area and its data content downloaded from an offline-enabled web map for use in offline applications built with ArcGIS Maps SDKs for Native Apps. Learn more for an area of interest from the web map A 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. Learn more of the stormwater network within Naperville, IL, USA . You can then use this offline map without a network connection.

Prerequisites

Before starting this tutorial:

  1. You need an ArcGIS Location Platform or ArcGIS Online account.

  2. Your system meets the system requirements.

Develop or Download

You have two options for completing this tutorial:

  1. Option 1: Develop the code or
  2. Option 2: Download the completed solution

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 The ArcGIS Basemap Styles service, also referred to as the Basemap Styles service, is a location service that provides basemap styles and data for the world. It returns styles as Mapbox styles and web maps, and data as vector tiles and/or map tiles. It supports all of the styles in the ArcGIS Basemap style and Open Basemap style family. An ArcGIS Location Platform or ArcGIS Online account is required to use the service. Learn more .

Open an Xcode project

  1. Open the .xcodeproj project you created by completing the Display a map tutorial.
  2. 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 Tools, also known as developer tools, are ArcGIS software applications such as portal and ArcGIS Pro that developers can use to prepare content and data for custom applications they are building. Learn more to create and view web maps A 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. Learn more . Use 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. Learn more to identify the web map 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. Learn more . This item ID will be used later in the tutorial.

  1. 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. Learn more in ArcGIS Online ArcGIS 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. Learn more . This web map A 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. Learn more displays a stormwater network within Naperville, Illinois, USA.

  2. 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. Learn more at the end of the browser’s URL.

Display the web map

You can display a web map A 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. Learn more using the web map’s 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. Learn more . Create an Map from the web map’s PortalItem, and display it in your app’s MapView.

  1. In Xcode, in the Project Navigator, click ContentView.swift.

  2. In the editor, modify the map variable. Provide the web map’s item ID.

    ContentView.swift
    @State private var map = Map(
    item: PortalItem(
    portal: .arcGISOnline(connection: .anonymous),
    id: PortalItem.ID("5a030a31e42841a89914bd7c5ecf4d8f")!
    )
    )
  3. Create a private class named Model of type ObservableObject and add a @StateObject variable of the Model to the ContentView. Make the Model the @MainActor. See the programming patterns page for more information on how to manage states.

    ContentView.swift
    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")!
    )
    )
  4. In the Model, create a @Published variable named offlineMap of type Map to store the output of the downloaded map.

    ContentView.swift
    @MainActor
    private class Model: ObservableObject {
    @Published private(set) var offlineMap: Map!
    }
  5. In the ContentView, modify the map view to display the model’s offline map or the online map. If offlineMap has no value, then the map view should display the online map.

    ContentView.swift
    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 A 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. Learn more 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.

  1. In the ContentView, wrap the map view inside a MapViewReader and expose the MapViewProxy in its closure. Name it mapView. MapViewProxy provides operations that can be performed on the map view, such as envelope(fromViewRect:). For more information see Perform GeoView operations.

    ContentView.swift
    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)
    }
    }
    }
  2. Wrap the map view reader inside a GeometryReader and expose the GeometryProxy in its closure. Name it geometry. GeometryProxy provides access to the size and coordinate space (for anchor resolution) of the views enclosed in the geometry reader.

    ContentView.swift
    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)
    }
    }
    }
    }
  3. Add an .overlay modifier to the map view. The overlay contains a red rectangle that encompasses an area to be downloaded.

    ContentView.swift
    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 An offline map is a map area and its data content downloaded from an offline-enabled web map for use in offline applications built with ArcGIS Maps SDKs for Native Apps. Learn more 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 A map view is a user interface that displays map layers and graphics in 2D. It controls the area (extent) of the map that is visible and supports user interactions such as pan and zoom. Learn more .

  1. In the Model, create a @Published GenerateOfflineMapJob variable called generateOfflineMapJob and a OfflineMapTask variable called offlineMapTask. These objects will contain the job and task needed to perform the download function. Learn more about Tasks and jobs.

    ContentView.swift
    @MainActor
    private class Model: ObservableObject {
    @Published private(set) var offlineMap: Map!
    @Published private(set) var generateOfflineMapJob: GenerateOfflineMapJob!
    private var offlineMapTask: OfflineMapTask!
    }
  2. Create a @Published Boolean variable called isGenerateDisabled and set it to true. Create a variable called isShowingAlert 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.

    ContentView.swift
    @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
    }
  3. Create a private URL property named temporaryDirectoryURL. This property will generate a unique URL at which to store the offline map on the device.

    ContentView.swift
    @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
    )
    }()
    }
  4. Create a deinit function to remove the temporary directory after the app closes.

    ContentView.swift
    @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)
    }
    }
  5. Create an asynchronous function called initializeOfflineMapTask(onlineMap:). This function loads the online map and creates a OfflineMapTask from it. Set the isGenerateDisabled Boolean to false.

    ContentView.swift
    @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)
    }
    }
    }
  6. In the ContentView, call the initializeOfflineMapTask(onlineMap:) function in a .task modifier, passing in the online map. The task’s function is called as the map view appears.

    ContentView.swift
    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)
    }
    }
    }
    }
    }
  7. In the Model, create an asynchronous function called makeGenerateOfflineMapParameters(areaOfInterest:). The function returns GenerateOfflineMapParameters, created using offlineMapTask and the area of interest.

    ContentView.swift
    @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
    }
    }
    }
  8. Create an asynchronous function called generateOfflineMap(extent:). This function is called when the user initiates the download. Set the isGenerateDisabled Boolean to true and create default parameters for the given extent. If the parameters fail to create, set the isGenerateDisabled Boolean to false.

    ContentView.swift
    @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
    }
    }
    }
  9. Create an offline map job using makeGenerateOfflineMapJob(parameters:downloadDirectory:overrides:) and start the job.

    ContentView.swift
    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()
    }
  10. In a defer closure, ensure the job does not continue running by setting it to nil and set the isGenerateDisabled value according to whether or not offlineMap is instantiated.

    ContentView.swift
    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)
    }
    }
  11. In the do closure, wait for the GenerateOfflineMapJob to produce an output that contains the downloaded offline map. Assign the output to the offlineMap property. Use EnvelopeBuilder to expand the extent by 0.8. Set the offline map’s initial viewpoint to this new envelope. Indicate that an alert is shown to the user.

    ContentView.swift
    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 ProgressView to display the download progress, and use alert(_:isPresented:presenting:actions:message:) to indicate when the download is complete.

  1. In the ContentView, add a @State variable called isGeneratingOfflineMap and set it to false. This Boolean indicates whether an offline map is currently being generated.

    ContentView.swift
    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")!
    )
    )
  2. Add an .interactionModes modifier to the map view. When isGeneratingOfflineMap is true, the user should not be able to interact. Otherwise, allow the user to pan and zoom.

    ContentView.swift
    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)
    }
    }
    }
    }
  3. Add an .overlay modifier to the map view. If isGeneratingOfflineMap is true, create a ProgressView to track model.generateOfflineMapJob?.progress. Customize the progress view style, frame, and other attributes as required.

    ContentView.swift
    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)
    }
    }
    }
    }
    }
  4. 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’s isShowingAlert property to determine when to present the alert.

    ContentView.swift
    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
    }
    }
    }
    }
    }
  5. Add a Button labeled “Download Map Area” to a toolbar at the bottom of the map view. When you click on the button, the isGeneratingOfflineMap property is set to true. 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.swift
    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)
    }
    }
    }
    }
    }
  6. Add a .task modifier to the button using isGeneratingOfflineMap as its identifier. When isGeneratingOfflineMap is true, create a frame from the geometry using frame(in coordinateSpace:). Use the frame to create an envelope in the map view using envelope(viewRect:). Pass the resulting envelope into the model’s generateOfflineMap(extent:) function and set isGeneratingOfflineMap to false.

    ContentView.swift
    153 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 SwiftUI
    import ArcGIS
    @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
    }
    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)
    }
    }
    }
    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 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)
    .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.

You should see a web map A 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. Learn more 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

  1. Click the Download solution link under Solution and unzip the file to a location on your machine.

  2. Open the .xcodeproj file 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 ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more used in this tutorial, you must implement API key authentication API key authentication is a type of authentication that uses an API key to authenticate requests to ArcGIS services and secure portal items. Learn more or user authentication User authentication is a type of authentication that allows users with an ArcGIS account to sign into an application and allow it to access ArcGIS content, services, and resources on their behalf. The typical authorization protocol used is OAuth2.0. Learn more using an ArcGIS Location Platform An ArcGIS Location Platform account, formerly known as an ArcGIS Developer account, is an identity associated with an ArcGIS Location Platform subscription. Learn more or an ArcGIS Online An ArcGIS Online account, also known as an ArcGIS Organization account, is an identity associated with an ArcGIS Online subscription. It can be used to access ArcGIS tools and develop applications with ArcGIS location services for an organization. Learn more account.

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 An access token is an authorization string that provides access to secure ArcGIS content, data, and services. Its capabilities are determined by the privileges it supports. It is obtained by implementing API key authentication, User authentication, or App authentication. Learn more with privileges Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. Learn more to access the secure resources used in this tutorial.

  1. 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. Learn more :

    • Privileges
      • Location services > Basemaps
  2. 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 ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more , 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.

  1. In the Project Navigator, click MainApp.swift.

  2. Set the AuthenticationMode to .apiKey.

    MainApp.swift
    // Change the `AuthenticationMode` to `.apiKey` if your application uses API key authentication.
    private var authenticationMode: AuthenticationMode { .apiKey }
  3. Set the apiKey property with your API key access token.

    MainApp.swift
    31 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 SwiftUI
    import ArcGIS
    import ArcGISToolkit
    @main
    struct MainApp: App {
    // The authentication mode.
    private enum AuthenticationMode {
    case apiKey
    case 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 = apiKey
    case .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.

Run the solution

Press Command + R to run the app.

You should see a web map A 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. Learn more 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: