Generate a local geodatabase replica from an online feature service.

Use case
Generating a geodatabase replica is the first step toward taking a feature service offline. It allows you to save features locally for offline display.
How to use the sample
Zoom to any extent. Then tap the generate button to generate a geodatabase of features from a feature service filtered to the current extent. A red outline will show the extent used. The job’s progress is shown while the geodatabase is generated. When complete, the map will reload with only the layers in the geodatabase, clipped to the extent.
How it works
- Create a
GeodatabaseSyncTaskwith the URL of the feature service and load it. - Create
GenerateGeodatabaseParametersspecifying the extent and whether to include attachments. - Create a
GenerateGeodatabaseJobwithgeodatabaseSyncTask.makeGenerateGeodatabaseJob(parameters:downloadFileURL:). - Start the job and get the result
Geodatabase. Inside the geodatabase are feature tables, which can be used to add feature layers to the map. - Call
syncTask.unregisterGeodatabase(_:)after generation when you’re not planning on syncing changes to the service.
Relevant API
- GenerateGeodatabaseJob
- GenerateGeodatabaseParameters
- Geodatabase
- GeodatabaseSyncTask
Tags
disconnected, local geodatabase, offline, replica, sync
Sample code
// Copyright 2025 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
struct GenerateGeodatabaseReplicaFromFeatureServiceView: View { /// The view model for the sample. @State private var model = Model()
/// The text describing the status of the sample. @State private var statusText = ""
/// A Boolean value indicating whether the geodatabase is being generated. @State private var isGeneratingGeodatabase = false
/// The error shown in the error alert. @State private var error: (any Error)?
var body: some View { GeometryReader { geometryProxy in MapViewReader { mapViewProxy in MapView(map: model.map) .task { do { try await model.setUpMap() statusText = "Tap the generate button to take the area offline." } catch { self.error = error } } .overlay(alignment: .top) { VStack { Text(statusText) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding(8) .background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
// The red rectangle representing the extent of data to include in the geodatabase. Rectangle() .stroke(.red, lineWidth: 2) .padding(EdgeInsets(top: 20, leading: 20, bottom: 44, trailing: 20)) .opacity(model.geodatabase == nil ? 1 : 0) } } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Button("Generate Geodatabase") { isGeneratingGeodatabase = true } .disabled(isGeneratingGeodatabase || model.geodatabase != nil) } } .task(id: isGeneratingGeodatabase) { guard isGeneratingGeodatabase else { return } defer { isGeneratingGeodatabase = false }
do { // Creates an envelope from the area of interest. let viewRect = geometryProxy.frame(in: .local).inset( by: UIEdgeInsets( top: 20, left: geometryProxy.safeAreaInsets.leading + 20, bottom: 44, right: -geometryProxy.safeAreaInsets.trailing + 20 ) ) guard let extent = mapViewProxy.envelope( fromViewRect: viewRect ) else { return }
// Generates the geodatabase using the envelope. try await model.generateGeodatabase(extent: extent) statusText = "Generated geodatabase successfully." } catch { self.error = error } } .overlay(alignment: .center) { // Shows a progress view when there is a job currently running. if let progress = model.generateGeodatabaseJob?.progress { VStack { Text("Creating geodatabase") .padding(.bottom)
ProgressView(progress) .frame(maxWidth: 180) } .padding() .background(.ultraThickMaterial) .clipShape(.rect(cornerRadius: 10)) .shadow(radius: 50) } } .onDisappear { Task { await model.cancelJob() } } .errorAlert(presentingError: $error) } } }}// Copyright 2025 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 Foundation
extension GenerateGeodatabaseReplicaFromFeatureServiceView { /// The view model for the sample. @MainActor @Observable final class Model { /// The generated local geodatabase to edit. private(set) var geodatabase: Geodatabase?
/// The generate geodatabase job. private(set) var generateGeodatabaseJob: GenerateGeodatabaseJob?
/// A map with a San Fransisco streets basemap. let map: Map = { let tiledLayer = ArcGISTiledLayer(url: .sanFranciscoStreetsTilePackage) let basemap = Basemap(baseLayer: tiledLayer) return Map(basemap: basemap) }()
/// The task for generating and synchronizing the geodatabase with the feature service. private let geodatabaseSyncTask = GeodatabaseSyncTask( url: .wildfireSyncFeatureServer )
/// A URL to the temporary file containing the geodatabase. private let temporaryGeodatabaseURL = FileManager .createTemporaryDirectory() .appending(component: "WildfireSync.geodatabase")
deinit { // Removes the temporary geodatabase file and its directory. let temporaryDirectoryURL = temporaryGeodatabaseURL.deletingLastPathComponent() try? FileManager.default.removeItem(at: temporaryDirectoryURL) }
/// Adds feature layers from the feature service to the map. func setUpMap() async throws { try await geodatabaseSyncTask.load()
// Creates feature tables from the feature service. guard let layerInfos = geodatabaseSyncTask.featureServiceInfo?.layerInfos else { return } let featureTableURLs = layerInfos.compactMap { layerInfo -> URL? in guard let id = layerInfo.id else { return nil } return .wildfireSyncFeatureServer.appendingPathComponent("\(id)") } let featureTables = featureTableURLs.map(ServiceFeatureTable.init(url:)) let featureLayers = featureTables.map(FeatureLayer.init(featureTable:)) map.addOperationalLayers(featureLayers) }
/// Generates a geodatabase from the feature service. /// - Parameter extent: The extent of the data to be included in the generated geodatabase. func generateGeodatabase(extent: Envelope) async throws { // Creates the parameters for generating the geodatabase. let parameters = try await geodatabaseSyncTask.makeDefaultGenerateGeodatabaseParameters( extent: extent ) parameters.returnsAttachments = false
// Creates the job to generate the geodatabase. generateGeodatabaseJob = geodatabaseSyncTask.makeGenerateGeodatabaseJob( parameters: parameters, downloadFileURL: temporaryGeodatabaseURL ) defer { generateGeodatabaseJob = nil } guard let generateGeodatabaseJob else { return }
// Generates the geodatabase. generateGeodatabaseJob.start() geodatabase = try await generateGeodatabaseJob.output guard let geodatabase else { return } try await geodatabase.load()
map.removeAllOperationalLayers()
// Adds the geodatabase's tables to the map as feature layers. let featureLayers = geodatabase.featureTables .map(FeatureLayer.init(featureTable:)) map.addOperationalLayers(featureLayers)
// Unregister geodatabase since we are not editing or syncing features. try await geodatabaseSyncTask.unregisterGeodatabase(geodatabase) }
/// Cancels the generate geodatabase job. func cancelJob() async { // Cancels the generate geodatabase job. await generateGeodatabaseJob?.cancel() generateGeodatabaseJob = nil } }}
private extension FileManager { /// Creates a temporary directory. /// - Returns: The URL of the created directory. static func createTemporaryDirectory() -> URL { try! FileManager.default.url( for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: FileManager.default.temporaryDirectory, create: true ) }}
private extension URL { /// The URL to the local streets tile package of San Francisco, CA, USA. static var sanFranciscoStreetsTilePackage: URL { Bundle.main.url(forResource: "SanFrancisco", withExtension: "tpkx")! } /// The URL to the Wildfire Sync feature server. static var wildfireSyncFeatureServer: URL { URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/WildfireSync/FeatureServer")! }}