From 4623a029a41efcddca7bc8874d7ef7202264e8c3 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 30 Nov 2022 15:15:19 -0800 Subject: [PATCH 001/244] use changes authentication APIs from SDK --- .../AuthenticationApp.swift | 8 ++-- .../AuthenticationExample/SignInView.swift | 38 +++---------------- .../UtilityNetworkTraceExampleView.swift | 6 +-- .../Authentication/Authenticator.swift | 23 ++++++----- .../TokenChallengeContinuation.swift | 6 +-- .../AuthenticatorTests.swift | 24 ++++++------ .../XCTest/XCTestCase+ArcGISURLSession.swift | 6 +-- .../UtilityNetworkTraceViewModelTests.swift | 10 ++--- 8 files changed, 46 insertions(+), 75 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index bec2b8367..3c374b880 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -24,10 +24,10 @@ struct AuthenticationApp: App { // Create an authenticator. authenticator = Authenticator( // If you want to use OAuth, uncomment this code: - //oAuthConfigurations: [.arcgisDotCom] + //oAuthUserConfigurations: [.arcgisDotCom] ) // Set the challenge handler to be the authenticator we just created. - ArcGISEnvironment.authenticationChallengeHandler = authenticator + ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = authenticator } var body: some SwiftUI.Scene { @@ -62,8 +62,8 @@ struct AuthenticationApp: App { } // If you want to use OAuth, you can uncomment this code: -//private extension OAuthConfiguration { -// static let arcgisDotCom = OAuthConfiguration( +//private extension OAuthUserConfiguration { +// static let arcgisDotCom = OAuthUserConfiguration( // portalURL: .portal, // clientID: "<#Your client ID goes here#>", // // Note: You must have the same redirect URL used here diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index 4393c1eb2..90af4db85 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -61,10 +61,10 @@ struct SignInView: View { return } - if let arcGISCredential = await ArcGISEnvironment.credentialStore.credential(for: .portal) { - lastSignedInUser = arcGISCredential.username ?? "" + if let arcGISCredential = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credential(for: .portal) { + lastSignedInUser = arcGISCredential.username } else { - let networkCredentials = await ArcGISEnvironment.networkCredentialStore.credentials(forHost: URL.portal.host!) + let networkCredentials = await ArcGISEnvironment.authenticationManager.networkCredentialStore.credentials(forHost: URL.portal.host!) if !networkCredentials.isEmpty { lastSignedInUser = networkCredentials.compactMap { credential in switch credential { @@ -128,41 +128,13 @@ struct SignInView: View { } } -private extension ArcGISCredential { - /// The username, if any, associated with this credential. - var username: String? { - get { - switch self { - case .oauth(let credential): - return credential.username - case .token(let credential): - return credential.username - case .staticToken: - return nil - } - } - } -} - private extension Error { /// Returns a Boolean value indicating whether the error is the result of cancelling an /// authentication challenge. var isChallengeCancellationError: Bool { switch self { - case let error as ArcGISAuthenticationChallenge.Error: - switch error { - case .userCancelled: - return true - default: - return false - } - case let error as OAuthCredential.AuthorizationError: - switch error { - case .userCancelled: - return true - default: - return false - } + case is CancellationError: + return true case let error as NSError: return error.domain == NSURLErrorDomain && error.code == -999 default: diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 87b7b0c66..dce548010 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -55,7 +55,7 @@ struct UtilityNetworkTraceExampleView: View { viewpoint = $0 } .task { - await ArcGISEnvironment.credentialStore.add(try! await .publicSample) + ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(try! await .publicSample) } .floatingPanel( backgroundColor: Color(uiColor: .systemGroupedBackground), @@ -89,8 +89,8 @@ struct UtilityNetworkTraceExampleView: View { private extension ArcGISCredential { static var publicSample: ArcGISCredential { get async throws { - try await .token( - url: URL(string: "https://sampleserver7.arcgisonline.com/portal/sharing/rest")!, + try await TokenCredential.credential( + for: URL(string: "https://sampleserver7.arcgisonline.com/portal/sharing/rest")!, username: "viewer01", password: "I68VGU^nMurF" ) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 624ddb2b1..1f03cd274 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -19,7 +19,7 @@ import Combine @MainActor public final class Authenticator: ObservableObject { /// The OAuth configurations that this authenticator can work with. - let oAuthConfigurations: [OAuthConfiguration] + let oAuthUserConfigurations: [OAuthUserConfiguration] /// A value indicating whether we should prompt the user when encountering an untrusted host. var promptForUntrustedHosts: Bool @@ -31,10 +31,10 @@ public final class Authenticator: ObservableObject { /// - oAuthConfigurations: The OAuth configurations that this authenticator can work with. public init( promptForUntrustedHosts: Bool = false, - oAuthConfigurations: [OAuthConfiguration] = [] + oAuthUserConfigurations: [OAuthUserConfiguration] = [] ) { self.promptForUntrustedHosts = promptForUntrustedHosts - self.oAuthConfigurations = oAuthConfigurations + self.oAuthUserConfigurations = oAuthUserConfigurations } /// Sets up new credential stores that will be persisted to the keychain. @@ -48,23 +48,23 @@ public final class Authenticator: ObservableObject { access: ArcGIS.KeychainAccess, synchronizesWithiCloud: Bool = false ) async throws { - let previousArcGISCredentialStore = ArcGISEnvironment.credentialStore + let previousArcGISCredentialStore = ArcGISEnvironment.authenticationManager.arcGISCredentialStore // Set a persistent ArcGIS credential store on the ArcGIS environment. - ArcGISEnvironment.credentialStore = try await .makePersistent( + ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent( access: access, synchronizesWithiCloud: synchronizesWithiCloud ) do { // Set a persistent network credential store on the ArcGIS environment. - await ArcGISEnvironment.setNetworkCredentialStore( + await ArcGISEnvironment.authenticationManager.setNetworkCredentialStore( try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud) ) } catch { // If making the shared network credential store persistent fails, // then restore the ArcGIS credential store. - ArcGISEnvironment.credentialStore = previousArcGISCredentialStore + ArcGISEnvironment.authenticationManager.arcGISCredentialStore = previousArcGISCredentialStore throw error } } @@ -74,10 +74,10 @@ public final class Authenticator: ObservableObject { /// right away. public func clearCredentialStores() async { // Clear ArcGIS Credentials. - await ArcGISEnvironment.credentialStore.removeAll() + ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll() // Clear network credentials. - await ArcGISEnvironment.networkCredentialStore.removeAll() + await ArcGISEnvironment.authenticationManager.networkCredentialStore.removeAll() } /// The current challenge. @@ -93,9 +93,8 @@ extension Authenticator: AuthenticationChallengeHandler { await Task.yield() // Create the correct challenge type. - if let url = challenge.request.url, - let configuration = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { - return .useCredential(try await ArcGISCredential.oauth(configuration: configuration)) + if let configuration = oAuthUserConfigurations.first(where: { $0.canBeUsed(for: challenge.requestURL) }) { + return .useCredential(try await OAuthUserCredential.credential(for: configuration)) } else { let tokenChallengeContinuation = TokenChallengeContinuation(arcGISChallenge: challenge) diff --git a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift index d6340ab8a..06d1b80a3 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift @@ -38,9 +38,9 @@ final class TokenChallengeContinuation: ValueContinuation Date: Fri, 2 Dec 2022 11:12:17 -0600 Subject: [PATCH 002/244] Remove Bookmark conformance to Hashable, as it's in the SDK now. --- .../ArcGISToolkit/Extensions/Bookmark.swift | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/Bookmark.swift diff --git a/Sources/ArcGISToolkit/Extensions/Bookmark.swift b/Sources/ArcGISToolkit/Extensions/Bookmark.swift deleted file mode 100644 index 5530bd8aa..000000000 --- a/Sources/ArcGISToolkit/Extensions/Bookmark.swift +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2022 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 -// http://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 - -extension Bookmark: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(name) - hasher.combine(viewpoint) - } -} From 0846c53209f6bf95336638ba51fc4258e8bfce76 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Dec 2022 11:07:22 -0600 Subject: [PATCH 003/244] Build fixes for @ObservableObject changes to Map/Scene --- .../Examples/BasemapGalleryExampleView.swift | 10 ++++--- Examples/Examples/BookmarksExampleView.swift | 10 ++++--- Examples/Examples/CompassExampleView.swift | 8 +++--- .../Examples/FloatingPanelExampleView.swift | 7 +++-- .../Examples/FloorFilterExampleView.swift | 11 +++++--- .../Examples/OverviewMapExampleView.swift | 14 +++++++--- Examples/Examples/PopupExampleView.swift | 8 +++--- Examples/Examples/ScalebarExampleView.swift | 8 +++--- Examples/Examples/SearchExampleView.swift | 7 +++-- .../UtilityNetworkTraceExampleView.swift | 10 ++++--- .../Components/OverviewMap.swift | 14 +++++----- .../UtilityNetworkTraceViewModel.swift | 2 +- .../ArcGISToolkit/Utility/MapDataModel.swift | 27 +++++++++++++++++++ .../Utility/SceneDataModel.swift | 27 +++++++++++++++++++ 14 files changed, 123 insertions(+), 40 deletions(-) create mode 100644 Sources/ArcGISToolkit/Utility/MapDataModel.swift create mode 100644 Sources/ArcGISToolkit/Utility/SceneDataModel.swift diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 243b17e0b..5fbadc109 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -16,8 +16,10 @@ import ArcGIS import ArcGISToolkit struct BasemapGalleryExampleView: View { - /// The map displayed in the map view. - @StateObject private var map = Map(basemapStyle: .arcGISImagery) + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: Map(basemapStyle: .arcGISImagery) + ) /// A Boolean value indicating whether to show the basemap gallery. @State private var showBasemapGallery = false @@ -32,10 +34,10 @@ struct BasemapGalleryExampleView: View { private let basemaps = initialBasemaps() var body: some View { - MapView(map: map, viewpoint: initialViewpoint) + MapView(map: dataModel.map, viewpoint: initialViewpoint) .overlay(alignment: .topTrailing) { if showBasemapGallery { - BasemapGallery(items: basemaps, geoModel: map) + BasemapGallery(items: basemaps, geoModel: dataModel.map) .style(.automatic()) .padding() } diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 5bc7332a2..25170e185 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -16,8 +16,10 @@ import ArcGISToolkit import SwiftUI struct BookmarksExampleView: View { - /// A web map with predefined bookmarks. - @StateObject private var map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + /// The data model containing a `Map` with predefined bookmarks. + @StateObject private var dataModel = MapDataModel( + map: Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + ) /// Indicates if the `Bookmarks` component is shown or not. /// - Remark: This allows a developer to control when the `Bookmarks` component is @@ -29,7 +31,7 @@ struct BookmarksExampleView: View { @State var viewpoint: Viewpoint? var body: some View { - MapView(map: map, viewpoint: viewpoint) + MapView(map: dataModel.map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } @@ -50,7 +52,7 @@ struct BookmarksExampleView: View { // bookmark selection. Bookmarks( isPresented: $showingBookmarks, - mapOrScene: map, + mapOrScene: dataModel.map, viewpoint: $viewpoint ) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index de395df1c..137cf8808 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -16,8 +16,10 @@ import ArcGISToolkit import SwiftUI struct CompassExampleView: View { - /// The map displayed in the map view. - @StateObject private var map = Map(basemapStyle: .arcGISImagery) + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: Map(basemapStyle: .arcGISImagery) + ) /// Allows for communication between the Compass and MapView or SceneView. @State private var viewpoint: Viewpoint? = Viewpoint( @@ -27,7 +29,7 @@ struct CompassExampleView: View { ) var body: some View { - MapView(map: map, viewpoint: viewpoint) + MapView(map: dataModel.map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { Compass(viewpoint: $viewpoint) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 8105645e6..cef65cca7 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -16,7 +16,10 @@ import ArcGISToolkit import ArcGIS struct FloatingPanelExampleView: View { - @StateObject private var map = Map(basemapStyle: .arcGISImagery) + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: Map(basemapStyle: .arcGISImagery) + ) @State var selectedDetent: FloatingPanelDetent = .half @@ -27,7 +30,7 @@ struct FloatingPanelExampleView: View { var body: some View { MapView( - map: map, + map: dataModel.map, viewpoint: initialViewpoint ) .floatingPanel(selectedDetent: $selectedDetent, isPresented: .constant(true)) { diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 99c512ab0..ee30c5c9e 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -45,11 +45,14 @@ struct FloorFilterExampleView: View { scale: 100_000 ) - @StateObject private var map = makeMap() + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: makeMap() + ) var body: some View { MapView( - map: map, + map: dataModel.map, viewpoint: viewpoint ) .onNavigatingChanged { @@ -62,7 +65,7 @@ struct FloorFilterExampleView: View { .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: floorFilterAlignment) { if isMapLoaded, - let floorManager = map.floorManager { + let floorManager = dataModel.map.floorManager { FloorFilter( floorManager: floorManager, alignment: floorFilterAlignment, @@ -89,7 +92,7 @@ struct FloorFilterExampleView: View { } .task { do { - try await map.load() + try await dataModel.map.load() isMapLoaded = true } catch { mapLoadError = true diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 6c0844c91..65a41758f 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -47,14 +47,17 @@ struct OverviewMapExampleView: View { } struct OverviewMapForMapView: View { - @StateObject private var map = Map(basemapStyle: .arcGISImagery) + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: Map(basemapStyle: .arcGISImagery) + ) @State private var viewpoint: Viewpoint? @State private var visibleArea: ArcGIS.Polygon? var body: some View { - MapView(map: map) + MapView(map: dataModel.map) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( @@ -75,12 +78,15 @@ struct OverviewMapForMapView: View { } struct OverviewMapForSceneView: View { - @StateObject private var scene = Scene(basemapStyle: .arcGISImagery) + /// The data model containing the `Scene` displayed in the `SceneView`. + @StateObject private var dataModel = SceneDataModel( + scene: Scene(basemapStyle: .arcGISImagery) + ) @State private var viewpoint: Viewpoint? var body: some View { - SceneView(scene: scene) + SceneView(scene: dataModel.scene) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay( OverviewMap.forSceneView(with: viewpoint) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index a871b094d..648fdfa36 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -24,8 +24,10 @@ struct PopupExampleView: View { return Map(item: portalItem) } - /// The map displayed in the map view. - @StateObject private var map = makeMap() + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: makeMap() + ) /// The point on the screen the user tapped on to identify a feature. @State private var identifyScreenPoint: CGPoint? @@ -42,7 +44,7 @@ struct PopupExampleView: View { var body: some View { MapViewReader { proxy in VStack { - MapView(map: map) + MapView(map: dataModel.map) .onSingleTapGesture { screenPoint, _ in identifyScreenPoint = screenPoint } diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 67dd87a86..4868b9f7c 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -28,14 +28,16 @@ struct ScalebarExampleView: View { /// The location of the scalebar on screen. private let alignment: Alignment = .bottomLeading - /// The `Map` displayed in the `MapView`. - @StateObject private var map = Map(basemapStyle: .arcGISTopographic) + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: Map(basemapStyle: .arcGISTopographic) + ) /// The maximum screen width allotted to the scalebar. private let maxWidth: Double = 175.0 var body: some View { - MapView(map: map) + MapView(map: dataModel.map) .onSpatialReferenceChanged { spatialReference = $0 } .onUnitsPerPointChanged { unitsPerPoint = $0 } .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index afe2dfe6b..b574f4542 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -23,7 +23,10 @@ struct SearchExampleView: View { maximumSuggestions: 16 ) - @StateObject private var map = Map(basemapStyle: .arcGISImagery) + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: Map(basemapStyle: .arcGISImagery) + ) /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. private let searchResultsOverlay = GraphicsOverlay() @@ -49,7 +52,7 @@ struct SearchExampleView: View { var body: some View { MapView( - map: map, + map: dataModel.map, viewpoint: searchResultViewpoint, graphicsOverlays: [searchResultsOverlay] ) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 403d67aff..a4f1a5255 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -18,8 +18,10 @@ import SwiftUI /// A demonstration of the utility network trace tool which runs traces on a web map published with a utility /// network and trace configurations. struct UtilityNetworkTraceExampleView: View { - /// The map containing the utility networks. - @StateObject private var map = makeMap() + /// The data model containing a `Map` with the utility networks. + @StateObject private var dataModel = MapDataModel( + map: makeMap() + ) /// The current detent of the floating panel presenting the trace tool. @State var activeDetent: FloatingPanelDetent = .half @@ -42,7 +44,7 @@ struct UtilityNetworkTraceExampleView: View { var body: some View { MapViewReader { mapViewProxy in MapView( - map: map, + map: dataModel.map, viewpoint: viewpoint, graphicsOverlays: [resultGraphicsOverlay] ) @@ -66,7 +68,7 @@ struct UtilityNetworkTraceExampleView: View { ) { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, - map: map, + map: dataModel.map, mapPoint: $mapPoint, viewPoint: $viewPoint, mapViewProxy: $mapViewProxy, diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 76fe3cae3..6194b98e2 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -28,7 +28,10 @@ public struct OverviewMap: View { private var scaleFactor = 25.0 - @StateObject private var map = Map(basemapStyle: .arcGISTopographic) + /// The data model containing the `Map` displayed in the overview map. + @StateObject private var dataModel = MapDataModel( + map: Map(basemapStyle: .arcGISTopographic) + ) /// The `Graphic` displaying the visible area of the main `GeoView`. @StateObject private var graphic: Graphic @@ -83,11 +86,11 @@ public struct OverviewMap: View { public var body: some View { MapView( - map: map, + map: dataModel.map, viewpoint: makeOverviewViewpoint(), graphicsOverlays: [graphicsOverlay] ) - .attributionText(hidden: true) + .attributionBarHidden(true) .interactionModes([]) .border( .black, @@ -131,9 +134,8 @@ public struct OverviewMap: View { /// - Parameter map: The new map. /// - Returns: The `OverviewMap`. public func map(_ map: Map) -> OverviewMap { - var copy = self - copy._map = StateObject(wrappedValue: map) - return copy + self.dataModel.map = map + return self } /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 4f5e36ff2..b58a5bb97 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -347,7 +347,7 @@ import SwiftUI guard let configuration = pendingTrace.configuration, let network = network else { return false } - let minStartingPoints = configuration.minimumStartingLocations.rawValue + let minStartingPoints = configuration.minimumStartingLocations == .one ? 1 : 2 guard pendingTrace.startingPoints.count >= minStartingPoints else { userAlert = .init(description: "Please set at least \(minStartingPoints) starting location\(minStartingPoints > 1 ? "s" : "").") diff --git a/Sources/ArcGISToolkit/Utility/MapDataModel.swift b/Sources/ArcGISToolkit/Utility/MapDataModel.swift new file mode 100644 index 000000000..65d07b660 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/MapDataModel.swift @@ -0,0 +1,27 @@ +// Copyright 2022 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 +// http://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 SwiftUI +import ArcGIS + +/// A data model class containing a Map. +public class MapDataModel: ObservableObject { + /// The `Map` used for display in a `MapView`. + public var map: Map + + /// Creates a `MapDataModel`. + /// - Parameter map: The `Map` used for display. + public init(map: Map) { + self.map = map + } +} diff --git a/Sources/ArcGISToolkit/Utility/SceneDataModel.swift b/Sources/ArcGISToolkit/Utility/SceneDataModel.swift new file mode 100644 index 000000000..8a8982b3f --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/SceneDataModel.swift @@ -0,0 +1,27 @@ +// Copyright 2022 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 +// http://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 SwiftUI +import ArcGIS + +/// A data model class containing a Scene. +public class SceneDataModel: ObservableObject { + /// The `Scene` used for display in a `SceneView`. + public var scene: ArcGIS.Scene + + /// Creates a `SceneDataModel`. + /// - Parameter scene: The `Scene` used for display. + public init(scene: ArcGIS.Scene) { + self.scene = scene + } +} From 3a0e373d5fbd1c754b1c14579f9fada59b6214f4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Dec 2022 12:00:49 -0600 Subject: [PATCH 004/244] map and scene in the data models should be `@Published`. --- Sources/ArcGISToolkit/Utility/MapDataModel.swift | 2 +- Sources/ArcGISToolkit/Utility/SceneDataModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/MapDataModel.swift b/Sources/ArcGISToolkit/Utility/MapDataModel.swift index 65d07b660..8929fbfba 100644 --- a/Sources/ArcGISToolkit/Utility/MapDataModel.swift +++ b/Sources/ArcGISToolkit/Utility/MapDataModel.swift @@ -17,7 +17,7 @@ import ArcGIS /// A data model class containing a Map. public class MapDataModel: ObservableObject { /// The `Map` used for display in a `MapView`. - public var map: Map + @Published public var map: Map /// Creates a `MapDataModel`. /// - Parameter map: The `Map` used for display. diff --git a/Sources/ArcGISToolkit/Utility/SceneDataModel.swift b/Sources/ArcGISToolkit/Utility/SceneDataModel.swift index 8a8982b3f..3e366cf36 100644 --- a/Sources/ArcGISToolkit/Utility/SceneDataModel.swift +++ b/Sources/ArcGISToolkit/Utility/SceneDataModel.swift @@ -17,7 +17,7 @@ import ArcGIS /// A data model class containing a Scene. public class SceneDataModel: ObservableObject { /// The `Scene` used for display in a `SceneView`. - public var scene: ArcGIS.Scene + @Published public var scene: ArcGIS.Scene /// Creates a `SceneDataModel`. /// - Parameter scene: The `Scene` used for display. From 4420a4b88dc81506526195296517939f95954238 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Dec 2022 12:22:46 -0600 Subject: [PATCH 005/244] update doc for models. --- Sources/ArcGISToolkit/Utility/MapDataModel.swift | 8 +++++++- Sources/ArcGISToolkit/Utility/SceneDataModel.swift | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/MapDataModel.swift b/Sources/ArcGISToolkit/Utility/MapDataModel.swift index 8929fbfba..27239742f 100644 --- a/Sources/ArcGISToolkit/Utility/MapDataModel.swift +++ b/Sources/ArcGISToolkit/Utility/MapDataModel.swift @@ -14,7 +14,13 @@ import SwiftUI import ArcGIS -/// A data model class containing a Map. +/// A very basic data model class containing a Map. Since a `Map` is not an observable object, +/// clients can use `MapDataModel` as an example of how you would store a map in a data model +/// class. The class inherits from `ObservableObject` and the `Map` is defined as an @Published +/// property. This allows SwiftUI views to be updated automatically when a new map is set on the model. +/// Being stored in the model also prevents the map from continually being created during redraws. +/// The data model class would be expanded upon in client code to contain other properties required +/// for the model. public class MapDataModel: ObservableObject { /// The `Map` used for display in a `MapView`. @Published public var map: Map diff --git a/Sources/ArcGISToolkit/Utility/SceneDataModel.swift b/Sources/ArcGISToolkit/Utility/SceneDataModel.swift index 3e366cf36..5ae999bae 100644 --- a/Sources/ArcGISToolkit/Utility/SceneDataModel.swift +++ b/Sources/ArcGISToolkit/Utility/SceneDataModel.swift @@ -14,7 +14,13 @@ import SwiftUI import ArcGIS -/// A data model class containing a Scene. +/// A very basic data model class containing a Scene. Since a `Scene` is not an observable object, +/// clients can use `SceneDataModel` as an example of how you would store a scene in a data model +/// class. The class inherits from `ObservableObject` and the `Scene` is defined as an @Published +/// property. This allows SwiftUI views to be updated automatically when a new scene is set on the model. +/// Being stored in the model also prevents the scene from continually being created during redraws. +/// The data model class would be expanded upon in client code to contain other properties required +/// for the model. public class SceneDataModel: ObservableObject { /// The `Scene` used for display in a `SceneView`. @Published public var scene: ArcGIS.Scene From 784fc74106ec470ba23ea54f683ba61664799f0b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Dec 2022 13:52:08 -0600 Subject: [PATCH 006/244] Move the data models to the Examples app; add data model to the one component that needed it. --- .../Utility/MapDataModel.swift | 0 .../Utility/SceneDataModel.swift | 0 Sources/ArcGISToolkit/Components/OverviewMap.swift | 12 ++++++++++++ 3 files changed, 12 insertions(+) rename {Sources/ArcGISToolkit => Examples}/Utility/MapDataModel.swift (100%) rename {Sources/ArcGISToolkit => Examples}/Utility/SceneDataModel.swift (100%) diff --git a/Sources/ArcGISToolkit/Utility/MapDataModel.swift b/Examples/Utility/MapDataModel.swift similarity index 100% rename from Sources/ArcGISToolkit/Utility/MapDataModel.swift rename to Examples/Utility/MapDataModel.swift diff --git a/Sources/ArcGISToolkit/Utility/SceneDataModel.swift b/Examples/Utility/SceneDataModel.swift similarity index 100% rename from Sources/ArcGISToolkit/Utility/SceneDataModel.swift rename to Examples/Utility/SceneDataModel.swift diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 6194b98e2..eaafc83a0 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -185,3 +185,15 @@ private extension Symbol { ) ) } + +/// A very basic data model class containing a Map. +public class MapDataModel: ObservableObject { + /// The `Map` used for display in a `MapView`. + @Published public var map: Map + + /// Creates a `MapDataModel`. + /// - Parameter map: The `Map` used for display. + public init(map: Map) { + self.map = map + } +} From cd1f7809a2b7e8a5c7fa30d9da98e7182a91133c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Dec 2022 14:21:42 -0600 Subject: [PATCH 007/244] Apply suggestions from code review Co-authored-by: David Feinzimer --- Examples/Utility/MapDataModel.swift | 2 +- Examples/Utility/SceneDataModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Utility/MapDataModel.swift b/Examples/Utility/MapDataModel.swift index 27239742f..49cf71443 100644 --- a/Examples/Utility/MapDataModel.swift +++ b/Examples/Utility/MapDataModel.swift @@ -16,7 +16,7 @@ import ArcGIS /// A very basic data model class containing a Map. Since a `Map` is not an observable object, /// clients can use `MapDataModel` as an example of how you would store a map in a data model -/// class. The class inherits from `ObservableObject` and the `Map` is defined as an @Published +/// class. The class inherits from `ObservableObject` and the `Map` is defined as a @Published /// property. This allows SwiftUI views to be updated automatically when a new map is set on the model. /// Being stored in the model also prevents the map from continually being created during redraws. /// The data model class would be expanded upon in client code to contain other properties required diff --git a/Examples/Utility/SceneDataModel.swift b/Examples/Utility/SceneDataModel.swift index 5ae999bae..0d2a57fb6 100644 --- a/Examples/Utility/SceneDataModel.swift +++ b/Examples/Utility/SceneDataModel.swift @@ -16,7 +16,7 @@ import ArcGIS /// A very basic data model class containing a Scene. Since a `Scene` is not an observable object, /// clients can use `SceneDataModel` as an example of how you would store a scene in a data model -/// class. The class inherits from `ObservableObject` and the `Scene` is defined as an @Published +/// class. The class inherits from `ObservableObject` and the `Scene` is defined as a @Published /// property. This allows SwiftUI views to be updated automatically when a new scene is set on the model. /// Being stored in the model also prevents the scene from continually being created during redraws. /// The data model class would be expanded upon in client code to contain other properties required From 49494b7f6d28c3427e2798327219cfe24cf75ba7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Dec 2022 14:29:15 -0600 Subject: [PATCH 008/244] Make the Example models internal and the OverviewMap model private. --- Examples/Utility/MapDataModel.swift | 6 +++--- Examples/Utility/SceneDataModel.swift | 6 +++--- Sources/ArcGISToolkit/Components/OverviewMap.swift | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Examples/Utility/MapDataModel.swift b/Examples/Utility/MapDataModel.swift index 49cf71443..0c3553ec1 100644 --- a/Examples/Utility/MapDataModel.swift +++ b/Examples/Utility/MapDataModel.swift @@ -21,13 +21,13 @@ import ArcGIS /// Being stored in the model also prevents the map from continually being created during redraws. /// The data model class would be expanded upon in client code to contain other properties required /// for the model. -public class MapDataModel: ObservableObject { +class MapDataModel: ObservableObject { /// The `Map` used for display in a `MapView`. - @Published public var map: Map + @Published var map: Map /// Creates a `MapDataModel`. /// - Parameter map: The `Map` used for display. - public init(map: Map) { + init(map: Map) { self.map = map } } diff --git a/Examples/Utility/SceneDataModel.swift b/Examples/Utility/SceneDataModel.swift index 0d2a57fb6..e55403ad6 100644 --- a/Examples/Utility/SceneDataModel.swift +++ b/Examples/Utility/SceneDataModel.swift @@ -21,13 +21,13 @@ import ArcGIS /// Being stored in the model also prevents the scene from continually being created during redraws. /// The data model class would be expanded upon in client code to contain other properties required /// for the model. -public class SceneDataModel: ObservableObject { +class SceneDataModel: ObservableObject { /// The `Scene` used for display in a `SceneView`. - @Published public var scene: ArcGIS.Scene + @Published var scene: ArcGIS.Scene /// Creates a `SceneDataModel`. /// - Parameter scene: The `Scene` used for display. - public init(scene: ArcGIS.Scene) { + init(scene: ArcGIS.Scene) { self.scene = scene } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index eaafc83a0..5d07b381b 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -187,13 +187,13 @@ private extension Symbol { } /// A very basic data model class containing a Map. -public class MapDataModel: ObservableObject { +private class MapDataModel: ObservableObject { /// The `Map` used for display in a `MapView`. - @Published public var map: Map + @Published var map: Map /// Creates a `MapDataModel`. /// - Parameter map: The `Map` used for display. - public init(map: Map) { + init(map: Map) { self.map = map } } From 45510fd21fece9eb0844e3ac8cd8735bd5bde0bb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 16 Dec 2022 14:22:53 -0800 Subject: [PATCH 009/244] Apply fix --- Examples/Examples.xcodeproj/project.pbxproj | 16 ++++++++++++++++ .../Models}/MapDataModel.swift | 0 .../Models}/SceneDataModel.swift | 0 3 files changed, 16 insertions(+) rename Examples/{Utility => ExamplesApp/Models}/MapDataModel.swift (100%) rename Examples/{Utility => ExamplesApp/Models}/SceneDataModel.swift (100%) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index f01552aa4..f8810b4ad 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 4D19FCB52881C8F3002601E8 /* PopupExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D19FCB42881C8F3002601E8 /* PopupExampleView.swift */; }; 75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */; }; + 752A4FC4294D268000A49479 /* MapDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A4FC2294D268000A49479 /* MapDataModel.swift */; }; + 752A4FC5294D268000A49479 /* SceneDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A4FC3294D268000A49479 /* SceneDataModel.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 */; }; @@ -44,6 +46,8 @@ /* Begin PBXFileReference section */ 4D19FCB42881C8F3002601E8 /* PopupExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopupExampleView.swift; sourceTree = ""; }; 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityNetworkTraceExampleView.swift; sourceTree = ""; }; + 752A4FC2294D268000A49479 /* MapDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDataModel.swift; sourceTree = ""; }; + 752A4FC3294D268000A49479 /* SceneDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDataModel.swift; sourceTree = ""; }; 75657E4727ABAC8400EE865B /* CompassExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExampleView.swift; sourceTree = ""; }; 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksExampleView.swift; sourceTree = ""; }; 75D41B2A27C6F21400624D7C /* ScalebarExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalebarExampleView.swift; sourceTree = ""; }; @@ -78,6 +82,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 752A4FC7294D26DC00A49479 /* Models */ = { + isa = PBXGroup; + children = ( + 752A4FC2294D268000A49479 /* MapDataModel.swift */, + 752A4FC3294D268000A49479 /* SceneDataModel.swift */, + ); + path = Models; + sourceTree = ""; + }; E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( @@ -117,6 +130,7 @@ E47ABE422652FE0900FD2FE3 /* ExamplesApp */ = { isa = PBXGroup; children = ( + 752A4FC7294D26DC00A49479 /* Models */, E48A73412658227100F5C118 /* AnyExample.swift */, E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */, E48A73402658227100F5C118 /* Example.swift */, @@ -250,6 +264,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 752A4FC4294D268000A49479 /* MapDataModel.swift in Sources */, + 752A4FC5294D268000A49479 /* SceneDataModel.swift in Sources */, 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */, E48A73452658227100F5C118 /* Examples.swift in Sources */, 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */, diff --git a/Examples/Utility/MapDataModel.swift b/Examples/ExamplesApp/Models/MapDataModel.swift similarity index 100% rename from Examples/Utility/MapDataModel.swift rename to Examples/ExamplesApp/Models/MapDataModel.swift diff --git a/Examples/Utility/SceneDataModel.swift b/Examples/ExamplesApp/Models/SceneDataModel.swift similarity index 100% rename from Examples/Utility/SceneDataModel.swift rename to Examples/ExamplesApp/Models/SceneDataModel.swift From 09dccc973a1c1259bbb74b00644d76b2e9e43f6f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 16 Dec 2022 14:23:42 -0800 Subject: [PATCH 010/244] Rename --- Examples/Examples.xcodeproj/project.pbxproj | 6 +++--- Examples/ExamplesApp/{Models => Utility}/MapDataModel.swift | 0 .../ExamplesApp/{Models => Utility}/SceneDataModel.swift | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename Examples/ExamplesApp/{Models => Utility}/MapDataModel.swift (100%) rename Examples/ExamplesApp/{Models => Utility}/SceneDataModel.swift (100%) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index f8810b4ad..8b8291f8b 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -82,13 +82,13 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 752A4FC7294D26DC00A49479 /* Models */ = { + 752A4FC7294D26DC00A49479 /* Utility */ = { isa = PBXGroup; children = ( 752A4FC2294D268000A49479 /* MapDataModel.swift */, 752A4FC3294D268000A49479 /* SceneDataModel.swift */, ); - path = Models; + path = Utility; sourceTree = ""; }; E40F58042656E509006F5CB9 /* Examples */ = { @@ -130,7 +130,7 @@ E47ABE422652FE0900FD2FE3 /* ExamplesApp */ = { isa = PBXGroup; children = ( - 752A4FC7294D26DC00A49479 /* Models */, + 752A4FC7294D26DC00A49479 /* Utility */, E48A73412658227100F5C118 /* AnyExample.swift */, E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */, E48A73402658227100F5C118 /* Example.swift */, diff --git a/Examples/ExamplesApp/Models/MapDataModel.swift b/Examples/ExamplesApp/Utility/MapDataModel.swift similarity index 100% rename from Examples/ExamplesApp/Models/MapDataModel.swift rename to Examples/ExamplesApp/Utility/MapDataModel.swift diff --git a/Examples/ExamplesApp/Models/SceneDataModel.swift b/Examples/ExamplesApp/Utility/SceneDataModel.swift similarity index 100% rename from Examples/ExamplesApp/Models/SceneDataModel.swift rename to Examples/ExamplesApp/Utility/SceneDataModel.swift From e64fdec10e59829a70194903341ca1d96d897185 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 16 Dec 2022 14:27:04 -0800 Subject: [PATCH 011/244] Update OverviewMap.swift --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 5d07b381b..057add21b 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -186,8 +186,8 @@ private extension Symbol { ) } -/// A very basic data model class containing a Map. -private class MapDataModel: ObservableObject { +///// A very basic data model class containing a Map. +class MapDataModel: ObservableObject { /// The `Map` used for display in a `MapView`. @Published var map: Map From 1951ca8fbf3e7d4ff67bfec756f0b15188840e5d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 16 Dec 2022 14:30:25 -0800 Subject: [PATCH 012/244] Update OverviewMap.swift --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 057add21b..9a494096a 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -186,7 +186,7 @@ private extension Symbol { ) } -///// A very basic data model class containing a Map. +/// A very basic data model class containing a Map. class MapDataModel: ObservableObject { /// The `Map` used for display in a `MapView`. @Published var map: Map From 93cbd81b9510b5c42856c8daad97cc0ead96c6ad Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 16 Dec 2022 14:35:41 -0800 Subject: [PATCH 013/244] Move back to Mark's location --- Examples/Examples.xcodeproj/project.pbxproj | 2 +- Examples/{ExamplesApp => }/Utility/MapDataModel.swift | 0 Examples/{ExamplesApp => }/Utility/SceneDataModel.swift | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename Examples/{ExamplesApp => }/Utility/MapDataModel.swift (100%) rename Examples/{ExamplesApp => }/Utility/SceneDataModel.swift (100%) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 8b8291f8b..c2670b0c8 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -113,6 +113,7 @@ children = ( E47ABE622653005F00FD2FE3 /* arcgis-maps-sdk-swift-toolkit */, E47ABE562652FE7F00FD2FE3 /* Examples */, + 752A4FC7294D26DC00A49479 /* Utility */, E47ABE422652FE0900FD2FE3 /* ExamplesApp */, E47ABE412652FE0900FD2FE3 /* Products */, E47ABE5A2652FF1200FD2FE3 /* Frameworks */, @@ -130,7 +131,6 @@ E47ABE422652FE0900FD2FE3 /* ExamplesApp */ = { isa = PBXGroup; children = ( - 752A4FC7294D26DC00A49479 /* Utility */, E48A73412658227100F5C118 /* AnyExample.swift */, E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */, E48A73402658227100F5C118 /* Example.swift */, diff --git a/Examples/ExamplesApp/Utility/MapDataModel.swift b/Examples/Utility/MapDataModel.swift similarity index 100% rename from Examples/ExamplesApp/Utility/MapDataModel.swift rename to Examples/Utility/MapDataModel.swift diff --git a/Examples/ExamplesApp/Utility/SceneDataModel.swift b/Examples/Utility/SceneDataModel.swift similarity index 100% rename from Examples/ExamplesApp/Utility/SceneDataModel.swift rename to Examples/Utility/SceneDataModel.swift From bcc112be816d0cb69d0ffbcf0c5b7cc1957521a4 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Mon, 19 Dec 2022 12:12:39 -0800 Subject: [PATCH 014/244] Remove uses of `CustomStringConvertible.description` from `UtilityNetworkTrace.swift` --- .../UtilityNetworkTrace.swift | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 6d165d3fd..f43c331a6 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -138,10 +138,13 @@ public struct UtilityNetworkTrace: View { } makeDetailSectionHeader(title: assetGroupName) List { - ForEach(assetTypeGroups.keys.compactMap({$0}).sorted(), id: \.self) { assetTypeGroupName in - Section(assetTypeGroupName) { + ForEach( + assetTypeGroups.sorted(using: KeyPathComparator(\.key)), + id: \.key + ) { (name, elements) in + Section(name) { DisclosureGroup { - ForEach(assetTypeGroups[assetTypeGroupName] ?? [], id: \.globalID) { element in + ForEach(elements, id: \.globalID) { element in Button { Task { if let feature = await viewModel.feature(for: element), @@ -151,14 +154,14 @@ public struct UtilityNetworkTrace: View { } } label: { Label { - Text("Object ID \(element.objectID.description)") + Text("Object ID \(element.objectID, format: .number.grouping(.never))") } icon: { Image(systemName: "scope") } } } } label: { - Text(assetTypeGroups[assetTypeGroupName]?.count.description ?? "N/A") + Text(elements.count, format: .number) } } } @@ -343,16 +346,18 @@ public struct UtilityNetworkTrace: View { set: { currentActivity = .viewingTraces($0 ? .viewingFeatureResults : nil) } ) ) { - ForEach(viewModel.selectedTrace?.assetGroupNames.sorted() ?? [], id: \.self) { assetGroupName in - HStack { - Text(assetGroupName) - Spacer() - Text(viewModel.selectedTrace?.elementsInAssetGroup(named: assetGroupName).count.description ?? "N/A") - } - .foregroundColor(.blue) - .contentShape(Rectangle()) - .onTapGesture { - currentActivity = .viewingTraces(.viewingElementGroup(named: assetGroupName)) + if let selectedTrace = viewModel.selectedTrace { + ForEach(selectedTrace.assetGroupNames.sorted(), id: \.self) { assetGroupName in + HStack { + Text(assetGroupName) + Spacer() + Text(selectedTrace.elementsInAssetGroup(named: assetGroupName).count, format: .number) + } + .foregroundColor(.blue) + .contentShape(Rectangle()) + .onTapGesture { + currentActivity = .viewingTraces(.viewingElementGroup(named: assetGroupName)) + } } } } @@ -365,15 +370,17 @@ public struct UtilityNetworkTrace: View { set: { currentActivity = .viewingTraces($0 ? .viewingFunctionResults : nil) } ) ) { - ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.objectID) { item in - HStack { - Text(item.function.networkAttribute.name) - Spacer() - VStack(alignment: .trailing) { - Text(item.function.functionType.title) - .font(.caption) - .foregroundColor(.secondary) - Text((item.result as? Double)?.description ?? "N/A") + if let selectedTrace = viewModel.selectedTrace { + ForEach(selectedTrace.functionOutputs, id: \.objectID) { item in + HStack { + Text(item.function.networkAttribute.name) + Spacer() + VStack(alignment: .trailing) { + Text(item.function.functionType.title) + .font(.caption) + .foregroundColor(.secondary) + Text((item.result as? Double).map { "\($0)" } ?? "N/A") + } } } } @@ -635,9 +642,9 @@ public struct UtilityNetworkTrace: View { // MARK: Computed Properties /// Indicates the number of the trace currently being viewed out the total number of traces. - private var currentTraceLabel: String { + private var currentTraceLabel: LocalizedStringKey { guard let index = viewModel.selectedTraceIndex else { return "Error" } - return "Trace \(index+1) of \(viewModel.completedTraces.count.description)" + return "Trace \(index+1) of \(viewModel.completedTraces.count)" } /// The name of the selected utility element asset group. From 7a7a7203c5984152ce9babe414d298e4ab552ef6 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Mon, 19 Dec 2022 13:30:14 -0800 Subject: [PATCH 015/244] Remove use of deprecated modifier `View.navigationBarItems(leading:)` --- Examples/Examples/BasemapGalleryExampleView.swift | 10 +++++++--- .../FloorFilter/SiteAndFacilitySelector.swift | 14 ++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 5fbadc109..6115b5187 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -43,9 +43,13 @@ struct BasemapGalleryExampleView: View { } } .navigationTitle("Basemap Gallery") - .navigationBarItems(trailing: Toggle(isOn: $showBasemapGallery) { - Image("basemap", label: Text("Show base map")) - }) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Toggle(isOn: $showBasemapGallery) { + Image("basemap", label: Text("Show base map")) + } + } + } } private static func initialBasemaps() -> [BasemapGalleryItem] { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index ceec6b376..7db12ea8c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -132,13 +132,15 @@ struct SiteAndFacilitySelector: View { isHidden: isHidden ) .navigationBarBackButtonHidden(true) - .navigationBarItems( - leading: Button { - userBackedOutOfSelectedSite = true - } label: { - Image(systemName: "chevron.left") + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + userBackedOutOfSelectedSite = true + } label: { + Image(systemName: "chevron.left") + } } - ) + } } } .listStyle(.plain) From 97ee90a392db17d8d872ec4f2451452714de0ce2 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Mon, 19 Dec 2022 14:10:47 -0800 Subject: [PATCH 016/244] Removed orphaned doc The initializer does not take a parameter named `activeDetent`. --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index f43c331a6..3255d0109 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -531,7 +531,6 @@ public struct UtilityNetworkTrace: View { /// A graphical interface to run pre-configured traces on a map's utility networks. /// - Parameters: - /// - activeDetent: The current detent of the floating panel. /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace graphics. /// - map: The map containing the utility network(s). /// - mapPoint: Acts as the point at which newly selected starting point graphics will be created. From 540b3aa73db4265a4809e23845dc19615147c64f Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 20 Dec 2022 17:02:30 -0800 Subject: [PATCH 017/244] use renamed apis --- .../ArcGISToolkit/Components/Authentication/Authenticator.swift | 2 +- .../Authentication/CertificatePickerViewModifier.swift | 2 +- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 2 +- .../Components/Authentication/TokenChallengeContinuation.swift | 2 +- .../Components/Authentication/TrustHostViewModifier.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 1f03cd274..c34f73a59 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -94,7 +94,7 @@ extension Authenticator: AuthenticationChallengeHandler { // Create the correct challenge type. if let configuration = oAuthUserConfigurations.first(where: { $0.canBeUsed(for: challenge.requestURL) }) { - return .useCredential(try await OAuthUserCredential.credential(for: configuration)) + return .continueWithCredential(try await OAuthUserCredential.credential(for: configuration)) } else { let tokenChallengeContinuation = TokenChallengeContinuation(arcGISChallenge: challenge) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 00478596e..71feed99f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -73,7 +73,7 @@ import ArcGIS Task { do { - challenge.resume(with: .useCredential(try .certificate(at: certificateURL, password: password))) + challenge.resume(with: .continueWithCredential(try .certificate(at: certificateURL, password: password))) } catch { // This is required to prevent an "already presenting" error. try? await Task.sleep(nanoseconds: 100_000) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 1788c9ba8..198b17309 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -123,7 +123,7 @@ extension LoginViewModifier { challengingHost: challenge.host, onSignIn: { loginCredential in challenge.resume( - with: .useCredential( + with: .continueWithCredential( .password(username: loginCredential.username, password: loginCredential.password) ) ) diff --git a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift index 06d1b80a3..7467528e3 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift @@ -53,7 +53,7 @@ final class TokenChallengeContinuation: ValueContinuation Date: Thu, 22 Dec 2022 15:32:06 -0600 Subject: [PATCH 018/244] Remove `map` modifier on OverviewMap. --- .../Examples/OverviewMapExampleView.swift | 17 ++++--- .../Components/OverviewMap.swift | 46 +++++++++++-------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 65a41758f..ef1e09c8a 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -63,12 +63,12 @@ struct OverviewMapForMapView: View { .overlay( OverviewMap.forMapView( with: viewpoint, - visibleArea: visibleArea + visibleArea: visibleArea// , + // map: .customOverviewMap // Uncomment to use a custom map. ) // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. // .symbol(.customFillSymbol) -// .map(.customOverviewMapForMapView) // .scaleFactor(15.0) .frame(width: 200, height: 132) .padding(), @@ -89,11 +89,13 @@ struct OverviewMapForSceneView: View { SceneView(scene: dataModel.scene) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay( - OverviewMap.forSceneView(with: viewpoint) + OverviewMap.forSceneView( + with: viewpoint// , + // map: .customOverviewMap // Uncomment to use a custom map. + ) // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. // .symbol(.customMarkerSymbol) -// .map(.customOverviewMapForSceneView) // .scaleFactor(15.0) .frame(width: 200, height: 132) .padding(), @@ -131,9 +133,6 @@ private extension Symbol { } private extension Map { - /// A custom map for the `OverviewMap` used in a MapView. - static let customOverviewMapForMapView = Map(basemapStyle: .arcGISDarkGray) - - /// A custom map for the `OverviewMap` used in a SceneView. - static let customOverviewMapForSceneView = Map(basemapStyle: .arcGISDarkGray) + /// A custom map for the `OverviewMap`. + static let customOverviewMap = Map(basemapStyle: .arcGISDarkGray) } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 9a494096a..aabb518ed 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -29,9 +29,7 @@ public struct OverviewMap: View { private var scaleFactor = 25.0 /// The data model containing the `Map` displayed in the overview map. - @StateObject private var dataModel = MapDataModel( - map: Map(basemapStyle: .arcGISTopographic) - ) + @StateObject private var dataModel: MapDataModel /// The `Graphic` displaying the visible area of the main `GeoView`. @StateObject private var graphic: Graphic @@ -43,37 +41,57 @@ public struct OverviewMap: View { /// - Parameters: /// - viewpoint: Viewpoint of the main `MapView` used to update the `OverviewMap` view. /// - visibleArea: Visible area of the main `MapView ` used to display the extent graphic. + /// - map: The `Map` displayed in the `OverviewMap`. Defaults to `nil`, in which case + /// a map with the `arcGISTopographic` basemap style is used. /// - Returns: A new `OverviewMap`. public static func forMapView( with viewpoint: Viewpoint?, - visibleArea: ArcGIS.Polygon? + visibleArea: ArcGIS.Polygon?, + map: Map? = nil ) -> OverviewMap { - OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea, symbol: .defaultFill) + OverviewMap( + viewpoint: viewpoint, + visibleArea: visibleArea, + symbol: .defaultFill, + map: map + ) } - + /// Creates an `OverviewMap` for use on a `SceneView`. - /// - Parameter viewpoint: Viewpoint of the main `SceneView` used to update the + /// - Parameters: + /// - viewpoint: Viewpoint of the main `SceneView` used to update the /// `OverviewMap` view. + /// - map: The `Map` displayed in the `OverviewMap`. Defaults to `nil`, in which case + /// a map with the `arcGISTopographic` basemap style is used. /// - Returns: A new `OverviewMap`. public static func forSceneView( - with viewpoint: Viewpoint? + with viewpoint: Viewpoint?, + map: Map? = nil ) -> OverviewMap { - OverviewMap(viewpoint: viewpoint, symbol: .defaultMarker) + OverviewMap(viewpoint: viewpoint, symbol: .defaultMarker, map: map) } /// Creates an `OverviewMap`. Used for creating an `OverviewMap` for use on a `MapView`. /// - Parameters: /// - viewpoint: Viewpoint of the main `GeoView` used to update the `OverviewMap` view. /// - visibleArea: Visible area of the main `GeoView` used to display the extent graphic. + /// - map: The `Map` displayed in the `OverviewMap`. init( viewpoint: Viewpoint?, visibleArea: ArcGIS.Polygon? = nil, - symbol: Symbol + symbol: Symbol, + map: Map? ) { self.visibleArea = visibleArea self.viewpoint = viewpoint self.symbol = symbol + _dataModel = StateObject( + wrappedValue: MapDataModel( + map: map ?? Map(basemapStyle: .arcGISTopographic) + ) + ) + let graphic = Graphic(symbol: self.symbol) // It is necessary to set the graphic and graphicsOverlay this way @@ -130,14 +148,6 @@ public struct OverviewMap: View { // MARK: Modifiers - /// The `Map` displayed in the `OverviewMap`. - /// - Parameter map: The new map. - /// - Returns: The `OverviewMap`. - public func map(_ map: Map) -> OverviewMap { - self.dataModel.map = map - return self - } - /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display /// at the a scale equal to: `viewpoint.targetScale` x `scaleFactor`. /// The default value is `25.0`. From f943b32c48b5d2aec364b514c2b24289185dde3b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 22 Dec 2022 15:42:01 -0600 Subject: [PATCH 019/244] Update doc for map argument to static methods. --- Documentation/OverviewMap/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 2a8177867..d2707fda8 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -16,12 +16,12 @@ OverviewMap: - Displays a representation of the current `VisibleArea`/`Viewpoint` for a connected `GeoView`. - Supports a configurable scaling factor for setting the overview map's zoom level relative to the connected view. - Supports a configurable symbol for visualizing the current `VisibleArea`/`Viewpoint` representation (a `FillSymbol` for a connected `MapView`; a `MarkerSymbol` for a connected `SceneView`). +- Supports using a custom map in the overview map display. ## Key properties `OverviewMap` has the following instance methods: -- `map(_ map: Map)` - The `Map` displayed in the `OverviewMap`. For example, you can use `map(_:)` to display a custom base map in the `OverviewMap`. - `scaleFactor(_ scaleFactor: Double)` - The scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The `OverviewMap` will display at the a scale equal to: `viewpoint.targetScale` x `scaleFactor`. The default is `25`. - `symbol(_ symbol: Symbol)` - The symbol used to visualize the current `VisibleArea`/`Viewpoint`. This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. @@ -84,4 +84,6 @@ var body: some View { } ``` +To use a custom map in the `OverviewMap`, use the `map` argument in either `OverviewMap.forMapView` or `OverviewMap.forSceneView`. + To see the `OverviewMap` in action, and for examples of `OverviewMap` customization, check out the [Examples](../../Examples) and refer to [OverviewMapExampleView.swift](../../Examples/Examples/OverviewMapExampleView.swift) in the project. From 042ef2aec9f85c3d8b78d57f20c88a0ae93581bf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 28 Dec 2022 13:40:04 -0800 Subject: [PATCH 020/244] Update UtilityNetworkTraceViewModelTrace.swift --- .../UtilityNetworkTraceViewModelTrace.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 27e0aced5..09dd129f3 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -82,19 +82,21 @@ extension UtilityNetworkTraceViewModel { /// The extent of the trace's geometry result with a small added buffer. var resultExtent: Envelope? { - if let resultEnvelope = GeometryEngine.combineExtents(of: [ + let geometries = [ utilityGeometryTraceResult?.multipoint, utilityGeometryTraceResult?.polygon, utilityGeometryTraceResult?.polyline - ].compactMap { $0 }), - let expandedEnvelope = GeometryEngine.buffer( - around: resultEnvelope, - distance: 200 - ) { - return expandedEnvelope.extent - } else { + ] + .compactMap { $0 } + .filter { !$0.isEmpty } + + guard !geometries.isEmpty, + let combinedExtents = GeometryEngine.combineExtents(of: geometries), + let expandedEnvelope = GeometryEngine.buffer(around: combinedExtents, distance: 200) else { return nil } + + return expandedEnvelope.extent } /// A collection of starting points for the trace. From 5baf47e950180a5765dfdeb7cf1b892350f120cc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 28 Dec 2022 14:27:17 -0800 Subject: [PATCH 021/244] Repair tests --- .../NetworkChallengeContinuationTests.swift | 5 +++-- .../Test Support/Utility/ChallengeHandler.swift | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift b/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift index be80f1866..d3126fe9e 100644 --- a/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift +++ b/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift @@ -24,8 +24,9 @@ import XCTest func testResumeAndComplete() async { let challenge = NetworkChallengeContinuation(host: "host.com", kind: .serverTrust) - challenge.resume(with: .useCredential(.serverTrust)) + challenge.resume(with: .continueWithCredential(.serverTrust)) + challenge.resume(with: .continueWithCredential(.serverTrust)) let disposition = await challenge.value - XCTAssertEqual(disposition, .useCredential(.serverTrust)) + XCTAssertEqual(disposition, .continueWithCredential(.serverTrust)) } } diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift index b4b6a184f..8cba8a3b6 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift @@ -65,13 +65,13 @@ class ChallengeHandler: AuthenticationChallengeHandler { if challenge.kind == .serverTrust { if trustedHosts.contains(challenge.host) { // This will cause a self-signed certificate to be trusted. - return .useCredential(.serverTrust) + return .continueWithCredential(.serverTrust) } else { return .continueWithoutCredential } } else if let networkCredentialProvider = networkCredentialProvider, let networkCredential = await networkCredentialProvider(challenge) { - return .useCredential(networkCredential) + return .continueWithCredential(networkCredential) } else { return .cancel } @@ -84,7 +84,7 @@ class ChallengeHandler: AuthenticationChallengeHandler { if let arcgisCredentialProvider = arcgisCredentialProvider, let arcgisCredential = try? await arcgisCredentialProvider(challenge) { - return .useCredential(arcgisCredential) + return .continueWithCredential(arcgisCredential) } else { return .cancel } From 15f3e866f72c01a2bb31984777d6c29253ae8edb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 28 Dec 2022 14:28:33 -0800 Subject: [PATCH 022/244] Update NetworkChallengeContinuationTests.swift --- Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift b/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift index d3126fe9e..fdc4e9406 100644 --- a/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift +++ b/Tests/ArcGISToolkitTests/NetworkChallengeContinuationTests.swift @@ -25,7 +25,6 @@ import XCTest func testResumeAndComplete() async { let challenge = NetworkChallengeContinuation(host: "host.com", kind: .serverTrust) challenge.resume(with: .continueWithCredential(.serverTrust)) - challenge.resume(with: .continueWithCredential(.serverTrust)) let disposition = await challenge.value XCTAssertEqual(disposition, .continueWithCredential(.serverTrust)) } From 045d9ae0767ddaab8670c4256b4dec1226dd2b6a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 28 Dec 2022 16:41:16 -0800 Subject: [PATCH 023/244] Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 7db12ea8c..1bd8a9217 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -122,6 +122,7 @@ struct SiteAndFacilitySelector: View { }, set: { newSite in guard let newSite = newSite else { return } + userBackedOutOfSelectedSite = false viewModel.setSite(newSite, zoomTo: true) } ) From cd2559c4aba4bdf4da98409a794f792dc48bebf7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 3 Jan 2023 08:41:39 -0800 Subject: [PATCH 024/244] guard unwrap `utilityGeometryTraceResult` --- .../UtilityNetworkTraceViewModelTrace.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 09dd129f3..000f3f737 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -82,10 +82,12 @@ extension UtilityNetworkTraceViewModel { /// The extent of the trace's geometry result with a small added buffer. var resultExtent: Envelope? { + guard let utilityGeometryTraceResult else { return nil } + let geometries = [ - utilityGeometryTraceResult?.multipoint, - utilityGeometryTraceResult?.polygon, - utilityGeometryTraceResult?.polyline + utilityGeometryTraceResult.multipoint, + utilityGeometryTraceResult.polygon, + utilityGeometryTraceResult.polyline ] .compactMap { $0 } .filter { !$0.isEmpty } From 562b176c003e2a0fdf9f59b8cbdd204b2fc52c3b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 3 Jan 2023 08:43:40 -0800 Subject: [PATCH 025/244] Merge compactMap and filter into compactMap --- .../UtilityNetworkTraceViewModelTrace.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 000f3f737..7d29e79c9 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -89,8 +89,13 @@ extension UtilityNetworkTraceViewModel { utilityGeometryTraceResult.polygon, utilityGeometryTraceResult.polyline ] - .compactMap { $0 } - .filter { !$0.isEmpty } + .compactMap { geometry in + if let geometry, !geometry.isEmpty { + return geometry + } else { + return nil + } + } guard !geometries.isEmpty, let combinedExtents = GeometryEngine.combineExtents(of: geometries), From 8fe56b4083955c78b2cedf052c54a842e876a3f0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 3 Jan 2023 09:21:19 -0800 Subject: [PATCH 026/244] Update UtilityNetworkTraceViewModelTrace.swift --- .../UtilityNetworkTraceViewModelTrace.swift | 114 +++++++++--------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 7d29e79c9..30e40cda0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -17,36 +17,6 @@ import SwiftUI extension UtilityNetworkTraceViewModel { /// A trace performed on a utility network. struct Trace { - /// - Parameter name: A name of a utility asset group. - /// - Returns: The set of utility elements returned by the trace that belong to the provided - /// asset group, grouped by type. - func elementsByTypeInGroup(named name: String) -> [String: [UtilityElement]] { - let assetsInGroup = elementsInAssetGroup(named: name) - var result = [String : [UtilityElement]]() - assetsInGroup.forEach { e in - var assetTypeGroup = result[e.assetType.name, default: []] - assetTypeGroup.append(e) - result.updateValue(assetTypeGroup, forKey: e.assetType.name) - } - return result - } - - /// - Parameter name: A name of a utility asset group. - /// - Returns: The set of utility elements returned by the trace that belong to the provided - /// asset group. - func elementsInAssetGroup(named name: String) -> [UtilityElement] { - return elementResults.filter({ $0.assetGroup.name == name }) - } - - /// A set of the asset group names returned by the trace. - var assetGroupNames: Set { - var assetGroupNames = Set() - elementResults.forEach { - assetGroupNames.insert($0.assetGroup.name) - } - return assetGroupNames - } - /// A user given color for the trace with a default value of green. var color: Color = .green { didSet { @@ -80,32 +50,6 @@ extension UtilityNetworkTraceViewModel { /// A user given name for the trace. var name = "" - /// The extent of the trace's geometry result with a small added buffer. - var resultExtent: Envelope? { - guard let utilityGeometryTraceResult else { return nil } - - let geometries = [ - utilityGeometryTraceResult.multipoint, - utilityGeometryTraceResult.polygon, - utilityGeometryTraceResult.polyline - ] - .compactMap { geometry in - if let geometry, !geometry.isEmpty { - return geometry - } else { - return nil - } - } - - guard !geometries.isEmpty, - let combinedExtents = GeometryEngine.combineExtents(of: geometries), - let expandedEnvelope = GeometryEngine.buffer(around: combinedExtents, distance: 200) else { - return nil - } - - return expandedEnvelope.extent - } - /// A collection of starting points for the trace. var startingPoints = [UtilityNetworkTraceStartingPoint]() @@ -123,6 +67,64 @@ extension UtilityNetworkTraceViewModel { } } +extension UtilityNetworkTraceViewModel.Trace { + /// - Parameter name: A name of a utility asset group. + /// - Returns: The set of utility elements returned by the trace that belong to the provided + /// asset group, grouped by type. + func elementsByTypeInGroup(named name: String) -> [String: [UtilityElement]] { + let assetsInGroup = elementsInAssetGroup(named: name) + var result = [String : [UtilityElement]]() + assetsInGroup.forEach { e in + var assetTypeGroup = result[e.assetType.name, default: []] + assetTypeGroup.append(e) + result.updateValue(assetTypeGroup, forKey: e.assetType.name) + } + return result + } + + /// - Parameter name: A name of a utility asset group. + /// - Returns: The set of utility elements returned by the trace that belong to the provided + /// asset group. + func elementsInAssetGroup(named name: String) -> [UtilityElement] { + return elementResults.filter({ $0.assetGroup.name == name }) + } + + /// A set of the asset group names returned by the trace. + var assetGroupNames: Set { + var assetGroupNames = Set() + elementResults.forEach { + assetGroupNames.insert($0.assetGroup.name) + } + return assetGroupNames + } + + /// The extent of the trace's geometry result with a small added buffer. + var resultExtent: Envelope? { + guard let utilityGeometryTraceResult else { return nil } + + let geometries = [ + utilityGeometryTraceResult.multipoint, + utilityGeometryTraceResult.polygon, + utilityGeometryTraceResult.polyline + ] + .compactMap { geometry in + if let geometry, !geometry.isEmpty { + return geometry + } else { + return nil + } + } + + guard !geometries.isEmpty, + let combinedExtents = GeometryEngine.combineExtents(of: geometries), + let expandedEnvelope = GeometryEngine.buffer(around: combinedExtents, distance: 200) else { + return nil + } + + return expandedEnvelope.extent + } +} + extension UtilityNetworkTraceViewModel.Trace: Equatable { static func == ( lhs: UtilityNetworkTraceViewModel.Trace, From 65b8b5c3c865b22e968f28ce088168d163f28532 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 3 Jan 2023 13:28:02 -0600 Subject: [PATCH 027/244] PR review changes. --- .../Examples/OverviewMapExampleView.swift | 15 +++++---- .../Components/OverviewMap.swift | 32 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index ef1e09c8a..581291f28 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -56,6 +56,9 @@ struct OverviewMapForMapView: View { @State private var visibleArea: ArcGIS.Polygon? + // A custom map to display as the overview map. +// @State var customOverviewMap = Map(basemapStyle: .arcGISDarkGray) + var body: some View { MapView(map: dataModel.map) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } @@ -64,7 +67,7 @@ struct OverviewMapForMapView: View { OverviewMap.forMapView( with: viewpoint, visibleArea: visibleArea// , - // map: .customOverviewMap // Uncomment to use a custom map. + // map: customOverviewMap // Uncomment to use a custom map. ) // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. @@ -85,13 +88,16 @@ struct OverviewMapForSceneView: View { @State private var viewpoint: Viewpoint? + // A custom map to display as the overview map. + // @State var customOverviewMap = Map(basemapStyle: .arcGISDarkGray) + var body: some View { SceneView(scene: dataModel.scene) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay( OverviewMap.forSceneView( with: viewpoint// , - // map: .customOverviewMap // Uncomment to use a custom map. + // map: customOverviewMap // Uncomment to use a custom map. ) // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. @@ -131,8 +137,3 @@ private extension Symbol { size: 16.0 ) } - -private extension Map { - /// A custom map for the `OverviewMap`. - static let customOverviewMap = Map(basemapStyle: .arcGISDarkGray) -} diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index aabb518ed..cd1276eb2 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -29,7 +29,7 @@ public struct OverviewMap: View { private var scaleFactor = 25.0 /// The data model containing the `Map` displayed in the overview map. - @StateObject private var dataModel: MapDataModel + @StateObject private var dataModel = MapDataModel() /// The `Graphic` displaying the visible area of the main `GeoView`. @StateObject private var graphic: Graphic @@ -37,6 +37,14 @@ public struct OverviewMap: View { /// The `GraphicsOverlay` used to display the visible area graphic. @StateObject private var graphicsOverlay: GraphicsOverlay + /// The user-defined map used in the overview map. Defaults to `nil`. + private let userProvidedMap: Map? + + /// The actual map used in the overaview map. + private var effectiveMap: Map { + userProvidedMap ?? dataModel.defaultMap + } + /// Creates an `OverviewMap` for use on a `MapView`. /// - Parameters: /// - viewpoint: Viewpoint of the main `MapView` used to update the `OverviewMap` view. @@ -56,7 +64,7 @@ public struct OverviewMap: View { map: map ) } - + /// Creates an `OverviewMap` for use on a `SceneView`. /// - Parameters: /// - viewpoint: Viewpoint of the main `SceneView` used to update the @@ -86,12 +94,6 @@ public struct OverviewMap: View { self.viewpoint = viewpoint self.symbol = symbol - _dataModel = StateObject( - wrappedValue: MapDataModel( - map: map ?? Map(basemapStyle: .arcGISTopographic) - ) - ) - let graphic = Graphic(symbol: self.symbol) // It is necessary to set the graphic and graphicsOverlay this way @@ -100,11 +102,13 @@ public struct OverviewMap: View { // with the graphic during panning/zooming/rotating. _graphic = StateObject(wrappedValue: graphic) _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) + + userProvidedMap = map } public var body: some View { MapView( - map: dataModel.map, + map: effectiveMap, viewpoint: makeOverviewViewpoint(), graphicsOverlays: [graphicsOverlay] ) @@ -198,12 +202,6 @@ private extension Symbol { /// A very basic data model class containing a Map. class MapDataModel: ObservableObject { - /// The `Map` used for display in a `MapView`. - @Published var map: Map - - /// Creates a `MapDataModel`. - /// - Parameter map: The `Map` used for display. - init(map: Map) { - self.map = map - } + /// The default `Map` used for display in a `MapView`. + let defaultMap = Map(basemapStyle: .arcGISTopographic) } From 3affdfdb0319f75342dff60cd6e842ca86a19823 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 5 Jan 2023 14:59:57 -0800 Subject: [PATCH 028/244] Update UtilityNetworkTraceViewModelTrace.swift --- .../UtilityNetworkTraceViewModelTrace.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 30e40cda0..41bc4f038 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -115,8 +115,7 @@ extension UtilityNetworkTraceViewModel.Trace { } } - guard !geometries.isEmpty, - let combinedExtents = GeometryEngine.combineExtents(of: geometries), + guard let combinedExtents = GeometryEngine.combineExtents(of: geometries), let expandedEnvelope = GeometryEngine.buffer(around: combinedExtents, distance: 200) else { return nil } From 28a5a4ffebfa47bd9efc321d085736f6fd137001 Mon Sep 17 00:00:00 2001 From: Carl Wieland Date: Fri, 6 Jan 2023 10:59:23 -0500 Subject: [PATCH 029/244] Adjust how certificates are handled Add security scoping to the url which seems to fix issues accessing iCloud Document url's Switch to using a detached task to avoid deserializing the certificate on the main thread. --- .../CertificatePickerViewModifier.swift | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 71feed99f..7eb38bc5d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -71,14 +71,18 @@ import ArcGIS preconditionFailure() } - Task { + Task.detached { do { - challenge.resume(with: .continueWithCredential(try .certificate(at: certificateURL, password: password))) + guard certificateURL.startAccessingSecurityScopedResource() else { + self.showCertificateImportError(nil) + return + } + + defer { certificateURL.stopAccessingSecurityScopedResource() } + + await self.challenge.resume(with: .continueWithCredential(try .certificate(at: certificateURL, password: password))) } catch { - // This is required to prevent an "already presenting" error. - try? await Task.sleep(nanoseconds: 100_000) - certificateImportError = error as? CertificateImportError - showCertificateImportError = true + self.showCertificateImportError(error) } } } @@ -87,6 +91,13 @@ import ArcGIS func cancel() { challenge.resume(with: .cancel) } + + private func showCertificateImportError(_ error: Error?) async { + // This is required to prevent an "already presenting" error. + try? await Task.sleep(nanoseconds: 100_000) + certificateImportError = error as? CertificateImportError + showCertificateImportError = true + } } /// A view modifier that presents a certificate picker workflow. From 4a457d965edb3fa78b0b0d995035f182b5efc9e4 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 6 Jan 2023 09:28:12 -0800 Subject: [PATCH 030/244] fix build error --- .../Authentication/CertificatePickerViewModifier.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 7eb38bc5d..bf21ebb65 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -74,7 +74,7 @@ import ArcGIS Task.detached { do { guard certificateURL.startAccessingSecurityScopedResource() else { - self.showCertificateImportError(nil) + await self.showCertificateImportError(nil) return } @@ -82,7 +82,7 @@ import ArcGIS await self.challenge.resume(with: .continueWithCredential(try .certificate(at: certificateURL, password: password))) } catch { - self.showCertificateImportError(error) + await self.showCertificateImportError(error) } } } From 868d9e60969073a8a5925b456e92ac98d5df8978 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 6 Jan 2023 12:56:55 -0800 Subject: [PATCH 031/244] added certificate errors --- .../CertificatePickerViewModifier.swift | 73 +++++++++++-------- .../CertificatePickerViewModelTests.swift | 4 +- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index bf21ebb65..f50ff82fa 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -18,6 +18,16 @@ import ArcGIS /// An object that provides the business logic for the workflow of prompting the user for a /// certificate and a password. @MainActor final class CertificatePickerViewModel: ObservableObject { + /// The types of certificate error. + enum CertificateError: Error { + /// Could not access the certificate file. + case couldNotAccessCertificateFile + /// The certificate import error. + case importError(CertificateImportError) + // The failure error. + case failure(Error) + } + /// The challenge that requires a certificate to proceed. let challenge: NetworkChallengeContinuation @@ -34,10 +44,10 @@ import ArcGIS @Published var showPassword = false /// A Boolean value indicating whether to display the error. - @Published var showCertificateImportError = false + @Published var showCertificateError = false - /// The certificate import error that occurred. - var certificateImportError: CertificateImportError? + /// The certificate error that occurred. + var certificateError: CertificateError? /// The host that prompted the challenge. var challengingHost: String { @@ -74,15 +84,16 @@ import ArcGIS Task.detached { do { guard certificateURL.startAccessingSecurityScopedResource() else { - await self.showCertificateImportError(nil) + await self.showCertificateError(CertificateError.couldNotAccessCertificateFile) return } - defer { certificateURL.stopAccessingSecurityScopedResource() } await self.challenge.resume(with: .continueWithCredential(try .certificate(at: certificateURL, password: password))) + } catch(let certificateImportError as CertificateImportError) { + await self.showCertificateError(CertificateError.importError(certificateImportError)) } catch { - await self.showCertificateImportError(error) + await self.showCertificateError(CertificateError.failure(error)) } } } @@ -92,11 +103,31 @@ import ArcGIS challenge.resume(with: .cancel) } - private func showCertificateImportError(_ error: Error?) async { + private func showCertificateError(_ error: CertificateError) async { // This is required to prevent an "already presenting" error. try? await Task.sleep(nanoseconds: 100_000) - certificateImportError = error as? CertificateImportError - showCertificateImportError = true + certificateError = error + showCertificateError = true + } +} + +extension CertificatePickerViewModel.CertificateError { + var message: String { + switch self { + case .couldNotAccessCertificateFile: + return "Could not access the certificate file." + case .importError(let certificateImportError): + switch certificateImportError { + case .invalidData: + return "The certificate file was invalid." + case .invalidPassword: + return "The password was invalid." + default: + return "The certificate file or password was invalid." + } + case .failure(let error): + return error.localizedDescription + } } } @@ -139,8 +170,8 @@ struct CertificatePickerViewModifier: ViewModifier { } ) ) - .alertCertificateImportError( - isPresented: $viewModel.showCertificateImportError, + .alertCertificateError( + isPresented: $viewModel.showCertificateError, viewModel: viewModel ) } @@ -199,7 +230,7 @@ private extension View { /// - Parameters: /// - isPresented: A Boolean value indicating if the view is presented. /// - viewModel: The view model associated with the view. - @MainActor @ViewBuilder func alertCertificateImportError( + @MainActor @ViewBuilder func alertCertificateError( isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { @@ -212,23 +243,7 @@ private extension View { viewModel.cancel() } } message: { - Text(message(for: viewModel.certificateImportError)) - } - } - - func message(for error: CertificateImportError?) -> String { - let defaultMessage = "The certificate file or password was invalid." - guard let error = error else { - return defaultMessage - } - - switch error { - case .invalidData: - return "The certificate file was invalid." - case .invalidPassword: - return "The password was invalid." - default: - return defaultMessage + Text(viewModel.certificateError?.message ?? "The certificate file or password was invalid.") } } } diff --git a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift index b4f312d8d..4de2ab5d0 100644 --- a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift @@ -23,7 +23,7 @@ import XCTest XCTAssertTrue(model.showPrompt) XCTAssertFalse(model.showPicker) XCTAssertFalse(model.showPassword) - XCTAssertFalse(model.showCertificateImportError) + XCTAssertFalse(model.showCertificateError) XCTAssertEqual(model.challengingHost, "host.com") model.proceedFromPrompt() @@ -42,7 +42,7 @@ import XCTest // Another yield seems to be required to deal with timing when running the test // repeatedly. await Task.yield() - XCTAssertTrue(model.showCertificateImportError) + XCTAssertTrue(model.showCertificateError) model.cancel() let disposition = await challenge.value From 30735401dc49ac7376819fdac4a4d65f78af4f3e Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 10 Jan 2023 16:19:07 -0800 Subject: [PATCH 032/244] pr review from Ryan --- .../CertificatePickerViewModifier.swift | 31 ++++++++++++------- .../CertificatePickerViewModelTests.swift | 18 +++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index f50ff82fa..805f89fe7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -24,8 +24,8 @@ import ArcGIS case couldNotAccessCertificateFile /// The certificate import error. case importError(CertificateImportError) - // The failure error. - case failure(Error) + // The other error. + case other(Error) } /// The challenge that requires a certificate to proceed. @@ -93,7 +93,7 @@ import ArcGIS } catch(let certificateImportError as CertificateImportError) { await self.showCertificateError(CertificateError.importError(certificateImportError)) } catch { - await self.showCertificateError(CertificateError.failure(error)) + await self.showCertificateError(CertificateError.other(error)) } } } @@ -111,21 +111,28 @@ import ArcGIS } } +extension CertificateImportError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidData: + return NSLocalizedString("The certificate file was invalid.", comment: "Invalid certificate") + case .invalidPassword: + return NSLocalizedString("The password was invalid.", comment: "Invalid password") + default: + let errorString = SecCopyErrorMessageString(rawValue, nil) as? String ?? "The certificate file or password was invalid." + return NSLocalizedString(errorString, comment: "Other certificate error") + } + } +} + extension CertificatePickerViewModel.CertificateError { var message: String { switch self { case .couldNotAccessCertificateFile: return "Could not access the certificate file." case .importError(let certificateImportError): - switch certificateImportError { - case .invalidData: - return "The certificate file was invalid." - case .invalidPassword: - return "The password was invalid." - default: - return "The certificate file or password was invalid." - } - case .failure(let error): + return certificateImportError.localizedDescription + case .other(let error): return error.localizedDescription } } diff --git a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift index 4de2ab5d0..14ae16630 100644 --- a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift @@ -12,6 +12,7 @@ // limitations under the License. import XCTest +import ArcGIS @testable import ArcGISToolkit @MainActor final class CertificatePickerViewModelTests: XCTestCase { @@ -48,4 +49,21 @@ import XCTest let disposition = await challenge.value XCTAssertEqual(disposition, .cancel) } + + func testCertificateErrorMessage() { + let couldNotAccessCertificateFileError = CertificatePickerViewModel.CertificateError.couldNotAccessCertificateFile + XCTAssertEqual(couldNotAccessCertificateFileError.message, "Could not access the certificate file.") + + let importErrorInvalidData = CertificatePickerViewModel.CertificateError.importError(.invalidData) + XCTAssertEqual(importErrorInvalidData.message, "The certificate file was invalid.") + + let importErrorInvalidPassword = CertificatePickerViewModel.CertificateError.importError(.invalidPassword) + XCTAssertEqual(importErrorInvalidPassword.message, "The password was invalid.") + + let importErrorInternalError = CertificatePickerViewModel.CertificateError.importError(CertificateImportError(rawValue: errSecInternalError)!) + XCTAssertEqual(importErrorInternalError.message, "An internal error has occurred.") + + let otherError = CertificatePickerViewModel.CertificateError.other(NSError(domain: NSOSStatusErrorDomain, code: Int(errSecInvalidCertAuthority))) + XCTAssertEqual(otherError.message, "The operation couldn’t be completed. (OSStatus error -67826.)") + } } From da9c4d689eab4366e9401ef8847f6eb135870563 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 11 Jan 2023 10:25:36 -0800 Subject: [PATCH 033/244] make CertificatePickerViewModel.CertificateError as LocalizedError --- .../CertificatePickerViewModifier.swift | 12 ++++++------ .../CertificatePickerViewModelTests.swift | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 805f89fe7..d87d5b228 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -125,13 +125,13 @@ extension CertificateImportError: LocalizedError { } } -extension CertificatePickerViewModel.CertificateError { - var message: String { +extension CertificatePickerViewModel.CertificateError: LocalizedError { + public var errorDescription: String? { switch self { case .couldNotAccessCertificateFile: - return "Could not access the certificate file." - case .importError(let certificateImportError): - return certificateImportError.localizedDescription + return NSLocalizedString("Could not access the certificate file.", comment: "Could not access certificate file") + case .importError(let error): + return error.localizedDescription case .other(let error): return error.localizedDescription } @@ -250,7 +250,7 @@ private extension View { viewModel.cancel() } } message: { - Text(viewModel.certificateError?.message ?? "The certificate file or password was invalid.") + Text(viewModel.certificateError?.localizedDescription ?? "The certificate file or password was invalid.") } } } diff --git a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift index 14ae16630..87e302b80 100644 --- a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift @@ -50,20 +50,20 @@ import ArcGIS XCTAssertEqual(disposition, .cancel) } - func testCertificateErrorMessage() { + func testCertificateErrorLocalizedDescription() { let couldNotAccessCertificateFileError = CertificatePickerViewModel.CertificateError.couldNotAccessCertificateFile - XCTAssertEqual(couldNotAccessCertificateFileError.message, "Could not access the certificate file.") + XCTAssertEqual(couldNotAccessCertificateFileError.localizedDescription, "Could not access the certificate file.") let importErrorInvalidData = CertificatePickerViewModel.CertificateError.importError(.invalidData) - XCTAssertEqual(importErrorInvalidData.message, "The certificate file was invalid.") + XCTAssertEqual(importErrorInvalidData.localizedDescription, "The certificate file was invalid.") let importErrorInvalidPassword = CertificatePickerViewModel.CertificateError.importError(.invalidPassword) - XCTAssertEqual(importErrorInvalidPassword.message, "The password was invalid.") + XCTAssertEqual(importErrorInvalidPassword.localizedDescription, "The password was invalid.") let importErrorInternalError = CertificatePickerViewModel.CertificateError.importError(CertificateImportError(rawValue: errSecInternalError)!) - XCTAssertEqual(importErrorInternalError.message, "An internal error has occurred.") + XCTAssertEqual(importErrorInternalError.localizedDescription, "An internal error has occurred.") let otherError = CertificatePickerViewModel.CertificateError.other(NSError(domain: NSOSStatusErrorDomain, code: Int(errSecInvalidCertAuthority))) - XCTAssertEqual(otherError.message, "The operation couldn’t be completed. (OSStatus error -67826.)") + XCTAssertEqual(otherError.localizedDescription, "The operation couldn’t be completed. (OSStatus error -67826.)") } } From 614fa38acbad22ab8778500c535d16809483f538 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 11 Jan 2023 10:27:46 -0800 Subject: [PATCH 034/244] add comment for random failure of test --- Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift index 87e302b80..fdc72c1d1 100644 --- a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift @@ -43,6 +43,7 @@ import ArcGIS // Another yield seems to be required to deal with timing when running the test // repeatedly. await Task.yield() + // Sometime this fails. See details in https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/issues/245. XCTAssertTrue(model.showCertificateError) model.cancel() From 31b53b5b58dba1be281c743ad4b60b4beff56319 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 13 Jan 2023 08:35:39 -0800 Subject: [PATCH 035/244] added localization support and moved few strings in the strings file --- Package.swift | 1 + .../CertificatePickerViewModifier.swift | 19 +++++++++++++------ .../Resources/en.lproj/Localizable.strings | 7 +++++++ 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings diff --git a/Package.swift b/Package.swift index d137808e4..16436287a 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ import PackageDescription let package = Package( name: "arcgis-maps-sdk-swift-toolkit", + defaultLocalization: "en", platforms: [ .iOS(.v15), .macCatalyst(.v15) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index d87d5b228..91348701d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -115,12 +115,15 @@ extension CertificateImportError: LocalizedError { public var errorDescription: String? { switch self { case .invalidData: - return NSLocalizedString("The certificate file was invalid.", comment: "Invalid certificate") + return String(localized: "The certificate file was invalid.", bundle: .module) case .invalidPassword: - return NSLocalizedString("The password was invalid.", comment: "Invalid password") + return String(localized: "The password was invalid.", bundle: .module) default: - let errorString = SecCopyErrorMessageString(rawValue, nil) as? String ?? "The certificate file or password was invalid." - return NSLocalizedString(errorString, comment: "Other certificate error") + let errorString = SecCopyErrorMessageString(rawValue, nil) as? String ?? String( + localized: "The certificate file or password was invalid.", + bundle: .module + ) + return errorString } } } @@ -129,7 +132,7 @@ extension CertificatePickerViewModel.CertificateError: LocalizedError { public var errorDescription: String? { switch self { case .couldNotAccessCertificateFile: - return NSLocalizedString("Could not access the certificate file.", comment: "Could not access certificate file") + return String(localized: "Could not access the certificate file.", bundle: .module) case .importError(let error): return error.localizedDescription case .other(let error): @@ -250,7 +253,11 @@ private extension View { viewModel.cancel() } } message: { - Text(viewModel.certificateError?.localizedDescription ?? "The certificate file or password was invalid.") + Text(viewModel.certificateError?.localizedDescription ?? String( + localized: "The certificate file or password was invalid.", + bundle: .module + ) + ) } } } diff --git a/Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings b/Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings new file mode 100644 index 000000000..9c5c553bb --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +"Could not access the certificate file." = "Could not access the certificate file."; + +"The certificate file was invalid." = "The certificate file was invalid."; + +"The password was invalid." = "The password was invalid."; + +"The certificate file or password was invalid." = "The certificate file or password was invalid."; From 491337437a04c6923f6fdcbfac3a76f19a9ed344 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Mon, 23 Jan 2023 15:03:42 -0800 Subject: [PATCH 036/244] pr feedback from Phil R. --- .../CertificatePickerViewModifier.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 91348701d..19feb751a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -83,17 +83,17 @@ import ArcGIS Task.detached { do { - guard certificateURL.startAccessingSecurityScopedResource() else { - await self.showCertificateError(CertificateError.couldNotAccessCertificateFile) - return + if certificateURL.startAccessingSecurityScopedResource() { + defer { certificateURL.stopAccessingSecurityScopedResource() } + let credential = try NetworkCredential.certificate(at: certificateURL, password: password) + await self.challenge.resume(with: .continueWithCredential(credential)) + } else { + await self.showCertificateError(.couldNotAccessCertificateFile) } - defer { certificateURL.stopAccessingSecurityScopedResource() } - - await self.challenge.resume(with: .continueWithCredential(try .certificate(at: certificateURL, password: password))) } catch(let certificateImportError as CertificateImportError) { - await self.showCertificateError(CertificateError.importError(certificateImportError)) + await self.showCertificateError(.importError(certificateImportError)) } catch { - await self.showCertificateError(CertificateError.other(error)) + await self.showCertificateError(.other(error)) } } } @@ -119,11 +119,10 @@ extension CertificateImportError: LocalizedError { case .invalidPassword: return String(localized: "The password was invalid.", bundle: .module) default: - let errorString = SecCopyErrorMessageString(rawValue, nil) as? String ?? String( + return SecCopyErrorMessageString(rawValue, nil) as String? ?? String( localized: "The certificate file or password was invalid.", bundle: .module ) - return errorString } } } @@ -253,7 +252,8 @@ private extension View { viewModel.cancel() } } message: { - Text(viewModel.certificateError?.localizedDescription ?? String( + Text( + viewModel.certificateError?.localizedDescription ?? String( localized: "The certificate file or password was invalid.", bundle: .module ) From 74cf22abdd4825c9821435c0b7f59a20601d0b08 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 24 Jan 2023 13:14:52 -0800 Subject: [PATCH 037/244] remove public from internal errorDescription --- .../Authentication/CertificatePickerViewModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 19feb751a..8f2862b24 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -128,7 +128,7 @@ extension CertificateImportError: LocalizedError { } extension CertificatePickerViewModel.CertificateError: LocalizedError { - public var errorDescription: String? { + var errorDescription: String? { switch self { case .couldNotAccessCertificateFile: return String(localized: "Could not access the certificate file.", bundle: .module) From e055c2624a96fa612c6870a41019a04ddd1c9b52 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Wed, 25 Jan 2023 08:18:58 -0800 Subject: [PATCH 038/244] Fix build errors in `SearchViewModelTests.swift` Some of the references to `Polygon` were not qualified when they needed to be, so they were resolving to the `Polygon` type in the Application Services framework. While I was at it, I made the geometries computed so that they don't stick around longer than necessary. We've also seen problems with re-using geometries. --- .../SearchViewModelTests.swift | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 2844b8fcb..96efabfcb 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -446,27 +446,33 @@ extension SearchViewModelTests { } } -extension Polygon { - static let chippewaFalls: Polygon = { - let builder = PolygonBuilder(spatialReference: .wgs84) - _ = builder.add(Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) - _ = builder.add(Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) - _ = builder.add(Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) - _ = builder.add(Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) - return builder.toGeometry() - }() +extension ArcGIS.Polygon { + class var chippewaFalls: ArcGIS.Polygon { + let points = [ + Point(x: -91.59127653822401, y: 44.74770908213401), + Point(x: -91.19322516572637, y: 44.74770908213401), + Point(x: -91.19322516572637, y: 45.116100854348254), + Point(x: -91.59127653822401, y: 45.116100854348254) + ] + return .init(points: points, spatialReference: .wgs84) + } - static let minneapolis: Polygon = { - let builder = PolygonBuilder(spatialReference: .wgs84) - _ = builder.add(Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - _ = builder.add(Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - _ = builder.add(Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - _ = builder.add(Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - return builder.toGeometry() - }() + class var minneapolis: ArcGIS.Polygon { + let points = [ + Point(x: -94.170821328662, y: 44.13656401114444), + Point(x: -94.170821328662, y: 44.13656401114444), + Point(x: -92.34544467133114, y: 45.824325577904446), + Point(x: -92.34544467133114, y: 45.824325577904446) + ] + return .init(points: points, spatialReference: .wgs84) + } } extension Point { - static let edinburgh = Point(x: -3.188267, y: 55.953251, spatialReference: .wgs84) - static let portland = Point(x: -122.658722, y: 45.512230, spatialReference: .wgs84) + class var edinburgh: Point { + .init(x: -3.188267, y: 55.953251, spatialReference: .wgs84) + } + class var portland: Point { + .init(x: -122.658722, y: 45.512230, spatialReference: .wgs84) + } } From 8b1199d8bb332e2a9db99c2bb7c162582cbab783 Mon Sep 17 00:00:00 2001 From: Ting Chen Date: Fri, 27 Jan 2023 10:13:31 -0800 Subject: [PATCH 039/244] Change `Int32` to `Int`. Related to `swift/pull/3200`. --- Documentation/Search/README.md | 4 ++-- Package.swift | 2 +- .../Components/Popups/PopupMedia/Charts/ChartData.swift | 4 +--- .../Components/Search/LocatorSearchSource.swift | 8 ++++---- .../ArcGISToolkit/Components/Search/SearchSource.swift | 4 ++-- .../Components/Search/SmartLocatorSearchSource.swift | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Documentation/Search/README.md b/Documentation/Search/README.md index 126f160f4..8f11bb748 100644 --- a/Documentation/Search/README.md +++ b/Documentation/Search/README.md @@ -40,10 +40,10 @@ public protocol SearchSource { var name: String { get set } /// The maximum results to return when performing a search. Most sources default to `6`. - var maximumResults: Int32 { get set } + var maximumResults: Int { get set } /// The maximum suggestions to return. Most sources default to `6`. - var maximumSuggestions: Int32 { get set } + var maximumSuggestions: Int { get set } /// Returns the search suggestions for the specified query. /// - Parameters: diff --git a/Package.swift b/Package.swift index 16436287a..fcea24859 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/Esri/arcgis-maps-sdk-swift", .upToNextMinor(from: "200.0.0-beta")) + .package(name: "arcgis-maps-sdk-swift", path: "../swift/ArcGIS") ], targets: [ .target( diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift index 7ac243186..44ab55db0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift @@ -28,9 +28,7 @@ struct ChartData: Identifiable { /// - value: The value of the data. init(label: String, value: Any) { self.label = label - if let int32 = value as? Int32 { - self.value = Double(int32) - } else if let int = value as? Int { + if let int = value as? Int { self.value = Double(int) } else if let float = value as? Float { self.value = Double(float) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 5601e4745..62fb363a1 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -30,8 +30,8 @@ public class LocatorSearchSource: ObservableObject, SearchSource { string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" )! ), - maximumResults: Int32 = 6, - maximumSuggestions: Int32 = 6 + maximumResults: Int = 6, + maximumSuggestions: Int = 6 ) { self.name = name self.locatorTask = locatorTask @@ -45,13 +45,13 @@ public class LocatorSearchSource: ObservableObject, SearchSource { public var name: String /// The maximum results to return when performing a search. Most sources default to `6`. - public var maximumResults: Int32 { + public var maximumResults: Int { get { geocodeParameters.maxResults } set { geocodeParameters.maxResults = newValue } } /// The maximum suggestions to return. Most sources default to `6`. - public var maximumSuggestions: Int32 { + public var maximumSuggestions: Int { get { suggestParameters.maxResults } set { suggestParameters.maxResults = newValue } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 4423e2d61..8ebe36364 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -19,10 +19,10 @@ public protocol SearchSource { var name: String { get set } /// The maximum results to return when performing a search. Most sources default to `6`. - var maximumResults: Int32 { get set } + var maximumResults: Int { get set } /// The maximum suggestions to return. Most sources default to `6`. - var maximumSuggestions: Int32 { get set } + var maximumSuggestions: Int { get set } /// Returns the search suggestions for the specified query. /// - Parameters: diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 97b7ec270..9d75feffe 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -26,8 +26,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. public init( name: String = "Smart Locator", - maximumResults: Int32 = 6, - maximumSuggestions: Int32 = 6, + maximumResults: Int = 6, + maximumSuggestions: Int = 6, repeatSearchResultThreshold: Int? = 1, repeatSuggestResultThreshold: Int? = 6 ) { From 62dcf293359f574544c0087ec82fb729af55d95a Mon Sep 17 00:00:00 2001 From: Ting Chen Date: Fri, 27 Jan 2023 10:23:35 -0800 Subject: [PATCH 040/244] Revert dependency change. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index fcea24859..16436287a 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,7 @@ let package = Package( ), ], dependencies: [ - .package(name: "arcgis-maps-sdk-swift", path: "../swift/ArcGIS") + .package(url: "https://github.com/Esri/arcgis-maps-sdk-swift", .upToNextMinor(from: "200.0.0-beta")) ], targets: [ .target( From 6a12aa4ef94c21b61fa6a79d4a7df302732e4cb6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 2 Feb 2023 18:10:17 -0800 Subject: [PATCH 041/244] Update BasemapGalleryViewModel.swift --- .../Components/BasemapGallery/BasemapGalleryViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 9e02eb5a9..68fed46f7 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -172,7 +172,7 @@ private extension BasemapGalleryViewModel { if useDeveloperBasemaps { basemaps = try await portal.developerBasemaps } else if let portalInfo = portal.info, - portalInfo.useVectorBasemaps { + portalInfo.usesVectorBasemaps { basemaps = try await portal.vectorBasemaps } else { basemaps = try await portal.basemaps From 3035bd3c27b259528fd74febde032a68a947c691 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Mon, 6 Feb 2023 15:49:08 -0800 Subject: [PATCH 042/244] handle authentication challenge handler changes from SDK --- .../AuthenticationExample/AuthenticationApp.swift | 5 +++-- .../Components/Authentication/Authenticator.swift | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 3c374b880..4172b8abd 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -26,8 +26,9 @@ struct AuthenticationApp: App { // If you want to use OAuth, uncomment this code: //oAuthUserConfigurations: [.arcgisDotCom] ) - // Set the challenge handler to be the authenticator we just created. - ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = authenticator + // Set the ArcGIS and Network challenge handlers to be the authenticator we just created. + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = authenticator + ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = authenticator } var body: some SwiftUI.Scene { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index c34f73a59..7d99eef56 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -85,7 +85,7 @@ public final class Authenticator: ObservableObject { @Published var currentChallenge: ChallengeContinuation? } -extension Authenticator: AuthenticationChallengeHandler { +extension Authenticator: ArcGISAuthenticationChallengeHandler { public func handleArcGISAuthenticationChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { @@ -106,7 +106,9 @@ extension Authenticator: AuthenticationChallengeHandler { return try await tokenChallengeContinuation.value.get() } } - +} + +extension Authenticator: NetworkAuthenticationChallengeHandler { public func handleNetworkAuthenticationChallenge( _ challenge: NetworkAuthenticationChallenge ) async -> NetworkAuthenticationChallenge.Disposition { From 02ef53d03b7912f8b0f5bd5ce0370a1593489967 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 9 Feb 2023 14:13:59 -0800 Subject: [PATCH 043/244] Repair tests --- .../Extensions/XCTest/XCTestCase+ArcGISURLSession.swift | 8 ++++---- .../Test Support/Utility/ChallengeHandler.swift | 2 +- .../UtilityNetworkTraceViewModelTests.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift index 9f2ef5f5b..5acd4eebc 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift @@ -20,9 +20,9 @@ import XCTest import ArcGIS extension XCTestCase { - func setChallengeHandler(_ challengeHandler: AuthenticationChallengeHandler) { - let previous = ArcGISEnvironment.authenticationManager.authenticationChallengeHandler - ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = challengeHandler - addTeardownBlock { ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = previous } + func setChallengeHandler(_ challengeHandler: ArcGISAuthenticationChallengeHandler) { + let previous = ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = challengeHandler + addTeardownBlock { ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = previous } } } diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift index 8cba8a3b6..cfd9e8b09 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift @@ -21,7 +21,7 @@ import ArcGIS /// A `ChallengeHandler` that that can handle trusting hosts with a self-signed certificate, the URL credential, /// and the token credential. -class ChallengeHandler: AuthenticationChallengeHandler { +class ChallengeHandler: ArcGISAuthenticationChallengeHandler { /// The hosts that can be trusted if they have certificate trust issues. let trustedHosts: Set diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 1a3958cb6..58deb08ed 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -31,7 +31,7 @@ import XCTest func tearDownWithError() async throws { ArcGISEnvironment.apiKey = nil - ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = nil + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = nil ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll() } From 5b836a38cb9f245fb8747f2f551e3b2a5462147e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 9 Feb 2023 15:04:48 -0800 Subject: [PATCH 044/244] Update project.pbxproj --- .../AuthenticationExample.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj index f16309c19..f47e307b8 100644 --- a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj @@ -26,7 +26,7 @@ 88AD137A2834355000500B2E /* SigninView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigninView.swift; sourceTree = ""; }; 88AD137C2834355100500B2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 88AD137F2834355100500B2E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 88AD13862834355C00500B2E /* arcgis-maps-sdk-swift-toolit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "arcgis-maps-sdk-swift-toolit"; path = ..; sourceTree = ""; }; + 88AD13862834355C00500B2E /* arcgis-maps-sdk-swift-toolkit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "arcgis-maps-sdk-swift-toolkit"; path = ..; sourceTree = ""; }; 88AD1387283435EA00500B2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 88AD138D283443F800500B2E /* MapItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapItemView.swift; sourceTree = ""; }; 88D24DEF288F062D007A539C /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; @@ -51,7 +51,7 @@ 88AD136C2834355000500B2E = { isa = PBXGroup; children = ( - 88AD13862834355C00500B2E /* arcgis-maps-sdk-swift-toolit */, + 88AD13862834355C00500B2E /* arcgis-maps-sdk-swift-toolkit */, 88AD13772834355000500B2E /* AuthenticationExample */, 88AD13762834355000500B2E /* Products */, 88AD13882834366100500B2E /* Frameworks */, From 11d7fd5f29ac7873fc83bbe2be8cbfe1e671d1c4 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 10 Feb 2023 12:43:43 -0800 Subject: [PATCH 045/244] updated authenticator and doc --- .../AuthenticationExample/AuthenticationApp.swift | 5 ++--- Documentation/Authenticator/README.md | 8 ++++---- .../Components/Authentication/Authenticator.swift | 6 ++++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 4172b8abd..db2e0a4bb 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -27,8 +27,7 @@ struct AuthenticationApp: App { //oAuthUserConfigurations: [.arcgisDotCom] ) // Set the ArcGIS and Network challenge handlers to be the authenticator we just created. - ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = authenticator - ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = authenticator + authenticator.setupAuthenticationChallengeHandlers() } var body: some SwiftUI.Scene { @@ -51,7 +50,7 @@ struct AuthenticationApp: App { .task { isSettingUp = true // Here we make the authenticator persistent, which means that it will synchronize - // with they keychain for storing credentials. + // with the keychain for storing credentials. // It also means that a user can sign in without having to be prompted for // credentials. Once credentials are cleared from the stores ("sign-out"), // then the user will need to be prompted once again. diff --git a/Documentation/Authenticator/README.md b/Documentation/Authenticator/README.md index a14081918..198fdf0b1 100644 --- a/Documentation/Authenticator/README.md +++ b/Documentation/Authenticator/README.md @@ -1,12 +1,12 @@ # Authenticator -The `Authenticator` is a configurable object that handles authentication challenges. It will display a user interface when network and ArcGIS authentication challenges occur. +The `Authenticator` is a configurable object that handles authentication challenges. It will display a user interface when network and ArcGIS authentication challenges occur. ![image](https://user-images.githubusercontent.com/3998072/203615041-c887d5e3-bb64-469a-a76b-126059329e92.png) ## Features -The `Authenticator` has a view modifier that will display a prompt when the `Authenticator` is asked to handle an authentication challenge. This will handle many different types of authentication, for example: +The `Authenticator` has a view modifier that will display a prompt when the `Authenticator` is asked to handle an authentication challenge. This will handle many different types of authentication, for example: - ArcGIS authentication (token and OAuth) - Integrated Windows Authentication (IWA) - Client Certificate (PKI) @@ -56,8 +56,8 @@ init() { // If you want to use OAuth, uncomment this code: //oAuthConfigurations: [.arcgisDotCom] ) - // Set the challenge handler to be the authenticator we just created. - ArcGISEnvironment.authenticationChallengeHandler = authenticator + // Set the ArcGIS and Network challenge handlers to be the authenticator we just created. + authenticator.setupAuthenticationChallengeHandlers() } var body: some SwiftUI.Scene { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 7d99eef56..e7a817db5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -37,6 +37,12 @@ public final class Authenticator: ObservableObject { self.oAuthUserConfigurations = oAuthUserConfigurations } + /// Sets the ArcGIS and Network challenge handlers to be this authenticator. + public func setupAuthenticationChallengeHandlers() { + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = self + ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = self + } + /// Sets up new credential stores that will be persisted to the keychain. /// - Remark: The credentials will be stored in the default access group of the keychain. /// You can find more information about what the default group would be here: From c5fb63a56e53891e628e54639d5502d616852c7f Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 10 Feb 2023 16:00:36 -0800 Subject: [PATCH 046/244] pr review feedback --- .../AuthenticationApp.swift | 7 +- .../AuthenticationExample/ProfileView.swift | 2 +- Documentation/Authenticator/README.md | 7 +- .../Authentication/Authenticator.swift | 102 +++++++++--------- 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index db2e0a4bb..ca6ebdc2a 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -26,8 +26,9 @@ struct AuthenticationApp: App { // If you want to use OAuth, uncomment this code: //oAuthUserConfigurations: [.arcgisDotCom] ) - // Set the ArcGIS and Network challenge handlers to be the authenticator we just created. - authenticator.setupAuthenticationChallengeHandlers() + // Sets authenticator as ArcGIS and Network challenge handlers to handle authentication + // challenges. + ArcGISEnvironment.authenticationManager.handleAuthenticationChallenges(using: authenticator) } var body: some SwiftUI.Scene { @@ -54,7 +55,7 @@ struct AuthenticationApp: App { // It also means that a user can sign in without having to be prompted for // credentials. Once credentials are cleared from the stores ("sign-out"), // then the user will need to be prompted once again. - try? await authenticator.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly) + try? await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly) isSettingUp = false } } diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift index 1dbb55985..635341632 100644 --- a/AuthenticationExample/AuthenticationExample/ProfileView.swift +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -61,7 +61,7 @@ struct ProfileView: View { func signOut() { isSigningOut = true Task { - await authenticator.clearCredentialStores() + await ArcGISEnvironment.authenticationManager.clearCredentialStores() isSigningOut = false signOutAction() } diff --git a/Documentation/Authenticator/README.md b/Documentation/Authenticator/README.md index 198fdf0b1..096aa6825 100644 --- a/Documentation/Authenticator/README.md +++ b/Documentation/Authenticator/README.md @@ -23,7 +23,7 @@ The `Authenticator` can be configured to support securely persisting credentials @ViewBuilder func authenticator(_ authenticator: Authenticator) -> some View ``` -To securely store credentials in the keychain, use the following instance method on `Authenticator`: +To securely store credentials in the keychain, use the following extension method of `AuthenticationManager`: ```swift /// Sets up new credential stores that will be persisted to the keychain. @@ -56,8 +56,9 @@ init() { // If you want to use OAuth, uncomment this code: //oAuthConfigurations: [.arcgisDotCom] ) - // Set the ArcGIS and Network challenge handlers to be the authenticator we just created. - authenticator.setupAuthenticationChallengeHandlers() + // Sets authenticator as ArcGIS and Network challenge handlers to handle authentication + // challenges. + ArcGISEnvironment.authenticationManager.handleAuthenticationChallenges(using: authenticator) } var body: some SwiftUI.Scene { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index e7a817db5..65e39e669 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -37,55 +37,6 @@ public final class Authenticator: ObservableObject { self.oAuthUserConfigurations = oAuthUserConfigurations } - /// Sets the ArcGIS and Network challenge handlers to be this authenticator. - public func setupAuthenticationChallengeHandlers() { - ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = self - ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = self - } - - /// Sets up new credential stores that will be persisted to the keychain. - /// - Remark: The credentials will be stored in the default access group of the keychain. - /// You can find more information about what the default group would be here: - /// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps - /// - Parameters: - /// - access: When the credentials stored in the keychain can be accessed. - /// - synchronizesWithiCloud: A Boolean value indicating whether the credentials are synchronized with iCloud. - public func setupPersistentCredentialStorage( - access: ArcGIS.KeychainAccess, - synchronizesWithiCloud: Bool = false - ) async throws { - let previousArcGISCredentialStore = ArcGISEnvironment.authenticationManager.arcGISCredentialStore - - // Set a persistent ArcGIS credential store on the ArcGIS environment. - ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent( - access: access, - synchronizesWithiCloud: synchronizesWithiCloud - ) - - do { - // Set a persistent network credential store on the ArcGIS environment. - await ArcGISEnvironment.authenticationManager.setNetworkCredentialStore( - try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud) - ) - } catch { - // If making the shared network credential store persistent fails, - // then restore the ArcGIS credential store. - ArcGISEnvironment.authenticationManager.arcGISCredentialStore = previousArcGISCredentialStore - throw error - } - } - - /// Clears all ArcGIS and network credentials from the respective stores. - /// Note: This sets up new `URLSessions` so that removed network credentials are respected - /// right away. - public func clearCredentialStores() async { - // Clear ArcGIS Credentials. - ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll() - - // Clear network credentials. - await ArcGISEnvironment.authenticationManager.networkCredentialStore.removeAll() - } - /// The current challenge. /// This property is not set for OAuth challenges. @Published var currentChallenge: ChallengeContinuation? @@ -137,3 +88,56 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { return await challengeContinuation.value } } + +public extension AuthenticationManager { + /// Sets authenticator as ArcGIS and Network challenge handlers to handle authentication + /// challenges. + /// - Parameter authenticator: The authenticator to be used for handling challenges. + func handleAuthenticationChallenges(using authenticator: Authenticator) { + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = authenticator + ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = authenticator + } + + /// Sets up new credential stores that will be persisted to the keychain. + /// - Remark: The credentials will be stored in the default access group of the keychain. + /// You can find more information about what the default group would be here: + /// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + /// - Parameters: + /// - access: When the credentials stored in the keychain can be accessed. + /// - synchronizesWithiCloud: A Boolean value indicating whether the credentials are synchronized with iCloud. + func setupPersistentCredentialStorage( + access: ArcGIS.KeychainAccess, + synchronizesWithiCloud: Bool = false + ) async throws { + let previousArcGISCredentialStore = ArcGISEnvironment.authenticationManager.arcGISCredentialStore + + // Set a persistent ArcGIS credential store on the ArcGIS environment. + ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent( + access: access, + synchronizesWithiCloud: synchronizesWithiCloud + ) + + do { + // Set a persistent network credential store on the ArcGIS environment. + await ArcGISEnvironment.authenticationManager.setNetworkCredentialStore( + try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud) + ) + } catch { + // If making the shared network credential store persistent fails, + // then restore the ArcGIS credential store. + ArcGISEnvironment.authenticationManager.arcGISCredentialStore = previousArcGISCredentialStore + throw error + } + } + + /// Clears all ArcGIS and network credentials from the respective stores. + /// Note: This sets up new `URLSessions` so that removed network credentials are respected + /// right away. + func clearCredentialStores() async { + // Clear ArcGIS Credentials. + ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll() + + // Clear network credentials. + await ArcGISEnvironment.authenticationManager.networkCredentialStore.removeAll() + } +} From eb16869e9da7b92c0b83ef0ed0ff284acfa8156d Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 14 Feb 2023 10:14:52 -0800 Subject: [PATCH 047/244] updated authenticator, doc and app --- .../AuthenticationApp.swift | 4 +-- Documentation/Authenticator/README.md | 15 ++++++++--- .../Authentication/Authenticator.swift | 26 ++++++++++++------- .../AuthenticatorTests.swift | 7 ++--- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index ca6ebdc2a..b3ee996d2 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -50,8 +50,8 @@ struct AuthenticationApp: App { .environmentObject(authenticator) .task { isSettingUp = true - // Here we make the authenticator persistent, which means that it will synchronize - // with the keychain for storing credentials. + // Here we setup credential stores to be persistent, which means that it will be + // synchronize with the keychain for storing credentials. // It also means that a user can sign in without having to be prompted for // credentials. Once credentials are cleared from the stores ("sign-out"), // then the user will need to be prompted once again. diff --git a/Documentation/Authenticator/README.md b/Documentation/Authenticator/README.md index 096aa6825..a66ede1e1 100644 --- a/Documentation/Authenticator/README.md +++ b/Documentation/Authenticator/README.md @@ -39,6 +39,15 @@ To securely store credentials in the keychain, use the following extension metho ) async throws ``` +While sign-out, use the following extension method of `AuthenticationManager`: + +```swift + /// Clears all ArcGIS and network credentials from the respective stores. + /// Note: This sets up new `URLSessions` so that removed network credentials are respected + /// right away. + func clearCredentialStores() async +``` + ## Behavior: The Authenticator view modifier will display an alert prompting the user for credentials. If credentials were persisted to the keychain, the Authenticator will use those instead of requiring the user to reenter credentials. @@ -66,12 +75,12 @@ var body: some SwiftUI.Scene { HomeView() .authenticator(authenticator) .task { - // Here we make the authenticator persistent, which means that it will synchronize - // with the keychain for storing credentials. + // Here we setup credential stores to be persistent, which means that it will be + // synchronize with the keychain for storing credentials. // It also means that a user can sign in without having to be prompted for // credentials. Once credentials are cleared from the stores ("sign-out"), // then the user will need to be prompted once again. - try? await authenticator.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly) + try? await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly) } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 65e39e669..d11c154b2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -90,12 +90,12 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { } public extension AuthenticationManager { - /// Sets authenticator as ArcGIS and Network challenge handlers to handle authentication + /// Sets up authenticator as ArcGIS and Network challenge handlers to handle authentication /// challenges. /// - Parameter authenticator: The authenticator to be used for handling challenges. func handleAuthenticationChallenges(using authenticator: Authenticator) { - ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = authenticator - ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = authenticator + arcGISAuthenticationChallengeHandler = authenticator + networkAuthenticationChallengeHandler = authenticator } /// Sets up new credential stores that will be persisted to the keychain. @@ -109,23 +109,23 @@ public extension AuthenticationManager { access: ArcGIS.KeychainAccess, synchronizesWithiCloud: Bool = false ) async throws { - let previousArcGISCredentialStore = ArcGISEnvironment.authenticationManager.arcGISCredentialStore + let previousArcGISCredentialStore = arcGISCredentialStore // Set a persistent ArcGIS credential store on the ArcGIS environment. - ArcGISEnvironment.authenticationManager.arcGISCredentialStore = try await .makePersistent( + arcGISCredentialStore = try await .makePersistent( access: access, synchronizesWithiCloud: synchronizesWithiCloud ) do { // Set a persistent network credential store on the ArcGIS environment. - await ArcGISEnvironment.authenticationManager.setNetworkCredentialStore( + await setNetworkCredentialStore( try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud) ) } catch { // If making the shared network credential store persistent fails, // then restore the ArcGIS credential store. - ArcGISEnvironment.authenticationManager.arcGISCredentialStore = previousArcGISCredentialStore + arcGISCredentialStore = previousArcGISCredentialStore throw error } } @@ -134,10 +134,18 @@ public extension AuthenticationManager { /// Note: This sets up new `URLSessions` so that removed network credentials are respected /// right away. func clearCredentialStores() async { + // Revoke tokens for OAuth user credentials. + let oAuthUserCredentials = arcGISCredentialStore.credentials.compactMap { $0 as? OAuthUserCredential } + oAuthUserCredentials.forEach { oAuthUserCredential in + Task { + try? await oAuthUserCredential.revokeToken() + } + } + // Clear ArcGIS Credentials. - ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll() + arcGISCredentialStore.removeAll() // Clear network credentials. - await ArcGISEnvironment.authenticationManager.networkCredentialStore.removeAll() + await networkCredentialStore.removeAll() } } diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index 6c8e201ee..291a6186c 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -36,9 +36,8 @@ import Combine } // This tests that calling setupPersistentCredentialStorage tries to sync with the keychain. - let authenticator = Authenticator() do { - try await authenticator.setupPersistentCredentialStorage(access: .whenUnlocked) + try await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlocked) XCTFail("Expected an error to be thrown as unit tests should not have access to the keychain") } catch {} } @@ -55,12 +54,10 @@ import Combine ) ) - let authenticator = Authenticator() - var arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials XCTAssertEqual(arcGISCreds.count, 1) - await authenticator.clearCredentialStores() + await ArcGISEnvironment.authenticationManager.clearCredentialStores() arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials XCTAssertTrue(arcGISCreds.isEmpty) From 0c5a37270bc331a7afd612f196d21434d4e451bb Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 14 Feb 2023 10:15:13 -0800 Subject: [PATCH 048/244] updated challenge handlers and tests --- .../XCTest/XCTestCase+ArcGISURLSession.swift | 18 +++- .../Utility/ArcGISChallengeHandler.swift | 47 ++++++++++ .../Utility/ChallengeHandler.swift | 92 ------------------- .../Utility/NetworkChallengeHandler.swift | 63 +++++++++++++ .../UtilityNetworkTraceViewModelTests.swift | 5 +- 5 files changed, 127 insertions(+), 98 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/Test Support/Utility/ArcGISChallengeHandler.swift delete mode 100644 Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift create mode 100644 Tests/ArcGISToolkitTests/Test Support/Utility/NetworkChallengeHandler.swift diff --git a/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift index 9f2ef5f5b..286b1fa16 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift @@ -20,9 +20,19 @@ import XCTest import ArcGIS extension XCTestCase { - func setChallengeHandler(_ challengeHandler: AuthenticationChallengeHandler) { - let previous = ArcGISEnvironment.authenticationManager.authenticationChallengeHandler - ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = challengeHandler - addTeardownBlock { ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = previous } + /// Sets up an ArcGIS challenge handler on the `ArcGISURLSession` and registers a tear-down block to + /// reset it to previous handler. + func setArcGISChallengeHandler(_ challengeHandler: ArcGISAuthenticationChallengeHandler) { + let previous = ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = challengeHandler + addTeardownBlock { ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = previous } + } + + /// Sets up a network challenge handler on the `ArcGISURLSession` and registers a tear-down block to + /// reset it to previous handler. + func setNetworkChallengeHandler(_ challengeHandler: NetworkAuthenticationChallengeHandler) { + let previous = ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler + ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = challengeHandler + addTeardownBlock { ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = previous } } } diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ArcGISChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ArcGISChallengeHandler.swift new file mode 100644 index 000000000..34d5087fd --- /dev/null +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/ArcGISChallengeHandler.swift @@ -0,0 +1,47 @@ +// +// COPYRIGHT 1995-2022 ESRI +// +// TRADE SECRETS: ESRI PROPRIETARY AND CONFIDENTIAL +// Unpublished material - all rights reserved under the +// Copyright Laws of the United States and applicable international +// laws, treaties, and conventions. +// +// For additional information, contact: +// Environmental Systems Research Institute, Inc. +// Attn: Contracts and Legal Services Department +// 380 New York Street +// Redlands, California, 92373 +// USA +// +// email: contracts@esri.com +// + +import Foundation +import ArcGIS + +/// An `ArcGISChallengeHandler` that can handle challenges using ArcGIS credential. +final class ArcGISChallengeHandler: ArcGISAuthenticationChallengeHandler { + /// The arcgis credential used when an ArcGIS challenge is received. + let credentialProvider: ((ArcGISAuthenticationChallenge) async throws -> ArcGISCredential?) + + /// The ArcGIS authentication challenges. + private(set) var challenges: [ArcGISAuthenticationChallenge] = [] + + init( + credentialProvider: @escaping ((ArcGISAuthenticationChallenge) async throws -> ArcGISCredential?) + ) { + self.credentialProvider = credentialProvider + } + + func handleArcGISAuthenticationChallenge( + _ challenge: ArcGISAuthenticationChallenge + ) async throws -> ArcGISAuthenticationChallenge.Disposition { + challenges.append(challenge) + + if let arcgisCredential = try await credentialProvider(challenge) { + return .continueWithCredential(arcgisCredential) + } else { + return .continueWithoutCredential + } + } +} diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift deleted file mode 100644 index 8cba8a3b6..000000000 --- a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// COPYRIGHT 1995-2022 ESRI -// -// TRADE SECRETS: ESRI PROPRIETARY AND CONFIDENTIAL -// Unpublished material - all rights reserved under the -// Copyright Laws of the United States and applicable international -// laws, treaties, and conventions. -// -// For additional information, contact: -// Environmental Systems Research Institute, Inc. -// Attn: Contracts and Legal Services Department -// 380 New York Street -// Redlands, California, 92373 -// USA -// -// email: contracts@esri.com -// - -import Foundation -import ArcGIS - -/// A `ChallengeHandler` that that can handle trusting hosts with a self-signed certificate, the URL credential, -/// and the token credential. -class ChallengeHandler: AuthenticationChallengeHandler { - /// The hosts that can be trusted if they have certificate trust issues. - let trustedHosts: Set - - /// The url credential used when a challenge is thrown. - let networkCredentialProvider: ((NetworkAuthenticationChallenge) async -> NetworkCredential?)? - - /// The arcgis credential used when an ArcGIS challenge is received. - let arcgisCredentialProvider: ((ArcGISAuthenticationChallenge) async throws -> ArcGISCredential?)? - - /// The network authentication challenges. - private(set) var networkChallenges: [NetworkAuthenticationChallenge] = [] - - /// The ArcGIS authentication challenges. - private(set) var arcGISChallenges: [ArcGISAuthenticationChallenge] = [] - - init( - trustedHosts: Set = [], - networkCredentialProvider: ((NetworkAuthenticationChallenge) async -> NetworkCredential?)? = nil, - arcgisCredentialProvider: ((ArcGISAuthenticationChallenge) async throws -> ArcGISCredential?)? = nil - ) { - self.trustedHosts = trustedHosts - self.networkCredentialProvider = networkCredentialProvider - self.arcgisCredentialProvider = arcgisCredentialProvider - } - - convenience init( - trustedHosts: Set, - networkCredential: NetworkCredential - ) { - self.init(trustedHosts: trustedHosts, networkCredentialProvider: { _ in networkCredential }) - } - - func handleNetworkAuthenticationChallenge( - _ challenge: NetworkAuthenticationChallenge - ) async -> NetworkAuthenticationChallenge.Disposition { - // Record challenge only if it is not a server trust. - if challenge.kind != .serverTrust { - networkChallenges.append(challenge) - } - - if challenge.kind == .serverTrust { - if trustedHosts.contains(challenge.host) { - // This will cause a self-signed certificate to be trusted. - return .continueWithCredential(.serverTrust) - } else { - return .continueWithoutCredential - } - } else if let networkCredentialProvider = networkCredentialProvider, - let networkCredential = await networkCredentialProvider(challenge) { - return .continueWithCredential(networkCredential) - } else { - return .cancel - } - } - - func handleArcGISAuthenticationChallenge( - _ challenge: ArcGISAuthenticationChallenge - ) async throws -> ArcGISAuthenticationChallenge.Disposition { - arcGISChallenges.append(challenge) - - if let arcgisCredentialProvider = arcgisCredentialProvider, - let arcgisCredential = try? await arcgisCredentialProvider(challenge) { - return .continueWithCredential(arcgisCredential) - } else { - return .cancel - } - } -} diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/NetworkChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/NetworkChallengeHandler.swift new file mode 100644 index 000000000..36c9681db --- /dev/null +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/NetworkChallengeHandler.swift @@ -0,0 +1,63 @@ +// +// COPYRIGHT 1995-2022 ESRI +// +// TRADE SECRETS: ESRI PROPRIETARY AND CONFIDENTIAL +// Unpublished material - all rights reserved under the +// Copyright Laws of the United States and applicable international +// laws, treaties, and conventions. +// +// For additional information, contact: +// Environmental Systems Research Institute, Inc. +// Attn: Contracts and Legal Services Department +// 380 New York Street +// Redlands, California, 92373 +// USA +// +// email: contracts@esri.com +// + +import Foundation +import ArcGIS + +/// A `NetworkChallengeHandler` that can handle trusting hosts with a self-signed certificate +/// and the network credential. +final class NetworkChallengeHandler: NetworkAuthenticationChallengeHandler { + /// A Boolean value indicating whether to allow hosts that have certificate trust issues. + let allowUntrustedHosts: Bool + + /// The url credential used when a challenge is thrown. + let networkCredential: NetworkCredential? + + /// The network authentication challenges. + private(set) var challenges: [NetworkAuthenticationChallenge] = [] + + init( + allowUntrustedHosts: Bool, + networkCredential: NetworkCredential? = nil + ) { + self.allowUntrustedHosts = allowUntrustedHosts + self.networkCredential = networkCredential + } + + func handleNetworkAuthenticationChallenge( + _ challenge: NetworkAuthenticationChallenge + ) async -> NetworkAuthenticationChallenge.Disposition { + // Record challenge only if it is not a server trust. + if challenge.kind != .serverTrust { + challenges.append(challenge) + } + + if challenge.kind == .serverTrust { + if allowUntrustedHosts { + // This will cause a self-signed certificate to be trusted. + return .continueWithCredential(.serverTrust) + } else { + return .continueWithoutCredential + } + } else if let networkCredential = networkCredential { + return .continueWithCredential(networkCredential) + } else { + return .cancel + } + } +} diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 1a3958cb6..bf7672700 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -23,7 +23,7 @@ import XCTest ArcGISEnvironment.apiKey = apiKey try XCTSkipIf(apiKey == .placeholder) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.sampleServer7.host!])) + setNetworkChallengeHandler(NetworkChallengeHandler(allowUntrustedHosts: true)) ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add( try await tokenForSampleServer7 ) @@ -31,7 +31,8 @@ import XCTest func tearDownWithError() async throws { ArcGISEnvironment.apiKey = nil - ArcGISEnvironment.authenticationManager.authenticationChallengeHandler = nil + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = nil + ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = nil ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll() } From 0ccf031a4eb5a21a93e81e51f0f832a5912540aa Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 14 Feb 2023 10:21:39 -0800 Subject: [PATCH 049/244] fix doc --- .../AuthenticationExample/AuthenticationApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index b3ee996d2..30c9c5d96 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -50,7 +50,7 @@ struct AuthenticationApp: App { .environmentObject(authenticator) .task { isSettingUp = true - // Here we setup credential stores to be persistent, which means that it will be + // Here we setup credential stores to be persistent, which means that it will // synchronize with the keychain for storing credentials. // It also means that a user can sign in without having to be prompted for // credentials. Once credentials are cleared from the stores ("sign-out"), From fb0ed9a9195c8f2041507c7efe20f18f4ae054d7 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 15 Feb 2023 11:51:40 -0800 Subject: [PATCH 050/244] Update Documentation/Authenticator/README.md Co-authored-by: Mark Dostal --- Documentation/Authenticator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Authenticator/README.md b/Documentation/Authenticator/README.md index a66ede1e1..177b579ea 100644 --- a/Documentation/Authenticator/README.md +++ b/Documentation/Authenticator/README.md @@ -39,7 +39,7 @@ To securely store credentials in the keychain, use the following extension metho ) async throws ``` -While sign-out, use the following extension method of `AuthenticationManager`: +During sign-out, use the following extension method of `AuthenticationManager`: ```swift /// Clears all ArcGIS and network credentials from the respective stores. From 3dfa8c211e6e54d7d280af6dc8794b802eba4b8d Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 15 Feb 2023 11:51:57 -0800 Subject: [PATCH 051/244] Update Documentation/Authenticator/README.md Co-authored-by: Mark Dostal --- Documentation/Authenticator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Authenticator/README.md b/Documentation/Authenticator/README.md index 177b579ea..0a30c6fd3 100644 --- a/Documentation/Authenticator/README.md +++ b/Documentation/Authenticator/README.md @@ -75,7 +75,7 @@ var body: some SwiftUI.Scene { HomeView() .authenticator(authenticator) .task { - // Here we setup credential stores to be persistent, which means that it will be + // Here we setup credential stores to be persistent, which means that it will // synchronize with the keychain for storing credentials. // It also means that a user can sign in without having to be prompted for // credentials. Once credentials are cleared from the stores ("sign-out"), From e39496ec06d0a40625c24d99f73e8070d9bf400e Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Thu, 16 Feb 2023 09:53:45 -0800 Subject: [PATCH 052/244] pr feedback from Ryan --- .../AuthenticationExample/ProfileView.swift | 1 + Documentation/Authenticator/README.md | 7 +- .../AuthenticationManager.swift | 82 +++++++++++++++++++ .../Authentication/Authenticator.swift | 61 -------------- 4 files changed, 88 insertions(+), 63 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift index 635341632..1fb1fed6f 100644 --- a/AuthenticationExample/AuthenticationExample/ProfileView.swift +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -61,6 +61,7 @@ struct ProfileView: View { func signOut() { isSigningOut = true Task { + await ArcGISEnvironment.authenticationManager.revokeOAuthTokens() await ArcGISEnvironment.authenticationManager.clearCredentialStores() isSigningOut = false signOutAction() diff --git a/Documentation/Authenticator/README.md b/Documentation/Authenticator/README.md index 0a30c6fd3..2368cbf81 100644 --- a/Documentation/Authenticator/README.md +++ b/Documentation/Authenticator/README.md @@ -39,9 +39,12 @@ To securely store credentials in the keychain, use the following extension metho ) async throws ``` -During sign-out, use the following extension method of `AuthenticationManager`: +During sign-out, use the following extension methods of `AuthenticationManager`: ```swift + /// Revokes tokens of OAuth user credentials. + func revokeOAuthTokens() async + /// Clears all ArcGIS and network credentials from the respective stores. /// Note: This sets up new `URLSessions` so that removed network credentials are respected /// right away. @@ -67,7 +70,7 @@ init() { ) // Sets authenticator as ArcGIS and Network challenge handlers to handle authentication // challenges. - ArcGISEnvironment.authenticationManager.handleAuthenticationChallenges(using: authenticator) + ArcGISEnvironment.authenticationManager.handleChallenges(using: authenticator) } var body: some SwiftUI.Scene { diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift new file mode 100644 index 000000000..2487f227c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift @@ -0,0 +1,82 @@ +// Copyright 2023 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 +// http://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 + +public extension AuthenticationManager { + /// Sets up authenticator as ArcGIS and Network challenge handlers to handle authentication + /// challenges. + /// - Parameter authenticator: The authenticator to be used for handling challenges. + func handleChallenges(using authenticator: Authenticator) { + arcGISAuthenticationChallengeHandler = authenticator + networkAuthenticationChallengeHandler = authenticator + } + + /// Sets up new credential stores that will be persisted to the keychain. + /// - Remark: The credentials will be stored in the default access group of the keychain. + /// You can find more information about what the default group would be here: + /// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + /// - Parameters: + /// - access: When the credentials stored in the keychain can be accessed. + /// - synchronizesWithiCloud: A Boolean value indicating whether the credentials are synchronized with iCloud. + func setupPersistentCredentialStorage( + access: ArcGIS.KeychainAccess, + synchronizesWithiCloud: Bool = false + ) async throws { + let previousArcGISCredentialStore = arcGISCredentialStore + + // Set a persistent ArcGIS credential store on the ArcGIS environment. + arcGISCredentialStore = try await .makePersistent( + access: access, + synchronizesWithiCloud: synchronizesWithiCloud + ) + + do { + // Set a persistent network credential store on the ArcGIS environment. + await setNetworkCredentialStore( + try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud) + ) + } catch { + // If making the shared network credential store persistent fails, + // then restore the ArcGIS credential store. + arcGISCredentialStore = previousArcGISCredentialStore + throw error + } + } + + /// Clears all ArcGIS and network credentials from the respective stores. + /// Note: This sets up new `URLSessions` so that removed network credentials are respected + /// right away. + func clearCredentialStores() async { + // Clear ArcGIS Credentials. + arcGISCredentialStore.removeAll() + + // Clear network credentials. + await networkCredentialStore.removeAll() + } + + /// Revokes tokens of OAuth user credentials. + func revokeOAuthTokens() async { + let oAuthUserCredentials = arcGISCredentialStore.credentials.compactMap { $0 as? OAuthUserCredential } + await withTaskGroup(of: Void.self) { group in + for credential in oAuthUserCredentials { + group.addTask { + try? await credential.revokeToken() + } + } + + // Make sure that all tasks complete. + await group.waitForAll() + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index d11c154b2..bd909618d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -88,64 +88,3 @@ extension Authenticator: NetworkAuthenticationChallengeHandler { return await challengeContinuation.value } } - -public extension AuthenticationManager { - /// Sets up authenticator as ArcGIS and Network challenge handlers to handle authentication - /// challenges. - /// - Parameter authenticator: The authenticator to be used for handling challenges. - func handleAuthenticationChallenges(using authenticator: Authenticator) { - arcGISAuthenticationChallengeHandler = authenticator - networkAuthenticationChallengeHandler = authenticator - } - - /// Sets up new credential stores that will be persisted to the keychain. - /// - Remark: The credentials will be stored in the default access group of the keychain. - /// You can find more information about what the default group would be here: - /// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps - /// - Parameters: - /// - access: When the credentials stored in the keychain can be accessed. - /// - synchronizesWithiCloud: A Boolean value indicating whether the credentials are synchronized with iCloud. - func setupPersistentCredentialStorage( - access: ArcGIS.KeychainAccess, - synchronizesWithiCloud: Bool = false - ) async throws { - let previousArcGISCredentialStore = arcGISCredentialStore - - // Set a persistent ArcGIS credential store on the ArcGIS environment. - arcGISCredentialStore = try await .makePersistent( - access: access, - synchronizesWithiCloud: synchronizesWithiCloud - ) - - do { - // Set a persistent network credential store on the ArcGIS environment. - await setNetworkCredentialStore( - try await .makePersistent(access: access, synchronizesWithiCloud: synchronizesWithiCloud) - ) - } catch { - // If making the shared network credential store persistent fails, - // then restore the ArcGIS credential store. - arcGISCredentialStore = previousArcGISCredentialStore - throw error - } - } - - /// Clears all ArcGIS and network credentials from the respective stores. - /// Note: This sets up new `URLSessions` so that removed network credentials are respected - /// right away. - func clearCredentialStores() async { - // Revoke tokens for OAuth user credentials. - let oAuthUserCredentials = arcGISCredentialStore.credentials.compactMap { $0 as? OAuthUserCredential } - oAuthUserCredentials.forEach { oAuthUserCredential in - Task { - try? await oAuthUserCredential.revokeToken() - } - } - - // Clear ArcGIS Credentials. - arcGISCredentialStore.removeAll() - - // Clear network credentials. - await networkCredentialStore.removeAll() - } -} From f8ecfd27f3c7d3b64768da1c9f3a13afe67ab9cb Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Thu, 16 Feb 2023 10:37:11 -0800 Subject: [PATCH 053/244] pr feedback from Ryan 2 --- .../AuthenticationApp.swift | 2 +- .../Authentication/AuthenticationManager.swift | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 30c9c5d96..8d0824cdc 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -28,7 +28,7 @@ struct AuthenticationApp: App { ) // Sets authenticator as ArcGIS and Network challenge handlers to handle authentication // challenges. - ArcGISEnvironment.authenticationManager.handleAuthenticationChallenges(using: authenticator) + ArcGISEnvironment.authenticationManager.handleChallenges(using: authenticator) } var body: some SwiftUI.Scene { diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift index 2487f227c..05a72dd07 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift @@ -66,17 +66,24 @@ public extension AuthenticationManager { } /// Revokes tokens of OAuth user credentials. - func revokeOAuthTokens() async { + /// - Returns: `true` if successfully revokes tokens for all `OAuthUserCredential`, otherwise + /// `false`. + @discardableResult + func revokeOAuthTokens() async -> Bool { let oAuthUserCredentials = arcGISCredentialStore.credentials.compactMap { $0 as? OAuthUserCredential } - await withTaskGroup(of: Void.self) { group in + return await withTaskGroup(of: Bool.self, returning: Bool.self) { group in for credential in oAuthUserCredentials { group.addTask { - try? await credential.revokeToken() + do { + try await credential.revokeToken() + return true + } catch { + return false + } } } - // Make sure that all tasks complete. - await group.waitForAll() + return await group.allSatisfy { $0 == true } } } } From 9dc682d1487971f3ae4b44b93726be2b53797f7d Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 17 Feb 2023 13:07:36 -0800 Subject: [PATCH 054/244] added revoke token test and moved auth manager tests in own file --- .../AuthenticationManagerTests.swift | 70 +++++++++++++++++++ .../AuthenticatorTests.swift | 35 ---------- 2 files changed, 70 insertions(+), 35 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift diff --git a/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift b/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift new file mode 100644 index 000000000..1fccdcfdb --- /dev/null +++ b/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift @@ -0,0 +1,70 @@ +// Copyright 2023 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 +// http://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 XCTest +import ArcGIS +@testable import ArcGISToolkit + +@MainActor final class AuthenticationManagerTests: XCTestCase { + func testHandleChallengesUsingAuthenticator() { + addTeardownBlock { + ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = nil + ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler = nil + } + + let authenticator = Authenticator() + ArcGISEnvironment.authenticationManager.handleChallenges(using: authenticator) + XCTAssertNotNil(ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler) + XCTAssertNotNil(ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler) + } + + func testMakePersistent() async throws { + // Make sure credential stores are restored. + addTeardownBlock { + ArcGISEnvironment.authenticationManager.arcGISCredentialStore = ArcGISCredentialStore() + await ArcGISEnvironment.authenticationManager.setNetworkCredentialStore(NetworkCredentialStore()) + } + + // This tests that calling setupPersistentCredentialStorage tries to sync with the keychain. + do { + try await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlocked) + XCTFail("Expected an error to be thrown as unit tests should not have access to the keychain") + } catch {} + } + + func testClearCredentialStores() async { + ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add( + PregeneratedTokenCredential( + url: URL(string: "www.arcgis.com")!, + tokenInfo: .init( + accessToken: "token", + expirationDate: .distantFuture, + isSSLRequired: false + )! + ) + ) + + var arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials + XCTAssertEqual(arcGISCreds.count, 1) + + await ArcGISEnvironment.authenticationManager.clearCredentialStores() + + arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials + XCTAssertTrue(arcGISCreds.isEmpty) + } + + func testRevokeOAuthTokens() async { + let result = await ArcGISEnvironment.authenticationManager.revokeOAuthTokens() + XCTAssertTrue(result) + } +} diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index 291a6186c..341898f09 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -27,39 +27,4 @@ import Combine XCTAssertTrue(authenticator.promptForUntrustedHosts) XCTAssertEqual(authenticator.oAuthUserConfigurations, [config]) } - - func testMakePersistent() async throws { - // Make sure credential stores are restored. - addTeardownBlock { - ArcGISEnvironment.authenticationManager.arcGISCredentialStore = ArcGISCredentialStore() - await ArcGISEnvironment.authenticationManager.setNetworkCredentialStore(NetworkCredentialStore()) - } - - // This tests that calling setupPersistentCredentialStorage tries to sync with the keychain. - do { - try await ArcGISEnvironment.authenticationManager.setupPersistentCredentialStorage(access: .whenUnlocked) - XCTFail("Expected an error to be thrown as unit tests should not have access to the keychain") - } catch {} - } - - func testClearCredentialStores() async { - ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add( - PregeneratedTokenCredential( - url: URL(string: "www.arcgis.com")!, - tokenInfo: .init( - accessToken: "token", - expirationDate: .distantFuture, - isSSLRequired: false - )! - ) - ) - - var arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials - XCTAssertEqual(arcGISCreds.count, 1) - - await ArcGISEnvironment.authenticationManager.clearCredentialStores() - - arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials - XCTAssertTrue(arcGISCreds.isEmpty) - } } From 0d168b92244e57c1c3b13609d4d7ab0be0a9da3e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 17 Feb 2023 16:09:23 -0800 Subject: [PATCH 055/244] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 99c512ab0..f0b9dd00c 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -24,7 +24,7 @@ struct FloorFilterExampleView: View { )) } - /// Determines the arrangement of the inner `FloorFilter` UI componenets. + /// Determines the arrangement of the inner `FloorFilter` UI components. private let floorFilterAlignment = Alignment.bottomLeading /// Determines the appropriate time to initialize the `FloorFilter`. From bc18dec48d3488c313619fa68a8d9894139116e0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:36:28 -0800 Subject: [PATCH 056/244] Improve formatting --- .../Components/FloorFilter/FloorFilter.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 6c6fea8a5..817c48720 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -35,11 +35,13 @@ public struct FloorFilter: View { viewpoint: Binding = .constant(nil), isNavigating: Binding ) { - _viewModel = StateObject(wrappedValue: FloorFilterViewModel( - automaticSelectionMode: automaticSelectionMode, - floorManager: floorManager, - viewpoint: viewpoint - )) + _viewModel = StateObject( + wrappedValue: FloorFilterViewModel( + automaticSelectionMode: automaticSelectionMode, + floorManager: floorManager, + viewpoint: viewpoint + ) + ) self.alignment = alignment self.isNavigating = isNavigating self.viewpoint = viewpoint From 24ae75dab992bf4f76a1aec1ec6116669754e29b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:37:54 -0800 Subject: [PATCH 057/244] No need for this to have a default value --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 8b446748e..bde4e7023 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -34,7 +34,7 @@ final class FloorFilterViewModel: ObservableObject { /// - floorManager: The floor manager used by the `FloorFilterViewModel`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. init( - automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, + automaticSelectionMode: FloorFilterAutomaticSelectionMode, floorManager: FloorManager, viewpoint: Binding ) { From 7e5357a9a02f5666c1d517d5e92d2f34723c6201 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:38:00 -0800 Subject: [PATCH 058/244] Typo fix --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index bde4e7023..670a43c05 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -191,7 +191,7 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Private items - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// The `Viewpoint` used to pan/zoom to the selected site/facility. /// If `nil`, there will be no automatic pan/zoom operations. private var viewpoint: Binding From 2577ed30d7af518f23bb716dba347d337639d090 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:38:14 -0800 Subject: [PATCH 059/244] Use updated syntax --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 670a43c05..f0012c184 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -259,7 +259,7 @@ final class FloorFilterViewModel: ObservableObject { return GeometryEngine.isGeometry(extent1, intersecting: extent2) } - if let siteResult = siteResult { + if let siteResult { setSite(siteResult) } else if automaticSelectionMode == .always { selection = nil From f227c6bb80ed7d8052bac54df1d7116a8ac29ada Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:38:31 -0800 Subject: [PATCH 060/244] Use computed property for brevity --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index f0012c184..81284bdca 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -251,7 +251,7 @@ final class FloorFilterViewModel: ObservableObject { } // If the centerpoint is within a site's geometry, select that site. - let siteResult = floorManager.sites.first { site in + let siteResult = sites.first { site in guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, let extent2 = site.geometry?.extent else { return false From f5370c7ab067d3fcda920c02d4e65c1056a9051f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:41:16 -0800 Subject: [PATCH 061/244] `FacilitiesList.allSiteStyle` -> `usesAllSitesStyling` --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 1bd8a9217..4448520e0 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -128,7 +128,7 @@ struct SiteAndFacilitySelector: View { ) ) { FacilitiesList( - allSiteStyle: false, + usesAllSitesStyling: false, facilities: site.facilities, isHidden: isHidden ) @@ -163,7 +163,7 @@ struct SiteAndFacilitySelector: View { @State var query: String = "" /// When `true`, the facilites list will be display with all sites styling. - let allSiteStyle: Bool + let usesAllSitesStyling: Bool /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] @@ -198,7 +198,7 @@ struct SiteAndFacilitySelector: View { .keyboardType(.alphabet) .disableAutocorrection(true) .navigationTitle( - allSiteStyle ? "All Sites" : viewModel.selectedSite?.name ?? "Select a facility" + usesAllSitesStyling ? "All Sites" : viewModel.selectedSite?.name ?? "Select a facility" ) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -225,7 +225,7 @@ struct SiteAndFacilitySelector: View { maxWidth: .infinity, alignment: .leading ) - if allSiteStyle, let siteName = facility.site?.name { + if usesAllSitesStyling, let siteName = facility.site?.name { Text(siteName) .font(.caption) .frame( From 3903a6304e27679969e8c9cb20b30e01b755a1bc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:42:59 -0800 Subject: [PATCH 062/244] Unify location of toolbar close button Resolves #244 --- .../FloorFilter/SiteAndFacilitySelector.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 4448520e0..1e76fd277 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -30,6 +30,11 @@ struct SiteAndFacilitySelector: View { var body: some View { SitesList(isHidden: isHidden) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + CloseButton { isHidden.wrappedValue.toggle() } + } + } } /// A view displaying the sites contained in a `FloorManager`. @@ -98,11 +103,6 @@ struct SiteAndFacilitySelector: View { .disableAutocorrection(true) .navigationTitle("Sites") .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - CloseButton { isHidden.wrappedValue.toggle() } - } - } } .navigationViewStyle(.stack) } @@ -201,11 +201,6 @@ struct SiteAndFacilitySelector: View { usesAllSitesStyling ? "All Sites" : viewModel.selectedSite?.name ?? "Select a facility" ) .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - CloseButton { isHidden.wrappedValue.toggle() } - } - } } /// Displays a list of facilities matching the filter criteria as determined by From f17ef3187bccf3df0463a2267fc8760795249d75 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:45:35 -0800 Subject: [PATCH 063/244] Unified location for navigationBarTitleDisplayMode(.inline) modifier --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 1e76fd277..e277f3577 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -30,6 +30,7 @@ struct SiteAndFacilitySelector: View { var body: some View { SitesList(isHidden: isHidden) + .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { CloseButton { isHidden.wrappedValue.toggle() } @@ -102,7 +103,6 @@ struct SiteAndFacilitySelector: View { .keyboardType(.alphabet) .disableAutocorrection(true) .navigationTitle("Sites") - .navigationBarTitleDisplayMode(.inline) } .navigationViewStyle(.stack) } @@ -200,7 +200,6 @@ struct SiteAndFacilitySelector: View { .navigationTitle( usesAllSitesStyling ? "All Sites" : viewModel.selectedSite?.name ?? "Select a facility" ) - .navigationBarTitleDisplayMode(.inline) } /// Displays a list of facilities matching the filter criteria as determined by From 03c154db791338b4b5bf1bc06e15520bdd85f707 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:47:07 -0800 Subject: [PATCH 064/244] Revise `SiteAndFacilitySelector.body` --- .../FloorFilter/SiteAndFacilitySelector.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index e277f3577..6953fd20f 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -29,13 +29,30 @@ struct SiteAndFacilitySelector: View { private var isHidden: Binding var body: some View { - SitesList(isHidden: isHidden) + NavigationView { + Group { + // If there's more than one site + if viewModel.sites.count > 1 { + // Show the list of sites for site selection + SitesList(isHidden: isHidden) + } else { + // Otherwise, there's zero or one site only, so show the list of facilities instead + FacilitiesList( + usesAllSitesStyling: false, + facilities: viewModel.facilities, + isHidden: isHidden + ) + .navigationBarBackButtonHidden(true) + } + } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { CloseButton { isHidden.wrappedValue.toggle() } } } + } + .navigationViewStyle(.stack) } /// A view displaying the sites contained in a `FloorManager`. From 36a7fd78d34791002cc1f58524188e25a16b13c3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:48:31 -0800 Subject: [PATCH 065/244] Introduce the allSitesButton view --- .../FloorFilter/SiteAndFacilitySelector.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 6953fd20f..8583688ab 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -116,12 +116,26 @@ struct SiteAndFacilitySelector: View { text: $query, placement: .navigationBarDrawer(displayMode: .always), prompt: "Filter sites" + allSitesButton + + /// The "All sites" button. + /// + /// This button presents the facilities list in a special format where the facilities list + /// shows every facility in every site within the floor manager. + var allSitesButton: some View { + NavigationLink("All sites") { + FacilitiesList( + usesAllSitesStyling: true, + facilities: viewModel.sites.flatMap(\.facilities), + isHidden: isHidden ) .keyboardType(.alphabet) .disableAutocorrection(true) .navigationTitle("Sites") } .navigationViewStyle(.stack) + .buttonStyle(.bordered) + .padding([.bottom], horizontalSizeClass == .compact ? 5 : 0) } /// A view containing a list of the site names. From be4c59f3b403d08d1ed48a1c20d19374aefa3812 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:49:07 -0800 Subject: [PATCH 066/244] Indentation --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 8583688ab..71b215936 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -117,6 +117,9 @@ struct SiteAndFacilitySelector: View { placement: .navigationBarDrawer(displayMode: .always), prompt: "Filter sites" allSitesButton + .keyboardType(.alphabet) + .disableAutocorrection(true) + .navigationTitle("Sites") /// The "All sites" button. /// @@ -129,9 +132,6 @@ struct SiteAndFacilitySelector: View { facilities: viewModel.sites.flatMap(\.facilities), isHidden: isHidden ) - .keyboardType(.alphabet) - .disableAutocorrection(true) - .navigationTitle("Sites") } .navigationViewStyle(.stack) .buttonStyle(.bordered) From 5fa2a914c226a514b31d1dd000eb38a7361fd5ee Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:49:55 -0800 Subject: [PATCH 067/244] Add new, simplified `SitesList.body` --- .../FloorFilter/SiteAndFacilitySelector.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 71b215936..274ba2309 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -111,15 +111,30 @@ struct SiteAndFacilitySelector: View { .buttonStyle(.bordered) .padding([.bottom], horizontalSizeClass == .compact ? 5 : 0) } + VStack { + // If the filtered set of sites is empty + if matchingSites.isEmpty { + // Show the "no matches" view + NoMatchesView() + } else { + // Show the filtered set of sites + siteListView } .searchable( text: $query, placement: .navigationBarDrawer(displayMode: .always), prompt: "Filter sites" allSitesButton + } + .searchable( + text: $query, + placement: .navigationBarDrawer(displayMode: .always), + prompt: "Filter sites" + ) .keyboardType(.alphabet) .disableAutocorrection(true) .navigationTitle("Sites") + } /// The "All sites" button. /// From 76aaadefa906d589943aba6d77330da2deb198d0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:51:15 -0800 Subject: [PATCH 068/244] Remove extra navigationViewStyle(.stack) modifier It used to be in two places, not it is used once in `SiteAndFacilitySelector.getter:body` --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 274ba2309..205fbc941 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -148,7 +148,6 @@ struct SiteAndFacilitySelector: View { isHidden: isHidden ) } - .navigationViewStyle(.stack) .buttonStyle(.bordered) .padding([.bottom], horizontalSizeClass == .compact ? 5 : 0) } From 4dea00a021ee3052fd92278bbd011b156990f75b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:51:40 -0800 Subject: [PATCH 069/244] Remove old `SitesList.getter:body` --- .../FloorFilter/SiteAndFacilitySelector.swift | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 205fbc941..2786b36d6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -86,31 +86,6 @@ struct SiteAndFacilitySelector: View { /// A view containing a filter-via-name field, a list of the site names and an "All sites" button. var body: some View { - NavigationView { - VStack { - if matchingSites.isEmpty { - NoMatchesView() - } else if viewModel.sites.count == 1 { - FacilitiesList( - allSiteStyle: false, - facilities: viewModel.sites.first?.facilities ?? [], - isHidden: isHidden - ) - .navigationBarBackButtonHidden(true) - } else { - siteListView - } - if viewModel.sites.count > 1 { - NavigationLink("All sites") { - FacilitiesList( - allSiteStyle: true, - facilities: viewModel.sites.flatMap(\.facilities), - isHidden: isHidden - ) - } - .buttonStyle(.bordered) - .padding([.bottom], horizontalSizeClass == .compact ? 5 : 0) - } VStack { // If the filtered set of sites is empty if matchingSites.isEmpty { @@ -120,10 +95,6 @@ struct SiteAndFacilitySelector: View { // Show the filtered set of sites siteListView } - .searchable( - text: $query, - placement: .navigationBarDrawer(displayMode: .always), - prompt: "Filter sites" allSitesButton } .searchable( From 2e5844a97d1f533d3a7b98236d0471dae754fe50 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 00:57:47 -0800 Subject: [PATCH 070/244] Doc improvement --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 2786b36d6..8f3bb8ba9 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -36,7 +36,7 @@ struct SiteAndFacilitySelector: View { // Show the list of sites for site selection SitesList(isHidden: isHidden) } else { - // Otherwise, there's zero or one site only, so show the list of facilities instead + // Otherwise there're no sites or only one site, show the list of facilities FacilitiesList( usesAllSitesStyling: false, facilities: viewModel.facilities, From f12188f6167e9b873b82f4b196859b27773a81a0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 01:26:44 -0800 Subject: [PATCH 071/244] Sort sites by name Per design doc --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 81284bdca..c7b54a7f3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -116,7 +116,7 @@ final class FloorFilterViewModel: ObservableObject { /// The floor manager sites. var sites: [FloorSite] { - floorManager.sites + floorManager.sites.sorted { $0.name < $1.name } } /// The selected facility's levels, sorted by `level.verticalOrder`. From e7fbd81fa61b048c85cabb04475c4916e3f2c1af Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 01:32:04 -0800 Subject: [PATCH 072/244] Sort facilities by name Per design --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 8f3bb8ba9..799d45f70 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -192,10 +192,11 @@ struct SiteAndFacilitySelector: View { var matchingFacilities: [FloorFacility] { guard !query.isEmpty else { return facilities + .sorted { $0.name < $1.name } } - return facilities.filter { - $0.name.localizedStandardContains(query) - } + return facilities + .filter { $0.name.localizedStandardContains(query) } + .sorted { $0.name < $1.name } } var body: some View { From 5edfa5b99f78782802cc7abb2b773634c21d1080 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 02:05:46 -0800 Subject: [PATCH 073/244] Simplify backout --- .../FloorFilter/SiteAndFacilitySelector.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 799d45f70..2acdc6404 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -148,15 +148,8 @@ struct SiteAndFacilitySelector: View { facilities: site.facilities, isHidden: isHidden ) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button { - userBackedOutOfSelectedSite = true - } label: { - Image(systemName: "chevron.left") - } - } + .onDisappear { + userBackedOutOfSelectedSite = true } } } From 0d58b1abc65f7e6034f456ccab176a7bc7a023cb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 02:06:03 -0800 Subject: [PATCH 074/244] Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 2acdc6404..35849d3d9 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -42,7 +42,7 @@ struct SiteAndFacilitySelector: View { facilities: viewModel.facilities, isHidden: isHidden ) - .navigationBarBackButtonHidden(true) + .navigationBarBackButtonHidden() } } .navigationBarTitleDisplayMode(.inline) From c8eef865208e90661f48d6e9ad5f3ce727e69c7e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 02:11:55 -0800 Subject: [PATCH 075/244] Apply close buttons in any navigation links Addresses #244 --- .../FloorFilter/SiteAndFacilitySelector.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 35849d3d9..18b754f82 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -118,6 +118,11 @@ struct SiteAndFacilitySelector: View { facilities: viewModel.sites.flatMap(\.facilities), isHidden: isHidden ) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + CloseButton { isHidden.wrappedValue.toggle() } + } + } } .buttonStyle(.bordered) .padding([.bottom], horizontalSizeClass == .compact ? 5 : 0) @@ -151,6 +156,11 @@ struct SiteAndFacilitySelector: View { .onDisappear { userBackedOutOfSelectedSite = true } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + CloseButton { isHidden.wrappedValue.toggle() } + } + } } } .listStyle(.plain) From 52f1406b3c5fb0cdf5e579d6fc413957c70b7b16 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 18 Feb 2023 02:17:56 -0800 Subject: [PATCH 076/244] Revert "Simplify backout" This reverts commit 5edfa5b99f78782802cc7abb2b773634c21d1080. --- .../FloorFilter/SiteAndFacilitySelector.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 18b754f82..c71a714fd 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -153,8 +153,15 @@ struct SiteAndFacilitySelector: View { facilities: site.facilities, isHidden: isHidden ) - .onDisappear { - userBackedOutOfSelectedSite = true + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + userBackedOutOfSelectedSite = true + } label: { + Image(systemName: "chevron.left") + } + } } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { From c0d82cb78d1fbcc8e62a4bf7a5f53e7c1e2434d0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 21 Feb 2023 09:24:27 -0800 Subject: [PATCH 077/244] Revert "No need for this to have a default value" This reverts commit 24ae75dab992bf4f76a1aec1ec6116669754e29b. --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index c7b54a7f3..cda5c6c41 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -34,7 +34,7 @@ final class FloorFilterViewModel: ObservableObject { /// - floorManager: The floor manager used by the `FloorFilterViewModel`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. init( - automaticSelectionMode: FloorFilterAutomaticSelectionMode, + automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, floorManager: FloorManager, viewpoint: Binding ) { From ffda99809a3a81d1a306147c5018fea7d398338c Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 21 Feb 2023 13:11:40 -0800 Subject: [PATCH 078/244] Update Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift Co-authored-by: David Feinzimer --- Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift b/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift index 1fccdcfdb..14182413d 100644 --- a/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift @@ -54,13 +54,11 @@ import ArcGIS ) ) - var arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials - XCTAssertEqual(arcGISCreds.count, 1) + XCTAssertEqual(ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials.count, 1) await ArcGISEnvironment.authenticationManager.clearCredentialStores() - arcGISCreds = ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials - XCTAssertTrue(arcGISCreds.isEmpty) + XCTAssertTrue(ArcGISEnvironment.authenticationManager.arcGISCredentialStore.credentials.isEmpty) } func testRevokeOAuthTokens() async { From 326ce1527ee3ebb29da9896cc30fbc9f173129e2 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 21 Feb 2023 13:38:59 -0800 Subject: [PATCH 079/244] update test as per code review from Ryan --- Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift b/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift index 14182413d..b01465514 100644 --- a/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticationManagerTests.swift @@ -24,8 +24,8 @@ import ArcGIS let authenticator = Authenticator() ArcGISEnvironment.authenticationManager.handleChallenges(using: authenticator) - XCTAssertNotNil(ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler) - XCTAssertNotNil(ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler) + XCTAssertIdentical(ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler as? Authenticator, authenticator) + XCTAssertIdentical(ArcGISEnvironment.authenticationManager.networkAuthenticationChallengeHandler as? Authenticator, authenticator) } func testMakePersistent() async throws { From 7c56891a1be4ef0d268a89e69d7f0756381baa3c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Feb 2023 12:41:00 -0800 Subject: [PATCH 080/244] Fix errors --- .../Components/BasemapGallery/BasemapGallery.swift | 2 +- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- .../ArcGISToolkit/Components/Search/SearchResult.swift | 2 +- .../ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 8 ++++---- .../UtilityNetworkTraceViewModel.swift | 4 +--- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index af4d5cd63..0c3a3445a 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -225,7 +225,7 @@ extension AlertItem { init(loadBasemapError: Error) { self.init( title: "Error loading basemap.", - message: "\((loadBasemapError as? ArcGISError)?.failureReason ?? "The basemap failed to load for an unknown reason.")" + message: "\((loadBasemapError as? ArcGISError)?.details ?? "The basemap failed to load for an unknown reason.")" ) } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 8b446748e..499580b36 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -310,7 +310,7 @@ final class FloorFilterViewModel: ObservableObject { let targetExtent = builder.toGeometry() if !targetExtent.isEmpty { viewpoint.wrappedValue = Viewpoint( - targetExtent: targetExtent + boundingGeometry: targetExtent ) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 1f4978da7..fbb933300 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -99,7 +99,7 @@ extension SearchResult { geometry: geocodeResult.displayLocation, attributes: geocodeResult.attributes ), - selectionViewpoint: geocodeResult.extent.map { .init(targetExtent: $0) } + selectionViewpoint: geocodeResult.extent.map { .init(boundingGeometry: $0) } ) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 2b6f164b8..29b369031 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -381,7 +381,7 @@ private extension SearchViewModel { builder.expand(factor: 1.1) let targetExtent = builder.toGeometry() viewpoint.wrappedValue = Viewpoint( - targetExtent: targetExtent + boundingGeometry: targetExtent ) lastSearchExtent = targetExtent } else { diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 3255d0109..25081e11e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -149,7 +149,7 @@ public struct UtilityNetworkTrace: View { Task { if let feature = await viewModel.feature(for: element), let geometry = feature.geometry { - viewpoint = Viewpoint(targetExtent: geometry.extent) + viewpoint = Viewpoint(boundingGeometry: geometry.extent) } } } label: { @@ -281,7 +281,7 @@ public struct UtilityNetworkTrace: View { currentActivity = .viewingTraces(nil) if shouldZoomOnTraceCompletion, let extent = viewModel.selectedTrace?.resultExtent { - viewpoint = Viewpoint(targetExtent: extent) + viewpoint = Viewpoint(boundingGeometry: extent) } } } @@ -324,7 +324,7 @@ public struct UtilityNetworkTrace: View { Menu(selectedTrace.name) { if let resultExtent = selectedTrace.resultExtent { Button("Zoom To") { - viewpoint = Viewpoint(targetExtent: resultExtent) + viewpoint = Viewpoint(boundingGeometry: resultExtent) } } Button("Delete", role: .destructive) { @@ -439,7 +439,7 @@ public struct UtilityNetworkTrace: View { Button("Zoom To") { if let selectedStartingPoint = selectedStartingPoint, let extent = selectedStartingPoint.geoElement.geometry?.extent { - viewpoint = Viewpoint(targetExtent: extent) + viewpoint = Viewpoint(boundingGeometry: extent) } } Button("Delete", role: .destructive) { diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index b58a5bb97..7d8102416 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -364,9 +364,7 @@ import SwiftUI do { traceResults = try await network.trace(using: parameters) } catch(let serviceError as ServiceError) { - if let reason = serviceError.failureReason { - userAlert = .init(description: reason) - } + userAlert = .init(description: serviceError.details) return false } catch { userAlert = .init(description: error.localizedDescription) From 7898052449390ffb4188b369a255647758516f0d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 28 Feb 2023 12:21:07 -0800 Subject: [PATCH 081/244] Remove `onTapGesture` from compass --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index b21456bd8..f2dbe9114 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -60,7 +60,6 @@ public struct Compass: View { } .aspectRatio(1, contentMode: .fit) .opacity(opacity) - .onTapGesture { heading = .zero } .frame(width: size, height: size) .onChange(of: heading) { _ in let newOpacity: Double = shouldHide ? .zero : 1 From 5ca7dba2b470e16602bb7fd77d7021e6ddbc0095 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 28 Feb 2023 13:19:30 -0800 Subject: [PATCH 082/244] Update samples --- Examples/Examples/CompassExampleView.swift | 163 ++++++++++++++++++++- 1 file changed, 157 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 137cf8808..a6d8e171a 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -15,7 +15,48 @@ import ArcGIS import ArcGISToolkit import SwiftUI +/// An example demonstrating how to use a compass in three different environments. struct CompassExampleView: View { + /// A scenario represents a type of environment a compass may be used in. + enum Scenario: CaseIterable { + case map + case sceneWithCamera + case sceneWithCameraController + + /// A human-readable label for the scenario. + var label: String { + switch self { + case .map: return "Map" + case .sceneWithCamera: return "Scene with camera" + case .sceneWithCameraController: return "Scene with camera controller" + } + } + } + + /// The active scenario. + @State private var scenario = Scenario.map + + var body: some View { + VStack { + switch scenario { + case.map: + MapWithViewpoint() + case .sceneWithCamera: + SceneWithCamera() + case .sceneWithCameraController: + SceneWithCameraController() + } + Picker("Scenario", selection: $scenario) { + ForEach(Scenario.allCases, id: \.self) { scen in + Text(scen.label) + } + } + } + } +} + +/// An example demonstrating how to use a compass with a map view. +struct MapWithViewpoint: View { /// The data model containing the `Map` displayed in the `MapView`. @StateObject private var dataModel = MapDataModel( map: Map(basemapStyle: .arcGISImagery) @@ -23,19 +64,129 @@ struct CompassExampleView: View { /// Allows for communication between the Compass and MapView or SceneView. @State private var viewpoint: Viewpoint? = Viewpoint( - center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), + center: .esriRedlands, scale: 10_000, rotation: -45 ) var body: some View { - MapView(map: dataModel.map, viewpoint: viewpoint) - .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint) + MapViewReader { mapViewProxy in + MapView(map: dataModel.map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: .topTrailing) { + Compass(viewpoint: $viewpoint) // Optionally provide a different size for the compass. - // .compassSize(size: <#T##CGFloat#>) + // .compassSize(size: T##CGFloat) + .padding() + .onTapGesture { + Task { + try? await mapViewProxy.setViewpointRotation(0) + } + } + } + } + } +} + +/// An example demonstrating how to use a compass with a scene view and camera. +struct SceneWithCamera: View { + /// The camera used by the scene view. + @State private var camera: Camera? = Camera( + lookingAt: .esriRedlands, + distance: 1_000, + heading: 45, + pitch: 45, + roll: .zero + ) + + /// The data model containing the `Scene` displayed in the `SceneView`. + @StateObject private var dataModel = SceneDataModel( + scene: Scene(basemapStyle: .arcGISImagery) + ) + + /// The current heading as reported by the scene view. + var heading: Binding { + Binding { + if let camera { + return camera.heading + } else { + return .zero + } + } set: { _ in + } + } + + var body: some View { + SceneViewReader { sceneViewProxy in + SceneView(scene: dataModel.scene, camera: $camera) + .overlay(alignment: .topTrailing) { + Compass(viewpointRotation: heading) + .padding() + .onTapGesture { + if let camera { + let newCamera = Camera( + location: camera.location, + heading: .zero, + pitch: camera.pitch, + roll: camera.roll + ) + Task { + try? await sceneViewProxy.setViewpointCamera( + newCamera, + duration: 0.3 + ) + } + } + } + } + } + } +} + +/// An example demonstrating how to use a compass with a scene view and camera controller. +struct SceneWithCameraController: View { + /// The data model containing the `Scene` displayed in the `SceneView`. + @StateObject private var dataModel = SceneDataModel( + scene: Scene(basemapStyle: .arcGISImagery) + ) + + /// The current heading as reported by the scene view. + @State private var heading: Double = .zero + + /// The orbit location camera controller used by the scene view. + private let cameraController = OrbitLocationCameraController( + targetPoint: .esriRedlands, + distance: 10_000 + )! + + var body: some View { + SceneView(scene: dataModel.scene, cameraController: cameraController) + .onCameraChanged { newCamera in + heading = newCamera.heading.rounded() + } + .overlay(alignment: .topTrailing) { + Compass(viewpointRotation: $heading) .padding() + .onTapGesture { + Task { + try? await cameraController.moveCamera( + distanceDelta: .zero, + headingDelta: heading > 180 ? 360 - heading : -heading, + pitchDelta: .zero, + duration: 0.3 + ) + } + } } } } + +private extension Point { + static var esriRedlands: Point { + .init( + x: -117.19494, + y: 34.05723, + spatialReference: .wgs84 + ) + } +} From 0dbd77b811e024e4ef6baede740f8a50e3793c7a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 2 Mar 2023 14:32:29 -0800 Subject: [PATCH 083/244] Make changes --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 499580b36..bfe4c925d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -306,7 +306,7 @@ final class FloorFilterViewModel: ObservableObject { } let builder = EnvelopeBuilder(envelope: extent) - builder.expand(factor: 1.5) + builder.expand(by: 1.5) let targetExtent = builder.toGeometry() if !targetExtent.isEmpty { viewpoint.wrappedValue = Viewpoint( diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 29b369031..9a45f9600 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -378,7 +378,7 @@ private extension SearchViewModel { let envelope = resultsOverlay.extent, shouldZoomToResults { let builder = EnvelopeBuilder(envelope: envelope) - builder.expand(factor: 1.1) + builder.expand(by: 1.1) let targetExtent = builder.toGeometry() viewpoint.wrappedValue = Viewpoint( boundingGeometry: targetExtent From 1330c787c425e4b24d9311816f6c64b528f23052 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 2 Mar 2023 14:37:37 -0800 Subject: [PATCH 084/244] Fix tests --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 96efabfcb..fd671fe6b 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -140,7 +140,7 @@ class SearchViewModelTests: XCTestCase { XCTAssertFalse(model.isEligibleForRequery) // Offset extent by 10% - isEligibleForRequery should still be `false`. - var builder = EnvelopeBuilder(envelope: model.geoViewExtent) + var builder = EnvelopeBuilder(envelope: model.geoViewExtent!) let tenPercentWidth = model.geoViewExtent!.width * 0.1 builder.offsetBy(x: tenPercentWidth, y: 0.0) var newExtent = builder.toGeometry() @@ -149,7 +149,7 @@ class SearchViewModelTests: XCTestCase { XCTAssertFalse(model.isEligibleForRequery) // Offset extent by 50% - isEligibleForRequery should now be `true`. - builder = EnvelopeBuilder(envelope: model.geoViewExtent) + builder = EnvelopeBuilder(envelope: model.geoViewExtent!) let fiftyPercentWidth = model.geoViewExtent!.width * 0.5 builder.offsetBy(x: fiftyPercentWidth, y: 0.0) newExtent = builder.toGeometry() @@ -167,16 +167,16 @@ class SearchViewModelTests: XCTestCase { XCTAssertFalse(model.isEligibleForRequery) // Expand extent by 1.1x - isEligibleForRequery should still be `false`. - builder = EnvelopeBuilder(envelope: model.geoViewExtent) - builder.expand(factor: 1.1) + builder = EnvelopeBuilder(envelope: model.geoViewExtent!) + builder.expand(by: 1.1) newExtent = builder.toGeometry() model.geoViewExtent = newExtent XCTAssertFalse(model.isEligibleForRequery) // Expand extent by 1.5x - isEligibleForRequery should now be `true`. - builder = EnvelopeBuilder(envelope: model.geoViewExtent) - builder.expand(factor: 1.5) + builder = EnvelopeBuilder(envelope: model.geoViewExtent!) + builder.expand(by: 1.5) newExtent = builder.toGeometry() model.geoViewExtent = newExtent From fe79d19605c3f651f9e250e04995b0827a945311 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 7 Mar 2023 16:47:07 -0800 Subject: [PATCH 085/244] Reorder parameters --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index ba8cac697..3480c4caf 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -82,9 +82,9 @@ public struct Bookmarks: View { bookmarks: [Bookmark], viewpoint: Binding? = nil ) { + _isPresented = isPresented self.bookmarks = bookmarks self.viewpoint = viewpoint - _isPresented = isPresented } /// Creates a `Bookmarks` component. From 49a0d41bca373de545c9eebe66c71a5765904474 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 15 Mar 2023 17:17:20 -0700 Subject: [PATCH 086/244] `elementsByTypeInGroup(named:)` -> `elementsByType(inGroupNamed:)` https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/pull/234#discussion_r1086795732 --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 2 +- .../UtilityNetworkTraceViewModelTrace.swift | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 25081e11e..12e3c5136 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -132,7 +132,7 @@ public struct UtilityNetworkTrace: View { /// Displays information about a chosen asset group. @ViewBuilder private var assetGroupDetail: some View { if let assetGroupName = selectedAssetGroupName, - let assetTypeGroups = viewModel.selectedTrace?.elementsByTypeInGroup(named: assetGroupName) { + let assetTypeGroups = viewModel.selectedTrace?.elementsByType(inGroupNamed: assetGroupName) { makeBackButton(title: featureResultsTitle) { currentActivity = .viewingTraces(.viewingFeatureResults) } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 41bc4f038..9b225631e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -68,11 +68,12 @@ extension UtilityNetworkTraceViewModel { } extension UtilityNetworkTraceViewModel.Trace { - /// - Parameter name: A name of a utility asset group. - /// - Returns: The set of utility elements returned by the trace that belong to the provided + /// - Finds the set of utility elements returned by the trace that belong to the provided /// asset group, grouped by type. - func elementsByTypeInGroup(named name: String) -> [String: [UtilityElement]] { - let assetsInGroup = elementsInAssetGroup(named: name) + /// - Parameter groupName: A name of a utility asset group. + /// - Returns: The elements in the indicated group. + func elementsByType(inGroupNamed groupName: String) -> [String: [UtilityElement]] { + let assetsInGroup = elementsInAssetGroup(named: groupName) var result = [String : [UtilityElement]]() assetsInGroup.forEach { e in var assetTypeGroup = result[e.assetType.name, default: []] From e7736dacbc8cc77afb7938c458a486c9a769d368 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 15 Mar 2023 17:23:56 -0700 Subject: [PATCH 087/244] `elementsInAssetGroup(named:)` -> `elements(inAssetGroupNamed:)` https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/pull/234#discussion_r1086816175 --- .../UtilityNetworkTraceViewModelTrace.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 9b225631e..ef484328d 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -68,12 +68,12 @@ extension UtilityNetworkTraceViewModel { } extension UtilityNetworkTraceViewModel.Trace { - /// - Finds the set of utility elements returned by the trace that belong to the provided + /// Finds the set of utility elements returned by the trace that belong to the provided /// asset group, grouped by type. /// - Parameter groupName: A name of a utility asset group. /// - Returns: The elements in the indicated group. func elementsByType(inGroupNamed groupName: String) -> [String: [UtilityElement]] { - let assetsInGroup = elementsInAssetGroup(named: groupName) + let assetsInGroup = elements(inAssetGroupNamed: groupName) var result = [String : [UtilityElement]]() assetsInGroup.forEach { e in var assetTypeGroup = result[e.assetType.name, default: []] @@ -83,11 +83,12 @@ extension UtilityNetworkTraceViewModel.Trace { return result } - /// - Parameter name: A name of a utility asset group. - /// - Returns: The set of utility elements returned by the trace that belong to the provided + /// Finds the set of utility elements returned by the trace that belong to the provided /// asset group. - func elementsInAssetGroup(named name: String) -> [UtilityElement] { - return elementResults.filter({ $0.assetGroup.name == name }) + /// - Parameter assetGroupName: A name of a utility asset group. + /// - Returns: The elements in the indicated group. + func elements(inAssetGroupNamed assetGroupName: String) -> [UtilityElement] { + return elementResults.filter({ $0.assetGroup.name == assetGroupName }) } /// A set of the asset group names returned by the trace. From e8aa082451792d9e0e28c20cefd6aa6c8734bf44 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 15 Mar 2023 17:24:42 -0700 Subject: [PATCH 088/244] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 12e3c5136..55dcc189c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -351,7 +351,7 @@ public struct UtilityNetworkTrace: View { HStack { Text(assetGroupName) Spacer() - Text(selectedTrace.elementsInAssetGroup(named: assetGroupName).count, format: .number) + Text(selectedTrace.elements(inAssetGroupNamed: assetGroupName).count, format: .number) } .foregroundColor(.blue) .contentShape(Rectangle()) From c37d8e0c45dd828a88af2e32f026e25504138ceb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 15 Mar 2023 17:30:15 -0700 Subject: [PATCH 089/244] Refactor `elementsByType(inGroupNamed:)` https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/pull/234#discussion_r1086814537 --- .../UtilityNetworkTraceViewModelTrace.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index ef484328d..5f4897c15 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -73,14 +73,13 @@ extension UtilityNetworkTraceViewModel.Trace { /// - Parameter groupName: A name of a utility asset group. /// - Returns: The elements in the indicated group. func elementsByType(inGroupNamed groupName: String) -> [String: [UtilityElement]] { - let assetsInGroup = elements(inAssetGroupNamed: groupName) - var result = [String : [UtilityElement]]() - assetsInGroup.forEach { e in - var assetTypeGroup = result[e.assetType.name, default: []] - assetTypeGroup.append(e) - result.updateValue(assetTypeGroup, forKey: e.assetType.name) - } - return result + elements(inAssetGroupNamed: name) + .reduce(into: [:]) { result, element in + let key = element.assetType.name + var assetTypeGroup = result[key, default: []] + assetTypeGroup.append(element) + result[key] = assetTypeGroup + } } /// Finds the set of utility elements returned by the trace that belong to the provided From d722be1a623756ed240b82b9363587738d88f28a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 15 Mar 2023 17:31:49 -0700 Subject: [PATCH 090/244] Refactor `elements(inAssetGroupNamed:)` https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/pull/234#discussion_r1086816888 --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 5f4897c15..9404fa8c8 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -87,7 +87,7 @@ extension UtilityNetworkTraceViewModel.Trace { /// - Parameter assetGroupName: A name of a utility asset group. /// - Returns: The elements in the indicated group. func elements(inAssetGroupNamed assetGroupName: String) -> [UtilityElement] { - return elementResults.filter({ $0.assetGroup.name == assetGroupName }) + elementResults.filter { $0.assetGroup.name == assetGroupName } } /// A set of the asset group names returned by the trace. From 24b51d4909757f0d47aa0af7ba3a654c5480f92a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 15 Mar 2023 17:32:52 -0700 Subject: [PATCH 091/244] Refactor `var assetGroupNames` https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/pull/234#discussion_r1086817826 --- .../UtilityNetworkTraceViewModelTrace.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 9404fa8c8..603eabc5c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -92,11 +92,7 @@ extension UtilityNetworkTraceViewModel.Trace { /// A set of the asset group names returned by the trace. var assetGroupNames: Set { - var assetGroupNames = Set() - elementResults.forEach { - assetGroupNames.insert($0.assetGroup.name) - } - return assetGroupNames + Set(elementResults.map(\.assetGroup.name)) } /// The extent of the trace's geometry result with a small added buffer. From 19d858cfec89b41943f6fd8b10cbce99d7cf58e9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 15 Mar 2023 17:34:43 -0700 Subject: [PATCH 092/244] Refactor Equatable conformanc https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/pull/234#discussion_r1086821755 --- .../UtilityNetworkTraceViewModelTrace.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 603eabc5c..3415058f0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -122,10 +122,7 @@ extension UtilityNetworkTraceViewModel.Trace { } extension UtilityNetworkTraceViewModel.Trace: Equatable { - static func == ( - lhs: UtilityNetworkTraceViewModel.Trace, - rhs: UtilityNetworkTraceViewModel.Trace - ) -> Bool { + static func == (lhs: Self, rhs: Self) -> Bool { return lhs.id == rhs.id } } From bc35ab23811aefa28782de8d2db3501bd408ad9f Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Mon, 20 Mar 2023 14:05:00 -0700 Subject: [PATCH 093/244] updated authentication example and auth manager extension --- .../AuthenticationExample/ProfileView.swift | 3 --- AuthenticationExample/AuthenticationExample/SignInView.swift | 5 +---- .../Components/Authentication/AuthenticationManager.swift | 5 +++-- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift index 1fb1fed6f..948aa2f6e 100644 --- a/AuthenticationExample/AuthenticationExample/ProfileView.swift +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -17,9 +17,6 @@ import ArcGISToolkit /// A view that displays the profile of a user. struct ProfileView: View { - /// The authenticator that has been passed through the environment down to the app. - @EnvironmentObject var authenticator: Authenticator - /// The portal that the user is signed in to. @State var portal: Portal diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index 90af4db85..8c38fe680 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -17,10 +17,7 @@ import ArcGISToolkit import CryptoKit /// A view that allows the user to sign in to a portal. -struct SignInView: View { - /// The authenticator which has been passed from the app through the environment. - @EnvironmentObject var authenticator: Authenticator - +struct SignInView: View { /// The error that occurred during an attempt to sign in. @State var error: Error? diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift index 05a72dd07..819f18d6b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationManager.swift @@ -16,8 +16,9 @@ import ArcGIS public extension AuthenticationManager { /// Sets up authenticator as ArcGIS and Network challenge handlers to handle authentication /// challenges. - /// - Parameter authenticator: The authenticator to be used for handling challenges. - func handleChallenges(using authenticator: Authenticator) { + /// - Parameter authenticator: The authenticator to be used for handling challenges or `nil` to + /// reset the challenge handlers. + func handleChallenges(using authenticator: Authenticator?) { arcGISAuthenticationChallengeHandler = authenticator networkAuthenticationChallengeHandler = authenticator } From 2d8bf10ef7f108a337a12e72c318da9d3f26e3d2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 13:10:52 -0700 Subject: [PATCH 094/244] Change map to a simple state property Based on the latest Swift guidance this is acceptable and it simplifies the sample --- Examples/Examples/FloorFilterExampleView.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index e65fb6dc3..ff3179f5a 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -46,13 +46,11 @@ struct FloorFilterExampleView: View { ) /// The data model containing the `Map` displayed in the `MapView`. - @StateObject private var dataModel = MapDataModel( - map: makeMap() - ) + @State private var map = makeMap() var body: some View { MapView( - map: dataModel.map, + map: map, viewpoint: viewpoint ) .onNavigatingChanged { @@ -65,7 +63,7 @@ struct FloorFilterExampleView: View { .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: floorFilterAlignment) { if isMapLoaded, - let floorManager = dataModel.map.floorManager { + let floorManager = map.floorManager { FloorFilter( floorManager: floorManager, alignment: floorFilterAlignment, @@ -92,7 +90,7 @@ struct FloorFilterExampleView: View { } .task { do { - try await dataModel.map.load() + try await map.load() isMapLoaded = true } catch { mapLoadError = true From a027519bc83dab3b64600996c4d0b292ee429949 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 13:13:11 -0700 Subject: [PATCH 095/244] Initialize map on one line Many of the other samples do this, including, Basemap, Bookmarks and Compass --- Examples/Examples/FloorFilterExampleView.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index ff3179f5a..0bc546437 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -49,10 +49,7 @@ struct FloorFilterExampleView: View { @State private var map = makeMap() var body: some View { - MapView( - map: map, - viewpoint: viewpoint - ) + MapView(map: map, viewpoint: viewpoint) .onNavigatingChanged { isNavigating = $0 } From 27a61208f3d5f938ded2c9f949072f7215e9b167 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 13:13:44 -0700 Subject: [PATCH 096/244] Remove third slash This is not a docc comment so it doesn't need the third slash --- Examples/Examples/FloorFilterExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 0bc546437..6c4966914 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -56,7 +56,7 @@ struct FloorFilterExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - /// Preserve the current viewpoint when a keyboard is presented in landscape. + // Preserve the current viewpoint when a keyboard is presented in landscape. .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: floorFilterAlignment) { if isMapLoaded, From 58118d3543feb9d6f94f2f9652c18d22bb83d7ed Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 13:14:56 -0700 Subject: [PATCH 097/244] Shorten sample Since there is only a single statement in these closures, putting them all in one line should be acceptable. --- Examples/Examples/FloorFilterExampleView.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 6c4966914..897b1fd97 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -50,12 +50,8 @@ struct FloorFilterExampleView: View { var body: some View { MapView(map: map, viewpoint: viewpoint) - .onNavigatingChanged { - isNavigating = $0 - } - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 - } + .onNavigatingChanged { isNavigating = $0 } + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } // Preserve the current viewpoint when a keyboard is presented in landscape. .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: floorFilterAlignment) { From 10f5027637a96759ec6266168fe11fbfcdf6601a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 13:16:21 -0700 Subject: [PATCH 098/244] Remove completely unused property --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 817c48720..e77fc5a8d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -50,9 +50,6 @@ public struct FloorFilter: View { /// The view model used by the `FloorFilter`. @StateObject private var viewModel: FloorFilterViewModel - /// A Boolean value that indicates whether the levels view is currently collapsed. - @State private var isLevelsViewCollapsed = false - /// A Boolean value that indicates whether the site and facility selector is presented. @State private var isSitesAndFacilitiesHidden = true From c83e67e80380053f921fb3ff14b946c13dfe026a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 13:23:30 -0700 Subject: [PATCH 099/244] Better adhere to margin line --- .../Components/FloorFilter/LevelSelector.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 050422ffd..b7b61dfed 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -20,8 +20,8 @@ struct LevelSelector: View { @EnvironmentObject var viewModel: FloorFilterViewModel /// A Boolean value indicating the whether the view shows only the selected level or all levels. - /// If the value is`false`, the view will display all levels; if it is `true`, the view will only display - /// the selected level. + /// If the value is`false`, the view will display all levels; if it is `true`, the view will + /// only display the selected level. @State private var isCollapsed: Bool = false /// The alignment configuration. @@ -30,8 +30,8 @@ struct LevelSelector: View { /// The levels to display. let levels: [FloorLevel] - /// The short name of the currently selected level, the first level, or "None" if none of the levels - /// are available. + /// The short name of the currently selected level, the first level, or "None" if none of the + /// levels are available. private var selectedLevelName: String { viewModel.selectedLevel?.shortName ?? "" } From cc30a38efc56c6cdc68efb5f92f786efb7c76cea Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 14:00:12 -0700 Subject: [PATCH 100/244] `floorManager` can be private --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index d36746d5e..74cb8052e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -59,7 +59,7 @@ final class FloorFilterViewModel: ObservableObject { private let automaticSelectionMode: FloorFilterAutomaticSelectionMode /// The `FloorManager` containing the site, floor, and level information. - let floorManager: FloorManager + private let floorManager: FloorManager // MARK: Properties From 9ef8f8f320edc756ae482e6255aca20a33154f21 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:06:35 -0700 Subject: [PATCH 101/244] Add selection binding --- .../Examples/FloorFilterExampleView.swift | 19 +++++++++++++++ .../Components/FloorFilter/FloorFilter.swift | 22 +++++++++++++++++ .../FloorFilter/FloorFilterSelection.swift | 24 +++++++++++++++++++ .../FloorFilter/FloorFilterViewModel.swift | 12 +--------- 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 897b1fd97..004b28ddf 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -48,6 +48,11 @@ struct FloorFilterExampleView: View { /// The data model containing the `Map` displayed in the `MapView`. @State private var map = makeMap() + /// The filter's current selection. This may be a site, facility, or level. + /// + /// Optionally monitor or update the filter's selection. + @State private var selectedItem: FloorFilterSelection? + var body: some View { MapView(map: map, viewpoint: viewpoint) .onNavigatingChanged { isNavigating = $0 } @@ -63,6 +68,7 @@ struct FloorFilterExampleView: View { viewpoint: $viewpoint, isNavigating: $isNavigating ) + .selection($selectedItem) .frame( maxWidth: 400, maxHeight: 400 @@ -89,5 +95,18 @@ struct FloorFilterExampleView: View { mapLoadError = true } } + .onChange(of: selectedItem) { newValue in + print("The selection changed") + switch newValue { + case .none: + print("None") + case .site(let site): + print("Site:", site.name) + case .facility(let facility): + print("Facility:", facility.site?.name ?? "", facility.name) + case .level(let level): + print("Level:", level.facility?.site?.name ?? "", level.facility?.name ?? "", level.verticalOrder + 1) + } + } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index e77fc5a8d..bc4898785 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -53,6 +53,9 @@ public struct FloorFilter: View { /// A Boolean value that indicates whether the site and facility selector is presented. @State private var isSitesAndFacilitiesHidden = true + /// The selected site, floor, or level. + private var selection: Binding? + /// The alignment configuration. private let alignment: Alignment @@ -173,5 +176,24 @@ public struct FloorFilter: View { .frame(minHeight: 100) .environmentObject(viewModel) .disabled(viewModel.isLoading) + .onChange(of: selection?.wrappedValue) { newValue in + // Prevent a double-set if the view model triggered the original change. + guard newValue != viewModel.selection else { return } + viewModel.selection = newValue + } + .onChange(of: viewModel.selection) { newValue in + // Prevent a double-set if the user triggered the original change. + guard selection?.wrappedValue != newValue else { return } + selection?.wrappedValue = newValue + } + } + + /// The currently selected site, facility, or level. + /// - Parameter selection: The selection. + /// - Returns: The `FloorFilter`. + public func selection(_ selection: Binding) -> Self { + var copy = self + copy.selection = selection + return copy } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift new file mode 100644 index 000000000..6e126655b --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift @@ -0,0 +1,24 @@ +// Copyright 2023 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 +// http://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 + +/// A selected site, facility, or level. +public enum FloorFilterSelection: Hashable { + /// A selected site. + case site(FloorSite) + /// A selected facility. + case facility(FloorFacility) + /// A selected level. + case level(FloorLevel) +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 74cb8052e..a145ed090 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -18,16 +18,6 @@ import SwiftUI /// Manages the state for a `FloorFilter`. @MainActor final class FloorFilterViewModel: ObservableObject { - /// A selected site, floor, or level. - enum Selection: Hashable { - /// A selected site. - case site(FloorSite) - /// A selected facility. - case facility(FloorFacility) - /// A selected level. - case level(FloorLevel) - } - /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - automaticSelectionMode: The selection behavior of the floor filter. @@ -51,7 +41,7 @@ final class FloorFilterViewModel: ObservableObject { @Published private(set) var isLoading = true /// The selected site, floor, or level. - @Published private(set) var selection: Selection? + @Published var selection: FloorFilterSelection? // MARK: Constants From 14b7f8a840f09d6add2fa900d1838b1a355d8b6c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:24:48 -0700 Subject: [PATCH 102/244] Restore the model's selection to private set The set*() methods exist because additional logic is needed when a selection is made --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index a145ed090..2132d410b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -41,7 +41,7 @@ final class FloorFilterViewModel: ObservableObject { @Published private(set) var isLoading = true /// The selected site, floor, or level. - @Published var selection: FloorFilterSelection? + @Published private(set) var selection: FloorFilterSelection? // MARK: Constants From 60f7d3a5315ff6d7747ec69fb2195ac77dd95d26 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:25:26 -0700 Subject: [PATCH 103/244] Introduce `clearSelection()` --- .../Components/FloorFilter/FloorFilterViewModel.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 2132d410b..c74ab901f 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -117,6 +117,11 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Methods + /// Sets the current selection to `nil`. + func clearSelection() { + selection = nil + } + /// Allows model users to alert the model that the viewpoint has changed. func onViewpointChanged(_ viewpoint: Viewpoint?) { guard let viewpoint = viewpoint, @@ -235,7 +240,7 @@ final class FloorFilterViewModel: ObservableObject { // If viewpoint is out of range, reset selection and return early. if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { if automaticSelectionMode == .always { - selection = nil + clearSelection() } return false } @@ -252,7 +257,7 @@ final class FloorFilterViewModel: ObservableObject { if let siteResult { setSite(siteResult) } else if automaticSelectionMode == .always { - selection = nil + clearSelection() } return true } From 3a7cab07e99188504ea8c48016c85efde61d5ef0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:26:17 -0700 Subject: [PATCH 104/244] Update the selection with the provided members --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index bc4898785..50f4c8202 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -179,7 +179,12 @@ public struct FloorFilter: View { .onChange(of: selection?.wrappedValue) { newValue in // Prevent a double-set if the view model triggered the original change. guard newValue != viewModel.selection else { return } - viewModel.selection = newValue + switch newValue { + case .site(let site): viewModel.setSite(site) + case .facility(let facility): viewModel.setFacility(facility) + case .level(let level): viewModel.setLevel(level) + case .none: viewModel.clearSelection() + } } .onChange(of: viewModel.selection) { newValue in // Prevent a double-set if the user triggered the original change. From ebafaba388ca8fbf99dece0f7e6fb29ec97f6f9e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:38:56 -0700 Subject: [PATCH 105/244] Pull sample usage --- Examples/Examples/FloorFilterExampleView.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 004b28ddf..6581f8f23 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -95,18 +95,5 @@ struct FloorFilterExampleView: View { mapLoadError = true } } - .onChange(of: selectedItem) { newValue in - print("The selection changed") - switch newValue { - case .none: - print("None") - case .site(let site): - print("Site:", site.name) - case .facility(let facility): - print("Facility:", facility.site?.name ?? "", facility.name) - case .level(let level): - print("Level:", level.facility?.site?.name ?? "", level.facility?.name ?? "", level.verticalOrder + 1) - } - } } } From 5df59fe195f161a156e1d7e04ef66ba51478fca3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:39:16 -0700 Subject: [PATCH 106/244] zoom to new selections --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 50f4c8202..bb7ccf037 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -180,8 +180,8 @@ public struct FloorFilter: View { // Prevent a double-set if the view model triggered the original change. guard newValue != viewModel.selection else { return } switch newValue { - case .site(let site): viewModel.setSite(site) - case .facility(let facility): viewModel.setFacility(facility) + case .site(let site): viewModel.setSite(site, zoomTo: true) + case .facility(let facility): viewModel.setFacility(facility, zoomTo: true) case .level(let level): viewModel.setLevel(level) case .none: viewModel.clearSelection() } From b0e4d5992628d5e314a7632fd617d1ecafc7fd05 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:47:21 -0700 Subject: [PATCH 107/244] Use correct indentation --- .../Examples/FloorFilterExampleView.swift | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 6581f8f23..04ddfe621 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -55,45 +55,45 @@ struct FloorFilterExampleView: View { var body: some View { MapView(map: map, viewpoint: viewpoint) - .onNavigatingChanged { isNavigating = $0 } - .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .onNavigatingChanged { isNavigating = $0 } + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } // Preserve the current viewpoint when a keyboard is presented in landscape. - .ignoresSafeArea(.keyboard, edges: .bottom) - .overlay(alignment: floorFilterAlignment) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - floorManager: floorManager, - alignment: floorFilterAlignment, - viewpoint: $viewpoint, - isNavigating: $isNavigating - ) - .selection($selectedItem) - .frame( - maxWidth: 400, - maxHeight: 400 - ) - .padding(36) - } else if mapLoadError { - Label( - "Map load error!", - systemImage: "exclamationmark.triangle" - ) - .foregroundColor(.red) - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .center - ) + .ignoresSafeArea(.keyboard, edges: .bottom) + .overlay(alignment: floorFilterAlignment) { + if isMapLoaded, + let floorManager = map.floorManager { + FloorFilter( + floorManager: floorManager, + alignment: floorFilterAlignment, + viewpoint: $viewpoint, + isNavigating: $isNavigating + ) + .selection($selectedItem) + .frame( + maxWidth: 400, + maxHeight: 400 + ) + .padding(36) + } else if mapLoadError { + Label( + "Map load error!", + systemImage: "exclamationmark.triangle" + ) + .foregroundColor(.red) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .center + ) + } } - } - .task { - do { - try await map.load() - isMapLoaded = true - } catch { - mapLoadError = true + .task { + do { + try await map.load() + isMapLoaded = true + } catch { + mapLoadError = true + } } - } } } From 5ff99cb29b6cbba91a5f7bf31d49d26db1f3abc5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:50:56 -0700 Subject: [PATCH 108/244] Correct indentation --- Examples/Examples/FloorFilterExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 04ddfe621..d1f7fd871 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -57,7 +57,7 @@ struct FloorFilterExampleView: View { MapView(map: map, viewpoint: viewpoint) .onNavigatingChanged { isNavigating = $0 } .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - // Preserve the current viewpoint when a keyboard is presented in landscape. + // Preserve the current viewpoint when a keyboard is presented in landscape. .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: floorFilterAlignment) { if isMapLoaded, From c804d85017cc7b6e17a813b6dbac8319afb45a53 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 15:51:33 -0700 Subject: [PATCH 109/244] Single line frame modifier --- Examples/Examples/FloorFilterExampleView.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index d1f7fd871..60dd1e800 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -69,10 +69,7 @@ struct FloorFilterExampleView: View { isNavigating: $isNavigating ) .selection($selectedItem) - .frame( - maxWidth: 400, - maxHeight: 400 - ) + .frame(maxWidth: 400, maxHeight: 400) .padding(36) } else if mapLoadError { Label( From c0e5380def579f521911a8712065cc56c502bdfa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:01:24 -0700 Subject: [PATCH 110/244] Test newly added method --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 484946fc5..fd580dc52 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -164,6 +164,12 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(viewModel.selectedSite, level.facility?.site) XCTAssertEqual(viewModel.selectedFacility, level.facility) XCTAssertEqual(viewModel.selectedLevel, level) + + viewModel.clearSelection() + XCTAssertEqual(viewModel.selection, nil) + XCTAssertEqual(viewModel.selectedSite, nil) + XCTAssertEqual(viewModel.selectedFacility, nil) + XCTAssertEqual(viewModel.selectedLevel, nil) } /// Confirms that the selection property indicates the correct facility (and therefore level) value. From da4376fc71f8af47b5edc311fc41088866a54009 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:12:58 -0700 Subject: [PATCH 111/244] Limit scope, revert general refactoring --- .../Examples/FloorFilterExampleView.swift | 90 +++++++++++-------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 60dd1e800..4f03cf71c 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -45,52 +45,64 @@ struct FloorFilterExampleView: View { scale: 100_000 ) - /// The data model containing the `Map` displayed in the `MapView`. - @State private var map = makeMap() - /// The filter's current selection. This may be a site, facility, or level. /// /// Optionally monitor or update the filter's selection. @State private var selectedItem: FloorFilterSelection? + /// The data model containing the `Map` displayed in the `MapView`. + @StateObject private var dataModel = MapDataModel( + map: makeMap() + ) + var body: some View { - MapView(map: map, viewpoint: viewpoint) - .onNavigatingChanged { isNavigating = $0 } - .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - // Preserve the current viewpoint when a keyboard is presented in landscape. - .ignoresSafeArea(.keyboard, edges: .bottom) - .overlay(alignment: floorFilterAlignment) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - floorManager: floorManager, - alignment: floorFilterAlignment, - viewpoint: $viewpoint, - isNavigating: $isNavigating - ) - .selection($selectedItem) - .frame(maxWidth: 400, maxHeight: 400) - .padding(36) - } else if mapLoadError { - Label( - "Map load error!", - systemImage: "exclamationmark.triangle" - ) - .foregroundColor(.red) - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .center - ) - } + MapView( + map: dataModel.map, + viewpoint: viewpoint + ) + .onNavigatingChanged { + isNavigating = $0 + } + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } + // Preserve the current viewpoint when a keyboard is presented in landscape. + .ignoresSafeArea(.keyboard, edges: .bottom) + .overlay(alignment: floorFilterAlignment) { + if isMapLoaded, + let floorManager = dataModel.map.floorManager { + FloorFilter( + floorManager: floorManager, + alignment: floorFilterAlignment, + viewpoint: $viewpoint, + isNavigating: $isNavigating + ) + .selection($selectedItem) + .frame( + maxWidth: 400, + maxHeight: 400 + ) + .padding(36) + } else if mapLoadError { + Label( + "Map load error!", + systemImage: "exclamationmark.triangle" + ) + .foregroundColor(.red) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .center + ) } - .task { - do { - try await map.load() - isMapLoaded = true - } catch { - mapLoadError = true - } + } + .task { + do { + try await dataModel.map.load() + isMapLoaded = true + } catch { + mapLoadError = true } + } } } From bc0caf8b831c0901c8bedf3280474544773c02be Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:15:56 -0700 Subject: [PATCH 112/244] Revert other unrelated refactoring --- Examples/Examples/FloorFilterExampleView.swift | 2 +- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- .../Components/FloorFilter/LevelSelector.swift | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 4f03cf71c..c2e146ff2 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -66,7 +66,7 @@ struct FloorFilterExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - // Preserve the current viewpoint when a keyboard is presented in landscape. + /// Preserve the current viewpoint when a keyboard is presented in landscape. .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: floorFilterAlignment) { if isMapLoaded, diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index c74ab901f..1c6ae01fb 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -49,7 +49,7 @@ final class FloorFilterViewModel: ObservableObject { private let automaticSelectionMode: FloorFilterAutomaticSelectionMode /// The `FloorManager` containing the site, floor, and level information. - private let floorManager: FloorManager + let floorManager: FloorManager // MARK: Properties diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index b7b61dfed..050422ffd 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -20,8 +20,8 @@ struct LevelSelector: View { @EnvironmentObject var viewModel: FloorFilterViewModel /// A Boolean value indicating the whether the view shows only the selected level or all levels. - /// If the value is`false`, the view will display all levels; if it is `true`, the view will - /// only display the selected level. + /// If the value is`false`, the view will display all levels; if it is `true`, the view will only display + /// the selected level. @State private var isCollapsed: Bool = false /// The alignment configuration. @@ -30,8 +30,8 @@ struct LevelSelector: View { /// The levels to display. let levels: [FloorLevel] - /// The short name of the currently selected level, the first level, or "None" if none of the - /// levels are available. + /// The short name of the currently selected level, the first level, or "None" if none of the levels + /// are available. private var selectedLevelName: String { viewModel.selectedLevel?.shortName ?? "" } From 1f467d19fd9e9d2bd7e94cdc2c883028e990a2ba Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:17:02 -0700 Subject: [PATCH 113/244] Revert removal --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index bb7ccf037..4100d5a5b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -50,6 +50,9 @@ public struct FloorFilter: View { /// The view model used by the `FloorFilter`. @StateObject private var viewModel: FloorFilterViewModel + /// A Boolean value that indicates whether the levels view is currently collapsed. + @State private var isLevelsViewCollapsed = false + /// A Boolean value that indicates whether the site and facility selector is presented. @State private var isSitesAndFacilitiesHidden = true From a4440e9307fa6e19b58063d27824c4f3eb99a704 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:35:11 -0700 Subject: [PATCH 114/244] Remove extra slash This is not a docc comment --- Examples/Examples/FloorFilterExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index c2e146ff2..4f03cf71c 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -66,7 +66,7 @@ struct FloorFilterExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - /// Preserve the current viewpoint when a keyboard is presented in landscape. + // Preserve the current viewpoint when a keyboard is presented in landscape. .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: floorFilterAlignment) { if isMapLoaded, From eafce76f3d9d5a9a5a58e00758133ce365199f8f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:35:48 -0700 Subject: [PATCH 115/244] Remove unused property --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 4100d5a5b..bb7ccf037 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -50,9 +50,6 @@ public struct FloorFilter: View { /// The view model used by the `FloorFilter`. @StateObject private var viewModel: FloorFilterViewModel - /// A Boolean value that indicates whether the levels view is currently collapsed. - @State private var isLevelsViewCollapsed = false - /// A Boolean value that indicates whether the site and facility selector is presented. @State private var isSitesAndFacilitiesHidden = true From 197b0dc89e214e6ed163fc86a29180e7eadba295 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:36:23 -0700 Subject: [PATCH 116/244] Mark floor manager in model private No need for it to be internal --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 1c6ae01fb..c74ab901f 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -49,7 +49,7 @@ final class FloorFilterViewModel: ObservableObject { private let automaticSelectionMode: FloorFilterAutomaticSelectionMode /// The `FloorManager` containing the site, floor, and level information. - let floorManager: FloorManager + private let floorManager: FloorManager // MARK: Properties From d1812f7d54fa913d7c6fef344e7f1df6f5274751 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Mar 2023 16:39:06 -0700 Subject: [PATCH 117/244] Better adhere doc to margin guides --- .../Components/FloorFilter/FloorFilter.swift | 6 +++--- .../FloorFilter/FloorFilterAutomaticSelectionMode.swift | 4 ++-- .../Components/FloorFilter/FloorFilterViewModel.swift | 5 +++-- .../Components/FloorFilter/LevelSelector.swift | 8 ++++---- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index bb7ccf037..171f70aad 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -14,9 +14,9 @@ import SwiftUI import ArcGIS -/// The `FloorFilter` component simplifies visualization of GIS data for a specific floor of a building -/// in your application. It allows you to filter the floor plan data displayed in your map or scene view -/// to a site, a facility (building) in the site, or a floor in the facility. +/// The `FloorFilter` component simplifies visualization of GIS data for a specific floor of a +/// building in your application. It allows you to filter the floor plan data displayed in your map +/// or scene view to a site, a facility (building) in the site, or a floor in the facility. public struct FloorFilter: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass: UserInterfaceSizeClass? diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift index 12778410b..d10f10bf8 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift @@ -16,8 +16,8 @@ public enum FloorFilterAutomaticSelectionMode { /// Always update selection based on the current viewpoint; clear the selection when the user /// navigates away. case always - /// Only update the selection when there is a new site or facility in the current viewpoint; don't clear - /// selection when the user navigates away. + /// Only update the selection when there is a new site or facility in the current viewpoint; + /// don't clear selection when the user navigates away. case alwaysNotClearing /// Never update selection based on the map or scene view's current viewpoint. case never diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index c74ab901f..706db3cde 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -174,7 +174,7 @@ final class FloorFilterViewModel: ObservableObject { /// Attempts to make an automated selection based on the current viewpoint. /// - /// This method first attempts to select a facility, if that fails, a site selection is attempted. + /// This method first attempts to select a facility, if that fails, site selection is attempted. func automaticallySelectFacilityOrSite() { guard automaticSelectionMode != .never else { return @@ -262,7 +262,8 @@ final class FloorFilterViewModel: ObservableObject { return true } - /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. + /// Sets the visibility of all the levels on the map based on the vertical order of the current + /// selected level. private func filterMapToSelectedLevel() { if let selectedLevel = selectedLevel { levels.forEach { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 050422ffd..b7b61dfed 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -20,8 +20,8 @@ struct LevelSelector: View { @EnvironmentObject var viewModel: FloorFilterViewModel /// A Boolean value indicating the whether the view shows only the selected level or all levels. - /// If the value is`false`, the view will display all levels; if it is `true`, the view will only display - /// the selected level. + /// If the value is`false`, the view will display all levels; if it is `true`, the view will + /// only display the selected level. @State private var isCollapsed: Bool = false /// The alignment configuration. @@ -30,8 +30,8 @@ struct LevelSelector: View { /// The levels to display. let levels: [FloorLevel] - /// The short name of the currently selected level, the first level, or "None" if none of the levels - /// are available. + /// The short name of the currently selected level, the first level, or "None" if none of the + /// levels are available. private var selectedLevelName: String { viewModel.selectedLevel?.shortName ?? "" } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index c71a714fd..49965eca1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -84,7 +84,7 @@ struct SiteAndFacilitySelector: View { } } - /// A view containing a filter-via-name field, a list of the site names and an "All sites" button. + /// A view with a filter-via-name field, a list of site names and an "All sites" button. var body: some View { VStack { // If the filtered set of sites is empty @@ -232,8 +232,8 @@ struct SiteAndFacilitySelector: View { /// Displays a list of facilities matching the filter criteria as determined by /// `matchingFacilities`. /// - /// If a certain facility is indicated as selected by the view model, it will have a slightly different - /// appearance. + /// If a certain facility is indicated as selected by the view model, it will have a + /// slightly different appearance. /// /// If `AutomaticSelectionMode` mode is in use, this list will automatically scroll to the /// selected item. From 3762936a7145bbad587065bc88b4ecd950e7010f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 12:18:06 -0700 Subject: [PATCH 118/244] `filterWidth` -> `levelSelectorWidth` --- .../Components/FloorFilter/FloorFilter.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 171f70aad..c1c3b29ac 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -60,7 +60,7 @@ public struct FloorFilter: View { private let alignment: Alignment /// The width of the level selector. - private let filterWidth: CGFloat = 60 + private var levelSelectorWidth: CGFloat = 60 /// The `Viewpoint` used to pan/zoom to the selected site/facility. /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. @@ -100,7 +100,7 @@ public struct FloorFilter: View { sitesAndFacilitiesButton } } - .frame(width: filterWidth) + .frame(width: levelSelectorWidth) .esriBorder() .frame( maxWidth: horizontalSizeClass == .compact ? .infinity : nil, @@ -192,11 +192,22 @@ public struct FloorFilter: View { selection?.wrappedValue = newValue } } +} + +public extension FloorFilter { + /// The width of the level selector. + /// - Parameter width: The new width for the level selector. + /// - Returns: The `FloorFilter`. + func levelSelectorWidth(_ width: CGFloat) -> Self { + var copy = self + copy.levelSelectorWidth = width + return copy + } /// The currently selected site, facility, or level. /// - Parameter selection: The selection. /// - Returns: The `FloorFilter`. - public func selection(_ selection: Binding) -> Self { + func selection(_ selection: Binding) -> Self { var copy = self copy.selection = selection return copy From 617512b7e963c678711e80feb9c6744ceadfb5c1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 12:18:14 -0700 Subject: [PATCH 119/244] typo --- Sources/ArcGISToolkit/Extensions/ToggleStyle.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift index cea65c590..3404d8ade 100644 --- a/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift +++ b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift @@ -47,7 +47,7 @@ struct SelectedButtonStyle: ToggleStyle { } extension ToggleStyle where Self == SelectableButtonStyle { - /// Appears as a selected or deselected button despending on the toggle's current state. + /// Appears as a selected or deselected button depending on the toggle's current state. static var selectableButton: SelectableButtonStyle { .init() } } From aa66cb311cad32068a779fe6e10beaa96889273b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 14:47:37 -0700 Subject: [PATCH 120/244] Refactor `LevelSelector` to support small widths --- .../FloorFilter/LevelSelector.swift | 138 +++++++++--------- 1 file changed, 67 insertions(+), 71 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index b7b61dfed..abfb516e6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -19,6 +19,9 @@ struct LevelSelector: View { /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel + /// The height of the scroll view's content. + @State private var contentHeight: CGFloat = .zero + /// A Boolean value indicating the whether the view shows only the selected level or all levels. /// If the value is`false`, the view will display all levels; if it is `true`, the view will /// only display the selected level. @@ -30,36 +33,39 @@ struct LevelSelector: View { /// The levels to display. let levels: [FloorLevel] - /// The short name of the currently selected level, the first level, or "None" if none of the - /// levels are available. - private var selectedLevelName: String { - viewModel.selectedLevel?.shortName ?? "" - } - public var body: some View { - if !isCollapsed, - levels.count > 1 { - VStack { - if !isTopAligned { - makeCollapseButton() - Divider() - } - LevelsStack(levels: levels) - if isTopAligned { - Divider() - makeCollapseButton() - } + VStack { + if !isTopAligned { + makeCollapseButton() + Divider() } + makeLevelButtons() + if isTopAligned { + Divider() + makeCollapseButton() + } + } + } +} + +extension LevelSelector { + /// A list of all the levels to be displayed. + /// + /// If the selector is collapsed, only the selected level is shown. + var filteredLevels: [FloorLevel] { + if !isCollapsed, levels.count > 1 { + return levels } else { - Toggle(isOn: $isCollapsed) { - Text(selectedLevelName) - .modifier(LevelNameFormat()) + if let selectedLevel = viewModel.selectedLevel { + return [selectedLevel] + } else { + return [] } - .toggleStyle(.selectedButton) } } /// A button used to collapse the floor level list. + /// - Returns: The button used to collapse and expand the selector. @ViewBuilder func makeCollapseButton() -> some View { Button { withAnimation { @@ -70,64 +76,54 @@ struct LevelSelector: View { .padding(.toolkitDefault) } } -} - -/// A vertical list of floor levels. -private struct LevelsStack: View { - /// The view model used by the `LevelsView`. - @EnvironmentObject var viewModel: FloorFilterViewModel - /// The height of the scroll view's content. - @State private var contentHeight: CGFloat = .zero - - /// The levels to display. - let levels: [FloorLevel] + /// A button for a level in the floor level list. + /// - Parameter level: The level represented by the button. + /// - Returns: The button representing the provided level. + @ViewBuilder func makeLevelButton(_ level: FloorLevel) -> some View { + Button(level.shortName) { + viewModel.setLevel(level) + if isCollapsed && levels.count > 1 { + isCollapsed.toggle() + } + } + .padding([.vertical], 4) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .background( + viewModel.selectedLevel == level ? + Color(uiColor: .secondarySystemBackground) : + .clear + ) + .border(Color(uiColor: .secondarySystemBackground), width: 2) + .cornerRadius(5) + } - var body: some View { + /// A scrollable list of buttons; one for each level to be displayed. + /// - Returns: The scrollable list of level buttons. + @ViewBuilder func makeLevelButtons() -> some View { ScrollViewReader { proxy in ScrollView { - VStack { - ForEach(levels, id: \.id) { level in - Toggle( - isOn: Binding( - get: { - viewModel.selectedLevel == level - }, - set: { newIsOn in - guard newIsOn else { return } - viewModel.setLevel(level) - } - ) - ) { - Text(level.shortName) - .modifier(LevelNameFormat()) - } - .toggleStyle(.selectableButton) + VStack(spacing: 4) { + ForEach(filteredLevels, id: \.id) { level in + makeLevelButton(level) } } - .onSizeChange { - contentHeight = $0.height - } + .onSizeChange { contentHeight = $0.height } } .frame(maxHeight: contentHeight) - .onAppear { - if let floorLevel = viewModel.selectedLevel { - withAnimation { - proxy.scrollTo( - floorLevel.id - ) - } - } - } + .onAppear { scrollToSelectedLevel(with: proxy) } + .onChange(of: isCollapsed) { _ in scrollToSelectedLevel(with: proxy) } } } -} - -private struct LevelNameFormat: ViewModifier { - func body(content: Content) -> some View { - content - .lineLimit(1) - .fixedSize() - .frame(minWidth: 40) + + /// Scrolls the list within the provided proxy to the button representing the selected level. + /// - Parameter proxy: The proxy containing the scroll view. + func scrollToSelectedLevel(with proxy: ScrollViewProxy) { + if let level = viewModel.selectedLevel { + withAnimation { + proxy.scrollTo(level.id) + } + } } } From 1b0a71350093433118ddec0663fef853a222d7da Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 14:51:38 -0700 Subject: [PATCH 121/244] Remove now unused custom toggle styles --- .../Extensions/ToggleStyle.swift | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/ToggleStyle.swift diff --git a/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift deleted file mode 100644 index 3404d8ade..000000000 --- a/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2022 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 -// http://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 SwiftUI - -/// A style in which a toggle button appears to be either selected or deselected depending on the toggle's current state. -struct SelectableButtonStyle: ToggleStyle { - func makeBody(configuration: Configuration) -> some View { - if configuration.isOn { - Button { - configuration.isOn.toggle() - } label: { - configuration.label - } - .buttonStyle(.borderedProminent) - } else { - Button { - configuration.isOn.toggle() - } label: { - configuration.label - } - .buttonStyle(.bordered) - } - } -} - -/// A style in which a toggle button appears to be always selected despite the toggle's current state. -struct SelectedButtonStyle: ToggleStyle { - func makeBody(configuration: Configuration) -> some View { - Button { - configuration.isOn.toggle() - } label: { - configuration.label - } - .buttonStyle(.borderedProminent) - } -} - -extension ToggleStyle where Self == SelectableButtonStyle { - /// Appears as a selected or deselected button depending on the toggle's current state. - static var selectableButton: SelectableButtonStyle { .init() } -} - -extension ToggleStyle where Self == SelectedButtonStyle { - /// Appears always a selected button despite the toggle's current state. - static var selectedButton: SelectedButtonStyle { .init() } -} From 5b0249466b6ac2bc9046ebf341135105aec15803 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 14:52:05 -0700 Subject: [PATCH 122/244] Disable the collapse button if there's only one level --- Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index abfb516e6..3bf470c2d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -75,6 +75,7 @@ extension LevelSelector { Image(systemName: isTopAligned ? "chevron.up.circle" : "chevron.down.circle") .padding(.toolkitDefault) } + .disabled(levels.count == 1) } /// A button for a level in the floor level list. From a3e51c354a9d0c52adbb2ac4c39dcb8cab9348a4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 15:05:05 -0700 Subject: [PATCH 123/244] Accurately represent the collapse/expand direction --- .../Components/FloorFilter/LevelSelector.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 3bf470c2d..eca6cbce4 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -64,6 +64,14 @@ extension LevelSelector { } } + /// The system name of the icon that reflects the current state of `isCollapsed`. + var iconForCollapsedState: String { + switch (isCollapsed, isTopAligned) { + case (true, true), (false, false): return "chevron.down.circle" + case (true, false), (false, true): return "chevron.up.circle" + } + } + /// A button used to collapse the floor level list. /// - Returns: The button used to collapse and expand the selector. @ViewBuilder func makeCollapseButton() -> some View { @@ -72,7 +80,7 @@ extension LevelSelector { isCollapsed.toggle() } } label: { - Image(systemName: isTopAligned ? "chevron.up.circle" : "chevron.down.circle") + Image(systemName: iconForCollapsedState) .padding(.toolkitDefault) } .disabled(levels.count == 1) From 6f3c8ae1413fa399c736433d296865cd11d18442 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 15:35:32 -0700 Subject: [PATCH 124/244] Level button improvements --- .../Components/FloorFilter/LevelSelector.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index eca6cbce4..65b6e1834 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -96,16 +96,16 @@ extension LevelSelector { isCollapsed.toggle() } } + .foregroundColor(.primary) .padding([.vertical], 4) .frame(maxWidth: .infinity) - .contentShape(Rectangle()) - .background( - viewModel.selectedLevel == level ? - Color(uiColor: .secondarySystemBackground) : - .clear + .background(viewModel.selectedLevel == level ? Color.accentColor : .secondary) + .border( + viewModel.selectedLevel == level ? Color.accentColor : .secondary, + width: 2 ) - .border(Color(uiColor: .secondarySystemBackground), width: 2) .cornerRadius(5) + .contentShape(Rectangle()) } /// A scrollable list of buttons; one for each level to be displayed. From fb25ea9fcb08b95d84a9d3eac82547499e1b1f9c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 15:46:32 -0700 Subject: [PATCH 125/244] Fix hit test, use accent color, like the toggle --- .../FloorFilter/LevelSelector.swift | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 65b6e1834..051e7216c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -90,22 +90,21 @@ extension LevelSelector { /// - Parameter level: The level represented by the button. /// - Returns: The button representing the provided level. @ViewBuilder func makeLevelButton(_ level: FloorLevel) -> some View { - Button(level.shortName) { - viewModel.setLevel(level) - if isCollapsed && levels.count > 1 { - isCollapsed.toggle() + Text(level.shortName) + .foregroundColor(.primary) + .padding([.vertical], 4) + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 5) + .fill(viewModel.selectedLevel == level ? Color.accentColor : Color(uiColor: .secondarySystemBackground)) + + } + .onTapGesture { + viewModel.setLevel(level) + if isCollapsed && levels.count > 1 { + isCollapsed.toggle() + } } - } - .foregroundColor(.primary) - .padding([.vertical], 4) - .frame(maxWidth: .infinity) - .background(viewModel.selectedLevel == level ? Color.accentColor : .secondary) - .border( - viewModel.selectedLevel == level ? Color.accentColor : .secondary, - width: 2 - ) - .cornerRadius(5) - .contentShape(Rectangle()) } /// A scrollable list of buttons; one for each level to be displayed. From 4f0d4d8bfbac66452ee164aa3da1c2f66aa29e27 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 15:46:51 -0700 Subject: [PATCH 126/244] Update LevelSelector.swift --- Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 051e7216c..95c282cc3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -97,7 +97,6 @@ extension LevelSelector { .background { RoundedRectangle(cornerRadius: 5) .fill(viewModel.selectedLevel == level ? Color.accentColor : Color(uiColor: .secondarySystemBackground)) - } .onTapGesture { viewModel.setLevel(level) From be32f08711984ecf98e342c5a6b56b8fb48b02d4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 15:51:00 -0700 Subject: [PATCH 127/244] Break out color calculation --- .../Components/FloorFilter/LevelSelector.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 95c282cc3..8760fa111 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -96,7 +96,7 @@ extension LevelSelector { .frame(maxWidth: .infinity) .background { RoundedRectangle(cornerRadius: 5) - .fill(viewModel.selectedLevel == level ? Color.accentColor : Color(uiColor: .secondarySystemBackground)) + .fill(buttonColorFor(level)) } .onTapGesture { viewModel.setLevel(level) @@ -124,6 +124,17 @@ extension LevelSelector { } } + /// Determines a appropriate color for a button in the floor level list. + /// - Parameter level: THe level represented by the button. + /// - Returns: The color for the button representing the provided level. + func buttonColorFor(_ level: FloorLevel) -> Color { + if viewModel.selectedLevel == level { + return Color.accentColor + } else { + return Color(uiColor: .secondarySystemBackground) + } + } + /// Scrolls the list within the provided proxy to the button representing the selected level. /// - Parameter proxy: The proxy containing the scroll view. func scrollToSelectedLevel(with proxy: ScrollViewProxy) { From ba3a52a3c0c9afadf0616e9c0eebad0547b00ea7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 15:55:24 -0700 Subject: [PATCH 128/244] Match color in sites list --- .../ArcGISToolkit/Components/FloorFilter/LevelSelector.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 8760fa111..39afbf02d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -131,7 +131,7 @@ extension LevelSelector { if viewModel.selectedLevel == level { return Color.accentColor } else { - return Color(uiColor: .secondarySystemBackground) + return Color.secondary.opacity(0.5) } } From 48103d6786865424d39ac729a027e6ac161200c2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 16:44:13 -0700 Subject: [PATCH 129/244] Change selection to optional initializer parameter Discussion: https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/pull/277#issuecomment-1481747706 --- .../Components/FloorFilter/FloorFilter.swift | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 4100d5a5b..d4ab9be06 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -28,12 +28,14 @@ public struct FloorFilter: View { /// - automaticSelectionMode: The selection behavior of the floor filter. /// - viewpoint: Viewpoint updated when the selected site or facility changes. /// - isNavigating: A Boolean value indicating whether the map is currently being navigated. + /// - selection: The selected site, facility, or level. public init( floorManager: FloorManager, alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, viewpoint: Binding = .constant(nil), - isNavigating: Binding + isNavigating: Binding, + selection: Binding? = nil ) { _viewModel = StateObject( wrappedValue: FloorFilterViewModel( @@ -45,6 +47,7 @@ public struct FloorFilter: View { self.alignment = alignment self.isNavigating = isNavigating self.viewpoint = viewpoint + self.selection = selection } /// The view model used by the `FloorFilter`. @@ -195,13 +198,4 @@ public struct FloorFilter: View { selection?.wrappedValue = newValue } } - - /// The currently selected site, facility, or level. - /// - Parameter selection: The selection. - /// - Returns: The `FloorFilter`. - public func selection(_ selection: Binding) -> Self { - var copy = self - copy.selection = selection - return copy - } } From 43f8d7e308120b0c871a043b5636cb164d589fd2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 16:47:44 -0700 Subject: [PATCH 130/244] Remove selection demo from sample --- Examples/Examples/FloorFilterExampleView.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index c2e146ff2..e65fb6dc3 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -45,11 +45,6 @@ struct FloorFilterExampleView: View { scale: 100_000 ) - /// The filter's current selection. This may be a site, facility, or level. - /// - /// Optionally monitor or update the filter's selection. - @State private var selectedItem: FloorFilterSelection? - /// The data model containing the `Map` displayed in the `MapView`. @StateObject private var dataModel = MapDataModel( map: makeMap() @@ -77,7 +72,6 @@ struct FloorFilterExampleView: View { viewpoint: $viewpoint, isNavigating: $isNavigating ) - .selection($selectedItem) .frame( maxWidth: 400, maxHeight: 400 From 84cd2f5dec3c4d60ca0d9f2927397ae7a6ffad26 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Mar 2023 16:50:32 -0700 Subject: [PATCH 131/244] Remove auto viewpoint update --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index d4ab9be06..9890cff00 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -186,8 +186,8 @@ public struct FloorFilter: View { // Prevent a double-set if the view model triggered the original change. guard newValue != viewModel.selection else { return } switch newValue { - case .site(let site): viewModel.setSite(site, zoomTo: true) - case .facility(let facility): viewModel.setFacility(facility, zoomTo: true) + case .site(let site): viewModel.setSite(site) + case .facility(let facility): viewModel.setFacility(facility) case .level(let level): viewModel.setLevel(level) case .none: viewModel.clearSelection() } From e1f871ae984367176fe080ba09480516d2726ae0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Mar 2023 10:51:48 -0700 Subject: [PATCH 132/244] Migrate selection helper methods from model to selection --- .../FloorFilter/FloorFilterSelection.swift | 35 +++++++++++++ .../FloorFilter/FloorFilterViewModel.swift | 43 ++------------- .../FloorFilter/LevelSelector.swift | 6 +-- .../FloorFilter/SiteAndFacilitySelector.swift | 8 +-- .../FloorFilterViewModelTests.swift | 52 +++++++++---------- 5 files changed, 72 insertions(+), 72 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift index 6e126655b..10121b300 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterSelection.swift @@ -22,3 +22,38 @@ public enum FloorFilterSelection: Hashable { /// A selected level. case level(FloorLevel) } + +public extension FloorFilterSelection { + /// The selected site. + var site: FloorSite? { + switch self { + case .site(let site): + return site + case .facility(let facility): + return facility.site + case .level(let level): + return level.facility?.site + } + } + + /// The selected facility. + var facility: FloorFacility? { + switch self { + case .facility(let facility): + return facility + case .level(let level): + return level.facility + default: + return nil + } + } + + /// The selected level. + var level: FloorLevel? { + if case let .level(level) = self { + return level + } else { + return nil + } + } +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 1c6ae01fb..71b73410a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -61,7 +61,7 @@ final class FloorFilterViewModel: ObservableObject { /// A Boolean value that indicates whether there are levels to display. This will be `false` if /// there is no selected facility or if the selected facility has no levels. var hasLevelsToDisplay: Bool { - !(selectedFacility?.levels.isEmpty ?? true) + !(selection?.facility?.levels.isEmpty ?? true) } /// The floor manager levels. @@ -69,41 +69,6 @@ final class FloorFilterViewModel: ObservableObject { floorManager.levels } - /// The selected site. - var selectedSite: FloorSite? { - switch selection { - case .site(let site): - return site - case .facility(let facility): - return facility.site - case .level(let level): - return level.facility?.site - default: - return nil - } - } - - /// The selected facility. - var selectedFacility: FloorFacility? { - switch selection { - case .facility(let facility): - return facility - case .level(let level): - return level.facility - default: - return nil - } - } - - /// The selected level. - var selectedLevel: FloorLevel? { - if case let .level(level) = selection { - return level - } else { - return nil - } - } - /// The floor manager sites. var sites: [FloorSite] { floorManager.sites.sorted { $0.name < $1.name } @@ -111,7 +76,7 @@ final class FloorFilterViewModel: ObservableObject { /// The selected facility's levels, sorted by `level.verticalOrder`. var sortedLevels: [FloorLevel] { - selectedFacility?.levels + selection?.facility?.levels .sorted(by: { $0.verticalOrder > $1.verticalOrder }) ?? [] } @@ -136,7 +101,7 @@ final class FloorFilterViewModel: ObservableObject { /// - newFacility: The new facility to be selected. /// - zoomTo: If `true`, changes the viewpoint to the extent of the new facility. func setFacility(_ newFacility: FloorFacility, zoomTo: Bool = false) { - if let oldLevel = selectedLevel, + if let oldLevel = selection?.level, let newLevel = newFacility.levels.first( where: { $0.verticalOrder == oldLevel.verticalOrder } ) { @@ -264,7 +229,7 @@ final class FloorFilterViewModel: ObservableObject { /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. private func filterMapToSelectedLevel() { - if let selectedLevel = selectedLevel { + if let selectedLevel = selection?.level { levels.forEach { $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 050422ffd..3191ea3a5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -33,7 +33,7 @@ struct LevelSelector: View { /// The short name of the currently selected level, the first level, or "None" if none of the levels /// are available. private var selectedLevelName: String { - viewModel.selectedLevel?.shortName ?? "" + viewModel.selection?.level?.shortName ?? "" } public var body: some View { @@ -91,7 +91,7 @@ private struct LevelsStack: View { Toggle( isOn: Binding( get: { - viewModel.selectedLevel == level + viewModel.selection?.level == level }, set: { newIsOn in guard newIsOn else { return } @@ -111,7 +111,7 @@ private struct LevelsStack: View { } .frame(maxHeight: contentHeight) .onAppear { - if let floorLevel = viewModel.selectedLevel { + if let floorLevel = viewModel.selection?.level { withAnimation { proxy.scrollTo( floorLevel.id diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index c71a714fd..69cc987d6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -139,7 +139,7 @@ struct SiteAndFacilitySelector: View { tag: site, selection: Binding( get: { - userBackedOutOfSelectedSite ? nil : viewModel.selectedSite + userBackedOutOfSelectedSite ? nil : viewModel.selection?.site }, set: { newSite in guard let newSite = newSite else { return } @@ -225,7 +225,7 @@ struct SiteAndFacilitySelector: View { .keyboardType(.alphabet) .disableAutocorrection(true) .navigationTitle( - usesAllSitesStyling ? "All Sites" : viewModel.selectedSite?.name ?? "Select a facility" + usesAllSitesStyling ? "All Sites" : viewModel.selection?.site?.name ?? "Select a facility" ) } @@ -256,7 +256,7 @@ struct SiteAndFacilitySelector: View { } } .contentShape(Rectangle()) - .listRowBackground(facility.id == viewModel.selectedFacility?.id ? Color.secondary.opacity(0.5) : Color.clear) + .listRowBackground(facility.id == viewModel.selection?.facility?.id ? Color.secondary.opacity(0.5) : Color.clear) .onTapGesture { viewModel.setFacility(facility, zoomTo: true) if horizontalSizeClass == .compact { @@ -266,7 +266,7 @@ struct SiteAndFacilitySelector: View { } .listStyle(.plain) .onChange(of: viewModel.selection) { _ in - if let floorFacility = viewModel.selectedFacility { + if let floorFacility = viewModel.selection?.facility { withAnimation { proxy.scrollTo( floorFacility.id diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index fd580dc52..faee98b60 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -34,20 +34,20 @@ final class FloorFilterViewModelTests: XCTestCase { ) // Viewpoint is Los Angeles, selection should be nil - XCTAssertNil(viewModel.selectedFacility) - XCTAssertNil(viewModel.selectedSite) + XCTAssertNil(viewModel.selection?.facility) + XCTAssertNil(viewModel.selection?.site) // Viewpoint is Research Annex Lattice _viewpoint = .researchAnnexLattice viewModel.automaticallySelectFacilityOrSite() - XCTAssertEqual(viewModel.selectedSite?.name, "Research Annex") - XCTAssertEqual(viewModel.selectedFacility?.name, "Lattice") + XCTAssertEqual(viewModel.selection?.site?.name, "Research Annex") + XCTAssertEqual(viewModel.selection?.facility?.name, "Lattice") // Viewpoint is Los Angeles, selection should be nil _viewpoint = .losAngeles viewModel.automaticallySelectFacilityOrSite() - XCTAssertNil(viewModel.selectedSite) - XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selection?.site) + XCTAssertNil(viewModel.selection?.facility) } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. @@ -67,14 +67,14 @@ final class FloorFilterViewModelTests: XCTestCase { // Viewpoint is Research Annex Lattice _viewpoint = .researchAnnexLattice viewModel.automaticallySelectFacilityOrSite() - XCTAssertEqual(viewModel.selectedSite?.name, "Research Annex") - XCTAssertEqual(viewModel.selectedFacility?.name, "Lattice") + XCTAssertEqual(viewModel.selection?.site?.name, "Research Annex") + XCTAssertEqual(viewModel.selection?.facility?.name, "Lattice") // Viewpoint is Los Angeles, but selection should remain Research Annex Lattice _viewpoint = .losAngeles viewModel.automaticallySelectFacilityOrSite() - XCTAssertEqual(viewModel.selectedSite?.name, "Research Annex") - XCTAssertEqual(viewModel.selectedFacility?.name, "Lattice") + XCTAssertEqual(viewModel.selection?.site?.name, "Research Annex") + XCTAssertEqual(viewModel.selection?.facility?.name, "Lattice") } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. @@ -92,14 +92,14 @@ final class FloorFilterViewModelTests: XCTestCase { ) // Viewpoint is Los Angeles, selection should be nil - XCTAssertNil(viewModel.selectedFacility) - XCTAssertNil(viewModel.selectedSite) + XCTAssertNil(viewModel.selection?.facility) + XCTAssertNil(viewModel.selection?.site) // Viewpoint is Research Annex Lattice but selection should remain nil _viewpoint = .researchAnnexLattice viewModel.automaticallySelectFacilityOrSite() - XCTAssertNil(viewModel.selectedFacility) - XCTAssertNil(viewModel.selectedSite) + XCTAssertNil(viewModel.selection?.facility) + XCTAssertNil(viewModel.selection?.site) } /// Tests that a `FloorFilterViewModel` successfully initializes. @@ -151,25 +151,25 @@ final class FloorFilterViewModelTests: XCTestCase { let level = try XCTUnwrap(viewModel.levels.first) viewModel.setSite(site, zoomTo: true) - XCTAssertEqual(viewModel.selectedSite, site) - XCTAssertNil(viewModel.selectedFacility) - XCTAssertNil(viewModel.selectedLevel) + XCTAssertEqual(viewModel.selection?.site, site) + XCTAssertNil(viewModel.selection?.facility) + XCTAssertNil(viewModel.selection?.level) viewModel.setFacility(facility, zoomTo: true) - XCTAssertEqual(viewModel.selectedSite, facility.site) - XCTAssertEqual(viewModel.selectedFacility, facility) - XCTAssertEqual(viewModel.selectedLevel, facility.defaultLevel) + XCTAssertEqual(viewModel.selection?.site, facility.site) + XCTAssertEqual(viewModel.selection?.facility, facility) + XCTAssertEqual(viewModel.selection?.level, facility.defaultLevel) viewModel.setLevel(level) - XCTAssertEqual(viewModel.selectedSite, level.facility?.site) - XCTAssertEqual(viewModel.selectedFacility, level.facility) - XCTAssertEqual(viewModel.selectedLevel, level) + XCTAssertEqual(viewModel.selection?.site, level.facility?.site) + XCTAssertEqual(viewModel.selection?.facility, level.facility) + XCTAssertEqual(viewModel.selection?.level, level) viewModel.clearSelection() XCTAssertEqual(viewModel.selection, nil) - XCTAssertEqual(viewModel.selectedSite, nil) - XCTAssertEqual(viewModel.selectedFacility, nil) - XCTAssertEqual(viewModel.selectedLevel, nil) + XCTAssertEqual(viewModel.selection?.site, nil) + XCTAssertEqual(viewModel.selection?.facility, nil) + XCTAssertEqual(viewModel.selection?.level, nil) } /// Confirms that the selection property indicates the correct facility (and therefore level) value. From 21b0a02e60809be825320f7e79a2192e85c811a4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Mar 2023 11:32:14 -0700 Subject: [PATCH 133/244] Update branch --- Examples/Examples/CompassExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index a6d8e171a..a0f0495be 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -155,9 +155,9 @@ struct SceneWithCameraController: View { /// The orbit location camera controller used by the scene view. private let cameraController = OrbitLocationCameraController( - targetPoint: .esriRedlands, + target: .esriRedlands, distance: 10_000 - )! + ) var body: some View { SceneView(scene: dataModel.scene, cameraController: cameraController) From b88ac10e7a73fda594ecc858544250379d5ba4b5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Mar 2023 11:38:55 -0700 Subject: [PATCH 134/244] Add to readme --- Documentation/FloorFilter/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Documentation/FloorFilter/README.md b/Documentation/FloorFilter/README.md index 75c2490bc..339ab0c06 100644 --- a/Documentation/FloorFilter/README.md +++ b/Documentation/FloorFilter/README.md @@ -32,12 +32,14 @@ The ArcGIS Maps SDK currently supports filtering a 2D floor aware map based on t /// - automaticSelectionMode: The selection behavior of the floor filter. /// - viewpoint: Viewpoint updated when the selected site or facility changes. /// - isNavigating: A Boolean value indicating whether the map is currently being navigated. + /// - selection: The selected site, facility, or level. public init( floorManager: FloorManager, alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, viewpoint: Binding = .constant(nil), - isNavigating: Binding + isNavigating: Binding, + selection: Binding? = nil ) ``` @@ -55,6 +57,16 @@ public enum FloorFilterAutomaticSelectionMode { /// Never update selection based on the map or scene view's current viewpoint. case never } + +/// A selected site, facility, or level. +public enum FloorFilterSelection: Hashable { + /// A selected site. + case site(FloorSite) + /// A selected facility. + case facility(FloorFacility) + /// A selected level. + case level(FloorLevel) +} ``` ## Behavior: From f7bc93e1f3a8622cbcdd6168f06137deca26653c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Mar 2023 12:58:34 -0700 Subject: [PATCH 135/244] Create ArcGISToolkit.md --- Documentation.docc/ArcGISToolkit.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Documentation.docc/ArcGISToolkit.md diff --git a/Documentation.docc/ArcGISToolkit.md b/Documentation.docc/ArcGISToolkit.md new file mode 100644 index 000000000..13915b5d3 --- /dev/null +++ b/Documentation.docc/ArcGISToolkit.md @@ -0,0 +1,22 @@ +# ``ArcGISToolkit`` + +The ArcGIS Maps SDK for Swift Toolkit contains components that will simplify your iOS app development. It is built off of the new ArcGIS Maps SDK for Swift. + +## Overview + +- Authenticator - Displays a user interface when network and ArcGIS authentication challenges occur. +- BasemapGallery - Displays a collection of basemaps. +- Bookmarks - Shows bookmarks, from a map, scene, or a list. +- Compass - Shows a compass direction when the map is rotated. Auto-hides when the map points north up. +- FloatingPanel - Allows display of view-related content in a "bottom sheet". +- FloorFilter - Allows to filter floor plan data in a geo view by a site, a building in the site, or a floor in the building. +- Overview Map - Displays an "overview" (or "inset") map on top of an existing map or scene view. +- Popup - Displays details, media, and attachments of features and graphics. +- Scalebar - Displays current scale reference. +- Search - Displays a search experience for geo views. +- UtilityNetworkTrace - Runs traces on a web map published with a utility network and trace configurations. +## Topics + +### Components + +- ``FloorFilter`` From 0acb49b4b123db9df21ce6cb6c4288f3ef29bee0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Mar 2023 13:33:09 -0700 Subject: [PATCH 136/244] Relocate to correct folder --- .../ArcGISToolkit/Documentation.docc}/ArcGISToolkit.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {Documentation.docc => Sources/ArcGISToolkit/Documentation.docc}/ArcGISToolkit.md (100%) diff --git a/Documentation.docc/ArcGISToolkit.md b/Sources/ArcGISToolkit/Documentation.docc/ArcGISToolkit.md similarity index 100% rename from Documentation.docc/ArcGISToolkit.md rename to Sources/ArcGISToolkit/Documentation.docc/ArcGISToolkit.md From 6b3f906d572a36e62dc416dc0d7355ff6a645f12 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Mar 2023 13:40:05 -0700 Subject: [PATCH 137/244] Update ArcGISToolkit.md --- .../Documentation.docc/ArcGISToolkit.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Sources/ArcGISToolkit/Documentation.docc/ArcGISToolkit.md b/Sources/ArcGISToolkit/Documentation.docc/ArcGISToolkit.md index 13915b5d3..74e17920e 100644 --- a/Sources/ArcGISToolkit/Documentation.docc/ArcGISToolkit.md +++ b/Sources/ArcGISToolkit/Documentation.docc/ArcGISToolkit.md @@ -1,22 +1,3 @@ # ``ArcGISToolkit`` The ArcGIS Maps SDK for Swift Toolkit contains components that will simplify your iOS app development. It is built off of the new ArcGIS Maps SDK for Swift. - -## Overview - -- Authenticator - Displays a user interface when network and ArcGIS authentication challenges occur. -- BasemapGallery - Displays a collection of basemaps. -- Bookmarks - Shows bookmarks, from a map, scene, or a list. -- Compass - Shows a compass direction when the map is rotated. Auto-hides when the map points north up. -- FloatingPanel - Allows display of view-related content in a "bottom sheet". -- FloorFilter - Allows to filter floor plan data in a geo view by a site, a building in the site, or a floor in the building. -- Overview Map - Displays an "overview" (or "inset") map on top of an existing map or scene view. -- Popup - Displays details, media, and attachments of features and graphics. -- Scalebar - Displays current scale reference. -- Search - Displays a search experience for geo views. -- UtilityNetworkTrace - Runs traces on a web map published with a utility network and trace configurations. -## Topics - -### Components - -- ``FloorFilter`` From 5dda867188382723a9aaf57529d454e8aaf7d7f3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Mar 2023 14:44:03 -0700 Subject: [PATCH 138/244] Update doc --- Documentation/FloorFilter/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/FloorFilter/README.md b/Documentation/FloorFilter/README.md index 339ab0c06..8f12c8983 100644 --- a/Documentation/FloorFilter/README.md +++ b/Documentation/FloorFilter/README.md @@ -69,6 +69,10 @@ public enum FloorFilterSelection: Hashable { } ``` +`FloorFilter` has the following modifier: + +- `func levelSelectorWidth(_ width: CGFloat)` - The width of the level selector. + ## Behavior: |Site Button| From a6f8cfc6770583aedf90567368fd651a0d2f19be Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 12:10:29 -0700 Subject: [PATCH 139/244] Simplify sample --- Examples/Examples/CompassExampleView.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index a0f0495be..eb230f664 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -57,10 +57,8 @@ struct CompassExampleView: View { /// An example demonstrating how to use a compass with a map view. struct MapWithViewpoint: View { - /// The data model containing the `Map` displayed in the `MapView`. - @StateObject private var dataModel = MapDataModel( - map: Map(basemapStyle: .arcGISImagery) - ) + /// The `Map` displayed in the `MapView`. + @State private var map = Map(basemapStyle: .arcGISImagery) /// Allows for communication between the Compass and MapView or SceneView. @State private var viewpoint: Viewpoint? = Viewpoint( @@ -71,12 +69,10 @@ struct MapWithViewpoint: View { var body: some View { MapViewReader { mapViewProxy in - MapView(map: dataModel.map, viewpoint: viewpoint) + MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { Compass(viewpoint: $viewpoint) - // Optionally provide a different size for the compass. - // .compassSize(size: T##CGFloat) .padding() .onTapGesture { Task { @@ -146,12 +142,10 @@ struct SceneWithCamera: View { /// An example demonstrating how to use a compass with a scene view and camera controller. struct SceneWithCameraController: View { /// The data model containing the `Scene` displayed in the `SceneView`. - @StateObject private var dataModel = SceneDataModel( - scene: Scene(basemapStyle: .arcGISImagery) - ) + @State private var scene = Scene(basemapStyle: .arcGISImagery) /// The current heading as reported by the scene view. - @State private var heading: Double = .zero + @State private var heading = Double.zero /// The orbit location camera controller used by the scene view. private let cameraController = OrbitLocationCameraController( @@ -160,7 +154,7 @@ struct SceneWithCameraController: View { ) var body: some View { - SceneView(scene: dataModel.scene, cameraController: cameraController) + SceneView(scene: scene, cameraController: cameraController) .onCameraChanged { newCamera in heading = newCamera.heading.rounded() } From b7e130bc63bd10bdf56733d41b79773a3e177f14 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 12:33:51 -0700 Subject: [PATCH 140/244] Remove `SceneWithCamera` sample --- Examples/Examples/CompassExampleView.swift | 98 ++++------------------ 1 file changed, 16 insertions(+), 82 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index eb230f664..471e4596d 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -20,14 +20,12 @@ struct CompassExampleView: View { /// A scenario represents a type of environment a compass may be used in. enum Scenario: CaseIterable { case map - case sceneWithCamera case sceneWithCameraController /// A human-readable label for the scenario. var label: String { switch self { case .map: return "Map" - case .sceneWithCamera: return "Scene with camera" case .sceneWithCameraController: return "Scene with camera controller" } } @@ -41,8 +39,6 @@ struct CompassExampleView: View { switch scenario { case.map: MapWithViewpoint() - case .sceneWithCamera: - SceneWithCamera() case .sceneWithCameraController: SceneWithCameraController() } @@ -68,74 +64,12 @@ struct MapWithViewpoint: View { ) var body: some View { - MapViewReader { mapViewProxy in - MapView(map: map, viewpoint: viewpoint) - .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint) - .padding() - .onTapGesture { - Task { - try? await mapViewProxy.setViewpointRotation(0) - } - } - } - } - } -} - -/// An example demonstrating how to use a compass with a scene view and camera. -struct SceneWithCamera: View { - /// The camera used by the scene view. - @State private var camera: Camera? = Camera( - lookingAt: .esriRedlands, - distance: 1_000, - heading: 45, - pitch: 45, - roll: .zero - ) - - /// The data model containing the `Scene` displayed in the `SceneView`. - @StateObject private var dataModel = SceneDataModel( - scene: Scene(basemapStyle: .arcGISImagery) - ) - - /// The current heading as reported by the scene view. - var heading: Binding { - Binding { - if let camera { - return camera.heading - } else { - return .zero + MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: .topTrailing) { + Compass(viewpoint: $viewpoint) + .padding() } - } set: { _ in - } - } - - var body: some View { - SceneViewReader { sceneViewProxy in - SceneView(scene: dataModel.scene, camera: $camera) - .overlay(alignment: .topTrailing) { - Compass(viewpointRotation: heading) - .padding() - .onTapGesture { - if let camera { - let newCamera = Camera( - location: camera.location, - heading: .zero, - pitch: camera.pitch, - roll: camera.roll - ) - Task { - try? await sceneViewProxy.setViewpointCamera( - newCamera, - duration: 0.3 - ) - } - } - } - } - } } } @@ -159,18 +93,18 @@ struct SceneWithCameraController: View { heading = newCamera.heading.rounded() } .overlay(alignment: .topTrailing) { - Compass(viewpointRotation: $heading) - .padding() - .onTapGesture { - Task { - try? await cameraController.moveCamera( - distanceDelta: .zero, - headingDelta: heading > 180 ? 360 - heading : -heading, - pitchDelta: .zero, - duration: 0.3 - ) - } + Compass( + viewpointRotation: $heading, + action: { + _ = try? await cameraController.moveCamera( + distanceDelta: .zero, + headingDelta: heading > 180 ? 360 - heading : -heading, + pitchDelta: .zero, + duration: 0.3 + ) } + ) + .padding() } } } From 6435d32c5b6cbcdf3d3c891615fb4bbebf28dfbb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 12:34:21 -0700 Subject: [PATCH 141/244] Add action to compass --- .../Components/Compass/Compass.swift | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index f2dbe9114..4c22182db 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -21,9 +21,15 @@ public struct Compass: View { /// hide/show itself when the heading is `0`. private let autoHide: Bool + /// The last time the compass was tapped. + @State private var lastTapTime: Date? + /// The opacity of the compass. @State private var opacity: Double = .zero + /// An action to perform when the compass is tapped. + private var action: (() async -> Void)? + /// A Boolean value indicating whether the compass should hide based on the /// current heading and whether the compass automatically hides. var shouldHide: Bool { @@ -41,13 +47,16 @@ public struct Compass: View { /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. + /// - action: An action to perform when the compass is tapped. /// - autoHide: A Boolean value that determines whether the compass /// automatically hides itself when the heading is `0`. public init( heading: Binding, + action: (() async -> Void)? = nil, autoHide: Bool = true ) { _heading = heading + self.action = action self.autoHide = autoHide } @@ -61,6 +70,7 @@ public struct Compass: View { .aspectRatio(1, contentMode: .fit) .opacity(opacity) .frame(width: size, height: size) + .onAppear { opacity = shouldHide ? 0 : 1 } .onChange(of: heading) { _ in let newOpacity: Double = shouldHide ? .zero : 1 guard opacity != newOpacity else { return } @@ -68,8 +78,15 @@ public struct Compass: View { opacity = newOpacity } } - .onAppear { opacity = shouldHide ? 0 : 1 } + .onTapGesture { lastTapTime = .now } .accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)") + .task(id: lastTapTime) { + if let action { + await action() + } else { + heading = .zero + } + } } } } @@ -81,10 +98,12 @@ public extension Compass { /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. + /// - action: An action to perform when the compass is tapped. /// - autoHide: A Boolean value that determines whether the compass /// automatically hides itself when the viewpoint rotation is 0 degrees. init( viewpointRotation: Binding, + action: (() async -> Void)? = nil, autoHide: Bool = true ) { let heading = Binding(get: { @@ -92,16 +111,18 @@ public extension Compass { }, set: { newHeading in viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading }) - self.init(heading: heading, autoHide: autoHide) + self.init(heading: heading, action: action, autoHide: autoHide) } /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. + /// - action: An action to perform when the compass is tapped. /// - autoHide: A Boolean value that determines whether the compass automatically hides itself /// when the viewpoint's rotation is 0 degrees. init( viewpoint: Binding, + action: (() async -> Void)? = nil, autoHide: Bool = true ) { let viewpointRotation = Binding { @@ -114,7 +135,7 @@ public extension Compass { rotation: newViewpointRotation ) } - self.init(viewpointRotation: viewpointRotation, autoHide: autoHide) + self.init(viewpointRotation: viewpointRotation, action: action, autoHide: autoHide) } /// Define a custom size for the compass. From ad9015057bd0aa56ca7b901c28f1d45a314b15d8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 12:48:14 -0700 Subject: [PATCH 142/244] Reorganize --- .../ArcGISToolkit/Components/Compass/Compass.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 4c22182db..d835c3fb7 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -17,10 +17,6 @@ import SwiftUI /// A `Compass` (alias North arrow) shows where north is in a `MapView` or /// `SceneView`. public struct Compass: View { - /// A Boolean value indicating whether the compass should automatically - /// hide/show itself when the heading is `0`. - private let autoHide: Bool - /// The last time the compass was tapped. @State private var lastTapTime: Date? @@ -28,16 +24,20 @@ public struct Compass: View { @State private var opacity: Double = .zero /// An action to perform when the compass is tapped. - private var action: (() async -> Void)? + private let action: (() async -> Void)? + + /// A Boolean value indicating whether the compass should automatically + /// hide/show itself when the heading is `0`. + private let autoHide: Bool /// A Boolean value indicating whether the compass should hide based on the /// current heading and whether the compass automatically hides. - var shouldHide: Bool { + private var shouldHide: Bool { (heading.isZero || heading.isNaN) && autoHide } /// The width and height of the compass. - var size: CGFloat = 44 + private var size: CGFloat = 44 /// The heading of the compass in degrees. @Binding private var heading: Double From 38c0f097627841c059c3fa2155b3d7c04ce0d8d1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 12:53:26 -0700 Subject: [PATCH 143/244] Update Compass.swift --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index d835c3fb7..9d0648f7e 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -17,9 +17,6 @@ import SwiftUI /// A `Compass` (alias North arrow) shows where north is in a `MapView` or /// `SceneView`. public struct Compass: View { - /// The last time the compass was tapped. - @State private var lastTapTime: Date? - /// The opacity of the compass. @State private var opacity: Double = .zero @@ -78,15 +75,14 @@ public struct Compass: View { opacity = newOpacity } } - .onTapGesture { lastTapTime = .now } - .accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)") - .task(id: lastTapTime) { + .onTapGesture { if let action { - await action() + Task { await action() } } else { heading = .zero } } + .accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)") } } } From 13ebe9fe01610bf61ba8bc636138b9e80536020e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 13:11:39 -0700 Subject: [PATCH 144/244] Update CompassExampleView.swift --- Examples/Examples/CompassExampleView.swift | 37 ++++++++++++---------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 471e4596d..d318a727d 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -18,34 +18,39 @@ import SwiftUI /// An example demonstrating how to use a compass in three different environments. struct CompassExampleView: View { /// A scenario represents a type of environment a compass may be used in. - enum Scenario: CaseIterable { + enum Scenario: String { case map - case sceneWithCameraController - - /// A human-readable label for the scenario. - var label: String { - switch self { - case .map: return "Map" - case .sceneWithCameraController: return "Scene with camera controller" - } - } + case scene } /// The active scenario. @State private var scenario = Scenario.map var body: some View { - VStack { + Group { switch scenario { - case.map: + case .map: MapWithViewpoint() - case .sceneWithCameraController: + case .scene: SceneWithCameraController() } - Picker("Scenario", selection: $scenario) { - ForEach(Scenario.allCases, id: \.self) { scen in - Text(scen.label) + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Menu(scenario.rawValue.capitalized) { + Button { + scenario = .map + } label: { + Label("Map", systemImage: "map.fill") + } + + Button { + scenario = .scene + } label: { + Label("Scene", systemImage: "globe.americas.fill") + } } + .labelStyle(.titleAndIcon) } } } From e59a4f575b794450f93af2639f2f1edf552f99d7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 13:19:58 -0700 Subject: [PATCH 145/244] Doc --- Documentation/Compass/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index b0df66593..5fdd376c7 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -23,9 +23,10 @@ Compass: /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. + /// - action: An action to perform when the compass is tapped. /// - autoHide: A Boolean value that determines whether the compass /// automatically hides itself when the heading is `0`. - public init(heading: Binding, autoHide: Bool = true) + public init(heading: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) ``` ```swift @@ -35,18 +36,20 @@ Compass: /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. + /// - action: An action to perform when the compass is tapped. /// - autoHide: A Boolean value that determines whether the compass /// automatically hides itself when the viewpoint rotation is 0 degrees. - public init(viewpointRotation: Binding, autoHide: Bool = true) + public init(viewpointRotation: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) ``` ```swift /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. + /// - action: An action to perform when the compass is tapped. /// - autoHide: A Boolean value that determines whether the compass automatically hides itself /// when the viewpoint's rotation is 0 degrees. - public init(viewpoint: Binding, autoHide: Bool = true) + public init(viewpoint: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) ``` `Compass` has the following modifier: From 4d91afe156b173ef7f308fa038b8cf94a4a6616f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Mar 2023 13:20:48 -0700 Subject: [PATCH 146/244] Update Compass.swift --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 9d0648f7e..fe96e703f 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -29,7 +29,7 @@ public struct Compass: View { /// A Boolean value indicating whether the compass should hide based on the /// current heading and whether the compass automatically hides. - private var shouldHide: Bool { + var shouldHide: Bool { (heading.isZero || heading.isNaN) && autoHide } From cc721b1c73c599bc36f2e29f3bac20a17bc1e159 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 28 Mar 2023 16:29:33 -0700 Subject: [PATCH 147/244] rough test --- .../UtilityNetworkTraceViewModel.swift | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 7d8102416..5a05e440f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -125,11 +125,20 @@ import SwiftUI mapPoint: Point, with proxy: MapViewProxy ) async { - let identifyLayerResults = try? await proxy.identifyLayers( - screenPoint: point, - tolerance: 10 - ) - for layerResult in identifyLayerResults ?? [] { + + let identify: (Layer, CGPoint) async -> IdentifyLayerResult? = { layer, point in + try? await proxy.identify(on: layer, screenPoint: point, tolerance: 10) + } + + var identifyLayerResults = [IdentifyLayerResult]() + + for layer in layers ?? [] { + if let r = await identify(layer, point) { + identifyLayerResults.append(r) + } + } + + for layerResult in identifyLayerResults { for geoElement in layerResult.geoElements { let startingPoint = UtilityNetworkTraceStartingPoint( geoElement: geoElement, @@ -140,6 +149,10 @@ import SwiftUI } } + var layers: [Layer]? { + network?.definition?.networkSources.compactMap { $0.featureTable.layer } + } + /// Deletes all of the completed traces. func deleteAllTraces() { selectedTraceIndex = nil From 28240a5ee45093a9eb551a1a69805a5cccddb56a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 29 Mar 2023 08:49:48 -0700 Subject: [PATCH 148/244] Move `layers` to extension --- .../UtilityNetworkTraceViewModel.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 5a05e440f..2a0883159 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -132,7 +132,7 @@ import SwiftUI var identifyLayerResults = [IdentifyLayerResult]() - for layer in layers ?? [] { + for layer in network?.layers ?? [] { if let r = await identify(layer, point) { identifyLayerResults.append(r) } @@ -149,10 +149,6 @@ import SwiftUI } } - var layers: [Layer]? { - network?.definition?.networkSources.compactMap { $0.featureTable.layer } - } - /// Deletes all of the completed traces. func deleteAllTraces() { selectedTraceIndex = nil @@ -521,3 +517,9 @@ extension UtilityNetworkTraceViewModel { ) } } + +extension UtilityNetwork { + var layers: [Layer] { + definition?.networkSources.compactMap { $0.featureTable.layer } ?? [] + } +} From 48b230fbe909f1527be754e2f915fc0fc7502e69 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 29 Mar 2023 09:19:53 -0700 Subject: [PATCH 149/244] Refactor, rename `addStartingPoint` --- .../UtilityNetworkTrace.swift | 2 +- .../UtilityNetworkTraceViewModel.swift | 36 ++++++------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 55dcc189c..87705ffc5 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -615,7 +615,7 @@ public struct UtilityNetworkTrace: View { currentActivity = .creatingTrace(.viewingStartingPoints) activeDetent = .half Task { - await viewModel.addStartingPoint( + await viewModel.addStartingPoints( at: viewPoint, mapPoint: mapPoint, with: mapViewProxy diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 2a0883159..c5e769cfc 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -115,36 +115,22 @@ import SwiftUI } } - /// Adds a new starting point to the pending trace. + /// Adds new starting points to the pending trace. /// - Parameters: /// - point: A point on the map in screen coordinates. /// - mapPoint: A point on the map in map coordinates. /// - proxy: Provides a method of layer identification. - func addStartingPoint( - at point: CGPoint, - mapPoint: Point, - with proxy: MapViewProxy - ) async { - - let identify: (Layer, CGPoint) async -> IdentifyLayerResult? = { layer, point in - try? await proxy.identify(on: layer, screenPoint: point, tolerance: 10) - } - - var identifyLayerResults = [IdentifyLayerResult]() - + /// + /// An identify operation will run on each layer in the network. Every element returned from + /// each layer will be added as a new starting point. + func addStartingPoints(at point: CGPoint, mapPoint: Point, with proxy: MapViewProxy) async { for layer in network?.layers ?? [] { - if let r = await identify(layer, point) { - identifyLayerResults.append(r) - } - } - - for layerResult in identifyLayerResults { - for geoElement in layerResult.geoElements { - let startingPoint = UtilityNetworkTraceStartingPoint( - geoElement: geoElement, - mapPoint: mapPoint - ) - await processAndAdd(startingPoint: startingPoint) + if let result = try? await proxy.identify(on: layer, screenPoint: point, tolerance: 10) { + for element in result.geoElements { + await processAndAdd( + startingPoint: UtilityNetworkTraceStartingPoint(geoElement: element, mapPoint: mapPoint) + ) + } } } } From 92eb79cadb08fac9c4ef3fafa828ac63eb39dd3c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 29 Mar 2023 10:22:45 -0700 Subject: [PATCH 150/244] Add doc --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index c5e769cfc..3e6802a64 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -505,6 +505,7 @@ extension UtilityNetworkTraceViewModel { } extension UtilityNetwork { + /// The defined in the network. var layers: [Layer] { definition?.networkSources.compactMap { $0.featureTable.layer } ?? [] } From b699a377ac2075347e1ab1ad8df0c66892727ddc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 29 Mar 2023 13:56:51 -0700 Subject: [PATCH 151/244] Implement feedback --- .../UtilityNetworkTraceViewModel.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 3e6802a64..5d935947b 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -124,12 +124,17 @@ import SwiftUI /// An identify operation will run on each layer in the network. Every element returned from /// each layer will be added as a new starting point. func addStartingPoints(at point: CGPoint, mapPoint: Point, with proxy: MapViewProxy) async { - for layer in network?.layers ?? [] { - if let result = try? await proxy.identify(on: layer, screenPoint: point, tolerance: 10) { - for element in result.geoElements { - await processAndAdd( - startingPoint: UtilityNetworkTraceStartingPoint(geoElement: element, mapPoint: mapPoint) - ) + await withTaskGroup(of: Void.self) { [weak self] taskGroup in + guard let self else { return } + for layer in network?.layers ?? [] { + taskGroup.addTask { + if let result = try? await proxy.identify(on: layer, screenPoint: point, tolerance: 10) { + for element in result.geoElements { + await self.processAndAdd( + startingPoint: UtilityNetworkTraceStartingPoint(geoElement: element, mapPoint: mapPoint) + ) + } + } } } } From fa76d4a4b34df27adc1fe484e6045cbbc85168f4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 29 Mar 2023 15:18:02 -0700 Subject: [PATCH 152/244] Use `@StateObject` for view model https://developer.apple.com/documentation/swiftui/stateobject#:~:text=Use%20a%20state%20object%20as%20the%20single%20source%20of%20truth%20for%20a%20reference%20type%20that%20you%20store%20in%20a%20view%20hierarchy.%20Create%20a%20state%20object%20in%20an%20App%2C%20Scene%2C%20or%20View%20by%20applying%20the%20%40StateObject%20attribute%20to%20a%20property%20declaration%20and%20providing%20an%20initial%20value%20that%20conforms%20to%20the%20ObservableObject%20protocol. --- .../Components/BasemapGallery/BasemapGallery.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 0c3a3445a..abf75d493 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -42,7 +42,7 @@ public struct BasemapGallery: View { items: [BasemapGalleryItem] = [], geoModel: GeoModel? = nil ) { - viewModel = BasemapGalleryViewModel(geoModel: geoModel, items: items) + _viewModel = StateObject(wrappedValue: BasemapGalleryViewModel(geoModel: geoModel, items: items)) } /// Creates a `BasemapGallery` with the given geo model and portal. @@ -54,14 +54,14 @@ public struct BasemapGallery: View { portal: Portal, geoModel: GeoModel? = nil ) { - viewModel = BasemapGalleryViewModel(geoModel, portal: portal) + _viewModel = StateObject(wrappedValue: BasemapGalleryViewModel(geoModel, portal: portal)) } /// The view model used by the view. The `BasemapGalleryViewModel` manages the state /// of the `BasemapGallery`. The view observes `BasemapGalleryViewModel` for changes /// in state. The view updates the state of the `BasemapGalleryViewModel` in response to /// user action. - @ObservedObject private var viewModel: BasemapGalleryViewModel + @StateObject private var viewModel: BasemapGalleryViewModel /// The style of the basemap gallery. The gallery can be displayed as a list, grid, or automatically /// switch between the two based on-screen real estate. Defaults to ``BasemapGallery/Style/automatic``. From 2a9b3fb0b06906aa014b2664dc50d69c44fd6d86 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 09:29:23 -0700 Subject: [PATCH 153/244] Indentation --- .../Components/BasemapGallery/BasemapGalleryViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 68fed46f7..01bdd5648 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -51,7 +51,7 @@ import ArcGIS items = [] self.geoModel = geoModel geoModelDidChange(nil) - + self.portal = portal portalDidChange(portal) } @@ -80,7 +80,7 @@ import ArcGIS portalDidChange(oldValue) } } - + /// The list of basemaps shown in the gallery. @Published var items: [BasemapGalleryItem] From a0cc5481d2b1d0e158794e79cff0aa0dcbfe15b9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 09:31:21 -0700 Subject: [PATCH 154/244] Indentation --- .../Components/BasemapGallery/BasemapGalleryViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 01bdd5648..0aeb49862 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -62,7 +62,7 @@ import ArcGIS /// The error signifying the spatial reference of ``geoModel`` and the spatial reference of /// ``currentItem`` do not match. @Published private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil - + /// If the `GeoModel` is not loaded when passed to the `BasemapGalleryViewModel`, then /// the geoModel will be immediately loaded. The spatial reference of geoModel dictates which /// basemaps from the gallery are enabled. When an enabled basemap is selected by the user, @@ -109,7 +109,7 @@ import ArcGIS func portalDidChange(_ previousPortal: Portal?) { // Remove all items from `items`. items.removeAll() - + guard let portal = portal else { return } fetchBasemaps(from: portal) } From 871285ebc330538a8f99f6f5f84404a8ad6932de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 09:41:22 -0700 Subject: [PATCH 155/244] Simplify example This was redundant as the navigation title is provided in `ExampleList` --- Examples/Examples/BasemapGalleryExampleView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 6115b5187..7e2ff05b8 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -42,7 +42,6 @@ struct BasemapGalleryExampleView: View { .padding() } } - .navigationTitle("Basemap Gallery") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Toggle(isOn: $showBasemapGallery) { From ba7b10ffd507d2a31a5b3e741b26684286adf5b6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 12:03:52 -0700 Subject: [PATCH 156/244] Remove border per issue description --- .../ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index abf75d493..87caacf1a 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -118,7 +118,6 @@ public struct BasemapGallery: View { Text(item.message) } .frame(height: min(contentHeight, geometry.size.height)) - .esriBorder() } .frame(width: galleryWidth) } From 59fa20f239015ef25702d82cf1768f8fe9a0e56d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 12:47:47 -0700 Subject: [PATCH 157/244] Update example uses --- .../Examples/BasemapGalleryExampleView.swift | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 7e2ff05b8..3ce326cd3 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -35,13 +35,32 @@ struct BasemapGalleryExampleView: View { var body: some View { MapView(map: dataModel.map, viewpoint: initialViewpoint) - .overlay(alignment: .topTrailing) { - if showBasemapGallery { - BasemapGallery(items: basemaps, geoModel: dataModel.map) - .style(.automatic()) - .padding() - } + + // Display in a sheet + // + .sheet(isPresented: $showBasemapGallery) { + BasemapGallery(items: basemaps, geoModel: dataModel.map) + .padding() } + + // Display in a floating panel + // +// .floatingPanel(isPresented: $showBasemapGallery) { +// BasemapGallery(items: basemaps, geoModel: dataModel.map) +// } + + // Display in an overlay + // +// .overlay(alignment: .topTrailing) { +// if showBasemapGallery { +// BasemapGallery(items: basemaps, geoModel: dataModel.map) +// .style(.list) +// .frame(width: 150, height: 400) +// .esriBorder() +// .padding() +// } +// } + .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Toggle(isOn: $showBasemapGallery) { From feb6ce8c2e4ed2e5ab9225a0bc68a796aa2738dc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 12:52:30 -0700 Subject: [PATCH 158/244] Change frame --- .../Components/BasemapGallery/BasemapGallery.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 87caacf1a..de7f18fe5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -117,9 +117,11 @@ public struct BasemapGallery: View { } message: { item in Text(item.message) } - .frame(height: min(contentHeight, geometry.size.height)) + .frame( + width: geometry.size.width, + height: geometry.size.height + ) } - .frame(width: galleryWidth) } } From 92b78bfb36a893f15c520c8e184329c819d2df81 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 12:52:55 -0700 Subject: [PATCH 159/244] Remove style associated values --- .../BasemapGallery/BasemapGallery.swift | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index de7f18fe5..1d06b1f3d 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -25,11 +25,11 @@ public struct BasemapGallery: View { /// The `BasemapGallery` will display as a grid when there is an appropriate /// width available for the gallery to do so. Otherwise, the gallery will display as a list. /// Defaults to `125` when displayed as a list, `300` when displayed as a grid. - case automatic(listWidth: CGFloat = 125, gridWidth: CGFloat = 300) + case automatic /// The `BasemapGallery` will display as a grid. Defaults to `300`. - case grid(width: CGFloat = 300) + case grid /// The `BasemapGallery` will display as a list. Defaults to `125`. - case list(width: CGFloat = 125) + case list } /// Creates a `BasemapGallery` with the given geo model and array of basemap gallery items. @@ -66,7 +66,7 @@ public struct BasemapGallery: View { /// The style of the basemap gallery. The gallery can be displayed as a list, grid, or automatically /// switch between the two based on-screen real estate. Defaults to ``BasemapGallery/Style/automatic``. /// Set using the `style` modifier. - private var style: Style = .automatic() + private var style: Style = .automatic @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -77,18 +77,6 @@ public struct BasemapGallery: View { !(horizontalSizeClass == .compact && verticalSizeClass == .regular) } - /// The width of the gallery, taking into account the horizontal and vertical size classes of the device. - private var galleryWidth: CGFloat { - switch style { - case .list(let width): - return width - case .grid(let width): - return width - case .automatic(let listWidth, let gridWidth): - return isRegularWidth ? gridWidth : listWidth - } - } - /// A Boolean value indicating whether to show an error alert. @State private var showErrorAlert = false From 6f8c107b3f7c491a0e75a27f387e6bc8353fa6a0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 12:57:31 -0700 Subject: [PATCH 160/244] Remove map data model --- Examples/Examples/BasemapGalleryExampleView.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 3ce326cd3..660c67a84 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -16,10 +16,8 @@ import ArcGIS import ArcGISToolkit struct BasemapGalleryExampleView: View { - /// The data model containing the `Map` displayed in the `MapView`. - @StateObject private var dataModel = MapDataModel( - map: Map(basemapStyle: .arcGISImagery) - ) + /// The `Map` displayed in the `MapView`. + @State private var map = Map(basemapStyle: .arcGISImagery) /// A Boolean value indicating whether to show the basemap gallery. @State private var showBasemapGallery = false @@ -34,7 +32,7 @@ struct BasemapGalleryExampleView: View { private let basemaps = initialBasemaps() var body: some View { - MapView(map: dataModel.map, viewpoint: initialViewpoint) + MapView(map: map, viewpoint: initialViewpoint) // Display in a sheet // From f76661bebe604a40c1b1e3c49856498632513b99 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 12:57:44 -0700 Subject: [PATCH 161/244] Update BasemapGalleryExampleView.swift --- Examples/Examples/BasemapGalleryExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 660c67a84..5666ca7c3 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -37,7 +37,7 @@ struct BasemapGalleryExampleView: View { // Display in a sheet // .sheet(isPresented: $showBasemapGallery) { - BasemapGallery(items: basemaps, geoModel: dataModel.map) + BasemapGallery(items: basemaps, geoModel: map) .padding() } From e22d9882315987cb7be2d1ba2c68da0e6835ab6f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 12:59:17 -0700 Subject: [PATCH 162/244] Always choose grid for sheet iPhone looks too wide otherwise --- Examples/Examples/BasemapGalleryExampleView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 5666ca7c3..737858f5d 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -38,6 +38,7 @@ struct BasemapGalleryExampleView: View { // .sheet(isPresented: $showBasemapGallery) { BasemapGallery(items: basemaps, geoModel: map) + .style(.grid) .padding() } From 9be9a92f43170f02645789e3ecf84d786fd209a2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 13:06:49 -0700 Subject: [PATCH 163/244] Update BasemapGalleryExampleView.swift --- Examples/Examples/BasemapGalleryExampleView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 737858f5d..09822c574 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -45,7 +45,8 @@ struct BasemapGalleryExampleView: View { // Display in a floating panel // // .floatingPanel(isPresented: $showBasemapGallery) { -// BasemapGallery(items: basemaps, geoModel: dataModel.map) +// BasemapGallery(items: basemaps, geoModel: map) +// .style(.grid) // } // Display in an overlay From 26c369f932a23c8ff4b40607d3a21bae651405aa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 13:07:21 -0700 Subject: [PATCH 164/244] Update BasemapGalleryExampleView.swift --- Examples/Examples/BasemapGalleryExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 09822c574..285a9e1b8 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -53,7 +53,7 @@ struct BasemapGalleryExampleView: View { // // .overlay(alignment: .topTrailing) { // if showBasemapGallery { -// BasemapGallery(items: basemaps, geoModel: dataModel.map) +// BasemapGallery(items: basemaps, geoModel: map) // .style(.list) // .frame(width: 150, height: 400) // .esriBorder() From 1807f242bfa718d5b9266fe2b899c495618bd0e9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 13:24:01 -0700 Subject: [PATCH 165/244] This should be no longer needed --- .../Components/BasemapGallery/BasemapGallery.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 1d06b1f3d..88aa751e5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -83,9 +83,6 @@ public struct BasemapGallery: View { /// The current alert item to display. @State private var alertItem: AlertItem? - /// The height of the basemap gallery content. - @State private var contentHeight: CGFloat = .zero - public var body: some View { GeometryReader { geometry in makeGalleryView() @@ -179,9 +176,6 @@ private extension BasemapGallery { } } } - .onSizeChange { - contentHeight = $0.height - } } } From f7b6477c1d62e1b145c3c365c0428ebd73a016af Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 15:52:05 -0700 Subject: [PATCH 166/244] Update BookmarksExampleView.swift --- Examples/Examples/BookmarksExampleView.swift | 74 +++++++++----------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 25170e185..de924a39e 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -16,10 +16,8 @@ import ArcGISToolkit import SwiftUI struct BookmarksExampleView: View { - /// The data model containing a `Map` with predefined bookmarks. - @StateObject private var dataModel = MapDataModel( - map: Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! - ) + /// The `Map` with predefined bookmarks. + @State private var map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! /// Indicates if the `Bookmarks` component is shown or not. /// - Remark: This allows a developer to control when the `Bookmarks` component is @@ -31,44 +29,38 @@ struct BookmarksExampleView: View { @State var viewpoint: Viewpoint? var body: some View { - MapView(map: dataModel.map, viewpoint: viewpoint) - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button { - showingBookmarks.toggle() - } label: { - Label( - "Show Bookmarks", - systemImage: "bookmark" - ) - } - .popover(isPresented: $showingBookmarks) { - // Display the `Bookmarks` components with a pre-defined - // list of bookmarks. Passing in a `Viewpoint` binding - // will allow the `Bookmarks` component to handle - // bookmark selection. - Bookmarks( - isPresented: $showingBookmarks, - mapOrScene: dataModel.map, - viewpoint: $viewpoint - ) - - // Display the `Bookmarks` component with the list of - // bookmarks in a map. -// Bookmarks( -// isPresented: $showingBookmarks, -// mapOrScene: map -// ) - // In order to handle bookmark selection changes - // manually, use `onSelectionChanged(perform:)`. -// .onSelectionChanged { -// viewpoint = $0.viewpoint -// } + MapViewReader { mapViewProxy in + MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + showingBookmarks.toggle() + } label: { + Label( + "Show Bookmarks", + systemImage: "bookmark" + ) + } + .popover(isPresented: $showingBookmarks) { + // Display the `Bookmarks` component with the list of + // bookmarks in a map. + Bookmarks( + isPresented: $showingBookmarks, + mapOrScene: map + ) + // In order to handle bookmark selection changes + // manually, use `onSelectionChanged(perform:)`. + .onSelectionChanged { bookmark in + if let viewpoint = bookmark.viewpoint { + Task { await mapViewProxy.setViewpoint(viewpoint, duration: 1) } + } + } + } } } - } + } } } From e2a9378fbb5f07b24d4b6922e02ab083c9161cf3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 16:03:18 -0700 Subject: [PATCH 167/244] Rename parameter to match basemap gallery --- Examples/Examples/BookmarksExampleView.swift | 2 +- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index de924a39e..e67a82c2e 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -49,7 +49,7 @@ struct BookmarksExampleView: View { // bookmarks in a map. Bookmarks( isPresented: $showingBookmarks, - mapOrScene: map + geoModel: map ) // In order to handle bookmark selection changes // manually, use `onSelectionChanged(perform:)`. diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 3480c4caf..8771f3830 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -90,16 +90,16 @@ public struct Bookmarks: View { /// Creates a `Bookmarks` component. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. - /// - mapOrScene: A `GeoModel` authored with pre-existing bookmarks. + /// - geoModel: A `GeoModel` authored with pre-existing bookmarks. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. /// Alternately, you can use the `onSelectionChanged(perform:)` modifier to handle /// bookmark selection. public init( isPresented: Binding, - mapOrScene: GeoModel, + geoModel: GeoModel, viewpoint: Binding? = nil ) { - geoModel = mapOrScene + self.geoModel = geoModel self.viewpoint = viewpoint _isPresented = isPresented } From 9a57f725fb29f6b0e3d63849252bc1e38ba38d8f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 16:03:23 -0700 Subject: [PATCH 168/244] Update README.md --- Documentation/Bookmarks/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Bookmarks/README.md b/Documentation/Bookmarks/README.md index 385016581..3e19110f5 100644 --- a/Documentation/Bookmarks/README.md +++ b/Documentation/Bookmarks/README.md @@ -36,7 +36,7 @@ Bookmarks: /// Creates a `Bookmarks` component. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. - /// - mapOrScene: A `GeoModel` authored with pre-existing bookmarks. + /// - geoModel: A `GeoModel` authored with pre-existing bookmarks. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. /// Alternately, you can use the `onSelectionChanged(perform:)` modifier to handle /// bookmark selection. @@ -95,7 +95,7 @@ var body: some View { // bookmark selection. Bookmarks( isPresented: $showingBookmarks, - mapOrScene: map, + geoModel: map, viewpoint: $viewpoint ) } From 08d4ce7dbd6cacaa5416dc2a456b5a28ae72e305 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Mar 2023 16:51:24 -0700 Subject: [PATCH 169/244] Update Scalebar.swift --- Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index ca19bf4ea..25d4f4e0d 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -96,7 +96,7 @@ public struct Scalebar: View { useGeodeticCalculations: Bool = true, viewpoint: Binding ) { - self.opacity = settings.autoHide ? .zero : 1 + _opacity = State(initialValue: settings.autoHide ? .zero : 1) self.settings = settings self.style = style self.viewpoint = viewpoint From 3ef79b6baf239731da22b2f0d6f3355cae723199 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 Mar 2023 12:12:18 -0500 Subject: [PATCH 170/244] Move autoHide to modifier (from initializers) make action capable of being a closure. --- Examples/Examples/CompassExampleView.swift | 36 ++++++++++--------- .../Components/Compass/Compass.swift | 25 +++++++------ 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index d318a727d..3a69be441 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -69,12 +69,19 @@ struct MapWithViewpoint: View { ) var body: some View { - MapView(map: map, viewpoint: viewpoint) - .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint) + MapViewReader { proxy in + MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: .topTrailing) { + Compass(viewpoint: $viewpoint) { + _ = try? await proxy.setViewpoint( + viewpoint.withRotation(0), + duration: 0.25 + ) + } .padding() - } + } + } } } @@ -98,17 +105,14 @@ struct SceneWithCameraController: View { heading = newCamera.heading.rounded() } .overlay(alignment: .topTrailing) { - Compass( - viewpointRotation: $heading, - action: { - _ = try? await cameraController.moveCamera( - distanceDelta: .zero, - headingDelta: heading > 180 ? 360 - heading : -heading, - pitchDelta: .zero, - duration: 0.3 - ) - } - ) + Compass(viewpointRotation: $heading) { + _ = try? await cameraController.moveCamera( + distanceDelta: .zero, + headingDelta: heading > 180 ? 360 - heading : -heading, + pitchDelta: .zero, + duration: 0.3 + ) + } .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index fe96e703f..96aef9937 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -25,7 +25,7 @@ public struct Compass: View { /// A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. - private let autoHide: Bool + private var autoHide: Bool = true /// A Boolean value indicating whether the compass should hide based on the /// current heading and whether the compass automatically hides. @@ -49,12 +49,10 @@ public struct Compass: View { /// automatically hides itself when the heading is `0`. public init( heading: Binding, - action: (() async -> Void)? = nil, - autoHide: Bool = true + action: (() async -> Void)? = nil ) { _heading = heading self.action = action - self.autoHide = autoHide } public var body: some View { @@ -99,15 +97,14 @@ public extension Compass { /// automatically hides itself when the viewpoint rotation is 0 degrees. init( viewpointRotation: Binding, - action: (() async -> Void)? = nil, - autoHide: Bool = true + action: (() async -> Void)? = nil ) { let heading = Binding(get: { viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue }, set: { newHeading in viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading }) - self.init(heading: heading, action: action, autoHide: autoHide) + self.init(heading: heading, action: action) } /// Creates a compass with a binding to an optional viewpoint. @@ -118,8 +115,7 @@ public extension Compass { /// when the viewpoint's rotation is 0 degrees. init( viewpoint: Binding, - action: (() async -> Void)? = nil, - autoHide: Bool = true + action: (() async -> Void)? = nil ) { let viewpointRotation = Binding { viewpoint.wrappedValue?.rotation ?? .nan @@ -131,7 +127,7 @@ public extension Compass { rotation: newViewpointRotation ) } - self.init(viewpointRotation: viewpointRotation, action: action, autoHide: autoHide) + self.init(viewpointRotation: viewpointRotation, action: action) } /// Define a custom size for the compass. @@ -141,4 +137,13 @@ public extension Compass { copy.size = size return copy } + + /// Specifies whether the ``Compass`` should automatically hide when the heading is 0. + /// - Parameter newAutomaticallyHides: A Boolean value indicating whether the compass should automatically + /// hide/show itself when the heading is `0`. + func automaticallyHides(_ newAutomaticallyHides: Bool) -> some View { + var copy = self + copy.autoHide = newAutomaticallyHides + return copy + } } From 2df154156621804071e3d23176e4c82b8aafd4d8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 Mar 2023 12:23:50 -0500 Subject: [PATCH 171/244] Tweak examples; add Viewpoint.withViewpoint method; update doc. --- Examples/Examples/CompassExampleView.swift | 5 ++- .../Components/Compass/Compass.swift | 5 --- .../ArcGISToolkit/Extensions/Viewpoint.swift | 35 +++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Viewpoint.swift diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 3a69be441..ee7558a89 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -74,7 +74,9 @@ struct MapWithViewpoint: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { Compass(viewpoint: $viewpoint) { - _ = try? await proxy.setViewpoint( + guard let viewpoint else { return } + // Animate the map view to zero when the compass is tapped. + _ = await proxy.setViewpoint( viewpoint.withRotation(0), duration: 0.25 ) @@ -106,6 +108,7 @@ struct SceneWithCameraController: View { } .overlay(alignment: .topTrailing) { Compass(viewpointRotation: $heading) { + // Animate the scene view when the compass is tapped. _ = try? await cameraController.moveCamera( distanceDelta: .zero, headingDelta: heading > 180 ? 360 - heading : -heading, diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 96aef9937..5c26e6a92 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -45,8 +45,6 @@ public struct Compass: View { /// - Parameters: /// - heading: The heading of the compass. /// - action: An action to perform when the compass is tapped. - /// - autoHide: A Boolean value that determines whether the compass - /// automatically hides itself when the heading is `0`. public init( heading: Binding, action: (() async -> Void)? = nil @@ -93,8 +91,6 @@ public extension Compass { /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. /// - action: An action to perform when the compass is tapped. - /// - autoHide: A Boolean value that determines whether the compass - /// automatically hides itself when the viewpoint rotation is 0 degrees. init( viewpointRotation: Binding, action: (() async -> Void)? = nil @@ -111,7 +107,6 @@ public extension Compass { /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - action: An action to perform when the compass is tapped. - /// - autoHide: A Boolean value that determines whether the compass automatically hides itself /// when the viewpoint's rotation is 0 degrees. init( viewpoint: Binding, diff --git a/Sources/ArcGISToolkit/Extensions/Viewpoint.swift b/Sources/ArcGISToolkit/Extensions/Viewpoint.swift new file mode 100644 index 000000000..9bf0e397e --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Viewpoint.swift @@ -0,0 +1,35 @@ +// Copyright 2023 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 +// http://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 + +public extension Viewpoint { + /// Creates a new viewpoint with the same target geometry and scale but with a new rotation. + /// - Parameter rotation: The rotation for the new viewpoint. + /// - Returns: A new viewpoint. + func withRotation(_ rotation: Double) -> Viewpoint { + switch self.kind { + case .centerAndScale: + return Viewpoint( + center: self.targetGeometry as! Point, + scale: self.targetScale, + rotation: rotation + ) + case.boundingGeometry: + return Viewpoint( + boundingGeometry: self.targetGeometry, + rotation: rotation + ) + } + } +} From c923c5ec93b0cac3f9dfa30ffc15f09ccc941719 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 31 Mar 2023 11:31:46 -0700 Subject: [PATCH 172/244] Add `UISceneConfigurations` entry --- Examples/ExamplesApp/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/ExamplesApp/Info.plist b/Examples/ExamplesApp/Info.plist index efc211a0c..8e3dfc51d 100644 --- a/Examples/ExamplesApp/Info.plist +++ b/Examples/ExamplesApp/Info.plist @@ -24,6 +24,8 @@ UIApplicationSupportsMultipleScenes + UISceneConfigurations + UIApplicationSupportsIndirectInputEvents From c16eb883c6ed0d5d3e928b5d0b4262d12fbc5ab7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 Mar 2023 14:17:00 -0500 Subject: [PATCH 173/244] Apply suggestions from code review Co-authored-by: David Feinzimer --- Documentation/Compass/README.md | 3 --- Examples/Examples/CompassExampleView.swift | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 5fdd376c7..b43e1d1d2 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -24,7 +24,6 @@ Compass: /// - Parameters: /// - heading: The heading of the compass. /// - action: An action to perform when the compass is tapped. - /// - autoHide: A Boolean value that determines whether the compass /// automatically hides itself when the heading is `0`. public init(heading: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) ``` @@ -37,7 +36,6 @@ Compass: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. /// - action: An action to perform when the compass is tapped. - /// - autoHide: A Boolean value that determines whether the compass /// automatically hides itself when the viewpoint rotation is 0 degrees. public init(viewpointRotation: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) ``` @@ -47,7 +45,6 @@ Compass: /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - action: An action to perform when the compass is tapped. - /// - autoHide: A Boolean value that determines whether the compass automatically hides itself /// when the viewpoint's rotation is 0 degrees. public init(viewpoint: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) ``` diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index ee7558a89..cec42ea67 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -76,7 +76,7 @@ struct MapWithViewpoint: View { Compass(viewpoint: $viewpoint) { guard let viewpoint else { return } // Animate the map view to zero when the compass is tapped. - _ = await proxy.setViewpoint( + await proxy.setViewpoint( viewpoint.withRotation(0), duration: 0.25 ) From c2b6d55d6d2b7756a37ed8f97197a965e487307e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 Mar 2023 14:26:40 -0500 Subject: [PATCH 174/244] update readme for new changes. --- Documentation/Compass/README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index b43e1d1d2..219912861 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -10,7 +10,8 @@ The ArcGIS Maps SDK for Swift currently supports rotating MapViews and SceneView Compass: -- Can be configured to automatically hide when the rotation is zero. +- Automatically hides when the rotation is zero. +- Can be configured to be always visible. - Will reset the map/scene rotation to North when tapped on. ## Key properties @@ -24,8 +25,7 @@ Compass: /// - Parameters: /// - heading: The heading of the compass. /// - action: An action to perform when the compass is tapped. - /// automatically hides itself when the heading is `0`. - public init(heading: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) + public init(heading: Binding, action: (() async -> Void)? = nil) ``` ```swift @@ -36,8 +36,7 @@ Compass: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. /// - action: An action to perform when the compass is tapped. - /// automatically hides itself when the viewpoint rotation is 0 degrees. - public init(viewpointRotation: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) + public init(viewpointRotation: Binding, action: (() async -> Void)? = nil) ``` ```swift @@ -45,17 +44,17 @@ Compass: /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - action: An action to perform when the compass is tapped. - /// when the viewpoint's rotation is 0 degrees. - public init(viewpoint: Binding, action: (() async -> Void)? = nil, autoHide: Bool = true) + public init(viewpoint: Binding, action: (() async -> Void)? = nil) ``` -`Compass` has the following modifier: +`Compass` has the following modifiers: - `func compassSize(size: CGFloat)` - The size of the `Compass`, specifying both the width and height of the compass. +- `func automaticallyHides(newAutomaticallyHides: Bool)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0. ## Behavior: -Whenever the map is not orientated North (non-zero bearing) the compass appears. When reset to north, it disappears. An initializer argument allows you to disable the auto-hide feature so that it always appears. +Whenever the map is not orientated North (non-zero bearing) the compass appears. When reset to north, it disappears. The `automaticallyHides` view modifier allows you to disable the auto-hide feature so that it is always displayed. When the compass is tapped, the map orients back to north (zero bearing). From 0a202c1e2ab23d7a28b1caffb69c1dca949f570f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 Mar 2023 15:03:14 -0500 Subject: [PATCH 175/244] Fix tests; add Viewpoint tests --- Examples/Examples/CompassExampleView.swift | 1 - Tests/ArcGISToolkitTests/CompassTests.swift | 13 ++-- Tests/ArcGISToolkitTests/ViewpointTests.swift | 66 +++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/ViewpointTests.swift diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index cec42ea67..0bc95e98c 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -50,7 +50,6 @@ struct CompassExampleView: View { Label("Scene", systemImage: "globe.americas.fill") } } - .labelStyle(.titleAndIcon) } } } diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 9c8b8bb83..4b93de6b6 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -24,14 +24,15 @@ final class CompassTests: XCTestCase { let finalValue = 90.0 var _viewpoint: Viewpoint? = makeViewpoint(rotation: initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint, autoHide: false) + let compass = Compass(viewpoint: viewpoint) + .automaticallyHides(false) as! Compass XCTAssertFalse(compass.shouldHide) _viewpoint = makeViewpoint(rotation: finalValue) XCTAssertFalse(compass.shouldHide) } /// Verifies that the compass accurately indicates when the compass should be hidden when - /// `autoHide` is `true`. + /// `autoHide` is `true` (which is the default). func testHiddenWithAutoHideOn() { let initialValue = 0.0 let finalValue = 90.0 @@ -51,8 +52,9 @@ final class CompassTests: XCTestCase { /// Verifies that the compass correctly initializes when given a `nil` viewpoint, and `autoHide` is /// `false`. - func testInitNoAutoHide() { - let compass = Compass(viewpoint: .constant(nil), autoHide: false) + func testAutomaticallyHidesNoAutoHide() { + let compass = Compass(viewpoint: .constant(nil)) + .automaticallyHides(false) as! Compass XCTAssertFalse(compass.shouldHide) } @@ -64,7 +66,8 @@ final class CompassTests: XCTestCase { /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { - let compass = Compass(viewpoint: .constant(makeViewpoint(rotation: .zero)), autoHide: false) + let compass = Compass(viewpoint: .constant(makeViewpoint(rotation: .zero))) + .automaticallyHides(false) as! Compass XCTAssertFalse(compass.shouldHide) } } diff --git a/Tests/ArcGISToolkitTests/ViewpointTests.swift b/Tests/ArcGISToolkitTests/ViewpointTests.swift new file mode 100644 index 000000000..b8a420a52 --- /dev/null +++ b/Tests/ArcGISToolkitTests/ViewpointTests.swift @@ -0,0 +1,66 @@ +// Copyright 2023 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 +// http://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 XCTest +@testable import ArcGISToolkit + +final class ViewpointTests: XCTestCase { + func testWithRotation() async { + let viewpoint = Viewpoint(center: .esriRedlands, scale: 15_000) + let expectedViewpoint = viewpoint.withRotation(45) + + XCTAssertEqual(expectedViewpoint.rotation, 45) + XCTAssertEqual(expectedViewpoint.targetScale, viewpoint.targetScale) + XCTAssertEqual(expectedViewpoint.targetGeometry, viewpoint.targetGeometry) + } + + func testWithRotationWithBoundingGeometry() { + let rotatedViewpoint = Viewpoint(boundingGeometry: Polyline.simple.extent, rotation: -45) + XCTAssertEqual(rotatedViewpoint.rotation, -45) + + let nonRotatedViewpoint = rotatedViewpoint.withRotation(.zero) + XCTAssertEqual(nonRotatedViewpoint.rotation, 0) + XCTAssertEqual(nonRotatedViewpoint.targetGeometry, rotatedViewpoint.targetGeometry) + } + + func testWithRotationWithCenterAndScale() { + let rotatedViewpoint = Viewpoint(center: .esriRedlands, scale: 1_000, rotation: 45) + XCTAssertEqual(rotatedViewpoint.rotation, 45) + + let nonRotatedViewpoint = rotatedViewpoint.withRotation(.zero) + XCTAssertEqual(nonRotatedViewpoint.rotation, 0) + XCTAssertEqual(nonRotatedViewpoint.targetScale, rotatedViewpoint.targetScale) + XCTAssertEqual(nonRotatedViewpoint.targetGeometry, rotatedViewpoint.targetGeometry) + } +} + +extension Point { + static var esriRedlands: Point { + .init( + x: -117.19494, + y: 34.05723, + spatialReference: .wgs84 + ) + } +} + +private extension Polyline { + static var simple: Polyline { + .init(points: [ + Point(x: -117, y: 34, spatialReference: .wgs84), + Point(x: -116, y: 34, spatialReference: .wgs84), + Point(x: -116, y: 33, spatialReference: .wgs84) + ]) + } +} From a759615e75c24580bda5707962eaf5f8d50fe37f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 Mar 2023 15:09:14 -0500 Subject: [PATCH 176/244] PR review change for argument name. --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 5c26e6a92..b7a466971 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -134,11 +134,11 @@ public extension Compass { } /// Specifies whether the ``Compass`` should automatically hide when the heading is 0. - /// - Parameter newAutomaticallyHides: A Boolean value indicating whether the compass should automatically + /// - Parameter flag: A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. - func automaticallyHides(_ newAutomaticallyHides: Bool) -> some View { + func automaticallyHides(_ flag: Bool) -> some View { var copy = self - copy.autoHide = newAutomaticallyHides + copy.autoHide = flag return copy } } From bb3a0712b5b29c66e94418aa2ce07d7b15a61ea2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 31 Mar 2023 16:24:28 -0700 Subject: [PATCH 177/244] Update AttachmentsPopupElementView.swift --- .../Popups/AttachmentsPopupElementView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 762d062c0..08a4950df 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -80,12 +80,12 @@ struct AttachmentsPopupElementView: View { } } .task { - try? await popupElement.fetchAttachments() - let attachmentModels = popupElement.attachments.reversed().map { attachment in - AttachmentModel(attachment: attachment) + if let attachmentModels = try? await popupElement.attachments.reversed().map({ + AttachmentModel(attachment: $0) + }) { + viewModel.attachmentModels.append(contentsOf: attachmentModels) + isLoadingAttachments = false } - viewModel.attachmentModels.append(contentsOf: attachmentModels) - isLoadingAttachments = false } } } From a067be95d52c0763107ca94cd6da40074fd13505 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 1 Apr 2023 14:35:12 -0500 Subject: [PATCH 178/244] Make compass action synchronous. --- Examples/Examples/CompassExampleView.swift | 26 +++++++++++-------- .../Components/Compass/Compass.swift | 10 +++---- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 0bc95e98c..21916f3a0 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -74,11 +74,13 @@ struct MapWithViewpoint: View { .overlay(alignment: .topTrailing) { Compass(viewpoint: $viewpoint) { guard let viewpoint else { return } - // Animate the map view to zero when the compass is tapped. - await proxy.setViewpoint( - viewpoint.withRotation(0), - duration: 0.25 - ) + Task { + // Animate the map view to zero when the compass is tapped. + await proxy.setViewpoint( + viewpoint.withRotation(0), + duration: 0.25 + ) + } } .padding() } @@ -108,12 +110,14 @@ struct SceneWithCameraController: View { .overlay(alignment: .topTrailing) { Compass(viewpointRotation: $heading) { // Animate the scene view when the compass is tapped. - _ = try? await cameraController.moveCamera( - distanceDelta: .zero, - headingDelta: heading > 180 ? 360 - heading : -heading, - pitchDelta: .zero, - duration: 0.3 - ) + Task { + _ = try? await cameraController.moveCamera( + distanceDelta: .zero, + headingDelta: heading > 180 ? 360 - heading : -heading, + pitchDelta: .zero, + duration: 0.3 + ) + } } .padding() } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index b7a466971..80886c17a 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -21,7 +21,7 @@ public struct Compass: View { @State private var opacity: Double = .zero /// An action to perform when the compass is tapped. - private let action: (() async -> Void)? + private let action: (() -> Void)? /// A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. @@ -47,7 +47,7 @@ public struct Compass: View { /// - action: An action to perform when the compass is tapped. public init( heading: Binding, - action: (() async -> Void)? = nil + action: (() -> Void)? = nil ) { _heading = heading self.action = action @@ -73,7 +73,7 @@ public struct Compass: View { } .onTapGesture { if let action { - Task { await action() } + action() } else { heading = .zero } @@ -93,7 +93,7 @@ public extension Compass { /// - action: An action to perform when the compass is tapped. init( viewpointRotation: Binding, - action: (() async -> Void)? = nil + action: (() -> Void)? = nil ) { let heading = Binding(get: { viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue @@ -110,7 +110,7 @@ public extension Compass { /// when the viewpoint's rotation is 0 degrees. init( viewpoint: Binding, - action: (() async -> Void)? = nil + action: (() -> Void)? = nil ) { let viewpointRotation = Binding { viewpoint.wrappedValue?.rotation ?? .nan From 64d9acc237060a8b70c3bc54926d2e6c7d72dabc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 1 Apr 2023 14:35:43 -0500 Subject: [PATCH 179/244] Remove `rawValue` from Item.id constructor. --- Examples/Examples/PopupExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index 648fdfa36..bed264ab1 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -19,7 +19,7 @@ struct PopupExampleView: View { static func makeMap() -> Map { let portalItem = PortalItem( portal: .arcGISOnline(connection: .anonymous), - id: Item.ID(rawValue: "9f3a674e998f461580006e626611f9ad")! + id: Item.ID("9f3a674e998f461580006e626611f9ad")! ) return Map(item: portalItem) } From 6788766059bc17d579ac93d02ef10240fea4c5f0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 1 Apr 2023 14:36:56 -0500 Subject: [PATCH 180/244] Apply suggestions from code review Co-authored-by: David Feinzimer --- Documentation/Compass/README.md | 2 +- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 219912861..f9f241ce3 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -50,7 +50,7 @@ Compass: `Compass` has the following modifiers: - `func compassSize(size: CGFloat)` - The size of the `Compass`, specifying both the width and height of the compass. -- `func automaticallyHides(newAutomaticallyHides: Bool)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0. +- `func automaticallyHides(_:)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0. ## Behavior: diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 80886c17a..45cd76a96 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -134,7 +134,7 @@ public extension Compass { } /// Specifies whether the ``Compass`` should automatically hide when the heading is 0. - /// - Parameter flag: A Boolean value indicating whether the compass should automatically + /// - Parameter flag: A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. func automaticallyHides(_ flag: Bool) -> some View { var copy = self From 67ac0bc3bea97076221fb5e4968ac0a6dd3fe291 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 3 Apr 2023 08:57:51 -0700 Subject: [PATCH 181/244] Update Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift Co-authored-by: Mark Dostal --- .../Components/Popups/AttachmentsPopupElementView.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 08a4950df..5b8df8b97 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -80,12 +80,11 @@ struct AttachmentsPopupElementView: View { } } .task { - if let attachmentModels = try? await popupElement.attachments.reversed().map({ - AttachmentModel(attachment: $0) - }) { - viewModel.attachmentModels.append(contentsOf: attachmentModels) - isLoadingAttachments = false + let attachmentModels = try? await popupElement.attachments.reversed().map { attachment in + AttachmentModel(attachment: attachment) } + viewModel.attachmentModels.append(contentsOf: attachmentModels ?? []) + isLoadingAttachments = false } } } From cf770006fbcf5dbf6555deb9eae8851cc613861d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 3 Apr 2023 09:21:18 -0700 Subject: [PATCH 182/244] Update BookmarksTests.swift --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 40061a726..fe403ca09 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -65,7 +65,7 @@ final class BookmarksTests: XCTestCase { } var bookmarks = Bookmarks( isPresented: isPresented, - mapOrScene: map + geoModel: map ) bookmarks.selectionChangedAction = action XCTAssertTrue(_isPresented) @@ -120,7 +120,7 @@ final class BookmarksTests: XCTestCase { ) let bookmarks = Bookmarks( isPresented: isPresented, - mapOrScene: map, + geoModel: map, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) From 2bc8a8cb777a25f810bf935a38cb8a5887b1d3b0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 3 Apr 2023 11:25:20 -0500 Subject: [PATCH 183/244] Apply suggestions from code review Co-authored-by: David Feinzimer --- Documentation/Compass/README.md | 6 +++--- Examples/Examples/CompassExampleView.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index f9f241ce3..f2cc89e93 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -25,7 +25,7 @@ Compass: /// - Parameters: /// - heading: The heading of the compass. /// - action: An action to perform when the compass is tapped. - public init(heading: Binding, action: (() async -> Void)? = nil) + public init(heading: Binding, action: (() -> Void)? = nil) ``` ```swift @@ -36,7 +36,7 @@ Compass: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. /// - action: An action to perform when the compass is tapped. - public init(viewpointRotation: Binding, action: (() async -> Void)? = nil) + public init(viewpointRotation: Binding, action: (() -> Void)? = nil) ``` ```swift @@ -44,7 +44,7 @@ Compass: /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - action: An action to perform when the compass is tapped. - public init(viewpoint: Binding, action: (() async -> Void)? = nil) + public init(viewpoint: Binding, action: (() -> Void)? = nil) ``` `Compass` has the following modifiers: diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 21916f3a0..d92a3480c 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -111,7 +111,7 @@ struct SceneWithCameraController: View { Compass(viewpointRotation: $heading) { // Animate the scene view when the compass is tapped. Task { - _ = try? await cameraController.moveCamera( + await cameraController.moveCamera( distanceDelta: .zero, headingDelta: heading > 180 ? 360 - heading : -heading, pitchDelta: .zero, From b3e6621fe444884cd2e558f40f0d95d65d4be1d7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 3 Apr 2023 10:05:54 -0700 Subject: [PATCH 184/244] Add done button --- Examples/Examples/BasemapGalleryExampleView.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 285a9e1b8..e94cab823 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -37,9 +37,18 @@ struct BasemapGalleryExampleView: View { // Display in a sheet // .sheet(isPresented: $showBasemapGallery) { - BasemapGallery(items: basemaps, geoModel: map) - .style(.grid) - .padding() + VStack { + Button { + showBasemapGallery.toggle() + } label: { + Image(systemName: "xmark") + } + .padding([.top, .trailing]) + .frame(maxWidth: .infinity, alignment: .trailing) + BasemapGallery(items: basemaps, geoModel: map) + .style(.grid) + .padding() + } } // Display in a floating panel From 4990d2ab2fce8bc40da062872ec5d238f285f807 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 3 Apr 2023 10:11:45 -0700 Subject: [PATCH 185/244] Update BasemapGalleryExampleView.swift --- .../Examples/BasemapGalleryExampleView.swift | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index e94cab823..f833889c3 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -38,13 +38,9 @@ struct BasemapGalleryExampleView: View { // .sheet(isPresented: $showBasemapGallery) { VStack { - Button { - showBasemapGallery.toggle() - } label: { - Image(systemName: "xmark") - } - .padding([.top, .trailing]) - .frame(maxWidth: .infinity, alignment: .trailing) + xButton + .padding([.top, .trailing]) + .frame(maxWidth: .infinity, alignment: .trailing) BasemapGallery(items: basemaps, geoModel: map) .style(.grid) .padding() @@ -79,6 +75,17 @@ struct BasemapGalleryExampleView: View { } } + /// A button that allows a user to close a sheet. + /// + /// This is especially useful for when the sheet is open an iPhone in landscape. + private var xButton: some View { + Button { + showBasemapGallery.toggle() + } label: { + Image(systemName: "xmark") + } + } + private static func initialBasemaps() -> [BasemapGalleryItem] { let identifiers = [ "46a87c20f09e4fc48fa3c38081e0cae6", From d162f96c668e6d947b4c7dc53e40aab6b3cd6415 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 3 Apr 2023 14:59:13 -0700 Subject: [PATCH 186/244] `Compass.automaticallyHides` -> `disableAutoHide(_:)` --- Documentation/Compass/README.md | 2 +- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 6 +++--- Tests/ArcGISToolkitTests/CompassTests.swift | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index f2cc89e93..6ab33f349 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -50,7 +50,7 @@ Compass: `Compass` has the following modifiers: - `func compassSize(size: CGFloat)` - The size of the `Compass`, specifying both the width and height of the compass. -- `func automaticallyHides(_:)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0. +- `func disableAutoHide(_:)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0. ## Behavior: diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 45cd76a96..ad391fbf7 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -134,11 +134,11 @@ public extension Compass { } /// Specifies whether the ``Compass`` should automatically hide when the heading is 0. - /// - Parameter flag: A Boolean value indicating whether the compass should automatically + /// - Parameter disable: A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. - func automaticallyHides(_ flag: Bool) -> some View { + func disableAutoHide(_ disable: Bool = true) -> some View { var copy = self - copy.autoHide = flag + copy.autoHide = !disable return copy } } diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 4b93de6b6..1163ec510 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -25,7 +25,7 @@ final class CompassTests: XCTestCase { var _viewpoint: Viewpoint? = makeViewpoint(rotation: initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - .automaticallyHides(false) as! Compass + .disableAutoHide() as! Compass XCTAssertFalse(compass.shouldHide) _viewpoint = makeViewpoint(rotation: finalValue) XCTAssertFalse(compass.shouldHide) @@ -54,7 +54,7 @@ final class CompassTests: XCTestCase { /// `false`. func testAutomaticallyHidesNoAutoHide() { let compass = Compass(viewpoint: .constant(nil)) - .automaticallyHides(false) as! Compass + .disableAutoHide() as! Compass XCTAssertFalse(compass.shouldHide) } @@ -67,7 +67,7 @@ final class CompassTests: XCTestCase { /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { let compass = Compass(viewpoint: .constant(makeViewpoint(rotation: .zero))) - .automaticallyHides(false) as! Compass + .disableAutoHide() as! Compass XCTAssertFalse(compass.shouldHide) } } From 32ca9de7b594f2f0f16f4624f9e7d70dce7849b5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 3 Apr 2023 15:06:39 -0700 Subject: [PATCH 187/244] `disableAutoHide` -> `autoHideDisabled` --- Documentation/Compass/README.md | 2 +- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 2 +- Tests/ArcGISToolkitTests/CompassTests.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 6ab33f349..4956d70a5 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -50,7 +50,7 @@ Compass: `Compass` has the following modifiers: - `func compassSize(size: CGFloat)` - The size of the `Compass`, specifying both the width and height of the compass. -- `func disableAutoHide(_:)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0. +- `func autoHideDisabled(_:)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0. ## Behavior: diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index ad391fbf7..544b5466b 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -136,7 +136,7 @@ public extension Compass { /// Specifies whether the ``Compass`` should automatically hide when the heading is 0. /// - Parameter disable: A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. - func disableAutoHide(_ disable: Bool = true) -> some View { + func autoHideDisabled(_ disable: Bool = true) -> some View { var copy = self copy.autoHide = !disable return copy diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 1163ec510..23f831ff8 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -25,7 +25,7 @@ final class CompassTests: XCTestCase { var _viewpoint: Viewpoint? = makeViewpoint(rotation: initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - .disableAutoHide() as! Compass + .autoHideDisabled() as! Compass XCTAssertFalse(compass.shouldHide) _viewpoint = makeViewpoint(rotation: finalValue) XCTAssertFalse(compass.shouldHide) @@ -54,7 +54,7 @@ final class CompassTests: XCTestCase { /// `false`. func testAutomaticallyHidesNoAutoHide() { let compass = Compass(viewpoint: .constant(nil)) - .disableAutoHide() as! Compass + .autoHideDisabled() as! Compass XCTAssertFalse(compass.shouldHide) } @@ -67,7 +67,7 @@ final class CompassTests: XCTestCase { /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { let compass = Compass(viewpoint: .constant(makeViewpoint(rotation: .zero))) - .disableAutoHide() as! Compass + .autoHideDisabled() as! Compass XCTAssertFalse(compass.shouldHide) } } From a9d90b0581e00193879361cfc3cf5315e9a70bb8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 3 Apr 2023 15:46:17 -0700 Subject: [PATCH 188/244] Make modifier publicly accessible --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 89a842b08..aa78637cd 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -199,7 +199,7 @@ public struct FloorFilter: View { /// The width of the level selector. /// - Parameter width: The new width for the level selector. /// - Returns: The `FloorFilter`. - func levelSelectorWidth(_ width: CGFloat) -> Self { + public func levelSelectorWidth(_ width: CGFloat) -> Self { var copy = self copy.levelSelectorWidth = width return copy From bca52604017ce39c3dda8e1a4b54486e2577e389 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 5 Apr 2023 12:49:16 -0700 Subject: [PATCH 189/244] Update Examples/Examples/BasemapGalleryExampleView.swift Co-authored-by: Mark Dostal --- Examples/Examples/BasemapGalleryExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index f833889c3..aae55007f 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -82,7 +82,7 @@ struct BasemapGalleryExampleView: View { Button { showBasemapGallery.toggle() } label: { - Image(systemName: "xmark") + Text("Done") } } From 421194c06eb1a227c82435e2ec5d76e0ffe12aef Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 5 Apr 2023 13:53:34 -0700 Subject: [PATCH 190/244] Remove mention of static widths --- .../Components/BasemapGallery/BasemapGallery.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 88aa751e5..b837aee4a 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -24,11 +24,10 @@ public struct BasemapGallery: View { public enum Style { /// The `BasemapGallery` will display as a grid when there is an appropriate /// width available for the gallery to do so. Otherwise, the gallery will display as a list. - /// Defaults to `125` when displayed as a list, `300` when displayed as a grid. case automatic - /// The `BasemapGallery` will display as a grid. Defaults to `300`. + /// The `BasemapGallery` will display as a grid. case grid - /// The `BasemapGallery` will display as a list. Defaults to `125`. + /// The `BasemapGallery` will display as a list. case list } From 76effd0bfb3d85b80fd29e6850415bc27048c48c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 5 Apr 2023 14:21:14 -0700 Subject: [PATCH 191/244] `xButton` -> `doneButton` --- Examples/Examples/BasemapGalleryExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index aae55007f..48ff59f95 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -38,7 +38,7 @@ struct BasemapGalleryExampleView: View { // .sheet(isPresented: $showBasemapGallery) { VStack { - xButton + doneButton .padding([.top, .trailing]) .frame(maxWidth: .infinity, alignment: .trailing) BasemapGallery(items: basemaps, geoModel: map) @@ -78,7 +78,7 @@ struct BasemapGalleryExampleView: View { /// A button that allows a user to close a sheet. /// /// This is especially useful for when the sheet is open an iPhone in landscape. - private var xButton: some View { + private var doneButton: some View { Button { showBasemapGallery.toggle() } label: { From 9ed0ce0cc58ada30d16ff73309125cdea5033200 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 5 Apr 2023 15:29:24 -0700 Subject: [PATCH 192/244] Automatic no longer has an associated value --- Documentation/BasemapGallery/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/BasemapGallery/README.md b/Documentation/BasemapGallery/README.md index 82b55b392..6a0ba051f 100644 --- a/Documentation/BasemapGallery/README.md +++ b/Documentation/BasemapGallery/README.md @@ -83,7 +83,7 @@ var body: some View { MapView(map: map) .overlay(alignment: .topTrailing) { BasemapGallery(geoModel: map) - .style(.automatic()) + .style(.automatic) .padding() } } From 6afda00af02d67503ec13dba1446c3a3a2061d6d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 5 Apr 2023 15:30:12 -0700 Subject: [PATCH 193/244] Update BasemapGallery.swift --- .../BasemapGallery/BasemapGallery.swift | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index b837aee4a..39c0b9d10 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -26,7 +26,7 @@ public struct BasemapGallery: View { /// width available for the gallery to do so. Otherwise, the gallery will display as a list. case automatic /// The `BasemapGallery` will display as a grid. - case grid + case grid(maxItemWidth: CGFloat = 300.0) /// The `BasemapGallery` will display as a list. case list } @@ -84,7 +84,7 @@ public struct BasemapGallery: View { public var body: some View { GeometryReader { geometry in - makeGalleryView() + makeGalleryView(geometry.size.width) .onReceive( viewModel.$spatialReferenceMismatchError.dropFirst(), perform: { error in @@ -111,18 +111,19 @@ public struct BasemapGallery: View { private extension BasemapGallery { /// Creates a gallery view. + /// - Parameter containerWidth: The width of the container holding the gallery. /// - Returns: A view representing the basemap gallery. - func makeGalleryView() -> some View { + func makeGalleryView(_ containerWidth: CGFloat) -> some View { ScrollView { switch style { case .automatic: if isRegularWidth { - makeGridView() + makeGridView(containerWidth) } else { makeListView() } - case .grid: - makeGridView() + case .grid(let maxItemWidth): + makeGridView(containerWidth, maxItemWidth: maxItemWidth) case .list: makeListView() } @@ -130,15 +131,21 @@ private extension BasemapGallery { } /// The gallery view, displayed as a grid. + /// - Parameters: + /// - containerWidth: The width of the container holding the grid view. + /// - maxItemWidth: The maximum allowable width for an item in the grid. Defaults to `300`. /// - Returns: A view representing the basemap gallery grid. - func makeGridView() -> some View { + func makeGridView(_ containerWidth: CGFloat, maxItemWidth: CGFloat = 300) -> some View { internalMakeGalleryView( columns: Array( repeating: GridItem( .flexible(), alignment: .top ), - count: 3 + count: max( + 1, + Int((containerWidth / maxItemWidth).rounded(.down)) + ) ) ) } From a87db108097e0a79461cf88edb3f6534e3a39be4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 5 Apr 2023 15:36:25 -0700 Subject: [PATCH 194/244] Updates --- Documentation/BasemapGallery/README.md | 2 +- .../BasemapGallery/BasemapGallery.swift | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Documentation/BasemapGallery/README.md b/Documentation/BasemapGallery/README.md index 6a0ba051f..82b55b392 100644 --- a/Documentation/BasemapGallery/README.md +++ b/Documentation/BasemapGallery/README.md @@ -83,7 +83,7 @@ var body: some View { MapView(map: map) .overlay(alignment: .topTrailing) { BasemapGallery(geoModel: map) - .style(.automatic) + .style(.automatic()) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 39c0b9d10..34f1e8834 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -24,9 +24,10 @@ public struct BasemapGallery: View { public enum Style { /// The `BasemapGallery` will display as a grid when there is an appropriate /// width available for the gallery to do so. Otherwise, the gallery will display as a list. - case automatic + /// When displayed as a grid, `maxGridItemWidth` sets the maximum width of a grid item. + case automatic(maxGridItemWidth: CGFloat = 300) /// The `BasemapGallery` will display as a grid. - case grid(maxItemWidth: CGFloat = 300.0) + case grid(maxItemWidth: CGFloat = 300) /// The `BasemapGallery` will display as a list. case list } @@ -65,7 +66,7 @@ public struct BasemapGallery: View { /// The style of the basemap gallery. The gallery can be displayed as a list, grid, or automatically /// switch between the two based on-screen real estate. Defaults to ``BasemapGallery/Style/automatic``. /// Set using the `style` modifier. - private var style: Style = .automatic + private var style: Style = .automatic() @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -116,14 +117,14 @@ private extension BasemapGallery { func makeGalleryView(_ containerWidth: CGFloat) -> some View { ScrollView { switch style { - case .automatic: + case .automatic(let maxGridItemWidth): if isRegularWidth { - makeGridView(containerWidth) + makeGridView(containerWidth, maxGridItemWidth) } else { makeListView() } case .grid(let maxItemWidth): - makeGridView(containerWidth, maxItemWidth: maxItemWidth) + makeGridView(containerWidth, maxItemWidth) case .list: makeListView() } @@ -135,7 +136,7 @@ private extension BasemapGallery { /// - containerWidth: The width of the container holding the grid view. /// - maxItemWidth: The maximum allowable width for an item in the grid. Defaults to `300`. /// - Returns: A view representing the basemap gallery grid. - func makeGridView(_ containerWidth: CGFloat, maxItemWidth: CGFloat = 300) -> some View { + func makeGridView(_ containerWidth: CGFloat, _ maxItemWidth: CGFloat) -> some View { internalMakeGalleryView( columns: Array( repeating: GridItem( From 534b3435c1540f80e3c729ac0c033f108022a747 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 5 Apr 2023 16:23:08 -0700 Subject: [PATCH 195/244] Simplify example --- .../Examples/BasemapGalleryExampleView.swift | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 48ff59f95..81ae54840 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -33,39 +33,15 @@ struct BasemapGalleryExampleView: View { var body: some View { MapView(map: map, viewpoint: initialViewpoint) - - // Display in a sheet - // .sheet(isPresented: $showBasemapGallery) { - VStack { + VStack(alignment: .trailing) { doneButton - .padding([.top, .trailing]) - .frame(maxWidth: .infinity, alignment: .trailing) + .padding() BasemapGallery(items: basemaps, geoModel: map) - .style(.grid) + .style(.automatic()) .padding() } } - - // Display in a floating panel - // -// .floatingPanel(isPresented: $showBasemapGallery) { -// BasemapGallery(items: basemaps, geoModel: map) -// .style(.grid) -// } - - // Display in an overlay - // -// .overlay(alignment: .topTrailing) { -// if showBasemapGallery { -// BasemapGallery(items: basemaps, geoModel: map) -// .style(.list) -// .frame(width: 150, height: 400) -// .esriBorder() -// .padding() -// } -// } - .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Toggle(isOn: $showBasemapGallery) { From 2f6d19503ab7ce7d8faae29a094a32f3e2dfa852 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 10 Apr 2023 13:21:28 -0700 Subject: [PATCH 196/244] Add presentation control to example --- Examples/Examples/FloatingPanelExampleView.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index cef65cca7..b9e86739f 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -21,6 +21,8 @@ struct FloatingPanelExampleView: View { map: Map(basemapStyle: .arcGISImagery) ) + @State var isPresented = true + @State var selectedDetent: FloatingPanelDetent = .half private let initialViewpoint = Viewpoint( @@ -33,7 +35,7 @@ struct FloatingPanelExampleView: View { map: dataModel.map, viewpoint: initialViewpoint ) - .floatingPanel(selectedDetent: $selectedDetent, isPresented: .constant(true)) { + .floatingPanel(selectedDetent: $selectedDetent, isPresented: $isPresented) { List { Section("Preset Heights") { Button("Summary") { @@ -67,5 +69,12 @@ struct FloatingPanelExampleView: View { } } } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(isPresented ? "Close" : "Open") { + isPresented.toggle() + } + } + } } } From 2f300bbbcfe460e4529e77684cb3510748cc7cd1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 10 Apr 2023 14:42:00 -0700 Subject: [PATCH 197/244] Update view implementation --- .../FloatingPanel/FloatingPanel.swift | 110 ++++++++---------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index faad88e38..b847a6e34 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -75,54 +75,58 @@ struct FloatingPanel: View where Content: View { } public var body: some View { - if isPresented.wrappedValue { - GeometryReader { geometryProxy in - VStack(spacing: 0) { - if isCompact { - makeHandleView() - Divider() - } - content - .frame(height: height) - .padding(.bottom, isCompact ? 25 : 10) - if !isCompact { + GeometryReader { geometryProxy in + VStack(spacing: 0) { + if isCompact && isPresented.wrappedValue { + makeHandleView() + Divider() + } + content + .frame(height: height) + .clipped() + .padding(.bottom, isCompact ? 25 : 10) + if !isCompact && isPresented.wrappedValue { Divider() makeHandleView() - } } - .background(backgroundColor) - .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) - .shadow(radius: 10) - .opacity(isPresented.wrappedValue ? 1.0 : .zero) - .frame( - width: geometryProxy.size.width, - height: geometryProxy.size.height, - alignment: isCompact ? .bottom : .top + } + .background(isPresented.wrappedValue ? backgroundColor : .clear) + .clipShape( + RoundedCorners( + corners: isCompact ? [.topLeft, .topRight] : [.allCorners], + radius: 10 ) - .onSizeChange { - maximumHeight = $0.height - if height > maximumHeight { - height = maximumHeight - } + ) + .shadow(radius: 10) + .frame( + width: geometryProxy.size.width, + height: geometryProxy.size.height, + alignment: isCompact ? .bottom : .top + ) + .onSizeChange { + maximumHeight = $0.height + if height > maximumHeight { + height = maximumHeight } - .onChange(of: selectedDetent.wrappedValue) { _ in - withAnimation { - height = heightFor(detent: selectedDetent.wrappedValue) - } + } + .onAppear { + withAnimation { + height = isPresented.wrappedValue ? heightFor(detent: selectedDetent.wrappedValue) : .zero } - .onChange(of: isPresented.wrappedValue) { - height = $0 ? heightFor(detent: selectedDetent.wrappedValue) : .zero + } + .onChange(of: isPresented.wrappedValue) { isPresented in + withAnimation { + height = isPresented ? heightFor(detent: selectedDetent.wrappedValue) : .zero } - .onAppear { - withAnimation { - height = heightFor(detent: selectedDetent.wrappedValue) - } + } + .onChange(of: selectedDetent.wrappedValue) { selectedDetent in + withAnimation { + height = heightFor(detent: selectedDetent) } - .animation(.default, value: isPresented.wrappedValue) } - .padding([.leading, .top, .trailing], isCompact ? 0 : 10) - .padding([.bottom], isCompact ? 0 : 50) } + .padding([.leading, .top, .trailing], isCompact ? 0 : 10) + .padding([.bottom], isCompact ? 0 : 50) } var drag: some Gesture { @@ -224,13 +228,11 @@ struct FloatingPanel: View where Content: View { /// Configures a handle view. /// - Returns: A configured handle view, suitable for placement in the panel. @ViewBuilder func makeHandleView() -> some View { - ZStack { - backgroundColor - Handle(color: handleColor) - } - .frame(height: 30) - .gesture(drag) - .zIndex(1) + Handle(color: handleColor) + .background(backgroundColor) + .frame(height: 30) + .gesture(drag) + .zIndex(1) } } @@ -272,21 +274,3 @@ private struct RoundedCorners: Shape { return Path(path.cgPath) } } - -private extension View { - /// Clips this view to its bounding frame, with the specified corner radius, on the specified corners. - /// - Parameters: - /// - radius: The radius used to round the corners. - /// - corners: The corners to be rounded. - /// - Returns: A view that clips this view to its bounding frame with the specified corner radius and - /// corners. - func cornerRadius( - _ radius: CGFloat, - corners: UIRectCorner - ) -> some View { - clipShape(RoundedCorners( - corners: corners, - radius: radius - )) - } -} From 28b3760a9b45fd74758debe64409a7beb7951fc2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 10 Apr 2023 15:43:09 -0700 Subject: [PATCH 198/244] Compass with viewpoint --- Examples/Examples/CompassExampleView.swift | 36 +++++-------------- .../Components/Compass/Compass.swift | 27 +++++++------- 2 files changed, 21 insertions(+), 42 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index d92a3480c..44bfbfbbe 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -19,17 +19,17 @@ import SwiftUI struct CompassExampleView: View { /// A scenario represents a type of environment a compass may be used in. enum Scenario: String { - case map case scene + case viewpoint } /// The active scenario. - @State private var scenario = Scenario.map + @State private var scenario = Scenario.viewpoint var body: some View { Group { switch scenario { - case .map: + case .viewpoint: MapWithViewpoint() case .scene: SceneWithCameraController() @@ -39,9 +39,9 @@ struct CompassExampleView: View { ToolbarItem(placement: .navigationBarTrailing) { Menu(scenario.rawValue.capitalized) { Button { - scenario = .map + scenario = .viewpoint } label: { - Label("Map", systemImage: "map.fill") + Text("Viewpoint") } Button { @@ -72,17 +72,8 @@ struct MapWithViewpoint: View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint) { - guard let viewpoint else { return } - Task { - // Animate the map view to zero when the compass is tapped. - await proxy.setViewpoint( - viewpoint.withRotation(0), - duration: 0.25 - ) - } - } - .padding() + Compass(viewpoint: $viewpoint, geoViewProxy: proxy) + .padding() } } } @@ -108,18 +99,7 @@ struct SceneWithCameraController: View { heading = newCamera.heading.rounded() } .overlay(alignment: .topTrailing) { - Compass(viewpointRotation: $heading) { - // Animate the scene view when the compass is tapped. - Task { - await cameraController.moveCamera( - distanceDelta: .zero, - headingDelta: heading > 180 ? 360 - heading : -heading, - pitchDelta: .zero, - duration: 0.3 - ) - } - } - .padding() + Compass(viewpointRotation: $heading) } } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 544b5466b..90b3b88fd 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -20,8 +20,8 @@ public struct Compass: View { /// The opacity of the compass. @State private var opacity: Double = .zero - /// An action to perform when the compass is tapped. - private let action: (() -> Void)? + /// <#Description#> + private var geoViewProxy: GeoViewProxy? /// A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. @@ -44,13 +44,13 @@ public struct Compass: View { /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. - /// - action: An action to perform when the compass is tapped. + /// - geoViewProxy: <#Description#> public init( heading: Binding, - action: (() -> Void)? = nil + geoViewProxy: GeoViewProxy? = nil ) { _heading = heading - self.action = action + self.geoViewProxy = geoViewProxy } public var body: some View { @@ -72,8 +72,8 @@ public struct Compass: View { } } .onTapGesture { - if let action { - action() + if let mapViewProxy = geoViewProxy as? MapViewProxy { + Task { await mapViewProxy.setViewpointRotation(0) } } else { heading = .zero } @@ -90,27 +90,26 @@ public extension Compass { /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. - /// - action: An action to perform when the compass is tapped. + /// - geoViewProxy: <#Description#> init( viewpointRotation: Binding, - action: (() -> Void)? = nil + geoViewProxy: GeoViewProxy? = nil ) { let heading = Binding(get: { viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue }, set: { newHeading in viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading }) - self.init(heading: heading, action: action) + self.init(heading: heading, geoViewProxy: geoViewProxy) } /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - action: An action to perform when the compass is tapped. - /// when the viewpoint's rotation is 0 degrees. + /// - geoViewProxy: <#Description#> init( viewpoint: Binding, - action: (() -> Void)? = nil + geoViewProxy: GeoViewProxy? = nil ) { let viewpointRotation = Binding { viewpoint.wrappedValue?.rotation ?? .nan @@ -122,7 +121,7 @@ public extension Compass { rotation: newViewpointRotation ) } - self.init(viewpointRotation: viewpointRotation, action: action) + self.init(viewpointRotation: viewpointRotation, geoViewProxy: geoViewProxy) } /// Define a custom size for the compass. From 572fc0129e11556eb8eb1665827db04ae7ef9d34 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 10 Apr 2023 18:01:03 -0700 Subject: [PATCH 199/244] Update doc, sample --- Examples/Examples/CompassExampleView.swift | 85 ++----------------- .../Components/Compass/Compass.swift | 24 +++--- 2 files changed, 20 insertions(+), 89 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 44bfbfbbe..7f092a0fd 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -15,101 +15,32 @@ import ArcGIS import ArcGISToolkit import SwiftUI -/// An example demonstrating how to use a compass in three different environments. -struct CompassExampleView: View { - /// A scenario represents a type of environment a compass may be used in. - enum Scenario: String { - case scene - case viewpoint - } - - /// The active scenario. - @State private var scenario = Scenario.viewpoint - - var body: some View { - Group { - switch scenario { - case .viewpoint: - MapWithViewpoint() - case .scene: - SceneWithCameraController() - } - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Menu(scenario.rawValue.capitalized) { - Button { - scenario = .viewpoint - } label: { - Text("Viewpoint") - } - - Button { - scenario = .scene - } label: { - Label("Scene", systemImage: "globe.americas.fill") - } - } - } - } - } -} - /// An example demonstrating how to use a compass with a map view. -struct MapWithViewpoint: View { +struct CompassExampleView: View { /// The `Map` displayed in the `MapView`. @State private var map = Map(basemapStyle: .arcGISImagery) /// Allows for communication between the Compass and MapView or SceneView. - @State private var viewpoint: Viewpoint? = Viewpoint( - center: .esriRedlands, - scale: 10_000, - rotation: -45 - ) + @State private var viewpoint: Viewpoint? = .esriRedlands var body: some View { MapViewReader { proxy in MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint, geoViewProxy: proxy) + Compass(viewpoint: $viewpoint, mapViewProxy: proxy) .padding() } } } } -/// An example demonstrating how to use a compass with a scene view and camera controller. -struct SceneWithCameraController: View { - /// The data model containing the `Scene` displayed in the `SceneView`. - @State private var scene = Scene(basemapStyle: .arcGISImagery) - - /// The current heading as reported by the scene view. - @State private var heading = Double.zero - - /// The orbit location camera controller used by the scene view. - private let cameraController = OrbitLocationCameraController( - target: .esriRedlands, - distance: 10_000 - ) - - var body: some View { - SceneView(scene: scene, cameraController: cameraController) - .onCameraChanged { newCamera in - heading = newCamera.heading.rounded() - } - .overlay(alignment: .topTrailing) { - Compass(viewpointRotation: $heading) - } - } -} - -private extension Point { - static var esriRedlands: Point { +private extension Viewpoint { + static var esriRedlands: Viewpoint { .init( - x: -117.19494, - y: 34.05723, - spatialReference: .wgs84 + center: .init(x: -117.19494, y: 34.05723, spatialReference: .wgs84), + scale: 10_000, + rotation: -45 ) } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 90b3b88fd..124656246 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -20,8 +20,8 @@ public struct Compass: View { /// The opacity of the compass. @State private var opacity: Double = .zero - /// <#Description#> - private var geoViewProxy: GeoViewProxy? + /// Provides access to viewpoint animation. + private var mapViewProxy: MapViewProxy? /// A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. @@ -44,13 +44,13 @@ public struct Compass: View { /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. - /// - geoViewProxy: <#Description#> + /// - mapViewProxy: Provides access to viewpoint animation. public init( heading: Binding, - geoViewProxy: GeoViewProxy? = nil + mapViewProxy: MapViewProxy? = nil ) { _heading = heading - self.geoViewProxy = geoViewProxy + self.mapViewProxy = mapViewProxy } public var body: some View { @@ -72,7 +72,7 @@ public struct Compass: View { } } .onTapGesture { - if let mapViewProxy = geoViewProxy as? MapViewProxy { + if let mapViewProxy { Task { await mapViewProxy.setViewpointRotation(0) } } else { heading = .zero @@ -90,26 +90,26 @@ public extension Compass { /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. - /// - geoViewProxy: <#Description#> + /// - mapViewProxy: Provides access to viewpoint animation. init( viewpointRotation: Binding, - geoViewProxy: GeoViewProxy? = nil + mapViewProxy: MapViewProxy? = nil ) { let heading = Binding(get: { viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue }, set: { newHeading in viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading }) - self.init(heading: heading, geoViewProxy: geoViewProxy) + self.init(heading: heading, mapViewProxy: mapViewProxy) } /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - geoViewProxy: <#Description#> + /// - mapViewProxy: Provides access to viewpoint animation. init( viewpoint: Binding, - geoViewProxy: GeoViewProxy? = nil + mapViewProxy: MapViewProxy? = nil ) { let viewpointRotation = Binding { viewpoint.wrappedValue?.rotation ?? .nan @@ -121,7 +121,7 @@ public extension Compass { rotation: newViewpointRotation ) } - self.init(viewpointRotation: viewpointRotation, geoViewProxy: geoViewProxy) + self.init(viewpointRotation: viewpointRotation, mapViewProxy: mapViewProxy) } /// Define a custom size for the compass. From 74eff14b719ffcdc5e6858ae04b86b044e669986 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 09:40:41 -0700 Subject: [PATCH 200/244] Rough draft --- Examples/Examples/SearchExampleView.swift | 59 ++++++++++--------- .../Components/Search/SearchView.swift | 7 ++- .../Components/Search/SearchViewModel.swift | 28 +++++++-- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index b574f4542..7c0bc3a54 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -51,35 +51,38 @@ struct SearchExampleView: View { @State private var queryCenter: Point? var body: some View { - MapView( - map: dataModel.map, - viewpoint: searchResultViewpoint, - graphicsOverlays: [searchResultsOverlay] - ) - .onNavigatingChanged { isGeoViewNavigating = $0 } - .onViewpointChanged(kind: .centerAndScale) { - queryCenter = $0.targetGeometry as? Point - } - .onVisibleAreaChanged { newValue in - // For "Repeat Search Here" behavior, use the `geoViewExtent` and - // `isGeoViewNavigating` modifiers on the `SearchView`. - geoViewExtent = newValue.extent - - // The visible area can be used to limit the results by - // using the `queryArea` modifier on the `SearchView`. -// queryArea = newValue - } - .overlay { - SearchView( - sources: [locatorDataSource], - viewpoint: $searchResultViewpoint + MapViewReader { mapViewProxy in + MapView( + map: dataModel.map, + viewpoint: searchResultViewpoint, + graphicsOverlays: [searchResultsOverlay] ) - .resultsOverlay(searchResultsOverlay) -// .queryArea($queryArea) - .queryCenter($queryCenter) - .geoViewExtent($geoViewExtent) - .isGeoViewNavigating($isGeoViewNavigating) - .padding() + .onNavigatingChanged { isGeoViewNavigating = $0 } + .onViewpointChanged(kind: .centerAndScale) { + queryCenter = $0.targetGeometry as? Point + } + .onVisibleAreaChanged { newValue in + // For "Repeat Search Here" behavior, use the `geoViewExtent` and + // `isGeoViewNavigating` modifiers on the `SearchView`. + geoViewExtent = newValue.extent + + // The visible area can be used to limit the results by + // using the `queryArea` modifier on the `SearchView`. +// queryArea = newValue + } + .overlay { + SearchView( + sources: [locatorDataSource], + viewpoint: $searchResultViewpoint, + geoViewProxy: mapViewProxy + ) + .resultsOverlay(searchResultsOverlay) +// .queryArea($queryArea) + .queryCenter($queryCenter) + .geoViewExtent($geoViewExtent) + .isGeoViewNavigating($isGeoViewNavigating) + .padding() + } } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 194cf0aca..ea66f0511 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -21,13 +21,16 @@ public struct SearchView: View { /// - sources: A collection of search sources to be used. /// - viewpoint: The `Viewpoint` used to pan/zoom to results. If `nil`, there will be /// no zooming to results. + /// - geoViewProxy: <#Description#> public init( sources: [SearchSource] = [], - viewpoint: Binding? = nil + viewpoint: Binding? = nil, + geoViewProxy: GeoViewProxy? = nil ) { _viewModel = StateObject(wrappedValue: SearchViewModel( sources: sources.isEmpty ? [LocatorSearchSource()] : sources, - viewpoint: viewpoint + viewpoint: viewpoint, + geoViewProxy: geoViewProxy )) _queryArea = .constant(nil) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 9a45f9600..68d1a02f1 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -44,12 +44,15 @@ public enum SearchOutcome { /// - sources: Collection of search sources to be used. /// - viewpoint: The `Viewpoint` used to pan/zoom to results. If `nil`, there will be /// no zooming to results. + /// - geoViewProxy: <#Description#> init( sources: [SearchSource] = [], - viewpoint: Binding? = nil + viewpoint: Binding? = nil, + geoViewProxy: GeoViewProxy? = nil ) { self.sources = sources self.viewpoint = viewpoint + self.geoViewProxy = geoViewProxy } /// The active search source. If `nil`, the first item in `sources` is used. @@ -116,6 +119,9 @@ public enum SearchOutcome { } } + /// <#Description#> + private var geoViewProxy: GeoViewProxy? + /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. var isGeoViewNavigating = false @@ -380,9 +386,14 @@ private extension SearchViewModel { let builder = EnvelopeBuilder(envelope: envelope) builder.expand(by: 1.1) let targetExtent = builder.toGeometry() - viewpoint.wrappedValue = Viewpoint( - boundingGeometry: targetExtent - ) + let newViewpoint = Viewpoint(boundingGeometry: targetExtent) + if let geoViewProxy { + Task { + await geoViewProxy.setViewpoint(newViewpoint, duration: nil) + } + } else { + viewpoint.wrappedValue = newViewpoint + } lastSearchExtent = targetExtent } else { viewpoint.wrappedValue = nil @@ -393,7 +404,14 @@ private extension SearchViewModel { func display(selectedResult: SearchResult?) { guard let selectedResult = selectedResult else { return } - viewpoint?.wrappedValue = selectedResult.selectionViewpoint + + if let geoViewProxy, let viewpoint = selectedResult.selectionViewpoint { + Task { + await geoViewProxy.setViewpoint(viewpoint, duration: nil) + } + } else { + viewpoint?.wrappedValue = selectedResult.selectionViewpoint + } } } From 479111f5ff695e9bda515784b05364335fcb2a4f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 09:54:47 -0700 Subject: [PATCH 201/244] Update SearchViewModel.swift --- .../Components/Search/SearchViewModel.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 68d1a02f1..21a7ae243 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -386,14 +386,7 @@ private extension SearchViewModel { let builder = EnvelopeBuilder(envelope: envelope) builder.expand(by: 1.1) let targetExtent = builder.toGeometry() - let newViewpoint = Viewpoint(boundingGeometry: targetExtent) - if let geoViewProxy { - Task { - await geoViewProxy.setViewpoint(newViewpoint, duration: nil) - } - } else { - viewpoint.wrappedValue = newViewpoint - } + display(newViewpoint: Viewpoint(boundingGeometry: targetExtent)) lastSearchExtent = targetExtent } else { viewpoint.wrappedValue = nil @@ -404,13 +397,16 @@ private extension SearchViewModel { func display(selectedResult: SearchResult?) { guard let selectedResult = selectedResult else { return } + display(newViewpoint: selectedResult.selectionViewpoint) + } - if let geoViewProxy, let viewpoint = selectedResult.selectionViewpoint { - Task { - await geoViewProxy.setViewpoint(viewpoint, duration: nil) - } + /// Update the viewpoint to the new viewpoint, if a geo view proxy was supplied, the transition + /// will be animated. + func display(newViewpoint: Viewpoint?) { + if let geoViewProxy, let newViewpoint { + Task { await geoViewProxy.setViewpoint(newViewpoint, duration: nil) } } else { - viewpoint?.wrappedValue = selectedResult.selectionViewpoint + viewpoint?.wrappedValue = newViewpoint } } } From a8cb11433929d1cc823ddd31ae2b1a7089b40de3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 10:23:13 -0700 Subject: [PATCH 202/244] `viewPoint` -> `screenPoint` --- .../Examples/UtilityNetworkTraceExampleView.swift | 12 ++++++------ .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 14 +++++++------- .../UtilityNetworkTraceViewModel.swift | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index a4f1a5255..9a21f4535 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -15,8 +15,8 @@ import ArcGIS import ArcGISToolkit import SwiftUI -/// A demonstration of the utility network trace tool which runs traces on a web map published with a utility -/// network and trace configurations. +/// A demonstration of the utility network trace tool which runs traces on a web map published with +/// a utility network and trace configurations. struct UtilityNetworkTraceExampleView: View { /// The data model containing a `Map` with the utility networks. @StateObject private var dataModel = MapDataModel( @@ -33,7 +33,7 @@ struct UtilityNetworkTraceExampleView: View { @State var mapPoint: Point? /// Provides the ability to detect tap locations in the context of the screen. - @State var viewPoint: CGPoint? + @State var screenPoint: CGPoint? /// A container for graphical trace results. @State var resultGraphicsOverlay = GraphicsOverlay() @@ -48,8 +48,8 @@ struct UtilityNetworkTraceExampleView: View { viewpoint: viewpoint, graphicsOverlays: [resultGraphicsOverlay] ) - .onSingleTapGesture { viewPoint, mapPoint in - self.viewPoint = viewPoint + .onSingleTapGesture { screenPoint, mapPoint in + self.screenPoint = screenPoint self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } @@ -70,7 +70,7 @@ struct UtilityNetworkTraceExampleView: View { graphicsOverlay: $resultGraphicsOverlay, map: dataModel.map, mapPoint: $mapPoint, - viewPoint: $viewPoint, + screenPoint: $screenPoint, mapViewProxy: $mapViewProxy, viewpoint: $viewpoint ) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 87705ffc5..9f2af749a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -84,7 +84,7 @@ public struct UtilityNetworkTrace: View { @Binding private var mapViewProxy: MapViewProxy? /// Acts as the point of identification for items tapped in the utility network. - @Binding private var viewPoint: CGPoint? + @Binding private var screenPoint: CGPoint? /// Acts as the point at which newly selected starting point graphics will be created. @Binding private var mapPoint: Point? @@ -534,7 +534,7 @@ public struct UtilityNetworkTrace: View { /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace graphics. /// - map: The map containing the utility network(s). /// - mapPoint: Acts as the point at which newly selected starting point graphics will be created. - /// - viewPoint: Acts as the point of identification for items tapped in the utility network. + /// - screenPoint: Acts as the point of identification for items tapped in the utility network. /// - mapViewProxy: Provides a method of layer identification when starting points are being /// chosen. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. @@ -543,13 +543,13 @@ public struct UtilityNetworkTrace: View { graphicsOverlay: Binding, map: Map, mapPoint: Binding, - viewPoint: Binding, + screenPoint: Binding, mapViewProxy: Binding, viewpoint: Binding, startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) ) { _activeDetent = .constant(nil) - _viewPoint = viewPoint + _screenPoint = screenPoint _mapPoint = mapPoint _mapViewProxy = mapViewProxy _graphicsOverlay = graphicsOverlay @@ -605,18 +605,18 @@ public struct UtilityNetworkTrace: View { } .background(Color(uiColor: .systemGroupedBackground)) .animation(.default, value: currentActivity) - .onChange(of: viewPoint) { newValue in + .onChange(of: screenPoint) { newScreenPoint in guard isFocused(traceCreationActivity: .addingStartingPoints), let mapViewProxy = mapViewProxy, let mapPoint = mapPoint, - let viewPoint = viewPoint else { + let screenPoint = newScreenPoint else { return } currentActivity = .creatingTrace(.viewingStartingPoints) activeDetent = .half Task { await viewModel.addStartingPoints( - at: viewPoint, + at: screenPoint, mapPoint: mapPoint, with: mapViewProxy ) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 5d935947b..d5ab73179 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -117,18 +117,18 @@ import SwiftUI /// Adds new starting points to the pending trace. /// - Parameters: - /// - point: A point on the map in screen coordinates. + /// - screenPoint: A point on the map in screen coordinates. /// - mapPoint: A point on the map in map coordinates. /// - proxy: Provides a method of layer identification. /// /// An identify operation will run on each layer in the network. Every element returned from /// each layer will be added as a new starting point. - func addStartingPoints(at point: CGPoint, mapPoint: Point, with proxy: MapViewProxy) async { + func addStartingPoints(at screenPoint: CGPoint, mapPoint: Point, with proxy: MapViewProxy) async { await withTaskGroup(of: Void.self) { [weak self] taskGroup in guard let self else { return } for layer in network?.layers ?? [] { taskGroup.addTask { - if let result = try? await proxy.identify(on: layer, screenPoint: point, tolerance: 10) { + if let result = try? await proxy.identify(on: layer, screenPoint: screenPoint, tolerance: 10) { for element in result.geoElements { await self.processAndAdd( startingPoint: UtilityNetworkTraceStartingPoint(geoElement: element, mapPoint: mapPoint) From b13a49c82e1fbf63703142210b5ab4fe3ae59a17 Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Tue, 11 Apr 2023 10:23:56 -0700 Subject: [PATCH 203/244] Fix warnings --- .../Components/Popups/Attachments/AttachmentList.swift | 2 -- Sources/ArcGISToolkit/Extensions/Viewpoint.swift | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift index 02131c522..5b4eb8c33 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift @@ -95,8 +95,6 @@ struct AttachmentLoadButton: View { .aspectRatio(contentMode: .fit) .foregroundColor(.red) .background(Color.clear) - @unknown default: - EmptyView() } } .frame(width: 24, height: 24) diff --git a/Sources/ArcGISToolkit/Extensions/Viewpoint.swift b/Sources/ArcGISToolkit/Extensions/Viewpoint.swift index 9bf0e397e..7de5f3f06 100644 --- a/Sources/ArcGISToolkit/Extensions/Viewpoint.swift +++ b/Sources/ArcGISToolkit/Extensions/Viewpoint.swift @@ -25,11 +25,13 @@ public extension Viewpoint { scale: self.targetScale, rotation: rotation ) - case.boundingGeometry: + case .boundingGeometry: return Viewpoint( boundingGeometry: self.targetGeometry, rotation: rotation ) + @unknown default: + return self } } } From 3b0b482e390dc86fd6d1a3f446cb379d1fa49025 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 10:25:28 -0700 Subject: [PATCH 204/244] Update README.md --- Documentation/UtilityNetworkTrace/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/UtilityNetworkTrace/README.md b/Documentation/UtilityNetworkTrace/README.md index 63d2904fb..04aab30f2 100644 --- a/Documentation/UtilityNetworkTrace/README.md +++ b/Documentation/UtilityNetworkTrace/README.md @@ -23,7 +23,7 @@ A named trace configuration defined for a utility network in a webmap comprises /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace graphics. /// - map: The map containing the utility network(s). /// - mapPoint: Acts as the point at which newly selected starting point graphics will be created. - /// - viewPoint: Acts as the point of identification for items tapped in the utility network. + /// - screenPoint: Acts as the point of identification for items tapped in the utility network. /// - mapViewProxy: Provides a method of layer identification when starting points are being /// chosen. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. @@ -32,7 +32,7 @@ A named trace configuration defined for a utility network in a webmap comprises graphicsOverlay: Binding, map: Map, mapPoint: Binding, - viewPoint: Binding, + screenPoint: Binding, mapViewProxy: Binding, viewpoint: Binding, startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) From 37f5bed73591a86abf5bee060cf291903e1582b9 Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Tue, 11 Apr 2023 10:28:37 -0700 Subject: [PATCH 205/244] Remove unnecessary `self` --- Sources/ArcGISToolkit/Extensions/Viewpoint.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/Viewpoint.swift b/Sources/ArcGISToolkit/Extensions/Viewpoint.swift index 7de5f3f06..a8c014b1b 100644 --- a/Sources/ArcGISToolkit/Extensions/Viewpoint.swift +++ b/Sources/ArcGISToolkit/Extensions/Viewpoint.swift @@ -18,16 +18,16 @@ public extension Viewpoint { /// - Parameter rotation: The rotation for the new viewpoint. /// - Returns: A new viewpoint. func withRotation(_ rotation: Double) -> Viewpoint { - switch self.kind { + switch kind { case .centerAndScale: return Viewpoint( - center: self.targetGeometry as! Point, - scale: self.targetScale, + center: targetGeometry as! Point, + scale: targetScale, rotation: rotation ) case .boundingGeometry: return Viewpoint( - boundingGeometry: self.targetGeometry, + boundingGeometry: targetGeometry, rotation: rotation ) @unknown default: From 963a562b80f1aa385baa339f823a818d9b2aaf7d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 10:31:02 -0700 Subject: [PATCH 206/244] Animate to selected starting point --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 9f2af749a..e2971af7f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -439,7 +439,12 @@ public struct UtilityNetworkTrace: View { Button("Zoom To") { if let selectedStartingPoint = selectedStartingPoint, let extent = selectedStartingPoint.geoElement.geometry?.extent { - viewpoint = Viewpoint(boundingGeometry: extent) + let newViewpoint = Viewpoint(boundingGeometry: extent) + if let mapViewProxy { + Task { await mapViewProxy.setViewpoint(newViewpoint, duration: nil) } + } else { + viewpoint = newViewpoint + } } } Button("Delete", role: .destructive) { From 75b61a171533b06f001551605a0039b3c716335e Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Tue, 11 Apr 2023 10:33:37 -0700 Subject: [PATCH 207/244] Fix `AuthenticatorTests` compile failure --- Tests/ArcGISToolkitTests/AuthenticatorTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index 341898f09..1e56bdc67 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -13,7 +13,7 @@ import XCTest @testable import ArcGISToolkit -@testable import ArcGIS +import ArcGIS import Combine @MainActor final class AuthenticatorTests: XCTestCase { From f44347fba7b3df85113a4e4eb83ef8261d501fc9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 10:36:25 -0700 Subject: [PATCH 208/244] Animate to trace result extent --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index e2971af7f..40f1a2e4e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -324,7 +324,12 @@ public struct UtilityNetworkTrace: View { Menu(selectedTrace.name) { if let resultExtent = selectedTrace.resultExtent { Button("Zoom To") { - viewpoint = Viewpoint(boundingGeometry: resultExtent) + let newViewpoint = Viewpoint(boundingGeometry: resultExtent) + if let mapViewProxy { + Task { await mapViewProxy.setViewpoint(newViewpoint, duration: nil) } + } else { + viewpoint = newViewpoint + } } } Button("Delete", role: .destructive) { From b453abc7c0bb57ca05c77a164d4161dfc0bfce92 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 10:39:26 -0700 Subject: [PATCH 209/244] Update doc --- Documentation/UtilityNetworkTrace/README.md | 3 +-- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Documentation/UtilityNetworkTrace/README.md b/Documentation/UtilityNetworkTrace/README.md index 04aab30f2..b19499d89 100644 --- a/Documentation/UtilityNetworkTrace/README.md +++ b/Documentation/UtilityNetworkTrace/README.md @@ -24,8 +24,7 @@ A named trace configuration defined for a utility network in a webmap comprises /// - map: The map containing the utility network(s). /// - mapPoint: Acts as the point at which newly selected starting point graphics will be created. /// - screenPoint: Acts as the point of identification for items tapped in the utility network. - /// - mapViewProxy: Provides a method of layer identification when starting points are being - /// chosen. + /// - mapViewProxy: Provides access to the map view's identification and animation functionality. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. /// - startingPoints: An optional list of programmatically provided starting points. public init( diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 40f1a2e4e..9c9561108 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -545,8 +545,7 @@ public struct UtilityNetworkTrace: View { /// - map: The map containing the utility network(s). /// - mapPoint: Acts as the point at which newly selected starting point graphics will be created. /// - screenPoint: Acts as the point of identification for items tapped in the utility network. - /// - mapViewProxy: Provides a method of layer identification when starting points are being - /// chosen. + /// - mapViewProxy: Provides access to the map view's identification and animation functionality. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. /// - startingPoints: An optional list of programmatically provided starting points. public init( From 0f5ae9ecdb73ca4d69e8bc291add2528bbef97f5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 13:50:32 -0700 Subject: [PATCH 210/244] Update README.md --- Documentation/Compass/README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 4956d70a5..96fe4106d 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -24,8 +24,8 @@ Compass: /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. - /// - action: An action to perform when the compass is tapped. - public init(heading: Binding, action: (() -> Void)? = nil) + /// - mapViewProxy: Provides access to viewpoint animation. + public init(heading: Binding, mapViewProxy: MapViewProxy? = nil) ``` ```swift @@ -35,16 +35,16 @@ Compass: /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. - /// - action: An action to perform when the compass is tapped. - public init(viewpointRotation: Binding, action: (() -> Void)? = nil) + /// - mapViewProxy: Provides access to viewpoint animation. + public init(viewpointRotation: Binding, mapViewProxy: MapViewProxy? = nil) ``` ```swift /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - action: An action to perform when the compass is tapped. - public init(viewpoint: Binding, action: (() -> Void)? = nil) + /// - mapViewProxy: Provides access to viewpoint animation. + public init(viewpoint: Binding, mapViewProxy: MapViewProxy? = nil) ``` `Compass` has the following modifiers: @@ -63,7 +63,7 @@ When the compass is tapped, the map orients back to north (zero bearing). ### Basic usage for displaying a `Compass`. ```swift -@StateObject var map = Map(basemapStyle: .arcGISImagery) +@State private var map = Map(basemapStyle: .arcGISImagery) /// Allows for communication between the Compass and MapView or SceneView. @State private var viewpoint: Viewpoint? = Viewpoint( @@ -73,12 +73,14 @@ When the compass is tapped, the map orients back to north (zero bearing). ) var body: some View { - MapView(map: map, viewpoint: viewpoint) - .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint) - .padding() - } + MapViewReader { proxy in + MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: .topTrailing) { + Compass(viewpoint: $viewpoint, mapViewProxy: proxy) + .padding() + } + } } ``` From c478953e4c853322bf89a74c6151bde6bef752ba Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 11 Apr 2023 15:05:45 -0700 Subject: [PATCH 211/244] Correctly fix white bar issue The white bar was being produced by added padding, even when the fp wasn't presented --- .../Components/FloatingPanel/FloatingPanel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index b847a6e34..a201fb1a5 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -84,13 +84,13 @@ struct FloatingPanel: View where Content: View { content .frame(height: height) .clipped() - .padding(.bottom, isCompact ? 25 : 10) + .padding(.bottom, isPresented.wrappedValue ? (isCompact ? 25 : 10) : .zero) if !isCompact && isPresented.wrappedValue { Divider() makeHandleView() } } - .background(isPresented.wrappedValue ? backgroundColor : .clear) + .background(backgroundColor) .clipShape( RoundedCorners( corners: isCompact ? [.topLeft, .topRight] : [.allCorners], From 94f301ed4ba221055da63b3e1d0f5c93a423bdbf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 09:41:16 -0700 Subject: [PATCH 212/244] Add doc --- Documentation/Search/README.md | 7 +++++-- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- .../ArcGISToolkit/Components/Search/SearchViewModel.swift | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Documentation/Search/README.md b/Documentation/Search/README.md index 8f11bb748..af0cadf53 100644 --- a/Documentation/Search/README.md +++ b/Documentation/Search/README.md @@ -24,9 +24,11 @@ /// - sources: A collection of search sources to be used. /// - viewpoint: The `Viewpoint` used to pan/zoom to results. If `nil`, there will be /// no zooming to results. + /// - geoViewProxy: The proxy to provide access to geo view operations. public init( sources: [SearchSource] = [], - viewpoint: Binding? = nil + viewpoint: Binding? = nil, + geoViewProxy: GeoViewProxy? = nil ) ``` @@ -170,7 +172,8 @@ The `SearchView` will display the results list view at half height, exposing a p ```swift SearchView( sources: [locatorDataSource], - viewpoint: $searchResultViewpoint + viewpoint: $searchResultViewpoint, + geoViewProxy: mapViewProxy ) .resultsOverlay(searchResultsOverlay) .queryCenter($queryCenter) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index ea66f0511..a8ac17e17 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -21,7 +21,7 @@ public struct SearchView: View { /// - sources: A collection of search sources to be used. /// - viewpoint: The `Viewpoint` used to pan/zoom to results. If `nil`, there will be /// no zooming to results. - /// - geoViewProxy: <#Description#> + /// - geoViewProxy: The proxy to provide access to geo view operations. public init( sources: [SearchSource] = [], viewpoint: Binding? = nil, diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 21a7ae243..ef5921549 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -44,7 +44,7 @@ public enum SearchOutcome { /// - sources: Collection of search sources to be used. /// - viewpoint: The `Viewpoint` used to pan/zoom to results. If `nil`, there will be /// no zooming to results. - /// - geoViewProxy: <#Description#> + /// - geoViewProxy: The proxy to provide access to geo view operations. init( sources: [SearchSource] = [], viewpoint: Binding? = nil, @@ -119,7 +119,7 @@ public enum SearchOutcome { } } - /// <#Description#> + /// The proxy to provide access to geo view operations. private var geoViewProxy: GeoViewProxy? /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. From bee39901b118ff5c258a66711c7d2d6fcaa77f48 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 09:47:01 -0700 Subject: [PATCH 213/244] Update proxy doc to match that proposed in #300 --- Documentation/Compass/README.md | 6 +++--- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 96fe4106d..69b62775f 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -24,7 +24,7 @@ Compass: /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. - /// - mapViewProxy: Provides access to viewpoint animation. + /// - mapViewProxy: The proxy to provide access to map view operations. public init(heading: Binding, mapViewProxy: MapViewProxy? = nil) ``` @@ -35,7 +35,7 @@ Compass: /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. - /// - mapViewProxy: Provides access to viewpoint animation. + /// - mapViewProxy: The proxy to provide access to map view operations. public init(viewpointRotation: Binding, mapViewProxy: MapViewProxy? = nil) ``` @@ -43,7 +43,7 @@ Compass: /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - mapViewProxy: Provides access to viewpoint animation. + /// - mapViewProxy: The proxy to provide access to map view operations. public init(viewpoint: Binding, mapViewProxy: MapViewProxy? = nil) ``` diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 124656246..577a0ff56 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -20,7 +20,7 @@ public struct Compass: View { /// The opacity of the compass. @State private var opacity: Double = .zero - /// Provides access to viewpoint animation. + /// The proxy to provide access to map view operations. private var mapViewProxy: MapViewProxy? /// A Boolean value indicating whether the compass should automatically @@ -44,7 +44,7 @@ public struct Compass: View { /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. - /// - mapViewProxy: Provides access to viewpoint animation. + /// - mapViewProxy: The proxy to provide access to map view operations. public init( heading: Binding, mapViewProxy: MapViewProxy? = nil @@ -90,7 +90,7 @@ public extension Compass { /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the /// heading of the compass. - /// - mapViewProxy: Provides access to viewpoint animation. + /// - mapViewProxy: The proxy to provide access to map view operations. init( viewpointRotation: Binding, mapViewProxy: MapViewProxy? = nil @@ -106,7 +106,7 @@ public extension Compass { /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - mapViewProxy: Provides access to viewpoint animation. + /// - mapViewProxy: The proxy to provide access to map view operations. init( viewpoint: Binding, mapViewProxy: MapViewProxy? = nil From 0de9c4e2246fca5e4a28a92189dad2bddf86598d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 09:54:36 -0700 Subject: [PATCH 214/244] Update proxy doc Updates proxy doc to match that of doc proposed in #300, #298 --- Documentation/UtilityNetworkTrace/README.md | 2 +- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/UtilityNetworkTrace/README.md b/Documentation/UtilityNetworkTrace/README.md index b19499d89..03926a9f7 100644 --- a/Documentation/UtilityNetworkTrace/README.md +++ b/Documentation/UtilityNetworkTrace/README.md @@ -24,7 +24,7 @@ A named trace configuration defined for a utility network in a webmap comprises /// - map: The map containing the utility network(s). /// - mapPoint: Acts as the point at which newly selected starting point graphics will be created. /// - screenPoint: Acts as the point of identification for items tapped in the utility network. - /// - mapViewProxy: Provides access to the map view's identification and animation functionality. + /// - mapViewProxy: The proxy to provide access to map view operations. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. /// - startingPoints: An optional list of programmatically provided starting points. public init( diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 9c9561108..bb439890b 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -80,7 +80,7 @@ public struct UtilityNetworkTrace: View { /// The graphics overlay to hold generated starting point and trace graphics. @Binding private var graphicsOverlay: GraphicsOverlay - /// Provides a method of layer identification when starting points are being chosen. + /// The proxy to provide access to map view operations. @Binding private var mapViewProxy: MapViewProxy? /// Acts as the point of identification for items tapped in the utility network. @@ -545,7 +545,7 @@ public struct UtilityNetworkTrace: View { /// - map: The map containing the utility network(s). /// - mapPoint: Acts as the point at which newly selected starting point graphics will be created. /// - screenPoint: Acts as the point of identification for items tapped in the utility network. - /// - mapViewProxy: Provides access to the map view's identification and animation functionality. + /// - mapViewProxy: The proxy to provide access to map view operations. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. /// - startingPoints: An optional list of programmatically provided starting points. public init( From e18199c1ba292b70ab185870a03c968d5141a890 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 09:57:49 -0700 Subject: [PATCH 215/244] Simplify sample usage We've determined elsewhere that the map can simply be a @State property --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 9a21f4535..6bfb35be6 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -18,10 +18,8 @@ import SwiftUI /// A demonstration of the utility network trace tool which runs traces on a web map published with /// a utility network and trace configurations. struct UtilityNetworkTraceExampleView: View { - /// The data model containing a `Map` with the utility networks. - @StateObject private var dataModel = MapDataModel( - map: makeMap() - ) + /// The map with the utility networks. + @State private var map = makeMap() /// The current detent of the floating panel presenting the trace tool. @State var activeDetent: FloatingPanelDetent = .half @@ -44,7 +42,7 @@ struct UtilityNetworkTraceExampleView: View { var body: some View { MapViewReader { mapViewProxy in MapView( - map: dataModel.map, + map: map, viewpoint: viewpoint, graphicsOverlays: [resultGraphicsOverlay] ) @@ -68,7 +66,7 @@ struct UtilityNetworkTraceExampleView: View { ) { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, - map: dataModel.map, + map: map, mapPoint: $mapPoint, screenPoint: $screenPoint, mapViewProxy: $mapViewProxy, From afa256f6c26726cb0e25fdc3ba7cadac0d6db80a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 10:02:10 -0700 Subject: [PATCH 216/244] Make proxy a regular (non-binding) property No reason it needed to be a binding and simplifies usage --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 6 +----- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 6bfb35be6..a3b2e0b99 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -24,9 +24,6 @@ struct UtilityNetworkTraceExampleView: View { /// The current detent of the floating panel presenting the trace tool. @State var activeDetent: FloatingPanelDetent = .half - /// Provides the ability to inspect map components. - @State var mapViewProxy: MapViewProxy? - /// Provides the ability to detect tap locations in the context of the map view. @State var mapPoint: Point? @@ -49,7 +46,6 @@ struct UtilityNetworkTraceExampleView: View { .onSingleTapGesture { screenPoint, mapPoint in self.screenPoint = screenPoint self.mapPoint = mapPoint - self.mapViewProxy = mapViewProxy } .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 @@ -69,7 +65,7 @@ struct UtilityNetworkTraceExampleView: View { map: map, mapPoint: $mapPoint, screenPoint: $screenPoint, - mapViewProxy: $mapViewProxy, + mapViewProxy: mapViewProxy, viewpoint: $viewpoint ) .floatingPanelDetent($activeDetent) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index bb439890b..6c14553b0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -15,6 +15,9 @@ import ArcGIS import SwiftUI public struct UtilityNetworkTrace: View { + /// The proxy to provide access to map view operations. + private var mapViewProxy: MapViewProxy? + // MARK: Enums /// Activities users will perform while creating a new trace. @@ -80,9 +83,6 @@ public struct UtilityNetworkTrace: View { /// The graphics overlay to hold generated starting point and trace graphics. @Binding private var graphicsOverlay: GraphicsOverlay - /// The proxy to provide access to map view operations. - @Binding private var mapViewProxy: MapViewProxy? - /// Acts as the point of identification for items tapped in the utility network. @Binding private var screenPoint: CGPoint? @@ -553,14 +553,14 @@ public struct UtilityNetworkTrace: View { map: Map, mapPoint: Binding, screenPoint: Binding, - mapViewProxy: Binding, + mapViewProxy: MapViewProxy?, viewpoint: Binding, startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) ) { + self.mapViewProxy = mapViewProxy _activeDetent = .constant(nil) _screenPoint = screenPoint _mapPoint = mapPoint - _mapViewProxy = mapViewProxy _graphicsOverlay = graphicsOverlay _viewpoint = viewpoint _externalStartingPoints = startingPoints From 7e221c4248726404a0d6c3b1e305ba61004caafa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 10:02:44 -0700 Subject: [PATCH 217/244] Update README.md --- Documentation/UtilityNetworkTrace/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/UtilityNetworkTrace/README.md b/Documentation/UtilityNetworkTrace/README.md index 03926a9f7..937acb264 100644 --- a/Documentation/UtilityNetworkTrace/README.md +++ b/Documentation/UtilityNetworkTrace/README.md @@ -32,7 +32,7 @@ A named trace configuration defined for a utility network in a webmap comprises map: Map, mapPoint: Binding, screenPoint: Binding, - mapViewProxy: Binding, + mapViewProxy: MapViewProxy?, viewpoint: Binding, startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) ) From d2adb023b4465c6169f94b7500522c310e469fe1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 13:18:10 -0700 Subject: [PATCH 218/244] Update UtilityNetworkTraceViewModelTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 3415058f0..7b7887f73 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -73,7 +73,7 @@ extension UtilityNetworkTraceViewModel.Trace { /// - Parameter groupName: A name of a utility asset group. /// - Returns: The elements in the indicated group. func elementsByType(inGroupNamed groupName: String) -> [String: [UtilityElement]] { - elements(inAssetGroupNamed: name) + elements(inAssetGroupNamed: groupName) .reduce(into: [:]) { result, element in let key = element.assetType.name var assetTypeGroup = result[key, default: []] From 92de8fd536429c6f14c7d8497b73f002aab1913d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 12 Apr 2023 17:05:49 -0500 Subject: [PATCH 219/244] Update README and project file with new text and updated version number. --- Examples/Examples.xcodeproj/project.pbxproj | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index c2670b0c8..d771721c2 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -421,7 +421,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "200.0.0-beta"; + MARKETING_VERSION = 200.1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.arcgis-swift-sdk-toolkit-examples"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -449,7 +449,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "200.0.0-beta"; + MARKETING_VERSION = 200.1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.arcgis-swift-sdk-toolkit-examples"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/README.md b/README.md index 0f17a5e5b..e751a4b75 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![doc](https://img.shields.io/badge/Doc-purple)](Documentation) [![SPM](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift-package-manager/) -The ArcGIS Maps SDK for Swift Toolkit contains components that will simplify your iOS app development. It is built off of the new ArcGIS Maps SDK for Swift. +The ArcGIS Maps SDK for Swift Toolkit contains components that will simplify your Swift app development. It is built off of the new ArcGIS Maps SDK for Swift. To use Toolkit in your project: From 83beb49a1c96682b37e76c94a88b2ffcab9b06ea Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 12 Apr 2023 17:08:40 -0500 Subject: [PATCH 220/244] Update SDK package version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 16436287a..2637fef88 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/Esri/arcgis-maps-sdk-swift", .upToNextMinor(from: "200.0.0-beta")) + .package(url: "https://github.com/Esri/arcgis-maps-sdk-swift", .upToNextMinor(from: "200.1.0")) ], targets: [ .target( From 9aa5e5231531b03ae25fb443258573db574d06aa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 15:16:40 -0700 Subject: [PATCH 221/244] Update API --- Examples/Examples/CompassExampleView.swift | 2 +- .../Components/Compass/Compass.swift | 86 +++++++++---------- 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 7f092a0fd..99e2af733 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -28,7 +28,7 @@ struct CompassExampleView: View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint, mapViewProxy: proxy) + Compass(viewpoint: viewpoint, mapViewProxy: proxy) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 577a0ff56..fc5402ec9 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -20,36 +20,30 @@ public struct Compass: View { /// The opacity of the compass. @State private var opacity: Double = .zero - /// The proxy to provide access to map view operations. - private var mapViewProxy: MapViewProxy? - /// A Boolean value indicating whether the compass should automatically /// hide/show itself when the heading is `0`. private var autoHide: Bool = true - /// A Boolean value indicating whether the compass should hide based on the - /// current heading and whether the compass automatically hides. - var shouldHide: Bool { - (heading.isZero || heading.isNaN) && autoHide - } + /// The heading of the compass in degrees. + private var heading: Double + + /// The proxy to provide access to map view operations. + private var mapViewProxy: MapViewProxy /// The width and height of the compass. private var size: CGFloat = 44 - /// The heading of the compass in degrees. - @Binding private var heading: Double - /// Creates a compass with a binding to a heading based on compass /// directions (0° indicates a direction toward true North, 90° indicates a /// direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init( - heading: Binding, - mapViewProxy: MapViewProxy? = nil + init( + heading: Double, + mapViewProxy: MapViewProxy ) { - _heading = heading + self.heading = heading self.mapViewProxy = mapViewProxy } @@ -63,26 +57,33 @@ public struct Compass: View { .aspectRatio(1, contentMode: .fit) .opacity(opacity) .frame(width: size, height: size) - .onAppear { opacity = shouldHide ? 0 : 1 } - .onChange(of: heading) { _ in - let newOpacity: Double = shouldHide ? .zero : 1 + .onAppear { opacity = shouldHide(forHeading: heading) ? 0 : 1 } + .onChange(of: heading) { newHeading in + let newOpacity: Double = shouldHide(forHeading: newHeading) ? .zero : 1 guard opacity != newOpacity else { return } - withAnimation(.default.delay(shouldHide ? 0.25 : 0)) { + withAnimation(.default.delay(shouldHide(forHeading: newHeading) ? 0.25 : 0)) { opacity = newOpacity } } .onTapGesture { - if let mapViewProxy { - Task { await mapViewProxy.setViewpointRotation(0) } - } else { - heading = .zero - } + Task { await mapViewProxy.setViewpointRotation(0) } } .accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)") } } } +extension Compass { + /// Returns a Boolean value indicating whether the compass should hide based on the + /// provided heading and whether the compass has been configured to automatically hide. + /// - Parameter heading: The heading used to determine if the compass should hide. + /// - Returns: `true` if the compass should hide, `false` otherwise. + func shouldHide(forHeading heading: Double) -> Bool { + print(heading) + return (heading.rounded(.up) == 360 || heading.rounded(.down) == 0 || heading.isNaN) && autoHide + } +} + public extension Compass { /// Creates a compass with a binding to a viewpoint rotation (0° indicates /// a direction toward true North, 90° indicates a direction toward true @@ -92,15 +93,17 @@ public extension Compass { /// heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. init( - viewpointRotation: Binding, - mapViewProxy: MapViewProxy? = nil + viewpointRotation: Double?, + mapViewProxy: MapViewProxy ) { - let heading = Binding(get: { - viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue - }, set: { newHeading in - viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading - }) - self.init(heading: heading, mapViewProxy: mapViewProxy) + if let viewpointRotation { + self.init( + heading: viewpointRotation.isZero ? .zero : 360 - viewpointRotation, + mapViewProxy: mapViewProxy + ) + } else { + self.init(heading: .nan, mapViewProxy: mapViewProxy) + } } /// Creates a compass with a binding to an optional viewpoint. @@ -108,20 +111,13 @@ public extension Compass { /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. init( - viewpoint: Binding, - mapViewProxy: MapViewProxy? = nil + viewpoint: Viewpoint?, + mapViewProxy: MapViewProxy ) { - let viewpointRotation = Binding { - viewpoint.wrappedValue?.rotation ?? .nan - } set: { newViewpointRotation in - guard let oldViewpoint = viewpoint.wrappedValue else { return } - viewpoint.wrappedValue = Viewpoint( - center: oldViewpoint.targetGeometry.extent.center, - scale: oldViewpoint.targetScale, - rotation: newViewpointRotation - ) - } - self.init(viewpointRotation: viewpointRotation, mapViewProxy: mapViewProxy) + self.init( + viewpointRotation: viewpoint?.rotation ?? .nan, + mapViewProxy: mapViewProxy + ) } /// Define a custom size for the compass. From 188ef21ec08c539834360c527186ae4ba2ac960c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 15:19:02 -0700 Subject: [PATCH 222/244] Update Compass.swift --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index fc5402ec9..9723d2ad6 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -79,8 +79,7 @@ extension Compass { /// - Parameter heading: The heading used to determine if the compass should hide. /// - Returns: `true` if the compass should hide, `false` otherwise. func shouldHide(forHeading heading: Double) -> Bool { - print(heading) - return (heading.rounded(.up) == 360 || heading.rounded(.down) == 0 || heading.isNaN) && autoHide + (heading.rounded(.up) == 360 || heading.rounded(.down) == 0 || heading.isNaN) && autoHide } } From c0825f5c6c33f276f1eed437ac41e8b0a10e8c9d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 15:23:40 -0700 Subject: [PATCH 223/244] Update Compass.swift --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 9723d2ad6..79a8baf41 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -79,7 +79,7 @@ extension Compass { /// - Parameter heading: The heading used to determine if the compass should hide. /// - Returns: `true` if the compass should hide, `false` otherwise. func shouldHide(forHeading heading: Double) -> Bool { - (heading.rounded(.up) == 360 || heading.rounded(.down) == 0 || heading.isNaN) && autoHide + (heading.isZero || heading.isNaN) && autoHide } } From f3bf7eb3723a96f3880c3aa61ebccd6fd2b9d088 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 15:54:22 -0700 Subject: [PATCH 224/244] Update api, tests, readme --- Documentation/Compass/README.md | 22 +---- .../Components/Compass/Compass.swift | 12 +-- Tests/ArcGISToolkitTests/CompassTests.swift | 91 ++++++------------- 3 files changed, 34 insertions(+), 91 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 69b62775f..a7d1f10e4 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -18,23 +18,12 @@ Compass: `Compass` has the following initializers: -```swift - /// Creates a compass with a binding to a heading based on compass - /// directions (0° indicates a direction toward true North, 90° indicates a - /// direction toward true East, etc.). - /// - Parameters: - /// - heading: The heading of the compass. - /// - mapViewProxy: The proxy to provide access to map view operations. - public init(heading: Binding, mapViewProxy: MapViewProxy? = nil) -``` - ```swift /// Creates a compass with a binding to a viewpoint rotation (0° indicates /// a direction toward true North, 90° indicates a direction toward true /// West, etc.). /// - Parameters: - /// - viewpointRotation: The viewpoint rotation whose value determines the - /// heading of the compass. + /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. public init(viewpointRotation: Binding, mapViewProxy: MapViewProxy? = nil) ``` @@ -65,19 +54,14 @@ When the compass is tapped, the map orients back to north (zero bearing). ```swift @State private var map = Map(basemapStyle: .arcGISImagery) -/// Allows for communication between the Compass and MapView or SceneView. -@State private var viewpoint: Viewpoint? = Viewpoint( - center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), - scale: 10_000, - rotation: -45 -) +@State private var viewpoint: Viewpoint? var body: some View { MapViewReader { proxy in MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint, mapViewProxy: proxy) + Compass(viewpoint: viewpoint, mapViewProxy: proxy) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 79a8baf41..22db34933 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -14,8 +14,7 @@ import ArcGIS import SwiftUI -/// A `Compass` (alias North arrow) shows where north is in a `MapView` or -/// `SceneView`. +/// A `Compass` (alias North arrow) shows where north is in a `MapView` or `SceneView`. public struct Compass: View { /// The opacity of the compass. @State private var opacity: Double = .zero @@ -28,7 +27,7 @@ public struct Compass: View { private var heading: Double /// The proxy to provide access to map view operations. - private var mapViewProxy: MapViewProxy + private var mapViewProxy: MapViewProxy? /// The width and height of the compass. private var size: CGFloat = 44 @@ -41,7 +40,7 @@ public struct Compass: View { /// - mapViewProxy: The proxy to provide access to map view operations. init( heading: Double, - mapViewProxy: MapViewProxy + mapViewProxy: MapViewProxy? = nil ) { self.heading = heading self.mapViewProxy = mapViewProxy @@ -66,7 +65,7 @@ public struct Compass: View { } } .onTapGesture { - Task { await mapViewProxy.setViewpointRotation(0) } + Task { await mapViewProxy?.setViewpointRotation(0) } } .accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)") } @@ -88,8 +87,7 @@ public extension Compass { /// a direction toward true North, 90° indicates a direction toward true /// West, etc.). /// - Parameters: - /// - viewpointRotation: The viewpoint rotation whose value determines the - /// heading of the compass. + /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. init( viewpointRotation: Double?, diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 23f831ff8..df0f69802 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -17,76 +17,37 @@ import XCTest @testable import ArcGISToolkit final class CompassTests: XCTestCase { - /// Verifies that the compass accurately indicates when the compass should be hidden when - /// `autoHide` is `false`. + /// Verifies that the compass accurately indicates it shouldn't be hidden when `autoHideDisabled` + /// is applied. func testHiddenWithAutoHideOff() { - let initialValue = 0.0 - let finalValue = 90.0 - var _viewpoint: Viewpoint? = makeViewpoint(rotation: initialValue) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) + let compass1Heading = Double.zero + let compass1 = Compass(heading: compass1Heading) .autoHideDisabled() as! Compass - XCTAssertFalse(compass.shouldHide) - _viewpoint = makeViewpoint(rotation: finalValue) - XCTAssertFalse(compass.shouldHide) - } - - /// Verifies that the compass accurately indicates when the compass should be hidden when - /// `autoHide` is `true` (which is the default). - func testHiddenWithAutoHideOn() { - let initialValue = 0.0 - let finalValue = 90.0 - var _viewpoint: Viewpoint? = makeViewpoint(rotation: initialValue) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) - XCTAssertTrue(compass.shouldHide) - _viewpoint = makeViewpoint(rotation: finalValue) - XCTAssertFalse(compass.shouldHide) - } - - /// Verifies that the compass correctly initializes when given a `nil` viewpoint. - func testInit() { - let compass = Compass(viewpoint: .constant(nil)) - XCTAssertTrue(compass.shouldHide) - } - - /// Verifies that the compass correctly initializes when given a `nil` viewpoint, and `autoHide` is - /// `false`. - func testAutomaticallyHidesNoAutoHide() { - let compass = Compass(viewpoint: .constant(nil)) + XCTAssertFalse(compass1.shouldHide(forHeading: compass1Heading)) + + let compass2Heading = 45.0 + let compass2 = Compass(heading: compass2Heading) .autoHideDisabled() as! Compass - XCTAssertFalse(compass.shouldHide) - } - - /// Verifies that the compass correctly initializes when given only a viewpoint. - func testInitWithViewpoint() { - let compass = Compass(viewpoint: .constant(makeViewpoint(rotation: .zero))) - XCTAssertTrue(compass.shouldHide) - } - - /// Verifies that the compass correctly initializes when given only a viewpoint. - func testInitWithViewpointAndAutoHide() { - let compass = Compass(viewpoint: .constant(makeViewpoint(rotation: .zero))) + XCTAssertFalse(compass2.shouldHide(forHeading: compass2Heading)) + + let compass3Heading = Double.nan + let compass3 = Compass(heading: compass3Heading) .autoHideDisabled() as! Compass - XCTAssertFalse(compass.shouldHide) - } -} - -extension CompassTests { - /// An arbitrary point to use for testing. - var point: Point { - Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) - } - - /// An arbitrary scale to use for testing. - var scale: Double { - 10_000.00 + XCTAssertFalse(compass3.shouldHide(forHeading: compass3Heading)) } - /// Builds viewpoints to use for tests. - /// - Parameter rotation: The rotation to use for the resulting viewpoint. - /// - Returns: A viewpoint object for tests. - func makeViewpoint(rotation: Double) -> Viewpoint { - return Viewpoint(center: point, scale: scale, rotation: rotation) + /// Verifies that the compass accurately indicates when it should be hidden. + func testHiddenWithAutoHideOn() { + let compass1Heading: Double = .zero + let compass1 = Compass(heading: compass1Heading) + XCTAssertTrue(compass1.shouldHide(forHeading: compass1Heading)) + + let compass2Heading = 45.0 + let compass2 = Compass(heading: compass2Heading) + XCTAssertFalse(compass2.shouldHide(forHeading: compass2Heading)) + + let compass3Heading = Double.nan + let compass3 = Compass(heading: compass3Heading) + XCTAssertTrue(compass3.shouldHide(forHeading: compass3Heading)) } } From 648d82e66738812a22b28e2e17c9f4fb293648d9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 15:59:37 -0700 Subject: [PATCH 225/244] Update README.md --- Documentation/Compass/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index a7d1f10e4..e57d23826 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -25,7 +25,7 @@ Compass: /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpointRotation: Binding, mapViewProxy: MapViewProxy? = nil) + public init(viewpointRotation: Double?, mapViewProxy: MapViewProxy? = nil) ``` ```swift @@ -33,7 +33,7 @@ Compass: /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpoint: Binding, mapViewProxy: MapViewProxy? = nil) + public init(viewpoint: Viewpoint?, mapViewProxy: MapViewProxy? = nil) ``` `Compass` has the following modifiers: From 3c7eb9c9f29727ff698e410dff0ed8b168b71ebf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 16:01:29 -0700 Subject: [PATCH 226/244] Alphabetize --- Documentation/Compass/README.md | 16 +++++------ .../Components/Compass/Compass.swift | 28 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index e57d23826..99e8d6c61 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -19,21 +19,21 @@ Compass: `Compass` has the following initializers: ```swift - /// Creates a compass with a binding to a viewpoint rotation (0° indicates - /// a direction toward true North, 90° indicates a direction toward true - /// West, etc.). + /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: - /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. + /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpointRotation: Double?, mapViewProxy: MapViewProxy? = nil) + public init(viewpoint: Viewpoint?, mapViewProxy: MapViewProxy? = nil) ``` ```swift - /// Creates a compass with a binding to an optional viewpoint. + /// Creates a compass with a binding to a viewpoint rotation (0° indicates + /// a direction toward true North, 90° indicates a direction toward true + /// West, etc.). /// - Parameters: - /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. + /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpoint: Viewpoint?, mapViewProxy: MapViewProxy? = nil) + public init(viewpointRotation: Double?, mapViewProxy: MapViewProxy? = nil) ``` `Compass` has the following modifiers: diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 22db34933..b9db2487b 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -83,6 +83,20 @@ extension Compass { } public extension Compass { + /// Creates a compass with a binding to an optional viewpoint. + /// - Parameters: + /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. + /// - mapViewProxy: The proxy to provide access to map view operations. + init( + viewpoint: Viewpoint?, + mapViewProxy: MapViewProxy + ) { + self.init( + viewpointRotation: viewpoint?.rotation ?? .nan, + mapViewProxy: mapViewProxy + ) + } + /// Creates a compass with a binding to a viewpoint rotation (0° indicates /// a direction toward true North, 90° indicates a direction toward true /// West, etc.). @@ -103,20 +117,6 @@ public extension Compass { } } - /// Creates a compass with a binding to an optional viewpoint. - /// - Parameters: - /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - mapViewProxy: The proxy to provide access to map view operations. - init( - viewpoint: Viewpoint?, - mapViewProxy: MapViewProxy - ) { - self.init( - viewpointRotation: viewpoint?.rotation ?? .nan, - mapViewProxy: mapViewProxy - ) - } - /// Define a custom size for the compass. /// - Parameter size: The width and height of the compass. func compassSize(size: CGFloat) -> Self { From 1301522f4ae18127ef4841e7c2aa122bf07c4518 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 16:05:05 -0700 Subject: [PATCH 227/244] Update README.md --- Documentation/Compass/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 99e8d6c61..f3817f907 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -23,7 +23,7 @@ Compass: /// - Parameters: /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpoint: Viewpoint?, mapViewProxy: MapViewProxy? = nil) + public init(viewpoint: Viewpoint?, mapViewProxy: MapViewProxy) ``` ```swift @@ -33,7 +33,7 @@ Compass: /// - Parameters: /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpointRotation: Double?, mapViewProxy: MapViewProxy? = nil) + public init(viewpointRotation: Double?, mapViewProxy: MapViewProxy) ``` `Compass` has the following modifiers: From af92c8fde027242f8f3411a4f1b9608a0ce337b7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 12 Apr 2023 16:30:03 -0700 Subject: [PATCH 228/244] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 6c14553b0..7440052b8 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -149,7 +149,12 @@ public struct UtilityNetworkTrace: View { Task { if let feature = await viewModel.feature(for: element), let geometry = feature.geometry { - viewpoint = Viewpoint(boundingGeometry: geometry.extent) + let newViewpoint = Viewpoint(boundingGeometry: geometry.extent) + if let mapViewProxy { + Task { await mapViewProxy.setViewpoint(newViewpoint, duration: nil) } + } else { + viewpoint = newViewpoint + } } } } label: { From e09a3053ecb107707b3be6deff66d9f10a4be48c Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Thu, 13 Apr 2023 08:33:15 -0700 Subject: [PATCH 229/244] change auth example's target to be 15.0 and fix build issue with latest sdk --- .../AuthenticationExample.xcodeproj/project.pbxproj | 4 ++-- .../AuthenticationExample/FeaturedMapsView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj index f47e307b8..984301b34 100644 --- a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj @@ -237,7 +237,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -292,7 +292,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; diff --git a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift index 38522f506..b4c7300c6 100644 --- a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift +++ b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift @@ -30,7 +30,7 @@ struct FeaturedMapsView: View { if isLoading { ProgressView() } else { - List(featuredItems) { item in + List(featuredItems, id: \.id) { item in NavigationLink { MapItemView(map: Map(item: item)) } label: { From 158732c4c808cecf22a3fb7d6c9beced9dd55563 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 09:26:26 -0700 Subject: [PATCH 230/244] Update Sources/ArcGISToolkit/Components/Compass/Compass.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index b9db2487b..03be01b6d 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -14,7 +14,7 @@ import ArcGIS import SwiftUI -/// A `Compass` (alias North arrow) shows where north is in a `MapView` or `SceneView`. +/// A `Compass` (alias North arrow) shows where north is in a `MapView`. public struct Compass: View { /// The opacity of the compass. @State private var opacity: Double = .zero From 22398252d7c6a8bfdb39f8045113558b9fe24bc1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 13 Apr 2023 14:02:39 -0500 Subject: [PATCH 231/244] Update authentication example version --- .../AuthenticationExample.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj index f47e307b8..b15a8dbbc 100644 --- a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj @@ -322,7 +322,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 200.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.esri.Authentication; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -351,7 +351,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 200.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.esri.Authentication; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; From d7d62db5bf891162449479bb7c88edb4c98c9a3d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 12:08:23 -0700 Subject: [PATCH 232/244] Update Sources/ArcGISToolkit/Components/Compass/Compass.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 03be01b6d..73ea65da9 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -107,14 +107,13 @@ public extension Compass { viewpointRotation: Double?, mapViewProxy: MapViewProxy ) { + let heading: Double if let viewpointRotation { - self.init( - heading: viewpointRotation.isZero ? .zero : 360 - viewpointRotation, - mapViewProxy: mapViewProxy - ) + heading = viewpointRotation.isZero ? .zero : 360 - viewpointRotation } else { - self.init(heading: .nan, mapViewProxy: mapViewProxy) + heading = .nan } + self.init(heading: heading, mapViewProxy: mapViewProxy) } /// Define a custom size for the compass. From f734b6d6ae2d6af933336021add4b57caacccb4e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 12:15:20 -0700 Subject: [PATCH 233/244] Drop `init(viewpoint:, mapViewProxy:)`, update example --- Examples/Examples/CompassExampleView.swift | 2 +- .../Components/Compass/Compass.swift | 22 ++++--------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 99e2af733..f5954df67 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -28,7 +28,7 @@ struct CompassExampleView: View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: viewpoint, mapViewProxy: proxy) + Compass(rotation: viewpoint?.rotation, mapViewProxy: proxy) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 73ea65da9..67d2fc006 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -83,33 +83,19 @@ extension Compass { } public extension Compass { - /// Creates a compass with a binding to an optional viewpoint. - /// - Parameters: - /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - mapViewProxy: The proxy to provide access to map view operations. - init( - viewpoint: Viewpoint?, - mapViewProxy: MapViewProxy - ) { - self.init( - viewpointRotation: viewpoint?.rotation ?? .nan, - mapViewProxy: mapViewProxy - ) - } - /// Creates a compass with a binding to a viewpoint rotation (0° indicates /// a direction toward true North, 90° indicates a direction toward true /// West, etc.). /// - Parameters: - /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. + /// - rotation: The viewpoint rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. init( - viewpointRotation: Double?, + rotation: Double?, mapViewProxy: MapViewProxy ) { let heading: Double - if let viewpointRotation { - heading = viewpointRotation.isZero ? .zero : 360 - viewpointRotation + if let rotation { + heading = rotation.isZero ? .zero : 360 - rotation } else { heading = .nan } From c5614e228d3911667acbd4d412ca492a9c62c19d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 12:17:10 -0700 Subject: [PATCH 234/244] Update README.md --- Documentation/Compass/README.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index f3817f907..bdba525c7 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -16,22 +16,14 @@ Compass: ## Key properties -`Compass` has the following initializers: - -```swift - /// Creates a compass with a binding to an optional viewpoint. - /// - Parameters: - /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. - /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpoint: Viewpoint?, mapViewProxy: MapViewProxy) -``` +`Compass` has the following initializer: ```swift /// Creates a compass with a binding to a viewpoint rotation (0° indicates /// a direction toward true North, 90° indicates a direction toward true /// West, etc.). /// - Parameters: - /// - viewpointRotation: The viewpoint rotation whose value determines the heading of the compass. + /// - rotation: The rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. public init(viewpointRotation: Double?, mapViewProxy: MapViewProxy) ``` @@ -61,7 +53,7 @@ var body: some View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: viewpoint, mapViewProxy: proxy) + Compass(rotation: viewpoint?.rotation, mapViewProxy: proxy) .padding() } } From bf38ee70e4a552b3deac3824d079d28129be424b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 12:24:38 -0700 Subject: [PATCH 235/244] Doc --- Documentation/Compass/README.md | 7 +++---- .../ArcGISToolkit/Components/Compass/Compass.swift | 12 +++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index bdba525c7..e2274b49b 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -19,13 +19,12 @@ Compass: `Compass` has the following initializer: ```swift - /// Creates a compass with a binding to a viewpoint rotation (0° indicates - /// a direction toward true North, 90° indicates a direction toward true - /// West, etc.). + /// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates + /// a direction toward true West, etc.). /// - Parameters: /// - rotation: The rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. - public init(viewpointRotation: Double?, mapViewProxy: MapViewProxy) + public init(rotation: Double?, mapViewProxy: MapViewProxy) ``` `Compass` has the following modifiers: diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 67d2fc006..d2f40b574 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -32,9 +32,8 @@ public struct Compass: View { /// The width and height of the compass. private var size: CGFloat = 44 - /// Creates a compass with a binding to a heading based on compass - /// directions (0° indicates a direction toward true North, 90° indicates a - /// direction toward true East, etc.). + /// Creates a compass with a heading based on compass directions (0° indicates a direction + /// toward true North, 90° indicates a direction toward true East, etc.). /// - Parameters: /// - heading: The heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. @@ -83,11 +82,10 @@ extension Compass { } public extension Compass { - /// Creates a compass with a binding to a viewpoint rotation (0° indicates - /// a direction toward true North, 90° indicates a direction toward true - /// West, etc.). + /// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates + /// a direction toward true West, etc.). /// - Parameters: - /// - rotation: The viewpoint rotation whose value determines the heading of the compass. + /// - rotation: The rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. init( rotation: Double?, From 117516938f5677206dcee5f9cb0af0448ba1731b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 12:50:10 -0700 Subject: [PATCH 236/244] Fix map doc --- Documentation/Compass/README.md | 4 ++-- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index e2274b49b..c2d63c0da 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -19,8 +19,8 @@ Compass: `Compass` has the following initializer: ```swift - /// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates - /// a direction toward true West, etc.). + /// Creates a compass with a rotation (0° indicates a map rotation towards true North, 90° + /// indicates a map rotation towards true East, etc.). /// - Parameters: /// - rotation: The rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index d2f40b574..69bf85f61 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -82,8 +82,8 @@ extension Compass { } public extension Compass { - /// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates - /// a direction toward true West, etc.). + /// Creates a compass with a rotation (0° indicates a map rotation towards true North, 90° + /// indicates a map rotation towards true East, etc.). /// - Parameters: /// - rotation: The rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. From d9be3ebaceff2b161be4d35cd5ace758009504aa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 12:56:50 -0700 Subject: [PATCH 237/244] Doc --- Documentation/Compass/README.md | 4 ++-- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index c2d63c0da..e2274b49b 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -19,8 +19,8 @@ Compass: `Compass` has the following initializer: ```swift - /// Creates a compass with a rotation (0° indicates a map rotation towards true North, 90° - /// indicates a map rotation towards true East, etc.). + /// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates + /// a direction toward true West, etc.). /// - Parameters: /// - rotation: The rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 69bf85f61..d2f40b574 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -82,8 +82,8 @@ extension Compass { } public extension Compass { - /// Creates a compass with a rotation (0° indicates a map rotation towards true North, 90° - /// indicates a map rotation towards true East, etc.). + /// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates + /// a direction toward true West, etc.). /// - Parameters: /// - rotation: The rotation whose value determines the heading of the compass. /// - mapViewProxy: The proxy to provide access to map view operations. From 2eb42f80e386b3e842a91dc96d2938f59eada75e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 14:03:10 -0700 Subject: [PATCH 238/244] Update FloorFilter.swift Viewpoint changes should always be reported to the model, despite whether the site and facility selector is open or not. --- .../Components/FloorFilter/FloorFilter.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index aa78637cd..ed37dc371 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -146,18 +146,12 @@ public struct FloorFilter: View { Color.clear .sheet(isPresented: .constant(!$isSitesAndFacilitiesHidden.wrappedValue)) { SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) - .onChange(of: viewpoint.wrappedValue) { viewpoint in - reportChange(of: viewpoint) - } } } else { ZStack { Color.clear .esriBorder() SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) - .onChange(of: viewpoint.wrappedValue) { viewpoint in - reportChange(of: viewpoint) - } .padding([.top, .leading, .trailing], 2.5) .padding(.bottom) } @@ -194,6 +188,11 @@ public struct FloorFilter: View { guard selection?.wrappedValue != newValue else { return } selection?.wrappedValue = newValue } + .onChange(of: viewpoint.wrappedValue) { newViewpoint in + if let newViewpoint { + reportChange(of: newViewpoint) + } + } } /// The width of the level selector. From 7f9f79980258758b7f7d82c25af3173f2a139434 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 15:12:00 -0700 Subject: [PATCH 239/244] Drop `reportChange` It was only called from one spot at this point and doesn't carry enough weight as its own function --- .../Components/FloorFilter/FloorFilter.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index ed37dc371..fb2c70592 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -120,12 +120,6 @@ public struct FloorFilter: View { alignment.vertical == .top } - /// Reports a viewpoint change to the view model if the map is not navigating. - private func reportChange(of viewpoint: Viewpoint?) { - guard isNavigating.wrappedValue else { return } - viewModel.onViewpointChanged(viewpoint) - } - /// A view that allows selecting between levels. @ViewBuilder private var levelSelector: some View { LevelSelector( @@ -189,8 +183,9 @@ public struct FloorFilter: View { selection?.wrappedValue = newValue } .onChange(of: viewpoint.wrappedValue) { newViewpoint in + guard isNavigating.wrappedValue else { return } if let newViewpoint { - reportChange(of: newViewpoint) + viewModel.onViewpointChanged(newViewpoint) } } } From c0cf13a1e102f217f510ce47d07c4bf6ce2b270f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 13 Apr 2023 17:07:17 -0700 Subject: [PATCH 240/244] Update bookmarks comp usage, and readme --- Documentation/Bookmarks/README.md | 61 +++++++++++--------- Examples/Examples/BookmarksExampleView.swift | 19 +++--- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/Documentation/Bookmarks/README.md b/Documentation/Bookmarks/README.md index 3e19110f5..7cb2915cd 100644 --- a/Documentation/Bookmarks/README.md +++ b/Documentation/Bookmarks/README.md @@ -61,8 +61,11 @@ If a `Viewpoint` binding is provided to the `Bookmarks` view, selecting a bookma The view is displayed in a `popover` in response to a toolbar button tap. ```swift -/// A web map with predefined bookmarks. -@StateObject private var map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! +/// The `Map` with predefined bookmarks. +@State private var map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + +/// The last selected bookmark. +@State var selectedBookmark: Bookmark? /// Indicates if the `Bookmarks` component is shown or not. /// - Remark: This allows a developer to control when the `Bookmarks` component is @@ -71,36 +74,40 @@ The view is displayed in a `popover` in response to a toolbar button tap. /// Allows for communication between the `Bookmarks` component and a `MapView` or /// `SceneView`. -@State var viewpoint: Viewpoint? = nil +@State var viewpoint: Viewpoint? var body: some View { - MapView(map: map, viewpoint: viewpoint) - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button { - showingBookmarks.toggle() - } label: { - Label( - "Show Bookmarks", - systemImage: "bookmark" - ) + MapViewReader { mapViewProxy in + MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } + .task(id: selectedBookmark) { + if let selectedBookmark, let viewpoint = selectedBookmark.viewpoint { + await mapViewProxy.setViewpoint(viewpoint) } - .popover(isPresented: $showingBookmarks) { - // Display the `Bookmarks` components with a pre-defined - // list of bookmarks. Passing in a `Viewpoint` binding - // will allow the `Bookmarks` component to handle - // bookmark selection. - Bookmarks( - isPresented: $showingBookmarks, - geoModel: map, - viewpoint: $viewpoint - ) + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + showingBookmarks.toggle() + } label: { + Label( + "Show Bookmarks", + systemImage: "bookmark" + ) + } + .popover(isPresented: $showingBookmarks) { + // Display the `Bookmarks` component with the list of bookmarks in a map. + Bookmarks( + isPresented: $showingBookmarks, + geoModel: map + ) + .onSelectionChanged { selectedBookmark = $0 } + } } } - } + } } ``` diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index e67a82c2e..04ee6c57d 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -19,6 +19,9 @@ struct BookmarksExampleView: View { /// The `Map` with predefined bookmarks. @State private var map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + /// The last selected bookmark. + @State var selectedBookmark: Bookmark? + /// Indicates if the `Bookmarks` component is shown or not. /// - Remark: This allows a developer to control when the `Bookmarks` component is /// shown/hidden, whether that be in a group of options or a standalone button. @@ -34,6 +37,11 @@ struct BookmarksExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .task(id: selectedBookmark) { + if let selectedBookmark, let viewpoint = selectedBookmark.viewpoint { + await mapViewProxy.setViewpoint(viewpoint) + } + } .toolbar { ToolbarItem(placement: .primaryAction) { Button { @@ -45,19 +53,12 @@ struct BookmarksExampleView: View { ) } .popover(isPresented: $showingBookmarks) { - // Display the `Bookmarks` component with the list of - // bookmarks in a map. + // Display the `Bookmarks` component with the list of bookmarks in a map. Bookmarks( isPresented: $showingBookmarks, geoModel: map ) - // In order to handle bookmark selection changes - // manually, use `onSelectionChanged(perform:)`. - .onSelectionChanged { bookmark in - if let viewpoint = bookmark.viewpoint { - Task { await mapViewProxy.setViewpoint(viewpoint, duration: 1) } - } - } + .onSelectionChanged { selectedBookmark = $0 } } } } From 4685936ceb5192146078615034d55a3fa5f64aa2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 14 Apr 2023 10:36:26 -0500 Subject: [PATCH 241/244] Update copyright dates --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d9020ed64..d68f50c7d 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 Esri + Copyright 2022, 2023 Esri Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 12601b47a7958ad21aecaedda7bf20c332f3ad52 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 18 Apr 2023 12:05:25 -0500 Subject: [PATCH 242/244] Update license copyright dates. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e751a4b75..2739d532c 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Find a bug or want to request a new feature? Please let us know by [submitting Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/esri/contributing). ## Licensing -Copyright 2017 - 2022 Esri +Copyright 2022, 2023 Esri Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 0b8abb15185b84de77eedaf1f8c8a8ef2d44ddc4 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 18 Apr 2023 15:24:49 -0700 Subject: [PATCH 243/244] fix warnings --- AuthenticationExample/AuthenticationExample/SignInView.swift | 2 ++ AuthenticationExample/AuthenticationExample/UserView.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index 8c38fe680..b2abd75e1 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -71,6 +71,8 @@ struct SignInView: View { return "" case .serverTrust: return nil + @unknown default: + fatalError("Unknown NetworkCredential") } } .first diff --git a/AuthenticationExample/AuthenticationExample/UserView.swift b/AuthenticationExample/AuthenticationExample/UserView.swift index 0d4ac7c0d..fb1210037 100644 --- a/AuthenticationExample/AuthenticationExample/UserView.swift +++ b/AuthenticationExample/AuthenticationExample/UserView.swift @@ -73,6 +73,8 @@ extension PortalUser.Role: CustomStringConvertible { return "Admin" case .publisher: return "Publisher" + @unknown default: + fatalError("Unknown PortalUser.Role") } } } From 37cb9ef539cae07958b93ed9ea160ecde52b8725 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Tue, 18 Apr 2023 15:42:02 -0700 Subject: [PATCH 244/244] set correct destination for Mac Catalyst --- .../AuthenticationExample.xcodeproj/project.pbxproj | 10 ++++++++++ .../AuthenticationExample.entitlements | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj index 9165fcf10..2235b68cc 100644 --- a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 21B31E8B29EF53BE00A40B10 /* AuthenticationExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthenticationExample.entitlements; sourceTree = ""; }; 88AD13752834355000500B2E /* AuthenticationExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthenticationExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 88AD13782834355000500B2E /* AuthenticationApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationApp.swift; sourceTree = ""; }; 88AD137A2834355000500B2E /* SigninView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigninView.swift; sourceTree = ""; }; @@ -69,6 +70,7 @@ 88AD13772834355000500B2E /* AuthenticationExample */ = { isa = PBXGroup; children = ( + 21B31E8B29EF53BE00A40B10 /* AuthenticationExample.entitlements */, 88AD1387283435EA00500B2E /* Info.plist */, 88AD13782834355000500B2E /* AuthenticationApp.swift */, 88D24DF1288F2FA1007A539C /* HomeView.swift */, @@ -307,6 +309,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthenticationExample/AuthenticationExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AuthenticationExample/Preview Content\""; @@ -325,6 +328,9 @@ MARKETING_VERSION = 200.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.esri.Authentication; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -336,6 +342,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthenticationExample/AuthenticationExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AuthenticationExample/Preview Content\""; @@ -354,6 +361,9 @@ MARKETING_VERSION = 200.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.esri.Authentication; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements b/AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements new file mode 100644 index 000000000..ee95ab7e5 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/AuthenticationExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + +