Download vector tiles to local cache

View on GitHub

Download tiles from an online vector tile service.

Screenshot of download vector tiles to local cache sample downloading Screenshot of download vector tiles to local cache sample results

Use case

Field workers with limited network connectivity can use exported vector tiles as a basemap for use while offline.

How to use the sample

When the vector tiled layer loads, zoom in to the extent you want to export. The red box shows the extent that will be exported. Tap the "Export Vector Tiles" button to start the job. An error will show if the extent is larger than the maximum limit allowed. When finished, a dialog will show the exported result in a new map view.

How it works

  1. Create an ExportVectorTilesTask instance, passing in the PortalItem for the vector tiled layer. Since vector tiled layers are premium content, you must first authenticate with the Portal.
  2. Create parameters for the export by using the task's method, ExportVectorTilesTask.makeDefaultExportVectorTilesParameters(areaOfInterest:maxScale:), specifying the area of interest and max scale.
  3. Create an ExportVectorTileJob instance by using the task's method, ExportVectorTilesTask.makeExportVectorTilesJob(parameters:vectorTileCacheURL:itemResourceCacheURL:), passing in the parameters and specifying a vector tile cache path and an item resource path. The resource path is required if you want to export the tiles with the style.
  4. Start the job and await its output.
  5. Get the VectorTileCache and ItemResourceCache from the output and create an ArcGISVectorTiledLayer instance.
  6. Create a Map instance, specifying a basemap with a base layer of the vector tiled layer.
  7. Set the map's initial viewpoint to the area of interest and create a map view with the map.

Relevant API

  • ArcGISVectorTiledLayer
  • ExportVectorTilesJob
  • ExportVectorTilesParameters
  • ExportVectorTilesResult
  • ExportVectorTilesTask
  • ExportVectorTilesTask.makeDefaultExportVectorTilesParameters(areaOfInterest:maxScale:)
  • ExportVectorTilesTask.makeExportVectorTilesJob(parameters:vectorTileCacheURL:itemResourceCacheURL:)
  • ItemResourceCache
  • VectorTileCache

Additional information

NOTE: Downloading tiles for offline use requires authentication with the web map's server. To use this sample, you will need an ArcGIS Online account.

Vector tiles have high drawing performance and smaller file size compared to regular tiled layers, due to consisting solely of points, lines, and polygons. Learn more about the characteristics of ArcGIS vector tiled layers.

Tags

cache, download, offline, vector

Sample Code

DownloadVectorTilesToLocalCacheView.swift
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
// 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 ArcGIS
import SwiftUI

struct DownloadVectorTilesToLocalCacheView: View {
    /// A Boolean value indicating whether to download vector tiles.
    @State private var isDownloading = false

    /// A Boolean value indicating whether to cancel the job.
    @State private var isCancellingJob = false

    /// A Boolean value indicating whether to show the result map.
    @State private var isShowingResults = false

    /// The map view's scale.
    @State private var mapViewScale = Double.zero

    /// The error shown in the error alert.
    @State private var error: Error?

    /// The view model for this sample.
    @StateObject private var model = Model()

    var body: some View {
        GeometryReader { geometry in
            MapViewReader { mapViewProxy in
                MapView(map: model.map)
                    .interactionModes(isDownloading ? [] : [.pan, .zoom])
                    .onScaleChanged { mapViewScale = $0 }
                    .errorAlert(presentingError: $error)
                    .task {
                        do {
                            try await model.initializeVectorTilesTask()
                        } catch {
                            self.error = error
                        }
                    }
                    .onDisappear {
                        Task { await model.cancelJob() }
                    }
                    .overlay {
                        Rectangle()
                            .stroke(.red, lineWidth: 2)
                            .padding(EdgeInsets(top: 20, leading: 20, bottom: 44, trailing: 20))
                            .opacity(isShowingResults ? 0 : 1)
                    }
                    .overlay {
                        if isDownloading,
                           let progress = model.exportVectorTilesJob?.progress {
                            VStack(spacing: 16) {
                                ProgressView(progress)
                                    .progressViewStyle(.linear)
                                    .frame(maxWidth: 200)

                                Button("Cancel") {
                                    isCancellingJob = true
                                }
                                .disabled(isCancellingJob)
                                .task(id: isCancellingJob) {
                                    // Ensures cancelling the job is true.
                                    guard isCancellingJob else { return }
                                    // Cancels the job.
                                    await model.cancelJob()
                                    // Sets cancelling the job and downloading to false.
                                    isCancellingJob = false
                                    isDownloading = false
                                }
                            }
                            .padding()
                            .background(.regularMaterial)
                            .clipShape(RoundedRectangle(cornerRadius: 15))
                            .shadow(radius: 3)
                        }
                    }
                    .toolbar {
                        ToolbarItem(placement: .bottomBar) {
                            Button("Download Vector Tiles") {
                                isDownloading = true
                            }
                            .disabled(!model.allowsDownloadingVectorTiles || isDownloading)
                            .task(id: isDownloading) {
                                // Ensures downloading is true.
                                guard isDownloading else { return }

                                // Creates a rectangle from the area of interest.
                                let viewRect = geometry.frame(in: .local).inset(
                                    by: UIEdgeInsets(
                                        top: 20,
                                        left: geometry.safeAreaInsets.leading + 20,
                                        bottom: 44,
                                        right: -geometry.safeAreaInsets.trailing + 20
                                    )
                                )

                                // Creates an envelope from the rectangle.
                                guard let extent = mapViewProxy.envelope(fromViewRect: viewRect) else { return }

                                // Downloads the vector tiles.
                                do {
                                    // Sets downloading to false when the download
                                    // finishes or errors occur.
                                    defer { isDownloading = false }
                                    // Sets the max scale to 10% of the map's scale to limit
                                    // the number of tiles exported.
                                    try await model.downloadVectorTiles(extent: extent, maxScale: mapViewScale * 0.1)
                                    // Shows results when the download finishes.
                                    isShowingResults = true
                                } catch {
                                    // Shows an alert if any errors occur.
                                    self.error = error
                                }
                            }
                            .sheet(isPresented: $isShowingResults) {
                                // Removes the temporary files when the cover is dismissed.
                                model.removeTemporaryFiles()
                            } content: {
                                NavigationStack {
                                    MapView(map: model.downloadedVectorTilesMap)
                                        .navigationTitle("Vector tile package")
                                        .navigationBarTitleDisplayMode(.inline)
                                        .toolbar {
                                            ToolbarItem(placement: .confirmationAction) {
                                                Button("Done") {
                                                    isShowingResults = false
                                                }
                                            }
                                        }
                                        .overlay(alignment: .top) {
                                            Text("Vector tiles downloaded.")
                                                .frame(maxWidth: .infinity, alignment: .center)
                                                .padding(8)
                                                .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal)
                                        }
                                }
                                .highPriorityGesture(DragGesture())
                            }
                        }
                    }
            }
        }
    }
}

private extension DownloadVectorTilesToLocalCacheView {
    /// The model used to store the geo model and other expensive objects
    /// used in this view.
    @MainActor
    class Model: ObservableObject {
        /// A map with a basemap from the vector tiled layer results.
        private(set) var downloadedVectorTilesMap: Map!

        /// The export vector tiles job.
        @Published private(set) var exportVectorTilesJob: ExportVectorTilesJob!

        /// The export vector tiles task.
        @Published private(set) var exportVectorTilesTask: ExportVectorTilesTask!

        /// The vector tiled layer from the downloaded result.
        private var vectorTiledLayerResults: ArcGISVectorTiledLayer!

        /// A URL to the directory temporarily storing all items.
        private let temporaryDirectory = createTemporaryDirectory()

        /// A URL to the temporary directory to store the exported vector tile package.
        private let vtpkTemporaryURL: URL

        /// A URL to the temporary directory to store the style item resources.
        private let styleTemporaryURL: URL

        /// A Boolean value indicating whether the export task can be started.
        var allowsDownloadingVectorTiles: Bool {
            if let exportVectorTilesTask,
               // Only allows downloading when the task is loaded.
               exportVectorTilesTask.loadStatus == .loaded,
               // Ensures that the service allows exporting vector tiles.
               let vectorTileSourceInfo = exportVectorTilesTask.vectorTileSourceInfo {
                return vectorTileSourceInfo.allowsExportingTiles
            } else {
                return false
            }
        }

        /// A map with a night streets basemap style and an initial viewpoint.
        let map: Map = {
            let map = Map(basemapStyle: .arcGISStreetsNight)
            map.initialViewpoint = Viewpoint(latitude: 34.049, longitude: -117.181, scale: 1e4)
            // Sets the min scale to avoid requesting a huge download.
            map.minScale = 1e4
            return map
        }()

        init() {
            // Initializes the URL for the directory containing vector tile packages.
            vtpkTemporaryURL = temporaryDirectory
                .appendingPathComponent("myTileCache")
                .appendingPathExtension("vtpk")

            // Initializes the URL for the directory containing style item resources.
            styleTemporaryURL = temporaryDirectory
                .appendingPathComponent("styleItemResources", isDirectory: true)
        }

        deinit {
            // Removes the temporary directory.
            try? FileManager.default.removeItem(at: temporaryDirectory)
        }

        /// Initializes the vector tiles task.
        func initializeVectorTilesTask() async throws {
            guard exportVectorTilesTask == nil else { return }
            // Waits for the map to load.
            try await map.load()
            // Gets the map's base layers.
            guard let vectorTiledLayer = map.basemap?.baseLayers.first as? ArcGISVectorTiledLayer,
                  let url = vectorTiledLayer.url else { return }
            // Creates the export vector tiles task from the base layers' URL.
            let exportVectorTilesTask = ExportVectorTilesTask(url: url)
            // Loads the export vector tiles task.
            try await exportVectorTilesTask.load()
            self.exportVectorTilesTask = exportVectorTilesTask
        }

        /// Downloads the vector tiles within the area of interest at given scale.
        /// - Parameters:
        ///   - extent: The area of interest's envelope to export vector tiles.
        ///   - maxScale: The map scale which determines how far in to export
        ///   the vector tiles. Set to `0` to include all levels of detail.
        func downloadVectorTiles(extent: Envelope, maxScale: Double) async throws {
            // Creates the parameters for the export vector tiles job.
            let parameters = try await exportVectorTilesTask.makeDefaultExportVectorTilesParameters(
                areaOfInterest: extent,
                maxScale: maxScale
            )

            // Creates the export vector tiles job based on the parameters
            // and temporary URLs.
            exportVectorTilesJob = exportVectorTilesTask.makeExportVectorTilesJob(
                parameters: parameters,
                vectorTileCacheURL: vtpkTemporaryURL,
                itemResourceCacheURL: styleTemporaryURL
            )

            // Starts the job.
            exportVectorTilesJob.start()

            defer { exportVectorTilesJob = nil }

            // Awaits the output of the job.
            let output = try await exportVectorTilesJob.output

            // Gets the vector tile and item resource cache from the output.
            if let vectorTileCache = output.vectorTileCache,
               let itemResourceCache = output.itemResourceCache {
                // Creates a vector tiled layer from the caches.
                vectorTiledLayerResults = ArcGISVectorTiledLayer(
                    vectorTileCache: vectorTileCache,
                    itemResourceCache: itemResourceCache
                )

                // Creates a map with a basemap from the vector tiled layer results.
                downloadedVectorTilesMap = Map(basemap: Basemap(baseLayer: vectorTiledLayerResults))

                // Sets the initial viewpoint of the result map.
                downloadedVectorTilesMap.initialViewpoint = Viewpoint(boundingGeometry: extent.expanded(by: 0.9))
            }
        }

        /// Cancels the export vector tiles job.
        func cancelJob() async {
            await exportVectorTilesJob?.cancel()
            exportVectorTilesJob = nil
        }

        /// Removes any temporary files.
        func removeTemporaryFiles() {
            try? FileManager.default.removeItem(at: vtpkTemporaryURL)
            try? FileManager.default.removeItem(at: styleTemporaryURL)
        }

        /// Creates a temporary directory.
        /// - Returns: The URL to the temporary directory.
        private static func createTemporaryDirectory() -> URL {
            // swiftlint:disable:next force_try
            try! FileManager.default.url(
                for: .itemReplacementDirectory,
                in: .userDomainMask,
                appropriateFor: FileManager.default.temporaryDirectory,
                create: true
            )
        }
    }
}

private extension Envelope {
    /// Expands the envelope by a given factor.
    func expanded(by factor: Double) -> Envelope {
        let builder = EnvelopeBuilder(envelope: self)
        builder.expand(by: factor)
        return builder.toGeometry()
    }
}

#Preview {
    NavigationStack {
        DownloadVectorTilesToLocalCacheView()
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.