Export tiles

View on GitHub
Sample viewer app

Download tiles to a local tile cache file stored on the device.

Map of tiles to export Tile package result

Use case

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

How to use the sample

Pan and zoom into the desired area, making sure the area is within the red boundary. Tap the "Export tiles" button and choose an export format to start the process. On successful completion, you will see a preview of the downloaded tile package.

How it works

  1. Create a map and set its minScale to 10,000,000. Limiting the scale in this sample limits the potential size of the selection area, thereby keeping the exported tile package to a reasonable size.
  2. Create an AGSExportTileCacheTask, passing in the URL of the tiled layer.
  3. Create default AGSExportTileCacheParameters for the task, specifying the area of interest, minimum scale, and maximum scale.
  4. Use the parameters and a path to create an AGSExportTileCacheJob from the task.
  5. Start the job, and when it completes successfully, get the resulting AGSTileCache.
  6. Use the tile cache to create an AGSArcGISTiledLayer and display it in the map.

Relevant API

  • AGSArcGISTiledLayer
  • AGSExportTileCacheJob
  • AGSExportTileCacheParameters
  • AGSExportTileCacheTask
  • AGSTileCache

Additional information

ArcGIS tiled layers do not support reprojection, query, select, identify, or editing. See the layer types discussion in the developers guide to learn more about the characteristics of ArcGIS tiled layers.

The sample first tries to export the tiles using the CompactV2 (.tpkx) format. If it isn't supported, it will fallback to the CompactV1 (.tpk) format. Refer to the Tile Package Specification on GitHub for more information on the tile package format.

This workflow can be used with Esri basemaps.

Tags

cache, download, offline

Sample Code

ExportTilesViewController.swift
                                                                                                                                                                                                                                      
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
//
// Copyright 2016 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
//
//   http://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 UIKit
import ArcGIS

class ExportTilesViewController: UIViewController {
    // MARK: Storyboard views

    /// The map view managed by the view controller.
    @IBOutlet var mapView: AGSMapView! {
        didSet {
            mapView.map = AGSMap(basemap: AGSBasemap(baseLayer: tiledLayer))
            // Set the min scale of the map to avoid requesting a huge download.
            let scale = 1e7
            mapView.map?.minScale = scale
            let center = AGSPoint(x: -117, y: 34, spatialReference: .wgs84())
            mapView.setViewpoint(AGSViewpoint(center: center, scale: scale), completion: nil)
        }
    }

    /// A view to emphasize the extent of exported tile layer.
    @IBOutlet var extentView: UIView! {
        didSet {
            extentView.layer.borderColor = UIColor.red.cgColor
            extentView.layer.borderWidth = 2
        }
    }

    /// A view to provide a dark blurry background to preview the exported tiles.
    @IBOutlet var visualEffectView: UIVisualEffectView!
    /// A map view to preview the exported tiles.
    @IBOutlet var previewMapView: AGSMapView! {
        didSet {
            previewMapView.layer.borderColor = UIColor.white.cgColor
            previewMapView.layer.borderWidth = 8
        }
    }
    /// A bar button to initiate the download task.
    @IBOutlet var exportTilesBarButtonItem: UIBarButtonItem!

    // MARK: Properties

    /// The tiled layer created from world street map service.
    let tiledLayer = AGSArcGISTiledLayer(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/World_Street_Map/MapServer")!)
    /// The export task to request the tile package with the same URL as the tile layer.
    let exportTask = AGSExportTileCacheTask(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/World_Street_Map/MapServer")!)
    /// An export job to download the tile package.
    var job: AGSExportTileCacheJob! {
        didSet {
            exportTilesBarButtonItem.isEnabled = job == nil ? true : false
        }
    }

    /// A URL to the temporary directory to temporarily store the exported tile package.
    let temporaryDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(ProcessInfo().globallyUniqueString)

    /// Tile Package storage formats.
    /// - Note: Please read more about the file formats at [here](https://github.com/Esri/tile-package-spec).
    private enum TilePackageFormat {
        case tpk, tpkx

        var description: String {
            switch self {
            case .tpk:
                return "Compact Cache V1 (.\(fileExtension))"
            case .tpkx:
                return "Compact Cache V2 (.\(fileExtension))"
            }
        }

        var fileExtension: String {
            switch self {
            case .tpk:
                return "tpk"
            case .tpkx:
                return "tpkx"
            }
        }
    }

    // MARK: Methods

    /// Initiate the `AGSExportTileCacheTask` to download a tile package.
    ///
    /// - Parameters:
    ///   - exportTask: An `AGSExportTileCacheTask` to run the export job.
    ///   - downloadFileURL: A URL to where the tile package should be saved.
    func initiateDownload(exportTask: AGSExportTileCacheTask, downloadFileURL: URL) {
        // Get the parameters by specifying the selected area, map view's
        // current scale as the minScale, and tiled layer's max scale as
        // maxScale.
        var minScale = mapView.mapScale
        let maxScale = tiledLayer.maxScale
        if minScale < maxScale {
            minScale = maxScale
        }

        // Get current area of interest marked by the extent view.
        let areaOfInterest = frameToExtent()
        // Get export parameters.
        exportTask.exportTileCacheParameters(withAreaOfInterest: areaOfInterest, minScale: minScale, maxScale: maxScale) { [weak self, unowned exportTask] (params: AGSExportTileCacheParameters?, error: Error?) in
            guard let self = self else { return }
            if let params = params {
                self.exportTiles(exportTask: exportTask, parameters: params, downloadFileURL: downloadFileURL)
            } else if let error = error {
                self.presentAlert(error: error)
            }
        }
    }

    /// Export tiles with the `AGSExportTileCacheJob` from the export task.
    ///
    /// - Parameters:
    ///   - exportTask: An `AGSExportTileCacheTask` to run the export job.
    ///   - parameters: The parameters of the export task.
    ///   - downloadFileURL: A URL to where the tile package is saved.
    func exportTiles(exportTask: AGSExportTileCacheTask, parameters: AGSExportTileCacheParameters, downloadFileURL: URL) {
        // Get and run the job.
        job = exportTask.exportTileCacheJob(with: parameters, downloadFileURL: downloadFileURL)
        job.start(statusHandler: { (status) in
            UIApplication.shared.showProgressHUD(message: status.statusString())
        }, completion: { [weak self] (result, error) in
            UIApplication.shared.hideProgressHUD()
            guard let self = self else { return }

            self.job = nil

            if let tileCache = result {
                self.visualEffectView.isHidden = false

                let newTiledLayer = AGSArcGISTiledLayer(tileCache: tileCache)
                self.previewMapView.map = AGSMap(basemap: AGSBasemap(baseLayer: newTiledLayer))
                let extent = parameters.areaOfInterest as! AGSEnvelope
                self.previewMapView.setViewpoint(AGSViewpoint(targetExtent: extent), completion: nil)
            } else if let error = error {
                if (error as NSError).code != NSUserCancelledError {
                    self.presentAlert(error: error)
                }
            }
        })
    }

    /// Get the extent within the extent view for generating a tile package.
    func frameToExtent() -> AGSEnvelope {
        let frame = mapView.convert(extentView.frame, from: self.view)

        let minPoint = mapView.screen(toLocation: CGPoint(x: frame.minX, y: frame.minY))
        let maxPoint = mapView.screen(toLocation: CGPoint(x: frame.maxX, y: frame.maxY))
        let extent = AGSEnvelope(min: minPoint, max: maxPoint)
        return extent
    }

    /// Make the destination URL for the tile package.
    private func makeDownloadURL(fileFormat: TilePackageFormat) -> URL {
        // If the downloadFileURL ends with ".tpk", the tile cache will use
        // the legacy compact format. If the downloadFileURL ends with ".tpkx",
        // the tile cache will use the current compact version 2 format.
        // See more in the doc of
        // `AGSExportTileCacheTask.exportTileCacheJob(with:downloadFileURL:)`.

        // Create the temp directory if it doesn't exist.
        try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true)
        return temporaryDirectoryURL
            .appendingPathComponent("myTileCache", isDirectory: false)
            .appendingPathExtension(fileFormat.fileExtension)
    }

    // MARK: Actions

    @IBAction func exportTilesBarButtonTapped(_ sender: UIBarButtonItem) {
        if let mapServiceInfo = exportTask.mapServiceInfo, mapServiceInfo.exportTilesAllowed {
            // Try to download when exporting tiles is allowed.
            let tilePackageFormat: TilePackageFormat
            if mapServiceInfo.exportTileCacheCompactV2Allowed {
                // Export using the CompactV2 (.tpkx) if it is supported.
                tilePackageFormat = .tpkx
            } else {
                // Otherwise, use the CompactV1 (.tpk) format.
                tilePackageFormat = .tpk
            }
            self.initiateDownload(exportTask: exportTask, downloadFileURL: makeDownloadURL(fileFormat: tilePackageFormat))
        } else {
            presentAlert(title: "Error", message: "Exporting tiles is not supported for the service.")
        }
    }

    @IBAction func closeButtonTapped(_ sender: UIButton) {
        // Hide the preview and background.
        visualEffectView.isHidden = true
        // Release the map in order to free the tiled layer.
        previewMapView.map = nil
        // Remove the sample-specific temporary directory and all content in it.
        try? FileManager.default.removeItem(at: temporaryDirectoryURL)
    }

    // MARK: UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        // Add the source code button item to the right of navigation bar.
        (navigationItem.rightBarButtonItem as! SourceCodeBarButtonItem).filenames = ["ExportTilesViewController"]
        // Load the export task.
        exportTask.load { [weak self] error in
            guard let self = self else { return }
            if let error = error {
                self.presentAlert(error: error)
            } else {
                self.exportTilesBarButtonItem.isEnabled = true
            }
        }
    }

    deinit {
        // Remove the temporary directory and all content in it.
        try? FileManager.default.removeItem(at: temporaryDirectoryURL)
    }
}

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