Skip to content

Commit bd314de

Browse files
authored
Merge pull request #1011 from Esri/philium/UtilityNetworkTrace-visionOS-support
Add visionOS support to `UtilityNetworkTrace`
2 parents 22cb3b1 + 9280aae commit bd314de

File tree

9 files changed

+127
-111
lines changed

9 files changed

+127
-111
lines changed

Examples/Examples.xcodeproj/project.pbxproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1CC376D42ABA0B3700A83300 /* TableTopExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC376D32ABA0B3700A83300 /* TableTopExampleView.swift */; platformFilter = ios; };
1212
4D19FCB52881C8F3002601E8 /* PopupExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D19FCB42881C8F3002601E8 /* PopupExampleView.swift */; };
1313
4D995CD72B5B01DB00AD45FE /* FeatureFormExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B615012AD76158009D19B6 /* FeatureFormExampleView.swift */; platformFilters = (ios, maccatalyst, ); };
14-
75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */; platformFilters = (ios, maccatalyst, ); };
14+
75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */; };
1515
75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75657E4727ABAC8400EE865B /* CompassExampleView.swift */; };
1616
75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */; };
1717
75D41B2B27C6F21400624D7C /* ScalebarExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D41B2A27C6F21400624D7C /* ScalebarExampleView.swift */; };

Examples/Examples/UtilityNetworkTraceExampleView.swift

+42-19
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ struct UtilityNetworkTraceExampleView: View {
4040
var body: some View {
4141
GeometryReader { geometryProxy in
4242
MapViewReader { mapViewProxy in
43-
MapView(
43+
let mapView = MapView(
4444
map: map,
4545
graphicsOverlays: [resultGraphicsOverlay]
4646
)
@@ -54,24 +54,47 @@ struct UtilityNetworkTraceExampleView: View {
5454
let publicSample = try? await ArcGISCredential.publicSample
5555
ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(publicSample!)
5656
}
57-
.floatingPanel(
58-
attributionBarHeight: attributionBarHeight,
59-
backgroundColor: Color(uiColor: .systemGroupedBackground),
60-
selectedDetent: $activeDetent,
61-
horizontalAlignment: .trailing,
62-
isPresented: .constant(true)
63-
) {
64-
UtilityNetworkTrace(
65-
graphicsOverlay: $resultGraphicsOverlay,
66-
map: map,
67-
mapPoint: $mapPoint,
68-
mapViewProxy: mapViewProxy
69-
)
70-
.floatingPanelDetent($activeDetent)
71-
// Manually account for a device's bottom safe area when using a Floating Panel.
72-
// See also #518.
73-
.padding(.bottom, isPortraitOrientation ? geometryProxy.safeAreaInsets.bottom : nil)
74-
}
57+
#if os(visionOS)
58+
mapView
59+
.floatingPanel(
60+
attributionBarHeight: attributionBarHeight,
61+
selectedDetent: $activeDetent,
62+
horizontalAlignment: .trailing,
63+
isPresented: .constant(true)
64+
) {
65+
UtilityNetworkTrace(
66+
graphicsOverlay: $resultGraphicsOverlay,
67+
map: map,
68+
mapPoint: $mapPoint,
69+
mapViewProxy: mapViewProxy
70+
)
71+
.floatingPanelDetent($activeDetent)
72+
// Manually account for a device's bottom safe area when using a Floating Panel.
73+
// See also #518.
74+
.padding(.bottom, isPortraitOrientation ? geometryProxy.safeAreaInsets.bottom : nil)
75+
.padding(.top)
76+
}
77+
#else
78+
mapView
79+
.floatingPanel(
80+
attributionBarHeight: attributionBarHeight,
81+
backgroundColor: Color(uiColor: .systemGroupedBackground),
82+
selectedDetent: $activeDetent,
83+
horizontalAlignment: .trailing,
84+
isPresented: .constant(true)
85+
) {
86+
UtilityNetworkTrace(
87+
graphicsOverlay: $resultGraphicsOverlay,
88+
map: map,
89+
mapPoint: $mapPoint,
90+
mapViewProxy: mapViewProxy
91+
)
92+
.floatingPanelDetent($activeDetent)
93+
// Manually account for a device's bottom safe area when using a Floating Panel.
94+
// See also #518.
95+
.padding(.bottom, isPortraitOrientation ? geometryProxy.safeAreaInsets.bottom : nil)
96+
}
97+
#endif
7598
}
7699
}
77100
}

Examples/ExamplesApp/Examples.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ extension ExampleList {
6464
AnyExample("Overview Map", content: OverviewMapExampleView()),
6565
AnyExample("Popup", content: PopupExampleView()),
6666
AnyExample("Scalebar", content: ScalebarExampleView()),
67-
AnyExample("Search", content: SearchExampleView())
67+
AnyExample("Search", content: SearchExampleView()),
68+
AnyExample("Utility Network Trace", content: UtilityNetworkTraceExampleView())
6869
]
6970
#if !os(visionOS)
7071
examples.append(
7172
contentsOf: [
72-
AnyExample("Feature Form", content: FeatureFormExampleView()),
73-
AnyExample("Utility Network Trace", content: UtilityNetworkTraceExampleView())
73+
AnyExample("Feature Form", content: FeatureFormExampleView())
7474
] as [any Example]
7575
)
7676
#endif

Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift

+75-71
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ import SwiftUI
6161
/// To see the `UtilityNetworkTrace` in action, check out the [Examples](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/tree/main/Examples/Examples)
6262
/// and refer to [UtilityNetworkTraceExampleView.swift](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/blob/main/Examples/Examples/UtilityNetworkTraceExampleView.swift)
6363
/// in the project. To learn more about using the `UtilityNetworkTrace` see the <doc:UtilityNetworkTraceTutorial>.
64-
@available(visionOS, unavailable)
6564
public struct UtilityNetworkTrace: View {
6665
/// The proxy to provide access to map view operations.
6766
private var mapViewProxy: MapViewProxy?
@@ -180,38 +179,38 @@ public struct UtilityNetworkTrace: View {
180179
currentActivity = .viewingTraces(.viewingFeatureResults)
181180
}
182181
makeDetailSectionHeader(title: assetGroupName)
183-
List {
184-
ForEach(
185-
assetTypeGroups.sorted(using: KeyPathComparator(\.key)),
186-
id: \.key
187-
) { (name, elements) in
188-
Section(name) {
189-
DisclosureGroup {
190-
ForEach(elements, id: \.globalID) { element in
191-
Button {
192-
Task {
193-
if let feature = await viewModel.feature(for: element),
194-
let geometry = feature.geometry {
195-
updateViewpoint(to: geometry.extent)
196-
}
197-
}
198-
} label: {
199-
Label {
200-
Text(
201-
"Object ID: \(element.objectID, format: .number.grouping(.never))",
202-
bundle: .toolkitModule,
203-
comment: "A string identifying a utility network object."
204-
)
205-
} icon: {
206-
Image(systemName: "scope")
182+
List(
183+
assetTypeGroups.sorted(using: KeyPathComparator(\.key)),
184+
id: \.key
185+
) { (name, elements) in
186+
Section(name) {
187+
DisclosureGroup {
188+
ForEach(elements, id: \.globalID) { element in
189+
Button {
190+
Task {
191+
if let feature = await viewModel.feature(for: element),
192+
let geometry = feature.geometry {
193+
updateViewpoint(to: geometry.extent)
207194
}
208195
}
196+
} label: {
197+
Label(
198+
String(
199+
localized: "Object ID: \(element.objectID, format: .number.grouping(.never))",
200+
bundle: .toolkitModule,
201+
comment: "A string identifying a utility network object."
202+
),
203+
systemImage: "scope"
204+
)
209205
}
210-
} label: {
211-
Text(elements.count, format: .number)
212-
.catalystPadding(4)
213206
}
207+
} label: {
208+
Text(elements.count, format: .number)
209+
.catalystPadding(4)
214210
}
211+
#if os(visionOS)
212+
.listItemTint(.monochrome)
213+
#endif
215214
}
216215
}
217216
}
@@ -223,11 +222,9 @@ public struct UtilityNetworkTrace: View {
223222
Text(String.noConfigurationsAvailable)
224223
} else {
225224
ForEach(viewModel.configurations.sorted { $0.name < $1.name }, id: \.name) { configuration in
226-
Button {
225+
Button(configuration.name) {
227226
viewModel.setPendingTrace(configuration: configuration)
228227
currentActivity = .creatingTrace(nil)
229-
} label: {
230-
Text(configuration.name)
231228
}
232229
.listRowBackground(configuration.name == viewModel.pendingTrace.configuration?.name ? Color.secondary.opacity(0.5) : nil)
233230
}
@@ -237,13 +234,14 @@ public struct UtilityNetworkTrace: View {
237234
/// Displays the list of available networks.
238235
@ViewBuilder private var networksList: some View {
239236
ForEach(viewModel.networks, id: \.name) { network in
240-
Text(network.name)
241-
.lineLimit(1)
242-
.listRowBackground(network.name == viewModel.network?.name ? Color.secondary.opacity(0.5) : nil)
243-
.onTapGesture {
244-
viewModel.setNetwork(network)
245-
currentActivity = .creatingTrace(nil)
246-
}
237+
Button {
238+
viewModel.setNetwork(network)
239+
currentActivity = .creatingTrace(nil)
240+
} label: {
241+
Text(network.name)
242+
.lineLimit(1)
243+
}
244+
.listRowBackground(network.name == viewModel.network?.name ? Color.secondary.opacity(0.5) : nil)
247245
}
248246
}
249247

@@ -311,20 +309,21 @@ public struct UtilityNetworkTrace: View {
311309
Button(String.deleteAllStartingPoints, systemImage: "trash") {
312310
deleteAllStartingPointsConfirmationIsPresented = true
313311
}
312+
#if !os(visionOS)
314313
.buttonStyle(.plain)
314+
#endif
315315
.labelStyle(.iconOnly)
316316
.confirmationDialog(
317-
String.deleteAllStartingPoints,
318-
isPresented: $deleteAllStartingPointsConfirmationIsPresented
317+
String.deleteAllStartingPointsQuestion,
318+
isPresented: $deleteAllStartingPointsConfirmationIsPresented,
319+
titleVisibility: .visible
319320
) {
320-
Button(String.deleteAllStartingPoints, role: .destructive) {
321-
viewModel.pendingTrace.startingPoints.forEach { startingPoint in
321+
Button(String.deleteButtonLabel, role: .destructive) {
322+
for startingPoint in viewModel.pendingTrace.startingPoints {
322323
viewModel.deleteStartingPoint(startingPoint)
323324
externalStartingPoints.removeAll()
324325
}
325326
}
326-
} message: {
327-
Text(String.deleteAllStartingPointsMessage)
328327
}
329328
// Override default uppercase capitalization for list
330329
// section headers on iOS and iPadOS.
@@ -348,7 +347,12 @@ public struct UtilityNetworkTrace: View {
348347
viewModel.pendingTrace.userDidSpecifyName = true
349348
}
350349
.multilineTextAlignment(.trailing)
350+
#if os(visionOS)
351+
.contentShape(.hoverEffect, .rect(cornerRadius: 12))
352+
.hoverEffect()
353+
#else
351354
.foregroundStyle(.blue)
355+
#endif
352356
}
353357
ColorPicker(String.colorLabel, selection: $viewModel.pendingTrace.color)
354358
Toggle(String.zoomToResult, isOn: $shouldZoomOnTraceCompletion)
@@ -436,15 +440,18 @@ public struct UtilityNetworkTrace: View {
436440
) {
437441
if let selectedTrace = viewModel.selectedTrace {
438442
ForEach(selectedTrace.assetGroupNames.sorted(), id: \.self) { assetGroupName in
439-
HStack {
440-
Text(assetGroupName)
441-
Spacer()
442-
Text(selectedTrace.elements(inAssetGroupNamed: assetGroupName).count, format: .number)
443-
}
444-
.foregroundStyle(.blue)
445-
.contentShape(.rect)
446-
.onTapGesture {
443+
Button {
447444
currentActivity = .viewingTraces(.viewingElementGroup(named: assetGroupName))
445+
} label: {
446+
HStack {
447+
Text(assetGroupName)
448+
Spacer()
449+
Text(selectedTrace.elements(inAssetGroupNamed: assetGroupName).count, format: .number)
450+
}
451+
#if !os(visionOS)
452+
.foregroundStyle(.blue)
453+
.contentShape(.rect)
454+
#endif
448455
}
449456
}
450457
}
@@ -584,18 +591,23 @@ public struct UtilityNetworkTrace: View {
584591
viewModel.setTerminalConfigurationFor(startingPoint: selectedStartingPoint!, to: newValue)
585592
}
586593
) {
587-
ForEach(viewModel.pendingTrace.startingPoints.first {
588-
$0 == selectedStartingPoint
589-
}?.utilityElement?.assetType.terminalConfiguration?.terminals ?? [], id: \.self) {
594+
ForEach(
595+
viewModel.pendingTrace.startingPoints
596+
.first(where: { $0 == selectedStartingPoint })?
597+
.utilityElement?.assetType.terminalConfiguration?.terminals ?? [],
598+
id: \.self
599+
) {
590600
Text($0.name)
591601
}
592602
}
603+
#if !os(visionOS)
593604
.foregroundStyle(.blue)
605+
#endif
594606
}
595607
}
596608
Section(String.attributesSectionTitle) {
597609
ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in
598-
HStack{
610+
HStack {
599611
Text(item.key)
600612
Spacer()
601613
Text(item.value as? String ?? "")
@@ -802,16 +814,10 @@ public struct UtilityNetworkTrace: View {
802814
/// - Parameter title: The button's title.
803815
/// - Parameter action: The action to be performed.
804816
/// - Returns: The configured button.
805-
private func makeBackButton(title: String, _ action: @escaping () -> Void) -> some View {
806-
Button { action() } label: {
807-
Label {
808-
Text(title)
809-
} icon: {
810-
Image(systemName: "chevron.backward")
811-
}
812-
}
813-
.padding()
814-
.frame(maxWidth: .infinity, alignment: .leading)
817+
private func makeBackButton(title: String, action: @escaping () -> Void) -> some View {
818+
Button(title, systemImage: "chevron.backward", action: action)
819+
.padding()
820+
.frame(maxWidth: .infinity, alignment: .leading)
815821
}
816822

817823
/// Returns a section header.
@@ -890,11 +896,11 @@ private extension String {
890896
)
891897
}
892898

893-
static var deleteAllStartingPointsMessage: Self {
899+
static var deleteAllStartingPointsQuestion: Self {
894900
.init(
895-
localized: "All starting points will be deleted.",
901+
localized: "Delete all starting points?",
896902
bundle: .toolkitModule,
897-
comment: "A message describing the outcome of clearing all starting points."
903+
comment: "The title of the dialogue confirming deletion of all points."
898904
)
899905
}
900906

@@ -1064,8 +1070,6 @@ private extension String {
10641070
}
10651071
}
10661072

1067-
1068-
@available(visionOS, unavailable)
10691073
public extension UtilityNetworkTrace /* Deprecated */ {
10701074
/// A graphical interface to run pre-configured traces on a map's utility networks.
10711075
/// - Parameters:

Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift

-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import ArcGIS
1616
import UIKit
1717

1818
/// A starting point of a utility network trace.
19-
@available(visionOS, unavailable)
2019
public struct UtilityNetworkTraceStartingPoint: Sendable {
2120
/// The geo element to be used as a starting point.
2221
var geoElement: GeoElement
@@ -50,7 +49,6 @@ public struct UtilityNetworkTraceStartingPoint: Sendable {
5049
}
5150
}
5251

53-
@available(visionOS, unavailable)
5452
extension UtilityNetworkTraceStartingPoint: Hashable {
5553
public static func == (lhs: UtilityNetworkTraceStartingPoint, rhs: UtilityNetworkTraceStartingPoint) -> Bool {
5654
lhs.id == rhs.id

Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel+Trace.swift

-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import ArcGIS
1616
import SwiftUI
1717

18-
@available(visionOS, unavailable)
1918
extension UtilityNetworkTraceViewModel {
2019
/// A trace performed on a utility network.
2120
struct Trace {
@@ -74,7 +73,6 @@ extension UtilityNetworkTraceViewModel {
7473
}
7574
}
7675

77-
@available(visionOS, unavailable)
7876
extension UtilityNetworkTraceViewModel.Trace {
7977
/// Finds the set of utility elements returned by the trace that belong to the provided
8078
/// asset group, grouped by type.
@@ -143,7 +141,6 @@ extension UtilityNetworkTraceViewModel.Trace {
143141
}
144142
}
145143

146-
@available(visionOS, unavailable)
147144
extension UtilityNetworkTraceViewModel.Trace: Equatable {
148145
static func == (lhs: Self, rhs: Self) -> Bool {
149146
return lhs.id == rhs.id

0 commit comments

Comments
 (0)