Create and add features whose attribute values satisfy a predefined set of contingencies.

Use case
Contingent values are a data design feature that allow you to make values in one field dependent on values in another field. Your choice for a value on one field further constrains the domain values that can be placed on another field. In this way, contingent values enforce data integrity by applying additional constraints to reduce the number of valid field inputs.
For example, a field crew working in a sensitive habitat area may be required to stay a certain distance away from occupied bird nests, but the size of that exclusion area differs depending on the bird’s level of protection according to presiding laws. Surveyors can add points of bird nests in the work area and their selection of the size of the exclusion area will be contingent on the values in other attribute fields.
How to use the sample
Tap on the map to add a feature symbolizing a bird’s nest. Then choose values describing the nest’s status, protection, and buffer size. Notice how different values are available depending on the values of preceding fields. Once the contingent values are validated, tap “Done” to add the feature to the map.
How it works
- Create and load the
Geodatabasefrom the mobile geodatabase location on file. - Load the first
GeodatabaseFeatureTable. - Load the
ContingentValuesDefinitionfrom the feature table. - Create a new
FeatureLayerfrom the feature table and add it to the map. - Create a new
Featurefrom the feature table usingmakeFeature(attributes:geometry:). - Get the first field from the feature table by name using
field(named:). - Then get the
domainfrom the field as anCodedValueDomain. - Get the coded value domain’s
codedValuesto get an array ofCodedValues. - After selecting a value from the initial coded values for the first field, retrieve the remaining valid contingent values for each field as you select the values for the attributes.
i. Get the
ContingentValueResults by usingcontingentValues(with:field:)with the feature and the target field by name. ii. Get an array of validContingentValuesfromcontingentValuesByFieldGroupdictionary with the name of the relevant field group. iii. Iterate through the array of valid contingent values to create an array ofContingentCodedValuenames or the minimum and maximum values of aContingentRangeValuedepending on the type ofContingentValuereturned. - Validate the feature’s contingent values by using
validateContingencyConstraints(for:)with the current feature. If the resulting array is empty, the selected values are valid.
Relevant API
- ArcGISFeatureTable
- CodedValue
- CodedValueDomain
- ContingencyConstraintViolation
- ContingentCodedValue
- ContingentRangeValue
- ContingentValuesDefinition
- ContingentValuesResult
Offline data
This sample uses the Contingent values birds nests mobile geodatabase and the Fillmore topographic map vector tile package for the basemap.
About the data
The mobile geodatabase contains birds nests in the Fillmore area, defined with contingent values. Each feature contains information about its status, protection, and buffer size.
Additional information
Learn more about contingent values and how to utilize them on the ArcGIS Pro documentation.
Tags
coded values, contingent values, feature table, geodatabase
Sample code
// Copyright 2024 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 AddFeaturesWithContingentValuesView: View { /// The view model for the sample. @StateObject private var model = Model()
/// The point on the map where the user tapped. @State private var tapLocation: Point?
/// A Boolean value indicating whether the add feature sheet is presented. @State private var addFeatureSheetIsPresented = 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, graphicsOverlays: [model.graphicsOverlay]) .onSingleTapGesture { _, mapPoint in tapLocation = mapPoint } .task(id: tapLocation) { // Add a feature representing a bird's nest when the map is tapped. guard let tapLocation else { return }
do { try await model.addFeature(at: tapLocation) addFeatureSheetIsPresented = true
// Create an envelope from the screen's frame. let viewRect = geometryProxy.frame(in: .local) guard let viewExtent = mapViewProxy.envelope( fromViewRect: viewRect ) else { return }
// Update the map's viewpoint with an offsetted tap location // to center the feature in the top half of the screen. let yOffset = (viewExtent.height / 2) / 2 let newViewpointCenter = Point( x: tapLocation.x, y: tapLocation.y - yOffset ) await mapViewProxy.setViewpointCenter(newViewpointCenter) } catch { self.error = error } } .task { do { // Load the features from the geodatabase when the sample loads. try await model.loadFeatures()
// Zoom to the extent of the added layer. guard let extent = model.map.operationalLayers.first?.fullExtent else { return } await mapViewProxy.setViewpointGeometry(extent, padding: 15) } catch { self.error = error } } } } .overlay(alignment: .bottom) { // A workaround that allows the popover to display at the bottom of // the screen on Mac Catalyst. EmptyView() .frame(width: 1, height: 1) .padding() .popover(isPresented: $addFeatureSheetIsPresented, arrowEdge: .bottom) { NavigationStack { AddFeatureView(model: model) .navigationTitle("Add Bird Nest") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel", role: .cancel) { addFeatureSheetIsPresented = false } }
ToolbarItem(placement: .confirmationAction) { Button("Done") { model.feature = nil addFeatureSheetIsPresented = false } .disabled(!model.contingenciesAreValid) } } } .presentationDetents([.fraction(0.5)]) .frame(idealWidth: 320, idealHeight: 320) } .task(id: addFeatureSheetIsPresented) { // When the sheet closes, remove the feature if it is invalid. guard !addFeatureSheetIsPresented, model.feature != nil else { return }
do { try await model.removeFeature() } catch { self.error = error } } } .errorAlert(presentingError: $error) }}// Copyright 2024 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 Foundation
extension AddFeaturesWithContingentValuesView { /// The view model for the sample. @MainActor class Model: ObservableObject { // MARK: Properties
/// A map with a topographic vector titled basemap of the Fillmore, CA, USA area. let map: Map = { // Create a vector tiled layer using a local URL. let fillmoreVectorTiledLayer = ArcGISVectorTiledLayer(url: .fillmoreTopographicMap)
// Create a map using the vector tiled layer as a basemap. let fillmoreBasemap = Basemap(baseLayer: fillmoreVectorTiledLayer) let map = Map(basemap: fillmoreBasemap)
return map }()
/// The graphics overlay for the buffer graphics. let graphicsOverlay: GraphicsOverlay = { let graphicsOverlay = GraphicsOverlay()
// Create a simple renderer for the buffer graphics. let bufferPolygonOutlineSymbol = SimpleLineSymbol(style: .solid, color: .black, width: 2) let bufferPolygonFillSymbol = SimpleFillSymbol( style: .forwardDiagonal, color: .red, outline: bufferPolygonOutlineSymbol ) graphicsOverlay.renderer = SimpleRenderer(symbol: bufferPolygonFillSymbol)
return graphicsOverlay }()
/// A temporary file containing a geodatabase copied from a local URL. private let geodatabaseFile = GeodatabaseFile(fileURL: .contingentValuesBirdNests)
/// The feature table containing the features. private var featureTable: ArcGISFeatureTable?
/// The feature in the feature table. var feature: ArcGISFeature?
/// A Boolean value indicating whether all the contingency constraints associated with the feature are valid. @Published private(set) var contingenciesAreValid = false
// MARK: Methods
/// Loads the features from the geodatabase. func loadFeatures() async throws { // Get the feature table from the geodatabase. try await geodatabaseFile?.geodatabase.load() guard let featureTable = geodatabaseFile?.geodatabase.featureTables.first else { return } self.featureTable = featureTable
// Load the feature table's contingent values definition. try await featureTable.contingentValuesDefinition.load()
// Create a feature layer from the table and add it to the map. let featureLayer = FeatureLayer(featureTable: featureTable) map.addOperationalLayer(featureLayer)
// Create graphics for the features' buffers and add them to the graphics overlay. let bufferGraphics = try await bufferGraphics(for: featureTable) graphicsOverlay.addGraphics(bufferGraphics) }
/// Adds a feature representing a bird's nest to the map at a given point. /// - Parameter mapPoint: The point on the map. func addFeature(at mapPoint: Point) async throws { // Make a feature using the feature table. guard let newFeature = featureTable?.makeFeature(geometry: mapPoint) as? ArcGISFeature else { return }
// Add the feature to the feature table. try await featureTable?.add(newFeature) feature = newFeature
// Create an initial graphic for the buffer and add it to the graphics overlay. graphicsOverlay.addGraphic(Graphic()) }
/// Removes the added feature from the map. func removeFeature() async throws { // Remove the feature from the feature table. if let feature { try await featureTable?.delete(feature) self.feature = nil }
// Remove the feature's buffer graphic from the graphics overlay. if let lastGraphic = graphicsOverlay.graphics.last { graphicsOverlay.removeGraphic(lastGraphic) }
contingenciesAreValid = false }
/// Sets an attribute on the feature to a given value. /// - Parameters: /// - value: The value. /// - key: The key associated with the attribute. func setFeatureAttributeValue(_ value: (any Sendable)?, forKey key: String) { guard let featureTable, let feature else { return }
// Update the feature's attribute. feature.setAttributeValue(value, forKey: key)
// Validate the feature's contingencies. let contingencyViolations = featureTable.validateContingencyConstraints(for: feature) contingenciesAreValid = contingencyViolations.isEmpty
// Update the buffer graphic when needed. guard key == "BufferSize" else { return } graphicsOverlay.graphics.last?.geometry = bufferPolygon(for: feature) }
/// The coded values for the status field from the feature table. /// - Returns: The coded values. func statusCodedValues() -> [CodedValue] { // Get the status field from the feature table. let statusField = featureTable?.field(named: "Status")
// Get the domain from the field. guard let codedValueDomain = statusField?.domain as? CodedValueDomain else { return [] }
// Get the coded values from the domain. return codedValueDomain.codedValues }
/// The contingent coded values for the feature and the protection field from the feature table. /// - Returns: The contingent coded values. func protectionContingentCodedValues() -> [ContingentCodedValue] { guard let feature else { return [] }
// Get the contingent values result for the feature and protection field. let contingentValuesResult = featureTable?.contingentValues( with: feature, forFieldNamed: "Protection" )
// Get contingent coded values for the protection field group. guard let protectionGroupContingentValues = contingentValuesResult? .contingentValuesByFieldGroup["ProtectionFieldGroup"] as? [ContingentCodedValue] else { return [] }
return protectionGroupContingentValues }
/// The buffer size range for the feature and buffer size field from the feature table. /// - Returns: A range made up from the contingent range value's min and max. func bufferSizeRange() -> ClosedRange<Double>? { guard let feature else { return nil }
// Get the contingent values result for the feature and buffer size field. let contingentValuesResult = featureTable?.contingentValues( with: feature, forFieldNamed: "BufferSize" )
// Get contingent range values for the buffer size field group. guard let bufferSizeGroupContingentValues = contingentValuesResult? .contingentValuesByFieldGroup["BufferSizeFieldGroup"] as? [ContingentRangeValue] else { return nil }
// Create a range with min and max value from the contingent range value. guard let contingentRangeValue = bufferSizeGroupContingentValues.first, let minValue = contingentRangeValue.minValue as? Int, let maxValue = contingentRangeValue.maxValue as? Int else { return nil }
return Double(minValue)...Double(maxValue) }
/// The buffer graphics for the features in a given feature table. /// - Parameter featureTable: The feature table containing the features. private func bufferGraphics( for featureTable: GeodatabaseFeatureTable ) async throws -> [Graphic] { // Create the query parameters to filter for buffer sizes greater than 0. let queryParameters = QueryParameters() queryParameters.whereClause = "BufferSize > 0"
// Query the features in the feature table using the query parameters. let queryResult = try await featureTable.queryFeatures(using: queryParameters)
// Create graphics for the features in the query result. let bufferGraphics = queryResult.features().map { feature in let bufferPolygon = bufferPolygon(for: feature) return Graphic(geometry: bufferPolygon) }
return bufferGraphics }
/// A polygon created from a given feature's geometry and buffer size attribute. /// - Parameter feature: The feature. /// - Returns: A new `Polygon` object. private func bufferPolygon(for feature: Feature) -> ArcGIS.Polygon? { // Get the buffer size from the feature's attributes. guard let bufferSize = feature.attributes["BufferSize"] as? Int32, let featureGeometry = feature.geometry else { return nil }
// Create a polygon using the feature's geometry and buffer size. return GeometryEngine.buffer(around: featureGeometry, distance: Double(bufferSize)) } }}
private extension AddFeaturesWithContingentValuesView.Model { // MARK: GeodatabaseFile
/// A temporary file containing a geodatabase copied from a given file URL. final class GeodatabaseFile { /// The geodatabase contained in the file. private(set) var geodatabase: Geodatabase
init?(fileURL: URL) { do { // Create a temporary directory. let temporaryDirectoryURL = try FileManager.default.url( for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: fileURL, create: true )
// Create a temporary URL where the geodatabase URL can be copied to. let temporaryGeodatabaseURL = temporaryDirectoryURL .appendingPathComponent("ContingentValuesBirdNests", isDirectory: false) .appendingPathExtension("geodatabase")
// Copy the item to the temporary URL. try FileManager.default.copyItem(at: fileURL, to: temporaryGeodatabaseURL)
// Create the geodatabase with the URL. geodatabase = Geodatabase(fileURL: temporaryGeodatabaseURL) } catch { return nil } }
deinit { // Close the geodatabase geodatabase.close()
// Remove the temporary file. try? FileManager.default.removeItem(at: geodatabase.fileURL)
// Remove the temporary directory. let temporaryDirectoryURL = geodatabase.fileURL.deletingLastPathComponent() try? FileManager.default.removeItem(at: temporaryDirectoryURL) } }}
private extension URL { /// A URL to the local "Contingent Values Bird Nests" geodatabase. static var contingentValuesBirdNests: URL { Bundle.main.url(forResource: "ContingentValuesBirdNests", withExtension: "geodatabase")! }
/// A URL to the local "Fillmore Topographic Map" vector tile package. static var fillmoreTopographicMap: URL { Bundle.main.url(forResource: "FillmoreTopographicMap", withExtension: "vtpk")! }}// Copyright 2024 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 AddFeaturesWithContingentValuesView { /// A view allowing the user to add a feature to the map. struct AddFeatureView: View { /// The view model for the sample. @ObservedObject var model: Model
/// The name of the selected status. @State private var selectedStatusName: String?
/// The coded value options for the feature's status attribute. @State private var statusOptions: [CodedValue?] = []
/// The name of the selected protection. @State private var selectedProtectionName: String?
/// The contingent coded value options for the feature's protection attribute. @State private var protectionOptions: [ContingentCodedValue?] = [] { didSet { selectedProtectionName = nil } }
/// The selected exclusion area buffer size. @State private var selectedBufferSize: Double?
/// The range of size options for the feature's buffer size attribute. @State private var bufferSizeRange: ClosedRange<Double>? { didSet { selectedBufferSize = bufferSizeRange?.lowerBound ?? 0 } }
var body: some View { List { Section { Picker("Status", selection: $selectedStatusName) { ForEach(statusOptions, id: \.?.name) { option in Text(option?.name ?? "") } } .onChange(of: selectedStatusName) { // Update the feature's status attribute. guard let selectedCodedValue = statusOptions.first( where: { $0?.name == selectedStatusName } ) else { return }
model.setFeatureAttributeValue(selectedCodedValue?.code, forKey: "Status")
// Update the protection options. protectionOptions = model.protectionContingentCodedValues()
// Add nil to allow for an empty option in the picker. protectionOptions.insert(nil, at: 0) }
Picker("Protection", selection: $selectedProtectionName) { ForEach(protectionOptions, id: \.?.codedValue.name) { option in Text(option?.codedValue.name ?? "") } } .onChange(of: selectedProtectionName) { // Update the feature's protection attribute. guard let selectedContingentValue = protectionOptions.first( where: { $0?.codedValue.name == selectedProtectionName } ) else { return }
model.setFeatureAttributeValue( selectedContingentValue?.codedValue.code, forKey: "Protection" )
// Update the buffer size range. bufferSizeRange = model.bufferSizeRange() }
VStack { LabeledContent( "Exclusion Area Buffer Size", value: selectedBufferSize ?? 0, format: .number.precision(.fractionLength(0)) )
Slider( value: Binding( get: { selectedBufferSize ?? 0 }, set: { selectedBufferSize = $0 } ), in: bufferSizeRange ?? 0...0 ) .onChange(of: selectedBufferSize) { guard let selectedBufferSize, selectedBufferSize.isFinite else { return }
// Update the feature's buffer size attribute. model.setFeatureAttributeValue( Int32(selectedBufferSize), forKey: "BufferSize" ) } .disabled(bufferSizeRange == nil) } } header: { Text("Set the attributes") } footer: { Text("The options will vary depending on which values are selected.") } } .onAppear { // Get the status coded values when the view appears. statusOptions = model.statusCodedValues()
// Add nil to allow for an empty option in the picker. statusOptions.insert(nil, at: 0) } .onDisappear { selectedStatusName = nil selectedProtectionName = nil selectedBufferSize = nil } } }}