Edit features with feature-linked annotation

View on GitHub

Edit feature attributes which are linked to annotations through an expression.

Image of Edit features with feature-linked annotation sample

Use case

Annotation is useful for displaying text that you don't want to move or resize when the map is panned or zoomed (unlike labels which will move and resize). Feature-linked annotation will update when a feature attribute referenced by the annotation expression is also updated. Additionally, the position of the annotation will transform to match any transformation to the linked feature's geometry.

How to use the sample

Pan and zoom the map to see that the text on the map is annotation, not labels. Tap one of the address points to update the house number (AD_ADDRESS) and street name (ST_STR_NAM). Tap one of the dashed parcel polylines and tap another location to change its geometry. NOTE: Selection is only enabled for points and straight (single segment) polylines.

The feature-linked annotation will update accordingly.

How it works

  1. Load the geodatabase. NOTE: Read/write geodatabases should normally come from a GeodatabaseSyncTask, but this has been omitted here.
  2. Create FeatureLayers from geodatabase feature tables found in the geodatabase with Geodatabase.featureTable(named:).
  3. Create AnnotationLayers from geodatabase feature tables found in the geodatabase with Geodatabase.annotationTable(named:).
  4. Add the FeatureLayers and AnnotationLayers to the map's operational layers.
  5. Use a GeoView.onSingleTapGesture(perform:) modifier to handles taps on the MapView to either select address points or parcel polyline features. NOTE: Selection is only enabled for points and straight (single segment) polylines.
    • For the address points, an alert is opened to allow editing of the address number (AD_ADDRESS) and street name (ST_STR_NAM) attributes.
    • For the parcel lines, a second tap will change one of the polyline's vertices.

Both expressions were defined by the data author in ArcGIS Pro using the Arcade expression language.

Relevant API

  • AnnotationLayer
  • Feature
  • FeatureLayer
  • Geodatabase

Offline data

This sample uses data from ArcGIS Online. It is downloaded automatically.

About the data

This sample uses data derived from the Loudoun GeoHub.

The annotation linked to the point data in this sample is defined by arcade expression $feature.AD_ADDRESS + " " + $feature.ST_STR_NAM. The annotation linked to the parcel polyline data is defined by Round(Length(Geometry($feature), 'feet'), 2).

Tags

annotation, attributes, feature-linked annotation, features, fields

Sample Code

EditFeaturesWithFeatureLinkedAnnotationView.swiftEditFeaturesWithFeatureLinkedAnnotationView.swiftEditFeaturesWithFeatureLinkedAnnotationView.Model.swift
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// 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 ArcGIS
import SwiftUI

struct EditFeaturesWithFeatureLinkedAnnotationView: View {
    /// The view model for the sample.
    @StateObject private var model = Model()

    /// The asynchronous action currently being preformed.
    @State private var selectedAction = AsyncAction.setUpMap

    /// The current instruction being displayed at the top of the screen.
    @State private var instruction = Instruction.selectFeature

    /// The point on the map where the user tapped.
    @State private var tapLocation: Point?

    /// The building number of the selected feature.
    @State private var buildingNumber: Int32?

    /// The street name of the selected feature.
    @State private var streetName: String = ""

    /// A Boolean value indicating whether the edit address alert is presented.
    @State private var editAddressAlertIsPresented = false

    /// A Boolean value indicating whether the move confirmation alert is presented.
    @State private var moveConfirmationAlertIsPresented = false

    /// The error shown in the error alert.
    @State private var error: Error?

    var body: some View {
        MapViewReader { mapViewProxy in
            MapView(map: model.map)
                .onSingleTapGesture { screenPoint, mapPoint in
                    if model.selectedFeature == nil {
                        // Selects a feature at the tap location if there isn't one selected.
                        selectedAction = .selectFeature(screenPoint: screenPoint)
                    } else {
                        // Shows the move confirmation alert if there is already a selected feature.
                        tapLocation = mapPoint
                        moveConfirmationAlertIsPresented = true
                    }
                }
                .task(id: selectedAction) {
                    do {
                        // Performs the selected action.
                        switch selectedAction {
                        case .setUpMap:
                            try await model.setUpMap()
                        case .selectFeature(let screenPoint):
                            let layerIdentifyResults = try await mapViewProxy.identifyLayers(
                                screenPoint: screenPoint,
                                tolerance: 10
                            )
                            model.selectFirstFeature(from: layerIdentifyResults)

                            if model.selectedFeature != nil {
                                instruction = .moveFeature
                            }
                        case .setFeatureAddress(let buildingNumber, let streetName):
                            try await model.setFeatureAddress(
                                buildingNumber: buildingNumber,
                                streetName: streetName
                            )
                            instruction = .moveFeature
                        case .updateFeatureGeometry(let mapPoint):
                            try await model.updateFeatureGeometry(with: mapPoint)
                            instruction = .selectFeature
                        }
                    } catch {
                        self.error = error
                    }
                }
                .errorAlert(presentingError: $error)
                .overlay(alignment: .top) {
                    Text(instruction.message)
                        .multilineTextAlignment(.center)
                        .frame(maxWidth: .infinity, alignment: .center)
                        .padding(8)
                        .background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
                }
        }
        .onChange(of: model.selectedFeature == nil) { _ in
            if model.selectedFeature?.geometry is Point,
               let featureAddress = model.selectedFeatureAddress {
                // Presents the alert to update the feature's address if the feature is a point.
                buildingNumber = featureAddress.buildingNumber
                streetName = featureAddress.streetName
                editAddressAlertIsPresented = true
            } else if let polyline = model.selectedFeature?.geometry as? Polyline,
                      polyline.parts.contains(where: { $0.points.count > 2 }) {
                // Shows a message if the feature is a polyline with any part
                // containing more than one segment, i.e., a curve.
                instruction = .selectStraightPolyline
                model.clearSelectedFeature()
            }
        }
        .alert("Edit Address", isPresented: $editAddressAlertIsPresented) {
            TextField("Building Number", value: $buildingNumber, format: .number.grouping(.never))
                .keyboardType(.numberPad)
            TextField("Street Name", text: $streetName)
            Button("Cancel", role: .cancel) {
                model.clearSelectedFeature()
                instruction = .selectFeature
            }
            Button("Done") {
                selectedAction = .setFeatureAddress(
                    buildingNumber: buildingNumber!,
                    streetName: streetName
                )
            }
            .disabled(buildingNumber == nil || streetName.isEmpty)
        } message: {
            Text("Edit the feature's 'AD_ADDRESS' and 'ST_STR_NAM' attributes.")
        }
        .alert("Confirm Move", isPresented: $moveConfirmationAlertIsPresented) {
            Button("Cancel", role: .cancel) {
                model.clearSelectedFeature()
                instruction = .selectFeature
            }
            Button("Move") {
                selectedAction = .updateFeatureGeometry(mapPoint: tapLocation!)
            }
        } message: {
            Text("Are you sure you want to move the selected feature?")
        }
    }
}

private extension EditFeaturesWithFeatureLinkedAnnotationView {
    /// An asynchronous action associated with the sample.
    enum AsyncAction: Equatable {
        /// Sets up the map for the sample.
        case setUpMap
        /// Selects a feature identified at a given point on the screen.
        case selectFeature(screenPoint: CGPoint)
        /// Sets the address attributes of the selected feature to given values.
        case setFeatureAddress(buildingNumber: Int32, streetName: String)
        /// Updates the selected feature's geometry using a given point on the map.
        case updateFeatureGeometry(mapPoint: Point)
    }

    /// An instruction associated with the sample.
    enum Instruction {
        case selectFeature, selectStraightPolyline, moveFeature

        /// The message for the instruction.
        var message: String {
            switch self {
            case .selectFeature: "Select a point or polyline to edit."
            case .selectStraightPolyline: "Select straight (single segment) polylines only."
            case .moveFeature: "Tap on the map to move the feature."
            }
        }
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.