Skip to content

Commit d6bb88e

Browse files
authored
Merge pull request #308 from Esri/Caleb/Fix-CreateMobileGeodatabaseFileExporting
[Fix] Create mobile geodatabase sharing crash
2 parents 3a836d2 + f4d1245 commit d6bb88e

File tree

2 files changed

+164
-128
lines changed

2 files changed

+164
-128
lines changed

Shared/Samples/Create mobile geodatabase/CreateMobileGeodatabaseView.Model.swift

Lines changed: 111 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,23 @@
1414

1515
import ArcGIS
1616
import SwiftUI
17+
import UniformTypeIdentifiers
1718

1819
extension CreateMobileGeodatabaseView {
19-
/// The view model for the sample.
20+
// MARK: Model
21+
22+
/// The model used to store the geo model and other expensive objects used in the view.
2023
@MainActor
2124
class Model: ObservableObject {
22-
// MARK: Properties
23-
24-
/// A map with a topographic basemap centered on Harpers Ferry, West Virginia, USA.
25+
/// A map with a topographic basemap centered on Harpers Ferry, WV, USA.
2526
let map: Map = {
2627
let map = Map(basemapStyle: .arcGISTopographic)
2728
map.initialViewpoint = Viewpoint(latitude: 39.3238, longitude: -77.7332, scale: 1e4)
2829
return map
2930
}()
3031

31-
/// A URL to a temporary geodatabase.
32-
private let geodatabaseURL: URL
33-
34-
/// A URL to a temporary directory to store the geodatabase.
35-
private let directoryURL: URL
36-
37-
/// The mobile geodatabase.
38-
private var geodatabase: Geodatabase?
32+
/// A geodatabase file that can be exported with the system's file exporter.
33+
let geodatabaseFile = GeodatabaseFile()
3934

4035
/// The feature table in the geodatabase.
4136
private var featureTable: GeodatabaseFeatureTable?
@@ -57,125 +52,57 @@ extension CreateMobileGeodatabaseView {
5752
FieldDescription(name: "collection_timestamp", fieldType: .date)
5853
])
5954

60-
// Set unnecessary properties to false.
61-
description.hasAttachments = false
62-
description.hasM = false
63-
description.hasZ = false
64-
6555
return description
6656
}()
6757

6858
/// The list of features in the feature table.
69-
@Published private(set) var features: [FeatureItem] = []
59+
@Published private(set) var features = [FeatureItem]()
7060

71-
/// The error shown in the error alert.
72-
@Published var error: Error?
73-
74-
init() {
75-
// Create the temporary directory using file manager.
76-
directoryURL = FileManager
77-
.default
78-
.temporaryDirectory
79-
.appendingPathComponent(ProcessInfo().globallyUniqueString)
80-
try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: false)
61+
/// Creates a new feature table from a geodatabase.
62+
func createFeatureTable() async throws {
63+
// Create a new geodatabase.
64+
try await geodatabaseFile.createGeodatabase()
8165

82-
// Create the geodatabase path with the directory URL.
83-
geodatabaseURL = directoryURL
84-
.appendingPathComponent("LocationHistory", isDirectory: false)
85-
.appendingPathExtension("geodatabase")
86-
}
87-
88-
deinit {
89-
try? FileManager.default.removeItem(at: directoryURL)
90-
}
91-
92-
// MARK: Methods
93-
94-
/// Creates a mobile geodatabase with a feature table.
95-
func createGeodatabase() async {
96-
do {
97-
// Remove the geodatabase file if it already exists.
98-
if FileManager.default.fileExists(atPath: geodatabaseURL.path) {
99-
try FileManager.default.removeItem(at: geodatabaseURL)
100-
}
101-
102-
// Create an empty mobile geodatabase at the given URL.
103-
geodatabase = try await Geodatabase.createEmpty(fileURL: geodatabaseURL)
104-
105-
// Create a new feature table in the geodatabase using a table description.
106-
if let table = try await geodatabase?.makeTable(description: tableDescription) {
107-
// Load the feature table.
108-
try await table.load()
109-
featureTable = table
110-
111-
// Create a feature layer using the table and add it to the map.
112-
let featureLayer = FeatureLayer(featureTable: table)
113-
map.addOperationalLayer(featureLayer)
114-
}
115-
} catch {
116-
self.error = error
117-
}
66+
// Create a feature table in the geodatabase using a table description.
67+
guard let table = try await geodatabaseFile.geodatabase?.makeTable(
68+
description: tableDescription
69+
) else { return }
70+
featureTable = table
71+
72+
// Create a feature layer using the table and add it to the map.
73+
let featureLayer = FeatureLayer(featureTable: table)
74+
map.addOperationalLayer(featureLayer)
11875
}
11976

12077
/// Adds a feature to the feature table at a given map point.
12178
/// - Parameter mapPoint: The map point used to make the feature.
122-
func addFeature(at mapPoint: Point) async {
79+
func addFeature(at mapPoint: Point) async throws {
12380
guard let featureTable else { return }
124-
do {
125-
// Create an attribute with the current date.
126-
let attributes = ["collection_timestamp": Date()]
127-
128-
// Create a feature with the attributes and point.
129-
let feature = featureTable.makeFeature(attributes: attributes, geometry: mapPoint)
130-
131-
// Add the feature to the feature table.
132-
try await featureTable.add(feature)
133-
134-
// Add the feature to the list of features.
135-
if let oid = feature.attributes["oid"] as? Int64,
136-
let timeStamp = feature.attributes["collection_timestamp"] as? Date {
137-
features.append(FeatureItem(oid: oid, timestamp: timeStamp))
138-
}
139-
} catch {
140-
self.error = error
141-
}
142-
}
143-
144-
/// Presents the sheet containing options to share the geodatabase.
145-
func presentShareSheet() {
146-
// Create the activity view controller with the geodatabase URL.
147-
let activityViewController = UIActivityViewController(
148-
activityItems: [geodatabaseURL],
149-
applicationActivities: nil
150-
)
15181

152-
// Present the activity view controller.
153-
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
154-
windowScene?.keyWindow?.rootViewController?.present(activityViewController, animated: true)
82+
// Create an attribute with the current date.
83+
let attributes = ["collection_timestamp": Date()]
15584

156-
// Reset the geodatabase once it has been shared.
157-
activityViewController.completionWithItemsHandler = { [weak self] _, completed, _, error in
158-
if completed {
159-
Task { [weak self] in
160-
await self?.resetGeodatabase()
161-
}
162-
} else if let error {
163-
self?.error = error
164-
}
85+
// Create a feature with the attributes and point.
86+
let feature = featureTable.makeFeature(attributes: attributes, geometry: mapPoint)
87+
88+
// Add the feature to the feature table.
89+
try await featureTable.add(feature)
90+
91+
// Add the feature to the list of features.
92+
if let oid = feature.attributes["oid"] as? Int64,
93+
let timeStamp = feature.attributes["collection_timestamp"] as? Date {
94+
features.append(FeatureItem(oid: oid, timestamp: timeStamp))
16595
}
16696
}
16797

168-
/// Removes all the existing features and creates a new geodatabase.
169-
private func resetGeodatabase() async {
170-
// Close the geodatabase to cease all adjustments.
171-
geodatabase?.close()
98+
/// Removes all the existing features from the map.
99+
func resetFeatures() throws {
100+
// Delete the geodatabase.
101+
try geodatabaseFile.deleteGeodatabase()
172102

173103
// Remove the current features and layers.
174104
features.removeAll()
175105
map.removeAllOperationalLayers()
176-
177-
// Create a new mobile geodatabase.
178-
await createGeodatabase()
179106
}
180107
}
181108

@@ -186,4 +113,77 @@ extension CreateMobileGeodatabaseView {
186113
/// The collection timestamp of the the feature.
187114
let timestamp: Date
188115
}
116+
117+
// MARK: GeodatabaseFile
118+
119+
/// A geodatabase file that can be used with the native file exporter.
120+
final class GeodatabaseFile {
121+
/// The mobile geodatabase used to create the geodatabase file.
122+
private(set) var geodatabase: Geodatabase?
123+
124+
/// A URL to the temporary geodatabase file.
125+
private let geodatabaseURL: URL
126+
127+
/// A URL to the temporary directory containing the geodatabase file.
128+
private let directoryURL: URL
129+
130+
init() {
131+
// Create the temporary directory using file manager.
132+
directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(
133+
ProcessInfo().globallyUniqueString
134+
)
135+
try? FileManager.default.createDirectory(
136+
at: directoryURL,
137+
withIntermediateDirectories: false
138+
)
139+
140+
// Create the geodatabase path with the directory URL.
141+
geodatabaseURL = directoryURL
142+
.appendingPathComponent("LocationHistory", isDirectory: false)
143+
.appendingPathExtension("geodatabase")
144+
}
145+
146+
deinit {
147+
try? FileManager.default.removeItem(at: directoryURL)
148+
}
149+
150+
/// Creates an empty mobile geodatabase file.
151+
func createGeodatabase() async throws {
152+
// Create an empty mobile geodatabase at the given URL.
153+
geodatabase = try await Geodatabase.createEmpty(fileURL: geodatabaseURL)
154+
}
155+
156+
/// Deletes the geodatabase file.
157+
func deleteGeodatabase() throws {
158+
// Close the geodatabase to cease all adjustments.
159+
geodatabase?.close()
160+
161+
// Remove the geodatabase file if it exists.
162+
if FileManager.default.fileExists(atPath: geodatabaseURL.path) {
163+
try FileManager.default.removeItem(at: geodatabaseURL)
164+
}
165+
}
166+
}
167+
}
168+
169+
extension CreateMobileGeodatabaseView.GeodatabaseFile: FileDocument {
170+
/// The file and data types that the document reads from.
171+
static var readableContentTypes = [UTType.geodatabase]
172+
173+
/// Creates a document and initializes it with the contents of a file.
174+
convenience init(configuration: ReadConfiguration) throws {
175+
fatalError("Loading geodatabase files is not supported by this sample.")
176+
}
177+
178+
/// Serializes a document snapshot to a file wrapper.
179+
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
180+
return try FileWrapper(url: geodatabaseURL)
181+
}
182+
}
183+
184+
extension UTType {
185+
/// A type that represents a geodatabase file.
186+
static var geodatabase: Self {
187+
UTType(filenameExtension: "geodatabase")!
188+
}
189189
}

Shared/Samples/Create mobile geodatabase/CreateMobileGeodatabaseView.swift

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,40 @@ struct CreateMobileGeodatabaseView: View {
2020
@StateObject private var model = Model()
2121

2222
/// The point on the map where the user tapped.
23-
@State private var tapMapPoint = Point(x: 0, y: 0)
23+
@State private var tapLocation: Point?
2424

25-
/// A Boolean indicating whether the feature table sheet is showing.
26-
@State private var isShowingTableSheet = false
25+
/// A Boolean value indicating whether the feature table sheet is showing.
26+
@State private var tableSheetIsShowing = false
27+
28+
/// A Boolean value indicating whether the file explorer interface is showing
29+
@State private var fileExporterIsShowing = false
30+
31+
/// The error shown in the error alert.
32+
@State private var error: Error?
2733

2834
var body: some View {
2935
MapView(map: model.map)
3036
.onSingleTapGesture { _, mapPoint in
31-
tapMapPoint = mapPoint
37+
tapLocation = mapPoint
3238
}
33-
.task {
34-
// Create the initial geodatabase.
35-
await model.createGeodatabase()
39+
.task(id: tapLocation) {
40+
guard let tapLocation else { return }
41+
do {
42+
// Add a feature at the tap location.
43+
try await model.addFeature(at: tapLocation)
44+
} catch {
45+
self.error = error
46+
}
3647
}
37-
.task(id: tapMapPoint) {
38-
// Add a feature at the tapped map point.
39-
await model.addFeature(at: tapMapPoint)
48+
.task(id: model.features.isEmpty) {
49+
do {
50+
// Create a new feature table when the features are reset.
51+
if model.features.isEmpty {
52+
try await model.createFeatureTable()
53+
}
54+
} catch {
55+
self.error = error
56+
}
4057
}
4158
.overlay(alignment: .top) {
4259
Text("Number of features added: \(model.features.count)")
@@ -49,14 +66,33 @@ struct CreateMobileGeodatabaseView: View {
4966
tableButton
5067
.disabled(model.features.isEmpty)
5168

69+
Spacer()
70+
5271
Button {
53-
model.presentShareSheet()
72+
fileExporterIsShowing = true
5473
} label: {
55-
Image(systemName: "square.and.arrow.up")
74+
Label("Export File", systemImage: "square.and.arrow.up")
75+
}
76+
.disabled(model.features.isEmpty)
77+
.fileExporter(
78+
isPresented: $fileExporterIsShowing,
79+
document: model.geodatabaseFile,
80+
contentType: .geodatabase
81+
) { result in
82+
switch result {
83+
case .success:
84+
do {
85+
try model.resetFeatures()
86+
} catch {
87+
self.error = error
88+
}
89+
case .failure(let error):
90+
self.error = error
91+
}
5692
}
5793
}
5894
}
59-
.errorAlert(presentingError: $model.error)
95+
.errorAlert(presentingError: $error)
6096
}
6197
}
6298

@@ -65,12 +101,12 @@ private extension CreateMobileGeodatabaseView {
65101
@ViewBuilder var tableButton: some View {
66102
/// The button to bring up the sheet.
67103
let button = Button("View Table") {
68-
isShowingTableSheet = true
104+
tableSheetIsShowing = true
69105
}
70106

71107
if #available(iOS 16, *) {
72108
button
73-
.popover(isPresented: $isShowingTableSheet, arrowEdge: .bottom) {
109+
.popover(isPresented: $tableSheetIsShowing, arrowEdge: .bottom) {
74110
tableList
75111
.presentationDetents([.fraction(0.5)])
76112
#if targetEnvironment(macCatalyst)
@@ -81,7 +117,7 @@ private extension CreateMobileGeodatabaseView {
81117
}
82118
} else {
83119
button
84-
.sheet(isPresented: $isShowingTableSheet) {
120+
.sheet(isPresented: $tableSheetIsShowing) {
85121
tableList
86122
}
87123
}
@@ -106,7 +142,7 @@ private extension CreateMobileGeodatabaseView {
106142
.toolbar {
107143
ToolbarItem(placement: .confirmationAction) {
108144
Button("Done") {
109-
isShowingTableSheet = false
145+
tableSheetIsShowing = false
110146
}
111147
}
112148
}

0 commit comments

Comments
 (0)