Display maps and use locators to enable search and routing offline using a Mobile Map Package.

Use case
Mobile map packages make it easy to transmit and store the necessary components for an offline map experience including: transportation networks (for routing/navigation), locators (address search, forward and reverse geocoding), and maps.
A field worker might download a mobile map package to support their operations while working offline.
How to use the sample
A list of maps from a mobile map package will be displayed. If the map contains transportation networks, the list item will have a navigation icon. Tap on a map in the list to open it. If a locator task is available, tap on the map to reverse geocode the location’s address. If transportation networks are available, a route will be calculated between geocode locations.
How it works
- Create a
MobileMapPackageusingMobileMapPackage(fileURL:). - Get a list of maps inside the package using the
mapsproperty. - If the package has a locator, access it using the
locatorTaskproperty. - To see if a map contains transportation networks, check each map’s
transportationNetworksproperty.
Relevant API
- GeocodeResult
- MobileMapPackage
- ReverseGeocodeParameters
- Route
- RouteParameters
- RouteResult
- RouteTask
- TransportationNetworkDataset
Offline data
This sample uses the San Francisco mobile map package and the Yellowstone mobile map package. Both are downloaded from ArcGIS Online automatically.
Tags
disconnected, field mobility, geocode, network, network analysis, offline, routing, search, transportation
Sample code
// Copyright 2023 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 ArcGISimport SwiftUIimport UniformTypeIdentifiers
struct FindRouteInMobileMapPackageView: View { /// The view model for the view. @StateObject private var model = Model()
/// A Boolean value indicating whether the file importer interface is showing. @State private var fileImporterIsShowing = false
/// The file URL to a local "mmpk" file imported from the file importer. @State private var importedFileURL: URL?
var body: some View { List { // Create a list section for each map package. ForEach(model.mapPackages.enumeratedArray(), id: \.offset) { (offset, mapPackage) in Section { MobileMapListView(mapPackage: mapPackage) } header: { Text(mapPackage.item?.title ?? "Mobile Map Package \(offset + 1)") } } } .toolbar { // The button used to import mobile map packages. ToolbarItem(placement: .bottomBar) { Button("Add Package") { fileImporterIsShowing = true } .fileImporter( isPresented: $fileImporterIsShowing, allowedContentTypes: [.mmpk] ) { result in switch result { case .success(let fileURL): importedFileURL = fileURL case .failure(let error): model.error = error } } } } .task { // Load the San Francisco mobile map package from the bundle when the sample loads. guard model.mapPackages.isEmpty else { return } await model.addMapPackage(from: .sanFranciscoPackage) } .task(id: importedFileURL) { // Load the new mobile map package when a file URL is imported. guard let importedFileURL else { return } await model.importMapPackage(from: importedFileURL) self.importedFileURL = nil } .errorAlert(presentingError: $model.error) }}
private extension FindRouteInMobileMapPackageView { /// A list of the maps in a given map package. struct MobileMapListView: View { /// The mobile map package containing the maps. @State var mapPackage: MobileMapPackage
var body: some View { // Create a list row for each map in the map package. ForEach(mapPackage.maps.enumeratedArray(), id: \.offset) { (offset, map) in let mapName = map.item?.name ?? "Map \(offset + 1)"
// The navigation link to the map. NavigationLink { Group { if let locatorTask = mapPackage.locatorTask { MobileMapView(map: map, locatorTask: locatorTask) } else { MapView(map: map) } } .navigationTitle(mapName) } label: { HStack { // The image of the map for the row. Image(uiImage: map.item?.thumbnail?.image ?? UIImage( systemName: "questionmark" )!) .resizable() .scaledToFit() .frame(height: 50)
VStack(alignment: .leading, spacing: 2) { Text(mapName)
HStack { // The symbol indicating whether the map can geocode. if mapPackage.locatorTask != nil { HStack(spacing: 2) { Image(systemName: "mappin.circle") Text("Geocoding") } }
// The symbol indicating whether the map can route. if !map.transportationNetworks.isEmpty { HStack(spacing: 2) { Image(systemName: "arrow.triangle.turn.up.right.circle") Text("Routing") } } } .font(.caption2) .foregroundStyle(.secondary) } } } } } }}
private extension Collection { /// Enumerates a collection as an array of (n, x) pairs, where n represents a consecutive integer /// starting at zero and x represents an element of the collection. /// - Returns: An array of pairs enumerating the collection. func enumeratedArray() -> [(offset: Int, element: Self.Element)] { return Array(self.enumerated()) }}
private extension UTType { /// A type that represents a mobile map package file. static let mmpk = UTType(filenameExtension: "mmpk")!}
private extension URL { /// The URL to the local San Francisco mobile map package file. static var sanFranciscoPackage: URL { Bundle.main.url(forResource: "SanFrancisco", withExtension: "mmpk")! }}// Copyright 2023 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 ArcGISimport Combineimport UIKit
extension FindRouteInMobileMapPackageView { /// The view model for the `FindRouteInMobileMapPackageView`. @MainActor class Model: ObservableObject { /// The list of loaded mobile map packages. @Published private(set) var mapPackages: [MobileMapPackage] = []
/// The error shown in the error alert. @Published var error: (any Error)?
/// The list of file URLs that have been securely accessed. private var accessedURLs: [URL] = []
deinit { // Release access to all the accessed URLs. for url in accessedURLs { url.stopAccessingSecurityScopedResource() } }
/// Imports a mobile map package from a given file URL. /// - Parameter fileURL: The file URL to the "mmpk" file to import. func importMapPackage(from fileURL: URL) async { // Make the URL accessible. if !accessedURLs.contains(fileURL) && fileURL.startAccessingSecurityScopedResource() { accessedURLs.append(fileURL) }
// Load the map package. await addMapPackage(from: fileURL) }
/// Adds a mobile map package to the map packages list using a given file URL. /// - Parameter fileURL: The file URL to the "mmpk" file to add. func addMapPackage(from fileURL: URL) async { do { let mapPackage = MobileMapPackage(fileURL: fileURL) try await mapPackage.load() mapPackages.append(mapPackage) } catch { self.error = error } } }}
extension FindRouteInMobileMapPackageView.MobileMapView { /// The view model for the `FindRouteInMobileMapPackageView.MobileMapView`. @MainActor class Model: ObservableObject { // MARK: Properties
/// The map for the view. let map: Map
/// The graphics overlays for the view. var graphicsOverlays: [GraphicsOverlay] { if let routeGraphicsOverlay { return [markerGraphicsOverlay, routeGraphicsOverlay] } else { return [markerGraphicsOverlay] } }
/// The graphics overlay for the marker graphics. let markerGraphicsOverlay = GraphicsOverlay()
/// The graphics overlay for the route graphics. let routeGraphicsOverlay: GraphicsOverlay?
/// The blue marker symbol for creating a marker graphic. private let markerSymbol = { // Create a symbol using the Blue Marker image from the project's assets. let markerImage = UIImage.blueMarker let markerSymbol = PictureMarkerSymbol(image: markerImage)
// Change the symbol's offsets, so it aligns properly to a given point. markerSymbol.leaderOffsetY = markerImage.size.height / 2 markerSymbol.offsetY = markerImage.size.height / 2
return markerSymbol }()
/// The blue line symbol for creating a route graphic. private let routeSymbol = SimpleLineSymbol(style: .solid, color: .blue, width: 5)
/// The route task for routing. let routeTask: RouteTask?
/// The route parameters for routing with the route task. private var routeParameters: RouteParameters?
/// The locator task for reverse geocoding. private let locatorTask: LocatorTask
/// The parameters for reverse geocoding with the locator task. private let reverseGeocodeParameters = { let reverseGeocodeParameters = ReverseGeocodeParameters() reverseGeocodeParameters.addResultAttributeName("*") reverseGeocodeParameters.maxResults = 1 return reverseGeocodeParameters }()
/// The count of marker graphics in the marker graphics overlay. private var markersCount: Int { markerGraphicsOverlay.graphics.count }
/// The last marker in the marker graphics overlay. var lastMarker: Graphic? { markerGraphicsOverlay.graphics.last }
init(map: Map, locatorTask: LocatorTask) { self.map = map self.locatorTask = locatorTask
// Set up the properties used for routing if the map has a transportation network. if let transportationNetwork = map.transportationNetworks.first { routeGraphicsOverlay = GraphicsOverlay() routeTask = RouteTask(dataset: transportationNetwork) } else { routeGraphicsOverlay = nil routeTask = nil } }
// MARK: Methods
/// Loads the route parameters from the route task. func loadRouteParameters() async throws { routeParameters = try await routeTask?.makeDefaultParameters() }
/// Updates the marker to a given point or adds a new marker if there isn't one yet. /// - Parameter point: The point to set the marker to. func updateMarker(to point: Point) { if lastMarker != nil { lastMarker?.geometry = point } else { let markerGraphic = Graphic(geometry: point, symbol: markerSymbol) markerGraphicsOverlay.addGraphic(markerGraphic) } }
/// Adds a stop to the route. /// - Parameters: /// - point: The point to add the stop graphic at. func addRouteStop(at point: Point) async throws { // Update the last stop instead of adding a new one if there // isn't a route for the last one. if routeGraphicsOverlay?.graphics.count ?? 0 < markersCount - 1 { lastMarker?.geometry = point } else { addStopGraphic(at: point) }
// Create a route using the stop. try await addRoute() }
/// Reverse geocodes a given point. /// - Parameter point: The point to reverse geocode. /// - Returns: The resulting address of the reverse geocode if any. func reverseGeocode(point: Point) async throws -> String { // Perform reverse geocode using the locator task with the point and parameters. let geocodeResults = try await locatorTask.reverseGeocode( forLocation: point, parameters: reverseGeocodeParameters )
// If a result is found, extract the address from the attributes. if let result = geocodeResults.first { // If a result is found, extract the address from the attributes. let cityString = result.attributes["City"] as? String ?? "" let streetString = result.attributes["StAddr"] as? String ?? "" let stateString = result.attributes["Region"] as? String ?? "" return "\(streetString), \(cityString), \(stateString)" } else { return "No address found" } }
/// Adds a stop graphic to the marker graphics overlay. /// - Parameter point: The point to add the stop graphic at. private func addStopGraphic(at point: Point) { // Create a text symbol with the next marker index. let textSymbol = TextSymbol( text: "\(markersCount + 1)", color: .white, size: 20, horizontalAlignment: .center, verticalAlignment: .middle ) textSymbol.offsetY = markerSymbol.offsetY
// Create a graphic with the marker symbol and text symbol. let compositeSymbol = CompositeSymbol(symbols: [markerSymbol, textSymbol]) let stopGraphic = Graphic(geometry: point, symbol: compositeSymbol)
// Add the new graphic to the marker graphics overlay. markerGraphicsOverlay.addGraphic(stopGraphic) }
/// Creates a route using the last two marker graphics. private func addRoute() async throws { guard let routeParameters, markersCount >= 2 else { return }
// Create stops from the last two marker graphics. let lastGraphics = markerGraphicsOverlay.graphics[markersCount - 2..<markersCount] let stops = lastGraphics.compactMap { graphic -> Stop? in guard let point = graphic.geometry as? Point else { return nil } return Stop(point: point) }
// Set the new stops on the route parameters. routeParameters.clearStops() routeParameters.setStops(stops)
// Get the route from the route task using the route parameters. let routeResult = try await routeTask?.solveRoute(using: routeParameters) guard let route = routeResult?.routes.first else { return }
// Create a graphic for the route and add to the route graphics overlay. let routeGraphic = Graphic(geometry: route.geometry, symbol: routeSymbol) routeGraphicsOverlay?.addGraphic(routeGraphic) } }}// Copyright 2023 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 ArcGISimport SwiftUI
extension FindRouteInMobileMapPackageView { /// The map view for a mobile map package map. struct MobileMapView: View { /// The view model for the view. @StateObject private var model: Model
/// The placement of the address callout on the map. @State private var calloutPlacement: CalloutPlacement?
/// The text address shown in the callout. @State private var calloutText: String = ""
/// The point on the screen where the user tapped. @State private var tapScreenPoint: CGPoint?
/// A Boolean value indicating whether the reset button is disabled. @State private var resetDisabled = true
/// The error shown in the error alert. @State private var error: (any Error)?
init(map: Map, locatorTask: LocatorTask) { let model = Model(map: map, locatorTask: locatorTask) _model = StateObject(wrappedValue: model) }
var body: some View { MapViewReader { mapViewProxy in MapView(map: model.map, graphicsOverlays: model.graphicsOverlays) .callout(placement: $calloutPlacement) { _ in Text(calloutText) .font(.callout) .padding(8) } .onSingleTapGesture { screenPoint, _ in tapScreenPoint = screenPoint } .task(id: tapScreenPoint) { guard let tapScreenPoint else { return }
do { // Check to see if the tap was on a marker. let identifyResult = try await mapViewProxy.identify( on: model.markerGraphicsOverlay, screenPoint: tapScreenPoint, tolerance: 12 )
if let graphic = identifyResult.graphics.first, let graphicPoint = graphic.geometry as? Point { // Update the callout to the identified marker. await updateCallout(point: graphicPoint, graphic: graphic) } else { // Add a graphic at the tapped map point. guard let location = mapViewProxy.location( fromScreenPoint: tapScreenPoint ) else { return } await addGraphic(at: location) } } catch { self.error = error } } } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Spacer() Button("Reset") { resetGraphics() } .disabled(resetDisabled) } } .task { // Load the route parameters sample loads. do { try await model.loadRouteParameters() } catch { self.error = error } } .errorAlert(presentingError: $error) }
/// Updates the placement and text of the callout using a given point and graphic. /// - Parameters: /// - point: The point to reverse geocode and set the callout placement to. /// - graphic: The graphic at the point. private func updateCallout(point: Point, graphic: Graphic) async { // Update the callout text with the address from a reverse geocode. do { calloutText = try await model.reverseGeocode(point: point) } catch { self.error = error calloutText = "No address found" }
// Update the callout placement with the graphic and point. calloutPlacement = .geoElement(graphic, tapLocation: point) }
/// Adds a marker or route stop with a callout at a given point. /// - Parameter point: The point to add the graphic at. private func addGraphic(at point: Point) async { // Normalize the tap location. guard let point = GeometryEngine.normalizeCentralMeridian(of: point) as? Point else { return }
// Add a route stop if the map has routing. Otherwise, update the marker. if model.routeTask != nil { do { try await model.addRouteStop(at: point) } catch { self.error = error } } else { model.updateMarker(to: point) }
// Update the callout with the last marker. guard let lastMarker = model.lastMarker else { return } await updateCallout(point: point, graphic: lastMarker)
resetDisabled = false }
/// Resets the graphics on the map view. private func resetGraphics() { // Reset the view properties. calloutPlacement = nil resetDisabled = true
// Reset the graphics. if model.routeTask != nil { model.markerGraphicsOverlay.removeAllGraphics() model.routeGraphicsOverlay?.removeAllGraphics() } else { model.lastMarker?.geometry = nil } } }}