Skip to content

Commit 4fa8039

Browse files
authored
Merge pull request #528 from Esri/Caleb/New-EditFeaturesUsingFeatureForms
[New] Edit features using feature forms
2 parents fa08497 + b5b2c6c commit 4fa8039

File tree

5 files changed

+302
-0
lines changed

5 files changed

+302
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@
347347
D75E5EE62CC0340100252595 /* ListContentsOfKMLFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75E5EE22CC0340100252595 /* ListContentsOfKMLFileView.swift */; };
348348
D75E5EE92CC0342700252595 /* ListContentsOfKMLFileView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D75E5EE22CC0340100252595 /* ListContentsOfKMLFileView.swift */; };
349349
D75E5EEC2CC0466900252595 /* esri_test_data.kmz in Resources */ = {isa = PBXBuildFile; fileRef = D75E5EEA2CC0466900252595 /* esri_test_data.kmz */; settings = {ASSET_TAGS = (ListContentsOfKmlFile, ); }; };
350+
D75E5EF12CC049D500252595 /* EditFeaturesUsingFeatureFormsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75E5EED2CC049D500252595 /* EditFeaturesUsingFeatureFormsView.swift */; };
351+
D75E5EF42CC04A0C00252595 /* EditFeaturesUsingFeatureFormsView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D75E5EED2CC049D500252595 /* EditFeaturesUsingFeatureFormsView.swift */; };
350352
D75F66362B48EABC00434974 /* SearchForWebMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75F66332B48EABC00434974 /* SearchForWebMapView.swift */; };
351353
D75F66392B48EB1800434974 /* SearchForWebMapView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D75F66332B48EABC00434974 /* SearchForWebMapView.swift */; };
352354
D76000A22AF18BAB00B3084D /* FindRouteInTransportNetworkView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D7749AD52AF08BF50086632F /* FindRouteInTransportNetworkView.Model.swift */; };
@@ -570,6 +572,7 @@
570572
dstPath = "";
571573
dstSubfolderSpec = 7;
572574
files = (
575+
D75E5EF42CC04A0C00252595 /* EditFeaturesUsingFeatureFormsView.swift in Copy Source Code Files */,
573576
D7201D072CC6D3D3004BDB7D /* AddVectorTiledLayerFromCustomStyleView.swift in Copy Source Code Files */,
574577
D75E5EE92CC0342700252595 /* ListContentsOfKMLFileView.swift in Copy Source Code Files */,
575578
D7201CDB2CC6B72A004BDB7D /* AddTiledLayerAsBasemapView.swift in Copy Source Code Files */,
@@ -985,6 +988,7 @@
985988
D75C35662AB50338003CD55F /* GroupLayersTogetherView.GroupLayerListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupLayersTogetherView.GroupLayerListView.swift; sourceTree = "<group>"; };
986989
D75E5EE22CC0340100252595 /* ListContentsOfKMLFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListContentsOfKMLFileView.swift; sourceTree = "<group>"; };
987990
D75E5EEA2CC0466900252595 /* esri_test_data.kmz */ = {isa = PBXFileReference; lastKnownFileType = file; path = esri_test_data.kmz; sourceTree = "<group>"; };
991+
D75E5EED2CC049D500252595 /* EditFeaturesUsingFeatureFormsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFeaturesUsingFeatureFormsView.swift; sourceTree = "<group>"; };
988992
D75F66332B48EABC00434974 /* SearchForWebMapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchForWebMapView.swift; sourceTree = "<group>"; };
989993
D76000AB2AF19C2300B3084D /* FindRouteInMobileMapPackageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindRouteInMobileMapPackageView.swift; sourceTree = "<group>"; };
990994
D76000B62AF19FCA00B3084D /* SanFrancisco.mmpk */ = {isa = PBXFileReference; lastKnownFileType = file; path = SanFrancisco.mmpk; sourceTree = "<group>"; };
@@ -1279,6 +1283,7 @@
12791283
E004A6EE284E4B7A002A1FE6 /* Download vector tiles to local cache */,
12801284
D733CA182BED980D00FBDE4C /* Edit and sync features with feature service */,
12811285
9579FCEB2C3360CA00FC8A1D /* Edit feature attachments */,
1286+
D75E5EF02CC049D500252595 /* Edit features using feature forms */,
12821287
D762AF5E2BF6A7B900ECE3C7 /* Edit features with feature-linked annotation */,
12831288
D764B7DE2BE2F89D002E2F92 /* Edit geodatabase with transactions */,
12841289
D73E619D2BDB21F400457932 /* Edit with branch versioning */,
@@ -2394,6 +2399,14 @@
23942399
path = da301cb122874d5497f8a8f6c81eb36e;
23952400
sourceTree = "<group>";
23962401
};
2402+
D75E5EF02CC049D500252595 /* Edit features using feature forms */ = {
2403+
isa = PBXGroup;
2404+
children = (
2405+
D75E5EED2CC049D500252595 /* EditFeaturesUsingFeatureFormsView.swift */,
2406+
);
2407+
path = "Edit features using feature forms";
2408+
sourceTree = "<group>";
2409+
};
23972410
D75F66322B48EABC00434974 /* Search for web map */ = {
23982411
isa = PBXGroup;
23992412
children = (
@@ -3347,6 +3360,7 @@
33473360
4D2ADC6729C50BD6003B367F /* AddDynamicEntityLayerView.Model.swift in Sources */,
33483361
E004A6E928493BCE002A1FE6 /* ShowDeviceLocationView.swift in Sources */,
33493362
1C26ED192A859525009B7721 /* FilterFeaturesInSceneView.swift in Sources */,
3363+
D75E5EF12CC049D500252595 /* EditFeaturesUsingFeatureFormsView.swift in Sources */,
33503364
D7352F8E2BD992C40013FFEF /* MonitorChangesToDrawStatusView.swift in Sources */,
33513365
F111CCC1288B5D5600205358 /* DisplayMapFromMobileMapPackageView.swift in Sources */,
33523366
D7BA8C462B2A8ACA00018633 /* String.swift in Sources */,
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright 2024 Esri
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import ArcGIS
16+
import ArcGISToolkit
17+
import SwiftUI
18+
19+
struct EditFeaturesUsingFeatureFormsView: View {
20+
/// The map that contains the features for editing.
21+
@State private var map = Map(
22+
item: PortalItem(
23+
portal: .arcGISOnline(connection: .anonymous),
24+
id: .featureFormPlacesWebMap
25+
)
26+
)
27+
28+
/// The feature form for the selected feature.
29+
@State private var featureForm: FeatureForm?
30+
31+
/// The point on the screen where the user tapped.
32+
@State private var tapPoint: CGPoint?
33+
34+
/// A Boolean value indicating whether the feature form panel is presented.
35+
@State private var isShowingFeatureForm = false
36+
37+
/// A Boolean value indicating whether the discard edits alert is presented.
38+
@State private var isShowingDiscardEditsAlert = false
39+
40+
/// A Boolean value indicating whether the feature form has any validation errors.
41+
@State private var hasValidationErrors = false
42+
43+
/// A Boolean value indicating whether the feature form edits are being applied.
44+
@State private var isApplyingEdits = false
45+
46+
/// The error shown in the error alert.
47+
@State private var error: Error?
48+
49+
/// The text explaining the next step in the sample's workflow.
50+
private var instructionText: String {
51+
if !isShowingFeatureForm {
52+
"Tap on a feature to edit."
53+
} else if hasValidationErrors {
54+
"Fix the errors to apply the edits."
55+
} else if isApplyingEdits {
56+
"Applying edits..."
57+
} else {
58+
"Use the form to edit the feature's attributes."
59+
}
60+
}
61+
62+
/// The feature layer on the map.
63+
private var featureLayer: FeatureLayer {
64+
map.operationalLayers.first as! FeatureLayer
65+
}
66+
67+
var body: some View {
68+
MapViewReader { mapViewProxy in
69+
MapView(map: map)
70+
.onSingleTapGesture { screenPoint, _ in
71+
if isShowingFeatureForm {
72+
isShowingDiscardEditsAlert = true
73+
} else {
74+
tapPoint = screenPoint
75+
}
76+
}
77+
.task(id: tapPoint) {
78+
// Identifies the feature at the tapped point and creates a feature form from it.
79+
guard let tapPoint else { return }
80+
81+
do {
82+
let identifyLayerResult = try await mapViewProxy.identify(
83+
on: featureLayer,
84+
screenPoint: tapPoint,
85+
tolerance: 10
86+
)
87+
if let feature = identifyLayerResult.geoElements.first as? ArcGISFeature {
88+
featureLayer.selectFeature(feature)
89+
featureForm = FeatureForm(feature: feature)
90+
isShowingFeatureForm = true
91+
}
92+
} catch {
93+
self.error = error
94+
}
95+
}
96+
.overlay(alignment: .top) {
97+
Text(instructionText)
98+
.multilineTextAlignment(.center)
99+
.frame(maxWidth: .infinity, alignment: .center)
100+
.padding(8)
101+
.background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
102+
}
103+
.floatingPanel(isPresented: $isShowingFeatureForm) { [featureForm] in
104+
if let featureForm {
105+
VStack {
106+
featureFormToolbar
107+
108+
// Displays the feature form using the toolkit component.
109+
FeatureFormView(featureForm: featureForm)
110+
.padding(.horizontal)
111+
.task {
112+
for await validationErrors in featureForm.$validationErrors {
113+
hasValidationErrors = !validationErrors.isEmpty
114+
}
115+
}
116+
}
117+
}
118+
}
119+
.alert("Discard Edits", isPresented: $isShowingDiscardEditsAlert) {
120+
Button("Cancel", role: .cancel) {}
121+
Button("Discard", role: .destructive) {
122+
featureForm?.discardEdits()
123+
endEditing()
124+
}
125+
} message: {
126+
Text("Any changes made within the form will be lost.")
127+
}
128+
.errorAlert(presentingError: $error)
129+
}
130+
}
131+
132+
/// The toolbar for the feature form panel.
133+
private var featureFormToolbar: some View {
134+
HStack {
135+
Button("Discard Edits", systemImage: "x.circle", role: .destructive) {
136+
isShowingDiscardEditsAlert = true
137+
}
138+
139+
Spacer()
140+
141+
Text("Edit Feature")
142+
143+
Spacer()
144+
145+
Button("Done", systemImage: "checkmark") {
146+
isApplyingEdits = true
147+
}
148+
.disabled(hasValidationErrors)
149+
.task(id: isApplyingEdits) {
150+
guard isApplyingEdits else { return }
151+
defer { isApplyingEdits = false }
152+
153+
do {
154+
try await applyEdits()
155+
endEditing()
156+
} catch {
157+
self.error = error
158+
}
159+
}
160+
}
161+
.fontWeight(.medium)
162+
.labelStyle(.iconOnly)
163+
.padding()
164+
}
165+
166+
/// Closes the feature form panel and resets the feature selection.
167+
private func endEditing() {
168+
isShowingFeatureForm = false
169+
featureLayer.clearSelection()
170+
}
171+
172+
/// Applies the edits made in the feature form to the feature service.
173+
private func applyEdits() async throws {
174+
guard let featureForm else { return }
175+
176+
// Saves the feature form edits to the database.
177+
try await featureForm.finishEditing()
178+
179+
guard let serviceFeatureTable = featureForm.feature.table as? ServiceFeatureTable else {
180+
return
181+
}
182+
183+
// Applies the edits to the feature service using either the database or feature table.
184+
if let serviceGeodatabase = serviceFeatureTable.serviceGeodatabase,
185+
let serviceInfo = serviceGeodatabase.serviceInfo,
186+
serviceInfo.canUseServiceGeodatabaseApplyEdits {
187+
_ = try await serviceGeodatabase.applyEdits()
188+
} else {
189+
_ = try await serviceFeatureTable.applyEdits()
190+
}
191+
}
192+
}
193+
194+
private extension PortalItem.ID {
195+
/// The ID to the "Feature Form Places" web map portal item on ArcGIS Online.
196+
static var featureFormPlacesWebMap: Self { .init("516e4d6aeb4c495c87c41e11274c767f")! }
197+
}
198+
199+
#Preview {
200+
EditFeaturesUsingFeatureFormsView()
201+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Edit features using feature forms
2+
3+
Display and edit feature attributes using feature forms.
4+
5+
![Screenshot of Edit features using feature forms sample](edit-features-using-feature-forms.png)
6+
7+
## Use case
8+
9+
Feature forms help enhance the accuracy, efficiency, and user experience of attribute editing in your application. Forms can be authored as part of the web map using [Field Maps Designer](https://www.arcgis.com/apps/fieldmaps/) or using Map Viewer. This allows a simplified user experience to edit feature attribute data on the web map.
10+
11+
## How to use the sample
12+
13+
Tap a feature on the map to open a sheet displaying the feature form. Select form elements in the list and perform edits to update the field values. Tap the submit icon to commit the changes on the web map.
14+
15+
## How it works
16+
17+
1. Create a `Map` using a `Portal` URL and item ID and pass it to a `MapView`.
18+
2. When the map is tapped, perform an identify operation to check if the tapped location is an `ArcGISFeature`.
19+
3. Create a `FeatureForm` object using the identified `ArcGISFeature`.
20+
* **Note:** If the feature's `FeatureLayer`, `ArcGISFeatureTable`, or the `SubtypeSublayer` has an authored `FeatureFormDefinition`, then this definition will be used to create the `FeatureForm`. If such a definition is not found, a default definition is generated.
21+
4. Use the `FeatureForm` toolkit component to display the feature form configuration by providing the created `featureForm` object.
22+
5. Optionally, you can add the `validationErrors(_:)` modifier to the `FeatureForm` toolkit component to determine the visibility of validation errors.
23+
6. Once edits are added to the form fields, check if the validation errors list is empty using `featureForm.validationErrors` to verify that there are no errors.
24+
7. To commit edits on the service geodatabase:
25+
1. Call `featureForm.finishEditing()` to save edits to the database.
26+
2. Retrieve the backing service feature table's geodatabase using `serviceFeatureTable.serviceGeodatabase`.
27+
3. Verify the service geodatabase can commit changes back to the service using `serviceGeodatabase.serviceInfo.canUseServiceGeodatabaseApplyEdits`.
28+
4. If apply edits are allowed, call `serviceGeodatabase.applyEdits()` to apply local edits to the online service.
29+
5. If edits are not allowed on the `ServiceGeodatabase`, then apply edits to the `ServiceFeatureTable` using `ServiceFeatureTable.applyEdits()`.
30+
31+
## Relevant API
32+
33+
* ArcGISFeature
34+
* FeatureForm
35+
* FeatureLayer
36+
* FieldFormElement
37+
* GroupFormElement
38+
* ServiceFeatureTable
39+
* ServiceGeodatabase
40+
41+
## About the data
42+
43+
This sample uses a feature forms enabled [Feature Form Places web map](https://www.arcgis.com/home/item.html?id=516e4d6aeb4c495c87c41e11274c767f), which contains fictional places in San Diego of various hotels, restaurants, and shopping centers, with relevant reviews and ratings.
44+
45+
## Additional information
46+
47+
Follow the [tutorial](https://doc.arcgis.com/en/arcgis-online/create-maps/create-form-mv.htm) to create your own form using the Map Viewer. This sample uses the Feature Form Toolkit component. For information about setting up the toolkit, as well as code for the underlying component, visit [ArcGIS Maps SDK for Swift Toolkit](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit).
48+
49+
## Tags
50+
51+
edits, feature, feature forms, form, toolkit
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"category": "Edit and Manage Data",
3+
"description": "Display and edit feature attributes using feature forms.",
4+
"ignore": false,
5+
"images": [
6+
"edit-features-using-feature-forms.png"
7+
],
8+
"keywords": [
9+
"edits",
10+
"feature",
11+
"feature forms",
12+
"form",
13+
"toolkit",
14+
"ArcGISFeature",
15+
"FeatureForm",
16+
"FeatureLayer",
17+
"FieldFormElement",
18+
"GroupFormElement",
19+
"ServiceFeatureTable",
20+
"ServiceGeodatabase"
21+
],
22+
"redirect_from": [],
23+
"relevant_apis": [
24+
"ArcGISFeature",
25+
"FeatureForm",
26+
"FeatureLayer",
27+
"FieldFormElement",
28+
"GroupFormElement",
29+
"ServiceFeatureTable",
30+
"ServiceGeodatabase"
31+
],
32+
"snippets": [
33+
"EditFeaturesUsingFeatureFormsView.swift"
34+
],
35+
"title": "Edit features using feature forms"
36+
}
130 KB
Loading

0 commit comments

Comments
 (0)