diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 2133d2cb90..153fb52758 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 1CC376D42ABA0B3700A83300 /* TableTopExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC376D32ABA0B3700A83300 /* TableTopExampleView.swift */; platformFilter = ios; }; 4D19FCB52881C8F3002601E8 /* PopupExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D19FCB42881C8F3002601E8 /* PopupExampleView.swift */; }; 4D995CD72B5B01DB00AD45FE /* FeatureFormExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B615012AD76158009D19B6 /* FeatureFormExampleView.swift */; platformFilters = (ios, maccatalyst, ); }; - 75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */; platformFilters = (ios, maccatalyst, ); }; + 75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */; }; 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75657E4727ABAC8400EE865B /* CompassExampleView.swift */; }; 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */; }; 75D41B2B27C6F21400624D7C /* ScalebarExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D41B2A27C6F21400624D7C /* ScalebarExampleView.swift */; }; diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index c05c592518..6a546913bc 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -40,7 +40,7 @@ struct UtilityNetworkTraceExampleView: View { var body: some View { GeometryReader { geometryProxy in MapViewReader { mapViewProxy in - MapView( + let mapView = MapView( map: map, graphicsOverlays: [resultGraphicsOverlay] ) @@ -54,24 +54,47 @@ struct UtilityNetworkTraceExampleView: View { let publicSample = try? await ArcGISCredential.publicSample ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(publicSample!) } - .floatingPanel( - attributionBarHeight: attributionBarHeight, - backgroundColor: Color(uiColor: .systemGroupedBackground), - selectedDetent: $activeDetent, - horizontalAlignment: .trailing, - isPresented: .constant(true) - ) { - UtilityNetworkTrace( - graphicsOverlay: $resultGraphicsOverlay, - map: map, - mapPoint: $mapPoint, - mapViewProxy: mapViewProxy - ) - .floatingPanelDetent($activeDetent) - // Manually account for a device's bottom safe area when using a Floating Panel. - // See also #518. - .padding(.bottom, isPortraitOrientation ? geometryProxy.safeAreaInsets.bottom : nil) - } +#if os(visionOS) + mapView + .floatingPanel( + attributionBarHeight: attributionBarHeight, + selectedDetent: $activeDetent, + horizontalAlignment: .trailing, + isPresented: .constant(true) + ) { + UtilityNetworkTrace( + graphicsOverlay: $resultGraphicsOverlay, + map: map, + mapPoint: $mapPoint, + mapViewProxy: mapViewProxy + ) + .floatingPanelDetent($activeDetent) + // Manually account for a device's bottom safe area when using a Floating Panel. + // See also #518. + .padding(.bottom, isPortraitOrientation ? geometryProxy.safeAreaInsets.bottom : nil) + .padding(.top) + } +#else + mapView + .floatingPanel( + attributionBarHeight: attributionBarHeight, + backgroundColor: Color(uiColor: .systemGroupedBackground), + selectedDetent: $activeDetent, + horizontalAlignment: .trailing, + isPresented: .constant(true) + ) { + UtilityNetworkTrace( + graphicsOverlay: $resultGraphicsOverlay, + map: map, + mapPoint: $mapPoint, + mapViewProxy: mapViewProxy + ) + .floatingPanelDetent($activeDetent) + // Manually account for a device's bottom safe area when using a Floating Panel. + // See also #518. + .padding(.bottom, isPortraitOrientation ? geometryProxy.safeAreaInsets.bottom : nil) + } +#endif } } } diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 55c6591704..aae3261e15 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -64,13 +64,13 @@ extension ExampleList { AnyExample("Overview Map", content: OverviewMapExampleView()), AnyExample("Popup", content: PopupExampleView()), AnyExample("Scalebar", content: ScalebarExampleView()), - AnyExample("Search", content: SearchExampleView()) + AnyExample("Search", content: SearchExampleView()), + AnyExample("Utility Network Trace", content: UtilityNetworkTraceExampleView()) ] #if !os(visionOS) examples.append( contentsOf: [ - AnyExample("Feature Form", content: FeatureFormExampleView()), - AnyExample("Utility Network Trace", content: UtilityNetworkTraceExampleView()) + AnyExample("Feature Form", content: FeatureFormExampleView()) ] as [any Example] ) #endif diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index d92c6aaca6..93dd77874c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -61,7 +61,6 @@ import SwiftUI /// To see the `UtilityNetworkTrace` in action, check out the [Examples](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/tree/main/Examples/Examples) /// and refer to [UtilityNetworkTraceExampleView.swift](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/blob/main/Examples/Examples/UtilityNetworkTraceExampleView.swift) /// in the project. To learn more about using the `UtilityNetworkTrace` see the . -@available(visionOS, unavailable) public struct UtilityNetworkTrace: View { /// The proxy to provide access to map view operations. private var mapViewProxy: MapViewProxy? @@ -180,38 +179,38 @@ public struct UtilityNetworkTrace: View { currentActivity = .viewingTraces(.viewingFeatureResults) } makeDetailSectionHeader(title: assetGroupName) - List { - ForEach( - assetTypeGroups.sorted(using: KeyPathComparator(\.key)), - id: \.key - ) { (name, elements) in - Section(name) { - DisclosureGroup { - ForEach(elements, id: \.globalID) { element in - Button { - Task { - if let feature = await viewModel.feature(for: element), - let geometry = feature.geometry { - updateViewpoint(to: geometry.extent) - } - } - } label: { - Label { - Text( - "Object ID: \(element.objectID, format: .number.grouping(.never))", - bundle: .toolkitModule, - comment: "A string identifying a utility network object." - ) - } icon: { - Image(systemName: "scope") + List( + assetTypeGroups.sorted(using: KeyPathComparator(\.key)), + id: \.key + ) { (name, elements) in + Section(name) { + DisclosureGroup { + ForEach(elements, id: \.globalID) { element in + Button { + Task { + if let feature = await viewModel.feature(for: element), + let geometry = feature.geometry { + updateViewpoint(to: geometry.extent) } } + } label: { + Label( + String( + localized: "Object ID: \(element.objectID, format: .number.grouping(.never))", + bundle: .toolkitModule, + comment: "A string identifying a utility network object." + ), + systemImage: "scope" + ) } - } label: { - Text(elements.count, format: .number) - .catalystPadding(4) } + } label: { + Text(elements.count, format: .number) + .catalystPadding(4) } +#if os(visionOS) + .listItemTint(.monochrome) +#endif } } } @@ -223,11 +222,9 @@ public struct UtilityNetworkTrace: View { Text(String.noConfigurationsAvailable) } else { ForEach(viewModel.configurations.sorted { $0.name < $1.name }, id: \.name) { configuration in - Button { + Button(configuration.name) { viewModel.setPendingTrace(configuration: configuration) currentActivity = .creatingTrace(nil) - } label: { - Text(configuration.name) } .listRowBackground(configuration.name == viewModel.pendingTrace.configuration?.name ? Color.secondary.opacity(0.5) : nil) } @@ -237,13 +234,14 @@ public struct UtilityNetworkTrace: View { /// Displays the list of available networks. @ViewBuilder private var networksList: some View { ForEach(viewModel.networks, id: \.name) { network in - Text(network.name) - .lineLimit(1) - .listRowBackground(network.name == viewModel.network?.name ? Color.secondary.opacity(0.5) : nil) - .onTapGesture { - viewModel.setNetwork(network) - currentActivity = .creatingTrace(nil) - } + Button { + viewModel.setNetwork(network) + currentActivity = .creatingTrace(nil) + } label: { + Text(network.name) + .lineLimit(1) + } + .listRowBackground(network.name == viewModel.network?.name ? Color.secondary.opacity(0.5) : nil) } } @@ -311,20 +309,21 @@ public struct UtilityNetworkTrace: View { Button(String.deleteAllStartingPoints, systemImage: "trash") { deleteAllStartingPointsConfirmationIsPresented = true } +#if !os(visionOS) .buttonStyle(.plain) +#endif .labelStyle(.iconOnly) .confirmationDialog( - String.deleteAllStartingPoints, - isPresented: $deleteAllStartingPointsConfirmationIsPresented + String.deleteAllStartingPointsQuestion, + isPresented: $deleteAllStartingPointsConfirmationIsPresented, + titleVisibility: .visible ) { - Button(String.deleteAllStartingPoints, role: .destructive) { - viewModel.pendingTrace.startingPoints.forEach { startingPoint in + Button(String.deleteButtonLabel, role: .destructive) { + for startingPoint in viewModel.pendingTrace.startingPoints { viewModel.deleteStartingPoint(startingPoint) externalStartingPoints.removeAll() } } - } message: { - Text(String.deleteAllStartingPointsMessage) } // Override default uppercase capitalization for list // section headers on iOS and iPadOS. @@ -348,7 +347,12 @@ public struct UtilityNetworkTrace: View { viewModel.pendingTrace.userDidSpecifyName = true } .multilineTextAlignment(.trailing) +#if os(visionOS) + .contentShape(.hoverEffect, .rect(cornerRadius: 12)) + .hoverEffect() +#else .foregroundStyle(.blue) +#endif } ColorPicker(String.colorLabel, selection: $viewModel.pendingTrace.color) Toggle(String.zoomToResult, isOn: $shouldZoomOnTraceCompletion) @@ -436,15 +440,18 @@ public struct UtilityNetworkTrace: View { ) { if let selectedTrace = viewModel.selectedTrace { ForEach(selectedTrace.assetGroupNames.sorted(), id: \.self) { assetGroupName in - HStack { - Text(assetGroupName) - Spacer() - Text(selectedTrace.elements(inAssetGroupNamed: assetGroupName).count, format: .number) - } - .foregroundStyle(.blue) - .contentShape(.rect) - .onTapGesture { + Button { currentActivity = .viewingTraces(.viewingElementGroup(named: assetGroupName)) + } label: { + HStack { + Text(assetGroupName) + Spacer() + Text(selectedTrace.elements(inAssetGroupNamed: assetGroupName).count, format: .number) + } +#if !os(visionOS) + .foregroundStyle(.blue) + .contentShape(.rect) +#endif } } } @@ -584,18 +591,23 @@ public struct UtilityNetworkTrace: View { viewModel.setTerminalConfigurationFor(startingPoint: selectedStartingPoint!, to: newValue) } ) { - ForEach(viewModel.pendingTrace.startingPoints.first { - $0 == selectedStartingPoint - }?.utilityElement?.assetType.terminalConfiguration?.terminals ?? [], id: \.self) { + ForEach( + viewModel.pendingTrace.startingPoints + .first(where: { $0 == selectedStartingPoint })? + .utilityElement?.assetType.terminalConfiguration?.terminals ?? [], + id: \.self + ) { Text($0.name) } } +#if !os(visionOS) .foregroundStyle(.blue) +#endif } } Section(String.attributesSectionTitle) { ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in - HStack{ + HStack { Text(item.key) Spacer() Text(item.value as? String ?? "") @@ -802,16 +814,10 @@ public struct UtilityNetworkTrace: View { /// - Parameter title: The button's title. /// - Parameter action: The action to be performed. /// - Returns: The configured button. - private func makeBackButton(title: String, _ action: @escaping () -> Void) -> some View { - Button { action() } label: { - Label { - Text(title) - } icon: { - Image(systemName: "chevron.backward") - } - } - .padding() - .frame(maxWidth: .infinity, alignment: .leading) + private func makeBackButton(title: String, action: @escaping () -> Void) -> some View { + Button(title, systemImage: "chevron.backward", action: action) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) } /// Returns a section header. @@ -890,11 +896,11 @@ private extension String { ) } - static var deleteAllStartingPointsMessage: Self { + static var deleteAllStartingPointsQuestion: Self { .init( - localized: "All starting points will be deleted.", + localized: "Delete all starting points?", bundle: .toolkitModule, - comment: "A message describing the outcome of clearing all starting points." + comment: "The title of the dialogue confirming deletion of all points." ) } @@ -1064,8 +1070,6 @@ private extension String { } } - -@available(visionOS, unavailable) public extension UtilityNetworkTrace /* Deprecated */ { /// A graphical interface to run pre-configured traces on a map's utility networks. /// - Parameters: diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift index 676b41642f..a9cc95d1fb 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift @@ -16,7 +16,6 @@ import ArcGIS import UIKit /// A starting point of a utility network trace. -@available(visionOS, unavailable) public struct UtilityNetworkTraceStartingPoint: Sendable { /// The geo element to be used as a starting point. var geoElement: GeoElement @@ -50,7 +49,6 @@ public struct UtilityNetworkTraceStartingPoint: Sendable { } } -@available(visionOS, unavailable) extension UtilityNetworkTraceStartingPoint: Hashable { public static func == (lhs: UtilityNetworkTraceStartingPoint, rhs: UtilityNetworkTraceStartingPoint) -> Bool { lhs.id == rhs.id diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel+Trace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel+Trace.swift index 59d7f3b03c..7c8e672e47 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel+Trace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel+Trace.swift @@ -15,7 +15,6 @@ import ArcGIS import SwiftUI -@available(visionOS, unavailable) extension UtilityNetworkTraceViewModel { /// A trace performed on a utility network. struct Trace { @@ -74,7 +73,6 @@ extension UtilityNetworkTraceViewModel { } } -@available(visionOS, unavailable) extension UtilityNetworkTraceViewModel.Trace { /// Finds the set of utility elements returned by the trace that belong to the provided /// asset group, grouped by type. @@ -143,7 +141,6 @@ extension UtilityNetworkTraceViewModel.Trace { } } -@available(visionOS, unavailable) extension UtilityNetworkTraceViewModel.Trace: Equatable { static func == (lhs: Self, rhs: Self) -> Bool { return lhs.id == rhs.id diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 967b39bd54..fff9351e67 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -17,7 +17,6 @@ import Combine import Foundation import SwiftUI -@available(visionOS, unavailable) @MainActor final class UtilityNetworkTraceViewModel: ObservableObject { // MARK: Published Properties @@ -68,15 +67,13 @@ import SwiftUI } /// The map's utility networks. - var networks: [UtilityNetwork] { - return map.utilityNetworks - } + var networks: [UtilityNetwork] { map.utilityNetworks } /// The overlay on which trace graphics will be drawn. - private var graphicsOverlay: GraphicsOverlay + private let graphicsOverlay: GraphicsOverlay /// A map containing one or more utility networks. - private var map: Map + private let map: Map /// Starting points programmatically provided to the trace tool. var externalStartingPoints = [UtilityNetworkTraceStartingPoint]() { @@ -507,7 +504,6 @@ import SwiftUI } } -@available(visionOS, unavailable) extension UtilityNetworkTraceViewModel { /// Finds the location on a polyline nearest the point. /// - Parameters: diff --git a/Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings b/Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings index 473a99768c..dca52c22b8 100644 --- a/Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings +++ b/Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings @@ -17,9 +17,6 @@ content on a remote host. The variable is the host that prompted the challenge. /* A reference to all of the sites defined in a floor aware map. */ "All Sites" = "All Sites"; -/* A message describing the outcome of clearing all starting points. */ -"All starting points will be deleted." = "All starting points will be deleted."; - /* A message describing the outcome of clearing all utility network trace results. */ "All the trace inputs and results will be lost." = "All the trace inputs and results will be lost."; @@ -111,6 +108,9 @@ east, etc.). */ /* A label for a button used to delete all starting points on a pending utility network trace. */ "Delete All Starting Points" = "Delete All Starting Points"; +/* The title of the dialogue confirming deletion of all points. */ +"Delete all starting points?" = "Delete all starting points?"; + /* E.g. Close/hide a sheet or pop-up. */ "Dismiss" = "Dismiss"; diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 7d50ed0e26..60cfc3c558 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if !os(visionOS) import ArcGIS @testable import ArcGISToolkit import XCTest @@ -309,4 +308,3 @@ private extension PortalItem { private extension URL { static let sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/portal/sharing/rest")! } -#endif