Skip to content

Commit 4cd7ba1

Browse files
authored
Merge pull request #1463 from Esri/Ting/FeatureEditorToolbar
fe: Add `FeatureEditorToolbar`
2 parents fc53ef6 + 0b5e0bd commit 4cd7ba1

4 files changed

Lines changed: 204 additions & 34 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2026 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 Observation
17+
18+
/// The data model for the feature editor.
19+
@MainActor
20+
@Observable
21+
final class FeatureEditorModel {
22+
let feature: ArcGISFeature
23+
let geometryEditor: GeometryEditor
24+
25+
init(feature: ArcGISFeature, geometryEditor: GeometryEditor) {
26+
self.feature = feature
27+
self.geometryEditor = geometryEditor
28+
}
29+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2026 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 SwiftUI
16+
17+
/// The `FeatureEditorToolbar` component allows users to edit geometries in the
18+
/// feature editor.
19+
///
20+
/// **Features**
21+
///
22+
/// - Displays controls for performing common geometry editing actions:
23+
/// - Changing the tool.
24+
/// - Deleting the selected element.
25+
/// - Undoing the last action on the geometry.
26+
/// - Redoing the last undone action.
27+
/// - Configuring snap settings.
28+
/// - Supports styled vertical and horizontal layouts, or no built-in layout or styling.
29+
///
30+
/// **Behavior**
31+
///
32+
/// The toolbar is shown only while the feature editor is editing a geometry.
33+
///
34+
/// By default, the toolbar is displayed in a vertical layout. Pass `nil` for `style` to display
35+
/// the toolbar without built-in layout or styling, so you can show it in a system toolbar or apply
36+
/// your own layout and styling to the controls.
37+
///
38+
/// The settings button is only shown when the geometry editor has snap settings with non-empty
39+
/// source settings.
40+
///
41+
/// **Associated Types**
42+
///
43+
/// - ``Style``
44+
public struct FeatureEditorToolbar: View {
45+
/// The style to apply to the toolbar's controls.
46+
private let style: Style?
47+
/// The model for the parent feature editor.
48+
@Environment(FeatureEditorModel.self) private var featureEditorModel
49+
50+
/// Creates a feature editor toolbar.
51+
/// - Parameter style: The style that determines the toolbar's appearance and layout.
52+
/// A `nil` value displays the toolbar's controls without built-in layout or styling.
53+
public init(style: Style? = .vertical) {
54+
self.style = style
55+
}
56+
57+
public var body: some View {
58+
GeometryEditorToolbar(
59+
geometryEditor: featureEditorModel.geometryEditor,
60+
style: GeometryEditorToolbar.Style(featureEditorToolbarStyle: style)
61+
)
62+
// Only shows snap settings for features in layers.
63+
.snapSources(.layers)
64+
}
65+
}
66+
67+
extension FeatureEditorToolbar {
68+
/// A style that determines the appearance and layout of a feature editor toolbar.
69+
public enum Style {
70+
/// Displays the toolbar in a styled horizontal layout.
71+
case horizontal
72+
/// Displays the toolbar in a styled vertical layout.
73+
case vertical
74+
}
75+
}
76+
77+
private extension GeometryEditorToolbar.Style {
78+
init?(featureEditorToolbarStyle: FeatureEditorToolbar.Style?) {
79+
switch featureEditorToolbarStyle {
80+
case .horizontal: self = .horizontal
81+
case .vertical: self = .vertical
82+
default: return nil
83+
}
84+
}
85+
}

Sources/ArcGISToolkit/Components/GeometryEditorToolbar/GeometryEditorToolbar.swift

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,13 @@ import SwiftUI
1717

1818
/// The `GeometryEditorToolbar` component allows users to perform common actions on a
1919
/// `GeometryEditor`.
20-
///
21-
/// **Features**
22-
///
23-
/// - Displays controls for performing common geometry editor actions:
24-
/// - Changing the tool.
25-
/// - Deleting the selected element.
26-
/// - Undoing the last action on the geometry.
27-
/// - Redoing the last undone action.
28-
/// - Configuring snap settings.
29-
/// - Supports styled vertical and horizontal layouts, or no built-in layout or styling.
30-
///
31-
/// **Behavior**
32-
///
33-
/// The toolbar is shown only while the geometry editor is started.
34-
///
35-
/// By default, the toolbar is displayed in a vertical layout. Pass `nil` for `style` to display
36-
/// the toolbar without built-in layout or styling, so you can show it in a system toolbar or apply
37-
/// your own layout and styling to the controls.
38-
///
39-
/// The settings button is only shown when the geometry editor has snap settings with non-empty
40-
/// source settings.
41-
///
42-
/// **Associated Types**
43-
///
44-
/// - ``Style``
4520
struct GeometryEditorToolbar: View {
4621
/// The geometry editor that this toolbar controls.
4722
private let geometryEditor: GeometryEditor
4823
/// The style to apply to the toolbar's controls.
4924
private let style: Style?
25+
/// The allowed snap source types for snap settings UI.
26+
private var snapSources: SnapSources
5027

5128
/// The spacing to apply between the controls in the stacks.
5229
/// This is hardcoded to match the system styling for toolbar groups on iOS.
@@ -68,6 +45,7 @@ struct GeometryEditorToolbar: View {
6845
geometryEditor.snapSettings.isEnabled = true
6946
self.geometryEditor = geometryEditor
7047
self.style = style
48+
self.snapSources = .all
7149

7250
let model = GeometryEditorToolbarModel(geometryEditor: geometryEditor)
7351
self._model = .init(wrappedValue: model)
@@ -109,7 +87,19 @@ struct GeometryEditorToolbar: View {
10987
DeleteButton()
11088
UndoButton()
11189
RedoButton()
112-
SnapSettingsButton()
90+
SnapSettingsButton(snapSources: snapSources)
91+
}
92+
}
93+
94+
extension GeometryEditorToolbar {
95+
/// Limits the snap source types available in the snap settings UI.
96+
/// Disallowed snap sources are hidden in the UI and disabled in the underlying `SnapSettings`.
97+
/// - Parameter snapSources: The allowed snap source types.
98+
/// - Returns: A toolbar with snap settings restricted to `snapSources`.
99+
func snapSources(_ snapSources: SnapSources) -> Self {
100+
var copy = self
101+
copy.snapSources = snapSources
102+
return copy
113103
}
114104
}
115105

@@ -154,6 +144,53 @@ extension GeometryEditorToolbar {
154144
/// Displays the toolbar in a styled vertical layout.
155145
case vertical
156146
}
147+
148+
/// A set of `SnapSource` types allowed in the snap settings UI.
149+
struct SnapSources: OptionSet {
150+
let rawValue: Int
151+
152+
/// Allows `FeatureLayer` snap sources.
153+
static let featureLayer = SnapSources(rawValue: 1 << 0)
154+
/// Allows `GraphicsOverlay` snap sources.
155+
static let graphicsOverlay = SnapSources(rawValue: 1 << 1)
156+
/// Allows `SubtypeFeatureLayer` snap sources.
157+
static let subtypeFeatureLayer = SnapSources(rawValue: 1 << 2)
158+
/// Allows `SubtypeSublayer` snap sources.
159+
static let subtypeSublayer = SnapSources(rawValue: 1 << 3)
160+
161+
/// Allows all supported `SnapSource` types.
162+
static let all: SnapSources = [
163+
.featureLayer,
164+
.graphicsOverlay,
165+
.subtypeFeatureLayer,
166+
.subtypeSublayer
167+
]
168+
/// Allows `SnapSource` types that are layers.
169+
static let layers: SnapSources = [
170+
.featureLayer,
171+
.subtypeFeatureLayer,
172+
.subtypeSublayer
173+
]
174+
175+
/// Returns a Boolean value indicating whether a snap source type is
176+
/// allowed by the set.
177+
/// - Parameter source: A snap source to check for allowance by the set.
178+
/// - Returns: `true` if the snap source type is in the set, otherwise `false`.
179+
func contains(source: some SnapSource) -> Bool {
180+
switch source {
181+
case is FeatureLayer:
182+
contains(.featureLayer)
183+
case is GraphicsOverlay:
184+
contains(.graphicsOverlay)
185+
case is SubtypeFeatureLayer:
186+
contains(.subtypeFeatureLayer)
187+
case is SubtypeSublayer:
188+
contains(.subtypeSublayer)
189+
default:
190+
false
191+
}
192+
}
193+
}
157194
}
158195

159196
// MARK: - Controls
@@ -249,6 +286,8 @@ private struct UndoButton: View {
249286
private struct SnapSettingsButton: View {
250287
/// The model for the parent geometry editor toolbar.
251288
@Environment(GeometryEditorToolbarModel.self) private var model
289+
/// The allowed snap source types shown in settings.
290+
let snapSources: GeometryEditorToolbar.SnapSources
252291

253292
/// A Boolean value indicating whether the settings view is presented.
254293
@State private var isShowingSettings = false
@@ -269,9 +308,12 @@ private struct SnapSettingsButton: View {
269308
}
270309
}
271310
.sheet(isPresented: $isShowingSettings) {
272-
SnapSettingsView(settings: model.geometryEditor.snapSettings)
311+
SnapSettingsView(
312+
settings: model.geometryEditor.snapSettings,
313+
snapSources: snapSources
314+
)
273315
// Needed to override the font set in toolbarStackStyle.
274-
.font(nil)
316+
.font(nil)
275317
}
276318
}
277319
}

Sources/ArcGISToolkit/Components/GeometryEditorToolbar/SnapSettingsView.swift

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import SwiftUI
1919
struct SnapSettingsView: View {
2020
/// The snap settings to configure.
2121
let settings: SnapSettings
22+
/// The allowed snap source types shown in the source settings list.
23+
let snapSources: GeometryEditorToolbar.SnapSources
2224

2325
/// The view models for the root `SnapSourceSettingsToggle` views in the outline group.
2426
@State private var rootSourceSettingsModels: [SnapSourceSettingsToggle.Model] = []
@@ -47,7 +49,7 @@ struct SnapSettingsView: View {
4749

4850
Toggle(isOn: $snapsToFeatures.animation()) {
4951
Text(
50-
"Snap to Features and Graphics",
52+
"Snap to Features",
5153
bundle: .toolkitModule,
5254
comment: """
5355
A label for a toggle that enables snapping to features and graphics,
@@ -60,7 +62,7 @@ struct SnapSettingsView: View {
6062
settings.snapsToFeatures = snapsToFeatures
6163
}
6264

63-
if snapsToFeatures {
65+
if snapsToFeatures, !rootSourceSettingsModels.isEmpty {
6466
Section {
6567
OutlineGroup(rootSourceSettingsModels, children: \.children) { model in
6668
SnapSourceSettingsToggle(model: model)
@@ -78,10 +80,22 @@ struct SnapSettingsView: View {
7880
}
7981
}
8082
.onChange(of: ObjectIdentifier(settings), initial: true) {
83+
// Only allows certain snap sources.
84+
rootSourceSettingsModels = settings.sourceSettings
85+
.compactMap { settings in
86+
guard snapSources.contains(source: settings.source) else { return nil }
87+
return SnapSourceSettingsToggle.Model(settings: settings)
88+
}
89+
90+
// Disable snapping on other snap sources that aren't exposed
91+
// in the UI to avoid confusion.
92+
for sourceSettings in settings.sourceSettings {
93+
if !snapSources.contains(source: sourceSettings.source) {
94+
sourceSettings.isEnabled = false
95+
}
96+
}
97+
8198
// Sets up view's state properties using settings' property values.
82-
rootSourceSettingsModels = settings.sourceSettings.map(
83-
SnapSourceSettingsToggle.Model.init(settings:)
84-
)
8599
snapsToFeatures = settings.snapsToFeatures
86100
snapsToGeometryGuides = settings.snapsToGeometryGuides
87101
}
@@ -212,5 +226,5 @@ private extension SnapSourceSettingsToggle {
212226
}
213227

214228
#Preview {
215-
SnapSettingsView(settings: SnapSettings())
229+
SnapSettingsView(settings: SnapSettings(), snapSources: .all)
216230
}

0 commit comments

Comments
 (0)