From d375e4cbf302abe04f93c90e54646332593eb413 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 14 May 2021 17:18:41 -0500 Subject: [PATCH 0001/1722] Create and setup initial Toolkit swift package. --- .../arcgis-runtime-toolkit-swift.xcscheme | 115 ++++++++++++++++++ Package.swift | 33 +++++ .../Components/OverviewMap.swift | 22 ++++ .../XCTestManifests.swift | 9 ++ .../arcgis_runtime_toolkit_swiftTests.swift | 14 +++ Tests/LinuxMain.swift | 7 ++ 6 files changed, 200 insertions(+) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme create mode 100644 Package.swift create mode 100644 Sources/ArcGISToolkitSwift/Components/OverviewMap.swift create mode 100644 Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift create mode 100644 Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift create mode 100644 Tests/LinuxMain.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme new file mode 100644 index 000000000..641a84e29 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..b003a5de8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "arcgis-runtime-toolkit-swift", + platforms: [ + .iOS(.v14) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "ArcGISToolkitSwift", + targets: ["ArcGISToolkitSwift"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + .package(name: "ArcGIS", path: "/Users/Shared/Source/ArcGIS/swift") + + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "ArcGISToolkitSwift", + dependencies: ["ArcGIS"]), + .testTarget( + name: "ArcGISToolkitSwiftTests", + dependencies: ["ArcGISToolkitSwift"]), + ] +) diff --git a/Sources/ArcGISToolkitSwift/Components/OverviewMap.swift b/Sources/ArcGISToolkitSwift/Components/OverviewMap.swift new file mode 100644 index 000000000..0afb6b88b --- /dev/null +++ b/Sources/ArcGISToolkitSwift/Components/OverviewMap.swift @@ -0,0 +1,22 @@ +// +// SwiftUIView.swift +// +// +// Created by Mark Dostal on 5/14/21. +// + +import SwiftUI +//import ArcGIS + +struct OverviewMap: View { + var body: some View { +// let mapView: MapView + Text("Hello, Toolkit!") + } +} + +struct SwiftUIView_Previews: PreviewProvider { + static var previews: some View { + OverviewMap() + } +} diff --git a/Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift b/Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift new file mode 100644 index 000000000..5039df0f8 --- /dev/null +++ b/Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(arcgis_runtime_toolkit_swiftTests.allTests), + ] +} +#endif diff --git a/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift b/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift new file mode 100644 index 000000000..473596f34 --- /dev/null +++ b/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import ArcGISToolkitSwift + +final class arcgis_runtime_toolkit_swiftTests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 000000000..a526b03c5 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import arcgis_runtime_toolkit_swiftTests + +var tests = [XCTestCaseEntry]() +tests += arcgis_runtime_toolkit_swiftTests.allTests() +XCTMain(tests) From 7b182cfe2c4db441d75457db3e651796eabd8f77 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 17 May 2021 14:29:13 -0500 Subject: [PATCH 0002/1722] use relative url for ArcGIS package --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b003a5de8..dab8a22fd 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), - .package(name: "ArcGIS", path: "/Users/Shared/Source/ArcGIS/swift") + .package(name: "ArcGIS", path: "../swift") ], targets: [ From 78530486fe488a0b7de498099a6ec113a54bad2f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 21 May 2021 16:38:52 -0500 Subject: [PATCH 0003/1722] Basic swift package files, Example app, and Toolkit components --- Examples/Examples.xcodeproj/project.pbxproj | 424 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../Examples/SampleComponentExampleView.swift | 28 ++ Examples/ExamplesApp/AnyExample.swift | 28 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../ExamplesApp/Assets.xcassets/Contents.json | 6 + Examples/ExamplesApp/ContentView.swift | 26 ++ Examples/ExamplesApp/Example.swift | 20 + Examples/ExamplesApp/ExampleList.swift | 30 ++ Examples/ExamplesApp/ExampleView.swift | 27 ++ Examples/ExamplesApp/Examples.entitlements | 12 + Examples/ExamplesApp/Examples.swift | 39 ++ Examples/ExamplesApp/ExamplesApp.swift | 23 + Examples/ExamplesApp/Info.plist | 50 +++ .../Preview Assets.xcassets/Contents.json | 6 + .../Components/SampleComponent.swift | 26 ++ .../arcgis_runtime_toolkit_swiftTests.swift | 13 + Tests/LinuxMain.swift | 7 - 19 files changed, 875 insertions(+), 7 deletions(-) create mode 100644 Examples/Examples.xcodeproj/project.pbxproj create mode 100644 Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Examples/Examples/Examples/SampleComponentExampleView.swift create mode 100644 Examples/ExamplesApp/AnyExample.swift create mode 100644 Examples/ExamplesApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/Contents.json create mode 100644 Examples/ExamplesApp/ContentView.swift create mode 100644 Examples/ExamplesApp/Example.swift create mode 100644 Examples/ExamplesApp/ExampleList.swift create mode 100644 Examples/ExamplesApp/ExampleView.swift create mode 100644 Examples/ExamplesApp/Examples.entitlements create mode 100644 Examples/ExamplesApp/Examples.swift create mode 100644 Examples/ExamplesApp/ExamplesApp.swift create mode 100644 Examples/ExamplesApp/Info.plist create mode 100644 Examples/ExamplesApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Sources/ArcGISToolkitSwift/Components/SampleComponent.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj new file mode 100644 index 000000000..301440fdb --- /dev/null +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -0,0 +1,424 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + E40F58082656E695006F5CB9 /* ArcGISToolkitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E40F58072656E695006F5CB9 /* ArcGISToolkitSwift */; }; + E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; + E47ABE462652FE0900FD2FE3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE452652FE0900FD2FE3 /* ContentView.swift */; }; + E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */; }; + E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; + E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; }; + E47ABE5D2652FF1200FD2FE3 /* ArcGIS in Embed Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + E48A73432658227100F5C118 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73402658227100F5C118 /* Example.swift */; }; + E48A73442658227100F5C118 /* AnyExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73412658227100F5C118 /* AnyExample.swift */; }; + E48A73452658227100F5C118 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733D2658227000F5C118 /* Examples.swift */; }; + E48A73462658227100F5C118 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73422658227100F5C118 /* ExampleView.swift */; }; + E48A73472658227100F5C118 /* ExampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733E2658227100F5C118 /* ExampleList.swift */; }; + E4D9C33E26585AC4000AAAE8 /* SampleComponentExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D9C33D26585AC4000AAAE8 /* SampleComponentExampleView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + E47ABE5E2652FF1300FD2FE3 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + E47ABE5D2652FF1200FD2FE3 /* ArcGIS in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; + E47ABE452652FE0900FD2FE3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + E47ABE4C2652FE0C00FD2FE3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E47ABE622653005F00FD2FE3 /* arcgis-runtime-toolkit-swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "arcgis-runtime-toolkit-swift"; path = ..; sourceTree = ""; }; + E48A733D2658227000F5C118 /* Examples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Examples.swift; sourceTree = ""; }; + E48A733E2658227100F5C118 /* ExampleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleList.swift; sourceTree = ""; }; + E48A733F2658227100F5C118 /* Examples.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Examples.entitlements; sourceTree = ""; }; + E48A73402658227100F5C118 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; + E48A73412658227100F5C118 /* AnyExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyExample.swift; sourceTree = ""; }; + E48A73422658227100F5C118 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; + E4D9C33D26585AC4000AAAE8 /* SampleComponentExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleComponentExampleView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E47ABE3D2652FE0900FD2FE3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E40F58082656E695006F5CB9 /* ArcGISToolkitSwift in Frameworks */, + E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E40F58042656E509006F5CB9 /* Examples */ = { + isa = PBXGroup; + children = ( + E4D9C33D26585AC4000AAAE8 /* SampleComponentExampleView.swift */, + ); + path = Examples; + sourceTree = ""; + }; + E47ABE372652FE0900FD2FE3 = { + isa = PBXGroup; + children = ( + E47ABE622653005F00FD2FE3 /* arcgis-runtime-toolkit-swift */, + E47ABE562652FE7F00FD2FE3 /* Examples */, + E47ABE422652FE0900FD2FE3 /* ExamplesApp */, + E47ABE412652FE0900FD2FE3 /* Products */, + E47ABE5A2652FF1200FD2FE3 /* Frameworks */, + ); + sourceTree = ""; + }; + E47ABE412652FE0900FD2FE3 /* Products */ = { + isa = PBXGroup; + children = ( + E47ABE402652FE0900FD2FE3 /* Examples.app */, + ); + name = Products; + sourceTree = ""; + }; + E47ABE422652FE0900FD2FE3 /* ExamplesApp */ = { + isa = PBXGroup; + children = ( + E48A73412658227100F5C118 /* AnyExample.swift */, + E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */, + E47ABE452652FE0900FD2FE3 /* ContentView.swift */, + E48A73402658227100F5C118 /* Example.swift */, + E48A733E2658227100F5C118 /* ExampleList.swift */, + E48A733F2658227100F5C118 /* Examples.entitlements */, + E48A733D2658227000F5C118 /* Examples.swift */, + E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */, + E48A73422658227100F5C118 /* ExampleView.swift */, + E47ABE4C2652FE0C00FD2FE3 /* Info.plist */, + E47ABE492652FE0C00FD2FE3 /* Preview Content */, + ); + path = ExamplesApp; + sourceTree = ""; + }; + E47ABE492652FE0C00FD2FE3 /* Preview Content */ = { + isa = PBXGroup; + children = ( + E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + E47ABE562652FE7F00FD2FE3 /* Examples */ = { + isa = PBXGroup; + children = ( + E40F58042656E509006F5CB9 /* Examples */, + ); + path = Examples; + sourceTree = ""; + }; + E47ABE5A2652FF1200FD2FE3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E47ABE3F2652FE0900FD2FE3 /* Examples */ = { + isa = PBXNativeTarget; + buildConfigurationList = E47ABE4F2652FE0C00FD2FE3 /* Build configuration list for PBXNativeTarget "Examples" */; + buildPhases = ( + E47ABE3C2652FE0900FD2FE3 /* Sources */, + E47ABE3D2652FE0900FD2FE3 /* Frameworks */, + E47ABE3E2652FE0900FD2FE3 /* Resources */, + E47ABE5E2652FF1300FD2FE3 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Examples; + packageProductDependencies = ( + E47ABE5B2652FF1200FD2FE3 /* ArcGIS */, + E40F58072656E695006F5CB9 /* ArcGISToolkitSwift */, + ); + productName = Examples; + productReference = E47ABE402652FE0900FD2FE3 /* Examples.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E47ABE382652FE0900FD2FE3 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1250; + TargetAttributes = { + E47ABE3F2652FE0900FD2FE3 = { + CreatedOnToolsVersion = 12.5; + }; + }; + }; + buildConfigurationList = E47ABE3B2652FE0900FD2FE3 /* Build configuration list for PBXProject "Examples" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E47ABE372652FE0900FD2FE3; + productRefGroup = E47ABE412652FE0900FD2FE3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E47ABE3F2652FE0900FD2FE3 /* Examples */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E47ABE3E2652FE0900FD2FE3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */, + E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E47ABE3C2652FE0900FD2FE3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E47ABE462652FE0900FD2FE3 /* ContentView.swift in Sources */, + E48A73452658227100F5C118 /* Examples.swift in Sources */, + E48A73432658227100F5C118 /* Example.swift in Sources */, + E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, + E4D9C33E26585AC4000AAAE8 /* SampleComponentExampleView.swift in Sources */, + E48A73442658227100F5C118 /* AnyExample.swift in Sources */, + E48A73472658227100F5C118 /* ExampleList.swift in Sources */, + E48A73462658227100F5C118 /* ExampleView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + E47ABE4D2652FE0C00FD2FE3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E47ABE4E2652FE0C00FD2FE3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E47ABE502652FE0C00FD2FE3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"ExamplesApp/Preview Content\""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = ExamplesApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.esri.Examples; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E47ABE512652FE0C00FD2FE3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"ExamplesApp/Preview Content\""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = ExamplesApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.esri.Examples; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E47ABE3B2652FE0900FD2FE3 /* Build configuration list for PBXProject "Examples" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E47ABE4D2652FE0C00FD2FE3 /* Debug */, + E47ABE4E2652FE0C00FD2FE3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E47ABE4F2652FE0C00FD2FE3 /* Build configuration list for PBXNativeTarget "Examples" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E47ABE502652FE0C00FD2FE3 /* Debug */, + E47ABE512652FE0C00FD2FE3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + E40F58072656E695006F5CB9 /* ArcGISToolkitSwift */ = { + isa = XCSwiftPackageProductDependency; + productName = ArcGISToolkitSwift; + }; + E47ABE5B2652FF1200FD2FE3 /* ArcGIS */ = { + isa = XCSwiftPackageProductDependency; + productName = ArcGIS; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = E47ABE382652FE0900FD2FE3 /* Project object */; +} diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Examples/Examples/Examples/SampleComponentExampleView.swift b/Examples/Examples/Examples/SampleComponentExampleView.swift new file mode 100644 index 000000000..b9f9b6f4e --- /dev/null +++ b/Examples/Examples/Examples/SampleComponentExampleView.swift @@ -0,0 +1,28 @@ +// Copyright 2021 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 +import ArcGISToolkitSwift + +struct SampleComponentExampleView: View { + var body: some View { + SampleComponent(mapView: MapView(map: Map(basemap: Basemap.topographic()))) + } +} + +struct ComponentSampleExampleView_Previews: PreviewProvider { + static var previews: some View { + SampleComponentExampleView() + } +} diff --git a/Examples/ExamplesApp/AnyExample.swift b/Examples/ExamplesApp/AnyExample.swift new file mode 100644 index 000000000..abd58f9f9 --- /dev/null +++ b/Examples/ExamplesApp/AnyExample.swift @@ -0,0 +1,28 @@ +// Copyright 2021 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 + +struct AnyExample { + var name: String + var content: () -> Content + + init(_ name: String, content: @autoclosure @escaping () -> Content) { + self.name = name + self.content = content + } +} + +extension AnyExample: Example { + func makeBody() -> AnyView { AnyView(content()) } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..9221b9bb1 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/Contents.json b/Examples/ExamplesApp/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/ContentView.swift b/Examples/ExamplesApp/ContentView.swift new file mode 100644 index 000000000..75b6509a4 --- /dev/null +++ b/Examples/ExamplesApp/ContentView.swift @@ -0,0 +1,26 @@ +// Copyright 2021 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 + +struct ContentView: View { + var body: some View { + Examples() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Examples/ExamplesApp/Example.swift b/Examples/ExamplesApp/Example.swift new file mode 100644 index 000000000..442b16663 --- /dev/null +++ b/Examples/ExamplesApp/Example.swift @@ -0,0 +1,20 @@ +// Copyright 2021 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 + +protocol Example { + var name: String { get } + + func makeBody() -> AnyView +} diff --git a/Examples/ExamplesApp/ExampleList.swift b/Examples/ExamplesApp/ExampleList.swift new file mode 100644 index 000000000..35ef47066 --- /dev/null +++ b/Examples/ExamplesApp/ExampleList.swift @@ -0,0 +1,30 @@ +// Copyright 2021 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 + +struct ExampleList: View { + var name: String + var examples: [Example] + + var body: some View { + List(examples, id: \.name) { (example) in + NavigationLink(example.name, destination: ExampleView(example: example)) + } + .navigationBarTitle(Text(name), displayMode: .inline) + } +} + +extension ExampleList: Identifiable { + var id: String { name } +} diff --git a/Examples/ExamplesApp/ExampleView.swift b/Examples/ExamplesApp/ExampleView.swift new file mode 100644 index 000000000..734ddf040 --- /dev/null +++ b/Examples/ExamplesApp/ExampleView.swift @@ -0,0 +1,27 @@ +// Copyright 2021 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 + +struct ExampleView: View { + var example: Example + + var body: some View { + example.makeBody() + .navigationTitle(example.name) + } +} + +extension ExampleView: Identifiable { + var id: String { example.name } +} diff --git a/Examples/ExamplesApp/Examples.entitlements b/Examples/ExamplesApp/Examples.entitlements new file mode 100644 index 000000000..fc07546cf --- /dev/null +++ b/Examples/ExamplesApp/Examples.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.personal-information.location + + + diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift new file mode 100644 index 000000000..69331a729 --- /dev/null +++ b/Examples/ExamplesApp/Examples.swift @@ -0,0 +1,39 @@ +// Copyright 2021 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 + +struct Examples: View { + let lists: [ExampleList] = [ + .geoview, + ] + + var body: some View { + NavigationView { + List(lists) { (list) in + NavigationLink(list.name, destination: list) + } + .navigationBarTitle(Text("Examples"), displayMode: .inline) + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} + +extension ExampleList { + static let geoview = Self( + name: "GeoView", + examples: [ + AnyExample("Sample Component", content: SampleComponentExampleView()) + ] + ) +} diff --git a/Examples/ExamplesApp/ExamplesApp.swift b/Examples/ExamplesApp/ExamplesApp.swift new file mode 100644 index 000000000..15df78ef6 --- /dev/null +++ b/Examples/ExamplesApp/ExamplesApp.swift @@ -0,0 +1,23 @@ +// Copyright 2021 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 + +@main +struct ExamplesApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/ExamplesApp/Info.plist b/Examples/ExamplesApp/Info.plist new file mode 100644 index 000000000..efc211a0c --- /dev/null +++ b/Examples/ExamplesApp/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Examples/ExamplesApp/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/ExamplesApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Examples/ExamplesApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift b/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift new file mode 100644 index 000000000..191fd6893 --- /dev/null +++ b/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift @@ -0,0 +1,26 @@ +// Copyright 2021 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 + +public struct SampleComponent: View { + public init(mapView: MapView) { + self.mapView = mapView + } + + var mapView: MapView + public var body: some View { + mapView + } +} diff --git a/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift b/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift index 473596f34..757c5567b 100644 --- a/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift +++ b/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift @@ -1,3 +1,16 @@ +// Copyright 2021 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 @testable import ArcGISToolkitSwift diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index a526b03c5..000000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import arcgis_runtime_toolkit_swiftTests - -var tests = [XCTestCaseEntry]() -tests += arcgis_runtime_toolkit_swiftTests.allTests() -XCTMain(tests) From 88faa88f44df0a8b33443824f215b3c3dfc7d86a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 21 May 2021 16:39:14 -0500 Subject: [PATCH 0004/1722] Remove OverviewMap --- .../Components/OverviewMap.swift | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 Sources/ArcGISToolkitSwift/Components/OverviewMap.swift diff --git a/Sources/ArcGISToolkitSwift/Components/OverviewMap.swift b/Sources/ArcGISToolkitSwift/Components/OverviewMap.swift deleted file mode 100644 index 0afb6b88b..000000000 --- a/Sources/ArcGISToolkitSwift/Components/OverviewMap.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SwiftUIView.swift -// -// -// Created by Mark Dostal on 5/14/21. -// - -import SwiftUI -//import ArcGIS - -struct OverviewMap: View { - var body: some View { -// let mapView: MapView - Text("Hello, Toolkit!") - } -} - -struct SwiftUIView_Previews: PreviewProvider { - static var previews: some View { - OverviewMap() - } -} From c6ba4ce1e3a37cc31838b9575a8a6fe989ea92cd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 24 May 2021 10:28:28 -0500 Subject: [PATCH 0005/1722] Add copyright to Package.swift --- Package.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Package.swift b/Package.swift index dab8a22fd..872dd89e5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,17 @@ // swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. +// Copyright 2021 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 PackageDescription From c0d95d56a1bb58bb88691a1b7da685b5fbf7f857 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:21:12 -0500 Subject: [PATCH 0006/1722] Update Package.swift Co-authored-by: Philip Ridgeway --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 872dd89e5..2e2fab90e 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,8 @@ import PackageDescription let package = Package( name: "arcgis-runtime-toolkit-swift", platforms: [ - .iOS(.v14) + .iOS(.v14), + .macOS(.v11) ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. From bc551a79e896e38600b4387eacf017e09eda186c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:23:45 -0500 Subject: [PATCH 0007/1722] Update Examples/Examples/Examples/SampleComponentExampleView.swift Co-authored-by: Philip Ridgeway --- Examples/Examples/Examples/SampleComponentExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/Examples/SampleComponentExampleView.swift b/Examples/Examples/Examples/SampleComponentExampleView.swift index b9f9b6f4e..89343698f 100644 --- a/Examples/Examples/Examples/SampleComponentExampleView.swift +++ b/Examples/Examples/Examples/SampleComponentExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkitSwift struct SampleComponentExampleView: View { var body: some View { - SampleComponent(mapView: MapView(map: Map(basemap: Basemap.topographic()))) + SampleComponent(mapView: MapView(map: Map(basemap: .topographic()))) } } From c95aeee1aa26bdc9621c3042961137e62b1a8b98 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:24:32 -0500 Subject: [PATCH 0008/1722] Update Sources/ArcGISToolkitSwift/Components/SampleComponent.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkitSwift/Components/SampleComponent.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift b/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift index 191fd6893..216e936aa 100644 --- a/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift +++ b/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift @@ -15,7 +15,7 @@ import SwiftUI import ArcGIS public struct SampleComponent: View { - public init(mapView: MapView) { + public init(mapView: MapView) { self.mapView = mapView } From 3a886c8c785ed331c77f6dc729d9e814e11c9592 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:24:56 -0500 Subject: [PATCH 0009/1722] Update Sources/ArcGISToolkitSwift/Components/SampleComponent.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkitSwift/Components/SampleComponent.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift b/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift index 216e936aa..b2d6bca60 100644 --- a/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift +++ b/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift @@ -20,6 +20,7 @@ public struct SampleComponent: View { } var mapView: MapView + public var body: some View { mapView } From e9504d254f9f7adb1c0c39e39e83d3fa97dd78a3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:36:32 -0500 Subject: [PATCH 0010/1722] Add .gitignore; remove unnecessary file. --- Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift diff --git a/Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift b/Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift deleted file mode 100644 index 5039df0f8..000000000 --- a/Tests/ArcGISToolkitSwiftTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(arcgis_runtime_toolkit_swiftTests.allTests), - ] -} -#endif From f1fdb76d2004f8859d4b0663c9999c6fb41b90f1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:37:27 -0500 Subject: [PATCH 0011/1722] add .gitignore for real. --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..327e670a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata From 2c61fcb8a45224fe04d5cf3039725978c035f546 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:41:45 -0500 Subject: [PATCH 0012/1722] Remove unnecessary descriptions. --- Package.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Package.swift b/Package.swift index 2e2fab90e..d47b277aa 100644 --- a/Package.swift +++ b/Package.swift @@ -22,20 +22,15 @@ let package = Package( .macOS(.v11) ], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "ArcGISToolkitSwift", targets: ["ArcGISToolkitSwift"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), .package(name: "ArcGIS", path: "../swift") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "ArcGISToolkitSwift", dependencies: ["ArcGIS"]), From 787f3aefbcf7b97ed3e2683e61d0ca0ed46db5ea Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 13:43:35 -0500 Subject: [PATCH 0013/1722] Remove unused Test target and files. --- Package.swift | 3 --- .../arcgis_runtime_toolkit_swiftTests.swift | 27 ------------------- 2 files changed, 30 deletions(-) delete mode 100644 Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift diff --git a/Package.swift b/Package.swift index d47b277aa..6cb856f17 100644 --- a/Package.swift +++ b/Package.swift @@ -34,8 +34,5 @@ let package = Package( .target( name: "ArcGISToolkitSwift", dependencies: ["ArcGIS"]), - .testTarget( - name: "ArcGISToolkitSwiftTests", - dependencies: ["ArcGISToolkitSwift"]), ] ) diff --git a/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift b/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift deleted file mode 100644 index 757c5567b..000000000 --- a/Tests/ArcGISToolkitSwiftTests/arcgis_runtime_toolkit_swiftTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 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 -@testable import ArcGISToolkitSwift - -final class arcgis_runtime_toolkit_swiftTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - } - - static var allTests = [ - ("testExample", testExample), - ] -} From 3b897d9992ba4f9e22b0aff57c12a6bc8d7ff2c0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 14:51:49 -0500 Subject: [PATCH 0014/1722] update .gitignore, remove bloated code --- .gitignore | 1 + Examples/Examples.xcodeproj/project.pbxproj | 4 ---- Examples/ExamplesApp/ContentView.swift | 26 --------------------- Examples/ExamplesApp/ExamplesApp.swift | 2 +- 4 files changed, 2 insertions(+), 31 deletions(-) delete mode 100644 Examples/ExamplesApp/ContentView.swift diff --git a/.gitignore b/.gitignore index 327e670a8..f23d8f015 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ /.build /Packages xcuserdata/ +/*.xcodeproj DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 301440fdb..d4ef4d622 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ E40F58082656E695006F5CB9 /* ArcGISToolkitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E40F58072656E695006F5CB9 /* ArcGISToolkitSwift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; - E47ABE462652FE0900FD2FE3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE452652FE0900FD2FE3 /* ContentView.swift */; }; E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */; }; E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; }; @@ -39,7 +38,6 @@ /* Begin PBXFileReference section */ E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; - E47ABE452652FE0900FD2FE3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; E47ABE4C2652FE0C00FD2FE3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -98,7 +96,6 @@ children = ( E48A73412658227100F5C118 /* AnyExample.swift */, E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */, - E47ABE452652FE0900FD2FE3 /* ContentView.swift */, E48A73402658227100F5C118 /* Example.swift */, E48A733E2658227100F5C118 /* ExampleList.swift */, E48A733F2658227100F5C118 /* Examples.entitlements */, @@ -208,7 +205,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E47ABE462652FE0900FD2FE3 /* ContentView.swift in Sources */, E48A73452658227100F5C118 /* Examples.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, diff --git a/Examples/ExamplesApp/ContentView.swift b/Examples/ExamplesApp/ContentView.swift deleted file mode 100644 index 75b6509a4..000000000 --- a/Examples/ExamplesApp/ContentView.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 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 - -struct ContentView: View { - var body: some View { - Examples() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/Examples/ExamplesApp/ExamplesApp.swift b/Examples/ExamplesApp/ExamplesApp.swift index 15df78ef6..77e5f8b88 100644 --- a/Examples/ExamplesApp/ExamplesApp.swift +++ b/Examples/ExamplesApp/ExamplesApp.swift @@ -17,7 +17,7 @@ import SwiftUI struct ExamplesApp: App { var body: some Scene { WindowGroup { - ContentView() + Examples() } } } From 7a0fdefac14e9b7c5bf6417c015f6877901b9d8c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 14:58:20 -0500 Subject: [PATCH 0015/1722] Add declaration descriptions. --- Examples/ExamplesApp/Example.swift | 2 ++ Examples/ExamplesApp/ExampleList.swift | 3 +++ Examples/ExamplesApp/ExampleView.swift | 1 + Examples/ExamplesApp/Examples.swift | 1 + 4 files changed, 7 insertions(+) diff --git a/Examples/ExamplesApp/Example.swift b/Examples/ExamplesApp/Example.swift index 442b16663..d484aff79 100644 --- a/Examples/ExamplesApp/Example.swift +++ b/Examples/ExamplesApp/Example.swift @@ -14,7 +14,9 @@ import SwiftUI protocol Example { + /// The name of the example. var name: String { get } + /// A function which creates the example view. func makeBody() -> AnyView } diff --git a/Examples/ExamplesApp/ExampleList.swift b/Examples/ExamplesApp/ExampleList.swift index 35ef47066..8b628639b 100644 --- a/Examples/ExamplesApp/ExampleList.swift +++ b/Examples/ExamplesApp/ExampleList.swift @@ -14,7 +14,10 @@ import SwiftUI struct ExampleList: View { + /// The name of the list of examples. var name: String + + /// The list of examples to display. var examples: [Example] var body: some View { diff --git a/Examples/ExamplesApp/ExampleView.swift b/Examples/ExamplesApp/ExampleView.swift index 734ddf040..c5fb89478 100644 --- a/Examples/ExamplesApp/ExampleView.swift +++ b/Examples/ExamplesApp/ExampleView.swift @@ -14,6 +14,7 @@ import SwiftUI struct ExampleView: View { + /// The example to display in the view. var example: Example var body: some View { diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 69331a729..0af4e8035 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -14,6 +14,7 @@ import SwiftUI struct Examples: View { + /// The list of example lists. Allows for a hierarchical navigation model for examples. let lists: [ExampleList] = [ .geoview, ] From 48ec3ad71dc9e03f108923e9030b58d3c9e00911 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 May 2021 15:03:13 -0500 Subject: [PATCH 0016/1722] Rename library to `ArcGISToolkit`. --- Examples/Examples.xcodeproj/project.pbxproj | 14 +++++++------- .../Examples/SampleComponentExampleView.swift | 2 +- Package.swift | 6 +++--- .../Components/SampleComponent.swift | 0 4 files changed, 11 insertions(+), 11 deletions(-) rename Sources/{ArcGISToolkitSwift => ArcGISToolkit}/Components/SampleComponent.swift (100%) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index d4ef4d622..5a5288517 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - E40F58082656E695006F5CB9 /* ArcGISToolkitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E40F58072656E695006F5CB9 /* ArcGISToolkitSwift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */; }; E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; @@ -19,6 +18,7 @@ E48A73462658227100F5C118 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73422658227100F5C118 /* ExampleView.swift */; }; E48A73472658227100F5C118 /* ExampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733E2658227100F5C118 /* ExampleList.swift */; }; E4D9C33E26585AC4000AAAE8 /* SampleComponentExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D9C33D26585AC4000AAAE8 /* SampleComponentExampleView.swift */; }; + E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E4E57DC5265D8EB00077A093 /* ArcGISToolkit */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -56,7 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E40F58082656E695006F5CB9 /* ArcGISToolkitSwift in Frameworks */, + E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */, E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -150,7 +150,7 @@ name = Examples; packageProductDependencies = ( E47ABE5B2652FF1200FD2FE3 /* ArcGIS */, - E40F58072656E695006F5CB9 /* ArcGISToolkitSwift */, + E4E57DC5265D8EB00077A093 /* ArcGISToolkit */, ); productName = Examples; productReference = E47ABE402652FE0900FD2FE3 /* Examples.app */; @@ -406,14 +406,14 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - E40F58072656E695006F5CB9 /* ArcGISToolkitSwift */ = { - isa = XCSwiftPackageProductDependency; - productName = ArcGISToolkitSwift; - }; E47ABE5B2652FF1200FD2FE3 /* ArcGIS */ = { isa = XCSwiftPackageProductDependency; productName = ArcGIS; }; + E4E57DC5265D8EB00077A093 /* ArcGISToolkit */ = { + isa = XCSwiftPackageProductDependency; + productName = ArcGISToolkit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = E47ABE382652FE0900FD2FE3 /* Project object */; diff --git a/Examples/Examples/Examples/SampleComponentExampleView.swift b/Examples/Examples/Examples/SampleComponentExampleView.swift index 89343698f..96a630f61 100644 --- a/Examples/Examples/Examples/SampleComponentExampleView.swift +++ b/Examples/Examples/Examples/SampleComponentExampleView.swift @@ -13,7 +13,7 @@ import SwiftUI import ArcGIS -import ArcGISToolkitSwift +import ArcGISToolkit struct SampleComponentExampleView: View { var body: some View { diff --git a/Package.swift b/Package.swift index 6cb856f17..937146a1c 100644 --- a/Package.swift +++ b/Package.swift @@ -23,8 +23,8 @@ let package = Package( ], products: [ .library( - name: "ArcGISToolkitSwift", - targets: ["ArcGISToolkitSwift"]), + name: "ArcGISToolkit", + targets: ["ArcGISToolkit"]), ], dependencies: [ .package(name: "ArcGIS", path: "../swift") @@ -32,7 +32,7 @@ let package = Package( ], targets: [ .target( - name: "ArcGISToolkitSwift", + name: "ArcGISToolkit", dependencies: ["ArcGIS"]), ] ) diff --git a/Sources/ArcGISToolkitSwift/Components/SampleComponent.swift b/Sources/ArcGISToolkit/Components/SampleComponent.swift similarity index 100% rename from Sources/ArcGISToolkitSwift/Components/SampleComponent.swift rename to Sources/ArcGISToolkit/Components/SampleComponent.swift From ec854edcaca2dcf1b4d1ec496ee74082fb19d7f6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 2 Jun 2021 14:55:31 -0500 Subject: [PATCH 0017/1722] Initial starting implementation of OverviewMap. --- Examples/Examples.xcodeproj/project.pbxproj | 12 +- ...iew.swift => OverviewMapExampleView.swift} | 20 ++- Examples/ExamplesApp/Examples.swift | 2 +- Examples/ExamplesApp/ExamplesApp.swift | 2 +- .../Components/OverviewMap.swift | 147 ++++++++++++++++++ .../Components/SampleComponent.swift | 27 ---- 6 files changed, 170 insertions(+), 40 deletions(-) rename Examples/Examples/Examples/{SampleComponentExampleView.swift => OverviewMapExampleView.swift} (56%) create mode 100644 Sources/ArcGISToolkit/Components/OverviewMap.swift delete mode 100644 Sources/ArcGISToolkit/Components/SampleComponent.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 5a5288517..b5f5c472b 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -17,8 +17,8 @@ E48A73452658227100F5C118 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733D2658227000F5C118 /* Examples.swift */; }; E48A73462658227100F5C118 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73422658227100F5C118 /* ExampleView.swift */; }; E48A73472658227100F5C118 /* ExampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733E2658227100F5C118 /* ExampleList.swift */; }; - E4D9C33E26585AC4000AAAE8 /* SampleComponentExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D9C33D26585AC4000AAAE8 /* SampleComponentExampleView.swift */; }; E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E4E57DC5265D8EB00077A093 /* ArcGISToolkit */; }; + E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,7 +48,7 @@ E48A73402658227100F5C118 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; E48A73412658227100F5C118 /* AnyExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyExample.swift; sourceTree = ""; }; E48A73422658227100F5C118 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; - E4D9C33D26585AC4000AAAE8 /* SampleComponentExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleComponentExampleView.swift; sourceTree = ""; }; + E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,7 +67,7 @@ E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( - E4D9C33D26585AC4000AAAE8 /* SampleComponentExampleView.swift */, + E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, ); path = Examples; sourceTree = ""; @@ -208,7 +208,7 @@ E48A73452658227100F5C118 /* Examples.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, - E4D9C33E26585AC4000AAAE8 /* SampleComponentExampleView.swift in Sources */, + E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, E48A73442658227100F5C118 /* AnyExample.swift in Sources */, E48A73472658227100F5C118 /* ExampleList.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, @@ -339,7 +339,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; + CODE_SIGN_ENTITLEMENTS = ExamplesApp/Examples.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"ExamplesApp/Preview Content\""; DEVELOPMENT_TEAM = P8HGHS7JQ8; @@ -363,7 +363,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; + CODE_SIGN_ENTITLEMENTS = ExamplesApp/Examples.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"ExamplesApp/Preview Content\""; DEVELOPMENT_TEAM = P8HGHS7JQ8; diff --git a/Examples/Examples/Examples/SampleComponentExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift similarity index 56% rename from Examples/Examples/Examples/SampleComponentExampleView.swift rename to Examples/Examples/Examples/OverviewMapExampleView.swift index 96a630f61..6344d0fff 100644 --- a/Examples/Examples/Examples/SampleComponentExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Esri. +// Copyright 2021 Esri./APsPl0cx9SjC7zrU5rCZKCw // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,17 +12,27 @@ // limitations under the License. import SwiftUI +import Combine import ArcGIS import ArcGISToolkit -struct SampleComponentExampleView: View { +struct OverviewMapExampleView: View { + private var mapView = MapView(map: Map(basemap: Basemap.imageryWithLabels())) + var body: some View { - SampleComponent(mapView: MapView(map: Map(basemap: .topographic()))) + ZStack (alignment: .topTrailing) { + mapView +// .onViewpointChanged { viewpoint in +// <#code#> +// } + OverviewMap(geoView: mapView) + .padding() + } } } -struct ComponentSampleExampleView_Previews: PreviewProvider { +struct OverviewMapExampleView_Previews: PreviewProvider { static var previews: some View { - SampleComponentExampleView() + OverviewMapExampleView() } } diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 0af4e8035..db8ea7560 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -34,7 +34,7 @@ extension ExampleList { static let geoview = Self( name: "GeoView", examples: [ - AnyExample("Sample Component", content: SampleComponentExampleView()) + AnyExample("Overview Map", content: OverviewMapExampleView()) ] ) } diff --git a/Examples/ExamplesApp/ExamplesApp.swift b/Examples/ExamplesApp/ExamplesApp.swift index 77e5f8b88..038eb6f67 100644 --- a/Examples/ExamplesApp/ExamplesApp.swift +++ b/Examples/ExamplesApp/ExamplesApp.swift @@ -17,7 +17,7 @@ import SwiftUI struct ExamplesApp: App { var body: some Scene { WindowGroup { - Examples() + OverviewMapExampleView() } } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift new file mode 100644 index 000000000..4c8c8cb5c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -0,0 +1,147 @@ +// +// SwiftUIView.swift +// +// Copyright 2021 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 Combine +import ArcGIS + +public struct OverviewMap: View { + public var geoView: MapView + public var map = Map(basemap: Basemap.topographic()) + public var width: CGFloat = 200.0 + public var height: CGFloat = 132.0 + public var extentSymbol = SimpleFillSymbol(style: .solid, + color: .clear, + outline: SimpleLineSymbol(style: .solid, + color: .red, + width: 1.0) + ) + + @State private var extentGeometry = Envelope(center: Point(x: -9813416.487598, y: 5126112.596989), width: 5000000, height: 10000000) + + @State private var overviewViewpoint: Viewpoint = Viewpoint(latitude: 0.0, longitude: 0.0, scale: 100000000000) + + private var subscriptions = Set() + + private var scale = 10000000.0 + + public init(geoView: MapView, + map: Map = Map(basemap: Basemap.topographic()), + width: CGFloat = 200.0, + height: CGFloat = 132.0, + extentSymbol: SimpleFillSymbol = SimpleFillSymbol(style: .solid, + color: .clear, + outline: SimpleLineSymbol(style: .solid, + color: .red, + width: 1.0)) + ) { + self.geoView = geoView + self.map = map + self.width = width + self.height = height + self.extentSymbol = extentSymbol + + + // Ugh. + // Figure out how to pass in Viewpoint binding to OverviewMap and then do the `onchange` of that. + // figure out how to set an initial viewpoint on the MapView, although maybe "Binding" is not the correct + // way to go, since the MapView's viewpoint binding is nil + // + // Figure out how to get the publisher stuff working given the fact that we're already hooked + // into the coreGeoView.viewpointChanged stuff; it's just propagating that out to the client. + // The issue is that you can't use "subscription", so need a SwiftUI way to do it... + + +// geoView.onChange(of: geoView.viewpoint?.wrappedValue, perform: { (viewpoint) in +// print("viewpoint changed") +// }) + + geoView.viewpointChangedPublisher.sink(receiveValue: { +// viewpointDidChange() + print("wow") + }) + .store(in: &subscriptions) + } + + // Need to watch for changes to geoView.viewpoint + // When geoView.viewpoint changes, update overviewViewpoint + public var body: some View { + ZStack { + makeMapView() + .attributionTextHidden() + .interactionModes([]) + .frame(width: width, height: height) + .border(Color.black, width: 1) + Button("Zoom") { + extentGeometry = Envelope(center: Point(x: -9813416.487598, y: 5126112.596989), width: 2000000, height: 10000000) + } + } + } + + private func makeMapView() -> MapView { + let extentGraphic = Graphic(geometry: extentGeometry, symbol: extentSymbol) + return MapView(map: map, + viewpoint: $overviewViewpoint, + graphicsOverlays: [GraphicsOverlay(graphics: [extentGraphic])] + ) + } + + //Ideally component would take a binding to a viewpoint, use the onchange thing to be notified when it changes. + + + // OverviewMap needs to: + // - be notified when a geoview's viewpoint changes + // - be able to get a viewpoint from a geoview (both .centerAndScale and .boundingGeometry) + // - be able to set a viewpoint on a geoview + + // BasemapGallery needs to: + // - load a Portal's basemaps + // - get and display a thumbnail from a basemap's "item" property + // - set a new basemap on a map/scene contained in a geoview + + // Search needs to: + // - search using a LocatorTask + // - support both online and local (MMPK) locators + // - show suggestions from a LocatorTask + // - dynamically update list of suggestions as the user types + // - zoom a map/scene view to a given location and scale + // - zoom a map/scene view to a given extent + // - add one or more graphics to a map/scene view + + // TODO: Look at phil's viewpoint issue and figure out: + // - where to put get/set_viewpoint methods + // - where to put viewpointChanged publisher (and how) + + // Need... + // - to be notified when a geoview's viewpoint changes + // - to be able to get a viewpoint from a geoview (either .centerAndScale or .boundingGeometry) + // - to be able to set a viewpoint on a geoview + + // When geoView.viewpoint changes... + func viewpointDidChange() { + let centerAndScaleViewpoint = geoView.currentViewpoint(type: .centerAndScale) + let boundingGeometryViewpoint = geoView.currentViewpoint(type: .boundingGeometry) + + if let newExtent = boundingGeometryViewpoint?.targetGeometry as? Envelope { + extentGeometry = newExtent + } + + if let centerAndScaleViewpoint = centerAndScaleViewpoint { + overviewViewpoint = Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, + scale: centerAndScaleViewpoint.targetScale * scale) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/SampleComponent.swift b/Sources/ArcGISToolkit/Components/SampleComponent.swift deleted file mode 100644 index b2d6bca60..000000000 --- a/Sources/ArcGISToolkit/Components/SampleComponent.swift +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 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 - -public struct SampleComponent: View { - public init(mapView: MapView) { - self.mapView = mapView - } - - var mapView: MapView - - public var body: some View { - mapView - } -} From de30f73b6b072a0c099b36286d621bdffd1ced6b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 2 Jun 2021 15:36:48 -0500 Subject: [PATCH 0018/1722] add generated file --- .../project.xcworkspace/contents.xcworkspacedata | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + From 4de1799a4a5d22a5cd9f5e2d165037761e2c465f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 2 Jun 2021 15:37:57 -0500 Subject: [PATCH 0019/1722] Update Package.swift Co-authored-by: Zachary A Kline --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index 937146a1c..e5eaac6c8 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,6 @@ let package = Package( ], dependencies: [ .package(name: "ArcGIS", path: "../swift") - ], targets: [ .target( From 5c840343228a690cf54a79a364b75ba82728a7a9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 2 Jun 2021 15:38:13 -0500 Subject: [PATCH 0020/1722] Update Examples/ExamplesApp/Examples.swift Co-authored-by: Zachary A Kline --- Examples/ExamplesApp/Examples.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 0af4e8035..836f9c122 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -16,7 +16,7 @@ import SwiftUI struct Examples: View { /// The list of example lists. Allows for a hierarchical navigation model for examples. let lists: [ExampleList] = [ - .geoview, + .geoview ] var body: some View { From cc9c83552fc8263be2a2face1dc2cb9e067a7f5a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 2 Jun 2021 15:49:29 -0500 Subject: [PATCH 0021/1722] add autogenerated scheme stuff. --- .../arcgis-runtime-toolkit-swift.xcscheme | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme index 641a84e29..f1bfb0c72 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme @@ -48,6 +48,20 @@ ReferencedContainer = "container:"> + + + + Date: Wed, 2 Jun 2021 15:59:04 -0500 Subject: [PATCH 0022/1722] Fix location of Entitlements file. --- Examples/Examples.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 5a5288517..d8f4088e5 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -339,7 +339,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; + CODE_SIGN_ENTITLEMENTS = ExamplesApp/Examples.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"ExamplesApp/Preview Content\""; DEVELOPMENT_TEAM = P8HGHS7JQ8; @@ -363,7 +363,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Examples/Examples.entitlements; + CODE_SIGN_ENTITLEMENTS = ExamplesApp/Examples.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"ExamplesApp/Preview Content\""; DEVELOPMENT_TEAM = P8HGHS7JQ8; From 7819995a353a0a5c62915e7826837122d17f06a4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 4 Jun 2021 16:37:02 -0500 Subject: [PATCH 0023/1722] Use proxy and new GeoViewProxy methods. --- .../Examples/OverviewMapExampleView.swift | 12 +- Examples/ExamplesApp/ExamplesApp.swift | 2 +- .../Components/OverviewMap.swift | 110 +++++------------- 3 files changed, 33 insertions(+), 91 deletions(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index 6344d0fff..601b21abd 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -17,15 +17,13 @@ import ArcGIS import ArcGISToolkit struct OverviewMapExampleView: View { - private var mapView = MapView(map: Map(basemap: Basemap.imageryWithLabels())) - + private var mapViewProxy: Binding? + var body: some View { ZStack (alignment: .topTrailing) { - mapView -// .onViewpointChanged { viewpoint in -// <#code#> -// } - OverviewMap(geoView: mapView) + MapView(map: Map(basemap: Basemap.imageryWithLabels()), + proxy: mapViewProxy) + OverviewMap(proxy: mapViewProxy) .padding() } } diff --git a/Examples/ExamplesApp/ExamplesApp.swift b/Examples/ExamplesApp/ExamplesApp.swift index 038eb6f67..77e5f8b88 100644 --- a/Examples/ExamplesApp/ExamplesApp.swift +++ b/Examples/ExamplesApp/ExamplesApp.swift @@ -17,7 +17,7 @@ import SwiftUI struct ExamplesApp: App { var body: some Scene { WindowGroup { - OverviewMapExampleView() + Examples() } } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 4c8c8cb5c..8b61d6a10 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -19,7 +19,7 @@ import Combine import ArcGIS public struct OverviewMap: View { - public var geoView: MapView + public var proxy: Binding? public var map = Map(basemap: Basemap.topographic()) public var width: CGFloat = 200.0 public var height: CGFloat = 132.0 @@ -30,15 +30,17 @@ public struct OverviewMap: View { width: 1.0) ) - @State private var extentGeometry = Envelope(center: Point(x: -9813416.487598, y: 5126112.596989), width: 5000000, height: 10000000) + public var scaleFactor = 25.0 - @State private var overviewViewpoint: Viewpoint = Viewpoint(latitude: 0.0, longitude: 0.0, scale: 100000000000) + /// The geometry of the extent Graphic displaying the main map view's extent. + @State private var extentGeometry: Envelope? + /// The proxy for the overviewMap's map view. + private var overviewMapViewProxy: Binding? + private var subscriptions = Set() - private var scale = 10000000.0 - - public init(geoView: MapView, + public init(proxy: Binding?, map: Map = Map(basemap: Basemap.topographic()), width: CGFloat = 200.0, height: CGFloat = 132.0, @@ -48,100 +50,42 @@ public struct OverviewMap: View { color: .red, width: 1.0)) ) { - self.geoView = geoView + self.proxy = proxy self.map = map self.width = width self.height = height self.extentSymbol = extentSymbol - - // Ugh. - // Figure out how to pass in Viewpoint binding to OverviewMap and then do the `onchange` of that. - // figure out how to set an initial viewpoint on the MapView, although maybe "Binding" is not the correct - // way to go, since the MapView's viewpoint binding is nil - // - // Figure out how to get the publisher stuff working given the fact that we're already hooked - // into the coreGeoView.viewpointChanged stuff; it's just propagating that out to the client. - // The issue is that you can't use "subscription", so need a SwiftUI way to do it... - - -// geoView.onChange(of: geoView.viewpoint?.wrappedValue, perform: { (viewpoint) in -// print("viewpoint changed") -// }) - - geoView.viewpointChangedPublisher.sink(receiveValue: { -// viewpointDidChange() - print("wow") + self.proxy?.wrappedValue?.viewpointChangedPublisher.sink(receiveValue: { + guard let centerAndScaleViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .centerAndScale), + let boundingGeometryViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .boundingGeometry) + else { return } + + if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { + extentGeometry = newExtent + } + + (overviewMapViewProxy?.wrappedValue)?.setViewpoint(viewpoint: Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, + scale: centerAndScaleViewpoint.targetScale * scaleFactor)) }) .store(in: &subscriptions) } - // Need to watch for changes to geoView.viewpoint - // When geoView.viewpoint changes, update overviewViewpoint public var body: some View { ZStack { - makeMapView() - .attributionTextHidden() - .interactionModes([]) - .frame(width: width, height: height) - .border(Color.black, width: 1) - Button("Zoom") { - extentGeometry = Envelope(center: Point(x: -9813416.487598, y: 5126112.596989), width: 2000000, height: 10000000) - } + makeMapView() + .attributionTextHidden() + .interactionModes([]) + .frame(width: width, height: height) + .border(Color.black, width: 1) } } private func makeMapView() -> MapView { let extentGraphic = Graphic(geometry: extentGeometry, symbol: extentSymbol) return MapView(map: map, - viewpoint: $overviewViewpoint, - graphicsOverlays: [GraphicsOverlay(graphics: [extentGraphic])] + graphicsOverlays: [GraphicsOverlay(graphics: [extentGraphic])], + proxy: overviewMapViewProxy ) } - - //Ideally component would take a binding to a viewpoint, use the onchange thing to be notified when it changes. - - - // OverviewMap needs to: - // - be notified when a geoview's viewpoint changes - // - be able to get a viewpoint from a geoview (both .centerAndScale and .boundingGeometry) - // - be able to set a viewpoint on a geoview - - // BasemapGallery needs to: - // - load a Portal's basemaps - // - get and display a thumbnail from a basemap's "item" property - // - set a new basemap on a map/scene contained in a geoview - - // Search needs to: - // - search using a LocatorTask - // - support both online and local (MMPK) locators - // - show suggestions from a LocatorTask - // - dynamically update list of suggestions as the user types - // - zoom a map/scene view to a given location and scale - // - zoom a map/scene view to a given extent - // - add one or more graphics to a map/scene view - - // TODO: Look at phil's viewpoint issue and figure out: - // - where to put get/set_viewpoint methods - // - where to put viewpointChanged publisher (and how) - - // Need... - // - to be notified when a geoview's viewpoint changes - // - to be able to get a viewpoint from a geoview (either .centerAndScale or .boundingGeometry) - // - to be able to set a viewpoint on a geoview - - // When geoView.viewpoint changes... - func viewpointDidChange() { - let centerAndScaleViewpoint = geoView.currentViewpoint(type: .centerAndScale) - let boundingGeometryViewpoint = geoView.currentViewpoint(type: .boundingGeometry) - - if let newExtent = boundingGeometryViewpoint?.targetGeometry as? Envelope { - extentGeometry = newExtent - } - - if let centerAndScaleViewpoint = centerAndScaleViewpoint { - overviewViewpoint = Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, - scale: centerAndScaleViewpoint.targetScale * scale) - } - } } From 32f106e3c3d7505f9d79f2b2fdbca59fe181998b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 4 Jun 2021 16:50:05 -0500 Subject: [PATCH 0024/1722] Update proxy stuff --- .../Components/OverviewMap.swift | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 8b61d6a10..6cce4bd2a 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -36,7 +36,8 @@ public struct OverviewMap: View { @State private var extentGeometry: Envelope? /// The proxy for the overviewMap's map view. - private var overviewMapViewProxy: Binding? + @State private var overviewMapViewProxy: MapViewProxy? +// private var overviewMapViewProxy: Binding? private var subscriptions = Set() @@ -56,36 +57,50 @@ public struct OverviewMap: View { self.height = height self.extentSymbol = extentSymbol - self.proxy?.wrappedValue?.viewpointChangedPublisher.sink(receiveValue: { - guard let centerAndScaleViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .centerAndScale), - let boundingGeometryViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .boundingGeometry) - else { return } - - if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { - extentGeometry = newExtent - } - - (overviewMapViewProxy?.wrappedValue)?.setViewpoint(viewpoint: Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, - scale: centerAndScaleViewpoint.targetScale * scaleFactor)) - }) - .store(in: &subscriptions) + // self.proxy?.wrappedValue?.viewpointChangedPublisher.sink(receiveValue: { + // guard let centerAndScaleViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .centerAndScale), + // let boundingGeometryViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .boundingGeometry) + // else { return } + // + // if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { + // extentGeometry = newExtent + // } + // + // (overviewMapViewProxy?.wrappedValue)?.setViewpoint(viewpoint: Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, + // scale: centerAndScaleViewpoint.targetScale * scaleFactor)) + // }) + // .store(in: &subscriptions) } public var body: some View { ZStack { - makeMapView() + MapView(map: map, + graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, + symbol: extentSymbol)])], + proxy: $overviewMapViewProxy) .attributionTextHidden() .interactionModes([]) .frame(width: width, height: height) .border(Color.black, width: 1) + .onReceive((proxy?.wrappedValue!.viewpointChangedPublisher)!) { + guard let centerAndScaleViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .centerAndScale), + let boundingGeometryViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .boundingGeometry) + else { return } + + if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { + extentGeometry = newExtent + } + let viewpoint = Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, + scale: centerAndScaleViewpoint.targetScale * scaleFactor) + overviewMapViewProxy?.setViewpoint(viewpoint: viewpoint) + } } } - private func makeMapView() -> MapView { - let extentGraphic = Graphic(geometry: extentGeometry, symbol: extentSymbol) - return MapView(map: map, - graphicsOverlays: [GraphicsOverlay(graphics: [extentGraphic])], - proxy: overviewMapViewProxy - ) - } +// private func makeMapView() -> MapView { +// return MapView(map: map, +// graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, symbol: extentSymbol)])], +// proxy: overviewMapViewProxy +// ) +// } } From 2801278d581fd1d0de1c5ebc44b4ad5bec8ca225 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 9 Jun 2021 14:10:12 -0500 Subject: [PATCH 0025/1722] State and Binding changes for the MapViewProxy and Viewpoint --- .../Examples/OverviewMapExampleView.swift | 8 +- .../Components/OverviewMap.swift | 125 ++++++++++-------- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index 601b21abd..9514d2903 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -17,13 +17,15 @@ import ArcGIS import ArcGISToolkit struct OverviewMapExampleView: View { - private var mapViewProxy: Binding? + @State private var mapViewProxy: MapViewProxy? var body: some View { ZStack (alignment: .topTrailing) { MapView(map: Map(basemap: Basemap.imageryWithLabels()), - proxy: mapViewProxy) - OverviewMap(proxy: mapViewProxy) + proxy: $mapViewProxy + ) + OverviewMap(proxy: $mapViewProxy) + .frame(width: 200, height: 132) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 6cce4bd2a..43376b707 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -18,89 +18,98 @@ import SwiftUI import Combine import ArcGIS +/// <#Description#> public struct OverviewMap: View { - public var proxy: Binding? - public var map = Map(basemap: Basemap.topographic()) - public var width: CGFloat = 200.0 - public var height: CGFloat = 132.0 - public var extentSymbol = SimpleFillSymbol(style: .solid, - color: .clear, - outline: SimpleLineSymbol(style: .solid, - color: .red, - width: 1.0) - ) + /// <#Description#> + public var proxy: Binding + + /// <#Description#> + public var map: Map + + /// <#Description#> + public var extentSymbol: SimpleFillSymbol - public var scaleFactor = 25.0 + /// <#Description#> + public var scaleFactor: Double /// The geometry of the extent Graphic displaying the main map view's extent. @State private var extentGeometry: Envelope? - - /// The proxy for the overviewMap's map view. - @State private var overviewMapViewProxy: MapViewProxy? -// private var overviewMapViewProxy: Binding? - private var subscriptions = Set() + @State private var overviewMapViewpoint: Viewpoint? - public init(proxy: Binding?, + /// <#Description#> + /// - Parameters: + /// - proxy: <#proxy description#> + /// - map: <#map description#> + /// - extentSymbol: <#extentSymbol description#> + public init(proxy: Binding, map: Map = Map(basemap: Basemap.topographic()), - width: CGFloat = 200.0, - height: CGFloat = 132.0, - extentSymbol: SimpleFillSymbol = SimpleFillSymbol(style: .solid, - color: .clear, - outline: SimpleLineSymbol(style: .solid, - color: .red, - width: 1.0)) + extentSymbol: SimpleFillSymbol = SimpleFillSymbol( + style: .solid, + color: .clear, + outline: SimpleLineSymbol( + style: .solid, + color: .red, + width: 1.0 + ) + ), + scaleFactor: Double = 25.0 ) { self.proxy = proxy self.map = map - self.width = width - self.height = height self.extentSymbol = extentSymbol - - // self.proxy?.wrappedValue?.viewpointChangedPublisher.sink(receiveValue: { - // guard let centerAndScaleViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .centerAndScale), - // let boundingGeometryViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .boundingGeometry) - // else { return } - // - // if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { - // extentGeometry = newExtent - // } - // - // (overviewMapViewProxy?.wrappedValue)?.setViewpoint(viewpoint: Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, - // scale: centerAndScaleViewpoint.targetScale * scaleFactor)) - // }) - // .store(in: &subscriptions) + self.scaleFactor = scaleFactor } - + public var body: some View { ZStack { MapView(map: map, + viewpoint: $overviewMapViewpoint, graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, - symbol: extentSymbol)])], - proxy: $overviewMapViewProxy) + symbol: extentSymbol)])] + ) .attributionTextHidden() .interactionModes([]) - .frame(width: width, height: height) .border(Color.black, width: 1) - .onReceive((proxy?.wrappedValue!.viewpointChangedPublisher)!) { - guard let centerAndScaleViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .centerAndScale), - let boundingGeometryViewpoint = (proxy?.wrappedValue)?.currentViewpoint(type: .boundingGeometry) + .onReceive(proxy.wrappedValue?.viewpointChangedPublisher + .receive(on: DispatchQueue.main) + // + // I think "throttle" is what we need here because that does + // not reset the timer when a new value is published, whereas + // debounce will reset the timer, so if there are multiple + // values published that arriver faster than the timeout, + // none of them will be sent because the timer gets reset. + // + // That said, neither of these solve the issue of the + // extent rectangle not getting drawn while the user is + // panning the map. The extent rectangle disappears when the user + // starts panning and will not reappear until the panning stops. + // +// .debounce(for: .seconds(0.25), +// scheduler: DispatchQueue.main) +// .throttle(for: .seconds(0.25), +// scheduler: DispatchQueue.main, +// latest: true +// ) + .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + ) { + guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), + centerAndScaleViewpoint.objectType != .unknown, + let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry), + boundingGeometryViewpoint.objectType != .unknown else { return } - + if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { extentGeometry = newExtent } - let viewpoint = Viewpoint(center: centerAndScaleViewpoint.targetGeometry as! Point, - scale: centerAndScaleViewpoint.targetScale * scaleFactor) - overviewMapViewProxy?.setViewpoint(viewpoint: viewpoint) + + if let viewpointGeometry = centerAndScaleViewpoint.targetGeometry as? Point { + let viewpoint = Viewpoint(center: viewpointGeometry, + scale: centerAndScaleViewpoint.targetScale * scaleFactor) + print("overviewMapViewpoint updated") + overviewMapViewpoint = viewpoint + } } } } - -// private func makeMapView() -> MapView { -// return MapView(map: map, -// graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, symbol: extentSymbol)])], -// proxy: overviewMapViewProxy -// ) -// } } From 8adb2a2e6199e0d6f17f16809e69b530c1ab45c0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Jun 2021 11:33:51 -0500 Subject: [PATCH 0026/1722] Documentation and cleanup. --- .../Components/OverviewMap.swift | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 43376b707..ca10e7e0d 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -18,33 +18,41 @@ import SwiftUI import Combine import ArcGIS -/// <#Description#> +/// OverviewMap is a small, secondary MapView (sometimes called an "inset map"), superimposed on an existing MapView, which shows the visible extent of the main MapView. public struct OverviewMap: View { - /// <#Description#> + /// A binding to an optional `MapViewProxy`. When a proxy is + /// available, the binding will be updated by the view. The proxy is + /// necessary for accessing `MapView` functionality to get and set viewpoints. public var proxy: Binding - /// <#Description#> + /// The Map displayed in the OverviewMap. public var map: Map - /// <#Description#> - public var extentSymbol: SimpleFillSymbol + /// The fill symbol used to display the main MapView extent. + /// The default is a transparent SimpleFillSymbol with a red, 1 point width outline. + public var extentSymbol: FillSymbol - /// <#Description#> + /// The factor to multiply the main GeoView's scale by. The OverviewMap will display + /// at the product of mainGeoViewScale * scaleFactor. The default is 25.0. public var scaleFactor: Double - /// The geometry of the extent Graphic displaying the main map view's extent. + /// The geometry of the extent Graphic displaying the main map view's extent. Updating + /// this property will update the display of the OverviewMap. @State private var extentGeometry: Envelope? + /// The viewpoint of the OverviewMap's MapView. Updating + /// this property will update the display of the OverviewMap. @State private var overviewMapViewpoint: Viewpoint? - /// <#Description#> + /// Creates an OverviewMap. /// - Parameters: - /// - proxy: <#proxy description#> - /// - map: <#map description#> - /// - extentSymbol: <#extentSymbol description#> + /// - proxy: The binding to an optional MapViewProxy. + /// - map: The Map to display in the OverviewMap. + /// - extentSymbol: The FillSymbol used to display the main MapView's extent. + /// - scaleFactor: The scale factor used to calculate the OverviewMap's scale. public init(proxy: Binding, map: Map = Map(basemap: Basemap.topographic()), - extentSymbol: SimpleFillSymbol = SimpleFillSymbol( + extentSymbol: FillSymbol = SimpleFillSymbol( style: .solid, color: .clear, outline: SimpleLineSymbol( @@ -60,7 +68,7 @@ public struct OverviewMap: View { self.extentSymbol = extentSymbol self.scaleFactor = scaleFactor } - + public var body: some View { ZStack { MapView(map: map, @@ -73,24 +81,10 @@ public struct OverviewMap: View { .border(Color.black, width: 1) .onReceive(proxy.wrappedValue?.viewpointChangedPublisher .receive(on: DispatchQueue.main) - // - // I think "throttle" is what we need here because that does - // not reset the timer when a new value is published, whereas - // debounce will reset the timer, so if there are multiple - // values published that arriver faster than the timeout, - // none of them will be sent because the timer gets reset. - // - // That said, neither of these solve the issue of the - // extent rectangle not getting drawn while the user is - // panning the map. The extent rectangle disappears when the user - // starts panning and will not reappear until the panning stops. - // -// .debounce(for: .seconds(0.25), -// scheduler: DispatchQueue.main) -// .throttle(for: .seconds(0.25), -// scheduler: DispatchQueue.main, -// latest: true -// ) + .throttle(for: .seconds(0.25), + scheduler: DispatchQueue.main, + latest: true + ) .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() ) { guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), @@ -102,11 +96,10 @@ public struct OverviewMap: View { if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { extentGeometry = newExtent } - + if let viewpointGeometry = centerAndScaleViewpoint.targetGeometry as? Point { let viewpoint = Viewpoint(center: viewpointGeometry, scale: centerAndScaleViewpoint.targetScale * scaleFactor) - print("overviewMapViewpoint updated") overviewMapViewpoint = viewpoint } } From 5bc71d8f4352e3ea522e565d07aeb550216b2ee2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Jun 2021 13:26:26 -0500 Subject: [PATCH 0027/1722] Fix indentation. --- .../Components/OverviewMap.swift | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index ca10e7e0d..4625b546b 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -24,10 +24,10 @@ public struct OverviewMap: View { /// available, the binding will be updated by the view. The proxy is /// necessary for accessing `MapView` functionality to get and set viewpoints. public var proxy: Binding - + /// The Map displayed in the OverviewMap. public var map: Map - + /// The fill symbol used to display the main MapView extent. /// The default is a transparent SimpleFillSymbol with a red, 1 point width outline. public var extentSymbol: FillSymbol @@ -39,7 +39,7 @@ public struct OverviewMap: View { /// The geometry of the extent Graphic displaying the main map view's extent. Updating /// this property will update the display of the OverviewMap. @State private var extentGeometry: Envelope? - + /// The viewpoint of the OverviewMap's MapView. Updating /// this property will update the display of the OverviewMap. @State private var overviewMapViewpoint: Viewpoint? @@ -75,34 +75,34 @@ public struct OverviewMap: View { viewpoint: $overviewMapViewpoint, graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, symbol: extentSymbol)])] - ) - .attributionTextHidden() - .interactionModes([]) - .border(Color.black, width: 1) - .onReceive(proxy.wrappedValue?.viewpointChangedPublisher - .receive(on: DispatchQueue.main) - .throttle(for: .seconds(0.25), - scheduler: DispatchQueue.main, - latest: true - ) - .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() - ) { - guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), - centerAndScaleViewpoint.objectType != .unknown, - let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry), - boundingGeometryViewpoint.objectType != .unknown - else { return } - - if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { - extentGeometry = newExtent - } - - if let viewpointGeometry = centerAndScaleViewpoint.targetGeometry as? Point { - let viewpoint = Viewpoint(center: viewpointGeometry, - scale: centerAndScaleViewpoint.targetScale * scaleFactor) - overviewMapViewpoint = viewpoint - } + ) + .attributionTextHidden() + .interactionModes([]) + .border(Color.black, width: 1) + .onReceive(proxy.wrappedValue?.viewpointChangedPublisher + .receive(on: DispatchQueue.main) + .throttle(for: .seconds(0.25), + scheduler: DispatchQueue.main, + latest: true + ) + .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + ) { + guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), + centerAndScaleViewpoint.objectType != .unknown, + let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry), + boundingGeometryViewpoint.objectType != .unknown + else { return } + + if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { + extentGeometry = newExtent + } + + if let viewpointGeometry = centerAndScaleViewpoint.targetGeometry as? Point { + let viewpoint = Viewpoint(center: viewpointGeometry, + scale: centerAndScaleViewpoint.targetScale * scaleFactor) + overviewMapViewpoint = viewpoint } + } } } } From df4038016506ca48612963758d5861063340f791 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Jun 2021 16:00:16 -0500 Subject: [PATCH 0028/1722] Remove check for .unknown object type as that property is really internal-only. --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 4625b546b..a08bbc281 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -88,9 +88,7 @@ public struct OverviewMap: View { .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() ) { guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), - centerAndScaleViewpoint.objectType != .unknown, let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry), - boundingGeometryViewpoint.objectType != .unknown else { return } if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { From 1637bdcb91dd3d4c194d151ca8d778f2bf3cb424 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Jun 2021 16:01:05 -0500 Subject: [PATCH 0029/1722] remove trailing comma --- 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 a08bbc281..e0bec9e0b 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -88,7 +88,7 @@ public struct OverviewMap: View { .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() ) { guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), - let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry), + let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry) else { return } if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { From e88cafa520250f1a0aec26db7381b2f3564e27f2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Jun 2021 16:42:51 -0500 Subject: [PATCH 0030/1722] Remove extra characters in copyright notice. --- Examples/Examples/Examples/OverviewMapExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index 9514d2903..53e07dc72 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Esri./APsPl0cx9SjC7zrU5rCZKCw +// Copyright 2021 Esri. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 70deac4ee061caccb7b7256acb9a12e887e224dc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Jun 2021 16:48:38 -0500 Subject: [PATCH 0031/1722] Create new Search files. --- Examples/Examples.xcodeproj/project.pbxproj | 4 ++ .../Examples/Examples/SearchExampleView.swift | 30 +++++++++++ Sources/ArcGISToolkit/Components/Search.swift | 54 +++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 Examples/Examples/Examples/SearchExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/Search.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index b5f5c472b..2d4d35d71 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */; }; E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; @@ -36,6 +37,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -68,6 +70,7 @@ isa = PBXGroup; children = ( E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, + E42BFBE82672BF9500159107 /* SearchExampleView.swift */, ); path = Examples; sourceTree = ""; @@ -212,6 +215,7 @@ E48A73442658227100F5C118 /* AnyExample.swift in Sources */, E48A73472658227100F5C118 /* ExampleList.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, + E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/Examples/Examples/SearchExampleView.swift b/Examples/Examples/Examples/SearchExampleView.swift new file mode 100644 index 000000000..683761ef3 --- /dev/null +++ b/Examples/Examples/Examples/SearchExampleView.swift @@ -0,0 +1,30 @@ +// Copyright 2021 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 Combine +import ArcGIS +import ArcGISToolkit + +struct SearchExampleView: View { + + var body: some View { + Text("Hello Search") + } +} + +struct SearchExampleView_Previews: PreviewProvider { + static var previews: some View { + SearchExampleView() + } +} diff --git a/Sources/ArcGISToolkit/Components/Search.swift b/Sources/ArcGISToolkit/Components/Search.swift new file mode 100644 index 000000000..a6f500c66 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search.swift @@ -0,0 +1,54 @@ +// Copyright 2021 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 Combine +import ArcGIS + +/// OverviewMap is a small, secondary MapView (sometimes called an "inset map"), superimposed on an existing MapView, which shows the visible extent of the main MapView. +public struct Search: View { + public var body: some View { + ZStack { + MapView(map: map, + viewpoint: $overviewMapViewpoint, + graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, + symbol: extentSymbol)])] + ) + .attributionTextHidden() + .interactionModes([]) + .border(Color.black, width: 1) + .onReceive(proxy.wrappedValue?.viewpointChangedPublisher + .receive(on: DispatchQueue.main) + .throttle(for: .seconds(0.25), + scheduler: DispatchQueue.main, + latest: true + ) + .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + ) { + guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), + let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry) + else { return } + + if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { + extentGeometry = newExtent + } + + if let viewpointGeometry = centerAndScaleViewpoint.targetGeometry as? Point { + let viewpoint = Viewpoint(center: viewpointGeometry, + scale: centerAndScaleViewpoint.targetScale * scaleFactor) + overviewMapViewpoint = viewpoint + } + } + } + } +} From aff8b3ca95d041a767a271b6d33a7781afbd6dec Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Jun 2021 16:49:36 -0500 Subject: [PATCH 0032/1722] Remove extra lines from copyright. --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index e0bec9e0b..bb2affbab 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -1,6 +1,3 @@ -// -// SwiftUIView.swift -// // Copyright 2021 Esri. // Licensed under the Apache License, Version 2.0 (the "License"); From 253df603cd6cf68f0cd9125c6a29a8abbe35cf01 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 11 Jun 2021 12:24:17 -0500 Subject: [PATCH 0033/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 bb2affbab..df90171d5 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -37,7 +37,7 @@ public struct OverviewMap: View { /// this property will update the display of the OverviewMap. @State private var extentGeometry: Envelope? - /// The viewpoint of the OverviewMap's MapView. Updating + /// The viewpoint of the OverviewMap's MapView. Updating /// this property will update the display of the OverviewMap. @State private var overviewMapViewpoint: Viewpoint? From 80d4bf63916eda679dc99d8a5a60db84d9ac3fc5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 11 Jun 2021 12:24:31 -0500 Subject: [PATCH 0034/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 df90171d5..6984ee5c4 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -33,7 +33,7 @@ public struct OverviewMap: View { /// at the product of mainGeoViewScale * scaleFactor. The default is 25.0. public var scaleFactor: Double - /// The geometry of the extent Graphic displaying the main map view's extent. Updating + /// The geometry of the extent Graphic displaying the main map view's extent. Updating /// this property will update the display of the OverviewMap. @State private var extentGeometry: Envelope? From 077bb102cc01fee81894da1edb28371517e764b8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 11 Jun 2021 12:24:38 -0500 Subject: [PATCH 0035/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 6984ee5c4..b8018958f 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -30,7 +30,7 @@ public struct OverviewMap: View { public var extentSymbol: FillSymbol /// The factor to multiply the main GeoView's scale by. The OverviewMap will display - /// at the product of mainGeoViewScale * scaleFactor. The default is 25.0. + /// at the product of mainGeoViewScale * scaleFactor. The default is `25.0`. public var scaleFactor: Double /// The geometry of the extent Graphic displaying the main map view's extent. Updating From f23a007514571f6db2427eeb06171c008c0ef9fc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 11 Jun 2021 12:25:55 -0500 Subject: [PATCH 0036/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 b8018958f..b0ea57331 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -18,8 +18,8 @@ import ArcGIS /// OverviewMap is a small, secondary MapView (sometimes called an "inset map"), superimposed on an existing MapView, which shows the visible extent of the main MapView. public struct OverviewMap: View { /// A binding to an optional `MapViewProxy`. When a proxy is - /// available, the binding will be updated by the view. The proxy is - /// necessary for accessing `MapView` functionality to get and set viewpoints. + /// available, the binding will be updated by the view. The proxy is + /// necessary for accessing `MapView` functionality to get and set viewpoints. public var proxy: Binding /// The Map displayed in the OverviewMap. From 6f4dc8cde0179b9e3db367ddba955a9180f8d605 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 11 Jun 2021 12:27:05 -0500 Subject: [PATCH 0037/1722] Initial files --- .../Examples/Examples/SearchExampleView.swift | 10 +++- Examples/ExamplesApp/Examples.swift | 3 +- Sources/ArcGISToolkit/Components/Search.swift | 46 ++++++------------- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/Examples/Examples/Examples/SearchExampleView.swift b/Examples/Examples/Examples/SearchExampleView.swift index 683761ef3..573b22506 100644 --- a/Examples/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/Examples/SearchExampleView.swift @@ -17,9 +17,17 @@ import ArcGIS import ArcGISToolkit struct SearchExampleView: View { + @State var mapViewProxy: MapViewProxy? var body: some View { - Text("Hello Search") + ZStack (alignment: .topTrailing) { + MapView(map: Map(basemap: Basemap.imageryWithLabels()), + proxy: $mapViewProxy + ) + Search(proxy: $mapViewProxy) + .frame(width: 200, height: 132) + .padding() + } } } diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 78a96b14b..5bbf43128 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -34,7 +34,8 @@ extension ExampleList { static let geoview = Self( name: "GeoView", examples: [ - AnyExample("Overview Map", content: OverviewMapExampleView()) + AnyExample("Overview Map", content: OverviewMapExampleView()), + AnyExample("Search", content: SearchExampleView()) ] ) } diff --git a/Sources/ArcGISToolkit/Components/Search.swift b/Sources/ArcGISToolkit/Components/Search.swift index a6f500c66..d2a0ab9e3 100644 --- a/Sources/ArcGISToolkit/Components/Search.swift +++ b/Sources/ArcGISToolkit/Components/Search.swift @@ -15,40 +15,24 @@ import SwiftUI import Combine import ArcGIS -/// OverviewMap is a small, secondary MapView (sometimes called an "inset map"), superimposed on an existing MapView, which shows the visible extent of the main MapView. -public struct Search: View { +public struct Search: View { + public var proxy: Binding + + @State private var searchText = "" + + public init(proxy: Binding) { + self.proxy = proxy + } + public var body: some View { ZStack { - MapView(map: map, - viewpoint: $overviewMapViewpoint, - graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, - symbol: extentSymbol)])] - ) - .attributionTextHidden() - .interactionModes([]) - .border(Color.black, width: 1) - .onReceive(proxy.wrappedValue?.viewpointChangedPublisher - .receive(on: DispatchQueue.main) - .throttle(for: .seconds(0.25), - scheduler: DispatchQueue.main, - latest: true - ) - .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() - ) { - guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), - let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry) - else { return } - - if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { - extentGeometry = newExtent - } - - if let viewpointGeometry = centerAndScaleViewpoint.targetGeometry as? Point { - let viewpoint = Viewpoint(center: viewpointGeometry, - scale: centerAndScaleViewpoint.targetScale * scaleFactor) - overviewMapViewpoint = viewpoint - } + TextField("Search", + text: $searchText) { editing in + print("editing changed") + } onCommit: { + print("On commit") } + } } } From 2066b05020f4545eda32b122258b5237bebde59f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 11 Jun 2021 12:53:06 -0500 Subject: [PATCH 0038/1722] Doc changes and consolidation of `guard` and `if` statements. --- .../Components/OverviewMap.swift | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index b0ea57331..119688aa6 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -15,38 +15,38 @@ import SwiftUI import Combine import ArcGIS -/// OverviewMap is a small, secondary MapView (sometimes called an "inset map"), superimposed on an existing MapView, which shows the visible extent of the main MapView. +/// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed on an existing `MapView`, which shows the visible extent of the main `MapView`. public struct OverviewMap: View { /// A binding to an optional `MapViewProxy`. When a proxy is /// available, the binding will be updated by the view. The proxy is /// necessary for accessing `MapView` functionality to get and set viewpoints. public var proxy: Binding - /// The Map displayed in the OverviewMap. + /// The `Map` displayed in the `OverviewMap`. public var map: Map - /// The fill symbol used to display the main MapView extent. - /// The default is a transparent SimpleFillSymbol with a red, 1 point width outline. + /// The fill symbol used to display the main `MapView` extent. + /// The default is a transparent `SimpleFillSymbol` with a red, 1 point width outline. public var extentSymbol: FillSymbol - /// The factor to multiply the main GeoView's scale by. The OverviewMap will display + /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display /// at the product of mainGeoViewScale * scaleFactor. The default is `25.0`. public var scaleFactor: Double - /// The geometry of the extent Graphic displaying the main map view's extent. Updating - /// this property will update the display of the OverviewMap. + /// The geometry of the extent `Graphic` displaying the main `MapView`'s extent. Updating + /// this property will update the display of the `OverviewMap`. @State private var extentGeometry: Envelope? - /// The viewpoint of the OverviewMap's MapView. Updating - /// this property will update the display of the OverviewMap. + /// The viewpoint of the `OverviewMap'`s `MapView`. Updating + /// this property will update the display of the `OverviewMap`. @State private var overviewMapViewpoint: Viewpoint? - /// Creates an OverviewMap. + /// Creates an `OverviewMap`. /// - Parameters: - /// - proxy: The binding to an optional MapViewProxy. - /// - map: The Map to display in the OverviewMap. - /// - extentSymbol: The FillSymbol used to display the main MapView's extent. - /// - scaleFactor: The scale factor used to calculate the OverviewMap's scale. + /// - proxy: The binding to an optional `MapViewProxy`. + /// - map: The `Map` to display in the `OverviewMap`. + /// - extentSymbol: The `FillSymbol` used to display the main `MapView`'s extent. + /// - scaleFactor: The scale factor used to calculate the `OverviewMap`'s scale. public init(proxy: Binding, map: Map = Map(basemap: Basemap.topographic()), extentSymbol: FillSymbol = SimpleFillSymbol( @@ -85,18 +85,14 @@ public struct OverviewMap: View { .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() ) { guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), - let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry) + let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry), + let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope, + let newCenter = centerAndScaleViewpoint.targetGeometry as? Point else { return } - if let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope { - extentGeometry = newExtent - } - - if let viewpointGeometry = centerAndScaleViewpoint.targetGeometry as? Point { - let viewpoint = Viewpoint(center: viewpointGeometry, - scale: centerAndScaleViewpoint.targetScale * scaleFactor) - overviewMapViewpoint = viewpoint - } + extentGeometry = newExtent + overviewMapViewpoint = Viewpoint(center: newCenter, + scale: centerAndScaleViewpoint.targetScale * scaleFactor) } } } From 8eaa2042d5f0bb42839359af59099c8b6c4cd910 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 13:32:58 -0500 Subject: [PATCH 0039/1722] use GeoViewProxy instead of binding to allow for SceneView support as well. --- .../Examples/OverviewMapExampleView.swift | 27 +++++++++++++------ .../Components/OverviewMap.swift | 25 +++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index 53e07dc72..c7cf92ecf 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -18,15 +18,26 @@ import ArcGISToolkit struct OverviewMapExampleView: View { @State private var mapViewProxy: MapViewProxy? - + @State private var sceneViewProxy: SceneViewProxy? + var body: some View { - ZStack (alignment: .topTrailing) { - MapView(map: Map(basemap: Basemap.imageryWithLabels()), - proxy: $mapViewProxy - ) - OverviewMap(proxy: $mapViewProxy) - .frame(width: 200, height: 132) - .padding() + VStack { + ZStack (alignment: .topTrailing) { + MapView(map: Map(basemap: Basemap.imageryWithLabels()), + proxy: $mapViewProxy + ) + OverviewMap(proxy: mapViewProxy) + .frame(width: 200, height: 132) + .padding() + } + ZStack (alignment: .topTrailing) { + SceneView(scene: Scene(basemap: Basemap.imageryWithLabels()), + proxy: $sceneViewProxy + ) + OverviewMap(proxy: sceneViewProxy) + .frame(width: 200, height: 132) + .padding() + } } } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 119688aa6..73a751157 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -15,17 +15,16 @@ import SwiftUI import Combine import ArcGIS -/// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed on an existing `MapView`, which shows the visible extent of the main `MapView`. +/// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed on an existing `GeoView`, which shows the visible extent of that `GeoView`. public struct OverviewMap: View { - /// A binding to an optional `MapViewProxy`. When a proxy is - /// available, the binding will be updated by the view. The proxy is - /// necessary for accessing `MapView` functionality to get and set viewpoints. - public var proxy: Binding + /// The `GeoViewProxy` representing the main `GeoView`. The proxy is + /// necessary for accessing `GeoView` functionality to get and set viewpoints. + public var proxy: GeoViewProxy? /// The `Map` displayed in the `OverviewMap`. public var map: Map - /// The fill symbol used to display the main `MapView` extent. + /// The fill symbol used to display the main `GeoView` extent. /// The default is a transparent `SimpleFillSymbol` with a red, 1 point width outline. public var extentSymbol: FillSymbol @@ -33,7 +32,7 @@ public struct OverviewMap: View { /// at the product of mainGeoViewScale * scaleFactor. The default is `25.0`. public var scaleFactor: Double - /// The geometry of the extent `Graphic` displaying the main `MapView`'s extent. Updating + /// The geometry of the extent `Graphic` displaying the main `GeoView`'s extent. Updating /// this property will update the display of the `OverviewMap`. @State private var extentGeometry: Envelope? @@ -43,11 +42,11 @@ public struct OverviewMap: View { /// Creates an `OverviewMap`. /// - Parameters: - /// - proxy: The binding to an optional `MapViewProxy`. + /// - proxy: The `GeoViewProxy` representing the main map. /// - map: The `Map` to display in the `OverviewMap`. - /// - extentSymbol: The `FillSymbol` used to display the main `MapView`'s extent. + /// - extentSymbol: The `FillSymbol` used to display the main `GeoView`'s extent. /// - scaleFactor: The scale factor used to calculate the `OverviewMap`'s scale. - public init(proxy: Binding, + public init(proxy: GeoViewProxy?, map: Map = Map(basemap: Basemap.topographic()), extentSymbol: FillSymbol = SimpleFillSymbol( style: .solid, @@ -76,7 +75,7 @@ public struct OverviewMap: View { .attributionTextHidden() .interactionModes([]) .border(Color.black, width: 1) - .onReceive(proxy.wrappedValue?.viewpointChangedPublisher + .onReceive(proxy?.viewpointChangedPublisher .receive(on: DispatchQueue.main) .throttle(for: .seconds(0.25), scheduler: DispatchQueue.main, @@ -84,8 +83,8 @@ public struct OverviewMap: View { ) .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() ) { - guard let centerAndScaleViewpoint = proxy.wrappedValue?.currentViewpoint(type: .centerAndScale), - let boundingGeometryViewpoint = proxy.wrappedValue?.currentViewpoint(type: .boundingGeometry), + guard let centerAndScaleViewpoint = proxy?.currentViewpoint(type: .centerAndScale), + let boundingGeometryViewpoint = proxy?.currentViewpoint(type: .boundingGeometry), let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope, let newCenter = centerAndScaleViewpoint.targetGeometry as? Point else { return } From d861a64fba94160f31eb9b5f48e0b2e0720e75c7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 15:29:55 -0500 Subject: [PATCH 0040/1722] Update Examples/Examples/Examples/OverviewMapExampleView.swift Co-authored-by: R Olson --- Examples/Examples/Examples/OverviewMapExampleView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index c7cf92ecf..f5399f48b 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -23,7 +23,8 @@ struct OverviewMapExampleView: View { var body: some View { VStack { ZStack (alignment: .topTrailing) { - MapView(map: Map(basemap: Basemap.imageryWithLabels()), + MapView( + map: Map(basemap: Basemap.imageryWithLabels()), proxy: $mapViewProxy ) OverviewMap(proxy: mapViewProxy) From 99bac06a945559db96b4590460c9141844403e32 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 15:30:01 -0500 Subject: [PATCH 0041/1722] Update Examples/Examples/Examples/OverviewMapExampleView.swift Co-authored-by: R Olson --- Examples/Examples/Examples/OverviewMapExampleView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index f5399f48b..27a9f9146 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -32,7 +32,8 @@ struct OverviewMapExampleView: View { .padding() } ZStack (alignment: .topTrailing) { - SceneView(scene: Scene(basemap: Basemap.imageryWithLabels()), + SceneView( + scene: Scene(basemap: Basemap.imageryWithLabels()), proxy: $sceneViewProxy ) OverviewMap(proxy: sceneViewProxy) From 15868be95e65483accead4aeef532252ea89e90a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 15:30:25 -0500 Subject: [PATCH 0042/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: R Olson --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 73a751157..e3de6c915 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -46,7 +46,8 @@ public struct OverviewMap: View { /// - map: The `Map` to display in the `OverviewMap`. /// - extentSymbol: The `FillSymbol` used to display the main `GeoView`'s extent. /// - scaleFactor: The scale factor used to calculate the `OverviewMap`'s scale. - public init(proxy: GeoViewProxy?, + public init( + proxy: GeoViewProxy?, map: Map = Map(basemap: Basemap.topographic()), extentSymbol: FillSymbol = SimpleFillSymbol( style: .solid, From c84c670779e21b55d92fe8787a658f7bca2e9158 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 15:30:32 -0500 Subject: [PATCH 0043/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: R Olson --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index e3de6c915..5c0f1c0fd 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -68,7 +68,8 @@ public struct OverviewMap: View { public var body: some View { ZStack { - MapView(map: map, + MapView( + map: map, viewpoint: $overviewMapViewpoint, graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, symbol: extentSymbol)])] From b5073df0ac9c757ae408ac48903557142eaffdb2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 15:30:39 -0500 Subject: [PATCH 0044/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: R Olson --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 5c0f1c0fd..3086e8736 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -79,7 +79,8 @@ public struct OverviewMap: View { .border(Color.black, width: 1) .onReceive(proxy?.viewpointChangedPublisher .receive(on: DispatchQueue.main) - .throttle(for: .seconds(0.25), + .throttle( + for: .seconds(0.25), scheduler: DispatchQueue.main, latest: true ) From 9e343011de7da4f37420ab98cc070bdf7877e727 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 15:30:52 -0500 Subject: [PATCH 0045/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: R Olson --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 3086e8736..92d1f3cf3 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -93,7 +93,8 @@ public struct OverviewMap: View { else { return } extentGeometry = newExtent - overviewMapViewpoint = Viewpoint(center: newCenter, + overviewMapViewpoint = Viewpoint( + center: newCenter, scale: centerAndScaleViewpoint.targetScale * scaleFactor) } } From 0cf2adc7a5d211cad35180ff7e1a6bc27ed268d7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 15:35:59 -0500 Subject: [PATCH 0046/1722] Fix indenting. --- .../Examples/OverviewMapExampleView.swift | 10 ++--- .../Components/OverviewMap.swift | 42 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index 27a9f9146..d240cae4a 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -19,13 +19,13 @@ import ArcGISToolkit struct OverviewMapExampleView: View { @State private var mapViewProxy: MapViewProxy? @State private var sceneViewProxy: SceneViewProxy? - + var body: some View { VStack { ZStack (alignment: .topTrailing) { MapView( - map: Map(basemap: Basemap.imageryWithLabels()), - proxy: $mapViewProxy + map: Map(basemap: Basemap.imageryWithLabels()), + proxy: $mapViewProxy ) OverviewMap(proxy: mapViewProxy) .frame(width: 200, height: 132) @@ -33,8 +33,8 @@ struct OverviewMapExampleView: View { } ZStack (alignment: .topTrailing) { SceneView( - scene: Scene(basemap: Basemap.imageryWithLabels()), - proxy: $sceneViewProxy + scene: Scene(basemap: Basemap.imageryWithLabels()), + proxy: $sceneViewProxy ) OverviewMap(proxy: sceneViewProxy) .frame(width: 200, height: 132) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 92d1f3cf3..dc2c4a8da 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -47,18 +47,18 @@ public struct OverviewMap: View { /// - extentSymbol: The `FillSymbol` used to display the main `GeoView`'s extent. /// - scaleFactor: The scale factor used to calculate the `OverviewMap`'s scale. public init( - proxy: GeoViewProxy?, - map: Map = Map(basemap: Basemap.topographic()), - extentSymbol: FillSymbol = SimpleFillSymbol( - style: .solid, - color: .clear, - outline: SimpleLineSymbol( - style: .solid, - color: .red, - width: 1.0 - ) - ), - scaleFactor: Double = 25.0 + proxy: GeoViewProxy?, + map: Map = Map(basemap: Basemap.topographic()), + extentSymbol: FillSymbol = SimpleFillSymbol( + style: .solid, + color: .clear, + outline: SimpleLineSymbol( + style: .solid, + color: .red, + width: 1.0 + ) + ), + scaleFactor: Double = 25.0 ) { self.proxy = proxy self.map = map @@ -69,10 +69,10 @@ public struct OverviewMap: View { public var body: some View { ZStack { MapView( - map: map, - viewpoint: $overviewMapViewpoint, - graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, - symbol: extentSymbol)])] + map: map, + viewpoint: $overviewMapViewpoint, + graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, + symbol: extentSymbol)])] ) .attributionTextHidden() .interactionModes([]) @@ -80,9 +80,9 @@ public struct OverviewMap: View { .onReceive(proxy?.viewpointChangedPublisher .receive(on: DispatchQueue.main) .throttle( - for: .seconds(0.25), - scheduler: DispatchQueue.main, - latest: true + for: .seconds(0.25), + scheduler: DispatchQueue.main, + latest: true ) .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() ) { @@ -94,8 +94,8 @@ public struct OverviewMap: View { extentGeometry = newExtent overviewMapViewpoint = Viewpoint( - center: newCenter, - scale: centerAndScaleViewpoint.targetScale * scaleFactor) + center: newCenter, + scale: centerAndScaleViewpoint.targetScale * scaleFactor) } } } From 0c419ddb32af53f3bb03162b852967e83a337922 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 14 Jun 2021 16:29:04 -0500 Subject: [PATCH 0047/1722] Move default value doc from property to constructor (where they are set). --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index dc2c4a8da..4f4657869 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -25,11 +25,10 @@ public struct OverviewMap: View { public var map: Map /// The fill symbol used to display the main `GeoView` extent. - /// The default is a transparent `SimpleFillSymbol` with a red, 1 point width outline. public var extentSymbol: FillSymbol /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display - /// at the product of mainGeoViewScale * scaleFactor. The default is `25.0`. + /// at the product of mainGeoViewScale * scaleFactor. public var scaleFactor: Double /// The geometry of the extent `Graphic` displaying the main `GeoView`'s extent. Updating @@ -45,7 +44,9 @@ public struct OverviewMap: View { /// - proxy: The `GeoViewProxy` representing the main map. /// - map: The `Map` to display in the `OverviewMap`. /// - extentSymbol: The `FillSymbol` used to display the main `GeoView`'s extent. + /// The default is a transparent `SimpleFillSymbol` with a red, 1 point width outline. /// - scaleFactor: The scale factor used to calculate the `OverviewMap`'s scale. + /// The default is `25.0`. public init( proxy: GeoViewProxy?, map: Map = Map(basemap: Basemap.topographic()), From 988a6ea47cef9675be5b002deb52657d22be22e5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 17 Jun 2021 15:40:23 -0500 Subject: [PATCH 0048/1722] Add rotation to MapView extent. Revert to single example OverviewMap --- .../Examples/OverviewMapExampleView.swift | 27 ++++++------------- .../Components/OverviewMap.swift | 9 ++++--- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index d240cae4a..083520a40 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -21,25 +21,14 @@ struct OverviewMapExampleView: View { @State private var sceneViewProxy: SceneViewProxy? var body: some View { - VStack { - ZStack (alignment: .topTrailing) { - MapView( - map: Map(basemap: Basemap.imageryWithLabels()), - proxy: $mapViewProxy - ) - OverviewMap(proxy: mapViewProxy) - .frame(width: 200, height: 132) - .padding() - } - ZStack (alignment: .topTrailing) { - SceneView( - scene: Scene(basemap: Basemap.imageryWithLabels()), - proxy: $sceneViewProxy - ) - OverviewMap(proxy: sceneViewProxy) - .frame(width: 200, height: 132) - .padding() - } + ZStack (alignment: .topTrailing) { + MapView( + map: Map(basemap: Basemap.imageryWithLabels()), + proxy: $mapViewProxy + ) + OverviewMap(proxy: mapViewProxy) + .frame(width: 200, height: 132) + .padding() } } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 4f4657869..4804841b3 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -33,7 +33,7 @@ public struct OverviewMap: View { /// The geometry of the extent `Graphic` displaying the main `GeoView`'s extent. Updating /// this property will update the display of the `OverviewMap`. - @State private var extentGeometry: Envelope? + @State private var extentGeometry: Geometry? /// The viewpoint of the `OverviewMap'`s `MapView`. Updating /// this property will update the display of the `OverviewMap`. @@ -88,12 +88,13 @@ public struct OverviewMap: View { .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() ) { guard let centerAndScaleViewpoint = proxy?.currentViewpoint(type: .centerAndScale), - let boundingGeometryViewpoint = proxy?.currentViewpoint(type: .boundingGeometry), - let newExtent = boundingGeometryViewpoint.targetGeometry as? Envelope, let newCenter = centerAndScaleViewpoint.targetGeometry as? Point else { return } - extentGeometry = newExtent + if let mapViewProxy = proxy as? MapViewProxy { + extentGeometry = mapViewProxy.visibleArea + } + overviewMapViewpoint = Viewpoint( center: newCenter, scale: centerAndScaleViewpoint.targetScale * scaleFactor) From 9f39b1dd7d2c6bd78410b630f86fceac44c3fb36 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 17 Jun 2021 16:39:50 -0500 Subject: [PATCH 0049/1722] PR review comments. --- .../Components/OverviewMap.swift | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 4804841b3..e73633b27 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -15,21 +15,22 @@ import SwiftUI import Combine import ArcGIS -/// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed on an existing `GeoView`, which shows the visible extent of that `GeoView`. +/// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed +/// on an existing `GeoView`, which shows the visible extent of that `GeoView`. public struct OverviewMap: View { /// The `GeoViewProxy` representing the main `GeoView`. The proxy is /// necessary for accessing `GeoView` functionality to get and set viewpoints. - public var proxy: GeoViewProxy? + private(set) var proxy: GeoViewProxy? /// The `Map` displayed in the `OverviewMap`. - public var map: Map + private(set) var map: Map /// The fill symbol used to display the main `GeoView` extent. - public var extentSymbol: FillSymbol + private(set) var extentSymbol: FillSymbol /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display /// at the product of mainGeoViewScale * scaleFactor. - public var scaleFactor: Double + private(set) var scaleFactor: Double /// The geometry of the extent `Graphic` displaying the main `GeoView`'s extent. Updating /// this property will update the display of the `OverviewMap`. @@ -69,24 +70,26 @@ public struct OverviewMap: View { public var body: some View { ZStack { + let viewpointChangedPublisher: AnyPublisher = proxy?.viewpointChangedPublisher + .receive(on: DispatchQueue.main) + .throttle( + for: .seconds(0.25), + scheduler: DispatchQueue.main, + latest: true + ) + .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + MapView( map: map, viewpoint: $overviewMapViewpoint, - graphicsOverlays: [GraphicsOverlay(graphics: [Graphic(geometry: extentGeometry, - symbol: extentSymbol)])] + graphicsOverlays: [GraphicsOverlay( + graphics: [Graphic(geometry: extentGeometry, + symbol: extentSymbol)])] ) .attributionTextHidden() .interactionModes([]) .border(Color.black, width: 1) - .onReceive(proxy?.viewpointChangedPublisher - .receive(on: DispatchQueue.main) - .throttle( - for: .seconds(0.25), - scheduler: DispatchQueue.main, - latest: true - ) - .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() - ) { + .onReceive(viewpointChangedPublisher) { guard let centerAndScaleViewpoint = proxy?.currentViewpoint(type: .centerAndScale), let newCenter = centerAndScaleViewpoint.targetGeometry as? Point else { return } @@ -94,7 +97,7 @@ public struct OverviewMap: View { if let mapViewProxy = proxy as? MapViewProxy { extentGeometry = mapViewProxy.visibleArea } - + overviewMapViewpoint = Viewpoint( center: newCenter, scale: centerAndScaleViewpoint.targetScale * scaleFactor) From a3baa9bed762ad9cf71f4542a118e777b468eb3e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 18 Jun 2021 09:14:50 -0500 Subject: [PATCH 0050/1722] Move `viewpointChangedPublisher` definition to a function outside of `body`. --- .../Components/OverviewMap.swift | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index e73633b27..5372e616f 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -68,17 +68,19 @@ public struct OverviewMap: View { self.scaleFactor = scaleFactor } + private func viewpointChangedPublisher() -> AnyPublisher { + proxy?.viewpointChangedPublisher + .receive(on: DispatchQueue.main) + .throttle( + for: .seconds(0.25), + scheduler: DispatchQueue.main, + latest: true + ) + .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + } + public var body: some View { ZStack { - let viewpointChangedPublisher: AnyPublisher = proxy?.viewpointChangedPublisher - .receive(on: DispatchQueue.main) - .throttle( - for: .seconds(0.25), - scheduler: DispatchQueue.main, - latest: true - ) - .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() - MapView( map: map, viewpoint: $overviewMapViewpoint, @@ -89,7 +91,7 @@ public struct OverviewMap: View { .attributionTextHidden() .interactionModes([]) .border(Color.black, width: 1) - .onReceive(viewpointChangedPublisher) { + .onReceive(viewpointChangedPublisher()) { guard let centerAndScaleViewpoint = proxy?.currentViewpoint(type: .centerAndScale), let newCenter = centerAndScaleViewpoint.targetGeometry as? Point else { return } From b8184a7a567719c4795ba05a56042a53e4efb6d5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 18 Jun 2021 11:21:06 -0500 Subject: [PATCH 0051/1722] make viewpointChangedPublisher a var instead of a func. --- 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 5372e616f..738416152 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -68,7 +68,7 @@ public struct OverviewMap: View { self.scaleFactor = scaleFactor } - private func viewpointChangedPublisher() -> AnyPublisher { + private var viewpointChangedPublisher: AnyPublisher { proxy?.viewpointChangedPublisher .receive(on: DispatchQueue.main) .throttle( @@ -91,7 +91,7 @@ public struct OverviewMap: View { .attributionTextHidden() .interactionModes([]) .border(Color.black, width: 1) - .onReceive(viewpointChangedPublisher()) { + .onReceive(viewpointChangedPublisher) { guard let centerAndScaleViewpoint = proxy?.currentViewpoint(type: .centerAndScale), let newCenter = centerAndScaleViewpoint.targetGeometry as? Point else { return } From 0d023db36cc506e9bc13dfeeb351807cd7dec550 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 18 Jun 2021 14:27:40 -0500 Subject: [PATCH 0052/1722] Update Examples/Examples/Examples/OverviewMapExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/Examples/OverviewMapExampleView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index 083520a40..4fb1fbd09 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -12,7 +12,6 @@ // limitations under the License. import SwiftUI -import Combine import ArcGIS import ArcGISToolkit From 3102d51e23088e6ba05e4c47fcb55eac812561b1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 18 Jun 2021 14:27:50 -0500 Subject: [PATCH 0053/1722] Update Examples/Examples/Examples/OverviewMapExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/Examples/OverviewMapExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/Examples/OverviewMapExampleView.swift index 4fb1fbd09..a9f7ba0bf 100644 --- a/Examples/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/Examples/OverviewMapExampleView.swift @@ -22,7 +22,7 @@ struct OverviewMapExampleView: View { var body: some View { ZStack (alignment: .topTrailing) { MapView( - map: Map(basemap: Basemap.imageryWithLabels()), + map: Map(basemap: .imageryWithLabels()), proxy: $mapViewProxy ) OverviewMap(proxy: mapViewProxy) From 9bd83b5a7bd350eca6e0c7e3b945d5a4cd629e17 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 18 Jun 2021 14:29:15 -0500 Subject: [PATCH 0054/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 738416152..55bc591d7 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -50,7 +50,7 @@ public struct OverviewMap: View { /// The default is `25.0`. public init( proxy: GeoViewProxy?, - map: Map = Map(basemap: Basemap.topographic()), + map: Map = Map(basemap: .topographic()), extentSymbol: FillSymbol = SimpleFillSymbol( style: .solid, color: .clear, From f43c60642c0e0cc31617ea33c0eb8d024949ab84 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 18 Jun 2021 14:29:35 -0500 Subject: [PATCH 0055/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 55bc591d7..ed37e4865 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -102,7 +102,8 @@ public struct OverviewMap: View { overviewMapViewpoint = Viewpoint( center: newCenter, - scale: centerAndScaleViewpoint.targetScale * scaleFactor) + scale: centerAndScaleViewpoint.targetScale * scaleFactor + ) } } } From 137f65485b8c1b7918971a89e501974548779696 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 22 Jun 2021 15:13:09 -0500 Subject: [PATCH 0056/1722] OVM documentation --- Documentation/OverviewMap/README.md | 41 +++++++++++++++++++ Documentation/README.md | 3 ++ .../OverviewMapExampleView.swift | 0 README.md | 2 +- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 Documentation/OverviewMap/README.md create mode 100644 Documentation/README.md rename Examples/Examples/{Examples => }/OverviewMapExampleView.swift (100%) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md new file mode 100644 index 000000000..bd1af1c1a --- /dev/null +++ b/Documentation/OverviewMap/README.md @@ -0,0 +1,41 @@ +### OverviewMap Behavior: + +For an OverviewMap on a `MapView`, the `MapView`'s `visibleArea` property will be represented in the overview map as a polygon, which will rotate as the `MapView` rotates. + +For a `SceneView`... + + + +# OverviewMap + +`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be superimposed on an existing `MapView`/`SceneView`. OverviewMap shows shows the visible extent of the `GeoView` it is connected to. + +![OverviewMap](https://user-images.githubusercontent.com/29742178/121975740-34f07000-cd37-11eb-9162-462925cb3fe7.png) + +> **NOTE**: OverviewMap uses metered ArcGIS basemaps by default, so you will need to configure an API key. See [Security and authentication documentation](https://developers.arcgis.com/documentation/mapping-apis-and-services/security/#api-keys) for more information. + +## Features + +OverviewMap: + +- Displays a representation of the current `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 `Viewpoint` representation. + +## Key properties + +`OverviewMap` has the following customizable properties: + +- `geoView` - References the connected `MapView` or `SceneView` +- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for `MapView`s; For `SceneView`s, this is a red cross. +- `scaleFactor` - Defines the scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The default is 25. +- `map` - the `Map` displayed in the `OverviewMap`. For example, you can use `map` to change the base map displayed by the `OverviewMap`. + +## Usage + +```swift +// Waiting on final implementation details +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [OverviewMap.swift](../../Examples/ArcGISToolkitExamples/OverviewMap.swift) in the project. + diff --git a/Documentation/README.md b/Documentation/README.md new file mode 100644 index 000000000..dfa9209f6 --- /dev/null +++ b/Documentation/README.md @@ -0,0 +1,3 @@ +### Table of Contents + +* [Overview Map](OverviewMap) - Displays an "overview" (or "inset") map on top of an existing map or scene view. diff --git a/Examples/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift similarity index 100% rename from Examples/Examples/Examples/OverviewMapExampleView.swift rename to Examples/Examples/OverviewMapExampleView.swift diff --git a/README.md b/README.md index ca0aa1bc8..75286681e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ To use Toolkit in your project: ## Toolkit Components -* TBD +* [OverviewMap](./Documentation/OverviewMap/README.md) ## Requirements * ArcGIS Runtime SDK for Swift From 66311a9c8feec6447a16c9bf1d9ca43869695d46 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 22 Jun 2021 15:22:50 -0500 Subject: [PATCH 0057/1722] auto project update. --- Examples/Examples.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index b5f5c472b..6b97fe1d0 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -69,7 +69,7 @@ children = ( E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, ); - path = Examples; + name = Examples; sourceTree = ""; }; E47ABE372652FE0900FD2FE3 = { From ef4d6a9f89a6083485b77b48f56617ef77237328 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 22 Jun 2021 15:43:57 -0500 Subject: [PATCH 0058/1722] doc updates. --- Documentation/OverviewMap/README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index bd1af1c1a..46a23d5bd 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -1,14 +1,6 @@ -### OverviewMap Behavior: - -For an OverviewMap on a `MapView`, the `MapView`'s `visibleArea` property will be represented in the overview map as a polygon, which will rotate as the `MapView` rotates. - -For a `SceneView`... - - - # OverviewMap -`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be superimposed on an existing `MapView`/`SceneView`. OverviewMap shows shows the visible extent of the `GeoView` it is connected to. +`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be superimposed on an existing `MapView`/`SceneView`. `OverviewMap` shows shows a representation of the current `Viewpoint` of the `GeoView` it is connected to. ![OverviewMap](https://user-images.githubusercontent.com/29742178/121975740-34f07000-cd37-11eb-9162-462925cb3fe7.png) @@ -20,17 +12,23 @@ OverviewMap: - Displays a representation of the current `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 `Viewpoint` representation. +- Supports a configurable symbol for visualizing the current `Viewpoint` representation (a `FillSymbol` for connected `MapView`s; a `MarkerSymbol` for connected `SceneView`s). ## Key properties `OverviewMap` has the following customizable properties: - `geoView` - References the connected `MapView` or `SceneView` -- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for `MapView`s; For `SceneView`s, this is a red cross. +- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for `MapView`s; for `SceneView`s, this is a red cross. - `scaleFactor` - Defines the scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The default is 25. - `map` - the `Map` displayed in the `OverviewMap`. For example, you can use `map` to change the base map displayed by the `OverviewMap`. +## Behavior: + +For an `OverviewMap` on a `MapView`, the `MapView`'s `visibleArea` property will be represented in the `OverviewMap` as a polygon, which will rotate as the `MapView` rotates. + +For an `OverviewMap` on a `SceneView`, the center point of the `SceneView`'s `currentViewpoint` property will be represented in the `OverviewMap` by a point. + ## Usage ```swift From ecfe91d04cde3e40cf91b99fd8c9d71b47840359 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 22 Jun 2021 15:46:56 -0500 Subject: [PATCH 0059/1722] Doc changes. --- Documentation/OverviewMap/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 46a23d5bd..f8afe4f9b 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -10,16 +10,16 @@ OverviewMap: -- Displays a representation of the current `Viewpoint` for a connected `GeoView` +- Displays a representation of the current `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 `Viewpoint` representation (a `FillSymbol` for connected `MapView`s; a `MarkerSymbol` for connected `SceneView`s). +- Supports a configurable symbol for visualizing the current `Viewpoint` representation (a `FillSymbol` for a connected `MapView`; a `MarkerSymbol` for a connected `SceneView`). ## Key properties `OverviewMap` has the following customizable properties: - `geoView` - References the connected `MapView` or `SceneView` -- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for `MapView`s; for `SceneView`s, this is a red cross. +- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. - `scaleFactor` - Defines the scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The default is 25. - `map` - the `Map` displayed in the `OverviewMap`. For example, you can use `map` to change the base map displayed by the `OverviewMap`. From 27aa60d2868e0c24fac3742efdc334a4be8f19e1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 22 Jun 2021 15:51:42 -0500 Subject: [PATCH 0060/1722] Update README.md update paths --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index f8afe4f9b..178c1eb84 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -35,5 +35,5 @@ For an `OverviewMap` on a `SceneView`, the center point of the `SceneView`'s `cu // Waiting on final implementation details ``` -To see it in action, try out the [Examples](../../Examples) and refer to [OverviewMap.swift](../../Examples/ArcGISToolkitExamples/OverviewMap.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [OverviewMapExampleView.swift](../../Examples/Examples/OverviewMapExampleView.swift) in the project. From d5cab58187cc262e8cd1b18878edaed3f2c86fbd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 22 Jun 2021 15:52:51 -0500 Subject: [PATCH 0061/1722] Update README.md Punctuation. --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 178c1eb84..c2b3f87db 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -18,7 +18,7 @@ OverviewMap: `OverviewMap` has the following customizable properties: -- `geoView` - References the connected `MapView` or `SceneView` +- `geoView` - References the connected `MapView` or `SceneView`. - `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. - `scaleFactor` - Defines the scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The default is 25. - `map` - the `Map` displayed in the `OverviewMap`. For example, you can use `map` to change the base map displayed by the `OverviewMap`. From 0656ac83a7124f29cbddc611fa121bc6efbde158 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 23 Jun 2021 14:45:13 -0500 Subject: [PATCH 0062/1722] Add additional files. --- .../Examples/Examples/SearchExampleView.swift | 14 +- .../Search/LocatorSearchSourceProtocol.swift | 29 +++ .../Components/Search/SearchResult.swift | 43 +++++ .../Search/SearchSourceProtocol.swift | 61 ++++++ .../Components/Search/SearchSuggestion.swift | 38 ++++ .../Components/Search/SearchView.swift | 65 +++++++ .../Components/Search/SearchViewModel.swift | 139 +++++++++++++ .../Search/SmartLocatorSearchSource.swift | 182 ++++++++++++++++++ 8 files changed, 563 insertions(+), 8 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift create mode 100644 Sources/ArcGISToolkit/Components/Search/SearchResult.swift create mode 100644 Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift create mode 100644 Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift create mode 100644 Sources/ArcGISToolkit/Components/Search/SearchView.swift create mode 100644 Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift create mode 100644 Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift diff --git a/Examples/Examples/Examples/SearchExampleView.swift b/Examples/Examples/Examples/SearchExampleView.swift index 573b22506..e8d753ce4 100644 --- a/Examples/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/Examples/SearchExampleView.swift @@ -17,16 +17,14 @@ import ArcGIS import ArcGISToolkit struct SearchExampleView: View { - @State var mapViewProxy: MapViewProxy? - var body: some View { ZStack (alignment: .topTrailing) { - MapView(map: Map(basemap: Basemap.imageryWithLabels()), - proxy: $mapViewProxy - ) - Search(proxy: $mapViewProxy) - .frame(width: 200, height: 132) - .padding() + MapViewReader { proxy in + MapView(map: Map(basemap: Basemap.imageryWithLabels())) +// SearchView(proxy: proxy) +// .frame(width: 200, height: 132) +// .padding() + } } } } diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift new file mode 100644 index 000000000..cc0c53c8d --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift @@ -0,0 +1,29 @@ +// Copyright 2021 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 + +/// Uses a Locator to provide search and suggest results. Most configuration should be done on the +/// `GeocodeParameters` directly. +public protocol LocatorSearchSourceProtocol: SearchSourceProtocol { + /// The locator used by this search source. + var locator: LocatorTask { get } + + /// Parameters used for geocoding. Some properties on parameters will be updated automatically + /// based on searches. + var geocodeParameters: GeocodeParameters { get } + + /// Parameters used for getting suggestions. Some properties will be updated automatically + /// based on searches. + var suggestParameters: SuggestParameters { get } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift new file mode 100644 index 000000000..07c8a4986 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -0,0 +1,43 @@ +// Copyright 2021 Esri. + +import UIKit.UIImage +import ArcGIS + +// 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. + +/// Wraps a search result for display. +public struct SearchResult { + + /// Title that should be shown whenever a result is displayed. + var displayTitle: String + + /// Should be shown as a subtitle wherever results are shown. + var displaySubtitle: String? + + /// Image, in the native platform's format, for the result. This should be the marker that would be + /// shown on the map, and also shown in the UI. This property is available for convenience so the + /// UI doesn't have to worry about whether the GeoElement is a graphic or a feature when displaying + /// the icon in the UI. + var markerImage: UIImage? + + /// Reference to the search source that created this result. + var owningSource: SearchSourceProtocol + + /// For locator results, should be the graphic that was used to display the result on the map. + /// For feature layer results, should be the result feature. Can be null depending on the type of the + /// result, and can have GeoElements without a defined geometry. + var geoElement: GeoElement? + + /// The viewpoint to be used when the view zooms to a selected result. This property can be `nil` + /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. + var selectionViewpoint: Viewpoint? +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift new file mode 100644 index 000000000..66340734b --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift @@ -0,0 +1,61 @@ +// Copyright 2021 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 + +/// Defines the contract for a search result provider. +public protocol SearchSourceProtocol { + /// Name to show when presenting this source in the UI. + var displayName: String { get set } + + /// The maximum results to return when performing a search. Most sources default to 6. + var maximumResults: Int { get set } + + /// The maximum suggestions to return. Most sources default to 6. + var maximumSuggestions: Int { get set } + + /// Area to be used as a constraint for searches and suggestions. + var searchArea: Geometry? { get set } + + /// Point to be used as an input to searches and suggestions. + var preferredSearchLocation: Point? { get set } + + /// Gets suggestions. + /// - Parameters: + /// - queryString: Text to be used for query. + /// - cancellationToken: Token used for cooperative cancellation. + /// - Returns: The array of suggestions. + func suggest(_ queryString: String, cancelationToken: String) async throws -> [SearchSuggestion] + + /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the + /// `searchArea` property may be consulted but does not need to be used as a strict limit. + /// - Parameters: + /// - queryString: Text to be used for query. + /// - area: Area to be used to constrain search results. + /// - cancellationToken: Token used for cooperative cancellation. + /// - Returns: Array of `SearchResult`s + func search(_ queryString: String, + area: Geometry?, + cancellationToken: String?) async throws -> [SearchResult] + + /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the + /// `searchArea` property may be consulted but does not need to be used as a strict limit. + /// - Parameters: + /// - searchSuggestion: Suggestion to be used as basis for search. + /// - area: Area to be used to constrain search results. + /// - cancellationToken: Token used for cooperative cancellation. + /// - Returns: Array of `SearchResult`s + func search(_ searchSuggestion: SearchSuggestion, + area: Geometry?, + cancellationToken: String?) async throws -> [SearchResult] +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift new file mode 100644 index 000000000..456a8685f --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -0,0 +1,38 @@ +// Copyright 2021 Esri. + +import ArcGIS + +// 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. + +/// Wraps a suggestion for display. +public struct SearchSuggestion { + /// Title that should be used when displaying a suggestion. + var displayTitle: String + + /// Optional subtitle that can be displayed when showing a suggestion. + var displaySubtitle: String + + /// Reference to the ISearchSource that created this suggestion. This property is necessary for the + /// view model to be able to accept a suggestion, because a suggestion should only be used with the + /// locator that created it. + var owningSource: SearchSourceProtocol + + /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and + /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. + var suggestResult: SuggestResult? + + /// True if the search from this suggestion should be treated like a collection search, false if the + /// search would return a single result. This property should be used to display a different icon + /// in the UI depending on if this is a category search (like 'Coffee', 'Pizza', or 'Starbucks') and + /// false if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). + var isCollection: Bool +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift new file mode 100644 index 000000000..a4980776d --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -0,0 +1,65 @@ +// Copyright 2021 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 Combine +import ArcGIS + +/// SearchView presents a search experience, powered by underlying SearchViewModel. +public struct SearchView: View { + /// Used for accessing `GeoView` functionality for geocoding and searching. + /// Reference to the GeoView used for automatic configuration. + /// When connected to a GeoView, SearchView will automatically navigate the view in response to + /// search result changes. Additionally, the view's current center and extent will be automatically + /// provided to locators as parameters. + var proxy: GeoViewProxy + + /// Determines whether the view will update its configuration based on the attached geoview's + /// document automatically. + var enableAutoConfiguration: Bool = true + + /// Determines whether a button that allows the user to repeat a search with a spatial constraint + /// is displayed automatically. Set to false if you want to use a custom button, for example so that + /// you can place it elsewhere on the map. `SearchViewModel` has properties and methods + /// you can use to determine when the custom button should be visible and to trigger the search + /// repeat behavior. + var enableRepeatSearchHereButton: Bool = true + + /// Determines whether a built-in result view will be shown. If false, the result display/selection + /// list is not shown. Set to false if you want to define a custom result list. You might use a + /// custom result list to show results in a separate list, disconnected from the rest of the search view. + var enableResultListView: Bool = true + + /// Message to show when there are no results or suggestions. + var noResultMessage: String = "No results found" + + /// The view model used by the view. The `ViewModel` manages state and handles the activity of + /// searching. The view observes `ViewModel` for changes in state. The view calls methods on + /// `ViewModel` in response to user action. The `ViewModel` is created automatically by the + /// view upon construction. If `EnableAutoconfiguration` is true, the view calls + /// `SearchViewModel.ConfigureForMap` for the map/scene whenever it changes. Both + /// the associated `GeoView` and the `GeoView`'s document can change after initial configuration. + var searchViewModel: SearchViewModel + + public var body: some View { + ZStack { +// TextField("Search", +// text: $searchText) { editing in +// print("editing changed") +// } onCommit: { +// print("On commit") +// } + + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift new file mode 100644 index 000000000..702fae7cb --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -0,0 +1,139 @@ +// Copyright 2021 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 + +/// Defines how many results to return; one, many, or automatic based on circumstance. +public enum SearchResultMode { + /// Search should always result in at most one result. + case single + /// Search should always try to return multiple results. + case multiple + /// Search should make a choice based on context. E.g. 'coffee shop' should be multiple results, + /// while '380 New York St. Redlands' should be one result. + case automatic +} + +/// Performs searches and manages search state for a Search, or optionally without a UI connection. +public struct SearchViewModel { + /// The string shown in the search view when no user query is entered. + /// Default is "Find a place or address", or read from web map JSON if specified in the web map configuration. + var defaultPlaceHolder: String = "Find a place or address" + + /// Tracks the currently active search source. All sources are used if this property is `nil`. + var activeSource: SearchSourceProtocol? + + /// Tracks the current user-entered query. This should be updated by the view after every key press. + /// This property drives both suggestions and searches. This property can be changed by + /// other method calls and property changes within the view model, so the view should take care to + /// observe for changes. + @State var currentQuery: String? + + /// The search area to be used for the current query. Ignored in most queries, unless the + /// `RestrictToArea` property is set to true when calling `CommitSearch`. This property + /// should be updated as the user navigates the map/scene, or at minimum before calling `CommitSearch`. + var queryArea: Geometry? + + /// Defines the center for the search. This should be updated by the view every time the + /// user navigates the map. + var queryCenter: Point? + + /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate + /// number of results is returned based on the type of suggestion chosen (driven by the IsCollection property). + var resultMode: SearchResultMode + + /// Collection of results. `nil` means no query has been made. An empty array means there + /// were no results, and the view should show an appropriate 'no results' message. + var results: [SearchResult]? + + /// Tracks selection of results from the `results` collection. When there is only one result, + /// that result is automatically assigned to this property. If there are multiple results, the view sets + /// this property upon user selection. This property is observable. The view should observe this + /// property and update the associated GeoView's viewpoint, if configured. + @State var selectedResult: SearchResult? + + /// Collection of search sources to be used. This list is maintained over time and is not nullable. + /// The view should observe this list for changes. Consumers should add and remove sources from + /// this list as needed. + @State var sources: [SearchSourceProtocol] = [] + + /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there + /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, + /// a useful 'no results' message should be shown by the view. + var suggestions: [SearchSuggestion]? + + /// True if the `QueryArea` has changed since the `Results` collection has been set. + /// This property is used by the view to enable 'Repeat search here' functionality. This property is + /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to + /// this property are driven by changes to the `QueryArea` property. + @State var isEligibleForRequery: Bool = false + + /// Starts a search. `SelectedResult` and `Results`, among other properties, are set + /// asynchronously. Other query properties are read to define the parameters of the search. + /// Participates in cooperative cancellation using the supplied cancellation token. + /// If `restrictToArea` is true, only results in the query area will be returned. + /// - Parameter restrictToArea: If true, the search is restricted to results within the extent + /// of the `QueryArea` property. Behavior when called with `RestrictToArea` set to true + /// when the `QueryArea` property is null, a line, a point, or an empty geometry is undefined. + func commitSearch(_ restrictToArea: Bool) async { + + } + + /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion + /// requests before initiating new ones. The view should also wait for some time after user finishes + /// typing before making suggestions. The JavaScript implementation uses 150ms by default. + /// - Parameter cancellationToken: Token used for cooperative cancellation. + func updateSuggestions(_ cancellationToken: String?) async { + + } + + /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is + /// generally the same as `CommitSearch`, except the suggestion is used instead of the + /// `CurrentQuery` property. When a suggestion is accepted, `CurrentQuery` is updated to + /// match the suggestion text. The view should take care not to submit a separate search in response + /// to changes to `CurrentQuery` initiated by a call to this method. + /// - Parameters: + /// - searchSuggestion: The suggestion to use to commit the search. + /// - cancellationToken: ken used for cooperative cancellation. + func accesptSuggestion(_ searchSuggestion: SearchSuggestion, + cancellationToken: String?) async { + + } + + /// Configures the view model for the provided map. By default, will only configure the view model + /// with the default world geocoder. In future updates, additional functionality may be added to take + /// web map configuration into account. + /// - Parameters: + /// - map: Map to use for configuration. + /// - cancellationToken: cancellationToken: String + func configureForMap(_ map: Map, cancellationToken: String?) { + + } + + /// Configures the view model for the provided scene. By default, will only configure the view model + /// with the default world geocoder. In future updates, additional functionality may be added to take + /// web scene configuration into account. + /// - Parameters: + /// - scene: Scene used for configuration. + /// - cancellationToke: Token used for cooperative cancellation. + func configureForScene(_ scene: ArcGIS.Scene, cancellationToken: String?) { + + } + + /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, + /// and reset the current query. + func clearSearch() { + + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift new file mode 100644 index 000000000..537ba8c6f --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -0,0 +1,182 @@ +// Copyright 2021 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 + +/// Extends `LocatorSearchSource` with intelligent search behaviors; adds support for features like +/// type-specific placemarks, repeated search, and more. Advanced functionality requires knowledge of the +/// underlying locator to be used well; this class implements behaviors that make assumptions about the +/// locator being the world geocode service. +public struct SmartLocatorSearchSource { + public init(repeatSearchResultThreshold: Int = 1, + repeatSuggestResultThreshold: Int = 6, + resultSymbolStyle: SymbolStyle? = nil, + locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!), + geocodeParameters: GeocodeParameters, + suggestParameters: SuggestParameters, + displayName: String = "Search", + maximumResults: Int = 6, + maximumSuggestions: Int = 6, + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil + ){ + self.repeatSearchResultThreshold = repeatSearchResultThreshold + self.repeatSuggestResultThreshold = repeatSuggestResultThreshold + self.resultSymbolStyle = resultSymbolStyle + self._locator = locator + self._geocodeParameters = geocodeParameters + self._suggestParameters = suggestParameters + self._displayName = displayName + self._maximumResults = maximumResults + self._maximumSuggestions = maximumSuggestions + self._searchArea = searchArea + self._preferredSearchLocation = preferredSearchLocation + } + + /// The minimum number of results to attempt to return. If there are too few results, the search is + /// repeated with loosened parameters until enough results are accumulated. If no search is + /// successful, it is still possible to have a total number of results less than this threshold. Does not + /// apply to repeated search with area constraint. Set to zero to disable search repeat behavior. + var repeatSearchResultThreshold: Int + + /// The minimum number of suggestions to attempt to return. If there are too few suggestions, + /// request is repeated with loosened constraints until enough suggestions are accumulated. + /// If no search is successful, it is still possible to have a total number of results less than this + /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search + /// repeat behavior. + var repeatSuggestResultThreshold: Int + + /// Web style used to find symbols for results. When set, symbols are found for results based on the + /// result's `Type` field, if available. Defaults to the style identified by the name + /// "Esri2DPointSymbolsStyle". The default Esri 2D point symbol has good results for many of the + /// types returned by the world geocode service. You can use this property to customize result icons + /// by publishing a web style, taking care to ensure that symbol keys match the `Type` attribute + /// returned by the locator. + var resultSymbolStyle: SymbolStyle? + + /// The locator used by this search source. + private var _locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!) + + /// Parameters used for geocoding. Some properties on parameters will be updated automatically + /// based on searches. + private var _geocodeParameters: GeocodeParameters + + /// Parameters used for getting suggestions. Some properties will be updated automatically + /// based on searches. + private var _suggestParameters: SuggestParameters + + /// Name to show when presenting this source in the UI. + private var _displayName: String + + /// The maximum results to return when performing a search. Most sources default to 6. + private var _maximumResults: Int + + /// The maximum suggestions to return. Most sources default to 6. + private var _maximumSuggestions: Int + + /// Area to be used as a constraint for searches and suggestions. + private var _searchArea: Geometry? + + /// Point to be used as an input to searches and suggestions. + private var _preferredSearchLocation: Point? + + // @State + // private var address: String = "" + // + // @State + // private var result: Result<[GeocodeResult], Error> = .success([]) +} + +extension SmartLocatorSearchSource: LocatorSearchSourceProtocol { + public var locator: LocatorTask { + get { + return _locator + } + } + + public var geocodeParameters: GeocodeParameters { + get { + return _geocodeParameters + } + } + + public var suggestParameters: SuggestParameters { + get { + return _suggestParameters + } + } + + public var displayName: String { + get { + return _displayName + } + set { + _displayName = newValue + } + } + + public var maximumResults: Int { + get { + return _maximumResults + } + set { + _maximumResults = newValue + } + } + + public var maximumSuggestions: Int { + get { + return _maximumSuggestions + } + set { + _maximumSuggestions = newValue + } + } + + public var searchArea: Geometry? { + get { + return _searchArea + } + set { + _searchArea = newValue + } + } + + public var preferredSearchLocation: Point? { + get { + return _preferredSearchLocation + } + set { + _preferredSearchLocation = newValue + } + } + + public func suggest(_ queryString: String, + cancelationToken: String) async throws -> [SearchSuggestion] { + return [] + } + + public func search(_ queryString: String, + area: Geometry?, + cancellationToken: String?) async throws -> [SearchResult] { + // result = await Result { try await locator.geocode(searchText: address) } + return [] + } + + public func search(_ searchSuggestion: SearchSuggestion, + area: Geometry?, + cancellationToken: String?) async throws -> [SearchResult] { + return [] + } +} From 91574735421671e44a763af5eea478f523616d11 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 23 Jun 2021 14:45:38 -0500 Subject: [PATCH 0063/1722] Rename Search -> SearchView --- Sources/ArcGISToolkit/Components/Search.swift | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Search.swift diff --git a/Sources/ArcGISToolkit/Components/Search.swift b/Sources/ArcGISToolkit/Components/Search.swift deleted file mode 100644 index d2a0ab9e3..000000000 --- a/Sources/ArcGISToolkit/Components/Search.swift +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2021 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 Combine -import ArcGIS - -public struct Search: View { - public var proxy: Binding - - @State private var searchText = "" - - public init(proxy: Binding) { - self.proxy = proxy - } - - public var body: some View { - ZStack { - TextField("Search", - text: $searchText) { editing in - print("editing changed") - } onCommit: { - print("On commit") - } - - } - } -} From f7d47d516a758727ae7d8f6fbc4902e574588b4d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 24 Jun 2021 15:09:10 -0500 Subject: [PATCH 0064/1722] Flesh out new classes/structs; UI features --- Examples/Examples.xcodeproj/project.pbxproj | 8 +- .../Examples/Examples/SearchExampleView.swift | 10 +- Package.swift | 4 +- .../Search/LocatorSearchSource.swift | 56 +++++++ .../Components/Search/SearchResult.swift | 15 +- .../Search/SearchSourceProtocol.swift | 1 + .../Components/Search/SearchSuggestion.swift | 11 ++ .../Components/Search/SearchView.swift | 75 ++++++++-- .../Components/Search/SearchViewModel.swift | 73 ++++++--- .../Search/SmartLocatorSearchSource.swift | 140 +----------------- .../Utility/EsriBorderViewModifier.swift | 48 ++++++ .../EsriDeleteTextButtonViewModifier.swift | 42 ++++++ .../EsriSearchButtonViewModifier.swift | 40 +++++ 13 files changed, 334 insertions(+), 189 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift create mode 100644 Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift create mode 100644 Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift create mode 100644 Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 2d4d35d71..98f999abd 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -273,7 +273,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -328,7 +328,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -349,7 +349,7 @@ DEVELOPMENT_TEAM = P8HGHS7JQ8; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = ExamplesApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -373,7 +373,7 @@ DEVELOPMENT_TEAM = P8HGHS7JQ8; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = ExamplesApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Examples/Examples/Examples/SearchExampleView.swift b/Examples/Examples/Examples/SearchExampleView.swift index e8d753ce4..944b50168 100644 --- a/Examples/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/Examples/SearchExampleView.swift @@ -19,11 +19,13 @@ import ArcGISToolkit struct SearchExampleView: View { var body: some View { ZStack (alignment: .topTrailing) { - MapViewReader { proxy in + MapViewReader { proxy in MapView(map: Map(basemap: Basemap.imageryWithLabels())) -// SearchView(proxy: proxy) -// .frame(width: 200, height: 132) -// .padding() + SearchView(proxy: proxy, + searchViewModel: SearchViewModel() + ) + .frame(width: 300, height: 132) + .padding() } } } diff --git a/Package.swift b/Package.swift index e5eaac6c8..145609a6d 100644 --- a/Package.swift +++ b/Package.swift @@ -18,8 +18,8 @@ import PackageDescription let package = Package( name: "arcgis-runtime-toolkit-swift", platforms: [ - .iOS(.v14), - .macOS(.v11) + .iOS("15"), + .macOS("12") ], products: [ .library( diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift new file mode 100644 index 000000000..6efcb8664 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -0,0 +1,56 @@ +// Copyright 2021 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 Foundation +import ArcGIS + +/// Uses a Locator to provide search and suggest results. Most configuration should be done on the +/// `GeocodeParameters` directly. +public class LocatorSearchSource { + /// The locator used by this search source. + var locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!) + + /// Parameters used for geocoding. Some properties on parameters will be updated automatically + /// based on searches. + var geocodeParameters: GeocodeParameters = GeocodeParameters() + + /// Parameters used for getting suggestions. Some properties will be updated automatically + /// based on searches. + var suggestParameters: SuggestParameters = SuggestParameters() + + public var displayName: String = "Search" + + public var maximumResults: Int = 6 + + public var maximumSuggestions: Int = 6 + + public var searchArea: Geometry? + + public var preferredSearchLocation: Point? +} + +extension LocatorSearchSource: SearchSourceProtocol { + public func suggest(_ queryString: String, cancelationToken: String) async throws -> [SearchSuggestion] { + return [] + } + + public func search(_ queryString: String, area: Geometry?, cancellationToken: String?) async throws -> [SearchResult] { + return [] + } + + public func search(_ searchSuggestion: SearchSuggestion, area: Geometry?, cancellationToken: String?) async throws -> [SearchResult] { + return [] + } + + +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 07c8a4986..67edd5415 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -1,5 +1,16 @@ // Copyright 2021 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 UIKit.UIImage import ArcGIS @@ -25,7 +36,7 @@ public struct SearchResult { /// Image, in the native platform's format, for the result. This should be the marker that would be /// shown on the map, and also shown in the UI. This property is available for convenience so the - /// UI doesn't have to worry about whether the GeoElement is a graphic or a feature when displaying + /// UI doesn't have to worry about whether the `GeoElement` is a graphic or a feature when displaying /// the icon in the UI. var markerImage: UIImage? @@ -34,7 +45,7 @@ public struct SearchResult { /// For locator results, should be the graphic that was used to display the result on the map. /// For feature layer results, should be the result feature. Can be null depending on the type of the - /// result, and can have GeoElements without a defined geometry. + /// result, and can have `GeoElement`s without a defined geometry. var geoElement: GeoElement? /// The viewpoint to be used when the view zooms to a selected result. This property can be `nil` diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift index 66340734b..4aa921f84 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift @@ -12,6 +12,7 @@ // limitations under the License. import ArcGIS +import Foundation /// Defines the contract for a search result provider. public protocol SearchSourceProtocol { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 456a8685f..6e8c0c004 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -1,5 +1,16 @@ // Copyright 2021 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 // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a4980776d..a352f58b6 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -17,17 +17,41 @@ import ArcGIS /// SearchView presents a search experience, powered by underlying SearchViewModel. public struct SearchView: View { + public init(proxy: GeoViewProxy, + searchViewModel: SearchViewModel, + enableAutomaticConfiguration: Bool = true, + enableRepeatSearchHereButton: Bool = true, + enableResultListView: Bool = true, + noResultMessage: String = "No results found") { + self.proxy = proxy + self.searchViewModel = searchViewModel + self.enableAutomaticConfiguration = enableAutomaticConfiguration + self.enableRepeatSearchHereButton = enableRepeatSearchHereButton + self.enableResultListView = enableResultListView + self.noResultMessage = noResultMessage + } + /// Used for accessing `GeoView` functionality for geocoding and searching. /// Reference to the GeoView used for automatic configuration. /// When connected to a GeoView, SearchView will automatically navigate the view in response to /// search result changes. Additionally, the view's current center and extent will be automatically /// provided to locators as parameters. var proxy: GeoViewProxy + + /// The view model used by the view. The `ViewModel` manages state and handles the activity of + /// searching. The view observes `ViewModel` for changes in state. The view calls methods on + /// `ViewModel` in response to user action. The `ViewModel` is created automatically by the + /// view upon construction. If `enableAutomaticConfiguration` is true, the view calls + /// `SearchViewModel.ConfigureForMap` for the map/scene whenever it changes. Both + /// the associated `GeoView` and the `GeoView`'s document can change after initial configuration. + @ObservedObject + var searchViewModel: SearchViewModel /// Determines whether the view will update its configuration based on the attached geoview's /// document automatically. - var enableAutoConfiguration: Bool = true + var enableAutomaticConfiguration: Bool = true + @State /// Determines whether a button that allows the user to repeat a search with a spatial constraint /// is displayed automatically. Set to false if you want to use a custom button, for example so that /// you can place it elsewhere on the map. `SearchViewModel` has properties and methods @@ -43,23 +67,42 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. var noResultMessage: String = "No results found" - /// The view model used by the view. The `ViewModel` manages state and handles the activity of - /// searching. The view observes `ViewModel` for changes in state. The view calls methods on - /// `ViewModel` in response to user action. The `ViewModel` is created automatically by the - /// view upon construction. If `EnableAutoconfiguration` is true, the view calls - /// `SearchViewModel.ConfigureForMap` for the map/scene whenever it changes. Both - /// the associated `GeoView` and the `GeoView`'s document can change after initial configuration. - var searchViewModel: SearchViewModel + @State + /// Indicates that the `SearchViewModel` should start a search. + private var commitSearch: Bool = false + + @State + /// Indicates that the geoView's viewpoint has changed since the last search. + private var viewpointChanged: Bool = false public var body: some View { - ZStack { -// TextField("Search", -// text: $searchText) { editing in -// print("editing changed") -// } onCommit: { -// print("On commit") -// } - + VStack (alignment: .center) { + TextField(searchViewModel.defaultPlaceHolder, + text: $searchViewModel.currentQuery) { editing in + // For when editing state changes (becomes/looses firstResponder) + } onCommit: { + commitSearch = true + } + .esriDeleteTextButton(text: $searchViewModel.currentQuery) + .esriSearchButton(performSearch: $commitSearch) + .esriBorder() + if enableRepeatSearchHereButton, viewpointChanged { + Button("Search Here") { + viewpointChanged = false + commitSearch = true + } + .esriBorder() + } + } + .task(id: searchViewModel.currentQuery) { + // For when user types a new character + await searchViewModel.updateSuggestions(nil) + } + .task(id: commitSearch) { + // For when user commits changes (hits Enter/Search button) + guard commitSearch else { return } + commitSearch = false + await searchViewModel.commitSearch(true) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 702fae7cb..dd882ec69 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -26,7 +26,27 @@ public enum SearchResultMode { } /// Performs searches and manages search state for a Search, or optionally without a UI connection. -public struct SearchViewModel { +public class SearchViewModel: ObservableObject { + public convenience init(defaultPlaceHolder: String = "Find a place or address", + activeSource: SearchSourceProtocol? = nil, + queryArea: Geometry? = nil, + queryCenter: Point? = nil, + resultMode: SearchResultMode = .automatic, + results: [SearchResult]? = nil, + sources: [SearchSourceProtocol] = [], + suggestions: [SearchSuggestion]? = nil + ) { + self.init() + self.defaultPlaceHolder = defaultPlaceHolder + self.activeSource = activeSource + self.queryArea = queryArea + self.queryCenter = queryCenter + self.resultMode = resultMode + self.results = results + self.sources = sources + self.suggestions = suggestions + } + /// The string shown in the search view when no user query is entered. /// Default is "Find a place or address", or read from web map JSON if specified in the web map configuration. var defaultPlaceHolder: String = "Find a place or address" @@ -38,11 +58,12 @@ public struct SearchViewModel { /// This property drives both suggestions and searches. This property can be changed by /// other method calls and property changes within the view model, so the view should take care to /// observe for changes. - @State var currentQuery: String? + @Published + var currentQuery: String = "" /// The search area to be used for the current query. Ignored in most queries, unless the - /// `RestrictToArea` property is set to true when calling `CommitSearch`. This property - /// should be updated as the user navigates the map/scene, or at minimum before calling `CommitSearch`. + /// `RestrictToArea` property is set to true when calling `commitSearch`. This property + /// should be updated as the user navigates the map/scene, or at minimum before calling `commitSearch`. var queryArea: Geometry? /// Defines the center for the search. This should be updated by the view every time the @@ -51,43 +72,46 @@ public struct SearchViewModel { /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate /// number of results is returned based on the type of suggestion chosen (driven by the IsCollection property). - var resultMode: SearchResultMode + var resultMode: SearchResultMode = .automatic /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. var results: [SearchResult]? + @Published /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. - @State var selectedResult: SearchResult? + var selectedResult: SearchResult? /// Collection of search sources to be used. This list is maintained over time and is not nullable. /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. - @State var sources: [SearchSourceProtocol] = [] + var sources: [SearchSourceProtocol] = [] /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. var suggestions: [SearchSuggestion]? - /// True if the `QueryArea` has changed since the `Results` collection has been set. + /// True if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to - /// this property are driven by changes to the `QueryArea` property. - @State var isEligibleForRequery: Bool = false + /// this property are driven by changes to the `queryArea` property. + @Published + var isEligibleForRequery: Bool = false - /// Starts a search. `SelectedResult` and `Results`, among other properties, are set + /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. /// Participates in cooperative cancellation using the supplied cancellation token. /// If `restrictToArea` is true, only results in the query area will be returned. /// - Parameter restrictToArea: If true, the search is restricted to results within the extent - /// of the `QueryArea` property. Behavior when called with `RestrictToArea` set to true - /// when the `QueryArea` property is null, a line, a point, or an empty geometry is undefined. + /// of the `queryArea` property. Behavior when called with `restrictToArea` set to true + /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. func commitSearch(_ restrictToArea: Bool) async { - + guard !currentQuery.isEmpty else { return } + print("SearchViewModel.commitSearch: \(currentQuery)") } /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion @@ -95,20 +119,21 @@ public struct SearchViewModel { /// typing before making suggestions. The JavaScript implementation uses 150ms by default. /// - Parameter cancellationToken: Token used for cooperative cancellation. func updateSuggestions(_ cancellationToken: String?) async { - + guard !currentQuery.isEmpty else { return } + print("SearchViewModel.updateSuggestions: \(currentQuery)") } /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is - /// generally the same as `CommitSearch`, except the suggestion is used instead of the - /// `CurrentQuery` property. When a suggestion is accepted, `CurrentQuery` is updated to + /// generally the same as `commitSearch`, except the suggestion is used instead of the + /// `currentQuery` property. When a suggestion is accepted, `currentQuery` is updated to /// match the suggestion text. The view should take care not to submit a separate search in response - /// to changes to `CurrentQuery` initiated by a call to this method. + /// to changes to `currentQuery` initiated by a call to this method. /// - Parameters: /// - searchSuggestion: The suggestion to use to commit the search. /// - cancellationToken: ken used for cooperative cancellation. - func accesptSuggestion(_ searchSuggestion: SearchSuggestion, - cancellationToken: String?) async { - + func acceptSuggestion(_ searchSuggestion: SearchSuggestion, + cancellationToken: String?) async { + print("SearchViewModel.acceptSuggestion") } /// Configures the view model for the provided map. By default, will only configure the view model @@ -118,7 +143,7 @@ public struct SearchViewModel { /// - map: Map to use for configuration. /// - cancellationToken: cancellationToken: String func configureForMap(_ map: Map, cancellationToken: String?) { - + print("SearchViewModel.configureForMap") } /// Configures the view model for the provided scene. By default, will only configure the view model @@ -128,12 +153,12 @@ public struct SearchViewModel { /// - scene: Scene used for configuration. /// - cancellationToke: Token used for cooperative cancellation. func configureForScene(_ scene: ArcGIS.Scene, cancellationToken: String?) { - + print("SearchViewModel.configureForScene") } /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, /// and reset the current query. func clearSearch() { - + print("SearchViewModel.clearSearch") } } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 537ba8c6f..68a476443 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -18,44 +18,19 @@ import ArcGIS /// type-specific placemarks, repeated search, and more. Advanced functionality requires knowledge of the /// underlying locator to be used well; this class implements behaviors that make assumptions about the /// locator being the world geocode service. -public struct SmartLocatorSearchSource { - public init(repeatSearchResultThreshold: Int = 1, - repeatSuggestResultThreshold: Int = 6, - resultSymbolStyle: SymbolStyle? = nil, - locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!), - geocodeParameters: GeocodeParameters, - suggestParameters: SuggestParameters, - displayName: String = "Search", - maximumResults: Int = 6, - maximumSuggestions: Int = 6, - searchArea: Geometry? = nil, - preferredSearchLocation: Point? = nil - ){ - self.repeatSearchResultThreshold = repeatSearchResultThreshold - self.repeatSuggestResultThreshold = repeatSuggestResultThreshold - self.resultSymbolStyle = resultSymbolStyle - self._locator = locator - self._geocodeParameters = geocodeParameters - self._suggestParameters = suggestParameters - self._displayName = displayName - self._maximumResults = maximumResults - self._maximumSuggestions = maximumSuggestions - self._searchArea = searchArea - self._preferredSearchLocation = preferredSearchLocation - } - +public class SmartLocatorSearchSource: LocatorSearchSource { /// The minimum number of results to attempt to return. If there are too few results, the search is /// repeated with loosened parameters until enough results are accumulated. If no search is /// successful, it is still possible to have a total number of results less than this threshold. Does not /// apply to repeated search with area constraint. Set to zero to disable search repeat behavior. - var repeatSearchResultThreshold: Int + var repeatSearchResultThreshold: Int = 1 /// The minimum number of suggestions to attempt to return. If there are too few suggestions, /// request is repeated with loosened constraints until enough suggestions are accumulated. /// If no search is successful, it is still possible to have a total number of results less than this /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search /// repeat behavior. - var repeatSuggestResultThreshold: Int + var repeatSuggestResultThreshold: Int = 6 /// Web style used to find symbols for results. When set, symbols are found for results based on the /// result's `Type` field, if available. Defaults to the style identified by the name @@ -65,118 +40,9 @@ public struct SmartLocatorSearchSource { /// returned by the locator. var resultSymbolStyle: SymbolStyle? - /// The locator used by this search source. - private var _locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!) - - /// Parameters used for geocoding. Some properties on parameters will be updated automatically - /// based on searches. - private var _geocodeParameters: GeocodeParameters - - /// Parameters used for getting suggestions. Some properties will be updated automatically - /// based on searches. - private var _suggestParameters: SuggestParameters - - /// Name to show when presenting this source in the UI. - private var _displayName: String - - /// The maximum results to return when performing a search. Most sources default to 6. - private var _maximumResults: Int - - /// The maximum suggestions to return. Most sources default to 6. - private var _maximumSuggestions: Int - - /// Area to be used as a constraint for searches and suggestions. - private var _searchArea: Geometry? - - /// Point to be used as an input to searches and suggestions. - private var _preferredSearchLocation: Point? - // @State // private var address: String = "" // // @State // private var result: Result<[GeocodeResult], Error> = .success([]) } - -extension SmartLocatorSearchSource: LocatorSearchSourceProtocol { - public var locator: LocatorTask { - get { - return _locator - } - } - - public var geocodeParameters: GeocodeParameters { - get { - return _geocodeParameters - } - } - - public var suggestParameters: SuggestParameters { - get { - return _suggestParameters - } - } - - public var displayName: String { - get { - return _displayName - } - set { - _displayName = newValue - } - } - - public var maximumResults: Int { - get { - return _maximumResults - } - set { - _maximumResults = newValue - } - } - - public var maximumSuggestions: Int { - get { - return _maximumSuggestions - } - set { - _maximumSuggestions = newValue - } - } - - public var searchArea: Geometry? { - get { - return _searchArea - } - set { - _searchArea = newValue - } - } - - public var preferredSearchLocation: Point? { - get { - return _preferredSearchLocation - } - set { - _preferredSearchLocation = newValue - } - } - - public func suggest(_ queryString: String, - cancelationToken: String) async throws -> [SearchSuggestion] { - return [] - } - - public func search(_ queryString: String, - area: Geometry?, - cancellationToken: String?) async throws -> [SearchResult] { - // result = await Result { try await locator.geocode(searchText: address) } - return [] - } - - public func search(_ searchSuggestion: SearchSuggestion, - area: Geometry?, - cancellationToken: String?) async throws -> [SearchResult] { - return [] - } -} diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift new file mode 100644 index 000000000..6c722e1aa --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -0,0 +1,48 @@ +// Copyright 2021 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 + +struct EsriBorderViewModifier: ViewModifier { + func body(content: Content) -> some View { + let roundedRect = RoundedRectangle(cornerRadius: 8) + content + .padding(EdgeInsets(top: 8, + leading: 12, + bottom: 8, + trailing: 12 + ) + ) + .background(Color.white) + .clipShape(roundedRect) + .overlay( + roundedRect + .stroke(lineWidth: 2) + .foregroundColor(.blue) + ) + .shadow(color: Color.gray.opacity(0.6), + radius: 3, + x: 1, + y: 2 + ) + } +} + +extension View { + func esriBorder() -> some View { + ModifiedContent( + content: self, + modifier: EsriBorderViewModifier() + ) + } +} diff --git a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift new file mode 100644 index 000000000..795a9baa7 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift @@ -0,0 +1,42 @@ +// Copyright 2021 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 + +struct EsriDeleteTextButtonViewModifier: ViewModifier { + @Binding var text: String + + func body(content: Content) -> some View { + HStack { + content + if !text.isEmpty { + Button( + action: { self.text = "" }, + label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(Color(UIColor.opaqueSeparator)) + } + ) + } + } + } +} + +extension View { + func esriDeleteTextButton(text: Binding) -> some View { + ModifiedContent( + content: self, + modifier: EsriDeleteTextButtonViewModifier(text: text) + ) + } +} diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift new file mode 100644 index 000000000..8933b1051 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -0,0 +1,40 @@ +// Copyright 2021 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 + +struct EsriSearchButtonViewModifier: ViewModifier { + @Binding var performSearch: Bool + + func body(content: Content) -> some View { + HStack { + content + Button( + action: { self.performSearch = true }, + label: { + Image(systemName: "magnifyingglass.circle.fill") + .foregroundColor(Color(UIColor.opaqueSeparator)) + } + ) + } + } +} + +extension View { + func esriSearchButton(performSearch: Binding) -> some View { + ModifiedContent( + content: self, + modifier: EsriSearchButtonViewModifier(performSearch: performSearch) + ) + } +} From 000c126c98c75d942be7f3aee07fb50366fee383 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 24 Jun 2021 15:09:34 -0500 Subject: [PATCH 0065/1722] Remove unneeded file --- .../Search/LocatorSearchSourceProtocol.swift | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift deleted file mode 100644 index cc0c53c8d..000000000 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSourceProtocol.swift +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2021 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 - -/// Uses a Locator to provide search and suggest results. Most configuration should be done on the -/// `GeocodeParameters` directly. -public protocol LocatorSearchSourceProtocol: SearchSourceProtocol { - /// The locator used by this search source. - var locator: LocatorTask { get } - - /// Parameters used for geocoding. Some properties on parameters will be updated automatically - /// based on searches. - var geocodeParameters: GeocodeParameters { get } - - /// Parameters used for getting suggestions. Some properties will be updated automatically - /// based on searches. - var suggestParameters: SuggestParameters { get } -} From b42c7037b5d29dca9114a30a0edfbbe8d943324a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 25 Jun 2021 15:51:58 -0500 Subject: [PATCH 0066/1722] Good initial geocode progress; multiple sources, [SearchResult] use. --- .../Examples/Examples/SearchExampleView.swift | 9 ++-- .../Search/LocatorSearchSource.swift | 38 +++++++++++++- .../Components/Search/SearchResult.swift | 15 +++++- .../Components/Search/SearchView.swift | 51 +++++++++++++++---- .../Components/Search/SearchViewModel.swift | 15 +++++- Sources/ArcGISToolkit/Extensions/Result.swift | 19 +++++++ .../EsriSearchButtonViewModifier.swift | 2 +- 7 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Result.swift diff --git a/Examples/Examples/Examples/SearchExampleView.swift b/Examples/Examples/Examples/SearchExampleView.swift index 944b50168..37438b03f 100644 --- a/Examples/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/Examples/SearchExampleView.swift @@ -17,14 +17,15 @@ import ArcGIS import ArcGISToolkit struct SearchExampleView: View { + let searchViewModel = SearchViewModel( + sources: [LocatorSearchSource(), + LocatorSearchSource()] + ) var body: some View { ZStack (alignment: .topTrailing) { MapViewReader { proxy in MapView(map: Map(basemap: Basemap.imageryWithLabels())) - SearchView(proxy: proxy, - searchViewModel: SearchViewModel() - ) - .frame(width: 300, height: 132) + SearchView(proxy: proxy, searchViewModel:searchViewModel) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 6efcb8664..dffcc8653 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -17,6 +17,24 @@ import ArcGIS /// Uses a Locator to provide search and suggest results. Most configuration should be done on the /// `GeocodeParameters` directly. public class LocatorSearchSource { + public init(locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!), + geocodeParameters: GeocodeParameters = GeocodeParameters(), + suggestParameters: SuggestParameters = SuggestParameters(), + displayName: String = "Search", + maximumResults: Int = 6, + maximumSuggestions: Int = 6, + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil) { + self.locator = locator + self.geocodeParameters = geocodeParameters + self.suggestParameters = suggestParameters + self.displayName = displayName + self.maximumResults = maximumResults + self.maximumSuggestions = maximumSuggestions + self.searchArea = searchArea + self.preferredSearchLocation = preferredSearchLocation + } + /// The locator used by this search source. var locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!) @@ -27,7 +45,7 @@ public class LocatorSearchSource { /// Parameters used for getting suggestions. Some properties will be updated automatically /// based on searches. var suggestParameters: SuggestParameters = SuggestParameters() - + public var displayName: String = "Search" public var maximumResults: Int = 6 @@ -45,7 +63,9 @@ extension LocatorSearchSource: SearchSourceProtocol { } public func search(_ queryString: String, area: Geometry?, cancellationToken: String?) async throws -> [SearchResult] { - return [] + let geocodeResults = try await locator.geocode(searchText: queryString) + //convert to search results + return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } public func search(_ searchSuggestion: SearchSuggestion, area: Geometry?, cancellationToken: String?) async throws -> [SearchResult] { @@ -54,3 +74,17 @@ extension LocatorSearchSource: SearchSourceProtocol { } + +// TODO: add locator name from Locator.locatorInfo.name (can't do it here because search source is no LocatorSearchSource, but maybe could subclass toSearchResult.... +// TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. +// TODO: Should move this to the Extensions folder in it's own file (along with Identifieable compliance). +extension GeocodeResult { + func toSearchResult(searchSource: SearchSourceProtocol) -> SearchResult { + return SearchResult(displayTitle: self.label, + displaySubtitle: "Score: \((self.score).formatted(.percent))", + markerImage: nil, + owningSource: searchSource, + geoElement: nil, + selectionViewpoint: nil) + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 67edd5415..5a0ddddf6 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -26,7 +26,20 @@ import ArcGIS // limitations under the License. /// Wraps a search result for display. -public struct SearchResult { +public class SearchResult { + public init(displayTitle: String, + displaySubtitle: String? = nil, + markerImage: UIImage? = nil, + owningSource: SearchSourceProtocol, + geoElement: GeoElement? = nil, + selectionViewpoint: Viewpoint? = nil) { + self.displayTitle = displayTitle + self.displaySubtitle = displaySubtitle + self.markerImage = markerImage + self.owningSource = owningSource + self.geoElement = geoElement + self.selectionViewpoint = selectionViewpoint + } /// Title that should be shown whenever a result is displayed. var displayTitle: String diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a352f58b6..e056bf333 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -37,7 +37,7 @@ public struct SearchView: View { /// search result changes. Additionally, the view's current center and extent will be automatically /// provided to locators as parameters. var proxy: GeoViewProxy - + /// The view model used by the view. The `ViewModel` manages state and handles the activity of /// searching. The view observes `ViewModel` for changes in state. The view calls methods on /// `ViewModel` in response to user action. The `ViewModel` is created automatically by the @@ -69,40 +69,73 @@ public struct SearchView: View { @State /// Indicates that the `SearchViewModel` should start a search. - private var commitSearch: Bool = false + private var shouldCommitSearch: Bool = false @State /// Indicates that the geoView's viewpoint has changed since the last search. private var viewpointChanged: Bool = false + @State + private var result: Result<[SearchResult], Error> = .success([]) + public var body: some View { VStack (alignment: .center) { TextField(searchViewModel.defaultPlaceHolder, text: $searchViewModel.currentQuery) { editing in // For when editing state changes (becomes/looses firstResponder) } onCommit: { - commitSearch = true + shouldCommitSearch.toggle() } .esriDeleteTextButton(text: $searchViewModel.currentQuery) - .esriSearchButton(performSearch: $commitSearch) + .esriSearchButton(performSearch: $shouldCommitSearch) .esriBorder() if enableRepeatSearchHereButton, viewpointChanged { Button("Search Here") { viewpointChanged = false - commitSearch = true + shouldCommitSearch.toggle() } .esriBorder() } + switch result { + case .success(let results): + if results.count > 0 { + List(results) { result in + VStack (alignment: .leading){ + Text(result.displayTitle) + .font(.callout) + if let subtitle = result.displaySubtitle { + Text(subtitle) + .font(.caption) + } + } + } + // TODO: Figure out better styling for list + // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource +// .listStyle(DefaultListStyle()) + } + case .failure(let error): + Text("Error occurred: \(error.localizedDescription)") + Spacer() + } } .task(id: searchViewModel.currentQuery) { // For when user types a new character + guard !searchViewModel.currentQuery.isEmpty else { + result = .success([]) + return + } await searchViewModel.updateSuggestions(nil) } - .task(id: commitSearch) { + .task(id: shouldCommitSearch) { // For when user commits changes (hits Enter/Search button) - guard commitSearch else { return } - commitSearch = false - await searchViewModel.commitSearch(true) + print("geocoding...") + result = await Result { try await searchViewModel.commitSearch(true) } } } } + +// MARK: Extensions + +extension SearchResult: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(self) } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index dd882ec69..b3a206897 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Swift import SwiftUI import ArcGIS @@ -109,9 +110,19 @@ public class SearchViewModel: ObservableObject { /// - Parameter restrictToArea: If true, the search is restricted to results within the extent /// of the `queryArea` property. Behavior when called with `restrictToArea` set to true /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. - func commitSearch(_ restrictToArea: Bool) async { - guard !currentQuery.isEmpty else { return } + func commitSearch(_ restrictToArea: Bool) async throws -> [SearchResult] { + guard !currentQuery.isEmpty else { return [] } print("SearchViewModel.commitSearch: \(currentQuery)") + + var results = [SearchResult]() + for searchSource in sources { + let searchResults = try await searchSource.search(currentQuery, + area: restrictToArea ? queryArea : nil, + cancellationToken: "" + ) + results.append(contentsOf: searchResults) + } + return results } /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift new file mode 100644 index 000000000..e67f4984d --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -0,0 +1,19 @@ +// Copyright 2021 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. + +extension Result where Failure == Error { + init(awaiting task: () async throws -> Success) async { + do { self = .success(try await task()) } + catch { self = .failure(error) } + } +} diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index 8933b1051..f1f2ed133 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -20,7 +20,7 @@ struct EsriSearchButtonViewModifier: ViewModifier { HStack { content Button( - action: { self.performSearch = true }, + action: { self.performSearch.toggle() }, label: { Image(systemName: "magnifyingglass.circle.fill") .foregroundColor(Color(UIColor.opaqueSeparator)) From 1e85a9dbe990e1a606c8ea40bda28b8680361f39 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 28 Jun 2021 16:59:44 -0500 Subject: [PATCH 0067/1722] Get geocode and suggestions working; modify return types in protocol and how results are handled in the SearchView. --- .../Search/LocatorSearchSource.swift | 30 +--- .../Components/Search/SearchResult.swift | 17 +- .../Search/SearchSourceProtocol.swift | 11 +- .../Components/Search/SearchSuggestion.swift | 33 ++-- .../Components/Search/SearchView.swift | 56 +++--- .../Components/Search/SearchViewModel.swift | 160 ++++++++++++++---- .../Extensions/GeocodeResult.swift | 29 ++++ .../Extensions/SuggestResult.swift | 24 +++ 8 files changed, 248 insertions(+), 112 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/GeocodeResult.swift create mode 100644 Sources/ArcGISToolkit/Extensions/SuggestResult.swift diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index dffcc8653..51ac26fe5 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -58,33 +58,21 @@ public class LocatorSearchSource { } extension LocatorSearchSource: SearchSourceProtocol { - public func suggest(_ queryString: String, cancelationToken: String) async throws -> [SearchSuggestion] { - return [] + public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { + let suggestResults = try await locator.suggest(searchText: queryString) + //convert to search results + return suggestResults.map{ $0.toSearchSuggestion(searchSource: self) } } - public func search(_ queryString: String, area: Geometry?, cancellationToken: String?) async throws -> [SearchResult] { + public func search(_ queryString: String, area: Geometry? = nil) async throws -> [SearchResult] { let geocodeResults = try await locator.geocode(searchText: queryString) //convert to search results return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } - public func search(_ searchSuggestion: SearchSuggestion, area: Geometry?, cancellationToken: String?) async throws -> [SearchResult] { - return [] - } - - -} - -// TODO: add locator name from Locator.locatorInfo.name (can't do it here because search source is no LocatorSearchSource, but maybe could subclass toSearchResult.... -// TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. -// TODO: Should move this to the Extensions folder in it's own file (along with Identifieable compliance). -extension GeocodeResult { - func toSearchResult(searchSource: SearchSourceProtocol) -> SearchResult { - return SearchResult(displayTitle: self.label, - displaySubtitle: "Score: \((self.score).formatted(.percent))", - markerImage: nil, - owningSource: searchSource, - geoElement: nil, - selectionViewpoint: nil) + public func search(_ searchSuggestion: SearchSuggestion, area: Geometry? = nil) async throws -> [SearchResult] { + guard let query = searchSuggestion.suggestResult?.label else { return [] } + let geocodeResults = try await locator.geocode(searchText: query) + return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 5a0ddddf6..c9e877b59 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -14,17 +14,6 @@ import UIKit.UIImage import ArcGIS -// 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. - /// Wraps a search result for display. public class SearchResult { public init(displayTitle: String, @@ -65,3 +54,9 @@ public class SearchResult { /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. var selectionViewpoint: Viewpoint? } + +// MARK: Extensions + +extension SearchResult: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(self) } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift index 4aa921f84..0dd47e20d 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift @@ -34,29 +34,24 @@ public protocol SearchSourceProtocol { /// Gets suggestions. /// - Parameters: /// - queryString: Text to be used for query. - /// - cancellationToken: Token used for cooperative cancellation. /// - Returns: The array of suggestions. - func suggest(_ queryString: String, cancelationToken: String) async throws -> [SearchSuggestion] + func suggest(_ queryString: String) async throws -> [SearchSuggestion] /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the /// `searchArea` property may be consulted but does not need to be used as a strict limit. /// - Parameters: /// - queryString: Text to be used for query. /// - area: Area to be used to constrain search results. - /// - cancellationToken: Token used for cooperative cancellation. /// - Returns: Array of `SearchResult`s func search(_ queryString: String, - area: Geometry?, - cancellationToken: String?) async throws -> [SearchResult] + area: Geometry?) async throws -> [SearchResult] /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the /// `searchArea` property may be consulted but does not need to be used as a strict limit. /// - Parameters: /// - searchSuggestion: Suggestion to be used as basis for search. /// - area: Area to be used to constrain search results. - /// - cancellationToken: Token used for cooperative cancellation. /// - Returns: Array of `SearchResult`s func search(_ searchSuggestion: SearchSuggestion, - area: Geometry?, - cancellationToken: String?) async throws -> [SearchResult] + area: Geometry?) async throws -> [SearchResult] } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 6e8c0c004..9447ae3d5 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -13,26 +13,27 @@ import ArcGIS -// 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. - /// Wraps a suggestion for display. -public struct SearchSuggestion { +public class SearchSuggestion { + internal init(displayTitle: String, + displaySubtitle: String? = nil, + owningSource: SearchSourceProtocol, + suggestResult: SuggestResult? = nil, + isCollection: Bool) { + self.displayTitle = displayTitle + self.displaySubtitle = displaySubtitle + self.owningSource = owningSource + self.suggestResult = suggestResult + self.isCollection = isCollection + } + /// Title that should be used when displaying a suggestion. var displayTitle: String /// Optional subtitle that can be displayed when showing a suggestion. - var displaySubtitle: String + var displaySubtitle: String? - /// Reference to the ISearchSource that created this suggestion. This property is necessary for the + /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the /// view model to be able to accept a suggestion, because a suggestion should only be used with the /// locator that created it. var owningSource: SearchSourceProtocol @@ -47,3 +48,7 @@ public struct SearchSuggestion { /// false if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). var isCollection: Bool } + +extension SearchSuggestion: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(self) } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e056bf333..66fb17289 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -51,12 +51,12 @@ public struct SearchView: View { /// document automatically. var enableAutomaticConfiguration: Bool = true - @State /// Determines whether a button that allows the user to repeat a search with a spatial constraint /// is displayed automatically. Set to false if you want to use a custom button, for example so that /// you can place it elsewhere on the map. `SearchViewModel` has properties and methods /// you can use to determine when the custom button should be visible and to trigger the search /// repeat behavior. + @State var enableRepeatSearchHereButton: Bool = true /// Determines whether a built-in result view will be shown. If false, the result display/selection @@ -67,17 +67,18 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. var noResultMessage: String = "No results found" - @State /// Indicates that the `SearchViewModel` should start a search. + @State private var shouldCommitSearch: Bool = false - @State /// Indicates that the geoView's viewpoint has changed since the last search. - private var viewpointChanged: Bool = false - @State - private var result: Result<[SearchResult], Error> = .success([]) + private var viewpointChanged: Bool = false + // TODO: Figure out better styling for list + // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource + // TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. + public var body: some View { VStack (alignment: .center) { TextField(searchViewModel.defaultPlaceHolder, @@ -96,9 +97,9 @@ public struct SearchView: View { } .esriBorder() } - switch result { + switch searchViewModel.results { case .success(let results): - if results.count > 0 { + if let results = results, results.count > 0 { List(results) { result in VStack (alignment: .leading){ Text(result.displayTitle) @@ -109,9 +110,26 @@ public struct SearchView: View { } } } - // TODO: Figure out better styling for list - // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource -// .listStyle(DefaultListStyle()) + // .listStyle(DefaultListStyle()) + } + case .failure(let error): + Text("Error occurred: \(error.localizedDescription)") + Spacer() + } + switch searchViewModel.suggestions { + case .success(let results): + if let results = results, results.count > 0 { + List(results) { result in + VStack (alignment: .leading){ + Text(result.displayTitle) + .font(.callout) + if let subtitle = result.displaySubtitle { + Text(subtitle) + .font(.caption) + } + } + } + // .listStyle(DefaultListStyle()) } case .failure(let error): Text("Error occurred: \(error.localizedDescription)") @@ -120,22 +138,12 @@ public struct SearchView: View { } .task(id: searchViewModel.currentQuery) { // For when user types a new character - guard !searchViewModel.currentQuery.isEmpty else { - result = .success([]) - return - } - await searchViewModel.updateSuggestions(nil) + // suggestionResult = await Result { try await searchViewModel.updateSuggestions() } + await searchViewModel.updateSuggestions() } .task(id: shouldCommitSearch) { // For when user commits changes (hits Enter/Search button) - print("geocoding...") - result = await Result { try await searchViewModel.commitSearch(true) } + await searchViewModel.commitSearch(true) } } } - -// MARK: Extensions - -extension SearchResult: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(self) } -} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index b3a206897..134322054 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -29,13 +29,13 @@ public enum SearchResultMode { /// Performs searches and manages search state for a Search, or optionally without a UI connection. public class SearchViewModel: ObservableObject { public convenience init(defaultPlaceHolder: String = "Find a place or address", - activeSource: SearchSourceProtocol? = nil, - queryArea: Geometry? = nil, - queryCenter: Point? = nil, - resultMode: SearchResultMode = .automatic, - results: [SearchResult]? = nil, - sources: [SearchSourceProtocol] = [], - suggestions: [SearchSuggestion]? = nil +activeSource: SearchSourceProtocol? = nil, +queryArea: Geometry? = nil, +queryCenter: Point? = nil, +resultMode: SearchResultMode = .automatic, +results: Result<[SearchResult]?, Error> = .success(nil), +sources: [SearchSourceProtocol] = [], +suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) ) { self.init() self.defaultPlaceHolder = defaultPlaceHolder @@ -60,7 +60,15 @@ public class SearchViewModel: ObservableObject { /// other method calls and property changes within the view model, so the view should take care to /// observe for changes. @Published - var currentQuery: String = "" + var currentQuery: String = "" { + didSet { + selectedResult = nil + if currentQuery.isEmpty { + results = .success(nil) + suggestions = .success(nil) + } + } + } /// The search area to be used for the current query. Ignored in most queries, unless the /// `RestrictToArea` property is set to true when calling `commitSearch`. This property @@ -77,13 +85,14 @@ public class SearchViewModel: ObservableObject { /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. - var results: [SearchResult]? - @Published + var results: Result<[SearchResult]?, Error> = .success([]) + /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. + @Published var selectedResult: SearchResult? /// Collection of search sources to be used. This list is maintained over time and is not nullable. @@ -94,44 +103,83 @@ public class SearchViewModel: ObservableObject { /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. - var suggestions: [SearchSuggestion]? + @Published + var suggestions: Result<[SearchSuggestion]?, Error> = .success([]) /// True if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to /// this property are driven by changes to the `queryArea` property. @Published - var isEligibleForRequery: Bool = false + //TODO: should be reset if viewpoint has changed since last result returned. + private(set) var isEligibleForRequery: Bool = false /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. - /// Participates in cooperative cancellation using the supplied cancellation token. /// If `restrictToArea` is true, only results in the query area will be returned. /// - Parameter restrictToArea: If true, the search is restricted to results within the extent /// of the `queryArea` property. Behavior when called with `restrictToArea` set to true /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. - func commitSearch(_ restrictToArea: Bool) async throws -> [SearchResult] { - guard !currentQuery.isEmpty else { return [] } + func commitSearch(_ restrictToArea: Bool) async -> Void { + guard !currentQuery.isEmpty else { return } print("SearchViewModel.commitSearch: \(currentQuery)") - - var results = [SearchResult]() - for searchSource in sources { - let searchResults = try await searchSource.search(currentQuery, - area: restrictToArea ? queryArea : nil, - cancellationToken: "" - ) - results.append(contentsOf: searchResults) + + selectedResult = nil + isEligibleForRequery = false + + var searchResults = [SearchResult]() + let searchSources = sourcesToSearch() + for i in 0...searchSources.count - 1 { + print("commit search source \(i)") + var searchSource = searchSources[i] + searchSource.searchArea = queryArea + searchSource.preferredSearchLocation = queryCenter + + let searchResult = await Result { + try await searchSource.search(currentQuery, + area: restrictToArea ? queryArea : nil + ) + } + switch searchResult { + case .success(let results): + searchResults.append(contentsOf: results) + case .failure(let error): + print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") + } } - return results + results = .success(searchResults) + suggestions = .success(nil) } /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion /// requests before initiating new ones. The view should also wait for some time after user finishes /// typing before making suggestions. The JavaScript implementation uses 150ms by default. - /// - Parameter cancellationToken: Token used for cooperative cancellation. - func updateSuggestions(_ cancellationToken: String?) async { + //TODO: should the return be optional (according to .NET impl)? + func updateSuggestions() async -> Void { guard !currentQuery.isEmpty else { return } print("SearchViewModel.updateSuggestions: \(currentQuery)") + + var suggestionResults = [SearchSuggestion]() + let searchSources = sourcesToSearch() + for i in 0...searchSources.count - 1 { + print("update suggestions source \(i)") + var searchSource = searchSources[i] + searchSource.searchArea = queryArea + searchSource.preferredSearchLocation = queryCenter + + let suggestResults = await Result { + try await searchSource.suggest(currentQuery) + } + switch suggestResults { + case .success(let results): + suggestionResults.append(contentsOf: results) + case .failure(let error): + print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") + } + } + suggestions = .success(suggestionResults) + results = .success(nil) + selectedResult = nil } /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is @@ -141,10 +189,44 @@ public class SearchViewModel: ObservableObject { /// to changes to `currentQuery` initiated by a call to this method. /// - Parameters: /// - searchSuggestion: The suggestion to use to commit the search. - /// - cancellationToken: ken used for cooperative cancellation. - func acceptSuggestion(_ searchSuggestion: SearchSuggestion, - cancellationToken: String?) async { - print("SearchViewModel.acceptSuggestion") + func acceptSuggestion(_ searchSuggestion: SearchSuggestion) async -> Void { + currentQuery = searchSuggestion.displayTitle + print("SearchViewModel.acceptSuggestion: \(currentQuery)") + + isEligibleForRequery = false + + var searchResults = [SearchResult]() + let searchResult = await Result { + try await searchSuggestion.owningSource.search(searchSuggestion, area: nil) + } + switch searchResult { + case .success(let results): + switch (resultMode) + { + case .single: + if let firstResult = results.first { + searchResults = [firstResult] + selectedResult = firstResult + } + case .multiple: + searchResults = results + case .automatic: + if searchSuggestion.suggestResult?.isCollection ?? true { + searchResults = results + } else { + if let firstResult = results.first { + searchResults = [firstResult] + selectedResult = firstResult + } + } + } + case .failure(let error): + print("\(searchSuggestion.owningSource.displayName) encountered an error: \(error.localizedDescription)") + } + + results = .success(searchResults) + suggestions = .success(nil) + selectedResult = nil } /// Configures the view model for the provided map. By default, will only configure the view model @@ -152,8 +234,7 @@ public class SearchViewModel: ObservableObject { /// web map configuration into account. /// - Parameters: /// - map: Map to use for configuration. - /// - cancellationToken: cancellationToken: String - func configureForMap(_ map: Map, cancellationToken: String?) { + func configureForMap(_ map: Map) { print("SearchViewModel.configureForMap") } @@ -162,8 +243,7 @@ public class SearchViewModel: ObservableObject { /// web scene configuration into account. /// - Parameters: /// - scene: Scene used for configuration. - /// - cancellationToke: Token used for cooperative cancellation. - func configureForScene(_ scene: ArcGIS.Scene, cancellationToken: String?) { + func configureForScene(_ scene: ArcGIS.Scene) { print("SearchViewModel.configureForScene") } @@ -173,3 +253,15 @@ public class SearchViewModel: ObservableObject { print("SearchViewModel.clearSearch") } } + +extension SearchViewModel { + func sourcesToSearch() -> [SearchSourceProtocol] { + var selectedSources = [SearchSourceProtocol]() + if let activeSource = activeSource { + selectedSources.append(activeSource) + } else { + selectedSources.append(contentsOf: sources) + } + return selectedSources + } +} diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift new file mode 100644 index 000000000..b4c2d2f67 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift @@ -0,0 +1,29 @@ +// Copyright 2021 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 GeocodeResult { + func toSearchResult(searchSource: SearchSourceProtocol) -> SearchResult { + return SearchResult(displayTitle: self.label, + displaySubtitle: "Match percent: \((self.score).formatted(.percent))", + markerImage: nil, + owningSource: searchSource, + geoElement: nil, + selectionViewpoint: nil) + } +} + +extension GeocodeResult: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(self) } +} diff --git a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift new file mode 100644 index 000000000..eee4e1a2c --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift @@ -0,0 +1,24 @@ +// Copyright 2021 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 SuggestResult { + func toSearchSuggestion(searchSource: SearchSourceProtocol) -> SearchSuggestion { + return SearchSuggestion(displayTitle: label, + displaySubtitle: nil, + owningSource: searchSource, + suggestResult: self, + isCollection: self.isCollection) + } +} From e6ae1e861b11207caaa3657e649f577e5a8f37f1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 29 Jun 2021 17:36:39 -0500 Subject: [PATCH 0068/1722] Get rest of search model methods working. continue filling out locatorsearchsource. --- .../Examples/Examples/SearchExampleView.swift | 11 +- .../Search/LocatorSearchSource.swift | 39 +++-- .../Components/Search/SearchSuggestion.swift | 6 + .../Components/Search/SearchView.swift | 160 +++++++++++++----- .../Components/Search/SearchViewModel.swift | 1 - 5 files changed, 162 insertions(+), 55 deletions(-) diff --git a/Examples/Examples/Examples/SearchExampleView.swift b/Examples/Examples/Examples/SearchExampleView.swift index 37438b03f..9b854aec1 100644 --- a/Examples/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/Examples/SearchExampleView.swift @@ -18,9 +18,16 @@ import ArcGISToolkit struct SearchExampleView: View { let searchViewModel = SearchViewModel( - sources: [LocatorSearchSource(), - LocatorSearchSource()] + sources: [LocatorSearchSource(displayName: "Locator One", + maximumResults: 3, + maximumSuggestions: 3 + ), + LocatorSearchSource(displayName: "Locator Two", + maximumResults: 1, + maximumSuggestions: 1 + )] ) + var body: some View { ZStack (alignment: .topTrailing) { MapViewReader { proxy in diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 51ac26fe5..58e7a2815 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -16,41 +16,46 @@ import ArcGIS /// Uses a Locator to provide search and suggest results. Most configuration should be done on the /// `GeocodeParameters` directly. -public class LocatorSearchSource { - public init(locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!), - geocodeParameters: GeocodeParameters = GeocodeParameters(), - suggestParameters: SuggestParameters = SuggestParameters(), - displayName: String = "Search", +public class LocatorSearchSource: ObservableObject { + public init(displayName: String = "Search", maximumResults: Int = 6, maximumSuggestions: Int = 6, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil) { - self.locator = locator - self.geocodeParameters = geocodeParameters - self.suggestParameters = suggestParameters self.displayName = displayName self.maximumResults = maximumResults self.maximumSuggestions = maximumSuggestions self.searchArea = searchArea self.preferredSearchLocation = preferredSearchLocation + + geocodeParameters.maxResults = Int32(maximumResults) + suggestParameters.maxResults = Int32(maximumSuggestions) } /// The locator used by this search source. - var locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!) + private(set) var locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!) /// Parameters used for geocoding. Some properties on parameters will be updated automatically /// based on searches. - var geocodeParameters: GeocodeParameters = GeocodeParameters() + private(set) var geocodeParameters: GeocodeParameters = GeocodeParameters() /// Parameters used for getting suggestions. Some properties will be updated automatically /// based on searches. - var suggestParameters: SuggestParameters = SuggestParameters() + private(set) var suggestParameters: SuggestParameters = SuggestParameters() public var displayName: String = "Search" - public var maximumResults: Int = 6 + public var maximumResults: Int = 6 { + didSet { + geocodeParameters.maxResults = Int32(maximumResults) + } + } - public var maximumSuggestions: Int = 6 + public var maximumSuggestions: Int = 6 { + didSet { + suggestParameters.maxResults = Int32(maximumResults) + } + } public var searchArea: Geometry? @@ -65,7 +70,13 @@ extension LocatorSearchSource: SearchSourceProtocol { } public func search(_ queryString: String, area: Geometry? = nil) async throws -> [SearchResult] { - let geocodeResults = try await locator.geocode(searchText: queryString) + +// let tempParams + + + + let geocodeResults = try await locator.geocode(searchText: queryString, + parameters: geocodeParameters) //convert to search results return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 9447ae3d5..25f227612 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -52,3 +52,9 @@ public class SearchSuggestion { extension SearchSuggestion: Identifiable { public var id: ObjectIdentifier { ObjectIdentifier(self) } } + +extension SearchSuggestion: Equatable { + public static func == (lhs: SearchSuggestion, rhs: SearchSuggestion) -> Bool { + lhs === rhs + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 66fb17289..a0e64c4e3 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -69,7 +69,11 @@ public struct SearchView: View { /// Indicates that the `SearchViewModel` should start a search. @State - private var shouldCommitSearch: Bool = false + private var commitSearch: Bool = false + + /// Indicates that the `SearchViewModel` should accept a suggestion. + @State + private var currentSuggestion: SearchSuggestion? /// Indicates that the geoView's viewpoint has changed since the last search. @State @@ -78,72 +82,152 @@ public struct SearchView: View { // TODO: Figure out better styling for list // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource // TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. - + // TODO: better modifiers for search text field; maybe SearchTextField or something... public var body: some View { VStack (alignment: .center) { TextField(searchViewModel.defaultPlaceHolder, text: $searchViewModel.currentQuery) { editing in - // For when editing state changes (becomes/looses firstResponder) } onCommit: { - shouldCommitSearch.toggle() + // Editing state changed (becomes/looses firstResponder) + commitSearch.toggle() } .esriDeleteTextButton(text: $searchViewModel.currentQuery) - .esriSearchButton(performSearch: $shouldCommitSearch) + .esriSearchButton(performSearch: $commitSearch) .esriBorder() if enableRepeatSearchHereButton, viewpointChanged { Button("Search Here") { viewpointChanged = false - shouldCommitSearch.toggle() + commitSearch.toggle() } - .esriBorder() } - switch searchViewModel.results { + SearchResultList(searchResults: searchViewModel.results) + SearchSuggestionList(searchSuggestions: searchViewModel.suggestions, + currentSuggestion: $currentSuggestion + ) + } + .task(id: searchViewModel.currentQuery) { + // User typed a new character + if currentSuggestion == nil { + await searchViewModel.updateSuggestions() + } + } + .task(id: commitSearch) { + // User committed changes (hit Enter/Search button) + await searchViewModel.commitSearch(true) + } + .task(id: currentSuggestion) { + // User committed changes (hit Enter/Search button) + if let suggestion = currentSuggestion { + await searchViewModel.acceptSuggestion(suggestion) + currentSuggestion = nil + } + } + } +} + +// TODO: look at consolidating SearchResultView and SearchSuggestionView with +// TODO: new SearchDisplayProtocol containing only displayTitle and displaySubtitle +// TODO: That would mean we only needed one of these. +struct SearchResultList: View { + var searchResults: Result<[SearchResult]?, Error> + + var body: some View { + Group { + switch searchResults { case .success(let results): if let results = results, results.count > 0 { - List(results) { result in - VStack (alignment: .leading){ - Text(result.displayTitle) - .font(.callout) - if let subtitle = result.displaySubtitle { - Text(subtitle) - .font(.caption) + List { + // Get array of unique search source displayNames. + let sourceDisplayNames = Array(Set(results.map { $0.owningSource.displayName })).sorted() + ForEach(sourceDisplayNames, id: \.self) { displayName in + Section(header: Text(displayName)) { + // Get results filtered by displayName + let sourceResults = results.filter { $0.owningSource.displayName == displayName } + if sourceResults.count > 0 { + ForEach(sourceResults) { result in + HStack { + Image(systemName: "mappin") + .foregroundColor(Color(.red)) + SearchResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) + } + .onTapGesture { + print("user selected result: \(result.displayTitle)") + } + } + } + else { + // TODO: figure out why this isn't triggered. + Text("No results found") + } + // .listStyle(DefaultListStyle()) } } } - // .listStyle(DefaultListStyle()) } - case .failure(let error): - Text("Error occurred: \(error.localizedDescription)") + case .failure(_): Spacer() } - switch searchViewModel.suggestions { + } + } +} + +struct SearchSuggestionList: View { + var searchSuggestions: Result<[SearchSuggestion]?, Error> + var currentSuggestion: Binding + + var body: some View { + Group { + switch searchSuggestions { case .success(let results): - if let results = results, results.count > 0 { - List(results) { result in - VStack (alignment: .leading){ - Text(result.displayTitle) - .font(.callout) - if let subtitle = result.displaySubtitle { - Text(subtitle) - .font(.caption) + if let suggestions = results, suggestions.count > 0 { + List { + // Get array of unique search source displayNames. + let sourceDisplayNames = Array(Set(suggestions.map { $0.owningSource.displayName })).sorted() + ForEach(sourceDisplayNames, id: \.self) { displayName in + Section(header: Text(displayName)) { + // Get results filtered by displayName + let sourceSuggestions = suggestions.filter { $0.owningSource.displayName == displayName } + if sourceSuggestions.count > 0 { + ForEach(sourceSuggestions) { suggestion in + HStack { + let imageName = suggestion.isCollection ? "magnifyingglass" : "mappin" + Image(systemName: imageName) + SearchResultRow(title: suggestion.displayTitle, subtitle: suggestion.displaySubtitle) + } + .onTapGesture() { + currentSuggestion.wrappedValue = suggestion + } + } + // .listStyle(DefaultListStyle()) + } + else { + // TODO: figure out why this isn't triggered. + Text("No results found") + } } } } - // .listStyle(DefaultListStyle()) } - case .failure(let error): - Text("Error occurred: \(error.localizedDescription)") + case .failure(_): Spacer() } } - .task(id: searchViewModel.currentQuery) { - // For when user types a new character - // suggestionResult = await Result { try await searchViewModel.updateSuggestions() } - await searchViewModel.updateSuggestions() - } - .task(id: shouldCommitSearch) { - // For when user commits changes (hits Enter/Search button) - await searchViewModel.commitSearch(true) + } +} +//TODO: NoResultMessage = "No Results"; + +struct SearchResultRow: View { + var title: String + var subtitle: String? + + var body: some View { + VStack (alignment: .leading){ + Text(title) + .font(.callout) + if let subtitle = subtitle { + Text(subtitle) + .font(.caption) + } } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 134322054..400b65adb 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -154,7 +154,6 @@ suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion /// requests before initiating new ones. The view should also wait for some time after user finishes /// typing before making suggestions. The JavaScript implementation uses 150ms by default. - //TODO: should the return be optional (according to .NET impl)? func updateSuggestions() async -> Void { guard !currentQuery.isEmpty else { return } print("SearchViewModel.updateSuggestions: \(currentQuery)") From c6507af7d059cdda8b640babf6340bdf78cdc70e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 1 Jul 2021 17:17:10 -0500 Subject: [PATCH 0069/1722] Locator search source updates --- .../Search/LocatorSearchSource.swift | 31 +++++++++---- .../Search/SmartLocatorSearchSource.swift | 46 ++++++++++++++++--- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 58e7a2815..dec82361d 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -64,26 +64,37 @@ public class LocatorSearchSource: ObservableObject { extension LocatorSearchSource: SearchSourceProtocol { public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { - let suggestResults = try await locator.suggest(searchText: queryString) - //convert to search results + suggestParameters.searchArea = searchArea + suggestParameters.preferredSearchLocation = preferredSearchLocation + + let suggestResults = try await locator.suggest(searchText: queryString, + parameters: suggestParameters + ) + //convert to SearchSuggestions return suggestResults.map{ $0.toSearchSuggestion(searchSource: self) } } public func search(_ queryString: String, area: Geometry? = nil) async throws -> [SearchResult] { + geocodeParameters.searchArea = searchArea + geocodeParameters.preferredSearchLocation = preferredSearchLocation -// let tempParams - - + let geocodeResults = try await locator.geocode(searchText: queryString, + parameters: geocodeParameters + ) - let geocodeResults = try await locator.geocode(searchText: queryString, - parameters: geocodeParameters) - //convert to search results + //convert to SearchResults return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } public func search(_ searchSuggestion: SearchSuggestion, area: Geometry? = nil) async throws -> [SearchResult] { - guard let query = searchSuggestion.suggestResult?.label else { return [] } - let geocodeResults = try await locator.geocode(searchText: query) + guard let suggestResult = searchSuggestion.suggestResult else { return [] } + + geocodeParameters.searchArea = searchArea + geocodeParameters.preferredSearchLocation = preferredSearchLocation + + let geocodeResults = try await locator.geocode(suggestResult: suggestResult, + parameters: geocodeParameters + ) return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 68a476443..8d3a13d72 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -19,6 +19,46 @@ import ArcGIS /// underlying locator to be used well; this class implements behaviors that make assumptions about the /// locator being the world geocode service. public class SmartLocatorSearchSource: LocatorSearchSource { + public init(displayName: String = "Search", + maximumSuggestions: Int, + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil, + repeatSearchResultThreshold: Int = 1, + repeatSuggestResultThreshold: Int = 6, + resultSymbolStyle: SymbolStyle? = nil) { + self.objectWillChange = objectWillChange + self.displayName = displayName + self.maximumSuggestions = maximumSuggestions + self.searchArea = searchArea + self.preferredSearchLocation = preferredSearchLocation + self.repeatSearchResultThreshold = repeatSearchResultThreshold + self.repeatSuggestResultThreshold = repeatSuggestResultThreshold + self.resultSymbolStyle = resultSymbolStyle + + geocodeParameters.maxResults = Int32(maximumResults) + suggestParameters.maxResults = Int32(maximumSuggestions) + } + + public var displayName: String + + public var maximumSuggestions: Int + + public var searchArea: Geometry? + + public var preferredSearchLocation: Point? + + public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { + <#code#> + } + + public func search(_ queryString: String, area: Geometry?) async throws -> [SearchResult] { + <#code#> + } + + public func search(_ searchSuggestion: SearchSuggestion, area: Geometry?) async throws -> [SearchResult] { + <#code#> + } + /// The minimum number of results to attempt to return. If there are too few results, the search is /// repeated with loosened parameters until enough results are accumulated. If no search is /// successful, it is still possible to have a total number of results less than this threshold. Does not @@ -39,10 +79,4 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// by publishing a web style, taking care to ensure that symbol keys match the `Type` attribute /// returned by the locator. var resultSymbolStyle: SymbolStyle? - - // @State - // private var address: String = "" - // - // @State - // private var result: Result<[GeocodeResult], Error> = .success([]) } From 83f8cf9be62aa5c35738756a3550cb5bec4777fd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Jul 2021 10:00:58 -0500 Subject: [PATCH 0070/1722] updates for new viewpoint-handling model in the API --- Examples/Examples.xcodeproj/project.pbxproj | 8 +- .../Examples/OverviewMapExampleView.swift | 21 +-- Package.swift | 3 +- .../Components/OverviewMap.swift | 138 +++++++++--------- 4 files changed, 85 insertions(+), 85 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 6b97fe1d0..6cdf3059d 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -269,7 +269,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -324,7 +324,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -345,7 +345,7 @@ DEVELOPMENT_TEAM = P8HGHS7JQ8; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = ExamplesApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -369,7 +369,7 @@ DEVELOPMENT_TEAM = P8HGHS7JQ8; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = ExamplesApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index a9f7ba0bf..c3b57e7a5 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -16,19 +16,20 @@ import ArcGIS import ArcGISToolkit struct OverviewMapExampleView: View { - @State private var mapViewProxy: MapViewProxy? - @State private var sceneViewProxy: SceneViewProxy? + let map = Map(basemap: .imageryWithLabels()) + @State private var viewpoint: Viewpoint? + @State private var visibleArea: ArcGIS.Polygon? var body: some View { - ZStack (alignment: .topTrailing) { - MapView( - map: Map(basemap: .imageryWithLabels()), - proxy: $mapViewProxy + MapView(map: map) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onVisibleAreaChanged { visibleArea = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing ) - OverviewMap(proxy: mapViewProxy) - .frame(width: 200, height: 132) - .padding() - } } } diff --git a/Package.swift b/Package.swift index e5eaac6c8..36ada519c 100644 --- a/Package.swift +++ b/Package.swift @@ -18,8 +18,7 @@ import PackageDescription let package = Package( name: "arcgis-runtime-toolkit-swift", platforms: [ - .iOS(.v14), - .macOS(.v11) + .macOS("12"), .iOS("15") ], products: [ .library( diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index ed37e4865..e6b48b9bf 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -18,93 +18,93 @@ import ArcGIS /// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed /// on an existing `GeoView`, which shows the visible extent of that `GeoView`. public struct OverviewMap: View { - /// The `GeoViewProxy` representing the main `GeoView`. The proxy is - /// necessary for accessing `GeoView` functionality to get and set viewpoints. - private(set) var proxy: GeoViewProxy? + /// The `Viewpoint` of the main `GeoView` + let viewpoint: Viewpoint? + + /// The visible area of the main `GeoView`. + let visibleArea: Polygon? + + /// The `Graphic` displaying the visible area of the main `GeoView`. + @StateObject var graphic: Graphic + + /// The `GraphicsOverlay` used to display the visible area graphic. + @StateObject var graphicsOverlay: GraphicsOverlay /// The `Map` displayed in the `OverviewMap`. - private(set) var map: Map + @StateObject var map: Map = Map(basemap: .topographic()) - /// The fill symbol used to display the main `GeoView` extent. + /// The `FillSymbol` used to display the main `GeoView` visible area. private(set) var extentSymbol: FillSymbol - /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display - /// at the product of mainGeoViewScale * scaleFactor. + /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display + /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor. private(set) var scaleFactor: Double - /// The geometry of the extent `Graphic` displaying the main `GeoView`'s extent. Updating - /// this property will update the display of the `OverviewMap`. - @State private var extentGeometry: Geometry? - - /// The viewpoint of the `OverviewMap'`s `MapView`. Updating - /// this property will update the display of the `OverviewMap`. - @State private var overviewMapViewpoint: Viewpoint? - /// Creates an `OverviewMap`. /// - Parameters: - /// - proxy: The `GeoViewProxy` representing the main map. - /// - map: The `Map` to display in the `OverviewMap`. - /// - extentSymbol: The `FillSymbol` used to display the main `GeoView`'s extent. + /// - viewpoint: Viewpoint of the main `GeoView` used to update the `OverviewMap` view. + /// - visibleArea: Visible area of the main `GeoView`. + /// - extentSymbol: The `FillSymbol` used to display the main `GeoView`'s visible area. /// The default is a transparent `SimpleFillSymbol` with a red, 1 point width outline. - /// - scaleFactor: The scale factor used to calculate the `OverviewMap`'s scale. - /// The default is `25.0`. - public init( - proxy: GeoViewProxy?, - map: Map = Map(basemap: .topographic()), - extentSymbol: FillSymbol = SimpleFillSymbol( - style: .solid, - color: .clear, - outline: SimpleLineSymbol( - style: .solid, - color: .red, - width: 1.0 - ) - ), - scaleFactor: Double = 25.0 + /// - scaleFactor: The factor to multiply the main `GeoView`'s scale by. + /// The default value is 25.0 + public init(viewpoint: Viewpoint?, + visibleArea: Polygon?, + extentSymbol: FillSymbol = SimpleFillSymbol( + style: .solid, + color: .clear, + outline: SimpleLineSymbol( + style: .solid, + color: .red, + width: 1.0 + ) + ), + scaleFactor: Double = 25.0 ) { - self.proxy = proxy - self.map = map + self.visibleArea = visibleArea self.extentSymbol = extentSymbol self.scaleFactor = scaleFactor - } - - private var viewpointChangedPublisher: AnyPublisher { - proxy?.viewpointChangedPublisher - .receive(on: DispatchQueue.main) - .throttle( - for: .seconds(0.25), - scheduler: DispatchQueue.main, - latest: true + + let graphic = Graphic(geometry: visibleArea, + symbol: extentSymbol) + + // It is necessary to set the graphic and graphicsOverlay this way + // in order to prevent the main geoview from recreating the + // graphicsOverlay every draw cycle. That was causing refresh issues + // with the graphic during panning/zooming/rotating. + _graphic = StateObject(wrappedValue: graphic) + _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) + + if let viewpoint = viewpoint, + let center = viewpoint.targetGeometry as? Point { + self.viewpoint = Viewpoint(center: center, + scale: viewpoint.targetScale * scaleFactor ) - .eraseToAnyPublisher() ?? Empty().eraseToAnyPublisher() + } + else { + self.viewpoint = nil + } } - + public var body: some View { - ZStack { - MapView( - map: map, - viewpoint: $overviewMapViewpoint, - graphicsOverlays: [GraphicsOverlay( - graphics: [Graphic(geometry: extentGeometry, - symbol: extentSymbol)])] - ) + MapView( + map: map, + viewpoint: viewpoint, + graphicsOverlays: [graphicsOverlay] + ) .attributionTextHidden() .interactionModes([]) .border(Color.black, width: 1) - .onReceive(viewpointChangedPublisher) { - guard let centerAndScaleViewpoint = proxy?.currentViewpoint(type: .centerAndScale), - let newCenter = centerAndScaleViewpoint.targetGeometry as? Point - else { return } - - if let mapViewProxy = proxy as? MapViewProxy { - extentGeometry = mapViewProxy.visibleArea - } - - overviewMapViewpoint = Viewpoint( - center: newCenter, - scale: centerAndScaleViewpoint.targetScale * scaleFactor - ) - } - } + .onChange(of: visibleArea, perform: { graphic.geometry = $0 }) + .onChange(of: extentSymbol, perform: { graphic.symbol = $0 }) + } + + public func map(_ map: Map) -> OverviewMap { + var copy = self + copy._map = StateObject(wrappedValue: map) + return copy } } + +extension Graphic: ObservableObject {} +extension GraphicsOverlay: ObservableObject {} From 318a6ca55b3f79e172e54d2f8a1fff64c0f53a8a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Jul 2021 14:05:07 -0500 Subject: [PATCH 0071/1722] SceneView support --- .../Examples/OverviewMapExampleView.swift | 20 ++++-- .../Components/OverviewMap.swift | 62 ++++++++++++++----- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index c3b57e7a5..4990eb212 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -17,19 +17,31 @@ import ArcGISToolkit struct OverviewMapExampleView: View { let map = Map(basemap: .imageryWithLabels()) - @State private var viewpoint: Viewpoint? + let scene = Scene(basemap: .imageryWithLabels()) + @State private var mapViewpoint: Viewpoint? + @State private var sceneViewpoint: Viewpoint? @State private var visibleArea: ArcGIS.Polygon? - + var body: some View { + VStack { MapView(map: map) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onViewpointChanged(type: .centerAndScale) { mapViewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( - OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) + OverviewMap(viewpoint: mapViewpoint, visibleArea: visibleArea) .frame(width: 200, height: 132) .padding(), alignment: .topTrailing ) + SceneView(scene: scene) + .onViewpointChanged(type: .centerAndScale) { sceneViewpoint = $0 } + .overlay( + OverviewMap(viewpoint: sceneViewpoint) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) + } } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index e6b48b9bf..c34bc891d 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -33,13 +33,32 @@ public struct OverviewMap: View { /// The `Map` displayed in the `OverviewMap`. @StateObject var map: Map = Map(basemap: .topographic()) - /// The `FillSymbol` used to display the main `GeoView` visible area. - private(set) var extentSymbol: FillSymbol + /// The `Symbol` used to display the main `GeoView` visible area. For MapViews, + /// the symbol will be a FillSymbol used to display the GeoView visible area. For SceneViews, + /// the symbol will be a MarkerSymbol, used to display the current viewpoint's center. + /// For MapViews, the default is a transparent FillSymbol with a red, 1 point width outline; + /// for SceneViews, the default is a red, crosshair SimpleMarkerSymbol. + private(set) var symbol: Symbol? /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor. private(set) var scaleFactor: Double + private let fillSymbol: FillSymbol = SimpleFillSymbol( + style: .solid, + color: .clear, + outline: SimpleLineSymbol( + style: .solid, + color: .red, + width: 1.0 + ) + ) + + private let markerSymbol: MarkerSymbol = SimpleMarkerSymbol(style: .cross, + color: .red, + size: 16.0 + ) + /// Creates an `OverviewMap`. /// - Parameters: /// - viewpoint: Viewpoint of the main `GeoView` used to update the `OverviewMap` view. @@ -49,24 +68,22 @@ public struct OverviewMap: View { /// - scaleFactor: The factor to multiply the main `GeoView`'s scale by. /// The default value is 25.0 public init(viewpoint: Viewpoint?, - visibleArea: Polygon?, - extentSymbol: FillSymbol = SimpleFillSymbol( - style: .solid, - color: .clear, - outline: SimpleLineSymbol( - style: .solid, - color: .red, - width: 1.0 - ) - ), + visibleArea: Polygon? = nil, + symbol: Symbol? = nil, scaleFactor: Double = 25.0 ) { self.visibleArea = visibleArea - self.extentSymbol = extentSymbol self.scaleFactor = scaleFactor + if symbol == nil { + self.symbol = visibleArea == nil ? markerSymbol : fillSymbol + } + else { + self.symbol = symbol + } + let graphic = Graphic(geometry: visibleArea, - symbol: extentSymbol) + symbol: symbol) // It is necessary to set the graphic and graphicsOverlay this way // in order to prevent the main geoview from recreating the @@ -95,8 +112,21 @@ public struct OverviewMap: View { .attributionTextHidden() .interactionModes([]) .border(Color.black, width: 1) - .onChange(of: visibleArea, perform: { graphic.geometry = $0 }) - .onChange(of: extentSymbol, perform: { graphic.symbol = $0 }) + .onChange(of: visibleArea, perform: { visibleArea in + if let visibleArea = visibleArea { + graphic.geometry = visibleArea + graphic.symbol = symbol != nil ? symbol : fillSymbol + } + }) + .onChange(of: viewpoint, perform: { viewpoint in + if visibleArea == nil, + let viewpoint = viewpoint, + let point = viewpoint.targetGeometry as? Point { + graphic.geometry = point + graphic.symbol = symbol != nil ? symbol : markerSymbol + } + }) + .onChange(of: symbol, perform: { graphic.symbol = $0 }) } public func map(_ map: Map) -> OverviewMap { From 453c2ee60bb66d3662e565ea92c3f360e6ad8a45 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Jul 2021 14:47:27 -0500 Subject: [PATCH 0072/1722] Add separate SceneView OVM example --- Examples/Examples.xcodeproj/project.pbxproj | 12 +++-- .../Examples/OverviewMapExampleView.swift | 52 ------------------- .../Examples/OverviewMapExampleView_Map.swift | 44 ++++++++++++++++ .../OverviewMapExampleView_Scene.swift | 40 ++++++++++++++ Examples/ExamplesApp/Examples.swift | 3 +- .../Components/OverviewMap.swift | 15 ++++-- 6 files changed, 104 insertions(+), 62 deletions(-) delete mode 100644 Examples/Examples/OverviewMapExampleView.swift create mode 100644 Examples/Examples/OverviewMapExampleView_Map.swift create mode 100644 Examples/Examples/OverviewMapExampleView_Scene.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 6cdf3059d..92368507a 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -12,13 +12,14 @@ E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; }; E47ABE5D2652FF1200FD2FE3 /* ArcGIS in Embed Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + E4856844268FA111007BE537 /* OverviewMapExampleView_Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4856843268FA111007BE537 /* OverviewMapExampleView_Scene.swift */; }; E48A73432658227100F5C118 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73402658227100F5C118 /* Example.swift */; }; E48A73442658227100F5C118 /* AnyExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73412658227100F5C118 /* AnyExample.swift */; }; E48A73452658227100F5C118 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733D2658227000F5C118 /* Examples.swift */; }; E48A73462658227100F5C118 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73422658227100F5C118 /* ExampleView.swift */; }; E48A73472658227100F5C118 /* ExampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733E2658227100F5C118 /* ExampleList.swift */; }; E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E4E57DC5265D8EB00077A093 /* ArcGISToolkit */; }; - E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */; }; + E4F9BC99265EFCAF001280FF /* OverviewMapExampleView_Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F9BC98265EFCAF001280FF /* OverviewMapExampleView_Map.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -42,13 +43,14 @@ E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; E47ABE4C2652FE0C00FD2FE3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E47ABE622653005F00FD2FE3 /* arcgis-runtime-toolkit-swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "arcgis-runtime-toolkit-swift"; path = ..; sourceTree = ""; }; + E4856843268FA111007BE537 /* OverviewMapExampleView_Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView_Scene.swift; sourceTree = ""; }; E48A733D2658227000F5C118 /* Examples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Examples.swift; sourceTree = ""; }; E48A733E2658227100F5C118 /* ExampleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleList.swift; sourceTree = ""; }; E48A733F2658227100F5C118 /* Examples.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Examples.entitlements; sourceTree = ""; }; E48A73402658227100F5C118 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; E48A73412658227100F5C118 /* AnyExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyExample.swift; sourceTree = ""; }; E48A73422658227100F5C118 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; - E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView.swift; sourceTree = ""; }; + E4F9BC98265EFCAF001280FF /* OverviewMapExampleView_Map.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView_Map.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,7 +69,8 @@ E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( - E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, + E4F9BC98265EFCAF001280FF /* OverviewMapExampleView_Map.swift */, + E4856843268FA111007BE537 /* OverviewMapExampleView_Scene.swift */, ); name = Examples; sourceTree = ""; @@ -208,9 +211,10 @@ E48A73452658227100F5C118 /* Examples.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, - E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, + E4F9BC99265EFCAF001280FF /* OverviewMapExampleView_Map.swift in Sources */, E48A73442658227100F5C118 /* AnyExample.swift in Sources */, E48A73472658227100F5C118 /* ExampleList.swift in Sources */, + E4856844268FA111007BE537 /* OverviewMapExampleView_Scene.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift deleted file mode 100644 index 4990eb212..000000000 --- a/Examples/Examples/OverviewMapExampleView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021 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 -import ArcGISToolkit - -struct OverviewMapExampleView: View { - let map = Map(basemap: .imageryWithLabels()) - let scene = Scene(basemap: .imageryWithLabels()) - @State private var mapViewpoint: Viewpoint? - @State private var sceneViewpoint: Viewpoint? - @State private var visibleArea: ArcGIS.Polygon? - - var body: some View { - VStack { - MapView(map: map) - .onViewpointChanged(type: .centerAndScale) { mapViewpoint = $0 } - .onVisibleAreaChanged { visibleArea = $0 } - .overlay( - OverviewMap(viewpoint: mapViewpoint, visibleArea: visibleArea) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) - SceneView(scene: scene) - .onViewpointChanged(type: .centerAndScale) { sceneViewpoint = $0 } - .overlay( - OverviewMap(viewpoint: sceneViewpoint) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) - } - } -} - -struct OverviewMapExampleView_Previews: PreviewProvider { - static var previews: some View { - OverviewMapExampleView() - } -} diff --git a/Examples/Examples/OverviewMapExampleView_Map.swift b/Examples/Examples/OverviewMapExampleView_Map.swift new file mode 100644 index 000000000..82a3059d5 --- /dev/null +++ b/Examples/Examples/OverviewMapExampleView_Map.swift @@ -0,0 +1,44 @@ +// Copyright 2021 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 +import ArcGISToolkit + +struct OverviewMapExampleView_Map: View { + let map = Map(basemap: .imageryWithLabels()) + @State private var viewpoint: Viewpoint? + @State private var visibleArea: ArcGIS.Polygon? + + var body: some View { + VStack { + MapView(map: map) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onVisibleAreaChanged { visibleArea = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint, + visibleArea: visibleArea + ) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) + } + } +} + +struct OverviewMapExampleView_Map_Previews: PreviewProvider { + static var previews: some View { + OverviewMapExampleView_Map() + } +} diff --git a/Examples/Examples/OverviewMapExampleView_Scene.swift b/Examples/Examples/OverviewMapExampleView_Scene.swift new file mode 100644 index 000000000..b92465806 --- /dev/null +++ b/Examples/Examples/OverviewMapExampleView_Scene.swift @@ -0,0 +1,40 @@ +// Copyright 2021 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 +import ArcGISToolkit + +struct OverviewMapExampleView_Scene: View { + let scene = Scene(basemap: .imageryWithLabels()) + @State private var viewpoint: Viewpoint? + + var body: some View { + VStack { + SceneView(scene: scene) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) + } + } +} + +struct OverviewMapExampleView_Scene_Previews: PreviewProvider { + static var previews: some View { + OverviewMapExampleView_Scene() + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 78a96b14b..9b9c6a096 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -34,7 +34,8 @@ extension ExampleList { static let geoview = Self( name: "GeoView", examples: [ - AnyExample("Overview Map", content: OverviewMapExampleView()) + AnyExample("Overview Map - Map", content: OverviewMapExampleView_Map()), + AnyExample("Overview Map - Scene", content: OverviewMapExampleView_Scene()) ] ) } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index c34bc891d..9ff039160 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -56,7 +56,7 @@ public struct OverviewMap: View { private let markerSymbol: MarkerSymbol = SimpleMarkerSymbol(style: .cross, color: .red, - size: 16.0 + size: 12.0 ) /// Creates an `OverviewMap`. @@ -75,15 +75,18 @@ public struct OverviewMap: View { self.visibleArea = visibleArea self.scaleFactor = scaleFactor + // If the client did not pass in a symbol, set the appropriate + // symbol based on the presence of `visibleArea` (which indicates + // the main `GeoView` is a `MapView`). if symbol == nil { - self.symbol = visibleArea == nil ? markerSymbol : fillSymbol + self.symbol = (visibleArea == nil) ? markerSymbol : fillSymbol } else { self.symbol = symbol } let graphic = Graphic(geometry: visibleArea, - symbol: symbol) + symbol: self.symbol) // It is necessary to set the graphic and graphicsOverlay this way // in order to prevent the main geoview from recreating the @@ -115,7 +118,8 @@ public struct OverviewMap: View { .onChange(of: visibleArea, perform: { visibleArea in if let visibleArea = visibleArea { graphic.geometry = visibleArea - graphic.symbol = symbol != nil ? symbol : fillSymbol + let newSymbol = symbol as? FillSymbol + graphic.symbol = newSymbol != nil ? symbol : fillSymbol } }) .onChange(of: viewpoint, perform: { viewpoint in @@ -123,7 +127,8 @@ public struct OverviewMap: View { let viewpoint = viewpoint, let point = viewpoint.targetGeometry as? Point { graphic.geometry = point - graphic.symbol = symbol != nil ? symbol : markerSymbol + let newSymbol = symbol as? MarkerSymbol + graphic.symbol = newSymbol != nil ? newSymbol : markerSymbol } }) .onChange(of: symbol, perform: { graphic.symbol = $0 }) From 4d0fc56d57b8e6f9f955517deeefcd7643bbcc37 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Jul 2021 15:32:03 -0500 Subject: [PATCH 0073/1722] User Picker instead of two separate map/scene examples. --- Examples/Examples.xcodeproj/project.pbxproj | 12 ++---- ...Map.swift => OverviewMapExampleView.swift} | 35 ++++++++++++++-- .../OverviewMapExampleView_Scene.swift | 40 ------------------- Examples/ExamplesApp/Examples.swift | 3 +- 4 files changed, 36 insertions(+), 54 deletions(-) rename Examples/Examples/{OverviewMapExampleView_Map.swift => OverviewMapExampleView.swift} (56%) delete mode 100644 Examples/Examples/OverviewMapExampleView_Scene.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 92368507a..6cdf3059d 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -12,14 +12,13 @@ E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; }; E47ABE5D2652FF1200FD2FE3 /* ArcGIS in Embed Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - E4856844268FA111007BE537 /* OverviewMapExampleView_Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4856843268FA111007BE537 /* OverviewMapExampleView_Scene.swift */; }; E48A73432658227100F5C118 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73402658227100F5C118 /* Example.swift */; }; E48A73442658227100F5C118 /* AnyExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73412658227100F5C118 /* AnyExample.swift */; }; E48A73452658227100F5C118 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733D2658227000F5C118 /* Examples.swift */; }; E48A73462658227100F5C118 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73422658227100F5C118 /* ExampleView.swift */; }; E48A73472658227100F5C118 /* ExampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733E2658227100F5C118 /* ExampleList.swift */; }; E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E4E57DC5265D8EB00077A093 /* ArcGISToolkit */; }; - E4F9BC99265EFCAF001280FF /* OverviewMapExampleView_Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F9BC98265EFCAF001280FF /* OverviewMapExampleView_Map.swift */; }; + E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -43,14 +42,13 @@ E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; E47ABE4C2652FE0C00FD2FE3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E47ABE622653005F00FD2FE3 /* arcgis-runtime-toolkit-swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "arcgis-runtime-toolkit-swift"; path = ..; sourceTree = ""; }; - E4856843268FA111007BE537 /* OverviewMapExampleView_Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView_Scene.swift; sourceTree = ""; }; E48A733D2658227000F5C118 /* Examples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Examples.swift; sourceTree = ""; }; E48A733E2658227100F5C118 /* ExampleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleList.swift; sourceTree = ""; }; E48A733F2658227100F5C118 /* Examples.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Examples.entitlements; sourceTree = ""; }; E48A73402658227100F5C118 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; E48A73412658227100F5C118 /* AnyExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyExample.swift; sourceTree = ""; }; E48A73422658227100F5C118 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; - E4F9BC98265EFCAF001280FF /* OverviewMapExampleView_Map.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView_Map.swift; sourceTree = ""; }; + E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,8 +67,7 @@ E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( - E4F9BC98265EFCAF001280FF /* OverviewMapExampleView_Map.swift */, - E4856843268FA111007BE537 /* OverviewMapExampleView_Scene.swift */, + E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -211,10 +208,9 @@ E48A73452658227100F5C118 /* Examples.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, - E4F9BC99265EFCAF001280FF /* OverviewMapExampleView_Map.swift in Sources */, + E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, E48A73442658227100F5C118 /* AnyExample.swift in Sources */, E48A73472658227100F5C118 /* ExampleList.swift in Sources */, - E4856844268FA111007BE537 /* OverviewMapExampleView_Scene.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/Examples/OverviewMapExampleView_Map.swift b/Examples/Examples/OverviewMapExampleView.swift similarity index 56% rename from Examples/Examples/OverviewMapExampleView_Map.swift rename to Examples/Examples/OverviewMapExampleView.swift index 82a3059d5..48bacecc8 100644 --- a/Examples/Examples/OverviewMapExampleView_Map.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -15,13 +15,31 @@ import SwiftUI import ArcGIS import ArcGISToolkit -struct OverviewMapExampleView_Map: View { +struct OverviewMapExampleView: View { + enum MapOrScene { + /// The Example shows a map view. + case map + /// The Example shows a scene view. + case scene + } + let map = Map(basemap: .imageryWithLabels()) + let scene = Scene(basemap: .imageryWithLabels()) + @State private var viewpoint: Viewpoint? @State private var visibleArea: ArcGIS.Polygon? + @State var mapOrScene: MapOrScene = .map + var body: some View { - VStack { + Picker("Map or Scene", selection: $mapOrScene, content: { + Text("Map").tag(MapOrScene.map) + Text("Scene").tag(MapOrScene.scene) + }) + .pickerStyle(SegmentedPickerStyle()) + .padding() + switch mapOrScene { + case .map: MapView(map: map) .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } @@ -33,12 +51,21 @@ struct OverviewMapExampleView_Map: View { .padding(), alignment: .topTrailing ) + case .scene: + SceneView(scene: scene) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) } } } -struct OverviewMapExampleView_Map_Previews: PreviewProvider { +struct OverviewMapExampleView_Previews: PreviewProvider { static var previews: some View { - OverviewMapExampleView_Map() + OverviewMapExampleView() } } diff --git a/Examples/Examples/OverviewMapExampleView_Scene.swift b/Examples/Examples/OverviewMapExampleView_Scene.swift deleted file mode 100644 index b92465806..000000000 --- a/Examples/Examples/OverviewMapExampleView_Scene.swift +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2021 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 -import ArcGISToolkit - -struct OverviewMapExampleView_Scene: View { - let scene = Scene(basemap: .imageryWithLabels()) - @State private var viewpoint: Viewpoint? - - var body: some View { - VStack { - SceneView(scene: scene) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } - .overlay( - OverviewMap(viewpoint: viewpoint) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) - } - } -} - -struct OverviewMapExampleView_Scene_Previews: PreviewProvider { - static var previews: some View { - OverviewMapExampleView_Scene() - } -} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 9b9c6a096..78a96b14b 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -34,8 +34,7 @@ extension ExampleList { static let geoview = Self( name: "GeoView", examples: [ - AnyExample("Overview Map - Map", content: OverviewMapExampleView_Map()), - AnyExample("Overview Map - Scene", content: OverviewMapExampleView_Scene()) + AnyExample("Overview Map", content: OverviewMapExampleView()) ] ) } From 10cbdf7cfe4b71c233c273582279a68f5f491837 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Jul 2021 16:12:15 -0500 Subject: [PATCH 0074/1722] Update OVM documentation. --- .../OverviewMap/OverviewMap_MapView.png | Bin 0 -> 885203 bytes .../OverviewMap/OverviewMap_SceneView.png | Bin 0 -> 888604 bytes Documentation/OverviewMap/README.md | 126 ++++++++++++++++-- README.md | 8 +- .../Components/OverviewMap.swift | 23 ++-- 5 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 Documentation/OverviewMap/OverviewMap_MapView.png create mode 100644 Documentation/OverviewMap/OverviewMap_SceneView.png diff --git a/Documentation/OverviewMap/OverviewMap_MapView.png b/Documentation/OverviewMap/OverviewMap_MapView.png new file mode 100644 index 0000000000000000000000000000000000000000..56353d45835178491e2c5ac08bdfbc3ba36927c7 GIT binary patch literal 885203 zcmZ^~1yo$kwk_PaySsFPySs)4fu699nw4getBpd$a(tSC?~{Y?;U zrB$Q>fG^1y&*n&f^R$)|8VLYA{#At@0svlI0Kl;s03ebL0Fb!lcWQ|J zHAGtLDcY#009gLYr~o85d;r2<3GVL$fFlR|gA0P)`# zgTLuNLE&%whx4x#p&0IeJr=|NHyZAHF~Wb#2si)G)=bF;|0ZZ|3i@CG0E6J42nWc` zCj|iD`RqUFdFrVsi&(lkbC_GXT3B=VI=lVD0*LvF{1u(8J-No}O+Z zoSZ&BJ{&&09IhU=oZP~~!kk<@oIE`2e-Z3pKNnAPUv?KT@Lx{;+s`{|u%(B+o2R|2 z3++FC%`IHLJjLnh|0(Ey!oTWi?Q8$PCAonA-K@V2A$mgwsrykt(pW6KQE8if0grpnEtn>|3TIJUn)1ZAlH9V z|HJhEr^>oIxq4{1nOj;*aQ~a~AFBUA|5I}jH4l61zn%I|*Zl+kKXm`bi*f$bum91{ z{~E;q(*7Mq33M^e|CtdI=*hmexd4C^K=GaQ2Vc0l9={8lRsBt$jhnVV{^x~9WQoZ3 zHhgP%1}Q7U78bN<$d;?wk#8dhGlo;7wm-9Z8+|EgzeK@a;b6>2F^eK(hDZLjf@&Fw z7f#zR6Dq)GS2WKiF5dIn>N9(A6|@2U$&;s<0eyMhxZ4QGv<)e|$_LM$20h0W89O^W zw@pvi3mhirjye4JCfH{XeBNCX+?3kOk$39B-Y_Mz!m`O4ZwAiQxa%RN~#<_WjItPl%**Ox4&-QNnC*=J_|=QD^%=28G+QGNO23SF zP*VoLzT_gzcqmlWv&%2G8r2yTUE$q5&PuEzx4A%GkCd*z*nLZj5D?F49nylHTDiu1 z`#Y1G+^#`zny%-sfcb-iuE`fs!4-F&0**kpwVyxF`!RdX=L=Xvo^gKXOGDF+B5z?k z#lEefZLEh(Cc*nxpK8#!LN11ATy0Z^#;Aj<6TRD{Y!~)9jqKBsgGyCgX4gDVoJ1vj zdJFmjE3pF7ZX1J7e4rgyBx{zO0d-h{D5rKF>n82NJw0xx3}TdRJq0hVEnikmd|#sk zOqHQ!oKM}i*S0Q@+F4^)&VXl4QLuj>R`fW+b%)1Z`Oi-GE@vmoHn8p8+Usjht5bw` zAfLUUD>cMW&{grRr`1Ypn|*+5m-D$(#BvkWx=46P{ps0xBopu9fvr%a?VPUuhGtuI zgi(>It-TWVx{=6qcB;BG&1dV^=f}~Ul6S59> zTNOBtL#;jyx4CY*jE%{UAJE>K z1q4)yhuk=5xp@2dthrgPj#31fk;MdWm)W_x0_6h@RYP|bL*`tytmN*+ID2*p*LNqB zpZ77=emYZ>B^s6OEJ;eZ3{kYL-QSuU{_5v+nz&z`g$(_i_C6Vm5;%4(=X`*WaulC_ zcBb7#u&VZPTX~dcbOvw*S$*wmSw*HO6VU#18Bt{^;}1`oUXdaU?gNgIa6 zU%D@Bx|}~9f=_K<_xc=6pGW1_Iu;Ms#w5L(#la`5w->TiP(#zFkKQ&Su0U94wBjGh zjR(>38=a^4kYlgYU!jiuHxoN@U6;SS>{{)#4j&8e{-_2_ABcbNh3#>=O1?DQK~OkP z+uDL2PN2k{!Qo4R<H_Dk`${_nW2hY>|CkHwW z6_DizM0O<5yH$TQGUU}Z@Wmh5xQ}= zim)a_?{(t;bJ5JanI+(;=Xo}G{s2$}PYqLny|2$WVy+(Pvw zIjS-{9vhf}GK?OnxcWLn2VkydyDn-;)vPi?4;!V%!w;v%{&)j(IRZ2=Bipf4l}7(= z=OU5CRyPm|)7Ac{0J2T3xZ6YUXDse0)_m;YE|%g{{G4md{PG3 zGj>qwI-wi+)SV7usvX(_ej9H_uwN_k+yvfzq zR`BBo(XfGyuG_&IZwK2l@z-q>v|!{p^q9na3e33x!!Xu)0f0diKLyHxF7phE*TTXV_xSl6+?0>DO!J-2Glb< z+e#;1c+$Imte0HrW->ABSgC$EI<#I@w<&op8w9eA{eVFD1jGtfPnS4SDtuo`{76ht zk9QocSDKUswu)fMZ^U(phxL}$t54sYL!8W79c*XvU1lXhntzsVa3FMO1&DegxJJfH z&jSKmn^L>-pqTjvT}?vr9nLUm?9})b{oVZS`RPxlOou_c@2Jko7$L;sh#q!GiLe^W z2Fz|%x57l#r}?xHs&KSM^dEVSxA^55@;bmO(@pn zm8z9`FdpSAE91vqB1nZGGzIY&36!G0lap2**?*7nt)R7I?WOgFFvXAXmo%aAGiE8N z_O;nFY7LD!B#BE|2~z`RLfRI=|C1%KmGSHdFL$`#og+|qsl6Jfmh^i#U2yVky{+PG zS^M+MVaC;rzMtgYlIIC?HEJ=*w^Topn-SPI9CfL~IXnn1|IDF?jwCkGfExq_zKl-Q z4BUnlH#3dzG&PXWNnjt2uV7H{jkH#%SX6$9TGKnuT?PS0;wB4>QdK6*bxUM`2RnSH znFZImMk1%@oj4aSld{f_!CVr&GV`Qn#Ltwnh#acF7&>FFApI$LGV{7tHE z0qw)Qz_;eII9@VaVs760q(>DR`ife9$^jo6$q0WdhN|RE2AHay%AJOb2Q+j$raI;F zi0nR0lv8n&i_5|wE&Mhg{Asn~V~Y>bb4pfUO#;q*mkQrv{Jt2O_I3}Ucp%FeWUM<5_BW}+%YY_ z>I zd{5h9eD74LJUQl&M!Z!!jt~vOS1dWm$WHg4h&;q8aqY{xP~R~x0HnkcoGf1MPt_`v zO0H6y9Uz)@JF`uh^v8STkH-gm5cAQ5=`2)4&w zy3acPF~#Y^>k6Axx|wrN{5o1sqBlSx_LPJ#m)}B#rH{3}Gky>Gtv?)pS0D>wH-4$wxiO0n92NJY#p zkWTTK+M9Q2o|yLXv8Er{^(-dD$UuE2bzqbmjnm$q7M?0b9&+S`d04C_;wThW*dM(J zPT#j{uC~qln%5iVgY@DhBMWsePZ&xuL;yYC@}kZ+!M>BJ+e}sAJR=v$uC>kpY#V~@ zSAK-Ot*Jq~sJAhTEX3iuL=%WPL~t9fS@b?&lhckkwEG_HnUs{B5q5|)Dr;+3{8hTt zl~Nu}?0Z_}WP)@X#DM2>{4XWZdoG4O`_Eo+$~n0e=9qmGwwm*TE)e#Glc$77@8U7! zQC)iHx=Lq9xzE~05VG~FM^dA)xo@{IRveMjkl*3nR7kFP%5nW@VomR1+W{0mvqA=K z4MpsriC0=E#S_`zt=%f7NF!Dwg7plE5FSM6ne*9~U0j4@B3KzkP@3Oa8KY7ktoSWH z4r%m>U#=**As0E(JYJwcqrNpd+j5-oSJ6*B7i+^*QUn-DH`nb+I13zUo(`K&R}Wku zzjcbHoLaiFY1)_<5%|!4Q6vNkM*Oi`uteX1*Wzr0Ruu&{h`)ZEWh54MqPd5i^G10{ zU3CALNhu7vE>IGa@iT-vx4;>ge~*rFNn2w-Hdb`MYrCd3D>kAKJpKWP zEa|tk1ftM8Ipii<+MUuqUIbA$niQ2#y;K?(de-##LjUNxp-t{0EXpV3GHWjbdkPov zKY?g`gLYk&T6HDnckN}jEbkgJa5L4R^PPy{EAlYPEt^+6{vb^dmUrobG&2~zp31TtcbxjF`cp|;q;Dpd z7s!-)o5WgaGo47H(H+O`d8p&9WSeZD)`Yhc_@|n#p{)RH$BRfG32asTptctMm}FFE zlMaDzgF`RJBUF<5en&TsvJxg{s z^5)+SwqmKkkVKfG3yV$CCsX*C4n|1r#YVW_S!V#zL&63q^ZYF$Edkb2qJtARZs{PT z0)kz(0gX-AM?7s%I_kR#PXOxd*#1Rpo@%s1WGn6hzjMTT*3y2P;!pWBJ#?Hlt`|Ku1gpz8BYUa z%5W)bmd%&QdNms4HwaeNKnMXc!q6G+sD;lF>GBpoe>=8uO-%Y+)pC6V;dOQ$pr|zw z&HObmZs=I{fDU|G`*t+X@05sH`5|&wewi3i_#2ipGsNsoOQ+x9BQgjkODQJ2G$Mf= zD`ou-+B0u*Xl)Aqb)G73zM*KV# z&iBUAbVdwDJ(Jf@J?5W#!;sv!opE|(TT~h$e3c@4im`c_>m7EK+Fa!gJu$ueb%w$2^1f@o+SRbG{#W9fqoq; z{B(6^>J(6Z<1>>#)R1HB`FG!5Akqvqc|44VZbE971q?TrBx$9; zbiO3r-3xrx{;oS`K9N5>%fjUH_VpM3ud7dNQ0u{durHs5P^TS2-4 zoNf`al~bM}d3Qk@c>SsSx|e459-WaEMMVp!|A|AmPlF#pAc_b3Yxs_kB<3+v?VhfIbg>`?Hy0dv=WX6DW>U zcyS9O;Sg>$&a%{BXu6w?+3hR?9#6Gd%G$7M@;OynHW`&#Z44z_+ObA3iZSS_ggYe! zcX=E&OH?G*=YDOX?lL5%@dq_ALHC0mgJjX@V(SQZ&=S7JdEDN5BerpV9c^Dx# zJkPh6mRnA$l1ujzjiS^!{GE2OG5R-&MDcdyM`IJwqi}pRyv1-{GQz64G{JVb84sTV zI?L+r;|$FpSBHTGnmMVo{v~j_l~JOPDsoWi)^f!Qp75{IR-Ao+H`x3_@gC(Or`gCY z1s;dSZfJE&a5TyN%xw0bI&WiC5wR!rm&^uh2trQYb(5@DbFOdiwj{8HvmRXk92irX z{6@R@XPi{~m&a$>iat1CCmyhm$fK85rIUPpUr-1r2&`nNOrYi;&r6_uO0ZCQyRkvh znkO`f77rCrO#C$%GYy9U(Tn_U%eU(}(&D(h7A7r8xwhe9PKcc)Wifg_ly8%ywTrhu*Xu<-7>UW~nK>6w)GG<&|fLpi|%lL4W;Dhaal66 z5;PMJ5gkm7v>k96Ujpcbk<2Q8p!XqJ$=(@Tq#C10KrGwKTjQJYWq~c^+sl4(mf# z&?5L^^dY&TyEMohoisdG)E3y(sisr?P)X^A*qFEo``2D5jyAt9&0c>FHJxVy4MipL;wfDV?e! zOim2C`8q`9i(y4xq3ouUz3h9`U$;XTbE$oAeu37|(`)wb+}xBp=>8y8Lv*u5$?WDc zWSf)6c`m2dW8e((_R|bSe>mn!(dn)JUOxH};{6Y1=j@9WYOfLD(+KF{E-_Fi+&!Vo zx&AxNJ|~tQ3c=d2J>JCde1o2TMA1lpSyA0%{uehp{~Xu*o!B25oi+XiJhDP&=G0%< z=$v8i+_Uo%=7yn|sh%Ph?u+#id3F77V{K9?QO9OjG8RU9DNANcn#)JC7dU{%k2xLc ziSy0FTzeg|Mbt~$;z#MNs7*X~bcwqy7{lIQwuFEYuKS)bkb`asJT`wB_BGGn$+zH- zY1l8gpWh1TJ_#A7;4`3RPq#rOn2+k~wS;8h&CjIeK(+mti+OEXJO9v(9b9I(Fiz6@nj}J3#_y|c6aZ7pzIcqS^Rt=~I2STvs z$UC~kOo!Hf;&CKBpS}VP9590M6LU3;*BMf{eM50Dv{L)02$|-)>@`61vI&^LX*Q40 z8DM@mhPx+rC6TrV9KPzJp~#Qk_#g&E8CoWNofnU_+~HI_hL6XVOf~PzR}WH4xyv*U z94Ii}c}i5`W|2~&NK~Q{p*-Zcu!jb)MMyg#rTkFw9^hJSbLPX2mD8dvrrvFAT7 zu4F9@CyT>|t?=3TKK503a(+{&=xj@u5oM6Bo1-6h12Ij6+FrIpY(thT!X~PDNX6o9 zEu2v*T|(Wd5)L#|B8M@_yi9_pX;?3scm+YX%Ex~;*&&N3X(isptnO!u_X0@}#CT7% z35r|vyq`NGaB&F*BG~lyj2d6em9W?%Ssuzh<5b%lD-E^E_%5Kc}|P1<_Sx;6`a z^$sn|P74zeb}x4Tpf*@RKUkA!TAyVV501%18fT9vI#{KI#+ZIeUC4;S9#o6r0JSt+ zbKfhEV!w-?8BiiyV!{L^0Sc~&QS6i0c)v~f@>EhdD@pQJ7A2KTaNmwS=*dU=Sz-HP zyFMtap^kONSVnJx7y9rMWFl@SZ|V-C~dzR13^JT}1qTDG;}x{TL(rDlrms z-<~F|fgCs~`u6QwllX%M=t+c-WzEr_7&#+1JiX2iXWvc_U=+G%^N!e(C>#+F*LxQ+ zdIqHuhvN58jkBDdc$?2;$GXs+inH0_i=NgVU#DPc#&yj}(dDBy3gqX)ir-I~;T`3HQHr$>Yj zy3W5@UxYxk>6&k>=IEMGDNEux?@FPIXKI8HnsWW{+L80T}VB!BP+CI<^I&aELhBj6fsDO zLvheaW6o-fa&E-2nlIxh|2rugevxX~TD&@Oev#I8hO`?r-jcE;nd0?*n~3}duM#$F zts5hfYXGYYeEq<1&YjEchMQ#7k*;!B%RZw#2Ge=5TgC zkj~*jwv-V^t~L*%h0jCh@-r`e1GH=j13DnYMIgKT?Eh1S0GylhS@t3Jfq1K@5lL)@VY*BRww%i zk)+nw;UU!z-B1)~-x~(V)3Kz} z@QBFhi@BJ;8xmZqMOLv&1(1{4i;Tc!*P3QuZi&TsqNj87DkaL00+ugcbC-@Qtu4Sv+zV_kY>5Tm zbr5++8cy?e?@40)*gBWo#oZs6mfe3}%+7j~dEn!w`o^A1tieNo)Uf{vE=h8BqV$;~ z{bTc2=d$;q>ez(Zoyp-9uI5{w2s_)BKy3Xij_%TrVXB})BWibJU2!wQ13RCjGGjBY zO44Fdz8HKXQ;TOt(nO+UPN9P?E2~5%Ga-GVIN95A5F(_$jIU?!v&5d!{cZ(qGMXFT z#&Oh@-mz5MFNqqvNq!RJs7Mxbc}QaYi5iT zrT#uH8h(d#Xd0Pl2M#+l=J9|WK2W}hZ|fOTi+Y^GMKKF=>X|@I(=58Kq5M`M*Ej;c zchUwk`^uD5i0U&)U$#I6FVqj6ld*g{*$-QN-5BO^{5?1!$2V>VtB}2CfXRCBqqV)? zmM7$3(wvx#|H2wc zEiY|h#6i|}Z|WE7G@aomaCF*@zUiJOSw22#XRcQ2Kn9UMr@oHK*;NK4Di-#GDGNQ=+cpvr=IU7 z7@l9#u*uRoyi02;G%&fQ%~l3eDR6ECyyvq=O1AlK2u0qIDxVL$mHDdEN~)5dAX**S zK{^v2)>nkVd)5n<7{?risLRsl;i$|hXQvAb0Vanri6e6A^ zG5)g$OL3gM?JDWe*$XW7z&DMUe5^s!Q8%#0-I-^`#Kimq6{;tlbO%UPz1c(%cl88$xqX#~n$NBm=6;{@}CEcF5HuP#i*%*2?c?6G2n0TtFFr47UaNG2v=fALJv*VxqO%xG^NYYfZyCt6dgp1%`anO=urIYLLws znTw)5i{vvuLLE(efLcy|9vWkNHdtXS+kX`(!^~g2|IWNm;#Y|;uDg-c1RismGROV8 zZX1E++YVrquO^$9=@ODULRC!?Bd#Nwd$@Clg{eGjSvFxG;3LvKZISI|1jyUZkm)!?O4#{MJcmker7O%ZeRP_)w$$m&mZ7dA8-g@*qhj-c|N4fB~<_=vl+V%F} zwC_iLqbUB`<@jYLt?9w`tYRMwegF-WK2uR;f+jO(1;$oARMRPb&IM+xat2jVpK=hF ze_j;e(~y|g6%;4K8mBGRO6$0+Mvy0)rz^;n7^1307Gf_dR^KnU^O{-kyteYESd|0L zv+H&}RZ|dxULkJ%suYz^(o8gaSmWXGAZc5*)b+AA2guxoLtrbQK>p_O=fKJgA+eg$ z<~^O6ow*))$);rvk3+#g3mIOECPg+p^=`5}ML*|+q(zEPbIOH_a#VZzHObmKIqf-P z9NDPEDHfmX;OlBv)9KI3jV@PB%G~`k`^6a&N!Su+5S!;{_2Ag*{W$b);lea=%ER=` z?^0-qKS?Qy9&;h4p~Yb9eXGy=eLxdDCt$Y_4NAa6{#d+f@RfEVCJ04bK8lI3;)7U0 zB#-$J-C3u0oLURlfE-Q(Cf_IJ5!?afr6|d&#zCzMCr4?2i#=3~s5{goAAppRnE}0_ z84=Lp9c5T{iN|}Q!M$b(0u5x?50k4-Hu6L6sUSEl?qVdn6Q#JX-VJqIa! z$y_H!9!7L{-OmrYkXinRu7ao&dnjmbXvW92>yDhR_|8~jLeyFOF?*Te=io#@^}MlL~|hX#d)gP&>PM zJl%|{dLit5LVH2o^g`MgaRKO@Io0pylA84^TeZJ$(%~h*^>J|Z3i4XN13|k~5fDUw z-4->(9m;njF*P1rY?8~|29w}UfjuH$ncAfN8bWfJsHBsebj?y(pnI?JhgA@nbASXW z=C*>}2Z?vt$>ICThw4hHLdN%v;#I?D_i7_4cD`*yAWRt9)16{N3d%NaZheE;O~<9h zzCr>Y@#Uo-!>1S@TTs#5h6n}fh+-rfEiZc5*AWYpSDEusMs~J^VzEK+E|MJGt6|=S z+R)|3pBBJ0gu_|jTcHL_JzUGJ+nf)xSW{&*LcUZbBHOWQ^45>Kww?5y0_von>{A;E zm8AA3^_&|eoVghbFtB1|d4kHrej>)WqX6eoQjgqy*-b%gpQ`8i0}qT4f(J+07`1c( z1RuUENw4KM(b}V8p%TCZlTYfXK70FPn`XhVy=1{YK&A9HEx7!vlNY+H`^lzB_`=I& z={T83^FcRfigq zX&w#-(WHL)*p97k;Ae?3V)zmt-ST*2h$};a7s<_{-Km_4ZDD3K^xj(WL7rwkFW=JZ z2#&97AeKae5R)8i#YQD#s>uh2P>5bkcqM=1id#K1iLXiJ&qqK|V%A|IIfK9+uszRVnPSba8n1-Khi3+dBc@ z-psge>F-q>Xj^)UD?S@GcZ!{72IY~>=Cy1V)yol{Ekh5tU6KLwiM*`MnqYXkH`Ugh z1W=sfYv1*_>r;9ai)vy%D*kCqh>K{uy2cTFbqR0NgydRu`1Afy%liN?kO3gRus{{P z09+^DD~e}sFEJ8+4^dQHwHJf>D`PkR5h!!@NEZ;A<1 z)aG%zAnnt3q)ugYOj0&i%5kEyA`GeLYZQ(^<2R~DfknF=I@POv;hLA|JPbxpP8Tc4J*67J(X|u3*|Nb-a#%&7%i~oz z6+YU{6WfMhy|fyAulkWX%Y*Q+0lwo6rr{ktNmKNE`*{^(EBRtR^BJGf9oy;eIrow> z3M$sA^&LMl$=h03*|WmxT$)+JKx!Zne^H|`n~O#?(WsD35uHBHOYz^YSq!g6kiep=H`Yq?I>YO9~jx7k0bb!?@~HHBIQA zo!b85@3uF}MDJSRhqJD=F8E8gfx&Ty&+IPg5s)qE$f@_Cq)1!IGq@S?0478P#0?_* zBoGh$R^il4l5Qq{bxnruEiUN-XWtTwLK`UyNRj$hy%;sw6VH@3DGk@;fgS%Xr#&D4 z$!spZ{lr&{f>qM&`}U_ybiu5BbkV4c7U5*@utw8;e}}dc>x;13;riQ|Prd8I^!8>_ zT*c$kqP*$wwXz?*2}bq{*NV(=hH4|ylYea3j39E1!FgP9$b1@ z4$uGlx5o)NyPJaU<9z>u%Y6UkD}><>zH$RpsR}-k2du{sMsZVj$)@Ce^Q*+*zK#oe?#Cr|Vd5u&WgeyXO<8adhAy9A&zn_fzvnva4Ye=9x zxo%ZeY6+jS_=}=zL{&1$ZarZ`YK^H+vnc`5jjgh;YB9o!KOef#pWr9q7GIO$>^*iV z2k#;-==F+`ywjYBobt6YHYQ(v_D4yT0>VjC7+=!7Of)7nobiL%VYxDKkX>Y}lYCEo zLKvK+swlQbZaml>gkovxlG`-+o)N>+bUb%f=y;ba%t-J@a%$##_sY3gf1teUcmbO` zHlbu8)|O5JwF+GqTv%C#l^iS?xW~MVxB(Yi6iYyBy^BiC!R6e+_mbt32Buk$)2-~UN^4ic;jR?=1!vum`ha1rd0eQA?-r}{(-!ud8w_+AU zZL>|Cj^re8zdqnjVgjAgm-QWmD?+6;krrX6w61m{slB{okMB| zZa?V2EqHVKh=m3xc5-TsKjQ8x6mb@HS%DBQs+^iRGJXnXkP6(!iFHCcDj=FGt{1ot zFJ*bDi~4NWy=c>tCBeQE1KjIO>)h3rz_)o>JYhCX(8krB;w7D5$-$dPFUg@BNwPyG zjiE!->A-)Zpl6;U3}F`=baEy_Qel>1K{4L^BIEtsNJ9)yD!q%#cHcih^LkQj>=v51 zjf>nM_Z8PGW5v57S;cL!Cz1Ua^xMSfOYCR)H~H7>;b*+s`a%1#n}L<>{RSsLjL4Rc zfY@=+GhNn(6*OOYbVN&h*_JcIxgHu?!tUpxY*gF}WGufN9zM3{jmzHUZuzcVRNsx} z)_sqG*#N% zrGfdCS5^(*-QFBR71wPzHTJQ=UKdmX@R`rn-}^tk5EQC`bu;Ox(G5RADzO=XA-h7N z8uceDU%sB=t%S*09t>@F&~602F$UE;Fk6@Bz8H+T{^skcX%I5_UVVgEn($2hB}p1+ zPW~-5v(u)}@0El$h<72}JbF7nHW%c0me;|wkckm3GBbU;jT`9rfjVvc1J=uXga&5D z%76kXH$)bTTqsSrwW}j;)cRj`%{e-VW z7VX9h5i?OPJcAyBd1=gflw7&$Y@-q)G9uas3_XMuNXWhoDsbEfcY$vdaxh=#CS@NY zBJF`5wNwkEbJe5MuoaCg3)((;ME6Q%s+vlgjzJ*#^m@IO+=Kis(U?>saMzcle+KhDUd`(%G zpPZL6N(BpyG@si3N_1mfFJi(O+{~dl5l`-G4uAwEVQu>aQETUfC$~0EF;j|s;I#Yd z0L09fMR$`P1JsfIs4bZLdzt+d{jy?_bz_`{c_vJGnE6smK26@a-?L?#n-3n%c=kII zIiaqAagqYiy&(KDtG+%EWSJ_tX5VW zldc*hB5#>+W<19;nD}4p;g!>rw7x;nRVk$_wItC~pTxFt zjM7t_Xoyy9ZWa0)4-GQZ zp9#Uv1QYl;+}uMku+ve=95!FC`n(dRj??JZm-~?*>1uuHtm`uG5$g$8(>)k=*$WwJ zOS$H&|2*7dab&u`Wr=K4(M5kEMl_$dM+yGu%L&gh)Wb2I9>t{CIq~w@zU+tm9joMpQxRHbyPE0U_*;Pl2OvH<-AQ= z($XGj!o48$LkQ^!nN5*U7H^C0t(D1$5yVhzNfIxNuasv*&AU^3NNLRCzl7}@A?esQsPJz?LgzIy zDOz#HrLpv9YQpr{R`?2fD|v5ED$W;(*5q(Lc#GPKjmf_ixTsT3$@@&x)K+uxP#yc! zKpf0*u}BB=otFoiJ$Uc#lQwLWP)aafsrRNL2ry{-?n=~%IX=}pJiwZlg_ zJ1Uf0vr94ekdh~$kyMn|Uf<3m?`_9s)QyQ*cuvh|#gy-=Wy|w{w-kQp@;$C~Jgu;B zl;K3cCA?sHxB2a_RzW+!wyO;1^wCZGM@H$IDEDsToHz<7^-E3u8h#0EQry;Xb7S%x`4kYNaJDl>g;2p7C0>&@6?uy_B(MG}HkbzWz;f9T)L(Pv z`x9}EX)^+f(6+chc3Yu{<0|GGrs|+GUQJH2i^8u4tsi>YxSii25h7Jt?{?AP1bk=k zjB)mI^Ah7F-3;~K;Qe{VV!fpS+I?GHj`f>I0rYU+n8x1ib$Q%#DD!nvp{9(Zs+_7z zHw1#FDlYZ8l=C7!nKfju>NYmIeIFV(r|9Y0Klqs%&ImIZ4qxN|{he(C9d@O{M|y+? zUa87U(!$_5r1)D5b&(Tg@p9MQkC`pz>ndb_hRwe@8hD#zI*zO}QIT{yYFwx&@e7tG%BVa>Y=GR z{GSO>Fx+PN4{C`rQ6-zuGVikhL^JD{5qz&dkfYkYQBXs6Kjeny*=V_W<(iAW&tY+- zB!ri!krAyZ-bPjCwi{%knZ#YuPA_zA%z$6j`SJS+d&;Duft3V_B}`h;_gJSOd~p+w zvYbD2kbp;)#=g8chA^j0@51dK6<9v6M&Ze6#GlKuQ@oW1nqX2;ka$qRg8F=GwWs{h z!!Uyvo+qz?I?I38vk%CqlP_ss! zZQV_E0*MY&a+NZ0O;ryQQjXnYM)<2^r=Bab`?wm5oHCKDJT}cU8~!?4>nGg+EgOQbSICclrrm+*^qRv!@WPw@>J|DUy&>7 zJPO}+1f6R@SsG3>CDe(V5t^G0ae>p<#;Tw2M38zI*%sClL7lhul}TQr6R&axk!9d& zWC87_q1CY@)zhcrPQ$r}l;UXZHVs;Y*Z@xgoDa+%5%=h{N^Mx!Ufy#U`nXw#IDFIV zygwff)7%0)UwjT0CS^?y0D@yR*me7CQX&kNUxpkmsS3@Q4qQ_7AzDk8j)0b5bRPCx z3Biu0$X0|u@!k6^6N7ZQ5r$HXVFDMW5}Et!emqGq$I7E@FTw>meBpgJ>kyc6^7j_t z2BG(Ks<&lm5MS&}AoI_Yizp7G4St|9Pg9To4^u#_ zzv{X0cFZe4XiK{c0!DP*XekraFh!@*MUT`cqA(|Iy2Y}--NO`~!rTc`{kC(7bLWc- zUJD_D_1*Ht^im|N#1Y0g*w(t@owm9*Rot{*eTZe0+&UmHLnX}mUrAAlRy`}zw2JFt zKj|w3u5*QK42fW<{Ar#)z#t9)AdUT&#+u#g`mOK*YrCyt=B~H8>~KeKhY7}{E1!7smkjz_*wX26i!pN zEk`O412)>tLbs%iF(l}ZMoi3|%Sr};Yrf<=8%JOud}+D?_K4Kx7^mTa4Ws+Nr`iAj zKmbWZK~!^8J~}I#h1>5k2E`NcbDqu@d@s$4m|`X3$Bkz7E_?bT1EfbCIb)R$*%-pn z&kHYH?bi|1WEKOs<&=SKt>-80%-JnrKr>osCUZs@nusqh~R zl|}3EpHJz7`cwg%l=OPOxF$U{TJ>#P8BN}Eu7Q_!-cy!!_SR8GkkYhenR?p_nF8qH z;~gHt?cud_?z4Ns&U@$2vqvuuwGOA2%mRTc zk8e2_577Q6i)hE#0AyXe;Q~RkBZH)Np#z2b10JXHxoQ@dxwuMyt|^}32~T+Kn*oM; zDkg5!;}F(&R~7tZ89a|p+LyM=(X7)V{DwBsYvf)TkRQs8-x5STD-cv(i&xpuh3oP) z0gG9_$QoVI4?MK6!~;>7EFc;#^Y=Db!dQSx#9AF?kYl%WV)uw%5~sP9+JqMdc5XDd}!VE7%F#8HQ_4i4;| zBg!JupdcJ~jYah%y)yShEAm0(pT#!|#a0CLB37)iQa3l}Jz{Lwa z98E?@&_!i2${jIslRuTFRh~5hDwCh$kU9iqLbO9M9Av#! zh6n$7s5Q8S+3*~1m%WjJSJ+*#>FUB5dZtk9FLD=UbtI~hRgV@tUw91|320Pi)5w$o z9Pq;!fvZupWXMD8FwwO2q0ebD*W;X=uM9ipq5mFpf4BMGa_)`CFU*2EpL{@2ag`H7 zlkAk#<1q~uF=7x{mCD|;I+zW3A)hxeXuG>EHk!tvp67K~S?obSI_%6*GcW3c_AVmw z*}yDB+Hw_V$-|J%R9x-rh-|#4JQ<{v^D4=yzvWPrbA$3y5G5@0r{c}0_+;NYnGpNV zcgcQ@=rr^v;BR2qWs}@6y(>d6;b%rE7;u|%BXtTC-}xCE!?`+~Y#n^mCc6K1nKR02 zEBUYe&_Rl${Ely+4Q-eUJWIDb$4+^8TFz{3e<)k{xcVFUmyfimL$z)Qk zce6Lb=Wp+4ltv%Q?Sw~T%&55Ak+ePW9kP~6rU(3>`@Pd}!Cyvr&|&NeHcvY@g@#Nb zWW9^e_DeL)DU?U+K#@i^J zdqG@>C-Y!XNBeu*2qo7ys^uc4W$Ks8!S9rjaQ>6Y-7lNxxA!9?X)$GvFh&y2#~ru{ zUcgGD8op|Ut6g_|hb{r4AhD(Di+eIwh!mU{9!i6w_ro;$7Gk69TNoqfIJez$i~=>v zrLOgirtB&BSl2df`|hV4or)<1g@HyWA0?o?dsUXBbKgyjI7n^5KAq9Fh8P8Q^ruV+ zuU~ijtKar3uQd4fZJ#rouujOgKe%wlJ|vy5Y|}S+R6Eko-{1jz-DxM3;M{V~Y3gu% zUe{Eb!J75tMIvJ0oAEf!l#%Cv@459rR5?ntNTWuUDzI<7xXq&oC#+nxUAG`}V+09P zj#xmXlXSF>)D>YDv>BDEIMRTHC$x3Me!w$#@8)@J=lTtp3McAPPPS+~%jk%sMo3wv zKls-jIsnTGwMI_mj&b7K$H~$r{d?W?v5O~ktZp5sLmj&t7Gq7{t~2LN$&Nd9;6omH z2;V8Mfpt*^oMe>A$Q)zf?o!#xk%Q5rDLxZqf#NfAbcLR7f9>S%Gb2 zUy#D9JeI;cg~5Bs!ja5j1DL^|aw6{HTO&mA*QiTcDNyu?1an@&d^yz8fhtpfQ#^mV za?+&xc*8cUT_s$a*_ZrVx5HN%{^~dKObOfT;Pjf0w7_hFxpM5b>HYA4-(&W)?b4N& zOMP`q`-kj!*iK{?+4MnwkCKHo?bfh+Jup)*?E|mhU4MbciX2|^yo0CvjVPk0n~YND zSwwbfdq{!x!_=~iF>i{Rwmx*TLS z=<`U{g|=L&?eo^ol{!^G3G{>QLFw0LhOHk*w0^x^ocOxm(lc$o<*4c>?d!0yd5Qjz z_5@{Q%NP|V`@yQ)G3^J304x7AI5`!6L<7w8bZKuDV+*ByZ&{gg0! zJ9>8GM@ zh12TY(ft%>55Mi#QJQH2h1(;Gue@$s6cxrwQL=)L=zoKG16x?PQtfE0NZYwGE}K7a zzxr25WVY?PKm@@{bf$t(CfuPX8cSVJb(3jR9;W)NN8AtmTk3-&$QM({@BQ)Bjlt=h+hc1>E2HJG4&R`(|TNx1N0h69W&`N&f zKjrCY0&FuJDqPBAGZYN_To95uQ0nSX#ZgD-_I9Q{Z{Oy5Q&LkgC^#!&m&cJekf)iX zx`#t~E-^~xOe<9YKmu6C+vks+3D%{id z0g@}oyC6_-0}J&EcJa37{GmK_*G{2t9@3tI(4b`AYr@pQ?z!tEwwbFo5c73!j!<`* z+rEptZRr%Gavj32E*X{80#N$NfXVi>ckorN(6`P~+IjeuHsmqaT_{)POZ}oeGV55# zjD$g=sb16WI#d=PP{%BT@Kj1(2jqGLGe%j#PPvSpX=BbKOg_QjYV^nh9T&~`X8&OQ zjA`E?PBZhH1d+obFL1}Owv5 z4;WDsJn@b`Vn{O@Tm#k>&&_6`Et#MU0<`QE;x)fX3 zBb>l(nvdcDW<}caB!uByyttv0i6dOoOM$ds37Iq&0+#wVBSE_pn*c+=2(140VPjW} zVV}~b>=5n_pU52LDxiU%m`GEm1>S$cQB>{om(nP3+j<3buROO z#U?)D)G0;;ngU55!9r8BF8MZ1Cfo-oM6dr`Xd+!bOu3rcznAV_7kA4zlBszpV1$Ok zE5A|)3L52h&NxN~<@2yT`C@$t5QE|_D3z{3=_#gC`v{2}a%2;5>+#R>vdX!nA&(l= zqeD6gc&x!ws-=hgfd7t~JBR9Pq;~h%^#w(VF;q6vrojkD+V1$cHfr3-JTnf@y-Z)Y z>MCvC&q|Qx2u_qDI{XnYo2$lb&?YtKX+7}!OW8A+y7D}~k#XlLjq0Uqj2$G9J~pI` z~k9s`OcI-N~TkPoABVUuSWJxOBVRyMqr zjW94kHGMN6p*2ndE9V& zN&b}+5sq>eUAr1lM_V55!nAaA5z0UeXbK^Kr^i%`iwkY1wxUb%*dF(KR`;~IGRd!r_ zbuP-BbE%of1&7!+ssxfXAMDn)4Tw6u?Rc48mPj{cl{Dp@zxCdDh#oTMX*zkEZCy{F zIbAk~d%iBsDFXY~dCt)v;q*oKZn}dMr9V(6jg2m%1AQ0OL0|Vi&ZaNC0VEoP({xRT z!Gw-_W8)P4pQXOCOasY+kyT<^Bk6byt@7F@=$x{1(*%7`FMOQO!BjSyfi}b5DRkIX zzh;bpkGm_kgu>S6?M7J@Z=cE;zKE_&H>XcQrpwec@-k4G`=jkqD6LDIRati*0kf8 zO{8ljEa?{Jc2tA{a>A8hgzs!RAe?K=3`}gt z`IG&hJx|buIu92(J1>qcR-815v--7t@vrLzhjiScAE&=Q{)s2shkx&5Jg0n(0nimv&PSx0ciMvwT#nv7 z`q;zmjcUhA|qtJGa45$j(S{Z(4P3zQ|;p)`FLJb;TEy7$q^+( z1RPTeyOS+_mm$=pp^l$V8T{_>|g zkKRz$w{Dmm+QYePD3v=Mh>k>jb+Q^sBWrgna#IqYW44^Z%?1(mF?@58M&9%vdZlkG$47uhLQUfts?k_CofDoy%un2LH&5 zHhO)S*$EJwSNgMXwU3N&xvqTX2p!!Stj+1U4l{4njWo{@-aI z`oR0!7Xi;MN;2rPE`>*Pu(q^su))hkvXKM7XKWH8yaU_7ZkF3!a45@Vi!+$@ns)rPvxsBXNxh%7>6b>X5ONMC5=S&bs8^NaYCQ?3)ns zN*wAMR+ zy3XI%zwVXopS|N<`TXdQ{!IIOKlInzMdpg!EA%49;3gx?%`^MF=EpREqDZR>FotnJ zWOs!fN51EK{$_jb`RjMu{CB_Q8xbUzU6WQ|1iHa!>myHmqJ7J^{l_V@&lYK4_N8xZ z|Kgp$md|&8*I#IVxx1C#F5=NK$7m^FC}$#Xf*`NEaK#bU0gq5QWLvWscIO%auJF`|5s=XQ zsl9V{gd;cl=kbNZn~~$a-P6I{x}_^Bbs(W!fTJAe`RTnSj;GszL0xb|2j+;7Tjzd# zBxlY%j`T?c4nK~0B*E?MVwh}*{~A8`6&2RZU(I~)Zy9-aS7Hd2OptKJWFiMbl_U6x zBXp-Nop?aykFsnz^}B2Yjtpn&$jiBCN76o*YOt2+5`eM1&RQ9=O<~i}3#fY0_3S;b zNuSWj)Aivk@snnE9NSv%R7_lrbO^$8z+9CHyupM~Ak^_%Vn%nLGkd_EVp<4{I2J9R*n} zFVGtUMx3r2(1?>CZF)ws&~teG8m~Bm`3jCgTv0SItry%Bb7hP|%5}>GyR{7|HBWrpZocDw_jn@`t9HL=h_(r=*bU1(SG2s{+;%U zuXs!Q{%`-rc3Q;;7Nf2e8>9OUzPJDHKM`m8m;b|WZNK)O_c5n+CH?R5>azi_dupql z_S}^h+JE!6eklF^;1B#)?Q6g0tq^qYMTvQ&(wO5^<4-Y(C1(VrV_Yb;efoxCea6<*;p!na-Nr<^DY z3XwdiYeu}hjMx;W{LV;_(Kw4)PB45kBD~6c00Z?yBjD(aj1JBD3ODCYIw*2@LPQB} z*D>yEyj)=5dUFE^7wTv@Y5_&1u#Emkw(mczn}#I)Wk(_5FT*LFBIn9Ibd?r)4wY1_ zzxe}>e)w%+J{1swTONt^8dnJ-I+}FBN9j-Tl>#(Hr!vUjQDjc|Z7`s5vk&Pked#vm zopDWFap4A-05f{1UaWbc62~UfMIR&OE9VprZD-!z#C>j2ANW&GsOym4g5}zb(1?lc}Fept%JRsp$mG{tF#$yLmj7gn-K9M$@1=ZzqkGAulp{}?;ii=*S)p`8V4yef77sANk?G-M->0-bimy19{Xm0afVYg1V1? z`swyHZ+I1hr7P{RKl>lu?d<=jUqAP=|7&~8n?Jw3{p~+R_Omvfa1K^|(2kY?fTVCP z*ZFn;1^ zil}M3og-5F*?0V6`>EgkXg=TY);G1+Klb9_qVVlqJlEdyTOVkD`@jF6+JExB-`c+A zo4>yO{eSRd?d<6-oEYgdM#D9cztP_H?k9-UKHk3bJHDy?JAd)(NwJ)UT#NSNhaYS| z_M<=0-uqi0Y%hB7{*1;FA+Mw80D8}#5>|Bq#5g*oi811u{b^WDIgD_eV?HBNgXPRf zeJGGb&^;c`&t}^?E!xp!>T7iXA@W)&Oc6Wr-d%^^Ib^(t={!MkPSL1c14tL}Sy}LY zrQJHYh|{s0j-Nh^<~0NzUgBXO;xT7~51(-adWX;yd@{SVA7M1LDvM65mEgW&!v|$|sDl-SRZu`ml z08gG8CHF4m9?K z>AuuGx@W(lO517KKKRsJs(Mk**5D|M_|~+m@>E$LURL+aUKH)6g>6e~aE6byF2I$x zhd5I+It)OhA^K@xWziam7CiQ0?KOba6W1*3;-dqyjoE_8~$|rhOht1_VyqBN9||+@jFxh-}|or zqW$W--`*a2(fvGIim$AApEoD-OqExo$Q8)85m;3`BcRJ&Kgmhh^(uhUl1V_-@Kmms2+Z1Rw z64j6?e8MlyHFTVmW1IFTA+QavYe?^;3WbyQv#v x%$&lO?b>KS%usOHfTK{9l7FhA@Mu` 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

$i_tg+=|65pcSef!a|p}4QfKs zYHR7{Gn>zx)Y{NP;3G@UNcy6Zl6g{YxdO(zgI*Cv9%r3-7E=xFY0dga(#CC%rvdVV z`^>Us+2f)x(pd0tjK=|v94qDx?(OX*()g9>)}P-Kd%D8mQg{1&l$A15AP>jm&tZPq zw1u0&Ralx_m8Tj6Rc!R2lB)O2x_o9Bm#m)=mh@-_2sbk_g<=Gs4XWi?1+HEz53h|f zj!b4EycD4JMWH&PP=zPBPqz3UWgc_{g+t}LG=>!m+$o$$be7HF9f(*O%~jh*yh_`*F=x7i zjc*A=z=ED6A{cxVWQ8W?4}{1B8CJ}!qu^q?96=;QbD;k(=H3HP)2pic-KU@FGs$F< zOhS@LfPeu)Xns;dQ6nh52lxP`2p13wpy>Soy&@nT6+r}~*$V<9NVy`2Kp+BAQhZkYabKduT%C2j#z1G@muf2A`!76lS*4xvc`P`3d$*w(U zaa?LxCA?LJXMBVG5ZF?NzrA1T?drGX^pI=XZrq-}r|Wt3cll*NjKWNpQDHB!v-&VZ z@fy6k>^*c{`{7%i-Zn8Sz2r(xp1I`mwufy2Td_X8#RlXu;$ov`bqwhndp1O`fs z7}{1UpMrgQgJ%d*;UCsV!N=c~+GfzvbyhgAcT>7HR)JAuN|(atZjPkwf95X`QLN*7@ykUlyrdHJoa+v(G-ey%om` zU;gr!VPdEAY)3r(yzhN~+s--Xe%THo*qt4H;~U>dxz}8CP5b00Ke0lvXhhmCGm*I) zoEg|6Ki0)i_=|?%z(Hn&@5k`yfoNzrY@6BLK4y9n4V{>bjqBunUE8_yB=$L8$xLj2 z(tDV;4&-H~n>cFgI`CL4SjVRfKv?cN<&?zr007H&JIW=%<9_$M531YGMMEOo9iQT; z`@XR0EQ28; z5X%>5opo07ADvjtF>?y0(&#zLwZEm&S%yMDc0a5%*rhAigSE8Ix-hC-51zL#RmD3I zP}o#(7SBAI%|3I?yB3|XPh6dn*jDLY48>Td(bhww#aX?QpxU=xBNIRmtwg-95q|`{ zCOJa6P(@&-ywEVlw&UA1Rz032KwGkgN(lpC9eiXOh!rwb%>SBi8(4@gUU*%{*ZE)|C2;)&*N@}X2wDZli_cK6KzaaE!Mw z=73e;QVq_UO=bw2*b>8uK`5lJ&rcggF=c0=~*WUNO4>Gg+eXMSe0)A}92RCKpx4Ye~+q>TN zc2u>$cwyCxn17dSXG+(;i9#4&^Wiw)oT&SfyA`{(+5eVumNDIxg2uaV}O zqFj`6^MJlXsO|3x3+({|-*rLVrgMsqDtX(~<0YYS$WI)QQK-guW;U(SSH_Nql^=+O z^FMt2pW|ssPespp&d;Y5zkd3sf4bfO{^u}5{ZWJ}j{O23_b{%HI%+%W;76~-yWzzx zZ+S9818ax~N5XH@CInpS;h2R-VoX-2&&W(qVm4^Fjx|8xE7Q>_L?#f7Tk+Q~Eoc`I zYCO$HA?j;i|4Pz%G@DUmpsGj>-do;Ays<5MD!Z9*@7T$EaKCUs_46=B!<@3V-;+9~SlQ#TQ-7cAdAjUwGcLFg80W+XgkF)32`E#}o$aH5rw&(zx_< z);G-_>!@tez7fN*=5kEd@|l>Ziq+wY^^}o1<~=OG~TFMDTUO zBAEYc0(DG7Y~*U7LFqVEwKR|rf@}kw0m3+3hm<&)&PUGD1e!!hppFyQTGIj=S?gTp z(}|`CvV=6U2*5&!QNY5Q=$t}C<5{+O%0!9@6|+rR)ANxLsD|kSQVEe#=o1NX)x8X$ zYAsRXvYsd#Fc6r;Y8MiP2c(^GPB2afk6wFCTfhU-!g>hZ7S>o$d$?6Yl^ZOl1Ohv4 zj!bX>%~@usEu?02HV768iiY?4T1V9!!bDg*P}9f4i05I%zAy)2nt?_AB{JOmw{l(4 z68FZwzKf|E31qhp1$XKxe1?2PcHdP{?nBCB9e4z(-t9q?td*|Ui^(|a6S)6DU~QYH zM%!jo;3saKXr~>E72%F8xD3*>a{Um+5%qGT4IaQi}~(nyOpy? zEsDSR#jj%Y^e3ole+0&iijF)D=}!u&d)1BWQ-(MjJRpva?+w#fSsUCds*vcq;xeuX zoe5Z{Df+>#I|O4f)S5hymADyzh43&piBRAx40yH&<0QjoTm;$|y?$q$aT~V5yn(!H zCT0E_7Fm7kSj!YO>j(MCGuhDhmdS_vdgS*|kBX9VUH5c0p?cJr;ITW7%Q~bQnvoIL zvx}C93D(yFWE!r^Uv%+T+8xgN5mc4VLKMo~Rx^rogfpWKJ@?#mqxQb&qKjB7{g=V; z=q>!d9X0N`WaR{4EGo^&Tk+3*n8Vu?|G+o3KsH? zk9Z9d*aY!Z8aPS&7Tf>}9PLse+C$Zq_<=h;L0VxZoju6OH9Ju2pZl06wqJhU|7zd* zg-aL%C!v-3a+13iA9e2)-wOU%Y~cp4sk5|3?xa6%4gG|_A8F`*wkT<&q&Cg6v@?+I zPk=AZr0v_y9RH@UVV*8q7~d0}+|spqGxSotl+H&u1}~YlZ=}3g;!m0iq;Utt@5t6vE7YvOj68o@{{~5~J#HEPa6@GETsW|T{_2$oCzQjFyn zGcSfq&$E|Jym5WRc3Pd(X<){t9j6Z++(p2B@4$qRum+{#1l_xU;XL@FR38%9O{r^gk?sK2}VVLtf zS$8~|Wr4T7?T^A-Pde!o;<&z4oA{5zILTmNo_ONT!tg}>G>AsXblq-JGcMPBguU%Z zvWmv#28fdC$**C3tHIbhj7!W@JQY{0pN#Q>3oby#cNfll-I@#vW{p#KuFkC2c|B)7SlkQ4hl~U;C)PI^x%T zsS<1jWM|etgaOST{K4CzCO_c>i~xay_z+=~alqiBom15J%691`yW2w_vI{@JFkXp1 z5(+;@;jv7Az_-8snOOY(B$|W=U}W^n_E#8eJ>UWNXAO5l+qLUdRQG?B>*J5V z8F;~R&j9$S4Y{`eg|Gdz2xIKyOPmZ7pfiP&Gu<=KynVavZEw;3`mg`8J@~;7VK44o z5TG>RDb>0#(=*s%PM6uX4F-<-_ah(qNW0=22iw!0^yA>ii7Z9z14iau2vF;ztQ#^T zhnhfZ*390CDU5oY!UsXjxB*@|w$Sb1_H8G&yWRC}zCd{V5<ucftE@*7PyQ6|N<$XT;4==LHwVF= zVXE&(smgS2koM!@wDrLWhEl&3^U&a7j{44{x)cmbwWc#}2@|Vu?1Nh*>@c|D zwfV38$h3`P-XdTL9cm0@<{I-b0Qjhmip41^Let<=s2zgvUAug!U5LYk_kZ@H_D7%k zYWtI~e50Lz-Qo7}Z|rOD{^(b5BJk1n;rD;MeVIL<3&2IIX)PYJ=7N+RXWkIxF|`Z~ zvI)waK&q@$BG~K7pjKrVxIipO9DIlh4HZ$+GZ1t96l)~r;VjVeFfBtNK?0*irtSnk z_OXw!wd2h&g$9v(5Hk(ddKfD^ILuzdnF}tskafcIEC!~r9~JF2`E^$Im9Km`4CTcy zehKe!YJs{R{+xb3>#Q@qVD|Zi_PNh}ChMu^ob#iZsbG4McoL{v-@1%PKFxl=gSc&+ zz=HI^!QEW%hWM*`KGj)zHb%p7gctRT7jCGlXLjv3Ou?sad}nIFT*>1z-w zjbe_GcyT2S4~h`rrWh-;934A2xlvp6wnxcAT8`-S@xmeTjFQGtPh+VoU^0 zx*qkd?S2V;RpV>{_u5ymW_%6IdNKxPo?_#AwRA&x>g>Ysna_NdZ`?4#{B&-icO)5r zv)kLh#l3$B^r>;KBuL!}$Z8L|lsI<3x;#jstht zk+5+a_1(fj@F%y;{C@o7A8+q?*E`x6a?AtSCo2=$m$DtBrNyA@g8>x88Mqjr?d5D! zXri>pAWe`CTOWnX1nMIWg>JHrJ%c5N6N){n6Y0xION%Kb`%u*Y3Fz ze5^z1wc=GBF3!3B>7G)V-^?M-7Um*E6lI6sS5{X`gdT^lAZW_;98_B1K zlI%-;WTOop!W!VizZ+sn@%P}BtA+eKrEe-I-=roKBFg(u{6rNnw{Dy-GjC)tG>jUB;1q#%TB?R%)Y2-ZpdmXgVKiIi^(^1|16wx_KAjSvQ?zuVoNJ z35eQ}RodxhZk^g2q6!&cZ!BTEKt+~xj&GWwjVt|Qp#ULu29|-V`GAl_Py1|#aOyA* z9BHdKAw2tY%kQBEy6LK)`+a|}dVT>xpauYWDa;TmzWu+7Klc6-ZJMx+)OTu%&3)05Ke`urlU^^EePMfn`;uWuWReRUF-<5T+U8kPLJDKKK zyX0FJ$8WI}))>e77GuT*9mYsk;gI2NZ+lz&x#vEYwmWNHhIw3v1BQ3ghQDqveBtwf z!7XUx{;V@U{NayifBeUP&f4#LV7wj&b~0&-d7O9NXW0k)8W`~L?d-G91{bZPD);Kc zQomV~9~{It1)NQrwqXQyKj3vMoKw6z@O|h*AIfPp7zHwRRF!}8o8M@E1_S@oKY0&( zNq@NAm-y;GXYg7kd>;JZ2e+5K?6+%+43^f%?9eK?`twU)`eJ+Ud*7RO%7lSz_Uc+g z`&nkFRrm6;?)paCWtUyb?DehuUWZlpld0b(m^0gAKsv~F8fH8NFVA~D$V6iM_MLbn zdOVIK{t>4OZw?K3(vzN)JXx5!bR_ z3C+-oK0FC>q54)WgTYCW4k0$tA2L|&Fyspkc+&_68ML%4nmOKUg*`t{dg7rhWQ^jS z4ghCBn7=x;5r642V_|Myw%I01Tm6WSS<;}g%dVY5hiX|*y5W21855eoOA9a&R4YY| zcC3?x+VerfF-&6T(n!_9*}Mv%i`ekeIUQ(&A9FQnpW z)s0_74f;-E)8SHrNS|Fh$VA4DV8X{Tgjt9sSdb?rB zz#ewc(-(VoUyg^Lr!&L26-?oe*k}4sV2a#26@)Yr+yyrF0L>zT4gI#%VVHtDS%TSK6=r+Al|?tE6u? z81Deb+Z}b(@$Hv>>3Pib{*77R+k5}^0|*hn3uCxr zW*n+&Wp>~E#uwWo9`!Tvmh%XVQXcczbIEy`*~-5#dw)BO`s^?bX~1sarb|Y*m%QX9 zFTw-Ui`uuo^(~m)eWMz^?6S+-%U=F#c)e-uA3pp+nDFV$`mTYYZ3Dj5zhzvN>i@v6 z-pBj)T^FY2y7ZHtd@gI$&t+@U-_YiJu-#@uw*GwVqaQ(=@I_RXw`ebY>C4(Fr|g2s zT>&gP+2)uN>Ay#Yq5jNMpW2@Kl&4^Ae2c*J;)^eCuYA=jQe0-4amGU5sQP^G zd;c;{DlWV1G4#PxqbYE!$Ok|80gTN49>L+2?LPOp4@95!UhsC3J(toG)sy1mVf=WD zZ|gayQ>K6N$)~m7{oUV3K>GFe`@jDO`8@vdk7Zr=^vtF`_w~HbpGSZF9iFa^N2BxS z^y7ok7NSK&E42}y(QY+SvuFt2qYvc+8%OmoT#UAsy=n}}CpsKWBknkJQ&{#;^7%z3 z4oI|jx$|9ojR5%d_K-(Bpq=qUXGF6(0$pD#@{0|;b!&KJQ8)4E~ zJbRLiq#w|yjnXXXfsD2b5c0+a_=V5eV?!OOE1rgyIVaU|XuVSdt(D}u{_&<>^6Pca zjJOFQF2i*J*W4@E5dR#U#`Cx^t!uqj^=MvJ)Ue8gYmLvrA;I^(@qKUpUJF=1GC1?_ zdH?ua>7n2ITk(~iAbg}35Rqz!HmY`9rgLSrSuY{Ij8K&ZeaEv^fuYyMJPlR8j1`pw zNVllk6m{i-7Dud&jvnIOJaxtVdIGiW1cY_u5bHNE&GGFR8{z3>3Bt69T4Nrgn#H5A ze8fmY;{kQU%P@H0O+Nw&o`aog8Pr=r9QcG}fu zu8;(mr>Gz5rfj`|@QbvoH~PhRj&3ZP(jjdI)`^dg< z*un2|h>Y>&FaJ$ue^;?i_rMTu9Trdhq@I8P04m=vYD(`>v zN6&`%zCJT6*D9R>StrYJ#`LUbJspBr>zN1H0zwEG1wcs}dCug3wz&nDeH?*7Bfl3tb>o$JCf zMrXqZ4qOL=+(G%zNjhhY&v?eOl4jSg?J%q7wns37y(0q2A#4xknPGCwQnn;L^YHpf9B?Ue_2P@FAp9VZ}++PecJo~_CxLO@KyYYkAEC*RllBu&Z+PApa-;bAN|<& zfS>$H4(C3-9bo_MB8*7FeE^1h=55bx|MZU^Ywvjb+uLtE|5rE*r~+Ow+qqAFT6@A1 zp9B-VEeuEF7#ZOh>#0A9LD?sAaKiJR_sdBjsz3hmPk^DmH?w9nBU;P4S2eUth6)}Z z1m9=bBdJ3OeG@wybQw)X{@T~RF4_q94*xS=s^0OAcPHr1Kwqg`juU5vz z7toHGko%GET=M_2jnUa9(|kC!gib--j)`wuSGxW>;quJbz3Tpj0=OMD42B_ms&yB{+!swG_7_@6V6dOcwwr3wn@U4r% zpTLU&=3ud3sxL(btnZK_Wun5$=eEZ@H?W_}P06m4i0BSn-`iI6w*3koU1giXQeJ29 z{fT&Q%~yX-@7lK~fJg`|yTj9XdB2vSlAdMgz^9}>a1g?=ge1HF>tAgXNaq{4UjXh4 z>|I<04@a4?OtXGB$sk(*9tfALX9uT)zYa{9ntxuZHm`CEcO?w|TT|OFTqFqMtLlDV zR=XGn&WO^cYGY^Ca}d7;&JNwd!Q9#*IBS$(TTX{b3MVhu=v;$zpP^3D#UtVEnx1Mt zXQP*2eii-9PRmV}Z;@(BrbxSW7`V1cW@w5~B+x@7H5plpVw4Hs1 zGH9L5t15Oc>3aOq`rhuNDEf+{*$(dE9>)my^VKtr21pu7DXBX)t*>fN>u_K{jD|dq zWzF&YfBhnBg?DD=_M7IH0LLKfdX%zW zIt0XVgwh`)n57=uWVob>iIDK zUG1Bfe+gJ;-H-7G{f5rQr#Lh{R=a586coe}KUOL#4UvrgtyJV`aIv_yfqoIPA~%~kkfzJfBbUqK6b-1g(z?M}Z{(hDf_5!bb$aORpN zHh?7Tw6b1YW`8+5Nyf%G#Dq%fUwZ_9>~IPo0j=+dLN$3f5D`nK;?YU-s|upxRe=_2 zCs<37y5|wEG$!Stl`>wUCB1p5qpn$Iy{NRklPy%{?tT9??_RcPEw|tIa!qF%%k!5n zA{nphlKVV%z*VR+O&O9)^wm7fx87HtLy|nRjHnps#APjT*+!sKg)v;_O)|@L`$uDa zY99Zci}MF?YsDeYI&OgQGzi8b2tSWS>I`e92XJ&hg`_kc^`XdVEod(@CRLQ))yxVF zHAtlNrHVr)Cqna@V4MdF#1}$<>%Lw(V+a4ZPWw}4qU5UtMIg+3s>Y|<*WuG)K;&wsu;t1+ zr`}4Vz|}2SQ3oms6L;l(r7}gX4nnMCLi!jz>7)~hXEkaM-EIy=&_j>wrZY1Njn>(+ zY;(c8tQ@tHH3NFwwv+1@Jbcoo;|8R6})Vn?Hgdkl?2Qgv+C~Sj@zF6W#q1z zDl}wyK`NfO&KTz?>vH06p)9;XZRJo1^0b{A@9f#FqjP5$1;-gZh|PN}SVl^4AcvNB zU?k`7^+mqm$_PTH-gypVprRx9Z3w$#lv}ex$Ie3Ae;wA!GC1lXLZyX%(ddJ7*S1;G zu0zdv;&I0W53DnmF1%weGTWy%aHQe{{aKn0Et&U{-$5AbM!X8SADH;o83fdKukGz^ ztlAHFRx#H=xZYNJ@&%^$fr8-9ohS5{E590W3V-wVV?US59J`byEMW1MZRUieSobobGUHaFomGQM8x${Dmuj3lt7R9k@71Q&MBj_;b5cveQ@hyk{;e7!u&v{~dEXUv8*3^Oeg=jDU=9IW#j*~EeaeNADoU{Z| zjzPR8AdDj**OJUsuaFpKEFpc*;+uDFD<=5tYja!7xC53mFBiv=a9|o*CVN((Mm;(W zunx{vdp-Q^f!dQsm)eq8%7t#I$*2w3tkun~4c z1oQ4?n{VY;Dj3%_Eu(*ow_5iB{zIPTILR8hzBR?i2?pE*19{2y9t<(YAW~W5XJ)eK zOp-MbaYqRg5Tp&R!D_`FX*BgJ9LfO#z>KP_22o(R^Vk0L;~=q(18S_s?{SBBCJtCF zhyIg+6yB`%&1z!dS*00I#@6qJUzJy|cMs^ngIb&8HV|)6z3;U6mcUr1@rzN`I502& zDmbuF+d;f#;D)MKl~Xt*p{PEVfodsi5bnm+D~~uJEzpSL0OzbKnOhg@H;FJ)@oj6b zgX!})>8zi#-Rn`$r>@p}fN`fW)i&4I#S<;c42R^ZH3A+R~9VSrc9HDxO_ zfQxR4V-eq)v=LnJ;c33jKjaZ@i6sk@RcoxHS*zyWg8;#EGLeC-XQtcfSI>XtcW9&c zy@yxc4HvfEyxbyEFs&DsmwK9(n+_v?70k75IfM$`*>@k8+LlanQ1~>O4z-a7m}MVi z)+vpVVT~@~<9rO81*}=OFuRvg9^$b1oyXh)!Pi6KWqPyi!yo!^Dsj$FoJ0GU+NGCY z#EuY7bzyv3uV~+ht6->x!0l7|R`)zw$)RwvpJj}lSBR6s+S#Er#Tl;klIBER3}f`U zFi@*8#dbPvg2X>CDP={AiPmp3#&aWl+^RGRjdh0XS;M0o$RTXh+$A0GSG($Qk;2F` zv+WYMYA(5)sWECA;>>|&J3HjOuaPh%XfdwF%ACZr+t&Y4d+OXzu~rfZPKY7aF}2j$!!P^l~K+0%rukDd>2$w}9nVeQLSAbmtpmQ_Vw{i6Itc0`Alv86i z;dO)}7j|F9Ix55h10*(UwGr0>0RA+Db86cB+5d%96vG+TWtT-f=4&FO4T(flHMfU` zHrXeVPd-X>)J^zAfH5W4{aNn^+fZ$F7C}l7!{4mC()K(Pqb*BB5RM{D4?Z&t&wQu! zmtWu{tSlq-U@Mad@3UTJB^~lJtDd**kf~D#kGEq-AZlC)J9`ls59A@j!u*vS8Hld- zxo0fv-!R!ZgaWv625Ph}wpyj0H`n>S1!#%H8zI<<+J>Sy3p%PhoD(y7VYMbiXmKmyl2H51ukzdAJ zn|AG6$I;Y3eX5Oo_+xGU)h|V6wLNJ>qFJBHKP})HIr*OdeEBP2hLARR%lJco+r}`Y z+W3-}w)J%;~_d1+Urab^~op)`Yt2cEl;r-p3`9@ZX>vaTgFsz!IR*(ZE9nO+WN1`Khl-o+@3Xu4MJ z8Z2e*|4)PDHn#;OU9YnmBS`Uu^s0bFO8ZO3VtvGy zmE!DV0xjULt~(?2dEsv1CoM}1;cje0wNLz$p{`}rg>Jo^v~eZ>N}B=Bi_{5xE~Z+r zmuZQco+9|BAiMar*PtX0o8ncXaD)Bc&h$u!r~Sh*And~5zrZCd9A`I z3RP`rIX=c0`HZtHvk5x^_k_OTcVENSnTKW#_v-7v(YCPWzK*31^VNHij_WO1VStfNpKVNrv!RfW~UeV;#bfLpP)`Rd<&WWPake zz6qSYyYEzI5pfQ|%vf6&hG5%*eSr)-?P07eF=om}r6vQCc;Og&<`R+uK7UJ0LU{-i zbqvT%%@24PJz?leVcZeL=OU57?o8DI;MsEm!oj9RlmHjd8_~b}tba@w%P0{}#N~c= zmcqGKk6%n#qEx|rIumx_Te@+5p6;e{B7uM^B&)iSZSppkLXx_t);t)9DIFnD79iGH zkapdT_&GZfC{=#dmY#RYC3EZD%cyG%K&XS8fOSH>fNp#r!+O}Ee(V<_rysmB zNRTs#@(v>&I#kuD>HPHi+aLC6Kh)cHixdJNLQ!BNeC+4y53}rQa-XljocCeu_K85T zEaMJka+&KI=tzLHb@g2;YLtWyOEXx4dCa+fL8UgJ@;!Xj?snNXFKP$b488;P%@*7c ze&@P_?aHh7WG!U_o|H}=UytJqjEI=|7g4sKoaxr=O37C|Q$?erajTm~SLu{Q-Fa5^ z5VoqVjZ}lldIv@%tXZh=GJ$y2<4FuxLzUsZ@4q`FPaa#QuokX{6ve;<1sFfBt2uKk zdjlEw>g>|GP%r=eO+YEk!Ph$`4YlAb+B5K{!T)$r^fyZ%L zf^D_u`=$6#zq2nP#3Rgh#BY#;-|sHh?zPRqajyLpo*m}B9>$@uH9pLf+hIiXC^!B~ zLgD16mM&nJjC*mS%BUVtz*Hy_KA!kfePWh{U#SyDW(LQcALDuiU{t8SsHk3jS4jI- zA{bG|DGe_gYkLZ(z4tSW0iH{nReSE=w~wRacCufxA+5N#W$QNZX&=_Nd%+{rwirx0o1C3MMNE9#<9e%Wwr-iRull&rn9tC8jAhC< zq-nOK^DI@p&WKGXov5Woja?$V%EN@msG}|v)oSXkL*l11XlHy01FW=Y z3nQWaN`E4QCg@?HsbdZF2qg@%>Ms}aHCcC7O&!b=>fvAIQQ8KXkkVFQ9QcYO18xk@ zeP`&uUK7>t$}{Q6ncv=@yx@ILXWY7WEyMk$`lrA_6vwx5OxyYY#&#L%v17U&q+RD< ze07^;NA^a1lU{c@7N!vQ5!6q|-RyYIlO-K(R7!QAS_heF7@)-vd|dVOjye**_96@0 z(!xZ2&b36b$2SiLcKY2;Ba>Pav_E`)gM7_?2&VCUwNv1fKq#II69;_}s(NmnrqN~` zIKc6I2%j=6(?mOyEAy*y*7KHW9^gb6XSL?L8Adk=L#?q^f^3g#Udu2{gK<~kQ3R;p z8^6NvOErYeB#il}ZMZRnsb0T#j`b+UCq^|{M^;E5@+ynhWqXaD!&qHI= zMxj$R_E$^iE%d+Y-oscxAHs6lwR>ri^|S05mOz|cX2uHcyOt`Sk)gWwD*m|EX*-I> z)ub+TkHIm$z0g5yDk

ccO#tQ={9a;J&c_drjnK!S7k67TqE@C4i6!AP- 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 ^a)U&C-4Z*UDeLNrk6|X~R_V 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

YjE#~ZZSd!&nq+gw$Z6EpYoOQ7} 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Ⓢ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

_KUH?4^3Ack0(4;^6#q7ek>=nzK) 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⋙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%0Ao6|Hv7Y;qd{g1`>@BFR zeqM5aX<+z;PO#}=mNP#1R1jJG5-W~ zR{A9m-}$RaGYHd4UTU}|PS6O6l`u>5c*ebim4ea6FeAfA8#OZ4Gp_46w2e8H-52H} zh#MPB>06eR%$eIhe)%FqPk}6+aWZk%lDdKX3RCF>!qX#cVXt?DX$^#fAR8MNju`Do z#>aw4m@iC*2t_?K&Hg%`v(I0k$$=*Zu!hmjXzNt7Geq4%0R%D->9xRChT6l_N@Iy48eYtG##l{ge*zfr-1!mBz7D!h>b3V2wu}?13Ek*Wrm;`raj5x_Ia%k07HaNB z_STVzdiANl8zlGH_i2u1v^0k!Z+f$|HL)^aF7;)A%iT8^*JYu zqUB08K)exNUYR0)tTpCWzLC9U)wBlX?8*IjDA&)d`|rOKGB z1E>zYZkc?MO4Khv?d?tF0<9w!nfzv};B+YsxE^%0|vMA}3 zq1Nk|pzEPm)-BdM=^yE0PUp99pZ<{1YsG2d*|j-NdWIp49yP5I?iZ=;BhT6;k5&jLqZ|Y>!?O|+F=2KH{>PHvG#@J zZE}867WwAa0&Q*pMEhfTzsxnT9dCMH<@FlypynW5WZ199-2di5!VHjDD2uADs zn6G!AH%l1v8N0chIv2{PS70_S#Tvj&v)M5Hp*AccyLq`TaEEw5zhORW0At*APD@58 zQKnxH(e78&aSP2@FW(7a5665vR)~Ocx@*xZ=4-^M>o=~`ena%hSam`pwlMd?v6!#g z9_6BiyL{!03X+Uai_{(AWeXTj$Ww+sW^Lj^Cluh>uvBLB0tX~FSD@|j8fTXznbZOg z=#}%r&sgsB79oK5$83C_HInwL%n@elDKn+#`3H=_ZOoz8r8`E1G|zZP zxJ(&s9Lo&Cz&KoP+$0h0JtdUe34|#1_d}0%K+|`j5qy1d)R>duTUP4Xz6_4~we@O$ z{WA5^Cxh#ybjSKqFQ-M}eH#8uN>-*7$^2PH!2?I~cWofDK`#594! z2Kg#59@4LlP5wdw`K9spWyTw@&_U5ASNjwX1A!uQzG#o?k4nqur0?Fezb56GxNh1% zcKH7C@ow7su0Ah%W1a>1i?7T>yb~84GZHy{9PGCt9=Zzn3>i*>mjH|>du%a?hyEec zE?=(gFkaPcnMT0OF-71WBNQ!Qx-;FpQQ>62FA1i~mKBg=!JyD#X^+RS%Atp%eoTZH-T`<=a*96NWH=`ecZC-&iI-*bs z%_JOz0w=8%XHZf}2YmY?<(GQRmwsd4@V>N8KNc7=TYjxLKuqmZcYp24M^Y;7MyX$UBGx(0%`js%%War3Bc8cc+{H1Q&JnTu>?Si{ zFVP-LWt6fPXxxMGH3G`x@XC782~lSTY*Dsg&@3T9ZUF9j%mHgi2N15_(_MlbFeotZ zOj!fC!sD{<@|sgLy4yEl);PDldYORu^Yw9a1c5xo#5zTDU_Yp|U*|0Q>KGz6W3&Q}HxA;Nm@YQ`H%j0xnwVDHuC4TMS~E3rvTe+mO<359EGM?4#{25<13 zwW@?R99U0?H7FO{Q^T2a8hX(_(n?`JiId_}jVKX8+=qDve}q3o4Wg3JX)t?t{1~vH zz6hRaVbmErltWq?9k>NgxOc!8e*3EM-QS>HcW?Q85N8WN)QTcgh2RXZHifw8H{sko zec_xX6V6yT^!V+Fk#+a5HB}GmQrG@)og}oK+q$8K|EoaD(G9 zDm@QaZP2rhBNpAWK6r%!&_87Z04*Wp<yq$RHz-n^IvV03IE&W7#}e)kQLkWj*yzIlM7-ZiW> z!)xo!@4o(R^TFm$^NY{Fgq}bz6b2D;CX59woN-JdTgRx(O+mzI4uikgE-e7j9(kAD zs7Tw*r*fJ0NGCd2`b>B$#K^QgN|>fB$4PoZNHQ++)-r`pwbL?%Rka@+;lT#`E|0Zr z$ShS>L|}mCm^H`Bh#Yz0IL$Vi;Kgu@-5EwONq5z-W3$#O@*aiBjnzP75HUYZLm-gM zlQ-e^%&XR+_G)?#{?u>R@fOVKAfn>_OB!2JDariOHP39E%`b^u3EW209Fc_UAgm8* zo#vMyCAo;0(e zCb6jjrTJIwRMV@{R_!(ejQHMv9*Ik8hrm>QE+Tl^)uu=Q0DdJvB6#NYd`DUb&djxU zwx4Rr7!4iS4PxgP!tB_+et;A?gvn3U#+dVsqZY*IRD!ZWfPBoVJs1MR2IhrC3h`)$ z><`L_I>Kp2&20pM0o%zFCQf}8no40h&an(J1CL>jE6iwrP0w_Cp_n0BZjZUq8R_}NQtYPdSOdW$eGSv}YzZwONfu|s21;a*o@2C)$%PZ7g zwRb5{;>#h9t6AA^hOrCr;GWKnM|1*2P~Rffj3HnZZHVv1gN!M@6-~V-NAi*oNCjFV z&wCzQSkfmIFpFalH;$2vcc*ilLnY~eu#aIN1e#~<2COqXOCr3YK`vNV_)cm<`L@+f>R<&fkP9t(4E`2n;GNcw8fbv%mjR^4FjJ%qMrcgRRv zTF7_yU()kBUv!@(dEJmCM{=^B=EpN(b@>Pl;(_VA>%xi4srWk8$sevOzqH?PO&Z0; z{@~7jNPfP6HozRUnjIcEk3DQ|UcG_U>;)PfmH-^Ne)Z^O)4SDg{?=tiSibsNyvSe; zLB%qn&C|icu8&~bW7^R9Ovkc^f{o2np$IJueo&Xx(=pT4+p8J_!6CoJ!Oroo-^5VM zVWw92Cf^EkOQ`&-rR3Y@XKvC_r#Fqprr%DKKgMEWx6@~GK>ONJ9dBZk2(HN2dcllG zc<`CAgGBVM!$DF5K|_iOJ*u5FK$GwJ!NBn|Upff5?leLxXx$|g)e;fMN_XYhQGAc~ z*&$WlTNl15q?34n?u<)B5k9;}7oomNGR!$swwo2>bjZ8`u}`xMhB%{` zjcixh_USBEBjc^EBGT@+>z|OfA;7GXx7<_Z%3Qj|q9S5j}h7P3rj|i@C z%2J*yyqmIAMZv&bU+h;+F$s59W7_`#rvq^WQQ<3zBqqmF=^a5d2W zI?tgAEU!3ee)-^4^YyR4YhK~u>m2V_Uwqbl_St;`Vjz^j^|J3G1pWBxO|;U@W(%jS z3^WMs5-R{5n^7Z+(fn)$b;&QBmdQ!%X*8$*s{Wd*X9?V)DZbY8%4kVOK90{YT?RI! z0fO!l8<-zu30rYh#vlW8Zd&U_f{VoYfRe~a{PUll^hrlvb8yrR+b4uuR7eIJVeHiT z70zpz@Os+>b`h5_^PiH(9C)l0`XIL1+RXegktw)nDzmPRgSpFzI2E`w$r*!Mt-Qp-#+^77ayH zFSut|NExzl(o(PlPWFXk;{->$>E;K`9V{)6{$<`A5-n-Sc6yuKtDVHDi?$gPy3ghj zuYU8}<_zKQ2F!R}B28Jzm!($GqHO~3i}y8k&>Ck^Hpg~LjJV~nono8ssxQ2j=9%{W zqZmj%6+ham?t+$y5%t3Sx}#{QchZ=5pLc04|8_JtG*G3geOb^$*k{~m0@f4OX(#<= zw90J}&;bv^<~kbcY6okSmMidZF5HO)JUH9sHG+>8JmO7H2h3sHk9;2ulzd(4W&Cdi zOxNrSE;q@z3=joL$4zOaBw#!j{DAK{(*;+cqeob`42QVQHjN|wTEa7ce*h?6GA`|s z+}l!4uHU)msj(5FD8*U=erN1}msm(v;I>vk$0DSLu|^wcGb2*`>A-gx#P45M11_GP z_wGB7I;0V0?40T-8$%fsUO)g9X_TcFk^o9RlwtT#K)WoWLj==|A-Ll#(;z|87PC$M zKu3w)YjKAzDu9ulz_Spu?f_=FsYj@kb%gl@=>ay#o^na#5Qt#FwS=BZLu~c1U4!5; zuLprwV{-7t%UA#dXa|gWXigh2@FsXaD`mNm_$|At7zHV745?>!jDgrnaKl{5qieU! zRqet0LU}01`34Z1d|fwco?PPKQ+mNr#zazhQE4#Dv(8|qgJ(qcVZLz49-syjPg^jT z?cxx$z|rW+9p?UENJk*b5l#d4Lv1LmBuY7`2U`3Gh7Uu+9H0fy2qv$_FhoM_QT`GF zw`iRb4BLmzteL;0O-P(0mdTttcZ-=;la9+{Rv`Q-@jpkHRov7Wqcihfr3LFqvcVu- zN2?uRb~~Y+na?_WoHgOEPefy(+Uh%X!sOhlEK_%Gw41UQ}Sl&<^CjXQr{ z(<7pu{ZG4rmGw1^gIWAivX^-#A{fsW;Fq{Nhsm50Imx!8Rt1-XOW>z@h&$vQB6LRh zDtCfYaF8Kt6H>;XaOj);$DPAj!%WdE^j4`H7=rr}QSX<1EjuKSXHT4yH(-uAz z&v~yVtF@05B&>EiCd4x>f7u*_vNgTt8;GFOJ<>_*D=tbNJgV(J=P*0}cI9v z(#oW`;E{vQ`dM@TDar^;PBa}mPDVOKx)#y&>Pk1a^%q_;EP>l?N^Jd<=8)|%CDuAQ zV;)N^m8h(%^9NpwI6*M-=*8rE!Q^9lfN;8|QUYXct)JJ?xM(|LAM+)+V<;`o1)jQk z+4doF!zoK&VE8l4mjmWRS4!X4t}u7H5r%e*>C7pT4cewT$NEzl;6)u_9uxXPg1dnD z9zfVueKcFN+X1Ff&8Q0`eCZ{Rne!xK4O)WhJ$vNkhUyEK5g{!evq*jpQ!>9kLXzS4 zz*XY^stpt5+&Nyf;q`4cV^GVqK1hK*2>a{T>;OZ1jqU5k=f>Qe&@YDJ6ZXPSA!vfx zR?SQYiH?aHyI8MY1-39$@F~ofL89*Bavh_<=+tmb#FyPSyUmC9K5n*Hb9?>HJ(#&I7;8hR6!R|k-Kt)~AvJ8DjN19Yks1eP`?-BZwP~6y$r7Y9gc1pBre1(Kq8kVwY)5u8{pz&8Z|rkJD`Y5)n! zuPKha>I*)?1dqDJMp#$?s4EGRg~ZpsaSf~%F57H4Nat4Jl+6MyW{Y+N_Yl5z5vYu$ zwSh_4{?G(I;C>x^K4J;c?xP=?X9%Cy`=2)(Y)R%)A~$^R!CZxdPMT_S7k$bz%76cO z|JnQZMZezv>i?g6VFa*gPmPaS>R2>08>sLOY~(#ogY0FzzA-}BJYo$#0yj)=0^I)Q zCqHTKUxfxRp3jKg?Zyn+!{p|}C9p@D#$sa(5_flV~NM&flyQY$X|5H?Tl!T)OXP$=OD1Fo%Vk8MZ(1aTa z5dx!j>N?K2%>ytpC=)Y-!Zn~Hq-X8h4geXeQJpgEWA?31OWD*)Mjfs55)cC2^<96uJ(J&T!TSQB_y=<{T03# z002M$Nkl zm|tIM3^P~{hR}bF84}5TfR@=8MLImac0X|>J##TC|P(yj$jYREW* z@qo3OPai#u$MIDNuME*apsCaVA8%1-*!+ccrFWa@t($R-nxLtExOJ`BLE`=Gx8D#K znB8eq0cq!)u$VhIEv*w%SJQb+tH4V~q&1kif{R+crba!Zou@X41z2+-xMUE6OG>m5 zV1=HkI0_E@@csAAH3B%?zITuOAiO+t-LtWUH<@oX=B`nf42m&h*~W_PT4XFDjv`M4 zH6XQco?D2qm7Bx}P6$jjjd~QZl3~%sDz0!tMv$;^BvBW?m3fgP3HYlt%-M%24-5?9 zcM5UTZwPLfjJLo9xLapurp+WhXkMaC5&IcLFe8Qv)SM7=f+Uc|X15~BbBn)NUBMM` z!0r1Ic9Be7ziS=OAeIJ}7!qNI*3oF-jD|UfDY~vzJk^q*nb+q-=H+d3TQOjmR)Y8R zfu{tEw8B#@*bS;3_};||q_8JTrGFjongz0)N8F&23Jo+pL{pQmZT9q?yjzpM4rH3I zcRu-)F*Itv{>T5;C|C}eN-zfX5TVk!ZA6Xc8q9h^nGgQ;pP3ptYCakN6?+G(W!n{M zDhP1&`w@MuBaJjr`dKuAEZW0o{O8D@?(sXnf9Sh+68w=H%eqXn-!I$GERv#J&|zIi!_cd$p#-dIz3}=l01Vw9GZFYNA@C;;;!#^&6EB>)bJaZmC~Q zb@)uC?YLPH(?F*oX{T0>IFD&=p%wX#T?+QDEe?Rx{Gmio3BqBGL%MgZ< zox*(GdTjy|NE}IEWYi;gN7@ZHqihHV^+|Lo8;!^7{LzzUNuc*0jy-+M(-N}z8H562 z;aah?J2%71G zy4AdTeS|bVZhrBLubOQ=$)VoGO8i+PO`U2Swsj+4 zacP0Dp+yymr_G0t8B6Y*K^0Y3glNEc1 zj^@qpfA@#dv`OFjG2QQ#Ns8)w>($k)eb#>Md;hs~$RsKPp)Cwy1rQa=c;WLp-s(tL zSFaIO4OTjZWu9QU86gObSgZaTzlS-($R@(>Cf>eo9x(%NbQY(h!XTBwZGLb9;XPI{ z-%*}4Scd3W-efEqol5=?WrGC8EWvH+=xKZXMtW-N^mB0xx;Ffj@p*EJ5Z-<_W&1^^ zC$w1Do^oR2dj?Ix*c_~xdGhmK|0p~LK$c|E?r7T+hMMP>Uv`Q9g5Pw8jL%Zq%;9%w zm;k84ZL|!l)ktLyatDB6hdKDwT3nNO!Az}$8Ire+Vemye189!4ifyZRA0B5!un`7& zzG_0UF8-?=1cYy^DuqDxM8KKKXJrm6W~xTI22nQ>lG0LK@EtA5r>zr6AX3!)tI`nK z3H6k?7Q=*TQkOHdlSP)$IQJ>>a6S`;OGi7`=in7i8N`$TjbOk-{5fI4q=!hM9cI2v z;RCJ6@N7D!NW2V0uwst3k4IvUIY8~VeYR5S!H`$%?YB5&J{4_#Xy69dZi0w&h@}gh zCAgYEPS{1rT}t$dv`-|0YQN5bxc1Nh^=AY>*ajh|n7)?~QUwViYqX@r5Qec}8O=*Q zj}aJP5G%ZLm28h;rn@g*5WNP6A=)`qKmzDMjJ{(a;S4o^VNP$}Y{oaQVTvZME}G2@ z&tZe#AJDcdJKJG)Qy7_|px(g_oHH1z14`-E0cL(OTl*<}%CE3?zf||Tj38&9K-{Bx z!DwZw3+6~SXxBC7N!l;31oa)HiJ``RC;4qJxClw0nVc-Mx%)Fqm35xNDYcq3!V?Wp@oO0^rP|lFnY!>bd7_D_ z$$*0^tQlZBxC6nn3JSZ775@uz ztdVWIF8+BJbFmB!WHRUgpXOM~P`Q!@h@lD^7GfJ_XuGDXd0OE|pU~&GzWg%nef7&< zv0($22)^0Cgs#`H1Xn+W8!(BN;N$*-N6ia_zpD*+0?81zo0Q|Vca~{eJ?!U; zz(?4`|IgdZ|{_?Y~tcz#V!@8sjRi{+!Ji(>+X+yzD%~beFmt`c@0iIQ14}`GCs$uv@*EMcOHK0_zG@=-?mTZDFr#}ehOWcFPuXEOzYH}bS-Df7RA8@ z-g#WWD7Sz3PXUp4m;26ZPX1>O_5Cj`peyA4cYo@>?Wz=2l>@2iAO|wwL#jo?0x*el zn*l2@1e)5YOcaI+LG)SGyDIH<+@F;^+Eb?L0F|0J?~}4sT#4=?gTd7&9oE&udS~7# z0IkN0R18Bp3AB-!%j9?!=^6sLhPhE9X{xL%RZ``flB6k`ywa`+2-Pu`CsDUeVVJa8 zBoQI>KI=N)`gFu!6R__sUpV-DDubmR$Hd4r-e`{{8)-XCQ4R7KGl=!lOLm0xcSgX5 zIa{HPzJF2XGbB5xH$(;|$-L=`HH7oqI40e@lKEK!CrCu~R@OT)3$h_B>qwJ2>sTM1 zU}UD61%;FGTQ@LAYNm#(G>5cp6V20Y$yW3y5RXUll6KgSE=f6IL-9H7e({jK6TkVU zxrO)e8go<%QcKE}w3+{sWh!gT^KM?bmiWDAFf)T}pYiPsQ?+GWBfx`0d>6Cz8s_E+ zCOiA!E!%8W|FF?F3~(?&`=Q1jYA+5f9eK7eeY-2o3Fg@&@=TN$C<73u40f$yF@KLG zB0Hq{`KKQ=AKbat>{5pVgn&L~10H!hevPmQfbdiiH9KW1x3O z0GN>amZoLx_)6FTH@Vh=e7ZH>3L8kZ7AUUWll5W~}nu{N#=@sD*Qc53*_90&X~OsU{!B&R9fztGvlz4iVKW+xjeRe@^O0J`1c3 z*i|UYsTW2h&@8%hhF2^+ru+` zIzX5XEEN}EBH-N4f^e8<0#YG zb<*>MB-)BFR=@Q$A8BTnW`@VU7O$SMouNv0g7{`=SZQo<%4r(<+ICRDc{$SOf{o8o zgVf{VkLm48U$%Xkbl?rYs!MsE9}%EED{)d@$oETOcN<9#N3XPnYN(eSE(t?P)!NotAW^AYGx-$;7Ujm!>pOYkwcBBWBysXlfLOX z2dP>{4=n5?+i2$-(A{6}*L6POopthAq(5rvui6yZr_NFtnXB5+$l&cDg-k-J-Su!r zADTb@u{nG41PPG(u@l6Afxk2e2PVrUQGJ#$($!63=^7>o2Cc^3XFfqMM%3Pz2)8g1 z8?q|`0PUW*j6laAnFcUWyENRI_@pPO@n28SHddTdPX^W;Nfo1UGbMO}OD_5lr=wl= z8fEY~M?C>ugZM3Gu-s2_k04o7NF8d^@CjxWPDdV2J{rj7pg7+>~_=5(hl z^(Qcbn87U6$V-@X&{JQ&ajZmx=3DcQ2~2o_TeWP21Zji?$s);7@~=r8_eDFKX7 zv$k=F9c3UdZ~YKr)e?kgt2U6aV?7W#!8>78^)L*hJ`zGgVb0GKRY^^mj8Xs5IG_1% z$hFU79RQfeI1HU4kS*{IKb67ry(VGzbu~`~3LC7pCV-4AW(@+#lCl~vLOmLss9|)=IP^C&1< zlKJ@)M~5GOBFQ_i{@nEMrV|GAIsfXg&D~{paZ!vG#4spmgY;Ra#1qD0Eq9{bj8n(h zE4DBEzy1aD_!FE1S;^oW@(`!HeVh;XnTFA^?G@MSFL%kWkLeeIg@7mn;HMDfSoDzA zIbNi#TFqpxHD^n|F^-)IQ_vHfmK(5wr&_&=&O_(Kf1iQ7u0I{%BO=ZlLUZEMM*2fX z#CRK9Cgm;Mf)si$Uyd0ax@PqF(#l?Bz@r@JdH?=* zzyIXCAO~#qnGKbIDbZ(Am|@`BPzTKblc-B!Y>Y%NlhGn#TWDPB+W{V;NE)?BO;u+Q zD9xmLW!f2ukOQOzXv|yyQV-Gv64CjbD_J}6?IeL62Cw&*_cq=3Lm{0L(gZ5Q5pbrD z8G>?zl|;y;7&-<;qoy5dqnWd#-5tOsQnn=oPft{`_gaJr!&Hc%lVw(xVMlhzCCp|L zVcKLzyMEMAgGy8iAl{GpZUg~dz1?q)So_&~_ME*u(MH$NK$)x43vW)afw|F;@Wkug zW*(DyAp=85{P-GppxH541}p<{ZJ=wqG@lxbLLz40xOm+?5syjNgLoN)o>!;>1R@8< z#}F&zOgI~wAq>^_FYz2+F-O*CF4Ya8cVE6Fn8HzZrf@*1)kIJNQD&q$!aRNBqx;R) z&Na4K1M#2l;aG(UN&i0fA04sWWRrbI`!M2o(bCWAP2yDghB9?pa9&gxDrg0U;vTS7 zpvqDpyO=>)^PEV`2oo|KDg!a@5#RNsdH4h^A3}0PU5ruwF;eAE-U%I7jk%kqgh zeukOc_B$>e+j>++qcmOH9>5!dx02H=(@MxtxhG%qrUf-j z^C>BtEb& zc4^D*!zT!mFasK)fMb1SG@EFoU~03+W)ZKydxW4jZLVA=*goSkFiO3w+y6Jm1)=|s z^RtfPS9b(Xxv^3__m!R2rpax+sk*nH+S5)8WLgg7&Pt8s103kfqeJ?fv^TEkivTT0 zpqS#k_YwXN>)TknFr%vs)ExH;r$e@2MCh3i{6WpPGxORTs%Ui zCyw~0qpR`{zfmBq{IYAzkTn$uy%U(ROjUt;gz&7e=-#Jd6dkOz}V*zH6nNk4qoH^37GnR!wP_4J-9tq_N4@0^SK*2ziU|?i^ zSA&(@=@9pH9O5KmNWeKkY-h|HMk3;!>*HjmZVWC}aqXy`CBVlr61yToLYb}AT&<7h zSl2!l^JJh?mzbl|o_cORp3aflR@-mAL?Vw829$zJ^FBiBl=7})l1q&xo<(at`9wC zS&{Sb*VvNnCcBJ0dHxD>@L}_JKmQ4iMtEfdtL%US)m6AzbGZkkG>jObZWn3XE*vn= zuAr0Y0Ma?8ELVoUGW-!h?K8(5D@=Nv1mPjzt&%r*yaLbWnyC=b)SikN(B3q#hL@XS zdwPhMnw*yql`N}qEbuNSQy9QfV!}8QuPH5ud089BtLq3gM*~Ul7y-fEWcbeY!g~5@ zjnN^;pc`tDI{(dhrnW{y7!5Eb+vnJ0%!lcLvwXw}kt407(Xmfrt~A+|;5NVnXH0N| zZumyVnau|zs5CEvi9ob(Z7#nOZ=Z+3^K0IK8hnT5tkE7EpoVI?$Itm# z&uy9K61e$hPE;K=6b5GSNZCru30XHcKE6(Qc*U;{F==APW+R9#EF+|#RD$p`2DN0& zSx_)U6f4-x8~5<&?=^RBuu(zih7N1U+N?juXEEBWQwi%> z1J-f!dv^2;41fkQ2D{n>AEnXYZJeI$d#)Y7PN`%EE6UWGmuI{STD0H9Aq>G-dMg9& zjw3bzaf&SXY+g)dMDc0`x*;>pVaf+GCp`ew{FGX?Z^-~WM`$!UZMf;L1EES0fZTi1z&IK4t0pA_ zbXYiq9I$#+s*%KWjPy7rterqN?y1WzWVA<0$^cSHMHRp1Pb|zbs^enr1SupwR5A?T z&dQ#eAb_R-qr7NVmRLx*Br-BHKT&(uiRX-YhHjDlXoMM+GEFN?Y)^;R!YO9d%zKt6 zYR7zqf=AsKkrY;A9IR%TC8XB-XrUwK__pwt1$uHl<|PTi2Qxi={=C^nqAgfO-5n{j($(`qCf;Y#8m@ROjwuDL~slK|_*h zbbT8qpI~nd0_z+^rd@~3o2pT5=(WuN<5BaLdb`GW$RPjtv(JGG>*`*=Y<`EQ^ch0W zRZPi!q;7>934WLc1X#kZ`OC;bnyuF`k88>nKS)X1(yA;Yu}n$t60pL1NwP5JWzrTy z)ijA1#|Qj(UYWcL|0FODE=DEKJPM=vwIA~*kGlu_GUz!KEa}inPr5LwWF>PUFLsQo zHJ#R4ZGgUZ{V8xB(H`6F$1dYC^xlHA&m6k@U+VbdIze&U^@=0bC=Xq>29Bv27gMCK zXqQ#D)zS%Z*bO(9vHSC!{^Wnkkzv_pzM^P^10HK`6mI6>I#Ru{vm=jn(_NI!3Zmg~ z8RsOv5u(C{NA)2&3UFJg)18)%37s|5Vb8MGq$R=xqO94R0;hL3n_D|yG;ja$yXFY1 zNl&WZQ$VX{CAJ?9*;ft6h?R@e+uTbVSm}t(bB){t3M;P@bRC3ymaK& zYH}e{Q@+y!TxiKmZGz)fO;aVns8|MCcz|QpgGX2I2^c76hL12>?%(ob<)<)pMr9@7`LTYhA}slg88BSCjSXFaU9@2ziXx6)eu zFHdlaI%J6rz9@|ELuljf8%*{H%UhgL4!~!pg(^ZBWq_}KTm6#@OGp`(V;wTCOp%19 z;dy|;<@<-PNhgcu0D_Sey}ktS8u32l;EQ+rAx~3y`$CCEGp`IziC5y|E-`jWPPD_0 zaby`x%Ca6ZS|ooO?V@l^3-ym8Y1~=jGr~-9CP>h5G$seDbD54brIQd;YGj9NE|sC+ zvJ&QQ9%>HhC=f=C&pFBf$2K`|!-O^Sa_gX~i)v3R7$^+LD5J4r2CPmkQ|&Rtx=(Z& z_v0)rR7onC^IP6)YEzmf{oYfU27WD^t)KZPU$E2ldqlcJlXT*qc%uw{H`^wveA9pO zvROTO!CEgE3Iwj^2M{V;QwqEcA}~U79KcL_Mvx(*3qVP`vM8FqG29*vZ;7G}ufbAY z!@wP_Qi+~kg8)%C7%Sl^AROQV-=Wsx9g9|qG&sPExpxmoDEsvJ8-fAAFd^=GH z08^=$hBmxGy9`jSS=m9b&KlDzJ9tvFq-TXI;6g%ujgV$&+e700ZaGMR?QHgp@^J=2 z)7Gisj67uY;+O-~B~vqlOo+z}RtbRc9g^1mHo}q`xjWAE&_LB{j&Zzs!Tjk7bDKl7 zSLftj5!&z>>uKK*Woijw9A3T3Vs-J#G!hsD5I`mF=;$y8Z&!4ZI#x_uNGjE*j+Yky z6lCaEg_FbtrH&F6J_@{qnfWPzW{Lqp&icECRsxb~j7%#4pH@(f5COjMpYm<1d5~rD zbVSQ!$V29jrO5H!;g_XGL!-R>@=fOb#8bXI1^=~Nydo0T{3-En7Z1Szg2BT;Mub=1 zTHlIOn00eA^d%M_!R=Ob#jqvH)SbMtgdtblg;8dJaShX@ykfd7p%S-k56^4pn3hL* zEOYdRlOz4?JwlF&)pSdDJWrBRP3`GtmO?Tvgf{i1Ps}sxRJj(}!^rs()XseU7HdtL zj}#2gvIbi3edh-?Lo1v*ZLm-kmb|ClwZwOmbH5Dp$H2pvrbk@m5t zm1&PW?{5CcZ{Wa3mp)6%loAA}^6GO^b+_+-K!VhVI=XByhqy!@_Dl6;^+j0{q~BWB zWO$3_&aG|e(WrR~PCoti0ncM=egD0PZYmO~mfrJVKGGl#S)>(tU#{8>0cZJ@jO^!Pf zlUFR0DUV`#SD6*Cs$Sv<$k(M|6ubk=4z77vz=cNmw7Q53I>{Px76$r&`?e?Y+g^EMcY+ZSE!3&P8b^RO6B;Tp$a1ICp_Wn4+9FQ)A$a*CctU4L~sg5_4=g9UQ_$ zoa52HJK$TT4ai#v#wnhsE{9mtR7E-a1ijWhTid@(!9zh^&3T2%a6~Zm^+6Ghfi10EQgFbT8l z4_Hxm@X0WOBL-jZ%`=>K4w|!_8_i<#8dCl>wr;~jjB`k{r{I7Yc<*I1a-(yY?Gdoa zmTgF}r!WkKC{bo)z~3~18+*0&?xT0Z08e0k?yz&JMEiEede#M7!GT||pEDl@Bb^@- zhViJG0)tPGc6IT`eh$ttSTp|%F-7o5FlmupLGuPK<@CiroAEIkLy7^rDTT_uz zx$nEQMNDeoDsVYH;syeJv^T8*VU`jC_KFnu{MI}iQ!*M05$9xJWs0IupK+i4U%?k@ zL>h@xM}+*wd>mh-BgKL?jTi^k;gEJeXI}o*(}U*qqqoieZ-2>7DSrBNGua+QlI08~IBWLhg-7U%=2$gq z(@O_<4#A9RTY6@jX@_&#BCVdIw9HcTd0z94LK~kYm)dL9H$3B8V+0G2Q>6ffWXBvr zx_wbY#PZ3Nd)Jq43Z__{{{BDztodu^m!H1cYyOS(>2Ak(^V1KT|M_QMGTJG{;Y13-UO& zHejAkB~m$AcJq?@-JlNlKf0H`$g~+{+3!$i8wtIq{WcgrR2@*zwGL{~fi&s6Fx2;V-$^AIMn29U-QEr|(vX+qP4={}!o z9%BZyYoe4+B9T)M=Qt)fsdVc)Ez`(9&IeYV9jrDSWk@N8c_usX@*y1T2#@Za1m_GV z8rvmfG!Hv;i{&8kRD<#9Vd^`}HV@|=Bz8K@nAUG3iWYCao<8q{4(F|8aSAI+YBUwb z*=G+?V5x}`4R8hlA4AOaf)n;e>N%B2VV*RCOSV>1+XM7E7I+8)5z0-jL-^=Rk;Dy~ z?N`@YI#}uO5bGWsDj^g_TFr*m_n5NCZ%u-_pqxFlW$Sw#Z#w2xmzZZ}I1wGceA=A7 zcuvrWS@RXa3AKoTE^2Gezu(O3biwv*aY$oeBna@#cL_>uco-7D#p`e0xZye$Y z9)=UL^<@{iueH_;@8IM1|)vP|dP0)4bQz`olYsxV&IYcA*@cv!m{u9y; zhN^eEp0oRhn4igBoSneb=L9--7T93gHOSj>1Uv)F7Vc##tFhoflR4EhtVmCP z^PPn>Yae}>=>ppcrA+?t)iA9HKq?uG>8g)yZ;O8KN0@X+uG;&{&r|g_na#TV#d$ls zOngrQX@T{MQUo4&1SDKKZRUW7Iu+hUh?HMpZBfu6%^`jh-#vQT{QLJm(zgUexlMa9 z2mjaq`(K;C`Sg?K@BZqi?94=_NGxM$<}qV@PBbwYwsn^;NV9e1@>)1J1{`M@3-&t| zvP_Q}Gc`t};R=$mEb6StxLqw#AfV5u(0MI|9_BL!xyw4>z4aG9z@G7BLoBy&u*Q?{ z^1H%CpA~GZOQkjE3kc^~0W`cTl3it339G+^IQO18%%C2~gzbb>2H@(106e0RDoJSO z70EKFk_~_&yo`k z0?>u{P*1c|p7C6%_*jP(%8559<#Y^Dsi-4rF(=T-vf-gdWRk$3^h4i>5P7E3nOTPWE)F}7<)y_SR^oaluBW1rzbG-OXy3B@VvOy534(_^`eOPcNy zt;yv)bAmTKdCOp--|k`Ny?Os$GyU*Rb3*i^Y?Z~JJDBgW1n0Dw5WBa}8rp^(Ee2?+ z1GLiRRW#Nk1`6v?r+75?R|i-dAn4BNxu+@;sxgBdY;<3Md&XQ)k8Fl$)^reNW9M_# zE~qx}nchU3V7$1@XoY!v#y~uQIj|cY`#j>w2}~=5pYz9fm#c{hCvgigejP^KXMC8q)&M93p8%JPLCSFe0_%X~57hTE zp5!$NyR@k#H|g_BRlaoj2XcJx5ugwnIfP$eXr9n6G6M4$!$8D8;HOWlljHv}8?--t zvez)R+}!=(gX~X?pJMa%_us~;3BZ7_m0tR+)~=N>ei=*?NPpxZ;)$1C-c~_8k0Qxa z%d^cg-FzdXSBNK__^d`Nz0wS?^&yrklnEKGmN3^2yN9oEvTPfsz#xAL{!j-iP;e9< zEVNF4gv0q^MRIh%*3So3M}k|3=oVJV%+$7+Ch43?a!*Qz;&dBwy-y(AVLLUzo}fv@RIf~7Vqv+IbI_|lo^>?jti zq21J+r;YF6Vf%_8VY=X-?8d<&EY(WYL|wCXq-hZXZ<)f^>zIte6lhoKtT16&w!?Y= zDFhSdY0!B5#x;yfv@0Va4PWdvr<^WX+1MiD5&uqLE_&FhK`ddWJxr>`?_J3VKq$wp zu#vFFLB$S}!pTK_!!37|rS{JtqHn#gwSgv)h-1kGP9DysYE@;Df;Fzp9rAt!!iKS< z)seJ0CfLM2%zvL~Gks#YPBEKZA;|qt@7`+u_32^rn)(l}5V{Q_wb;aofjQr+nBnol zZ=O=0o2&)A%KYe*HLaulXE-Xca2#!Qj48X>yiHUd=8f4!hb?n(V0zTdb})r9cjLV4 zh)t+n&d_7ZL7$f+=9D#Ij$x83_X1;m>zKLIDFnlLqhsc27wq%er!BhHFV|r%Y%0Eq zdbTIA(4bG84$aqon8=4AuCgS(XD%US^op=Plj?(F}b0xx+`YKED?B>1$wV zI@1LINRwZxO;boyOthF+33QzhW_})~@t=qJbolOlm$%oNgQ1XlA7?f0&KQ#?IFt;b zil1V-`;-6|x5jAx8{i^l`|A*+`yXFzcJVI%_6Km0_KUN6)&fk*R%?*CN-V95_Y!7} zaJImQKDX`?8L>rnWPRI{4$7t&&Tv z8wxL!VL#OFCH7}az`K&GBJJJ9&t<#`9N(?tdBToHb?NK=k0kahUj_8SyZS(WK#M|0 z4340RFX5y6ox91G8ZL#%hjvwlr@J798ktE z2t976>}FsrT(zVL1lGXxItIDto}uF0%^-7FXu}JT*?`(W6Nbx)G}Hx(5YTpmv?E&GA5U5 zuwgaKJdBmL*ufGd(^{^FL{f|aT0Ga05lw=5;T7hU30mA>6K4S0;v)V5ZQH}Wf4RZ_ ze^+o{XFZ&og4ScKeG#H1TiewZ#W@So4}jf|LU6}$zE@eH#( zXu}*T+Uc<-FVKwo4PAbMAmNrBN;LR4L~iPD z{I2DQ<~VnW z)Av95sQKcj9}}QoD~eP05Pk8AQ(}S|7Z2%}_nkAuIYan}108eg@XTNFqZ8~pd;ows zx_f^;I_=JDzxB+)d!^(ET$jS|A{c;^&LOGj zxgPi!p<{uEIl~Ru(or)ABc4bj1Y4J)oVivrf|`~bm&19Y?S@DeEErMbEfy7Ci%Y3? z{zyX8e zvXVIp?j1~Ri#4_H-OsqW2jRzju>g1W&{A17f*i-bxQq+8xZ6M&USq4o7x+2sqrr`c zalH+WO&FUHBnZ58R3=f3IJ;v>8(SP`D{=nw`_HW>XEndlMTN57?PK4WpGkRWe+N$Q zmEr43=T#E7=}X`-bmRy@UEFq^?npdw&Jo%+VVwQ+tDlDsTp2;LfrZ?}04+T{o$X7u z0fBbYW)ve=j!C->5OZw2aK@GOQiCid2B%B>Vq7L8vgbF|r>_yboJJF09b?kj&~f;P zaNKme!&dRpFni*!`;q#{LiUcO18sV>izZafPB(RlfO z1wfQKmE7FyI_6ylagTd?0ws!4osW5oc@OPJ&8m-cOOGWg4y)`s5~n9gJj@Ve)j}@d zXUqqo=LNVOA(COq%tS7M?pf_CAo)BUg@j}K%4DEf59S>>C=H1$AP)I-*Ah+hQY*E^ zruXn0)6_h!jQAp-LL!$NR9TnJazTFQbohRT*;OYRX8h5lFvX!ojbPfwOm%CmIB#9S zWX%$pp6lWu_ z-RSuU!WqFx7-%)Xp-5$Sf)=RYbHt7^yH6i6XM(Ahh229OdOB{~b zXOrRA{PWj8Hp82kbgx}!z27L?qq%L{h`H6?I(^4d2DW~|PykVX%aRv&(V=gLQ#E7o zK<5M_DAC3q!q0#?+&**5p|=dQGrk*OdV5PC{oj4_q4bsQ*Pod_#WE0Yzz7xyCj1L7FvxYh)3Q(+7Z#7|_Q14bVl|kH zv4|()mxuTw-0f=*e;xFuFGlD+#DqTR7`hI0JV;v8_}+ZX(|hwzp7de&-rbi5?srPM zuAIVUv~1f({_&NNkkI}y%we@ZZhn0H3^*fPGFP~E7pD)_LmyDa_YdFT84Vs{?Hs~E z4DS|Z%KTUlm}r^OA<)CRsM!?ba*RosY?Dlrc7QlF@_40W2VT~@VyM_{A{2^~5^-8e zUM&b4U^XD+>|OW+432adoVTA@>-_<%W#=h6Wf5kj;~#B?fCYJ6&{(vFueS+mVn zhyBG~vtsjz6$%eKS2QOu*ALi*<}N-4-+cEq0tP!q-T#Pu%~PF64N^r4`feHIWm~oU z#j(gbai*QtjlRxU0Cz~1@5On)7v~k?i*7?Fl4nibnWximes4K4&6thJJ?XTb2qQD~ zhtpeeN{eZn0=<_O`aTJ)&n0;JqC0qxGn|NNgvpFj{pS(|@S&Iq(o5z@ zB?|deGuWlv0gn0PEj4lePC`tJge&(Tp$rw1+^GQoAcUE>g9x4=0Uj$IA25Utn196a zWkoQ7GYEQ*Ij$ZEx^Rib)XGq}lim=vF`ch__Z#Nkkk29bg&-Y)Qi_ogR!2$}bF7x0rcp zGsdYkPoH;=MH09O9z4s|RSE`3TIh0pK zhlniQ>WTr^6YFF$K;U=g{7L{8UdQ|%eB(j1z>tT_t!@C41(feLQ3?)aFwMd3A|UmQo`Pk?Uc zbBG67lbDg42N^FNewf(ry?4CTm@A=7oC>jVj8UFKm`qAM>e5-Br?}*M-^s)-@~?5+ z<(*3Lha|~MVR(TsN(C}E4)9|Kpfn)?`12}wt};W9TiEcKktvD!t>&=#4z;pe5zDaB#a@yGC&1^G1{gMEk`)99sYoK zI(Q_mONyLR*HLMWX^<;u`!8R=Wj^&eVfmPZtk0fBsGL$;bDbpM3BY)(!fcK6E2`Es&0v@tDwsEq$Ew zJX2s1GpLWaLq8SM6~7gvAqRYGf4c_#6uu$edv7#3_ltHsIKAexEUioNrOQ`rC9q2Nx@D0MlArd*+=u<2Ap9FI;wq`S2|aTmtP;1G@$)h@5)}7YWsLOePUA zI70ydg3=;F4Qd-go@83)U2EoG;fFsNIJ^wQ0Eq-<52krah`J*>O-7K&NpuSK6arHp zH$yypr>8P;7~QR%W=zzdC7Q~DwS(i=msQQ-@aRa=L*^ag{2Gk@ZL{&^ zUpLzXQJ525=9h~XM4G_^8fT>|cW)4_i1nyMyP3H>h&oS=%Z#Z}Q{O3YI5ChvCgM|w z)dHsK=IOn+#43J88`)sGkLI}`M4={Ny?94YU$I&DPIDEF_rLu0&zfH|xBTs+ADdr3 zc+8Xl%W`^Xsc(*&zxi;|e7VIgH^dt~Bku8m_y-;t$a_f07nj6%jC@9bih`Bng+UIS zCHY3!(m~Cm>-X;Z9em(nhhyT4Y3+mV+WXFY72IqIm>#pf6L%zV!r1RR2HSlv9@?LC z7@W_veE7_7J##Qr%1N!5Z6~$QFzXq>W9_4j<{Ge{LX@@{TN5RD7{z35(R}$S=K1N3 zW``v->!|c#Cnpvp@|2>8d$o=b`XF(mj5;N0?eLUwE5#7gpQ}2E2_dYi*Bcx61 zn?~)*b1b=3mHv^P1hCo?yMYr?5jBEiMvzz2j+c6;{A`x8Of< zXT3e_)2{EkXFk7t-Q9QTy!SARYP}c0zz6i1SnSR{*=b6;N)?T*?J~D$t>EMo%g^8c z{eNzL@)ZgMjB*`DsfX`^cA3X*tW$&WqWOn^_*+b}>+~9-OM!brqLLqI!L>N)wL*R2 zhhwX1K)I64KXGitJ%qfiuX$$)4(~jKm1D*@y4-LcsI}m7EU=5HNa!q|F@Z z1W9fL@zg1R{=#05Bo0GgZsB-x3IG{zgICz&*%jgp&0{Hpg(*$#6aeP}^#Bk6$qXfI zB2^()5hIC>9bw&NZZf%IoSKJWCUm9*(9`E0{t6={rfyDDM+KtIC9GW9gJmP*zRteYC!f*^KaaI-xOGm*c}|11si6hcNg4 zKJ6xNHxXX2$??jiI5cqc?2v7^z~Q?)%^IN+za{-swA~&|+MRB$5UAkl#&&Z`1cx_h z4#$**u^LAvwQu?WZ)Vr5Nnl(mBWx8|Wct{gsWW|+9(N2BemOvdxqaZ6FtUvJW#4;< zmma?N^t%pMy*8cr;UPgWQjyO)2I@EBO_$d6UT6A+akI?aZl(8|2)W6v`%?tWeByB* z=7tah>~@I5dY!Q}*xMywhaoW4HfcW_$A1FeuppKB@^ysP+LN{%5hYgQ5Y`)rs^t}3 z;Tbqv_;*ouT@}%jid;SeYqgJ`VZGU6ii3XKxTVj4 zZJ{+bq#Dx|_aFXVPHvxf_|yG%8ThU{>RQ|^oa>$o!nC_hn=iPoTj{xSxi~6w0xxB_ z2sB3Sy8qFg<}S-%mIzyGY~yAWvm*pQ9sL}4`dfT_?*pa|u&6MOU5(-NP&QHkHgy!U z<0)l{!#t&|i+)Lsct|;*lFCmpV7++}_P|ZYRa(Kg)D*2H&h}oMa!%j`|H!yYV;B>B zCCENV^RxFK0$@6qr$fY~DoMQ_Q>M5}4sz_4TKbO`55$WbsOe!FwkpdvipSmK) zbC-{7tdeI-69R;2oNXgWk`Cw^>45H{`p4``I&JYMtnWJ|X@|?Fr|CHi9SZ4)YUpy+$E;HXi^L@u?28 z&Pw$%!;bg}a80*LId-N*PkX$}BP3RHh<=%Vi&&Z7`4QK=`d&tPjOnpV4s5!ec)xTS zWm$H|th?>m%*%;)^RW&xYglS)>i_^i07*naRGpX>c*2@rv<^ZjM#G?!mzbF64}K(m zElydOHVwhGG%Al8lrhk@`IgwS1p0A7vET+n9Q48)C{T`!U_rY&DNd#wVh4QaLHdcm zZ5r`P?Ngla5C>`ykr(AG@GMx+U-7aIfu~5m5pD+vKyNAg2=nonHhp-NZHHgJZvOSb z>t^Tfo96zVd(FnZd(G(PPMnwMspiY0$2d{3$LZ~x%?`o{7DfgkZ9#*b!LX0$tNxPM zzA`h~w2h|ehVt-S`X3W0n)w;)ZhM%W$E;nQP#!$AS>C+W%rUPuHxXWN4jH^g6UG#L zA1~l5Ke^XzvzGEZ=5HUPC2zQu975V#gcO}8=HO(HHVzRC*eMT?GL-=SIcR&}g`;`} zW0mAOUig9mRqpv=T)^R_^b{7vg=T2gQ=L4nbe7V(dJ;Lh}h(=Gn%@1fA*n=`Iy@UUA`PTmO>7 z^nT}Y`P*Fcu5#9CArup&f2XWx~@FyR!PMmpFaLX~{*wq3ufc8uO#62x<A7JVA2wx$^f_5D^1mt z3Q!*PFyIMeX@c;pfGc0onkbVsQkl+z;-NG>z9Y2Td^{2nOBi`=!~A6ot8iqjz(^Gi zg@x;h4P_e!8}#KlpOjMQgRXD*ZDDSHLnmGn8<&hlC^K}~hT562M`fZ)$2YT?6!Tu0qp9YZbsc8ObzFr^#4TT&Feo>$g6WV5+HldiXrj6Zv$eW|_2W zu*}c&&NtOoL;PaqmWldZ)sF@Chx?c^Uo_KKyC4s9xiISmX1p1UL+uogAEJ<8I%R%~ zSPL959e}Q&;$m^*3d{Ii)&V&OMiOcUsclvQqkVRw+LH)To|_?AZC&VhM00V``MYS0 zq#vdb`UM6mqXSEsGsffb^?vhf=1yNyzb%;58btM&!6NJV<>Qyl8TI+uN4Ho4!~8c6 zRC5MI|LjXb5wf8-OJ2_OEM?%j8;x7D4aZn02sP;-DRZvZe)03>;l2IlpMU*z z^KZW<0uFPjKl|tg^ImujtL>`wOFST;>zp-!81xPVByq!;DP)ci22bhB8Q#YRt#sls@k^9MJlSNDEV?JcLNuiz8;mn(FI+ z^?CE^m*3;~_Pn{rG|Y&8&I$zTD~?<5Ia64>51u5CJ3s%7C}A!)!;Bx8yjuhB?BlM_ zF2Aj=zi3VO?lMli|Lwalu^&48OGy+~DSWP|9Rj%%kotMpHoDyH}?>2w?v!6B}^Uo!5BZM?4SiJcA2;4GeA>W+}^sec)Z0VnM zF|20-;XoTz0>mSOh|I7G?jh)@thmLdijX^H=?7xXWysRGH7qElC>6sk7_~wy_*PG5BdlNy}1;gnx_bE|2@30&9z^_0RGh{ooXW@|6k<1pd>7+^{*K6fqC^T#ZN zW{%LcNmI7Wx=Jk9yKFVKP0kCvH~Y+^>HKmmM)T1G#<-FR0}ul~(8@&4>0@48!LWKb z2Mv%g)_6Bq?%)W(HbU6^-I_{A9GS_9OdX(xc+p9$UC&xY1Gq1dcw`o7BSNQjHmyvN zN-|XonnCoI5XK2kK@!3hyHXg5$quq3I|)ewGIqHSM8KUyP8f`uE%a)3FHa>_V;C=h zxmYuzvp}1s{{{!Am&B9hI@{eN*sPovp!^jg@ca&OW(}r5|E#GYASLVBD}Kc6OSwG= zQaYPFBQXQ2ii-*A*jMyJh#YVZeWEnyAdJbHxr}riB0LQTBFHf3J2nw(b95lV}64LK2p#xVeiyB)O`d-s;f9HAv)yte-?9o>6 zW{YJ8>-4qG1q|!N?A21D&?Dh?M-&lR#5SP618{@Evtpeqj>YWM!rEpA9EgSE63ejo z=Q>%Jbgl469-tLmBXr&t!6v4+Zj!!_ruv95gv?W~uG60kLUnS9ryr?fU?%~q;!=)z&@h>< zc_dCN_hZb&pma=gQyuaMABn_Eum?6@KP2Wc^}lzW4H^(qH;Gg=1rD}{j!J#K7GAd9 za=gykMJ__XDT}JLfB5YV>zkU?1HZGjnnPn6m0pCoLI^={=hSI*8ZZ8;3V`{v*+w&7 zZ9Ip6C4#>5nUC97dDo>eRgDv$aM&jDD(2#M!jsctA>hgm5@T*@BQ(IsUw3ad$NSI# z;Is!#>r>w?1bzkpv=EvAthZUiyi3$H_gOu@j%EvuT%oBt=PDf%4NA}zZq+~5K^p53 zCKuq?5FP}WR@J&DNnq~FjuBTs3;#A9ZB0_X_p1FY<%Mz4DgabKtH0YS!!h4;yaR9+ zr(nRKp`O@gnA3dv3MzYq(pK<+ zu*$$h9Ghdi#Klfoks+!W90RLSXenAQX;csvL_<>1vEr^&kV@zb<@j{GxV%OmmkpNA ztwR&Cp5Bh5AYma5(XlFup_ydNSQoEpV;HJ*R>n*^;RNk2%c?!@K{}HA%WDqv&V>%& znuLm(I#(7(%CDNKnb|l2YlG`U5sTBNSPu{@2WdqjWMXh%jSd6>VI-ceTXm2*IF>Mx z9?@+?ex;2c9=&XS_1h;2&o^VP%P>(lwpf4o8BtiSV;E$F_woKb12~85Tr$F>tDSnm zAYWjsS5wmScfcN;J)BOK5ZT2(yON+)VgFi^cq2?jVoIk_!h)##O`?0|0llt|)QJk8lb3LRR2 z+bVN%IAJm9PGG7F=I5ZrEJ-O3wJCYRhKLk|)=N0NQygU$1R7WvS`Te(3BfkZ--2Zx zGW02BYy-1Xdi0nMi#5T%K@&4dP_zdJEPd*38W|5F724ka!`OQ+NtP^!nUR?%RcWoe ztJP>;5V!`=V8kx=3HFWGX!t`T7Pchs%)J=dT2zhY@*v2n6j;&JZf;u$xSG!mk-b;PM`(t}X_urDU2-J< z1s`}(Wz%K+OtZ9chU5T?_}>~JCcR9Z8~TR1ehTI{MY0C-oZg~U>ccmq6io%+jD}XL zMxH<*X~8s|Kt|QN3r~T`F~Zt`Y0W5K;M-%IYWPBjFwZU~>H-003?3xvt0GV(OvF_f zY0bT*5M)--a%a&j-Vxl@h562qv1v@8nfuN%r7)vQKp~3y@xfi0bND7T)v56!_;(EzD5PBm!PxT=*SC%P|eX+h9HgW7AX8U+N0>Nj=VKN{u-!{)+4h2vwG& zZPVYGHroDU2%L37e6Crx_WZ>=*7M$`umA7_S2z|dmaom?a;UJpD5Co*N4gc(4+>R~ zj2fLtv{T0CL5Uy!MBV<5Bj5Kl;KGA$*6xxXyICa%teR-JnrM3nyI`j|Xw~xQb_jz~ zBjR*R?0xC#+gAj}VcEbOZXWItiUd4}c>o?U_imtFXV2cHS?I>p4oh&PzdQ@@Von87 zp^YJ|LAIEvkZ<2{Fm=urhoaLzoh^3 z-fEhu&jJJHAn^;<5vf&ai-*4zCd><+qj#~IGoQlZNARVVOvk~*`f7`&iS;uyNJ?Hp z>!nxXg^|Y>`2BUmZDI9t-LM+0fXX;UPk}=)Q@({m z!RB&WBT4!ci~KJke%CPH3WVH15tmE!k%an0c8-Z0t-B4_5#ieauZsW9#s6cLT2u&< zDe$2oMiC84!u)0OagtYd#3r|GXBYlRno8i71PDS9gaRz`oNUk-kx)}T9p_RW<1v0-u2QejwrFYy0b_!Cu3x^0 z4aCnn1%A*$`eF&cFuO|};!`?g?dtBvQhKoVAgyh$r^TIZ#-$l~V6RDpzdpZ8q$mOd zIJ&aFLfuqJJ7eQ`+V2I&RHhIKJ!p@nY+V%QRUNQ|#;y#RKm~~)WIPhn4xLs)p23e> zgNcq^NKSpjEAUS30bgo?CTjF%9j3bjG7JbVnnknKS1*}q!tnHmMMN_Uy@I4%U}mc6 z1BVe;3|Mj}5;V9GOuKhZbo0SMXvMA76@~-NgMU}xta5Y}A+m)5<93QnVD6yNlQ=nV z;(rX3^Fxx|2#K3=?VMu=4D##?qf}^HBg{btsKr0NV@o{g1)mWx958+clbAAYz{fcn zlb@2J9$+Mdoy$&9X-WeP!&roQ`d2t@%|Q*GFEgi(_q*xr_Jkm^2dS|L{;Cl)vCPOY zsC4WCemi4JxhH|rEZAdSN%Z{pZw?;k$$xW%rpXQaY?0!q79N?aqC{N+iBJ+x@Lgxn zgBS)q%8-)wi2DIT3XwU*HDK(kJZjNVX0#$##YGvc_hhP+r8Wh9RCsd`Q~N>?Y%R^HIz6bV$ss`tuk-r|r>d*ti6v$+H>@;evA za|jABsz~kUm;u1j_ZZI!N5&$@VpP(TFSpWzdpl%9yT!C^b}^!wnNv5cPdzw0ih!&H zJvBOBrG)M!Zd@c$6V*8jkNRe9N%H;rC#TjAo3G}g8c-x84Q$dKWS{d&sz89}tzQaz|3DlG?2gaycxVWd0|l@4TcqUI_kWh=4*--p<9?nE0v z=-no(PoPbmWf5*gR)x6^UT3|)+$Z2eGiKxP1XCigYhbE|F{s(NyvX%!)j0xl5Ec?Uy@bY3d6I64 zmxJzx1Qm#_TIB~5a3BC}a$NcFXdYYI7lek3S;OI*I&u&TW=ZZVg^OrYp{-LVTQVKs z6HS=u=h&s?`ixy#kR(s>f!+{pZ9e7;hT#3>S;h~R2W_)k$HMjwOMI5n731UXJS_;x zJh;~eetk^w(`c&t#xlH^V3HTc#bgFm%%w7U36m39YlkTTjRMzSJfuA65%9tkMPEB2 zA7ea3LUlYUST}}PNrvPHW<)59K7oHD81fi@blX?~G}wa4t30g6;QG_NcPrz>#3?Aa z#=2;BY@i~{$>6{UZ4`SmYk<*wfopNPM*(w%B<34Bdo_J zfCI@OVCiI*D2SJu6g?OJ&1ORjFI$mQvPb8J^q!fQP>(&&Las2Qn>l4l_1E_$f0A0e%cfNt_kB z6nYF;tSro@?vtH##nP-3TpiBKS%g4|zi@B~U<-Vj!m`mNlhP&ETlI%zKLd}j7ur`> zDg+>@Gn*hM4dyHN$`cww`@NF;!6`?1?ssmhU7I`;Z~G9y6t2-{--=SoyHRI;nKHnw zur~0WAFy0nNf}k;VX@&s75PL^qcVjM&To+T@kpcR4H7bD-lGiS;27&W=dNE-qa>Mxn}$M5x=cN zJ`fDmLM=Q7D_WS(mB2)*JXr+sN2Xg!33=_Pol^3X_|iB`ze;q_4G@hcay~{P@3DsT zc<+#?xa+ixR?vco&aN${TQY9^)Bp2d)6UvvT3=Zs3kzB%#Nd>GoJGut4n&jonIPRr z!y%KrAbEqqL+*E3AsA&qs<|;cU_i0StVGZfM1DL=2)il2Sm2+~`YI6r8bK@dShBJO zLaad`c+W`*Q8yXl7>WFj0oP?C$x|{efJp^_Il*I{9_9(xigsA<=wL=*7ZR5P>|m>v zx-7;?Xi^9V+jW8UA!bREQONWnY7pdUt64H=hN5@TqOMO)Q)B;_BozocBsBHz;uU!`?}Wc5 zkce{#ra?$DQ#BkVWf5o^t+Nh{PRQf>>f2|jczK)y%3yZG8Q#0CBLFD`_0uutF4I6F zN~AX@2k8%kuR=@G7kkF~P&MZT{NxH28kI%La`}J)N-#mQu|UaU`#h`y9=3!35x~%= zA5)mw8{oi=%XPpBT?@ew!BodWEv<@wn!;0uah4%4qtlFBmvI?lC8;3*OtEaIj$d$y zX`0FdX*HL#IElbE1tbJ8e8UC%gO2f&RbjkHBJ`WG*-oQ6N1kE&(-^^|PxewDO|^rd zcsGwP38A5?rmNYUHnpqxXeqBS3lyEvtm|a7uV5eNxJxGA71|I7`0w1)V(+J9p2jTj zGZ?ANl#!d9BhBL?s4z!ln0;L_;(`xL6!?_$SC}^ODHp)I12a8&bCjMwJxcG`2lWii zyGsbC0U+Z4F6PN8o^4aHRu#ZanamC62yhC*3r(^b+ahj?Uow&pOfNdbA0H~eCg=GI zbR=*mvr^ZKQ!;*N*`f~$AL`@-1Ou0Wpx7R-D~bi4@Qa>uooT{7DVnoE0Y7NFH-nOU z!aW((q#e?{{C56!>ox`o%K;V=h^REdj7Ty7l+BnwTnXmxI@8wf7HgTYPPo+|))TW0 z&0ql$uZ58to4Zc)`0O;D{qi#08T0oxQ$cVOyGugMVL~uh2ut9N_~9n-V;KZ(3xU|C z*-FRrljB!E`SIypW+_k7ZZ0xxeE8!7UWf;U63>Vq;G7qQrLgw!5#b~xy%wlTFMM=d5IOt=lcwUlg8*h(9Pb6;_d`?FG4f{f8DIjy)k0E+%XG~8zn}`3Id-Op zX_G8D_wTKz7q7^I1M#tOfByc@m=Ye42YnMWEA@k1Q3&lf#u4*wZm!BVy-m;9M5N)}tw>F1x0(j3CelXWCPgr9Bt_z*MT2tTz!N@Va%Hb7lgzn4|?vyviP!x)VlgJE=mSs?{|TN*uB= zI{8g0vyb^{M2fy4KGw*MdxcLL{OQKzrGfFEKOC9qF;3Jap@8NDv38Ai`1PHuz{P0d zA@|+V&#@kO?se{aEp`TB4(4&sk>~> z9J9vwgdho1on^V&LQd*TF$!+d4BAP16s%>Vnl{5aLb<{G!JV)}GjBc~xxfCl88Vqq z4+Har3uu{{@#LnDAsdAiBJdT=VW6y*D!36<@Xi0Ij=q-tjBEVX9PK}89Ctq)wkQMw zzoqK5U(j$8#8SAq#7=9T`#49y3f&J)1=w4W#M^8@BXDmOf8ru|`1ViVkrI>b#28H1 zY%B1ZhEU(#9CPOsq5S2yKcpQP^!n!yLqRZ8m4eEYf|p1D_QhNj-oVvYw2-0jd1H; zRnlT>HGTd!n`vr=O-&YRTLV0l!iXfCgyOlY~VI!X^%R`K)y#HuBZ9dpcEi&KSP%fl4vW}qiAt8d$>;ltg#<&Nj z(pw}l#^$Fpe8fXlb}OmCTruR61f8H10$ySdOf=kT6Z0GdbVQrX zW4gjb4A$aDVrkN3S&9@=7#Qj3#E>zH_)4XI!lgjdoG_Yvf(X!FO&@)pQxmEqh>3@q zg4>K;2z>~0U9u@LQbZwlFh`xH6Ex`S9wzj6EV3uytBL73OnboT z4!F2uQU%SuCYL}89_gd~)xo$q7EWlMk$Erx9GvjO(>xEo!4dcT{&?RfD1vez3~n^Q zLlW>N{wwEd);j2oc*{MDkhls?OLf<%MaFBMv8!THO1fomL6`zEHcV&T;oa~I>kM#d z&cRp|IMBe*R$T-iW-`Wv|G9aT8$Y7-vca~Q&xhkzKjL2aI^!~b>=1{{S^VTbBjhn* zD$FtGnb&=k(@7cmS)Y{GTBl#qIq_e(`7OSCgci+}IJg#sIOdRw1b7Ak&h8Ojb-r*< zK_g7(Q3mjg$%7>WL3|!sQzM`4&FcGfx3{0pSWdPAO=K#f?Pq3WG&>Xwx5-sHeD)gp zQl#b2@8kP}21P??p>3eP(g&VrC2gRt5DKWfWR&*K`~J>rN6Q|U&vT!9GL6zinQ6SZ z!AxSD8ALwP`w1xQQ+z00SVv#&gyW3WiqV*3p6=d5pj>B9RDG9NdZt-6QA2Scp9L{7 z1ex9r(<-}7)rp~iS1|?X7xT#P5tc6*qb?o!iTlLnb=rWdrI=85CE55kFd|K^jGL8*sxLHg??$eOCca|PdA2(ehOV!ntnmPt9{ zr!K*OHe+KGD7+_QRFOxC&<`Tl+W%9KM z3^C%d#TGpUK)J&dcYvSZ1QS@72+9oz>=Kg7o16W#$3oz57L|J0{%2)A&CJcSc`-}` zpnkRc1wKxgCc!_q_!14iXu#kk-c=Tr!y?cW$h<@1bb;CHqN!poRf-$ARu%tM6OFVO zvMUQ3YUPxjN(e|Pkcuj3cth$ORv`!|ss#+U#`~!aKp+%di0l}|lX7F}z5XB7&wbhFRJ!dEp!;NO^AX9?KQzD^QN02z`~P!HnZ| z`u@d9`f8;}4-i=TXwC;PtuyeTftFLBgLqTc2?$mvq5ArR6@ra6F}04;1xtXgSW?hh zT8d~sKm-$|EmZFG1ua856{nOOg`Is->bD;XDZn~Cp{9IgOdnuTa?HbqI zc26@mT7n-u)dH)dU;+$#SQjc|*1;LE4&yBF8Q1Cva!c!Jw9I;2%&o;G{`!N%)LB_# z$qrT!%vUb6kT`?Dfg27%N#qm=TrS|gqK=sXW-g-%Gq|tNu22f-d%#VeOaZ1((*!Aj z_dKL*=7NMcgeAUZAkHI?xCTl@-P{BHXbXe|A;c~2Mn-@$ws!~$1BB5jBc#DsBuf}6 z^NoP4QI~BBu|CjBmt6E{FLK4lC+~ZnuSaX6EaB(;*6iq* z3D*#;oivhRDiY>Cr%Ehy)!(jp--y-*40Gebz4R6udH!ynl$&g9!T2)(`jUtPA+ohH zotBw%hX+UL4Gd^@o%zvTLCa+vz->2Yu#Mt>SPn&ek4e1&J7=(cp{Ft_1%3zBdl^RF z^OcXdQMwsILE=dnYWOUn@gwIu&z9PqK(n-5o`MA<0KIbkXMtr&_txB(_rp5WK`H1F zAlm4%_>T-}?-0IU;ghc(-leZQ>@2mzOj>fO5%iT3h3_?OcbQ=ui;jX{ADU>Kg|R0i z+$bIfT*^@GgZNwGaHbPEo+aoobVE4UemifU!euFv+XrrCemA)eJcVmmbE4afv9J0b zbH;RwWqp?EyZjBB3F7k~KH!{HpqHVBeR%RvX{xkI9!_Kd9uhwoTAnKnm@Dy?T&nL_ z%)iG+lknru9z*z2Raqnha5D0_R`5USnBK=HF@?Ev8i~6h;Aqfg|>c?S}n zoSg*%E~4?0$r=L3?kqDGsmX>tvx ztOQ8{u3%&;rpq_))BC4y(i=4UUHUVN*>;Y#h(Eq&IR(V@n^o4EftVev6&?CIvrKRq zYZi@=zJ-xOpHmylK?|RGQy&MKsakE9wkqvO@byP&W>pgvj!LEm83@BL>C!hfC9NDL zv@kLD9m0yHPEBy)se(!iA9q_T1RWfM4?PI}P4^P*YbmXwg@_RYZb!$lJ2u*-v2=|hhqeYz4Mpp(c?{EiiwzUl9wqIDU^R$QYK!o)J6S}{j0 z-H(6sH)l>>0p(Tt?va_PP~bY3VtE^#5)C4t$T`JtX_XK7b{-p_w_F(|~i} zR)e`Uw^!0)Zx;qlWbY|+7iv9+&}RYCQRmn)0ca!;JJn>$Y z%eti{j$8f>3^-TNj7{9R8lBIrjK@OA;+;}6&F&wW33vcJoMSOo;CTf8Nw35%%AJ79 zswU5Cse?`utftA=c`Og^4zR3Mp|Q9Q`q1$!6sm8({RJ94m+o!vr2qc?)AV_NHT~7X zgXq5_BJ;9s&Rc~CU1~HSG}Bm)gyq91UrDqYtn^9-=Ulfz(mU6kN+0IQCa3TtSR7CL zlLMR(VxkjV6DD!b`!R}f?IRzgzZBqdxqu5`$S@Gr9<*A9PMc+>(wz_K3%QOZG#BdQ zQC$ESLc@224SSnHT<;}%_0c};#DKxSKzn+JPxR%BHxN>o86Nd7*xG1|HHZ4{njm?B zl=gUQGrePST_CAly?VvgP;X%dm2?I1c5%F+^%8^Wa45Gh59%MqkH#dLgDDRxq=AAc zYUk{M&=t&^g!&+rBULe=N`3W$1us%$OvFP5b_gN4yQSTMARs2?3}x1J)~*tmR1n=6 z5-ny_qJM~zAx|s;Lsb$+m^;_tqycMN%?~_Al7wqR#TZC5)f}R)Swd+rhA0Lnf;WJh zTB^c`T9Y3oEteT+el>_HOL)K=&1Zzq%4|QqH!yRu4Vlp3=*%5GFi8^eTwsbGUSm3B z;9R$9AXXnuVL%-n`tRnw1J;C@^EPjf2ZN`rruoX#oIwY^4thvx5XWq$nn-4J%j-(~ z2E>Rg47EZj+858LwVR323V{bqs)O@knjo8b}gj&9h9ZSTZEt!df5=_Cuq9CllE=`K`H%zArRL5^>Ewu(wB8L>TLh z`;j6OWFY$YeQIA}O~4;KhFQ8Tp~))jpJjo_R6>7gw{W*F)DAu~xivBKkvPY5;JCZ6 z#IhN#OT>q9jr)`v>tw+peTvd1kRX3Jm)MFE{tI(qRK~c2NABce0s^;eYm;Ajf%!d! z=~^GRV#lz@vLbg`fq5zjP7M&?5M07_09{a<2^s=?f-uv!=!19{Y{|Qy9KZVc@rP$Y z!1!k9Y&6r3c?@odo1uyO41*CSIs3>du1FSjE z5I&)Gz^@5z>W0$#<4!c)L7P2-3B-K02Yyu3JpHSJmqz|dd8NNT?>~vSZHP8UTg0CL zRkShsG{GmD?c34Ml9`5fM<>IK4PDOeJNq{Q3f}<(%e2lIhkVT+(hDdK_4~;T>{jIf zVUVPC=@d)Wi~>CPR>jmii`8$Me*EjZ*ZAOZcRgChT7;ksEi}lBrZShZg{%(VYW|gR zDwf3x5`@E^L4GjS%=oDl#;H}WbQ3uW3g9X?f%rBW94v9s?~>O=e*${o3;)Ei_@-Cl z50Hsx^EF`r1OvZgkOMb@sRlz8KfrMT#+&p^>){wT-6o6jCZnsB_aN8 zTAv}h7qeT_XjQa;2EHowChK#{vJMQd&f3Q+i4DdCb+{OQfz6I(zC9uj?;yYqEAg}t zO@c}wrkehyAdD`iH)Wk0gljgD(YWDse~R^~?Nx}n5x@8XF;%Al!Ar9Q`rxmvA>mZc zjTB~GCu!>HY}`G>H#>x24k5CPG?RpH`eagy@Eg%bBfJzc8u&MbV>r@57Nw>;5(ac- z-cW}+%)gKMu?g`qaki#oGY6S0`ug-Vt)q4B5+wEf?Q={%5H;F!0qmP-gX<)@m|d75 zPzlKuvv-F~Kpo2OK(N3;CSGPlP8PRfGrCp9LWZtsmdAjugO6Ooc?InSGU-LmVNN12 z-zK#V2@Qf$gP~R#BX@&o!U(o7mw&auPBOstzZ|iosgJ20qC10my-%`)J1i4J#(Dn9 z6Lwu&4HNwZTJ{CL#(DZRMdA&lo!Hm(1|~RHLx@3JGosc-*_sU7_|k2QI4Ki#64?hE zS~6PusCLzYIqM(pGMVLe_E~e)6+vAC@U+e~v-!+pF8^|CJ7W08G##^XbBg7G$VM2osd=SXE1TGRJ zXyDP4Ef9w)gexZnncEFM}>Z%c3Gt*ilI^O$6 z7-v&!B(ks>)^A(5Yn}cMA&~PBWK`ApuLwnNGDD^BnVHfz=L;X#cSmpz=V2)kzvy!$ z8R7XDJMKx>_(eIC2cvX-t}8kUl^VuIdOTn}Dr8bS+&@Z}(BCC!>Na_`n^^k_=E)p8 zNfip+Y);_}H&H^k5M!J&2px3iHd8Hn=TamEH^((Aa#^r8JDNN8+(J*YTPV}rGWmkN z761wp`CJ%@2IX~mjdz7xhM6@6RJoqrp8RI4Wu75K$uyx~p_xJ_vunkd`Q$;ssKpN@ z6dO`XD!JCSC&pKqgxmUohMj~WBY*ar0B0CvEaQ@Q($*?OX9zKJ&1ivJ!Jw`x8nfo@w{PF3 zb8^x?*xA9a%l38*{{~D6BUKv0{QvRmKc*M>tZtbsJNLID(!8zt3c=08HOKJPkdC!R zn8aM76cRnKLCj?00VVK2D~FkIE~nVOYib3{0MWrL4XeC=o{Ag%^qZThvi*o9T`c7y z!noZdDp(D}3D4w~%{cAlMm3ll;$v*br@5(xRIY^X7ItIsE0GYTFxowu>Re=Q@OGQE z*aay>E10#X@37Vg7lhU>mJ!F%-%3$IJgiNv#`!`0nlL5Aj(U}Z6D_PlQzHb!CL2Y= zOb2LLGt6BXY#&BYVZIcMqXMoMVVRTzE-_z&FinVYjxd&n2vdo$&wqMIh(7)8xzBJh zhJklNuA+RIIZ0IdSi&m^KCw?SmZS>!Z92}1yD>q5M-blF!T;%HKqjLplJQ^#x1za6*;4pQZf_nZ&r61CheI$Am{Fae3rNy^+>H@v1T4bWEE8p(_>O*38ER;#;*!^> z#ly0(Fv3jvXs?!-X&MX@91R|k7SA+&T}+IKt3hB}5GI5ZUxa`S(=w9k5hZFV-QC^A z{8UY^{`5Zm>rc9BiJHX$w&{{a%KotLobcQY8rr7s5R&^J2EJ1{S!YcnwP&oLn> z$I$fx1a=T5&ZdsjuQ@a^1cOGRDk79`X5_EqY!DfZ$r zOdoS_owRscYb&X}G>e(hfrM#Oo|0XiC|lDBY9i`tHpcAkY|`f(LK4Ut4`yG`F1K@YGLO(WYrvrmA*eIZRV2YGle@q=Q0ICLnD3$)R%dEyo4}pT zneB8AgFeD1et?GX*?Bju;&ZLAyvgx!4f4Xu3W5cx8AV3NhY7{PAI8>%8=0UxJ~DT; zdB;u3**?blTKi1f+$2DvU^KOUU*}7W_l%@g7&E6Q^OSAiSa*yjt$F>;9*pz&JZ*En z!Ip1R?2xia1Y&iqnXbX%xdqHb)MN4x*ZK-Or=LhJ5s_hqBr5@I9pAhrm=I#+Li`rdsS68BX@v#zD{S3n)UWkBHWUiX7z>D>aFghZTY!#_ zy`x|!BX^^*lS^FSkRW9k@b#O62jhL~pk#4Tz99a>XGqpc$05e6o6KA1H1z`ifN2yK zqsaAmJ2%1WGFlPGc38jdi_hdz5Ds9-b99hf5c|M_d|o!VWT-ykM`R2b@P6U^#=7OY z=o*3vvH(lU7+>b$4DOh-pGLs=E|I^6`HrB#yjB3wb+t4kqY!ZlJtAW}EK>`i7(pQn zpv==~Z&XO7G|_fMTev3k)H9$)Z46587cdGq#>1es9nacn2%!vAECJE9jT>nM-tvcM z^0;aNuw?)7B;Dt_^~49-3w)qzWS#>rnSTm7t6N37gm1h#dPjufDUrYRbZ=!2A3PEN ztfeo&n<}YF?|{o4o1B=-yGbx<-Q|{c3jx)<>n238lg3zRZMcO2o3^nqC=MQR32!)a zZ)_|?3vTdE-OgFz0VYOUbL6}@ySnL zm2;4X0c(O7BU9VC{TCCx&!qN~oz%#tv<g+~12JkyS=T3*>DjA; z^!)V^8aj#MmoRHL#<8aJ(b{%ut`gWnpA8f;f?1!f>zzX2WX|YoaDPtL0x`(a8Yhwy z1{9ZQ(@uQ~no1i879+l0t`Q8K`+b(&48{;$G~FslWN?!yhng_NDJIvL(Esr?zGtp? zeteT|@qa6sDKszB&Q0L=V7Qsy5=iy*>{n=sTW6+w^|dsPgEa5zpMiJYKkd zl1VpaV(;L;U0)@d8(2JlwV!@^_9k7dt+SI+HLcvTmX%CL^9XozSXeqR(4LHh%r#?N z38Mny4AH3*!RrJG36ye#p$!yQy#Z*Yp@FYs23oS)1!rP&=~?iiAhOql0N_-OHoNmp zBLDJ$HOWrO8a}n~4_OvCuZC{mT9ttuAm{X%O zQb+dEmtRe#nFre>AVWB^Vw=Zd`O(Jsl`s3dBxAxVo{h#%9^!#u4&~1%U{XrFFuC#D zFUrW}PAYG%M&*-VWs{VNKQ^ko$=#^Ifdjyyv3+R0IF~=sSOAwTKK8@60_q`n*t{Ux zd`pOBF*!dl*BM()(BmE!FJd`x2C&{7+J>2=29uaWTV26wbM(vi>6&(!F-$?$rC1H- z#uRh|)}v@06JJ=z%apLR;@{VfuqT!0vS`#vAP zeMjLV2ni*r?SZ2NBk-=-#Gr-DBP*1l%04~pX$&E*zcHW4d(H))tfM7KV<1>*1%`Td zcS&Fy@gRHe)6&LjT1R16KyVpnVe1>ARSZBd`jB5OV_>EwfT< zHD>1$cibhe#YKd>9&_s&{?zCC63lvm)dQNY9iaFZ3~&OkXf#*3=KL+0sm#_nCbO0i z$Fe8~7rLbjK^K^ybP{C?i%`!v;(g&^eOX=+MhH`G@@;;E>YPuxyBRikR7FraML-t$W?+i_F(&SsN+kk+_--Bn%;CfoC$u@uQ6p zB*sXHO|~Vf9gzzdEw#1^BH&-rzgj=Ue8+(&&1c-SUY)>9 zNKQas254@GBS>IVzziabiEd2NhAR0{w}9P%Bp3hs;%72Et)#EeMrRvD$Kr#l6Af6S zUlIqGwCLxHJhvdG*?U}uq0}y7(9!5$8$v2<6in0rWnvMM&cqbqL`;{IRFU%9B;5xh zp%EFBl97Xi1PDeo5-^OAEMq+z!BTtsuUNZzkXD$4EhdeL1S(S|H@HqqtOx#vMcv;# zSV{}`?~z2KK_1T&CNc@=S44EI35=`O0fibD$Q${rMMPY80+_(T-!e?cN3)LO=etu_ z3rz6tZ=W&vpn^%nOio7Fb|FM=fL#H{g4jSnnrMm&ieG>BI2i5^uivJhzu!xXEQNaV z_+DBk@9GRDZ1-+eEY@#iq_hEE;!j>sPus+gprv`jlteeJiZ2Zb)40rpD-;m)I)rxG68o6)t)Lb-MxW_f)G^I`ef;=-S|>lZyK2qj z4w$DcoudO>T4!k*D`kcG*Sg@1GiVvKkToMrLu_#JB9yifxdvQg>h_69C*UB0ICDo$ ze&1!72!2iG(f0Nhn>3*K>B<>7D%Mc&5iW==U)(2$)aWw)6c?;?l5o8sL7!P z-LUhA^~u<5go9xN%SHPoBlTX^R76yv1)!pRm~CNNnp5f$LE3E@VfIrJgz@wUqA?wy zz|dSELzXbdz*DZsDC3s6drzc(SW;-RWt>H5u4G+uNeyLG z*q>GZNOP7T7}hd|Pk==6dLC`HKH(99fXhQBLVaDPC{d(9@; zbpmKMch)dd<4*=@+P0g4G8LKIra4MkNMNF)y%`)O%F6^qHv1^Ebwb!T8LMLo503jG zrGYd?I{JJQ;#i=mj@T$zS+s$s-(p5GLevD(>F2;~(<3xpfp(t#Ow~G_J3S`p9SGV( zg08nW)Ar*B2o($IjB)AX-Z6a zJ|&Y=D?O;sq!j|gYPc>WCT@&fts*7@GqbJO4kwEYB~MTq%kNA`J=KMvn?XM&_y9cN z$0(0_7U?qqJOzxZg4SClQX1qWxdR%vAD&S&$%iu_rfK}OUO!649*(bsQG)5*^k3Z1 zH{%IUa|sL$%&y9q+txgfY*g892_H{_FTuE+e`rTW6YCyPfNWopTX}k(P2!i+7MmLo zP@P`Bet}TsdUNIsV^;^(vHT?JwdS%fw&XX5b^qqlZ-0NdJ$d%`8Dz9PGtaUBs`e^F z^vRLiBi#I7{)+R-^Mcn;G|UG$s5+hv&D1df$sk9j0_)Z<`KCuh~BFnW{H3@+&2)dglP;IV3(wi4A(73MH-G}EXArt}XtwCoRxCDkI zgBaj*9l?~wC?d8_!Oxc2)|4$yNE>A);;@Xu zPxxki%_8Gzfy7H3&V#JsQ9HOR9{Rj1L5$bX3g<-8ts`#Eke1V>b+*859TU8u-zC<8 z{;CO&ShiLrm~?z{mEIrTroD?j+*WtNh!-&9S8y|pu|C<78nnu}Zc0IyYMzo;;M!y= zSUGbpdg+M$LL8v(z>>+Bmh+_icAN?|_Bi-vFn!PLYxZ@lTMVVcopyAGx_ZL-0h?uf zbXoZ#Fz{JimBQVNGGx=B7INcTk|Ly}i3l@g07OKoh1ucR`xBNSUZs05kQF4gI)JHQ z3LME$u>HAr<`Ter$fmkOG9*2I{3tzqct4QCh0HUv_h}rCc^VecuppUzjN+1fzqjnlh6#uA)<*Oli1TsNH zL`u(tsd8V%G1Pt#OJo{hp6g*ss31JB?t^uzXc!EJY4ey3Y^lapanQ&*=FPTIYnZ^H z@@#?*V*<(A_-9Sh-XS(b zh-r|viso#zExH!IQ?(Dq$3PS&(a^n4X_x+t3g%@unZDJ}f@~$xyT(*_LxgK%`9Z4f zJ^}|&;@`3{I-2qv0YLgU^tCA!yG4`M5Z8@GU;*tanW^owA8JsB&WChKOYM*M{H+Ny z1SmBC`cWrXM}M_`sO!!p0chq{hKYf@x1>h=<)^1*ABNP{57WR(D^_2mgQi zB7&*XzVN{#r)f#4k~l=+#0)QEzAG?T$3mJX0SE%=*l^~!*+`!(~OeHrb4%pupbKXNYm#RG_m_co<^uOHHo|$<(26oq#N5qHg+MxDc0JRS zI#1~$eX*au={Uy8hx71v@?0zxbe=Om98tA#VcTciq^Z))aEJ1~y93dQJm$cvJRCJG zRL%pHn40l3OK221+tC@?irHvLLO>f_wEl4SOV*m!(=XpYPY12HX>OaS-gS2H0Aa?g z@f@SBS3sI68aPAtix=zbxC#8=S(aZNXh?K;HOvEG$7f|QuH|g%&ly~phUi$98|B!yh0?@$$KTW@9(3VawSZ75Y zYxwr22mqN|StRiTW^Q1kY0`C;_DB47vAx>dh)vZ)GDWHq?F$&*5c6aB17e_o2j2=c zSzE<)4$`YZ$0UHjBYW{1hSrB6ArXggK_4pgb;KCyzs0bSjxR7HBUn7c|1pE~_7%Y@ z?!(%6P)jRpDqO?NdCB{?Ea};}mzSy7SjW@xa`}~J8hxRL!*_wb=G`lpwi87&uw!V@ zQORJvW*I%S^)Al}3w;|-H%()82$)Qx1*~27r%T`pE%fx@l5f@=<2waPse1gDr7bg< zrB~Aq(UTH`2KJjF!lc_uxg4fX67M=2Dbx@W%si!Vrb{6t*vYq(R?S^J6Gb!r0&@gH z5H*;-_hiaE5kzSualrgPBI;bBvO2_P1QGRpr*qa45VRz2j%`>ZAb7w{!Pb%01)~5N z#Ls6SXkn)E84Kd@+fN@CLl+HQToI1K-U%@TdUo1|~QdzOA7GhByQo1OVJ0^U~A9IHZJuySDT_<|rfQkUX#)anz_ z{LBq;+4kwWnwh~A&ip@RxuF)%Hm*hk3!~%lC(ln?T^0$$9)pYk0odOU9<^=(zlIs`fGi(2q>EP8CyZZzX)q^JXajd&^p`lQ;Yp z5KRvWx8KAvfk{EUtujci2a8}10f`i3@_iCDg>MXpCieg1 zSnxhDNCSW8kmqVJ?mDL2^x{2cU?f#OT}+S!WRe3H+x!X4h`w);lF63QUA4vV<<{1U}fT;&8Q)f za?i2JM1%IyP8o35^_XbZW0Hc7i9QyoTi`7JL(Ba6{p<9@pP#2ocD7j~7~%#TpQRd> z$Pr}@F(b$arA`|Zh;|zQvoCD>`NH= zh`$*=6e=Y7EkhdMqm7+x1PTIzgb5p(13*4vrExIEXX&#vzKfRb@)Gk{FV&aQm-WYF z^ubL7F3lsXe6_U|0aFVkG-sxy9+BP|ldRRc>0lh6o_oRm<+kPfzyH^Emj@#oB}S2- zzUm|8?|3}70Hy9@?Q!lyAImFG%U9I_tsy@FT;hzfq77bkWG3(Pk~z1%7qu}K2I}O! zXu(N_YXl=LzzxFk!?Y^>(5fKk^Vz!AAiynCGr-VEErXfg*=F7HWjY~&zLt@BlE|pt z*-6(>&wzucn0@b9o4SQS8D>ysI@31BFUFm5%+M-(=i8p=-;T3^jG?u0E&9MXgMbsf z;e2xVo^M58KlInmOdjx5oTUBEIrk=JQf8)1c^<*ArK-6<m(e&tp!A9{TgTmJFuN-fF^00F_1X|`@55dx;GYTNm)$)@!2cScM>^2h#5mxTML91uM~D1B%p+Bn~YW&6{{YLSHb^C9W=e znbyR}A;FOOseLN-IJn_k;C&;1C2%5(9a7bAU~Vuegx7Ks01%sJxEAJP_*Gi#6;b0cJ5vfQY%zn>nz zVpkZJ`&1Dimhh=JA4=P_&6YyV{&0=_)+zVP0|*^PM@Y57g7cwt0>a;PR_k zaHq-o*xC(dT!flwlS(-GfMbPi)`XL7iPVj(ZxvVl!~~=s`xDo|z3${3j)*`8b9Q1Y z*#mRUa7J;;81uwKkep0%>!2d;BWTcG1K8^Ncr-stCV;bor?c4XJ3;CR)R2Db+p!p7 z6sD^n5SGVv2y=GdFzLaK3i$iMl`;_wLST>~@Jb)N>vi#sg7OPXxN6$F1j%6*$3n4$ zFV%JABi6YV15E4WyjI|f%Berv8Q;G>FMm%uTRvl-e|t={AfGgw5gqvL6Y;ye8gS!^ zB>|TB_JPd2f2VwJP7cc`%gL8Ap_LAn<$67r#rOmMAfD7>(~(WVXvb&rjBN*q4Eu++ zRB>&L_?S-5WcganG@YBIB8HsjTcpBl?`)>dpTB3{T!c^>B~F=YQEk+BFv(!q!t_a8 zNGxr$<^BggxqZ=$U;W9ww}(6k!MqEe7i#9`1mUQ^S}Hhy+$}#SYA$le7wtcfF%5x zYo|pvCzol}&`kg9>3KRMwdEacZU?WhJ`2jp@@1-92+0Pk#@?p{f(HK6&w#6c7#rIn zF?L}b?&pFd>XD$+g#ZyLthfZ@qZHSOxWo+3otjyg0lOEpUgtQwf#4phld zH5(KVTnAr(>3cHs(KNnEw@H&ZOU&G9#`i2&b8rwi-cdS_2v;V4XtJ0TTST@BD2cXA z$8UcJu?9KJcvGNl%0Ro+HB^$K{6186j6~jr(7D)s3V&l0b8n3-J7y88Yk!jRNh%u1b!=!VKTL>v=r6G_2&yu+gNOU3d3QPw_H2+4Z zxaLHVqfl7EN)ajch%&B_2iM%jBTP^o`hCgn4tF4YD;8xJ>4<0DhN%eytq@~yi@Exo z&)qp&QlZs1w+K)}Lp6xX_Bp1Vj?!GV6xcq;#If|7Zp61*s`ngk$HqSD8!^Jy@hu~N zTQGdB=p%y%&<L{a>S@1THj_HWAfs@(-0^T7o*^8BKg zfO_1ErzQ`5dOo=nPkJtymB~REDM!W`2Lm4d$S9bxw$Lb-=QTOdfOwX1bWN^HybPW( zM|C{Mc#g0V^uQ<8X=f)5OgD;9IZKe7Yy6!n73RA; z1r1?HJp@D*!U}E`8C+BnKv2laM}WcVCmbY9+afL>pYd`qSi3@d?YI4i@}l1`AiKtM z;+8Lo!*Np9;YIoq{&Qeve2Da3t7gtp#CR~ja$*GP1|ABp@|iyT<*SFg>2LqzSGZqT zLPY%56qb>q2|dD}I>(%w#VkBPOEkGp6H|8xk}zx)3TZ>Qj->g*6@ew%Y2O$h6$ph- z8NJ`7hMM7FKl3~c=<&Sp8DQ<|F;^7mHTNo*%6o#B()Y})Gp+TabI$-K=c3~u)+?T~ z9nLqB%#eWsU*kXx0mcVaKwJe7d`>Y>4z^QT*<4Cr6M1$)a`_jpv3+kW;xAhwal1*L znfPci?t|#`HPxG%m4rejDKT}~hD0Z?iF9pYW~6GKP%Sds>**Z#{6vQ#;@%%nzjk%YEJ)W^YNR<>N1tB)1o0*yL({36g1Dr=(nEig z+J%v%-ViA|AdDtpqnsOtR)Z`x)~MEzbgCrgcLquL&d^4G#LV&PJ^oi1Y#V{%3TVWv{l=JNtp)i26O$JSMS2i++o~$O!gHvtY2PUC2t42 zNgigzK|@|-j3G=uRfd5aZ1 z`{B%=z6+eGzfZpaWyTT4Upx|D)Ld)eu6@f|tpt$yj;7zL&!%G{DBrQw-7(RZq-jQA zp-gsmdW#@<%Cf1yo_@gAWSD!Y!}|X{M`r6EpN*CWQuB81MlLI^1^W-J`Twu%T$=rz zw4yA}HpTfz*nA2n%lrW6sKAnBF8SYNvc;F}4mty_`IMToj5P!*ah3<1a}sVXK5@?P zfHU)x&=E9E#$IL%j)p41+zr=&g~M%$S;KHu5_%{l zSNI#<|FX4eb~(pCANJpa=6pB;Cj1c>qT!)S`4Dw8Uoy>&9tAB391r0!mn)9s0x1-P zwdhpo z0qq>+;|&6E9c$7ID??mKP@nJ=>y$3YSNwQR1A}SX!LsktlgMZ8yr$l4qMZO}(uV*@ zyx=%82Y^S`P!Zta6_#Pn1^uV?N7@n(8ycz)|#fw zU&{#ia*xFsK_Q*0^^? z8oWa^);j6PmeU$qX@h5%W?;(m1W8e+X7D;TB>QbU6%2%z11drp7+^vHuPJ|O?uD}i zLYGlz3NrRf$xFC-t{KSyAh-bS$6O39)Var?ATFYrLR?2|-j3|XBQQpMJAHo7f#9qL zNv;*$4eepUVI3Uf%eeH>Xgq=`Ia3Deu7ll1D&|%=&diwqSae zO`SdyB?Roe=j*v{&!adldCjtOWF&0hEt5`{bu(xX@a z-ZI`^^J6=sg2~W2K*BNpg<`@S;#~#d8iKg9V$9sAXftNs_*@-2G>--F(m5 zg~=g8`4Hw~TuGNCAeXq1iZ$X)R+yuqUD~^k2h9r{@Y^HWOWmQRc_I2^41kssTj_&* z2iRdA0S|?DuW>hS*g_7`HVUTAWko_D-uXzMWiBD)^X;3WDq=76Y79edrY}BwklydT zO;2CEV$ANP7WC^1MdSi{Y#P%u0+iObm(vbA^tj9tqdDyW?!u6{E3YW?4FtXJWm=dA zVL}&~ry(!}Zf0Oa)9uG6hkYzRaz*E{b1VmYD(pjB0;a%%NotwWT-y@NnnI)h^wo_| zeAwEGcW?I6zx?z69l1|;SXS`&fBYN#q?jxr5D$0P2uwOp-~Es8(h8Y-2B_s5tM}8r z^)(nLsqu)46ivpYfvU1wLP4e&qhpOmi~*uj%L!4A=e&M{|4cv&n7q9c4V!dMqU*<_ z+>%FavSg_40MWvfsH(5FV_h-|wMILnW+S7t;|^);X27`&NmHCAISI6N$W$#?bE1qV z_M)VH*O(7&rxIrmO;pWe4xix+*@y)E9f;n=LDnIPd4Oq6pG1MJzXo&fvrW+uEwoAg z;TkFUmhm$`S?7rnwWoOs!Ek*glYtr$;dd=nq^dK)0&7x)>6Q--L~1^ zC1Z8m6o6zFVZKMgH;3$yNay{dz4YwGTQ;_zN&o48`_C}83TDfF+Vno&|LTvQ;Ri$j zL-RJh8c<_65s1Jcrbxi8$+|ev6_EhboWv&+9BGT~bW-JOP9PwObZZ;cE(f&3H)Cxl z@2Rc2NjbcXLxM~#5{IC|T#2$71MmA$)AmDqEXO&Lvqv#M7*{PN3MN??ae`-J7q9at z6Fgq!h^eighcmUA5DqD$qPFDJpm6bk(!%sy1%5KfftCvJei?U0i$o<2h_B14Ky9oy zDa|5q)yZP!nq8NHWR@IN^2gu)%?GWHllU9Nd2-|5@g*b`n?9EI>z8t0%bI|eu=@Y1 z!(x7QOfK-HaH^@-Bfl@B$qa>o^eV(Oyll!h(IXj+&xFq#*c#!g@KrH732GS3ld+e^ zDlp!;{E8~v3m>m4~Bi%JFh zlIZi-?|>If1XIKpkMGlQ)}x+l0@}y#x0p7z9EShHrWIVa0Uwz7IGzo9VpEzi;3;`+YAxkFyzDK5^)06a$ZOk6AnY!r+ zt9%}j<93~WL<^X34+1`gX}t>3a+e+>e#L7AeX7anASkXDr-4Sy57YCp~msMSP9M)KmW0z-%m`;GQlZ&yHkB`~h> zR47omYEp(U&zdQS>6oFN8d)bkh!^@mJ{}@0gi0o=f19z>%si@KM#t=2pg}h~^>qIJ z8q@k=`iH;$59!f76OBGB++R-r<8&|WzdlZX{r6u)3PksX?J=(6;~0xfKB6A8|=k_EKa7qcqq4dAmH+44svk z@FC=Y@8Z2K4Fw15DjB(BoXt#memG~WBL`kshZ8&tRF z-1x`{bAJMEJQ}cMT%;u*-sdzk53l9xmWOkPkL7bsfyJ`zWUhg~{swM*qzL)hNvV?o z2|-Ch%kQ{P1EL5^iS|&Qx4Eb&;(sG|t;Nh#E~`L>%X2K+;ERj_2<1Ox%qnPPJp$>v z#7;ysJv2bNR^V1?vsdl_e|3-f=RB-Xm`NNAC>$$9)9xW_*9MGDLz6rAW#RI!@c&Bd zI5&^8tqOA|?aH-J{?cAuBOcN(-^+0Fiv6}MKFmR|)nEBq*`FvkGwF=y5F4ySSuvtq zgazS#!(UQw*o`La~g;&bA?<* zTdl{

?#6u>JQTIztG2pZ<0=tzk;-Q&x|p>kU%-Ez!O|+-wj~b(j8x$?Wxi%7v(g$tuw zXWKcq7gI8>A|OoxPqRP4LeexacdDp0wSYwn^XK@K-FBAN5Rgo(NFb6CZxB(k8ncS# znC+(afd!`OJN!LNR`;NtMQiCHafY9g^4z}b77Dxj=ss;FsO5|REeRF>!>7VMzhw%J ziAd{MNK9NGd<$dKx9fU8567R|F*Y(>nX$}hn!LhFr`HH5L-4~y3`G|KgbfS&aP&)} zQVsl?H%NjEV1t$Qa0{H`pD{+DizEzoXN_t%O`$?pghfF#3#UmuTXk3GxB^TM^Cs32PM!6Ie?T}qs^M~$% zm&_V&%d2FZ0#6G=6~bQv(yUR|8_Q#WjV5b`IQBcWte_uEydLh84#FNILmA9@#tZ{H zyGOD1d}*2NNvAx^I!m=x+6Ogd&cHnILH5p}2jo|7ASh~*bS#7MIhc$|;GA9NhJErr zKYS?8g=M}*52;(+$qZGzpwHg)V|nqeqr`cDoo93tMe&p_lY%Y=ZqiW&9a+5F_yzo8 zEYvKs5)<|EXOonct1AXD63qzCxy4-z8s$zw?)iN7{*+wexMff-@UvycCPSrOcO-K= z&BN;hm?`cV+G#pgEe*y)I7ad%2dWDGD|9OGs!axnCODDjRf!|?i<>gUP}VTvhye=j z&Yg(=F`og~EL(Ufh{${`)D;*i3ZcQ$$l}J#MMygI6gg0_vas_ z&F#&!#FCJBkryQ7I@KkNCPxr>5>w5N1ELfq23BgLWm0l_%J!6mO&^`-*@T!A zJNV3}ZS{P?RDzVRMwI&_QOsIYU0)jmH-h=OrzYDQvZWbBoTSjgV=zP4|5NMvBLX>0tx*p=zGiPWOitOD^#vlCSUw%nPFW%9PA{`#V)X&kB+0&3UHcWO*f%N_Q`=_b7$=-)g9jZ*9 zo%G%&?RN=~aFw`;n4a?on1O(NJKi!9&mm9rk2%J;F@ij1|FTKnJ}6X#7QsV~%`_2! z3OXaSP4q*`r#)OhIKk9TGKZ;|we$w7z>w>k5Akyo1+5=if3k2>bJHhpxvckY zBr?%d>dd$t3@Nb7KRoC-F~PLBcu!Y@{j(g)SMc)?$AV$fSNrEaw34MJle9d%?-=`@ zAIq|jUK3A+gXQ=w{)yA#zlZk&#u9qQG6XH!<#pe;rT+15{3x&a+xg7zA-*W+Qov!B z@&D$_-SowiZ3qP>4uu0WiD}##vjlRrstB6Q$5^Lr8NYK(YK=RVAAsd6&KYOG6m9ka zpOqEG|G1H_%H}fb_uG#LPlI=s=eY2PYkbFj37zNpJ5DEe%J(3B)a7f|Dwp`QY~J+s zDDUH!k3_S|CinX!`ac$`F>0 zy}zk2=m+0w++|Oj0MBc&3EYE;7adFtCk&K(o35;{rq92*OGgLq(Mk`~`pyoE;MwAgEr?XV z)g=6wv{+^ulb%7CV*Q>I-3c#22!y545=@DO*JKC|R%Vo0;!~4UqIWHhb zwA-%uj*?IhX~#h_^3%F4)1!rU6(9~^Sf`R;nd~7mYRmAQkm4rK2?Xma+h$p%2~RVy z>zty!^dXQGfQuj=V+gSc7KTb92$QnrJ_l}$IaSkwC&!%L<75FXL~16 zo3MORO2C##)PaVKIDqIxqnUi7hdYS$(MHt{ij8%&60~eG2DuKhkCmZE(9Q@cx&p(j zL2OZViR>Mc$Qy0+k|j34rcT{8Qc~8&7ipK@3lBCS$|U@N@wJ%PCJBK=1TKi&bOvDn zrU-&^6l>_}}Kfwsqqa{V}Z(Fi? zRRWXS=$AyoMEW&g?lWqEmT6#^LXjG%M-L%T0-~iR7$ZbTB2Wr?@eqGX{IX8RSiE$s zhH61^$gG{jS@07-ojc-$6TCbWFeRomI&e@j6WT+x*{66m{&SsQ@!?sup(br?Q*L7X zZLjN6hghbji4B+{n!UnqG}kvQzn7@u+M(l7t^NOafM1{d^j$j{tzA;~es#rMp;?1(6vdkF$Ntu3$ zITG-XZ1~eB<0cnud$wHoV_iNMfk+q!@Kpp-=Bv(1RrFfQVfmf!fbn&#Y0H|VF<96(6g1rm))Al?L zWk0>1uh~0~SV9$J1@<~$;{X?Z#j+4;a~U#kXgo0puueV-igpA>XZ_4q-4)XI(Eg

y~fD^lUx7da;MZ-cI*-*VE%Kwn-E)X35MYjRJH?BX-E?9npYP-X@e= zf&esm0=1pr2KTs`cb24;T=Ia4ND@iEZD1*V+Xe}O`<=@CVj{(W0t%Rk10$1lt*T|4 zD4<60gbc*8%5PO~fmMDo5OPP$dVM|<@4S2`{0G!uzT<$DW%dcSk+?}5MY=FqQ@+>A zzDcx{zv`GJ@aTw0K2l>5q_ajp{wnRJKfU-R9qhkL zmyhvBSJ7M%B)X)!bLokD!MZJ%WZ$JB)*%9jM7AL&Z46{4ylz1^0It`-=?daqAZcO0 zW2-z(v=DU0%Pqkg5aWXT-mYMx)l5HRQWxwZG`={*r-9jZdM;hjk1nRv`rHy*cHK|y zGa_wC+jmT0!G#Q%do8v~b5rnBmRUKe=GA~1&>_J-Ml;5~gdzur?bUqZw4?Zv(>Av7ThDDQq%WE~=`VkI z#j=lg2m~bOUTUQ$OY3QilxAw8BM4i=v9Ue0LcE z^t1uc@MTcGp4aE|&36km&5tDd_z?aAZvq0bervJcmk^^d-R|C zKgDeYjV5R?G0I)g?#38Z_|bNJOEK=P z0<~IuTsK7q2shecD9s)+BGxq&%`wXP1v23cleQne|GpfsC8&crO+L7SgOd!V z%V#(v-G7L-${L_TRpHVAN`_`d{x!@;?5U13Y>)?d3bJS%u3PclhH3)SysL2Zw@g#~ zbGqzIu<=-}WmbVHkvlt(c13bKQ`b* zOJGNf0x81K`R$c7q;jznR5y=r5M+i4HMz#_ylV+7Q zV1pa92<11z{M=^t@&!k=T@Clj_s55X_QS+VB&q=>U=N?psRl6pD;y6b3QwnD3{!v8 zr?i{)91~dEdnz`z%U}mOQ(eLUFCok!z}0k7So~kcb9dtm0fnciFke#jC8CkRDkhkA zTLkjZEHxr-@q5m-9ba_I-V>sjV0u|2XvN9l+wzZW>fgXr{_l2hq{8GrXKW5nTbz({ z3gYjSHO_8bcQ(x)Gq19C?gCT#EeiTt&Ck9%~}k^UB{^k z=SIN#yfSME(k`h@2e~tr=4o)w{(Je`itB8&T1RZN`J*I4pNQ+!XF8RSvD=nVpz=pT zg{TbBJoT~@6%O|8lE48kSnB!an{Uda`%lZ}#_jTd{OAA58OP7bi-#s5;O_6;C2-4w z^7SwGN`D0$)q+9W%r*)+e}f%+qxFt11@S!S8Yqa4#ox(ad8{J1(+FIwV2hWe^{r;H*!Ij-+in&i-t9NL|R$?`; zOa9rjXXQTU1TQm@dX1Iq&HlUc>h<@WYrKntCc8ij{God`fChMqb z^teW#x~E>6nz;r}+Ul@HcWA4EG&5;rLTJ!?nJB@7geCB z=Ik>IRj@V!@um zdX;q$e#*TIVCkMSJ&>8FRsy0vXWV-KN*5K@b*<2TIes!e$J8-V+Z=lqA$dSFsV7zn zXS4R-<%SK`n)cYjGkQ$eHWD1XJ1qY|Ter425s4U|%N{mPdzP4e_c$hNigT8RSPQe? zmL0;jLDs@e1(2Z%jmV-INI2Uz{&}8MX^wfl*9g@7=DBIS{|Vo!NwyMTWia)-TZlXy z8H)sc_G<7-{BLz|JN^Mt-u>;s@o$@RpIiRs!|M~A{AKyH zFHf5kd`6H8SMS-53X__ipWX?axRVCfeked{d+VpdGa$5>MY;;h6k#DgNb&j(%)lk< z+hRGzaLU1ZaIFQu?Mai+72GF5RhDn#2zqa?3=t~FI6ATuJON-7Z|*QKwB{|m+RtCS zgQrZ&*0Z}M|#?g9>L=-FEl@06+jqL_t(AG7vWvZX)>%W=wO1K`u>mz2e~?T@ATw&N zltpIEq&gzWFr1M#ae1BD+P*Lg8!v!tD4&+4@bc4!o6hn~>+^aR9$QW*vv~5`P(psJ@QUrL;{ux5t;j?9 zn(z_F-t};p*+xjM7#!dIR<2XB9=*vaGOMns;&jx7aRFThf<~jr$P9ZpAs8Ly3ZWI) zF5uDBFMM?w_wt@rfH`KA0m7&YkuKBMDdxM*$IG&UDa9~=!!6?FdiE#dI8b17@U^b{ zUB`~|8D}825O%hE$v7RpJ0^zjhf;8|8h~VbwEGZvyd>EDkyZiz&Oqc5+kCog+IEqC zY><70B0^nnZ)d%h=@=_i^T(%fRX{eE!qnx0KI^aXcBNb=CXR)d!RsfOPC-e=UxMwF z2Y0rJF};O2ZYph%@d<8%*?f#Z&P@x@#_fH~7-vN}iF%XEGoePqnO6{Itr3@Ey-+ z!!RvzvyS;EWv=42H0pXf6w1r`Gh3w`>$YDq2J54{veha8nuhRc*pXTO{K zpUMy}_M=|4j_da(c}lc=ui77r4NR@v{Pb5m)d6xUt#W?}Uz1A|@C&x0DexI7W>{Ek z;8_ViL%uG1r8gfx;K;nNmOb6U9QjvuzV_Rn_aaqg)kRGg^&s$eqBr;)H<3Q6;WcDc->5Z0RL?9hH` zb<80X;Gw+AZ8a*e))zd5h?j|gvgT=B9tKj6Z4^*xPI$A_Wf_(^0R{xeKVglDkWic{sU3I*6PJ_}o@nmmnU`&~ zALeP3#`FqbXR0Dmp7Ts688_W)5^$@~jD8M5fDW8z!b1BcuI(RLdvp$YftTq+M!Zjq z*(C;MaQ{x3U=0|8WD~ZyoUk_O)}w9W40|ZHAsjoHh;t?=CJ-a_naTaOx ztt+<2tkC}*oS)nq=@EPTr+ei`9IXy8qc81jl=ZtDRKOU*_U`|A8c>E)O=M8D#6xg^Y@3L>+VCL{Fa zKI$vZH|<}5&xXE)&OfsE{B-q#ZAdDs(qIqAMWUy{C%X7DOkzf>_*r415cOrNKYh2| z0l2rhfBiD6+zme9S-d$}gin>7++L)uf=U>;{A2E_Fw^oyS+zudaHX>JF&>P zCth?9`uDJl7789hCO93lDvGy1U&HKeHpUAas~fprfo<4O9^s-=)7!>o0I^4u?+FMN-pKqtFT9^Ye8ob@{f%nkNa z7|#gC*@7u6Tx@7GdbN&*5m2EU_Pf}bhs~jQM+nS>?lr!*ar5 z1dn%Fu6TSW2RbaRamFfp+y*#;Wh)jA4VE>+!ZN2li8URF=I{!0hWJcmVhN?pOB6#s|0x=yyz16`E9M~g3oIc zYC;kK5t=EUgA17Iou#I2rXal*8tiyD)}in?6@h^YdkeZz9lsri1#PFEnuf+xj%76Y zeE2D+@T^z!NmA}Lee;=5tWBYEoyyLo{oKh-KWQ25ZPw&0$C*o;F?2Y_;I7+E96Kim zPDYFyt20rYKfy`kNxS|z!>Y9V>{+>Z{)!+Qc&ig>%^)qXcY{L>+Ee@qUVSX@ z;lry>*ea8@pGlS^Na&@f{wIiZ(3^js#$m%vx$w0rL!+a1`hthD45;(SH%FL%2XqrN96FMcG=t#TJ9BvQNaB!kXkM zXZY=bJa^B~Vqi=P9~IX=cKs<e7`JXHs0h$%vG6%)$} zFdP#3=Nmkfhwl$*_fq))p_2;XIx2VALa{_J{}HpA6HbLV1|A+?<*dlaLz=IHbd=*3 zAP2pHXM+!V-FjslFcHhJWa4`HRza)dnal?mF~jz>3iwiDf4(Ln(((R&*-hV`B8vdd<0k)AmlTYey1`Zu;QHpFpKhzMs3rk&!r=#=#!eFsS6Z^yU_F57Mz z6*L|!u9$ow%HS&urcv^IY<~M|;S`s$z1QHk3YoZN-bdeWHAXSLk=U8PnBkh;zb zJEIGAHfkNVSDdcj&6hoRc9Q$9TP2cBq=D?vIx3sYbKtSPd%N66vHHM%$V(2m*9XR} z9hV4z6BLv#k!%zU%LA-dY@NZIjkZ_C!1}#x_fN0tjKy8xRNHqmg&Atw?Ytk*e-9dCL9qBY(m{Y2M0k;!oqSB!ABbwL!Wwi z%avw|2LW1zM*0^SApO!}Gjf^Ma%-3Qq|EHWSHNUEgrB&~q{Jl+6=!J=eq9wtsj*&} zb%Jv$qEpf{*-r2anc;8^dSR)mg;O1DhF}jW#iYfGILCB8bjc9-tZ%cN<2ECKn)ZSR z2vpCIw#CBKHrM{tJ~HxYcWBF5FXie`rMXuNfxbgpVKp7wZRxTi6HZ;o3GvJ_-EE@s zZF$bzo6B+|P*IL303zWfY`a~|fH;K^gD%tZsk2occrIsz4Fy?pkOhEga@79psR)*D zxI8^jt=x)bR5Hj2W4Q~Lj6`o=&p_21*u~^Cg2QX>bIXMSLdYmU>yc~-F}~}SX*o^s z^hRLI%)GCFY=oqr3c^1VtPHeHEeau@+qN(u`rsuHqY*FKsKAog8m5;SA^8+&BFq&Y zv>q(oA(kHjX3E8fb2Rxg)*5H!*I#^D9>bZwef|=U+g|yv1jz3kyet0`TT`AavCZS& zCXtIkAi{FFzBgt@<+d67G;z(4&lwu%%rJ@$?xIb?#`fFiykPu}g{S<$<~UUcCdy1s zr0*!PA>zVpRW!TABsAlMpxc@U5%vd|PG8f9E@f@u=(BSNYlTFHL&V2#{#5?{cmK!< z^z*X&_-XlXKfEpfg-B0Zz?rilAOd7HYYne1zb(JKS}DK&AGQfjfx%a^EyBx_?dO?w z)P2ZC8PT&(MZmQN5z=<6sHr&UQZKwsr{YC!(&7*W9&roe<2OS6&6{`S-G_JOtFJ!i z^ooZ#&b7)v{dh>c(@wd!jAI?bc}g@I!<}s~zGuv$FYr2cBR!DGc#2z^k`10Q1wP`| z6(;8;*O-B|?TOnzRtOn|IAA*(ftJCb4CzH?=>2onfW+(ORmV))N$TL4{c&8ivPd@) zX@jS!moWtT$y3K)CaZ%K%!B)SCB{3FUkVm+*XP!0J1bMG&V4l3Zo>Ff5~z zNHH|$czw>g)!yI!t5W_Q0vN4-3|&rH_cu)DfH0C{U~f>0?HvRd=$J#dQ$vMAtxYCI z&H4AuR^;b`Hw(uKOCMFxhzT-pxg+$aLJv(<9}aU1183%16rc${-Lj^+kzUoj={^e)+cyza&ll2yEdoIkky` z99M;rnzWmfM5;wAe5F;Wc~PU7{o1&I7iJX-=}~;AeBC|Fp~Tx8-^9TinTy^o8_D zl?pF~Mir-ll=*^B^o>u)%u9=X%&Z z29NQ`w(J?@zP+nB0Txo82WG@d37_}A>7_9hBNhA}lZ%c4H0VdY=p)Y?InvdP;y`;) zv!$=FM$!QxX1Jqe=EW9}KpNQ5BmggH;L{+8+Ja|L_L*wS(A7u-qu<5bqT8wKV+0YM6Mon6~{0W}c&mXiFPs1qPLazKO+4p2^V(+>cew zZ9HDtDuYT=4mj{&L@*DD&vnXeOny6@n6r#SL>t5z86y|r`{l!3g5{rbF6LSJYNLgK z+N6EP83snwvBZp4XRPiVLPsH+GLxZ4Fk}dnb~$chkBl%TF5o2m4Ax$i87&y%2hZuV z*9!QsO4KNNUd{<95vL+xbAf}`73;qvG*|nTqAE<4ylG}|QNkIyM#tENLdjckz}c=!mh@8zbPiou4*&E`0tH9F zQ58RDZRVGdvnP)4UJYJ0jz{JVS{K$vTc}sAwoaI?p+6 z4W9rPJfSA0S^F0_2uN$;RsXFdcdSpL;Yc)$%S2HCxGOBY9E-qI=1Y4NmSN;!Tn%&g zQzq`qx+}x3WeKxdll9cvEyJ{y?f3e+IUD_S-}X0`>1t}<{mU;ulFT=1pwZ%)mt&G# zjt_OJB-B>$oCorbOithh?wV+`H{EchDsDO53A%GU>~{{pP^n{#mYHpq*#rC=IF*IH znUm9L+2SD4_F^`rU{J z+t%9$6+CVN-zkcu2akB)eu;J}f8mKBfqSP5kD(R%uj^ zU}h?zcgx6DAvz`RlFzBADvVhMmDvF_garLV2vjZIy*nx&K{$ZWMAxrcfs&h9BW4GK z;rBfc6yYdxbu^CxSl3l0T-#-TL>BlrgN4bHj<{|jjAcSGSu%@l%$V8RXFdjH$bJ&n z8e~#+R*5$1VDR#{LUqD=ud^E&moEuwO6TZq(y1jn6N=D=$=H@=hG#y@ac~tkk+YzSL2gZa@WpMmHH-0T~aP>Hwp)t;2JY9tIJ%Y#&AKYaeF!OT!)fo;SeXw^I za$Pw;NVW0!W!(Nt$_EZ3Af+UCq#2>6(Ip!+az%Upsgb~yy$7Gb^&T?p`i z7{ksX>1$6OmR})c#xGxRB;CjID>jk;^4=QGN{)wa@}xvj8MEDH`!4I3ta;4*jlkh} zT)r#RVA_nE%7I#8yx!r?;X9>T1LZYcr>jEK}l3pb#?%0?f;o}!#h5ymZM zNpjvfxN~*J8Ij9FPQ4*NuMPrv${4g<8-`gaNH4%4v=L@l)CV$)GkWV)#x9M6fxuKG zwVhLDwcdjgAdafAh4AQVT}KsT#aWTldpaz*A5&A18nfmul|qG26(BNuLmoDCV>#w` z7Tj1*D$WXdl`GnUvKG2y%!A7?TM5=ObjHZ1O&~2Fq){+8FBl}1B()-;9dF*EeX98#kYL--Fg>S zVB7p4Es#$;(>CvivHDrRORLUYY-3eSoPC6XE2F3H26p$zM47hfRJ`qy0?bzmOB=Yk zkkIuxk7`8=7}R!Fc=*iAhJ3mzWEa0v2Py0_Fu@{`scAY-5L5~p z+wB-a2U-HI;1M21;YnHwEqQ^V_1uaFOGXJsh|IUk`sw|00Mpk5zsgPm@mTtU+^CBY z*Sbe8{rIeNDXxL-3AA%IOrG@d`htEc$LvCkM=^AW?)E zxpn56vtBgEF=;lz{{ye{1gBx$^aOM73|ijc{O$pKAzSfmtMmolcv@E*o@Q``5%3h^ zH!tanj2tA>>A9xU?eF6>0r#aY)*4GOJRV&x*QI4aO>+!!Gt(^^{Dh;|_WbNm!}mD;NH zE;ehP)FiBswBmE8?~J~lgq5)}WOPs=t0`CJVo-cPjWXht-@(xA)d*1AQ$ZVYnIs zi2w(4gyfWSTRT6#X1x|6$Z10R^kbiMLOrcw>*0g4b^jK^3#4VHu9;mVk!Um}sYDpU zu!|S+N6yAPJGv^{jQ=v&%}4+jX67;gW=)@e{+Lr;9zgs+0OVE}Tty&IUWN(f3mmIL zMghcl(-y~2xX01~k{Ska%KG#rgfW-!slS7!tS2=}VsY+!vEs~@^Lsz+mw#aG^yDGq z!wmcR4lB6vQ zxckgp=1x8N^mh{SYf(N4ni`VTA6#{fccB3Bpte<D z*DuN*G`h_WgYMP}hnf>g7M`K36DJj>5%`o7+Jt_TgzjY*_xjMbS7?HiZX@Xs(XEB$ zf{|hE)}=9(XX#v%D*TFa1(*0%drJY23?4{o-n{4CiBlMi*t2}7=a{W_0GfGmNrBu8YwD?KU@=Ja;fDg<6X2?NdsQ-s@uNxaNgXdza`xpg=Gb4G$po8gB zErjo58ZTmqcq0S>5|tSfgeljAflA!2#f+G3L|D-<2geRM0NE@{gDP+-2dvQ{$xbNg z?Vy6hIvOD-0(q2`f-M>kxdS-zM97oYPwzFCz%sx0?PxV?sl5YilKE<8v(~E;q50R? zjrOwxQmRa77oXu|Bw~yZ>TZ>>uQE!7^v>F5d9=;hePyj2y!crD{_w|gkLUO9+%B8I zfw^^gz%229efk+2vp37*ubz~<5AFx)RjizCdb)~8Dzo(^Y&`L#kHB?+6{t(V#BBx4 z3*$r-Wrw|`BHMFqz*az!cwZu(6HRQmG#>vK}SRLA{>`z_ZB#v*pyf`5I91(KXAk=RT+-`JM?r(Ca zLJN2;?L>IAK7#ROkY^LM{gYhrCxNgBWCXdo~@yQ zql%RlzuPDj>v;11^7A`olOXLMk1oqG2j5GOPd>lTsXYe7M*+e7EHhFwQ3+Fcph{EW zCmd;TMO+J!^H`yyP`6=@eS(_M7zt2XFkORtrZKCZ5~SlML%D&55x6QxRhX%u(NEHV za>*RLrx5YAd4;|f=smM8xEnMSJEP_K_w7JP>d zq}_I;&%WoD%F-eD$B8_L1Oxf|-~OTWF|YsoU;d1v2>W>VUUHh!xV-srQrc{@+JGMZ z(sh#-eeQ3!mDz0@Q9*7SxOm1>KD3Mg2Y$iQNmbL=vcmSss)A#i&C7GfvyOosG;XP^~WAdkY4zN(DY z%kR}xI+s>ZX0#9>TEH9msbgEm8qmcnuY(Z0t?vZ%{qf~;IY3$Nzdb5@_&yA95=5dV zI+|R>WtC-K`cN2TrH6HA4*h;O`w>stR@vgflP%61U1Bf1p4C(Z{OKb7F)|hmoOUd{ zV1=`IMn6>0Jjx8@iW$`~t&U%tv@v}y6lZyv%MD)Adxc>ImWrWUuwwC(=kmP6%-eNW zLk@Q7M}pb26x6aD1s!~AnVG!j@ESc%>!}!Q?bfelPk5$Y%g;-ir$54xwrGK}qiK#! z;!FDN#6YD1*eWzlUvbRVgPYK;j*?W$bK1)AX!#kzduPytb-ENI?5wY=bau@v8*xP% z2H(gsL1tz`A~L1&+chqjEfo&2ajRt*pn(h&TstPBH(Gdu$S$_~Qpjm1U!k*M3Jwqr zaYkbv8H5dsAb}A?xB`s+}JU>uueH3Pc$R|kI0Vz>}VpPJE~FSAYtQ^9wRXSSinmLx`I{)Rs6FyovN#iksb<>Z(X)>`%vOc%gQt#6wsESnr; z+^4J_<#!R{C}xPu-`m^y@;!qjtZ9XG8IYJXn-mfs6w5xa}B)upZx=i8n$W0fE^ z_q6-8LlZTM)e4SV_X*gb1J%m|wh9~`Wjs9*X={i2d*CQUIRVkbu;^n8^RakIjl4wg zN=*53{YGk}UvPpX5=WF1^Uyx=)ywxNcHC|Va{@wW@{+L4Z(qEmh!A7)*O{dE)ruup z|12XZ!?2tY@GE8T)PfdeHClCLveJod;w4?y5^94i4n|tn_3AT)Y84E2idLq4+9TbO zzm1!v0bQ0*sF~UTe@-F{Z1+sp+%d;Oa-Y|t+NU-Szx_q(kg7} zEQuENE%Kxt6kePAsm&>?78_W_lun|iU$q0aBh`F*^LH(VFU`a28ZwjY|fQFEY~j1g(&%^72O)(B?4$^P{oBA21Iu5`R!%;kRS+R-bK=-2hFARJ&@1h(E-U61w)1|&_xq|O_x|aQK z$&cYt#vh);^N^lusx0xKh?|}n=?(&S1fII5UYV~soOxP=tP46V%)v9BEAiAH=EW*}zZS?v&Xlur(Gqtz}J>6LOSImJ57T z0_Z7|B&gT`%>pp9C5ltaVOZH@4D)tmxLQo9tUQNavq-kg_Jj_ErXY}rMVDsgr$AGH zO3Vr~XRNv;>$njCNP1@{o(R$iNb}lBUjkmQDm&JcB$S_;kNYh}i;g>;S;vrEbgXg#ehEQm-_L>R2I*E2YFC(d zu>rvp!L}3J=OLOS`uqJQrXpr_uHPWMJk~Q(TVr%HCZ(#rHW2cAK@I3Dk3eAYWxdK0mv_%#C8fEdJ5s`k&45=J0cEBylqY2w7~s>zGl>lx@v~lhq0cd8S|ko-vN`c7QC3JKN{O!u zqh^K_;f0{mXP`3X+NTIS6-@;O`K%_wO@>67&MxDykta2eb@|K;w#)MJV$A7U&8HxB z%qWk+%k!)`g5$OP3U`Tx(!#*g2Ngr%L;03%+Pr{yhouB@eF;Tngd>ucquc9u$_CpO zI3y3nh!A-2xeaDXKVqF2M!{ILfo3!`XOo)i=DnsbKC4be%r9t%umid@)eb}Vc1Vnr zdkcpJ3~HIQo7BD>Cm&jFfRzN^t9?txltnp7RG*ow=1GRUWz6!R-n)kR&4jkqBDm^D z{cSUBk^jxJ^qc-PYw6Oe=RP+P;atMGWIbr&49q|96kWK|IQ_1sbn9i>cu^+pGpNcO zg{+0CSf#IvqTWEr(KMa#s4%Md7Vmllsk9jchL%D*41KJCF28M(XH0l5j}|4;+<=*M z_vk}8LSer|n$DTgs`$^LduQV_CKb-2yYyd68x5yuFh%@wz^=#AHZe(JJUrb?S>}Yr zr6-*;Gq|RgZfU#0Cv;lPx+%YB@-SW=TH>C4l_UF|AU51n`RUG;=nRI7>m#%t4@=c- zr%G}O@F_LzaVvwu$Fo75{b}2eot(;bY4$pYA?w%^a|snl=&nI(cJOkxcSV^Zt@#yP zGF_RgHgtPr-9EoyJBNhjtfn5LCa@GNpG>4O{6-)*>#H&eF9E5R;(&UuS=UH(CjhA< zl&0S4BE3xW;*7us)Tar->AwO%;b)sBIK&*o;Gcj0efbU~-dOLKXOHieTg)c6nHjF) zT%h&LWtrsI$Z=6Zwhm#9Nb5Tav}erIeLUxb$ew}Y}IER0;_9^SXa=v`tf z%=@EB`OWWMAmljonl?>+V;~jLdfKC?awPY!69@q?Kzej z82F$*TcjdEet*i81mbdS2%pYC{7W1CgpPn+nCOCbEF%JojM9hWD}JkNPYh0@8n8&gznRQLJV7wMp;S=JH81j@S1V?QYsz z?_1{0tJaZnfoZC;?j))2`N1>GQJ9H?=F&PdMJXLIHa#4^z+RFvQGoV622tM)88scj zTEL6qRH|j@#=F(=M~jtaPX>(%0co&;4G0#q`Ez&zd#}fACA)g{uAJ{5azF*MQk)9A zOn62J8GS*>6)TFnJ#tzUbuI(9Q}E(~K>Xk0$Ves*#CXi9O$b(KYYsnwddVq$15SA& z`8?o*M%YM}zvR(|PUWJr6_==lf4|R|%2ry3wv{uKp-jl^*V!O#m{Zn>GM!m3>)c8@ zR?%QN!05njTWf5o0S?S)+wiCf{Ovt3Fm`W$jU}hMyY!Ft3P0egf&xIPTPUln`2xe> zQ98s{MaPL-2>Y52d z?HgMJV*i45i7iaK8+gUO=)A@`2O))!US(0gi+376cNGRI0>IUXLaw7mpn@!FXgYCC z*_SoLK}X`5(=PXFIXmn#COVMm^}2qFGZUI2YA5TmIFjRlFv2_T3UkgGq4-+2cFNr~ z&H<&5ZXeQlr|ygbmh>+I10r_~PULZ1jBN>O022tD@tGoEJM>{4&2Pwak8E3K96TJ( z!vS^%qq1@9HbM1k9AM_XEaiyoj6JU)xeTAR+z<+-J5CObgk>=Ln1LB%Y6KpvN%)*+ zIRel1ZQ#>kKkgWl&MBtlDTIa-DzRs6&sj`WyTH8{*w8V?sEgvD)g`7;&|l)FLA?I* zbrV-iX#<7QI1M5n7YUoW}P5sTM zbYlzr^WS_jH7~2k1RS@_pto>~pDM;Mwu-%Cx+qpN! zkAl6XtCO;Ki|~Uml;6HR;IIbP(TO)XVO!W2Pj;EPqTHG}mt{5IYyFOUb6r2ntxxw! zNfLiIullX#6LzMsLq7ML=@+j>p`=RwrapgpE1xoJf8V@n^6NVdy!~vxUqAf+@x|^o zb@^#~Y@*EF-%eS!4`~b z-GOeP;?l!hyTuaDkPxD0>|MQp&POQKm(1XX3JgMaiWbsjH@*_!D~Pnh1YjAywFEuu zU-9u^A4Q#A1WYvM!~>WTL2cP+So$KtcPvEHD`Ex4$#2rStDx0lyBCQfWC8%*7X6M~ z(03vCEu!Kp!$Lmc15eR^@iPU*o^)L)%H<-hPOc(V5#OBJ3dH8n+XazZK6@ZEf*Ab;TR_xw81zrR)c{#W!wrk@( z>s}%-%&TzIblV(oF0v{l4YuHS)A+JJ2hq#V2xfq3nFf>?pz)b5!i4+IpjI(qyCp_o zT9!eqy);#?VE)=7nDf_P{~V{7werIsUNBpIQ~pc2D0?S6h>zuQ9(4f*I;@F-ed{0+4@d)JDBfJ6)K;4H~wr8Yqk0k^K&=))kq=^PA zgRd`PAp&(g`i5)9OjsB7-JQ+y<1x$@575WAQ6k#gc!k;a_I;ur(TClSvvTLIAqM{z zlX%s%rJxg(2~VHs;AZcHX*^?JFB+cs!eE2L;IAxT0aLp67cU;?GTrsX_ncO-=mQX= zoX|^zB)4g(`mk(`zJkNfG@Nxb(eDlxTVLvqr;V%eM{uOKVM)xuV0VSm`w(I6)`RUj7P22RB20-ZCEP@^=KZ@{u_3YsHe=JuA2ka}x`H}{K;9rm^A+?o**_@n znCV^twtG+Smd%GmT-w62rA3O-zLZbGm;8%|E&RdY{zlT0-wSRn29CdqTZ0FIJI^%J zCJ}q>>$Vp=jsk&{QoVF31tKnEmKP_$X~3+t?KYJ${E>DJi2k<0{?y(BVoI}o<=$I; zBv^fgK45QR{X&t7(Dptr`JB8*SPN&@aN~o+cu2pFwa?OKa|ml;#U#-{A~YlI4!^`o zD&D&rLUXastjfh1ub7>eFe^Hp&QzI<3Rbme*DMDJRkfm#vlGm0+K&y6jdB%Wsbx7d zLmD%N(f0vjf?S)Q%90OZaOnaw_Y_Ra2p1P7(>XeM&0Oh3I*njf>!*gKuR_n+Gsq-@ zxvhfEy49p%)}~RYW<6A`g0${Cb++eKr|&gNkH2*{4@0637s;I|^+56g?XON|Jwpe5?ZfVIos z8nJulglq$jy&Nf1Swz;58Bhl=!5e5-zVllGa4#&)V{ah9;`@1***IEZ2XEKq6ZX79 z0JA&HOz|Xc<3K|0(t+VEt?!j()=igz$sC6s@YQ1IFY7A?jc}F>0)wJKlI+`p0l1zZ zswjvYJMfeAFii-18RW)lUDkoo4lp_cc|NjMGt;EZNi*sLdU&H}GQ$)>EAvz_bNh|B zB_cOM2B6a*an6SM9RdWly3Awlz*LF{Io%%s-`<3+FLYoDe4KD!2L^=<(`MHAtxMd) zX#lfP4`O#MP<(K9I(H$8a`&M69KSCTPprjM-#-_Q1<+0(cC8D+g_nYtd$& z{}hIcDi$BMz}L-oHs5cg%q{4{el5~^E1R;=F>86RGqS#^>gdV#b?@VyLmZwalQi&jjZ9);k7pEK!arqpFrRT5N zV#c8dI4v#1w+C9Ppl>Zk@=%3fPg5&x#6do1JfnWoVdx$Y;v-JHxx&G4fLXb>L2wA_ zSmQkB9y}0-6Pj!YO8Ao(?Gk2zBR?xy+aGz5c+NBH1Xl7}d7hu*kZ^SPl2?x>@548y z)Hh;jtA*c2ho!XEz!@}ayAuNF^zj*C|LRaPIelm`8B8D<@}?4aNu4agH<4dPZj%3~ z>}NS|!P}%m>(85?*Oz0mXq&u*(J-Bn*kVOh1#m`yj2@!(8z0475c%#FLUxXpvJMP3 zaq9R$OxqWy#{h5(rrs?(#FkysB^>}6)IiQIq1k4JFxri4&$^4dw%jNVZiQj~odI^X zS+quAJ8~_7x|pIez7)XM zpb1~~tejEz2yZoyYZDo>&gpg;UXH9A%!dft3Ep;FE7Nit(=w_;S!XO)L6(m=SY0xU zKRZ9+ymC%%B$CO@F$UoQY+!4jBTN}%*EJQ6UgCt$DkFl3Aa(9hAtEs-b7;=?qSoxdSRHm%`&+nSgugzEpnnt)QphiTgkzQsnWaCV?VX_Yx(=YjK z8PP#zNJK98L#vjVf6>)N4gX&s_SMTzGkP_jH-FXV>Cg4^Chwo;30#|73s}~YcyF@g zlX_@ugCTvzZ1;i#`lV6K3m`lTsQ?dyQSUX$%$l;f7N@I9G11QMaZ#6W+TrM*{AP}`(y+%NBk(wt`8HN9owuSy(=)^? z{NC+|)qz#vs`6NUZBX7dx7#+xDs(_QGXB6rnoKk>+Nq#4FpG@biH?@rw%b602ok1! zw43&m^lawKY(++VgzcN{fEv!a<(Lu83e$f?9Oj+DXm*3 zU}i>Wkp^|9*d`gv02M=rcGJ2+rd_tJK(M<3a>aY zAtYz4O?>=tRQ~vf-|GutLT&-5M>F_t(Mu8r`&Wb8bxqKCO->h=`2usG|R+%qH@ z?WL{`OmMz_I~$+dtZOo4bOyqhu#N~4B&v!GQ+Shi01?iC&xms`4Ypw*4JU^xFmCvE zeN>{?vT%x5?FCK?XG7L+42cK9vD8`4Q8q3JuJCc6XgX^|XTq6k(8v9rIsg197)@T9MO_NeJKVkpnSr<4R63OD8ikBN+-UQ8jv}BkWFV1=Vexp)r$kOUC$bh1 zW89nAcfTFlY=`xn`5ahKt_9ImfG3gBIqG+iZI4&|bku!$uL`P9OtrS1rqMRuLO8Tj z26UZ+FX=@s$9FILBtABMua=ei)MsAe7YS$ zwx+$n);f57>Q_xEmSr<2|1JR#7;RjDrmW+&c`ja>Ra{1k_=>yLe-~BLKO+~&Wu5SvjzkD^h ze)`n0|9{J49g8+BvMrjBUloQvU*zYZW$A0-%VHINDtuG+iyO#A=5DF-BTcA{cdUwv z6N({seVz%nZW2fw6 zJvez=_P=?7(8gEf@#kgf7IAfnW;9^gDOLkJ3wk>tbG$@xP-`)06^@>pY>giFnIfS0naPekh5I`4 zx{T?#cgr>!4M23{tw2}^3;`4yvPeNiAHRWe?NX1_=Xj+3D_qdmF@esRVXedzO63lKGfv&)uPBc}R;bx9HwfEf zP31j=yI!t2@f{QLoLQsDJzGP_F<33O>$DF);v@w82}fq6kRj`^BIW?6gf`loTS+AB z2y2A9W_tQt|{8c4-}FGEN<;2DKz>i1Y7i1RCgI3ve3{x$Uy)Ox!{9br=v%Ql4lu<;0vJV<-ad-rXvne{r|mr~USd6({aP ze6)>0W(+mi&;b6u#K9T_|pwxJ4I@&OvlvgyJs=0zBewBovzHaZ&=M9DyhcE4eo&BoP(TPfwRx3aTSl>IT@)FU-$k6 z;=o6STQ4EUANg%N*=RF$@inGIXKBuEsJs3quNiqudKsYj#t&yL9q=!07q;Y0B}Wmo9jwa!jju2~zhfqTtFuGK+Fe73&eDx#s2= z1y`6|**}G4UAkgSqzja4!v$u$A@tQH#6zLgA$K%fL8}oz9N&GoY>xRxq)_ zJOdo023WfW1b68YG-L(k!CeWn{uZG`DgKJ^mniO&z11?q^xVbue89mTtpOInTR59C z4l|VQHtVkq?WwTGy2em+;`c+lm|VHP-z6=_P+S0WLhF*de97UPQkGk0qM-0V+7ve& zW5?b6J(QXrTlSQZ6P6)$`f_%xrMDsm1C$V7bXM#^EP+fovw#$OPv!Cx0uT{I8xcl8 zg@hn1tbG|2u@cakBF|^cbo6%;k<5>p6a=zN30;?e0SiU&8zd7;*#leei!eU3<8=Tm zzflM+qq)rHWqyz5ahr+eV*#UEbKfmL46uYJ#mCE-dea+;UX4!;4hNtX41O8povJYr z0p6L=*^lqa`yXD)!^<-cuzrHG%=yV``EP%EQU1t+{{P(ld3iu2lPQOa4>;zkgK5?k zsOfYyCe<1D=0nYt^anv`v?FJ2&Q4o?2P&XZgf3oQ6*-2OT1Y$BDT{V@w-^&I~Ew<+7U4$0aJh!}h@y(=pNh99n&A$27ENOQ}H2KV% z{66z9X~-f`m{wfMP3qyE0xViI_t0m!s0Um;#oN<>Au@O!-j<+&B^db(3&|LKnO#8R zX_s&0;oZYDlqD*8Rd_Nc(i+x<1YFNp(1X%fVJbNWkA7|2UOY!@C+B}gj1bOO8=>iJm9Gb zRE7DptyNj#Ga0>YTHDLaA3C1%ib4yYW9X{&f@h9})~Km|A=G7%8-uiKjvU|w>GH)Y z+gTJ+t^rCjaIyVzVGxUJO&|a-{c{}X%6!quuX6o=OgLWLp9EsAr41J@jA{q#kPlM z6D)vp6kC;P+jzzd*v1Y^K+6Of856ul2RW6IzPJN^r~r)l9o;gptVf6T;deSDNwi{- zQYr(;kI2sHiO`m=T&7P0=2yPE(wFE6j0-j z$}AuT_5ilD7gJG)P6U_m#dMG(73CgDX!v+_mwYf$#v*6U2?!e@cG+TyVL=cIYOv|k zJrE2@!t9YwSRW<`Y)l!5m`-4(ZT3Z`EfnWvQ~uKBeR_~4I~ZOejFek*R1?FZ4@m%Y zJT48>)(Fm`5!=05-Dn9%n5_59>6@1vP4{ED_23Z!<+sWz$k9h*y2at!+h;76Uwz0a z3C`*|M3Zu@*?m?6G|-v*XS--sL~fEDB6~7;!cZ8g9ST!tjDZo`Lv$gZW9pS@xIL!* zXcr^{d042w!1R2HP#uH7YNso%fzl5*NxOb%5Qs`#z?DlzBf`5*5yXJdx)5@YEjqf@ z!@eSr!4Md&vlV92Xld@RbZ_ojmo*DKh2Lp<=46*sH>WSn%ah^HGhWU@2Ou;#F~*TT>Q;9&?H76s~U%R2G2SEj)aG{jZ;v@h>Gry}B1>8gie) z6u$ZGa~RAE;{MLc*Izv@_XzM06k$$~2{UD*|4j5uhe>&MZ5XZ3K*sz9FU*mK2_i0# zS=^S;we#c>hy9Y2yjNx|R1LPlfKi7Ygr*1Q;Ph1(IIfgqog}l2`m*<6C-=j68T&NB z@d;#VeYMU!%3U6Y*^w<}y5?lr(XvTw|D+SkRHw+;``yC1A{@YrCXGT&ns95Fj)=l# zg+0$QZR1S;@a}E7de63`3k1(%Pq^NuI_9;R7_`SMHPHV)Cd(zZRjpuh&oT;lQ!&*n{XVK^q%&4jtNVbD2W?iIY##)|oDu@{CcS?Usd5 zNk4?U{q@Bg-|d@;PL=g9G6Y%+~LKhIMPZ>^q)H7`~(jWE}7v|&pJYF3!X17YiV<~N!Nmt zRmovO#GdAzgg4ww+n_>b)G!AI$k?}0dcg?=^pK^c$uSBBWxwZ{!58|#VePYrWdjrQ zCb+h8%w!1Ip6(Ejc!pwsj5YB99*D3PU%DzJ;GVky+`pRxE)d*q+w{Bj%a7gS>G((^ z4V~x+HbKT*sbCPSC(1QtD3-jO9JnObN8qdAC}^v7hBE25xZv!#J9_wt{?QAO$J{~% zz=?&SSKHC;~!UXHfEZNtko38{V)y7SlstnFrLQhT^wP- zt(a@98{)~PXdHU{u9J{Q6;ToC&71%FW^!)bywW*tP&yqH(JNzfe^;VtkSCd_-x;r$ z$fyaWgD0$t0py#(;pBiY4R|I&<&VmUS?Cqa{loDkaW&D4I6F2c^T4OxO~N3{IHyX<%nfaBd~?_l$EzJ#nRlbPDWfYMYrF z9#PB^&@4&cB6!0Lp>)88?hAge08~J$zhc^PR`1})X+b2SOkq+PxSgj=yKOocmkt4A z&JXpZl^NeFh&4TN52EdncoT2stJ%jK@T-#1$K0{Hv0b(w?3KG;Jt!9(?RIv-rgC;< z_b|Ima9o)YOajrIb!hj&TBQzS5iqn#=YYY?6B{H{#z}ZE#zvM=8SA4lui<(qgRdLH zL0AUj^{Wr%J1hduPM@*=^s6uLmEC)8Mn|EedpaoC#~CM1Oi^UOe#VntsVD&JFj-Pm zBmlDXFIExij?yQp32TAmGkyxST(q;|7wzSSijc6LkWL0*`7##GT1{SOLCKRcL(+@i z3)~b3Nf2|h6;W_gccnvHAeo^g!4Pdxlq2q0zh-?c9F`$twb>Y`=rMG|7@LNa`O|w^ zypmaJqI09Vbmh9W%>NwbEZw;LvF>qM9Pr>w^_-A(hd6_|=6`2ntMo3}$~Jb}*D-ya zm;2zsFMstl;U5|AMvV! zhOuAt#osb@g?DI&lv-yLsP@0&6YY?uL;vPQIE2>dC-@OK0+-Yvam(*y!fJwmBy}>8 z=(}f>vWJ!h{|IWK7&{XU-arfL3JSOm_@zQlMJdC!n?^i$JyPCj&>NjIEz9`kW^Qmx zX6+GuD#KokLTK1;&OCLzWmx7 zKk64%Ht>&Fy?dv0{_Qi?_1SCB#A5dDm^Ki~OaP=nCk`q~Jp{5sSw-D_u>wdF ztxljGEvhBXIt}DwU=uBwDlH28#CPX@a7;SqdmSTqBseQK%|S~f(zs@1h60jX5&TTIYe5`EloM3Cx0^f=|v&AM->XOR848}6_SSE#sF#(3+qQ)w<&OjnB*8q1Oo#L zeCOC*r-yjCIz}86-oZUyGj4}HX^w+T(!+@LY|c!HVjOTG9|j=kGp^L@w~QZV83d;S z!SIV7b*2lth^NVBh?CU`IM8BDnDeF`UDi5oGLy!JZ-UJ>ZI7~`+f%j ze3CN#8B0dmZa=iZP&&^S)^8EQ5*2_3jYjc;{asFcrouRq+ zQig8%RFie9C1y%tJ}5f!kv{8LI*5o5-mij|w!ru)g%|Lr)F|AvJ@A7j1d_O+lcD{! zU)jn>pTh8gPlT=EIJs0TUAwox-X~k&B7^jzh}7puDij<(ziW;hFyl1Xifd#lj7(~p0E|c zvapcIhjVxU`5D15dKFq>YZRY^ex=cXJ=Ph=G20uV9qCsAOvd;`&R+c9aSOWsU zETbT8?cFV>ACGe0>fzfrWr%g+g7!&^J!u-gj@>9ugYu&eRtcBt+y_o|s%}vdf~|I) zv7m$03p}Z>0;SR%m|w?~Tq<13oSbmQ*!Eez86!*sTX0*7YdOvY${DkK#&O6|Ecz^a z&2~6)lK%AAAK%4UQy#=>8p;#m9~{?*#;ui!R~}(XyQEKW%^eNG&5;ZpMd5LlE36$| z`bvIZ2Kdp;?F7k4b6KMW_Fmcg(69bjj8|#o z5tWT8=5A-o&J1NN`v*tmH}Bt;haL;Z8p?~agR+J7;FA5z?h*T%P1lpVl zK`yghtP5z1OOzwi)C^@H3pHmMOe17u9tw#i%uGf|k`dU(BeZT0>)j**lWgmfQ)S+~ zd|UqbyYEhrRPS$A~8%oz=QzKJ6QGwIA|u^Snt68AV};mpYFH+Uc} zh*dAsLoVZ0hBzW#BSZzc_u!>nC5_s!)1*v$PSqgW=H<^o0Cq}WU45Tt61($Vs z$sFvrCsg>W&K&b7_1C&aRgy2>dT2jq+M~kiuB= zsO36~5hm7WKF=ejiTr>Tn7yDE!-pJS@ut#B62e!6Uz_%5Qgxak?9vAtQ=AKk}4~DjU&&{E@^_u;+$IwTJNg%s zFV7Pqk-nIyM|g%G$>HT$dHbLK&$9Z|)4Lv(JM8iA5RI-& z^u#_9>)ZvxhC` z+zXCShPdPYR+Yh|NnRBCg!B_}$yj5MR>T%roRft5=%9!gjhF$J0Kx^#X5k6|zRFC~ z5eIC+0GyGj-I=a7hC${#1(UWi8%n?lmI0uV?%BM=%w*?sP9$xMVCI=7KP$4iX|kB_ zZ=PAdZL*{FtH7&KlzgbkNdz~enwNmkR71Dx!ahiN#eD_(EQ|Zho;Cm=LSlr6YYSpr zVTSq$;^_1_MP`G56<2tTv1MlS{$AN4xbzqe=1iieOoLju{Y1?MFhfKLS-gCg5X>I8 zB;(PPEz_g3EX;%82&E|qY8k{ndG)dkpM6$_8_VScSN1j6nt<_`K@X#XNoiWLC7>vvmJlIc z(ijct%C$NAB};POSH=$#@WACx8^;5-ZlDvC=RX{j-~8Pl6Y}!2pWQ8wa0t5#j8{-B z#_MDIDQ$w{eaSY&LrxvykYuP!OCA#*+MqyEISA}zG=Wh7L;DtZ2)+m- z^xVh@;h(tafxAK;MVGvmC!X7{dJ-DrfnqiVcRDL5*?K;Y2IT@p{~b=5llEHKWsP;Y zhbb6OD^ffS3tGfna?M<^(18{_S#SLgz`Nv}FHg|7zQ z$PN18t6Du!a7d%lsGkScNtucIz@p>+rqd26-Q<%-F{oD{fhCK zKLbe$J~c5;5zZ2CG%^I04A&+oOe7}ve9=v2jLhD6K4E5ag?2V#ol+B&Lg0Ye(cAYQ z*au3OJ%SrNfN9=gE6bhL`)Gb_RUtU@oc&US-{kZL5YiLnDeDpeBRqOI=eTw;88Z_^ zi|XR7rYT|$@*9vLUBW~$Wnq0ns4&A6irkm9!&770ggK0Z&pC?5NBV-$U1p&_?1IjS z%p<@OWCdGgpc0zITA7A>v2Z>@^9wVVk&8z$k%9TBladvRYLHC8%rFloUJx z2Ov1dNI>ZIm{4D|M+PU8ap1*q*I_^7+|$RO-6`8EXzlMmQYYhvnZ2i}oxaTQ`a9|1 z^#^l@Z9qiiM={aN)|M%qolK@E&gqdY%du>kX)RYW2;&$1c`AJC<#Rd;?I|Q8RPsQ^&`^<#uhOFBI=d9F zV{rh6env>rzp#z8u`+XULwpWha$iR+2rKz5ONPvi%;u#Kl@9c%G6L6){VKgU1{0PR zzI*$gWsjBe#e=)$+0zFcPM|Pl3s)3CafY#^Y>-J1*-Pk(%-m9#X{`t&r%j%ntO8=R z8T|_6Gp+$@wzv7s>``Wx*{S#-^Cz8*+Cnqq7Ds5H_#Gw(4mJ)?_T6&CC(F>ulH`H0 zg1)cf>o}qaC=?XrO9%Iy zv!Ucd1IcXJ>tI#i*R;tWfh37N!wQ^A5}=wh~%-8 z!cdsbvEYy>Gc9LZ@%|^hu$1;jSgc4@VZyx@6PF&_SC$EB=sDRJ#4bNS#tD++67|NN z>Rn6ZJUwEw-4Na~q#pYZFu^N_#s)~wlqGMQQviTuK4T2R~?q76x`Z8U66 z0NjEZL0AB2jsVpZHUufHNzHsVVY>kRQ79d;R{GDse_sCc|Md?P-725o-Y&n~+but1 zPuDhDQH!V~moS%X@xklNH9%)a%jCV{h&au+VYCV}2wpzgr-R*aQc&1+KkCo=Fs2{( zujW(*+Ka9OTxb=H+Q8MCST8t(bob#N=bGLw=LirTm^_wgoAnai*ku5grI|^BbAT%I zsZ1p^dBFsnMB+JwBtng2h7{&((E&$>n0T((ueJ<=tTGEce))klv@_y%Y!mTsr@W%B z$9VaD@#raG$OuISL)@XABNTw4>+TR&4*{kb&GGh6VHy)7xFeGj2Sh}_UtqpcP&>{F zKM6_i(H7=sH@<)NYa9_+D}3>+tzA(z&BglW_a z#eD>TOnZo@wi>FlCK-oLGGd^3U<8^7yx_gqL-CAHv|<=UG;EntgaphW@DpnRqJT^} zFk;MtbLv&w*OlH7WjaQ96j~TLX&noZR63B_Ed!F~xdAVrJLpW|mD#2(1V&Lptcsk$ z6Y-tA0~HIKPZ|UYnc2eNXLy_F(GBLK?bl~A1Lc*c zBb}o-$=Kh+Kqu3FdGzF2`Sr60gxFlkGQoIQ@!WOpDavAK8CWTVq)l;Bc&e<`S*o)@ z7&c5w1{qx7)Ap+5I%8U1TBSWI3unROWtJ;E?Misd#0-vL9~2I*aSA&xoeO1n4vTA& z(tDj%Dy%B=Go4DmY2u}f0F{vr!QB>18k%N(RuwuFY?m=8)Aq<$95uc&E`0K@a8Q`i z6JV;)BpvW~q=mi0T)1ZT1-vcSmuI4eDcdgUbBrSVC{uh9b}j>%-*iD4t0t}cDIHIh z2ywYy!mr}1zs+wOd~1jw zyKH0G!t}p=?>^h97;APIjPRQfQN&^o47nfbEAOZhlH0p_~eE!E(b1aGpen~B#~b2%s&x9p(J6z1}W<% zBd{lRa2er>FjPeLbWjFc2i_TOWu_wU2%*+tuv=}IGX;oH#(gv%Z5n8^ns!pIZEvo8 z%`F%O6^W!WFqHtLD}!T30RaM%&P}{CH8ewBu|_)3Be%`AluPzMzI|D4)0Y43t0&Ar zyO=dcN5_p3`eh7$IC%?w&E;A?eL1{&T^e4xDlEqTBI#R0fDe+R<=2Q z?ysLdEFTVunBR$BTNk zMy#R?7!@Y#LbP2bALuaF6|Xo&7Q*cgFf-rBwA-Rx>`!i^(a-mo`M)^f2s;#+`|Hg3 z$xDYOCpc=Ti~!dzVH(?v!3-x3g@Vf#iE09jmzG?kfx#*G$m{pU?BakS73NaG% zw9WRaSYN+E0%4*(%IIMxlVoM0V-7lxkOZ#!70?3u<0tvaV;NrJrGjVBV{INL&#<7} zcw7`vPbK0th@SDk41G&CQgnnwlCUGi3gYFZ3x?Ro)&Pl|O#{H-dzr90_r?r>=A14vTIMrS5smg8ul-2Ko`=M+ zm|?&ih3tC^BK7>_HDRd|>Lb+0z|iM71u=!+G|J31NYqSO0-UlQ8=jcca6%566%Vj^ zPHjv1CJv-K6+Q%${@D)&^f{{|G$N%BDWlD~;EFNm6{&nRMD$8E5*CP1$*(5(MJe&D z$WWef0qk`Iyx*fLY2ZU&lMN28HPB6ZvQ6cEMc~ohzLk3@VL0$0Ush znDA1I&})B-sUXG(c9=0@t`pcB!gspPBuABw(_QndF&$tK)$J5(XqAoy$1Y79;V7(r z_9gioc!((EOZGwb+!c8=PY5#;`zw@1XU8rLMc@kjT-R%18oT5?{uR}dPN1`wPo9(& z=yx4srMtaF^om-gz)WXK`qU6r4J2b67>epDqfAm#SrX;oPeaeK)<}_SqtS0OJTfr3 zu)-KPCn=OZi9>mhvWWs5foki1mJmGn9E+Hb5dbPse$!%>!&Sl}4;1jK4}tY*5CC(X zL>)zzrf6IGf@nm3m^z53Ej#BuhTmwjy4tj>Uzv+2*6Vc zJoSTazIIeqre4YK{Agclf4IvyI5FDWsluqzJK$! zyhnMSog9*NOBpO~OF{T?Xswi6LAjM+a0WIWAi;&+^)4IVLQALO%>`&#~$Et(&WC z-6pf3yy!o^QHU(QAN|rOa*Z5f1pIOnhgcBy_DLn`5YZs?2O%t>I!EsFfth&GP`jX7 zmHjq)alwYeV=^pS2;&xIURr&V*NB`5Umrj7I(7`IJC|V%2L6lh9x+P?$@a<|>gDd^sAw(@Z`o@HGylp&Hgh!OVC?g{5$E%&BTK zyH|Ki2QHN`yVGpZto;Q<3Elc!03;2mB!rOyR$i&X7$!$B`b<4~!%c0FW64do*;66+ zFhD~G(4GobK^wtszZr{ok#?Q-xsK3Bpg`{$%i>9eU$Oo?xC%`B?d-DnkS*^z7Jhi)zC@z<$yXkH+A!?~PIy zJ{c$1L|*dV73aSo+w z`$o!^@M`Ox! z`;2UWkhOkI-oc{6?RU)(F=(DFIK18e{az2B=aK*U?Dd9*z#8`IaNa54tI%%C7fTeURJ@e~~(fllf*;XZVaZp3qDn_%eRRnq{H8crl zagQ$dd|ZaiuFWHdp1nni7fweyTF-k-AFo$D4&iW?y0 z%@a`Q*D5!3(Z=nw1L6KCFWS3=)~1BWS+>r2$&fZN3>diA@Bi{2|FNvX7b_Y`EQ=a~ z>%*h#^2a}YRc_<4KIHxm^cY^}9+(eY;YA=*H0BKT&5>ImuR9jKRs~ts!3Lhl9bl~D zb6u;1Fs2@~)yXupX)xZMQwp}Bks_Vz%|=;*Q;lzNP!*WMvPlkukr=D+_7G;Zkc7&e z_W3UVxwT@hpnYK^%$zQ8zFC24RN1=abKuhurmG>$6|e}#5Jlo#Y@0Q{)ljz$=>quD zb{r3eQRj=L9exWRa1RePQsa1}1tT}!>oY}>^T`@^na<&SGUKHs+STh+sdE{IW4)gz zXY6{fDdPw(m2*%FJ2%xflMKmo4P%Z>x+K~_oC~G2RwMgUi3d4VZ@@2crmv1OQ z0^E6q$)t3a7qWxCD@W95j-?ty(pSZkJTLtGlRo{9xDH%GBN&F^Pv8XR!bdT_!G+$b zw29sVvKA1R5kMkrE}!LM{{P{SAdwtekusqKD#K)wUaC;$FwmM|S_zlNa@d?$uT1P0 z2E{`Xl1fK-D(z-&0Rb{WI<^eK5~kG3Y{TrX(@Yx>M?4;%Z=ceZea=$=lw)8hN?2ZY z4Sj$?pv~Z*B}N5Rg|kAyZR!*v?#nj@!HY231N8-Srr%LR^1polwCtZAmY?6gQEn4T z*u705dIWb5#A9K^L(hJvu}}zc2`3)hl9*Ul;|vU_3A48e2ddboc$JO|^%58=jbx%x z+p*g>YxPdc`*(X}K#wS{fTag~RnA5Z3@P?`4Pmkr*=HQFUNLU6rjjxCEMLafO)`gb zBwBV7gjVIWeGEK+5k>T;6w#0MOG!bCQG6tu@2`*d%LW?_KVnO$JHm%HJ~B@%!7foq zVF}Mf>+BG(o!%z2)6;XLi!vqvh@x?B&9ISkD~4A@*$Dcn0+BKF*xHUc&f{ys;I2(;qE-P^7RV(b4_kF{OlXQ)2@b25*y(OIGr}_EbB=s?*=+lr$lJnGPRd5HMZ~zc%M8kj;ZFCqYEjxsSm;1=BHcNo!b}{B@dVXiK zT)h5J_714f#zo8ErB8Is6$Krpgt|S#?nG$T;HL}bct1XVT-Gm%EFxo1E$4_KI7fs} zZ#$X95Q2^ z1|eel44NTMLknD>a`Qq7U4bLP)lXc-Lq%@pO8}%y?)-q)MXA!Vfahx-Y9oB(fu8e8 z^|NFcrOY`!5v@%`$G z>~*SO)5^8)&he;}yvHR+5~PeS^O!lSuuRE51b}Ng={y#sR-pYAD!?Fpr(fV7nvZ3B z#scbdUK6~*@0p(jEM>5QH@=8zfDk$5IU5)f^Sq{5)MJQOBWBFf9rzf*YOxE$ktk<~ z8DE7yRf`W``41XFLE~j8Y=w(k|D9YNqC^qieh0>}@VypK*g07RiQ76UEEM=Muj`nd z$C_X0Nr3$%ZZqVO$}WkRIQG zsa{VN_WZ0=6w3=u*qkVOQr^Jocs(og5p#J}yiB*w<{SPZZBF^o(Qs$o}X&AtLdy4JZ>=lq$1Wkg5mIeq3PVaxAMM|?yve{8*o zK9y#HpRy311O3E@Ax3PGNnOJ2CR(ev2&Eu`8~FwTfTF*blDK3Gq=iG z>HTu`;Wg7ySb*{}^?eDe5Yk1q06epV0R}#pb`$W4Q>D?jT%xA znG1v=n(&E4P>H#|8(#4`8C-^_;T1xfSbQGwl)}Y)5%C_ght9r%e1!(cfC=hz=q#P~ zdc7enS^Ja}|9U$=!Elu>Lbw@5h>qqVA|IuI(|UJa(5<`(wuKR{!H!m~{m>drVFT~w zJ_{y{STuafywLS~)?2;Vr#Qng>zh`}@9wXapRi@nllOb&Z|^>qza2g;|LfLD>9XOi zW0whqO~zyABBWIa^oM?{RLm4p2>9IrKzouQ|1knyj=#dJDiRk)pE6&0M)vCx*|h5@ z?|J?1r7xc_q&^#ALt7v*b`u)5&sQA;A5D@$agEHS4#V5y4x&n_Ss04KzTH9IR1 zA+tx|&}h~(0w;H{QGkjYy+#X7Xt2q!;$)F9uD)W*yg^&0L0p z8i64|D@`jz#CNldBg&L^rC5dteK18@R9@`1P}w<=_AOlw7s5^60_+@{`XVl(iKyZ;5DJQ^H11 zPFQwO^m<=oJbw)<`{Gil5z|Q?hI=&vGdIY1G)Ut)8Vcs+m@G(AGcNJubTK)qB9sm! zX^jMx4vIj9jN(;$#E}Yt-FaT|4c?zLW zpB3W^ZJ8R`^j&Xqpz)953je|zpfBNISz4HPkUMp!C<0h(Y&roQT(V=*3iYt(s7%zQ zs!!&Nq0FA}A#qMV){u%pMVv7^J~4){E^USs_)pJN1tvVCzZhR!XCLH+)DreS@!HTg zI0US@KG7IhOuaE2sY_h=QdwqWZf?}r2wm{G8WJQ}e&QKCK4{KrN|sji zzLnMW)Dcc)FiKQjk>Gemt@92c+rE7q3Ml#xtlilLj597I5lXR#ajbW$hdm|(*%<)& zW{gp}{*aB%Z`>-c|3KjhsyST~Fn2KRh^xoWaiq4X7`3)h>(L84S3#J><@~M%S;UbZxNws0Y*YG>7K01$ z(N!3XaK}ZG8e9RSHI#&iA6-ZrpRO^4R~vA!3i4{HB-<}q9Yw*t9zL9lBQ)!oidau8 zBqU&|zwr+2bY&PB5MpBh3SJp4A%lwcd=E2mbCLmggpBHS3L6!ep&*r$nTRrgV?^Jj zQ?@C!I@hH2Nz!Lo#`^f4qLE*}JS=bCzboshJZm5?2!%XA*(^Ym zmyFB)JB}elB7hqp+joa8k^9+h(={oUe~g(!tJOuv+!Vp(+9@LhClsH*hM`rODZj_N zaIldebp|8HUyTl!17RdC>N0}(c`zqTh4+yPF))SeMV-G)XEbK$)?6RKffuBb8c1yW zFUk}mTwreU*Sj~$E8y|wy~#fw!pQw+owxIT;NEew&D$vAH2etz{gBwCV6w8 zmadvquh}St0z=q{1BD!Ky!hjUujUY6RfxmXe3oua)LEjItz(d;yx#Nv9t9n^i&yav z9Q6!iFlg-H*+P@f?WhZEyS7`OSnpCmR>Kd#1wMHWbRzGmkX7!|jXQxfb3L^W!mk>k zGQGlIzBPK0f*+0=i9qeU)X z>r>!i0ogOATjx=CVo{PJ;3IA^JY{J1*|8i%U1?a)5S^$W?V2A<-$2q!W zP8b=br>~x=4nUPbIH$QZITo)7K?oq$?_N}tzX3GK>!Cw1UpDCD{m;DnyzU))A9u;zau48hYRGS13 zl|WUDC=fkmh(O`E$XcuFiIDt2B9ug~ko1`f3x%%t$y0pbSqdM7D>yJzq%gKPwgOWN zJ=;5BX{pe9WXn#-jJqDf@CXRYwq0V;y?geme0cUI8FFnDG-Wc&bFAwVsyvMsD4PD+ zL)L*YcR@eQ3>`3rfk_n_Er?E^4*>-b3Q(&HS9t5pRw+ zpVOW+&$AR{v)`s#?i}GChA;dy2Fymbe^H9F{tHWC=lcRn2joM&E=~bsU<8~(`?enKu9HwB2DZ}_;dfXb)hIHS>^OyYrOCjJ&r0|Xp<0bU@hvQcXs)kK zWNqQezbeWrT&@CbjZP$FF_&Ugx0m8b4r`jcf(UxGI!0`g_-cY*=D2fq)r$zA) zMAA4g%E7zOvafag{s4~cKhIUZ`3miL-r?DvFynj;9oqsj5P6_+Jw&nEdm9cdp-|2J zh!Ko3N}?F|dac(<>+)cmXoD5yH1vq?#hXl67`)7@D0!)XUE1Rs*Ax|z9+aNQ7#<`N|ZXKXJXVCzPlE1;O7h(X_R7diS_h+58qUm z4gdf^07*naR32!{@UA%wLyQhRXyKMWL;3K#@DjF_XV7VU_eJ(|RfvXgRjiFw8(`6} zN30J%`!4a?xWre9ziM)S1BAS|hCzjJ=reFlM`}BMw;fM1aIoHV#P&p(KY30|(r1Mr zi)7cL#F$Gd34CVjV%4^XL>Ui+MMr1{PXmQ;#&}&|tmisdVrK<6+2y{hIYSsbZ{}ES zS6m4%+xIV93Na3aSp+n{pE8edB~c;^1}ss)kK*|i{*VHdmyXNpZ~j)c?mdL*SIWC@ zKa@W|IW2#AMJVwdRyT=D9g8Bsoaa=GT0L1R_aU?kyy1>}#M1Y^AyC7&hPq)4APO;x zFa;dLncC-^af&vmc^@Ms9y zrAj~{G!&kuy*AO9d#pR&W&1!nLo|mZ8+hTEOeX6>_ZgS#BZu_Wsg*WuEYcq{$S*M9 zq({TPji44jSIntNF$z^--k*7d;di>$7$4zlVC#D`ma0%3X2nSoVc_c+m%MZ7?0SqU zcwL2B>CYhxnMPSdE}DVBLu15@T8$ie)V7sDhN1fyo1ieTvaj-06 zj*OLOSEJDH{tlo(h%nPBFsi6r@$g<>i2{HZ$84BcdNFCRaRxlcP?1^uH!~p2C>CA~ z%ous#nUw+HvS~ZZ8oABPY(vK#y!rVB>*-<`12?9r8f*i0g*t6rI2*O=dID z($N~j56W}r+f68A;NYA7yCP#odyVXnUOh(!xM5zIQH5hzt||x_EY(10+~kd_93<3w zW9B}hWF;`>40bciD4q zca==M5rP7Xyobg5?E4qxEpv+h;a~p8vdR2vi%^4tLPerS9HA&f6QXb&bQ!@hWgZRT zkFuD9kY&;~0vJq@S;=!*&uSgqeU|1B9w78ED4T>-<&7vBIvs-mn0QF!TFs`K3xi7Z zFogul%Zc<@@-h!z#{lm|X&|gr?7~#VWCfg;>>~2blJGZ&txzCzHFY^e)YZx*XJCeq^40oqV8A8S@4(T*k z@gB5R1(dKXV~7agyvq!*Q=^y+r*T7;YFRl&kH-byQ$mhy-UoWsz-#K2?+F{7@nudoam|-CHfUA6}f*a4~Qv)ro zONbTt>mgJDCCnXb9gE{?41Lf0J!9i{y%7)j!1+@ZqNg02*LdES_XMWkRcFELEUMwP zDoNLD_gPw{;T0O7zxGGkFo{MZyiamc=GnO^?QySkVsBjrLL=lKpimD!l-AZxnQl;A z;0n(a!xqV3x*O2eDq7X&p0U3to~y|@*LX>e+h~P2b<=I50`|Xw0T1LUzN%qZKl}V@ z5P<2~CH&6afe$#eO{*P=cx66ILaVgF<^Baa=AxuyO*1W0I+KH#dmR7~OF? z2}ecU1FVg#)y>~4UxlNAXA>n6@6FNBqu*JFDuMoB#U30SX8y=>UlnN$VT!P7wRvN` zuUe9ZS$xSbPEuB3^fR_tXK@=T~dga^F$3{qJuM z${NK3uBluz=$)0@6l8zT7GmFh^P=4O?589bxc(2z4r?}v06gD5*uQwi09@iyDH^Vm z#`=7BekoQkPtI-y7%-{s!u2Y;n!RbV5s5~;BxYe`SlD-kA-%89nT^%+36-K86ncnA z?u9zJ)bPF5wc%dJ?X_mZnMBYXjLYiIMi~O{H)Jt?I6f_llvTXN*luD%wg}~&USQpk zSa7{{yR1FD&oT*wD2$Q07N!R*OknW}afzhhH52~yC3AE1L)tTZBIAihtv?e4nxqF> zLum1XiUSg|e4_@TAwmVS-k%xDmOGmgNiiJLVJU)h#uG|P=@5_T^JKXuGe~Y0&Tvl4 z2!zT#&p?nVAOg?OeTAcR`(uTwQc+M>Z-B;{gL65=@eI#Zktqx@@Hi)K0*id+sobJ) zmnU3OqhPAh7mKdD@j><6feJX~c5bkbb**eQbfc(jmw(Qc+J;IZUX?t@1Eo88NCE5b z;V0UX27NV$pS{LI&oLdO;hGl#(CJ4Ah4FZoXi<=kphe%E^B=RsX+kJ&dTJIk#ujA> z`>Zu}8AG4tZYq7};4ALyW!^T=}gsVc+0375chlu}|4&RAZyf5~LUcz(Cu= z9jElvX%(D^PkSS3e!!hPkWjr)W3EGks*$sfLw>YF_0HTr`%f$?oAL%MJgvM}o?vJI zp9bFMYZx^=!y8mSC{w~4CN*)9M!8<8_>~TQu6>mULXWgrWh=54!m&Iik$yHizbJqG z_wN&G9o`}$xBy~6oxeRQYf}m}pja~%hs=7#T;?fRo2^et zAftcEluMR_EztIDiuYUca1O7yX9-xAa2;XHbWqS6I752v$%v-CE*|X?ICNZNyyB~m z`(;8nc#I)8B45HKZR!khis%3?HPQ!oa7<=&#SeA5037@XH$UdDuJt;rGE+HB*{u2_ zjkU`bos%(PyAX91LX|)TXoFA{UG-qK@=2AZ)0JuIl}Man_{R^e2$Y#?LW&IF_Ff9( zOT4xN04o7604VzqMn*B2zK_--+d0i^U0i(85Mq7nu?C}i1S#y^vl$AGmPi(AiHGp4z_@|gbQ(= zP=Us9o87H&S3uOywpACq!_16RBf)Dj*9KG16T;WHImd!C<4)ob4Y5KQuS+wIsChfQ zIwhZHSbp}?o8|P$`|>^OPX7qpSJ(~b{wZN?=1X_+O8Shi%^dmS!mxcCzm(5j|4H><|w=0wlPto~;EG6fR@h(PC7r5LpZU zueAJ0Lh2&8&HC%X9BxR^Vx1}z-sPIP%Ml@R=XTZG`V2{!KrwZ(M9p|s+1HG6=~W@( zxe8O?8~T)fe2JS%d&0gEZ1i3Z9B*!o}6=vIYIFVUX4W z7CZ|AR*0!kH74i~W49x9IZZr>89Y5(aU!**>v^xnhpXFz{GNmiKK9y%H1q<51-$q#;-I1rZcB(n0w4KahYn; z9PNw6E*7>)J8p#0F}s@x)`(5KJB#aVF-BFhm(Me=pSsUXvAF;2B{=ulF&gQs{fy$I zPt|x9SBzf+haWi$-c^Q;Jm4H~@gCtSEqF9ETWLe(;5mN;_K7xV_!vPmIjJg7y;5;! z-DGJ-*i~bY_Pw9++lKuT7t&us$6RZYm)W_YQ?KR0&Q0nV4awDb@%%MT$a?vE=Y1LB zG5+e_X4zTC07jwRq?GAX_Sb#=`W+EMWja$F@cQB^-Xk7!iy8n6Odrh8vq3NN^J;uv z?%%&jn#-N?-M7yupUN{(ws(H}S=sv<@0MqEkTHuq-{qLrEmrm@1J^OSM`Ob8WbscA z@KovFnFg%J64l_`I3P)9p(w$H@qiJfm#=Y|C5Fm#!3{)&bLd*J&(WQu@`NWOx@*Fd zh)P}(7Y&Ow5y@YhB^Dw_22_YQ3~bd9cRQaOT*cZpJXsZPo|uRDU(g99#qizd;L797qRHN&MiuVMG;= z!rV$46Ba{X5avBYnfCA$S62Gvmv^?yO_G83*~!4Xu@>u~+M7FN@BLMI%kqFZO0aE{ z`MX8t9vYUh>tWC%`k^(Xz_4%p4-)mF>W1;voD%E)oXSW$siFGd{HwO}eCGb@2T=VHx*H^XaM&N9P1OmxC-5l_E+T>kp! zzm=CKR6(L}!_Qckb%RjfBlZ*Bz*~Au==~rWHScAyhOI%F_wU)+ulyjB~y&(?Da)t#Ua;5BwpoWE3)ZR6ZUn3Y*Sm z&r-fOwBoyh*-)0BgG=5OD8Xa;Cyr^4>lEJ2p@&xsFCYlDqM=`lk9*YQq$@YfFpK6%18UWHnJPGm6brs%e zy^fD_8MC~=OWY_V4fSaVd52?n?XM4%*X1J(Jq>|5LfbD%n*09S_iXh3hNudBiV_{a zwZ8RLc|?D=_G=I8JfGgN@K=e|sVB!_UqeISFJ2J_ zNaL9~c6mb{Qto)2@Z(?ZYh;uPpjqx@dk#%}TP(yr$agBFn)u~>uHk28pt6h(NjE5) zvulb95MtD7bJoUn-$GWoCI8LAyFHV$i2O|08LD;sF%{CLYeaHZR?1*wo6zN1xqSYz zbne`yaDjEHG!UezDrc3iJT!RI32+)jyeOX?w?~X1J}TF|HUj{5;yQM{aOBU!pT%L| z!Z?Jr7sW#bZpuLUP&h_8@|)vMz6Ug6Wax^dK7lGt>kOzgZ6 z*5At~7{SC=Nj4gmRT38;e*P1b;ix=*_KIayr{z~)JY>WA^)et4^D~y>^&Wm+-mnpe zYww-gT;4z_1NUn-w>LlH-tCQY{_CHkL?4&;@Bgi=;8`xBAm=t$OMeqrn6hmvEa@_t z^6k5KBr|o&Dh7W_s|FT!Zo+-YUW|xi%B!|nD?1`WxreM;Aqu%n8BGlqS6Rq}CQoVf z!iq|CTj3RO1h)B-bN&V`ynDMBLKJ!K`oVZ{qfs7j_PEzjDsLj0>l^D_0+v`sj>P0= zfvu!oZSWs}Vo$xpqw?mvr)2i-F&TbM@rD!P?>EX#kiT<_@@ov=1%#7wOQLM2z+SZX@{)d{c(9p2>1${y*IR__b(o#w;COg&T9xW!Uo)F<%>Iow#EM! z@LYNrD^)>cnCXLU^R(GfiCL3FzH)1Gm97p9vjX9NN z<%x{R2g$~??UOVr?V~sm9=2tqDZe_0I^(TMZ?%h=(g3cn*^Bb)SMOO*5Bw2W8ZItV zI7V5#fAg-atyBKdxqIMbvYI$k=!2OEa-VAw-c-w<2UV}W(uHG2bO#2GQ)D#o)}V|5 zDvIb|Lq~d(CI@*mIQ0T?rCjoU3ruLJ=0=p1&oL?{T%ETRej0+lye~yjqF5bcT84hW zDfskBMp5F$(HA$(c8f;w4-hHVPMOYY_6!}Kpj0XU*=21b2{5fq<^!+Zpj27!enBJ< zI72y1E5SeXM4GBoJo1dVt+K(VdsN!2@QAVxeEel5Yk`U5@K1C{dGaCM_bLV=7E;|V zpP*xS2vNejN?n-N$b<=Y9-*nwRW(#%G{(R}-oR__Ok+hh4J(yJ$_rY=fgPz@OeL8? zzQ8zdQCV$wXPYq~OOZ$4JbllzfGwF?Yw*i$WDJ8Z|MkEBxxA(hz$W<+U*gHmV{`~5 z3pd5!>gMz~|Wa*>jSJyF)43L=P zGDcpzC7ljq3q^lUe#<2^+?gzv6P`h`Mp^HZe8(Ye#_-_zRV)ZqVJm;k7=$yyi#Kp| zzxn3-nZVHN#8S~BA?$iW0eI(CyibC(0e`PbO{=CdZ==Y3U{zohskxDpKCE`5KG6Sj zEd(Rcq-`O=N!zwvU!&mxm01o$+zJ%0GZgIreUe1qJlpFfMEj`6rb0pna;Q`!RVht~ z)_1QnGx6=S7v*pN_Qzx-ULdp&F2?1v8$;HfE|%>LQouLX*{KA?KxgNUK0o*`a0XxrFRfg8OxIv56^{atGb(2L zsIlVVnBt&tP9ahSgv$_6>U7R`=SXE7?Bi_BruCf-4^Tv>>|gi{W%||c|HLBv)AE!1 z56T??^qNW{|LM(f`HZSaBa~#HvWTBkmFXt)k#CmH%hv}dc<`6h@?{?$#$W=_xuI9f zC!!=>k}`6x#Sj>;p&@uIc*U|pw9S3OMzB{ZnEfvSaIz{SXp8Z?FdO3$!j_Pr0|CLk z7I3i2Nnk@b#-%=QZy!V(dR5o~#Rmewz8c<{Iy}>|Z=1#-ZnHv?RZW#GpSI6KxuAb8(65U0MW< zn)P_eoH|UFu;ReITcx8ws4}YU0w8cpNSW(>UYGW2wm5y`cS{bcO!a1~K{ksT(HHw6 z{Y3fl9^uS5oaS)M;V)>)_tYr{HWQXD#6z9Yb5}2)u|$c|p5Z&&G#YVoyic{r^YWWt z{bQCLoDogA``~st+kZoH0cL~^eMK9FBLhi5w?XhOv%eG3P-C#(inl5W(XJ6{svRZ0Yk2%}yxVVAlCO{kxtO^DvSj*_x%T(RQjs2VUovS@Q9Lh<7)bJC6mS(d{{AB;=dF=C=O zPs$EwPmnhgl`O~*ASQ~m> ze)Eu>tJWwJ$lBx%@VWj#bPHNoqY(b$4Z`JkzWQBVvZ--g%dO4d;`t+c=;H{*Xj#z# z%5n)AHbQgGqqw(sZ{dlxvg%&;b`s6zDK?B?K!?Nt?|f?7X)7G?kOU%&-%nAhVl7KsxDTKJ zKfPQG3|=Z^g}cUs@4h??+npK0U=gz8G?9vRBLeUbQoZF0s=x7hsh{C$eX5LF%yQRIe%R_)340DO)Ip$d@<^arV*-0~w z#l6OX^x>Fg`Wh;=kK!1T3{!gEa)^OVev@Xr2kA>2ury-_&-A260s<$>grpZk>;;3M z<@_vVAt&y62#iKrx{PH2(ksLff@tCels->P2#N$wgqSJ%W0oUf9MDNUMg^DfR7q)^ z__+%2ZaHPFo^$_6f6w)1gCa}^Fs}+gnlvnSPMu>7CA}yQds2lk3;FS=U9TsDS};J^ z=m}jRO!SmAoE*Q0_h)kh1tt(k|G=}`ktulXj|%0AkeQzqJX6MRyM~bsO*%&BXVYl# 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%0DQ)}{?)>TH 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%0AkvGF^!;nVFkulpVi2<=pRffw}_ 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

O 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%0AW) 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

7sW&I`)1|93bOQ!|A10G`Ilcw) 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

kqQkvzxy(n%Qumyyz}Y@N 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 z z53U=52WrM{V~hC~<&|+PnZJn9Tcd*VG4Dw^3BoU4A6ZAaqEINe3-v|D5Mwdv^40KY zdC9!Gnn?M66gbq&m>0@XbL4ttWN0|$Ugb^9pUGd@yCk+-jAcaV7RSI>oI`{Iyt&Yk zbnD#Dwo)hNMNv>AN#(TFOb-OeFrN%$eKyKaboH@m=D8-gE2 zc1C6f8D9*kHb!m&D|#Qxy!PJYu6%%T<-iN;DI&fF!9P%OrdA=k!+%O^A-4j!f?qvq zTv=P?bBd`U&J-1Q0$vNZkK2ciHG%BvlaE&;btVedq(j6=6fKdXmXmI=(M#Ms*uKa_ z@xx)9)XwWnBr|OGG$C+rqVb1JK()#%FSoIHldQfRqGJ_VW3vB9zL zIeqMpNcTDmG3Dr$z4!I8;OkYrqkWPIRZTB04&>aM?tP~_6R@(m-hPP%xOC({s~C7$ zX;iZWukqT~FgH1Ruw_S$4c)t{3$PBNa9QG4vIn~XT*}T2eDg%{M>6gRm=$GbNPl0H z9WJfTL-^FVzHJI4SI6m{OS%uW1^Otyx6$T^pvhgsVbF}Ge4J7L&i7W@QB?ydcQ`8$ zzlTHMn$d8q`h$EmaMflI-hwy`9@Q{?m=0lr^-rQ2tZCdWeTy(2hB!^jQd2 zg4aD9wgqP-sf}#R{P1%$)RON_9 zmGMuP9TAo&$h0T?mwnlbez0A+oHY_UQhKNvCVpv3#Foq`;$z~E3dF}8r{0FQI%IU@MB}UCDXHM8 zz$+XiHdV~yV01^h$~-9>lEk}7d*b@B&uN+xNCj`9u7%rVXJ@T6>sl~)Dk`9}GT82V+MG$%Txf#tYH(u{R#aO~?o>NPs}hKd`7V2j|hAsCwZzIERD z8bNM-v174&*~$H#Gd44q`X5ieVGN!Kr;IU-UnTXQn2>Nxw^vM0z+r@yjRMd| zF|KUNLI^wD#zFLo2a0YgpTBYyAPaZ}f$0vgr*_PR?ok_DRziPk8DaQ1o265X1-6Rn zf&qTe?l+9u>OO{N_d0yPrghKlNgh|%abB+FP4_`>GRW_dS zwEl@}Sfn%K^ijkMSCsLKE1ouh@@qcac8_tj$K=O~r+Dk>zz2a+#DCo#zF~?{}RU8q4XC50*wLRj+{SN))<%JtM^nq4*f(UG^Y9*OFC_vz2 z7x+F`ZIq2Wb7HYYI{0ctXTIUAW1QFudt-M zvlsAvqt)0ffFJL7&xS~^3b5V=ueh1IsuQo5RWC;`?fJ2~oJYRW-pf)hk5V5q;Rhzn z?orgsQO%Fs{?ooi^Rp0`;w5=$#5c!6gbsL>d1Uu-U|%#puMyu&MHe#U^cRKTaC&Vh$JfI|y^EVu)r+Ukcz6U0rT4XOeSIcaP+Dc|=`{K-%8 zv`ZkelT+w~h<_7t>pt;4v;;A_*2=3EQJa^1&59l={fEBp8Pdh@n)a`Jw-Fd~fN@$! zoO8+!q?-0F;VUe~{HWVn{isIh2!}Z=*j)DgR^BqwN~qsN^QZc%Z}p|X6FfQ+CjLe3 zTe*d(zlEx`9_jz4-a~xv2sr?8{!m$6<@;)_q!XlLpHa1C8I|FIbAW{egtPmiQJ5Pd|jm zb8Y`e!Z*|f(GVh_E>+aat;?p-%K;XcqFnU_EWRr7`_MzSpdIhR$|y%`g#Pgp;CwWB zov5b^kBU?v{W?UFoy7M!qouiB&en}T&FdD72cz12t~Mhm<3VKw z;fkUl=jUyDa+`WlN}%S$huZ$R7rXEy#11*_Pe}?A;2U1QYl=r0b z{pD*L`#WJnYhuHI%Zw`5v?$f#(SEsXFoK~_^E}cykE17qu@&iBbd|4Ew47n#mmvRe zT(1D*uO8L^;65G`QF|=76ug-+wS_+8u6XrK=rg<~yeoWy`X)?=q}k#?j5V>SGv`eJ|h6Ae9;_beidGk{4eWYo5&46?eV9np)PbAjPXnn zl+^QfW~d0TgLX5jv2Nq-GJYa_OM6TYgRkq(ZxQC5+r~;Vzb5`)=Pa8f&3xPZ(Imgo z&DYOA7I<8*sKfBJG}|B5IQ0r&b&4@=`48SL_>O*T)Gky7uU_TSohvQ_KIxlheM$OH z?Agv{*uwX2HFpW+msMqje<{x!8Khqr%8$uD!v<%pyE6+q)E3$$GHdXCgI6XGx;Iv+ z0_HaS2P2Bb^sBk^YV6DKulnZtiXVWde(IcWJn8FuXJzBUBL0%=0XJxYs32}@>*Nv; z8mFOXIXPNYUwZTp{lP_}4X_&SI-nzOu$sgni)QA#-xjoDJl?m%=PL01CnPb-#YU}< z{u%u9JL47OA{&L5V>cz#qW(hX6VU|VoMLccH0CG_{WMcf<9;FFf9p7dSZ9YtEJrfezjH6rebm zej9j<^O7fl54AtkEZ`GbwdkdTrHoi(%30(&#$4*}szzYc0KUD=LFT{O@5bao|EAJL zInb}_tj!MYq5jkEW8nbdQJknWB!6SCrEEC8Vmvlp2K-mF{*8ey{H4qa;-&|;vW_ga(=h&z;FF7~kCeygnrTu>oVPo@V zwZaC{@1cP;#V3KU*vO4+=C5yW%Ez(HGksxE8% z_cbc2$icNrJE0f0yBXZ;*4#JT;jRHbpF^Y_46LZX!Rdg8a-^^7OS-YFQuK$1!rqJ8 zF80ADTINX)6bi3U%s<8(W(V+o*E;U&Lb~F-;n;$9rl^Hhf97rXm2;TbhC?I@L|4mPl zc6!RWCr894i|T~vPU&@Tu*ZOT@8CCz34%teqZk$91*?{_Xz)8cl2HLTiL-kcAeS*s z*_rbs&d+)b4PUO-8YO^>!uuQ-emwr+bNAoidv9#77^v8useMY&5bYNjYYRHE(7qX& z!o-}EJ1LJ2Mt;A){yq(7r&w8xaW%iy)clWfhUCl?zdrH51vbbv*@6WRpUIj)JtOU< z4*5AwS}cNrr|T_+@mfVFvm38ltj=>PeohqW;=X_pvHqE0(u8vok!$?idM2FIXbbq7qc#M2+aij>CbvRii9b%5J7GQ6YPo5L6N*;7Fyff5d9wGHH$DYg3Ciogg{lI zH^q%m&c7&WVX*oDHBoHn+$^3bSqJ_r<0*@Aa?6HWMwsxbs&`Td8u6+pIxTsc07n$W zx5JE|a=cc~SQiG2?Mr85brBe6MKxocy+d-X8NV$h-D~7;t%{2v#QrR5l&McOikO=S zm*lX5e(-wwuUc2iEvgbk5Y(D_OXP^zu4O0K1dYq46&?Tg&@jou+DhM~KTmr{@yZp9 zRxgR~?o<*!Wz-QBzIxkiZbF~j63O%}BEQCS#GMFt)Mw}?N!MPb3jOiq#V-h}nq4mR zZ0K#u8{l+wZc<)V!zVjWO-B<)I+a4p<4Iq`TjuQ`-E?oO{Ue{xYNJ&j(p|JKs3aKQ zt9j4otj9*zvnt3A+uE-Db_d2~#m$=6DL-yevDCC|;@!Ae@sCp8F?)n)T)FH#OWX#y zBfLhUDP)^cTIho@?IsH#$E06Jk1(#OlI|43Yg9&*!CY3yv6IpCV(L>l^71b-v*TW* z+?SY%K>ePoXgo{$4w&-7fS`eS;=cwjH``q1u~y%`DHA`kD{?3dBX){;gY4hI*D_;g z4D_!x#RHM#PuKp82>tx1E~@D;h=E3Ne?`*Ia?XiOmzUu*kRvpGtkKWUxERCq=q+0N zS$C=!+e0_i1<%3q;hiXKsX$+d&?v}Y3xd^7Ef)4mgTCiXgakSq*FU+*b3 zje7n5XDrHb5+gAY;nNot!8s)c_ z*ueMe99n!%Ju4zz^XgO2W-T^+3HC8kYgE93q8w*bA@f`6boOqG zMj%{?SM4Z=N7*M@PY@sCT1*?lecaM^d&c8G|KXAqNO#LfO72g)Yk9s%3hkT7wv~49 z)YWvI{^_IL!R`>kr}Yozc9a@#A z*ShijD@V^_{IHl=Y=H+gPc^mw1uxaTJU1RqFe5mL^T=vy?c@GsBe&-ssz6!oL8gtRm@FyE6}e;(B0Etvx70jdJTMjtM)_~ z@6i0vxxyQypXV(xcG157%}!uIpBx=*;8`pSGjf6}%8?013f-K)8b zvl+4%Uq4PiCc95(1*w08bHJWY_M5tHcO+j6?PsqA@Tj{}OaxQ|ZJ5|(somO(=9}c7 zr&ae8)_S@}Eym9)Yj3dk(C-SkWTw9CO_3UT@Gr-7j~0GPzB;~bC5J&TiS8-$ARDZ1 zWpW>F#Qb>1*X+_bHYg=89r&*m>%j0Lp@)ob>eA%*a%rD6(BFhZNUh+E>&6kEeg5(% zH)vnsZhf!*GxP1u#H7G=*3~XW;*!26zE!9}(4l>As{Td-d}5#bpAG#XnExx6sGLM+ zvm7m&sd{h!CB`M_%km?@;k>19_#3{@w68jp6FzQ_4mSipf0*}B7O;_bxnB+4?8Z^` zSmF;E4UF2PPxhV={$ouwR{MUTobt|z2;=Sk?wqp`oY=5$mi3&|hoIzFJ@x(~Nbz!P zL_wd}iVoB2(auHo_RwL<-C+(l;Wv5~k9D;GuOCBUK(vQrq zgS<^1h(i%~0z*tZ4fCig6Op?w-|H=C$o=3Sg;W^)wZO@FNc@RHElj~yYRvw4%JHS7 z?oYl={7P6{3h)LVg17L!eax~zPhfW7iP(=o_^Xk65$e-^h!of`{Hz=IZ)L~;cMSg< zo=v@<6fBkN{LXdPlgAgevHtr;0r4w6AA2Uvex;y60rlA<^6KR=FnC|tYp$bT1GHN* z!Myj~%W_7m|Eh1(87S`?&+kHrYAWKuAi{C%dHw|UAFA5dpv>aEqUItzw#!L8;Tqug!A zYX*4NA6l!T#*0`L!N=KhIa<}G;Noro@f{ft`h#Hfv4Y+_h4o-M@H-sE zZY91mmO;aS-7(n(j~rP)sqJN)>c(D*m5DQk{nH8HH;6RF_}&?#a~K{f@Y`~!0`b=o z4I|U>KPi_Zy=gKp%<|R_{SEN=*7X62@q}0VMR4S#xr^0-cZECe3h&y25=fwr!%Nn#u#rC{?dDDThE#)3vH z1TB2=`Q0dQwUPdiaWso+?-ssA{xs)6a2NSEyPLF;gum0;2bjO=XQP3=47f)5Eash6 z&uAM{mG5g@zbrW1Le;`K2t;%IYy3}^CCX+<-H`NX8a5%9W6zn3I< zsG>z&=mtB?I>I+OQFvF5evNwehI@(Bry^NbX*VYXZ)Zcl-p9_Ua5~{njWP*6DEAZd zLj(G7$2vQ#=ZN2EnC7FTb9rRDEcNYl{E;c7+r}fKx!|RevBYBhwe{+loOF2bQJH_? z!qAs_iue_5m-0!sRXuM&zgGXqncGt-x2TsMVZ0-gwdZ0;AFxVPt-^Sj%MxbA{t3T1 zoj&+fAC}ioGWIyXQSLx*hfe!^p-@^L^smJSf=QF9=W%b5@U2_M)yM7xaAVim8W{oq z@vBmt_|D|pQ;*q#T~%hl!x2A22tW@J;7ketop_AougOQ2t=G zMl|Z4>+W~@0f%zcr2x3m4Zx}KOQ$avn@!uXd)TMJ?*{X#{wDc9Hm0a2D7Q3k7uduf zF~0WyOZpR7Pjv;};4M(vt$(U-)uA&x&t71EO}=Y-Gd4eri&4|a2G+z;a4qn$-8^g# zr+vq?E-v#;b<@8#^MFTqe+7PI?1@2CVhhT zdi2QYE~e7WNFTu8mf&eKN@MB&O++{d25(!HMG#@xFDn8Ew;5}xCxLJC*3y2;{o7kF zj#t^e7RR26qdDbM9}*sb(u8_^M>xYS#)SGV?^s|f-=B%O8qA=2+ghft7yM~8+|{K2 zR}GfiK4>VOz)DQx?W`_h>%*Onmg)lW7cs(yAUW4A;B;Ix7<*B_B0v+CiRnkwcpCU3 zdL9@tcaVmN?FRzQi0I{lYuFHhFHWM<#a0VJ;bYMVa%Eyi)Fx{q;)9gq!<*O|V4U_7 zUhy@&hz%D%V%lDe=+6><6SH%nyWTvFQVlqq`cj{^S8;Usj5P__iS zI#x(@g~NdOTSE(^p%?0o-GC-9$wiITOCw%ANBlCxFd4U-?s-uqt8F~a`ZD=!S})~~ zV?hx-XJJa547?7Tiz8i4ZK#YFyXR8+G?KsLE^+9m(<<_m1O3Fkk-9mSiRQi$DJu?F zeNVnSYFndB_~;Ye#sG_Jq}kbcx23#u3`{y*CD*PMW?JWqT#y3VpXfopw*~ zqK{IWU+a&^lTJ+X7B2I~3uvdTvecX5z8(>VzScQX_z&d{KUc-&Dzxeh??v_ zPP#AL@!~&9h1?c$*}|^i91bt0{Ovq{4ny~Ew?@ZIqJOcW-#IkY^0V#caaZW~oB9=5 zV6c!VizuuaUA1ig1M;6#yAoc+mPEH&KYW4j-@0Qn8IQu{_Dejzg4+?_-wEIg_rFa9 z@1ey;PPj36K5h54|0ceZJKBMc%wae2*ik0L-;c9+{fA*ia9MJX(7{-n4gsSOy{F<~!6JDu0u zaS`YqE~^_=fZy^KBw^eq?2#k&*qp%Zgdvo7Tw9wro%nB^o;LhO$HAH^M!gF-T&5g0 zDn;28Y5F1kq&bNAKhZ=yOL_k~JH@}3N`gySq&vXut4TW>h7ZZpCZRZPV?ECI9JNg} z(Z+r~+hV?XvKH4oG_#xWr4PP~O8RwI0k689 zqwWTp`WwcxP)2TO1FZL?d}nGQ_^@2B?m6%r=@tq2ZV|qy7x$PiP7ix%IFWHZT^piCI20^kPjKx=5{r8g81v!TCTxpW?g6h_0rVa*zKQv0I1Y~7}|&5 zuN?{9D0htBMCJ#Nou`=h!GANWw2yh>{Ny;I*+XynH8Vg{^Hubc41W7_Wn3aT;VJSY zl9^(pTZ4hUj8P`(3{&ryz__Td*sf-gPLK2swIkf$os-Tu>!m}3u2R2sM7LtDk?&pa znAlXsx3pRdUr)YX{%Q>TKUh4{-UJ`=Ae#xD1{!5g)PEt}2c;4#EkScR!j~mBnJ2Fv zof5pt{2lagTYhua*n7W-2N^>B{*ynP*{LA1o5r-Nhp@TA!2f-1Pv(%a{{4nxS zAU!6PbXCo@+7ap*=AUD&q8(2cd0cF5gMG6Oi_Z)=V#yN4 zfMZPlQuGWw4o@KrxEjH7wvPsl;IYy%gkRzsG36F~TyV9Z3-#WNw2XlGz_7qq76k5< zbq~ljcy7$SAcBi~haLJE;){FhS(-JwruTshg0wrTlt{!zQ{Uj23WSd!!W{%+Y%K$d zTJbuESJ>X*kTijCOZMASfPJwvyGuJ(sE+DU;5aOa))Rk2&xsnn593nicJjTd^${af z^^vjEfbklue8+rDOtfmRMvb$%J?LE{Ulna=R2A7vbrThTR9x`^!`#1o8<@j$p-d!i z0Rjd6K)PSY++{7?j(B5gyLxKJ8nvf%Jho({Xw1mz3f=M zx3O@dJnD=UcL8AMH*W&)6D;i(0GGo?F9A~!Z6HuuS3v|n7O!$qo3ll`5OmJ@Xg%#=5P4C0n@VAs*QhFFF`a|hC*H<%c^Aqx8rMxe!J%J3;?{^Q{ zOw7m#t$k3&e=2Vr{6o5k@rR%M#yHg@O8-r`R|B26op~h={OGgwPc87G@71d6eJJN$ z?K2S^#Yuo1eIP#G70s1*i^Ffm8^!t*oqi)07Z>`)Pw~v)VHm}~ym;V1FU@0)CPMcv z9@1q241N$Uncsr)lT1f{j&=mRmVpM8`x)(gg>u?@-#Itv*All_+9bX=($0vDg70fR zAuRp4tg0#K7n{Unxn`g6ogR=4lhIB~6OC12P3LphAl-*hJNzQpHPW>JJ`mm<`6g7F zdVNNB{Ug42@O~9rEv*K&qA*qr4(8zzh@%Q~of${_&)cWNx!^DD&LdebkndvAE(zdI zb&0DRc!TdpgZDzq`941KIV@d6|20PFjK_-?iAE1QnIgt@;52QaUX=JZocF}0R6VVp zaQBja3iEL%-%IfNCgWu0>G?)mzPDG+WZ~jXM!!7UV;8zX7rgSaTnQ2Au_r9=d`7)j zy>}weqh8ECZqRS_9!K{uU^Nm!5z|Yn4 zs10LJ%<`cd#H@l4^#^N&>dNh3UJ!jpLFeJGBBkZzfd{MlM&M`0a^nk~e(Pm)%a}*_ zS$&*o(63rfju?MCmz^cfU*xaDygx<0E8aPTc#bT&v@)Fj#i-smOwlkQNs*12cg*b} zHk0~A^{sl9?+tK;OZzywZz!5uZY6#nwf!8R{xo7}XNuQMY`l2tWNC!;$V@XAi{GWf zKqxgYnfA7}zu*R@=KSSxC<}dE_5YIq9UB+4gm5kJxH9rgcmw4wX0<7xou^e9-#X~C z4Sa=p9Jr;ub;D+S)`w@?(7hS!><^J%tL!X+bE1h*kNJ84JF9c7eBY{8%J_%pMe2v(mtJqTJe*524jNOe8{}(X)HE4a z-Oy$l2Pn4|HT5Okd^Y+*2hXW9n2%_jFmxsazxVPC`iaBoN)h;+Rl~Ud|5!Q?_$rF$ zf$!ekdoMj90Ya#u2nb3?5Nx1Q6%~*sC<-D)x*(z`C?Kdbm8Mh$MJ%YG6zPH}MXAyy z^bQGWuUz?m=YD_QM>o5-b#``kc6Mgg`I>t6=qHMSbF6jrwpjluWOrX6#^ha{_?{$hPr;vPMBUSbrd!N@a|2*Hj z#p~ile6ML_Sge!Kgt&}2Dc95)?@`XF;EasWKlY|BMmy?##@Q;yHXJXH7B!^YYu+{4 zZ>>h2YXUECa`>;gQYv3H7khv$=TzTF`F2k<{`ZVe6N z`**brxL;J5U*Lj3IHdDOESbEvu;2%d_ZFQi}5F0rQeQR+FXesro4 zewp-ea1-fI-AXHj-s?TQ2*EsS4Uage)!y=8vb~rCc+&FBzM~jf$T}hens|U+P{C9D~hG)+1ENyahe?vFu7@EGQtq5Cpj<9&*?pux#N#>; z1f)v?{i~}Gk`pEn`O8_cPJr>SVG)=S*oM}mH+KJVw-En^yL$ojyK1VpnDj5P?t#Ff z6u!o{PFoMUY$g4t|1nOgDYBM#`TjNn{pV?Ko&Hqrnh~>3$Fy4+^^rKWC;m1*?fVnn zg7IKsnZNKt*bU&(!$=L>#mQv6Y86dl;Z=01$sPFR>_`d95x5HWjYl&(ULrHBKj#+L;HmI&14% zgbkTT5ElKC);?pl0`}&6(pz{Bid79KC}eWPGaxr-spl%LWiJx%srpI#7+L=+k~*&E z@`0t&pMaR(lJ7q>;K;ACVaJ&-zL)YPN*-6I@~;TFw5#CFEG4dV6bJ}_dk?}#O^#`hl zFwEe}5Krp;!kj0PBKz;y%W_2$oD*+`>VvPz_C%ziA_1659Vl@c(~UPMS3b7New29a zP+1`?!4oOR5|`5Lzpk#o{vsPfUok!Vm_kSha(F zh&Mua&0w9GZbOlnA>kY(_@b_RUk4@dnKJ)) zzP5u;QSN6oh%JQ!ky}OlxMA+y3xf=FM~nnvu-!Fkp_9n?E=0{@=t(rwelqeO<)1b} z2J6zBW=a|6h3!X#1N&U;MF~ZOUx&-IZ zcNP5@|4H;OP}?jOWWEEPtz7}&>8^GySO#=dj5Q*$)f;hLJA-yR$GSzCXBf&Hku2z~ z#xFvzoL2Tr&Ij~+Fi$NIa!}syo~FNbQ4bgRwDfArK?m!uidg1xpwN$t(E|tiyTwY& zD9u(R@SMw=iVdhQ>U?RhBL0P5;$Jx(<;f}5SM=fN{ZabITAdc1N!SbLLeMLy$VkmP zj@5RWi}6CBvh`kY2>B1F6ZQq#@8#8wJxu;rdFul_ivH*{61)su3{^}RP5M!b(zofs07OKK-k!iOp0nhi;38yUISLl*L*_?UK=gxfE zN##!ILDJ{*h)q?(2QZ&XBfW>=1jjRvt*oLD^*JZpDRw;dRCWhDiQVv;R+AXWLg@2^K z3wSP@_T4eX-`p9+_Ysl3c@u%#uEN{4=jU6lg&s`CL(Au^`~F_G30@+QI1^yq*>hA& zp=<6_MtPHQTVbzTl<^u9oh5|>B{Z)Wb)nt`_Ja}VL3paMC-S)TEAHJO?d#6LK}oFV zK>fgb!87!KhMVdDPu<1K3M`}C@7`4BG1^%ceyE83%IIwN2@1Ry8&l-U()q|eA^4~U zy1%)-slOU;@WQVI(vpva`q7_(?6%CS*W>mt1^-fS#k4a?tOE>Dj>`ew-R8x))PqO* z#(FRLV{h}QDjQYtk{g|F7E+)uTPhhsgVVFpzXDN~C48AQg$fplOn?eLwg0q}X`nI} zEPs=K7guIw_@3mQ5Q5HTyPZcW6wJe){#8+B?MpWFQI#MfSfp0PzDU_%+XSJFH}s>L z0b=f1SFBfxM}!wgD{>+mm=;ZWjr1JnCmART(p`NWrS>f2cZp}<>57F2lkgXttkN@$ z)ne74Cg5>vD(#nq-J*X?Ts>bxlE*pfHu2Hu>UJlCb`}jT-267PTGq{9WNSJ}c(K>{wT%|os_&w@4JIHqsR?H#3 z8D+|ol%IgB)7hlI1lu)_^p|u~U6=4H*e0nbgr}&Ighjwb;QKooLR*Mu>Ry_O)8+r? zvLE?_k`u5RYBzIWG$6WhuuRJ8Wc}sM0TJG|?~$t#?+qX(?G4hY2K|9a8zGDc%)nDZ z2xzSsl85MvbK@n#yZPNq{+h62LE?!pK6%8)V@B{H@eps9Nj;s}@MXNWkWK394-%}+++Nj6aIotV@JY#wVfJJ3Wj|jLVOjHvq${&|NIks z9;0Ghemx<=>;FH_I>--e6JN)r@>R+oQ5TqTj2yfT;_;f=e95W(R=7*?e{^SdC_65P zZvGt_61*FJ?Y`S7`P1oav{d;LOPH5+UL)r)38(JhM-e;(RzDXFzW2KvE z-*Y>}j?urHI$0!)Mxss;NespWgI%ewGP1yyq*u``4VU}_!QeIw{||FY{O=H&5$*;&<`%0t2@kZ z^c6JDUL)Up^I_{Z`dQYU?^~Tdi&2W;|1|e!-+^sYy-Aem?Z1j15#txUs&#Q#F9~bY zONq5*)Q&ZY2>vANzeSgk16C?bIy#s*;9O34*!vz+dF4*T^FH~R|D{HG(!Wq`jqXV@$OE;PN(CTg<01 z1rr14x>=eloD<|P87MZW9~A6-R(RtstHE6aaO}PkWxpCHB_7z5(W-k){Sz23;b!m9LOOZ z$+8#?#md^x+RStCK;Q?lrgZMug=keW&-RUIO~#jN{i{pUZXc_4LNCI6^>N?<$mx*a z(f_!pgFR|(hU`$Vkr9wU4s{7)|HB6@)r81APZF=tLfL;Be|ST(s0?G1Dm6`w(p zHK5$lAaK64^n$(x{kYle-xIqESl4UQzAMRiqZx(Cc>(&fO4s+T4HtUlHFyKtxFK;b z+y1oDy({zGz7XtMvIp(P2NQibw@LV*Eck@G>kM}$@c1Uy*=|5RUpdc-;byCi}OL|t}e;0-W zpMT9#3GaaCb*wgl5_Icx7$xu_?oI2zz*XuQ%$vvT7on$9uev-VF^j@I>f%)|NGiSB)GxZjCx{-YyM6>3{k4#FZ!ojKK zQd!WPW4?9=(NLj1R}hiyY_)&YaznQk{}1{!2A51E#>BQP6vX`N1W{Gv|6Q@EJs5f( zB%Y63gL@g+Ak|;)#Pu~C1h9aR?9>rm8MVvUXY^#_Nhmx}Bon!b3#$$wLg!EwYa#t; z?H&{5c&v$@G1l>YFWc$wlnX&V*$`2bKCF)uKZcO{9N{V0m*u3_@L2{fmths+Nl);9 zN@ufiv%rvQG!jXhw-cGhIPx#RH!JNySnd-9V!Wn*HCKs?qH;`R7Wvr_*z8Ycx3yda2Li^OLSVRtre2q=1N7d0HX`bBs5B|T&D2UXc0&t6~R z(433U2gvsTH8&$)Bd?w)^LvxgWa&+~(A(>QAUcD;cKPYkT-xCg?2{%482QoJV0EY5 zHr3BtLVuUSDhna}4OW*03`{o&!eHI-t=XA)PgUS6k*2Wy#7O^wE4Cj<|I6DY)^j+I zC@+#K^s&Z@=+l>Qkq=_pit9{P7Hm@zp~=V#NgoDVK9+bnl-EOqF`#jRur$zPq2mnU zvYhzP2^u0IdapfXa|h|q@g#O{^5r2@l8y2IB~xG(vS+@_gElM}=J0=yI$uDYK8JL9 zw3+cR%QeM1R8Q3JzzT|a_7-2_b{zRYKfd#X1=3b@IFN8bP+%;51{Oy4e+*natUB`p zTZz32K@B8Z0`~%Dp9O+d-cZ3`9JS8Z;H&M0w-@KIUb+VB#PtQebg4aQEm7H;clNJK z5pI6Z-5F3=jb>`3OcFN-&J%W77ecDvHtGbrk>0ApsWK3b+s~fqmcxP!-!R?`^=92= zmAbDac&}SVpNmbP9XyK56&5BP-`E=fb6*(NEaLy@W+wGwu~jA7l<=zes?CNDbDSRl z?rcyA&gaCxa8K)N)HmDcVSPurmRQ??*VvG*aIO=+k1Z)1fZ2^V%)ld*;zvYJ(VsKX zPQ|ZCKdero>{)m|Rx;&n@TsZypg8=@cqsV6ZN?ivGGJH`G?dT#2FrqfgX}SRS0G%y ztoaFb7|#Zyy(o2u-VBtFXP(frDPc~cofh6M=O5xfdi{;&)Vu9}Pm5SN#uU+65*yT|Ncfd6_r2bTfF7dcOgk~DJT zE3%&Pe%4eDd@!H8<;n~js_7bh^147*o+teV$1owVz0CR6R^Tzo>|y>v*wJ(Xv#9SI zJ3*{wVelrVw=9KY~aSs6!U%RT&h5o*TUoX}l&N;r-4XCe@suWD&dw+Mk&=*WU2IwH+0dHw& z9^)A1HpKCZ!X`A=1+QYuqC1`4%;VHpAMry4dtYGmq(AvQYft}qoV%(pf?SlZ_VH%F zHC`D1KqG5_W}RAR0&9aGGw)Na+6mAJ^Qaj%SeMZ=xFBYpdEcR$FtpY%tGV@()Q{=V zPlWfVAKi)c>#ADhjiVg4W0t@<=49Hk4jZXzV#UbQ+zSla@ycT6(3)^Nj#X}wexLeD z7(X*)ykOiQy|+$sFA`3|Tw^A180?iXSRbJS!PNoQk<%G1Dd3B@qQAtgjx)e*AVy5Q z*YTxq%rMC~V=`Vn+Phoa3IOX2cO79%wbuZj-3i_+vAaoLujU7#V`{N`R<2C#rI9Ix z*~EwIDMkmvy6&4X>R0zTuZb0!-P3(2!n`9N9vj&TyayX?%N+wfD!bjwvTmaLtU;o3 zfIf6zV>I=59nwQ|6-pW0@$qQ1q* zy1u2J8aGGfE#doyuQ8`dhz_FACK4KoN_hu(JCJBo14(E)M3hPG1BGcAlkj?rCgf1IsDlE zT{Z{B?U^+y9c7#Ck@0nQXX^dNdjxeJjJk}!%^GBUK9m|_ouScp%GkoV{@|{I^`(b< z16|e|PG>4OO^C<5uVOOqC^Li@A2!lc{r$eN(Mt|q_HM7Tv6J*9{h#?Y^D)qS7X4IZ zZq_kz2VojUQxSY{CHk4cJn2)aj{{z!18uu)2-^pZH0Ix(<_1M2LRVFbE`nD)boZrA zIpZx=Ih;&`Ws~(bY!YCfzzDLk%r`} z437t*m>+uEgE1P+#oI$V<=#ZD#0I2?c=M2fDbveM(^|o=Ac-_h0m+c&a=xyP(`;){& zob9E41l7V2;3@khMS1)t|04Zidk(Uhf;s=s2-HG%NG33q?H9dR+|D?Iqn<>h! zNQ7dt;bPv}Ud&3_##ZuY^#bl62Jt-)yONfqx1d$& zS8bh!>_s76IxP&AS4b}et^sOJAbuT)gP)jHSNGj*^hVbWyitX;w~cTraGOWiMm;W! zsqP4#l%m{zwG~;G(sv_iyTkWXShOmHMabU%B^0?>7GWABly>NEDnFDGC=bD7-8#M#>kpCO6acnv5URIVUeS*VWCKAuo z7Yt3lCdTQ2OSlE?ho5P8lJ{tFfY3%cdl=)YWlVO*LO@@1w#$`ixQv=y0{CYQFniv< z3ZZ|@oNPbO^4b{7Fr8R>5{~2jUY1%n%lr^9~ zTn%>d{isDEHSJf(mzMN&TvdwSRHt}uOX=#^IZJ(Oyl6XSZ^9udtZeL^rL0G>lTd#Nu$ z|NG)hr!L>MbH#j#`ky!c6Nb&nat`>Co%3p(IgNVG+qXqkA^+R_nZ+-#4DWVk7q;<%DU-eqgMPNBwbC#UV87#eC6h zYEZBDA(vz~fI|rmQkB5>1y-uS(VGS}p`T6%9Atw3x*g_Nw2OPpXLMWg59L7j5&2u0 zZG*rItZ8k@ykt0ioI%9<8(=f_oOPRX=u_?#{K^SEd)Ld1(jN49w*|hq3*5TS0WyB* zn$9xnH}&hWwS?34op|QQ{6oKLQGeWY%S&kozIIUcA|B)GYR4?r16I2gWP5Qp80W>U zihEr*GKbRd4Q>fsswj6SW<6^N@3@OIRxw`-)aEqk16RfG`&ZT&sB z{e8yVW*sB`pF7B59N`_Rt$PG`oK>%h&a!t#mo1JF@9I4s1yADolzP$Zs8n{a)UPJ^ z747deN`pQ)tC6ApMc7}@cfqfumrL9#JCE{@M(YgzwD%kgNECgEUw9vY>FT}Zyh6V=d#j^$fZHi!O7dd*)yDiGqY?cW<>s2*`JSL^ zVZn{{wZ=Mv^lI)5FbXqj$?dLZxr|@k?X0!mVH}6`ie&J|XqV8W7IY=L9xUI&_u*dEFym*}JEfLz z;F#d`6#C|c^(K)-!)CP+-RV1v?0O>EP0UFcb?qDG;pNmO`QsQz^&7KpZl&JuO6&Bl zj2jX5SPej;$DHF8pp1u4V&2W>22RV~BC2Yi%CH>@r;h^YJcuUkI z@dykf)K|vI{rfg+7YnO{S@03^k!ne(PzC6Y40{_`ZDgPRHku;urg}Uk5K++ zNGk+EO#(84P%F@oI!=6(ZYL)c-2;iy0^)NS)dj+eY&#i*A9yeNI!~QA(}C#5DBL=L z*s!V3K&tV8>-{NDyg>9?qGqeqLgPJyjR~e_jz~bg<<3fhgDxR07}Kow)s_zh>Q zA5Zk02*JQ%wVCSULsPX$t`_xF0Qx`+pxKe zXhizU(LfY9x$n4!!}q|hz*`AvNZJM(Cj+%$lve5^afcwhHRBS@ntXpr{S}4ri_P+O zMuW8TtJ-QlOMjlweFCq5e?J6ziex%^!fjHx7=HHWXtn$t+WiH}GKKcq+n2?vF;+E} zR{ReA_|yE<0Df+sJzoxJAuresAPS^U{l+i2(P=b% z`2r7xIwh1N{bMw&WP`{E^$R^fegAmN-F~z`yzuX$v5X_Wpj(7>;I;6k$iX14OWdOg zjcNakZs$yb4z08whzw%B3h}>2Ke74!N0;UM>)`hb)b|vQYMT&Gwb~o6lkXKCJD2a{ z%;hEo+ISw%c|)kbEQapD6|1K0as}wNb@QEj`Tm&MLDPSn-JG*NroCQjBzia;sGx3Z z2+MJtX-ha!z3f3(oC@xKdoX0+M&L|5{Y)B@SR7mr0e>U>U~!c1gKQ1`I zAA7MbhAx;Nn@6mYwEH+}#1-jJ5vt<7`96}n2EOAY=27Pl+Mj4&_51gcH^!X}oU_a_ z*7KBG;Y@YEpug9>pF~$C{z~9x!rRPOUuSGAoAkwTC+%;++ZTcvA@HCmwJ28(_+=dlZKU2S#@ph@DEx`>ar{W)F?DYV)~Q>pz6%#J4mZ*} z4}1p?{DX_?8t76Ad@(+DhdR^~8XRiibSK{zdT)FPx;wSJtz*6?-0R3jTLEL2Z)TMUAfY6?9ezdm5>5Gy=AjX8}GHui>U=FeNA%7{*x zQHi?@=}!sI)+^}eUT?a23HjZQ%@dFBV=7&(mSnH(w44{MLq6s%>H5x4AXN%Kg|4S?|a>p^pN)hh8oKFN>}wI zd&f9hFn>lpubBxwvHvb!a+C4gxOAn>`4(uh9H{txjrGi5^SBd9E9vK3XvOs-%<|3z zCsY4oFE6&2ewKGG<)5SQU_R&i8v!elLH!BdR`G*p=Q<6d^h+nGweSw&hUA}f-%)?m%qWZ_ z9H)N>P+w%2`Md}s?o3iYh?0NoLHA}5*y}T@k{nn)rG{Cv39GX+0)G^nw!tAFaple8 z1*E@&_+u^Y2Kf?|gN7Z-0zZ>p$#6{QW&9a8?Vkeb{q6R-yk+1ko8x|cAAs9nJQ}J> zn2+6AV=5}c`{?Hg*Li;;q*E-mk4zfos^K-cy6_Gg<6{sAJX~K0{l)jmI#*6qY&!SM;ekVLFMn`a^|FroJgfBC+IuHSn1F#<< zt}5aY{Q*ih#6{_1@;w84lSTSr7`4@eE=xiF#r^LJN&U!)ZzuU5WZNx#m9Ea^|fSUf>|H#%lp3Cr<= zfH5PE--Ap@1QX69#!KZ&042@oNT0-W`@hJ1L?_@po4P_peX3YJLPV}h`&@~hkrSJq z0Sg7fYZr%_z(;+?3Hd9+A{fTagv;^x%|^s^-F1dFv8>nnTo{AHO1D&z4 z3W5X!Qo)CV0VFRHzl=r#<3v@pBM{=Mig(4Oe|wM@K>!)!zrYm95|=!m^Sve{y$SJU zI2IU4$i=Jp2NBIEy`OL~=gwK=`wzL`1>znOrXs>~utNg>c+bDOqpf274e>2__X4po zB*A-r*$tR5+6>3?0I3W4&n^+u-^^=#1#g5Zap%Bx`G zAp1qq4BmRToD(MaZqzZ7Oz^=vZ*~g^oCflU5_n)N z@T$0P6R!;G_z46u=g!GUF?4Tq;?1D-n@x|4TKO+oPi5uykSlh_o0f?NT{8rT}L_b2Of^)!q2({fP_66w)x}H8yz5(>H zJN=l7JL0esdZ6FeYuxeFpKaF^-H)IXcp?Pc?eFbJ(UMU}kOu@<{|UMAXM%*$ zxshYV5cp_wJ2&zq<;UVja0+$jab{*6dV|nbBGlK+U1mAr@37v%uUI?#e&3%{z*rTh zfzSI!S{!h2*Ez*u@Y^=*?!^lz_X#qm>5Ow$@W-Uyq?hLr?E2I*!Fye--rUmO5HSj1 z6{X9@5UgeSFJeT~W9y@x7$0eJKr;_>RZ9ErX7|fjUE)KGLZK^I7r$$~L-ED2dU(B8 zW=V6eRS&%E7N~4)1b$D4ng{+N-!gBy_Z{`68js5SAsPBnBm+3AjJZo_e+thd0}pqO zdl`#pg$MQzojLS(46=+>Tse8yu11-!%%NF56WdY$zkzDTY4U&IJ}577xbL~G#OQ%* z-CA1Ua5p`xNc>MEwo~cX3M*)_?u;3Fl{K37C#uHIDd3Z*+PUCqutuboJhU}S4yA#knwCO(Y8co;#-|6#v8!rW#>GXr&tg3>SZ(Xzkwa|JA7Zq zfo3iBR#KgOhnXil(}G?C)hpb(h&5#!(RubvBU_Am)Mov&Rf&Vx?2^l}SU-hTZuh&I zOM5+IedPca`X{wxnL)JoNYY&yN9kWf>vwUd5KXr0WeQ%@cW>A&_NTm~T~N4#dCM}-a_ghavG$cP@D2p@#^7)CXNzu7t^sfuq|W;~-Nh(5 z!9T!XB+HdGyTvh;LzoXkUz>bUc9prrpJqPOXs;V9`7Y>A9j{lJbnv8kskKe%f9TMi z+(RGGFO|wkjRnPh<^;o{-~qNT0I6PQdI5y~Lp>}L!;55I5(o;JttAt~i+kt&t2H#5 zTo#ym4^QVz5GUGS{0^q7uf6i*JFND4*?iv&D*>RnYppAyHW;BrnhqA8F4EzDLGZ8R zSBbJPS1$3$J|y2vq!QxqYMj@D_(c3(?FW?(p%e?m z+sF3IgdlO5g&vbIP&O=%h+@CeH;K#Z^lTiey%ttYl(eV&jK?(?pA*Cr{YxZf0)hBy z;yckXdzmnaFKY-ja4JRUk^=y98XPOSU!Y%IIVm8SY3fU6{^Dl5A@Px3hWPK`s&AO2$jA~vqOGtZxbw(3lBK~eZFjvyf40T&{NO%PKqUc=lbc1R1C%%U(L+R&CHCV2m zU^p5`KlOk5kZefkc^&X|mu@3bs=)UY;F-+#*O3f@z^HuA@aNG50cY@iKiXRx2v5P1 zbs#j6!<8l6i7YA`Fn#4Ddh{1vj&xHNbPJSr1AM<9hU+l(KJ4WPqT-6<8__jW>+yav znD8^Sb$%v{vGiHMoc?zC4Db^5yLgVq=5wj&=y{JIA#F!^zt`B;x%nM3(1>#H;W8JB zgt7ngO7P$*uPKfwux>#MB!hSyKK{WIELlJBC3Y>m)?Rbs&Cr2^KyY<=2W=VU{@}b6 z{++;t5_cOXvnpjOWnE{u?-V^>xQlsgTa;AH23dF{ z?^^!XEZFG@iE%^VJNu>|F9Dp40*)r9ssCN0ZKxXYapph`zIv0rw`5+u7dY47Bz>M< zVG_D8yO$l{rV~|;0sf-Iy((0-Vb_L5e#rOc#!h{Wa;I@S5>LM?@(lDr(hc297(FzpGBt$XsqgHJrPBZBtY?D> zq&Lv>t@kJg3oa7FxW}w-;s=Ar4+dt(%X<9Y8*bdDzLVZ7k!Q&Dv0V_WKzL7dn7BPi zs9E-Q=}+hvS9G#YE`NME_Y(8({9y;>3N&q7i5E(=0S$XtV`Hx{&Lx2sk`9BEC6WX; z*B$HBp}v*KB_AV9!*~REnV%TDP3pDAn1*!=xMdr826T)Io(xg4kEPn%#ER6ZVRw3-SUD!vPZwMH5_Kjc>nTdi`jUjC}I}^cVnXQss%i-5gjS+I&fDzCQwYh zADK>&e6fM{zp+f>lesLTJW}p>QSwC>^C7oA`G3~i%rb;oM*F}%>N{l&cPi7rCA{2l z5jbtN`UYPn{<@na`$2S}vn9Nc`6(HAHkr_P*r+Qv2Zi0kHS=fDzc105twsJT?l^ZB z;WT%vNKQ$6%BOq%v1eucthNsBBFb+v{xF$WRI?v63h76Jt|?Ymp^^!U0(piBRG8}^JFAgs!YZA<(2;)vutaYHQ> zU8A@fW@chCItpi@(Rl#!WW!bpu2-6@XUNK;AQ&QN z6(_g?xgvJg8*lkK%|+fw130AAk1aSS^!B~Dyu3MNtDkr&mw2eswZ{)%A-*`hWwoE^ z_p|!t&=$(CQ=Mi1)u+^x=EH;^>r4Etcq4LWkomwk>a!U5%`MU+E_8_lZF3Q{sErtH zG2dR#z=Xt(@E?OKFKu*{dUq6C`40fUO?k(z{s=w%{!;T(Q^+^VYk%Sc=vAAvgBcOl z%Y~HPS=3{G8GkTY>YHfvNu`{<(VLVa`*>4lz1$$#Bh|Zdl^>p`%#@zMW@0d_>?h2} zS?^4Apx~cZDUW?GuC2-}W(O^@>QUiZ9?P zgX?U@1sxsfyDORNeqe-XC#W`-SVsG+lNUDXPj)ttb#mo~`Qjt8VdKDSiy?rP4HbZ3 zDN#ol2vjXAcv}hksCPwV&M7V=2(5l+TBz8IlK(j22sH*1W#RCy_#5mRw&LH3|AGkp zEP%LR^wom_;M1|k?4`1xVxQaJA$fVRWb@SFx%RrDoCx8v=X$*f zTjTMk74fNcfQkynlpD^0nc z@ZA+;!cfZiyJ3&EkFXE&p=R{wI+|8D$+yg#A&i$--?M!y%${7ZNYn3E6gwq9kw?w5ueRjp)Bzi)g%51BErQI3j&`y zPmqREBm{|t0TVkxbU9$7zVKyF`;k#f`fD)Cg11ePPz@n`i6tWhz7Vokn{XErP2j?{ zcsJ1r<7uCTJ_C(EEuBPq9avp~K@qPvvQZcho3;ra*{zCREo9+^k;uvhZSIP-;Id8y zmZ>KrJ$dJTv_OuhFKKLI=O72`QiFZPn&R=p2S zBe7w2sfP!Vl<@=gKAA8#Wf%l$Sz?`($B6&xwurDk3pZbDbS0a0vmj@Q5SYM^DL*BH z2iC&Sop|sqR@zxDPeY)q_omv!_v%TF61GzBLh~7zVb%$X0GGDhShYn4$Jh`G)nsW_-<6sQ$ z**oN?kVz&v4oXXFr$)OPR^ z`H$QGu~Jm1X5dUn{l0_0am@-w^zlVq1$!Qhqyrglf`m0hg8S>F-J8!K^p*)JBU{ZeWY~RuKHv z-&jjT>Df48>S0pD7I5}TAgM4=w)nMJhqgXS$N$d#Y-RWkD`v>oU zIxLT0Tdx^j0Q@)ZHhY>)`QDpj9vcP>L5>)~xSibE&J^l9>KqUPZC%3NOz2e9`x1;m zV#XEvDnirn#Phn_&mQOiCu_5LIk=mCuF@~Xm|uH1%Anf_-?vLdFA%0W7wnCMOP&2D z@DC;g_r`%YXySA?Pt)F)(aTZl3$Kj!D}??yHdcSoJ05*MBI^xzG<&SAwEMX3XSSnU zl;^;KA4UxBz+TUR50KkuyQ z02(cf-;9Up{~K1s`i=NRSaZe^d(}>lf*1Bmw~f=9i-wH!vQ`3Mup(!p6X{=Rdt0nB z`JRuyAQB(DMB#0*_Dr6Y9E%4Jt!aTlsyFrR)GdO4(r#BZSFU)}uf`z%iZ$EJNc@<3 zR(mP-FKqmFv}HVa>NF0F6A86F(j1vVIiU5oyxob-`l`?##`CbMA)fBz_84Odi%5UP zc{ctT*54ZaL+V!A%|K~@dFC2BY(GNyBv+pO2|wU*WY!UM=x-53um{F_%Lq$xU~f!# zm;>iOgyLkT&<}6DMTZucL^M^#bCcHSQ#j zECAnNH{ImoOwy~Vw_;y14_8X=h@TD|n_Sy{z6|4Bd-<_*xxj5S zaaw8W#hp`ww9n}7Z}C%8=~u#}Qqjt+Gg}v)%iYOv8{Qs&6?`pjd+zih@FlP!?s_1~ zIBHuEj|zdq@&lFO<0?oj&#)&un8V`~oWy`kC~BMD!?Ki^R5(W-f{6!Se@M>0DI z7@g1bbCi2WpDojydIp#aZ!o^_2kzI!wC_C^{JqQ!=4(l;rnr;w<{)6X%zP>$12|KE$#t`aVruX{$%0m5*uS-22eu(~}X?aul24ksVnO_nA z6O(~6uikl2Wia}m=2Vo)!)MCK%m}HAOuX* zcQV21Q?4im5>}-rCf^NCR09Zaz+f>UT#R)R!jIOU*h#7)`jKyJ{Z49WBFbZb#3zNL^kHvNYA9@38ZJx zQb*z$oHt4n_E7V6Rl*aT1*-9Vvb&vYLpWpom?%GU8vB=1jYxX3d@F_H$O}Lq#y*_7 zu^QCZSX?Yz{giOlZoaB%fcQFx$DIf!2+PK*>!V}GIJp3BBUeo5Ol;Nn5w5|vK5*jUgL2~FkyCw0u2$7}{GLIek&9Ik zxIvsE0(UNC{kN*PdB2UKt0@z~L31t%l&~S2-i@ATC5RK)f^rH=Vz4z+zq{Oh8W7 zp7h>cYiBF@R^axe9pN3WNFluXPniI5K(s`c^uYa&!^aa-GkjSOLU)-2?4%ZFt1Hbyk^(dl#+-Ba(Aq~86UjoOJ zS=aor;DV_{J#u0`qU#J3b^As%BkUeK)%Kp_kc%k zoLe=zfbt1Yp3j-GG3IAtjb;9B6gw6K^rc90l=ZG=vTA+;zwJPUU>)i?v{c2ElRZQ&GBiS38qq zA5rcVRXUJHdK~sy9f>dE@fBf+)|rk-X4nOe8)ay(mbp)E63j#9ID^pr%5EzRyL(4p zcQfhNH9UV^=lfA}y9q9vKj>ir=FfXZzi*BpUoW>6@3p}|bMT`7ykk*I*0Xbu8;T7i zy*`hW9VXweUN!ec;!C`qqGI44c2Ea6X%l$?-u->A7C0$z%lFl%7I01b3rWz}`K2yfmFl4k}WH=jBf8qOM98dOz9DQl>&(0f@tp{qZ=^5$Z~Bl@_nTG2$$bjOpgwtFjf zh0wx03OwS8fkHij_BW>%mdr+8^LSdH($deKk?KYF(4Wlwd#|sDK5ff)Z>0j~q?;Sg z1Lt5kafw_dS_w&S%axnAEvOUQl0GHwr-a^w6|EvUU^yQ-VLM2^LxI~y4e~Wno0904 z-O?K>O7Ct{_dd)^RQRoedd2ggoBt$CNbNzpWnx?8h9~xQ}zu zP4jtE@VSGxN!(dkTm_ENAR3Q3zv(-~>_OTB3S8mML63|<7%J(&8s+#W{F%m&D7JG& zk!L_LE2@oM3q z$SV-u$Kv+~L5N6|@bbt->J2zoME3#dz)}~&$0_kQ@tb3OZ?reQpuVB1zJImX6JNk= zi0F{L=}XE=tINI{gKaQ^X8}N6<6*h_gkV=Q8jvqW@2Cg&0~_@P;;j(i@*RoHE?*bu z71T`+^8Ifv5&tB;09UE$#J6!N2pkX>{^p-FX28KRf%*#dfPngU(ghK_Am?jAydEbv zCZ1lD_8E|V@Zgf~qG~P5vuG$(hRIX9It~p%e3U=u7}Bp5oQOve?@PN(C~RA*FR|Oq zWh#itRVgHk^p$RDku-V{UM25L{6j9r2usvPr=*Kgmomav;#nYZ{enl5V<7fA^S9z$ z5VE@NZ2d?*ThvMSJ<3(^auoF;AzUw4nlM@i*z84ula7KrqFYEpU&04qDJv5eK^}f4 zJb;YvOTxO4jc*BW@!xOMGl5qYoAdWcu7qLCc$(uNcB2CS%VPb>)k1%R`9*40I7Wr z*J$wPrqRm;UUzC=&AfI8ye=IpX>SFd4`f72^Znx7i|4K%0FR$6c&^Z8gRfEQyY%`H zfOfd+0w!EsobskK?t#V?{V(xd$R)Sa|1v4dR!RTGI0AyBa*$=pygmXUg;3z4 zpe;lk$sGrl;l%&Yy?jyr0QYJABJqo%??hK4FfguN{1=pO&uD%ne-3Y{JIKM(VU+5@CtkLwuUkNErM zY#a^Xw?^Lz0f+RDg4c^nvy5jK?TcT;vRoE^KlnZS;jv)V7~_cS#Tfca;IvKs<=hP% zR+_&AJ_U{g%sTd~q#txH#U7_$!`L>MABfoJav*aLx^1x#!z|v|Ba$okZnuj(*}_Hd z80Hv>kULWtQoEY-wfzv|H1$Nm+Z#u2&AA3aMk-akkohffZ(@a%9O9qbyTir6<)WD+ z*V$qRuw|GtLLByZqBJe5VT%pQ(;RxJCEoAzu*;ga9YaKM2g}`56UoIvaUZ#Fp zBv|oxf*sPR-|VWNG8nhs;B6^ny_!#2{q+k}lNQkk8}^{eSs8QP`}jWJ zycS^G<|>Q}&QV{hShvXA^eX}T(tF82$?GPbxz#l!oui3IyzLI_7|YCOtUm}J)z##x zju!=fm8)R%hhB}c4+J{}iW0%!s1Y3%okhJ%?ctHev{Tl+$9R_Z>)JoL(0iEnW^%=j z##sy(3~X%3{L&fklYz-5bk#g(%n~(rl0^t6^Hm$*eKdXv7?&LKv9(l0Ql$eO=9vF3x`+%VG#^kDxuh>pCSAQy4Zm% zD`4=c$Jm9^1bAzdL-~9M#c!$oFVP*UY`@mmj z$Ix-o@5|_$*@yhWykYswoAXDu`rf0O^ap zEpl>*u5@Y^=K%QD(UKH>ZV9yKi<68W3@OE4ufqH!3s>Y}pd=IGJ;zogsum4#Awt^vgpZFs|LV#I9Qk~)VG=ok-D_ET3f|^n>;(%`*LK^~5M7d+gE6R~S8OAA{ z_$^p0DOU%f>4U^e^R&oy!YN3Szz^e&al@bVKPpV<`x30b>ClR-jJ;dKAmfty< zR|S0Dl%X>i|IKE3jf7g^1g=quruwDL(@@ zW2I;>!T!uXO!pa2=mVd5{Uk;J~ z&uAr4m&A)$`RwGB`(NDi4_s~e@;B*gDfa^1YNY1+2jcS>pCvJlwfbi8OVWQ+XPh2{ z1B~^Fz$5aaky^bC-8`mG>HzBn=OypjjcE6iz(Wr6ky^!iJgqtDtGu@(1>_&$9VvW*>0Dz^zYe`h;ib(q z`s0SJvndOK^E=MlanRl3q24tM{O5(>UUASzyCM?kuZSOazZZ#Vs9xY*!NWVl^X%J` z8UD50Xu^EjecX7{pq@L=8BNm5Q7+CKmb9Al_9vNBcNL>DSji|1UxE8(i(VQZFi&RE%3?d%|%vxG4v}ym0Pk zxxj@v-Q@iN9oUhSm2d|*G~;of*X0Vi*a)*v#IH|yG42)0sYa+unD0RUhG9EGvK?0ASYV=n@3C1!|^%aG}Qn z8Hw}A=Qv-=Yu$RhdQVOgMPC*F6b3=rklqjMr=7t(33E5)``|nEI-#j5J1jKBa07g0 z=gqhR1>mThFg5Ld002M$Nklxq`ubG2#25%1pM?%uoVKw^}ulx;RqhPVc|pg z{^qo-xW;PgyGMA7bn>Zhu>YYScp_hcbV$*wX`I~@s_a)e+aW=M0RukXU9b+_spl#e zb{B~6fniH#0!v|Z;IPq^x4W1S$U^PSivtl$27-MGWt>TFS)2KQVT(79P+x6b$G@U` z1{SS1@m)qeE&aQNlKyt$SXb)Zgk?E-l_WG_xlR+N!kPxiSB~?UjH5OZtPtV9T%fEY ztcjKuh|7FDFIO3cp}NU9k?1uQf(~1_NEjYrCVb_<+OSRnzmn7-^P9lMBMTOycFLc} zVo>-t@rKM}9m3npA_Nv4rc)}1FcEnl{YGk)<`X#aCts1cSba*qAfx$~w$^c>*n;?K z)O&v-tiiZqq_0ws>gvREbak#Pan$K8k*g1CpCRM0dxRf|vT#)6;0|OtDm=~p+5R5E zQ^B3I@Fb4Y35==r(3=4dC`;xmTP!)KGfiifiLy%vAa|x_b!4IUK%~Uq&Z>p#-3HicEv=$Su z!>e|GN{IR;gclFffB2HmkGY_P0OQA~ym&B2Uv8#l(yyw)w+-qy(?gs6E7pE^R(gi- z>vRO%Q^8C2IFa;(@3!NMYmo1o*s17D;88PoOwk|U<@{LvOL-8Ge0Q2W+I;0i+~ktr zWu=)VZp0r0A6t5@ApR=$uRAO9D(xS{m-syDZ>N^afedTQtRQ&7i;)i*OZdLk-7dO2 zuoDR~PNSc(Cb*vQPUDS)RlxOPv?w~C?@#_8Wp5s@Qx!e_pMB1GhC2`Ly=Ja4vy4SV zM5YpDC^Czb&@4o#REQ!(Dq|99L?ncwjAhO|yY4(b&vSEqxb7;49uZi(QZv%>yY{J-gsn|TU+HdgUg7<`UJeYa?^@EBfBPiH!g8ZSqmq@NGz z*Zk9IcZF((4qA@L@5kkg_<>6mcNSI!`^7N)C#*;QEU7^LpN)Y=E5dj5j?whbzr}15 z^pU@_*51>ojK;0QmwX?p9|_POGuv1jYyq5H>w%KV)HBO!U&un{wos2cZGp@E&UQIk zQt=!@wxRvE)a!C=?HqRt?H3uxclrPWxWXmA_B7gEwHNFg^y`LI)`oBC^L5)0dgy6C zEw&;mo$E;t5Fd^|c@{V;!=5EaiwJI|6yNo)-A{GuD+!jwdZqu#6}F_q^HS_N3&|;=P$u&>*ZxTQB@1aL#{AzPm%5cT$0GNe>l{1z*|Jec%L0 z&*#jb4E25lx7kkEmFqO`lAfaO^5E4A5druk1|5Q8#dfob=N70u96w0>G}}Aq32x#O zTU0hk@)n2iseEsvCkKC$PkuzWa#?F7G!eTB2`?j>NxUzYwgi5+xD`p@W`LcSDDt}d zl-LGx1*Dzv4B<5Ol=xqYnWl9tNMOG#!VivN5*acCMF1 zA$V<#+?x;lZA*=i%QpJ!aaE#QQ{VFZmW9lNRx?;H;R@fsvipjuPN;NbY%%l4>d;*- zdGHR!pOUWiQ_U@hdh58?M5C!jI=O`(BQx$*uJs9YcHP_;eT4FR=;b;DM$$6A)h*^t=EBGQC zc})aahbOdTBWyZloMHz=QgcA#flNRjrs{nP98!DqV#29B>Hy+9bKI>K2x|ad( zN~#(U!(q4ex4KCFvDzp2+(K~R^(qEJEi***&hbfIkP?Qhrl^m}Kc9PVGT}EteHbgp zC(pCAW2*%@Fwxqlcu;?XFk7va9iP@4bIZAeKKC0jhDSs3nHUG6IccNrAzZ1g5{(_` z9_y*%e^RTx+XF+iUf$8-BK5hr9Y6p>S46(YIl+a2tNlC%kxv|3DvuB=X>HkypjqSl z*Q8IvmQ4_N9c0S{uzW;Bu;EV0UBWndQe>knyzG}Idkp+Hjxed8K7Q_vvl9Z?SiZl) zp#mXl_66w7IEzRB)P&)z)#u1lDLIQmPe(H^c_pCoX5^w{(ee3-b?vM6cJ1gDGf z7WI6DSOcMPP!;eP!!Ou6?Id24{hEwxH-g)rg!wG@IrQT?r~PM$@6-MkTNfH*3QiL(yuiW}O z=hb78&mp~@CsJwZEi_u-AzIN@qLF}U{w0En`=uJ}UGwgyt>bk#5x8>9`+!?#D^rf7 z+&8RuEf$*82TN@&1wp&ZoWtA{(XMp78haD?m0eo4TH5pA_XH#8qo26*s4b5ZbI?5CxILC+xpzed>2E#tab#E9xi)9DzX#N-dguae=1=U|)OTHc);lE8Y(^%$( zp-;de@G8P5dG7mX`ul!)G(~uJFAg&w83Qx> zZCiuzyI69~8&<<9fu5PkI-4&aoX4{3K4Z={YZ3oht7c>~-a%@t3H>29d?pSwEe;sN&?^VmY1nyc&bu6THl*B8 zbv~*#<1WxMeDE`CzVe&HHGa5v`5F04_m#*8^y8#GIZ|2L*FMwdQ_nVT>w>3|Th`y< zG_udY99QONieAQ7lW?;-> zf%qA#St04pAAy5$@c;Z;T7FtK<&(866Z%odvAI1?yU(+tyhQ!u*}6<(yw&ua@FB)o zq?so3jTwO<0yW$1AUu-}VeXwyD7VjP2Z1um6RZNPP7&hrjz>PBA1ZYTi%A9FiQhIc zDT41opJ0!;P-Xy}5gaBw%0{s_?PZBxmY;kyg!PH%xDV?^UV3F&7tQr3&Y|TJw=l2CdKEd~9t;l>xH_Ff)fnGivz=&gk>rzvj_im1<8<)a z+K};A?sC#+C3@?J^OCBg=aK$&`0p_IZ2jpp%LWdZ>es>AR&!o-9h1=;L+LXhMO}Q}q&D?K98~w>&*FEoa1fLc*Op{GcvKrqdQ&*WRO0<{M574z>G|d+=)EAisBsDO z-1cOp-7Sbas%H-4jhY?xbIN%7wF~Pg_=x!JGbwWQB={T~)B}+^gull0ZEy&M#!&Z-Xy%10 zXyqc0)9<6uEqKH#{fa(?ez)ccrkRw72*#jW&~!(&)nDfWM!uq%fr zs`i8d_WM4{4RZZ12+o5Weo^UhDcBI(Dg1}Piikgp2s45BYwBUqtYAE-Q&uz!dl3#a ztt}TF7LYF&$DZkgS=`~sBK-slS|;TN!WtPT%%_!TQqUAUArr41=H9|Q;^h&!7_Yk+ zQH^?>3mlR4ApLvQMztf{$${2H@?SyBx=ILpza_+3T#-($<1U$;hAfeip7 z$|hWoxF!qaPwaHo5q5*{htS>}XO(v(n5!QSe*qya(4PwmoF5N4dN<(vj{mYSP-Bgr zqFbltUzvayLQ! zGlU(rM?G6DUV8IxU4&sT2IZbdU}M~fi38=fD9os{=Z+wZzz`-b0uqFVK*ntG!y~I6 z9AaTaz?u3pJot;i6iYmj-}3*3QbL~6u?bCDGYRD`Nm3jg|T3~&hUQ7Nm zo~R}^6zw^_)tu(|qXO@Zsm{})8L zi24=jhm7G2|KusA(u6)nA#ipX7YvoZB_{cbJpokmQxAfpB@7!k6vIRV%aa;Q1s_y8 z&}mGuW$dN;lKvEr%`Bwdrp`O|B)+dx6UD)#+M}I7$Q0x5EusnH9ffXU+hvcl+lZq` z|5ASyUken351gKG{w5pr$4}gKb_M%~8togjPD9R(9PRZFc%JuR!R6xCC@|O8opIMR z%4ev%kL#)W4J$6(6R;?m<+9^gdnuBw|VgAf0# zK632L=P=mX!3kn?AL$n{#lv}>gLP*3p^wu;|NZGA;K^H;^L#F_uUos`qjy2K32ywx zIKiubunnuowC&Nq4**wxL-QS$dM$h)QeQTb;lHgviw4raAKlYpbEI#>7K?e(-_siy zz)h7^qt$itb#yiqO`x73zMp+t7}s|7c+6tTt#>BL^>{ni9v^;+_M&+Kp#gldbmUY7 zeASQZIB6#R26!y=^d_(S6@gA&)p!&BX5KU_nvARXgK)q;PyQvmG?31`H**(?1`+>M zt|!PWa0?!}~paUgcN{4YPZ zL*Zvat1I0LCt*JN^=pI|*xuw2Rw1P_;WoGx{0j%oM^(DufmW{M1pWJ4 z-ywePN@{C?!27^+n_I^Zo)?|6&-teS*HM}kY9ksW=Y(Ddc-Ay>%&pY3)!i5wK=`V9 zTHc1lc}P`jDq#a{j%U-nqwpDTy;zvEDt0sN{A@gB+)w_-);Hlwv@;1&2D(w(c$4!1 z(u=gYo@vSJs;($poR8coXw`6wf$b`Ug68>0Su08gMb;q9Y9{{QWnH%)b7xC?<_X?d z4AIVX^FYy0>?@bq9j@(Qep*E4CZ7X;!g{UP!_-ql9;>6?BiuTNu2d#`u^RB{qRm$4 z2`_2svQF|AYa8#RV2L>{_7HTLSHY+ofz4k%nfK={ma9dvjZ1;&>aW1XWbnwjWao#6 z5MQL8b%8&oLIXuJ%ac@J%f=4;I*oQzv)V`A6@eRX2q{s|)7~_7pYSbxJdXD}D9~!cw zc!Y`jm#L>XWYX;B z&c?`P5c_xQlkj;E=IdaCco3$bcizk5Nt z%J^Vs;2gE2%7VjAVm8Nonz|d0Nv-M6oBZFDa3A}MU4(ty;UdtWLVuTCvIa58uk6r2 z;sttHi2e~li<@)ZUmYD(h> z7`^C{op9d8faWOY)AeZXy!+NuR0S6dN=dk2*&9-_tmVrT=;YL`|F$ zG$y?if&+xmx4WkwbV#Ad^u4AL*jelK#t4 zqT~}njPV1Lo1TCsN1IZw4+fS;D7SZN5oTzq;&+PK{1uGl8Zb}(e8m*K>Z@&b2CftH z=H&fAdNuc~(}-a;Pg$1oFX>mDh<$}=T*D4)HLB^pX=9StkzUQdA^v!ZwiPug0>0Pp zy}teijH7g`(y1wJnbz{b(mvqm7UH@2I`wvD_Jq!C`ddU;1`SmiLSfG`z8roF2u1uB zMqB~-$MbnPPXql#e4qMQfgjKxy&_DezI^_u1|Ca%j`~0cZ=G+QTC!txpLIU5SyJWToKwObr-+7h(6 z5w|986#J|XcbDIPmTBx6JYoDvyIaC*{((`Kty8{iY3SNs7FixXjv&_ZQpGEbt8Di& z@uf>x$97eXuRI=;mLE4Z`qhQ4O$s#skl6RWRPy}wdW?cvGW8K)4Xz>2Rkc;13 zf^pwidcEnbsr3JMW46gWG>K`NSFbefB-?LzjzW*C=k$KSaig=`deXu}`Ba-J?8z<4)CVntx}pOq zAxozpuW{)6Eb+IT6IOS|TQTx#1iDmjs&gVZnSYqO#5mm@@3eHnq|brASdfE{`#MG~ zq+eUKenvX|YAq^>ZdK2bqsfrs}%E=^trkBR(;CXHh;F~t+qU_XJA^X| zj-=UYm1UfVw2MwM;doY&IEpQBhl!vPRo!rlp(i6##T3$h=(@AJ%%{LD7iZ=pXk@G# z6AQnH*{LT*EvES-ZBYUAgm<`-V(`#@M{RQEdU*9r_yqXzj6OY@d5)eHOpb+rgto;z zWe=h~x1>&_C*OMnw#VGZJk&QVAABV;$&QWCj`KPm?cOCkr5zAkO4Yzvr~_y7S-pb+ z-Z|}^-C^*O&G))^@YG1vFPbULQyVn&8<@XR{-X)ZJ8Hc5^FM-}^$ph+w?ZX1^ucAR zN84k%!gmz_L>VG{{C-p@Z0vb5DvEe4UI8+_^fMZK1H+9*a)P32YIlnur{^>aR5-V5R-t1f8@V$x$$K{AQO)ed|pVB*-w-m0q0Uy#g zAya551n!c{RC-g^$BX3aq>ZzW(ay(u!yxmMztk5~_hZI&#C|lEaYX;6FTM!=23D%e z{uxa3*Y=uVfbx@-=rj@Rbl0g0yqwE!hNn4t+$|*z#MBghybj}Hk#FTW68!`b;#CSf ztTlEjQ9(am!QwNv9-|b1R-drYZzn!ZdssX)<5sbTxOqULt{l~d^fvB0qM8jwLZ#$Z zOTxEF^JS+P{v=edXfqSDO@B8CL%>bl2B!w~k5L=cYm{ru8U0qmc;sa!j^nM77#51g zXu8-)IbUH64kF?K`8jc@s8Vs*@HP4By3M_QY~g@P((mEO3Iu|vdUM__4nWIuY(xV8 z(1U5$(&IyaU%@B`qrQC4_g`$QS=D#TVQX z-$oGOlolN>PrYq$5C?+?tup0E48!~t{A%d?LZF1~im!UAS<$5ZN&R9pUPQh#jN~-m zd+ra|BW3a3yG?N z#f{*_I26Uei@R7~2;7YmDofmoShGs%6*GT&Ei=lXm>Oax8-EZ!Y7Mf17YDDK^ccE3 zLHpi;fX$QUS$#L?Q#+}OIK<2Ep1-EB4PA+pM2G7#+)hym0T`=2Qw?!olvv=&ufQit z1Ss&f6uVi`Qh84*;X~|#H@~8hlkalNus+|bB8F8YUWaj&CT=4xHX|%xwuDYXg5hlFS4&X zJM8^F)7W6w-CAoW{2=&7P>+F5s`RY-TP5f&@8g2iMQsVI=8eiDUx8h)Mr^dIU#cFg zIu`+@-1V){l3i#{0gU+C^`D8 z8dq!iT`<lRP z8}uMxf79O~4tliybz`ho35V#j!ruw}0`&rmiN9#YizeAO(5&ePuHfSeZ$tK>bBo=t z#*3SiWZ^Sf8rKcrYKEN~=JVj=1>a%SfO4tkcm83}^&9>^2~)_|MLXj=K!5AuNh}Gx z`^0{t;5_wYa5SF{{(Yc-8*EIwqxD0fH2U{~SU!*T=4XCnc`opr;0%A#KVN#hIdBjetN zL-bvw-(z(MF@M%>?2ccizGM1jhxyy?-<$eWJCtyu^_2YthWe-c&<&h{6O-opOV z>O_5*R2at@?+)XaD4X&=E!PL1;R2Mcnv=hS-Wu%&*Z5U6Ir>jJS?ZSn^rBTQd^*7V z;|u9F|9JzC`|x|bM!kZWgBEyQX`%bbztbpHyr2G=?yi_4jBAUNr}v=#EgZVacpBk` zhkBj&FuWK=`H#6I^&|EBxQy^Oq0Xw(i_nC-6cN%zkFVvD!aP8qnf#M=AOMHbR4q;S zY>MlEm6YSA?Wa2HjN4OtP)`Ofj`6I5-v!$#y>K7ZC%Fv--^_CEBzvIOk8xr|jq8Xj z2IIl0&6JoY!GmOTo;?6|cPz5HfCW7~-kR?D9oy14+9?zCCU{-CxKkwwAg6Y{rTFby!xQo+pU*M zmQXJz9Z4qbja9lF(Q`+$#QmJ`JK)xuFrIbVAWUbqeu6NQRYlgDuYvA8jQ^ar*MTm0 zmvXY%hx+$aJX~uM8^q85{^vL9kC~8j{3`fwmP^X4v>Nz5VhtAu!JNB%s(>#-)&Xyx zYQj$yaaaH@U2r(d3L@iNt+f=}R{by@^DW|2v{zl=9$sriRfew_joj(i8DCrnt=Z`r zphdJZ%$P~PhMTvd`!OG7oevU6(>{cBx7hBeOZs{-W@P8P$F5I9gEU*N-Lt+q`bZk; z%&om34He=~=oH`6_;fd6B1YKP30LBf4}frMWuFg#=$nm&#(V_*P~ohio|M~<4NeLK zpTuG2BTU3zZY|M47I>Tcabk;cg2Myc7-;i#LJY=Mi>3fEtA(g4wXuNYMZO16pla zSlKE4>>Y(}*S2eK5Py(EupWekEL{22H- zsMY#4^7lijTZVXFo&tD{_#bLAGs5+F*aw6Llej3C84y}?g)ysm5E}v`Pc6yEYzo9E~(>uU#iZi4}j}@j%J0C)&hH;BwPYkw4%FtJaq(qIkEN$ zxrAZl7+;tpAh@%>@)E)fnrJWfnOBqRkU zqtu_!k1G0U<`}M7fqL@soZX##zlSS^!OMiWgn#5#Pt^lehE$~9#$)e(yf*kAJuP-_ z%vaF=PXBWO>M7b7YEjY;2K7ZKSKLNC`B>Z9cV7ivvr0adqif&TNbkJ4+)4N)wOJJN zQ9rHXxpL4!^pJ#h{-cmrle6Dkp2vbTC#HVF5MD~!U-6oNslq?ywx;%BocHPdAr}u+kY?csIBkU*8`taZIxOM`Ih$AN(8>e zuZ5Q7G(!HJUHq~bdB?VnX_NdN>&2R^hi<%0d`3*~Amx}}Tfu(|%)*R<$AIG(@k5gl z7-Eury#s$x|HMFl{RVJ89eLdvNqKWykzWyDzg9 zg&wqJk$q|}@Eq@JU%Z;}%+g1Oz{lJx)}(m&Y^bJo!w(-Yr>K<<`0MU)GiAYb`rt~t zHT~SoWuo)c^Avm({KE8rO#CaL3-Y7jf4PLIa3AI!!;ci9NwtLYlTq{|Q}>H|BK=3s zMoJ++v{xS#$CEz5K9&Rg@i5+4G)Dd?^H^aP<+~a~VyKyQXQ|N=xLj~ui}~Yn#(wb= z=^L$2Fol2B)+&p81#V{VC}Nz!mcDd}n^lal{x%5Q!y}#YALjd1cMs+=XwcPg?7mP~ z7^6i4R8p9CE}QZC(h@o+1W6x(`U3n%9T6io&BiOdUAT~b#%NPSptAq4KNEj@7|3i_ z;8jU#s9iqg*|_x5fiF*)k4B3nxBn*;KyOxN;M3R}(0$Xu7je*i^1XWVi||w9F()sl z9V~B>cC9q9MEHCpFqQJnaS)P9*pLHN@Bka(pr<(5!K%}MxXs2BI^w{2hMGgUrg+2} zPJD1+M|?VT)2wW2)lSUo<5%mR9*>RJy!cLOUoek7Rdv6VGq5U?@y8;@!MBLVdmC{% zQOGA=nwbp~$_Y*q^>4-xnaprg3`qtN$6KZYeic3G_Dh2<>|IV3+33dib!+?!zUdR4 z>)CY}?+kshY;<+*c(wM9(Bj--_FHshjQgJV1|XUT5o~xhex@oz#cSE`GayGib6+C< zBd=9`NBBINBFcFS0u5kk@d930HH^OYxFAr$!NM*@FpA*is5kr-n8>ci3^_9K9X3nL zEgH_?Z*t6`8RUW>vcL?Er%;uSRE<-zR0paT${JlyziTarU&YQ=`HDv6FHi``i z%dsh%8twdT%@lP!EAA=WLds{5zQkSP!U*tQJ{z^A#?vd^-G>P8VF_(Zh+i1b_UKFZ zJ24_g(^bZM$|d3|Tt;$a$!WVuh{Y}Ez1TLX1W_^b_V|N(7vM1)AI`Ih--qMQj(op` zE(U_ZcBY9K@4MOVOJduBTgOW3Ecvf#A37jPVb#bsgi5(w&_M*bm?w>otVhXLp8GP? z2NH!m0q-V9joHNay8AeeghISu8uWg}2s_pjpzdgv#0>+4{Qv!k&6FUVzzGB-w;b-V z^6@JOd`Cd~uRbp>`TuwN9gP*A{-1vC)ORQI|BNdV|FgY98?{@ExH->XFB&>VtsqHM zClKmQLJRgWn(q@3f!_f>^W3At@Nma^uRNuL-r*4oMy-mCh2{m~UC>+Z=X)*9yH%u4 zQa||3?anH4DFnB-+a2cBDd)DZBZRf=6zfUKA7YDeh3|gX6r*$Uci=G`&Ap!xhRtl= zCp#h*j9IdAKsfX?bkNo%aF3OOxZuYf!SX-&q4^ZogfgExW1>NRQwYx?LP5}IUVj${ zm$>to(McsRl-q<>|(Jz1Y>{#D0}9yC`cTrCW~RhwDuTtygN zy{NjK>kQYlMzMF#t_NM#?og{-_0P~yt*$mE9sH`;s8X-8#RyU_RL!b5i*#T0is{g8 z>aNs-$rBNDW|sfCTz}HvuDPP>E)>9(OZQCm!vMD@PLJyf+BYqKyex2X%Ll6lDzl;7 zT=H}QaF1V{m^_GpPqqI*U`KTX%v=u;KO+RTA``f6iD;aj@~U~lmLG%5`Hl9KIq#3PfH2F zVX2*I{X{>qEq~|}80Va#_j8w#f0+4~wFL6$Q}SNXGc0^Jjrizi>3`k0S*aCiub2Nw zR65hs&n_!R>dtq(S-?VMHL~urVYu*+vqEoXU;V?_aq#QpD<|@4zi2|y`TU0Lw4*OJ zF6_rT{n@pX#hXY!Saed1#zQX_S%Eb8=8pUZHuF$WBfoMam;J-w%;hDoahag_vC!QxB^0o7A08ZcO8Q`>Lb+-0Li2K(XQ-a-q?_j;Ln54MhI7e*O2j`sI zIPwtTJ|`4;nR3U~F$a7o+T-RWgZH@^+EtuhpFc?#M&?dS2yFe za1QOXbl>5Eg%+A&V_re{uJgDrK);?qBnR$140G6ppE!T%WzEZ^e~pHF0pVu+rtul- zOh*WT!SXMYe=htPe1p#VMk@%PXmA0T`|dq@O9BNmsc^S~=ZYi>bzv6J2E znFt;!zZ$PzzQ}_7uu8c;7x;~JRp=h*v9`NhY=rD&?vOKo*2C@`sW0kRwIKpMn!AkW zZSXkqpmUd;P(<<(l$+D<58Q4Ra0)lD>V%=o@Rdky3%qb1a(ml8%4OvboT!PoPPEP{Lq`0sXEKXKk}mUbOd~GvfMKkceny5$N5h zWcbXrd~fMYv}wogPtOrj%EAE`%gYVs3f0%T$VYt(onaBkT ztjvp}Jvfc_Zh;@d)ZsMo9b6y#PUZ#ZjwdcAJjV$O;O9ZYZRis3<@*{nn>A9ivhAI6 zau&a_blWO_(a+896ix@Uy!>me|9zP8Z7@gaz!4t+N#3pZR;UI}di9AmOE&0*9$!^# z&A|u$E=yY=7Se>wZ=(AMykFyL&0$CIVieouF2GmUFPDR!i)I+(Dt-ezW~h}x#uE(a z4T`8&r(=6cKme<|9a~a?f_YphrvQ%sn;13#8D>B4FoBNlq{7kfXTT*R(oFJos^r+8{O9fVpDdm*|5^#q+6wjiDZ3Q`Y1Eibp& z*x9%8ZV_bg8sQ1z^EqJqjPG)(H4DJ(;s|Xgq2|Vlpu{!cN`Ojp8=w!UPyPYu6$TOh zsrIUEe1BFA_Kx<}U>&xG^fJ6Eb{AnTZ!TpLM({8X!r>OQK#s`tvMN!=&8@3cv_qm9 z+onJ)py3(KqfL5S!Cn+4!hWRpIu zp^tvbPX?B5r3s56&yVTnRkxX3`-VN-Xi2=T`<5LCJzoz_iCRf| z1@|ZK4ayI+m2%qz0oMl%G&PvL8u*Cwj(Hl4iKm}xMz#oJvgSq}0RB^ro8TmrMo*YG zG_XTN)2a=^==gEMXSk^TD&;${PwP(nKaHHabJTh#6jOea?|0%NSPFxY9~rM4B})BO z_?2<|!|y5jxm5c|1T9qu{g{m7G>&wihwS>hPs`Dm+m}b3z)yD#Zy9Xo`x!PAf76Yd z)`iHujH4<`_gTQ&JKP{V5%6^=u3rp2hc1S8hbB{RZ(QL5uafp9t;5iPGm%#grT+Eq zw;FgC8W3(19zyx|LL22aTclLvge;7Odx~}!Kgsu%C7}`+sQDJJ$g$9r)K-^8pw<2g zw2msrf|wt@LZ1G~UhXeWJqQ20mAbqB6za+G4Ug*re0$_4-J<`YTZKLI4p8sHyfy{& zEAnpf`I1@m!wgRm4PD{mxhL|ezwq~*9XY4T-#2GoHuO=Ho_XcgO!#%Fj7k~IcTv|Y zD+|02zjdqCt?9(yyD}jIe6hdGI$jL@+5cwmlcSL0$8N65Mi3}!cVl_RaM1lrZe48# z9@Wq+|3v?D%xC22&AA8blCg|$nK?>Pe{7w&Q&G`q=!Tw4&tzUw2B#g5+XVdEXxAbv zIG1)@crAnSaibC*Fc^Q)Z^cJ*_aUf^%)F7CM!av4R@{;P{+?ePSqI(QWygnZz=y?S{doDbve`z^q7>lKcDoRF4^aD!1v^F$*06N=KSKbzg#V3 zJ^k=jyKLw*_1?723CY0ewUW-oP04>X_p!X{EWn$CpZeEO&*A((#27q(TwbqY=)n3S z5*E_{ayO7$k}A(I^%bm3f0XJkM|s7oiyq4ZFYNB2WYa+~tsfJgCJSJ_;HVVf5q;L* zFOGD7f^G)DXa9S?Zoz}h>nZ(vu}wv@&{%Fu>ZjBS9`Myl62gZI7>D(i^Fq-*#J{xP z@-GKIS?-Y}^0^DxDsQCNvrdxOHbvd1r-V)**#GKpetjfxdCu43#+S@PZ{256&-I1Q zzLeh?Z!>3(_mogM_m~B}x)yIsfOkCcGn^Tv0p9t5PSFGBdi>d7gT}fDJu3PLLZSH= z`V4S)I-annc2xSUR=e$0W$<7gTdkIa&3Vd=dAE6J&0Gw8{=#t%^i=R(xI!5Gj`VOo z(`)hlvK!B-lIA<(b}6dFyc8G}vRVPxQfAj8`Wu|=-pEOzT;ouktoxXcmL-RCN~r%2 z>%K62SpQX3@w?#N3#x;Ua{hR;n%s)BmWOv)y9ECujY^)Po#y^dqM0kU3OW}oB%W?h zvhQV_mv~N_{%Je7HP8CuKB>Qjj)q&x&M|8s_4mu?Bocy;I-wS6Lc)AWZx!5(Mz%bmbAT*~V?eU>msDO^nu2;8oJm z3PWqIV*2)9edz2|>Z?{9ojr+seORENS8cHSh^(V*gi1Ucx>(L_ej%O>_pCwaha>w4 z>6^EKU5~NSPNFm1YkFD2)>@9ejc>_r1*bpwn4-+IPZ;Nq?o=P`MpoFT^Xmg2Y&5or zv8yxCxgRSd%}_>peHazI$E$Uh=-5boxVoFTQhxn7@uBW};-HT^XlKw8XlgOKmiCl? zPJcv$F5#OQC?mb7s=CIg5HPv zIiK#$;he@3H3z+&Ku_%vB@571t(m6@2ysfwz%a?bAU0UM^z?|_isEigvItBFHT6Zc zh_kVGIF93br+Qp&yQv+>t$T=%)>g}cfduGDPTDt5!&^CSJX|SH-sBZvq9svf# z8bneP5ky1rUt2XG-ku5VOx)#e-&R5~re;D>tTyzHY97HxZ9Mq~b8-wE$+c1YmF#jv zT8%hNRu)LSaa|HWTZnBlz46V%$NOB;2RN1k!{cD7jR-j0B>5ZjKT(w~5KWYRk?RLt znZS>oFN|+Mpbg=+25^my_N`=Wmo@b{OE}{|{0Sk7ie#Q5M}928 zGd<1YY8Ke9Nl*N5A)kqvDGMZQp7oU5wEgFRkCUxw;c4>HpWyo!fy|z=Ug*yc7RVpo2_2V|ux*=N;|h zj>J1c%V(c?nf^A#yc>ka0_%W+k?0BNd`!}H?wyW;Fl$)NtS5lm;>g31GoV#lUr#>- zgOifa9pHwyw7n3W2CPnq3f(DkJ{RLlql-FWET`T~?O}O?lUrI}8g)oFjf3VO(oe#8 zdlG)8W(ppu+Vl^`LhnQ=EeyglV)r%@Fd~Thwvj)7dc-Y9uQq?n!8Z)l({M2!C2$s8 zk|Wqkgd(^KyyeH>dlNP+&;`$u9OgoZ2++1H09ZJF;|cJuVTtt)-y3ngMd0!+js|`t z3;?ZK^08Yi$EzvBn?{A=BY&hs#>CxXC^tu^v z_>Jo`;FWWRXWCm)ehK|t0Do}MWx()pCw!eTaH}}*q<fl@2oEo|HC)3^DaZkmZ2dyVt>wM6I8)sH` zfLHKz>Y-HdHMkpBv+L-`XU@(r^v|XI#R_^+v+z^4mVVX5eG722C)ztL;9(S7KSpPe zKgRLiS~U0S_xXjd4cAYZuhEZR)gt|A1hK!1e=7to#eZk_&ZVBv!JMlFi)gQNqDq7= zoU#6+#unP^rq2||pz*0m{ro4;5VgB->sEQt*D9sNWFam&VWV#a(>wF#2^;v@lhv1b zO$jThrk)Mcb*FRyej9x zH>DrR!5aSdtREkw1e5C_X!I%jrnp(isF|_o7V}r}jmkf!bff=Mia!m)hxFaXr*g!Y z+CJ4x=+FGVXU&Vi2lO#|XA`_ri?t?BW8kyf*r6Xm!+39*E=kqN*W7uknD1pbmj5l~ zOY;4rUyu!L<;|5Fr98$uRbpQD0|#GoW0-#@@tW!z@wCly`~p0dj-!Lg`%NRi;U^PhZxU6wbbnmTpQ@`2jUR;8d=>FP1^s#Ni`49&&xbf7G|Ex z>)GL#=-*=Bl3UE9*;nO9z<>P%ER5m9Mg`x``I!hNKZUY_?oBzW<<9A`&DS$Hste`T z`+m(D%Dg|QwaJc#%*RI_%45AQs!*~jpZ0Cb`N^3Ad}kO-{f_|OkBn^pCF(z~zu=q6 z_qp!f;qi=bgYlQ~5%5gFVNeU;xjx}sd_&fe3fD8T#sJ58CDlp}^S!cp!S^lWxrxPY z1It2B^Y!y(7|eh04f`3{h=rEeJz}79lon-Jab&&B z729dG2X{)JP~Uw(`xVkFqC@l%u0#z%sP0l{vCh`)2KIdGJoT>P?$89*!%C$}N`pt) zjV^Z=zLKP>#Wm|izhB}+;s?rAhA%<~$O`p51xYWq)T4>tWZi|{y`MV#e-iHc7VVet zBR;GgxTlM$fbfpm2w3_Y6#4f5-SDd7I>zNdq@F#YtB zjl@T~_c#Ku_i!+JkoZ|=y*32|fGt}qNdHPJ*6WeKw{~3!iUn+m`1?^iR68v3wcIaq zUMc{f$q%OfB5xUKGgNEuh@_M@LS>T=jfoOKiB~ERlE;V7i?K0Ug61+{cNz=ee)1)& z1!AO*Ug%@-Lxnqm5_A-@Q{RMCD-I*Sk~3PKU_L7TO;||kdpnEu*o;Bocp~2JZsYq6 z1O^a8s|Q!je@M9*++KvReTVc?HvP`tR$}@Fk?)pv$t0dUKd>mb z9}`^Q{^|qXl&Yc2y#4LIC&s=kaPv9NVrBA_pJFucJ&Fz5td+BNgp#gaJ~7F3h|5tS z2#L_xTx{(aWd&~`KLe_rBfcGSf*=tzCwaDQqUkD4To(EQ+84cxFrEhg!S9aEl-#8h zc>eDn_kSn+ulRqqVa!#$r(wIr$YEHpv)cm7e`jR{Pyhfx07*naR7T;D{hOXj`99Sg zYbT7xN+O8p8&EQ{7A`UJGI6{Xr(BaX4sOP90y&KQ!&l=DR+k*YhKQNTx=1_Ae z@pPxM=OI6pM{&o}P8ptnN&|nt^;Z|Pa7NkD_B8mX9(OYlcm+R??~zysfx}GtE-{$} z^vKPV*XW;_8M{|De9=#qzMe3gZm-r~NE!ziAJ;}iou|I9@~4HMmz*by-jXMKN?%Xg z6_*M8pHo#35w*yP@Pi@fk9WoTh^>a+Or`lO#&rtzx`}v(eon>GzxwW7a^!&czFL)H zp{nMuBZ8HBN?&2rqWsBTYN?K~$b(4TRwuot*nSb)Aw zUJ|>2^kw$?&~@NsyRSJPu!;X6^^KUe@Hy;)2C@anZ(UFW08+~u@T&1 z07rQ2LR;|lf~swT2S#aqui2h{wRheWg+Nh^YxukHy(4zS|5EQFJRN4Rd{r^eh68`ld^R887ht1Xw8 zEMi|V#^o`e_>(#+{H3_qml*(`tjUq2;t=?dVo#EzV75Emv?;*jmebUHoqFHl1>naR z$2sRD*1%ezbYNHP65@VmrCh$jM*3YZR>`nA^x58Yx+Xf8u;(C zQF_tN1UoLWm-?bOs}VjllY>?0NPC;Nb*s{^v)G!Qr=3pbpJsK+FToXeHtAovKYO=G zKhwL5ARB%+(z&=D>3_N@*~~MR&|iz~A~1JEuvKre(sUS5WW`#HF(0-U9z<*)p&3HOe2Yd5G-i|30?kW4b#lcgYpn3tUU;lFqp8D^{D5 zrIa79HTG?#JzkWtm?ze>*S&ei1Vd;3=*@XnQc>K@L3WUhdIj|c$@8@_x(zzCaw~@0 zf_KlTC#?_3*I#Xu4UXwlagyZL0rpKzyxVwn8P+#sIH{g7Dg&5i`uiTk-m2cPy3vu7 zjw>o#go+NLA~P!M2_}Fto15JOVGzbCuFees{EQeU^(NrujF{XYgojs*i*m~Zv7>=F z3dNCHv^>c|C!h0-+3ItM<0rlfZx^4F?|EJ^l!5L??*zc<+fY!S3@rB$4uaLU=(1#o zu@K=wZu2~f*JSaN(h$Q)Zi%tnz+fpQ(=KPWqKjchseyiSoH;-c6@lXhOP-{Aj^RL3 z6JnBy^h-YUH8o^Mk2F10Y#3)NL(GbV2N9@R5iaGmuhY~gnv6NrKLqyL zjC3BHP(KqF2cWsiJ`P}(-JInu#OS)*R z5(vdn>6!l#zn%BCKnOgvhtm{Ze`wN5S{FfV$%Ry+JqgS2)^^! z3~2UHdy`#*^etdYdBO|&0|F0itNX1yErK3#x_5*mUb?^&1bD|2oKpWA_Z?666!CsU zkYv25>Gxw-g$7bQToG2(YRJw}dyJPBAY`;@au_XwY$Yw7dehM*)*~zn{t6?iN-N34 zYa(W~AYMZLDumJx=8>2e=K&hbifOMJezNv69o1Ds%4pE)ZbVMa_(}EEi%9=P|4^R= z{6^qt5(b99repR#>L0Jh>eINL+^ zHzxiMhy2Ww>G-Dk!0Up`zPAkM&dD|!1z`9IITfC%GJ<*x-#5Mo$)`J0W&I}on&D(dL)SP=ZYCN@=XrO1 z2>K5n2yZ}i){Hquci(deF2{Y5z#7K&w6jAV)73wQ|Ikl+r2U}K@W-CVeCuH~mfCf@8i4CtjG4ahiT#w}XX0uo0@AdNHvb{hJ$E z=FjAN<&wEM^B7N(S-Z){i1~*mBoYI zUh;2o-w+KV!eKS>>l8c|_#)7i{I9`r84piLmv@Tk*K+r!IMP+eR6AL)>}Tzr`bzTE za{ksW`Ztub3-H{^vcC!EQEma&gzxbE3|my-YCmI5a;b+$!KWI`ryX`HN5FG^x?V}I zPd{TKMUg*&<8fmJI$JH$I6N_`IsKa8PK*EtbEk33@0D}Bw@`7`G0eI^J3pz#F8yai zzt4D?`WC4P=DYOsbG>;0c&kP{i3ebyWYpxM)|{TYo{~wb<^4#g8tk%H01E>$T2Gz7C~yM)>_^o zbV@lp?WZXBjP3>m9yg5vI{ZKUMPyWnaRx4?exKZj`5cxxJtssx{({^h`tPe6*EN15 z^}lS7wH~IPjkXm5Z!!9wsDpQA6LXW8q<~$W<#HRh-&5@6w0ltv$6*K>V*QZuCi$zN zkvvQK2Pz36i}hZ0mUYVZI|a7DBUC(~kohzFM=c4Apk4e*S>H2`u$y8&Nx33sf_9Ke z@4NALpvmn&|KIB8jFET!Fu^QK$|P*bD+N-WLBp=!D3s!?C| zbC>oU>jwEG$JUlhB-$x`R5WUm0aYZI1_B4w+G5H_x7XvMhcRz=sU9|TswaMzcrgBB7M2T1o1z~jT$a2s zdLW4UNZdrxh|_|4HQEOD{%7o%EIVS3C}KMllv6Bm(t?RtU! zviPNnZXRY8RbMqD{L)>) zvpql@IaLrZ7W4OH!rkf@B?FWrA^CZp1?_p#g^|Np)CX!BsHXAG;6@P?A&xYW?6Il( z#=B;_PC^`zrKpUlX=)TZ;gM zgg#3cICn6Alm$^OMWe79JbEs&#)LpFbS$#sC$mF;HM}zjp=Q2U+(6v93(Q|~D(nqZtbnjP*ACjVN*tS!{{yV?gNE(@P@g zqH;|ajh?rF!|)JCFM9&4?Ds6Uq61>(g~*@Lpv>Gpw)xa%-D_ZjnbV*vGS=Mj*HDBl%~DFY)4 zSw0&E#rC9Fa3#t)weO22tNV``}UvS{{`Nmp{7KC zSchU$xcdnmaR>BnMZdM#JYx(U;<9g-`XCvTW+(3pN(`5u|QcHtgj<6rs~Vr>Oz zRwHoTgr8KZm-?~fuhgaDY*|1v-z`oqn22e}tg}TKz#%s&|5(lw)KfdME;I)4_uz47 z>eK7$DMHuaZ;%^bn%hog8So3WMNiX*(tb2YjEBIBC1yu`E9ndMc;8Emb5^9WRfX~W z>Nb^~J$~DNmW5wC%a*8+d`*3eOy)aSw!)jG=uckTtob*3&<}NP?%7=Gz3w!R0C%IW zc}xVy=$Fj~$rtF)K()z#n)=V!t3&YJ(9&@K5c!-R?OJv@!rvpiE#QVe;q?)NCUpY8<7|E{)B^>&ylq2GkW@a!AH9KdPV;t z;|6QKB)tbu@XRLtFJJBG0{VZy??xQ-Y;H3walptG8&~kjddhk!!UE;9&BpQ;Tkx3w z;kb3=>#G0dKSLPeo@pHQl=jt+J5Rf_)NY6G2q148;~>|YzLp6re1WCDd*n#SJa5eO z0~el*?<)Mo>Elij+ZLRuv=es*&N9wY?k3+XG5l}1~!^n>m4bYlh|9}@VM4J40?$daSRoiFf)(?veZ^h9E7bQJ*a_WmaCTg7 z8KWJpsThY{)14936*!+_o(D6YI!0%cej7VfM|~IZEyhyg8u5{MFsmW+=zbbe;O%qa z8sf;d;H~2IImcQ4dPKf1VBEemUmxG2gmZi;fl`EDn}7KafJcAnBTVv{rOoI3uTo!a z!zb&f-bfvF!5e3~^>uhA>B;^HKGyM==H>MAchS$u`S(V$8P>eQRfT+KeLf_nDr&Y_ z#$aBJu>X+xEBT(~&h$UTd#MDYJsZ%3K-p;E8Xe%8;cb+w{fcY?9ak>^wqurXE=E0}nT33W`qbw;zdnoIQ zEAjw5*V}+H2eEAFMbQz_pi3=X+vmV9&;;+34Q05#JzF-Mv2)6_sX=|dN#-Cwf?D?b zmp?wdwEx=U~W;3IIRfT;sc72KC{T;Tz|5CM+BtS=gBHQ21I=CWNg=X3FEM zC3C~$MRizGJ@iij11yP;)GR#0_koe*qA38Zh5crzIRohudOP$1cz3*TiQI|_ZVs05 z(NXjXW7th|D@(gq4W$PksIT4igfrb`o+hjw!*56Yag=IZ2&Zbx#Kk=7y@?_?;YhTP zXN(+R>40Hy3+n;ydx=rq3GZkm1yvn{!(-5AydC~~+K2Kq4(`g*`U_n|M$1^mMz)wO@y8M0)qmCkp0P##t>Vbcos5Tl z5S?BPb3q9G7xRGZF!idsPxpa1N8EosPwS1DiTkL(F8V772}S2zkOw11^ekg&XP&XZ zc$@E4IAaGs&RT4d(}^bxN8twHf;m0+877+cayv$sQ%Onf$N{^%S3a=!afV_ z8E5b8zo!wtir6xO@BxZL#6OSkM`J^Pd5e9y!eb(6l^A(n<< z`Us9NT!h)X<%kl;x>)bg??T?n6!?6?Li#S@6$IMzgy-C|+^CfW;P#Up^O%{BiOZ#l z0?Lih2FZ~&$5^vO;AEa&6Wa!`<(ANqzLVeGj4R4E>;U1L2;k$$UlmNKLp^m^Fo2Ui z${J}`BAjdeWT#R7CXOq<;d_jxY^!ttbg%PU-`Wk&1-(N$Y+lTb)XdqUQ zz7sr>1=jBj@$l*ix3${9_tS_lg)9^=pnK`eG}h*EUi#xai0+X2^ZEV1`AWl&I)uL| zCgj**v>d%uT3oymNo;63Jz zg@B7JBAC>dx7~{8Mox*t%90l&Ul#w&M!I+Sv%+79zZJ=nP_@yH+BF%+*VuGjCqB$* zC;~UztO6T+iaZN#`}-IBpx4A#tCp)syWvE2T#kBjpFTI}EOe_1+zH}&+c^Ur zE+YQ`Lq)+q^&R>LzVCs@58MW&yq)2uSh*B{pr*Vx1fGBlNR*peCHU>1?t_-9)B10d~R>B;peyzXk~z3Y%}V3 zN6F9P2x2gBnPD_D;ZOSiA?>~6t*C*(-%WP+l-uv6_g(}nfPyF}qKHy$6bns?h$x^S zDu^i3L{N&@Xadqjii&~-0RaIK6%pyZb9+78-gl1rYw!K>KJOhq5Ry%qBr}uAWHNbM zW$IDDchF1;fuG?~cCUnakZEWD?;3b4UdKfWye^E#afT@+8$=wA+{0 z6%(j$gI;d}7dB7NipD6IfY-lIfU^pZ2>nRBt1(T+)jt!Uo1y#xHQSQ~y_El$@I&>6 zzon!bbXnWV&7ohZADO%J$fs{tFN#BDounR;+j{;|e}b5rs1X=#z?XR<%}2Q7D9R{n zPA1;p-@?TcgC}At`oPB%*ef-72Co~!F_+L$L$URR8Z{U%zkeQ=rzjkSj>3^_LK7-x z{#k${Y}$GIkQ0M%$ow3yThbpq!N)b@{=8tB+(!2stE_~LEX+efTSP+?o*t?n8BD$p z)ilxEa@X*z@htc=#4U72GCx22hVu^fmsg$T1r)p3ej;>L@Wfsj-c0(tYKwgmp!%wP z<{|3A##H}8e6MO?u@3MuY*iU=^^md6gzs5%&3<9wc zlW(UrBJnGRWx9DL^a=H5=m$dd69MdU663DU8h7jEl$(vKPxw=`e5kiPR_(|1C!&G& z9t}E)#*btA$Ixx#MF2k8l29H_(}ZG4>JXZYzRW`ejs|@2VX=;5y+;Stm#~2IAjU_f z1dZfUo$KgF5}5Dw3RQR1p9}rJx%(VoNl(1H!72x*)g$E_jmYR-5!*03!nXzetLHV zpBE2=Fw?zG`AnR%%5@4a5*~I3mw=#tpJ1Qr0|3>n3=0BrJ9}>xGXee`K^yUx6*^&G z2s1FT?_y!+J}Mrr=Ce08B8wuULkxs})oT^NSp5Y-Q=gEBc%FfX3`}BcsSk;lGj5AZ z{w09Pgc-GXNP~f=6Y22*eI2&{I{DXfUuzlR2}G0Qd>^0&sv(4BnF<&KgiuO>P_T6~ zKJao-^*8b1>`h-Fl!Z5+aOb~{M4v#Q>&`%JMI*DG^oJ4T9-#P}>SY5)!Ck~3)Fr}p z=&_oCn7ge*W@pCXl6g_LV&MAf!Qzlr{j85_5LU?(&oEFP$hlWV2)C$j744xHxIux~ zI1cToU_6LbFA z@kez%^F`|a#{SG2M}5DVJ51)mdDyAuP!IdYheY7E>)G`!>Itq7x_j@&>+xh^3+Ygr zU6t>5Sf5#xLl77(gy0->_RG^5`d!3D(otl0*D!Q*uyxdYlyIO`ASz!}#3$rz91AWj zM;DA*e{^7COLiqLV2NO55&ze10so^h{>l-eCj8Gv>A$#qmvImQO~QXQHsF%{kzd@n zn_B&NET8|y_+OZu;4>=z((#?BJZ%Ai(U!wuDW$w~B2a`6L>DK*;PoR(tCJhCkpGn( z$=XDJ%7O(S0Z#Q`B%%{X5L}R^AND@`kOdy0KdVHKI10uJ>sP`QFg+^uf`tN!NvqNy z`IYeZKh2z627vHY^1tw31FizvxdGki?Sz-Ow|ScUBZ8OX#+Y)nm`MH)I4WO67~>u= z_2N!%mIydFlwHZS4#QZCmE#Abzptn18N?suh@AQLH{$W`+Xbzq9e1eL z*8?BST4vNLL;r7AcOaY^PQ=kpbG~=;9v4li)!J$kg6a*Zf9LEZUkC4%qHBaxU9)I3a4rlo9N_Xg`Oi3y5#Jqbl%rg|MBf(5r2Xc;jvp!S zHS*OG=4}jmv;Kr{ao@Tz(4D zQ~jzRk|P#>t3SnI{DKGkTg3<(0bq$md5+ZI&}Rr|@TLazrxMvX3Loxe91@L|)zR9V zK&Y;oQTtu$ImMyjD(Xq{f6>#4Z-A=6Beb+{I5mj3z`FPy!X3u1mP5ZM1e4>}^;a6> z^=|5!6Eu+*KiFhUX0vI)!;dNY9T%Ur5?+8F!6&Fu%Z8hu7w0oRDZ;-bHG@zrSK*Jw zNjQ7~;YwqQ2y~cC_mlOB?aOMpUC0KtqFm0xt@e}XPC2m&60N_1<6*e8@R1MM5i&m| zk&*+&UxQyCImL2$AvVidS6r9IA9oA9Hz=2brf(hnx#r#>532HMV*qcO zF;X<+tn2A<6CD@RkP-nKHAphyQTQnB!>=od!h^BbaBexKEhB<1;?~cyLDYA!%?CdJ zP49Cr4|o!iE0w>6Wx}Ztsd<8p*Wi;I4qPU^wp&~%eED-V#k@ke5^sNX@ol=yt~MMs@!CN>K}eSFd` zfDsqxepc`?6LRh1`D+)TyIU`)Yb+SWmv~$PLa>flE9{p6|2`GyPJ}geZ_&i@DD-!T zj4U8}vAK-+%WALKpqSJ26jhaWZ!rdnvHO)zF806l0(A4?wWo4mIH&iVb}!IQUax|( zCG@MHwezS6eD-I$s|jNAG|6>p9B^)O*NPvSKg%85cY=lD((Fq$v*`Dk(r4beaFp_| z6ziKa5Q zLGIxEhq%?W{_Leoz;k8jkYxS3X0cDN8tIni7|Hu?N34C^0% zT=M^aQsjUC&%fY*Nfy1M5X%4ji7t|@8GjUE0jAxOU&IorSKbt>N$44+<3@~KjPLX* z#&sKxLDC3+^*`}pctKZZm$!)V=xEm0y~uCyxWW|DE1@ZRit!ok_Vir(ze0bi2qF5C zxUJYF+6xg+fMp9~khcGWSKyIfA%Fk7efggoi+}sTVtz1wnc#OsiY>JNC1T)4@~_0b z$7;e-{3%O#nC%2`$H!0ll|#G2c;wb5bjad&Ixg@Hz%l3$;@pi@H&EXq%wL&rH5Su$ z=mY&mhWIb?Jaktc`M&0DgYQT$&-2wVSe!cyihJblpxbGSen;JWvAqeFa-{zn;kMv_ zJgtG)_Cx%-cQY{jHsYT$j-ONhGmfseP;V1h=S`$HBEV`m!-cfy~8kSbglSesrRAZkIgCyMw|ao(J9QEjf<~m z4wWPP%P(DRZXKqcHrSeg2kO^gzVjhdwxwuB{zstmd&Ot+C|8mcn=kGSjCE|J7^lcg z>z#xx(A{=(n#DpK4E3A%(-^OpiY#%9V>KzWDIK`{J0tBw)So%v%*iv1Xa2Tx)6Roe z>Y>=HF5?n@KYBhf3%Cc^EyKX6ZweO5PTi~MS8~Tte}t`5J@`Utv%T0jsf-|M0hg|= zo5)eG>jo2I;BD+v@4dp6v^U#0;E>;I?>F>UkZ&RmXMU#KIK3X-wBhd#s`GB0VJrzc zn7|E>8LVrOekeE`2!74xk?U9J&snTq&rr`EuctEy_{Q3YLVwe*j^>R~<_AIgTeCm) z-=){G)iAhtlIA}^Jzw~b34cYiv%(TlEVWEoXnbgz8ZfE~Sxv$8x zfO6fi?gX#7#a;nn2_tYnPvGQ^doCAE*k~Hh`;CC-tKgiIM*1!0GFjlQcIKxc;m;e@ zv$9UvwT%@{UGhJLXRCd*w>-Gs>_dE;am{N>z4D;iNa7J%6FRTPa($Li=qW;|px+M#1AO}9m-AoppCK&t!w8Xvvm-V%mJB%M%;B=Z zjy{o4E*boZ4vMr(m5MeY20z00dMe#)M}4c!Dc1G0)52ejF^b{*==3T9&Z0IYb8?q( zfYUOs*+uX@`*{8-c_PZ2VBh2epIV*XF<}Pn9!vQ-(wcERUgmNs=rM6=nfj%})bnh9 z+5F|mHFusrdW!a(Z~TnHLzJKG?Qw=M9531z66?@^$J&&1fpDnxNEp7a?lYcset;bx z^VX`~z~LCXlEFh`gud5nM!4U|<6?^Nvplq`5|5ioz%_1>jV*JNW{jhr`{9Pr8>xlN zkfCNVUtNiJG!BZ*AX3LX-I{TD%Glz41|8IP_rpnz)J+v0Z?Kj0PHH(E*2sSI!i$HP zr|@F4lm<^YahhuZmodt)eejOUdyNdn4Q=%j*;rXKRgpWE{)BZ=F>rfdO;Kb|WQ)AL6Kq$d#O*>XqvTit6BG-a3Wa4U5L5GP z$v=i?n;GEz{+Fv>CYo2_?Bc5%X{c@Xch?|*;x{t;WeVbZ-e=BH>RW-IsD%JH-|TJD z5i<$h&Rf*?q`E<`BYfMKqhKf;sAPfSVx@lqvuZr2`!hw85Y%I*aUJEGbD(%L;aBPl z5uCVf_e)$uP?m+J8u6M)uuaI{0>KT0@;`$0&m;WPKPjG!gI)d)@uzN9vBy|L{)*_# z>hV2^9cBZ*XW$|JE8yv*Og)l*w~W2xUts;pI=uTB16G_=yxSQ>{^q#QpGy8S#?M|E z1d3^SRzBl(a?gnm&cPr~e|9S6;veMyuJ|c=>fqFbQ{B%)NFLX5Wt=apzu4pI0rD+8 zd*4MEz{$T)U%SvAMb5%2{c{57`TNY_nIF?m%^d$MV4kaSrr`W!5OR?>z?wqBuj-;b z3&b2#@Pzvi8%}h;K9qfI zA>Tjq2FV#Y=jEN_M_RX{nMojCo_%0>LNSSV3IA7{Bo_St*T4USmjAOrUR-Y_f*e@O zFAtH*CKSsjX+MW_=9iW4l5BRGm)Dwp_wKe0UXsQ2I7eet%z z1E?VGVV@_J?v{-Jwupg-4pCeGsD9!5f9@~-(eH7M82uNFh27jgk86t_FGgIBP?;Z= zgY>vSw$q4v!e&2sF@6c+6=q}{@K08w<>)cA-QH{eNWW$T3&hrn+i3eu7)dB2lo~EY z^}jk_I4roaXso(Z6?oEAb#CMu`W17G`2ppI1jB=c4Da9G@1mlI+UudE5{>F<4ITOg zejUGn^!oaUUQGHhjyQKw-|IyOi!Rb$BkK|KNya117#!Es4N!$L58SbRm_8V$V8h)(m}%ZrOSx1f5m8-#y0&_W&F;ePZ79&z1-OUUixQ^v|ps&i5TNUCtB%< z3FEb{+FLE)({=H0rzy8kt(1ksIqS3*!OL&sRV&^`f1cNWdGJTm=6>9E7R3H#($a~C zzbxY>(vP56f9t;ve0N>DmfwwjozDEcurKwlbJsd+`JQ22l?A{W#?jrM2n=PdGu9F6 zn`NvGUq?N6=tc?j+gzn*SR*O_9sBtE2p`rXSbU7aXNsRLj8Lv_=xe#0WGvMq75K?9 z?F{bUa;@C{LmYjoYWg{KmGs9k@+Cb^li(}%s}02{-khsD*o=$0+?*&IK5tR5RE{8H z%VHblRwVC#r1&Q(_m{C*G^ywV2AGdi->3fLar&BJv^SYQyuww}@Gs+I-U7LuetxGn zsCP(z7IFV>@;__bEABziEbZj1LHx`;=6ue0jzyOXJ_k3lW%c>K2usnX^y_0?-IV-C z^$L^u!cGQ`%eWjfo-F(h^k1z;=QEB7Ky{4)l>dtDPfy|%;0TH^nT6UR44@m~6k>}i z{75v+5)a~E0R%m}q{~5-&}jhok}$^l1zth@PlzTwcqKS3wp|u?d<*I^UKjO%0`SWn zkLPFT!keW}7hj>j%Zz6FHS%FwXu(ghGk(dOOFuWGgZqneUs)fTFB6tFuQ%aW!3O%z zJbJIVD+Hh1172FuGnCt*RzWGO8$nyq?E2k|M@yb&Tt8Hf9M##L{aN^L;QI-^_8sIO zsi&&rw0qdDXEOewF7`5W8tshpH_AMauZmvCxNVJ&Ob5=C+~_Bzp-X>kXoT>g*o_%? z+V4~UJg>Rmn*2{)dnq5hV7oUAYi5II6e`PGn*L(%hN6$a_dh~EBt1;n#TuV>J?wCd zb*uXT<-ak8xut10#|UvDOH742h+vK2{I{HNaRd1s%yp2Af`($Mg3@DwE=T{5scbn4 z#y&U|Q3*He!31+Q@g9go*;1a>>j~;V%ZBt8>d6iAW#i&rv|mf0e;LQCtg63(<>2qVhwi=(BhVg#t}559&7U*e0yK(UzWQeTDX6?a#JAY+fmo-CP9b8Zis$&*zkRXT&h;FGh>&%8@Nzev0*{}AJGTW00lU7SeGDEC;|&g_iG zUuv73!~|a}laOj6pgfn}JL6;EIe@hU2vxOzJ}FE+YlW&SzvVOOOtRkxAv(ah0V%vY9^ zJ+P38=OA;T^#+2~TbV)T{ow7+)SW4>)BgkzIg5V3;2ClhBkO1Q5rR0q$YC?P0uDvdrxagXMB?$}3H$SvBREhfc z@g!4Q+7GL@ycNtxXRZhArrzVm9tA@{%<%a?8TrFF#DC2D|F55>w3x_CJ^vcJ!%nl} zf{55q*ra=4&Sk=FD3qBuV=5$+27EpJzkL`R2cZ??JN`!Y^OnGu$Di&qh%|$i+xB@?6+=U!_qT!`oA#`ESg0~{}#Qu#(3FRm4OMk%w_1~k_{~G_Zp}0wo zsCk5Jl3GaE2Hh)!j2)XDx5bhxAfFHyi!JJb5tWS#svd$?G4V#&k_cUR?DT0D`;>Wd z1dfG6*Pst@p9)ULk8YmCImHC>_tA|sjL(_sOmSua)_6TtPb8dT*0F@aoG^}IC1P0j zT0<@RrC;Yy73zH#TQu+&+lu+dOy>KfeaJo<9~Va)6f;U^eVN}w{yG&VelzY$uF)BleOzz1m|wRq`l>-v zUvSYQ9x7IT!hM9R64K22gvY`Uiw){|^XwgRWN^84)-9T_Y^n4HRwCs)#3IG*pu1WH zg<@NpY9+r=2EOH-@t+pQj~o^B&;FfuAM>xm(Red7{6-@5=-uHwQWVGQSUK-;P~bv% zUvxZhtx9}34fx&lC4*x%p}TjpI$xg z2yt={{78Q;qGNlD??3oI`@o?KIb4RGu^AjGx3hzZ=oXITyM&e05DrQ4v*p{a zPyL63mEzVW_K}(|{;rC*sUR^z`}pmP`>8qyef~jyaVk@lFcqo?KRA$zzXggQ<;Xh3 zdXo|tpyb8DRN@IxR~})6ax#zkPD76}9s0lQ!eS>1E6Roen=Q8FBfZJordS=*sWez!x+q-3YCxfq{Q*(NH^I;@h+<2L#I)eG`dib&z*EEcU2G6}kHw6yOMj?s#vJN>AsA_9L*F~R z>p~AQ&t0w5V(^QXm=}wt7LWGFMA4viXS1i+Ht4WbWPM9TJ<%<_%K)!(GmLS>yJub& ziBvzF`3^gusmU=`TiW(V;!&Y1!GTNT^7^PySaUePst}PAWAh%EWlTlzP<; zf#3r&M#>I1`&RyLVPXI4gmLs{2zOAyk=$g$I!0IbH_A`LG7Cb*GUk3!En_iNSNv;n zaCdLq4^|^|D-5W=)|(S|^Kd{Nl{0gmU|uHvDg4(t!~k)Pmj2VCI>+17!^$FTrbej@ z;F@nPGruF=OZAL1;=ehVVnVFDt!8#R())8*0?u+=Fxq6|3q}@>GpI9oP9JszLv&Yqg(BuBJ>ggb2(yqkUqI(g<7AAj| zn2zS^qx0R*Q(sE?lv@(tgPz+Z_mU?YLW%Y+tjCP#tb{9}?o9Zq^w}v8QgD!`f*_Pi zBg$9KXw0yT3+*eQ-h$iQ^KxC@bfVLeex%<^>{RhQet6oslB{A7q*29QmEWQLClkK1 zs{!XhYhFkL*Q4n}qaa+FTT1njM_g~{(eK%CAyNYpbtOlq7)PH+WMZp-=W{!~eG46$oI^zlZilXBIvG@Z) z#(xR_hW}rKA`1R%t3;juc$OC%E~#HE_R0d!8Co3zJj*a~y@~LBbwQ5!f*NS1?jqj= z^s6632ex@q7G~p@U=qTUVZ6ZWkT4JCZK)uMR~hOxg=9RQ!k90P*&E4a1DtHE7QsYaRhLkIbHvA<`FrSL znZ43p%lC-J><;?f7W<`SKVTJk`JTY{E^mR+fBz_#OPFV)H7pbS^jr8h_^qf1mA5q%lZB_a4ev{CSG&dx%zK;i7;0qdZN_q^z*de8rzNcg;D1Y z5l`*L3M>~}l0V(GUB=Hp?EfZ5j{1Zy(%`*U!`a2r>)V>|UkB?w^1B~-HRKi+ zp7mFmcR~l9?Pvt}^}G62y9ViB>Kj5s8TLk@3W??UUSz+YkVX01gD>SM$*tmy$p_!l ze@%bUTE{qiSXjXsh`@FHLM7RV7RF*-#YQdhqvUUG@SB^@yGzah_j%`;!hGsKVm@sG zZ)lG_S!|5_=e@j=bHICtThF-yyfg7&54_wStSxXjHDjAaBWTRhSN#a}n&sz|`H}KL zwb_+f5bOl)Sd6#V?CcZQCc}@)_Z!G1QQDr#Msc8CRYv9_qZnyv{&01;5Q7 zw6Y!Y$3i_4P7`m3sc{PBeqvuQ_}mSQ>r4CZc|SVfMRE7on!+&fHuT%L%$xcQ(FXkR zRnSHJp}D`i%f(HP_mnqI1l`bd`;^W2VJb0Qze4-(2RHjisehE-f=e9{fJ>W<51QDn z>UX|3<4;ZES`6uI&i0(sn%}lE7s>k zn80#(y<9djx0Q%Y8!vSyL z0*O$9&|?tiKSfX!;{+A{V^j5yWvB3`;<$h%wmv0%7eC*uThMGSWkC$6j(Ju$vGIrB zaDe(FYmcEf>knM$AX?X~guN-Z`gG%jJ>)Af-!f-1zuWBvDe25hf$b&1kLY7F{RZ$q z7c_EX)XOcs_)~ASIjm|%`Z(Uq97g&_K}SWuaY-}>pHtjgWrOk!?TWs-J9x0s8Bus0 z^I6_~!K*{QAC0Qw_QC1m%qdz6TqATFvjg+C#_n$opx!+1de&e}9MtUsw>o4j7e1{& zH(cvG$~RWa?PcWSyusbYc)Qk81)nzmG=8+s(e902rh5Q37%gADY%lt=C1HLtqfd&wjHWXI5@n|6|5&Qqi)azhKAxFMoUj z{-K_xUqq1G?fo3-3B62l8+x~qemMBmd7JuI>Z?{8 zLaUQE6Xh4T;R@{-1Lpjm^|c$XfvdylvT~7~tf=|=lou97Yq6E=noMHKlm=-kqE*vT$lVEg!f`$PE?X=1doC;pn4LoB&{Rf7YB6IuRb=~$TfBr#W&(-x-Vmn_b%xV>K^(Y z(qA@)axY1aCKt;sDDPH(jRPEI?=SmP`bH-9<_cM*h^MYksgM8yRGnTmR0+l(ejxmK zi1DiSV%1sY2O&_*tuQv(z`{|ba}}>V<5+!3jgKob zaY?TwMUrZh?p7&Zeje*#TBROkm>?_FtSvjkQdKkhrSt=lek?sNV?OEQ)HauXmM;uj zg^YWZN6U}L^NvxwbM5CUj{@Gx6+>12U|cJNw`bf6LeD8|Wr7H)4bw`c(9g;>D|ami zf|fdg25~jv_A<@N(!a#=i6tU1C)^+HWo;(^Pia}Hm6^vwnsb>+E|RC)knI4sz!V!ud9zF-%a%AP10es>L>cY z%3QA*Z*c4VxMnGVtwI9%|NDsvzu>DF%~D_dm(Rcd!Ij6pIET0#B|?v|?;Kbr2nkDf z$yOZ(&iE=h;(&qvkKaY(q5-3W+5aQBN$FJL`#C`Wnfwblf(O4ay?skAmvDsly!cOJ z`_Vpb%vqZStPylG&75jZBLDs9%7}B>V;g%m!>VUhwQ3TthQ}}#JZ>!wkOeMehY~{I z1MnvboEYnN*IkJ3rQDB_->43x0>_THO&SOL3Pz`onqd?A2!9hEAF2mDYy9bcA>ACz zh3=)aS3cBKHU!R&oK<cwX|DN&)fbk1zllCFk2ROtG@bLjE&WU zm=Y8T!wq&gW60M%n64HB=RzZpH&YXiCH@*^Tq+$cKS(s+3A3VUa#WbPF6(}|v~lV0 z%(`Nu=!|iHu}1U#HLHU-1WmjYX`iu-b~d9Sh*1Cj;E~wPz|+C{Dg=FmXWDOwjgYEs zB>B`24%UysC)~xs_}F;rYvZ&mn&=0`mh zyWrDq(8>-%kJ8>2du&J+gf{p~Dj_^#l=Xk0-uK)DCxr!Mt@E0^mFE2rG>Cx*>Q2n> zwgcAybx}0p)Cidc2}6IwXv*+Til#)Y_zmE!+0UU~3^Lc(NX|0~@}`hR)& z3|;2M`9~2s1u!fAmA4b2*E)zr?zn;M}TVLXf{L&u$$DbJd=#HbEmyG&$ zhj{Z9Ec)%Y@YeeD*S`}lP}0xGF}PVtyB+oG;pf52xsh2Zv}@O}R@xoOSChw^XVU&R zelxEd_03nmi=V8>;n0+%d%?Rtk^afhqjz(V?0iOlb{4nK2On|$vr#iY;ofG4Wca52 zl2H(PfPClub1waeluN%k{U_=_%C!o{!!ZjR#Wh%Cv?WK4oQ~X(0GJ@QAl#*%A&mx{t&AgfqKaO-a&WRte*zH(*Hl@Brs)rg% zeaC{|LySjiBkQaD?Fd>apt!{{&$tI$Qkq48T2BN%(O(d?Z@Mnm`9CdQJRWsAq-xi@i3+;+$^JX98O5Nl3$n zwbY#^o}GgWkuK(Y5W3Fqk(UHOi+C9b!o({{e;pJv4m?INB3JPlhIve$oF` zc5uA$*jV7i(qN)Eq+|h!in|Bn47avS!p+>?q5pn=ub+&Yah_9U5D^=X!7T1LtP4{7 z1jhA|;5&C82-3&;JSP{#A8Di&cZRUF`;rYpc*jeAE(s90nif{DZH(ftWiL&wf<~=t z!A4sc@N6CfolU+U{HOsQc+0&9{9j2=!8DrwxydlX*O~veu}9t3!1F5%5=8f2^WOKa z=lczAQ|~3<>Erj{n8t{`<-8-eN@G=GRh`S^``Ozp3ncm#@fvTaifC3U@qZC}e8T^J za@1uJ5lxoNm;8YDfBF66u#~?1-+q$zU;2MLPZ6w`7wVA1K>&&W!O<Nm--|U|z7>py%Gx{3U32()B@;!v@@JK!wxKC)$97X(oqm|g$_%=G6WyBTY{s!jx zNAsvTkoXznba0+9!hTo!SDHWN0q+cA5cq_!^gsPxNq_%^3!x}gY`cIiCIVGl3`-`I zUW4DHNlCF8%Z|e>UlegMc4tB4Z#l7Xq1}Is+shzI(5_Ki)e&1m9=y3vjJJac{%ih3 znl123JQxL!PQMdBI_!t$4fuWZ6xeSVIDt3_B^zd;nHx8HuBgk(4v_a9{)%h79^{B- z5c!vRANreUM=mFbfXD(Uj9quq?KJK266h^j=psSfuS5IObVsoOZa|U0{jru>$^sr-ygZ9z4Q`$BrsfL5I+!{$ zKgMcoGi9D8>WAfs5Dis*xi#rt=g;=mQNFRc--3QZP0YTb3e?k1?H5C8v~JbK-;F9Z zu4(W;@~|~03LPhIj69!u2l1`u0~}WxiS;9G(&39`eoG$`xtR^eN2N}p8#Jsp-0uU% z;o`EaO~EeK?TweZmoQJ+Il1-Z5nRNBInnO4J1Bgcyg{gsSsz>UGq%iFCMN^#efl|v zdGN>UyG7Gso>SjjPlLA8jaWDWT(>&gjBlB*iMh!I;A5ngwJdxa`Fj>WAezOTFHQ|T z+l6?8Ygh8}>DQb2yJORd-|2lM8W25J_YsYwS5dX_dQ#sg)z|DxzUo1LV;uQj_gBj$ z0>-Ph%{bU)%<{4zK|^5h}%S+HO|Ft zajLSNpdFr6>n)d|%=XwIO{V>hUN4t^Iqlp~ER}js1+EMHICpI(N2Ka^<0dg>;gRF& z@)Q}zO{YW9sq%5?BXG$EWE}PSz)_J<_yyy}Po9UD^l+S)$p7UhNBHvHBbE7x(qlb%E*zZ|A2nXWSuD1 zB^?Tn1Ly6&`P&r#2igo{b^&w071WX z`rvZlQ|2SbcmH-X<1;##;k5$JONjkPfbTnFY}^mv3xTVl|NQkq`BeB^VCl7yhp4x| zalgDEqr*>?9+tKk#!*sfT7^DHe%H#5uDpj2f9OBe8nCTpNm@Pt==%Og>~aiisy=Kt z2O-K7Jt!KUuoKo+4+g5D>1q(qo#eESji#YV$mk#d=f=s{Ns0f9WDy7uOP=tT0C2{n*~qibOHgBFDIY-P�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

e2jxoS<<_(nt-46k-f@Q5$;tks;^UD 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 z�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ᏄmL}wff>w|4c9Wj< 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%0D)J%qEcf>O=D=yAf4lP#?Uqg|A7(%4x%5|w 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

&R&3x$DV|030Iltx2$$?n_#Tm+b3?-X8wh7 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

EOVj)P#R~nd>HUVDUNbqTuoPI^S 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 x%$&lO?b>KS%usOHfTK{9l7FhA@Mu` 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

$i_tg+=|65pcSef!a|p}4QfKs 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

ccO#tQ={9a;J&c_drjnK!S7k67TqE@C4i6!AP- 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 ^a)U&C-4Z*UDeLNrk6|X~R_V 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

YjE#~ZZSd!&nq+gw$Z6EpYoOQ7} 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Ⓢ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

_KUH?4^3Ack0(4;^6#q7ek>=nzK) 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⋙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%0Ao6|Hv7Y;qd{g1`>@BFR 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%0AkeQzqJX6MRyM~bsO*%&BXVYl# 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%0DQ)}{?)>TH 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%0AkvGF^!;nVFkulpVi2<=pRffw}_ z&!#@kHI}^QC(B}dU$~|~$GplER-M3C;T4wh(`rMtu`5TodrylqYod8qI9Psy4B^pZ zlvIVXUcUPy3^&GL`d|vvQSD|gW{Gw(f&ql8Rv9GaC7~!N?S7rh679R+xoV4k85BG5 z)se{0{B8l~eV=EqM0fV65O}l&uDLW(*qaMfL4}M;&k;C$fV1*EsQ>O00jnTdZ zkhqJ!;FiAr=>y^PH_aJ=7Ux(-=J?z&4>+TrD;SCED@~3)V`)MN@fC}L*AuRrJB$QH!s5ABN>CAiK`6c- zcNV)fXi=@P9(8YSVRp{_ZTmRZk6toA$u?NH#n%g#i*SFmX9I35sWnV)3BGCxCE-8J z3r~EeDu@7T4rXYSENkfX&e{HqHUeKQ(JhhUu2CJ3006N>-)J8SwgSB}96?03n}5M6 za7pV{fe@TvKn7aQ)Z3>$`z7#$NJj{+kwi|1&sLdQk( zT6FY)_A?%oDehZ@_ho$2uSxTDk-Oij3W-*Q%dIL&MB$(C4JGjHyE0W380!+gl8uQz ziVm>wz)1rP%`#?Wu5*h6l4odX%E%{p%Lc$erS3T%rRNvVd6(racu)*(Jm8vnoFQ}y zrXMn9;yR4dozsU6aUBXa756OPFu>6MNMGV1os5fDg^o5zPr^@lDhPxHmnqlrrEd^^ zm_1{gu}%rVvU-;H(|_rc@e+R(43&_D`V^{^XIpJo_@R}yhh+jlE2ACYwBOwK69MFS z02hzY0^=4-miH`1+E1)VDyh2CPRrnK87YiHep@f?q`vTBe$_HHa6$QG{oqFGwSWG$ zey&-jfn4$-=}9=*Zu?vO8(d>-eK!J?ZFbUJ}My@$|>G^R2>Q8>1le+@)zZ z;kVahD;q&;@e1Mcf4ndvU&U;7SHOjt*$}u3h1pmdr5jd}AwaDm|NC743}C5DO^)8i z;_+T*$pxamlmTfG+%kM#?R`p;23%J|+%vk@+z%18n&2enOvyUY#~ml~V8?;1G6RGY z05r>q?U5*`4In~LGHffN2=JXR2Edm+OZ}czJArxoe6>kekWywN*;&gUiTj9Dgvdv-{L zOXkk@zi!*;D{PgfN4(`&XHyWaePh4`!+yo3QBvm8W4hqf?4 z=`R{hlArfrj7B*3uoR_(V#vUw?Q-4$=Otzt8a)SWa5p{*Vn2zR29^_3$&pA%$ZVu3 z`zj~o8Le3HC=`EFK_}Ia}+Uuym!rKX2~Xyn4Gj zCzA__K**M~AaH9&AeYwTnEv(Y+u$Ar4tF?U3tXgEQ+Vz#NKrR^M%(a40o(l}3JY_< z{Wq_h890VL)m&iRAJT67u)zhq+yWc8!;}nPQHH=b^9T!vu^Yd`_}#F1McrEILdoPC zGCc7>fhTj+L*q7EGGGraGf`J86Q9cM2F$>PSoGc#hFAwF3mnzu>6%mTk8$aXYe^4> zvRTR@wS&IFMfxla>HaKcYElp@L(tGx>(k;RfCJ^s-?jvLGZ>BE=m$xnzC*Daz^5iO z_J;I}??3#h`JUXqQ(*b*1qu%1bb0x*d5NW3-M62tSoY9i%O*FFjgDW9wi{F zJstlmp;j<(yp#ymkv^n7(6*i8im=iU%k+-pNj>(>x_!bj(|-ZZbq}xk37h7r$c5q} zxo;V%&|i5?UP5`oOrG7A?>z&xoU?Wr=o;^d^(rh4c+)uG;Rs)qdO3I6EPUEFRQntg z>5A+}(jNWDayi~t5mV7}`bNWpCkR5tn4$3&DRds;!NV$+5kf|q z`7A68wZI+?UQxb+uJ6w5bVf$x!h5x%VFH3gJ~asEP0}-fNmpQsm^%=Hh$ixB0e}(! zX`iJH0D|Y$677F6PTC^89bx{(n&4d#(BKOJoZU#Q59-o_Tw`fIu+H=vE5Ub8NOpe8 zG7dp3D#>Vc;=15=jf-swLXBCs_KcjLCo)8F24onVVS&-`1#?$hB7U`^Myy#q~GzdDcSe@vK(Q60<|HygaHB8 zFj%hnf`sJ~1DTabA%YgM`cov#W5CfGIp<^EOp#E$Q7H5n@8p8znP{3YHq3!A;UQMe z77eA}CX*j972qJoN(%RXp#IWkYTy;W;-_EP<73O>lymKzrJ#kHQIB)w=nfXl_ z5p(og*39jg2i%{b(CwHrozuS+S((U?)jQ@I(-7uv-cartF%3pKt}=El(*>V&Ps%`@ z7hS6Sa6g_2NKb1nOeMS%90DeBqXl<#8*81`R%%m%%)*GFDXmE9Ec8qrQRrIyN}wEY zkv=Eb*{cnB_@3`B-H6uW`0nVuc_c7Ki^N6eMv%c(C>)Jb&6$GL zknLn2B$2?U2m%?%9HAFqK5W5e>7H^OsQC|N9Q=R*z)&H|-DlF6U4Mol;>slOrbk_S z+;ui+NjU~jYzUNCVUeuI1n0bEYc>SaBf{0KkER4MD0r~Ru{v0n2LnA|UZ)aZt05^I zAV}H_^qLX#N@Lb3uE@6u^|9z5?Vfpv6|Suf+M{`N4PL@_VgqRAawt!W#sm*rilXPS znzVF8m^FRSk{;oDR1j+^EBqEe{6u8mHCFSg`S#U|<`926?1gec4EnxdZ6FP5t{~hm z^!4c3W%K;|HUy50 ziL69cL^E~p)e84&@CYQDo}*BtaHg&Vr`rFzE5F?LH5t5H+N~bphD`Vl$%83VRA?)C zbP*{~{2Y;PPK!_s6+t~@n6*?u<_Yqm%rMa#!dBaJIe=rN5Y!!Gsy(sBImRJDTSHTh ztVm(E0>0!+-EN35-`+Pz_uvxBRR4%TmPfV>+uUK{W7)i5`36B+Q|1H)1THC zxyD^MBCw0{n0wdK8!@FZA>XVi^cJ`>A3y&b_iD0P0a)iy9ZMBT8LUeVqGZvnXsL|9 z%A-nK?i+wZ|D1}*T0Z*?WtL5bAH6J_=Kx zIlvK0(gYl{%tzp}v>nWox?XuqAH1>0O1wkJ4N2y3@bIPCFlW3(!E}6v*9gn$T{F7< z$eb~r5ZqDE=T`_*rc_wZ>wdgFXs!!uTU zuv50#xYj6Bv1(|C{N1#iD%Dw=9h?&wJdYBzW!zOnoI^DbYk>tSJPex31_Va9ayXCa z)@52`rp?q-MJG`jC37r9emkcs`qc;CA|WsiYhCOa4}UT~Ji{PS7P#Pe*fw4Y?)n_R zWsu^ZOKJ|`CtiaUL-!~V^sf@1gM!wHa@B)5xQ!nRJ#JrZpb(M zOM8LsUh7!=v>_5{;FB^q z^>C~`Qj6u$AkTTay-Jy0@qOT8Fp>gK@8wFjJM;z$hz=YodKh0!ZA_6jdLOb00`IDD zsJumJ^1kh}9U1{%r;*$>wbZX_inzYV}L*ehAakXQ1?9Xvw?&$5QScbB+%SnG$d zSYz?IelE$$u@<*#@c@hFI#M(omm0!y#fz=+f^!6f!*#b}en3q`tI_p%&hPZOzFS<8 z-O+0b4v!)m|bN=xtsZlKpxgyYB`dCTMMoSa=NRzFNN8 z$c}TjGuxm^>e>oKfVtX&7N-F3pOD~O*C~WFIC{n~kOc<8U{u})!Dw`?OAyS;vnB%u zD^2504f65y5ux|G5#z2yd+yPv5sb|2NDC~UyH6xA|M5q*p4&8M%!R^d5T!6U>X<@s z3`6q;_uCb2Tp8l?=P#OPB)acoky)d*XPYS7VC+5 zjJcP90)iI6h*%{uVHd}D8r1;ToGvdFAyKhAemQHg)f?@p2;^zBMG;gB3d^OB+X9Xo zL1&RsQ$QM?2+{hMu1JZ}`gPSy{KDTrGoMof9562C;#EG4%XqiA zX$q|4O<_@X@BjfTZWt}y1)v%9WU5NDQH8>YyIxl< zrQnSL4EWGvPPjKC_<`j_3UoaS1C&>D2d_S|cW|HJERr!a7bqr-K{g!_5meZ8@j9q5 zj6RXj`zzp!GCfdelkTpMg5o}>d)&5i$eKjpdEb%U!-Pz#ujS@?(_fS%pd>zoN!5I$ zOim`FYi>c}k+|TXLm9ba?_8zlE+vtkWn?l_m%PMsw0>Y|U13(hLQjX+qRjBDG$5G8 zxiGUlBL@K2xFK_uF5({KYPgxNWe~Ju>GCTzj~Z#g#1E zQYk@ce`MF6I~eMcF`S&eB+8t2Q;us=H7vAf3~oBpEso;iR8~A>GHc2bZ_*dqQ;Gt= z#UsJ(cy))k$nWlBZ~YYAdc70~-?Ptgn?tnHoC-wnS#7aBe9n3uD?nIn@OS%&IF8(* zTT&p~I;Qf?ixv~gxl(9zkh?gGX zYw{ZSUdF4|P{$)i{o=YtLKL>`bGBn*KFKRMO#lEu07*naRAi^M17lP2Ze`T! z0}!~--+~zH%y%hdAqGCqBNF0-kOYy@7Wxpe?vov^k>=)ev-|YE*?##5zz~L5hdW$X z=KU1GSQXB^6^$eiMQw45tFDK&cJP{cF4tXwdj>t+K0d;j4dS6*2b@j|?6`mV*4m&c z{41?Xh7rr!K#TeNxV-jQT&^V@m?T1Lx&NwJV*yjUJpchI&p`la8cU|{n*fIaA6) zJ75BP&XR+T$WGqRCGfg^*fcjJBiE&SI2|@8&tSAz??EVtBtadtD?~tB+?fJONxK*o zB?mnKrGP~azr>yY?;*5!MaCuUL@<8GZQc?*WKW;@U(=W881xWX$Jl%~M8~^TbNw%R zYo&@{9tHzgwk|pusR-(OF4lJrOeEa`=MhR=f6tmmEnAel33-tFFm4mZi+{Z(E?^5f zKzNeNmywFVTd~|~%AE8BkHuC@JAtp;Tjl&)J8|ZH41YZ9LjF#Cr!1ew4b)< zt>_4Ci^pVVlo$On`xl_k^S&XrC>RZ7Q_e(@5r8Tq!pZ3ln5-JCOv1(B;;hf^2}lu- z)@w2kJ(5rb9chn1TPt+1KDTEmaNy{OGWtB{E;?pkNuFrh<2YdPJ8`UgFUpYtPoXd1#0Z@IgS(w!KUp9CID@7MfyXPm~nec|vsW`SZ|UlvMht z5+bBZsGy(#1lQ{sXFk-~7|JQG5g;sR@#n-S_VBk?2N2jFCo zp31>70zLDX0E}S?W21&VD`U0)6;MOBfRUDnR;QMm3V*x{GDvAw{Nz5xq>aTmMWh;2 znXy)Ltr4Cg)D@)Cg%%XgabGJL`vd$yXhRD@GQ66Mm z6I|+*aKeGN^T5F|rCqTO;eio$t!XI?i;7eRsN0qZ0IyR-9rasy2n)ZR%JDIAS1VS5 zsxVC?Z^jUBR`)vnvMB3P7y%mUvVB#v_4#r7Ep@l={kib-qPX8FCN(B;>(UPIJvG-~ zHl9!1%AV71TjBMpuM`6N;JA3rdtIIKJYP#NmMxYoQikQ(XKSmrqW)n$Y7t9w$*sV6 zr7Zhst$c|)dS&Qka5!E##$>30N59pVedlY1$7M&tRwc%<5!e6mv*NTn0S(QE>*m98 zX;F~UcFH33DK0&etvi3<94F-5%#p$y=3>T%jYCWdQ^WiXMwk*{vm_8f;kd%x+I+++ z{rp4rdK?~J;_kYTyD(3N79o*a7Fym6rq?IC&H3-XZ5|-LHS;OH!Ave$ zED^L6T)(Dea{xtu5R1XrMJ6-b!>~jWkLrUFL!GTw}4lqmymBxYScrU;~AfxTOUDU1`jNWU$?|1^|c zOz#pN@o$*e0B{JG-3kOr0u!!L4VL+oK%d_<2G}oHOM3GU{~hJ?yqSLcu9;%3nuJ89 zVoSc*Lvr*^=;HzfV};P!kk5Dvz7C)>wQz`!`Bki2erHJyOUyP$Sb+$>304wwCb8O` zzCp6lTfVVCou}QgZhG*NEwB#Hn+8}L)@42DCPpdX@P5{8 za-OnboLu4~jtW#hN>6K|O}a@_rnuR@pBFe!Tlrt1e?S|?;71=NXpa|2m!LG306;2m z`IgUV_0Xt_8KEC;_4bNAnSXl!q51p&!T3>&(W6G2zkU0>d5uwUetz6sGacl5)I&TF zw$lJ4%?A)Z5$9Rz0gsKfMSp}-+@@{&s?FeRb+%x|J#o`}pc*d)5P_-V0k@#3;(5VU zK|rZI_~(b?VYw;0yH6jxCmqYckngXL&@E5-ZOJP9fA$2;Nt?dDO1r9_YFAetC?Z(v z=uYeoe}P8ZzD_xEEV^={%-XZI43$6mf$dTtMjr6K*L}y2*FC(aS7HWFcf3nL%HuGG z;M*#ETKwsw@`|2D;gR}P!4jGf_#%;yQY{yTndl#bW zPuVGh1n~Rk%prn&Zgy_xR%8!y(qF&0>eUpNi+iX#Z@Pg1-NabHIKQbIMIx06mzj>3 zr-Nw|TI-HrmcbVmt}{o}dv+W*vg`PUq*L4KP9-<6BajrcNARp%>O*SA344Z0I5G@z zUH9V(Mz+NIT9HZW$ZRGkGTz^kS9L)e$L*JEEDGigu~H3&GqJl#4rq1+=zzNSC_;PY z{Oq^M6m+*cr>@)7#0V}^+8iL{EeBz)JKF8g>=Ttm!ALe6+U^ply{>-p5oe1r9#GIK z)W^*0PX-7v`l%aMA&xH4^q7yI;qmAZg)9?X6Yai4!5aYw-Ley=4@OAj$vMhMOLrtcK1nUvH1BT0Ac$-%R{hHIqjpL;M0b>HsGRtLsvnlBIt;q9#EFiictjUyklv9 z03Tznm1&{|=GSq#uZdJIJgI(*C(@pEh&RQ|im6#f$F!ZcP_Y6_a!p1g4tWHA-0|Ju z9nU&2GEs%GAL|sayslMg**>e_H2BQ23YUSYd<4N2c8~I@*DcrJNm**ARVa_+9d~-k znoY{vM5*&#=he$1%^GK2*YE_F+tgS?qqO@N_Vic3df7Z1{xuFaGGnna0jM;Wzxz$| z`kOb5b44{v2lA8D189Yn{mD)_A{)(-4ghzKK*cfeo(F9Uttc4toOIK{TRy-8d6sX^ zr3^}>&s8q3Jss7Sc6@x=NAumDaPH$N_32V~QZ(-K(t4B`+r+s`A3d~Gq1K!$RH>=X zZ5NljlI)N7{LZ=keR}fgscK&u$@4r@V=Nlb7HB@+)3i9Hg-0nj;spTi!KVn}>TBVg zz3Y@UGXXNXEk1hQrJ!``CdL^s_s=qwF|LG=XK6q@d1*2Y=Q>oh@(B(2#lowO>2`OJ zb*W^LGmVbRl};FTZvI&tbwES52(*nt3ue$K!p-@aArt?@V&InOEr!7f7BGuouaY{%3n{PVs*Q#*9H z0x*b0h6HIaAB3Bk`KSJu_qezi2qGA_&)N3s3+oeMwri}PJt^!mKL!zH83}!I{>*GJ z6bw_G5%ls?bNlyy-)ug7Zl2+$)ivv!Y4Bb+0Wbr!?qR0pP(5Z1-^*7on{zf_-oO}F z5PY`sxt7P+o#X3*!MJI2DGrQ_G;c-U-T7OMP)i(+m+! z?ands84=EKb~Qm$)Jf2W|EmO9r(@SKMkAut+@~#0|&$GIz#j zhXtASgtXClSRE;@gLLb&0dx2!eBTh=yE16Q9Lq3sZX-zWjDUYyj8OvUmwmF2nFrMa zLY+hAHV4kxb6-xt-GUK6N4RDuL~v&NH`=CKP|Me)hct=+C;}mWeaYtA(uGv&wJ|OdPJQ;Vb zaF-xzw%*g?lc$(uUS2acU$sgKxRB*~ z;DztaptZm&B@_;cMPQP2T9)>co-fbP2>;O+18DTvxow{8Uj^GfNiy;@VfK=Mx6A3bctEgOz|j@uAHMq) zN)74hz~c#UFQ$utMnSWLtS7|{FaCSbGyZ3cJqP|OF_|0XT4A2uEm-E8^P+j5FMe$~ zbx@|b_-W=ykAYC-a0Iv+wEZ+s!reW<6&7T&);6+^VF)G z)UPxZIMrJ|R%LYMbY*%!Z*Y=lQfIZhE7%}Nlh62$I1tm1m&B1yX{b`D&vNptZ57t_ z+xxb=E2Ha2@i}NqeBk3z7A)i9I6+rtx$5fjea}6HM$hW1RC}<1wTMDv;aU*9qXCn% zV*l6I)LJhrhqVX`v&tUHDH6;f)o2^!N%Rmye* zQ8*hI{LB9XJ$O&0No!cfs1*ueQ&)(Z>$U;z5_k*qVxC2aGP~}Y_|tvDxcEIa&m}@I z78m{TU4sXLtS}$P%6j6)voTl+Q-t4yIZ4(aC6Dc%DTF?|t?XSof=h6Vr8LLO0oEXJ zT|QI`vN>r^V73R0xrDGXn*0?@kG|s?bFOY!aR2E88z0;xdB8F4((ULTvO(r!K8i>4G*1%v5H8P@Z@E9LN0eExu%2!!_t8^|}5}JTQ7!H?CHv z>qlK~qjD~T6wOR}@=G~|J6d)MJ4ykdJtg1}+Kg7H8yvXzs`H_;ZXlC> z{UU&9tN1Oi!L#8Kr@RMFYP|wp#I&{dCN$oR?_t-nP!K3gdD&zEzLyQBG97!aVJ0-$~irOS$$D zdU6`AJA76}tb)htyc;V9mNDT;KAi=rQcz`a51i6fZbmROU8OFHwtZc_@|%BrRGsj5 zJs}*z)u||-En_X(8EsVt6kJ0W3QgK>*`3_!`s%x$4+9(!TFqG`w|KXEa!ysN#5JTc zN13{71-B9v@ExFe?+~POLdSbVsE&E2e{x<(8pdKE3GcEVZZ~JS3_@e@=1tR_U$U-{ z1m+02dl;dCBf6NKJIP_AKw>kKESUdsO`;nuJ7+dxt(*;)8EXY~R<-ZxlCq;}&JIv5 zm{)TF0Nm@d8^jKVI)t$eZxC|q;?W!6LSz1Ibqm8s_`7J=XixqnaO06Vxa+_BzM22= zhvpc8eS)QRgau<_an5JxS zGivU!#^C#^2IEA}pyX#YY86;5$VqA7ZW2 z)`JTKGLhUXydA60WY;3mwvq88g6Nzt7Cif8U?5?L8#m6m*Ug416euNROGAz?m6it_TNT1Si zTx%eT&$;|f*R4WcoO0?%OU}Bz-!Z|$Rp`&_)F*vcE@lG#XunDFlQaX~W4)QC5T|** zm~vU+W8kcyG>~$|n9lW}0ap>j>4c|e-PhHt5oC@8<4HTE305~ZKa#`s@%p|wfBoAm zC0nsu&km)~z)SfF94`G;3GwYtaLISwR#5Skd4JA)A_5}f3RMAQabk3%((e93ZED@~Q*`VLov8@k_J)nP^3VbjHpP;AYw(DC0^~<79$=4

O zr{@r>mL!)=^YR-qv%E@x!;%0UBQd8C+7S_n&Z}vmb(v_k-18&R>bf!zX}Dod0-+>B zmtop*nWzBMw{0iA7okfV)P#npe`@9pxi+`Dzi{;qKCoHxcW=<-0p5)HCi>848ODH2 zC$4SU(!V8huJ`}&hi3Nu_iVU))4YJW9}&c4{$AJLj&QM!h}5`e0(z#F$-LbWiN!C@ zS&ORRurIjidIVgp5Z*UD|BCWz#GGIf>($e=Rtj(^6@hyKM!M;Lx920TDw$C*P|TCdu;3Z=_fgtPGO?h9*? zQbbaqidZ%0Rc%(7)-wjOV2LAC9k)V2#zw`9cP&%P>45p?36_$3%_{6R2xcT0^Yd67 zw55kD95$IuJqD|d5F%QFTa=_^FQE)!F++f8b!Pb%Yo;}`wz?6pI!6PP9fXh!Xen_b zP$yg;)9)Qt%93C(2~q(m)T@mUBq+qt2N#!SVlfB^!qye)X4+PqXS&7&naX>#LSaWT5}B}b;+$s58w3LUiw{^^c_|=&Xe!%= z*#fy2hY_UCTN=4-AV!#o3M=ImgOPDr3I|_dPPE3ibIyr46NN{`06`^;wL&Mj+O5X2 zoX1?EwEL#>k1}NI^;zMe!1cFEz>+{ety1aUkA;LW@=ndKV%zhc-XC3ebdYmkM%in4 z)SNM&yMa69YALBCWl9N!YM_^cet11}$auJZRJ&)z8raVSR{rUSPXwO-lpvQmftfur zcpbnij>v<2Ol(LLXX=th5_qSu=ly~cJc#~jHCJCPznH%QtAtF!-j}!wP0|!T6QZmn$u;^}DzYo`US5LhxT%77MNXN0rle8yg zfOl3GVTvK<(e>AwzCQeI-~XvYA?Y-M33aj&fQnR$4~~~KXnk>$TQ@LsdLdQN_O9Iu zRB^(i##6_W%BFJ9_JdR2<0CwTuMbA)q8!`qGlfso483}<_@QrcAO0|OLu5u7Esc3T z-5w$g54vU)U5|o5B%U`d9_$Ot6sCh_y?D=3n7dhX>U7*Ti}N@zLIg?1Ir_ zYomcg|IP23C7Tk@v^?30&BWy9v~_2d6V`RQd_qB@>ruj;8@yr}=@`E3Xc7<;z>WKm z<}cAIB_5fBfVE@(Ha(QWh7NEK21@%SCMv-_NLeIW7$G2?=W1{p_px3^x~=;+Sr%lT z*xBb#&D9_N(A@v&Pee*CNPK;YD^V*JD+XA(nRg#eUx9ta{n4w7<{j4e6q8^=Ue6wG z$FXY$Sr)UVT_0~B*p-4lp-*2vJ88~HHa>6(4rS0dKIWsv@md<^Ep`k=09Tgi>|pA<4b~w9tHn1V^ma$^(<5F7a{^BhdVqy)L1f^!bDD8a zQHpKpGq0$1O9#MLtw3>C`NHa*j>C~OxX_Y8ywDGpsA1V zoB8KIA)paZE_)&Jb~+&jfPSyH%;V!ScBdCc1KL7h0>=Z=Dmo2gCMNMxxw&Tdm;oEJ zACvq-m$ADp*%mD){$bfuw;lqm#DhEsr%|~|v^Vvbe^*7zg!+(p7^UrtAPLjceFm(wD!3jOG5ttn zt7ywd++RS$M%TL(NPP7Whs94n_NNaIP>?3TOj?uiQLmAyE(4llQEHt^XU-?eOozC* zV-bN9*=kL+<}ZB2hQg>+{AnV;-~p@}_;-K%x8ON~lIsU#)Hxxr?AL$!o91u+?cdOT zIj2eYS(_RjOFgtt=4{CkCKk(hrg#hHD8VE%cL)F!cp#lcIRHoUxZ?txeI-H#+^cdK zM_}#Yb9FqOYM-Ppe%(&ch75`P|AG=gn!*3zt&~$JHjHpu|0fz5K#rMtm~Iic=^lsYmD^ucK|G0-Pg4$r^t zTXO#L42*{Ae1`jWkLP;OulQ3-pJ)^P)4lAvY1^hk7@nZ=i!v&{*rwqllj9qhiV55^ zZ-dr3LF0AM7HAf6$3kRG5H_n%x~Q&LSpL``F0kluo1&qwVYmWvh(_!m?J(Yse&zT5Z%UEb3~yX9fBa7jKpefWRXnas}R;`H5pRgY4LXg=u7^ z+k5HSktikDa#gGOcGfBLPqgU}g6Od;1YDP!M$f_=u^?A)ox#-=OA27z1PzU5WQu{2 z&#!Um(YKrTe`>D(_>#G$RI0Fop{krVVkr*YQb|$6x{yjj*iHpK=DIHqGqm=VDN=~poSN8F@I&j%Bn%Jhh+HrYH=99WuQAH$4F0+S`Eg}(y?s)0%3 zP-GkJ$of2)DtN%l>5Gbj@Yg=ehV=&i@a&xE_W^Z`ESN)PrE8AWHz-3YQ_Q>KE{&&z zxwOf6NuS-vQvvM!dRa@-h3ivohp9Ou6eLCIU)S^hfv=x$j` zHNyHHu@j6IuNJ6DRh*MU)}jnNHn&$dxEsH)H62zq@E;;r{`)lD4Q=qh-)~$HfKtTDs2D>LmfDjCUV?=C>QT%S2}|L*o#1_bw2q$?n8> zsTdUnK~L}*6G}MOYT+vNreYj{1EwKev3#iyJYEpw_^*HeF8orao(x>5c!$ifpS?I` zUR6%15C9YClXRzID2`}N0Wt8gt(E%ed5(I%etVw%O;e6^iov7e0j#v-g}Fi?icAY{ z3(=yoyei%{cwTk+UFZCU7CapK)aXygAlJGUc`cW`nok52!suz-1(bcTpsx>q8|#r# z;?y?yXbTs|)uW3Pjy&}OkBKiTL+P7BFKtC}Wb88a1uPBx(nG10EUme{p7->Ah%0F= z=biE3b#dK*zFcEm1gB$Suv(Ncafd(O)(h6bxa*sJcKS-0BYt{cD|omBQPj>P8vAAB zoyaHiGoP;3x*LD@K=$SXK}A1*W^h4VTp4{56*%9N5OoRNLrIU<>`>vxx|LH&jW`DZNS&}EybPXbnUxW8SmI5z-= zB|sAL3C#2PDFT#!-m{L--6@WFU$@K}qHs45Eu2H#OhYvOH9|6dqyDVbgYLvIk6h^4W8|lZDnWE(&=IB# z%yZiu#fEhtLp%_=a%bR#K{IZ(mG{Lda43Wb!fIK%+{6vEJ@#1VdPt1S?cvVdvn|#Z z#$<{`M?+jO;f4WpYsT7)UIw|w>HvS7f*43hoE!CPfSH2K{z)`uAaXsZFjJXJf&i9= zsTO7)kn&-dnQwGkK`skf4Q>9>u7Bc^IkK%x0 zA~Ay(x_NsnpV{Jt*3gH)6p8& zGIc)ofN?#5jSP?@(^& zi!gU?%=Z;QQ`1MG6o_NiAw#XmUFcf+mM$Dy;6KLm;aC9V&|8)dffJxXa86}>g)pXo zD0un|Os1hgR)KK*V%<`&bTAzLJmL3FP)7CAn zd04jX1cQP%;%1a)%2FQHGc_jSwxxBl$2RI|^S2caPrl$fL|E-eAix$WGzz8&L!UtS z=V%q?KK=B=A6a&CLrOG+0CRr>f+uuk5_u2G(hTMXm#p=B#5CWNmv#F50?X|USzZkC z0SRRuFu)PpwH4CEE2(~asYsdt|F~C6$ zA=)bK%A5=YtD84-B>;bkE2l>kv2&*zf`eQZF+~_2us*hV^DRL?2)ldoqQa2B;I{eU zKmMl#9GzheyfJV^3l}9Ju17fO3?8)~o84dqz8d`;_OE7nZRFRTgruqAoCA1<(iC7%u(A_Xo^IcyJ2i< z_Xdy=To@Bc>0)?#4vyj6B>G3B ztbuJ0MQClH6>E1-Pq60MbJK~v53>&>wqS`Bb&P-h1dhPGF8@;h4RiA!oBgLxdr@N|ba5I%|E4)5a#Slu{-X7? z#vFI)&=zmUF=Fu$>vMffIgc#=0-h751WeL!0_`*;g?J=O*$NNU_=3H5cjOQzS-x6t zpMGs{A#WYq9xx>!t`APe#VBL3O>_)hnYm)eRc0g3Xz^j~f&arkUYG;tQVs5KTEeLc znTnCRDAfx0DdS0b2Iq()OM=bZA?KJBhXy@4UPdV!PvIQdDP9PtI&`yX70l%g2<>wD zS_W`rAQbG*o9>wknCL=B@WR^Z8-fglb_|}%D_2AiL-LFtxXx6Q@2c#Ndbo($S2uxL zyg5XP6}QI*MyV80&d|Ac=ceiWnjNCt>4yPegK2|~u=tlm>)-F1S0riqtJeg({m2@1 zMrFo2>D|XavX<BT)?QIOh$3iu8gChPOs#)wGZR6Hf(nCjJ3)F66mvzMw)#e}%mt z4{@RX;5jXg`z?KUbPg22RXsu#V-jV8{wRzPM+~U+o8lsvN`X@Na*t11$^#`J#Pd#= ziLYgisx*7wdkUc*-U(16Ld=USk`%w>P_KKpKc@)7ITfS@@kf4Dh;NYKLFFmq{xiZNwPu3~<-( z3C~0W=!za76v^dL(T!qMT~;<5C$TaFf-fG5J6$YrB?jF;4g?)CN1(> zUTI+xr-aFUKaRc^>YrbnOH>lQ8oJLhRtQC>17uhRWM%mTw}iPbTVDFd7|BG9M6Nk) zEO?Ua{~yBMw7bnC%kvBZ;7&>+MN#`=EA6y1)7f2>RWnudV&)6XXYLn$rq7&N&Z(}> zPTOf)mSyb=MT+7Ef&k|C`(Heyvb*}sBY^-O#1nDj#@*vagqHjqxXvaWOcIP>3N7+8 zHU)92U~!59pnr`eFlcUet|EE>W6r|}T5FysIv@hz@)-|f&^z@!##81(-3Qui;yDZp z;gL@4?>YudEKcB)QM#RP?4nj{&aEU%*nUb^8f1ye=$ZHepYvvMBOXOja3%XfH<%sZ zK#?O3_(hFFAw7d76u;JMB0GFfhAHK2M_igbqVTFj8U-an0;L>}MIiue;>+>%(&)+j z<@s2F52~Ul8R#lkgr>^m>Ux`hj^TmAn1k_?Z451qIGpj2knkb~<`kuQWVSF#U8{If z{Rz0n08+s8;OoFZn^c|pZD07hmvTzMC|>!K`;NbKXc-k=&ZFhsI)^F^@M@p(%nWZS zr#zs3y<0DNp)7pH5GAn0$v1}n97~ghR5wjfXIr4J%>CbJOX`ox|eCFQcr%?n7XAbDF4rwU#BVxg8S`r(@v-aJ# z0iJg{Bi;B0ET4vWt4K-)&f@oysnVa}OKH zqV1Fe7O##r3jg&z2AS~sds^f(ZH9TcCwxYB9Gisp42uSBF7-$JC0w)4d48WptjPLndpJkeQ$?4X?Q@lEB z*>!zplS*(!oTYsA_=NkI(4Y5h+jbL*MaURY@IZk@Jlql9j)^TNedb-62-Nc)Fn7Pe zF>NMWO-Ve$1!WV5@}7)v7_xM$JYnm>Oe&Q#Z~^u548m`z7eS@BVcQBv@mIvJwJL5D zR0{F3ZuN@dCE8C6NwWw`5YsuzbzwhoE}nczQ}7rs{>*Tt;8!?Uev$CK9)x8RvgDJG z3X_e8A;FW1(6%*%GvZ8gNnP*`_ZPu?AI2Y%oTpMV1K!AuQwIuFrD@26Z;o~K z+cEXKRL={zb^Roryaa!N99MA zsr}$iF2^|I!g!XTc3NyEIO+c$(X6K=Ev{YL#&E`fr@e&zS-z$r|CA}C5j497e%DA& zbX~puQYptM6OS`oD)enjQ2Af5^X@Hi?Opq<_iq$N9`jY>QpzN-2B+m**ngK<*@okC zeq}_E%ZyyO%VhQuVhy%e(M)%1s%l{Ipsx%cZY;Px8|2 zS?@d6%2MgTF}7Tlt+AIby63A6q6-+pX7^{xf&R#;_ET0L7_RLh42Xk33=kW{F#h$| zgm@S5Iz2+ogb3HRnZf2RMn{A+_2&mDpSkO`k-dua6fef436-uyV7`?bap%Mk0i|(= z%SHDpEz{(aq!3HJ-1f3?&D3?0x0J2#D$_HRo}IRXo$~Dv;B(&3Zw!h^S6H}v(FBiY zg`Hzeu#t%rvcvuT_U^^Y_V&rI?J5fTDv3l3tDNou(<{`LVI~FNAs+Zg6zBZq?RNRz z?Y4gV8bXTg;haZuKVFo;0e#&?DeS)CXe}(Gn%d$>TmYZu%!uF_S39FiAY>bG%`^B3S)laB{LSjOGp+I zr~i>0l%t3lpbL zu6Y#dJf7vjklla?)wzy#jnLUL8^D`KtY?6ZX<*SYwlbLJ;T=36# z*}fE^0;dFjEM^%Pm^-CP!C7e(A_7&CvV7RJ4q!+JR zMo@wOY^}*UZ|BNg5@cIl{{lRJf+xmW;nWE+FwT8Le%Q|@O5p)pPLH4J!_x{sGuEXO zBMG)^2u;sHy7s>JYXpapS`;YdlGY7|0m!6q92GXi{vOtU=VA1O^l&mqO zuv00}8;TUooKdfI>Czf`Tlj6?(RAz;s6LSm&f(eYkxV=GIa5lEpT>ZE;hxhc7`Z0O z{PREkQ@c+h+1w$==271j)}NoTB{Aw*RA;+g zHg5QkJAt3JvV6$`Kq@Sbk7dM*_1IVGQ@Ff}kE={qE@df@cgygy9_6NEWMb9@|J3iv zIiMZe;14uCQ@Y%f=N%tmbc}qWR5105@4yIrz@>r-k9@*IDpo_YJ{p77->4|r*^KX^ z(GGcD9^s!#OBtyVB_1^3%h!^iSt)9;x=VH|jB(_izHj&2>PzMbPFTi* z0&z2WvX_4T&b@*;Qofo*u zT)s|3_hPnG3LDzFjLBfm25abSwv%^}#%gilrT-{7Lg5d~BV1WRaseb}~oZ8ZY z7gmL)8Lsvxiiik-ipf5E4}B2J$Nt%O6_&zFW$Yk3z$2_}2N-lvMxiB9NZ|CBaZ6Te zNSEL3JC_9gx0lSued_SJeDGZYkuaGP)`}h>nB7!D9<%-O1S_$~`noX`2Gz6)F zIMCaZcLvN^EIW6>;Q^C51R`+HeSjIjr6zt&*3QyslXE~6)^%CCB3N^f4-Vd-6sxO1D zl&bjo^m0tf8?rft2V+dThJnR}uZ#o?UKy#(dj%$JSH{w91Qlc|5ruq&eZ1FNP9&Z? z+BSj9!>V0AkkgMGCJZjaTfxsZAxFIK=}XqL%B$oQxG&}^$#SQMB-&vBFK>`IN5mkB zMK12aD_<11C^JA0?}&?nJt*X}@A*^jjF5Pc&-5_mRr{bQNw`#1?OZ5)aYClI9+_Th z&991UFc@0%IqUU?f)Ch{9STb4P|a#KG9ouTk^F%~~8_RmXWXdA6V>DE*OY0^)Aa4hO8O2F%mR96!(bj1CLoNd5LgZ4}mx#9K)=91`3IU z@Vms?%WJLA9KnR-je+a=2qU@ACu$HYqvp-VcuONRgMy!~(kR4;uyr8poInb=S1A#M zAfE+p9-y_*?4)HZD?&R@trL_%4CP$QDgs2J&#P1`%M@gSBsbpgk-0l$j`Q7*kJ|Ci zPe{C~Fz*uZPOxZH^lJ>_5T&}O$G|4$>(>bJ-n!MUAYfr}LdggZj13P2-$huMj4~uK zX$^1t+Eos{ChN~RQ^*&C0({b+oS=b4>Sp}*)0cg5V7$8im1ja&97|dz?ZSnIjWs+j z1b-2W7QaO7#sre3mgJa024jd!1r6f|Zuo)!MQ{L?ftQer0A;KqL~bOkrnTkoc{Jrq z=$(QW@3}-s;n$5{5t{|;lGt3fWGL~EIa z!`m_(it*BCj=1j}A3efsLuk<-o^=k> z_{b#U*=J-d1*<|p!I%SvBVY?xK2O1}muXMjiIkuhI7F+ZEvpc9EZw&^V-8)qfk|z~ z;PeN~7{$XlhQ{gRta{;+))l5c<0cDk5@7WU(4C`bebE zSmku=K7AiBSKU*}IjvC2(ukA_PT8Sxg?A!7)28$o0mgR) zptups1s?FiTUp-4xQ%Eln>>g+%Ewp%&xR5$Z??J;M`3Sh(fcK1s2lL(h4Wj6IYV1K z6b}TtG=a&zo=?ZS?HxvR|BS6IVe-Wlc7FrzBY56%F>LM9q%rUym7rT-21hD=6Ow$- zvR~;59b(VTN_f%s%sEKKotS=XzQ{cVni45TAa(cnOb+H&6Q~ zmtMWCDgrbl!W+oMw8iuOmj9INWN+k@PxF(PWs80^giFz+eV$Hz!UnQKFVv%YbR2}; z&yg$ewqxMui+&ZRyl45MQz4?D-2uOPrv8iZ$~fAGOd+s2cJWi;AZ=O4jBZ7PA?C;f z>)|=+AyFrO6CWn4RUSaeZRr_MxWuDvW8@7@2zk!D1qllyvhSW#_BBM`!yEYngyS+7 zJ-h*PE@L)=9&(WL37Jt#EB&^{fb>wXix5;V3;5F&DtAD%>_8?!4$^Jsnt9@ck(T@ZAyGLL5hz#BS`U|#1y{>%cgdCJHLVvh% z4P33Y4`k~;{qP#s{*-f0FSqO4+sqYmwkes#`|Or-8KtAJb0It;A0cW;s=R6mLKyun z+{MrpQtF6PgCYF_XC@@sPYKKC#D_+L{S^mp0Ac9db-okNm!*Eh1HUoIz>e$;p~y^< zU##5_3}edJDD1p!JK-pBAnt|Bm$JaV6r&iPF6E_QOxQ(1M*;ieqM>pT8 ziBk-`WW>^s6%vBPgA7wvseLzW>al`ujxlNXL-qlzGO-_2stGa^@xNq6^b}taNZbr?LSR7w7#0~9F*o)fJau}huYL+$z$ne%i>;3qYQ)xIm7#bZe@GHc-IKtR; zH?S**?Tmej<+()^=$b~T0?4+3&;HgFj6&J(CBPz>BS0NzH&0P%VEiCpDukv%Y`9uF zQlZxUHQE-}A3yGgFXdfB+b0;8vq~93w@QrY{z(&Pd%)httS zZa7opDC>OzGW$jsRRwl&Wbf&IXd|$|%n6lqTO8zP>hfRy`pho~?d5m8N4>=(~{BhO&J|xlpSiN`9=M15Fq`$0Z*IZl*A0L-5xS^A9Zb zc*KSWyUY=6GkMQ>zJXV?h{r>>k z$PfSkKmbWZK~xqqUb)(~zW%ITx<%4~#D_rm$aaBj1;OU(EFU7weS%<9xZGqObpz$u zqyHng#@$zt8XWH?p|D>u^L0lIBGkhciwqdTGzu{avlv#(e29UemtR8cg1POR{Zc4P z49=TMn_Aj^cD%9ws6udLrnXFq&?PxSBE#V-at>~akY%Rq?88&3$iLK2*Ljz)E(DBg zZVmSuPSyi7%w}2Ph+?w=k&D;O6Dv|vI)fv7|R$VD60{kxPq<|f`zvZpJnX|mvrT& z@b)@k`GbaBLPYdMJQ)E4cC^dq!S^yOIm6G>S_a2+A&N4zFXNlcxGO$@^&4d53PK|yIN29@;&8j9Kf z**DACb;o%TrQkSA2MRw>0FP??IaQ^_TW2+!E(^n&1p02rkQABxOj*apmd_Y@ub;K=zx$Qbq?o$~_C9iMiS?&f2zlQp zdGQ*{fQF|Slk|}q=o7R~{bt=74YTe=#!u2pf5bLzcK8;5n46j#-1i4wGHSdczw&(s zEd^6Y_&&yfIMtXy4p4vUq>SPspCdPBRnn)-f76baxbzjiPQ_K19{-$Kfm|^f!DmAE zxgh}5OyBAiODA>MkFI~hPXc7 z&;>&`W`kV__c;Xd9M9$%-oX==$|&4!kzF}Y(vh1UcvfU1`Dn^!3x?i2>s01Wa)|@% zIwTlHCQt!p{=>N>fK;oyz2UK51@#PgxyH?7p|n`-P&}h_)B!0=LWL^g91N9^)`pjZ zEOJGpOzFFfCp`XU*)X|Yfgv#j*U8J5?U3Z73C6-D>fd5Zz=26v5SvHS36~p=4?*4p z;eLrdhqpOqZS^K=?Koj(x`T(bd6|$l$wzGUHi!4_eC7t$=@!d6496bf6^u|6D#c}( zSSr>ySZ4Q}BMcCQ!4k|PtVIwtzo!7I@M(p!wI&ttW~2GMSA>tDQQ>odAq@Lj{jPaq z30ng~tBu)d^*02_*nmG7SE(6g5Q32->$eCb>XYeA7JyDEE0M@Jkdnp@o}&BJN`Q`w zYiWf;e0Q06jw83y7X_HhXmSb!UXp7vr>yOC*Q!< zdNYfcLuzs&%rW#;3=U&JTVp&AmFY!jRy>HOj+s@WTF7Ok3bTEz-AtTfpzqQUFMtaD z2)lF?qYk{fY{^TZ(Oo56x^FxnWzL?L!Ne{#R-GWPp-KnB z>NvVLF%^~(35>Q~8!}W_N}=P%{?3oO>`dMvZV_fb#K7w@MY9YZPFSin8NT7@y8U+M zR+Wr}2bzyhSpo;2&td2+LH~=aeeEfDr32u1tj~oZcg2Z)%!S0Sgc`sH5DgCJ5B*t> zQvn!{)g(M%k_Uv{`V^4e1`UpZUb!1 zxr5?%DccpICku;rnX&)ac32;O$T(si#!$VCA$a$}EzUv4NMv2J5ipgwdpR5Gb)MC^ z*(eNPbGeaxBOJD6Uj(Dxi(>;8@-#4}+qomKxd0PDRS2aNN)}qDLY2y;buRnh=h@f# zPT$f;{;I6&H_OyBUH?_4j)NN`bietY&nlT2?CC*dO|?~HB@P26Fd4>2R`b;sI#`51 z;Z^Z4gDGRnIQZ_E`EEVpPCiztak@)*NH`K5;+|!UZd91Ra9uE4MmkT+Gnj>3+L+Nm zzJWH{>aK#Z@S#%G@u>dRv(+~-g*<#lsP{ko<*zIlUuxSd3)yCCzHMSI-#mQKHg4Y` zTNMUSFaV@pl!9V~K?Xm1< z%yY{m*yyb)6kSibf&knit4|?(>RcHDz^q2YH_I3v$6D|SX z>4?iHX_D$NWBEzXkWU!4E5Nj<5ll2m{2H!KKdB?Mz~ACE`Zkk2;@iH1%Fuw1#7KaC zN9=+$=Tw6{j$!FeUXW&ui{U`VM*TEk3=O9m^>Wv&$`ID`9JXEfRMf6v$R{C%@iPpq zC#L~X$`qqj&qQ!z1n|z~?oU$`E$F0=dZ3IiJfvYN{3_TJazpg`Rq!hS=Qz{J+4x-u z&>By$3GLbAnzPk0(8vWI`Zz}fXq_V&JzSjS0VIk+GbAdGNJJX32lPCVEvF9ChYZ+j(v6CE$8^i ztKv%Dj{(Vj%hm5Z%kSqe>&h=x=Atg!D5X35`TQ+&tWi+H&(k_lvwsDnaFo0$`tiGE zP(3r6fM~*hj*-fW*6Y6?eR0_jU&8Js@XT1tf$Tr*H zF~3Ji14D-w%L$|HUEly194nK;&~-FX^@8ZM#k|M@4wEC6+zAIpao^oJ7|5(mvwY2dbE~#9tl)jl9+hF1 z3F#R*oJa79M9+hix!g>9gfg~6BF2}3S^*+rRTdTIMLp2@n$sxc+Ia<(NIOA5yJONC z^Kc4s6P`To?2t^}*Z=xw=H%EI0VdlZE7Ccg0pVS}Xx()e`R@^yQ-Q4CywmR8yW2LH z-|LM@G$MIp{*BfQFSaL~V|}i>vTSa%-gd(L9vC}hzO<*H13>${Or3g`7;&s!WwQf1 zzK+1Tgwb-s&NO;=Dj@sjARMvJugcQz5`=>3bfyH%IJD2!tspEMUb+<+j0e}rC=fe2ZHX<(RR zZp&S%@G9^;Ha6H41Gt?-%^E)NZW;TR@EAn5yh4U4V>4tIpSdYXOb8R_te1{oV^xZO zd?S=;5+_OQT7amR5l+*aS2^v6W6PLZJjSzk!wZ$Wxb$=)GkfQtn>D5+=J9s4ILlil zM2KRdm%*1@8KWcwUrZ;h`SP-##SAh>-S@Z5p($+PX6f#4FK>MMs_{{P`!5Mb%$uT7 zfvc3ZVMRkvdQU@)$wRz*;dKLi_aN09R&m-^?HW`kvFLR>=33cq)-F6BS#C69{4|)@ z&oT4;r_?=n$Sz*c%RUAj#Yyl*WI%7m08~J$zjWLxZ=}FRpvyBJFoD#EM&%!<7}{6p zVv44{4nOu=8jbO#K!={BL%q)fmNns6vWW%}ETc@SY_j`nT7vmm+t&jox}hLd0`k~p4eT@JFc#OG z3blk>bSu?4q6F?_rA$1+a2~s{H=E?dZ~~SAvM|9PU1aLd(h&I&KAHl%v{)}A6O0kB zC_&)y@;8w0qri<2&sdaq-6@97Bl}c2L{`cd)-!9P#+CM{L(kLuUY6splMOEtjRdI_t;VjNwz4H{ z`K-Ezmxf^K5(e%olYBQkTUYfngguQhhLHnO|n+DwTo3uzv-FhxGEq|rYIdZO=oUy1Z*loy^+i-k5!J9U}eIC z1LXiKBpg%+SR=tQMPM?&!bl6MJb8tI`wdI5ddcn#WEe?>*oUddjE(oE%nxPI9rs8K z-np#Tb|d%|{)TvAa0N=D5LkjL@CXK#fb(Ys2rooo6zY(WHi6Isi10;Fiw6&S2b=L^?61iK zOROsvR+)ny$frE#C*FBQFajUc;|~!+5)BM3*l@j7<+569R*)3q z6oZMonvVtsgzOZ<%{gp$JTlub?ZB9Qjvbuq8LCDwCkt8hssI=#U<4S1el6M2!%}G| z?8UJLtlQq58sX5}L9jb*;&g_40GmJGN7y)|&d;fk<}fZjmd;ICSSu5J_c30AOb%;5 z#=tvUtu=qLag0X#Y~M0Az@yTS5Cps^A+ZXd(jBVdclycDK@WPJHSb)8aZOp2hY;+? z=u_#|mV_FGMieR(r=Zjrw9XjZ&|9Cx9p`ctXptux1m|O@44_+#Q#>d6gg$$!OlW|5 z9TUAt`NuiW2@!#86izuDR7avlOu$U<6)QI**lGNy*$uYurhGCa8hmWCe@L*pi zDANJC6)MA#JA`3y_EEB)BBk+ghOEnxmyD598bEALM z=Q)*YR$utdJ_(bs1z>x{zu?U0yw3w&Z`3cdvK0B7%f}=CcrRsIbpo1bO>*(N23ysPyVJcKz7c zVrxN?3b1y@2#;i7L9~kqZcna1CCOw3&%xy)UO6_lNF6Y5xA;7IE{Rt<{KHLfbqC6v_^i}(PAK7_zm_SpUC z|NMVR&Un(UZ>`}KF&7*Up%kgef}Ke6Kok*5++bPE)!+PA(FB341GBrU5_lQH7$ zz)YEkCFGa=WEmsNEMW{Bv2IxFUIs_*g}Aiv#BGci-fb%TSMSNQwg(`JO-^`!%nzfSiFg?-R_LQ&BFDRhQ)#y$~56BJ4l zvSF{@K@s3};-!0%$%?0$P?sTIkM=VZt`%!moof{psAdTI?i{v6tpF~;?0x6&91nV= zFq5&o=x>CxWvO3=@sdsbrvf+0hiWgF$$ONkE!-45hKQ>zKJbH9G$Cq)smp~lm{Z1M#ysgn19pTUHN#eA zjLkxO%-fUcd<8>T{EsfNKKv4%J{i${_jh(2poQ@N1_J8NF)XL;IennGgQ zBFT(}&59&wJeu*Z5LFu-bLx^Pb@w*CQ$x2Auo#kkj00)UHr!oF0j^;=Al&K(5Q?}7 zw32b96-PF7gHJCHX_P3WU0O5dGzCZ`ebw9XXg|jv5k{RVObi<4`ST)TwIq)ay~6FF zV)8)X^+j?iCYKp2c)#}w9=5NpRX53zcGP!sOvB)}fD3-ZEL2DXCKSXQxQ0nG{(jUD zP{rFPEY*xJH|dYG#yyKu(0?L{zl<&6b?9Fl+gJN;6XGm~+fzt6A)VW=Z0rwBs!*-h z`$@#IO6s!_>71KZ!dGRHGF_Ndj=*BW`HeD`5B+Np=`_(-u}bGv_~A#MH9(dY?1s}6 zz;B4K#FX64c5;kO8Go>dbR~7rF90ePb+LFR#oK9$(_gHZ)Fzp}V{n z9ExyZ5ubjZUH)D&0a^eD;;2u=&Gu7u>h?KrQNOeTEPj@t1}{n2`O8mF+8$xIO9=Rn zZ{N3Vw#{1vYl{%@65i0Fabr9L=SWUbG^d2EJZI4pW2~nZSWEB<8wQSlYH$+_v}zns z2VBKw=S*PGbIs<}42+2A!k&($mFDyjz8JcZK+Zt^ka|wZO720N3wTO%F2MnjK{VO7 zAAWh(M$cZc{9-@zo2v+X!*9-~C9DRc9~_L>uwlQg-?-T>-M-b9Nh%s30O#}!*z9}( zL4QP@M+gg(jFwdbgx7XSviaec=k2?np0-1jEU!W_PUlHt;utm!FTIHY#)LwC!pT0a)0-fm7ul6-0<78c zQN=-BLh8FTBQD%jz01H2f@K2eOnB^*?`lHGg_r;2d%bmN$T+;0#AJE!qk=0>(rQ!$ z(lBLJcww>`vq_U)xktS+Kf=P@yYxy8gNCsbSV~cgIv(`3#zY!an9f4DPCY>L-qQ>Ox=P6%?wQC}i(h+7W)|u#J55pdkhb;{*W*EMkU^ z0;@uA!U-uDE{uWrJtcx6O3W6nK0xo_y66Z22aJwYMMm6Qgv27~SYaa#*k7kzGG_^o zOSewJNZX!AYiQdta|m&TyoRB4?$n1uNWSc$bVdk84G`P4jQw|{$arAPi;gqz3Cn#v zfo>=l?=^x=oJ*_77dmW2!wnr&zT((=G|)XZ(#xeZ8luM<;lL%?*rz@R2QcFNtL$Lu zVTtI^J773woLvg0qL4BttUvAbIr@>!Gdxyra^+dV5cZxjem_!l}{9hGL4s% zYJ(BRfu`umRwI=8+4ua;yMc~;=efWJ4Jup3VUz(lOdK3^|d3+(n+8v5@qnoj-7mUz_wE3ZzW0bwd$pbrulh_Jj5Kwb2v z`mNj$KV~BrSd^1zdD%~m1jyP40oZ;CyZuon@LjqNh5Nkl3%3nb|Agby%kSOqpMFl6 zS=sK9DyRGq?2G5D-k0~JX^nMTkp~B_U%!C>=2$ni)vmLf$~;PQUuzi;@G_?)+_-k9 zZR53FTEiQ09t5vY>t?L*W`M-D;Y>FXNSLSk8w5M(3J!&=pGB0H0E^H*&nUoTgoK2n zko^rJU{FM!Y44q4aW7c!D}0#LC0Z-9VtKxQ-;P*cdi?XR97uhb!@$=N(hGRdS~6q$ zWRlPcCu1C8WiD*qYHPP2wDs*xU|!pP?=qacmZK)XfAY=Mg)cK%9s7_)i0c(qKF z28z_<>?%n_Lq}zZG6hzTp3{03KI@J01m||ix+QcKBVsCT+rDEY0ahCt4MY~ehshXy z9yBaTT(gbASb20rV@LDvB@)k$wz06W5)s#GQRpkf|udag;I1OeQpC;|{$yj4m&yT!d9z^!a_j z{N0hc1pMw;FAEhmE%ZS7y1`)xN z<7ywoq0tpXxvMT7^$z6?*K14)=Mqs51*zYqJ?X+wai1eOxsxg8vbmw>T5D4>3^DxL-IKGP?LVIAL&j{U88+cxbfm$H(VEy`=& z^_yA8Dg*hy6e@R9uHFL|Jm*rvczC>f&wr^H{1-vV^Yjwqqf^SWZl%7W9?A#+@cEkx zHSkzp`ba-%TYOX>{0wcg179BC3F*+!okH!(DvN@X07aSE<-M*<@ZA0I-@JeNo1Z`Z zZqZI>efn6zV0WrN;UPYY=Ya(E)o<>%YuB!F)Yw`(M4A4T`NyZMN&Um!TkW@Bf8MU& zxsC#b`5{`vFnUMMIjB(Gg+hTFL`Oh%4Ha?($cAN>PS68J7}D+3M2Hqj4W>ek@4$u{ z3=&0A&I?6gY5B+$TFfRYxcuU5Z?7Hw@@qT(>mx!i$L$73#}P%L4hVpH;w7U*wVHQ# z58Cqft#;@4zi*2ijipLhKmiU|{zBY}Eu8pg3|J@%RB!wP85l$!rXjgu;pN8Hb14hKYOBi?pSh^}g1*jgXo~Z-a z86NS*!Izi!`rtfuQD1}tcupu0Tt>+uaNI^Of{z%<0@vZFxSUV{=FXR6J)q zQ@7MJX@pjIVQmAR-j)#qpHt8neiwHUG?bD7WQ8bGV9b08ce_N0glw>cO|o%LcgQ}n z8eX1RI)`Vy$k;g_JjBb?KPJ|V*T}w8cuGv%M_4KF^{N#R;^@kmo57<*T4js(bn7%inB}_hKT2LeZ);7q0@m*^cyEzgb*tTDKNI3J2p! zCE*yTFvOkkoA_oQV)!An7BM=bNatrh$afS8 zZHbUhFjZgq4!(p*r6kYyv;VQ)0NJ_xOVSVf)@D5|#^IFhNDGfBFx#g82k@M8eQvWz z)AUP)MElff)~@tbLWx>IN0x0-zHfUdP!+#J6@@{El?(RWHL#AaA%5RIxLqQ4JVJBu zM}>wj9ApK%UYRcyLmdE&^(j#Sgv92{P8r(uU5iB^5lhT>GJi;2 z%j|9>qmQZQ;N_dP^V8!t!oyypp36M5MAqRv);dFqutCxAI>uT&VXkg6S#DRqxJx!K zhXJz=uSfDx4{M&)*CZi%*6Fmp=WI^rk_~fumgWETfB#E%SGs&-yZyu8|EcXFAb!OQ zzQMuipFh0GyfS;1A|M`r|8v{Jd;Rja58KKbI~L&$F0%oK%B7F@KUI(bzk)RkI+(h>0K!l-HAj7$Xqz^t9acuPQl9dM+G|xCDNi^xY^!% zDkHL9mq_lK$C~dGQlB?e?;2I;?2P_34_Kfr4}p&-3+$fG5mM!6m6gn`Bsr!o>t9-; zEQ-mI_P@*>#RqM~8-=Dk$U!grLXpbYToer>_#039ucVZ}V`CSge7T(~M@#^kVMlJk&NxMtJDG+~h+ojU23P5G>2D?X(Q;dke!6_lxo zc^)@*gfZuM8-g^-5v7gGHjC*ADC*cEp)FD%4T{hMF}hE3rwzH6juQL@4{>mCzsUXHRhOCm)zm?Gh!8D`(E!AU*Sz z8s79p8KjI-)`V}U*Qn4D#)MNOM#VJn+)rF1&T%pMPI?zEY2CfJkI3X-;6Mkh^aIA} z>8qE>q;b3V*=>@;rtJ|&=l$njj@nn8LwgScX^nA6HZgU$UvH9~q^0g9A&E)eZKESG zZr1G%%lUnF6O5HWR~`9GDMNpaq6z+}4;(s&sS+(2BmLTTT+>+{&^B|t5CczFUGBW) z+@!Q9K#UeVFI^gW;+_}xt+VPCPqQ}CgmOo3zGOHx@c@0~w>(E3J_iY^w`V=9y7(%H zz{UqI;czT1q^$IQ!m8mBVY15lJrrpHzU#quiGcq6=~Xa)dgtP^zxfi6=jBqDZ5L48 zyFgj@r}(#~DrQ+G(l$3&nLj0ziGum~@i4pj{061-#eL=%Nm%ft43i)hJfaEVZK$9G z7znMt&_XqC2PNcAp(?z)&jPE^_Is2FgkA&;98?s+4X_C@@!bh^0X^jz*Nm!=may{2 zY(8=L>UG=u;ZYksdB*m2ELR~BV2Pb#1|$dgOrOxO=dON0*bG^-*T>Ubzk8#tv;Xii zOM&K%^)EwsJU8|LbJFj3*&^%%J2x33z3d_9jQ$G0FTfx-?%iiCDoZy|1S+q^t+{rH z;{EQYT@oC|?b%~amw3HHAA0S(VMgSz{0Tzwgf(ik43-Kn4x)plK+;+l zKfY9~5~p~wjDkRGyp{&lymaTSDKspn;_;sKC_ognodS0b%4D97X+RCa6G*VU^=xdD zLEyOx?;37&nN3Gv!pj%R6!ADl5)~tafOqamCa(E06^Vk>Rr@9-37ZEA$mGt~DS#K343j#SH={i&&c84x zf55$-j?8*~(RN+-C;$2CQ+zr9ZB`-=+UM>!9{@It%OrW>35suDDvQ*@7*%4F@YXUh z4G_n&6uZ_tiX~p{EOBY($tna5OL}UG%2xqpLX+p=x;>%7F59Znk~o&ul3d1j(1Bv9 z0}0$rV?=uI5MFi74Xm{eGeS%5iS*$t~F+gulvk zOlXR_ZQs=n;?Ih$TQL1Jd#kea=hI6(b+1neQ@DeB#>prAu6%F|EuX$H#(wrUahpC- zq`bjdE5gDUM%GHR;MIBi%-w@S!7(qu@fB9-rc?gbU?1#(eF*<0Yz&>zuJ9yUWFeY2 zP^cnJ;k@~v*NBNDhhyHzvcxZ@|0og!LJz=B>2JId z%9ik``BVF<0`fGL2mlySVI*@bG9P2v65OTuEYsanAo<;w0>gU5yWc%BD;sH`jK#GH zFAHNfoB*Fk97#7op~@(Zjks2+tB8G80tw+J24i^15UUpHC(I%paV1p9^Dg0#flZ=0 zVlMB}>J?7e!CM6;h5ZDao?%4IW01@t(9eKxgyJ~knE~EZpSH8+5HBXI!m}#S>LVdg z#>xz4`zTF-wl-a?@S@QZVde>7)HvWb#v>OrzyR>_!|WcvL5}C&JJvk+RQ)NJg)Uxt zK5k+!?fY3F_6l97*eABRDMU}GSlkoz6r(Tx9kx4&# zUCw16k`GV@1d*NyWj$b&5I&N7!irZtbyyo7f<92RAP77H4+>*Uzx&W24Q5`o=J8GQ zLD?{7&dru!mG{721YH@#(x1F;)PRmg(CHyc!-&!3J;yjI`L9%+eZp= zDXXMCFMrGL0INbZ!`eCA?1^o&{GN-?)R2$o_v)8_^KVygSGLgb3GQdG*s52RFP!j= zsWQTFHwaf;yN2L}$d|MNA=KkjHos>9|IW*|?O*=uUs#yG+P?h5@7r&_cu2SeMvUO1 zqbgDn72K5~4Y7l*&Q&)%m6l-)6ADazQ6OQ>npv98c1YFxUq5Wy9HphV*dwbq2|u9d7{RE;Ewf9s4uXnHT%Q$1CmRiTPY@971&E>q#>Gk%Z4)^nh&^()Y|lpP}#%^60L zVJpM63PgAFF&opm!>}P5NJbj(YXa1dGXPD;YsYiXs4Mjb5-W2}QungbwyRhl?`R-LR zLxfmM;nQ)KzBFjcSTWPQ=0FXfn$@c}mmQ7psHH)1kWe7+w2-XdZe6vw(J;3K6R*Un z^x?~$aI&)%s1{!ui$-{))d~YTjf7#(bdIN44WtRcbZJUpRvrOqb~;w>YS%D9w`Wpw$iygqRb26!$C7y1+*!qkn0WkZ_$wRcf2mg8nhOCPf& zv-4*l^6}pYD0ZHzJx%m3g{@I)P6-Dlsue4hWa_u_vfOUk7{3h@V=a_|9RH`X?~ z?H?uD$;Z?793`?tc;hfP!Hxv#Fwn&X2YTqv9qUO1(KIg1k%((vt0>#-zejpx>x zcUxM=E9D} z67F?26}Z9vO38h!!3f{K&U@rUUwv3>kZQb?}fEa3gL8b&=VO6==0U6X69V`an zv$UE^dhzp&Vd|zmE83lXiqJ`iEg-WJF%eehoXd?DaILiep5b@^NIagPgieCCw zU?|x29%jO1Wa8tQyJVZcKXOx^{$(Z+6p)Ypt znPXf%N{o0cS^9+GS>HNF`C<8h1?V!cScgJMi20LxZJYZ%n-}G>#*%SYAu7Q1#6P7` zGujg-HQ5I`c2DR;La4GlsSu%kEtiX0f=VSBnuwCrTA{p;gb?b_}R`+hIA8+WfFOvxL7{*r`*$CbIu^h>(3 zZ^ic!)WV`7;=lb8{_e`}d?`6veaUY;?{C2;5f{ifircWZ#**C@2YJrNxTHzj4od14 zUu|Sq*@qUF*r-10YyMrqL6FBs5r_rE($>9B?ha6o8Zmhgx|F z8&hY-iFR$P<5T$|232@W9@g__tP0e8j>5DvKAXQ?AK=a0pY=5l%@B}s|KmR$t}RhA z;i7ES2?*4k7xi}Se2P!+`CUamFhlN`ooc2eS4?4&Jv^N6U%hF6{^3b`z!5~h`}%k7 z;cvfgb5EWye|XYfzkEfw<2D&>ct8k7_v&S6X-dS&_7|4}rFGXSQyH)GXzZ@^BC(%w!i-H0qruk`1)Nt)cd({mD6&rwo}$#?tVCGFG#|25N~qq+GRGo&S?}{HWCra zeMH>{cy>z&rLVvGf}NsJ@F@Qy>i_=F|Ft~_A8u&=Pyg_D?G~r-EZ{9K^*D|UY`6g5 z6K3>i4Iix{Sh_PX#ZFo07uyg@NQcaosSG6|g_QKsDb-HNhTy1o1l;1Z!>2-QnK@yF zvUMns#r*=uN&yNUpq?zDR>sF?R8T4`8Mz}2+mj%YP@#aPuM!xn!hM*^kZA_YdkQK9 zGq`ZBy345aL>0r8+Nbjf?E!G~m)U)a4C+|H?7oE@B2>#fwrdpG9m>tZPl<+j=zt5f z&F<@%S_WM~fXe8?=XklHfxi!j%u6$$>3Ty2rCzOoxsElLyRHC#LcKD#Ld!c6p7;Oe zl2K&J5+L@^g4|8XC@p^12r)^>F||Gwzlu_YCSJSCF>s9aX2pk#)fG;9t7mSC0vrQ8 zf(Un>HJ8962Hj-A`kceFkA||(6)2`LzMZ5C_s<- z$UgPYgSRtqdxix%+#|#KGB4m9uzjO6a(;nm`)~U>T1%YMw+VBx<8jTas%(YRe1ept ztS~zU+|InOpUYqbkB$p4Y3wL`e75ois2AQ3-&05MfxIwL$V^`EA31-Ff&+UonoX#Y zAAA{JK4vLa#*^nAE!*N_!3AOHdHc^xIx#n34jJGPY8rEnxuM`=cy$Pmc*wk-t;$0@ zIO#v&l%xsc*@xdBe(|7PWf{%WSNrYT@4s(PNJ_fDb2&TAUAn^3Arqw*n6g0^bPk4e zV4sz95y(L>f0TbTkq~X@uM915>eUHvOLg~rKwgR$Mwv3yHCh~#!fmRjvw>fHQ@1-5 ziJ08w2k~V6zHH7qqnM#bm#icr0v?n(8c{SzYr>&pfHFls$d7(kK4i@={DCEyvb1Us zW^ICuC~^7r`?s!h-0@@g zxo}&hyL*1?K6kkBUVZjceeU`~Y3}&)e%_FIA6{j-BrEF?cD=TH{EB0tmfGKaP6+c4 zf5;rrwMMJWyOj-g@eMT;^5Qvkhm+8UX@Pdy~_vSB^Kf#n%{lsU!{ z?r&ae4{raC(_(HhUx+{&?lRwjAXo)vmS!BW7wFF3L3{P~4GNUqjM%_pm7RRtsNXC| zdKCg0!pM7UEjC8dIPbGeB8Q&IW0vQ9`xDD^5HMeV^;P@q=CyX~)+TdOC^42OOz@bd zJigrVKdbHWpn|7?vK@$&d=AiOaIhqkH@fzwgDJ9k!%FOk23N*_opqzU$ z%h})kZe0qt?z{Et*=g0INy8Xs23WU(Q_H+O2j|?JFVfQ>A#gID1^7jwzrwivx&(7SN^IU^}KCN4Ev zTQNX=EiRDx6*`7t#Zv?z1$^gm>V|pwd`c+WBi9Vy2QL_GUQ-tQt1QHW%0Ii}(4PgA znEg8?tX*MTmx|PU@Zvm?A)Ym4DIRNw8QKhPF#s7XH*`?A%Qy0vn?wkc_)k=U@0Ij| zh_e6S$8rhf(a$m9#Va5wY|=y6p@iJ@!QGbZlk>InY(!vqE+KAX#qgp61T3a4(N!l8 z<)`#JVRj5@JyBs(@kOA`o)yQ^m1|?gwhBX-jAST$RdO1COKdPNPNiSR+EbdO=Lk{9 z+$kM)(Q>@So664DaLDF6DvhKZ zVE7%d!N_S0&U!AJRPbDA3Y@-tJ=eZ{^tL^D{}x#>Xb*@=-Me+I-TB|XAj#<#GK(eb zm)D78Y2-4Vz-@b8;xb+`ZHLChMPRY5QY@be?>sd7)7_5}vqN4~Tc4`_;LE<+4)n`g zTDIK;#=RE6D zEjXA4{r!d1eWcS+c}fz{jh#0!?t446YP0YWd-Do1*FbXGAKYYZZC zkQ}sZ!v@kt;ymqn|Ae^;!v}^!2?)0n7WyUu`R!TPWQvyiJw@IDZMnoZ$h@5#pU<@t&NKrN=tdJ=Z@1?*`>I z2n{dLXQ?X$NnehzR!{JjMUWnXi@W;>j|I;3TqN=7()t2Q^mcp5-18k06BY>EA-L1m3*!4ga1Q+K^I&Wr3*Za=7RfybY z{jR|F)p@uo#t88kE0kfR%@hq&BShUcE%gxE>Z7ERfl9k>S+`86pV=h@nnI3oWBG(0 zeTbVfV%SlY&_bjbP+oIbug-tC4V~WLvR*J=kp@Gb`+dUneQ|8$0)u4=z9!5|dAI;p zQycM}iE7&8Mc>1CHW0>yav&KKq6+TX%t9DeScc?polvsh3UE=M29OlPY>^5;;j@P?<-SP7AvG3!+IyxsFz z<)l&;e;ITBP!%|W9)Qn#ieKAx-gL~~-_oLI1FQTM#Z(_-z~rCNP;ZlDD`d+VT1T{L z5J`UuYx_T9MeAK$+Z}o_~p_k4E0qE=cVi(1|JYva7w`)(SAV&9v+U`O|t*pPW0vT zXW-+g{po-DckMQZWsF=}!yb?>QDV^(QFg-VGk%F2R3M8#-hG9qB4Ok+Aq7fc@2*dk zKv%Zt-g1h; zJAa}`a}Vfez*bLb$+~^>k9Fm5!7og8zkchKrr&bqM{%f!%*A)K0~Y$xqt$)+$+=EA7quSM9}rW*z39`?AirFCRQ;4{ls(OLW>0>=Fug zb?aL8(ai#1Ds&BLP5Q$)Sdv=3o&|V{x1=R5Fs`v%K!Li6_!&fLBw*exJluTY^?uv? z>6do);@7r?aesv@I`kPg}rg_GqP*%!g&35y#F3Z z5KJ4F+wdxhLp*Fq^aD$S_6T(;sIOn$;t*gh%2KB8q$JYJJUd`7QBZ{9bDkKXjDYF6 z6ACat{LLNOL2+YrEOVO98OwJL5k@=o>*?`g`{nWTc75%%eTn2+!82Y)AY^Z142YvK zOOx2kRIkZXQe2*cj6{eijP>*lmpLcoE=m;+)_ZmAOlQ}NQGqICC2|Fs1mbtSQU#3L z*cGu!UL>l}??pUupTlCwvVk#kYG-WRuR?Vl?ua#Zv7p^nO)~^RAoEHkQF!1*qf4AR zey~o6J&6buGjy4xD41-FrQd(I)8!2fJu7L<^nVqRR=43#mn|9Qbm^5_J2p1UU-1l6yhO!@;~B$==N&J3PB}KxxKR)R zuLn?!pcngx=EzvPR&dBh1xI*VdT$G~;XmiYr{H_M2;IFuXseA4H4p;ShS2AEawCQx zcgem+-HUJ5eww+N9lxN>%;|zV|8)XBJ4wNWes?=LcV%+!(OuO%iqW}wf1BW?#T$fp zO~f*UYbL42iVPp&%Q&YjZ9D!awhKqTN(fL z&cKR}VJf#=DNE{vf6m7b8jA99e1dZ+p`Njwc%w3x09h|MlnG@v(Jr(!|d^iu=CIx=k&X!%aP8p*;L zV+Gj!Y%!tA%RkjS{)!J3p*QImIM1iR)ID3jStdNov`n>A5vRf_eZ}$c3cm7*8mY&| zJDg;vB;9yDrV}hSiFKXLuJ7Ew-G2Q38SAaCwa@O|U=8F_d-m>4`{h>%jHN3#n0x%{ zcV954aG!ZH=+ed5$O*kx!!xHK=g_s6GC35tw9L7ID5oi&jRWZpKI^@!?E_~w?!SK8 z8i`P6&tA7HANJcK3M;1!fH#FAYB|>FT$aOlCL~SaA#b8YuaND#?%WZJ{!b3h+DC-m z0lOtFaN0>9PvDTmm|fP19TFDa;25?IHnLv=M#m)Ky;$ka0~%U0)KD8OsQAwj$$&V= zu#86!vQj;0S5+PgO|+rRwyYy15lKQBhYLzrK_gz_dE zlmw?EfL{VG=ZF-XvG55aI{y^~%9t6}Re^eW=H!&$KlM^w0%Mp=i{61x$QWT^l9D?@ z>AgAVdOBSn%sy?5lV#j4PD{~@%N0Ur$tXp@O5@$e0kNo|6}p7~`F3=kgYh zCmt8I3VVka`y!DGv-q*U9fQPp;JXBE$XtB#G0%79c(g7VD25Y_@ws-?AvBH=Vv>={ zHTO_jwvU7joyMzIaH!O%Nt}W_UI7&E$R5a8m?QkwjfYhz?7HPpj*-8+DFp~;?4UVb zlX%sYu;;TCwll0tC?28avJDk}jtoP1MW7;-gvXc9EE5S4tPyj2Wh4-WrS1`iDkhzP z@?TujE#`Bvz(Vh1&mbk!ejX)bl*q8PiB!s{MaJ1>Iq`&H7Gc-3opX~0>T|B$7)B4T z4t7V$6R8jgjIIvn(pTnvsb@f6GJmN+138_L1*GZ&Fv)Woapf`NiGsh(UsFc>N(Ur=XzJ*tu z`|dw4`|V5k^p>UH?n?PQ2l6RiPZX}IjL-4O-}2+^SqUvKe#=MxXjFJv78smz;w`^h zCKqipdMPW;JK7BoBI_m?<3<_O2P)L8SO9lX$_%hDB)S7laX|&^uhw5x=7A3H`I2_z=X^_B zg&6PQ+#v6iwbEFOoFI-8vvDgZ({NSbUtyWGnAncQt@W*iD1&a`-MR67n=Epq87Hz3 zrhD{=dDNYc%waCH%h&G`Y8|#;o;+)p37dW-MEZ939qHrCZIfp+zhTFK+HfguFa%^q zF9IIv>EOt{fOZ^m2(MJ=MJ2Mt7Hq6UZ+4+5(*L%fK)P_P*Jd0#(M>m zz3hL%QQz&``6VIqlJMwb)a^TKpNYgmgKx)kpw z7$uC$XMdwII7cM`JqQbJS-&rtu46EhC2|3@-f6dnFEj#99gheEaHP=mzL)i6{8a9a zCpd3ME^jeZO`Fz-Fmx=$fni?Q9QrAxs-km_TR0Nh6js{Tt5mJ~GTF@e(O&RIeJezm zmPoY0!0l1rfN^xW*O((2-TrZ5>t>=HdcX=9$6cD&3$SkGgg7sC%zFfp$^bgyvxad5 zGPIjMQveQ6KjTTO(YOFLcwuzFgM8I_CWT+|qQNAtJjP3!b<0i_s{O4>R4Vf6%t)X` z_cPA#42gnY?@Pg*BOdb^x{A33iR`7WH`DF-s@n1S5uqQRFnbz;YYp5ERUd>zsB9hvHH4%}av+ z@2)(>v%LF}BGSyobF*i_NpPLI@+XyAPLFQpNL|!pbrwr|eqj6<7Cl9wtuSW&D$KrO zm_c{)iESB`F#@51rSg!EJi4)>CzN1_0w=!-zlvO46ffL&?%Q$4(~n*@<(JTw&(Hdm zzWEcsNn;XSr%x3AmM7HdEex(g`89)!98!@9A=kjLg&L_X%jG1l?I*5$T&?;3mQvY|TO zAEstD3>dNOFA^X1%l;9~nofyQMjbZqr{&)_nDY7HttdZ)D@ZA?%wL2@TsgiPa%b?MbZGfF z8hYQ*r_(Qzf5y}|l`}LNBa(J(%FFtE<>FpIkT>(&tXJH0MMZt19)5(RxU8g}Pj#U; zey=Yn5u-VMOpV-=rhq*yTkx3;gq%ruD7drQb@ z|1aOQ?Q3`2KmYrGY{`a!4Szf^J38SJ=R8R_IYN>nl-Rxb3z3if; zE*=eEdN_byY8VGz$Ud=@8!=^J#KH1CylX>=!jbtRYZhm~8gCN@l+I$^Qc-X zSbNR6zGSGHP}XNYldu2?!Z3hFAPfl2{uF-2qtyf>^ea0b0a{gz@U!FG_jmg!EYgp{ zW{P1Hl0_0Bco1f-KAq@{VIP{5(<5Brt)@-=P>^KZsfs4 zuJ8xDoS9_EBeW?^6is)^%)aPNd!kHQ7oxrPnXM@U5U*qS%=|~ap9i4jUqK_veNXc~93Xo7u^GD3`7|e!TnLsf3 zZ0$YsWIONPwwEv8u_3}H$G&a0FYj%&2X{By3d?Ffk|43qJl-l^;erB+5N2W(E8@7iBGhkWE3YPQ=-r+o9o-@3p5sXCZBwDazi8%~R@~^Qu{o>};wtkHa zUI)?spzSBj^^RFXI#H332z17#6#FC|LD|e%vK#=RVTVl?(HoOV6-Xx#4WI?d3T};v zQktM@HdqRZT3Bz`vl3FuYg17T+TB~*M0Xb3&p$pPI`onD`t9D$&Gz}-dmQVwg;&4J zrv7YbzB(mD?!2;Dku3LMv-+~+g-uId0d8A{`b_@O>aY1#2Pts#o7uYx6%~3Z92F%Z zhcWZHnj@+;tSS>>^}dQ&c&)$liWPu98=(k({7nJqMiw62UZG&0Ls3ab{H9l`%_igZ z-0Yq+WtR!rn`|@X__Z-xGEUs6gAEsY7#3r~WMeG)G2!_+jEH&q?mo@Ksn86xL&T@id3We?nZ~k;mr+#l%DMpcx;APg91%)BTrcA?FF>xPW z5t{IW@{#edh7MlqGifsy&j;6OxZsU~nZb~LQKqf}M2HI_`_1$I%TLt?4H53G_We~j zBC&+ga=zJz^i_T@qaZkE+^yf=yO*t{fqbYx^;_Mq_Uk!+wY?a1f?WvD*aWcw-XHwn zh|R{4PZ;8rC2HjgY6}@dHW|W9PInQye*H>&v-7gO+<6tR^u^ORWVWreFF*UDeNE=; z7D}T($8wXCrS_hA);*THY|+y-k;q({7S9o3l)axHw-^8AFKv_giY<06>XD=(BFwZ@ zh(YxXr1L;T4Drm&(A`8~Z{E2V1wF?pE+0|Mr(`Ctk{sa1>S1#D%p`@~!}sm^+gEH} zeHBl3lYSWDWuA8DT{dr6Nm5a5A}%7F6Yx@@dMVT#ka#Hw_!82UeW@^x*Q209fnUZu zULq{^oQ&rq+Snun{pDRwvH_nO9N+%*vVCAqcyoKLeRb(N`X6!T(iH@*xK>$MpTbo* zR91|PMeO|(EU%HpX;k~rSxcYgK=WI(Q zukbMh3|SERLYVQ%vuaH~QotHso8xyGY`}c^C6{4%)+T#9w=FWg35)6BPndfev2#rS z0|{npC|P#Yn&QbL8DWl^i#f3_`o4mfivK~F9=t#!*F_)=7v+W>aS-2OTrBcWSfB_vZgXH>kweedydo?b!g6()-CFr0cJEck_A#l6?; z-HN$XuGZx^gm1u;VM4F&+l0nZ^%>+i@w(pZ;rNo;ve-X zSQNxL&BiE-u<%{lQHhCdMHzGP~;6h%*#kVKoTvZ)K1 zg$b3ye^b3K>J{eToMH+{H_k7*g|^(&mfySiU3q9k4u*W9OhKRWkhqvBQ}HAofYCmv zIIYtNiDRvirxDAo7{bzkdmB5}(rxfZ1=4-?MW%oDyD~lze&QSN%xq%kgR>TzGS(}f zlo6>h;FJq5;8NLo*oOyz^pGoy>^gEr=I?71=h5J4JGy+(?jYxGJlJR(_ph`wqGirq zyFPZ};#0?0TtiflHwxD13ro4+*K@-SxF`txym4hL>A#=4D_^XMJ25o9e-R<}|G<%?XVmacB;_M1E+9P(BK=3R!DjK2mi8 zAGkIGWD$AKcWd)1Wm=zewA^)GuQc>AL?~c?+lJv8TS-Eza(DM`VFz@CBZ&f zY<-_O7p<0m`PXlmPk7zF`ts}co4dE$&CPX^f;gj-dC8pv)?%K%%Et3IuU~JMV5(K+ zJ8M7Rzj)T(KYxm_-EFtoFW32rd6;&U#qCGbd&~fiS;uw4nVl!>>|%QI((TXM3S0TD zxQh{q7IUlz>mi)iSn_hVw8mTo$`si%dkNG12Qa`YQX@( zvDIvL-|pLYta+}SBQqky-}hT_GOJ11K2;|p;+#FMz4n^++G{@~)-GOqS2#3$hgiLC zsy{j)usxGvj{PCBVKd(cqhwTX=4!7>_#j|j8+oRSr&$@l64jPv^ zihxe~9^QvCaocJ;J9AoT#_aV&+YHfXQk%|pxGw6ahmXsQveypS>oKXh*k>|OiCkBX z&x}JWA{oTu1g9;DoNGp&P7>T;VhwpF?lRL*^MM~uMk}tDs;lC7)Wa*1&^+04(MO1y zVIA-kkiE?-ZOUF|0}ISxz9UYC(X@GlRu+*G)7p%n=>yt6!_+(WbQIcTkOqYS`%{Ec z$diRZsYz3W=5nIcL~#6k7H!Y(FlphWkl-Z~Kqq5hA$}nkJeSzd$KCfa_sR$u2Q3C( z?zfeI+kiNi!PGU?Da3`2QpXfQPVL%Y91Cm_62{dQ*cos)t<0wCs9ERUX46=Y77Cfc zlxL2M0+wExn&6|QGZ&rgU06t(%vj0j?2gM3Rb~i62phlof9}B1f6FpD)d>vs9Fyb% zdoD*ASIR{QI$>!frc^8^B%@CL(q>^}--T_;rW{F{4uGe?L8c~CENykZOd!)HuUfF% z+|(X@FU-_7YHkV6$|nK0d~wq;SK~P+OxlEiB*II9Qequbp7|NGF`BvBx${%3u+iDx z2K;RMglJAZB5%1>ukfTatMb-1kAfXb8?1pIQpPfmM&R7S0p?oJ^UBU{`$~VQ{lTC8 zDZUBQw)6h`Sj8Ai@Kl06Wt)(uP9skp!$q~KcvPU`i+vU8yYoKNIBK*V-6bw}SK4O> zpoLyPna=)J;_8P2A^1!3i|78BUrDOaJ!d{(0O@?`oT)@1{XXMX9E%yAzL!wscq(iF zd;ZFVwN9BeLg-vr<7aTg!nHh@f~1v@ytb{oOpfTTCxs^NT;gLwxbQ z{o%ny+A?wTJO6lPXKWye11H)UIQrYyZ(OwO;#UV#wPV}c;kjwL_WNnsmDYAxVtxK( z+5WO@gj**~2rLbdn0ZMEw3$y}Hnw$ zLiGsaw}SK27AD>m%7F)>39%-q*0RnT9fzRH3|!OdF=Jo*-hLJVkoY3eIsq8quJ10m3pjxo&sG?XOv|g*HS4`ZU<~s*1DPZ& zyVtKUiD2HW%YOFRmpCoFYTKNEbN!vSF%Nk}9pUqkT&EoCWIpG_8q8Nyvj}D|Zi#Ra z6;XMX2s3@sFQCcG7#wWNQIeN}8JD0a1e>b$LxI!SYjP?R`$53kkW> zxlC29aTSeQhZfgmC+XGG^09*`!qJbHVjr<91C=-)$QNA|?2EGa% zUJ+_E)n>TFX4^BL$b9cKD)B5DnZnonvOF1VjX#rDMnQ8FAjCo2uD89yfZL1Q4kY>T zN*y%TzB=Zv_2wCk72hp8Dbu0MvRFSxXnGU8+!xTWdNLyC8Rky$@05AEz`M}|M=~;- zOJeGgacO%PnL>n&3yqJwMhij)La203&2NP?{$(CfhW&yn)>u^Q`CGrksOQWy{YFp} zKVxN}4;fR5HFXwacg|kI*Y`#)>&9K{2niS%Z64Au=nK^g51p*shpw4BqM{-?$7LLsyTzm-I6f}P#Ji4Tot>!7M z^l@F6Wp$4nTgwrqwn42lmJ#M%`~nmnDIiN5N$F2XgDd9cGF`RFw$^ol zOKGd^S7?(Kq{c2;p4-*TcP2o(B!j(3;TLI5J@Ip3K@wOmftU3=2&<-^85n2u7N{Ou zVK&+~zDBscciv%34FMF0|NDf)z3;P6>!VMe5LIc?esSk+TStmqU~lQ+y?gEXCm*-t zXOG(k+SoSxSSzv;1P~2EeA_z#IU#ftqQRVCLL6Mj;ppAlZHs-Kt87>tq4hF6?U>-| zr)a~&5zG`KR_0gsN7C+B#DIN%u!kngR+=4xH6R_I9I;`Yel0I=Wq+qgtiz9hGZIle z8tor_mYI3^PK`jO8%+VGA|TZYm6ZMNL{>U>UDL@gLEFSL`4wx5_aA-H?&0-&WA{2{ z@Vl6V2_FbC9OHQMa!+R_G-4bcRxqKP*D+9VkvJ;ZT4%T3sg_xcq6m720ETYs1ADbu z^Vu(%egq&0QW1~Za+AS@0iWSTYu~&~=dI=%lYZ8@7zmMt4=1H@hVS6}r)tZ}lx!r1b} zpAhPh+L~Z;urhtg5hZ*~65uIeR_pUw5d>b%r%%R!H%=YP&2oX7u#QPQvu%!tclX^F zJvAHJ6H(Q1u#e!48jfR^K9S#Xxp2NAjRIr#VN#(>FSa8B6@+`lmbOvMi&_&P29)E` zYTY<@M9@BtV1R2JR!+p0#$lcuPyk*d%;|aGgP_j9|EVXeV1f>Zp^a(*StGq5BKELw z12YEwcRb3>s{7iZov*js}-fiK3DokfDbTx&mizSq9}``>Fj?|#rW2>NhDAJ;uf4%{+O4cl@flrP}z zt_N|qeWNTGRAzH*TY)b$soGpbI5D~ioR6+tzZ8Br|5ZS2zs&8zVDu=qWq%m^Kr^g{#3cvUv2O?vYdx-LzF>610KXlhsmuO zV+nH>WT_6<#;e!8g)0O0S|YuD??;c?4?g-7L_&Lg@2z(C`cAuvf^hoqVf*aigLd!? z^DlW;HX$BFghAQmLh1(C?maZzo#XNhqB=%v-TCG>+ZER0)&a@@v!d&+=REHdH&X&I zgZLX^M`Qx8@dzBZ$E=N>uwJoweVx#VYcAIkL|_l85y=x*c!{&jm+r;{sySPR={{IP zkcli3Gzpthx#l+sxlC3hPq1-7l;;O zNVRj;*m}V09Laq~7{?*&aJ_>UZGqq(3ouN@k$o%!Cq%3oqP32UjXVT78H5Sax>kn+ zqS|;0%9a5iAdDv?C3_SN4jr1Kam+ibtGJ%@VjyX&jMp)QdfEMT%L7}+=b z5RFZxcs8w0JOk3v0p^G1_`-kJiS1)T(2>RfB}`4)g5ZUq*R8t?58>#z@izoN zV`xXX{4T=_Jzy+lP|ickujohY?dG?{s)j_1jd(6`c5M^^gm!Ki1ctq(5fH48Y6mP^ z5c7yy$ShGtaER&Im${hJz;wVKY9=)_ZJ9tdckmN=-CjH))ZoYWiTO>GB&<#^u@e2+ z_rKrRU(mL0>(z^b0sZQ6!^(lPLMrnd2gL>AcpJ-mfiz`Gw6hEfcTA{^jpHfJ6|Vyu zJ2<~OLa$}Ot7Eh&VxA@+DYnXvAmPCK|FWobJ!yF-ew!e)9GJk9GCx8a!$=F)YYssB zo$tcRsQ}DrcE*JfE9UEbQR7D-i6a)JJGD}$0Z#mBn%Fen$015#r7cpPOvQkORN!mVgd70Ij%TJrENHH z0;!8VsTnldk4`?U{mRfh3-l0kv6^^~!^j;RB^{Aj8XSi)(0ij@21{ReFmC!e>8`?<~JqoFagO3)V75@Bcu5RYhF%vXb{ zXDyBV_EBcxcqrl9ewm2Ryd^?1-t0T1-H=Xk6paw11MUl&pj^7d^lDTak5QW;B#5-) zzGJcyW=3B%8)DjplD84*^&*|IUfnRY_kb~662~K$JX@*o#DXEQo(3~JC5+=aY5I$p zvZ=d|P-1-4Q;90W;qrluK0*}rsX@pSY`4y2beVC0QNVO%K%-F!ImZOuPr^urZV~dR6ShzL zz)v(m;pNgq!NgPwG=PIYuG1;RipS9gc&Bwjfle(MF)8^pFN>eTKrJ@m>TEA~6xxK9 z!!a!dKnCDaO<4)R05Z)rKk-~x03YWpUx6q1O?rJfPGOp>FwNy#EHfd1zku$%SizH* z<*VDw!vpBiJ`R;n9$w7deAaYv9Vqn25jnsBn;1-K|n9! zti)?!6e|dA0DA-y4{hoEN-zn835cg7mgE49;@xX9^`9 zI2@kbCi8yMSe6L6)Uz}SD{o|h%~ zuM6fv2b0gf@i^@d{7Bz1)*1`voF#P6s|#3R@l1n|naSumdg?)msQ{-L5Cz(XB!{$5 z`C0S_U{Gl!x{dbEm9_Tn#wHRW4oPgM`SdxGEPLHHuHGiLCwm|d*xdi}5q4FADL`DO z5L4!E+vfDE}I4`C-t5CHS|t??Mv&Tk24T>eEC zZng9!0>x`S^=POg7)NP+##n`6Aw;Obo?xcb>>1M}((($7`3OgcF$@AJfJ4l0WoyyL ztS{qJg(>(-dzZDvGuB4;38sI#gqaAq_7?~$!9+YI;KMoY{MM_N?573HKb6;ldc0Gsb6beIc^Zba9hVf}Csvf$3x3UP7~WOOsKLrYFD- zC#ESAOyOjOh*lz_YtBPvjQv_s!vHQa%|jeLHkUXs9w#%A)hJ!FnGRu^f$0w8C&QDm zil>>hPF5vl`Xz(rFF00{I+I*VOr3G}=exuiD&h>tb0=&j2G1nSdi7@Gzc|P5qzr?Q zh^Un?(Uj0(swrQhW37?0Ar{OPiHMTAS~yY?ZAJJ|c;lHCpaB9+7(2wp=4IWJd6A>F zLL4A6_KRmU58R-?FfX^B@e5|<`Z_RSd|b;Ej&+jGoY*;kgbCXJIcA&*cm$tg9Qa^- zjF4o-dR(6qdJpUiLtWfw8ygJu_zpra&Q*gkCSSHcX{KL9_!{6`H^dYg6EcETpE>UN z!xH6V&JaCgtTp$_5FMYgyug;{VfZA2;;`Fb%%c|S9919`maf-2r(}qll3mZ6fWYER zh_T-lj{K(v!tFK{$4H_k%q-I~LTm*Wx8CXEiXPLJ?RPKtI)m{518CKvCTk^b^)WVJipm5`|7v?!!X|YZzcl+MjbYdQS$<7q;d9{ zxALFs0qI48 z4sQ}^31_DhfYOKaZpr|hxOh(<8GG|NnlYN`2vgpbD_4mRTw(OGCy(}3hXszNKDV2| z^c)O=DyUg{3j`e+63>=MVdhbqb{L)HQ_O%e0qbz$Mv?~58|-;i!qxLvAp6ceXP$|g zFn_>sh`Hwk?fZm?D^9Ej51zCyzI@QW^)o-$c5$|P$hMzXUp!^sCQd0gmfIeu?QBD^ zHdq^`8LVZwrUsc?UC;hpyz{fCw=;jrm?R{c-_)4>ZCex1)G8q=?gd3|fmrasu|yy_ zWg?H+wgVN6SK|g0!8FHEf6t2T!SR;}xUtBZxWVN;>TXPbN34B&DEPwDFWZd6*y%K< z_wfEY!WOuv=jbvTq{rJ0kRv=f#RII5==(spz^I)Bo_a&(Y9~xU#FfZsV(gy*V~Cw_ zw(l}c`{W@EYBQSsh6EA;y&#wfAz-$*K`11oUYY1J3ja9~^UQG;W2qUKNpxg3;st*~ z;GFQ0OdU(xv_vV6wG62H`mSG7i_#5!qC zX9g1pY_KHF91r1y^5tJE>W zQE+tto!f4^>>NV%wrQ(;n+fm#X1O6D0y16~7}WF{l!98Air}@A)*3 z5}$yGD#?g_%}=Wkk@?`It#-9m9@UQ)HEuFS2RN zL5SoZU+Z$r>|FY63NGs*Z?fl|Pq}$uJN%l@q^Y@V8yve1-nJoWNPO82;RJplASe{- ztzMTk**1UqU7?P4`HS>bX2E5k=jn*n`A~QM;VJ}9Ga_q-#1Mj+;r*-g1qIn12Wbd` z9uo;?ABUs^8ny@$?jaQ`(dihpzs?MS=@kZUncOcB66t6{2q*OX*>rF;SJU>Nj36KfIm z(l}g~6gGP>oCo*N#z9a|lX(IY`uInmw5K>K-6t~CYfkns1fknft`aPN1uZV|hhchn z=_HDf>wg~fO7)qj9x`CtBnH;2R_ZOGP>AUF>jWAF1x?R7lBfkp2qzF!C~L;6M>#7n zZcp@3o0x)Jdh;$pID7jLCWM-y4q|L$T3F{maBdG%PQ(8(j1;ZqvkF1LV?Ic+-V=lQU~xWji}~k;6+gd z%OaRs*BEZf7jx+s^yrW+eV;yf!10UhURd4YbQv6zSgKeAragqi+wa_L@7%df@EXoi zMpIn^SE#YtPu)-zic@GTrV@=XV?f(!qkR`2ZMS3QC1VSNF&%BO6#shJZE-36y?B~e z`6H|>x2kZ=y?s^n$>^(I>-Iahy`?Ib8B8zUd-**K(!8WqYqxIk((x&4jXnuAmn zEDUKwOv1@SnQ26oBONWgwZGyY^@1zG`+5JY-z)8@ul`CoDT-hA!&|tOrd@qEnUDEv zb92`ppZRXywt4=Pkr%)MEyCX&4hkbnuuj|O+|pmbyH1NL+;+@s9-3A-I-im?ZBE;1 znC%*Y(NnO0yEYJg3B~h7`r)>n#JaYi_5_WPpNhSKxPLxz=t3kY9 zz1(j<_`%1(5TSv6m@-pMu^xo|$)`W!oW&^z2XKnd2M848-!zgCn#T!)dU8$}LdJH; zIiZH(J7zDgu{4L6-8~FhA;Es=faD}lSSY4y^rs3mVgEFd>SUqmQ+Py5WrB<_8LrXa z0qysUM}vB^V%U~oxXbLn8z3n37{3|Tgego+~`{S+pBjFvTGyd>g*Er_MIoD+O#i%9Ep+bTke$PyXh>g99$r!?Dn`PzmEMfAfh zB=+6Cp60i2O8uEEj*pzhazJ#843lAG+(#vXN2@dTs%T=<`XK?N`QTe={NkerQDp8gk6fdXfb?BDa6Y-k3?}Ls z#It)Gamd))W(DL0<_Ro~byTbnhVf)wTr;%e5mUI5x`G2B>C4!;9iL*LbH}f=TzD42 zahxPDj#1WG(OPn)|Mrt{3m!mwfWLD}B3{l#GA`2;4x84(6g)Zx8ZTs?B|4J=68&(V zmoO!SMMs1${y9FLRa(LvXC5=Z)oQ2Al|4d-dXDl}2~e}gQqoJd%Itw_ZZF!|VZS?V zT;tFWWMp716;(*3MPV>=D+S+epY_{nUw0a!+Hu;&n-F5>2)HNC2M5x^IZ;omO=s&b zKl3B+f@$P%-c*{)cQ!$Mt9EqXO{QjQeO|W3ch=FtCt5!FDzEPxM}>Da@`9oDWxUf> z(z+&XftF8t$7lv#<}+=R78E^5-N6U*nc}iXv|jyW zS_|%;%=6F7$TnWvc1)205PwtnqA{Q z@L=HAm{ErijtwT3r0JYN_ki6)G$-TZuHZ60g8`lpmh091&)egVe$WPJHaC!XpEJQd zqHRDqU%9^9HVq}m*?@Pyeh14T>u*fZ)%V|T(BifYLN3AxHL1Hsdcr#D8EX|2i1aBo zm=Oc8h$NLGra=0AoR99^yPre3x3AxbHleDmre#lE(^T@3pc?sQwycvxF&?!SnQfUc ziXcm1HPJ}4J=9x-aWYGw^#Wbu0CBg~2uwAjP8$;VYEWueb;Oj&Dd8oCFCi3T`el9U z#;sfJ!Tm?=)6YI_%V^Qxxb;4SSlp! z6{52DFqgS6cmY8mlI!kvn_*S(Bq0N+_Ynk)$2-ByehhIwBH%ry0VxXW-)!%pJ&*+Q zOOWxnW^zqgKVBtPv0BlPbyUsNI+{31T|X=#q$YKZiAasffy=e*Gfn#-yRcUhcI<@z z5=>NUiO63>0hobrmap*SC1Ey3@{loA%vZqCA3$7?2^(=IlM{Fr(w2 zG6HG`l;jmIYIdZ-lrEmyJ}0`?Bm+qVZp;IlEKvl4X`jr`b<0=@5ORiUPZ9yC3GfZR zIcGpH*UbA^Ch%F~WEz$Yz`qkT)WKB*TJU7TI=}UL8j%iAbTi8YqNxN_xOvAG?_7X> zcop$hs{xMb70*aF1@By1@sVSh@ncRMf{R+8+-@Tt80Xpc38NzZ#Q;P!nZJZDS~X?J ztj?qb^iiS9xghR1NRE?jNSnkbVG~bp)^d41TSk~9H0EubAy@HlxPEo3-Cz!l33EEZ zhhj+o*<9LQ6Z?CCU=z#a?`JJrMrh%4fkW8HtWvFQP4zI;`4?V9E9HF{46u+%2^&(+ z+i6-_N;^A(T}0brWgtFJSE_Ds!@(C<&31X2=#3}*46f0>z*879S8Ts)?-amq@FK9Q z=V7MIJ!y=d-cU@&E-gqKg3FWug7I;QP(jRgh?@~O`SiWzI9B3SF)8~n zzwBW8L=u6>Rc)q?{5E9=gS_mz$-6E+-;zfhG*8isDyP$uO&g(X4u`;tc8g2aS6d=k zV}}Puh_;0yJ)>ot&EK`dPk;0Oq7BeGq(T5VgE1*{L^7waMWj@>HJqWD9ltzoFFq#{ z5nE!;ou$$*#El`)6Qsr-Oe(;J2r`$_7WXagJ$}v})7KpA&3Yz|Pd$!a8$h_nFm|PRH8-_O z*M1GE-DBOu3AxH7@$g{R*km9ARsd3R{z(L6q(w-X2mwX`-N9FLm3gWWiBvTx7a^4J|CimWK!_1hbc`5 zGo6uSFeu%e^$4TJ)TIX51Aa8p{TP%1vteBO#3i1wHiwSf4#ZCO|0k%x`scmPh%9IH&hM_6MWP;FE&FX5ewmaK1Bz7=>VyG3_ z$<6Mo;!JA6uGNnU^WEq-Wu zpt;E_ql;Fm*@r%P%UsK}#~3;3Eho2=&H^ENq|w}!F=O1~ovW5kyB*n_@9Vs@YWgK9 zw7-t8pENg@!a;}uLP&%n7^>rMxnq_IPF(y0-u$tkl$D4{!4U-)k0N7@6=7JXCxr{! zAhXxvGo>QqUyq2xJb43MAd-AVcS;o(=k<`f`NsYS%$bxy~2dM%;E~g=!s~Uj(^~2 zv81&v9n%VU(#9IAm@ui+HOwjNqdE}oF;AZx$&EeMeGUh|jx*t43p+S-d=0ptu>EBZ zZl_^C>U#+ZgNbj&IpmZaNe-xmOElMVJ@?qaO8C#BS9rB88rp7eR|pJJwd zc9&1ep{6V44VarFkE_o@N2ZW^9YbK~Qi7L)hj^LgA+0bnM{uK~HO?F7!kfCVc+gpF zR3?hKlyH^aY7IefuQ^e|Uag^84|(AUj$JX-%Rbp2+bu-=v@GlL+(pd#002M$NklN>aHMz>{ll3Mh z+5wsi{vu;_E|$5V`A*% z$>KgVrRYBOxoyRRde1OJzIgT=0zPfm-+eo$${+T){rKh+)iXCZ;&GRRP&$B$pP zhXXV-%yL_3+M8R~W7a+)lwz5Ai};8Pj-5nU#@@BZcAh!V5SUf^=NbloIA9uq1}R$; zVUIQ5bDW-LFgs7x=y3{64?$(d-q5M$KZDwHpS#@!0qcl&hags;dNyE)YMCbnQoy^> z!v!=Yx=b*@%(%dBPZ}}0lEM&nGLWbwc=5}2il~mAZIvL)AQXTckC>NWerjOCR*6tr z7$Jc^IbKe1^LmHqh^N46nejejEXVXqGobH1K}f--7>&#o^v_yk$biFs;v$%M`)SC7lyt z4IJG@fs}E)0WL~iWff2bEtokWpsUCKQBL_frW|5Rb_P# zQeO9?d(Y?mQRaeG)vJ87S>kA_VlD-Dt%F8)@R!ibn1wyB_|EvFk;cqVUMC=H*(2w=YOVTHEc+`~Sae zSC_E**ulMMMWE|kBVFb!?dd*2)AUPCJ%yyK!c`q2eIFr>KE<+NS8}Jd$!q=HJ&@CD zI+cN(=!m34k19JvcF3B_vmgGrz54Mz&J;!CB6tJj3xtQ+$ctUyL6f_UrZc+pRy#z~ zxw%E?Kujw?#5?YzFWA5JT{K#N`}Oy?+SfnaZ8tEj7(m{&j|C?BIl%2B4KCsoFlAfJ zix;o4Hon4Lf3;n?&2}0>*qyrWgT^q0a2RYsA}q1fq-RGogE}EKiK-1cXmK)P@C95k zMHvv95R*-9L%4NPmH_i|(w1gZ#0MQsR+4(nHz!P=B;+`^Jnb`-~CtZ%ad13^;1kx*V@ni zG$9msFrT8bOJHR7YU6GJQk(MfneDbNkzsjs+G-SAe z(p{FVMTds?pEk&p?0;$IYDnUSm#|reA&$`)E6_f5@+?O`G8e4hu`7YB*1sJOWhK$b zadyjC zC1AdQ)*rz|YZ?WH2~m#0Z^V1G&-C58?42FVl@^ev@JXg+A7n&OF7BRrqm{ zoiA^IGi5ub^O}Nj6Ew;UIRBj3defsmr#OV=4?g037( z(Ng2wl5tvg1Rmf~TMBY3h{DsDSt>u&mqLZCC~yDF-Y1D=eK4jup!-RW2E*_p%?l z8qffN;$9*H5>JQ_a^N~5p0zK&_`FTo=CVZrGSU@*o=7)fFmgt;krTrH5xSu*VLIGi z+kr@~C*sa4HcYQvS#Dpw&6*+F%MsCXK6&zz1H!LC9BQ3w`zj$0nLuv4Ib*xYYYqlK z#uPQe%(!agBiz={3=DypYsra4+Kw5Dcy`?OiwIs$Hl2qg%F#dplFT=g34jPO2~-b_ z&Fcc#7b92_o5-aGsKz6pwXaJQ+``b63mIqNIQfuJWSV`CVFH$~HYJk~!Q4zPlhrOi zh1eO3_zbi78SRuHag;z1s!9{yTR#pIo=}7*JXa({_Lm6-oJOc|2cnz*1aQTmc1j2!V&5!xZd=erB z6g7F;WLn{o`2q|>Xh;`gJ117cA~58)pJ9&7g%C4xcnLfA*cu&8haj1(W7~P@7p@2| z1UQ`O4EWzU)ntq)VSu2X^68r`$r>=94sZ7U0-Hsc=n^rtC(Pdw^^6gaj|i&2!G6>c zur?CVIRZ2+h9w6O5N2q%gr^Q+&OMq2gWw&vW!w7t^sA#$ILZfUeDB2%Cx@2+<$LvC zT&>5vs4o|Jy62SVvw8OJsh@%C8xQi^;!V$=?!99moV+AGC8P2N?1D^-%FU-nHpBm5Q07NLa`d-Ys-}~+lhF-q$YYS{O}yxq z`mk!YY`lJ9fP2istc_l|z8wdEDU;iZmSFaaY^NEZ)kShZ`w|HRghWMP7xB@QQ;sSD zFl{oOk`fRHFv6$+;&Txt#EJY}dWo(`Br*4vU?{cMM7Ed$yk!~izD!9XAWl^m)(SngB~m=xENzcpgt-dYz8b&=f}_BIaN#<&>wNAJ6)p*h z=Y;hJuR4T}SyS>&8x!b&?{O#-mn0|%ITOZOA}M-C!{?i09q-aADRqeHMczrkw;B(g zWd_t#@x6CN_9`MyElhZrDV7cyNi8u5FUFH>podj6-?1LZv?TPx)>^7qeiy?Kk!n0@ zOl(OK=1cwQ@X5(0v+=j7(?f7d$0~_)(%D+IY;eS|gTh~|=}hBVbC?KJ%T2q4qn^Kg zEH>V1rZzGtA|s%WrvP9;pTBr%t6VJ7~{(^ zfnyDFu>m3FlZ(D5G`7j!KB7aP(DRa&CuB}PUU?Vsb0J@1R{w` ze}~wjnAxXj{WI2@XPAB$p6%0K(%@B$OI2G~zJe)tBU@skML{qTSE&;?B1|lx?G7l{ zFohm$V2qtHJbQ{3YhZ}k8JfCgG1{ZHpvPs-q)dCO%{JMsR8BmF0tF9Y+>M2rmQLlu>s-#LtzKFQ5CYuhp~gj}YO*rkMa}u(R~&8T<)>Qu6pIBFNFdc741lk7 zW)WCw5}To=PK|m5?&tufSN{}E*4FnC1ocji{{aO$CLt`2E8iVI=bh!IY^hh$2+ujZ z_^mpX-;`N+OX<8XA`5&hn`Ci90(yQr-hmu2DWX<_)tKd+33ZAGLvUvaniT>)FT-}% zI16eCJc26%pXpr^ip3zkvn|v1;^DLQIhLUN_g=QAY+)M%(>2_q&+uDli>K{dzxa)I z9q;xrRwES-SCsHsW3Ak_|UudQjsZyf7g@#mATp{cS3q*;d;vT*ROY z=QYbrDxS`9x<0G)`Kyb3-P0b3xXMf*rrYWJ0{@WJr%)#!R{ilq)%Qx$_^vlaUw*B()Ar(vC3kE*?FP$#9j)wSsUKDIb7N+2S%rlXPFGF+0zRva)(>v#k)v_X-X! zs{rPNn64h3G&nnKdjx(sAll9m8tWO%pP-OiN}^TLgd; z<#H0SJ+0gEU`qB=%JN( zCmt*9_6$qM@{~!fyq9%M0yOj?xN!P*&Q>GOB-ML*NNC0ZW1tu3Y@c><#GVc(Jx#fU zhCD+^m=Pp>xJvlQIfE0nYC%1O5+_;Wlp0_Isb9ve2scH77O6CO>uhz5*{w`-zzQJ9 zIAR6_j+Ws&Ad!EC@LD$v)Zh)=gZ#h{4NZYa4WCutjI&y>`Sm0n@mwHF=p{@UTOCul z!#t@kgWwFeMsTt&CM|uEAWP&ePnp5(Br=(cHt^oJany#d^9zT9yrDNBp%3sD8I@EQo_YH(9Bf?KehJk?_+*ho_mJ1km%8> zwMM)v6Lb(Q1l|J*>XHb^pwvbYPHdy?rl0Uv{35hM(IzSj(PO=Qu!VQ`qvyZ@#~V&D z>S41V_V5KDTGGi0pE%JO?0rCc6euJzo?fF?EIi;-!HGl899}{A>;xFcJbj}*d`-JL z0u|y;^C3%tFTe&*5=rY3gVc= z(XloFNSLZ~8`yX{)>BT2+S*}`5ZP>xdOmpjW_$SbdHd1*`}C8g6Xy1Wxf0=!Hc=Gi zfp?T=1?eYW?L*A(R%-d8bfD!YV@$?+MIQ6=G|Y&OW-XmL6gni*kYIs%m9hf^)ALxA z<_cz~eCkYJQ{}uiVP+zsDPAW_o(Z35pFC11D%>=$g;ADm$xS{AiB~-7QVuv+f%ND7 zu#9vPppch=@bV*M?eW_1QW^8ro>l@eKC2cl9Uf z^ObxMLtE*qj+XQDUwd6B<>sxcXcKHD zQF`Q_-g=j_Oo@j%dE7pMIX`5R^z5-~k*Dn*(Snrz^}h6EhBManJl*^lf-qs}W|e>q z$DFI=-o+(?#k=>_(0C^hw*>}E#!*f|rQvuOw&q3=NTd*91gHc%5+hh*9E1IR)D z$?IYeL;_wpJl*BMYJQW~NHovW~{R0#(e`7qHhgVsC*p z!dXSLo9lB6OE@A;IXoP~9`9a+j5zwxKo40q0Q?DXUz)=|ijI(UQh z_i`bx%$YT782b`hm)3=v{H%ZX)zZ|I(1H;v)Xe!K!g{2lf&eCsS^oFnMGCRGP&!?41yM5Jd5#7_-gr zAL;oYBA&@e;}|O)dL*(rr~o2=ZvTLv@n#iL3=~fTFij)Cu+ExKg&Z3jq5`gqSDK6& zkJL%|!rXxJ3UcuvwrqZjCuUpGQp463HHop4n?N0Udg~3}$MI%3!@_WmsecFqWs6&H z{}lo85x^RPE-`NxSkCF;Hv#pAm|18D_c+49Coio@48%S#bJSf}MRT z_^ZKGUme35ZgTjLzL4wk;+fAQRMD>)un%Eptn#mGqj2kTT8=FcALsv4_Po^0GX~k_ z1;#Qi8X>*lOPF8aD>LI?gc9-143urUEG5`3`&80kN{@9+hRR)P!kATju;78^nkEcheEav%ieQS& z$R2w*M-cQ8?xH=myo`ze=az&sBsdSDK5>oZot<{&{X67=81F;m_5jK{YpjMG#+-+a`%KHR7SZP1vc%&B~?FV1G6QVtVaX)06%#xZe z6JK}uO(rJ>nD&H&tIv_tnM!C@wS7brpk^GeI15Q&YZSud+@q=qtu8zM=f4$jeA?xk zgO|Yu0eN0hut+MPYr?u_Eb_QjLoLd^mA)(U99kc+GQRHG+LcUt!~DrmeBQ%6x`rtevr-dCy(?KLPJ}|v26iIcEgpn6e#a%;xklp0HB_ZOrgq zbc@uK{+uE4OQh^)n3o*i{z@Q&U?rH;+HeshC#07ItJaw-G-3%bYV_oVuzOu1p@s&w zEzS4xb?=0a%z21OyoixRkv;<785&!a$x;K3UySy=jjZPa=h`|Yrg!x?0j<>1Yck~L z95<4V@%6NkObo{+^FrcM1V&*@Yk-8SYrAtt_;kX7Qn%WWfiXxWxI(0?3k2aw^}+Oc zbn(Y>)V6^Ux6H;%;;(tvd|C*?*kKy+goWrzRGbrO18@~yJ;uO#9kVk8Dw&TOvYwe9 zq_D_A1MUT`wNUCMpIy~+5x=<9ts~+wd6w`67{bTThX_-gWjj2E4jyNXecOH` zKq9Hb+NJNTXll_~Pl6YuivUQuVLT84trzxH{*lXK=ie2g$`Qvm)tdZPjT>m(SAKJh}W+{kr_! zirIXIRo?R4JxWNGo_I~`OcrQc5yER=;k{&gVa-k zweOu_Hp1kz4zW+r0D$QJND(1UEi~AP)ghmqDy>m3NY*(*V7g~ASTt3n;0vTLI9@=h z#^Wb}Zdwt*q>j52(~<3h3v|I8hA<<6Y#W@^Syv;kp22Q!ks*j2YGbx*pFO#1G6S^k z+t;?+1P|yhK7Y{u#dkj96bk}AfKwm9NN-%-A@UpMUe<@{IVMW_ZVzBKBLe)Nvu_uD z7jy4^TY>Pp7CPl{?>^@9##*ys;|u{fVBLCwAQF0P>$N#!>k+|9$&ZHTNg-$)*%A?+ zr=t+22l^rb_o@g&lFAY!>`TF2hRV2e3mZS9F~Ec+TEZg4A7>Zg<2>pna+j|{)5-Xl zK>RbmgwQB95)MtAJOCHn3r!O+x64dVUCWIrth8L2spFC{bHdR^iVI@%my0*F!6LFI zlA$JDU(m5V#aN_y>o=wQ#9xUgfrK`Wx8hyEJnk@LwgZNwE<3q9>5(K_7IF=pbL1yF36Oi zSRtmdT)@x};=-JXyyX7WWf+mB(LM%NA1%~JnQD6#TJ}|x$UR?lw(;oVctga#+eU%ta9uJ{hR0ai|Lc!JUr;MHtUk% zWnRtOE3U>mB#vj^Qc&u$3`_MVHGvFHIDONV?3KR5w5KD+s^Jvd-ig%ef;f!TDW2BTljm)Pn9AocOqMV4;JpsRWN#^MxC5NNdYIbJFxkZ{ zC}AM9pzjIz!d4dur^E_rSn$|R31&<_Aa7jT>4lT*!i>r9Z0R6R2@4?e94=u*e3!@x z8^?Ztso2SHkaO|VI_fIIVcrlB9#%QUr1h>z$lUC==`P1hA(pm#YZ?!{NCfc=ZegKOJLLg5pc9w=4E6@A0}pS(hyr}!f< zpC-K54?!m>hgU8)$AFBv9ievLq^`WB(!xQ}!4}l#o8R-0KbZ@Ri~UN!S)wy1#+ zW|+gvSC8B8|G^)&r;lE?jn(b;SAO{yILCAwXQiX|ryt*IkMBQ(@m+7%Zf~=mh>01- zya%%wLkI?p!WN9=gy=k{0CxexAW92pO+H>7G3?AjuC4h_A~L{}STudJ(>0LET7$sX zi)RH_o|8+_irfyk10cXO0a5*YQj46JxG`Z{S7(AMuVWY|9>JDte$Aya!Z8HujCH8> ztE2X_8((SPGvPDO~S+(s&Iw4$?xALgd^*q z3v79TV-ju=hoWoO2r&pzgc54nZEVSLf2o@6Atv52WeKAd{fS^uo|no|u&j@vSu=3a zM(LYethv{U;fgAjLwuiDcIPHPG|1Bj?vwd-xB_q{L- z@tfDBmSP#gNXH;r#CM63-@UyYZOA2Uh^Y<&DiPI!;bl9k4Chi!$b6dLvbbp;m*c>* zIa9SRue2Q^S@^*xFA#iKuX4*EvDd})!n+Rzy+VPja z&+-6EE!4D&V79!mOB;X>eOZ^eM%%=D7zTF&PPvrO#|J`ObFEj$Jw4=W?Hs~P-(>c} zTrhbFck#wKTl{*d+zdTem1I^ zv19{d1cDjH>oI-{3J62@Ad+U6T?qpM(kyOoa%cxp%GlN*#i4n@MBz>ez**QAKHL7n zJ;$QENaH^nKEGf^RAijFI-(e>xF6S{tF3t@MhZX zyYt@i-xLHREYbk0AIvZlPLV*zAm9rI;WMHXoiA^7u?G3z z5$B__ePn5E+-`G9&FYw~JDk^f1mc5E34geZG`nhfFz!R7(-R``D9MWyUJ}=COND^3 z(?07aUuS-+LAV^W4vB<}K*(>ua9sjUvlv7Hm`DKPImx7V;3P_x83YZ(0gw<$ou*_8 zMWA8);3T-p4o(vQ7;HQjlR~_>^_HsYj;~dXP#&G!6+e|LFKd`{AdL3CjJj zy>)jNNg9|RK5Ii@-6vk=`R3aM)yGQ{GyMVr$P8_NijbnYWxTo;hpsUi>jI2jVy33z zI{2xAkAuae(kTk`2!hym8JGP?`}rN>6XY#qcY2g20I=wPA(vxa{g=TS&BwjK9n-@^ zv?ksgAt#!d+8NA|CPfRY^(F8nZKfGK51gncxXb)=t$x5V$Pgh_zW^Di4VJ+Uu^2fX z_SoM=~y`TVoIkS(Q0_7??HF<$>SKs zv1mTV!nN=NT%xUd%?om$)!cLbl{2E_utRTwGQO*$PL zCvBgoWb2rfZ!kZMV?IWk)y&*OVOYXqHefr^v$NOjKp`h%CLTL4=wPZWX8%|k1fA)2Ja;5Qc24-ok>%c*?dy%%6uhb zXGKXJN%9xlW9cc4{Fag5^Uq>jp%mS#ya(KNpzfr#F2~pYBuUDw2GS}s$epZVAP-EXI1*F5ug{WsrF)>9>Q@BOUy&pT1CyF@9+ z_a&&<*WyP^HkMB~YOHre2t6j(YrIyUf>;}a4YWp>7z^`PKroLKdkmxPW8&T6U~i^F z&eBBAM(aXT)Et)y&N?2s~)_Nokn*BQBYEL`nX(uJh zw($~ZZw`Q|>v9mfD}$6DH8>7R2owaXlb(I8xvc+1^Axi>hqh4WUi)x(v)v@B(juq7 zxV}e33>-$#OliXTF#^!*N5lo*hpQoZ@9g40f*GDks(18I;zT)TjHQ9q$C%+X#V)B? zKyZS8b5T*EENuO>cLrSWy%ohQm1htc{;8dH@MX|>U&4%G;}m)p3A^Z66pSS}>&uuX>)T{wo;v5u#LyC5M&^~bnG7RUC=vD5)P2;IkrXzgAyln&pL;&&`|KjL` zfM>seO7Ly0C4lwy0p?HJw~v5&pFl1TS&rF5Fdkz~`t9pnDx$8nll& zdFU4O;4ThB$57E_85cDwptxUEZO}QcIL*-?+bl_Rxg_JOW0!;o zVN8K6xTnAu=7_*f`I>o4a3~>F9E)HFFlE*WHAksBP>P$5t8f$tGFK_fxZp2|IsWv? zbF5!S?Hz>l;r0$@Y<(Pf&fy+LG26v6ej6{5F}O2fp6HD{La5UF+;Ssq&BvIZ)Dz%m z_^i8NFn?ID(v}WiJDl^C-*c0XlJbB4hvz<E^$I<7R%W5eB zcw2W@Z`Te#J6!d9F2D|k=WC^}`o%+kM~qBe^9pSt^FG!a%SbWBV7mG&?9Dnm9C7^o z?)4@;?b6yd)0BDFRMkfLir@W6ZaVK0QP&seM^f;t%Ch`9ElOR58&!sNSazl1Z_=9n z&GuJ{`kPib4Z(N&?VZd1a1UOvwzj^D;}GVLrRUGuO^EEL51+S>KL4WK`QUDQ``z2r zect})kN$x8u1oE$civ)ApS4mLFVi9>#6B9U`yQvPp)zHdG^``yc=PlLK^|E0cC)d zQcr)uQHlvBpk$g(P_F`t14&Z>uv;lC!*ovIOk!Vh`K-I%1e#J&q3rGJbm-1XPD-`YW=TyEEhapaO z0@`ypEFO!V%%k-HCW?@>k3-oMMsAo#lCdVnZ|cTE7Xns&ke~ri5X_(kh3slbb0Vj7 zP8&57ckmPr1z(XA3k3p$pAxxl5*EQ1K^+Sre>oYu@v&ahxpo$hOoThfQ8?-~s^_%F zxXDZele8m&nXA5XieuSBV-?i@(v%A_9X*`EiGjrDF z6=J}3^CmZ0Eu$lzuKP|9n#6~yPl8sA-uxgIZKn+K%jjfs)+fX9dy1vRw9SbMo@m)I zuLszVh^VEeoLVJ9Aw=eq8hA5i3I)gkULFhQR;D2ZYO;12!gc*aBsBMvF5-*?G7+?d zc?HZ*5V9uV!xgL~`}BKp>rVT(e(Iaiihl%6*xY;Aej4xbpS$}O>-AW)6vV*QW7k;q z7Dtn#!)d<<7ASPuS;xb9YQMal6A>=`LA%A591ll%I-Ge4KXIytM~3V?CoCX1A--9r z+4W11p^G~sVi0G$5CkppXlaWU330*S#zcPg;-Eda_pp8UJKt?T{q+x7e!`&%4PF{B z2A}Rfe$syM`NQ@ev915|Is!Yw{Rm$YBv9rPUi~msgT$f@ysFM(+;+Iy zE8{m+*Dl-EF?Y+c+^!(s&42qDOG@RVl$6u;%M#O%`KQ0Vd|z|ba=h!K`dq&Y9@Z&t zcXj)VfAcIp_iJZu8tfk;nS%6J771*Bn>9fQ$4f$w-MjykKn%~a*G+MB=PFJ!I=6uL zXAoSGKrhT?Y?w1AEljiz0Cb2rn-o$;rkvQ zhF3i>9On=>`Lj_Iry2-2QlbG9JVj=YwoH+>mRbK9GSN=hBj+VCldz_Hv?By|-b)eP z-!jD__+qLEd5J)O9&nY_!BA=`CTjZRp&Zv^;u%SwG-Fo1MMO(Gt1UeejRTKB(_F+D zDA_KfeV##_Cwf^LfyXr+_Vt=}0EO$ZZRj=6fAr{C`;t?8-Y01O^;=ik)~*Kuu#ODj znS@ZehK9&;1jBmy7*jlJrAQrPgd&gSk|?>icL7Znjz&CGwF#K-lu-pn9!|{&v89Qz zqFo*$U3el&Kp>EMJ597ss26ce!+R2TNL7edNGV*qF_7@Vv-7)PRO4hFp^=m&qY*VN zE67OEn*0)jIU2>>l&@w6GgU*-`NcXTvC_`YzreJjCEz>)lobD_(A*p zInlwep6u_vYEPKQAL27{mm>z3dxvd*vfmEa*W5?Q)q`n-V~*h|6 zr43c6(nv2YHkXh($nRX)ZEtO{_Y_}(a|8mNDL2sm-^LN?!7I{1zYYk4x<*J;V}Bto z2M=7{a=Z%HofnLqRxojZ=Y`cpXWpbOrY9p=m3AtBJu4a|`X+LgCG&dDL-^$bNwx9#7)f#eE0p#|+Rp@u|J`PrZSOY9}R%A`f>V#B?g(d@j4KLZdR+vOgz zL!v+IvmSF!0Eiu=+HE9SJ%10;XgyzbMEIpUoJ{ipb*v+04j4e424?G&rK8a^TE~Rk zU!WXJpdK#0iFUgN!%yf8`k~h6Bmu87=taB=e1+T&ItgL&z6&sbOgG5|W+9~cyY~*R zCdTI5}IFmeJZZd!4e*Dlr@Y zk3NKMw*Q=wV$B(6rE~V3x<2F)YJCXyIVP)odIS*Wc`uGchIdS2+|$M6o?QpnF8|stcmUc=Zv!`Z%Q7Ci4d2 z$wMJkrbL6a!j!L&!&rM;pv+q>%TSWtUQuCTSrRhw-7+m&skBVYYMjMPZHG8>ag!%z zA;MJl*t@Oy-F#{!)i4QiwM#-~c{=@!^h!lr^p;r}Oy4-fdiL5@({|9vSlKGZM=)vG z^-nZm9FQ3K2v-UTuxavZ`DHnPJ?A@w*Sku8jQu-!`YHz@>~WgVpM3e__F`|0qu{9h z(t9`C4J;dHM^D@SqmA~A=TjK!wHvz#evB7V)U3za<05A1Fg=!T2;p}|#I#yIKp17z zX9x)a4v1c!l``M+FeO>JpC@q7wMTDRX04z42E2Vox(Ka}8~Ha)fmTcu{D^&-%pN zj*%$vtPrwO!Ly?$CnqmxD}qMwu5iYFnP+AB26IO|b#Ty%PhZm}1aD|eMSf%8x`~lY zH6N=!VFGKFu~L@n(82-mq$=wdmjz>|Dfz4}KfCYqN2%!YnYdaCfEERp=B{yS*EaA| zfOlcfvd^G(tSjT&aXP^=22ScA=e$%PvW!Rs_WiPG@PcvSw_Wp=55;4dwk}`+SY5dW zTwE4W!^ffqFLK5L_2lsHMRVtH?O^Du?&|1IB_Wq>ur3}H9jKHAt753#xTWnTpO@c# zn#1oMEGfw+byL0!wtFZ1`N0|CtWACP;!)dMRvKGt8w}Qm@7-xv?yM!i{Q^v835L3k z$0+dtnN&v*93~e;^B71O*<=-hJtP{*vps;$`i2wN$)<*SWSmU5jId|p#ycDB*4sY? z5I5L!O7lSI^Vbl6juh+fVy1kAoBhk9_Wb-w`)rJ-A=2VCoH4eRIhzo~RV&u9LdN2n zOfi5?S`(RQPv{hX>~@Gc{bbO6=fstG3xr6)a%9s5Xfi^id7T`n$FkK(mvGQpU3+ByoxmY19Ax^Zzb#v_|C@f_QYjkke0C-NXvoZ(?9 zK} zYN?G5!4s`Mi)xJYc?@oxf#(yfL(Y8HX{S4jZTIb?b`$5MKl$jOU0J=-e(KK6c9(5l z#4%&Z2Eq8FdjvoDl0zL1+Ry*O&$TVQ(AN-nhf6mZ2Di>YV5#t&`w%C(1;%6%;Y4j) z|AJa4tvMSS93en!j$>9H%Px^*?{`h+yt~js$z&KxwRqRoWiX43nHr<{jjW{iuea9{ zLggaDu*(MuBClS*Y6p|64d-6MZvtmv7>iM?Za`6ID=a%&-0;`{2YPKCSZN(g8<|Vu z%G&B}4vV@K9PqhIOhcS9U%dJ#_0}Kf62kS$4feHuFn^wrqJ4Yq$+Pb=t_VrY=Wrdy z)c%MYj<5LdmME7W)cV~<=6lCeTolJ?PUuxd5K|doPSS31Gq^@|-CT4&c-ewvofn|? zJV4mngl27ST!#iu+H(TJoUmWl`59pb0elnY`<=3OeW z9-c>NrN%JTjL{r#8QUw{?=ud!l00prVXyW+!EfXqc%rsyE9dm3ntqWsbK72!rvCXV zy}C$~C-Xk$g9p{7qD9^NFw_nx$z6W}zc+rr`L!#jpvfEi%0H;bN_$O=*XKkjI)HH@ z#36Amwd+Pn+ElJo8pp{2%}Nu85`^GTbq7C1oi{rvvpI5%Bq z&{sH(#SUmLvO`6%O2g=o#gP-h{`XJJ33F(cB;r23a*%M}r~~J3JyXLFNgG6_%}!FK zH0%4F-}%SwcYpVH+h70d|9<;xf9=g=sG+G#f z#R7y3$%O7O4rq`E%LBGG5H=2C;I^e@nLiVb!GTuNDuf%u1fFsv-oSHIVGz-hB(nTl z#@t0R;HO|AJLX^D1xgfutqJR-eJ)I#ydFOn7tJO+?#g}N8NZ@|D#bpGs{q)n%+CTh< z|2;&6bx6wd5=1f~!EF5Io40OZ4f?tE^>2KgysI$Lhgc`DNDy_aM;`c7#xRFC3uEfb z;p5^xoum113N>!$Eo}rRG+QoW!ZaF{Oh<+~A|cqCGz$`R`sMbTmnT@P5OO!k_thJl z?Q6vA-XZj)`->kUEdSw;9-*b2wL8R#z6B2AqTcopaIRuu`ak~ZKW+c`fB)aw-~8Ku zr~S$=|4Q3`$=M?0UjP6=07*naR2n2>mqrDRm|Ng)0&>s>wLEciNUZ1+tX1NRTYJ<* z9q@Am)I{{6KL!Npv@d2t;9b@PHBrh+zi=TSMByR=V}e7(Y9O&Uf)fWg5SZq4a@fA} zgU{MMj+R_UC|!Jn#fa@{uOEEb9^><|d*?=5xq7v&p)f>o;DLYK3|OOy0g$11g1$5!v^5{9vM#R`THg@DmV>6uL3 zouF2Zasb_QyI(N}k+tT64YUs5j|Q7)?%x3ZgZAxj|KIJO|MUN|efwYjv-W@ev+t0; zu3!2qAGCktSN_fR%Z%%<{_3x`Ti1UY@9-bu*mfUys6p45h<;wqk-$L27RmVt4X_Ce zuGY;JqH;Zd{=EIxZ~a&8!w)`a|KYFyO>n~qcrsmJ0kTIKVv@-^06W5(g{cuU=G^{_ zg){xJPu3^!x%kK&sD=av!og?KprobYwv`|L&z0Y1pZ~6>*3X;Jzk1<&O3QO;fIz^? z^O{4wpSLT!TWyblU&q8V#9W4vl2C#Wa3-75-ib~@Ao?<~KIv{wfN>w(7+3mN1erd< zWMsgCbr$qx80L|H$*>aWoe65(R1rO=XMo^CwtWnU_A|lM`RdtW8(v*+&zLxW{QWQ6 zH@^9`w#)gU_X*4Q>6iE038ugI?%obTlu4>ZSWjJcv;bu%od7~ZbBu`PI|1lA=oC|Q znReAyVJytuvx&|_c;bTK1Iq2Zhfj$XqqYPkYaf67ar+R43e0zy7_Y2UZ@g1^F#$(EnMzPn^*7Hh#G zzGVn+t+6?R%n8fy+7NGhnFu^Hi(}}7mw48p@lGVocvG0pZ@2&SKjG9N$}3Y8ngZ!- z4V3YqomqE+5IfU;05koq|N8H@zx=cBw?F*jAKRe+&+FHJ{cq7^Gmv}AlpgZZ0j7)q4Qsfoa=4NAv99-U%S-$wK-}-ObFa6>- zA;PSIW9rwlo9-ZxvW3`W^Xu>Zz5lZP28`(%Tg{F!?@!SPWsDU|m(lA80``* z;9(pzl``iP4h&+!N?`IAL8x(1DCjglO?Wz^J$U|_rH7;T4g4FvNjOoqOsC(UzuIqq z%3ku%o*%ZKdFN{T)}3o@2iW{Se7y&lT~!sZy{Av6WhTA%UI-7uZU>{H&el}*7Cl@MTKUWV6} z>lTe`5f~vndOb=2Sf;A=>uJo`$-y_9Th=oMmQgxro+%A^_#XUM82CRqQ%3UnmjIOBO2rT2j}=GPDI;uCUQK0vt6=Z z-k$&XJANIv_{)=n!K={$8T!YFRJY z#5&kUDm85(oxLA%(@x?yO=x&(T|;3ewNx&d|JK`NzphSW$T+ShLs221yC(+7;35fv zjupl!&sU>UbCsY9+PW?>5m$k!%UBD^>|w_i^?5@p5*cOoWIU2iUqyTJL^Dl2bREgu z8%cg?!EG~uQjOKPvk1AQomenMEK8`jN)tQB9C5^vY0A`TY53@oAU6V3W((rGe5H&n z)+$*4SKLIuiKlkdHKZmsGKQv8RS$Jhu}V9!`dc8NE|#FQ^J)e@73CDY$E(nn_T2fy z0y*8d)UB7S3mTFHSrll-%6t)LrBFvP4>sE|cE<^3%#Dmwg3Ab`kjR#+OxOA63f?nA zli`X$`Q?a&hh= z&l@*y4|8Loco+Md>D^7Ni(RluieZbtz`K;bbkWz+o_p>YijL1NjLW9|oS91xuB-mQ zi}sm!NlHfF-K7PN0N{*7F2l$75#vy4vM*XJjxoc4ztRa;30t3~@}<(K#o;m@@hZEC zgd`kMprN5!<=0NaIYsTky<0;yX;`V;J@4kD}WtUx>mMmJ7zVqGh;VvAV zHa2bJx;&4!R@FpK9DW{N0mZ@yp$eb}og{LqqV$%&3FRi)YHR2Kf+Q3&cj@=n3=8y5n3;M9T zl{}}Mdc*A$9#+})JN6m;gmzGfg+WCNmKv5#R`yUNxk6gj+Ns6sHJN(iZ9`a#x_v8x zViPgbcET?#3Fc?Q98Yd1bGy~OkE2#GN(Pp-zw03@16Exqce$X2z03XaA%KA(XHO#S-x!(&OP3VPkac98%1&wtV{e(|&P>tFo=%W`A-;SYa| zz@11<0h2IU(&u)MJlIz(3*?KHrA%X zg!@uP(5fl9Lkhd>N-Ai-GNX)$GHj4WW(RRK;W}Eg8N%GOm7@5?X%G)^hlgTZ&g~F; z2f`I!7ukkP7(zm=Lice1w=f9ag^^4w#}y=4T_OS=1mv)fTBn60EDzf1Fi{6aQ_b2} zuWWrZmj8Y@g;1SlT})t#*f;;PPczyb5p0l?3- zvyJQ%+lBk99F4aEVOdEqzI^z2*D%;Y1PGcrOs;(g+q6i3Y2b= z3%YRGDda?lSQzgb_(kOIAfc+gjopJtqKaqulPJW`CAf@q-$W9Jb{hz< zMc2w2*$(beC>MyLoBP{IL@Ft5U`L&P2zi28^jX2_p>?==t?yc;Fjd;NuUe!oHE{

COaE9vOaqQ&yyyI-&gjhWb$B`77UK-TA62;RPYRMDorHnPD@v$G?%^)rovT!_7ZNUM5IDaJR3D~XqvgpZfVvoyAY$RNw@y`&NO}c zjP!*IE=;uzBkA)-+LjqP&n%B3zF$rPSB4*N#Ln7Um{W=pPznp2Ail}VEMf6m#i6T{ zIW(i5zD4FRpZfF}L7b1`D>Zn2DhH=^!$+pR9b1`65=O>NPxav3LAaycg{q!@mXc7@ zvi4o#`HKm9p=6Mq-ZO%EVO$<2Ckn5o*w})Sh9zv;O}w$Tu3xOh77oJCJ{c3=NwSlC zN4`>q0H~@O7VvYfnpgmhKytr_EkCKQ8NxU5#Iw*?Bg@4)gdNb}l)^N74|Lyy|E#aC z4A_wD!nk$Uw}6i|)zrAICP9)tnR|eL&8V4Cq0LRlP24NNIxWXD<{>Oww=RpmBJzN` zFD5r|Tsq3i7h&7zmi@4MGU!lnoT-99<#U0DID7IS`Kfs+qF>HAHx+u1?+tO%oPqBEH$uvJu4-xFfhL*hQ!rKI=2( zXTeDMEHA389LT))OOO2Rkr1YP?=wAJb=9}iK?lu)50cT%dgN_vu%RVbQ#~LZdE^mk z*s!6%diM2Ov<@qK!L09lcg`V`XW5;^N zwUPehLy)Ohi4R^=U1gsdf~TrHH#V*UM@Sw6j`DL|!TtJ<>4jBpYsdDrv`>{dh8MKq zIDwR;{Qn-p;ls1G>%VMW=YE6{2EPgGzxUFBT|Cq0Vx;xru=jh!JD*FZPM-j>la_Ab z04$^~GOctiL?L*zJ}oq3Tq3(jU;tljMc=oAM*vq_9G}M262sa~&|rbwnXD?kJi(uStmsJ{-r$%sn zZJ+)S{|1zUt-v2g92)J?r7M9!N~75d&STlK+CovQ0b)ArgB#LOa3B@R+MlzVE@hV|(a%N`I>(R=FC6keMy22yu2S8kB|g+_P!Z5(ue>Ozzq=2J_9BW*nh~G63dK zE53MyZQbxbZNrR7$S{_;0&j`ysz;0%9>qLfe(@Eq zt4fn6Po>18N?>E)OvL8#;)oM9wOU+RDa#2)#2&%#^{ZcJ9;7i`jR@xwlGVmx!G7wT zPo-y`ek9#>*Kg8Ck2(rPVIoC8+Jc5k%SdoSA!(t+>VBV=oFGp_<#F54oDm!yzVlH^t_TN?~y$dDnaZr9z?PByvk(|;ms zmOIn(MT^t+ZOc;?*2*Aa{KH2MOO;diqsrOnwBYZ5r3}J~R6mX6w?h0H7o;@El*RUe zwf(@~x47{myDEKlMTy0J(S1v@kkbaB8q8vVaSSW~FHZNyB8=(VY_~4(- z*(}52xM_p!oBzYA^P&!bI(ij=FNk*uVjuEaD%)$-?Mqk`v}hQY1XV+k%RE^ zEC0fia(6oBm}An!iR0+Ap+9nQShqZY81#_A6ET!Fr`OpG#U)F63&qVLL#C$fJL*|h z`EGBYG93J(5@M)j^(y!j7&L_CgY^i{4IAI2&)LGQuNw<59e`(ab&N~=sH+=6*z%n` zE+6Ok{_6;t^TGE*d-xx>!!(a@ZSYItv{}~GN-+RW?w8ykbtXH*i zikd80cL=NsTs5V{9RM}`HAB}bH}rw;8L>fj)j7@-T)Zv_H%_P4q-(^5-)f!`qt}Q) z_FLDoFWpU90mu^}@O(CdjF%aBCAfUM=^GzN{wQS=fxKhRop;}r#*Ukj-h1QiRKxET zv|UfOUmLEGJMXzG9e()!Y2QPROh+DdLYlMhA!(mUv(it0^ux3TSXE5lBQ;_9mkpW# zBOjh#dj5rU(S;YL-Dd4gV!$D3&fEjj=yB81jW>Ni1VIH^hh5!@^d1m)dpi5AuDH7D#{sUK5K~<{D$d<;#~ta^Q%_8@cHN64oVf^*>9~-76oPs3 zM3@+OB-{mc!8+CDDPEoSgPQ;P5ne*J;GNe$9H!uGBCX1&9P8-Fhol ztVq*m&Pjj&`=b!`%Ji)(uVgOfrQ3e}>wtY3IIXwA_+K;eaRsz^X}{yeRU*G$4K3Xo zEU8-7MfRy?*$Lmf*^aN1r315O?V67I*vaXY|13!h-dV^!C~#%;p=>}1(^p@85$oVW zl9zTR+jify$Lzh+$Was0_r7->Om-#Da!CgUQe zk;k4SaS%G4Iw!rlY9ldVvh~g8=O_!;GV#cI)>Dt#7e`A&dho#q(n%+MJngmjLFr>h zpOlXH=y6!%bJFGC{3iW+4`DEttj|#hgFYxko6@PL9Gz~w@djYLB3%iLr-Fm~(Ds}; z`=`SW-zVL9=N;e?b+E~xb(}K7T9|Js$zPBB?cwyvGe41b-EHqQm-c4wxqmwRuzg}a zx3f`4^`P<0Q62bF4AbA3#!cBZ-T&u5rEMG5rR%S|F73YS-s#eBd>KJTp&A%swm7qQ zO+~p+uJYs$Da@3%7)KIs0@!}=G@&UqA z`=)~rJ{Td@0{>XTz2wrk%nL=k92%;m&T-?;cq36a>@w{93#sFp4PlV0Kt zxdB~EH$VRI4^d|JNH4$qB4h0ftTpKLU}-52OiPz7PLpPmw0Q367z9|a$RDlS*ucCO z%092~5;qife$R&t!U-*sw_$r*YtKOwdQLbrlRiOx1YxIy*={sHr ztSy$UIL(p7g0`=9=5m_GMkTQVN$rm!3p+H~**c8hY7l~E=$VXUNh1pwxVwah9kBDh zYq9!L2pY3gsQUa0Cd}gZBM0}3r2y|OSwbDe0a$q@X)Q!FpuUV9$8g(lO%;2CcGE|N zs5_DbfIyVUbmLusYEgsvFg+^j#K;*seXu?IuCNs7T8?fv<&j-PSd|EAiN*vO)BEkO zeN;Fb6N83gEZ!r4ZO6@Bb+Nz*rm7RxJMeAN)%?{~Om(hqox5f8hnGX5=mqU3Gf;>8C;V zkEO}uYGK^BAf(5|xL$hc#q{+{t_sBa@sDr8LK(&!00#ud;K8HA({R;Q*QBSO{4?2? zw}6~-U4koRYsq5#MV^2+A`r_x(^*8>AIj#-s=lSQKPhb1` z6#>VquDTkxAS)LcpX-lrzx~#9-siuPK5^Qy>4C3aMgfokB;0IE_uv2LblN9Aov#1h zmFY`g`Z9Akgg#sAxH$dxx4*)T`89}gUi#f{e~Dr?9IbRk`rO&)rI$}W8pb*_<}l-t zE=1-UUagfHj7ucP#VsQf!@bVKaDeND`wyDL6frkJTm>SEOR(gdsp7SY5QkdrwsiUB zm!@C;=H7JeHJ7G&2ObE6u10I$oE~`Kf%KWrU6fX?T9q#T%2(N^b{u`rjM68qS+h1> zamAI)%|q!cUp_C*-D^(tL;Nry$m9`m4g>*jp)c{K_lUSE-`&Ek26w7%W7x6iim-GU zRx30~ta8`g;|{Zfgg5a4FSu&pk_q^taOU&;L99 zNGtW&gz4G<+=9_P(8B{>3K$z&!sH&zy8gP2!jn|`{qds;FaYS}4 zqHi14Zc5i&eRaC~-UrgxFTip+cpmqar=>99uUznzbnl~2rJvvOlXT2cN2VbxsoUY2 z@m_)2gN22^DwW_lDZ|>l@7{aT$)}zTJ{+4Kx%?`UUf~IQ#5#fI9_H@NT&~ z*1dnYC!K!gXVb@yIw<||zB{mTh6axkx6k?X7t-SQ-%H=P?1~VoFc8|xO29UfGjF)@ z`{_G3-MJWNZ-Aif3RdTg5i+H+}_nbX6-XuwnT zpZTwnD{x>u^5;MM0dSiVs6K!Gf6|w~a#?!p?+>Qm+Rw{ zS!Qx6?&THBm&bDsJm8?1m$tU`?84(F3S>l^)s8UoaC~TK7)~3AyG}u|nVW9^&7aaT zD(V$@y7-ISPJe&&A1KGb@$2WO0}q@BUiXka{B-)|tv9C&E<78%JKGyHRGJoWJZi%*{#rWoze4a7J2L-6(MRp-CJcNOL_1&1Ymb*$-B%z7};iji8 z<%_{^iB$=%>r@1Za$vlh=%N720Nf$jk!9#zqB*0g~3(OUJuskFtqkz6ox3l4PA;qU!le23_?T} zcnppS*NXPYMY>fAs8frEt@RQyHzrBOre&)|D%15{fndJ`lr9OK#R_c4?>WbykS-#@ zNCNurvDjX?v=FFKatkV`bH@=#tP9B(U`^`Nd@}9s!I2WO@ z&kbR8HiGEDd4Ecu{p`8S9kFb%ts1TL`R5*Ecb`MkV~@Xz`)Cx5K?__r+Mx8*6Hf#K zKK}S)V?MXD<=bEW@)tD9sp+DNzLZ9do`eu%uT@;Ld+xahxX_U;Wg&ER^Fl=<9!x82gkXzELsEJz0*a#+lRlir1*CQsAdFR?fVq?Q;QFA34ZtZL`l z?-8f>caDsIUBIM@zU)nSV+4JvNDn=9UkKBiZoCFJ)@QN4iDxsHNP;wD<_xYWO;=s> z{WN>_9O6fZ$Fsz{U3VFS#r0<_^1ad%|9q7>7?JzJ72OdF6gN>G_*d}K&~bYb#2d^P zh0RJ#mBdQT;^eWJ_z@;_AYMa<4dI}eE- z-l_Zk_{R{YKfUp~^vSc%z>9h3>%$}I^o20(fJpo1NV9!1#cp2&>L|} z=6UGG&DujEV-+5O_ZKb*VfxXd4oug4`{JqND5}AD_nI>Y7vRuz{0XP0U;XOW zC@7alf|*uI4b{?Kef60%Z{A1KHP>8C@{1OY)sFVVwLS@Ww8uDS&z?=3d1^p*{`}WM zn4bQL>FELIdUAR?Y^X9a@_s&lELY%v!1crCDD=Tasw0}h@S21tOj_Zj)h;MMWR-(>E3 z&q~-I`t|hYYwOZI_uiIHIN>;OSH-_8?Y%dBN)HL^$6Go?L?Gi6pCzEe?h`ch!ap3rl5C zjx6)3H^W0$zgfo3ga|CTX?Csg(41+VEkNPX&hkkuw4u0TJ+@K9?J#$Dv)R=1_AEF>*+*cVE?r`(Lyc9x0a)UVk!H>Py z^uEb1e2$7nmS6dYf-tqe;++GHb=3%l;6*X#UHhVz&sUf!puAQMq>!nL4M-A|>{OjZlhOkF40>vpiQ zwi$p5d*dgzZ9jsE^$K~v3p~#W7%d0|sS1T|9!m$hK-@k9;@%f;9KAf6^@rR`JT!JA zk;vrsj0gJ!$Ao~zP2Ov<>-xc`7r*=qA>?t=u}7!T#3j2|yq$Kge4aIJOVVfS z1FaG!x_%>9b){F((q4S=opi~i*WlLKlkwsTCw8_Im!0rF{IEkwBAA2L_W-Uk5DRyw z%v)l3fAO|-{ujQ0FdfUzK8w=wr7wZh+i`*InGQYT@U(2%61)(&_+X+th?ly2%5r}N z%Sr-lYu$kb)d%3p;>tFvGpU*kx9E9ma;+Me87jDBrgU6&(ShMT# z{0lDzV>6Kd_~TE+ISFOzv|Uom&UIPYnS zK50k=P<(jy+2?q@M%$O-7MvYI&g~USG`KD8dwcc<6!1bMXH0C~co21n|?J z{T2qg3gs6!S{Zas=By0$H@DxLuDbGTFuBp-Zz=V*SEQSNbZz?hN9UyzPdy=3qu92i zj5)V%^gf4z4p&|NO^pOx(yyhRSmSg)#%z0J=dlA4JY`1{7W}{B)aj?5jxe1_!rqFs zYW?dd&@Z8MG?PtwKvDpH|NFP3B}{O`WI35NO!gy}$B^KYR{yb8XqOfzTh zLP+7bxc23jUk=(82mbMo$Ku@CXP=EQor*h9I@to>*n}_|m_Ck@>hbKqpJgc$f)ait zf0On%zq=uQ?Hk`nQ>RZOv1~OSqs8Pvyn{DmJ@clOiqHbr3?~_SP|$?+!(0dH`y{44 zn76FJgEopU7$pEU`@U-31nr2z}!p^)^5lA$~|@RxBeUD}hr`qlHGyQ84n z77T*t@bbKdcdUOX*52c0^a4a{{(p}Sj3o1Njymc%=4k=pfOnxILpAk;B>RE~Ytv2N z|7AM!lgFj8W5*&72$A3lcRVq+kUNe~8p=n(t(%2qo`}M9#GE>>|MOyc7!q^ptMH)S zcVQvUNXL=n74y!I^Z^T14NHj(a`7@AXojroNZYYYw^4vy%|ffjT~sVCU?J|ZQe7R? z+l4{7ep5tnsfLrL5Q~M{bVd|WkXFKgw0zarWe_4vGnS`FWYUxMG%Lwg+X<0Xv$UoX z2H%Bn>#jrK)i;nNu?>y&?TEKlvR2ltvcZG z1F-IU39En>?znaq3ys@@r~DC47fPvehvN7#7UFWAOG|>nA=Uk$R@(^fNXN0WW_*?$vf8A42K8qG3K zB$c2Cr{*225p;ctBVi?RIA3%t5Z(3yfjOZ7ir*o6U2&l-foBU~h~N44W$E%Or_e_f z8|RM;B?$fmvDIQRM~sVKbhX|8mwynu`g~~hTQ{$!ez1x_y zc*#=mrWyAvS+6LLufMq*?f*g&zEFrdh#!*J6>)hOgt1ZERKX0o;#`cy!Eo4ne(pVB z&fuhk7(YYQTAOsAIkwcBj7PqY{{w;A3iEjJ#pmOk)|a6Qv*G*Tf_JcnY;@Bff^~o8 zW#7j2zC8`a^5{oW+=27vAvpg4LwD+S< zAX~PUOwaAW4b8uTU0BTA#)=xv5;l)jxL_4BRnTN5{q$%<;k732b`}RmW1{4XFTIfV-Dg%P5XJ%{Hvp?f#mg#FgNF=CCmeqS<69NN zwol)@GGW82rD=~N4>qMa_LO~AxM&F#%516pZPT(q+ zJI%hpQ?Qn~&hJi8;G#eEB7hmICW?97cH6JP=kByO9u$)Rd&*orQJ0P)Zms*=xt2e@ z@y45R{gkO0&n-w(49zLXjoIs#-wSyD?v6jjy5P|xc|Lv6B`S{Cv^AVHK%^;)nRw(_ zF!REg-PgJp%TJ3nj1I1|Xpj5-nsDiT8K)C5O+75Gy!bF4YW%+nzO-`9e5$rlrqJq5 z2)cR_FFj1uRB8Hef$IIXWfi6xvgYT`)eDx=OE0~QwLS;_G!B|BPj9n?(&LarJ`x^> z4V&Jj%h@HX5m&;}LgAhZWMo=&(EckN8UCCR*<(zFUzj@2{qGUiIQIX2hEuKN%l|^w zIpKL%4XiXIt^!UMsP5esws&%AiIz|Uu@w`4wB8h~A$$RB-eH7$s5M)`(wBk|67f6~ zS_QY40Q*4TGWZq*QYG%S@*)yQ(2iBNlX<#{)af_hd>`Z~ zMu^rEtLqyvL9<0IJto44RA#@n;^t8+i&~U0Rt35U5HVc()y+8tsYllQ1!VqgjMadA zq_i_pGR6P*fP`_Cn@Q}t(_)^DIEP1|T;R4YBIplxptR?Ed+3v_bUO(Ui{JcDnswkF zB%(A#rKC2JQYug;N?_pIwk@GO5(lg_MftO4wER_A5c}=7Yx>cTev}cAj;cKP&%O)q z6)Tsq^dN;sI%VoE(Os>&2DaZaF6`K~#Jx{gU_1xn2w%sii`}YGpFQ{7V8XZj^tSYi zU;Yj^a|4#$8EF@UoNl(slPA+Rm)?-P%9!eeML zCUMm(OcwH(gA4JHLk~qDvTizH&hHXI%x!STu;>UR;YOuG7l%u2RQ7gmXX`!gDP`_e z3@vWq+PLu(XOfLgVG$S?4+Dm|EnSr+w6!(k5rIa6fie!2DEnHdX)8=3EYVB6VuEv* zM}{bZ4pa=nwT5t2SQV2{<+3ZaagRDO@Zo5#t)~;I@8SEA0P`C}FM-$3Z6ZE&4tj|RY-%oSK zuzUig8EKr#5K3h*<#OGm0L%W?k4?Kd zF!x$wtt?@wPwn81MI?B1(3w_aS0}W?Il~_wwh{D%29z0bX0HDA5Iddie3tL|MBtgR z;$~_P8+-r7tv|#A^`&&qr(TUaPWuE&NyI3p5F1tb89sa%{mM)F7B5*sV&Zt#g8nMq z@w?x}^KGg3(X>XEEO78x&)je$`01MJyevuOCladQUs}B8Jb37lQyG+JwzQx)ppeU7 zoFB)a;O!!r(w(tX9ygMO?O`^uiJJl6JZy{smS8Hx6|xvL0T22Sxh>oiOWo*jK_T%u zeinb*$dYfP($#bnuzmNNN>bK8(rKrjgqLk3{9{MtlX(5CS-YUv&B6u?7{`(r?>8mhbd2R!gg0uG?f0w5?XkLKHN0n-gmf&|b z$m;MFrp8s=+R5aKew4whsjUi&(#$}$y?RpT)m{`@x`>-8QlPMsaofRy)tg;jZg8D5 zWfi7sCt48Q(w?s4Zm6Q2?U|?=(@mXQ1xa>WAqMFoQbLHhWwx(fo5s%EJ(aTN ztdyb6nArf(0`FyeTz=VbUoVv@mBu>~yb{!T~Ogx%S#`lNEYK#5rDk@x}DL>wn?^ z;`rt_FHRTWLLM}DM0hQPx63^yO_>@nRl^qHGNeFTKHqS|w{48iG6WLaU}mmsb`I>j z`;VGEZah2?l)kWJHgGghl|L5Kj_2|miQGBO$ZWUbs_38iXI<3Bt;lW@Vp=@eFT@gdrYbwzx?xE-4CO$aTi$#hD0!I3fRaWTv1cmMe)_cs)~9??rSt zL^$-71;1ku_xRnxM8ExuDu{q+h29j}?X1uv)TsaXf77m@&9%D`9$i2ZU}< zSjRF$Y1;n!Y@g;F>@sr}uD`=jJlX4*)7`8!o-koT`r<|Br9a&LGs@(Bj!I;MBE$0! z_db+v`PmQCfWah@Jik@1EPTq^zs4T?L= zpV{>ChERHfHeFeu!cLM9crGrv@r0kpjA5A$JV3!G-uY56I1iVSNLEaIw-QBD1wt!@ z=CVZt@#V6Ue5Y_i1w0V~hqtk-6MUtMF)B>7en6$H{Pk@}%T@|wabM3Cb3r&rMT!$K zCi~>(7KXw+#3viw*|aV-^c_Qj)#+H)`vlFs{`%|bp@)fI-}PWz`;kNUB020kVX+xM zj07ooj$_o4m8Uws5GagKSWcflo$L1rSGoAxjK{7U_pA8nb!NY-xK%R`BO~u2r6tRj zf`7mTFHwtYuX$Dna8e03-m0=uiIrN*I&5Xbn{U1m{ToHm8eyB~Qb;;?j-G!!LuJyO z7iZUh@q1juH~Lao>lvTpd>3)+wRj(=p!gl1F1_U1^xk{#r?KP4v3KuU42xf-^Uwbz z{T>DUHZfke^{i$dbK8#Bb_@K$-MR#J0S^FG{%%+J64}Ax0G|tRjIq$yn7w>M@9!wB z&{GIuEciT}!Z{5blagQ*YUO|#!PperuBp*f0>oNMk6BsAEq1`6-q@!VQL>d18F_SG z0+B&l;!Evbw*#mi=X6&ogq?V`2RotlKoXgf1B-!mdYpELjS5^!3ObkQbP*5QMrQ1y zMJv;aHCxh7l9I+w9i2wD)~3Pz*~NxBoQPuFAjCXzE0~p5#=*_?Us^H|JN0F_p|zANEn&vw>rS<77;GPfR56q*oj*jAHs_%!(Xs@vLj;Dm&*C;WzaN#@Y-h1y$-~RTEFxa8# z|D1Or?m?D1>7Hi|@&y-sKHM}a4a_7j58*45_r>^WXB&x6`{9yXi8xj(* zaGNl>p>bgknT^+#Sztq0(uZt?Qrmjx#`Mm@`7ozL!lI5FdjEv*2Ctn5=gIzBK*49p zjHD?O=t7~xB4dF31WtsGWoLXjkBm>HjIYIp5U||(^&3IlX-#_K@ki3c@wEuQ>2bf$ z?2BjP#Xr7FGSwY0PPBBb2n*f^oHI`6IS>ZsE~vonV14JPbkM;cN#DQeXN+kf&)hA& z@@lq-=3?2KaoB1yztK?Fqh-K_56#(EGaQ+$;|buQ}F;Q5|N}=R-&u2DD6NQ^{DLieh5_C)157D zbip8u6ZWMnrC&rLRL9TLHfC-k_>O*I&IoC!XQ)0PCh1m;rt7+ zGFPz-v=+P@u8CL2gvVMk zZC@j?^6|$W0RM)jsb&}pTiO+V`N#q-7waI0r34|@`%AoWt^d65|2oeZm6FD$^~~R7 zl#G4jauYcZAvXN(yYI#K{i)()A)!@k7c*D#v&?Jdb=?%sF(*OKPZvuqYUnZmP5!N0w@1L( zfV94GEz@$4N2aIAUxSd7*fg>2kcjNV^vCO3w4=H~IUPb(VJhNjL27A9e8B*@UX7xN zHGWoP>c;O~rbb8r-P)2`GTV5q+TM~DE?tXT2iGE5hBGFNNPEnjn8pq7#}b>Zsbd?( zB|sP=9Kl0e16WPob_ruLsY!urdKc8HI?MoTIOr6SgSm5i8@iU&`ib}sHM##)3h_pqor15FJB2Bs-k zr+fxg?qoEIIB97bN2ZeHca-K)!2qWlgS(Skx-C;=4$K zdNIbmV>|sLZZ(u7v5!LJU;gq%R47U5{r4B)Qr$`BT|*i_VI~_kzsyGR8)=1VP*e$O zX04jbQam=YuJ4|E?tu6nhq1S(frAE-;y*n-{K(U3-THM5-xwC25tM*()+$z1)um<2 zmc_ji3OxrOv`UCah9Y2$AMXM4TS6#70T#+duRwK74l0BqmX=k8%5YKXQtwlPvIcHg zK-}jJ8$N<%1y!+>#LXe9s08Bq9v>1I1cY!A-dS0r zkI;zy=>}KCC9`;4YwAk`&9Ik^2zUO5R;?0e)f)xbV6v-sW9||OU=OAz|23cFo3kjn zIT{P88TzOsJ~+{C;6_L5;7MDztwT{+1YRryjwq*CY5(}gL*UU}EVL)~3B0V;04;7Y(>0_csK?|>Y=l=A)6lRJSjFpPiE^azZ7Y ztt_#6?z!g@+mS}03~z)p0E;^M`7#9_HU?j4BKve_<5qrF5h_b7S1h-SX>?dv?lM$P zLeb3h!gEh&S0{XE$L1Aj`^I-%-RbWSJq%BPHc_BectWpaVx3!g zni=4%%HkFTDEOc&G;k1n6aP$rvFeXT#N^2eloVr-ztyl5rW{X4Ika7l;IF0XQyIlp z7QeqZJ^94bxNGO8f&F!}qJW`PJo>kPute)Y#^&5nZHkQCe*LP+lHUVM?DOeDs$@O- z=s#k<6^!ONc)a)id-Q{Slu1ZiyY?NzJ@2CG!|?Jp~aqf`b}3Rs+lY zMLsR=wUBJMvuR7jn~y#2Q>;E%M)jlnBlbOb=yWXA{((ErqlQp76_*+sCNSB<%o6+eYQCNT)_7H0Ve#1W{?pWs2jFOb0p6^>q57ry!)?3F*#PiL2(fOYLC$yRrx!aG#S$8_p&w9=tv0q_h%C_p{z6e~rFGS8{*E zwR~^B-@l#+S`A(aO~X?%Z_xvhc~J2FGSHFGT_1*H?`p+FQ-y-q=@OSN!qATWrhJI2_D1g{Iwg4SZR zZp5bMoy52{Kuqg`K}?%DHSN9otTX`)b_B7X2Fj@wty~VltVyi^%JNpVluj(KMwq}a zTXFYc@If!uSj|E|tYZ|13J|>xH)SnMwi;$z$%FtRm>{mZG8W;prCB=ywS(n1C^)!q zVd^Rm{PQRc=xfy?xG4u$Iudv1q&cZ<;y$Tl=x*$oGZ~(QwTI=6Y904P&=MFJe>~v8 zd2#N^#~)*j8yRf_rl%48cMpwh)yfs=_SaaNKYu>1k&|PsU%1k^R)PsQBUshIjBz+mx$7SyHmBk`iQ9ySPn`Z~)`;F7 zicekLfVjQ~?cCp;pDiQ=8Z%J=Gr_>MYcIX>erVFhLTjlyDEwPNQ) z_NGFxhWP0I`|s~?$o734g`gcqI)ky!NJB=X4CDPY*!o?rZ)1*O)1uh5hbIZqcLc&uMPa5x^lwq9>7*2MbB zK9uC^Q!^sf^&1yeo3^kK!X0jD#zM-i8aG4S8xCTKmbWZK~$&@rgP7_D2*F2 zk}Kgk{8bPvTDT(CFIJ+UjvBcua2SmRIGDAQi(-kF&)q$i4UsQ^rMlmN2N2&)>B)aS z5jfm1WF&13gjR<7#Pm-TyHBu2b^aS~uoo@5M3} z4dZ$94kB*ZFYpU=A=8{fD#xA{$yIvj%ysC*^CKLxv7?(+F9?k=hTZTQcqoKj3*E0D zNoNg1$R`>QoG^{vkajl?DrT9@Fx)@{yb(F<4tzh51=9v_Av4L|-yJ1yHn|4WEGD+9r|Ks^`EJ^M_t9kWpG+IY$d5HrU4~!u z&Og!yiU^(=Wr)8Q4nO5@UX#xXdY%ydGBo3Q*9MgKN+2J@2&&H9I3Cc3^f!?M}_zcumf(?F9sup&hHil~|8SRI?G+$bkCPfQ8y; zE!ryeK)YDl(L)jFQi$I~sV@3pQEwhbOqTe7?SVkBN2D>53M`@`vUQ7j^;jpD@lG6BmR?3%+R550-AMrpp2q+~i!DN< z)ireJyaUp(31e~h$nXKQRuR`T(`nwJA4#VjeQ5g1mETPRai8vg&_P&c)i8GM?#99W}@D3<4+@N_QVrUB#DC^X@K!F|Nck%?svbPNM1}=U40E! zR86?XL^9p(-NdI0f{0$keCR^+kiZ5F8WVFt=hE1*W7E}FeIs3W?RUwv+&vvia*qi@ zT5T)eeLem5_TR>R`ya4>-0R$)c;X3Iptq+BSdS>N?#EhFpWE8D2CMWjmcU#D)c^;6pI_x==bDro?Z#y{G^6RJ!NBzr`JY`|G`|H=PknH&WWcwaqM9*^|Qg zUnbUl6H3_NSUwU7UaXgV{f&R8?|$bR>S3pJ<>lW(fvG}CSq?4;F?MsJ6yYI<9FnfS z<_6r3gDG7(9{idOXEA=8>vFf?Eg3jyWIFeA|0n(N&yRw4H>NLs@rz-J$sm_5UzmQ6 zz`qSezL>kuUg^sho)`3bAVoLCfdva*VjHyIqA)x} zeEw?Ojx+FzY-Mbf=`fys)RFt9FMav4R7Vn}+0J^O-2U#Fr|(anI^*1Q(nt1ApZd&a zNE`&{STD}acwX`gJqGeC^l8e8@{(te@fr#lzl+;?FpRr4Gv=*Z*G9bZ#v89o7ku%O zs7$tO*|M}731B9)nZUVd(IN_|{5jou=bxFYLs<@TB>b)sWoQ-jHwi26RNT40Pq*Is zOLnX|E4%~pC_N7rbh+V%o7n;8i?L4B`n)sAm-xD+2KjX%YBOCt=`1U|(yB(M4 zQ&C+?fZqB0Cy1f;C%m!%S2#?rEb}mbld3GA*xIs{tm#!Gtj#97{|?pzpP9b&`3ndM zEJzA|OK-Xzo(W>2K0Hbb>yTugrfDk_MT9L|wg5~DMFSLXDReMyjYjfAd?-&329ueK1JKQz8K}S& zQB7cy2^WNE6pw&;SyjYz|DGOpy#VM6v+SnG#OeN%b$wy%5y!AbDp~?rkHv#14309; zPC`@@s$VZr4)_riF}#EH66^8;`G$#n>-lfp%@33t`Nfs-k;a8bbRQkFva+H1}j1n{z?*KhK~ zvUJxy50Mo&1I1`DT67H$Do(Gz_E1{2EI-q~S!>IaF9NP!wVXxe%K}CUR5Q0-8~V&M zPlO=!o@0+WI1uu|ho1Cn`o+(0f}yaBf9I1uXc5A zLm13Wk3RC3boSZD5?_3rSBa%HAzEj5E=sz78$R% zsH{RZmRxN)Z-SdYB^6&ricURWMe(Um! z)6vHq6MMpHId-;hBBz1GKr$L@`;JW)ed!`WL45m)cvaa<5JiyPUH!IFbopcY-Q(DcJOg$ED^i@h99uJ=}IzY&yVZ&+jm;} z_p=M)^N9!ViIQ|JUJ;V&wk}P5YFVCw&|l22Yu2vH%#=A4Mex~Turt4HxRYVvh2$sq z<4x3wX15Bg`t{IT1BEZn3Z8Z8*Dt2f$4k-H|Ngn)f^*V^7k&wP9)OG18p9h=SO=ld z?8$zhmqSC>gmSn4{&;J!tY3KH5rkL@A^aH>04?J!Tb5$^jzUR2PnJu;mutvt_& z353NKC89CRwbtw1Ja-N-#DfRU^r7~xVW2K7Re5tkxb+awZ``pGWwM4fy2nvb?csFm zFMpA4{Ql44^}l2A`{&O)ivlvAqly>xo_DN6*jjU0fyumMOnB&Jlr9W;B*Gc4#L$b8 z>62tE7~2Y!Kv+9_g1Nu~Jk%P3x6K9sea ztJkG27LT`qq?@tGFe_6-1wbyYCfSE~Tv(Mn(;Sr!+^arIre2K69>AnO%ybZyOFW`9 zAp~maHQ0nUP3+cCQra&qAu(rXBTUKkf7~)z>hcDXD#&B`gCvR76W3IQFYAypSxp*p1JqKi1mJU1pFf&mMzJ;#LYhG=s3k zC3Og)X~2*o53jsJ9D#-O_zkrPN0I5a`|i7w@i{7DTbnm6#G0Q1rS`7W+DW5^Rx)ZCO{J2*oYD!7&jl@`Te;N64kvz6r$ z<$Z>7Zxib~7coCM-WsFv8i&LYc?x714;?xrJPb`#@sVkpqhMTE;w~nUr;18XZP3BX zue_Y*vrgIN1bQUKju{)Vw~@HBn_W-2a}{_l48(VE8Q51+0&>aHCE(>&ERYe=)}{?_ zMH)-Z-c4J#rN2M@Oj?G;X7Xbj{hBp-a+-$~I%YJP z#I0LXQ}_DRcj(NtZf#>a_2iF3H`CM4e|8J*&=nEyHA~izlv^SiV=RZ(cQ)BR{K&bb zaA_an(2nh;mtKnOP}e@!GNwtuX7;YLQUhzgJBZKq0Go2wz_zkR@~w9kq%vZ*Q>R$9 zt0lFtwm6l;%jjo2yRFpJQ$i6Bh71`%%1OZ~6eX16Zk8)lRgXXjmj)ib0q(9|%`z)^ zn(kF`$u-w1IJFo8+qbVkARvTzj)JDPb{w?VjjR6Ei05zKyoKMJBiYJqT`e?W-rCCk zxfBm6VpTvj?TOFxaqnAs%xXpAl%*yW&|}Aq1wXbyV+)}DA~v3I_ZDazp`x%8pCk%H zZYr84rq0jVGTG1#GFC_CuuFY5ZF>h8A^6CP>EAFFKCwA1TecL>)WGPgeQ?{kg(MqUSY;9l zdNajQwr*cUzo={m&A9RTAE*@d$urMmDc=v$*=L_c;r{oS_tgQv0u5!sDlKIXr*3(Z z86Bw82Aj4y#!e+b=CT}*hE}m$K4%C^gdbX+PO~=@gr&UGHazb+B$~q2U^z3FYsi|M46l{36&ed|oQ67B|Ct4>spff? z4V!Tg3bLF53}fIh#?pqiudt2h0SMFY6{rgL++ls-#4@lzJ3)jBVqZ15qAT^w_aDrr z+s%onJP(7gnWWAJmZ4y@sCl`#T&uMXcd7{jW(tO;V&fq&84d3OUIn3u_Q@j+!jN2w z5U`0VE2UV#6<7tueX*E)xE3!08IG+Kav(Iwj3J2WJkKs8WJ(N~-^$70llhxG(1r`A zi1nByTQ;VyfumCS@X55|gmduBC~1a5(%85GqRT}X%4sj}lW5IX_JXBkoU>d&8kK-H zvWs`_+&~w!-W_`%;$6c*&}uHF&<05=B0f&>7%!XxK|xo zhU!uzm$cx%W}X89Ayzfqoosa7(Y~2^MEevDsvQWVK7HsHj5F3#igYm~EeAHHZ(_U< zRo42rj2}xW_FYvoDDDZ^alI684O<0T85*<5Y2VusSPDmpzJPDyhT5+Sy^D*f<^_|{ zGBs(d2f^3D&M@Lvz=;O!kNuY@JjBruQYu%h`?U&7zMpxd-Tc|ROcbH;M#L7)bfnTW zwP{Uy%l#vXB>D#q5Wfr}6-PKiK||3pPTovT0O{Y1c_7vRih&F*h&`^%O~zdkt%Xr| zE{v+DD{K6Lj}WmmB+R>Uzm%g4m{DrjLcA8P5_=bJ%$+2vbYQg!uc(p(OeKC@<8=*V zh?#=R;0DqjMROa1ep%!#^dafcb%uI~oUfuH`b-h^VsKi}>GCbW$u06aEw%t`TqeQ> zkYvA_xW(8QLkG08Y0;Z$A;P${erOsqYc{p4*$K(SFEV>=qXrL^>#sL2ep=3>=rt2*sL~TD5ai7l|wNvb&87of(&#e^AT-}b+-3R<_p!j+@ zRjt?<2pF0`2z|H1QE{1C6Skajl%kXd6CN@)l|U<;M>Bs%@uL7e7^uWHQc{A|Y1m4LL*jSx} zJc+FAozQ`O54Dj$81aqVc)@O@=7XZ6evb}h!Rc=juMAmX<#V0HoMEOMQd1)97cE4r z%mH_v%lBa7TrW@se5?}!cw}~@b~KC5o+`pwFw0H24eFQ(Q~c3>n0+1yI=$Yp_hRpo!dcQR6!|xWn@9f}as_Q^?sk@;s@mnX5BmiFU_84C~?a1s)fprO%Y0<{jER|^iE(kRi zio008jmk(ZWK?(Hju3IRbcBm8cuS~l-uMng2@@rCU3wOV&8%y+v!@44oHi>|iFXR8>;z#wikQcQeN=O%(cI zug?ycweja};?gQX3fmIa4mZYrg)-f|)>p+0amuy86k)8zBTT}}0YSN4TqD++Lbw#6 zsD{_cm=U3TFQJ`H^oLGy#w9SQVtmdB?z;lf`ONNL84bdRxy$#+;3OX4AB@Il@FdU0 zocml4ahX0*)3zC*>G+Fas&T}%;$~jE+N1E}HusNh3HazT61L_Apgb|!fFu3lA<>q_ zCE8K=j0H+x9qb{xbH`d3J;D$MQeqwejcGND<2E2Xtw1JoE3afyaVyHujQ@K$ZkIs0 zbZQ&!zb-yo2RPd1ceACtU{cbhyXyE@Yr_T|N=v|Ghr*L&6hIN~QgN(6!_H|@k=gi; zo!P)GzJ1$j@Jgl0@DFpveeMq%$p^Sav!{kfsn3^WT^T3~&>)(~y!W6CxgDWhS72Hqbu-%yHa2Q`Qsf1^aJ{LPVL5T$yoS@w7ML66 zRrp)}ar?H7X$y&RwYB|tPOemB=BqdW8gU=E5=tugrIH&okojQ<0>?^PC>*uPmkvc#ViltP(?UUI2Fc{EGnwl5}8Xl1nPxWW*i31K8&ZEai1 z(xPQ3iW+9LDJlpdowwMl^*iYsmtL60j2S~4dF`pz zy@xcP>E5;+6Aul_@>hSS9psjcSvoP4DDcxzqr0k^0F}p|uMeoiAHNqqaDVS1{5@Q0}GO3fJ-h? zELbmsfXdKfdm2G1+`APrS<20Iv0kzRzo?c7%4(iE!c`Op$RiDp<#&vJl6U1d~O36 zfXx1`X6gAZguQcAOctt%7{;0EP+q!NBIF!dt;RW5SUP_yOIBi$`RiJjfmgsZOI*)~ zcL1CPdBqig&KLV2(~V^wB*cVZW{g^#(O;PX7iFCBbDlps9DVoQ_T?^MV!**Rv=(F# zAv|HaGTvCY@0fX}hp>|XeD}yotFfhASi*LFW?000>fym9%w4#3xJPCc3QF(u6mVP? zv&GYRBJD${ZjNjTOzAVt3SmNdQL%CtjWT-06Z7r4nRe1uhWjH)r zMc4JK_BRW9gbE2F#z~9teh3#tKKJ1B;M#5(ES-~9w0Lc^ow+d~u!6FM14ax@WiaX0 z>|V3@y`|}$mFybAHj$I}-X#@N84WoBzED#bi@I%la~iOn_JuKj%IK5hQm8gHvuQO2 z1q5^jI9`e1VU~a`Atvx$o>4*H`cP%cq!m3Com=tDxa@|CZzb?Ay-aTQ(6f~UFZe|l zbI_@ayM`hwD2Z(@8$v0pU=G3^if}Y@KBSm)E8;TNV5WSOuL!ejp@1ulyOW|8EzG@p z5*Oj}6;GXacXv_vGFl$fi?@ydB~B~Sxo%gn!{uz9ZkH>`lNC_ideKl*@M+9cQ7Y0p z?epiq9`L@O4LdxJI_jw4GaFgi;e9b)&O1I|fv{pF^y>WBrZkjgm2IgMWjgX^aoZ|< z5o`6gkd(grTRgb;nY(1Iq^U{R-4=m#M+@u208NGxcf>5A=Flrsf_TiXRewcf+FInl@ON_)U{-J_=1!Jt=0{M z6{aP$<*Num*p1fOg>WlY$iWZ@fFMlCD#q$Xnx$@HFK%F6hAX$5bzSWcXqf>>G*MSS zAx`83!E|(kxwV__1PUgj(9+tn?U-90i$hE9uFSY+5c2O_b~K^~6QC=q7l zcRgwB*dY$|o)f`sldrvfcl10{IQkCL|3L6bMEr}S194FcfVQ$lk#SUb@+t1M-NNS# z1Fw;*WP%dV(F_8s2!u4IUWk(jH-$>zHjhyg&pql@0JvFUWxBreVpB3R{`Srd@Y6a4 zryNmXUS$Nq4D6$G!5H*hXz4nx90%3%WW6?huw1B@N*h|J1zK|+tvlQ9`a7&LnV;}2 zXPHhVS-2KZcl<%K;C+lA2Fi$|k5*3e9?vTv9G(4gVZ6*odU9SFC4=W4u616u({&NL z{G<%pHV>c&O?B`n;(E>PX$0j(yPDWBXF7GeYs3Qh4T`4u14Sruomjxj@idJl8ObdW zpL}is<3d4zCsji04QzAQOhvI~l>au)*_J#|Sa(+d2GD$H5@!^u8K?90f%`l{ep#0^SZ1^{!@_1s-g%#gKt zUdOaB2*$;`S;`^`E%1t|;uDj`jyg_5kBA2&?5T%Q+78bwMnDw-i>{qm@(AtX?P!>+ zN9*F+D2R=_P=(_|;;s0v3FA`!N5fLeAj5W(Mt zeI~R#x3*Ux91%v1*Y+x4fV#3oGUv{F73vCd`z1m69Dj4p^Rs=vSZWzFlL64meSm~3 zq+}%A6a9YPpYj?t0I0-adJ~m5=nO`wAka9R-kg@<^MEnQ%@ds1FZ`%kMsm ze#@P`R;uS?Sh~G^&^_cd=kaCuX94SY?NffL7P8DJ7@F-fF7erzz7O?#xT=AXZsZ(; z$?ayw5)GqB<+{eY_H?TSqrnASfUgRin=xn^1iT$&j7?!DJhTejIfS|Ak;aU)b^`9b zKq>3JCm0WI>xc_NFM26TfuGM2M|DX$z_9*<0oh0UjF8|NKqKH4r38hS;|1=zJ+(Zl z8|u=S3FA@&8N^MS!1n>r9m1?_*`~B^110^WL)@SQV2NXvQ)joCQiX%kYb^|lfGIQugLOHS9;6mxdYu%3sKo**L6E!Id@&Wu_<+KSOPr)KcR}f* zYEyB&mEdX_N;UMx5*pm4&Z}fngfP_O)D!e0eB^I1X<*blYWk9ywrG?wS-g`ca-MfV zEVzY37O1_r;T*+po>vfwBf_|VOA0B3CwT($W^qU_qVFnHx^kuKa9cuWw(fj~av|>b zK1-|=XM6=6at%KQ?THH|2ojS)z24t_*8`%m63QKYw12ux1Gi~Q<49ugcTPJv{%3mj zt9=QCaBhO8n6O?t^ta4om~R0f_5`qVA8kvTaHYZ%PdtDptR>b?Pa~;|p0Y{93i`vn z|8*3`>bpllnJ8S6PdiVHo%X#dV8Rc^*I}82D==c5clr|jVM2rEn7mLVdWEX@sZi!| z<`(j2ulGCi=A`Jhi|jT8io_R)L(6~!BhA=cd0Mt?SvcI6nSjbd(wHDF83bd*V`;-qO4Pa-on-rBd!E;#&4b`N23LZ6vk!o2WhNUb?^lIo zUD=3*v<^#k2-YMn)~+Q>a0!h@DB-TglI@}|5{Zl~7%iRDGPSf=37T#vG2%`(0wA!+ zE*Q8m`%zFKvf{8Rh#Qu$UaY7IH>-VO4Q#?H?J(I$WdYc!t*gb9;=1XBYYklEKkZWT zjz;(mVGC1oGjh_dQYBVH7i(-g$xf|UxiV3ei&!wlxDn9oVU~KfZ%CarY@vo#6J;4C z=s-?kNu6V~YEU7zeUb>QJzGuRL_Xa&QB&0Mg6P&h6^05nVX6fwJOc@1rapwfUnN)( z!m}|KUyI^*`zsGfP*1^JD zz2=>BUrYMuua;ONJ)9tUv z1AI~e>}3|Q){^nb{Io`7avp-n8$#d$1J4#ADRsB$Ey~swb1KZ}hr}N-L!R4=5*X*f zVYjmDW>=_cT&{yW4{mHHH)GNA)--m!Wd*Hr6OF}h*W46Wm`8p zbkN;sID$S@A*3k;(^+4Wsz@}muy`k9MTCYoNj!x$G9)eDR^gMfsi4Ea9^k-WJ4hRR@w!Ib-%}Pk{JK(_m zV$9OrPBPZF?O0B~#4Gv~{bt^6;J*&P7ha)g_QE$`(o1!;L#JpjDR!(Ju zP9-M`Z+QaOGX`&UJ~Q{BZNK-P#vw-fWKU`a+X)?fP;tml6GMmr@CWzklIWD&_VgBu9A*gue# zdnza?XM9TMY&&kB(oLkBLx3d+p00kx2gGez0Uiisfh=$%nIYzzi8tA$ml3!3f%u3KfadIDP5;?ZrVEX_crefJ zg6VB0zO!{xYDIZy1ukV_esMdKS^+mIrx3JTDV3J7?j3vb!!D1kUE@rasLPqs z_DxtzASx^hU%xvS!dxxP-}2|cjzCBeOT|XGi=f^|cbPw2ye47+OL18StW~a+ECL2& zl~HJ^@ltTQz+HlMvA@KsyRNj1xgeWV))2}YL}Sg+z!z}O&oOCq$-dfH;D*eBcC=`P zpM6(zF2OqC(Fh8XyUdUUr|h4T5ivOMLJz3~B5~MG2rv1Y2j4?;W-Jn`FK;ivCC6Fv zS(@za6VIVPLQO{D^FqkcGw;>9pv)G1(;bJqkSnv|!PsP82%9`!dx;_hK6l{(=|JeX zUR7EN#lbdsiEqKc5o(@~{&Ox-wzI&1g@>E5cTnH8lVwc`IO*6m-^R=Gee@e#v@l6W z2vXapON^n@gcjNg#Y#Bg3f9^U=8Rz?96+Bad=PFu`k|1GqZh7zQ-u%+uqQzaY=r}q zg_6rtX{RM^fX{Rh2OK$MV45^+1WP~MyOj0s6t@`9oNh&FeRIR6Gzj`1QAI)z^xZaT zVk$z|mJy$C*P84EPpRn5!la;s*sNhY+4VcgL12uj-#|hrcyCz4Ub<-$$}2QW@*a6n zD71tqRDeqeLD4_OLmO)*by}IcqP1Eo^Mu|-L2R&~9j972Gw{GW#}k+#E^^wJzw22L zr=*`Es<26G-Wy&6e@8%pb-Qtf3H)=Ev?;&T!ndFDiU1tQ(m~?i<}GhA2SAlE zhcZjDQl?!MJ{~Gk{rACl&qzPS?xy9jyU(6z4y;7zt+!#sZS7YMf&@7 z&tkYqCD$!PXN(jaJk26~H>3PC!Ef10Q6fPAq;dehl`aBh8YFys0#QtZB*3+rFiW=@ zNWBwzy1bW(AJOf>sS-ZSf09GzOrpg!{w|yDe%F>JFFSbrUgN2kwfyedA?2iNsR>M1 z?>E+IFXD-y6YT1{;J{l1e%FTfcTKZr4=$QqbdkfndytX-Iz|1mPZKtV3AWmMyYA^) z#4igh!Z?Cs0yRR!0lVMf)?a$O?CtrG1AXOkB_Mkn{t0+9lugsW&2e2e;YauH?b9q* z8&+K1;&z!bdFxus8vly$b{u?}j4vjNt%C13_A4h5 zQwfR7Gne#Q9t3dU>z;B9e`6Kdc#^WgZ@2Pm;zbAxn#So&IThOfDI*!-z(+Z2tt-p1 zfaFW%Lnp~cvB+68ai5&H)@Z-}y~4Z7F``RIDusMLZ*s~r^vD?XXq*t{CM~^-ZW&#U zUt~Tr3d^}NEBOY2l@uP!TZ)cuiwivfXE^{^dU;prJSUUkGr&t2{>d(?E^U|A@{Ut% z-0Gqa!?-X+n_$;6wawiI1nZzDqNgj>!MJ7n$1r6wD;)ml8a=yoVOx^24&1xt13j8? z(e4laPtQu_u`|=ku3TMT5g>ZzEc!Qm;30y0dZXgsZEz1Art~Nm&({w4CO^6Y`yDJJ zRsFuK9{M3byljb^vfh2?QJcdzY!ke;*8g?AK~LHr_xGE(@iHC%hp#?6{Xc*9v(ulv zm7#A=!ILlB^r0;X8-YGOY&L!UF6l^aE*?K^65q?ZdQJ3uP;hEvAg$8&80|-OXR~ZK z+4{iFSMETD*ol5O>DtSO*`l1pbvR!(u`ohsQ}B}g3*H>6VDPFy<;~VgMxgkx20L`B ze;6{3xYXg?!=CBM=|}unnhcGG-{gFqA$`I-b&ZqECZO5Uy?p&v#vyZ4h8lW-&dTv| z?H8L|dTFZn+UZrvyhez?Zn|q@cn}X|6`7ZI@lL_Z)W*y)}Te# z^O5^=nNc+nqQ`4TRyKwt;kr&%eNO4(EMGREj9inC5TAVHcj!03!BK-wO&|xG@2>OL z(5W(yZ`?iTBANPLUFDs-zNegUB#$N9bYAuW&*)6=!N>W#*u{Zm%2_*mI8^+|F$;jWDev`{%%&3;?|fmh36qofs}10iLNA&6n3AQ8~9 zqlQf+FnkDHI(gg&($LE6(zsP?sm-^ar#MCKW)(7w?>6?~`{kpE@j?6Q<_Iam^N$-V zdXd^Sf#LG&)+8d0R%ME3ca6Ig^@?(4WE~&(qKNgjnYrv9^Mdy|X~Qdhcs`+f6fV@4UT+vmmeUpGVXlO`p7 zR5#S`)N1{^U!C4ITi7^GV_$`r8QaDXTHAGz!+5iuZ|*m7;-ao9<84|a6*WpT$Bzxu zV=v{&si8Zj>xRb39q8ZBT;0IaFh^52=Y6nPbb zOmU=`s4H4sWK2)T5s{mndC15igYF5dyq*zs9d}5*?^c;OJB5$jilQ@0WJ)U>Y-T=F ze2T3#O_sH`!TKFzqu0(UU6S7S)QQ*A_>o!N0_v|>nhj5S4o)TEPE zl<$J$d`h?7@#q-Nb>$=_O(<-RK=S+Wjl0vo_{onO7kGC1^S}MO)4%`u&*Ibb(;t8I z;pvZ>HUFD-_xb-coAYtwnxEW!P>@l#?d`qOpZ)z8ryo84`*fsUn)jODkYRs%TQ5?^ z>&I5+9gC4k%qhQy~DQG4C z@R&_O!qYL3?gjnQ;D)aF|)n9_Dlz)0RkZhJR)Bzub&Spy{b2eInm38%`O z4^4qDe3l-Q@W1g#anndEH$MVS7g&9c3mh)>!k-RU!6NA3yj|d#PifEqy!$+Qx#qTV z3J3?UfmdT|08V)Nw!bZ6=sKC1Wf6sv5;#-Uoipl?=r7wan$dfw@d>SunW8-eIfP-E zJ{4E9=nKKOJ7FiP90(H#05N#J$1p+S{2AsD;>rLX2(KCF;IfDB>CIK*3sd`ZhWDDa zWWvF1%6PBkKBt>DfOxw|R{fo`ck`q@<;r&P-VbWgw!ppmpXGQjTB~;57|U%l3UA-3 zHR=eqM|A`x=rUZ?Wzx|+!!Q$_v=qSfzx4vQ3muPRU1Z;s@ULnaLaKP1^|jVXVEAXS=eb<8;k3wnQbqg_CaT*@-LJ zC~4Q(;8Y(RrX^k3`(_;=nAF8Nj$){&OL;>>^@1I2BRKs?SKMm_l(Nst>(ab(J?_tI zd0xk;mFb;yCeSdGYMySd;mH~)@`es$=%cx6sWrOFJH~5%Gl=I2ZeS{gv5s{Dse-K} zp|pB-_MFgTZL{^Z3@qODqmqNEZ!Oi?8ynqw`jP_V++sa&4z)xF5k zSI@vY-vz~5MVm`Q+<9yMMNDqE8lM|oZe$inWZ=W6|+fTpxv|gQ$ zPWL}7K(;pg7e71Qe)>u4Q5#m(y6p320ouKGSjzGZkJ-AQ9oa$er#VlP0W}yn1Ur8@ zJe^qR5fWU{1LQ7Bh;H#Y^m~17!71Oc#jcuI+N@pMp)-yWAD5pFZ_g%I-Z>ay+mt^+ z3PSdO(we^@@LIXSV@3$fba8dsP)>jZkb0$8w)#dEduj6==kGoVz7h=$=*93Vnb7S? zSG8(QKs487x^}~xyn=h)r+?~2hMCYtj{2a4G1kMr>pn-Qk+KDD%1Oif212wT`yBpR z7glb9Lkj=P=i^#>&d>kk&u_mw?vHO7zFW@HZ0lM23mVTbon?0#LYOeuK-isoj0vty z&^<~Jt1)zl)uR}6ult+ECPZL1MW#fQNdeBjeE<1-b=f{EYHOWT#uecZ1#paj>p;Nw z3eP#3>wXE1Kq~~&97rB{H~&6AhwpW^_d%Ry7$KAuxNj+OJ-8G|M%om<*x z3^x55ZNKr#2M@J)1V0aI{cRAnJRc=0E$2ruwu`99eB@#COYb5h>IBbmub$=cS#f#= z)=Fww|{Tk0K2W!=zt_+S@LF*O<=uY683IHp<Rc!p=I6TXP8%y$WVfk{bb?Sb+X>o_iDB z!a+|ABYQVSJ$;LRw%#*Co8cP()PV*A&%&gb0|sC%oR!xYSS7cN4L&(#a$nXr(&^u8 z1BiDU!gx>%=&ROe|M^euq`?Rk{9k zUAQe`eR!bwBWP! zP8}0q#zS!9n+cpHiAOedDQHP&jC~U(vMmp~%mp>cyPw(6y(bNHZde-&=owZ z_rsqGm2cn;;KiZ>qX$TIb)SEA7hQVTq@)O@(*E^p{@U`+lwvI!6>0JlGF`9VYcEVk zy)iht1rR^hmu>}Wh>o}cf(ZuacmGIRTkVnwM&0LVv3}Rc%0u`tW{{~Ii2Gb7S&LpZ zo{(;&bpe~@I+Xw&H8IWJ+f!a=Sb}#z29b6u_7i$LLx~<=?JMPN6=XL$weg2Ei zRW%(wg=xak%f?^tipp+ljou~@<4_ZrYlb;ou~%e!Qv}8cSwHvP3ylAzYYE@mBAUj% z=XzGF@~&p))f;Vq(Shj?eqGDI^@FFMG?Tb`@0#NO=r?Uu*4osYoczP!ej1%0hbQHJ zU|;0&zL{asWufJVrmnL*zM>&|qCfuV8hx{ty?ZnU_c{ZPF+);X%PeePh&Gfpt2)Am z2Fh^hO`yyNAB}TQ7h-}G3(Jd_0e>KIJqfOIdUo)63N0U4)OmDKdr#@->URK_ zW~aJ)b{Ivn%#oUmqXj!#nCVUExyd=Cd+~&`s!1-EWTbMA-VG0X>HFa4v=DkPzqFGt z8hMZAjLfkM3;Ou@i|1R;@uX+c5~ZATKn|^Kd8hOpo9i+7)47>ElETl>$KcEPRfgbF zcWs|5+p;leYWFK+>2Mh*u2({P8VJEF*4|#gjZJ2<6q}I zHfgZ{th#2bw0Gz_R8iq~=mpz>A?M}$I@sBe>t?9G2tL8^!*G02%lWN(V1E3A|KarQ zXWyJY|MS0YPwFpEfA-@)KK;udd=!pfpML(cpEuO>S*^bZr}sYWsItab-)_zJ|kYH>D*<^nl)Mj|dxjo!svv9Pms_F}~IY7a!MHkW44{T72D4L#lVm5tO~G!J&M%IRTjoWmQ`RNl8w~#crr)Xxcqn zz?qKDm3RV#o@>!=juD&Vk0OR9%2~Zp7dfUMewU!H<3hhz@6HcBa~<^YTX~y#>9ITn zPjWp$@@rnau6OGmhEI_}5H4k!vgVPW$6b1&>L>|#=$#Ab8&MG2xQ3R;GD^gZSXV8j zY2pv;`p;oXzbzU4^n{BuULi!x>4Tq^t`?Covesf&-8$Rt0{*UXA3_BkLio*KBB%)A zo0!_1h0|N2&F-}p8K;|O^F3=K&&Ah8a-X&g=6U%(css{f`9-{zrd*|Tb}c&D=ByJo z?%HGaHb?fN^HgtgsAdEb7_bS>r=<`KcFM^rB|G64{BzV(b;q_oudjodnDX0~mZ|6W zYBk@hOyjVR>%!8CzNmZaWrqsit0jC#Or#@m%Q3f+6(WZ~fPLAzDjc8$@p2o@LF zT3A+s39j_YARF?yABR*Pc*uhbjQ*4z%o7RsE-dExxwP)QWBkdkDb4n|Q+5yR+<|kDDaK z)&J;&_VjJI<>$3R|LxCz+B*1Wr~mU${^ay~js2UEdy^gdvY_Jgx{IzpFGy<^Fj=Zi z+NmY~P`B>W)9=20ar&yR*BABb-0w)rs91|O*>1+Ixd6IfH4yaH2qG)sl`O(=<&Evx zUKP(cI~+Z?pnyoD&urlRK~Urwkw5^ z!$23KPC4goXjB5mEUw>ddo)Y6)@cbQVy!NiQ* zQ?qn0TReXIgM{V1k8`47Tx)F3M9q<2G)2FduJOwEPA@v#?`6)OgDcbY_N#M9MeybI z+|rt!&TWxrwH}}W^q*a8P|koKZw{hY=5#Surt?#;Iv(n}slU%#dwSDsrJD;Ar2&zKwWq6)#-6vNRF7(Wmg+NL2lczy1E{8BF&4&L`*_KOZ3ODx9qqC=0M?O z&{yR7=pjRF8PiTEW4IS)c`0qmXq<_p^uaImR}O<>D>IJQ@|J^_`1gv>rL}#cuGsH7 zJhRA^J&FP{G@xbY%X7~0aOxhz1AMPx+%mv&ytf&NVi-ztcW;*EfJcOF%&JyE&wcZB zGby6pmKhm{= znG`e{pzrFavdlve=*_iYPdSINK}&E3*?0Kn0qO7R&wTs63HLjiS6y@#fb`)eOL*H7 zx2x!)N_uqLyO$|%(mE3$zyG5joj%U_Jt|`Oq~jz1`txs2zbP>PQGxM)_oMeu?}X z%CpMJ<@Ao+ZXij15kWa*7jH&}M}F2;u5M%#ljCU(NuRS?eObMkpkBu6Ro}|yYdxY< zY@&27WePNpU{QLF7xp?d%YRG&6oh80vvUk8d79M~##+0;Prjx;byqI9Mt*yLi~+A5 z>0NjodS3D_>FHpw^ga3!t*2#PS>%C#P|lQ{wPkZQ=UK|mUt=@&)xbGvD&we|to`=< zmR=991jp*pibg9vvEUrK((A5N<9sb1${SF?H}*8hAQjO2y~)v{O;IS#zF5TN?O()W zs0bW|e%&qS_{MZ7Ay)oBHAyO`dii$UO79nFwD$M9U7;?% zm*Z=F-o2R=)P$s#Sv;-#_S;%5=;xFhbRXp>Bz?}Tn&0ZD5M~0;c9R+9HJTg6t+Fr8 zrCS`Je-^%Yc%$owGu1QEWDWa0Ywd7KB6u=MZhYjMw7nxA(kOS!#%B<%w2n}h`H&%L zC_a4FQOC<@#^5b}x{^1I))cwh%`F5AuY(61*<@qGUq+HVZTCngGKa=;oXnnQAP=_>;8w!y^KAl zW|Ie#g!BfG*)mMJKph;1=kYzw3hs>I1g+7jF4h<61lpM~EU?gBD?f*7_p!k}W0_Qs z5#w0{^G<_;y3I3~s z>if~-zD-N|G<)gvc{6kW{#T!#{UiN)2dvMtz)JH9;x!2aJXrZO1 zC(a=t=1y{-PGn|DL4 zNskBX?B?MTkp+uqdV$D#SXR99g&`amolVBG6Ng?--2=t|GwJEb>T}X@F5YBLf$tmH z5c0 zr=C?VKcEY6cjDUMKGG&HTUVg;AHH3cbLgB~u$+nLcIclZ?(ezpcmL65Zx_ztg+K?N zx|9i)@muiOrxP5l&pGR`7uC5A7?0`65>yT3MH1x&Du)_lFbm?LF#z{Vs+kZmTL*4e zpm&H$0FShuZP)GVrvL6UyWL~!u=n>AJM}Oi#?w|{!2mHAVMdGru(VnU15@y5VLr@= zzpTsWaf{dQdEfZqt)rNJ-NcJdp|Ql}<%ew&(FPCKrzR&gTlKCJ3NG4(=&F{>Mcvi6 zozQU85n?aG`(?BG?2UTegoc+X{w3)%_R-h8k2Dj7R@arlz&o}uq7FgB26Iza*Hx3fE>q~cBI_sbrTnFR_|=Qk2a)|n zySy0ZEk_Zz-Af8$GC&07Q4=;O3?sl89@hcK@VYv5cR4YJ!f`~0;4jVFK%z2+=2O5L zPCP!81W(;-p+0X?l_-}3nDMq~INih1)X5oI#0-+IH4gV){JVb4Y*` z1p+e8a^aYWoAK#6yfz=FvPK`gYh1{tS-(%uPNT6pPC42*ud))Ro&z^^!etB(J%~0w zv-j>;1_pzv*}514X(LqWK)agB%SfDmGtJ6n6{NVI|`ZxbW&hp~)aV=DbeE-v5{rvPdo!Il_ z>zmWR{=Gjq{jhb~U$pArFMi(1D;@Ij(c307y>N#C-9Ww2H zKoB}#dLeCs0u8z?O-*7n7gkE4Ro?axof=+;e zLfvp3m@4bA8%H28o`)`k-%1V47WVn&rVF0Xnn1Xl*K+2aoV`C7Id`xBs2eSoUMq|2 z0NaQ+f@u0Pv)W5bPDfU!p4a6}i#f$uS&j$bQK#zRCaL?%^xb?kk5ud)k!WZ4C0GU3 zZE^PGDcAM&wj`AdFp4)`p5g_J2>UR|OVlb!DZG1);1UZ|4lVXv8ECYEwE_s%<63cX z7vj}D1KV{yUbidC{U+~RG|AvLMY(8_&Z&tGcNzKHmYtkz<=2^D|=kksB zi`t4VW4g^A?l^-YWX5OdG1UyrvtMbn;Y^JQ5ksDoy+vHt!Q3JAKH9&CdcORcvPTNl zQ~@cwHFVT0-#G%Cx9b?V#v*^ygs>cgGb!)dMe4Hkb&ndixtFndG0SJ7gPosJ^c#nL zw|DW;k~`>6={PH6a$xj9-vcc4c8wrScmS8B{Z1r%J$A{UY>UNsiB0mG^i=^2O5SoE zSBg4|x(Qch6D2>6h!1`Aco23w34v!7c7+*Dz%q(Ba1~&j<%_Hh1rOj$d!+M@!Qedi znYDwR16!9fJ-@Q63%mrF<$p~J1|lP09Td~^<1$GH0-nrIKav;9Jw~qEV+>JRaey7QaP4F*>ZeQ}ZS}s&?%vIyF{mJmBFAZe@ z^k@WVJ<}J5K9G^YJJ_^dv`W!vH&-`FOhD6m@%Y9N2kpqS-X6vpeWcG_LVIZK8|@_) zfT9Cgc$v*vebQ^Qy)ctDS)7TUWT#B{HCOx{sXGx}SxuVkn?4mksh>owlpc;7#lbaPM6{O(%WBc(5<)Q zTMQR~38fYe*sU*oSa*TtC9cD;iE>^OV8O%yA4XvJlyo4uZT@uQrL-YG#%0m~RKBQR z%k((|o>6{4pzFH2tE=~oZxf)#PH);7$z+~uYg;;q{H0cDPU%IkeVLQJY+vFx>LPmM zY0CVd;DC_|-b8wEK!x;n#?}bzbp$Ma$Z=sig&$Jp+`vbo$4QLG_7R{=!wkY9ihid28BY6(MzE?JuOQ3>F%4w>;)?X z&^w3Fzu5#4(W#xYCT)q|ZW9cxRo%Q4&qfLyu4s@9kOjvNKRV>!Ye8{TjWl{;9D%(CY7iFkRL8ERDH&e}?toi|^~lb$LD-gG#|v8k5g}KuUp- z-;LIF5oBm1!9eZJ+3u(C4^0RMUEGh%@~m8+mspe)x(4$P_}b$LBO2?t|bzqq`?#6 zcbBvi!j=EkPq|Jg4}mvL*}{8eK= zmz_v+@kuz?67j=#PM3}6JP0ol^YbPpUA+v~7PVjJK%Nxuw)d}V>Du;Je)j91=NLK(t2L^B{6~Ls`qQ8Mpx&FC z(_jD7&s)#>{qBAJL6MdN+WD>yBEd(hC#qn56rErdAN>vX{_8$kwV z{dzbEDxb7pGuk@rKqKX4`gmikn|$}tYP&f1=CPZIK*z}D*ktrfhXh>SJM3Z{Y|nQ7 zLQ<06qMPn(!S9^cdzN|p!t3d#POq%T#h;f29ySPZV1)-GPvoU{u7M>dICdx)M=yHo zp8!NAlg+|*a406P-e5o5I6CDb2lRlp+X9PFK=4`XJBQzH|g^OClSB^L8ZzQ6!<=AZQG3LZ&`zun3&`2x1gW zfKu*)nY#yfIn#odl#Wc`w@6;Ak|WzScA{5h>=@0UJy?)Lua=%G%!04Zv<)UkfZR>Q z7`(T}wle@Sp^4>LFKapN;l?>OTLtnZ!6^zqCMIzhuW3uMHd3#k>e`+ay(kQX@haoh zBIHq?_)AnbqOX2D!VmAOEYnjdB_O+b5VY}rg2sNDkqr+xLRHU$0(X&n{1cQ`$6U(p znl2+h)DtcCXJyaN@{)a5F4tj0T3$nR8IJ@bta}G;208 zNcRF@V1wh!X4^8hT0HmLGyG*i^b=0MJRF0-;X0Wf=P)=2Z+i%T^t}&HUw`$xr+@hA zKb?NoxGB;6e!=t~esurzZU>-$^6PI;fA@EvpFXRMCqF3Q%Ta&7-N)X3@@Cz@(V$5P z&zdXow9WY$ZRabCm|qrO`0!(nej9yPV zwZzUx%TBzCZZVfEk~wn3(cz^$o?$*6lxBDoPsr;0j%IWOO*vvbz&m^&N723Cs}r>< zD?@LBK+=0OuFX*`>Ic<2A9P06s_Me4Enm=2el(!ezj#O+%zePFq{&s@l62QU<&7SA z*#F=i-6-J%n`Cfcl>r`A-bWo^;4{V%rsLzhORIONSJ_~HCwXveT`T`=3)s(eQx-b) z5ANFvRxdago~cJ!Jq)B?R0l$UN!k5dD@ga0Ro$wuY(JJKy-PCYQNLXB%!9tJDbceW zm^~RW0@0R<30VT;0;qJ5CIngy51J*XB_YkUQUYvRrlkk)R=z+?AwBbafF5WIqsv$~*Q2^Ycj5dxgXlRzF+&Rd9#}m@zrd;3 zx??%n^KVA&%I;+n?$d*Vh7Nd>?T>Q@OpDup( zv(wEdzitPpHl{GvQ_JpQ(dYAaQ+ip%f2rH5Zb98IFPr`NG^b;X>?+6fuoj0g5qp$! zT8uXuY+bq+DdGy@Ds4Ch54adqc#F1m=g*x+44tTbQALW3W~hJl8RYME7Gn(gj9g%j~g)&>MI%_I0)1CWmMs;@7?9-6-q~2jt9#@A1B} zyz7tu|&)A{VX}Kb)PQB z+3qg+H-3lSf-ON8Tsbs+!;hVvx^k)xxVymh%GgA!p-XVM4msJo{fnM1bgZ;Bj7#XA z>c-bis`V>aFpARz!}Qy1b#nH|h9U*QbdxMuhxxpgoneGGS||SdfB55cx6^ZSmW~bk z@BZ|UPanSg_36uW^8V?a=73!M^iqbO@gDdb-6U zD%{wx2taV+2VD=@e$&a3ujrR1Mvru|Z_iwKs%L-Sni06pH&@vdV5vpL()q@%nVe}kP#=w-%HQ++BTaIWVH|sAGD*Y!E`=P^t z^H|Sfh;c&amEL=r0cZ=ci?7=OsX{Juil;a0?tG)ILrQnqd5RaGe^R-P<+hgd?)|39 zcih@cKoG)4Kq7%~4pruKp6SNS@LifppMq$uu7O3ssM-HPFJLha0Xap$k1!$gNGhy6 zpZ`4ze|43alRz6ZV~{RW7L%GRus?(b4+*Ys<&D51Ru>(pfYY8*AZMz&l-S8Drm%b^F%MepL&>y1J2#;o}N5%+ZIe=(2|^B_F(_!*)aBz#4wE z39WIi@;+}1#V2}6YF#}z(|}UnYw-FIU`}FMD?JZ^p&0`!_$%sI`c)QMx;8L?y|I1k z`kseJJsiP({YI51V=uMvIIZA0mar&$A(a;&Im0O?zV}1%eR7|xy^}W?B-VeU_ zQJbK(t64hyenTOj{5C7~BPQU-*yQfxWXCK0%j?!QTP{*!$9Rl4t9;Pq(bWy>A;>zU^xKb2;P%0AW) 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 W-?Py+M<)O%F%3)LD-QkTsK=gUR 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 X*bBgv2q&-z00000NkvXXu0mjf+-nD9 literal 888604 zcmd42WmH_vwl>;0K|^r25F8Q+?!n#N-6gm;5J+&B;O_3yKm)l_)B?U=z6e1J=0DvwnC8h!Zz%T&-uz2qg-*P&3 z?Tp?sjIBjQm25>NMIG%NomHKTP0huuU7YNUy_DDi0J=C+BO?`QCb~gmV{;9wUJ+avtkR&?qkrN27G3LDzyw z(ZyUuwOIP+xqLuX;_S8=#S2oXY65Dw3;_XwYJlC|RXpr_5ZoXhJuENEU>2N7vFIYP z@ZXQfPsm#GQnBo@esF^tQsYwaY2Ej5gY(kgJ}63>h@K+AIHZhj2W9ligaAxot7za7 z3p5e~tPY-ohVw>mB0nHV8qpOpc<$4j9MKUF5~Q(z4(K7tdvu}TW+Lnxr|3;PXaBr- z27OBo!M_*$5r0MrXX^1`nGoTjp#`m?q0zeIeZ->)3i%!Qo1Qr;aBIdH5pi7#5pfGu zSYWEpx?jS`OM;9R5pmuJ3I%Gs5tTjKb>Yzig@W;4piqx5O4p=_KjFLpxRmB+90OBN zZ=8FYYe`$k%L5qRWMlvW%m)D6n*{Ur0l*Le5dI|t05UMd|1GP)(EUdT76ABV4S@fT zj?UZlpCtA6eY5`06*dRG3-!M%`LBA!%w0^Kt({!09UUnC zsn^)V(alwWit3++{yY5}r@5!~|7yv>FQ5 zUw-|&IsSht<5jZuG`G_hv$i*PaCw6!_=TPQEC0XR`QM8E7t;S>s`bB2xi~)mPs{&N z@;@y9fx@fkY;FF=(mz}Xe&J{R-}3&W&(Hb~QU8Ouf8UjV<-Xk(K@@)0|Gve7DC)lS zXaImPKw3;h%@gKtCQ=5jc+;=)wx^#%>}Wj5QcCebB~mJCYNO#M!kV8c_A7pikHS!+ zd=kUK?XECVXd!SC`G`WrQ#{1Z<$Cv|pr0nfbyVe;?RZ+YG1{vCmfXsYGk}>Wl}X=U zr_t74!F{)espKR6?rYnY3tKPQY#(8vx3j|NE*vgm<=eglA;mbyv-t*EN`H(mz$jeU3hQS^TNhIDow9Vq23(Y zPn>dJoRk9b!!q-HWnC#M8fd}_7RkB#xN}EKF%%QPyp`jThJpp~F|dGZ6Yq^)c>P0aMKPW|zUSnA zsGjBBQQ!S?$)r!fU{LyJfK2f0qE<%S0jqk}*1nHXoxk#aqHiW~dDX{dVywffv+xZr z$16BO(%AZ6a3`E7%F=4uT=;70@b)>I=3zpw7|?2l(>H9?UZwD)qy8?{AY4f~HqMvK zjb4yuEbkgy$M~mQt0%%39gUY0C7OgiUWU`BuwZT8BxV-JnhxJ3k1SXAH+uj(zf<=o z2q}P##$bYDZelpP!n6CA7&M2Tvj7#qKOw5YP{ivV$u~d12VD~Vzarnmr5t}0Y*De< z|G|Eq{g9)=?rga`I#`L$%F}3OG*sHSV3Tyo2OlgX`tNW<<%!kKU`e;o7}<7x8f>@y ziwe*HX;fUA0b0gLpU=iTNZVO`9JlfRuDb-OtbGubIKOUD>DcJFUtAx|7jWm>n-(Cr zH-{>z#dhdk;M~YH>t*JutrdaRlraZqPxd2dZzRUu^qh{<6v)#(7aIwr&U7;Hc|ypg znI3lG7?-gswjWdUk!xPf=R+|636#x%5dwKUkA0$QHO!r)Mum12TgwPd8%W07Mx_OM zvP*|2qWehSwI1<~d2{srWXM~*zjxkTa1pK#_WL!bppc#+n{n1S^wnU)RzHDK1{clh zQD*BMpgLyeqyzJdROT-9kUWO}k>a83sZ?bn>t)-vp6@d@w?)q%7XwX(tcCk$&zNOb zhK-~au}+I^{0)<#<*#m+LGpmsu0n8S#m?2LyJl&pI_^rfKP2WYl5CPgFyCU9 zbypa{L=1jNp`Woj*Ap z{yZ-oUy)W}x?vlz2B^o(s(7&WG!GH2ZSW59YM0VX__A5O6Fy>{Km5X8!@tKrf3=?x zlUPP?cr7*Ny1ikUSt3EN{xW!+QqD;Sc`4JJy?dyLZ24Obw=ixx^)#K~rUBc9SFL>( zMzX5CR<0w0t+oFSs;S?Ccw)Gxcds`X2$jI7P})wiv1pgU58KGiAx-iubl~d5(?S5f z_Xs6=MSJ6yVz&$21LK$IuTR=>IyCT}J>f6xiF%dLG|jO@fD$#-3aH7XNnY&WEKr#2 zV@Lvi&RynG$I$my-GkAqZeDPy`JsZoiU_nVW3^Piu5C?WP+xNP`h&uxW0RJo!JgY8 zAbAS&a$-t?{Jh7~VD#c_5c1_MqQ7(3sDur;n)8=SHj^^(rucEYco>NySGXHV+_Ucr z4+d@o!M$3q&%IhV(ih&AN77)zSk2X2SQh~@H7~Dn0gC zenBGj2C0BZIp{h;T5U}*<@eT6ntdJsBY|p?CSWxvOw+L|XN{%QPw~0&C;PTmOA|=q z*w@8w>^8vgjv8JI9BpX)Hx>@agaL0`&PLk=tsN#y=KKc>6b*?#Ingu>Men$Y8MAoY ze8%J6jClF()*PeEF#{yJ(`+b_)n=x4e_t(q zhoX+5PH(BPC4p#sz!PVL=4bg{PJ|{CzMAW)bU&TT9!1 zs^^S8czu~eYMXzRV3NMw4H~~if<8C4IqrV-Gh?n%$zIgQRSgZBSzwX=+(*FubkVMP z+Rc=8u9a#Y{o}9)HneILJs@fSYlS`P@efOXbaZAd@K0Ui)&HBjz!bRhSyBshtE^*( z&d*bWYpM$F9s@~2+nrrFn_1fIm2gAfRP5P6&q0p3cqIlXs3{8-MZJGiLWmt-@4iEO zV7|)e7cSI6QxmpGWs9X9UA^E^8npsST3}S_7kjSpuf-5G9^~|1&4Br>cM;GKzzKdm zLnOgl@A#L;V0t7bCU~}(ZNdqxmHzyPrp$>ZgD`xA-#myPSJf`l>!2RlTCI;NN$0U6 z^&Km-MrpF{Zb+;?znmVPUHSza0aIT&b{w+|Ii?*e&wA=`OF*9lK#9|C!dC=`UBnA=XAofO70eNopcP)CT_u{$T58 z;AWR%V01;L!@?7zQ({0?Rw z+}(aL^}ay93__Srm&9<^RlC(0I6Rky?@cAN0+? zKNz08HV0~A(`5Da_m}65i|dE;!j(KGk?AeR$eY?eR-QC#ly;s&URusUOchJA{7ACV z_!BNJkq6}+{R{hi&$!N;9)tfhfWb&Js`8)E#l~n*+cs~08w&RctMl~pe8hO(r9+tB zhDn=e=qrw%QTKMz_OrmH^Y$(0f&5*OmtFClhUZ8Jt_aNs+mya7IZoadq}}(Yp0XVSCR=wcz#86Ap~CP;%c}AFjZdt6 zMRgisnl?F?WxGT1v@%&n3{IlA{eRJAIy9jj-sf}GeiRyp{M=4Ko;}Tekr^)GY#XB$ zQADpOJQtvyH~#%S)IV6z?REhJ;G&e(nLQPghx;v?`bM;e&#G5dv3=LS6=nF#;umP9 z+w5yvsDgciIcho=`tNnmF%@dxS-q#_7X0kV%Pser4h%Z|Ln`8Vu?odSA12+Ub+Lj$ zf=!+AG4ohc%$&8w6N#Ahu)%9qoU%NoY(y-hs1uaWF8p=&eqE(e-B&Ed`i~pQ1Kmbm z98UDg1S zdD!>-(2s~;!CBA_mO_m`2ticM7XhK(k5q1ipx(b^k4DaV()*kS8@{{$^c-ckqieU) zEDh)X!fyxz4Z0<~Db5~1)lvThSq6!;!Blo6Jm40PPiAjm?uyz!jr7VX5m{HC%rUrp z^YKnMzdM+KN-R)zJw_1_e!_kWr6CwB#=z+WG{m4s>PD$*iE5H^q!R3R{))_BQ5s3U zFrXA83_QK7{6-S?K5=6kHjn^$oPqtafV=#Y@!4D_b7-np6&@`7R;nI?mu%YD3`(Q+ zM0E6qMh<1+HCu+cmW_aJ)|kAjjFAT;O>F{u4LF2fyHEW0wA?3_hHR&R=H`c-=( zy;3isTY0Fu@mclWs+E_*FyU_82=q7VF+Ij?+}(i#qsM3$_QSn9q89vvL-?NbHqRxHlQn2`WM~W~RL!kqRc**^<#Y{L)ub*G7^Cb*#@!4mlRH!O#BZdI-`wbd z5kj-sQuoLmG8>BboCoJsS>3k1A>D6V=iiZZUpy-(G3X>JrJA@6;8^bB_)eS}R zid`d_5S>iXMnMH1zp$q!UPS@%!(yA-+KN8(V5Np2qM16~%XPgoQ~UKC%Xs^>d-FcSrFKcDfEh2I{7p(JCV)l0((hjlUwUWUpEi-9PQc-8(Y}Xz9sL zr#Rayo3D6LY>Bji%71=PJ(qW&S^Idj7Z*ePj6e6aYC>85xnId^h3!?P0PlkS4;L)d z4zm!sYI>9xO6ylMV@EyqFSik$I-p4)u+4Cia8bPSO;~X$>u~E5E0b=h?bRoJ_ zbd?`E4B$I-&NK-r>8Vw}+q-r?%#8p=vJTvuWjdb^lq;yX`2e_LSmZwg@RU1(o5NxS zBG;^U|A2;YA2&|e0gBmGx7bFxn6SJ!&UZMC5xX$G>H+?I;V8$PK7&c zAx$g?0&A|a0@=@z7ugpB*z2Mmf6@wO$MocLTbMfx*43&89(G;?1}7{8WUooNUt-tg z6V6@ZK;s>$CkyW=$=xwQsq&pS`Nz0!aIT4+8kqYJ@QtD!K9Y}^_gryjY#v3;sV9;z z&Ay%J_-k|i!qp#z1kZsf_j4D7{$ic=4h_-N2CcxtJId~n=@ieAR%km>HR|Mo=8~!1 za%)zx<8&~nifdF~%1iLN>aMyw*3D;EC*Aib>(RlFXu_)@8KnxzlkIJ4yTQjw5YX%zQ@icgP6De#CE)^t}V;Mc6IQ$_CvINO@l1S|2DdP3@HC&~S=g@ZRd%)M&(}>^bj$hYclO9eDR;owqm;9U*RL+lPkzS}@rE)heS?8MbA0ht zXYeG&8LwtWWOtC2W4tZUSurqYj}b!Iq%#Cn-bW1@Zd08|xHZ0;vEPOFe4Oy}HY^*s zZS&@0iyq$Ctb)cLGnE&OU94u$h8)Wr)*p-Tgw8riUf~0D{9lR34b!|l`Ec>Jo&$c~ zu|FfbMr{yG52M|$e-Tx zNx>+<3R(5-L-zcNVKH>W8~iS)s1M_LNkifHQUAmS8QBH>rtJUcX8+HD{{O&YHKr36 z%v-+V-cg&_y{Az2;7Cg@ikhsO%awnd@(CXGuUmF zE2lrla?JYDXjlj$8d+#w1d6r&jhqN9BA`TNHh*q^r!&N@-J2l?2{X^hXdX)Jm3&3( zUmO;QT#c_<83(E|Nx#vA#kdPj02la58p``%_Iy@O$pu}~rON?Vk@; zG1r%Wq~*E0Ml8~4m?zF9FBko2SXyy&b{50mT(Ex&e|as*C;J??x-kM4E;8Wcl=76D zDdjcI$F`OpJ~Ga>^~fV*X~vAw$2bvmDq4(*1BV^!0qOqQ9IYKBke|x{OM&#KH@|LH z$WCkRJoiebIcHgJp#xuFewu4d$pTF>qw*GKc{GR>2wqUM8%2ifx zP-Nr8R;TSw#ok9eW+Iv)Y39qTSOxl6b%U>X9|@YzgIC{|dkrV4+aiKfHZO~|-c$5! zhZ)hWmO3U|))$^5|82oDeTi0w?)b&j=kY(UjF9xA&e{T_4cD6fE-9P1ThXWY6L9^O zOfA!bSCP=CyX!F3!Ht-kDss7J9LLz3lT`oj)X9s)E&kda%*mpEncPNS$ZI#G9$BZW ztp;{W)%;*83nY!K^W^`^5VdcRQ=j$-$*nP|w(M3ZJr?^L#w3>NJVH{a9AB>E>SCPr zD~6wGQ{SB#w$-Kix4u52(0;*`JG#2D^DvX{5Nr8*~1Sy?!7}9LyQxm~l35{C4?bjWT zY)R=^CuCuNm2HR;dS~+uk)&&~UY!JctjQN&;a+n1wjgt#O_xrJ$>MGa9e3<@i2)4f z+k@PG6p+;E0jq6$Lgl4GL<2DAQ=tYc+Mjrz-T{R3yS#MHVO|;^HhLX&r8=5{bzkY9K||a=W{cz?M8Nk zQiT4QgywfG8~wdcd?u4ZLozVflD~Vgn&+qnO;QG)EaksVnYGHV-Q^x=#^5~6gCBdh zXfyNG>M@Bs)%g*-uX4Usc29KpeYr31r99)#4LvE3Jn8^pZM<#VmyhYBud$IU%U52p zB$u|SRq<)yN!D+Fvd>C)v{B4TS;*aLk}s&h`y=E~A80LQ1UG=g=%Nt6Ios;ch&+0e z-O=ehmN<+1tvzGZ;iEMKK9Z_hFv2uPWLiYA3&A zaUe8%5$}w}r)6^K7-tdBA1v=8*nJV&HDrDDQXinTSm(oMwyC~-Rc;c}+^S|V$Wi*| z?JQqkV-vg-f#zrV>xk*4GoLDiVc$P6qOYoW8Hy1hTRt;jx(&)*h7o6%x2cvsf_8n0 z>_Y^taaUyXxfgIXphJZ;iypn{l0q=ZnSTL{o2GYSMe9vA2|_2@Bkdk1ZZxc#9a208 zwYvvSi<4C4yyL_fF?rXd00Z;yd+c))%GKgWDQm^bo_$LKbB_R*vzwTEY zW37hsRA-xb^}MpFA&?KAFSmTBT1L=2eZgc?7+K)NOZ^jQhRhvGj2WYbSvnPr`l37T zbm_QuUtaSB{J<7G1~24xHv(XE0+MwwfEonH!16YkvI2M@o`r!ekM&=zJ9SVrLttc| z{HiI8;5LJ{?OgQjWK5D9K)xe^^JSWE4mh_8C3`^2F%LS|Zgoe6(Q3Im`QU*Zv!?5} z$$NTQr&hFKxM#?;@Q7@|Q2V&+?il&`BVPP>Gb6ny1dz=>U`Q@hqL}9B~cvx zLPqL5p>v0qFXS^!NrY{ukna{PfC=(t#mZW7U{@f`u-8WtuI8f&BObPtz%zRQuG|YE z+~d!im7o00r@Cp;1-NaoaA0B#_<$1pyr{T7cOy5?zo7)M{NK~=msfeoxzG8?b7G&0 zFlFv&__+g|u*1z&?c0S$S&t4K?5$sT8K}}GHzru`bZ38RDwdyVMZs1H&T<4XX>;*> z62KO3R+^Y34F$6rW=ke%a(Spm(&IAP{2WdrSKplY`hFXiBDiGv=P{$k_mWwHYAL@c zz1HrN9k9Z^@1}HMt_~A@S4?7{!`=!LKMjA>wit!wJEIxZIVF9xuJoPcCrp*>5pA#f z(2J%)aMP}3EblDSH^Jd`Lx_ZO7C|ZJWI4L6nHl^mY~6>xB1MX9q1bGnHEp(dMO?3! zXXfBL5q*i!W{iLklsn8m7Nudx2aeR*hUt+TIJUfMgIL~B<#rkVgi**jRXeCHjX$(& zl)cN%Q0%N(F9O^D20;quvlr)p7KV@MhSs>->>{bZx8d-LE;IWb_3FaDSFJrA%~3c) z{_n?h(O2(BMlHkqJncA`N2)4E`MpqtK80gh8goA;lwkjy?0QU-%;c-vOD^P`y4M23 zgf*|$)^Up$6yNqgjuYP;8Z8FrYt`F@nr4qIvPW#wEFe*_2HUl19CIaQz32{n=j=@H zHfXEr!5`O`j)~H}Z#?Qi$JDJGIgise$Axy%CfQ$~Sp+MMbjXCi)=iO>gWlFS$6H;B zcSd#}Tb}matkgLo9QCo9ExncWkXRJBicNe30f04c5c~I(89$L465v zkSUO9u}ST5HpfPGgvrLXZ)QK$7u%U(aALec`r1jNIXGnfBx)==dF3#)eOx&p`MlUr z%Gj$cBbB>X)NKG{Ms9v8`H-zJe(7w5WccxuSHmx>K;HhB0B5wawkAZY+T*w}%M@6c zW0KT4YFP5-+j9Sp97z@(+;=mF9}C@=3cG1S{?IMRoC6#k?*$Yync)D-ixTW=hh578 z01`KHE(nyG8A!O0+^W(LnYlow41_>6Wrs#DacHOT3dnKPg! z>XGZw?Fdc*$0&L&I$b0ZjqI>nMKySM$1R~p&aqCQeJz`lv)<5}t;6-r!{p?|t8QVq z+Sv0BB(nTT&LSI6*;U|;l}dD<6qN2oAi`2F=9f}Ni7BdTA+4J)Ju zO8G(f=o(@~Z+NBF4AKfN7n11+i;^$&E)c>WOqo4Oex>cz%@LZdlseE5OzOR^=zS9Y z$)@pMRC#k&n0BOeD%0f|@I*(fw%%_`}A??Da&`lZ$;Yj)4KYt`K@D!XqF^X^}qC~Cy~ zqSNj(u!?3VrUNwEUQZ#8l|bFn1meuqdox}&2ClcAq$#;GzK5J4Kdg))D~50i1&M?> zv&8MM>;~?9VNg>Z<<&sjAD$~A5&d#Eg+apbYL0&G9Plu=IqRx8LH=@%4ez;MIYPhT zNc_dMgwNBYIe5Z}mf(z7;!giph5dO~Abs?b>3-XA}hn8YC#Kd56WeT-wj}56$Nf08wjY z8&z1HG0sSZ;{l3mYd=d_l_RMuPy~o%mDmrABVJWs#jiMixz{`&rn#8Qu=p4~*JyEi zUeY}&M0j6(>Rw!;_f<^KtuZj>5-;WHs0WDwBown{Re6Dq_}CF8GfMLMe27#;fW#U~GaYgv7F9)QU>3)r0z|2j@)8#W zY(+?ypCmUb+e};&4P0S#$MOi3E8Zww39Ne!wGm~kR#5SMr?PFjTl6$icO3Q#e9cHM zJ@!?*ci&>)1tDRp`XOgWAmDA^u+Y7uKy;6Dytz!>se~iWSIF}P9)U_*XoQ*tNC}WGbLS`cR4?< zWS9Jb=mSz)wL;Uoi(?>tiCPbDZcT$LL>JBK*%utyNhq?98T=|6fG@Oe0fW>XS^lkQ zcc&H})2vFY0y9aw$bz~9RsndRj1(0MqF`Xbg=n^seh-D1_JXz!AouNWRwLePKY>ET zDp=9*2pV$1lsWKf)pt{0El8D}`Crh4T$8$fmmnV!H##XX@!{EgKrmz(P}hv$a_O*#t| z$PskK#n$4P6bQhif-c_`6F3N9w8VIUw0woU`8bmEK~vAk&_sF>vkSbI z>!n4|l=k#}sj>(I2@$|FyY?#Eqj;k65*9gHX?cT}SW;(kVtlN3oZss$d~ca@9IRDU zqZD<)3j1CU`(Zur6XU=q425{|C%wDjT}-*k%3;IzJ{YLehr9Cg+)k^%II5R^5PI}B zlW32-HP{yD276cOYH)u#Z~*kvbGUnR2kc~odz34RPON6vGx(!lrg3v=0V4WUBRVHK zaK(MRoU4EzAm1B{`=&J^!jHK4ed>NK*#kCYg%eZwgSE)SAp5mOJA|&|k-tYKGLY!+ zG4iU83TuOLQ^s07EaQ9(PdGgjZW~TQ`Q)HE#W^M=n`TpDw2{4sApLqy$!wZefDbU&2W?8Ad^YK7@N}+T_LnS-l&IaZJ&}5FN zG<4+B;L+81F9*d5_`Kq2%|+NHF-I(GiL~sddPc4&1j~IPx8D_{(FL&96tFggK%w>hkk5TIA(wOBh6JfKHH$X_Dc<= z74J9ILdO!V-jG|?GJZMUfaPZhP9yaT z*KT$MMQR6y4PGn8b5+S{mgwCVA)sJ82G0d>IB+w8Oh3^xi7dgNNdU30PMiO2XRvQ! zf6~Bcq8V$@fv>q^CHmsq&eZ$<1&QbUSCk^Xkh8<4OYN1qZ!lb_2ozw3+F7Yl*j`*% z-X_$DZcNe3hFb&mY)T~uVVDXr3IVaxP=NlR=ZC&Pr!0!gO5;a{u|eMW8q7NwrlI0h zJJX@G9&Ur_>P-P;vtZkOct#$3+P-V&$Zs63oY?Vs1pJ=>GP%xaQN64mHI<+gz(l%O zS5DA_UzeJzjNpv#5ugXj27mg zye|h8U?!+jIt-uC1mQkSS$XDYsbsLBgkKkGNlZl0_t}+1nu$|}?}-c}aHIo*t4CV! zj9FP)SR*z#7Sf({bLu$d3mi?=C?1RwUn*hr(inU4mlSL}SYf@%7(ZtqKID?yiZPC^4j(&oZ9f$%?WR8u zBfNX`7%H60RA~UOx|rl=I1m70==@nu#Kwj%EeD)Vyg0JcERzDUzRxOzJcuGZ#I{{YK-wC<2L)9|B zSKB`|!v6L}Jo4+I`nl3VZfQUH-Xu{8)F8R65GO4%|4qi{+KOhn+H5Ae;eerMi*Oy6lb=?;-*QPoItIwqVv0Si^(F|K%2<8XJEOm&srOlA3 z;RHeuudYYs{=@S~{th*M!s_!Ng2|HIGVeM^Il^fPDE&$^Zk_IJM<(ROgONs!H^w*d z{rYc>B=JY=+KkrOyRAnB`6qYQEAMb$smjYwBZ=nTp&lp^=w_GxKJ=QqoKO0MQU&84 zptRRD=@cn$iuzbVn!Od9dxl@47lJ=!iH~OFtS9rgen4qC!`^4v>g~W83-@~_g=}%vl>ScpJEJl!eiR`|ew4$2!$4)60e7Q0 zypZMOdzc!O=?8tBV~>n?!O`E51*l7OD3VX8VlfbO?8G-&=9E*nLo=+_%y6DekKMIp z_ttV8wqiv>NRQB!aeiNeP(N>eO_wzbA83_P{6algtn4%Pj=a}^&1>KbvJzIiIkFfI z$qea>*hTAv7o7R=WXBpvs_6F(Uu32DXT#_SE#y|CAV#$Nef!FBTV;;Og!QJ=DPfh<7U`H_$hWRv}1nhO7j9f!aWEhqCJj z$(X6#8PngTNnAjs?S53|9KbaPvzShOer|3$#H)}{e&iLJV9Z~_WIi|K(N4}Y1DB>)#fZx97^^f-Bzdv&HMYf$Sr@}b={L?>Ic=z8zQxzGJ?rj zar**G1_dDh@m-Igzw7dD#^SgTMMGOF6CY5V{r!*ObukFS0un_ooN3M@z3a+ zXA(dXT!2)WnubmLluBeoN{XwN&El!gzWf3^v6dBdJ&pbEeKx z8bnoG%2mmGblX&x{rc$h7^B>NIe+A>xb70&SnMFpOAP2w)ZBn^EgyJUJMCv{eCPBu zoAcY^LJ_d|A)s*b)mt+xTX2o|n6;R+x82d~l$i0#VYf<#$&H-DHvu(+s~^04GcZbn zCC|{U&Ww&&Au=e&X)KS%GN@RU-G^bpvx!FzTcI-^4E84{K_+tgyg*7aGWnyCL*S4g zY18aCl365cV$#S9mrjrps3WMQQ;w9nV5(>b--DkoQ zPhOk%*TZpg2$@ka9D%Ss*`?-%&o&Ci(j#ba3Q(%nRe+DF!hc&g?uUNE13Cva0dlal zLnu);HQ=d)|3KkkVoav=VI-t`_7;`V^}AOXa_W_^cgMUX;XsqBtSgy36vBIn5*X%f zlHx+O_%c4i$Xx8h`zo{VlG$1JeAwm=GGlnDHcC?0&}){4iQoGMKSCvXgwk= zQ?YLL3~1)rQ-?hzao)SvvC0bulhbKr$qn+}#`>HV9J~`XWcFFnmssPi}sGE^fQI z@KaHiR)eV!=$G<3-4S+&Ze+^$u>9zhsws%F`Q2s7IH^GOLjCr7%I6T+-1_7bF?qmh zXRqW>7Qm3S!rbq+b)xMb>^;mJ-SJ+8k_%jRrtkm=>zBkkbDaO{TLj_-=8`P%{NhD= zfBtZY$>}?hMj*hoOzg`BuGRIWLwSwj#l#2DIWHefFmXq@%A(BU^25vT`X;Qw_6Z4A zvmM|$4Suz?vva%xIzS}P{J(BYmNj$ve$IffFfb zW@-N+;^ibtT-L61`b_tz*<3Zk=ZBDdQ3i2)cC*W~{c)woE3S4FK#;GH#q|KTq_TJG z$dPV*sx*~NeFR{n=|;Hss!^HJcc~+oOMUum+o&=V<60_(J}s~aRAZV@A1Iz@0O)KR zxJM`y^0jaJGop^o+_jt#WDI@zK;4^{5#E~cd}plKJg73huX$-nH3l)728A%=lU)+k zs@Mv=YyQbMi*M{KRxvr+T@5+v8^ko5#+#Yw%*$|In^;8Ep6M&7>cz~+@KJ1o&Q^*z zR8neOFpsgzT-@Z#FQwNXv7UE6oavr-4z(7u3ue4h`VJc%1Ji5>DBlh~{)U@gE$l`L zu;yj-E^=XJ41b^DigyWHfp@KW5_GW~d$GW}6>#yP`=`=)Uj-}mu`e9mF=Z8uA)t6Y zfEBlON{0nhvRHl_h3t8!reRDuIrGwf-MfAG5y^h=*x=42W&m~CL<>=1nIXKz`-!x1sx!YF%|ltSuM*_7NW)Z)D)yDi ztFP3N;FI@2@&K#=X20L~sv|AcSlM}@L9*>BAQnb&qt`*)XI^ZI)gf2;Bz;A zV|yz7^@bxJmDXH~$&_E#=38e7;_g=Mp|&=RU$1t%%AnKXSTZUEas9hClJG2ktUExE<5mj^b{IP$H`|*;DLH^1GUxM=d^ja|i_A-}txH(pVQ@Ja zg{n`hZ6?<$3-T){^Q;cJ*^I;V_Q&Q<$T}K6f)(k2w4y+*nSPd#;5>QX0&s>a3D7yM zd?X1$WHN58{WOL8+eR-#ckG>6nXB^{e`;QYTn)%Pmm+bRKqkyL56D`gmch_*+ z#N(F-4%^=OWBlzn-O7N26zgw4x-Xe&dgfFO5&*RH)(9^>TBED zML-F+!|>H|m(C7q*h`g2T0?*19Ylb(eQvlf^26Bq_}!KjqFl}vc$ky^rCf;AKj5t{ zLw-j0pmvTJJvI{kD!kV^tG>gGf3?dlpeA~Cv{z~T>_zqbJALjWEZAj5g0;5v+^sOI zIna*5ThT0Kup5VV!ZsW}wo!lMv)Cp#j!K^2<=TiKY{ROyzpi|nI#9d-4!vTX(xcf?Ryq2|Zt+UqH^qw^x%^|zPXfa8JLa79FIauQoZ zHa{m{l1bi&#Mpg?!UZrIwo?a(1<$eN-TGk|Z!V|*J!hcix%C&IGNQb5D%Np-;-+?G zT?_K`tI;<8Vmvk}$`&6*W|Q4h=Z#BfR&6$y5bke1@PHN_{sU3 zDhCs(y{j-x19}WS5bC9M+SbFL^lmv9{L5pZX^3_CG1E?&ox3Y=OALo$VnigK@YSs6 z-D96Z)`;(C@vJ006r+(A3s}B)f+7NTJ#BdgN(36Hrz6WmFSUYGTJRg&1cw8m<)Vqz{IX$cV0O#)X)){B6%d{vP`~%A41G@vxwN#ELR*v6E zk{!&9E6(3=!(H|Y3)enShXM6{!|Tz02t$K#r-lxpJKI=K^Z+e`sbIURh^smaNh!x} zdfQWGIu^_L7aC$RwSj$k!!oWwUtcqU6Mj_Cc>}msB6s3=0ZmT-+hasiMHwJq_xv>D zKwcD6DDEZn?NkpR$JFFY^i|`$uA45CJ`#=}nWV@9IRT#<6Z54CSYKsY_aI7vOs-Sa z?S&vYKT`w0DO4X7@XX*~Osb5K;=RLQp(60)1n5G!-_hn<0VQOA_YaDxq*M5^qS{QW z*QrH!>WQkya?lnZ(=r*Y1t8toVUi*3K?-7_dbR%RU6A=jYf+Y5__=YBO(?i&XI5ZZ zo<+*=RQ{u(V=t{_?N~2fKTEWrbrD{1Vds9U{kUU(mDyTMgAg2Ykkc`zi{6jFUlMPJ z7{{PS3%Mk>A}@{}-No)i(?mocbdVO6kcjBK>nxT_$5dN`M^boc`UpCt59I`r8_0Nn zg}EHZgh7w$WO2Fk5n2ux9@;1#78XiQmSPwOGWBvND5o$;lASSk;-_fx%SxMved1@< zUSN_fKMm0IPoqtg`Km_0bhR!2lm^1ERka&#jF^37UOgRs&}gVHlR6f|2v*`yERHS? z`SYySgn4mYb->SgoL)(UE$`JlZECG4WBZG3Fwsgi?mFyw_OeHE-m_C|L^CebFK|8L zj8GQLFre@`^F9qG*?BeOFl0=NfyU6DO|Mt5eH75KqAdp%*uuS``0i1s$!+7G3Tzv~ zcmZ+VR|63q={BM48Jl-#Q#ypz(+Rz*ullP^XDn|A;cXFrQvUM}5;9VdV}-dDd;jV~ zFj)`sXGFi~gi2*4f8coEx9_#3(9hbZi9W$DoT%#R;{yt$JQe`GdNFs}evdlFrGer) zvN~dR3ymr1TTd9haf=*ySl;RWHl4dY?;nlo9>Sk93;C?3XV$dVSJ1pU4TDXC2iO0s zi3eOW<+cMkkEwgy;#K2n0<_s7{BXv*2~+VK^h{N_;_logTthx9yr*V&#%;4cY0|Js ztAAb=p#cYulM!eaM1O{U6vVW!e7=2=nf;!2f}9K6^@@uG92AwPIf$*=*9k}t1`gpc z1;mts&+;xHDRQ$;8{~BE)Q56R*m=m?y9PLmuZc3lz+b$w@# zA?@hS{>o;dJ3KEXzy8#nhqB8wk4FmPgQ6RiChT}SZ0M3hiOX)2OZ(=yG@j|b*u9HI zof}sWDP~#RBi*=B8ob76m@qY5(yPEFSHggqo|}gf!MU7IP=20dO16zACV}- z5s60m_pQdK`c21<(pF~E3(!>6gQ!VNf`jrJhW4(|`f!*IaZN=*IZ?5HFl!2&7g4Si zc)!;%Kn-H0ki3!PJifgcdmt@@#}o zY7mBlqwb=BoR`_^pi2(f+R@*vFaSAz7;bk~dv<{W8wY7Wx`B5%Yw;kLTc_ozT0l+z zB+Z#}-S7Y}4zV3@S|}*KQw|>QJvEd^Ocd9F6fIM#EIZX~Wq?>FN&YWQlj0GwN#PF2K%;$WeNVSZDSypWM5ng${{nYF zh`)UonE{4Oa7y@KdU_7Ud%};&z%F%edOw+Z^}Di`tAy~4z&7&Z-o{}Yi+KO5O!req|hcP2V){4+7-l-v7u2Kr5)X zVE21)4r3i)a2@#gkJ1S5g&YVu@CW9A00419<=DL{XcEySf?rmUh?S=`f(%jSm$FD( z0LY!(1%O2!v*bi76*pt zovaF2Eh1|j?nlMRR9&$MZgt_d151af&a%c!N3Z~)ApJ|Qv7uP(toEW>002M$NklTNn*unz@{y%gQhrDce0?fpTlof$2icChnJk1j9rKl0Jp3SVU=I}C^HuvK&fxw_{xH8Y0LrTLZQds==03K3 zcdrleABk7SiC5>?_yDUZ{A&RKI*RlsS$*_=yl*9I?q*VNrRj%cdpeLir$H9s0qn`@!Xs7Gnn!5S-m{!J&nN+5##&)F8kg31b@jnI^VjM5)7 zYqums+Lx?Bia~p`#2*C@@Xj*>jI{u%o5=TVKneOCAI$FTp9yV6soo;|a5c&A&*P8y zo7thNa%M#9Ik#Xj99poy?Ed^=z}DBRJJD~lQ+WbW8a62&UbuvIv5*5f?&bMQ{R$=- zRpJA8yzItHJmxMGV3nTEW?|~z9B#4!`T+0@eyevl)>Ha6d}aEKmuq{yq%=aS-EY2V6^)g)Zbk$bpap z|Cj?Zau!#O2owP=YqUgA%AO$+WWAWM5&`u#;@?GJd5mq#pj$EUBUss6Gg$<^|1>KT z01_OlV2eV?xyyb}zG5Fe%jzR1(y(J{S1p3JtE%UtIGv~UGct9UQ`cE2+u?I>H(KY2qRBcc(nk)6EI%WW6w5>6p47LT zXe8zV+gRdWVLUI2FPHJNltW-u7E}OHASM6Nm{wv*JmG~nxklj{CyFx9r~ynv{zfx4 zmSZ3R6#g)#KA=`VN`*$YI;q*F9Oq;25kPsk+KGk3%3r3g^-+ZOJsjmoy6!8f4T&KD z*!8L7t4>FMdyzL%w=i|KtFt8i4pTd0Hh#Q?F@4E-rOQiE?A0gNEJ(b~lv>a1SL3`d zUXW#HjUe7$Ro}=I(#EFgYoen86jx@C&aDSuU$th~j|%W{AC_`dz3SRb2CK5gsZV9? zQrYLpYrGdwxU0Og#Zsg%)jjntq`Nt?N)7_8I4<#FYB~Vz_T1CrS1Hxv`g*+zJ}3Y6 ztftm#p5L=(WoiaM@PwSRbKzU1uV+RpDc88jPFXi&2|LVd5?ccRyDRx^q!rg4-I~~3 z$~UB_Zs9djkB*hsi0XQ+XjC!$DS9?1H@+Wy%H!LMjp5r>aBd&2=hOP69#3N;i+1Tv z^-q;{=#KMFmGhUl>zMkU&wtuoYN~sg0FIhjX;&;^-c3L%039ou_LufX>)s^wp}R9> zyNGmJvS)TS?em+gMltwSab4p4H2uSyahI>e)ViWAu7eGC|;nccAq z)Ls5?!cSLy z>p7$SB-+^_`LAZt?sBh*?qom1^G{%p>_B!-^t(&@uS%r48X$Kr11|VY+c3wt(57YB zA}86K%`K`vLn+!J_Qd+8-|R~&lK+mx7e34GU+Islu;5BOSrM8dpDXleVkqE#FX`9% z6Md_>{~a;qeR=dE;j3)X(NOA3`XuYW7D+qS7szp@(2MjwB7e5Ko^K3|7|B( z4_(NCkOLtH{xJtKD?LO3nLc?ee?NJ&EPgI4ObqaQkp7{hf}*#E zu(TaP&7q4^NR=PtMnvn5z$z%CiD2#pziMd>YE2ZI2yH39TuVB#qDI0k)Pr32)SZkV zAH+lrK+cYlgN8izm9x9qhF``6&5y+E#wkk(d_O`d0J6>plnRSa{(X^KbPEguqZ6Ns zqNx|ndp~{z7N4hb?~0D4{GTS)&pb7OB?`IFQUIGA(Og@^XKE0W~CBQdU z>U4GvBYtO2J%i6>=T_~VXoensz3kP>9OAv>9^m%JQZOQ~QT7`Em#F&yM?|XPvC&>~ z9#!Qi z8JuNLgV8-*RHfI;O}XQ!-(IN-|08h5vrY@=S@PA%8ZYg^-@`wFX;xXgrm>YMrlEI{ z_M%(qD^2^1GI5O}X9{VPSgu&1w1-jHcLI+*o_~ueFcdphnYeBT{T?+g?V*QmApBvc zTVM4m^gdN)y$0S{;|}yj!oQEbR{m4ux09{(aN;ikrqSMby1B7v5p3E6j}poVfGhY0 z{HQXPu3)W}^eU+sv3Ei_uYQ_-m+Lv|J2?l<@8);I8YRGGl}SHJ%Of4XCsStdnbI90 z2b1#c*1lMUmG_l5$LmNs`FsNEPWW`Ne%XRQrSH@fr>NiPV|JxCm~x!twih5ByU~77 zcZE)mE{>4Xyh%Chvf)1m{;tHftT?>2g};{OeqZ`*pq%n9rrltH<$D3`>FdglS`BFN z%5CAD#B{9EGC<=!YKlL{lzb?*D*6X7)ivQUuBE(H03-mQZpd*jjk$i0Z)?(DD8)*7 z&S_^4l{JXoF?^h!exX)dQ$+sgwR))mYTv?fslOYkIe?qfi6eq4ul-|r5r$P{SD_L&N6p0|HVEPBiixuSczK7YKCukA#zGJP%K&i6JuDQ)WmKa)Bk^mDVwNTKC<0cx z$o@b9l!XRRy@{Hyp&X66oIK4LL9tY5Ma+T2q(|$=b5=3UUz6Ne9oO4e<6Y*ZSd#Z z&!U52f5XI~)l-R=^6JXLytx-TPcX@%vQM*m_+NlqY)&3J5Fl3^nPlCH;(e`R@5()S ze|GZI%5%u?GwDwf7xVZ;fDH;fei<9}QJ9MqJ|jpgad=oNa80E^V{s$)nto;g^uj9Q?5F+H^2SUP3`OfWNew*Zt-#MnjJNWFMpv(N4a`1Pv0 zMy5jfl++0Z5On8ho9B1J@*)cUdM3Nu06;#Dyy8$_m1m{D@LKXZT{fbsKk>FX*7+?m zmM~dN4WOlN<)~2-HlB==I`5}3&=6q#CTt872|4hG z;eh^|B@MPY3+NC*6~uB7X(DB^5X+(qr^px98j>{K+1l>I)8+ey!V58DChv77E017Hf7k|bWF%=%guvqy{VV^N(d zXKds?Q8e)_&M+CXM)%v&%puWW>5;=$GF{m%tbuZ`^y`t><9Nh-3#B zfK`2=o0e~}*(4+a*_Q5iacr`>!K(d-kf(r`~xgvqpIqs0V?i;>6_E!Yv)k~EoJv!)0O#aq5!Md z!PW{+SW=BwN79Bq2#-!Hoq-SIWKRgaP1YPY|9`bNL0hijgvt}-80mM=+CFVtnJ z0W!U?xdOBRPMME(_XKcL4}1jsboQyK)NJ=W|2yti@VwOTAskkMg7C zMSxXi&mjrmCB7_nmv*)mzX{r!_Syt`Hf!}xhswEFFq>ObAc?c|YJ{~xn5oC3$i!dzBRHC zJ`{O*GNo@lukV(FT$7gDNp@L!M`JSsIAh@}Gx)@kkLPBCy}e|VmU4)lClTMCvYO?+PSRTRCd~`PtRAew<7n; z<^lUnYXP=!`E*bZ1DN}<#9sJSO6e=1Q(4lE+^{Q{4BVOPyIGYcK>Hq6;fcGO8kFn6 z#Y}F4tKW<|zgbFQLLmo24*d6VK)}T>aWaJ=3M-DHK%gRIGY2Ay1xV5mHBtzbQ>4~I zSF@~D07|g^yb0=~oa-YI!y=Vrg+^njoNq0PMb@C8$XWU-Ps^IG7AC#SF^i(G1P&?o z050aYxXc=_a*naQgKX_>AEMtN+P>Ch{%{mW1M3O{cuv*dh@emOOFtp|_PoK~AU+$y z+#BO=0a7Da)71dl8Ff_+l~a@?eK{6Fz+{YEWfXO_xt4Fhi2^(+1N?Np{#?ovJ2Ucv z06MF)-eStRSS>^30}$av9`O%k^ExGSp0>t{<<8m1+82`?c&Rrz zfDy$Psjp}FPr}y0(J!5AP?#OPiF|ZKInDGLviqy%`}BJGM9q?3TR*Qk1$>)yTFKN} z$AbDh=fKywEjKKxngU-AjPywEkKS%=PmI*X;xxQkC-23AQ8zh0wF#WLOCu*wEFA~b zyp{rhu5MU!zbLS#??i`LPl1cC-h5=~c=9>eeNq4u6HnK$uvLIu`EG2UZM@SC_#uxT%rvp9kUVrzDF@AN1D*sPid zu_39}qwDM!e}(dua1bj14<%bI+yA2%GW9c<<0Ed`cx&ZM-q**vU)oFM^GV-*hWJyn z?vp7~eTO~48A$!U$JEyKGxfQSn>VJa`1jVciI7N$kciVfOuN8Klp|s%)V3POChjjp!z=I5cE8trCfdDFLXIc1sr~oo)FY-}o>JxAn zk==g`OP&>o?(gTH?qABa9OJPJx}LiLK7EDmC>CD#A@@t4`f}3tf!J_W@#$HUqKA^t z4({~{+I{}$*hLP2j}zA$aj=1M3hf7E5r6D}$P3Pq^ph>=w>+-(xB7ZgRC)w!x_HiB z%;I&@V};=-0<`o>wpSEUoY|RAgA#!7rnqQ1odxI2J^ zN#dPuN93E2(bw!LvN+p`TjjD#(0;=*pN?^cSqJDg0H>RhH7UxSdp8yU_-Reky*2#h z*n#@2UQsporZ03`D7Eb4(hubmH2M*1KWhd{;kFhY62FJ*5`COp?=-gH>ex8+*Higk z48XHKQM$460RYa+S=s^s6!(gavrtr9%d4(P!lwrNaOOHdqj2w7S1E3~yEk6ezT`d= zYvZISer0i=>}lw?OL9gS0irD@$N7r_N z|DD)nO1ar$FiRA&0bCyMJDv%s8m`yT4n=X_3S2@#d$XB>CExA{>UT5Yva+WY^lJ5h z&-?WrI>!1Ul~o+;6nzo?yzf8dpF=*MPdKTUi5IxGz>EkN-(i6g0H5iF((Z#6pNT2$ zU0Qz7?*w*9y9@fQ050io5>7n-Bt9l}GS_@_kZE~2o@A>3G*q*B1HNEKJ4Y;qUQ7QC zA7CAM#5zQd`6#$4ZbisX`MhL%cP-`nk<+D4Bm6J%mCiS$+Xg=Lp**Mfqs5Y^=fNT2 z*FZH55JSIUf)0MgzI67L^Xnqbt#i$0l{^-o=MjGp@J`Cx9{45Y-2#jf{+07z$~TSv zDFdr9NCg3uo#|I1k=oN+q#kx>;3UtjBR=&*-;va0m*j3H{&Lb2KFEitBwh(I>93S+ zC+UyaFMFBYg8i8)mGq7kn2CFz_odCR%s<&w@W0@sNTCZk5OUzZl>?jqhrRazx1!kg zeLK#$GK-vZQlcP1Bnk#V5fuRtzAbU_gR^fC?fgB8q~5sGtZ)lq{K* z(~KSNZ%oC#-+A}D-#N#9&%5v3)$@Z|ovN!wRgdxSkqY(&<5#;PuInZ`343MBa%H(; zNz|8?*x!n4IbYf=B~dfMg2ZGeEax(Skp*mofn8?&RGGN%^1kGa5r!omB*Ilv*pn;v z!x-Cg@ji7ei*+a@#r#eLm6Obk*TOGPlS&i8?W}S?C*zH*71uyaX6O_05&|xX_Q@CM zKQ>{@0^Aad;R0s<$p0h%%NhB?7X%<0H3UD5xE5wU@=xO0j-Z3_{vvCiB|TwoRXg$g z8?{~%9pwpYsm>_``3_^b`V@+TrST(tiz`l&^Byc;v8y^4`C3+NpYxUbHT}p%_ap-N z7&3UZyDsj9!$G@)@Y|U``1yvTN zd;vf>XBKFw$mvrBr`7c=`ukyii9{=wTOIV6Bcan5nw9`M14v!T{efA5?*lDq-{Ej^ zcr6;RE5qsWrqFL=Y>&mvD$0&_wmKm9?h3zT0X(!;+7b;7Na`EOHzduYf7|Wj&YK+H ztu+;ZsLjxF#FxnYK>I-?Ms#`SY& zhyWAy1hW^qC&0VRvNgI0j(A7qC5ARGh`#@~VjXbmd+^yrP|DrGYvCxgU)& zdK>5)2)yN)4*<0C#)N2xb%~{5sHc3n5LrQb{u@^w|7C!Th@(+f`@fuI^Uuk|rJw4nl0*rZA zHp_bu-D(?&6%andc<*pujF}HT)a&p7tO-3YqCb>&Z3Na-q7)Cd({%F)?UeUC%KItz zO=XtRUwpF$iN?^WOk#yIoVVzQyiX3-GaoS*wiSM)*7)NC%5CFLlJ^@}`?Zwrhdkfl zy5&Gjseg>xPUwZ<`ZjUGw;y-j6wR*jtMNEzC12McA3}*8{!8e)F7N#7O6^})RcTG7 z6ey*@|8W%P%9ma>7|<`Qb3gbWtz)5yEs{@03-U$ErVV3JkM;a96Gp|CD-l@b62M0E zL}B*J19XIOB>`+alpffb{^xP|LeAuj2mp9lLX-0`&?=q1E6LDw1AMZ%Q^i8?WS=T9 ziV)}*%IB=j-^qu2kJyuO>er&UBL}(uRyqR4M=<_RHrO(oz$+1O;?o6#<*VA4 zXqPV`s*v=w8wO1jSTo}B6#Yx}b-bVyL(>n#%$3m8*eA9Kwu4R+`$jY~-MptY>Z>p@ z@VVBV?i}zvBug`y{q?ZW!)#$pKzE^-R~k)3BVg1w#!I49?Tq%hT8nxbsJBah^{x7o z^jXo?YCnqU1{l_!uWB*rBo>i2hid7fnerajyI~GhjEB`tBA7`<(LS0MUU|y%EX@?-^t=4I0 zBe=SQXC#@Rl=80xkC{($T~l>2qFQm^B1_WUkpE|`wK9_Sj90TnW0CY@;Ag*!CTT-X zwJ?JFWKltGv=i-as0`Il0nkPT4|&I(*DBChD!$aVo<+5!@+zD~UsYv8Q}@N}}S zmdx~?8HTL`T&muf`k@ctMmki*X5KlECzkP60y{mUjpq1dRmpfbkc`5kxP1K->Zw*S zeL#Tr2EX$>XfV%GoB4JacY%W!WFIYfnql}k`_5D- zr@3f43mA_Tz&)8?8Gp{J{694=+Y$e0aNI`9T+CrO=kxwt&Hk&dPgW@Kt}*xM!2X~`FD37htVGCs z{cDpns8AWQWs4ljYn>QijgVw=Auj zb}Rc1ZugA)@%DL(Lk*zEd(PD?QlpgpD0rurcKfdbYSpB@n_`c~1o(E-UQ#W_bFb4+ zd6WBVP-`Lm_#CbRieRCsdAxZF2HQQ-KC`<%MV_bE)gBQ#8iVzhRNCnsW%N>+x8AGt zV~O{uAz9mG9vq-m@?Mn#s!4z5ksbs7!aLB1JG^+54oKZABfzNc;8Ik{s{NA!N^Ga;_o@X3%_%}NM!#~)b`5U1aM1K&VLnXHP`*C zxBm@Q{;Qk#e@T6Upi4*N|B}J}FYM-6vONJLV01lH1n%Wce^FZ_nw#+Y7-9&aD7LIX=oa-WPDItJS&*ACH$w2l@-gS1OyuCj}<^nK-us3nnOwC|^e}1mjfR z<~RZxea%MsV%1`5RS_H|PloT0%!k2Mjt9apy40^dH@GKx?)LaqR$rcT6|q{BxTtTE zuz$cthM4&NUz?3sfsz09oqzeuzmVhk9GA@KCn=v$KEMp4m?7PZzl##7j=S?r1nl&} z@nQNE&Ilh4HABNNH>-QzMFOts=cX0|I7*_i5iM7S=ckhgi1yR3=LB$h^0a-*wFrt zc{NzsWIqy(or*03n2j`_Q4!$!AnhZ)6AI7gFTPPc00rjF1$!dQA9tcMMgW4Zy0HSY zvcyV$oY3seW@(#b+Tpd!y0p`%rq5J2)9$l+5A7%DS=ZOu*u-~2m-35}`vJh}X7&iK z=Q&-KD@C)FbF|>uV(93MjNc_1x|Cg>yVc!{>v@t9e8;?9Oa`lijN=|OC;(0TwVEdQ zv$&YY*&mC+n^NtdESK@KGM%%|X71}_{AL0WW7&FdZ(Xj7Xyrw7So~f5V+lGq`*6V} zF=hU>-K(~h`JtQyg3Eg+X+w)BWnM@Ax{8K!7W&a=H319?neB4EDjHe==&gWPEfd$b z<&%PrLZx)tH=Jn0PV5%|FRHPhgyx3lm%TnP#zX%yHv4OPh5-PVUD{p*u>5^rLAUS* zs%@28#s47m9cWdsJUl*$yytJSzrlG@fQ&2cWqtzdGG!dl^8TS|KFyPOFN|NcFXa9A zS>&yIjqB%GLt;yK??!}^OK7h)U7N0Mp*@Fj&sRBLolI5q+pgriAwGwyp}s40$B;WK z0jw%ngT+K?y=RvZVCY`wJ}*9tv1(Q&NzY-u$l}pExbJP=d}+@nYLW372mGb|?lolo z&xYTevYFjn|F+XNF}||H=9VyvgzlF&aj9@G@BHV>Qjz|jr`_MX^YY@JmutD~+-RT~ zFgPgR=jP>;rDZ7v{+$#M!=C+*Ypns!cn1dVDKZ~42LI!D;rrnqByUl;f2@ZMSjz6kqs~=J}R#&N$D4$h*>M8hmRr1`t0>eC}R+RJF z7ixJGAf^shcS`gRcfX15L-9mCon+|~@0$T7N3Uie3Q{{mFvgC&fw=ueZSq&pRcn)RnRYK_LO7VY?kZ|NX{?b@hiEkdx} zlzXg@e){fnhsJnL&HCwmy)gc&4N`vb(C*A@J=+}wOL%8VLjhW+l5?^Om$UzTv~lcd z~mV8{hgr#P9QXlOj%9P-6Z6O+(Hh~W6Nd)#q@7Lm!5}0Rh zk(L<+S|UFY&dx_AS}J*!r;nH{l9rmo#q{Uxs@)`jOus>Gt|CB;vHB@=TS~l={fo06 zi_e@;p9tflU8lV+K+b5OZXo3%>7e}OeNR!}3rT&GMUa)#DoX;$!nLuU2th?Xqpgxf zc*7^-cgs>TCtKxbMtJV2eFev3^AV^Y<$EIzz_MTTOS>`Gzljq*0oXDXbxj}fg{ z$nouJOltx_x;?Ph_!IziBs(b|jYsYeCH11)0jMW)Qi~b^xGtet;`;Pift}{_)U!!> zQt04mqR-LFa{iQgUVoMLKk3{X?@41ZnIkdzJv=s`XNz z@Snvw|B;G$spp`NPklhv$}^Vf71XBGJ6CTc^gsDgZu!vTOp7s*Iq~+)r>5?AvLsI+ z$J52=gM#sJxdllG@{H&F^F4#PZg~6#;@_04m-1^CUc>nID!#5{D%V#r8>kO5Pi``z z(tp5k5HBIcEyha=I$_AVLVyk_Csrz6_CF!0B23wmSqQMueqy#WKk>l6I?8@(7vBW) z2=ndU^o4<2$%vkx)3~@I5XK1|5sv4c!r8e4G~c`x14i)l~Z@ zOq!GUfY_R7g^&NafS*lSp7zr|K=B#zQF9xUZ)zE3AtuU~5NnUJgi|Fh_mkwI6Z5;K zvkJ4G;%udJM;Z4I08A?>Gpzzy44SnF`^!oX_R~&JF=eX*)ia6yU8i21@Xay?o$b0)r+7hxEC z=~haC|0M;4sr=Jse5G9gGAa4TNEImzSVRPjDtt}!KorK4b}@Kd{~dk{Iia!cppWuJU+nW#iv1kZ59v)#$5sgS} zzkL$zi{dx*mEvROU8=tz8Xvb4UPU|?Sh|qjD1}2p*T>GGX&YKJE_5gLedBk;$LM0` zg1br>zseb9Mj8l|;@@I-#=26^Ax|^U1N7T8=Nr2r#{_(=Xcu|Git%cKZy2Y%0KEKu zC3i-)(f=ovG`zmmqG4BATJP&ntBK)`ZUT} z`23Wo{a2g2w5n)^9?Klyho0y66jz8GgFa>A-`b0~?>@ z31P3OoAsu6mnr%cl^G4xo9v zD!Cty@RIjykHvg0xhC2=PP;NU1ZR3v0MMSI`mzXdNV6I_^t0~!8Mpa)uG7}3it$d- zzSWNkFim#6j@|$qFfh2u1n8L)v_a|;=&cpBDyChTGlKVeZ(^S281qf~8yn{|a2hdA zE!^?~+|?UMWKfZgRYC73^K>Al#2Dw7pbv!ZBxY@(4&itk`|~*S(f6G(N@E<=0jj1l zu4O+6?(yc(|IX?ziH5wqskPA9GOkL0fJ&TFVp46&ApZogj6^?7KR9aptu`IsVVQrSPa`zPedInre)=AG;2#Kv(Rh3$Gm^extIXVjfi68z4VONFKNk& zQsZRmeZGDGnlS_k3!aMDAADJT7nv!&hA$EKyoL#KZ<5t z??)n&>U49EohctOb_<~Le(sAAG$O!pA!!zP-<2y^o>}^N3%Wb#WS?;g-IJ7WIQ`Vt z)N`8_<%~4#LV}K7bZidwsEUk^)c4?@d`5N^wI#D#=y!ShpL@bxKKt)3VLheWe+dOH zYpDJNR=S6umkD-6e*XYg2(ZpLB#VZCA1%8KtRzgGiCemrQs93{fqxS%=uB~zxHrOZ zKGQS_55K&HDn}Q{qP$tp6^3KCAi#U%-(L(jC}}I#JX^9%ymO3z`V+w8wMdKjYY4`5(TwOxf{-4l&?WgGA21cX8x6Rv z)!iBDAUNDXILW>VLvHRo=+fU<*e=05sc7Sf2ZIi^M`yke+($H>8<^rj$kn@$nU{oM zyP|Em*8{aNc@4uxvS$L&$kYGDLn=$%NLNLlE+(y8D8DAX<59*yy< zG$(nk55d;7U8Oe3Q|M2f^d(;AMf3R;mIV=T^%`czq`%I$z-crF>H%e;IuUw11mQEj zB|D-|Tl5#7oFyXnLyw_hSWi3a6%C4y))7#CW?;yAwsDcfMuOlGw+JvhVGSX~nyk zHrnVAo zjGr+S(;v?OJ&^b?-bBvyLgx8yYm}I*?9S*}*vG)S1jwN{uk(3Pk7=ut>7IF1c3RSn z-p{!Hb8D~7yg8p!JR;hbd05H$l;oy=ZSL_uO@y+5QZ9#0{!zbxDQ3C=tp)<*kPT3W zO$T1_zPQ8OcSzBokOk{kG5IPlk-46Jy6s#qMpRkM+9f9z^il3tf1v-5`dqUZr}hiw zcO>1VC|6IfYGtfO!?r8qSa2=<^*d+c9Vvg)^Yw86{LH{qKlc%p_<@+J0YTwJ3(5ne zboM2|F8xour?f9&MwRzfJfOmqS7Tab{u07+RMrx7&bXaeq)38X8VVqbcO;p}M&`r1 zcnV2}6)VXZN{)9~TB}c@zfI7Cab4nBLX#B!64WA^I)JG99=b$1e-~T?AQ%789wCdj zyHn9W3LQJ6(H6SxRqvH{qc3}qqq3Bdnb5$=4x773*_SZE{;|si3D|gf?>}8iQ%kr1 z3JUz0fmb~&KVHljzbK5kPiEiDKCDrBrsMgJFdp}7mL2_?@_G3&UvJiWYd6!t!o=^t zqNSzJ`2RBnBnVs>-=CW?UVmOR70jVZZq>_25n!KYmI>Sgzq(a-><^fSPkSdS_X8+2 z^Sm<)LD^585gUfd|F%kU~{#6IR+bgel)(Io*`O^zV{;7%f9IOl+>6?$P?;X!iPl8gT<>OtynrYuvr!9P5;KACg6do%w$-ekIrKE1D}O;)K_D1l9iPAtC`wY z0ILz%4U4$H=7f|^#xCl++WuBdO*tDQXKnho-^`v7x`ui2aSu$+2`@w?V+u2-hNYs^>l*|hVyG|Sfl z0soNwhKmLxyu;e+ybAr=D&G_NsptnuZUAsXYeOw;gV6*PvHv8?Zu6nx-bliSLXLMw z56OEFU(Mo75Aptt_rK$91HHNtg}H-zT6!KA07}&8+dBO%cqx1)#__*4$@G4az2raV zJ2b}oi;0b(2dJo>Fkr>m7sD9y3bUmoYEba3N)jJ6NlW1Q7K9cRuqAZ}r(#{NYS^>)MiMcGHoQLM9 z6_#lA4#a-A9bf02p$+^O<{m( z8~o3iRWUVPh*Y!?kQZJpt`Hl*JnrHQl)O56W9K=ylgv}{Rli9+UbRGp!6skp%hCtK z$a|aDtBqmc-vTk-Lhzj{=q@IL%)FGkvR0keL(L$&vf^|HV89?Td_0R6i3piQG$70` zBk|?D8&k;R4u6~4Id?L0Y4#STQj8={AMG(*TwjxeX$!r{$=S84cC+rR`&rt{R zE!%_oU1zrQF71=0dk|ver&1G6{acjIY9F4rPnqpbrc`0m$0&BGBrCM%i`A zBa}luXZaL*hw>fg1s4J6c$E!mBg&(0fAJ9t)HJ4QOAxf*nVUrLM%UTZ9IDM4R8mj` z!1C4BGswiF)LWJLOUgdvJ0n}Hw@O~1tZLWR%G0m0 z!EpijM8=%Jeg{FSZ)ExB8dRJ{%-7opte5N~+RJEQs%m#h2Ju2Q@>2x*s!oOn~dIGzZxLhMbwjYr@b0jn7ckMIa}+=>h~ZX8HRXPeGT}+S5i8>U$-; zG2WH>@{D!bFromDmDv=a%rA_7Ux*-#>u+cBja-j(=~!Q^Mj-|{zB^eR%@v8fx7GI3-yaO@IkROW4FB% zJUA*n$G?l~)3n1X0ylWQr@6uWh^5%mWImb?7|o4AwAWU9X!J*&rZz+yg00MX$AvD+ z4{DyI)lfR1bG11CE;a|9c`+LJTABTZ=#Us!&(oeN9-fzUOVUh@@y~v{&{N!u_hF3Z zfYFx^n-pIe^Aqa%b5n03^HU9Wo&oMc=fuAbl(I&lY>#0eW1I*8Ig$AANMZKUPoi!k z0uM=8diOv$TTvL&kV9u6qk z80!Kq`pzDw)y4$vl&zP3KlIpQUlJXT^{jKijZkk5^mx#N*t?j#H|k<_shIXKn^s9^ zltQ#yS}IFTWl_pTy8uHn?fLRT&%}IlW)UNrOMCmE5sPzqUo=d#!@XPm!-RK-yFxys zFkG%yD>BZu-e00tV-u|cysnh^Ev%8anD_R5bydQm^RxOIoI~=@{lel#isx$MNlB~1 z8i-ZJ1Zv0mgy;p`S9!Z>r)W~hKSi3Om3N+t50}5-3X{2hgLS!@>Cp-*!ZoJ6;FyR9--n|#|lTv+nT6fRK2%IlVu87Wr!M~XQhz)SbSdcOz_yz** zMr|%R))l)7Mmz)q4)srpW)1Vsb)s3JHyJ=9MV(Ff_U!dsAzO@l>2)a}%b^Os4_H12J*b zOZa>|tK6bq!~Pg$CJ{}F^NhRF*(Lq8Jp#nsfo>|9suZ<>I-Q6?MR{NS+Mzv>2KL#= zIvDp*^{B~t|MT8Q{0;e#x+mDsL_p-%3a!frpcNblzjfgf@@s4Q?6R~EjrDZv0)VJa z_YIk(og@8K-$U1g3CbDcKyPn<YbEY-6W~>kmCb6y-hy+6Vf0eLon@ec1q13CnrGF#|7@lmf_aaZzC$k;FI z+7iaY9_pqM*QS(9PurJL!hU_HhD1v`LH80VVio;9Vsd$|9tK|V{@^dPPXJf^S!b$z zpqQVr2y!Olytc?J@iEVp{Pzpj(*K6Z1O4b*caub*_4U0*sAgX{k!I?^joK)48pYbDiY9+O_ChLBTA> zp^4T}g}&(RdgX6?uSb*dywEdOFN#sNC>$MH z$aQ~i@o%l^HubaJE+$K~WkrehCl3AR|C6qlZvP)p!1>zw#`*ex@?cq^ErkhlPJaJ% zKE!a#ge~1lDexbmKm^7G;|DkmQU74*iMw$?pDb&BsUBs#gy*Yu)j1N~rXAF7!Vy%_ zx@%vm_rb_IXcILUx7HOC!(d@xu-k{ZzJRseDBWxV`(d>1aOGVBs)bQ*7Ztynt~zQV z_t(>2(-DYD6ZN8Y4UGCQ!P3>>e|7zP#e`HL;OQ)xt_!_I(~A)pv01j}Ao$FB+6r|K z&uyZ%R1tv6MBonsochB87+GKKdbb~fVR_1sAdK_e;?Q?aT{J2CiYG>=a(^RZlsNBe zjqs}Cc>u=F>J0W9vTQGcUfZq>(-u*_pkAekpt?%gD}c-W)jciF=zI^K#0_7;Z{?ht z&Au2HVfdjz(U!zEI(8e7qo$iGjldRY|#42PWdxx(n5R-Y?b#(h^CmqHKH zFIz1x>IER$?yj;Bq~6NfPA`B_Rh?YtBkunt{&+TmJ(6tYhpTh{Oy^;XabSsFN1V1j z_i8sMao+jajmDmbjz2jM#Oa4}6#!{KmcO1uPf^dyXcB&*Kc^Te1T@ar*GZyN{5$t) zXBmesdbSR~xj3cx=GZC(|4oqs$wL*I5S=A?$kO)tQq9KzWIOb;WFS(q?kYMHqQA+e zXN^ufo6oL%Me25j@%r*dlJ4L+565qjH3|!Bl{g^;YUGD#Jc=ML_Locv0Z2SUwALzu zsvsLkH=6!sho|-xjsO5a07*naRH}rbch%#`i%kTcQCWLmlYVv6(QN@RJEt}ELm%H- z^PrgU3a<`b8J^DmR&6~2^@{eCeuKUrp!%hGN~?t?>Qw47N!V$XAs*}uK4u@L4Nrou zW|mQ`cjx+It6lUuOkH;+-C@k-cy)8W$$TlwjLx^efUYIV5SDLI^hdQDBr|-~RcQ|d z=ugwy<>n^wo)ot$Sr%ek&)3iP=AHofl{L}@V4F?!MdW@`>~BdE`Z49JK&xWteU1-+ z5CVOn`H0LT|5+_fTs?{>M?Q(%!2JWfRm?%`hht4`#=CGxBp3(Su=xR3;wyCk?=Bh+ ze2P$7OgY{kG(dl(g$Mu{QrSPwr^{Z-4Se>jr5pipN#g!?xVO_U^(*vStpT8Qi*JoI z2PkcJuh-ge{84Y8Kr84T4^~Q=CLc6uNvQ_qCrM@eQ@QRpyRzg(BWksSyw6@!A0ZlF zPgu8Aj(g+!Lo9c#m_O*hO5QKH!)~6gXu7(CI6g%yP=6C(No?g$2?anER07b07`}@M zK-fcI8SU(kZkqnFg5%5d3>z;{5_&25MD;Nb(G0do0IrTCpRk!%XXAH~L{tIB)}=-z zaM65FR$TD(&}%8ZDOm$jqEx+~GV07zV;8fdB}DZY4~w@|y6L@662iJCof{;$!*TTq z<^eu9jDX=|UR~aZPP|q4-Cvs@*vfnKcQ@_tR#1BXe~iteGb-_8h9rI*DBVga@E@Q+fY}Cv!5`t5gz4?_trB|&qf@#lo!w?kuY5Nm zrUSPYIg(+>7$Q!I?+N!d6V*Iv5gUu<3WL-T$yAMr;1Ll_XoVUj1jKhv0YXb*MYamA z2!h7kN47r!ZKIpw176 zl48{WFlFQA?F}&SH?%Lr$B4Hrz8r}>g70+Kr#BT-6u4cvX*sgs2_l1SG=GAK^Pex zf2x@KqSfq=N=|X#4maYgroL=zaTwrM^jdW1`I|W(aRwFutNShok8{}&RZ9qKH-bh+t7ZbU^3TNQ0I!q41GvHZ36vS=#;eqKJFSwfaXU1Rz0A7 z#r^|sOB?!VPpZ4kISA^Vo@X)}AV`*JUzQ6=J<#Gq9k4-V4aCW@~^#!cQ80I0bYCnW&@s*F!= z?(GRZn#8*~2s-sgoQ)aBVAPYTj{@Mlnd6?7F0+fy$pQ|Sjgd@z026VDR&Ml(h(Xz?WPW$y-ElXuydFttLnQ!)X z%WJJ+`b_k?20$$|JGwf~cm`6u%M1Z92er;F{SRreZE@OTBa+r$_3yg<84%w{^*d2J&}>&5kdZfX^Dvs}+RStP)MQT-#1Eg8p>a_qlECiz#X zH-k5a!u2o401kj z(DSg#d{+9n+0I(}T|kf(K!tP)Z;A#K2hI)h0cDONVF>d?S&2aoV55Gb_GND3d#Nnu zr9X(l0-AKG6>Ic|kkM@AAQaDoZt2|3r+^KmWrNh{L8}ROp#LXW5T` zY%5TNBe;~!sQSO={C)0VNyk9Wn)g{YR-99RKo_MbQ#{##Jp zBk@v-8&;*NLs5Xq$IcOo|mVRS=vQ#4vu zsM7!3_UrHpWoYq0w8 zy!fDe1a7E`wK9{S zxyR^fuBAMs)e+#KTiP+bJ?F#Pa=jtXSwh|&7`r=zT-pyHD26Lnh=#^}MJpdiFj|f> zHFO8pg)m6=WB-ivP~L$dYH#uU=~nZCGn5^zUIho>|Bu_{OFpNa+pLZG z2o$nD4U#W*_Y?O;v;*QBbEVKlH}yC4@zgU;-Kf!SbGN=ifLqEs@8be=(zf|$m@7H{ zp!TE&UG1+}RA>gLpA@`WGUWVdu?^4t;eiFO*}JQvg1fCODuh=$$!m2oE`4WB4| zDn=U?`DN)8`fXMw!@U=PbwimE68%{7p7gK9q-mda_B%gu-6?W(w}npEsf7~#>C_;` z6_IRaXro0pQY9m0i}w=Gec761cgMuHA?qbc=Wr}HyI%?OH1?W$+{^>;b@FsF#<0KF zS|Y&+{v^*LqdEQiC@GjU1C7$hfd*oljs9jev;qLQ@3kA1anyT<^RdqTM6Nxc4TSCi z0{m~pbX2wQy~x}2=S4k3=4XSi%7p#wd&)bnSiBdfmR*YEKrd~nn`zxo|GzP+sn=6a z1+s@7rk>B8dnB)uENU!?G?nYr)}k4S^odUu%~_zV*-6(q-&|dt(D-smVyg($@bQQ@D8pbbuVjbx|3bT$9c~^k^s_&oPdM$j(u(ovt!hET>Dad($1$Au8S}) zm27j9=XKh7v;MI7d_^CLKjaMO{=x3SM6gSSQja951XC%;)cW-Aq<&0&2D;^YvQ+4& z-Qk?J2SCp@MlVI)+ex^aGrx^)M5W64=hXRXb?WP24T*n8b6?dSPvp5;pv@KFM7H&_ z3BV+Min4z{nyWbNTu3q%`iJZMo8mI#hTYo}fbtskO1TbjrY8VpeYIW!Kt1Hn62N6| zac@bOz$e0)xQ+zKH51pL##fX1?)gyv7B>LJSjlL%pq@U;VbOe9Unp;=cfvm&54VvI zs@w)EJJ)?l z@=~c0ZGd*uKTbFUoCWSY_8m1S0#2(&KDH|Cm&aAN4vep?8x#$LUDqBiCLVX1yEze^ zwt=iy<+!dnVeJ4B0M-KeK;VmViaU+!YB=pBJ%DqK`>^;75pCrYCt(tj#zjMs|86AB zg5jOGm{&Xc0*vlOW4j1AZM<4nOfD5aNS^N9$QSo}Nk=>l_#mE{-pjWI2HC&(o+21# z&Vk~rNMri*G^RTQ$;pe^uNU5o<|;L>Zb2e4&m&OQFOs zIh(>l=PejyIqunSyykg~>vt;E6Hdx^DZM0L6vC>F^Eu@M$~hN-tW8wUsq`xrbsj55 z5LnN+v!e(I)mGYRxyZ12R<~jR1|ZsyC^q#V-Ycis?}MxRAj-xVdZb-dF)_jQo&7-H z%8c&_;PFU~58+PgaVE1uBs0kWf|(-D^38@+D4%qU{#Mi%sL7m4O_1rXZL zxy#Ug$RfhKTn5 z&k8y^%g-pD-;L?otvoMDyH0rn)9;-{>*MDc|GJSktesfM&gI`LrpVwVe>Gn|8N(L6Y|4YM1BvYqIPx&xuyFK9Y=F_OH=RkvjN{Wd^#M03H12hDwwqy^f2o z-Kh(CmBV8Q%DZ@OMqx*yCik7YrD!nqpEgweUaEg3CF=c}dQW)vdzR3?!N$)dZcvO5 zwQ0&L&|{l=M0|_lSmSMHC+TkcN1{$I?99C_pX;(}<@YS4J!gN;UYo}_ z7oH(bmU)%(n0Kqzhjz|U+lxlYP2uf70X=>IP6gRNKyn?eA)O*yQH3T3`yNdt;Z5NDm6pZ$i+D`;5 zF47A`ki=^_^&J>r?gOzd2zBMWRWiw5gimStNOv*4REefG@gaDK_41gz2;c8@@v343 zh2b++nPCLM&w4wFhCHz6+&ej^(ID(r%32TOJF~6Q)2RTuYKu}o@WP<$-49%dH9(zII4_z%b0!a8hC1)tp7*{O-1LKM>6w;%tUy(Y`Xq z4q4RLUq_pk{xtO2>5LW=SnRm-qNJ_xo->|Gf|2{b(=VmfrybqhTY{6RZ@6_qGFMwI z?G>^2IX+5D^{t`&Q0wf=g1-Hg!5V@$_P$-V_$lh0rg+4}t7mI5qlEq4%9An2hji>^3F1#znX(c zZbUsJ34Yj5sfp(g$A4Gu6rUIMeWgF)4AYGq&-#L zDsDCC`J0j=32Ff$x%lrXm#m_F(J(;a*u~1lcOkIfh)ov3PXt+Gm-!hASRL$vXxvr> z`-{m@t*u?9Ev25PoPcQ5)QdzBu7++&q}wRO1oU96z-GQXsZK+Oe%ni(`r`cUgfJ}E z1@Ppm>m2B9ey`0CXMPM-2gR9Qj{~vSGf&szT-Oyq*E9FAa4i7X@WO4PSxp@otm9`M z8N*5caSip&c9te`gP&CHl@AC!n8r%}ETZC`5vTC02%iyx4cAss$VO0Fo!9HdErN5UlK%MT)0%o zygom!C?28SoI$Zfzsf$CuBQ#A9|yJf#FtWqMvT1THU;*UrTT7eIrnU$yh@_+SgL7} z+Jx&ud)D zROz;qnE;(O?s{u0?{#+ZC`r)kt#AA)nlSGN=o7nBHZ)!$;e|rvZxaDb`e~9yKu7Ot zCjD;4NI(aGp#i0p zkyc#+ zG`83RFd$w-ZUJT3?LdU2lD{Cb#isqS`PNwRrNKG=E>=5Jh&Wnoo@alWt2+%~^k?<@ zNtO8mujBox0*qLHRNZ54roU62Z{mDulM}0<

7sW&I`)1|93bOQ!|A10G`Ilcw) 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

kqQkvzxy(n%Qumyyz}Y@N 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 z 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�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

e2jxoS<<_(nt-46k-f@Q5$;tks;^UD zTQy?*PQP`2IfO4^gS(J$FS2~rm6oO7(yjsjAEHI$HBoIH2rddf4V=Hl_K5Wr&x77{jxO$Jb2j|9zR9+z5Q&Q`pgVFHO73v6#E2EeQS+Fp@)n+ zO}pj+|C|fXDOX~40B!nV;$bHym%X%))iuUC`oGZ`E$&~`iLAG6lv|;_=b=8Mm-dHV zi}Y8OV)_Znb+x{imrIIHFZw~6$@#tA-jip)gdA5)JD)ljyzCnpDEm^ZYZf|M^gy7;j|Q_+ZimDVQ( zJh7`8wGHsV^SZx~|3Att54BBLi$HBzrHK_B%7ye)^)K=z+ehts^i#v2<2&*{=z)D`@#_)fV&#h912Pd)ih8|X{bZB?$7EdrVEdpGqCbgM|+LWPSB0bMdKoxJrU z4C3bNHmfE5DC%zZqL)6(AtL*07+?8`L}5DbLQY6#u&T^um55)k^AjZhVI3jB|OyiAzH2@C>;+Uv4}T0dqMb2_%KQTA&2_G#PGKd%?y`XG^Y#@U z-d6M+<82+nhVeBVnsNZ()EH9eCrs)tvF+h58Ptk$Rbh|n5XLq>2*U_79}vb4bz|G>h$ zNtlXEmW8X%aQeE&_|s8bH>00hfhhCGroUTWLeO8<3XC4)PuFa9EcG8Qn3Kmsjn~h; zmTew6>b8Cpv!;WTJmUXxO%KrHu#|)_RJESk>ZC+6?lh0=iZB6iIrueT$x%> z0=y~MZC{t$TE3>93|VJZ3MRPVsZmzn9mt@3iql#yb40tD;e7B~>!JyV{MZCusp}G}rTxjc?j;sYX1=VtR=%~2dJO1I zDe^aSo)sHYXRh_FYg2UH`N9N`VqKik9^jX6I5&)))EDsI)QYe?4+Jv{F9#lD!qr5j z8=sxmri6VBH7+H)c1qZ$~_2Huvgm^XlEX)W<2e#^wiUd>u3EJMWv`+ zH;#CkFut{sP(FBq3VuWoy66P#hIWYjQSF|W`q?YSJG}^du!RCooEH>zZ3jyr&jxRh zaF26MNq*$`%(~`aP*(jF_|LSziv3Q!k@kt+kMh4cC%g@aUxIG$qMi{LK{GCY2k+w^ z@IEgyvLgBli&uF9N$jR%}+0_2+4) zA4_;1i=QrSzHW1ba&^_uWPO+ERAzL^AA!T$+2gVk=-0v2?P+6a{~b??PJ0?&#Iy{; zvA9O>LA%ZDESvSGx7J7MfC=XLZ#W7?`$0YL?u$Qh{}npn)Uh48OlNSio#m>~+ClDF z;Mk%3?dp1KvazvnAj`jrqeV8Q5V9Oat*g{?ulct}UIBt)C-lHIl3tCHi;h}r%~Epm zvdWrE%tlOjTH@gZ5D&H84VPi~*LhDEIJGV3;2`ND0ka7``~=~X`K9x{7^xQVS|D~| z!hYPBV1UXD+#J*pOaJ#!z8Ad(5x5=qq--3Bnd`d?^A7Tj zOT?3C&Bymz^q>my9-LR`H{0q(~AKv$fDZ8_yr2s>E5rXL3@)AJSlTDe3!I zhAb$Y63k!==Y$5^^eN#}D3Ry~H=MqdlQ#0P?XKtDZ?Qy?crt7)^`Z`QRg$Wy{v{hQ z%-W~Iz;UH{jJrYgzy)k?(s5@ak}%XewPhaImLEw!k$`+H3?yo!mNI{wM$0G}q<@H9 zj*V9N5}P3AMQe_c-gdw=`u3rFz}Ja5PsqIU;71o#g|IeGIv(0h{xdL`QqN9=%M;lM zjzupC;Wz3U)AXk)cOUeA9Rj^KFw!?2!o4V~dhP)7Pk})h3<7FabGrll(6P|UAnoYI z^dj10lzWd`JIx5!vW+qxZ5lQ|EorZnGuD=IWovUZHY!xLm%$KWR5xS&#Sus#NqCi> zT_kLQ4Hwy6B=GH)G0_jcF=CU4{kEi6Mif!a6_h3=OX(K9J)cpml3?az5Vr-9Jy+!U5 zLeFa%a&WU=w1&nz(r!sqjTIPgBel0anf&iNWmtm>^2eo`wBJB!ZJj5a1NJ)9^9ZB) zzI^YiJTB{qwRwt7L9r(}bw%alcu_^Lfpad~ZjUG5aJ7jBK~yU_Z;DFQ^Ne21&pPwQ zy+gfS!Ta}bL>>qMeU;1WB^c*;W1vWY?_IffGmCQNDr9J-PO`C2j#sgOgSJ=S4osBr z^l*;6DXDMsEfIGYk?+HM@)Pd%zTW2c#0qlv5o zJ!y^sJ}0*OKc3zM{-@%5{J(RT*ZSVQ-*3B6M2I3Hgh+~t3Z*0wMJdrjNg>%KWyxMD zDuf~-OJpf2Bum-X_rAZDyZoQ$_4$4OUJs7BckV1_&YU@O&Y3fzGZqVg{-C!;Ha2=a zwZx#GME|Mmt)YBJtuT4-G>*x60oPbFR$ z2C!8X^h0l^5~jk)RCOiMu9mS+C7^Be!@OII7;p3C)b7c%sOPD0tpE#f>?XUeIO)bu z$7aW&J9DHp#kEhuNOuTx@U zu}<~Z;lEyT>c$lC6TBt)LUL33IWkr!eh2mB+aJa$N3Z9-=nN;{-<;}VoH@HZRKfq9 z*|9ZI_=xR~`77Q8{{_)DVp<}|P&cYW@M`n}W1o!_I*cPW0%Hz;h*Mug2p!SFI(T;xR0c#HB)yC_Wb`zduW}=*8R)`OI ze~|TN3-om=Z)fpKns(;T3zx3*yh=vq56RS*lAPQ{ZU(Twx4%hj%*SvIIf4SKS+3p# z*oCcGW6D3|rFry2pR5)d1L?qn?H!^-SgU>0`;iRpnS~!E>PG(UO&n=QwYy{VMfjcsJ;qm}dj3ev}@= zV!5nzoBId`o)(v97BX&`cV*>^FI2pF@xXIQl+(kYKE0mfM$V9k+J)_OhjvdGL*jfZPbth|u^P0nkr16mt_G_|EDoQ_>gk7$%qIrIF6*gCkrFa;NqT7xlHeD z;KK+wckzB4f)R{K4MAT5L1@pR(GffiRecfqaN^{8*ipb&IEX(I*qjNlpYH}zYa!`B ziJkX^O#6nq(-gP1^0$U|u1VNMbYneoQYDhW9 z5FE%)c!uBg!yF5~cKO-dDG>aC{K*9f3N4$rysc?H#Qyt+RzJf4MT3;D3Le-Tsps(?~ zCAvlyFoY_F@BwFFyd%7?3|0l-S%~a{*BJCs1eC)L^87ra*hRko7`JC8km`U4_XX)k z@!Dy?dqx*?GP-UAkS)MQ+F4;h;wdMWKqHn7Pn;%wkm)Aly}=vd)#7`D&GJY&Jw3_oad-gDEYB|>-c$kp=Y8$KxSTxi#R!Z>pQUY+t74FdzL$-gQDM9lc#iak+;%yheEr<0 zE&W`js!PB`{Z-t;yO6y<=f0o|x5%v-8%()PG4=llnV)dBm1gkWP&XDiOa3nEadQlK zD>T3K|4aTay-iM6@cO6SQG7It>J=rIQ{Sc6FHOy>&2+gvrD>4r&t#tQ@)#fE7h|Fi z`l(o3_HNla)``BEQPJ>R{rp;+oZgVv-mJM9k5cbj(P!c#LC4qmZA-vwspbwYqMgbA zB-INoB>nmI_7^WuemwJq6!@SQ^6m-w(a z*83{vn{R{OCpd}sO}VW3ZW^eoKV0zceCU=&b4Z&wLC{Xb+csiq^Tz0t%^T?FQtfY> zakFoVwk+$<^T%>~l`$^^>(k6iEBS0?c(SxFmK_b0ms9VLUbh$vzE#86Ztdm$YE{>G zpJ|q6mqdDkj~_7-e<^gSU2rI`v_`aB9Qv&?F5}Kj=vMcP`D}Hx_(A(UoAw(O`V0#? z!USF48^HUwv#CBmk9w{nUN8=7 zmGg&Og@*b4kX%x1 zF`a6?(U0E+>#lkAxhv_wN>`>_mh#a{TARsV!+s~en0&XYb9ySz*Kmg>^hAh|Rf&1; za1%u}-k)L{qsTwg3u1%dY9F`0vx9bz;kL{9z(M|-eW!W8Mjs~uZuPebo3a7H;+cZl za>@9$WqAXN;VapJ%-2&tMc#>~-IjbW<<<4pE9gXxC9sGKtfs2!wRrv|{C_OzQA8M& z{&aIg>QCT9+B34D)6a58BklAWyBB4=ojH*`6#?GY({2|Nv{`Hhd_o6vwY^d1k@ta9 z#d!tzG!fXIC;w7=RP-qMKP!8#XaVV(gljm(`@?!S^%&(}G`E<{YeHBIai5?a+14_n zFa7u=cU*+=Fgu4kVVY+#gr5H{s@w+i_mF&deSXYB6Mq{w;ZcfXQ8rMr3lRP{a< z_%_D*Py0TkU$Z#|z&N^l^-n~ez$(+lycKwdHckX?H{{N->0hit>9|t(jZ-VKA+nls z2;A8+p&Gr@yH}j?(ByPa_&R)tcM=_fxJ+~>8dsF`lZD^=oN2I+^iSgb1l*6WMT~sY z)XO>@jIN3A0h_rW`J8KPIDF^TAU@{26}ad_kV%a zGFEbC002M$NklYNJlc@58BzXFjQ+%Wm?7`9N$iB_H%=~Xja|)owZ71*1dW*_pM{+~dG7+WQQ=ds?##Ifz+URT-4;>$NZ;ar|(Ohu=m zm5#W6;JQq-lIWTk^y}=5e_}I^MLZft*Gf-9eZWAnh1kcB) zHU{)c7|&hC6rS&LrzAAHPiUvT`n-P_(P})fF}`WEhvjRDoT^37dt*XlCiDFZ(#weF z)0lFin3|yj{0_Xmm+p{LQGbKFB=4RJOsNP5#a-M9)#q z(`uQ74Z+8(K>Fzrp4L8rpku_}FYLv_SRQSX^9aM+&{``c>ndS}m(Xw^eFXwlnE((#BA)qy^x4tDB-jZ+dSPlLgcI$vE%l zd*fbo4qbVEUF&H+Pdf^G`@Kg9Y6j25PdB4#=R|H=E0(I&~SxQCuaujdEfY5Ov~mveP(a~aI#k61MfuQuSf)JKBxQiFTlsmc3kcj)W@k= zhV-jB=is1w=Ik$WN>vTeQ{DH;_k>dOJG4W;B3s2OiftQCP$12u9Uy;Hdh0GV*L!|$S_6;ig zpzH=<^|IeeX;17x+4d6XCYDi>Qn(L3(lqalT`)oy7I1Gd+La)p}NDD+Bj19UJrYh~rd^sjB@=hNXA z;YL;FRYs5qM6(WOf=Az+jBnG!@HPI5#RE5K{Tq1-)#2Zw*XC^$g?XfjDG>;{|0~m?@zht5GY4{#BzA} ze+%j&?-@?;5vUHM&;Z~K(yX7T^Y z%1H{lW2izR6QcQ>_ z=4Z0_h+w@Uffro4)gL^+0X=3L@`=l}(9#KYN?jtKj*ml}bcnwx{QXKT5L2(7j5yJh z_pfNh>{aCs!YZ<3)jOE;%_%Ur@A3}j3f|r`vxLr*KB#l-#)dGIrm^19VU+WNcHXN^ zy?=046TGO~*&9i{ovm%gr+l}8R@R~1LtdGBf%lV?qmJ`_kCrX|WZp$Q51}u&$az%a zwZM6Ebm-y=VP=d)k4z$FpFOXW+8A1 z{Y=tcb?5QE1#zv;k)8!z3B6ZA6PC^tJ8C>^j3xgLG}J=J8EODC1eGPelw~|`1=eNW z-wd{n0v+a|JeMnO1%DCZOhF&&M#dd_f=kqrQ+b$RH@FjMebg7G!48JiMx`EwrVMZ_mi}F!S&$tYdzgsL_2OJQqUyopY9bn(0Rpx*v+LZgvJ&9n%D8&CzOk7v<`2eGdJWi9b@z7XcQkiHO6rt>$7*Fs zKO|9}`=EPEjPle}JvDTN^g=hqrabox?P>E9zI#gF=$vJtZix>m^c;P{U2j0=)^nYv7;oo1v*p?RJ~c{R|yZlWKrd+*xNEoQ`~@;&svP&Y}h@J`B22?XxU z)qemF!?_dWW8l;1SfNv*UoCeSFZ0VlICwjF`yh2yY7X^=!dC%Y?qzP_dO#ejj^y8}He*B3pvpEv*UZ}+*eq!32pgSJ zU#N%45aw%av7sNo8D}?a^jY#wFgetV7w0d^gZ| zPWq3d#Sr~3$}M0EKY(_OLga>yjLpICLwo4Q34f2E;MvgEiVK79Ztq%rI{nPk@A1D) zxn<6?aoVT0>i3EBKYZvleK_r1qms>+C~q{EtU*WW2d9-BuF!_q<%tdIZ0~n*E#T(H ze{I_9jdLG~J&uB~@8#{$H(`~N!{3_YSQ2+-kjGI|FnSd$;eEF=T zy-AFlRWI1wSI$2Dw(}b=+<_qWWZKhkhU<*KuX8XQ>O%kf zWH(PLpq+iS`B=|16ZL8z_QK+;)DPi8W8dCMj@9FzGgTN9R|C(GhIsoK7>|4lKvxf7 zCzjr6y#Ijr5UTT#eU}_>jej10HC~tXD;Cm|GRfDLa|%ohZwgndxTM=?wK1j%LrD`& z3zo|{vcp0za}8aBXwU{?rlCOeP9TtfJu!hGTqJz75{x~W$N9XC@cb@y7iKnQu2w@B zzt&2%kdEQ>{u+}GPQa$m0iNbp$ajQ~OhoV%JrxU#c2Mn6hsoa_SH`ZSCCtLVzyJ#g z_~43zr8s(Dp7?Nd2G*R~?G%!82o{2C8 zRcQ$Wr|v{?E%+A07K{u-5&?_+c_I{emY2z- zmM|JWD<&LKeVT~R31-F`8WW6go1dd=6jf;jHEP8t?3au-Fw~drD&kCP?$bkvXlR(+ zoy1hfm%*6l1CJFK-F^m)+gHu0=}crI-C~8}Qg47zlaG>53~IjsZ}lD`@CH{m;F`aZ za++XTeS!3Ucwejq-peV$T)x{*BpVnCPNMh8_@H8+M{Ff_q=*3YLm%w5&|@@zuHD#v zhWvB9hKcw$Ip`9Y5HR1D?FV^2i-;^Y12e&^CFP=lf*zrz|7o`VPtz#|MCwB*m7m7p zfevz9Ibo=+1bhL(Lf#+p&WhlIfb)nx9{4JT)-B{u##ar7g+RMY!fF_WM3NuOL|T$J zJCF7|d+q;ZM;f}6<;!kH{(A@v0OK*oT8*vKq=$gj8~FYe)c}gsaP~hBc~u z{;S?8H-qP|p?~H(`mkH*UV8{H0jcK)+>`l^Fb#9<&uRZU_p>N?M(7zM3xwOr`!1fu z^U=;{b`9zshYu5Y(QabPwFGE-)xGmT7ZbD0ljFp8e+`H!Uyg~>nsS*ZidpPI&zz+l zC8iZNACZL5S~UVk!icrOMCy|HdWG|lV@ak{b9-siSA<@-wNPnUVfMLBs%jGuEF6G462vtR$m zX#~6Ip$!dm2aYy&NMvdZPd64W0EW~z#z#!6kNw|P$s}DG-B<>Lw`aKf>?f$#)%%AQ zQEm#x^>yT19(dO7K>cyGFFu9x$E$lQdO^Rh>yJzPEpLg_(*F?s(v3C}$0>SO{7fnQ z�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ᏄmL}wff>w|4c9Wj< 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%0D)J%qEcf>O=D=yAf4lP#?Uqg|A7(%4x%5|w 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

&R&3x$DV|030Iltx2$$?n_#Tm+b3?-X8wh7 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

`eX2V(OTYn&&pJO+p9?Bs0k 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 TM&jjg00000NkvXXu0mjfCE%H{ From 7835dd9c4e613cf5fe29e62a69a5d5a273d5cb19 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 6 Jul 2021 10:33:56 -0500 Subject: [PATCH 0078/1722] Fix link to scene view image --- 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 777e7a15f..6c848c3fb 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -3,10 +3,12 @@ `OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be overlayed on an existing `GeoView` (`MapView` or `SceneView`). `OverviewMap` shows shows a representation of the current `visibleArea` (for a `MapView`) or `viewpoint` (for a `SceneView`) of the `GeoView` it is connected to. Map View + ![OverviewMap - MapView](./OverviewMap_MapView.png) SceneView -![OverviewMap - SceneView](./OverviewMap_SceneView.png.png) + +![OverviewMap - SceneView](./OverviewMap_SceneView.png) > **NOTE**: OverviewMap uses metered ArcGIS basemaps by default, so you will need to configure an API key. See [Security and authentication documentation](https://developers.arcgis.com/documentation/mapping-apis-and-services/security/#api-keys) for more information. From eea19801bd7a80fe7efd52dc9ec0a4f5932aabe4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 6 Jul 2021 11:15:15 -0500 Subject: [PATCH 0079/1722] use basemap style map constructors; warning for api key. --- Examples/Examples/OverviewMapExampleView.swift | 4 ++-- Examples/ExamplesApp/ExamplesApp.swift | 15 ++++++++++++++- .../ArcGISToolkit/Components/OverviewMap.swift | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 48bacecc8..8247bc5ce 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -23,8 +23,8 @@ struct OverviewMapExampleView: View { case scene } - let map = Map(basemap: .imageryWithLabels()) - let scene = Scene(basemap: .imageryWithLabels()) + let map = Map(basemapStyle: .arcGISImagery) + let scene = Scene(basemapStyle: .arcGISImagery) @State private var viewpoint: Viewpoint? @State private var visibleArea: ArcGIS.Polygon? diff --git a/Examples/ExamplesApp/ExamplesApp.swift b/Examples/ExamplesApp/ExamplesApp.swift index 77e5f8b88..5ab9b181c 100644 --- a/Examples/ExamplesApp/ExamplesApp.swift +++ b/Examples/ExamplesApp/ExamplesApp.swift @@ -12,12 +12,25 @@ // limitations under the License. import SwiftUI +import ArcGIS @main struct ExamplesApp: App { - var body: some Scene { + var body: some SwiftUI.Scene { WindowGroup { Examples() } } + + init() { + #warning("Require user to sign in with an ArcGIS identity or set your developer API key") + /* + Use of Esri location services, including basemaps and geocoding, requires either an ArcGIS identity or an API Key. For more information see https://links.esri.com/arcgis-runtime-security-auth. + 1) ArcGIS identity: An ArcGIS named user account that is a member of an organization in ArcGIS Online or ArcGIS Enterprise. + 2) API key: A permanent key that gives your application access to Esri location services. Create a new API key or access existing API keys from your ArcGIS for Developers dashboard (https://links.esri.com/arcgis-api-keys). + Production deployment of applications built with ArcGIS Runtime requires you to license ArcGIS Runtime functionality. For more information see https://links.esri.com/arcgis-runtime-license-and-deploy. + */ + // Uncomment the following line to access Esri location services using an API key. +// ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") + } } diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 0ef12b000..ff5803137 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -37,7 +37,7 @@ public struct OverviewMap: View { /// The `Map` displayed in the `OverviewMap`. @StateObject - var map: Map = Map(basemap: .topographic()) + var map: Map = Map(basemapStyle: .arcGISTopographic) /// The `Graphic` displaying the visible area of the main `GeoView`. @StateObject From 783c01680f7b156811d72bdfcdfd2a49a8480c62 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 7 Jul 2021 12:32:03 -0500 Subject: [PATCH 0080/1722] Updates from feedback. Break up example into separate map/scene view views. Add separate init()s for map/scene view use cases; move infrequently changes properties to modifiers. --- .../Examples/OverviewMapExampleView.swift | 102 ++++++++--- .../Components/OverviewMap.swift | 168 +++++++++++------- 2 files changed, 177 insertions(+), 93 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 8247bc5ce..b5561ba6a 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -23,12 +23,6 @@ struct OverviewMapExampleView: View { case scene } - let map = Map(basemapStyle: .arcGISImagery) - let scene = Scene(basemapStyle: .arcGISImagery) - - @State private var viewpoint: Viewpoint? - @State private var visibleArea: ArcGIS.Polygon? - @State var mapOrScene: MapOrScene = .map var body: some View { @@ -40,32 +34,90 @@ struct OverviewMapExampleView: View { .padding() switch mapOrScene { case .map: - MapView(map: map) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } - .onVisibleAreaChanged { visibleArea = $0 } - .overlay( - OverviewMap(viewpoint: viewpoint, - visibleArea: visibleArea - ) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) + OverviewMapForMapView() case .scene: - SceneView(scene: scene) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } - .overlay( - OverviewMap(viewpoint: viewpoint) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) + OverviewMapForSceneView() } } } +struct OverviewMapForMapView: View { + let map = Map(basemapStyle: .arcGISImagery) + + @State private var viewpoint: Viewpoint? + @State private var visibleArea: ArcGIS.Polygon? + + var body: some View { + MapView(map: map) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onVisibleAreaChanged { visibleArea = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint, + visibleArea: visibleArea + ) + // These modifiers show how you can modify the default + // values used for the symbol, map, and scaleFactor. +// .symbol(.customFillSymbol) +// .map(.customOverviewMap) +// .scaleFactor(15.0) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) + } +} + +struct OverviewMapForSceneView: View { + let scene = Scene(basemapStyle: .arcGISImagery) + + @State private var viewpoint: Viewpoint? + + var body: some View { + SceneView(scene: scene) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint) + // These modifiers show how you can modify the default + // values used for the symbol, map, and scaleFactor. +// .symbol(.customMarkerSymbol) +// .map(.customOverviewMap) +// .scaleFactor(15.0) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) + } +} + struct OverviewMapExampleView_Previews: PreviewProvider { static var previews: some View { OverviewMapExampleView() } } + +// MARK: Extensions + +private extension Symbol { + /// A custom fill symbol. + static let customFillSymbol: FillSymbol = SimpleFillSymbol( + style: .diagonalCross, + color: .blue, + outline: SimpleLineSymbol( + style: .solid, + color: .blue, + width: 1.0 + ) + ) + + /// A custom marker symbol. + static let customMarkerSymbol: MarkerSymbol = SimpleMarkerSymbol( + style: .x, + color: .blue, + size: 16.0 + ) +} + +private extension Map { + /// A custom map for the OverviewMap. + static let customOverviewMap: Map = Map(basemapStyle: .arcGISDarkGray) +} diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index ff5803137..08dbc35c7 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -21,24 +21,16 @@ public struct OverviewMap: View { /// The `Viewpoint` of the main `GeoView` let viewpoint: Viewpoint? - /// The visible area of the main `GeoView`. + /// The visible area of the main `GeoView`. Not applicable to SceneViews. let visibleArea: Polygon? - /// The `Symbol` used to display the main `GeoView` visible area. For MapViews, - /// the symbol will be a FillSymbol used to display the GeoView visible area. For SceneViews, - /// the symbol will be a MarkerSymbol, used to display the current viewpoint's center. - /// For MapViews, the default is a transparent FillSymbol with a red, 1 point width outline; - /// for SceneViews, the default is a red, crosshair SimpleMarkerSymbol. - private(set) var symbol: Symbol? + private var symbol: Symbol - /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display - /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor. - private(set) var scaleFactor: Double + private var scaleFactor: Double = 25.0 - /// The `Map` displayed in the `OverviewMap`. @StateObject - var map: Map = Map(basemapStyle: .arcGISTopographic) - + private var map: Map = Map(basemapStyle: .arcGISTopographic) + /// The `Graphic` displaying the visible area of the main `GeoView`. @StateObject private var graphic: Graphic @@ -47,84 +39,60 @@ public struct OverviewMap: View { @StateObject private var graphicsOverlay: GraphicsOverlay - /// The default fill symbol. - private let fillSymbol: FillSymbol = SimpleFillSymbol( - style: .solid, - color: .clear, - outline: SimpleLineSymbol( - style: .solid, - color: .red, - width: 1.0 - ) - ) - - /// The default marker symbol. - private let markerSymbol: MarkerSymbol = SimpleMarkerSymbol(style: .cross, - color: .red, - size: 12.0 - ) - - /// Creates an `OverviewMap`. + /// 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`. - /// - extentSymbol: The `FillSymbol` used to display the main `GeoView`'s visible area. - /// The default is a transparent `SimpleFillSymbol` with a red, 1 point width outline. - /// - scaleFactor: The factor to multiply the main `GeoView`'s scale by. - /// The default value is 25.0 + /// - visibleArea: Visible area of the main `GeoView` used to display the extent graphic. public init(viewpoint: Viewpoint?, - visibleArea: Polygon? = nil, - symbol: Symbol? = nil, - scaleFactor: Double = 25.0 + visibleArea: Polygon? ) { self.visibleArea = visibleArea - self.scaleFactor = scaleFactor - - // If the client did not pass in a symbol, set the appropriate - // symbol based on the presence of `visibleArea` (which indicates - // the main `GeoView` is a `MapView`). - if symbol == nil { - self.symbol = (visibleArea == nil) ? markerSymbol : fillSymbol - } - else { - self.symbol = symbol - } - - let graphic = Graphic(geometry: visibleArea, - symbol: self.symbol) + self.viewpoint = viewpoint + self.symbol = .defaultFill + let graphic = Graphic(symbol: self.symbol) + // It is necessary to set the graphic and graphicsOverlay this way // in order to prevent the main geoview from recreating the // graphicsOverlay every draw cycle. That was causing refresh issues // with the graphic during panning/zooming/rotating. _graphic = StateObject(wrappedValue: graphic) _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) + } + + /// Creates an `OverviewMap`. Used for creating an `OverviewMap` for use on a `SceneView`. + /// - Parameters: + /// - viewpoint: Viewpoint of the main `GeoView` used to update the `OverviewMap` view. + public init(viewpoint: Viewpoint?) { + self.viewpoint = viewpoint + self.visibleArea = nil - if let viewpoint = viewpoint, - let center = viewpoint.targetGeometry as? Point { - self.viewpoint = Viewpoint(center: center, - scale: viewpoint.targetScale * scaleFactor - ) - } - else { - self.viewpoint = nil - } + self.symbol = .defaultMarker + let graphic = Graphic(symbol: self.symbol) + + // It is necessary to set the graphic and graphicsOverlay this way + // in order to prevent the main geoview from recreating the + // graphicsOverlay every draw cycle. That was causing refresh issues + // with the graphic during panning/zooming/rotating. + _graphic = StateObject(wrappedValue: graphic) + _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) } public var body: some View { MapView( map: map, - viewpoint: viewpoint, + viewpoint: makeViewpoint(), graphicsOverlays: [graphicsOverlay] ) .attributionTextHidden() .interactionModes([]) .border(Color.black, width: 1) + .onAppear(perform: { + graphic.symbol = symbol + }) .onChange(of: visibleArea, perform: { visibleArea in if let visibleArea = visibleArea { graphic.geometry = visibleArea - let newSymbol = symbol as? FillSymbol - graphic.symbol = newSymbol != nil ? symbol : fillSymbol } }) .onChange(of: viewpoint, perform: { viewpoint in @@ -132,19 +100,83 @@ public struct OverviewMap: View { let viewpoint = viewpoint, let point = viewpoint.targetGeometry as? Point { graphic.geometry = point - let newSymbol = symbol as? MarkerSymbol - graphic.symbol = newSymbol != nil ? newSymbol : markerSymbol } }) - .onChange(of: symbol, perform: { graphic.symbol = $0 }) } + /// Creates a new viewpoint based on the `viewpoint` center, scale, and `scaleFactor`. + /// - Returns: The new `Viewpoint`. + func makeViewpoint() -> Viewpoint? { + if let viewpoint = viewpoint, + let center = viewpoint.targetGeometry as? Point { + return Viewpoint( + center: center, + scale: viewpoint.targetScale * scaleFactor + ) + } else { + return nil + } + } + + // MARK: Modifiers + + /// The `Map` displayed in the `OverviewMap`. + /// - Parameter map: The new map. + /// - Returns: The OverviewMap. public func map(_ map: Map) -> OverviewMap { var copy = self copy._map = StateObject(wrappedValue: map) return copy } + + /// 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. + /// - Parameter scaleFactor: The new scale factor. + /// - Returns: The OverviewMap. + public func scaleFactor(_ scaleFactor: Double) -> OverviewMap { + var copy = self + copy.scaleFactor = scaleFactor + return copy + } + + /// The `Symbol` used to display the main `GeoView` visible area. For MapViews, + /// the symbol will be a FillSymbol used to display the GeoView visible area. For SceneViews, + /// the symbol will be a MarkerSymbol, used to display the current viewpoint's center. + /// For MapViews, the default is a transparent FillSymbol with a red, 1 point width outline; + /// for SceneViews, the default is a red, crosshair SimpleMarkerSymbol. + /// - Parameter symbol: The new symbol. + /// - Returns: The OverviewMap. + public func symbol(_ symbol: Symbol) -> OverviewMap { + var copy = self + copy.symbol = symbol + return copy + } } + +// MARK: Extensions + extension Graphic: ObservableObject {} extension GraphicsOverlay: ObservableObject {} +extension Symbol: ObservableObject {} + +private extension Symbol { + /// The default marker symbol. + static let defaultMarker: MarkerSymbol = SimpleMarkerSymbol( + style: .cross, + color: .red, + size: 12.0 + ) + + /// The default fill symbol. + static let defaultFill: FillSymbol = SimpleFillSymbol( + style: .solid, + color: .clear, + outline: SimpleLineSymbol( + style: .solid, + color: .red, + width: 1.0 + ) + ) +} From 1d1b97cf6c91889169f5c0b2412e5546663fbc59 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 7 Jul 2021 13:25:52 -0500 Subject: [PATCH 0081/1722] Add `.onSymbolChanged()` to set the graphic symbol. --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 08dbc35c7..7c7ac8a26 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -102,6 +102,9 @@ public struct OverviewMap: View { graphic.geometry = point } }) + .onChange(of: symbol, perform: { + graphic.symbol = $0 + }) } /// Creates a new viewpoint based on the `viewpoint` center, scale, and `scaleFactor`. From f13b2916861e89143ecce4a4452ba4b1500e3465 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 7 Jul 2021 18:41:36 -0500 Subject: [PATCH 0082/1722] Doc updates; @State formatting. --- Documentation/OverviewMap/README.md | 80 ++----------------- .../Examples/OverviewMapExampleView.swift | 13 ++- 2 files changed, 17 insertions(+), 76 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 6c848c3fb..44b561bd4 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -23,11 +23,11 @@ OverviewMap: ## Key properties -`OverviewMap` has the following customizable properties: +`OverviewMap` has the following modifiers: -- `map` - the `Map` displayed in the `OverviewMap`. For example, you can use `map` to change the base map displayed by the `OverviewMap`. -- `scaleFactor` - Defines the scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The default is 25. -- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. +- `.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 default is 25. +- `symbol(_ symbol: Symbol)` - The symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. ## Behavior: @@ -39,10 +39,10 @@ For an `OverviewMap` on a `SceneView`, the center point of the `SceneView`'s `cu ### Basic usage for overlaying a `MapView` -Note that for `MapView`'s, you need to provide the `OverviewMap` both a viewpoint and visibleArea, as well as providing a `FillSymbol` if you want to customize the display of the visible area. +Note that for `MapView`s, you need to provide the `OverviewMap` both a viewpoint and visibleArea. ```swift -let map = Map(basemap: .imageryWithLabels()) +let map = Map(basemapStyle: .arcGISImagery) @State private var viewpoint: Viewpoint? @@ -65,45 +65,9 @@ var body: some View { } ``` -### Displaying a custom fill symbol and scale factor for a `MapView`'s visible area - -```swift -let map = Map(basemap: .imageryWithLabels()) - -@State -private var viewpoint: Viewpoint? - -@State -private var visibleArea: ArcGIS.Polygon? - -var body: some View { - MapView(map: map) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } - .onVisibleAreaChanged { visibleArea = $0 } - .overlay( - OverviewMap(viewpoint: viewpoint, - visibleArea: visibleArea, - symbol: SimpleFillSymbol( - style: .solid, - color: .clear, - outline: SimpleLineSymbol( - style: .solid, - color: .blue, - width: 2.0 - ) - ), - scaleFactor: 32.0 - ) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) -} -``` - ### Basic usage for overlaying a `SceneView` -Note that for `SceneView`'s, you need to provide the `OverviewMap` only a viewpoint, as well as providing a `MarkerSymbol` if you want to customize the display of the viewpoint. +Note that for `SceneView`s, you need to provide the `OverviewMap` only a viewpoint. ```swift let scene = Scene(basemap: .imageryWithLabels()) @@ -123,33 +87,5 @@ var body: some View { } ``` -### Displaying a custom marker symbol and scale factor for a `SceneView`'s viewpoint - -```swift -SceneView(scene: scene) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } - .overlay( - OverviewMap(viewpoint: viewpoint, - symbol: SimpleMarkerSymbol(style: .x, - color: .blue, - size: 24.0 - ), - scaleFactor: 32.0 - ) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) -``` - -### Changing the map used by the `OverviewMap` - -To change the `OverviewMap`'s `map`, you use the `.map()` modifier on `OverviewMap`. - -```swift -OverviewMap(viewpoint: viewpoint) - .map(Map(basemap: .darkGrayCanvasVector())) -``` - -To see the `OverviewMap` in action, try out the [Examples](../../Examples) and refer to [OverviewMapExampleView.swift](../../Examples/Examples/OverviewMapExampleView.swift) in the project. +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. diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index b5561ba6a..03b577be7 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -23,7 +23,8 @@ struct OverviewMapExampleView: View { case scene } - @State var mapOrScene: MapOrScene = .map + @State + var mapOrScene: MapOrScene = .map var body: some View { Picker("Map or Scene", selection: $mapOrScene, content: { @@ -44,8 +45,11 @@ struct OverviewMapExampleView: View { struct OverviewMapForMapView: View { let map = Map(basemapStyle: .arcGISImagery) - @State private var viewpoint: Viewpoint? - @State private var visibleArea: ArcGIS.Polygon? + @State + private var viewpoint: Viewpoint? + + @State + private var visibleArea: ArcGIS.Polygon? var body: some View { MapView(map: map) @@ -70,7 +74,8 @@ struct OverviewMapForMapView: View { struct OverviewMapForSceneView: View { let scene = Scene(basemapStyle: .arcGISImagery) - @State private var viewpoint: Viewpoint? + @State + private var viewpoint: Viewpoint? var body: some View { SceneView(scene: scene) From 297547eafe37ae6a6a34d3aa0d6062d1c8ff4d4d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 7 Jul 2021 18:49:48 -0500 Subject: [PATCH 0083/1722] minor doc update to "current `VisibleArea`/`Viewpoint`" --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 44b561bd4..5f2921d33 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -27,7 +27,7 @@ OverviewMap: - `.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 default is 25. -- `symbol(_ symbol: Symbol)` - The symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. +- `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. ## Behavior: From 901a0788a5dbb5e0ea94e2d15db4ae758c1b715d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 8 Jul 2021 13:41:59 -0500 Subject: [PATCH 0084/1722] PR review changes. --- Documentation/OverviewMap/README.md | 6 ++--- .../Examples/OverviewMapExampleView.swift | 4 +--- .../Components/OverviewMap.swift | 23 ++++++++++--------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 5f2921d33..f5b6ea5c5 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -26,7 +26,7 @@ OverviewMap: `OverviewMap` has the following modifiers: - `.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 default is 25. +- `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. ## Behavior: @@ -55,9 +55,7 @@ var body: some View { .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( - OverviewMap(viewpoint: viewpoint, - visibleArea: visibleArea - ) + OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) .frame(width: 200, height: 132) .padding(), alignment: .topTrailing diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 03b577be7..d0d2dba53 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -56,9 +56,7 @@ struct OverviewMapForMapView: View { .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( - OverviewMap(viewpoint: viewpoint, - visibleArea: visibleArea - ) + OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. // .symbol(.customFillSymbol) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 7c7ac8a26..80366e500 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -81,7 +81,7 @@ public struct OverviewMap: View { public var body: some View { MapView( map: map, - viewpoint: makeViewpoint(), + viewpoint: makeOverviewViewpoint(), graphicsOverlays: [graphicsOverlay] ) .attributionTextHidden() @@ -107,9 +107,9 @@ public struct OverviewMap: View { }) } - /// Creates a new viewpoint based on the `viewpoint` center, scale, and `scaleFactor`. + /// Creates an overview viewpoint based on the observed `viewpoint` center, scale, and `scaleFactor`. /// - Returns: The new `Viewpoint`. - func makeViewpoint() -> Viewpoint? { + func makeOverviewViewpoint() -> Viewpoint? { if let viewpoint = viewpoint, let center = viewpoint.targetGeometry as? Point { return Viewpoint( @@ -125,7 +125,7 @@ public struct OverviewMap: View { /// The `Map` displayed in the `OverviewMap`. /// - Parameter map: The new map. - /// - Returns: The OverviewMap. + /// - Returns: The `OverviewMap`. public func map(_ map: Map) -> OverviewMap { var copy = self copy._map = StateObject(wrappedValue: map) @@ -136,20 +136,21 @@ public struct OverviewMap: View { /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor. /// The default value is 25.0. /// - Parameter scaleFactor: The new scale factor. - /// - Returns: The OverviewMap. + /// - Returns: The `OverviewMap`. public func scaleFactor(_ scaleFactor: Double) -> OverviewMap { var copy = self copy.scaleFactor = scaleFactor return copy } - /// The `Symbol` used to display the main `GeoView` visible area. For MapViews, - /// the symbol will be a FillSymbol used to display the GeoView visible area. For SceneViews, - /// the symbol will be a MarkerSymbol, used to display the current viewpoint's center. - /// For MapViews, the default is a transparent FillSymbol with a red, 1 point width outline; - /// for SceneViews, the default is a red, crosshair SimpleMarkerSymbol. + /// The `Symbol` used to display the main `GeoView` visible area. For `MapView`s, the symbol + /// should be appropriate for visualizing a polygon, as it will be used to draw the visible area. For + /// `SceneView`s, the symbol should be appropriate for visualizing a point, as it will be used to + /// draw the current viewpoint's center. For `MapView`s, the default is a transparent + /// `SimpleFillSymbol` with a red,1 point width outline; for `SceneView`s, the default is a + /// red, crosshair `SimpleMarkerSymbol`. /// - Parameter symbol: The new symbol. - /// - Returns: The OverviewMap. + /// - Returns: The `OverviewMap`. public func symbol(_ symbol: Symbol) -> OverviewMap { var copy = self copy.symbol = symbol From 6ff2fa7b6a765092087db285321d8bc7d615f7bd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 8 Jul 2021 15:18:17 -0500 Subject: [PATCH 0085/1722] PR review updates; new OVM static methods for creation. --- Documentation/OverviewMap/README.md | 7 +++++-- Examples/Examples/OverviewMapExampleView.swift | 7 +++++-- .../ArcGISToolkit/Components/OverviewMap.swift | 17 +++++++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index f5b6ea5c5..bb3bff19e 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -55,7 +55,10 @@ var body: some View { .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( - OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) + OverviewMap.forMapView( + with: viewpoint, + visibleArea: visibleArea + ) .frame(width: 200, height: 132) .padding(), alignment: .topTrailing @@ -77,7 +80,7 @@ var body: some View { SceneView(scene: scene) .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } .overlay( - OverviewMap(viewpoint: viewpoint) + OverviewMap.forSceneView(with: viewpoint) .frame(width: 200, height: 132) .padding(), alignment: .topTrailing diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index d0d2dba53..3b67bd951 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -56,7 +56,10 @@ struct OverviewMapForMapView: View { .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( - OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) + OverviewMap.forMapView( + with: viewpoint, + visibleArea: visibleArea + ) // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. // .symbol(.customFillSymbol) @@ -79,7 +82,7 @@ struct OverviewMapForSceneView: View { SceneView(scene: scene) .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } .overlay( - OverviewMap(viewpoint: viewpoint) + OverviewMap.forSceneView(with: viewpoint) // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. // .symbol(.customMarkerSymbol) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 80366e500..d9ecdd3a5 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -39,11 +39,24 @@ public struct OverviewMap: View { @StateObject private var graphicsOverlay: GraphicsOverlay + public static func forMapView( + with viewpoint: Viewpoint?, + visibleArea: Polygon? + ) -> OverviewMap { + return OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) + } + + public static func forSceneView( + with viewpoint: Viewpoint? + ) -> OverviewMap { + return OverviewMap(viewpoint: viewpoint) + } + /// 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. - public init(viewpoint: Viewpoint?, + internal init(viewpoint: Viewpoint?, visibleArea: Polygon? ) { self.visibleArea = visibleArea @@ -63,7 +76,7 @@ public struct OverviewMap: View { /// Creates an `OverviewMap`. Used for creating an `OverviewMap` for use on a `SceneView`. /// - Parameters: /// - viewpoint: Viewpoint of the main `GeoView` used to update the `OverviewMap` view. - public init(viewpoint: Viewpoint?) { + internal init(viewpoint: Viewpoint?) { self.viewpoint = viewpoint self.visibleArea = nil From 8c0353730ed05ef2acbb0f39a776cea03847e18c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 8 Jul 2021 17:06:35 -0500 Subject: [PATCH 0086/1722] PR review changes; add refdoc for new static methods; remove unnecessary `internal` declaration. --- .../ArcGISToolkit/Components/OverviewMap.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index d9ecdd3a5..a948fe43e 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -21,7 +21,7 @@ public struct OverviewMap: View { /// The `Viewpoint` of the main `GeoView` let viewpoint: Viewpoint? - /// The visible area of the main `GeoView`. Not applicable to SceneViews. + /// The visible area of the main `GeoView`. Not applicable to `SceneView`s. let visibleArea: Polygon? private var symbol: Symbol @@ -39,6 +39,11 @@ public struct OverviewMap: View { @StateObject private var graphicsOverlay: GraphicsOverlay + /// Creates 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. + /// - Returns: A new `OverviewMap`. public static func forMapView( with viewpoint: Viewpoint?, visibleArea: Polygon? @@ -46,6 +51,10 @@ public struct OverviewMap: View { return OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) } + /// Creates an `OverviewMap` for use on a `SceneView`. + /// - Parameter viewpoint: Viewpoint of the main `GeoView` used to update the + /// `OverviewMap` view. + /// - Returns: A new `OverviewMap`. public static func forSceneView( with viewpoint: Viewpoint? ) -> OverviewMap { @@ -56,7 +65,7 @@ public struct OverviewMap: View { /// - 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. - internal init(viewpoint: Viewpoint?, + init(viewpoint: Viewpoint?, visibleArea: Polygon? ) { self.visibleArea = visibleArea @@ -76,7 +85,7 @@ public struct OverviewMap: View { /// Creates an `OverviewMap`. Used for creating an `OverviewMap` for use on a `SceneView`. /// - Parameters: /// - viewpoint: Viewpoint of the main `GeoView` used to update the `OverviewMap` view. - internal init(viewpoint: Viewpoint?) { + init(viewpoint: Viewpoint?) { self.viewpoint = viewpoint self.visibleArea = nil From be1a73b2584600d616d0cadd3e2d49c69a3d494f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 08:09:04 -0500 Subject: [PATCH 0087/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: R Olson --- 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 a948fe43e..7aa3df628 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -41,8 +41,8 @@ public struct OverviewMap: View { /// Creates 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. + /// - 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. /// - Returns: A new `OverviewMap`. public static func forMapView( with viewpoint: Viewpoint?, From d1b34d1ac8ff5f8bdaf0a126716ee6a2f2c751c4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 08:09:13 -0500 Subject: [PATCH 0088/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: R Olson --- 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 7aa3df628..d8766fe07 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -52,7 +52,7 @@ public struct OverviewMap: View { } /// Creates an `OverviewMap` for use on a `SceneView`. - /// - Parameter viewpoint: Viewpoint of the main `GeoView` used to update the + /// - Parameter viewpoint: Viewpoint of the main `SceneView` used to update the /// `OverviewMap` view. /// - Returns: A new `OverviewMap`. public static func forSceneView( From f046d27ac26c72130864e0d99c76eda4fce9c884 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 11:49:29 -0500 Subject: [PATCH 0089/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 d8766fe07..917d86f3c 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -18,7 +18,7 @@ import ArcGIS /// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed /// on an existing `GeoView`, which shows the visible extent of that `GeoView`. public struct OverviewMap: View { - /// The `Viewpoint` of the main `GeoView` + /// The `Viewpoint` of the main `GeoView`. let viewpoint: Viewpoint? /// The visible area of the main `GeoView`. Not applicable to `SceneView`s. From 03105ff283c846a20d88e75f8638e07bf1fa1575 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:10:38 -0500 Subject: [PATCH 0090/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 917d86f3c..bccbdc3f8 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -108,7 +108,7 @@ public struct OverviewMap: View { ) .attributionTextHidden() .interactionModes([]) - .border(Color.black, width: 1) + .border(.black, width: 1) .onAppear(perform: { graphic.symbol = symbol }) From b10db09921a84fbc0b3bc2e3d2993247f9789123 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:10:52 -0500 Subject: [PATCH 0091/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 bccbdc3f8..c644d280f 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -21,7 +21,7 @@ public struct OverviewMap: View { /// The `Viewpoint` of the main `GeoView`. let viewpoint: Viewpoint? - /// The visible area of the main `GeoView`. Not applicable to `SceneView`s. + /// The visible area of the main `GeoView`. Not applicable to `SceneView`s. let visibleArea: Polygon? private var symbol: Symbol From 352976c75da0d468629dac31bb2e997032c43430 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:11:14 -0500 Subject: [PATCH 0092/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 c644d280f..5f01a742e 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -48,7 +48,7 @@ public struct OverviewMap: View { with viewpoint: Viewpoint?, visibleArea: Polygon? ) -> OverviewMap { - return OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) + OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) } /// Creates an `OverviewMap` for use on a `SceneView`. From 142cae9d74724ff29b19fc7afa548dca9c7f5bda Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:11:24 -0500 Subject: [PATCH 0093/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 5f01a742e..a3f0c4424 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -58,7 +58,7 @@ public struct OverviewMap: View { public static func forSceneView( with viewpoint: Viewpoint? ) -> OverviewMap { - return OverviewMap(viewpoint: viewpoint) + OverviewMap(viewpoint: viewpoint) } /// Creates an `OverviewMap`. Used for creating an `OverviewMap` for use on a `MapView`. From 6ef39e2ad321a58ba144a7d2654d0801ba4260a6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:11:31 -0500 Subject: [PATCH 0094/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 a3f0c4424..654adff37 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -155,7 +155,7 @@ public struct OverviewMap: View { } /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display - /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor. + /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor`. /// The default value is 25.0. /// - Parameter scaleFactor: The new scale factor. /// - Returns: The `OverviewMap`. From f328fb44b62d2b948779984782cc7c76b14da1fb Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:11:38 -0500 Subject: [PATCH 0095/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 654adff37..d0f925b71 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -156,7 +156,7 @@ public struct OverviewMap: View { /// 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. + /// The default value is `25.0`. /// - Parameter scaleFactor: The new scale factor. /// - Returns: The `OverviewMap`. public func scaleFactor(_ scaleFactor: Double) -> OverviewMap { From dbf354a96703f11b4f0c4c4d435796babad8ea7c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:11:54 -0500 Subject: [PATCH 0096/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 d0f925b71..cf23fa447 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -169,7 +169,7 @@ public struct OverviewMap: View { /// should be appropriate for visualizing a polygon, as it will be used to draw the visible area. For /// `SceneView`s, the symbol should be appropriate for visualizing a point, as it will be used to /// draw the current viewpoint's center. For `MapView`s, the default is a transparent - /// `SimpleFillSymbol` with a red,1 point width outline; for `SceneView`s, the default is a + /// `SimpleFillSymbol` with a red 1 point width outline; for `SceneView`s, the default is a /// red, crosshair `SimpleMarkerSymbol`. /// - Parameter symbol: The new symbol. /// - Returns: The `OverviewMap`. From 65eff108f8739e3d80bd8b9d39e48d0f761c1c5f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:12:08 -0500 Subject: [PATCH 0097/1722] Update Examples/Examples/OverviewMapExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/OverviewMapExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 3b67bd951..3634310c3 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct OverviewMapExampleView: View { enum MapOrScene { - /// The Example shows a map view. + /// The example shows a map view. case map /// The Example shows a scene view. case scene From f7d0ff6088769514d7c5ebef40a81da70749495e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:12:14 -0500 Subject: [PATCH 0098/1722] Update Examples/Examples/OverviewMapExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/OverviewMapExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 3634310c3..a14159f2d 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -19,7 +19,7 @@ struct OverviewMapExampleView: View { enum MapOrScene { /// The example shows a map view. case map - /// The Example shows a scene view. + /// The example shows a scene view. case scene } From 84a7b28c1e08f7a614799a866e97fb1fb3bdd66d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:12:26 -0500 Subject: [PATCH 0099/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index cf23fa447..b424a3813 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -65,8 +65,9 @@ public struct OverviewMap: View { /// - 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. - init(viewpoint: Viewpoint?, - visibleArea: Polygon? + init( + viewpoint: Viewpoint?, + visibleArea: Polygon? ) { self.visibleArea = visibleArea self.viewpoint = viewpoint From a98a1722c7704ae56051a5d1d6f3b959a18ae7f9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:12:36 -0500 Subject: [PATCH 0100/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 b424a3813..6559d6ffa 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -77,7 +77,7 @@ public struct OverviewMap: View { // It is necessary to set the graphic and graphicsOverlay this way // in order to prevent the main geoview from recreating the - // graphicsOverlay every draw cycle. That was causing refresh issues + // graphicsOverlay every draw cycle. That was causing refresh issues // with the graphic during panning/zooming/rotating. _graphic = StateObject(wrappedValue: graphic) _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) From d2452ed560b82ba0a4fe1980f98510f9deca3a41 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:12:46 -0500 Subject: [PATCH 0101/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- 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 6559d6ffa..1304eaec0 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -95,7 +95,7 @@ public struct OverviewMap: View { // It is necessary to set the graphic and graphicsOverlay this way // in order to prevent the main geoview from recreating the - // graphicsOverlay every draw cycle. That was causing refresh issues + // graphicsOverlay every draw cycle. That was causing refresh issues // with the graphic during panning/zooming/rotating. _graphic = StateObject(wrappedValue: graphic) _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) From 68e1c229cc7cc43d0414f446cc000a535da2c61f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 12:13:29 -0500 Subject: [PATCH 0102/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 1304eaec0..ec2780076 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -133,15 +133,11 @@ public struct OverviewMap: View { /// Creates an overview viewpoint based on the observed `viewpoint` center, scale, and `scaleFactor`. /// - Returns: The new `Viewpoint`. func makeOverviewViewpoint() -> Viewpoint? { - if let viewpoint = viewpoint, - let center = viewpoint.targetGeometry as? Point { - return Viewpoint( - center: center, - scale: viewpoint.targetScale * scaleFactor - ) - } else { - return nil - } + guard let viewpoint = viewpoint, let center = viewpoint.targetGeometry as? Point else { return nil } + return Viewpoint( + center: center, + scale: viewpoint.targetScale * scaleFactor + ) } // MARK: Modifiers From c5af9b7b7b9cdf3f0aaef05dc0e822519003f6d3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 13:07:01 -0500 Subject: [PATCH 0103/1722] Remove unnecessary type specifications. --- 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 ec2780076..73b957617 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -26,10 +26,10 @@ public struct OverviewMap: View { private var symbol: Symbol - private var scaleFactor: Double = 25.0 + private var scaleFactor = 25.0 @StateObject - private var map: Map = Map(basemapStyle: .arcGISTopographic) + private var map = Map(basemapStyle: .arcGISTopographic) /// The `Graphic` displaying the visible area of the main `GeoView`. @StateObject From 782846954e06e96744cf96016cfc5e99a19df659 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 9 Jul 2021 14:05:28 -0500 Subject: [PATCH 0104/1722] Update Examples/Examples/OverviewMapExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/OverviewMapExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index a14159f2d..c0a2cb800 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -124,6 +124,6 @@ private extension Symbol { } private extension Map { - /// A custom map for the OverviewMap. - static let customOverviewMap: Map = Map(basemapStyle: .arcGISDarkGray) + /// A custom map for the `OverviewMap`. + static let customOverviewMap = Map(basemapStyle: .arcGISDarkGray) } From 5f4e1836400894ab2d2710cb87154cfe6285e1f9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 10:57:55 -0500 Subject: [PATCH 0105/1722] Start supporting viewpoint and search selections. --- Examples/Examples/SearchExampleView.swift | 46 ++++++-- .../Search/LocatorSearchSource.swift | 32 +++-- .../Components/Search/SearchResult.swift | 6 + .../Components/Search/SearchView.swift | 110 ++++++++++++------ .../Components/Search/SearchViewModel.swift | 45 ++++--- .../Search/SmartLocatorSearchSource.swift | 33 ++---- 6 files changed, 171 insertions(+), 101 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 9b854aec1..c1ea8986f 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -18,22 +18,48 @@ import ArcGISToolkit struct SearchExampleView: View { let searchViewModel = SearchViewModel( - sources: [LocatorSearchSource(displayName: "Locator One", - maximumResults: 3, - maximumSuggestions: 3 - ), - LocatorSearchSource(displayName: "Locator Two", - maximumResults: 1, - maximumSuggestions: 1 - )] + sources: [LocatorSearchSource(displayName: "Locator One"), + LocatorSearchSource(displayName: "Locator Two")] ) + @State + var showResults = true + + @State + private var viewpoint: Viewpoint? + + @State + private var visibleArea: ArcGIS.Polygon? + var body: some View { ZStack (alignment: .topTrailing) { MapViewReader { proxy in MapView(map: Map(basemap: Basemap.imageryWithLabels())) - SearchView(proxy: proxy, searchViewModel:searchViewModel) - .padding() + .onViewpointChanged(type: .centerAndScale) { + searchViewModel.queryCenter = $0.targetGeometry as? Point + } + .onVisibleAreaChanged { + searchViewModel.queryArea = $0 + } + .onChange(of: searchViewModel.results, perform: { results in + print("Search results changed") + }) + .onChange(of: searchViewModel.suggestions, perform: { results in + print("Search suggestions changed") + }) + .overlay( + SearchView(proxy: proxy, searchViewModel:searchViewModel) + .enableResultListView(showResults) + .frame(width: 360) + .padding(), + alignment: .topTrailing + ) + Button { + showResults.toggle() + } label: { + Text(showResults ? "Hide results" : "Show results") + } + } } } diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index dec82361d..9479eb1f5 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -16,7 +16,7 @@ import ArcGIS /// Uses a Locator to provide search and suggest results. Most configuration should be done on the /// `GeocodeParameters` directly. -public class LocatorSearchSource: ObservableObject { +public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { public init(displayName: String = "Search", maximumResults: Int = 6, maximumSuggestions: Int = 6, @@ -60,22 +60,9 @@ public class LocatorSearchSource: ObservableObject { public var searchArea: Geometry? public var preferredSearchLocation: Point? -} - -extension LocatorSearchSource: SearchSourceProtocol { - public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { - suggestParameters.searchArea = searchArea - suggestParameters.preferredSearchLocation = preferredSearchLocation - let suggestResults = try await locator.suggest(searchText: queryString, - parameters: suggestParameters - ) - //convert to SearchSuggestions - return suggestResults.map{ $0.toSearchSuggestion(searchSource: self) } - } - public func search(_ queryString: String, area: Geometry? = nil) async throws -> [SearchResult] { - geocodeParameters.searchArea = searchArea + geocodeParameters.searchArea = (area != nil) ? area : searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locator.geocode(searchText: queryString, @@ -89,12 +76,23 @@ extension LocatorSearchSource: SearchSourceProtocol { public func search(_ searchSuggestion: SearchSuggestion, area: Geometry? = nil) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] } - geocodeParameters.searchArea = searchArea + geocodeParameters.searchArea = (area != nil) ? area : searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation - let geocodeResults = try await locator.geocode(suggestResult: suggestResult, + let geocodeResults = try await locator.geocode(suggestResult: suggestResult, parameters: geocodeParameters ) return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } + + public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { + suggestParameters.searchArea = searchArea + suggestParameters.preferredSearchLocation = preferredSearchLocation + + let suggestResults = try await locator.suggest(searchText: queryString, + parameters: suggestParameters + ) + //convert to SearchSuggestions + return suggestResults.map{ $0.toSearchSuggestion(searchSource: self) } + } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index c9e877b59..8915f463f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -60,3 +60,9 @@ public class SearchResult { extension SearchResult: Identifiable { public var id: ObjectIdentifier { ObjectIdentifier(self) } } + +extension SearchResult: Equatable { + public static func == (lhs: SearchResult, rhs: SearchResult) -> Bool { + lhs === rhs + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a0e64c4e3..3d9821942 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -19,16 +19,11 @@ import ArcGIS public struct SearchView: View { public init(proxy: GeoViewProxy, searchViewModel: SearchViewModel, - enableAutomaticConfiguration: Bool = true, - enableRepeatSearchHereButton: Bool = true, - enableResultListView: Bool = true, - noResultMessage: String = "No results found") { + viewpoint: Binding? = nil + ) { self.proxy = proxy self.searchViewModel = searchViewModel - self.enableAutomaticConfiguration = enableAutomaticConfiguration - self.enableRepeatSearchHereButton = enableRepeatSearchHereButton - self.enableResultListView = enableResultListView - self.noResultMessage = noResultMessage + self.viewpoint = viewpoint } /// Used for accessing `GeoView` functionality for geocoding and searching. @@ -47,38 +42,31 @@ public struct SearchView: View { @ObservedObject var searchViewModel: SearchViewModel - /// Determines whether the view will update its configuration based on the attached geoview's - /// document automatically. - var enableAutomaticConfiguration: Bool = true + private var enableAutomaticConfiguration = true - /// Determines whether a button that allows the user to repeat a search with a spatial constraint - /// is displayed automatically. Set to false if you want to use a custom button, for example so that - /// you can place it elsewhere on the map. `SearchViewModel` has properties and methods - /// you can use to determine when the custom button should be visible and to trigger the search - /// repeat behavior. @State - var enableRepeatSearchHereButton: Bool = true + private var enableRepeatSearchHereButton = true - /// Determines whether a built-in result view will be shown. If false, the result display/selection - /// list is not shown. Set to false if you want to define a custom result list. You might use a - /// custom result list to show results in a separate list, disconnected from the rest of the search view. - var enableResultListView: Bool = true + @State + private var enableResultListView = true - /// Message to show when there are no results or suggestions. - var noResultMessage: String = "No results found" + @State + private var noResultMessage = "No results found" /// Indicates that the `SearchViewModel` should start a search. @State - private var commitSearch: Bool = false + private var commitSearch = false /// Indicates that the `SearchViewModel` should accept a suggestion. @State private var currentSuggestion: SearchSuggestion? + private var viewpoint: Binding? + /// Indicates that the geoView's viewpoint has changed since the last search. @State private var viewpointChanged: Bool = false - + // TODO: Figure out better styling for list // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource // TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. @@ -100,10 +88,13 @@ public struct SearchView: View { commitSearch.toggle() } } - SearchResultList(searchResults: searchViewModel.results) - SearchSuggestionList(searchSuggestions: searchViewModel.suggestions, - currentSuggestion: $currentSuggestion - ) + if enableResultListView { + SearchResultList(searchViewModel: searchViewModel) + SearchSuggestionList( + searchViewModel: searchViewModel, + currentSuggestion: $currentSuggestion + ) + } } .task(id: searchViewModel.currentQuery) { // User typed a new character @@ -123,17 +114,67 @@ public struct SearchView: View { } } } + + // MARK: Modifiers + + /// Determines whether the view will update its configuration based on the geoview's + /// document automatically. Defaults to `true`. + /// - Parameter enableAutomaticConfiguration: The new value. + /// - Returns: The `SearchView`. + public func enableAutomaticConfiguration(_ enableAutomaticConfiguration: Bool) -> SearchView { + var copy = self + copy.enableAutomaticConfiguration = enableAutomaticConfiguration + return copy + } + + /// Determines whether a button that allows the user to repeat a search with a spatial constraint + /// is displayed automatically. Set to `false` if you want to use a custom button, for example so that + /// you can place it elsewhere on the map. `SearchViewModel` has properties and methods + /// you can use to determine when the custom button should be visible and to trigger the search + /// repeat behavior. Defaults to `true`. + /// - Parameter enableRepeatSearchHereButton: The new value. + /// - Returns: The `SearchView`. + public func enableRepeatSearchHereButton( + _ enableRepeatSearchHereButton: Bool + ) -> SearchView { + self.enableRepeatSearchHereButton = enableRepeatSearchHereButton + return self + } + + /// Determines whether a built-in result view will be shown. If `false`, the result display/selection + /// list is not shown. Set to `false` if you want to define a custom result list. You might use a + /// custom result list to show results in a separate list, disconnected from the rest of the search view. + /// Defaults to `true`. + /// - Parameter enableResultListView: The new value. + /// - Returns: The `SearchView`. + public func enableResultListView(_ enableResultListView: Bool) -> SearchView { + // TODO: this doesn't work; the view doesn't redraw +// self._enableResultListView.wrappedValue = enableResultListView + self.enableResultListView = enableResultListView + return self + } + + /// Message to show when there are no results or suggestions. Defaults to "No results found". + /// - Parameter noResultMessage: The new value. + /// - Returns: The `SearchView`. + public func noResultMessage(_ noResultMessage: String) -> SearchView { + self.noResultMessage = noResultMessage + return self + } } +// TODO: why no results? Why .onChange for results/suggestions don't work +// TODO: get currentResult working in Example. + // TODO: look at consolidating SearchResultView and SearchSuggestionView with // TODO: new SearchDisplayProtocol containing only displayTitle and displaySubtitle // TODO: That would mean we only needed one of these. struct SearchResultList: View { - var searchResults: Result<[SearchResult]?, Error> - + var searchViewModel: SearchViewModel + var body: some View { Group { - switch searchResults { + switch searchViewModel.results { case .success(let results): if let results = results, results.count > 0 { List { @@ -151,6 +192,7 @@ struct SearchResultList: View { SearchResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) } .onTapGesture { + searchViewModel.selectedResult = result print("user selected result: \(result.displayTitle)") } } @@ -172,12 +214,12 @@ struct SearchResultList: View { } struct SearchSuggestionList: View { - var searchSuggestions: Result<[SearchSuggestion]?, Error> + var searchViewModel: SearchViewModel var currentSuggestion: Binding var body: some View { Group { - switch searchSuggestions { + switch searchViewModel.suggestions { case .success(let results): if let suggestions = results, suggestions.count > 0 { List { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 400b65adb..e9583e031 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -33,9 +33,9 @@ activeSource: SearchSourceProtocol? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, -results: Result<[SearchResult]?, Error> = .success(nil), +results: Result<[SearchResult]?, RuntimeError> = .success(nil), sources: [SearchSourceProtocol] = [], -suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) +suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) ) { self.init() self.defaultPlaceHolder = defaultPlaceHolder @@ -50,17 +50,17 @@ suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) /// The string shown in the search view when no user query is entered. /// Default is "Find a place or address", or read from web map JSON if specified in the web map configuration. - var defaultPlaceHolder: String = "Find a place or address" + public var defaultPlaceHolder: String = "Find a place or address" /// Tracks the currently active search source. All sources are used if this property is `nil`. - var activeSource: SearchSourceProtocol? + public var activeSource: SearchSourceProtocol? /// Tracks the current user-entered query. This should be updated by the view after every key press. /// This property drives both suggestions and searches. This property can be changed by /// other method calls and property changes within the view model, so the view should take care to /// observe for changes. @Published - var currentQuery: String = "" { + public var currentQuery: String = "" { didSet { selectedResult = nil if currentQuery.isEmpty { @@ -73,45 +73,50 @@ suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) /// The search area to be used for the current query. Ignored in most queries, unless the /// `RestrictToArea` property is set to true when calling `commitSearch`. This property /// should be updated as the user navigates the map/scene, or at minimum before calling `commitSearch`. - var queryArea: Geometry? + public var queryArea: Geometry? { + didSet { + isEligibleForRequery = true + } + } /// Defines the center for the search. This should be updated by the view every time the /// user navigates the map. - var queryCenter: Point? + public var queryCenter: Point? /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate /// number of results is returned based on the type of suggestion chosen (driven by the IsCollection property). - var resultMode: SearchResultMode = .automatic + public var resultMode: SearchResultMode = .automatic /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. @Published - var results: Result<[SearchResult]?, Error> = .success([]) - +// public var results: [SearchResult]? = nil + public var results: Result<[SearchResult]?, RuntimeError> = .success([]) + /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. - @Published - var selectedResult: SearchResult? + @State + public var selectedResult: SearchResult? /// Collection of search sources to be used. This list is maintained over time and is not nullable. /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. - var sources: [SearchSourceProtocol] = [] + public var sources: [SearchSourceProtocol] = [] /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. @Published - var suggestions: Result<[SearchSuggestion]?, Error> = .success([]) - +// public var suggestions: [SearchSuggestion]? = nil + public var suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success([]) + /// True if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to /// this property are driven by changes to the `queryArea` property. @Published - //TODO: should be reset if viewpoint has changed since last result returned. private(set) var isEligibleForRequery: Bool = false /// Starts a search. `selectedResult` and `results`, among other properties, are set @@ -157,7 +162,7 @@ suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) func updateSuggestions() async -> Void { guard !currentQuery.isEmpty else { return } print("SearchViewModel.updateSuggestions: \(currentQuery)") - + var suggestionResults = [SearchSuggestion]() let searchSources = sourcesToSearch() for i in 0...searchSources.count - 1 { @@ -178,7 +183,9 @@ suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) } suggestions = .success(suggestionResults) results = .success(nil) + selectedResult = nil + isEligibleForRequery = false } /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is @@ -193,7 +200,8 @@ suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) print("SearchViewModel.acceptSuggestion: \(currentQuery)") isEligibleForRequery = false - + selectedResult = nil + var searchResults = [SearchResult]() let searchResult = await Result { try await searchSuggestion.owningSource.search(searchSuggestion, area: nil) @@ -225,7 +233,6 @@ suggestions: Result<[SearchSuggestion]?, Error> = .success(nil) results = .success(searchResults) suggestions = .success(nil) - selectedResult = nil } /// Configures the view model for the provided map. By default, will only configure the view model diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index b4158be9e..677e94606 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -27,7 +27,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { repeatSuggestResultThreshold: Int = 6, resultSymbolStyle: SymbolStyle? = nil) { self.init() -// self.objectWillChange = objectWillChange self.displayName = displayName self.maximumSuggestions = maximumSuggestions self.searchArea = searchArea @@ -40,26 +39,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { suggestParameters.maxResults = Int32(maximumSuggestions) } -// public var displayName: String -// -// public var maximumSuggestions: Int -// -// public var searchArea: Geometry? -// -// public var preferredSearchLocation: Point? -// -// public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { -// <#code#> -// } -// -// public func search(_ queryString: String, area: Geometry?) async throws -> [SearchResult] { -// <#code#> -// } -// -// public func search(_ searchSuggestion: SearchSuggestion, area: Geometry?) async throws -> [SearchResult] { -// <#code#> -// } - /// The minimum number of results to attempt to return. If there are too few results, the search is /// repeated with loosened parameters until enough results are accumulated. If no search is /// successful, it is still possible to have a total number of results less than this threshold. Does not @@ -80,4 +59,16 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// by publishing a web style, taking care to ensure that symbol keys match the `Type` attribute /// returned by the locator. var resultSymbolStyle: SymbolStyle? + + public override func search(_ queryString: String, area: Geometry?) async throws -> [SearchResult] { + return try await super.search(queryString, area: area) + } + + public override func search(_ searchSuggestion: SearchSuggestion, area: Geometry?) async throws -> [SearchResult] { + return try await super.search(searchSuggestion, area: area) + } + + public override func suggest(_ queryString: String) async throws -> [SearchSuggestion] { + return try await super.suggest(queryString) + } } From c1df97610995b8da13955528e078090d8e097141 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 11:01:41 -0500 Subject: [PATCH 0106/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Philip Ridgeway --- Documentation/OverviewMap/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index bb3bff19e..3f7e27781 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -25,7 +25,7 @@ OverviewMap: `OverviewMap` has the following modifiers: -- `.map(_ map: Map)` - The `Map` displayed in the `OverviewMap`. For example, you can use `.map()` to display a custom base map in the `OverviewMap`. +- `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. @@ -89,4 +89,3 @@ var body: some View { ``` 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 785bf1cdd4203a2ee244706a4a569aabee878b67 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 11:01:59 -0500 Subject: [PATCH 0107/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Philip Ridgeway --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 3f7e27781..f24315d92 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -26,7 +26,7 @@ OverviewMap: `OverviewMap` has the following modifiers: - `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. +- `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. ## Behavior: From 1781f05d1a91ae3a63f680c93240d6d1be3e5552 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 11:02:25 -0500 Subject: [PATCH 0108/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Philip Ridgeway --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index f24315d92..199146026 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -39,7 +39,7 @@ For an `OverviewMap` on a `SceneView`, the center point of the `SceneView`'s `cu ### Basic usage for overlaying a `MapView` -Note that for `MapView`s, you need to provide the `OverviewMap` both a viewpoint and visibleArea. +Note that for a `MapView`, you need to provide the `OverviewMap` both a viewpoint and a polygon representing the visible area. ```swift let map = Map(basemapStyle: .arcGISImagery) From fbf13d80c2a4863af1c2c75b2ea9c813b0793376 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 11:02:52 -0500 Subject: [PATCH 0109/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Philip Ridgeway --- Documentation/OverviewMap/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 199146026..b2fa0ad08 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -42,7 +42,8 @@ For an `OverviewMap` on a `SceneView`, the center point of the `SceneView`'s `cu Note that for a `MapView`, you need to provide the `OverviewMap` both a viewpoint and a polygon representing the visible area. ```swift -let map = Map(basemapStyle: .arcGISImagery) +@StateObject +var map = Map(basemapStyle: .arcGISImagery) @State private var viewpoint: Viewpoint? From 33797d03f34329a711fdaed9f3080ca8550b5b5c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 11:03:06 -0500 Subject: [PATCH 0110/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Philip Ridgeway --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index b2fa0ad08..445715aad 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -69,7 +69,7 @@ var body: some View { ### Basic usage for overlaying a `SceneView` -Note that for `SceneView`s, you need to provide the `OverviewMap` only a viewpoint. +Note that for a `SceneView`, you need to provide the `OverviewMap` only a viewpoint. ```swift let scene = Scene(basemap: .imageryWithLabels()) From adbd6eaa8784be3d6e65f79867fa728f92443b86 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 11:03:34 -0500 Subject: [PATCH 0111/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Philip Ridgeway --- Documentation/OverviewMap/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 445715aad..4bf288faf 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -72,7 +72,8 @@ var body: some View { Note that for a `SceneView`, you need to provide the `OverviewMap` only a viewpoint. ```swift -let scene = Scene(basemap: .imageryWithLabels()) +@StateObject +var scene = Scene(basemap: .imageryWithLabels()) @State private var viewpoint: Viewpoint? From 03a07cff42208275db66bb5d4d85cbd6db15cf3f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 11:42:15 -0500 Subject: [PATCH 0112/1722] PR review updates. --- Documentation/OverviewMap/README.md | 2 +- .../Components/OverviewMap.swift | 32 +++---------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index bb3bff19e..da1ae96c1 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -1,6 +1,6 @@ # OverviewMap -`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be overlayed on an existing `GeoView` (`MapView` or `SceneView`). `OverviewMap` shows shows a representation of the current `visibleArea` (for a `MapView`) or `viewpoint` (for a `SceneView`) of the `GeoView` it is connected to. +`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be overlayed on an existing `GeoView` (`MapView` or `SceneView`). `OverviewMap` shows a representation of the current `visibleArea` (for a `MapView`) or `viewpoint` (for a `SceneView`). Map View diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 73b957617..939876958 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -48,7 +48,7 @@ public struct OverviewMap: View { with viewpoint: Viewpoint?, visibleArea: Polygon? ) -> OverviewMap { - OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea) + OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea, symbol: .defaultFill) } /// Creates an `OverviewMap` for use on a `SceneView`. @@ -58,7 +58,7 @@ public struct OverviewMap: View { public static func forSceneView( with viewpoint: Viewpoint? ) -> OverviewMap { - OverviewMap(viewpoint: viewpoint) + OverviewMap(viewpoint: viewpoint, symbol: .defaultMarker) } /// Creates an `OverviewMap`. Used for creating an `OverviewMap` for use on a `MapView`. @@ -67,30 +67,13 @@ public struct OverviewMap: View { /// - visibleArea: Visible area of the main `GeoView` used to display the extent graphic. init( viewpoint: Viewpoint?, - visibleArea: Polygon? + visibleArea: Polygon? = nil, + symbol: Symbol ) { self.visibleArea = visibleArea self.viewpoint = viewpoint + self.symbol = symbol - self.symbol = .defaultFill - let graphic = Graphic(symbol: self.symbol) - - // It is necessary to set the graphic and graphicsOverlay this way - // in order to prevent the main geoview from recreating the - // graphicsOverlay every draw cycle. That was causing refresh issues - // with the graphic during panning/zooming/rotating. - _graphic = StateObject(wrappedValue: graphic) - _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) - } - - /// Creates an `OverviewMap`. Used for creating an `OverviewMap` for use on a `SceneView`. - /// - Parameters: - /// - viewpoint: Viewpoint of the main `GeoView` used to update the `OverviewMap` view. - init(viewpoint: Viewpoint?) { - self.viewpoint = viewpoint - self.visibleArea = nil - - self.symbol = .defaultMarker let graphic = Graphic(symbol: self.symbol) // It is necessary to set the graphic and graphicsOverlay this way @@ -177,13 +160,8 @@ public struct OverviewMap: View { } } - // MARK: Extensions -extension Graphic: ObservableObject {} -extension GraphicsOverlay: ObservableObject {} -extension Symbol: ObservableObject {} - private extension Symbol { /// The default marker symbol. static let defaultMarker: MarkerSymbol = SimpleMarkerSymbol( From 39570ba9a36e217707d9992e007dfb53cc0cae00 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Jul 2021 15:56:05 -0500 Subject: [PATCH 0113/1722] Update for viewpointType change. --- Examples/Examples/OverviewMapExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index c0a2cb800..382a31e38 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -53,7 +53,7 @@ struct OverviewMapForMapView: View { var body: some View { MapView(map: map) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( OverviewMap.forMapView( @@ -80,7 +80,7 @@ struct OverviewMapForSceneView: View { var body: some View { SceneView(scene: scene) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay( OverviewMap.forSceneView(with: viewpoint) // These modifiers show how you can modify the default From 651e717e68250cec0098fd4f4314662dc4439c06 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Jul 2021 10:39:08 -0500 Subject: [PATCH 0114/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Zachary A Kline --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index bf2f87edf..1f8b8d341 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -2,7 +2,7 @@ `OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be overlayed on an existing `GeoView` (`MapView` or `SceneView`). `OverviewMap` shows a representation of the current `visibleArea` (for a `MapView`) or `viewpoint` (for a `SceneView`). -Map View +MapView ![OverviewMap - MapView](./OverviewMap_MapView.png) From 99aea780689acd03489d312e203384728fd67ebf Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Jul 2021 15:15:56 -0500 Subject: [PATCH 0115/1722] Add conformances back --- Examples/Examples.xcodeproj/project.pbxproj | 8 +++++++ .../Extensions/Graphic+ObservableObject.swift | 22 +++++++++++++++++++ .../GraphicsOverlay+ObservableObject.swift | 22 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift create mode 100644 Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 6cdf3059d..82399a042 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( + E4B60CB226A09F59001C4955 /* Extensions */, E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, ); name = Examples; @@ -131,6 +132,13 @@ name = Frameworks; sourceTree = ""; }; + E4B60CB226A09F59001C4955 /* Extensions */ = { + isa = PBXGroup; + children = ( + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ diff --git a/Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift b/Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift new file mode 100644 index 000000000..7e0a7d750 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift @@ -0,0 +1,22 @@ +// +// COPYRIGHT 1995-2021 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 SwiftUI +import ArcGIS + +extension Graphic: ObservableObject {} diff --git a/Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift b/Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift new file mode 100644 index 000000000..b457c5ea2 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift @@ -0,0 +1,22 @@ +// +// COPYRIGHT 1995-2021 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 SwiftUI +import ArcGIS + +extension GraphicsOverlay: ObservableObject {} From b9348acb79fde5b230c65039038504d804d78c4c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Jul 2021 15:26:41 -0500 Subject: [PATCH 0116/1722] Update Examples/Examples/OverviewMapExampleView.swift Co-authored-by: Philip Ridgeway --- Examples/Examples/OverviewMapExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 382a31e38..f21a6867f 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -24,7 +24,7 @@ struct OverviewMapExampleView: View { } @State - var mapOrScene: MapOrScene = .map + private var mapOrScene: MapOrScene = .map var body: some View { Picker("Map or Scene", selection: $mapOrScene, content: { From 97612bfbf0d405aeea321310a3091cb75147f44b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Jul 2021 15:27:02 -0500 Subject: [PATCH 0117/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Philip Ridgeway --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 1f8b8d341..c49a9559f 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -53,7 +53,7 @@ private var visibleArea: ArcGIS.Polygon? var body: some View { MapView(map: map) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .overlay( OverviewMap.forMapView( From 8620048f047cb0540685ddb49482c6477f1c432e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Jul 2021 16:24:54 -0500 Subject: [PATCH 0118/1722] Fix header info. --- .../Extensions/Graphic+ObservableObject.swift | 29 ++++++++----------- .../GraphicsOverlay+ObservableObject.swift | 29 ++++++++----------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift b/Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift index 7e0a7d750..e102a6e80 100644 --- a/Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift +++ b/Sources/ArcGISToolkit/Extensions/Graphic+ObservableObject.swift @@ -1,20 +1,15 @@ -// -// COPYRIGHT 1995-2021 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 -// +// Copyright 2021 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 diff --git a/Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift b/Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift index b457c5ea2..8930951f9 100644 --- a/Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift +++ b/Sources/ArcGISToolkit/Extensions/GraphicsOverlay+ObservableObject.swift @@ -1,20 +1,15 @@ -// -// COPYRIGHT 1995-2021 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 -// +// Copyright 2021 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 From 5ce5bb0bf223740d5b8636438e6a0fc54f202c8a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Jul 2021 10:21:07 -0500 Subject: [PATCH 0119/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Ting Chen --- 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 939876958..37424909e 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -135,7 +135,7 @@ public struct OverviewMap: View { } /// The factor to multiply the main `GeoView`'s scale by. The `OverviewMap` will display - /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor`. + /// at the a scale equal to: `viewpoint.targetScale` x `scaleFactor`. /// The default value is `25.0`. /// - Parameter scaleFactor: The new scale factor. /// - Returns: The `OverviewMap`. From fd91ed8fbc649ed5935495f2c1ed9dcb25f0d513 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Jul 2021 10:21:18 -0500 Subject: [PATCH 0120/1722] Update Documentation/OverviewMap/README.md Co-authored-by: Ting Chen --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index c49a9559f..e86f3e1da 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -26,7 +26,7 @@ OverviewMap: `OverviewMap` has the following modifiers: - `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`. +- `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. ## Behavior: From eb3c9c5112434bfc17431e8734c536f664de2746 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Jul 2021 10:31:38 -0500 Subject: [PATCH 0121/1722] overlayed -> overlaid --- Documentation/OverviewMap/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index e86f3e1da..7c91f7215 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -1,6 +1,6 @@ # OverviewMap -`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be overlayed on an existing `GeoView` (`MapView` or `SceneView`). `OverviewMap` shows a representation of the current `visibleArea` (for a `MapView`) or `viewpoint` (for a `SceneView`). +`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be overlaid on an existing `GeoView` (`MapView` or `SceneView`). `OverviewMap` shows a representation of the current `visibleArea` (for a `MapView`) or `viewpoint` (for a `SceneView`). MapView From a48c1564bd0cce1e591ba0d3bb1e3e4dd020793a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Jul 2021 10:35:26 -0500 Subject: [PATCH 0122/1722] Remove moved "Extensions" group. --- Examples/Examples.xcodeproj/project.pbxproj | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 82399a042..6cdf3059d 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -67,7 +67,6 @@ E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( - E4B60CB226A09F59001C4955 /* Extensions */, E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, ); name = Examples; @@ -132,13 +131,6 @@ name = Frameworks; sourceTree = ""; }; - E4B60CB226A09F59001C4955 /* Extensions */ = { - isa = PBXGroup; - children = ( - ); - path = Extensions; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ From b13e0714694a8c2b0cc88b8c6030bdc4b397f4c6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Jul 2021 10:39:12 -0500 Subject: [PATCH 0123/1722] Fix "already owned" map issue. --- Examples/Examples/OverviewMapExampleView.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index f21a6867f..18b7cc605 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -63,7 +63,7 @@ struct OverviewMapForMapView: View { // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. // .symbol(.customFillSymbol) -// .map(.customOverviewMap) +// .map(.customOverviewMapForMapView) // .scaleFactor(15.0) .frame(width: 200, height: 132) .padding(), @@ -86,7 +86,7 @@ struct OverviewMapForSceneView: View { // These modifiers show how you can modify the default // values used for the symbol, map, and scaleFactor. // .symbol(.customMarkerSymbol) -// .map(.customOverviewMap) +// .map(.customOverviewMapForSceneView) // .scaleFactor(15.0) .frame(width: 200, height: 132) .padding(), @@ -124,6 +124,9 @@ private extension Symbol { } private extension Map { - /// A custom map for the `OverviewMap`. - static let customOverviewMap = Map(basemapStyle: .arcGISDarkGray) + /// 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) } From cc4cbb16d88b0119e81594414c0651e2b2b8e3c2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 19 Jul 2021 17:43:08 -0500 Subject: [PATCH 0124/1722] Display search results in `onChanged` modifier in Example --- Examples/Examples/SearchExampleView.swift | 115 +++++++++++----- .../Search/LocatorSearchSource.swift | 4 +- .../Components/Search/SearchResult.swift | 12 +- .../Components/Search/SearchView.swift | 125 +++++++++--------- .../Components/Search/SearchViewModel.swift | 18 +-- .../Extensions/GeocodeResult.swift | 23 +++- 6 files changed, 179 insertions(+), 118 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index c1ea8986f..e95779849 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -17,51 +17,97 @@ import ArcGIS import ArcGISToolkit struct SearchExampleView: View { - let searchViewModel = SearchViewModel( - sources: [LocatorSearchSource(displayName: "Locator One"), - LocatorSearchSource(displayName: "Locator Two")] + @ObservedObject + var searchViewModel = SearchViewModel( + sources: [LocatorSearchSource( + displayName: "Locator One", + maximumResults: 16, + maximumSuggestions: 16)]/*, + LocatorSearchSource(displayName: "Locator Two")]*/ ) @State var showResults = true - @State - private var viewpoint: Viewpoint? + let map = Map(basemapStyle: .arcGISImagery) @State - private var visibleArea: ArcGIS.Polygon? - + var searchResultViewpoint: Viewpoint? = nil + + var searchResultsOverlay = GraphicsOverlay() + var body: some View { - ZStack (alignment: .topTrailing) { - MapViewReader { proxy in - MapView(map: Map(basemap: Basemap.imageryWithLabels())) - .onViewpointChanged(type: .centerAndScale) { - searchViewModel.queryCenter = $0.targetGeometry as? Point - } - .onVisibleAreaChanged { - searchViewModel.queryArea = $0 - } - .onChange(of: searchViewModel.results, perform: { results in - print("Search results changed") - }) - .onChange(of: searchViewModel.suggestions, perform: { results in - print("Search suggestions changed") - }) - .overlay( - SearchView(proxy: proxy, searchViewModel:searchViewModel) - .enableResultListView(showResults) - .frame(width: 360) - .padding(), - alignment: .topTrailing - ) + MapView( + map: map, + viewpoint: searchResultViewpoint, + graphicsOverlays: [searchResultsOverlay] + ) + .onViewpointChanged(kind: .centerAndScale) { + searchViewModel.queryCenter = $0.targetGeometry as? Point + } + .onVisibleAreaChanged { + searchViewModel.queryArea = $0 + } + .overlay( + VStack { Button { showResults.toggle() } label: { Text(showResults ? "Hide results" : "Show results") } - + SearchView(searchViewModel: searchViewModel) + .enableResultListView(showResults) + .frame(width: 360) + .padding() + }, + alignment: .topTrailing + ) + .onChange(of: searchViewModel.results, perform: { searchResults in + display(searchResults: searchResults) + }) + .onChange(of: searchViewModel.selectedResult, perform: { _ in + display(selectedResult: searchViewModel.selectedResult) + }) + } + + fileprivate func display(searchResults: Result<[SearchResult]?, RuntimeError>) { + let searchResultEnvelopeBuilder = EnvelopeBuilder(spatialReference: .wgs84) + switch searchResults { + case .success(let results): + var resultGraphics = [Graphic]() + results?.forEach({ result in + if let extent = result.selectionViewpoint?.targetGeometry as? Envelope { + searchResultEnvelopeBuilder.union(envelope: extent) + } + let graphic = Graphic(geometry: result.geoElement?.geometry, + symbol: .resultSymbol) + resultGraphics.append(graphic) + + }) + let currentGraphics = searchResultsOverlay.graphics + searchResultsOverlay.removeGraphics(currentGraphics) + searchResultsOverlay.addGraphics(resultGraphics) + + if resultGraphics.count > 0, + let envelope = searchResultEnvelopeBuilder.toGeometry() as? Envelope { + searchResultViewpoint = Viewpoint(targetExtent: envelope) } + case .failure(_): + break + } + } + + fileprivate func display(selectedResult: SearchResult?) { + guard let selectedResult = selectedResult, + let graphic = selectedResult.geoElement as? Graphic else { return } + + searchResultViewpoint = selectedResult.selectionViewpoint + if graphic.symbol == nil { + graphic.symbol = .resultSymbol } + let currentGraphics = searchResultsOverlay.graphics + searchResultsOverlay.removeGraphics(currentGraphics) + searchResultsOverlay.addGraphic(graphic) } } @@ -70,3 +116,12 @@ struct SearchExampleView_Previews: PreviewProvider { SearchExampleView() } } + +private extension Symbol { + /// A search result marker symbol. + static let resultSymbol: MarkerSymbol = SimpleMarkerSymbol( + style: .diamond, + color: .red, + size: 12.0 + ) +} diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 9479eb1f5..48372f3d4 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -45,13 +45,13 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { public var displayName: String = "Search" - public var maximumResults: Int = 6 { + public var maximumResults: Int { didSet { geocodeParameters.maxResults = Int32(maximumResults) } } - public var maximumSuggestions: Int = 6 { + public var maximumSuggestions: Int { didSet { suggestParameters.maxResults = Int32(maximumResults) } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 8915f463f..e6eb01682 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -31,28 +31,28 @@ public class SearchResult { } /// Title that should be shown whenever a result is displayed. - var displayTitle: String + public var displayTitle: String /// Should be shown as a subtitle wherever results are shown. - var displaySubtitle: String? + public var displaySubtitle: String? /// Image, in the native platform's format, for the result. This should be the marker that would be /// shown on the map, and also shown in the UI. This property is available for convenience so the /// UI doesn't have to worry about whether the `GeoElement` is a graphic or a feature when displaying /// the icon in the UI. - var markerImage: UIImage? + public var markerImage: UIImage? /// Reference to the search source that created this result. - var owningSource: SearchSourceProtocol + public var owningSource: SearchSourceProtocol /// For locator results, should be the graphic that was used to display the result on the map. /// For feature layer results, should be the result feature. Can be null depending on the type of the /// result, and can have `GeoElement`s without a defined geometry. - var geoElement: GeoElement? + public var geoElement: GeoElement? /// The viewpoint to be used when the view zooms to a selected result. This property can be `nil` /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. - var selectionViewpoint: Viewpoint? + public var selectionViewpoint: Viewpoint? } // MARK: Extensions diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 3d9821942..39ec2dbe0 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -17,22 +17,12 @@ import ArcGIS /// SearchView presents a search experience, powered by underlying SearchViewModel. public struct SearchView: View { - public init(proxy: GeoViewProxy, - searchViewModel: SearchViewModel, - viewpoint: Binding? = nil + public init( + searchViewModel: SearchViewModel ) { - self.proxy = proxy self.searchViewModel = searchViewModel - self.viewpoint = viewpoint } - /// Used for accessing `GeoView` functionality for geocoding and searching. - /// Reference to the GeoView used for automatic configuration. - /// When connected to a GeoView, SearchView will automatically navigate the view in response to - /// search result changes. Additionally, the view's current center and extent will be automatically - /// provided to locators as parameters. - var proxy: GeoViewProxy - /// The view model used by the view. The `ViewModel` manages state and handles the activity of /// searching. The view observes `ViewModel` for changes in state. The view calls methods on /// `ViewModel` in response to user action. The `ViewModel` is created automatically by the @@ -47,10 +37,8 @@ public struct SearchView: View { @State private var enableRepeatSearchHereButton = true - @State private var enableResultListView = true - @State private var noResultMessage = "No results found" /// Indicates that the `SearchViewModel` should start a search. @@ -61,12 +49,10 @@ public struct SearchView: View { @State private var currentSuggestion: SearchSuggestion? - private var viewpoint: Binding? - /// Indicates that the geoView's viewpoint has changed since the last search. @State private var viewpointChanged: Bool = false - + // TODO: Figure out better styling for list // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource // TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. @@ -89,34 +75,43 @@ public struct SearchView: View { } } if enableResultListView { - SearchResultList(searchViewModel: searchViewModel) + SearchResultList( + searchResults: searchViewModel.results, + selectedResult: $searchViewModel.selectedResult, + noResultMessage: noResultMessage + ) SearchSuggestionList( - searchViewModel: searchViewModel, - currentSuggestion: $currentSuggestion + suggestionResults: searchViewModel.suggestions, + currentSuggestion: $currentSuggestion, + noResultMessage: noResultMessage ) } } - .task(id: searchViewModel.currentQuery) { - // User typed a new character - if currentSuggestion == nil { - await searchViewModel.updateSuggestions() + // TODO: Not sure how to get the list to constrain itself if there's less than a screen full of rows. + Spacer() + .task(id: searchViewModel.currentQuery) { + // User typed a new character + if currentSuggestion == nil { + await searchViewModel.updateSuggestions() + } } - } - .task(id: commitSearch) { - // User committed changes (hit Enter/Search button) - await searchViewModel.commitSearch(true) - } - .task(id: currentSuggestion) { - // User committed changes (hit Enter/Search button) - if let suggestion = currentSuggestion { - await searchViewModel.acceptSuggestion(suggestion) - currentSuggestion = nil + .task(id: commitSearch) { + // User committed changes (hit Enter/Search button) + await searchViewModel.commitSearch(true) + } + .task(id: currentSuggestion) { + // User committed changes (hit Enter/Search button) + if let suggestion = currentSuggestion { + await searchViewModel.acceptSuggestion(suggestion) + currentSuggestion = nil + } } - } } - // MARK: Modifiers + // TODO: Show error but filter out user canceled errors + // MARK: Modifiers + /// Determines whether the view will update its configuration based on the geoview's /// document automatically. Defaults to `true`. /// - Parameter enableAutomaticConfiguration: The new value. @@ -148,33 +143,34 @@ public struct SearchView: View { /// - Parameter enableResultListView: The new value. /// - Returns: The `SearchView`. public func enableResultListView(_ enableResultListView: Bool) -> SearchView { - // TODO: this doesn't work; the view doesn't redraw -// self._enableResultListView.wrappedValue = enableResultListView - self.enableResultListView = enableResultListView - return self + var copy = self + copy.enableResultListView = enableResultListView + return copy } /// Message to show when there are no results or suggestions. Defaults to "No results found". /// - Parameter noResultMessage: The new value. /// - Returns: The `SearchView`. public func noResultMessage(_ noResultMessage: String) -> SearchView { - self.noResultMessage = noResultMessage - return self + var copy = self + copy.noResultMessage = noResultMessage + return copy } } -// TODO: why no results? Why .onChange for results/suggestions don't work // TODO: get currentResult working in Example. // TODO: look at consolidating SearchResultView and SearchSuggestionView with // TODO: new SearchDisplayProtocol containing only displayTitle and displaySubtitle // TODO: That would mean we only needed one of these. struct SearchResultList: View { - var searchViewModel: SearchViewModel - + var searchResults: Result<[SearchResult]?, RuntimeError> + @Binding var selectedResult: SearchResult? + var noResultMessage: String + var body: some View { Group { - switch searchViewModel.results { + switch searchResults { case .success(let results): if let results = results, results.count > 0 { List { @@ -192,34 +188,38 @@ struct SearchResultList: View { SearchResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) } .onTapGesture { - searchViewModel.selectedResult = result +// searchViewModel.selectedResult = result + selectedResult = result print("user selected result: \(result.displayTitle)") } } } - else { - // TODO: figure out why this isn't triggered. - Text("No results found") - } // .listStyle(DefaultListStyle()) } } } } + else if results != nil { + List { + Text(noResultMessage) + } + } case .failure(_): - Spacer() + EmptyView() } } + .esriBorder() } } struct SearchSuggestionList: View { - var searchViewModel: SearchViewModel - var currentSuggestion: Binding + var suggestionResults: Result<[SearchSuggestion]?, RuntimeError> + @Binding var currentSuggestion: SearchSuggestion? + var noResultMessage: String var body: some View { Group { - switch searchViewModel.suggestions { + switch suggestionResults { case .success(let results): if let suggestions = results, suggestions.count > 0 { List { @@ -237,26 +237,27 @@ struct SearchSuggestionList: View { SearchResultRow(title: suggestion.displayTitle, subtitle: suggestion.displaySubtitle) } .onTapGesture() { - currentSuggestion.wrappedValue = suggestion + currentSuggestion = suggestion } } // .listStyle(DefaultListStyle()) } - else { - // TODO: figure out why this isn't triggered. - Text("No results found") - } } } } } + else if results != nil { + List { + Text(noResultMessage) + } + } case .failure(_): - Spacer() + EmptyView() } } + .esriBorder() } } -//TODO: NoResultMessage = "No Results"; struct SearchResultRow: View { var title: String diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index e9583e031..29558c0d5 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -90,14 +90,13 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. @Published -// public var results: [SearchResult]? = nil - public var results: Result<[SearchResult]?, RuntimeError> = .success([]) + public var results: Result<[SearchResult]?, RuntimeError> = .success(nil) /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. - @State + @Published public var selectedResult: SearchResult? /// Collection of search sources to be used. This list is maintained over time and is not nullable. @@ -109,14 +108,12 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. @Published -// public var suggestions: [SearchSuggestion]? = nil - public var suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success([]) + public var suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// True if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to /// this property are driven by changes to the `queryArea` property. - @Published private(set) var isEligibleForRequery: Bool = false /// Starts a search. `selectedResult` and `results`, among other properties, are set @@ -127,7 +124,6 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. func commitSearch(_ restrictToArea: Bool) async -> Void { guard !currentQuery.isEmpty else { return } - print("SearchViewModel.commitSearch: \(currentQuery)") selectedResult = nil isEligibleForRequery = false @@ -135,7 +131,6 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) var searchResults = [SearchResult]() let searchSources = sourcesToSearch() for i in 0...searchSources.count - 1 { - print("commit search source \(i)") var searchSource = searchSources[i] searchSource.searchArea = queryArea searchSource.preferredSearchLocation = queryCenter @@ -152,8 +147,8 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") } } - results = .success(searchResults) suggestions = .success(nil) + results = .success(searchResults) } /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion @@ -166,7 +161,6 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) var suggestionResults = [SearchSuggestion]() let searchSources = sourcesToSearch() for i in 0...searchSources.count - 1 { - print("update suggestions source \(i)") var searchSource = searchSources[i] searchSource.searchArea = queryArea searchSource.preferredSearchLocation = queryCenter @@ -181,8 +175,9 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") } } - suggestions = .success(suggestionResults) + results = .success(nil) + suggestions = .success(suggestionResults) selectedResult = nil isEligibleForRequery = false @@ -197,7 +192,6 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// - searchSuggestion: The suggestion to use to commit the search. func acceptSuggestion(_ searchSuggestion: SearchSuggestion) async -> Void { currentQuery = searchSuggestion.displayTitle - print("SearchViewModel.acceptSuggestion: \(currentQuery)") isEligibleForRequery = false selectedResult = nil diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift index b4c2d2f67..378449f52 100644 --- a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift +++ b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift @@ -15,12 +15,23 @@ import ArcGIS extension GeocodeResult { func toSearchResult(searchSource: SearchSourceProtocol) -> SearchResult { - return SearchResult(displayTitle: self.label, - displaySubtitle: "Match percent: \((self.score).formatted(.percent))", - markerImage: nil, - owningSource: searchSource, - geoElement: nil, - selectionViewpoint: nil) + let subtitle = self.attributes["LongLabel"] as? String ?? + "Match percent: \((self.score).formatted(.percent))" + var viewpoint: Viewpoint? = nil + if let extent = self.extent { + viewpoint = Viewpoint(targetExtent: extent) + } + return SearchResult( + displayTitle: self.label, + displaySubtitle: subtitle, + markerImage: nil, + owningSource: searchSource, + geoElement: Graphic( + geometry: self.displayLocation, + attributes: self.attributes + ), + selectionViewpoint: viewpoint + ) } } From 6656f664742cb5703584176d5f8d3d3b942d2921 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 20 Jul 2021 17:18:09 -0500 Subject: [PATCH 0125/1722] cleanup. tweaks for search/suggest methods, hashable stuff. --- Examples/Examples/SearchExampleView.swift | 2 +- .../Search/LocatorSearchSource.swift | 54 ++++++-- .../Components/Search/SearchResult.swift | 10 ++ .../Search/SearchSourceProtocol.swift | 11 +- .../Components/Search/SearchViewModel.swift | 9 +- .../Search/SmartLocatorSearchSource.swift | 121 ++++++++++++++++-- .../Extensions/SuggestResult.swift | 2 +- 7 files changed, 172 insertions(+), 37 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index e95779849..a6a17661e 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -19,7 +19,7 @@ import ArcGISToolkit struct SearchExampleView: View { @ObservedObject var searchViewModel = SearchViewModel( - sources: [LocatorSearchSource( + sources: [SmartLocatorSearchSource( displayName: "Locator One", maximumResults: 16, maximumSuggestions: 16)]/*, diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 48372f3d4..524383d89 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -60,8 +60,13 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { public var searchArea: Geometry? public var preferredSearchLocation: Point? - + public func search(_ queryString: String, area: Geometry? = nil) async throws -> [SearchResult] { + // + // This differs from the .NET approach; .NET only uses the + // center of `searchArea` for the `geocodeParameters.preferredSearchLocation` + // and only sets `geocodeParameters.searchArea` from the `area` argument. + // geocodeParameters.searchArea = (area != nil) ? area : searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation @@ -69,30 +74,51 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { parameters: geocodeParameters ) - //convert to SearchResults + // Convert to SearchResults and return. return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } - public func search(_ searchSuggestion: SearchSuggestion, area: Geometry? = nil) async throws -> [SearchResult] { + public func search(_ searchSuggestion: SearchSuggestion) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] } - - geocodeParameters.searchArea = (area != nil) ? area : searchArea - geocodeParameters.preferredSearchLocation = preferredSearchLocation - + + geocodeParameters.searchArea = nil + geocodeParameters.preferredSearchLocation = nil + if preferredSearchLocation == nil, + let area = searchArea { + if let point = searchArea as? Point { + geocodeParameters.preferredSearchLocation = point + geocodeParameters.searchArea = nil + } + else if !area.extent.isEmpty { + geocodeParameters.preferredSearchLocation = area.extent.center + geocodeParameters.searchArea = nil + } + } + else if preferredSearchLocation != nil { + geocodeParameters.preferredSearchLocation = preferredSearchLocation + } + let geocodeResults = try await locator.geocode(suggestResult: suggestResult, - parameters: geocodeParameters + parameters: geocodeParameters ) + + // Convert to SearchResults and return. return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } - + public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { + // + // This differs from the .NET approach; .NET only uses the + // center of `searchArea` for the `geocodeParameters.preferredSearchLocation`. + // suggestParameters.searchArea = searchArea suggestParameters.preferredSearchLocation = preferredSearchLocation - - let suggestResults = try await locator.suggest(searchText: queryString, - parameters: suggestParameters + + let geocodeResults = try await locator.suggest( + searchText: queryString, + parameters: suggestParameters ) - //convert to SearchSuggestions - return suggestResults.map{ $0.toSearchSuggestion(searchSource: self) } + // Convert to SearchSuggestions and return. + return geocodeResults.map{ $0.toSearchSuggestion(searchSource: self) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index e6eb01682..4d8acd903 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -66,3 +66,13 @@ extension SearchResult: Equatable { lhs === rhs } } + +extension SearchResult: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(displayTitle) + hasher.combine(displaySubtitle) + if let geometry = geoElement?.geometry { + hasher.combine(geometry) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift index 0dd47e20d..bffdb8bb7 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift @@ -43,8 +43,10 @@ public protocol SearchSourceProtocol { /// - queryString: Text to be used for query. /// - area: Area to be used to constrain search results. /// - Returns: Array of `SearchResult`s - func search(_ queryString: String, - area: Geometry?) async throws -> [SearchResult] + func search( + _ queryString: String, + area: Geometry? + ) async throws -> [SearchResult] /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the /// `searchArea` property may be consulted but does not need to be used as a strict limit. @@ -52,6 +54,7 @@ public protocol SearchSourceProtocol { /// - searchSuggestion: Suggestion to be used as basis for search. /// - area: Area to be used to constrain search results. /// - Returns: Array of `SearchResult`s - func search(_ searchSuggestion: SearchSuggestion, - area: Geometry?) async throws -> [SearchResult] + func search( + _ searchSuggestion: SearchSuggestion + ) async throws -> [SearchResult] } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 29558c0d5..4685c2b1b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -71,7 +71,7 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) } /// The search area to be used for the current query. Ignored in most queries, unless the - /// `RestrictToArea` property is set to true when calling `commitSearch`. This property + /// `restrictToArea` property is set to true when calling `commitSearch`. This property /// should be updated as the user navigates the map/scene, or at minimum before calling `commitSearch`. public var queryArea: Geometry? { didSet { @@ -136,8 +136,9 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) searchSource.preferredSearchLocation = queryCenter let searchResult = await Result { - try await searchSource.search(currentQuery, - area: restrictToArea ? queryArea : nil + try await searchSource.search( + currentQuery, + area: restrictToArea ? queryArea : nil ) } switch searchResult { @@ -198,7 +199,7 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) var searchResults = [SearchResult]() let searchResult = await Result { - try await searchSuggestion.owningSource.search(searchSuggestion, area: nil) + try await searchSuggestion.owningSource.search(searchSuggestion) } switch searchResult { case .success(let results): diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 677e94606..b8b04cb2f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -19,15 +19,19 @@ import ArcGIS /// underlying locator to be used well; this class implements behaviors that make assumptions about the /// locator being the world geocode service. public class SmartLocatorSearchSource: LocatorSearchSource { - public convenience init(displayName: String = "Search", - maximumSuggestions: Int, - searchArea: Geometry? = nil, - preferredSearchLocation: Point? = nil, - repeatSearchResultThreshold: Int = 1, - repeatSuggestResultThreshold: Int = 6, - resultSymbolStyle: SymbolStyle? = nil) { + public convenience init( + displayName: String = "Search", + maximumResults: Int = 6, + maximumSuggestions: Int, + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil, + repeatSearchResultThreshold: Int = 1, + repeatSuggestResultThreshold: Int = 6, + resultSymbolStyle: SymbolStyle? = nil + ) { self.init() self.displayName = displayName + self.maximumResults = maximumResults self.maximumSuggestions = maximumSuggestions self.searchArea = searchArea self.preferredSearchLocation = preferredSearchLocation @@ -60,15 +64,106 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// returned by the locator. var resultSymbolStyle: SymbolStyle? - public override func search(_ queryString: String, area: Geometry?) async throws -> [SearchResult] { - return try await super.search(queryString, area: area) + public override func search( + _ queryString: String, + area: Geometry? + ) async throws -> [SearchResult] { + // First, peform super class search. + var results = try await super.search(queryString, area: area) + if results.count > repeatSearchResultThreshold || + area == nil, + searchArea == nil, + preferredSearchLocation == nil { + // Result count meets threshold or there were no geographic + // constraints on the search, so return results. + return results + } + + // Remove geographic constraints and re-run search. + geocodeParameters.searchArea = nil + geocodeParameters.preferredSearchLocation = nil + let geocodeResults = try await locator.geocode( + searchText: queryString, + parameters: geocodeParameters + ) + + // Union results and return. + let searchResults = geocodeResults.map{ $0.toSearchResult(searchSource: self) } + results.append(contentsOf: searchResults) + var allResults: [SearchResult] = Array(Set(results)) + + // Limit results to `maximumResults`. + if allResults.count > maximumResults { + let dropCount = allResults.count - maximumResults + allResults = allResults.dropLast(dropCount) + } + return allResults } - public override func search(_ searchSuggestion: SearchSuggestion, area: Geometry?) async throws -> [SearchResult] { - return try await super.search(searchSuggestion, area: area) + public override func search( + _ searchSuggestion: SearchSuggestion + ) async throws -> [SearchResult] { + guard let suggestResult = searchSuggestion.suggestResult else { return [] } + + var results = try await super.search(searchSuggestion) + if results.count > repeatSearchResultThreshold || + searchArea == nil, + preferredSearchLocation == nil { + // Result count meets threshold or there were no geographic + // constraints on the search, so return results. + return results + } + + // Remove geographic constraints and re-run search. + geocodeParameters.searchArea = nil + geocodeParameters.preferredSearchLocation = nil + let geocodeResults = try await locator.geocode(suggestResult: suggestResult, + parameters: geocodeParameters + ) + + // Union results and return. + let searchResults = geocodeResults.map{ $0.toSearchResult(searchSource: self) } + results.append(contentsOf: searchResults) + var allResults: [SearchResult] = Array(Set(results)) + + // Limit results to `maximumResults`. + if allResults.count > maximumResults { + let dropCount = allResults.count - maximumResults + allResults = allResults.dropLast(dropCount) + } + return allResults } - public override func suggest(_ queryString: String) async throws -> [SearchSuggestion] { - return try await super.suggest(queryString) + public override func suggest( + _ queryString: String + ) async throws -> [SearchSuggestion] { + var results = try await super.suggest(queryString) + if results.count > repeatSuggestResultThreshold || + searchArea == nil, + preferredSearchLocation == nil { + // Result count meets threshold or there were no geographic + // constraints on the search, so return results. + return results + } + + // Remove geographic constraints and re-run search. + suggestParameters.searchArea = nil + suggestParameters.preferredSearchLocation = nil + let geocodeResults = try await locator.suggest( + searchText: queryString, + parameters: suggestParameters + ) + + // Union results and return. + let suggestResults = geocodeResults.map{ $0.toSearchSuggestion(searchSource: self) } + results.append(contentsOf: suggestResults) + var allResults: [SearchSuggestion] = Array(Set(results)) + + // Limit results to `maximumResults`. + if allResults.count > maximumSuggestions { + let dropCount = allResults.count - maximumSuggestions + allResults = allResults.dropLast(dropCount) + } + return allResults } } diff --git a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift index eee4e1a2c..fdbf1cca7 100644 --- a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift +++ b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift @@ -19,6 +19,6 @@ extension SuggestResult { displaySubtitle: nil, owningSource: searchSource, suggestResult: self, - isCollection: self.isCollection) + isCollection: isCollection) } } From 22023967bfe9cf69861581c2f4559befc1d0fd8f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 21 Jul 2021 18:05:11 -0500 Subject: [PATCH 0126/1722] Add Hashable conformance. --- .../Components/Search/SearchResult.swift | 15 +++++++++++- .../Components/Search/SearchSuggestion.swift | 23 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 4d8acd903..89963e983 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -63,16 +63,29 @@ extension SearchResult: Identifiable { extension SearchResult: Equatable { public static func == (lhs: SearchResult, rhs: SearchResult) -> Bool { - lhs === rhs + lhs.hashValue == rhs.hashValue } } extension SearchResult: Hashable { + /// Note: we're not hashing `geoElement.attributes` as results with the same title, + /// subtitle, geometry, and owningSource are considered identical for searching purposes. public func hash(into hasher: inout Hasher) { hasher.combine(displayTitle) hasher.combine(displaySubtitle) + if let geometry = geoElement?.geometry { hasher.combine(geometry) } + + if let locatorSource = owningSource as? LocatorSearchSource { + hasher.combine(ObjectIdentifier(locatorSource)) + } + // If you define a custom type that does NOT inherit from + // `LocatorSearchSource`, you will need to add an `else if` check + // for your custom type. +// else if let customSearchSource = owningSource as? MyCustomSearchSource { +// hasher.combine(ObjectIdentifier(customSearchSource)) +// } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 25f227612..aaf01af0a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -55,6 +55,27 @@ extension SearchSuggestion: Identifiable { extension SearchSuggestion: Equatable { public static func == (lhs: SearchSuggestion, rhs: SearchSuggestion) -> Bool { - lhs === rhs + lhs.hashValue == rhs.hashValue + } +} + +extension SearchSuggestion: Hashable { + /// Note: we're not hashing `suggestResult` as `SearchSuggestion` is created from + /// a `SuggestResult` and `suggestResult` will be different for two sepate geocode + /// operations even though they represent the same suggestion. + public func hash(into hasher: inout Hasher) { + hasher.combine(displayTitle) + hasher.combine(displaySubtitle) + hasher.combine(isCollection) + + if let locatorSource = owningSource as? LocatorSearchSource { + hasher.combine(ObjectIdentifier(locatorSource)) + } + // If you define a custom type that does NOT inherit from + // `LocatorSearchSource`, you will need to add an `else if` check + // for your custom type. +// else if let customSearchSource = owningSource as? MyCustomSearchSource { +// hasher.combine(ObjectIdentifier(customSearchSource)) +// } } } From b7313ed3a1152fb3b2a7f4f153acc2eb671d96e6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 22 Jul 2021 15:38:58 -0500 Subject: [PATCH 0127/1722] minor updates and start of error stuff. --- Examples/Examples/SearchExampleView.swift | 28 ++++++++++++------- .../Components/Search/SearchView.swift | 15 ++++++---- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index a6a17661e..d60f816a0 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -44,6 +44,13 @@ struct SearchExampleView: View { ) .onViewpointChanged(kind: .centerAndScale) { searchViewModel.queryCenter = $0.targetGeometry as? Point + + // Reset `searchResultViewpoint` here when the user pans/zooms + // the map, so if the user commits the same search with the + // same result, the Map will pan/zoom to the result. Otherwise + // `searchResultViewpoint` doesn't change which doesn't + // redraw the map with the new viewpoint. + searchResultViewpoint = nil } .onVisibleAreaChanged { searchViewModel.queryArea = $0 @@ -62,23 +69,19 @@ struct SearchExampleView: View { }, alignment: .topTrailing ) - .onChange(of: searchViewModel.results, perform: { searchResults in - display(searchResults: searchResults) + .onChange(of: searchViewModel.results, perform: { newValue in + display(searchResults: newValue) }) - .onChange(of: searchViewModel.selectedResult, perform: { _ in - display(selectedResult: searchViewModel.selectedResult) + .onChange(of: searchViewModel.selectedResult, perform: { newValue in + display(selectedResult: newValue) }) } fileprivate func display(searchResults: Result<[SearchResult]?, RuntimeError>) { - let searchResultEnvelopeBuilder = EnvelopeBuilder(spatialReference: .wgs84) switch searchResults { case .success(let results): var resultGraphics = [Graphic]() results?.forEach({ result in - if let extent = result.selectionViewpoint?.targetGeometry as? Envelope { - searchResultEnvelopeBuilder.union(envelope: extent) - } let graphic = Graphic(geometry: result.geoElement?.geometry, symbol: .resultSymbol) resultGraphics.append(graphic) @@ -89,8 +92,13 @@ struct SearchExampleView: View { searchResultsOverlay.addGraphics(resultGraphics) if resultGraphics.count > 0, - let envelope = searchResultEnvelopeBuilder.toGeometry() as? Envelope { - searchResultViewpoint = Viewpoint(targetExtent: envelope) + let envelope = searchResultsOverlay.extent { + let builder = EnvelopeBuilder(envelope: envelope) + builder.expand(factor: 1.1) + searchResultViewpoint = Viewpoint(targetExtent: builder.toGeometry()) + } + else { + searchResultViewpoint = nil } case .failure(_): break diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 39ec2dbe0..c77032c7b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -57,6 +57,7 @@ public struct SearchView: View { // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource // TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. // TODO: better modifiers for search text field; maybe SearchTextField or something... + // TODO: Get proper pins for example app. - How to use SF font with PictureMarkerSymbol?? How to tint calcite icons/images. public var body: some View { VStack (alignment: .center) { TextField(searchViewModel.defaultPlaceHolder, @@ -158,8 +159,6 @@ public struct SearchView: View { } } -// TODO: get currentResult working in Example. - // TODO: look at consolidating SearchResultView and SearchSuggestionView with // TODO: new SearchDisplayProtocol containing only displayTitle and displaySubtitle // TODO: That would mean we only needed one of these. @@ -204,8 +203,10 @@ struct SearchResultList: View { Text(noResultMessage) } } - case .failure(_): - EmptyView() + case .failure(let error): + List { + Text(error.localizedDescription) + } } } .esriBorder() @@ -251,8 +252,10 @@ struct SearchSuggestionList: View { Text(noResultMessage) } } - case .failure(_): - EmptyView() + case .failure(let error): + List { + Text(error.localizedDescription) + } } } .esriBorder() From eacd19bd1198514f8b464a99167adc5a75a8a334 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 23 Jul 2021 17:02:08 -0500 Subject: [PATCH 0128/1722] Update to `.attributionText(hidden:)` modifier --- 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 37424909e..df0b10f3b 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -90,7 +90,7 @@ public struct OverviewMap: View { viewpoint: makeOverviewViewpoint(), graphicsOverlays: [graphicsOverlay] ) - .attributionTextHidden() + .attributionText(hidden: true) .interactionModes([]) .border(.black, width: 1) .onAppear(perform: { From 572ce0ae5c3d204b4f961dc20174a191bb9b0e0b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 26 Jul 2021 12:57:49 -0500 Subject: [PATCH 0129/1722] Fixes to how search area is used and display of matched % --- .../Search/SmartLocatorSearchSource.swift | 14 ++++---------- .../ArcGISToolkit/Extensions/GeocodeResult.swift | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index b8b04cb2f..e0e3402d0 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -71,9 +71,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // First, peform super class search. var results = try await super.search(queryString, area: area) if results.count > repeatSearchResultThreshold || - area == nil, - searchArea == nil, - preferredSearchLocation == nil { + area != nil || + geocodeParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. return results @@ -81,7 +80,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil - geocodeParameters.preferredSearchLocation = nil let geocodeResults = try await locator.geocode( searchText: queryString, parameters: geocodeParameters @@ -107,8 +105,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { var results = try await super.search(searchSuggestion) if results.count > repeatSearchResultThreshold || - searchArea == nil, - preferredSearchLocation == nil { + geocodeParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. return results @@ -116,7 +113,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil - geocodeParameters.preferredSearchLocation = nil let geocodeResults = try await locator.geocode(suggestResult: suggestResult, parameters: geocodeParameters ) @@ -139,8 +135,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ) async throws -> [SearchSuggestion] { var results = try await super.suggest(queryString) if results.count > repeatSuggestResultThreshold || - searchArea == nil, - preferredSearchLocation == nil { + suggestParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. return results @@ -148,7 +143,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. suggestParameters.searchArea = nil - suggestParameters.preferredSearchLocation = nil let geocodeResults = try await locator.suggest( searchText: queryString, parameters: suggestParameters diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift index 378449f52..dd49205a8 100644 --- a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift +++ b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift @@ -16,7 +16,7 @@ import ArcGIS extension GeocodeResult { func toSearchResult(searchSource: SearchSourceProtocol) -> SearchResult { let subtitle = self.attributes["LongLabel"] as? String ?? - "Match percent: \((self.score).formatted(.percent))" + "Match percent: \((self.score / 100.0).formatted(.percent))" var viewpoint: Viewpoint? = nil if let extent = self.extent { viewpoint = Viewpoint(targetExtent: extent) From 5d739811e4bbda0b8d9e4c4a91b66baca92a27df Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 26 Jul 2021 14:43:58 -0500 Subject: [PATCH 0130/1722] Result & error stuff. --- .../Components/Search/SearchView.swift | 2 +- .../Components/Search/SearchViewModel.swift | 15 ++++++++++++++- Sources/ArcGISToolkit/Extensions/Result.swift | 11 ++++++++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index c77032c7b..c0053e103 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -98,7 +98,7 @@ public struct SearchView: View { } .task(id: commitSearch) { // User committed changes (hit Enter/Search button) - await searchViewModel.commitSearch(true) + await searchViewModel.commitSearch(false) } .task(id: currentSuggestion) { // User committed changes (hit Enter/Search button) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 4685c2b1b..eb639c484 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -28,7 +28,8 @@ public enum SearchResultMode { /// Performs searches and manages search state for a Search, or optionally without a UI connection. public class SearchViewModel: ObservableObject { - public convenience init(defaultPlaceHolder: String = "Find a place or address", + public convenience init( +defaultPlaceHolder: String = "Find a place or address", activeSource: SearchSourceProtocol? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, @@ -146,10 +147,15 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) searchResults.append(contentsOf: results) case .failure(let error): print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") + case .none: + break } } suggestions = .success(nil) results = .success(searchResults) + if searchResults.count == 1 { + selectedResult = searchResults.first + } } /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion @@ -174,6 +180,8 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) suggestionResults.append(contentsOf: results) case .failure(let error): print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") + case .none: + break } } @@ -224,9 +232,14 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) } case .failure(let error): print("\(searchSuggestion.owningSource.displayName) encountered an error: \(error.localizedDescription)") + case .none: + break } results = .success(searchResults) + if searchResults.count == 1 { + selectedResult = searchResults.first + } suggestions = .success(nil) } diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index e67f4984d..0503e5195 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -12,8 +12,13 @@ // limitations under the License. extension Result where Failure == Error { - init(awaiting task: () async throws -> Success) async { - do { self = .success(try await task()) } - catch { self = .failure(error) } + init?(awaiting task: () async throws -> Success) async { + do { + self = .success(try await task()) + } catch is CancellationError { + return nil + } catch { + self = .failure(error) + } } } From bd8c3f2065b76497e10930c4f498f6884e898e8f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 27 Jul 2021 10:18:42 -0500 Subject: [PATCH 0131/1722] Single source; added SearchError --- Examples/Examples/SearchExampleView.swift | 18 +-- .../Components/Search/SearchError.swift | 30 ++++ .../Components/Search/SearchView.swift | 73 ++++------ .../Components/Search/SearchViewModel.swift | 135 +++++++++--------- .../EsriShowResultsButtonViewModifier.swift | 40 ++++++ 5 files changed, 170 insertions(+), 126 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Search/SearchError.swift create mode 100644 Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index d60f816a0..b6d4e298e 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -22,13 +22,9 @@ struct SearchExampleView: View { sources: [SmartLocatorSearchSource( displayName: "Locator One", maximumResults: 16, - maximumSuggestions: 16)]/*, - LocatorSearchSource(displayName: "Locator Two")]*/ + maximumSuggestions: 16)] ) - @State - var showResults = true - let map = Map(basemapStyle: .arcGISImagery) @State @@ -56,17 +52,9 @@ struct SearchExampleView: View { searchViewModel.queryArea = $0 } .overlay( - VStack { - Button { - showResults.toggle() - } label: { - Text(showResults ? "Hide results" : "Show results") - } SearchView(searchViewModel: searchViewModel) - .enableResultListView(showResults) .frame(width: 360) - .padding() - }, + .padding(), alignment: .topTrailing ) .onChange(of: searchViewModel.results, perform: { newValue in @@ -77,7 +65,7 @@ struct SearchExampleView: View { }) } - fileprivate func display(searchResults: Result<[SearchResult]?, RuntimeError>) { + fileprivate func display(searchResults: Result<[SearchResult]?, SearchError>) { switch searchResults { case .success(let results): var resultGraphics = [Graphic]() diff --git a/Sources/ArcGISToolkit/Components/Search/SearchError.swift b/Sources/ArcGISToolkit/Components/Search/SearchError.swift new file mode 100644 index 000000000..d32d10617 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Search/SearchError.swift @@ -0,0 +1,30 @@ +// Copyright 2021 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 Foundation + +/// A value that represents an error that occurs while Searching. +public struct SearchError: LocalizedError { + /// A basic description for the error. + public let errorDescription: String +} + +extension SearchError { + init(_ error: Error) { + self.init( + errorDescription: error.localizedDescription + ) + } +} + +extension SearchError: Equatable { } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index c0053e103..051704cc1 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -37,6 +37,7 @@ public struct SearchView: View { @State private var enableRepeatSearchHereButton = true + @State private var enableResultListView = true private var noResultMessage = "No results found" @@ -68,6 +69,7 @@ public struct SearchView: View { } .esriDeleteTextButton(text: $searchViewModel.currentQuery) .esriSearchButton(performSearch: $commitSearch) + .esriShowResultsButton(showResults: $enableResultListView) .esriBorder() if enableRepeatSearchHereButton, viewpointChanged { Button("Search Here") { @@ -144,9 +146,8 @@ public struct SearchView: View { /// - Parameter enableResultListView: The new value. /// - Returns: The `SearchView`. public func enableResultListView(_ enableResultListView: Bool) -> SearchView { - var copy = self - copy.enableResultListView = enableResultListView - return copy + self.enableResultListView = enableResultListView + return self } /// Message to show when there are no results or suggestions. Defaults to "No results found". @@ -163,7 +164,7 @@ public struct SearchView: View { // TODO: new SearchDisplayProtocol containing only displayTitle and displaySubtitle // TODO: That would mean we only needed one of these. struct SearchResultList: View { - var searchResults: Result<[SearchResult]?, RuntimeError> + var searchResults: Result<[SearchResult]?, SearchError> @Binding var selectedResult: SearchResult? var noResultMessage: String @@ -173,29 +174,21 @@ struct SearchResultList: View { case .success(let results): if let results = results, results.count > 0 { List { - // Get array of unique search source displayNames. - let sourceDisplayNames = Array(Set(results.map { $0.owningSource.displayName })).sorted() - ForEach(sourceDisplayNames, id: \.self) { displayName in - Section(header: Text(displayName)) { - // Get results filtered by displayName - let sourceResults = results.filter { $0.owningSource.displayName == displayName } - if sourceResults.count > 0 { - ForEach(sourceResults) { result in - HStack { - Image(systemName: "mappin") - .foregroundColor(Color(.red)) - SearchResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) - } - .onTapGesture { -// searchViewModel.selectedResult = result - selectedResult = result - print("user selected result: \(result.displayTitle)") - } - } + if results.count > 0 { + ForEach(results) { result in + HStack { + Image(systemName: "mappin") + .foregroundColor(Color(.red)) + SearchResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) + } + .onTapGesture { + // searchViewModel.selectedResult = result + selectedResult = result + print("user selected result: \(result.displayTitle)") } - // .listStyle(DefaultListStyle()) } } + // .listStyle(DefaultListStyle()) } } else if results != nil { @@ -214,7 +207,7 @@ struct SearchResultList: View { } struct SearchSuggestionList: View { - var suggestionResults: Result<[SearchSuggestion]?, RuntimeError> + var suggestionResults: Result<[SearchSuggestion]?, SearchError> @Binding var currentSuggestion: SearchSuggestion? var noResultMessage: String @@ -224,26 +217,18 @@ struct SearchSuggestionList: View { case .success(let results): if let suggestions = results, suggestions.count > 0 { List { - // Get array of unique search source displayNames. - let sourceDisplayNames = Array(Set(suggestions.map { $0.owningSource.displayName })).sorted() - ForEach(sourceDisplayNames, id: \.self) { displayName in - Section(header: Text(displayName)) { - // Get results filtered by displayName - let sourceSuggestions = suggestions.filter { $0.owningSource.displayName == displayName } - if sourceSuggestions.count > 0 { - ForEach(sourceSuggestions) { suggestion in - HStack { - let imageName = suggestion.isCollection ? "magnifyingglass" : "mappin" - Image(systemName: imageName) - SearchResultRow(title: suggestion.displayTitle, subtitle: suggestion.displaySubtitle) - } - .onTapGesture() { - currentSuggestion = suggestion - } - } - // .listStyle(DefaultListStyle()) + if suggestions.count > 0 { + ForEach(suggestions) { suggestion in + HStack { + let imageName = suggestion.isCollection ? "magnifyingglass" : "mappin" + Image(systemName: imageName) + SearchResultRow(title: suggestion.displayTitle, subtitle: suggestion.displaySubtitle) + } + .onTapGesture() { + currentSuggestion = suggestion } } + // .listStyle(DefaultListStyle()) } } } @@ -254,7 +239,7 @@ struct SearchSuggestionList: View { } case .failure(let error): List { - Text(error.localizedDescription) + Text(error.errorDescription) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index eb639c484..a9a45d84f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -34,9 +34,9 @@ activeSource: SearchSourceProtocol? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, -results: Result<[SearchResult]?, RuntimeError> = .success(nil), +results: Result<[SearchResult]?, SearchError> = .success(nil), sources: [SearchSourceProtocol] = [], -suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) +suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) ) { self.init() self.defaultPlaceHolder = defaultPlaceHolder @@ -91,7 +91,7 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. @Published - public var results: Result<[SearchResult]?, RuntimeError> = .success(nil) + public var results: Result<[SearchResult]?, SearchError> = .success(nil) /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets @@ -109,7 +109,7 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. @Published - public var suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) + public var suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// True if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is @@ -124,37 +124,36 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// of the `queryArea` property. Behavior when called with `restrictToArea` set to true /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. func commitSearch(_ restrictToArea: Bool) async -> Void { - guard !currentQuery.isEmpty else { return } + guard !currentQuery.isEmpty, + var source = currentSource() else { return } selectedResult = nil isEligibleForRequery = false + suggestions = .success(nil) + - var searchResults = [SearchResult]() - let searchSources = sourcesToSearch() - for i in 0...searchSources.count - 1 { - var searchSource = searchSources[i] - searchSource.searchArea = queryArea - searchSource.preferredSearchLocation = queryCenter - - let searchResult = await Result { - try await searchSource.search( - currentQuery, - area: restrictToArea ? queryArea : nil - ) - } - switch searchResult { - case .success(let results): - searchResults.append(contentsOf: results) - case .failure(let error): - print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") - case .none: - break - } + source.searchArea = queryArea + source.preferredSearchLocation = queryCenter + + let searchResult = await Result { + try await source.search( + currentQuery, + area: restrictToArea ? queryArea : nil + ) } - suggestions = .success(nil) - results = .success(searchResults) - if searchResults.count == 1 { - selectedResult = searchResults.first + + switch searchResult { + case .success(let searchResults): + results = .success(searchResults) + if searchResults.count == 1 { + selectedResult = searchResults.first + } + case .failure(let error): + results = .failure(SearchError(error)) + break + case .none: + results = .success(nil) + break } } @@ -162,34 +161,31 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// requests before initiating new ones. The view should also wait for some time after user finishes /// typing before making suggestions. The JavaScript implementation uses 150ms by default. func updateSuggestions() async -> Void { - guard !currentQuery.isEmpty else { return } + guard !currentQuery.isEmpty, + var source = currentSource() else { return } print("SearchViewModel.updateSuggestions: \(currentQuery)") - - var suggestionResults = [SearchSuggestion]() - let searchSources = sourcesToSearch() - for i in 0...searchSources.count - 1 { - var searchSource = searchSources[i] - searchSource.searchArea = queryArea - searchSource.preferredSearchLocation = queryCenter - - let suggestResults = await Result { - try await searchSource.suggest(currentQuery) - } - switch suggestResults { - case .success(let results): - suggestionResults.append(contentsOf: results) - case .failure(let error): - print("\(searchSource.displayName) encountered an error: \(error.localizedDescription)") - case .none: - break - } - } - - results = .success(nil) - suggestions = .success(suggestionResults) + results = .success(nil) selectedResult = nil isEligibleForRequery = false + + source.searchArea = queryArea + source.preferredSearchLocation = queryCenter + + let suggestResult = await Result { + try await source.suggest(currentQuery) + } + + switch suggestResult { + case .success(let suggestResults): + suggestions = .success(suggestResults) + case .failure(let error): + suggestions = .failure(SearchError(error)) + break + case .none: + suggestions = .success(nil) + break + } } /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is @@ -206,6 +202,7 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) selectedResult = nil var searchResults = [SearchResult]() + var suggestError: Error? let searchResult = await Result { try await searchSuggestion.owningSource.search(searchSuggestion) } @@ -216,7 +213,6 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) case .single: if let firstResult = results.first { searchResults = [firstResult] - selectedResult = firstResult } case .multiple: searchResults = results @@ -226,19 +222,23 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) } else { if let firstResult = results.first { searchResults = [firstResult] - selectedResult = firstResult } } } case .failure(let error): - print("\(searchSuggestion.owningSource.displayName) encountered an error: \(error.localizedDescription)") + suggestError = error case .none: break } - - results = .success(searchResults) - if searchResults.count == 1 { - selectedResult = searchResults.first + + if let error = suggestError { + results = .failure(SearchError(error)) + } + else { + results = .success(searchResults) + if searchResults.count == 1 { + selectedResult = searchResults.first + } } suggestions = .success(nil) } @@ -264,18 +264,19 @@ suggestions: Result<[SearchSuggestion]?, RuntimeError> = .success(nil) /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, /// and reset the current query. func clearSearch() { - print("SearchViewModel.clearSearch") + // Setting currentQuery to "" will reset everything necessary. + currentQuery = "" } } extension SearchViewModel { - func sourcesToSearch() -> [SearchSourceProtocol] { - var selectedSources = [SearchSourceProtocol]() + func currentSource() -> SearchSourceProtocol? { + var source: SearchSourceProtocol? if let activeSource = activeSource { - selectedSources.append(activeSource) + source = activeSource } else { - selectedSources.append(contentsOf: sources) + source = sources.first } - return selectedSources + return source } } diff --git a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift new file mode 100644 index 000000000..a264a2bb0 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift @@ -0,0 +1,40 @@ +// Copyright 2021 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 + +struct EsriShowResultsButtonViewModifier: ViewModifier { + @Binding var showResults: Bool + + func body(content: Content) -> some View { + HStack { + content + Button( + action: { showResults.toggle() }, + label: { + Image(systemName: showResults ? "eye.fill" : "eye.slash.fill") + .foregroundColor(Color(UIColor.opaqueSeparator)) + } + ) + } + } +} + +extension View { + func esriShowResultsButton(showResults: Binding) -> some View { + ModifiedContent( + content: self, + modifier: EsriShowResultsButtonViewModifier(showResults: showResults) + ) + } +} From 407197266b4d1cbdbc1438754fe52e55b42b695e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 27 Jul 2021 11:45:18 -0500 Subject: [PATCH 0132/1722] Don't show results if there's only one. --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 051704cc1..3243e9523 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -173,8 +173,9 @@ struct SearchResultList: View { switch searchResults { case .success(let results): if let results = results, results.count > 0 { - List { - if results.count > 0 { + if results.count > 1 { + // If we have only 1 results, don't show the list. + List { ForEach(results) { result in HStack { Image(systemName: "mappin") @@ -182,7 +183,6 @@ struct SearchResultList: View { SearchResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) } .onTapGesture { - // searchViewModel.selectedResult = result selectedResult = result print("user selected result: \(result.displayTitle)") } From 765d1125c0a2f7c0451aa31bd2045f6d5ad47238 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 27 Jul 2021 16:03:02 -0500 Subject: [PATCH 0133/1722] cleanup --- .../Components/Search/SearchView.swift | 52 +++++++++++-------- .../Components/Search/SearchViewModel.swift | 26 +++++----- .../Utility/EsriBorderViewModifier.swift | 23 ++++---- 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 3243e9523..06dea1141 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -64,7 +64,6 @@ public struct SearchView: View { TextField(searchViewModel.defaultPlaceHolder, text: $searchViewModel.currentQuery) { editing in } onCommit: { - // Editing state changed (becomes/looses firstResponder) commitSearch.toggle() } .esriDeleteTextButton(text: $searchViewModel.currentQuery) @@ -99,8 +98,9 @@ public struct SearchView: View { } } .task(id: commitSearch) { - // User committed changes (hit Enter/Search button) - await searchViewModel.commitSearch(false) + if commitSearch { + await searchViewModel.commitSearch(false) + } } .task(id: currentSuggestion) { // User committed changes (hit Enter/Search button) @@ -110,8 +110,6 @@ public struct SearchView: View { } } } - - // TODO: Show error but filter out user canceled errors // MARK: Modifiers @@ -160,9 +158,6 @@ public struct SearchView: View { } } -// TODO: look at consolidating SearchResultView and SearchSuggestionView with -// TODO: new SearchDisplayProtocol containing only displayTitle and displaySubtitle -// TODO: That would mean we only needed one of these. struct SearchResultList: View { var searchResults: Result<[SearchResult]?, SearchError> @Binding var selectedResult: SearchResult? @@ -177,14 +172,9 @@ struct SearchResultList: View { // If we have only 1 results, don't show the list. List { ForEach(results) { result in - HStack { - Image(systemName: "mappin") - .foregroundColor(Color(.red)) - SearchResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) - } + SearchResultRow(result: result) .onTapGesture { selectedResult = result - print("user selected result: \(result.displayTitle)") } } } @@ -202,7 +192,7 @@ struct SearchResultList: View { } } } - .esriBorder() + .esriBorder(edgeInsets: EdgeInsets()) } } @@ -219,11 +209,7 @@ struct SearchSuggestionList: View { List { if suggestions.count > 0 { ForEach(suggestions) { suggestion in - HStack { - let imageName = suggestion.isCollection ? "magnifyingglass" : "mappin" - Image(systemName: imageName) - SearchResultRow(title: suggestion.displayTitle, subtitle: suggestion.displaySubtitle) - } + SuggestionResultRow(suggestion: suggestion) .onTapGesture() { currentSuggestion = suggestion } @@ -243,11 +229,35 @@ struct SearchSuggestionList: View { } } } - .esriBorder() + .esriBorder(edgeInsets: EdgeInsets()) } } struct SearchResultRow: View { + var result: SearchResult + + var body: some View { + HStack { + Image(systemName: "mappin") + .foregroundColor(Color(.red)) + ResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) + } + } +} + +struct SuggestionResultRow: View { + var suggestion: SearchSuggestion + + var body: some View { + HStack { + let imageName = suggestion.isCollection ? "magnifyingglass" : "mappin" + Image(systemName: imageName) + ResultRow(title: suggestion.displayTitle, subtitle: suggestion.displaySubtitle) + } + } +} + +struct ResultRow: View { var title: String var subtitle: String? diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index a9a45d84f..3bde5fdd9 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -127,11 +127,6 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) guard !currentQuery.isEmpty, var source = currentSource() else { return } - selectedResult = nil - isEligibleForRequery = false - suggestions = .success(nil) - - source.searchArea = queryArea source.preferredSearchLocation = queryCenter @@ -141,6 +136,10 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) area: restrictToArea ? queryArea : nil ) } + + selectedResult = nil + isEligibleForRequery = false + suggestions = .success(nil) switch searchResult { case .success(let searchResults): @@ -165,16 +164,16 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) var source = currentSource() else { return } print("SearchViewModel.updateSuggestions: \(currentQuery)") - results = .success(nil) - selectedResult = nil - isEligibleForRequery = false - source.searchArea = queryArea source.preferredSearchLocation = queryCenter let suggestResult = await Result { try await source.suggest(currentQuery) } + + results = .success(nil) + selectedResult = nil + isEligibleForRequery = false switch suggestResult { case .success(let suggestResults): @@ -197,15 +196,17 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// - searchSuggestion: The suggestion to use to commit the search. func acceptSuggestion(_ searchSuggestion: SearchSuggestion) async -> Void { currentQuery = searchSuggestion.displayTitle - - isEligibleForRequery = false - selectedResult = nil var searchResults = [SearchResult]() var suggestError: Error? let searchResult = await Result { try await searchSuggestion.owningSource.search(searchSuggestion) } + + suggestions = .success(nil) + isEligibleForRequery = false + selectedResult = nil + switch searchResult { case .success(let results): switch (resultMode) @@ -240,7 +241,6 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) selectedResult = searchResults.first } } - suggestions = .success(nil) } /// Configures the view model for the provided map. By default, will only configure the view model diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 6c722e1aa..abbc839c8 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -14,16 +14,12 @@ import SwiftUI struct EsriBorderViewModifier: ViewModifier { + var edgeInsets: EdgeInsets func body(content: Content) -> some View { let roundedRect = RoundedRectangle(cornerRadius: 8) content - .padding(EdgeInsets(top: 8, - leading: 12, - bottom: 8, - trailing: 12 - ) - ) - .background(Color.white) + .padding(edgeInsets) + .background(.white) .clipShape(roundedRect) .overlay( roundedRect @@ -39,10 +35,17 @@ struct EsriBorderViewModifier: ViewModifier { } extension View { - func esriBorder() -> some View { - ModifiedContent( + func esriBorder( + edgeInsets: EdgeInsets = EdgeInsets( + top: 8, + leading: 12, + bottom: 8, + trailing: 12 + ) + ) -> some View { + return ModifiedContent( content: self, - modifier: EsriBorderViewModifier() + modifier: EsriBorderViewModifier(edgeInsets: edgeInsets) ) } } From ee374ad0c69a8661b6cb75925131c25b653b5c7c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 27 Jul 2021 16:28:51 -0500 Subject: [PATCH 0134/1722] Fix indentation --- .../Components/Search/SearchViewModel.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 3bde5fdd9..d03570b37 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -92,7 +92,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// were no results, and the view should show an appropriate 'no results' message. @Published public var results: Result<[SearchResult]?, SearchError> = .success(nil) - + /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets /// this property upon user selection. This property is observable. The view should observe this @@ -110,7 +110,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// a useful 'no results' message should be shown by the view. @Published public var suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) - + /// True if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to @@ -140,7 +140,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) selectedResult = nil isEligibleForRequery = false suggestions = .success(nil) - + switch searchResult { case .success(let searchResults): results = .success(searchResults) @@ -174,7 +174,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) results = .success(nil) selectedResult = nil isEligibleForRequery = false - + switch suggestResult { case .success(let suggestResults): suggestions = .success(suggestResults) @@ -196,7 +196,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// - searchSuggestion: The suggestion to use to commit the search. func acceptSuggestion(_ searchSuggestion: SearchSuggestion) async -> Void { currentQuery = searchSuggestion.displayTitle - + var searchResults = [SearchResult]() var suggestError: Error? let searchResult = await Result { @@ -206,7 +206,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) suggestions = .success(nil) isEligibleForRequery = false selectedResult = nil - + switch searchResult { case .success(let results): switch (resultMode) @@ -231,7 +231,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) case .none: break } - + if let error = suggestError { results = .failure(SearchError(error)) } From d3e0af3414f34c6e58b42871f25071de5952886f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 27 Jul 2021 17:23:19 -0500 Subject: [PATCH 0135/1722] Add task cancellation support so we don't get results and suggestions coming in the wrong order. --- .../Components/Search/SearchView.swift | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 06dea1141..d071e41f1 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -54,6 +54,13 @@ public struct SearchView: View { @State private var viewpointChanged: Bool = false + @State + private var currentTask: Task? +// private var searchTask: Task? +// private var suggestTask: Task? +// private var acceptTask: Task? + + // TODO: Figure out better styling for list // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource // TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. @@ -94,18 +101,19 @@ public struct SearchView: View { .task(id: searchViewModel.currentQuery) { // User typed a new character if currentSuggestion == nil { - await searchViewModel.updateSuggestions() + await suggest() } } .task(id: commitSearch) { if commitSearch { - await searchViewModel.commitSearch(false) + await search() + commitSearch.toggle() } } .task(id: currentSuggestion) { // User committed changes (hit Enter/Search button) if let suggestion = currentSuggestion { - await searchViewModel.acceptSuggestion(suggestion) + await accept(suggestion) currentSuggestion = nil } } @@ -158,6 +166,32 @@ public struct SearchView: View { } } +extension SearchView { + func search() async { + currentTask?.cancel() + currentTask = Task(operation: { + await searchViewModel.commitSearch(false) + }) + await currentTask?.value + } + + func suggest() async { + currentTask?.cancel() + currentTask = Task(operation: { + await searchViewModel.updateSuggestions() + }) + await currentTask?.value + } + + func accept(_ suggestion: SearchSuggestion) async { + currentTask?.cancel() + currentTask = Task(operation: { + await searchViewModel.acceptSuggestion(suggestion) + }) + await currentTask?.value + } +} + struct SearchResultList: View { var searchResults: Result<[SearchResult]?, SearchError> @Binding var selectedResult: SearchResult? From 87f53c4d55ca48a584924dbf158c4b3cacd0fc11 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 27 Jul 2021 17:24:52 -0500 Subject: [PATCH 0136/1722] remove commented out code. --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index d071e41f1..bde9f842a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -56,10 +56,6 @@ public struct SearchView: View { @State private var currentTask: Task? -// private var searchTask: Task? -// private var suggestTask: Task? -// private var acceptTask: Task? - // TODO: Figure out better styling for list // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource From 8d51dd8ee3e2dea2195a9a83986f213612b1246c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 28 Jul 2021 17:03:04 -0500 Subject: [PATCH 0137/1722] Cleanup and some MVP details. --- Examples/Examples/SearchExampleView.swift | 20 +++-- .../Search/LocatorSearchSource.swift | 67 +++++++++----- .../Components/Search/SearchView.swift | 67 ++++---------- .../Components/Search/SearchViewModel.swift | 90 +++++++++++++------ .../Search/SmartLocatorSearchSource.swift | 49 +++++----- .../EsriSearchButtonViewModifier.swift | 2 +- 6 files changed, 162 insertions(+), 133 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index b6d4e298e..69209c000 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -18,12 +18,7 @@ import ArcGISToolkit struct SearchExampleView: View { @ObservedObject - var searchViewModel = SearchViewModel( - sources: [SmartLocatorSearchSource( - displayName: "Locator One", - maximumResults: 16, - maximumSuggestions: 16)] - ) + var searchViewModel = SearchViewModel() let map = Map(basemapStyle: .arcGISImagery) @@ -63,6 +58,19 @@ struct SearchExampleView: View { .onChange(of: searchViewModel.selectedResult, perform: { newValue in display(selectedResult: newValue) }) + .task { + setupSearchViewModel() + } + } + + /// Sets up any desired customization on `searchViewModel`. + private func setupSearchViewModel() { + let smartLocator = SmartLocatorSearchSource( + displayName: "Locator One", + maximumResults: 16, + maximumSuggestions: 16 + ) + searchViewModel.sources = [smartLocator] } fileprivate func display(searchResults: Result<[SearchResult]?, SearchError>) { diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 524383d89..4b58f779a 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -17,50 +17,71 @@ import ArcGIS /// Uses a Locator to provide search and suggest results. Most configuration should be done on the /// `GeocodeParameters` directly. public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { - public init(displayName: String = "Search", + /// Creates a locator search source. + /// - Parameters: + /// - displayName: Name to show when presenting this source in the UI. + /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. + /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. + /// - searchArea: Area to be used as a constraint for searches and suggestions. + /// - preferredSearchLocation: Point to be used as an input to searches and suggestions. + public init(displayName: String = "Locator", + locatorTask: LocatorTask = LocatorTask( + url: URL( + string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" + )! + ), maximumResults: Int = 6, maximumSuggestions: Int = 6, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil) { self.displayName = displayName + self.locatorTask = locatorTask self.maximumResults = maximumResults self.maximumSuggestions = maximumSuggestions self.searchArea = searchArea self.preferredSearchLocation = preferredSearchLocation - - geocodeParameters.maxResults = Int32(maximumResults) - suggestParameters.maxResults = Int32(maximumSuggestions) } - /// The locator used by this search source. - private(set) var locator: LocatorTask = LocatorTask(url: URL(string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer")!) - - /// Parameters used for geocoding. Some properties on parameters will be updated automatically - /// based on searches. - private(set) var geocodeParameters: GeocodeParameters = GeocodeParameters() - - /// Parameters used for getting suggestions. Some properties will be updated automatically - /// based on searches. - private(set) var suggestParameters: SuggestParameters = SuggestParameters() - - public var displayName: String = "Search" + /// Name to show when presenting this source in the UI. + public var displayName: String + /// The maximum results to return when performing a search. Most sources default to 6 public var maximumResults: Int { - didSet { - geocodeParameters.maxResults = Int32(maximumResults) + get { + Int(geocodeParameters.maxResults) + } + set { + geocodeParameters.maxResults = Int32(newValue) } } + /// The maximum suggestions to return. Most sources default to 6. public var maximumSuggestions: Int { - didSet { - suggestParameters.maxResults = Int32(maximumResults) + get { + Int(suggestParameters.maxResults) + } + set { + suggestParameters.maxResults = Int32(newValue) } } + /// Area to be used as a constraint for searches and suggestions. public var searchArea: Geometry? + /// Point to be used as an input to searches and suggestions. public var preferredSearchLocation: Point? + /// The locator used by this search source. + private(set) var locatorTask: LocatorTask + + /// Parameters used for geocoding. Some properties on parameters will be updated automatically + /// based on searches. + private(set) var geocodeParameters: GeocodeParameters = GeocodeParameters() + + /// Parameters used for getting suggestions. Some properties will be updated automatically + /// based on searches. + private(set) var suggestParameters: SuggestParameters = SuggestParameters() + public func search(_ queryString: String, area: Geometry? = nil) async throws -> [SearchResult] { // // This differs from the .NET approach; .NET only uses the @@ -70,7 +91,7 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { geocodeParameters.searchArea = (area != nil) ? area : searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation - let geocodeResults = try await locator.geocode(searchText: queryString, + let geocodeResults = try await locatorTask.geocode(searchText: queryString, parameters: geocodeParameters ) @@ -98,7 +119,7 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { geocodeParameters.preferredSearchLocation = preferredSearchLocation } - let geocodeResults = try await locator.geocode(suggestResult: suggestResult, + let geocodeResults = try await locatorTask.geocode(suggestResult: suggestResult, parameters: geocodeParameters ) @@ -114,7 +135,7 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { suggestParameters.searchArea = searchArea suggestParameters.preferredSearchLocation = preferredSearchLocation - let geocodeResults = try await locator.suggest( + let geocodeResults = try await locatorTask.suggest( searchText: queryString, parameters: suggestParameters ) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index bde9f842a..7dd049842 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -15,28 +15,27 @@ import SwiftUI import Combine import ArcGIS -/// SearchView presents a search experience, powered by underlying SearchViewModel. +/// SearchView presents a search experience, powered by an underlying SearchViewModel. public struct SearchView: View { - public init( - searchViewModel: SearchViewModel - ) { - self.searchViewModel = searchViewModel + public init(searchViewModel: SearchViewModel? = nil) { + if let searchViewModel = searchViewModel { + self.searchViewModel = searchViewModel + } + else { + self.searchViewModel = SearchViewModel( + sources: [LocatorSearchSource()] + ) + } } /// The view model used by the view. The `ViewModel` manages state and handles the activity of /// searching. The view observes `ViewModel` for changes in state. The view calls methods on - /// `ViewModel` in response to user action. The `ViewModel` is created automatically by the - /// view upon construction. If `enableAutomaticConfiguration` is true, the view calls - /// `SearchViewModel.ConfigureForMap` for the map/scene whenever it changes. Both - /// the associated `GeoView` and the `GeoView`'s document can change after initial configuration. + /// `ViewModel` in response to user action. @ObservedObject var searchViewModel: SearchViewModel - private var enableAutomaticConfiguration = true - - @State - private var enableRepeatSearchHereButton = true - + // TODO: go through these properties, and in the SearchViewModel and make sure they're implemented correctly. + @State private var enableResultListView = true @@ -50,10 +49,6 @@ public struct SearchView: View { @State private var currentSuggestion: SearchSuggestion? - /// Indicates that the geoView's viewpoint has changed since the last search. - @State - private var viewpointChanged: Bool = false - @State private var currentTask: Task? @@ -64,21 +59,15 @@ public struct SearchView: View { // TODO: Get proper pins for example app. - How to use SF font with PictureMarkerSymbol?? How to tint calcite icons/images. public var body: some View { VStack (alignment: .center) { - TextField(searchViewModel.defaultPlaceHolder, + TextField(searchViewModel.defaultPlaceholder, text: $searchViewModel.currentQuery) { editing in } onCommit: { - commitSearch.toggle() + commitSearch = true } .esriDeleteTextButton(text: $searchViewModel.currentQuery) .esriSearchButton(performSearch: $commitSearch) .esriShowResultsButton(showResults: $enableResultListView) .esriBorder() - if enableRepeatSearchHereButton, viewpointChanged { - Button("Search Here") { - viewpointChanged = false - commitSearch.toggle() - } - } if enableResultListView { SearchResultList( searchResults: searchViewModel.results, @@ -102,8 +91,8 @@ public struct SearchView: View { } .task(id: commitSearch) { if commitSearch { - await search() commitSearch.toggle() + await search() } } .task(id: currentSuggestion) { @@ -117,30 +106,6 @@ public struct SearchView: View { // MARK: Modifiers - /// Determines whether the view will update its configuration based on the geoview's - /// document automatically. Defaults to `true`. - /// - Parameter enableAutomaticConfiguration: The new value. - /// - Returns: The `SearchView`. - public func enableAutomaticConfiguration(_ enableAutomaticConfiguration: Bool) -> SearchView { - var copy = self - copy.enableAutomaticConfiguration = enableAutomaticConfiguration - return copy - } - - /// Determines whether a button that allows the user to repeat a search with a spatial constraint - /// is displayed automatically. Set to `false` if you want to use a custom button, for example so that - /// you can place it elsewhere on the map. `SearchViewModel` has properties and methods - /// you can use to determine when the custom button should be visible and to trigger the search - /// repeat behavior. Defaults to `true`. - /// - Parameter enableRepeatSearchHereButton: The new value. - /// - Returns: The `SearchView`. - public func enableRepeatSearchHereButton( - _ enableRepeatSearchHereButton: Bool - ) -> SearchView { - self.enableRepeatSearchHereButton = enableRepeatSearchHereButton - return self - } - /// Determines whether a built-in result view will be shown. If `false`, the result display/selection /// list is not shown. Set to `false` if you want to define a custom result list. You might use a /// custom result list to show results in a separate list, disconnected from the rest of the search view. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index d03570b37..1c46e293b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -14,44 +14,42 @@ import Swift import SwiftUI import ArcGIS - -/// Defines how many results to return; one, many, or automatic based on circumstance. -public enum SearchResultMode { - /// Search should always result in at most one result. - case single - /// Search should always try to return multiple results. - case multiple - /// Search should make a choice based on context. E.g. 'coffee shop' should be multiple results, - /// while '380 New York St. Redlands' should be one result. - case automatic -} +import Combine /// Performs searches and manages search state for a Search, or optionally without a UI connection. public class SearchViewModel: ObservableObject { + + /// Defines how many results to return; one, many, or automatic based on circumstance. + public enum SearchResultMode { + /// Search should always result in at most one result. + case single + /// Search should always try to return multiple results. + case multiple + /// Search should make a choice based on context. E.g. 'coffee shop' should be multiple results, + /// while '380 New York St. Redlands' should be one result. + case automatic + } + public convenience init( -defaultPlaceHolder: String = "Find a place or address", +defaultPlaceholder: String = .defaultPlaceholder, activeSource: SearchSourceProtocol? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, -results: Result<[SearchResult]?, SearchError> = .success(nil), -sources: [SearchSourceProtocol] = [], -suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) +sources: [SearchSourceProtocol] = [] ) { self.init() - self.defaultPlaceHolder = defaultPlaceHolder + self.defaultPlaceholder = defaultPlaceholder self.activeSource = activeSource self.queryArea = queryArea self.queryCenter = queryCenter self.resultMode = resultMode - self.results = results self.sources = sources - self.suggestions = suggestions } /// The string shown in the search view when no user query is entered. /// Default is "Find a place or address", or read from web map JSON if specified in the web map configuration. - public var defaultPlaceHolder: String = "Find a place or address" + public var defaultPlaceholder: String = .defaultPlaceholder /// Tracks the currently active search source. All sources are used if this property is `nil`. public var activeSource: SearchSourceProtocol? @@ -68,6 +66,7 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) results = .success(nil) suggestions = .success(nil) } + isEligibleForRequery = false } } @@ -75,8 +74,18 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// `restrictToArea` property is set to true when calling `commitSearch`. This property /// should be updated as the user navigates the map/scene, or at minimum before calling `commitSearch`. public var queryArea: Geometry? { - didSet { - isEligibleForRequery = true + willSet { + var hasResults = false + switch results { + case .success(let results): + hasResults = results != nil + case .failure(_): + break; + } + + // When `queryArea` changes, whether the model is eligible for + // requery is dependent on whether a previous search was performed. + isEligibleForRequery = hasResults } } @@ -115,8 +124,11 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to /// this property are driven by changes to the `queryArea` property. + @Published private(set) var isEligibleForRequery: Bool = false + private var subscriptions = Set() + /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. /// If `restrictToArea` is true, only results in the query area will be returned. @@ -124,11 +136,12 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// of the `queryArea` property. Behavior when called with `restrictToArea` set to true /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. func commitSearch(_ restrictToArea: Bool) async -> Void { - guard !currentQuery.isEmpty, + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } source.searchArea = queryArea source.preferredSearchLocation = queryCenter + selectedResult = nil let searchResult = await Result { try await source.search( @@ -137,7 +150,6 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) ) } - selectedResult = nil isEligibleForRequery = false suggestions = .success(nil) @@ -160,9 +172,8 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// requests before initiating new ones. The view should also wait for some time after user finishes /// typing before making suggestions. The JavaScript implementation uses 150ms by default. func updateSuggestions() async -> Void { - guard !currentQuery.isEmpty, + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } - print("SearchViewModel.updateSuggestions: \(currentQuery)") source.searchArea = queryArea source.preferredSearchLocation = queryCenter @@ -249,7 +260,30 @@ suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// - Parameters: /// - map: Map to use for configuration. func configureForMap(_ map: Map) { - print("SearchViewModel.configureForMap") + // Reset existing properties + sources.removeAll() + activeSource = nil + currentQuery = "" + defaultPlaceholder = .defaultPlaceholder + sources = [LocatorSearchSource()] + + map.loadPublisher.sink(receiveCompletion: { [weak self] completion in + if let localItem = map.item as? LocalItem { + let mmpk = MobileMapPackage(fileURL: localItem.fileURL) + + guard let self = self else { return } + mmpk.loadPublisher.sink(receiveCompletion: { [weak self] completion in + if let locatorTask = mmpk.locatorTask { + self?.sources = [ + LocatorSearchSource(locatorTask: locatorTask) + ] + } + + }, receiveValue: { _ in }) + .store(in: &self.subscriptions) + } + }, receiveValue: { _ in }) + .store(in: &subscriptions) } /// Configures the view model for the provided scene. By default, will only configure the view model @@ -280,3 +314,7 @@ extension SearchViewModel { return source } } + +public extension String { + static let defaultPlaceholder = "Find a place or address" +} diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index e0e3402d0..b78c7385f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -19,28 +19,33 @@ import ArcGIS /// underlying locator to be used well; this class implements behaviors that make assumptions about the /// locator being the world geocode service. public class SmartLocatorSearchSource: LocatorSearchSource { - public convenience init( - displayName: String = "Search", + /// Creates a smart locator search source. + /// - Parameters: + /// - displayName: Name to show when presenting this source in the UI. + /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. + /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. + /// - searchArea: Area to be used as a constraint for searches and suggestions. + /// - preferredSearchLocation: Point to be used as an input to searches and suggestions. + /// - repeatSearchResultThreshold: The minimum number of search results to attempt to return. + /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. + public init( + displayName: String = "Smart Locator", maximumResults: Int = 6, - maximumSuggestions: Int, + maximumSuggestions: Int = 6, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil, repeatSearchResultThreshold: Int = 1, - repeatSuggestResultThreshold: Int = 6, - resultSymbolStyle: SymbolStyle? = nil + repeatSuggestResultThreshold: Int = 6 ) { - self.init() - self.displayName = displayName - self.maximumResults = maximumResults - self.maximumSuggestions = maximumSuggestions - self.searchArea = searchArea - self.preferredSearchLocation = preferredSearchLocation + super.init( + displayName: displayName, + maximumResults: maximumResults, + maximumSuggestions: maximumSuggestions, + searchArea: searchArea, + preferredSearchLocation: preferredSearchLocation + ) self.repeatSearchResultThreshold = repeatSearchResultThreshold self.repeatSuggestResultThreshold = repeatSuggestResultThreshold - self.resultSymbolStyle = resultSymbolStyle - - geocodeParameters.maxResults = Int32(maximumResults) - suggestParameters.maxResults = Int32(maximumSuggestions) } /// The minimum number of results to attempt to return. If there are too few results, the search is @@ -55,14 +60,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search /// repeat behavior. var repeatSuggestResultThreshold: Int = 6 - - /// Web style used to find symbols for results. When set, symbols are found for results based on the - /// result's `Type` field, if available. Defaults to the style identified by the name - /// "Esri2DPointSymbolsStyle". The default Esri 2D point symbol has good results for many of the - /// types returned by the world geocode service. You can use this property to customize result icons - /// by publishing a web style, taking care to ensure that symbol keys match the `Type` attribute - /// returned by the locator. - var resultSymbolStyle: SymbolStyle? public override func search( _ queryString: String, @@ -80,7 +77,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil - let geocodeResults = try await locator.geocode( + let geocodeResults = try await locatorTask.geocode( searchText: queryString, parameters: geocodeParameters ) @@ -113,7 +110,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil - let geocodeResults = try await locator.geocode(suggestResult: suggestResult, + let geocodeResults = try await locatorTask.geocode(suggestResult: suggestResult, parameters: geocodeParameters ) @@ -143,7 +140,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. suggestParameters.searchArea = nil - let geocodeResults = try await locator.suggest( + let geocodeResults = try await locatorTask.suggest( searchText: queryString, parameters: suggestParameters ) diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index f1f2ed133..8933b1051 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -20,7 +20,7 @@ struct EsriSearchButtonViewModifier: ViewModifier { HStack { content Button( - action: { self.performSearch.toggle() }, + action: { self.performSearch = true }, label: { Image(systemName: "magnifyingglass.circle.fill") .foregroundColor(Color(UIColor.opaqueSeparator)) From 3c55b858175e43dd7ddfd620d9aa167117b977e2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 29 Jul 2021 13:56:37 -0500 Subject: [PATCH 0138/1722] Remove ConfigureForMap/Scene --- .../Components/Search/SearchViewModel.swift | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 1c46e293b..dd7ad5143 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -254,47 +254,6 @@ sources: [SearchSourceProtocol] = [] } } - /// Configures the view model for the provided map. By default, will only configure the view model - /// with the default world geocoder. In future updates, additional functionality may be added to take - /// web map configuration into account. - /// - Parameters: - /// - map: Map to use for configuration. - func configureForMap(_ map: Map) { - // Reset existing properties - sources.removeAll() - activeSource = nil - currentQuery = "" - defaultPlaceholder = .defaultPlaceholder - sources = [LocatorSearchSource()] - - map.loadPublisher.sink(receiveCompletion: { [weak self] completion in - if let localItem = map.item as? LocalItem { - let mmpk = MobileMapPackage(fileURL: localItem.fileURL) - - guard let self = self else { return } - mmpk.loadPublisher.sink(receiveCompletion: { [weak self] completion in - if let locatorTask = mmpk.locatorTask { - self?.sources = [ - LocatorSearchSource(locatorTask: locatorTask) - ] - } - - }, receiveValue: { _ in }) - .store(in: &self.subscriptions) - } - }, receiveValue: { _ in }) - .store(in: &subscriptions) - } - - /// Configures the view model for the provided scene. By default, will only configure the view model - /// with the default world geocoder. In future updates, additional functionality may be added to take - /// web scene configuration into account. - /// - Parameters: - /// - scene: Scene used for configuration. - func configureForScene(_ scene: ArcGIS.Scene) { - print("SearchViewModel.configureForScene") - } - /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, /// and reset the current query. func clearSearch() { From 7ef78729c27ac4dcd3dc72e1d5610b5835a535db Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 29 Jul 2021 16:15:30 -0500 Subject: [PATCH 0139/1722] Cleanup --- .../Search/LocatorSearchSource.swift | 4 -- .../Components/Search/SearchView.swift | 46 ++++++++--------- .../Components/Search/SearchViewModel.swift | 50 +++++++++++-------- .../Extensions/GeocodeResult.swift | 3 ++ .../Extensions/SuggestResult.swift | 3 ++ 5 files changed, 56 insertions(+), 50 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 4b58f779a..dbf54c2f1 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -128,10 +128,6 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { } public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { - // - // This differs from the .NET approach; .NET only uses the - // center of `searchArea` for the `geocodeParameters.preferredSearchLocation`. - // suggestParameters.searchArea = searchArea suggestParameters.preferredSearchLocation = preferredSearchLocation diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 7dd049842..ee5113e01 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -34,38 +34,38 @@ public struct SearchView: View { @ObservedObject var searchViewModel: SearchViewModel - // TODO: go through these properties, and in the SearchViewModel and make sure they're implemented correctly. - + /// Determines whether a built-in result view will be shown. Defaults to true. + /// If false, the result display/selection list is not shown. Set to false if you want to hide the results + /// or define a custom result list. You might use a custom result list to show results in a separate list, + /// disconnected from the rest of the search view. @State private var enableResultListView = true + /// Message to show when there are no results or suggestions. Defaults to "No results found". private var noResultMessage = "No results found" /// Indicates that the `SearchViewModel` should start a search. @State - private var commitSearch = false + private var shouldCommitSearch = false /// Indicates that the `SearchViewModel` should accept a suggestion. @State private var currentSuggestion: SearchSuggestion? + /// The currently executing async task. `currentTask` should be cancelled + /// prior to starting another async task. @State private var currentTask: Task? - // TODO: Figure out better styling for list - // TODO: continue fleshing out SearchViewModel and LocatorSearchSource/SmartSearchSource - // TODO: following Nathan's lead on all this stuff, i.e., go through his code and duplicate it as I go. - // TODO: better modifiers for search text field; maybe SearchTextField or something... - // TODO: Get proper pins for example app. - How to use SF font with PictureMarkerSymbol?? How to tint calcite icons/images. public var body: some View { VStack (alignment: .center) { TextField(searchViewModel.defaultPlaceholder, text: $searchViewModel.currentQuery) { editing in } onCommit: { - commitSearch = true + shouldCommitSearch = true } .esriDeleteTextButton(text: $searchViewModel.currentQuery) - .esriSearchButton(performSearch: $commitSearch) + .esriSearchButton(performSearch: $shouldCommitSearch) .esriShowResultsButton(showResults: $enableResultListView) .esriBorder() if enableResultListView { @@ -89,9 +89,9 @@ public struct SearchView: View { await suggest() } } - .task(id: commitSearch) { - if commitSearch { - commitSearch.toggle() + .task(id: shouldCommitSearch) { + if shouldCommitSearch { + shouldCommitSearch.toggle() await search() } } @@ -103,7 +103,7 @@ public struct SearchView: View { } } } - + // MARK: Modifiers /// Determines whether a built-in result view will be shown. If `false`, the result display/selection @@ -168,12 +168,11 @@ struct SearchResultList: View { List { ForEach(results) { result in SearchResultRow(result: result) - .onTapGesture { - selectedResult = result - } + .onTapGesture { + selectedResult = result + } } } - // .listStyle(DefaultListStyle()) } } else if results != nil { @@ -205,11 +204,10 @@ struct SearchSuggestionList: View { if suggestions.count > 0 { ForEach(suggestions) { suggestion in SuggestionResultRow(suggestion: suggestion) - .onTapGesture() { - currentSuggestion = suggestion - } + .onTapGesture() { + currentSuggestion = suggestion + } } - // .listStyle(DefaultListStyle()) } } } @@ -230,7 +228,7 @@ struct SearchSuggestionList: View { struct SearchResultRow: View { var result: SearchResult - + var body: some View { HStack { Image(systemName: "mappin") @@ -242,7 +240,7 @@ struct SearchResultRow: View { struct SuggestionResultRow: View { var suggestion: SearchSuggestion - + var body: some View { HStack { let imageName = suggestion.isCollection ? "magnifyingglass" : "mappin" diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index dd7ad5143..820d55df7 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -18,7 +18,6 @@ import Combine /// Performs searches and manages search state for a Search, or optionally without a UI connection. public class SearchViewModel: ObservableObject { - /// Defines how many results to return; one, many, or automatic based on circumstance. public enum SearchResultMode { /// Search should always result in at most one result. @@ -29,14 +28,22 @@ public class SearchViewModel: ObservableObject { /// while '380 New York St. Redlands' should be one result. case automatic } - + + /// Creates a `SearchViewModel`. + /// - Parameters: + /// - defaultPlaceholder: The string shown in the search view when no user query is entered. + /// - activeSource: Tracks the currently active search source. + /// - queryArea: The search area to be used for the current query. + /// - queryCenter: Defines the center for the search. + /// - resultMode: Defines how many results to return. + /// - sources: Collection of search sources to be used. public convenience init( -defaultPlaceholder: String = .defaultPlaceholder, -activeSource: SearchSourceProtocol? = nil, -queryArea: Geometry? = nil, -queryCenter: Point? = nil, -resultMode: SearchResultMode = .automatic, -sources: [SearchSourceProtocol] = [] + defaultPlaceholder: String = .defaultPlaceholder, + activeSource: SearchSourceProtocol? = nil, + queryArea: Geometry? = nil, + queryCenter: Point? = nil, + resultMode: SearchResultMode = .automatic, + sources: [SearchSourceProtocol] = [] ) { self.init() self.defaultPlaceholder = defaultPlaceholder @@ -112,6 +119,7 @@ sources: [SearchSourceProtocol] = [] /// Collection of search sources to be used. This list is maintained over time and is not nullable. /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. + /// NOTE: only the first source is currently used; multiple sources are not yet supported. public var sources: [SearchSourceProtocol] = [] /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there @@ -125,17 +133,17 @@ sources: [SearchSourceProtocol] = [] /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to /// this property are driven by changes to the `queryArea` property. @Published - private(set) var isEligibleForRequery: Bool = false + public private(set) var isEligibleForRequery: Bool = false private var subscriptions = Set() - + /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. /// If `restrictToArea` is true, only results in the query area will be returned. /// - Parameter restrictToArea: If true, the search is restricted to results within the extent /// of the `queryArea` property. Behavior when called with `restrictToArea` set to true /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. - func commitSearch(_ restrictToArea: Bool) async -> Void { + public func commitSearch(_ restrictToArea: Bool) async -> Void { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } @@ -168,10 +176,8 @@ sources: [SearchSourceProtocol] = [] } } - /// Updates suggestions list asynchronously. View should take care to cancel previous suggestion - /// requests before initiating new ones. The view should also wait for some time after user finishes - /// typing before making suggestions. The JavaScript implementation uses 150ms by default. - func updateSuggestions() async -> Void { + /// Updates suggestions list asynchronously. + public func updateSuggestions() async -> Void { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } @@ -199,13 +205,11 @@ sources: [SearchSourceProtocol] = [] } /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is - /// generally the same as `commitSearch`, except the suggestion is used instead of the - /// `currentQuery` property. When a suggestion is accepted, `currentQuery` is updated to - /// match the suggestion text. The view should take care not to submit a separate search in response - /// to changes to `currentQuery` initiated by a call to this method. + /// generally the same as `commitSearch`, except `searchSuggestion` is used instead of the + /// `currentQuery` property. /// - Parameters: /// - searchSuggestion: The suggestion to use to commit the search. - func acceptSuggestion(_ searchSuggestion: SearchSuggestion) async -> Void { + public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) async -> Void { currentQuery = searchSuggestion.displayTitle var searchResults = [SearchResult]() @@ -253,16 +257,18 @@ sources: [SearchSourceProtocol] = [] } } } - + /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, /// and reset the current query. - func clearSearch() { + public func clearSearch() { // Setting currentQuery to "" will reset everything necessary. currentQuery = "" } } extension SearchViewModel { + /// Returns the search source to be used in geocode operations. + /// - Returns: The search source to use. func currentSource() -> SearchSourceProtocol? { var source: SearchSourceProtocol? if let activeSource = activeSource { diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift index dd49205a8..4180f0f87 100644 --- a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift +++ b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift @@ -14,6 +14,9 @@ import ArcGIS extension GeocodeResult { + /// Converts a `GeocodeResult` to a `SearchResult`. + /// - Parameter searchSource: The search source generating the result. + /// - Returns: The new `SearchResult`. func toSearchResult(searchSource: SearchSourceProtocol) -> SearchResult { let subtitle = self.attributes["LongLabel"] as? String ?? "Match percent: \((self.score / 100.0).formatted(.percent))" diff --git a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift index fdbf1cca7..cb4c61e61 100644 --- a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift +++ b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift @@ -14,6 +14,9 @@ import ArcGIS extension SuggestResult { + /// Converts a `SuggestResult` to a `SearchSuggestion`. + /// - Parameter searchSource: The search source generating the result. + /// - Returns: The new `SearchSuggestion`. func toSearchSuggestion(searchSource: SearchSourceProtocol) -> SearchSuggestion { return SearchSuggestion(displayTitle: label, displaySubtitle: nil, From 888237fd7343ce60fcde66cfd5937cf3f0990753 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 29 Jul 2021 16:36:01 -0500 Subject: [PATCH 0140/1722] Initial Test. --- .../SearchViewModelTests.swift | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Tests/ArcGISToolkitTests/SearchViewModelTests.swift diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift new file mode 100644 index 000000000..749dda79d --- /dev/null +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -0,0 +1,63 @@ +// Copyright 2021 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 Foundation + +import XCTest +import ArcGIS +import ArcGISToolkit + +class SearchViewModelTests: XCTestCase { + func testIsEligibleForRequery() { + let model = SearchViewModel() + XCTAssertFalse(model.isEligibleForRequery) + + model.queryArea = createPolygon() + XCTAssertFalse(model.isEligibleForRequery) + + model.results = .success([]) + model.queryArea = createPolygon() + XCTAssertTrue(model.isEligibleForRequery) + } + + func testCommitSearch() { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + XCTAssertEqual(model.sources.count, 1) + + model.currentQuery = "London" + let result = await model.commitSearch(false) + + model.queryArea = createPolygon() + XCTAssertFalse(model.isEligibleForRequery) + + model.results = .success([]) + model.queryArea = createPolygon() + XCTAssertTrue(model.isEligibleForRequery) + } +} + +extension SearchViewModelTests { + func createPolygon() -> Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: .london) + let _ = builder.add(point: .paris) + let _ = builder.add(point: .rome) + return builder.toGeometry() as! ArcGIS.Polygon + } +} + +extension Point { + static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) + static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) + static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) +} From f5a34f678ddfa1dfee2b68767318e560190f4836 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 29 Jul 2021 16:44:32 -0500 Subject: [PATCH 0141/1722] Remove tests for now. --- .../SearchViewModelTests.swift | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 Tests/ArcGISToolkitTests/SearchViewModelTests.swift diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift deleted file mode 100644 index 749dda79d..000000000 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2021 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 Foundation - -import XCTest -import ArcGIS -import ArcGISToolkit - -class SearchViewModelTests: XCTestCase { - func testIsEligibleForRequery() { - let model = SearchViewModel() - XCTAssertFalse(model.isEligibleForRequery) - - model.queryArea = createPolygon() - XCTAssertFalse(model.isEligibleForRequery) - - model.results = .success([]) - model.queryArea = createPolygon() - XCTAssertTrue(model.isEligibleForRequery) - } - - func testCommitSearch() { - let model = SearchViewModel(sources: [LocatorSearchSource()]) - XCTAssertEqual(model.sources.count, 1) - - model.currentQuery = "London" - let result = await model.commitSearch(false) - - model.queryArea = createPolygon() - XCTAssertFalse(model.isEligibleForRequery) - - model.results = .success([]) - model.queryArea = createPolygon() - XCTAssertTrue(model.isEligibleForRequery) - } -} - -extension SearchViewModelTests { - func createPolygon() -> Polygon { - let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: .london) - let _ = builder.add(point: .paris) - let _ = builder.add(point: .rome) - return builder.toGeometry() as! ArcGIS.Polygon - } -} - -extension Point { - static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) - static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) - static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) -} From 131eaaa2152df1304fd118d1d12f8fe54fcc2246 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 30 Jul 2021 08:53:01 -0500 Subject: [PATCH 0142/1722] use actual Pin image for search result locations. --- Examples/Examples/SearchExampleView.swift | 6 +- .../MapPin.imageset/Contents.json | 67 ++++++++++++++++++ .../MapPin.imageset/iOS8_MapPin_End108.png | Bin 0 -> 1909 bytes .../MapPin.imageset/iOS8_MapPin_End36.png | Bin 0 -> 1256 bytes .../MapPin.imageset/iOS8_MapPin_End72.png | Bin 0 -> 1624 bytes 5 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End108.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End36.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End72.png diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 69209c000..f2edcc1be 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -123,9 +123,5 @@ struct SearchExampleView_Previews: PreviewProvider { private extension Symbol { /// A search result marker symbol. - static let resultSymbol: MarkerSymbol = SimpleMarkerSymbol( - style: .diamond, - color: .red, - size: 12.0 - ) + static let resultSymbol: MarkerSymbol = PictureMarkerSymbol(image: UIImage(named: "MapPin")!) } diff --git a/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json new file mode 100644 index 000000000..e4f68a3f0 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json @@ -0,0 +1,67 @@ +{ + "images" : [ + { + "idiom" : "universal" + }, + { + "filename" : "iOS8_MapPin_End36.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "iOS8_MapPin_End72.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "iOS8_MapPin_End108.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "idiom" : "iphone" + }, + { + "idiom" : "iphone", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "scale" : "1x", + "subtype" : "retina4" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "subtype" : "retina4" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "subtype" : "retina4" + }, + { + "idiom" : "ipad" + }, + { + "idiom" : "ipad", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End108.png b/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End108.png new file mode 100644 index 0000000000000000000000000000000000000000..462e1a52b9aad96381bbff52ab651d181d228d41 GIT binary patch literal 1909 zcmeAS@N?(olHy`uVBq!ia0vp^mOz}t!3HERL`xk6Qj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07?@QuLn2Bde0{8v^Kf6`()~Xj@TAnpKdC8`Lf!&sHg;q@=(~U%$M( zT(8_%FTW^V-_X+15@d#vkuFe$ZgFK^Nn(X=Ua>OF1ees}+T7#d8#0MoBXEYLU9GXQxBrqI_HztY@Xxa#7Ppj3o=u^L<)Qdy9y zACy|0Us{w5jJPyqkW~d%&PAz-CHX}m`T04pPz=b(FUc>?$S+WE4mMNJ@J&q4%mWE% zf_3=%T6yLbmn7yTr+T{BDgn*V%gju%GIlgKb2fIfG;wxvG&FQIaWrvpbFpwXG%_`E zGj}$0gXwk2PcF?(%`1WFO+n~&#HkmQ7;+1MHoK%2WtOF;xE1B+Du6w0m5J3Y#%31g z2F^ImgX&Ge?G_81di8;h(Fa8>QdGl)fawRsgeP1e2cGm(^ML8S2$--v>W}OK=9lN5 zE{-7;x8B^b&%f*-!ccJAca}--EG2;^#*n~`il(C6Ldf+m)@&BcaEA@mHqzm=fz9ryw2!S zUm7IIcVpUuyTbnKU4L|RS>!q2Zr%2IS=8N&7tPC=M3T%F-0kn^liZ;B)_UP4{HN~vAmgZb@f5qqWZu{+X_fB?xmY$`w zp+=s0n~*f;0r7$gula}1=$t=mdA-O|c|+iX__fnMFTVJ}SnsHJgI#wE(5NTrR_~bZ z^w!vgaQ1cmt*YH1{Gn3PBJsz{iZf0-(;rxdf2@6f#psG{ zP4#o*<7RoD5c8bjt!KiSH`^C0@1JwrF?4U8f8*&1J9l$)hj|-!@HzA)>ilb5b9F(; z?G&X${}T3Zs8>0)=s&aJVu@z{=KY4%<_lf6_OCy$do0Xu@`ma^!?Sw+DuhqUg ze$BkY6StG^y8X)@6=^QXrJ@~w)gSJTDLiTM;Cx$O~37*sLt3I^HGJ>O0(e2ro3ALvIhdPuV*@z zGCJ#D$aOAdQoh@Iqv;i!+rFztyrzsjrS_RzSzHpomqnUiQ7KrI&sO5-%dGzIS8~t_ z-UZX%S3GjcW!zdjQB*WbxZ|Oflyi<}&+mX=)dwCOo3%sq(TdvbKFnK{7dz!L^eP+% zD(zJ~?wHHetJLmTc>ehNDSr z1<%~X^wgl##FWaylc_cg49qH-ArU1JzCKpT`MG+DAT@dwxdlMo3=B5*6$OdO*{LN8 zNvY|XdA3ULckfqH$V{%1*XSQL?vFu&J;D8jzb> zlBiITo0C^;Rbi_HHrEQs1_|pcDS(xfWZNo192Makpx~Tel&WB=XRMoSU}&gdW~OIo zVrph)sH0$HU}&Uo07PcGh9*{~W>!Y#3Q(W~w5=#5%__*n4QdyVXRDM^Qc_^0uU}qX zu2*iXmtT~wZ)j<02{OaTNEfI=x41H|B(Xv_uUHvof=g;~a#3bMNoIbY0?5R~r2Ntn zTP2`NAzsKWfE$}v3=Jk=fazBx7U&!58GyV5Q|Rl9UukYGTy=3tP%6T`SPd=?sVqp< z4@xc0FD*(2MqHXQ$f^P>=c3falKi5O{QMkPC z!8&|>tvvIJOA_;vQ$1a5m4IgGWoD*W89N%AS(qD`nK(N+8XCHqIGVV)xmY+G8krio znL8W0!SuT1Czs}?=9R$orXcj1m}l%r{phve z(v+Zomm4(yPC2%tYO;^o!kO}y`=9hKaJh5)o9-#oP0cpTOKOykpP24`T=JNHhr}g! z$vF|zY_d%_ruOkDtL|Ja>RU5^pH2VjL;mFs^Xm=fvGq3Rtrk^`G%UQkVwTf{)LA6~ zt0!_XNu5pm!_LFUx}Z<>$O`q;XDXgLa_tPPpL{hx9o}&IF2DHMG{q_6_fM;G-ZA^W z`LNUfEpf|NZ9MS#_LuLA3JYdW-KfB`{qLm(_B_3rKFLy-Z`3E8I;a_J+E+Lu=a*=5 z`1{M5suLEnYJK0F{6IzSF~eQy9_Ksf{l1*}#=3dIT=5Ul2K-)o&l+vuxD~ll$XZI@ zc>VOZ+qP_+QBZN9d%>OeI@yPeG|D$UX}MAK8U}fi7AzZCsS=07?@QuLn2Bde0{8v^Kf6`()~Xj@TAnpKdC8`Lf!&sHg;q@=(~U%$M( zT(8_%FTW^V-_X+15@d#vkuFe$ZgFK^Nn(X=Ua>OF1ees}+T7#d8#0MoBXEYLU9GXQxBrqI_HztY@Xxa#7Ppj3o=u^L<)Qdy9y zACy|0Us{w5jJPyqkW~d%&PAz-CHX}m`T04pPz=b(FUc>?$S+WE4mMNJ@J&q4%mWE% zf_3=%T6yLbmn7yTr+T{BDgn*V%gju%GIlgJvoJSsG;wxvG&FQIaWrvpbFpwXG%_`E zGj}$0gXwk2PcF?(%`1WFO+n~&!l@UO7;+1MHoK%2WtOF;xE1B+Du6w0m5JLe7C6m= z>P^Az7E7FZ^?{Dj2SqJXRKtXT=?BDwCtM&0p7c}mfa$#mn6Qn@dw_X@>6E97V@SoV zH@7zCT@DaodysAIx7eiDI>du#fx$)t3B`>D4E`5Bu}oQNoZnEv{7kP!-NA-2HNny8 z)d3z+p`@1q>RBC=)gJzN7WeX)=bYECZ%=zD^YTPl=+8RK<7G;+)rSoD7G=yf^Vdi| zb$p8KOxHrC&%#d)pI%IyG-u|T=aLQIGC#*Gia8?q)S9VmZ30V`=j`w)w_oL3C(cN< z=5V?E-8E#jDA%NJZo?UVuK7lS%O4j`i&{{$p7X~s4mX7@&-dm;vhBE?(>43+E)LuE zK`W1Kp83O8s`LHf#I|XTV)n`pPI^f!Z1~xs#aNhM9DL{A4THEJB?dFB*IjJ?^wLH# z)M81k%<3OAmp!`uv$AdK)oQODk6CB*IqjCq%bmI|KyF%p!@27Vip@D*U9<~`|5bbS zk>#A_cMBbkW=IB@ZRA_V9DSuTAWXnwOT;$O9mb*2F)s|)E&siC+Kx9f0+tHxIu}$q zH}UK<`xG7BLzA!mY811}+P-=BIWr&aW$v@J!k1rLGNvJuBnWOOY2ebF+Q^I*S_v`f@aeU z&R+Omw7F@rY4o5)~0Shi`S0QPb3enh~K>49|$L&)^QxoWc2y-!W!^*WqgqKOF1io+7%2 Date: Fri, 30 Jul 2021 10:15:05 -0500 Subject: [PATCH 0143/1722] Cleanup. --- Examples/Examples/SearchExampleView.swift | 25 +++++---- .../Search/LocatorSearchSource.swift | 55 +++++++++++-------- .../Components/Search/SearchResult.swift | 22 ++++---- .../Components/Search/SearchSuggestion.swift | 20 ++++--- .../Components/Search/SearchView.swift | 46 +++++++++++----- .../Components/Search/SearchViewModel.swift | 33 ++++++----- .../Search/SmartLocatorSearchSource.swift | 37 ++++++++----- .../EsriShowResultsButtonViewModifier.swift | 32 +++++++---- 8 files changed, 162 insertions(+), 108 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index f2edcc1be..8e7114033 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -23,7 +23,10 @@ struct SearchExampleView: View { let map = Map(basemapStyle: .arcGISImagery) @State - var searchResultViewpoint: Viewpoint? = nil + var searchResultViewpoint: Viewpoint? = Viewpoint( + center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), + scale: 1000000 + ) var searchResultsOverlay = GraphicsOverlay() @@ -78,8 +81,10 @@ struct SearchExampleView: View { case .success(let results): var resultGraphics = [Graphic]() results?.forEach({ result in - let graphic = Graphic(geometry: result.geoElement?.geometry, - symbol: .resultSymbol) + let graphic = Graphic( + geometry: result.geoElement?.geometry, + symbol: .resultSymbol + ) resultGraphics.append(graphic) }) @@ -91,7 +96,9 @@ struct SearchExampleView: View { let envelope = searchResultsOverlay.extent { let builder = EnvelopeBuilder(envelope: envelope) builder.expand(factor: 1.1) - searchResultViewpoint = Viewpoint(targetExtent: builder.toGeometry()) + searchResultViewpoint = Viewpoint( + targetExtent: builder.toGeometry() + ) } else { searchResultViewpoint = nil @@ -115,13 +122,9 @@ struct SearchExampleView: View { } } -struct SearchExampleView_Previews: PreviewProvider { - static var previews: some View { - SearchExampleView() - } -} - private extension Symbol { /// A search result marker symbol. - static let resultSymbol: MarkerSymbol = PictureMarkerSymbol(image: UIImage(named: "MapPin")!) + static let resultSymbol: MarkerSymbol = PictureMarkerSymbol( + image: UIImage(named: "MapPin")! + ) } diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index dbf54c2f1..a577b01ef 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -24,16 +24,18 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. /// - searchArea: Area to be used as a constraint for searches and suggestions. /// - preferredSearchLocation: Point to be used as an input to searches and suggestions. - public init(displayName: String = "Locator", - locatorTask: LocatorTask = LocatorTask( - url: URL( - string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" - )! - ), - maximumResults: Int = 6, - maximumSuggestions: Int = 6, - searchArea: Geometry? = nil, - preferredSearchLocation: Point? = nil) { + public init( + displayName: String = "Locator", + locatorTask: LocatorTask = LocatorTask( + url: URL( + string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" + )! + ), + maximumResults: Int = 6, + maximumSuggestions: Int = 6, + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil + ) { self.displayName = displayName self.locatorTask = locatorTask self.maximumResults = maximumResults @@ -72,18 +74,21 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { public var preferredSearchLocation: Point? /// The locator used by this search source. - private(set) var locatorTask: LocatorTask + public private(set) var locatorTask: LocatorTask /// Parameters used for geocoding. Some properties on parameters will be updated automatically /// based on searches. - private(set) var geocodeParameters: GeocodeParameters = GeocodeParameters() + public private(set) var geocodeParameters: GeocodeParameters = GeocodeParameters() /// Parameters used for getting suggestions. Some properties will be updated automatically /// based on searches. - private(set) var suggestParameters: SuggestParameters = SuggestParameters() - - public func search(_ queryString: String, area: Geometry? = nil) async throws -> [SearchResult] { - // + public private(set) var suggestParameters: SuggestParameters = SuggestParameters() + + public func search( + _ queryString: String, + area: Geometry? = nil + ) async throws -> [SearchResult] { + // TODO: think about this... // This differs from the .NET approach; .NET only uses the // center of `searchArea` for the `geocodeParameters.preferredSearchLocation` // and only sets `geocodeParameters.searchArea` from the `area` argument. @@ -91,15 +96,18 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { geocodeParameters.searchArea = (area != nil) ? area : searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation - let geocodeResults = try await locatorTask.geocode(searchText: queryString, - parameters: geocodeParameters + let geocodeResults = try await locatorTask.geocode( + searchText: queryString, + parameters: geocodeParameters ) // Convert to SearchResults and return. return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } - public func search(_ searchSuggestion: SearchSuggestion) async throws -> [SearchResult] { + public func search( + _ searchSuggestion: SearchSuggestion + ) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] } geocodeParameters.searchArea = nil @@ -119,15 +127,18 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { geocodeParameters.preferredSearchLocation = preferredSearchLocation } - let geocodeResults = try await locatorTask.geocode(suggestResult: suggestResult, - parameters: geocodeParameters + let geocodeResults = try await locatorTask.geocode( + suggestResult: suggestResult, + parameters: geocodeParameters ) // Convert to SearchResults and return. return geocodeResults.map{ $0.toSearchResult(searchSource: self) } } - public func suggest(_ queryString: String) async throws -> [SearchSuggestion] { + public func suggest( + _ queryString: String + ) async throws -> [SearchSuggestion] { suggestParameters.searchArea = searchArea suggestParameters.preferredSearchLocation = preferredSearchLocation diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 89963e983..7634bd0da 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -16,12 +16,14 @@ import ArcGIS /// Wraps a search result for display. public class SearchResult { - public init(displayTitle: String, - displaySubtitle: String? = nil, - markerImage: UIImage? = nil, - owningSource: SearchSourceProtocol, - geoElement: GeoElement? = nil, - selectionViewpoint: Viewpoint? = nil) { + public init( + displayTitle: String, + displaySubtitle: String? = nil, + markerImage: UIImage? = nil, + owningSource: SearchSourceProtocol, + geoElement: GeoElement? = nil, + selectionViewpoint: Viewpoint? = nil + ) { self.displayTitle = displayTitle self.displaySubtitle = displaySubtitle self.markerImage = markerImage @@ -77,15 +79,15 @@ extension SearchResult: Hashable { if let geometry = geoElement?.geometry { hasher.combine(geometry) } - + if let locatorSource = owningSource as? LocatorSearchSource { hasher.combine(ObjectIdentifier(locatorSource)) } // If you define a custom type that does NOT inherit from // `LocatorSearchSource`, you will need to add an `else if` check // for your custom type. -// else if let customSearchSource = owningSource as? MyCustomSearchSource { -// hasher.combine(ObjectIdentifier(customSearchSource)) -// } + // else if let customSearchSource = owningSource as? MyCustomSearchSource { + // hasher.combine(ObjectIdentifier(customSearchSource)) + // } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index aaf01af0a..63bd645b1 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -15,11 +15,13 @@ import ArcGIS /// Wraps a suggestion for display. public class SearchSuggestion { - internal init(displayTitle: String, - displaySubtitle: String? = nil, - owningSource: SearchSourceProtocol, - suggestResult: SuggestResult? = nil, - isCollection: Bool) { + internal init( + displayTitle: String, + displaySubtitle: String? = nil, + owningSource: SearchSourceProtocol, + suggestResult: SuggestResult? = nil, + isCollection: Bool + ) { self.displayTitle = displayTitle self.displaySubtitle = displaySubtitle self.owningSource = owningSource @@ -67,15 +69,15 @@ extension SearchSuggestion: Hashable { hasher.combine(displayTitle) hasher.combine(displaySubtitle) hasher.combine(isCollection) - + if let locatorSource = owningSource as? LocatorSearchSource { hasher.combine(ObjectIdentifier(locatorSource)) } // If you define a custom type that does NOT inherit from // `LocatorSearchSource`, you will need to add an `else if` check // for your custom type. -// else if let customSearchSource = owningSource as? MyCustomSearchSource { -// hasher.combine(ObjectIdentifier(customSearchSource)) -// } + // else if let customSearchSource = owningSource as? MyCustomSearchSource { + // hasher.combine(ObjectIdentifier(customSearchSource)) + // } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index ee5113e01..548e0e7c2 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -28,9 +28,9 @@ public struct SearchView: View { } } - /// The view model used by the view. The `ViewModel` manages state and handles the activity of - /// searching. The view observes `ViewModel` for changes in state. The view calls methods on - /// `ViewModel` in response to user action. + /// The view model used by the view. The `SearchViewModel` manages state and handles the + /// activity of searching. The view observes `SearchViewModel` for changes in state. The view + /// calls methods on `SearchViewModel` in response to user action. @ObservedObject var searchViewModel: SearchViewModel @@ -38,7 +38,6 @@ public struct SearchView: View { /// If false, the result display/selection list is not shown. Set to false if you want to hide the results /// or define a custom result list. You might use a custom result list to show results in a separate list, /// disconnected from the rest of the search view. - @State private var enableResultListView = true /// Message to show when there are no results or suggestions. Defaults to "No results found". @@ -48,7 +47,7 @@ public struct SearchView: View { @State private var shouldCommitSearch = false - /// Indicates that the `SearchViewModel` should accept a suggestion. + /// The current suggestion selected by the user. @State private var currentSuggestion: SearchSuggestion? @@ -57,18 +56,27 @@ public struct SearchView: View { @State private var currentTask: Task? + /// Determines whether the results lists are displayed. + @State + private var isResultDisplayHidden: Bool = false + public var body: some View { VStack (alignment: .center) { - TextField(searchViewModel.defaultPlaceholder, - text: $searchViewModel.currentQuery) { editing in + TextField( + searchViewModel.defaultPlaceholder, + text: $searchViewModel.currentQuery + ) { _ in } onCommit: { shouldCommitSearch = true } .esriDeleteTextButton(text: $searchViewModel.currentQuery) .esriSearchButton(performSearch: $shouldCommitSearch) - .esriShowResultsButton(showResults: $enableResultListView) + .esriShowResultsButton( + isEnabled: !enableResultListView, + isHidden: $isResultDisplayHidden + ) .esriBorder() - if enableResultListView { + if enableResultListView, !isResultDisplayHidden { SearchResultList( searchResults: searchViewModel.results, selectedResult: $searchViewModel.selectedResult, @@ -91,13 +99,14 @@ public struct SearchView: View { } .task(id: shouldCommitSearch) { if shouldCommitSearch { + // User committed changes (hit Enter/Search button) shouldCommitSearch.toggle() await search() } } .task(id: currentSuggestion) { - // User committed changes (hit Enter/Search button) if let suggestion = currentSuggestion { + // User selected a suggestion. await accept(suggestion) currentSuggestion = nil } @@ -113,8 +122,9 @@ public struct SearchView: View { /// - Parameter enableResultListView: The new value. /// - Returns: The `SearchView`. public func enableResultListView(_ enableResultListView: Bool) -> SearchView { - self.enableResultListView = enableResultListView - return self + var copy = self + copy.enableResultListView = enableResultListView + return copy } /// Message to show when there are no results or suggestions. Defaults to "No results found". @@ -164,7 +174,7 @@ struct SearchResultList: View { case .success(let results): if let results = results, results.count > 0 { if results.count > 1 { - // If we have only 1 results, don't show the list. + // Only show the list if we have more than one result. List { ForEach(results) { result in SearchResultRow(result: result) @@ -233,7 +243,10 @@ struct SearchResultRow: View { HStack { Image(systemName: "mappin") .foregroundColor(Color(.red)) - ResultRow(title: result.displayTitle, subtitle: result.displaySubtitle) + ResultRow( + title: result.displayTitle, + subtitle: result.displaySubtitle + ) } } } @@ -245,7 +258,10 @@ struct SuggestionResultRow: View { HStack { let imageName = suggestion.isCollection ? "magnifyingglass" : "mappin" Image(systemName: imageName) - ResultRow(title: suggestion.displayTitle, subtitle: suggestion.displaySubtitle) + ResultRow( + title: suggestion.displayTitle, + subtitle: suggestion.displaySubtitle + ) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 820d55df7..85bc97812 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -55,16 +55,13 @@ public class SearchViewModel: ObservableObject { } /// The string shown in the search view when no user query is entered. - /// Default is "Find a place or address", or read from web map JSON if specified in the web map configuration. + /// Default is "Find a place or address". public var defaultPlaceholder: String = .defaultPlaceholder - /// Tracks the currently active search source. All sources are used if this property is `nil`. + /// The active search source. If `nil`, the first item in `sources` is used. public var activeSource: SearchSourceProtocol? - /// Tracks the current user-entered query. This should be updated by the view after every key press. - /// This property drives both suggestions and searches. This property can be changed by - /// other method calls and property changes within the view model, so the view should take care to - /// observe for changes. + /// Tracks the current user-entered query. This property drives both suggestions and searches. @Published public var currentQuery: String = "" { didSet { @@ -77,9 +74,8 @@ public class SearchViewModel: ObservableObject { } } - /// The search area to be used for the current query. Ignored in most queries, unless the - /// `restrictToArea` property is set to true when calling `commitSearch`. This property - /// should be updated as the user navigates the map/scene, or at minimum before calling `commitSearch`. + /// The search area to be used for the current query. This property should be updated + /// as the user navigates the map/scene, or at minimum before calling `commitSearch`. public var queryArea: Geometry? { willSet { var hasResults = false @@ -90,18 +86,19 @@ public class SearchViewModel: ObservableObject { break; } - // When `queryArea` changes, whether the model is eligible for - // requery is dependent on whether a previous search was performed. + // When `queryArea` changes, the model is eligible for + // requery if there are previous results. isEligibleForRequery = hasResults } } - /// Defines the center for the search. This should be updated by the view every time the - /// user navigates the map. + /// Defines the center for the search. For most use cases, this should be updated by the view + /// every time the user navigates the map. public var queryCenter: Point? /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate - /// number of results is returned based on the type of suggestion chosen (driven by the IsCollection property). + /// number of results is returned based on the type of suggestion chosen + /// (driven by the IsCollection property). public var resultMode: SearchResultMode = .automatic /// Collection of results. `nil` means no query has been made. An empty array means there @@ -128,7 +125,7 @@ public class SearchViewModel: ObservableObject { @Published public var suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) - /// True if the `queryArea` has changed since the `results` collection has been set. + /// `true` if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to /// this property are driven by changes to the `queryArea` property. @@ -209,7 +206,9 @@ public class SearchViewModel: ObservableObject { /// `currentQuery` property. /// - Parameters: /// - searchSuggestion: The suggestion to use to commit the search. - public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) async -> Void { + public func acceptSuggestion( + _ searchSuggestion: SearchSuggestion + ) async -> Void { currentQuery = searchSuggestion.displayTitle var searchResults = [SearchResult]() @@ -257,7 +256,7 @@ public class SearchViewModel: ObservableObject { } } } - + /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, /// and reset the current query. public func clearSearch() { diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index b78c7385f..451e7f645 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -60,7 +60,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search /// repeat behavior. var repeatSuggestResultThreshold: Int = 6 - + public override func search( _ queryString: String, area: Geometry? @@ -83,10 +83,12 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ) // Union results and return. - let searchResults = geocodeResults.map{ $0.toSearchResult(searchSource: self) } + let searchResults = geocodeResults.map { + $0.toSearchResult(searchSource: self) + } results.append(contentsOf: searchResults) var allResults: [SearchResult] = Array(Set(results)) - + // Limit results to `maximumResults`. if allResults.count > maximumResults { let dropCount = allResults.count - maximumResults @@ -94,12 +96,14 @@ public class SmartLocatorSearchSource: LocatorSearchSource { } return allResults } - + public override func search( _ searchSuggestion: SearchSuggestion ) async throws -> [SearchResult] { - guard let suggestResult = searchSuggestion.suggestResult else { return [] } - + guard let suggestResult = searchSuggestion.suggestResult else { + return [] + } + var results = try await super.search(searchSuggestion) if results.count > repeatSearchResultThreshold || geocodeParameters.searchArea == nil { @@ -107,18 +111,21 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // constraints on the search, so return results. return results } - + // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil - let geocodeResults = try await locatorTask.geocode(suggestResult: suggestResult, - parameters: geocodeParameters + let geocodeResults = try await locatorTask.geocode( + suggestResult: suggestResult, + parameters: geocodeParameters ) // Union results and return. - let searchResults = geocodeResults.map{ $0.toSearchResult(searchSource: self) } + let searchResults = geocodeResults.map { + $0.toSearchResult(searchSource: self) + } results.append(contentsOf: searchResults) var allResults: [SearchResult] = Array(Set(results)) - + // Limit results to `maximumResults`. if allResults.count > maximumResults { let dropCount = allResults.count - maximumResults @@ -137,7 +144,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // constraints on the search, so return results. return results } - + // Remove geographic constraints and re-run search. suggestParameters.searchArea = nil let geocodeResults = try await locatorTask.suggest( @@ -146,10 +153,12 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ) // Union results and return. - let suggestResults = geocodeResults.map{ $0.toSearchSuggestion(searchSource: self) } + let suggestResults = geocodeResults.map { + $0.toSearchSuggestion(searchSource: self) + } results.append(contentsOf: suggestResults) var allResults: [SearchSuggestion] = Array(Set(results)) - + // Limit results to `maximumResults`. if allResults.count > maximumSuggestions { let dropCount = allResults.count - maximumSuggestions diff --git a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift index a264a2bb0..d94c9a91e 100644 --- a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift @@ -14,27 +14,39 @@ import SwiftUI struct EsriShowResultsButtonViewModifier: ViewModifier { - @Binding var showResults: Bool + var isEnabled: Bool + @Binding var isHidden: Bool func body(content: Content) -> some View { HStack { content - Button( - action: { showResults.toggle() }, - label: { - Image(systemName: showResults ? "eye.fill" : "eye.slash.fill") - .foregroundColor(Color(UIColor.opaqueSeparator)) - } - ) + if isEnabled { + EmptyView() + } + else { + Button( + action: { isHidden.toggle() }, + label: { + Image(systemName: isHidden ? "eye.slash.fill" : "eye.fill") + .foregroundColor(Color(UIColor.opaqueSeparator)) + } + ) + } } } } extension View { - func esriShowResultsButton(showResults: Binding) -> some View { + func esriShowResultsButton( + isEnabled: Bool, + isHidden: Binding + ) -> some View { ModifiedContent( content: self, - modifier: EsriShowResultsButtonViewModifier(showResults: showResults) + modifier: EsriShowResultsButtonViewModifier( + isEnabled: isEnabled, + isHidden: isHidden + ) ) } } From b92064a6ee97836af928a8856dbf44636a828dd8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 30 Jul 2021 11:05:11 -0500 Subject: [PATCH 0144/1722] Fix visibility of constructor and properties --- .../Components/Search/SearchSuggestion.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 63bd645b1..e9d642676 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -15,7 +15,7 @@ import ArcGIS /// Wraps a suggestion for display. public class SearchSuggestion { - internal init( + public init( displayTitle: String, displaySubtitle: String? = nil, owningSource: SearchSourceProtocol, @@ -30,25 +30,25 @@ public class SearchSuggestion { } /// Title that should be used when displaying a suggestion. - var displayTitle: String + public var displayTitle: String /// Optional subtitle that can be displayed when showing a suggestion. - var displaySubtitle: String? + public var displaySubtitle: String? /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the /// view model to be able to accept a suggestion, because a suggestion should only be used with the /// locator that created it. - var owningSource: SearchSourceProtocol + public var owningSource: SearchSourceProtocol /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. - var suggestResult: SuggestResult? + public var suggestResult: SuggestResult? /// True if the search from this suggestion should be treated like a collection search, false if the /// search would return a single result. This property should be used to display a different icon /// in the UI depending on if this is a category search (like 'Coffee', 'Pizza', or 'Starbucks') and /// false if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). - var isCollection: Bool + public var isCollection: Bool } extension SearchSuggestion: Identifiable { From dc0ae675175beb96cf445403714b0a52ec7b2632 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 30 Jul 2021 11:29:31 -0500 Subject: [PATCH 0145/1722] results and suggests property setters are private --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 85bc97812..d8075c28b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -104,7 +104,7 @@ public class SearchViewModel: ObservableObject { /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. @Published - public var results: Result<[SearchResult]?, SearchError> = .success(nil) + public private(set) var results: Result<[SearchResult]?, SearchError> = .success(nil) /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets @@ -123,7 +123,7 @@ public class SearchViewModel: ObservableObject { /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. @Published - public var suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) + public private(set) var suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) /// `true` if the `queryArea` has changed since the `results` collection has been set. /// This property is used by the view to enable 'Repeat search here' functionality. This property is From a34614c3032b189cf3f4e1fdf47d338ef0d5b696 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 30 Jul 2021 15:07:56 -0500 Subject: [PATCH 0146/1722] use .onAppear instead of .task; fix visibility of properties. --- Examples/Examples/SearchExampleView.swift | 2 +- .../Components/Search/SmartLocatorSearchSource.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 8e7114033..4e7caf4de 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -61,7 +61,7 @@ struct SearchExampleView: View { .onChange(of: searchViewModel.selectedResult, perform: { newValue in display(selectedResult: newValue) }) - .task { + .onAppear() { setupSearchViewModel() } } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 451e7f645..5884724e4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -52,14 +52,14 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// repeated with loosened parameters until enough results are accumulated. If no search is /// successful, it is still possible to have a total number of results less than this threshold. Does not /// apply to repeated search with area constraint. Set to zero to disable search repeat behavior. - var repeatSearchResultThreshold: Int = 1 + public var repeatSearchResultThreshold: Int = 1 /// The minimum number of suggestions to attempt to return. If there are too few suggestions, /// request is repeated with loosened constraints until enough suggestions are accumulated. /// If no search is successful, it is still possible to have a total number of results less than this /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search /// repeat behavior. - var repeatSuggestResultThreshold: Int = 6 + public var repeatSuggestResultThreshold: Int = 6 public override func search( _ queryString: String, From a271a4839d7d6f0150998d7eb3c120207f28614a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sun, 1 Aug 2021 11:44:05 -0500 Subject: [PATCH 0147/1722] Add PlainList View to simplify displaying a ListStyle(.plain) list; dark mode support --- .../Components/Search/SearchView.swift | 12 +++---- .../Utility/EsriBorderViewModifier.swift | 6 ++-- .../EsriDeleteTextButtonViewModifier.swift | 2 +- .../EsriSearchButtonViewModifier.swift | 2 +- .../EsriShowResultsButtonViewModifier.swift | 2 +- Sources/ArcGISToolkit/Utility/PlainList.swift | 33 +++++++++++++++++++ 6 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 Sources/ArcGISToolkit/Utility/PlainList.swift diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 548e0e7c2..7fff27b26 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -175,7 +175,7 @@ struct SearchResultList: View { if let results = results, results.count > 0 { if results.count > 1 { // Only show the list if we have more than one result. - List { + PlainList { ForEach(results) { result in SearchResultRow(result: result) .onTapGesture { @@ -186,12 +186,12 @@ struct SearchResultList: View { } } else if results != nil { - List { + PlainList { Text(noResultMessage) } } case .failure(let error): - List { + PlainList { Text(error.localizedDescription) } } @@ -210,7 +210,7 @@ struct SearchSuggestionList: View { switch suggestionResults { case .success(let results): if let suggestions = results, suggestions.count > 0 { - List { + PlainList { if suggestions.count > 0 { ForEach(suggestions) { suggestion in SuggestionResultRow(suggestion: suggestion) @@ -222,12 +222,12 @@ struct SearchSuggestionList: View { } } else if results != nil { - List { + PlainList { Text(noResultMessage) } } case .failure(let error): - List { + PlainList { Text(error.errorDescription) } } diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index abbc839c8..6e6a8ad53 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -19,14 +19,14 @@ struct EsriBorderViewModifier: ViewModifier { let roundedRect = RoundedRectangle(cornerRadius: 8) content .padding(edgeInsets) - .background(.white) + .background(Color(.systemBackground)) .clipShape(roundedRect) .overlay( roundedRect .stroke(lineWidth: 2) - .foregroundColor(.blue) + .foregroundColor(Color(.separator)) ) - .shadow(color: Color.gray.opacity(0.6), + .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2 diff --git a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift index 795a9baa7..e1575f619 100644 --- a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift @@ -24,7 +24,7 @@ struct EsriDeleteTextButtonViewModifier: ViewModifier { action: { self.text = "" }, label: { Image(systemName: "xmark.circle.fill") - .foregroundColor(Color(UIColor.opaqueSeparator)) + .foregroundColor(Color(.opaqueSeparator)) } ) } diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index 8933b1051..ced5a550d 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -23,7 +23,7 @@ struct EsriSearchButtonViewModifier: ViewModifier { action: { self.performSearch = true }, label: { Image(systemName: "magnifyingglass.circle.fill") - .foregroundColor(Color(UIColor.opaqueSeparator)) + .foregroundColor(Color(.opaqueSeparator)) } ) } diff --git a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift index d94c9a91e..73c2be830 100644 --- a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift @@ -28,7 +28,7 @@ struct EsriShowResultsButtonViewModifier: ViewModifier { action: { isHidden.toggle() }, label: { Image(systemName: isHidden ? "eye.slash.fill" : "eye.fill") - .foregroundColor(Color(UIColor.opaqueSeparator)) + .foregroundColor(Color(.opaqueSeparator)) } ) } diff --git a/Sources/ArcGISToolkit/Utility/PlainList.swift b/Sources/ArcGISToolkit/Utility/PlainList.swift new file mode 100644 index 000000000..df0fee803 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/PlainList.swift @@ -0,0 +1,33 @@ +// Copyright 2021 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 List with the `.plain` list style. +public struct PlainList : View where Content : View { + let content: Content + /// Creates a plain list with the given content. + /// - Parameters: + /// - content: The content of the list. + public init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + /// The content of the list. + public var body: some View { + List { + content + } + .listStyle(.plain) + } +} From 4bb2c74d5687cffd85d71d9a3f36544c1abc51db Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 2 Aug 2021 09:37:41 -0500 Subject: [PATCH 0148/1722] Move async task management into SearchViewModel. --- .../Components/Search/SearchView.swift | 37 +-- .../Components/Search/SearchViewModel.swift | 223 +++++++++++------- 2 files changed, 141 insertions(+), 119 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 7fff27b26..1af8f0aa9 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -51,11 +51,6 @@ public struct SearchView: View { @State private var currentSuggestion: SearchSuggestion? - /// The currently executing async task. `currentTask` should be cancelled - /// prior to starting another async task. - @State - private var currentTask: Task? - /// Determines whether the results lists are displayed. @State private var isResultDisplayHidden: Bool = false @@ -94,20 +89,20 @@ public struct SearchView: View { .task(id: searchViewModel.currentQuery) { // User typed a new character if currentSuggestion == nil { - await suggest() + await searchViewModel.updateSuggestions() } } .task(id: shouldCommitSearch) { if shouldCommitSearch { // User committed changes (hit Enter/Search button) + await searchViewModel.commitSearch(false) shouldCommitSearch.toggle() - await search() } } .task(id: currentSuggestion) { if let suggestion = currentSuggestion { // User selected a suggestion. - await accept(suggestion) + await searchViewModel.acceptSuggestion(suggestion) currentSuggestion = nil } } @@ -137,32 +132,6 @@ public struct SearchView: View { } } -extension SearchView { - func search() async { - currentTask?.cancel() - currentTask = Task(operation: { - await searchViewModel.commitSearch(false) - }) - await currentTask?.value - } - - func suggest() async { - currentTask?.cancel() - currentTask = Task(operation: { - await searchViewModel.updateSuggestions() - }) - await currentTask?.value - } - - func accept(_ suggestion: SearchSuggestion) async { - currentTask?.cancel() - currentTask = Task(operation: { - await searchViewModel.acceptSuggestion(suggestion) - }) - await currentTask?.value - } -} - struct SearchResultList: View { var searchResults: Result<[SearchResult]?, SearchError> @Binding var selectedResult: SearchResult? diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index d8075c28b..c5cbf2d36 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -134,6 +134,10 @@ public class SearchViewModel: ObservableObject { private var subscriptions = Set() + /// The currently executing async task. `currentTask` should be cancelled + /// prior to starting another async task. + private var currentTask: Task? + /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. /// If `restrictToArea` is true, only results in the query area will be returned. @@ -148,29 +152,12 @@ public class SearchViewModel: ObservableObject { source.preferredSearchLocation = queryCenter selectedResult = nil - let searchResult = await Result { - try await source.search( - currentQuery, - area: restrictToArea ? queryArea : nil - ) - } - - isEligibleForRequery = false - suggestions = .success(nil) - - switch searchResult { - case .success(let searchResults): - results = .success(searchResults) - if searchResults.count == 1 { - selectedResult = searchResults.first - } - case .failure(let error): - results = .failure(SearchError(error)) - break - case .none: - results = .success(nil) - break - } + currentTask?.cancel() + currentTask = commitSearchTask( + source, + restrictToArea: restrictToArea + ) + await currentTask?.value } /// Updates suggestions list asynchronously. @@ -181,24 +168,9 @@ public class SearchViewModel: ObservableObject { source.searchArea = queryArea source.preferredSearchLocation = queryCenter - let suggestResult = await Result { - try await source.suggest(currentQuery) - } - - results = .success(nil) - selectedResult = nil - isEligibleForRequery = false - - switch suggestResult { - case .success(let suggestResults): - suggestions = .success(suggestResults) - case .failure(let error): - suggestions = .failure(SearchError(error)) - break - case .none: - suggestions = .success(nil) - break - } + currentTask?.cancel() + currentTask = updateSuggestionsTask(source) + await currentTask?.value } /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is @@ -211,50 +183,9 @@ public class SearchViewModel: ObservableObject { ) async -> Void { currentQuery = searchSuggestion.displayTitle - var searchResults = [SearchResult]() - var suggestError: Error? - let searchResult = await Result { - try await searchSuggestion.owningSource.search(searchSuggestion) - } - - suggestions = .success(nil) - isEligibleForRequery = false - selectedResult = nil - - switch searchResult { - case .success(let results): - switch (resultMode) - { - case .single: - if let firstResult = results.first { - searchResults = [firstResult] - } - case .multiple: - searchResults = results - case .automatic: - if searchSuggestion.suggestResult?.isCollection ?? true { - searchResults = results - } else { - if let firstResult = results.first { - searchResults = [firstResult] - } - } - } - case .failure(let error): - suggestError = error - case .none: - break - } - - if let error = suggestError { - results = .failure(SearchError(error)) - } - else { - results = .success(searchResults) - if searchResults.count == 1 { - selectedResult = searchResults.first - } - } + currentTask?.cancel() + currentTask = acceptSuggestionTask(searchSuggestion) + await currentTask?.value } /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, @@ -265,6 +196,128 @@ public class SearchViewModel: ObservableObject { } } +extension SearchViewModel { + private func commitSearchTask( + _ source: SearchSourceProtocol, + restrictToArea: Bool + ) -> Task<(), Never> { + let task = Task(operation: { + let searchResult = await Result { + try await source.search( + currentQuery, + area: restrictToArea ? queryArea : nil + ) + } + + DispatchQueue.main.async { [weak self] in + self?.isEligibleForRequery = false + self?.suggestions = .success(nil) + + switch searchResult { + case .success(let searchResults): + self?.results = .success(searchResults) + if searchResults.count == 1 { + self?.selectedResult = searchResults.first + } + case .failure(let error): + self?.results = .failure(SearchError(error)) + break + case .none: + self?.results = .success(nil) + break + } + } + }) + return task + } + + private func updateSuggestionsTask( + _ source: SearchSourceProtocol + ) -> Task<(), Never> { + let task = Task(operation: { + let suggestResult = await Result { + try await source.suggest(currentQuery) + } + + DispatchQueue.main.async { [weak self] in + self?.results = .success(nil) + self?.selectedResult = nil + self?.isEligibleForRequery = false + + switch suggestResult { + case .success(let suggestResults): + self?.suggestions = .success(suggestResults) + case .failure(let error): + self?.suggestions = .failure(SearchError(error)) + break + case .none: + self?.suggestions = .success(nil) + break + } + } + }) + return task + } + + private func acceptSuggestionTask( + _ searchSuggestion: SearchSuggestion + ) -> Task<(), Never> { + let task = Task(operation: { + let searchResult = await Result { + try await searchSuggestion.owningSource.search(searchSuggestion) + } + + DispatchQueue.main.async { [weak self] in + var searchResults = [SearchResult]() + var suggestError: Error? + + self?.suggestions = .success(nil) + self?.isEligibleForRequery = false + self?.selectedResult = nil + + switch searchResult { + case .success(let results): + switch (self?.resultMode) + { + case .single: + if let firstResult = results.first { + searchResults = [firstResult] + } + case .multiple: + searchResults = results + case .automatic: + if searchSuggestion.suggestResult?.isCollection ?? true { + searchResults = results + } else { + if let firstResult = results.first { + searchResults = [firstResult] + } + } + case .none: + break + } + case .failure(let error): + suggestError = error + case .none: + break + } + + if let error = suggestError { + self?.results = .failure(SearchError(error)) + } + else { + self?.results = .success(searchResults) + if searchResults.count == 1 { + self?.selectedResult = searchResults.first + } + } + } + }) + return task + } + +} + extension SearchViewModel { /// Returns the search source to be used in geocode operations. /// - Returns: The search source to use. From 7daf661d8eb98cbf40cdc16bf4ce3718c50b9d2b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 2 Aug 2021 13:24:28 -0500 Subject: [PATCH 0149/1722] Fix indenting. --- .../Components/Search/SearchViewModel.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index c5cbf2d36..b9af50b0e 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -137,7 +137,7 @@ public class SearchViewModel: ObservableObject { /// The currently executing async task. `currentTask` should be cancelled /// prior to starting another async task. private var currentTask: Task? - + /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. /// If `restrictToArea` is true, only results in the query area will be returned. @@ -230,7 +230,7 @@ extension SearchViewModel { }) return task } - + private func updateSuggestionsTask( _ source: SearchSourceProtocol ) -> Task<(), Never> { @@ -258,7 +258,7 @@ extension SearchViewModel { }) return task } - + private func acceptSuggestionTask( _ searchSuggestion: SearchSuggestion ) -> Task<(), Never> { @@ -270,7 +270,7 @@ extension SearchViewModel { DispatchQueue.main.async { [weak self] in var searchResults = [SearchResult]() var suggestError: Error? - + self?.suggestions = .success(nil) self?.isEligibleForRequery = false self?.selectedResult = nil @@ -315,7 +315,7 @@ extension SearchViewModel { }) return task } - + } extension SearchViewModel { From 4161c2b44f22efc8f9cd4d05306ef5c447b59ed2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 2 Aug 2021 15:29:16 -0500 Subject: [PATCH 0150/1722] Add title and subtitle to displayed graphic's attributes. --- Examples/Examples/SearchExampleView.swift | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 4e7caf4de..55c4f5d78 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -81,12 +81,10 @@ struct SearchExampleView: View { case .success(let results): var resultGraphics = [Graphic]() results?.forEach({ result in - let graphic = Graphic( - geometry: result.geoElement?.geometry, - symbol: .resultSymbol - ) - resultGraphics.append(graphic) - + if let graphic = result.geoElement as? Graphic { + graphic.updateGraphic(withResult: result) + resultGraphics.append(graphic) + } }) let currentGraphics = searchResultsOverlay.graphics searchResultsOverlay.removeGraphics(currentGraphics) @@ -113,9 +111,8 @@ struct SearchExampleView: View { let graphic = selectedResult.geoElement as? Graphic else { return } searchResultViewpoint = selectedResult.selectionViewpoint - if graphic.symbol == nil { - graphic.symbol = .resultSymbol - } + graphic.updateGraphic(withResult: selectedResult) + let currentGraphics = searchResultsOverlay.graphics searchResultsOverlay.removeGraphics(currentGraphics) searchResultsOverlay.addGraphic(graphic) @@ -128,3 +125,13 @@ private extension Symbol { image: UIImage(named: "MapPin")! ) } + +private extension Graphic { + func updateGraphic(withResult result: SearchResult) { + if symbol == nil { + symbol = .resultSymbol + } + setAttributeValue(result.displayTitle, forKey: "displayTitle") + setAttributeValue(result.displaySubtitle, forKey: "displaySubtitle") + } +} From ef7711de7109420785a1617ca8b4cd032ee9d4c5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 3 Aug 2021 15:41:25 -0500 Subject: [PATCH 0151/1722] WIP --- Examples/Examples.xcodeproj/project.pbxproj | 4 + .../Examples/BasemapGalleryExampleView.swift | 104 ++++++++++ .../BasemapGallery/BasemapGallery.swift | 183 ++++++++++++++++++ .../BasemapGallery/BasemapGalleryItem.swift | 40 ++++ 4 files changed, 331 insertions(+) create mode 100644 Examples/Examples/BasemapGalleryExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift create mode 100644 Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 65d3ad5ee..94aebe193 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ E48A73452658227100F5C118 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733D2658227000F5C118 /* Examples.swift */; }; E48A73462658227100F5C118 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73422658227100F5C118 /* ExampleView.swift */; }; E48A73472658227100F5C118 /* ExampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733E2658227100F5C118 /* ExampleList.swift */; }; + E4C389D526B8A12C002BC255 /* BasemapGalleryExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C389D426B8A12C002BC255 /* BasemapGalleryExampleView.swift */; }; E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E4E57DC5265D8EB00077A093 /* ArcGISToolkit */; }; E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */; }; /* End PBXBuildFile section */ @@ -50,6 +51,7 @@ E48A73402658227100F5C118 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; E48A73412658227100F5C118 /* AnyExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyExample.swift; sourceTree = ""; }; E48A73422658227100F5C118 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; + E4C389D426B8A12C002BC255 /* BasemapGalleryExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasemapGalleryExampleView.swift; sourceTree = ""; }; E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -69,6 +71,7 @@ E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( + E4C389D426B8A12C002BC255 /* BasemapGalleryExampleView.swift */, E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, E42BFBE82672BF9500159107 /* SearchExampleView.swift */, ); @@ -216,6 +219,7 @@ E48A73472658227100F5C118 /* ExampleList.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */, + E4C389D526B8A12C002BC255 /* BasemapGalleryExampleView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift new file mode 100644 index 000000000..34bc11739 --- /dev/null +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -0,0 +1,104 @@ +// Copyright 2021 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 +import ArcGISToolkit + +struct BasemapGalleryExampleView: View { +// enum MapOrScene { +// /// The example shows a map view. +// case map +// /// The example shows a scene view. +// case scene +// } +// +// @State +// private var mapOrScene: MapOrScene = .map + + var basemapGalleryItems: [BasemapGalleryItem] = [ + BasemapGalleryItem( + basemap: Basemap(style: .arcGISLightGray), + name: "ArcGIS Light Gray", + description: "A vector basemap for the world featuring a light neutral background style with minimal colors as the base layer and labels as the reference layer.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap(style: .arcGISNova), + name: "ArcGIS Nova", + description: "A vector basemap for the world featuring a dark background with glowing blue symbology inspired by science-fiction and futuristic themes.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap(style: .arcGISNewspaper), + name: "ArcGIS Newspaper)", + description: "A vector basemap in black & white design with halftone patterns, red highlights, and stylized fonts to depict a unique \"newspaper\" styled theme.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap(style: .arcGISNavigationNight), + name: "ArcGIS NavigationNight", + description: "A vector basemap for the world featuring a 'dark mode' version of the `Basemap.Style.arcGISNavigation` style, using the same content.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap(style: .arcGISStreets), + name: "ArcGIS Streets", + description: "A vector basemap for the world featuring a classic Esri street map style.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap(style: .arcGISTerrain), + name: "ArcGIS Terrain", + description: "A composite basemap with elevation hillshade (raster), minimal map content like water and land fill, water lines and roads (vector) as the base layers and minimal map content like populated place names, admin and water labels with boundary lines (vector) as the reference layer.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap(style: .arcGISMidcentury), + name: "ArcGIS Midcentury", + description: "A vector basemap inspired by the art and advertising of the 1950's that presents a unique design option to the ArcGIS basemaps.", + thumbnail: nil + ) + ] + + var body: some View { + Group { + BasemapGallery(basemaps: basemapGalleryItems) +// List { +// ForEach(basemapGalleryItems) { basemapGalleryItem in +// Text(basemapGalleryItem.title) +// } +// } + } +// Picker("Map or Scene", selection: $mapOrScene, content: { +// Text("Map").tag(MapOrScene.map) +// Text("Scene").tag(MapOrScene.scene) +// }) +// .pickerStyle(SegmentedPickerStyle()) +// .padding() +// switch mapOrScene { +// case .map: +// OverviewMapForMapView() +// case .scene: +// OverviewMapForSceneView() +// } + } +} + +struct BasemapGalleryExampleView_Previews: PreviewProvider { + static var previews: some View { + BasemapGalleryExampleView() + } +} + +// MARK: Extensions diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift new file mode 100644 index 000000000..0bbc025d7 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -0,0 +1,183 @@ +// Copyright 2021 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 + +/// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed +/// on an existing `GeoView`, which shows the visible extent of that `GeoView`. +public struct BasemapGallery: View { + public init(basemaps: [BasemapGalleryItem] = []) { + self.basemaps = basemaps + } + + public var basemaps: [BasemapGalleryItem] = [] +// /// The `Viewpoint` of the main `GeoView`. +// let viewpoint: Viewpoint? +// +// /// The visible area of the main `GeoView`. Not applicable to `SceneView`s. +// let visibleArea: Polygon? +// +// private var symbol: Symbol +// +// private var scaleFactor = 25.0 +// +// @StateObject +// private var map = Map(basemapStyle: .arcGISTopographic) +// +// /// The `Graphic` displaying the visible area of the main `GeoView`. +// @StateObject +// private var graphic: Graphic +// +// /// The `GraphicsOverlay` used to display the visible area graphic. +// @StateObject +// private var graphicsOverlay: GraphicsOverlay +// +// /// Creates an `OverviewMap` for use on a `MapView`. +// /// - 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. +// /// - Returns: A new `OverviewMap`. +// public static func forMapView( +// with viewpoint: Viewpoint?, +// visibleArea: Polygon? +// ) -> OverviewMap { +// OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea, symbol: .defaultFill) +// } +// +// /// Creates an `OverviewMap` for use on a `SceneView`. +// /// - Parameter viewpoint: Viewpoint of the main `SceneView` used to update the +// /// `OverviewMap` view. +// /// - Returns: A new `OverviewMap`. +// public static func forSceneView( +// with viewpoint: Viewpoint? +// ) -> OverviewMap { +// OverviewMap(viewpoint: viewpoint, symbol: .defaultMarker) +// } +// +// /// 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. +// init( +// viewpoint: Viewpoint?, +// visibleArea: Polygon? = nil, +// symbol: Symbol +// ) { +// self.visibleArea = visibleArea +// self.viewpoint = viewpoint +// self.symbol = symbol +// +// let graphic = Graphic(symbol: self.symbol) +// +// // It is necessary to set the graphic and graphicsOverlay this way +// // in order to prevent the main geoview from recreating the +// // graphicsOverlay every draw cycle. That was causing refresh issues +// // with the graphic during panning/zooming/rotating. +// _graphic = StateObject(wrappedValue: graphic) +// _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) +// } + + public var body: some View { + List { + basemaps.forEach { basemapGalleryItem in + Text(basemapGalleryItem.name) + } + } + } +// MapView( +// map: map, +// viewpoint: makeOverviewViewpoint(), +// graphicsOverlays: [graphicsOverlay] +// ) +// .attributionText(hidden: true) +// .interactionModes([]) +// .border(.black, width: 1) +// .onAppear(perform: { +// graphic.symbol = symbol +// }) +// .onChange(of: visibleArea, perform: { visibleArea in +// if let visibleArea = visibleArea { +// graphic.geometry = visibleArea +// } +// }) +// .onChange(of: viewpoint, perform: { viewpoint in +// if visibleArea == nil, +// let viewpoint = viewpoint, +// let point = viewpoint.targetGeometry as? Point { +// graphic.geometry = point +// } +// }) +// .onChange(of: symbol, perform: { +// graphic.symbol = $0 +// }) +// } + + // MARK: Modifiers +// +// /// The `Map` displayed in the `OverviewMap`. +// /// - Parameter map: The new map. +// /// - Returns: The `OverviewMap`. +// public func map(_ map: Map) -> OverviewMap { +// var copy = self +// copy._map = StateObject(wrappedValue: map) +// return copy +// } +// +// /// 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`. +// /// - Parameter scaleFactor: The new scale factor. +// /// - Returns: The `OverviewMap`. +// public func scaleFactor(_ scaleFactor: Double) -> OverviewMap { +// var copy = self +// copy.scaleFactor = scaleFactor +// return copy +// } +// +// /// The `Symbol` used to display the main `GeoView` visible area. For `MapView`s, the symbol +// /// should be appropriate for visualizing a polygon, as it will be used to draw the visible area. For +// /// `SceneView`s, the symbol should be appropriate for visualizing a point, as it will be used to +// /// draw the current viewpoint's center. For `MapView`s, the default is a transparent +// /// `SimpleFillSymbol` with a red 1 point width outline; for `SceneView`s, the default is a +// /// red, crosshair `SimpleMarkerSymbol`. +// /// - Parameter symbol: The new symbol. +// /// - Returns: The `OverviewMap`. +// public func symbol(_ symbol: Symbol) -> OverviewMap { +// var copy = self +// copy.symbol = symbol +// return copy +// } +} + +// MARK: Extensions + +private extension Symbol { + /// The default marker symbol. + static let defaultMarker: MarkerSymbol = SimpleMarkerSymbol( + style: .cross, + color: .red, + size: 12.0 + ) + + /// The default fill symbol. + static let defaultFill: FillSymbol = SimpleFillSymbol( + style: .solid, + color: .clear, + outline: SimpleLineSymbol( + style: .solid, + color: .red, + width: 1.0 + ) + ) +} diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift new file mode 100644 index 000000000..4af27056f --- /dev/null +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -0,0 +1,40 @@ +// Copyright 2021 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 + +import Foundation + +public struct BasemapGalleryItem { + public init( + basemap: Basemap, + name: String = "", + description: String? = "", + thumbnail: UIImage? = nil + ) { + self.basemap = basemap + self.name = name + self.description = description + self.thumbnail = thumbnail + } + + var basemap: Basemap + var name: String + var description: String? + var thumbnail: UIImage? +} + +//extension DisplayableBasemap: Identifiable { +// public var id: ObjectIdentifier { ObjectIdentifier(self) } +//} From 2ce7b6b925603d8f6f37468ad6c3486f58064661 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 3 Aug 2021 16:55:37 -0500 Subject: [PATCH 0152/1722] use new `graphicsOverlay.removeAllGraphics()` call. --- Examples/Examples/SearchExampleView.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 55c4f5d78..ac1e99eb1 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -86,8 +86,7 @@ struct SearchExampleView: View { resultGraphics.append(graphic) } }) - let currentGraphics = searchResultsOverlay.graphics - searchResultsOverlay.removeGraphics(currentGraphics) + searchResultsOverlay.removeAllGraphics() searchResultsOverlay.addGraphics(resultGraphics) if resultGraphics.count > 0, @@ -113,8 +112,7 @@ struct SearchExampleView: View { searchResultViewpoint = selectedResult.selectionViewpoint graphic.updateGraphic(withResult: selectedResult) - let currentGraphics = searchResultsOverlay.graphics - searchResultsOverlay.removeGraphics(currentGraphics) + searchResultsOverlay.removeAllGraphics() searchResultsOverlay.addGraphic(graphic) } } From fad11a2a7d7da05fc55cbf43ffaa61a34a34fe6d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 4 Aug 2021 16:47:46 -0500 Subject: [PATCH 0153/1722] Prototype images and toolbar button for hiding/showing. --- .../Examples/BasemapGalleryExampleView.swift | 91 ++++--- .../LightGray.imageset/Contents.json | 21 ++ .../LightGray.imageset/Image-1.png | Bin 0 -> 21045 bytes .../Midcentury.imageset/Contents.json | 21 ++ .../Midcentury.imageset/Image.png | Bin 0 -> 44133 bytes .../NavigationNight.imageset/Contents.json | 21 ++ .../NavigationNight.imageset/Image.png | Bin 0 -> 52142 bytes .../Newspaper.imageset/Contents.json | 21 ++ .../Newspaper.imageset/Image.png | Bin 0 -> 41376 bytes .../Nova.imageset/Contents.json | 21 ++ .../Assets.xcassets/Nova.imageset/Image.png | Bin 0 -> 43542 bytes .../Streets.imageset/Contents.json | 21 ++ .../Streets.imageset/Image.png | Bin 0 -> 58688 bytes .../Terrain.imageset/Contents.json | 21 ++ .../Terrain.imageset/Image.png | Bin 0 -> 52275 bytes Examples/ExamplesApp/Examples.swift | 1 + .../BasemapGallery/BasemapGallery.swift | 226 +++++++----------- .../BasemapGallery/BasemapGalleryItem.swift | 26 +- 18 files changed, 313 insertions(+), 178 deletions(-) create mode 100644 Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Image-1.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Image.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Image.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Image.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/Nova.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/Nova.imageset/Image.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/Streets.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/Streets.imageset/Image.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Image.png diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 34bc11739..60de0a025 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -16,82 +16,97 @@ import ArcGIS import ArcGISToolkit struct BasemapGalleryExampleView: View { -// enum MapOrScene { -// /// The example shows a map view. -// case map -// /// The example shows a scene view. -// case scene -// } -// -// @State -// private var mapOrScene: MapOrScene = .map - var basemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( basemap: Basemap(style: .arcGISLightGray), name: "ArcGIS Light Gray", description: "A vector basemap for the world featuring a light neutral background style with minimal colors as the base layer and labels as the reference layer.", - thumbnail: nil + thumbnail: UIImage(named: "LightGray") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/0f74af7609054be8a29e0ba5f154f0a8/info/thumbnail/thumbnail1607388219207.jpeg")! ), BasemapGalleryItem( basemap: Basemap(style: .arcGISNova), name: "ArcGIS Nova", description: "A vector basemap for the world featuring a dark background with glowing blue symbology inspired by science-fiction and futuristic themes.", - thumbnail: nil + thumbnail: UIImage(named: "Nova") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/90f86b329f37499096d3715ac6e5ed1f/info/thumbnail/thumbnail1607555507609.jpeg")! ), BasemapGalleryItem( basemap: Basemap(style: .arcGISNewspaper), - name: "ArcGIS Newspaper)", + name: "ArcGIS Newspaper", description: "A vector basemap in black & white design with halftone patterns, red highlights, and stylized fonts to depict a unique \"newspaper\" styled theme.", - thumbnail: nil + thumbnail: UIImage(named: "Newspaper") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/e3c062eedf8b487b8bb5b9b08db1b7a9/info/thumbnail/thumbnail1607553292807.jpeg")! ), BasemapGalleryItem( basemap: Basemap(style: .arcGISNavigationNight), name: "ArcGIS NavigationNight", description: "A vector basemap for the world featuring a 'dark mode' version of the `Basemap.Style.arcGISNavigation` style, using the same content.", - thumbnail: nil + thumbnail: UIImage(named: "NavigationNight") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/77073a29526046b89bb5622b6276e933/info/thumbnail/thumbnail1607386977674.jpeg")! ), BasemapGalleryItem( basemap: Basemap(style: .arcGISStreets), name: "ArcGIS Streets", description: "A vector basemap for the world featuring a classic Esri street map style.", - thumbnail: nil + thumbnail: UIImage(named: "Streets") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/e3e6df1d2f6a485d8a70f28fdd3ce19e/info/thumbnail/thumbnail1607389307240.jpeg")! ), BasemapGalleryItem( basemap: Basemap(style: .arcGISTerrain), name: "ArcGIS Terrain", description: "A composite basemap with elevation hillshade (raster), minimal map content like water and land fill, water lines and roads (vector) as the base layers and minimal map content like populated place names, admin and water labels with boundary lines (vector) as the reference layer.", - thumbnail: nil + thumbnail: UIImage(named: "Terrain") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/2ef1306b93c9459ca7c7b4f872c070b9/info/thumbnail/thumbnail1607387869592.jpeg")! ), BasemapGalleryItem( basemap: Basemap(style: .arcGISMidcentury), name: "ArcGIS Midcentury", description: "A vector basemap inspired by the art and advertising of the 1950's that presents a unique design option to the ArcGIS basemaps.", - thumbnail: nil + thumbnail: UIImage(named: "Midcentury") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/52d6a28f09704f04b33761ba7c4bf93f/info/thumbnail/thumbnail1607554184831.jpeg")! ) ] + let map = Map(basemapStyle: .arcGISNova) + + @State + var selectedBasemapGalleryItem: BasemapGalleryItem? + + @State + var showBasemapGallery: Bool = true // NOTE: Set to false when BasemapGallery is back in the navigation stack. + + let initialViewpoint: Viewpoint? = Viewpoint( + center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), + scale: 1000000 + ) + var body: some View { - Group { - BasemapGallery(basemaps: basemapGalleryItems) -// List { -// ForEach(basemapGalleryItems) { basemapGalleryItem in -// Text(basemapGalleryItem.title) -// } -// } - } -// Picker("Map or Scene", selection: $mapOrScene, content: { -// Text("Map").tag(MapOrScene.map) -// Text("Scene").tag(MapOrScene.scene) -// }) -// .pickerStyle(SegmentedPickerStyle()) -// .padding() -// switch mapOrScene { -// case .map: -// OverviewMapForMapView() -// case .scene: -// OverviewMapForSceneView() -// } + ZStack(alignment: .topTrailing, content: { + MapView(map: map, viewpoint: initialViewpoint) + .onChange(of: selectedBasemapGalleryItem) { newValue in + if let selectedItem = selectedBasemapGalleryItem { + map.basemap = selectedItem.basemap + } + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + showBasemapGallery.toggle() + } label: { + Image(systemName: "map.fill") + } + } + } + if showBasemapGallery { + BasemapGallery( + basemapGalleryItems: basemapGalleryItems, + selectedBasemapGalleryItem: $selectedBasemapGalleryItem + ) + .frame(width: 300) + .padding() + } + }) } } diff --git a/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json new file mode 100644 index 000000000..8c132e32f --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Image-1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Image-1.png b/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Image-1.png new file mode 100644 index 0000000000000000000000000000000000000000..537f371c95dc1b797aeaa4a1b6dd3b9681b2410a GIT binary patch literal 21045 zcmV)+K#0GIP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91$e;rN1ONa40RR91g#Z8m06RFLZ2$m307*naRCod1-D#7Z*Htg@9yQyN z$9PC=$s;6{5IdwQgd&8hq$;6+f|nE@04{aE61?WsrJ!O0h0D%pLhRTiAtCXk*plU0 zmNmAd9`0}L|2bR7-RHEsHMN>u_SV{Kuf66yJi~c<`uU&yY-VO=e#h)Bx7@O0#~gTN zWof0#^wh9SPfx?EA(p3Bs$QB}UY5zj`Q_#1>8aV}mBk_X>VJ)`A;_ku_w3oT zfB*iuxw)y83y?4=O=4A3(~F_UGdfI9&!V|_@qFmCPTo+5~2R^!w=7&zj)#N;_U2fQ*7qK;zeUqj15^Sv{c@h znVrIO&z@a3-+XiHgqt=tluL~UoZ*X$ixEfk&Ye3oX}){+ZuN~*XvHG>P!-I9{KJH+ zrQuu{&Cbl1`(|c%S-7XBR(K62g*^=UV1TA#BNN$WnP`rZN%%O(HQvbm5=~ zTX-T!2oc;kW#H$gJZ;N^%gdR2S4GJ=j=+(xj~}lYZ&Gqo%b{|sLW5odE?!t#SU7X` z?AbO;Lo-dYBArn150f}uQW7ycQ)?KEVjKh-ay3)8s12ke=o|z|Vo8e!AAArVlegb~ z+X{5oU3VFK^ypDtx3*$I{mpNFgLvP*eTNPm(q(quci(-dPMxx<$>H6*_LlgT%`>wl z8n#x85 z?QegZcYgGvAHjU!QJ@d>nci(;Ycfb2x#J~RauWi24>y=ktkxx>M`~K;l{)t^L zzWCCoKmBPwtrTfne1yi=sjbqq-FMSoSu$sttx{G(hZtO%wG2ZUOEvXcv0wtg*RUI` zP){)v;siFvkSkY{*|ns%q`A0o?!w8FCtK=VT)YU&XERHSE6yTk&n%q$@bm}AKeV=G zMv~2C@gU{aQDxMp=-FV=dh^)ZCq6i7{hOQJF*7|!#Cz`^f8*$zr%z`slt7i-C}o2o z;h*6c_IdU{|MNee`OIg2|M!0%^7!%Nw!$xb;R`mw_uqe?Er0L_fAIL@k6Vd<{_~&z z?(hEY*S_|(GiT0x@WBTUJ@k++OB%Le#i;1XC`f|hZoL*f9Arr_>JZS1H`Hh(s{$}E zu!R%c0yhJW{x?n?GoueB6Uy%AE7x-lNAcrEQ;=s(yp?vQ;R_o_jw5kpvIhyNO_SOI zWz;YbZ6dGH2+t_pdtjtXra2@jnx#ZL&B!*#cB{_f;0X6;^9>2>2QiS;U@elUz$Gjqkz~M&S zx++3oONgv`?z!hg{3~Di%CpZt`@Qdd&uS!l?zrO)-f`Y?w0E8R!yo=o)bG6W&PN`3 z#9nFaAOG^cp$+|MXv$7<;ylhp=I<#>1EToHG%w<|Lu&$n@lbX&} zT${C7fw@sID9Ix6w%cw4(7C7JSpRGZvW0jsQJcdDhPdpxD*DW=%0tYFRP;{`()IK`a>Z zCOZpC>$F-i)|%m1JUhFytsk}6kkA$v6Qc;sigrXRz3>rZK(f#kf>C?tcE9t^@j6E? zTTbR}{tM?$NQ>pAIyUXvx$n+94$xfwEY43^$KBR|7XbRu0XI#se1ep?hzZK|rY63` z>CH-jeyd8{PG=g@igTzE$6= zl9J(+i++$)WC6{@K(ChCEDsUGU$F&C^eDI53{Gz4Ew#(gk8bz_b0?}E%icPI&D=V}`@aCIu+N15x zPKO9GByyYlEvTqRD*fe3TWHIrR#|3Jj|r4#peUeZokME#VkKhKHJ0I!i3KDWGG&rU z*F3T4M1zQ@PzOLv-MOr@^q0TruCMJ^%nby`&llEXbPQ@lODT5uNwtN?O8bK&V;hLFfSLxji0fgH_ZAfJ@1zIa~$ zlF7pz(WC>Eq*dG{!)VI{;Q*)TH6*b>Msto;VnX@11!$5**CVmWrbjerx()QBZstJ; zbR=&8nK!x=W9zJ4w?(Q)ZJ`PP)+M80=H_;2a)<<~yCzf4e>3ioeR$&3J8!>t?2WhH ze(PO#42b1!ezaIRkC!&nb>T)LZ@Qdx*EIKFWgB0c*7iiVEfDCkEM=|w{O3RKroS#% zouB>eXWdRikAtkTxxDn!OU^M^C$y6SaINt?uTUfYB#z;k0APAHC)AEXPq4WZ=g2&! zNw`EL6W9=xUFT7_BLiIXt&kXorKU%UrVXD}tW`2ChBW@IAt*CxG{MLK!jD+7WZF&!nAuAC!z%fHLg57uxazZ2_``X)KjM zd`K5?4qkCccWJamV-)=jhe50Wx|7$jX8QRT{(GJ7EAQ%hUba8 zf&%OQa2?Mv%veoqVZ;C|#dTp~tR~&ScG5&Ek;6wWTBUd&V7jX#U6+AfhwBM2hiEmO zqqa3kVH%_0*+pT^Q6=L9NCu{h)yy=_VhDout+?hZRy0u4EUF|nY-3M7hpci*OXgXD zBnp#QuBqEt_szkK#0m#vH4#`s0AU_k8G?ksVDu_rg+!oja%QS*$FS)mzXxwRK=L)| z(n1o;4^2LW=`jFcs5iD?t5=SSoJ7=Qf?D=NqQP^k3eBlOF$(iJ&H0B>t_oqmG*{w> zuBtAD&M;`$A|hZw@PH1FHSL-wR+46T5oddjx?|6~e z$YfQ`$gqh@94AY_mM(#jXXYzpr%7^49W?4;j#jjgA+|*(vHGZty%u$>Ac*wly1M9- z4KSL`z2yqMYFe2U)BwQL44;_+vy?4}DR0t3B69%13K_hywhJ%NvTsN=kU=4!>N+p`E#uy6W| z=Zc2TYGb1+m?45;K$3|FhN$5ISrr5ZfljqVrBKNdMLi`mA!cd<;!l#38Jbov zHJu@i6$rx<0z7T^kc`T)44HxQm30bya#U|;ZWM;sDX}Qcu&4zl*hc4Gktsqm81ulA z;NsuvK;7;pUEtUW-gs%(jD}V*s@3lJ4JKAHL*3jF{^K_+1k^^!zb3AlXXy+d&+5V- zK8eLNuoaF(7O`4Wy>ZPikFxA+ixY7IPpjP8#f?e|g*#`Ilz;WrSDEIJiqg##_gP$g zP}M~)7!$Ck2o?fkU7aw>C39{dFbA}_(x5eAPNx-Lq-%Y9(a_%zF;D=C;gsZ7 zwc!Y&B#G#mU~SVygdw`3$@+q>us1qjLV-x^n$uS4c$ufT&V{7vYp=bgsaFmiJ$f`o zNi3Sfppk-ZzQ{bW^KzSnW7;dPykd5NGnVvjBMkVOuUIjUB#K&}C@L^6onRCS(Ssx4 zgJGC7>RyPabOAP52L3g+9lc5s90($G@4kIjZ{BV~37mM^$6&>T%O+0WxqHOSO# zL{CI=&M1b*#!c$KIr2Er819wO8Jwth6DU@uwj&_q!w)~q1hKGU8A5ZTELT`j-X5aM z^8;49hv&|V#NsqGIM^<|RATsp4?YOVA&^Ng3SfRc5{o_3#l}ZArDLm?WXA5urG=9L=e}j#Ip>OL#(%)xt!QSh{kKj_n&!`?6_Bq?_SU zSf0U<8|%~o;$dXg8{wy+)Y0_V08D2?6issmE0Q*Zn!<+f?outQPuJZ52=RN3|=z;WF+!(6Mn%~B6k&C z;M`I{?4-i)G)ex&12H5po|+wFNTeh@q1i}{UZf)npqb5@K4wC?@S&}yf`GlUia5H- z8)Ff}Q-)nqhcaP>9tMFSGRY`3dJ!9L6c}e_qEu^M^TgVSQ#L|FU1YIb{BF8=Z)T+5 zVpx!6jf1J&MSyHzTRTo=AY_8aW+h}D;2i|q1cNNjg5mL^Zbh$Ax>7W82J^LoQVcj3 zhI8qLSRLtzalM@`Hvh`ZNLZ-rGJL zNL9+a^GQk`^e3&xMNivC&?i#qrP*CE3 zK37Lc09o;aB$K#gxm9756m${5AHb(B* zTQ9VLxhb}Ho3T4zV zY6P^hHs}RF>ct@gdmRixFodeFq=Xv|QI!W2%D819W3oq^jg;XQ>kTrx)w8vB00>fs zWf8)MV#!Y4?uLM^TWVu5$ER{=ZcgnEs+T@=IaXq_Z4!&Ayf0eTDcZ{(i+q_kJGFby z&V93WqC~Urrajq`xRSvzl0cna&f-}m+<074Vw;k@QOBW}XSh)az+8!s;fYybH9w-! zt2SH{G0s79G*;mdxq(whJ@{VMVtUs)01?s3@D$RMK+NG2`ZzXmnk?Zxr$F>n1;bc% z?1{hgJHKO;>klFpM25+&V2`4;cFSj7hKOZXn5^A-}%m?k3O0c zD-L>b(A7khCUIqINL`SIE20G>U|`v1piKvmu>pEhZ}93Ud3klr#8#n9L^A|l0!b;i z41V>iU)l0L@rh3$-~oC9qOJR%Rxjr&7|CJjl0$r@SCjr(fMpeinr=+MV8v+^hIi%o z;)^fp6(c!1``3T-x0h$hbsoj*T;HZh#ojOe+011L2YcwFPIy|p2QEg8EZRbuRR8|( z|L&L5lIrQFpH}1JRN)O{qZ|>qUQVbF9Xh1e1+QR&6+RrT2__4}NF*9YiB=J(OEWal z5FQO!sT^!aK$y^9N!>LW6-Q4Z)Yd0PNiV5JE6Wcu3lGsU!yH4vE=crdS}qCQBZXmt zHx>b!l19@IJdw#KO~dh#XY@DQ-K+Sa7W9g1eU z3}iFsI(az8R$!Nw{^ei(1=0^}zWUX#dY@Qs`Bna_ufA#q|J%R)n|F>)#6SP@KSzmG zi5@vi;jD0IWVJ-EOY{(%c`=rbEDQx2Rqc53-gn$#xWYAX%T3j~ia+fN}pw01|mhoJ}92*u@*cO=#Rvoo}J zM5Wh?)t*y4{9?Qx*m#N5%yafM;Uo|yk3II-Lk~R!;7W<>^>*9{ZCuOaiuoG3kbJEG z3_&Kkx)1~dPA}IBOk$H!{VVI08iy7Nf z5OWc26=P9mh$j8@W^dIMjwbpEr&8G{v;$Wf)y<&-ht{ETp8u zaq+CKO_hui00A~Ifk~txl^(E7vE2yqs>GEsawXHlaQc9y2$eX5CIEQ)fMKMLB07); zTJkMXV8K$5aSIV$is8yN|1kv(#ukpGP)BWr=0F&`F^U&0-0lUBp%J8k(a^ z)wYb)O4hP4XOudMHvFcyjAT_4QG&Rs$sJtwgX5iag)GRZx(e3ZNQtFn0Vp7T;%XqC z3Ou1?ZG_dPXfP*>L?TQi3l#;xX@YT!y9DIMH4>Al25uf>zpJO#c-4K7+Q3bMw(#yq+o8D+_J=~Z+-iSeEU?DuST9< zsP_wad2YUDm@ak8w(>yBxy`!b#-h(^)J!YI5_y17so@iQczI(bfa#?M42NW4K&&_U z0Zw{_Og%afE3-Gbg&whzft`LMKqYTW-zT4ZGH>>g@|S=4m-a;XoYKULdMth7i6^!! zu_(b>d$z1+Ds5OxEiY72)Vj^T=2gqBTF-_Dtc^I~MxmAk5cAsSzDfp?g3Z9dfZSeU9y8rfQtQ&p3I~=qkjX``{EIK*nb2dKCw=IZ$Wkk^L1IBh zG!Br)2HD6rR1xGFi^2x};0Hf&JkJhj*MpQ;M~)ov;7h7`0w&3}EU}nJSJo;t%d`VG zzj$<0ZzuZpvGhX88)!Dg%t+ z?pudeEFb*!La8P5kw+f!2AeEDcI?=eJ%rwrRfuoDt=?W@SZ;2?@VlC;$FkzS5UAr# zfZ@Eu(Ez0luWKu7hPcA$%D}{%_xTGF2g8j;DO7?QGG4|6F#<)GIgQ2;G%z?|i`d1v zUR{VIcdXhSi9>D|dEpKdqG_nkb_qkD%srHNTA2V!>?Jzpur55|0jU8rUxn#p*y3Zj zu>fiVq>DnjBb1kmTx`l_y3xuDAQbbOvUHbc$Z7!&yBojuDK-|e$9)Ob3f0Hz9LMos z0+Rs8Q~aLZ1OPX^$O3Rye9DP@6hVTg7fmw8aIAFU0IyC6lQ&7YNQ93Lni1MH*EF^_ zM}bZm0=5&){3sdz=%WLb9Dqc(3!;xgqB6=B9L^aS=3R&Aqc#cwJQz>>t%x#)8eUL# zqKY4w2JkL2S?Jj=y&yHWO=2aS{Ni=I<_OAHHmvemZ?Y2wBP}6jGfK0KU#uKxxWM$n zM?6e2WM0cbR!o~Wjnc)NZ^@y(W=8l42x6N*729x)rurYCTaSm%E5yNoe7*V z{XqZ>54S(TD$n@Zxgo!ga}})@#&0{h5*dkd3xPyI%!z!8AfgiwNFo%-z=#tbi50)E zRdp-|6ZCR7A~Y=+SV7VUH*i)~U33G}2TWB%vSZ4#E)-1!1waD`9Br9^7zfSZGd39F zD1#Ybv}MhoVuU1oZ%#n1HF#6t1OZ zRLjw*jN-xJ&k6uO0Ibl12}M9pC6pbnEA+YmMk(PO0A&Dtz>3}?8Wsb~s&$=FMuVGt76-kYi3zu zWwH_=0Qe1s@cYn;MN0-CHe4am*c3>BIw=xmf=ucplh6o8QUM;5FpQo?0gO@tuhpMQ zka`Wo$`XYMwlt@g*CVFZCYCKeg4grSU;vRKZp=Ie|v7!D)spI`qJZ2PtD! z*9EWF5O8WD=%DCo0tXUhiA9k__^gg*OPbOSlEjn_0~Acum{XM$g|^_V+>wrhl1vSK z8#>T!$apYS(!_A^!}T1Ei6IF?q$fNu;vqKXIt)q}fS9S3xZaQ! z$2_fCtA2L;+PKk65w$HzbQPj$sEdKg%jD215t?*RQzj)6gE7$XC_^g*m?)+V0#ejb z9wmv61c)Ovu1g)Bu}j@8**5pFn8$_~Ai6mx0WMw`eAu2-ScQ!JD7%H%8HkdU5h{U^ zkuai_kR+BcJ)+b@%r0Hv;MH>jgxdA%I?Qo2@V!vyinZd{L3xS%>KKc84NZh3Q?_ z8mL@vzs1XLLP^R9iJ3qp`gd(bjxsP=Bn(v0poh1YX<}um(8SHMA&}@tO$%}Av8Hj6 z-p)3|rBw1JSDKJGH~E7%{$OyEI>yq0hF~eh9EXSO0PqTPB6n01!?#Uhb>9Ivg{h2W z?bJ=Xc%(-w9z-+h{jnauc1Y3_7&-`;B9LOB3D6Z=q9i;34Wr~AKq@c|Y>$QvtI)7F zcBKv z{$TaCNi2Se#%!ZEdt!sB3{D9KS@(E1___fC!`%vr(MkXSW&+JcG0F!3E3%Ekl8KFI zvecO^V5^j|dcpW414eE6SWB@b0vYDC(!>V_9G>`NSozgHHmNI31T;ZR2g(?TfT~7W zL>2g`)b$xvX4@oIwC4qurv$@(){ff7JOmwcodNTnblxXd4Q?T;y#^qIYtjJ*LC;Z9@eW%69lrji2}5EZj|&fN=|fv^(u-E$UZYPs zqA^c2k%^*BC;A5&d&3I8H^Chl`q3IIy-}%WhOsgx*`PNCGR1I|ak8$Mko1Zzc*-qW z(J-?bf0XI~#bH#58yTiI^@g_Bzrm;jq*txUJOo=}j_o7fk0zo*ljIXvFu=VFrQ;5T z;d5#PTb~p(iCMUHCTs0tEaJ6zNF}`idb%W6GNikUAow)F=~V}al5wnFd4`*Hi@h15 zs~6(7O=88oa#eE{V}@mLCWS{6WZ)vb?uEyb7Q7tcR00bFAnS*WY00zr5C8BFroK1F zP5r<7yTALr-}^n!!bQuY%rAcNi+O9nC}YtZEC2AUP!Fe#UYOLAY)xu*8clp8v@}w% z!kLBuGyu6mA7rj%sS1gn0@Oeck3IkbU8PN1YlBR4+awlyy5pjHnjZ2U*nhiZ6E&w~ zWY&ci)U7#Yi3-0fG*PH3Fg&h^WmBy3K)}SwesuKcQHK>Z0>m!3;6)4a<(FS}(;ve> z`lCMrdlCit@WT&#a^QlWi0+3bsunp3+)||7v+x13+H)P4>geX#jMt}5pFUwAS-Rvj z4pvIdq*Ti7Q1QX&#!YQEclXHr{(tMy+7yX!Cl)e5!Pb zY4X0{Wpcw%FDsB4?@5vDu|(yiIfl4Vl>GB1g$I`s>xCCyfERCng4*kv;T9XgqlH`< z5a}c;#F6U>y3F!or!MsJ5?eaxN^hDl3IYSs6Ok25Q=Wl!;jf90G?H^x=oGy}u>RdP zD^{GfPTdZYb+0AI_pM4TuBT+r>?D?&-xHFo3dt?u^eIZYBC9+*5@TdSE2xI}ENWIV z%b2)&lqnJQ=3cz$VnUn}Ky9Z|XSZCuSYM&_+9^sjahl#O zMHhXHqHvnsE+W{XM$;TVxO#}O5%j+#vD_5qK91nQn0JFD7BACBEn|c;@klBNj2-8m zd+zbTJ71eci3}bAN`dzwIO!F;gb=-TLS{&X?3y*hi9=T|v({wMh)~fo-zf2meRoS0!xplwg2P?NfakI?(9IN+eC4eluYMvBv znt^oFD{qoOXqxK=EakdpL`XrsI#LgX)=3K-5$O%Vwofc6W%|aXxIie`7P#Ag-2kE$ z;^J!olNpqhMt$CgK%MAj8_If+(~MD!Qlk;Mo*CHUgAyJRpd^RLgo)J_k-8Dk{(zp9 z1+ikRnkEkNIjiB1UUCEMfQHLCm?+#P>fp*zR#BcN3Oq_fu!@GGzHOqa+V+WM?}%YM z!pngfHfrtlgjb2yq?H(mWkNz3fl;EZP{|=pp_ONn3}p`QEb5VKBdUVLQ;koU51W;0 z=yd_G!a1^F$Z|24rnRu`v-N5y*qt+%oP?ri7eIzq(Zm@*ZEOLN9?{7#1PXjgni;rl z5{v5mvbNOkTc^C0#=OBDZPzg|el79!#PBk40-!i_JBh2!dqte2j!>a-)@j7Le?-Ut`p`629qH7|HIT4gtB$-OsM^EvC z%!A$5rFpY*j)mZE|BYspG?GlwP{4%BGS(J@S{M?Z1WdDG%BF5b>sqQmbUnEk)n}jm z&Q~xB)7Cb_iD+zV`NZSYhabuqMUz%;L&$ha`a7iZ4*}g^jM7A~RxEC7ksUK;K=0cY zfb}3#r`I>8+TtWUloXF15>03NcC5E`=y!tg44XxDj$sH&p-m~frZ1)1Dl^gd@B^Ha zBm`ti4Q&N0T{b}&1QsMB6Wkjcf)1W+~%=Y!_jvh5SgtYSPP*~TMl zExbW6Vq>pKw5TMas}yF%APZVb$*rDY4bMPJPOT+l3lH?+{2(XNOqRqy9g{>!m$gmm zD6%bhWD!9n^A_9>vZJE$4BJs;hquict82UpkXUg9BY7dY)m^iScnh0iCU^;LSC7su zfogi^T8#dZk5Lu{$k1;@ZApNvLOnd%hm}<*!@KCvLQami8Dvkd&UTD8{|ge! zBx7o9;{wi#2iG@ytS_!9(lMECwCYFrMAx6-@b!QT=Y3=1tfzba(+p;QM}FWS=(hXz z!op+~IRW#WrwOv{E}UzB6=ra+VKC)FrXEOd86*GP(C_Pgx{^=vr$mR0W8-agfJquv zGH&Z4v7$da`E`A3x6o3taphIfXdgp!>qyK*i_QS&8?}Y_f#7urug zyrWUVUrxwlkBN+EYhIn+R@siG4*|&C5Tm391rT&tZ-~{MPh|DxDq!MPX!FI|c5gs& zt9x=)E}TCX06vb;6y2=4tGKtZWi5r&nq8Yd1Nx8<;0eGpK=Q`$(8n;nnyl@Mb_At2 zd^+1J1V+!xopba1N3KS@hEjo6Sx1q<8>J*#@adveLVo7#IXPsPM3cvFWrfEqRyeO` zojrf{oIAkviz}O>Pnb`w)Y~kE$L-+J7H>FcYfkl%l{XhtU_LPGdg#D@CnSz zG*O~e@Vb&N@LeKg$O0J19KGS7W=Og+1Plm=4m$wh4DWZ8kjg58jLpFAC^T-epE`Bs z%%bm8n5)^&Dk-A_0wrT%XtH}({be;6zK!kS>2wdTKqj|nZW%Tt!&lW46HBrpjScD= zRv%#hh#r}IO}9(R5x8QzlqBnuFcWz z#^-v&Jgy)Q@u)b>QBCC5uAO_%o;^pd1Z~764UM{w#a6DIJb5M=<3Tt~4~HBIWtdPS zQi{>IGBL5bqDEnnnkuYJL>&)8a5~rf~)GC7T-$BXNZdV@VmX8 zew14mA=n7AJm43aQK22ymAC*RS=QxaBCa}JeOe{R9lCswUWg$TtlOI62dh?A+d__C zWxKdotE8rzaH4Npj6TM$EfW*VG{7r`OPev(a#r54bMB^__tJit0Y$lg<)*dVVLULW zt~9rbI)C`Rnq7TyX>rGVy^;{iM*(`raq1?n&gKjttHf944VH@J84QAJS9m%4(1B>R zJ%Tb(k6cLgDm63_q(nFG8hG>0!#jS^8v-n+XDee+hDnCNWRyBtBAdZ_V}9V`G8`KY z85JgAy`ePDTMh?qxmiNA*OV^~pC1lF&^T4)k8>MtQT9YgHapw-)788_TW77xE#`BA z5<_4jbX7euvHZ=5rIo=CFuI}!+p0Y{Cz@|fq3jD*o&EdACl+lG8)DV-B|3hA!NY%O>f7tb?SufyJ~C#9{p8uwqgh1F=?@sL+ou;OB_bi}3% z_3R65sQ?V)8Y^J3v;YWAZHTgPZqddEX$V#TAyIhHSV#pKOa?-ROfyB7ndyv`66P6* zKr?XgFr!VK4YBO+k2*h?;Psc?hXun7A00&*NGk%Xi8daF0?hC07C%JcXgFF=Oe{j% z)Ab~2{?1)F>!yA6aSQCCike#RtGv)tcZ(^>Mk7+w@}Y;+4lbX zl~wS46gXC(1SBK7H;^gay3o`u)?M|UCnp4hCYFviBL&`YTB*SvE+=jh%iq^IyHGFg z>B0ehN{MVGav>@2kOR1Re|=Y1Cp|SordNOhud3HgEbH*Jl=AES`ZHZ^)tOnmSZ{>k zLahLlU=;&au}(8IYV^puC3@2}3UnAHGcrnzxi8b>Aj9A>Q5As;GcmgI*daNXoWHg z`ZPn~~#RrfL` zl!@=WFE2Vw)IwF)6uV_*35@IstgbCO{^e6T&_t7W)N!=X3Tp2~5Z@slEmRa{WC|FW z0!HJM8cO&8IOzJ#Ye&Z#rnhqr2Thb#V#+9*@<~$iK7guY`ApB$-3PbYO(iDC)ih`4 z1{a3BZlKU&SWH$Hh_D&OgvPUmMEY70!80>m9eC~aW4dDgXcq-ELxwOf{KuxD43P&i>If{<>eULzL zhpECHMp3_cp(bU^lG?~1GaSYobjrVrM(&TxYOCQ&TRM@<;ZfyX#Rjncr_m=)T=OIAr0 z>a{7Px-c=ZjLhIBvb2wFW2IgPB!f8BNS4yjc4O1g4A1B+|0rdB zwKOm;{Fy@X80N#ZXKk0uJxgf}41Y9$wUk6ahLU3?5De#-dysyV=z^EMu4g!3X@59( zabjY%`CY&GE~YDvP-Q&iDu=Gs<62{4Cl|yEOyY5FvPxTu7@V`0=MrYTqL=;hOc!F7 z2X8o5of`lKrJ9+VEN#7Ge9gv4&+ybSJa~MR?kmVqno$=?6iu=7I-McmY@q!`Vp%dX zFs$%7cg`)FMaH^~XjADzla8aD$HGxhmlVM>O0qW^j_TPdqk!M7k^A9??&8;}-w~?l>!NlzDeg0xF&E*jD)YPUt zysIJkhf@AIff}A913;=#s=Kcf%PwwxAPS(tL7i*{bo`{@i7wI6fl5?0y#39d!3&(m zB48Kh7cVTY%uRbf4feML=x*t3QpQh!Npjopx+*d@I-rD%!hj4+Q_U^GAl8g~dD2}1 zYna}4l=L#=z>tZF6^=$KZChb-J&@koLaT0bToZ$XWU~^O+K81-0IwM<^m2_N+Z3^s zf$_4MjmQE_vKOn~wH6MBGJwFHcol$6?gkLoRw-4zXi9AZDdwsa9HU@dBHMyo^`HE( zE{fewqp99uCahwADYI8m5P`XT#XMMAv}%LSbvG}Lak3@W|xDS$8RkLfTW zImK`_0)Sf1wU-OV7#bQhQS1baGu-RapmRn|$W+Zj-*9{<=thLhP?ooUwlW_1UA;Y1syNQ`z>oMo<+*#{+ zRx*HIGf(Y>@3A%-7<|Nangke!Nd%p8Fnmgc1~=k~!)>u1mR#7qtNxs6xUCxE5{xJO z7Y)UvTeq5Wayzmp$z+1Uq3OJOONCTS!sd-iL0qF4*drffJwLQJxM|{)t7*bcypL7S z00y~LkE?FI?SRAr*ccfPP)DR1C0`JnM8;alaFoSpD@xQGRRGKv@RX1K>Zr=Udg-sX z50P;1z3aNHA_tk8Dd3As^&=|7GbCN`_-x8nf3}4$xlXz3?mMjrc;c|=nQA-ZzW;vR z_~zqQueiTRN`uS8Pd)vlrlrme?5}<8YnE7}6lP*bH?zZCCDTbsPM`-YuUwd&$=PPc zwWl+P(2i3zl#YTP>7%L>-Tme3#r7@k5kqgIU#fv#}AZ1tm_;bdVoJc(Btg?Ut! z!w6u=g`o^sP4mC_i@&g({PREmKYs$qmmOYx^;P@3Kg|64>#uvA!KIvUe)xkf{&tW< zmFF{u4m$_ z=z8q2$G`QhCv?8~=3DQ*_x`)@zW35gFI$cN^FRO7cSnBs!yozz*SEj@ZOQl4Q%~K0 z|9!6Jm@jVlINjmUmga%eNP+3`-E0&mu2@Y>aRU&qM3NJq38Ao=<=l;iktPQvlz0Ik zT2?j9Eh!XE9ssDPrbfl2mB8xIWdG`^gFa;NX0Y_*1lN&_LO;T$TpvN`af|tYjhDt@ zkdclPZj3~w=J>jijeo1_fmPT>B3br0uj4)X=%dd(^UT46_tD>@`mcWVtMr#Z_uhN2 zRmu?CXErB3sK1#*hp0gc(@e0l z?To*1yCxSs@`p2}u@H97y<}fIY0%Ve8V7a@o3KK|7!tFBj&sb90HAH@7P%jA4fm|hO zBLk2HLy9Kr09GTj{a^p}UqAP`&pS#VJNA}=_ul&{yIV`63*$?dxCvy4A}g z;8=uPY>X5i(260EtE;(UX0wSkaVzxDoq#5+T()O#{TTudRV8B><9?&%G%J%Xit3^e zRpcB%p_$>=zqpL`$5g-j-R~Ycbcpn5B63kIzD}HID?jL|3&s~geY?@Q1m3_}EoN(z zT3+!dM%}TTURqwz2Z%>{@TdVo!@l%~zJt3~P>7gB1iRFw!d+dV$rt z#73%ZN};Po%-#Mns;@A%1MGz#{NM*t%{Q*$%~wz3!Rh_N7rtPUAP^pB-o#bRV3_dm z!;gyD_rCW%ipe&_FTM1VWAaTm-}1l%5A;hGE@D2b0h=4ihcQqKc=AVyQPH4?M z5AXbzOIiq4;)Q_zdWEKOtOKzwvM{VNRlO-_02zp2vj$FFPQ3V2vt!oPPaQH&`J+5` z^g3+}mc-I*#9CTumIZAkWsf8iT?$35bZMf5(F`1oAz1>CNooXfT-as^7-d8lh#0tVBqn}LKEbhGXF5!__lG0ab z^tJGRvr!ahkH7CPPuFwIrR57e<-gf!H{vDk zAhDM4PaY?kr|10Zb?qFxN|3RQz%FyVeg(AO29jJ6F4hUCV~Zcjx>e?~uQ_veWqGbG ziH6tJJts$Jy%ubR8(V~3mS!c40&e5aCOoMu>u|2AL=%I&ReDTHCUVBaEc~PSOs@SAQW+x?Gz8*MmYtzt%vgoy{ zQfVCrt<|!$ssZL&Ocqev0}))u70Pn`#B@;Nv0-aN|2hd+fN?V`G9Z2?P7_RLuoGlH zzBNgGZ}7S$W{z>J+-kWu*fjA8w?qeA<@p!>d#r-GNL47%qP}F-Qqe0wW9iXQkyJGs z+Q9l?E7ycR45LKP49E-&-Y5VLkcwt>#Sj6$5kc-B#355l2wkDnTwBoRofDBx*3tj~ z5tB(oK~y^6bL7a8I9Ht&2!E-A1G^;Cd+!|w%b~1-U>v~S_pWY!+aqx5;iQ8q(@ECh8kkM~ZUJfn`1*;&{AwUn`-3kr?|3U_a44E2-G&=yfwxE#^ zm?(P1CiGNDuG_462N`E6KFdKLK$w%84C|O^2%mVwp+T}~fNaiPtIRqW_njlF8|B0; zzb{;@+k}ovXHK7^*wBl$5w62+ZP#GmYKjp-7KbqE*4u7Itf>lKv*|Et9|)(CU=;6J zuql>DPmND?DUj+*)D3z0RWrV97QYtF@)x8!5mob)BL5HsR!1jw3Y$^VyDw1yDH6`; zZAQ_O7n&o+>x_wcDZjA=lNk8K2Pgev&O0YE$-}a_*&QC8`Y;PFXe3j35){5kFB~Y= zk$5uHgqKnc5P0j2z)XtDcqL)|d~@%<-Cj5W?Ag1^Guc|Jrpj-MDQb;D7SS*h8BOkp zMdhrv1H_*m4z_7Nn7wXsG~N^Sj{+^SR+hRwcHqFx);?~56ljH)6wRvt`fvVrtV;Ue zsiJQ1O4gxCbCS9^vOKJfq6#>^2p{#2fBa*aVe6rXKM^C?M!|;S}*Cz1~l;t(SFZZK4^tFUCla{)_+PiUd3gTX_lC2CbVXJoDu?woG2299+^IM-V#@wfam zeC3M%RRYk%&=bICUM{KP=qlrf^F z&`eR)aQcG`v_=SPd<8QxvHXI<*Edk`mM^F~`|sLWzu_NsUe_3zB5W?faFRxl*H!MJP8!Y28#gb>PPWaB_tkZR8o3F&>_S^lftHCec zWZQ7{s-3}8VyQ(Tez7uHokwW4 z!uQh5t{-1yZIm&z(c5&Q&0wH>vI4#H&O7o+VmUlnhm5s)nV0rE+n=U&)mRu40@844 z)a*I$n1#7sO)9P9TB9O;VWDn0I&H5*Tm>t5stO z7Ttn$^hxoO@akQFOz64nQ9zkp@^mt*3`Cl+4J zv(UwO43utF$!tY0GV-Jd0f{}bY>PsLK(DOw#LcZE%aYnH-3K3hP)^qISaqAbCaVDBm@4tuFPqIxYOa%87N^*StN~>eK|%Je zNJ(>*#LH_YUo$OnC?ylx7Hlx5>7_}!E>FtZQER!8!dFsc=!#kkGOz=DgT@2+I{Thk z`0&Ijf7QbGPGGDlB4t_Gf8Zt?<-{ALESAzAg+YuaibXdBfN`aM_j3IB+u1RZ$p;6oNp}OnCklYcQY#hzgJG2#7Yl?rOn+snW?18yoM0UYCHW zxk`k_Bd_lvzbNY?gtD2yJDs z(7J+#j4WDlnWLeNyQ?%De)<1m<-N&L&^fT&K5#;V>nstkQm=AgkPnwHE+fMn9MjTd z-OSY72gmF8BZ@n>rV{{_xkfda*S=pOcIA^cy!i2qEt{UcScX!VCMH&6S7Q{{3yhC! zEMQ9PNC}2orWZJYjK<&)8m?3>w`{{l6I;Ci#I&Nf;fY^TCuE%=0D)327{bA67@FA zd>#7mk0D^rBtl{Ha8!Mu!8Mj9ysCm#b6Kz;WL1Twp1!vNX^ZsbThFTN^LRrpWBxVT z5To0S85|yySQHv926GL;oE|YoaYvZKU{zWSk6hq%0Th(F;#{(Hsd39dwUTB=1y&my z%-WiI5(|N`y%$_z99Ot(Zbv=+^-gBX=9SqwpK+Y_a)Z5(ZW)9jcVj+|$7SLwN-bg; zubHArTqfIPvYtGwyCt&XsOH#9=R8}UUR~@3U>4#uI|ul3?8=H?ez=kA30y7tb-${0 z=u?cziejF?`!d$eH}A8zg0uItlcdOtHc2VlWtw4v-s%-%u+sA^O*Q}!2M>UePpe`^ zP^Zj5H9;907!5$(0nNAGdD}CKx)5%cAkoeFTsg*c13P2ak!b06w%i8Rq3RBN& zXJ)}l*ha3i>$TXsH2tpXQWWg&ve?H*5z7D=OD$ePCd7JS=xoE+#t;Zee?509Eg z4YsSh7_j2HyjGL?-Sc2m>I5AaJh8_v91Y`8-ASo-4O~yMAmi&=0$K@Wo7t6l_E#{# zDuoh2X+M{e~3z>9JFHH1NkFGWRf*`#B&dR<6W`g3f873wm>y%+TD)h2p zw<67U=`O*f5Z{ZVa89>VN*01)Pz3mDZoy-9W-RN3GJ)n3C9_c%JSPaR#PG{$7;|nc zpE5=Vismio`a9wd>pS_kxLvBQ1+H$(>33$GM-DobdSL(2$V}BN1(ctH7S_~FfvW7Lt}RUIA+N( zTOv5Dv5S8V0jLGi^FCyNcn1<$XEh@9YJyQ?El0D0EqbE^wWDWr(?wO8M@hvF$T|Qd zdc+*QfwBIt1^ihg`|?ATGiN|RCE zZ^7T2Ql1=J61>^+SAX?a;urDa6u|((l5k_CUI!%gqJwEr?rF149ntYiTozaME(C$$ z!DOnTvxS|GfUXEF1%_|T#Iz35^q_~tji`R1E%A3b^$Q}EMIKP?f_ z%Ovu0#KR2+H$35^-z4!@j>kV)vWk^RkS@KAyFX?SrcnNx(x1(=BtYtcfft)&d>eR6}-?Te$r;n%BL+bh2XP^5Izr}v*tz%wowk*ND z{PL^f77=mgb1kB?f!g+>S1F>qL^&(X%~H zroT*r;wDz8+W{-*_tLA;SAdxiE6_w?%!6laFjt}W9;R0XTe_ItLxz6n*Wn~nR2ueg z_+KVuo&(A`!*r3y<(Gy5=w)Wv>s97CWw{#=l#k_4Sa+%$Oe_jv~0H z6j)aZ7{MqFGC2TWbve&*e&jN_j`%vq6tB(v6q@%XQ+6>dKM^i2h}E<X%gaOJ;<{7$*`M_>(nMkvcHwlQC)b4qqdH2%*hw6*ga@%_xo}~QV{4$ndG3(WSatFWU`4JfQ{Wu9*Fq?z-SbSDk1@4P2rGhQ8e*>J1bK1(8K%G A!2kdN literal 0 HcmV?d00001 diff --git a/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json new file mode 100644 index 000000000..17f6fd948 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Image.png new file mode 100644 index 0000000000000000000000000000000000000000..48735a706b22b9eecc3852842636b76d7ce760cd GIT binary patch literal 44133 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π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 FV1lYE_9XxS literal 0 HcmV?d00001 diff --git a/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json new file mode 100644 index 000000000..17f6fd948 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Image.png new file mode 100644 index 0000000000000000000000000000000000000000..2f67475c133f9656f1c96ae31db1811f60713a6e GIT binary patch literal 52142 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 LNkvXXu0mjf8jjP< literal 0 HcmV?d00001 diff --git a/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json new file mode 100644 index 000000000..17f6fd948 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Image.png new file mode 100644 index 0000000000000000000000000000000000000000..e69d7979f94d74d90360dff0a9c9988d606d1c3c GIT binary patch literal 41376 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

SI6So=t`BdDf>ZvUk8(khY=u$vLRf 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 + ) { + self.basemapGalleryItems = basemapGalleryItems + self._selectedBasemapGalleryItem = selectedBasemapGalleryItem } - - public var basemaps: [BasemapGalleryItem] = [] -// /// The `Viewpoint` of the main `GeoView`. -// let viewpoint: Viewpoint? -// -// /// The visible area of the main `GeoView`. Not applicable to `SceneView`s. -// let visibleArea: Polygon? -// -// private var symbol: Symbol -// -// private var scaleFactor = 25.0 -// -// @StateObject -// private var map = Map(basemapStyle: .arcGISTopographic) -// -// /// The `Graphic` displaying the visible area of the main `GeoView`. -// @StateObject -// private var graphic: Graphic -// -// /// The `GraphicsOverlay` used to display the visible area graphic. -// @StateObject -// private var graphicsOverlay: GraphicsOverlay -// -// /// Creates an `OverviewMap` for use on a `MapView`. -// /// - 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. -// /// - Returns: A new `OverviewMap`. -// public static func forMapView( -// with viewpoint: Viewpoint?, -// visibleArea: Polygon? -// ) -> OverviewMap { -// OverviewMap(viewpoint: viewpoint, visibleArea: visibleArea, symbol: .defaultFill) -// } -// -// /// Creates an `OverviewMap` for use on a `SceneView`. -// /// - Parameter viewpoint: Viewpoint of the main `SceneView` used to update the -// /// `OverviewMap` view. -// /// - Returns: A new `OverviewMap`. -// public static func forSceneView( -// with viewpoint: Viewpoint? -// ) -> OverviewMap { -// OverviewMap(viewpoint: viewpoint, symbol: .defaultMarker) -// } -// -// /// 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. -// init( -// viewpoint: Viewpoint?, -// visibleArea: Polygon? = nil, -// symbol: Symbol -// ) { -// self.visibleArea = visibleArea -// self.viewpoint = viewpoint -// self.symbol = symbol -// -// let graphic = Graphic(symbol: self.symbol) -// -// // It is necessary to set the graphic and graphicsOverlay this way -// // in order to prevent the main geoview from recreating the -// // graphicsOverlay every draw cycle. That was causing refresh issues -// // with the graphic during panning/zooming/rotating. -// _graphic = StateObject(wrappedValue: graphic) -// _graphicsOverlay = StateObject(wrappedValue: GraphicsOverlay(graphics: [graphic])) -// } + + public var basemapGalleryItems: [BasemapGalleryItem] = [] + + @Binding + public var selectedBasemapGalleryItem: BasemapGalleryItem? public var body: some View { - List { - basemaps.forEach { basemapGalleryItem in - Text(basemapGalleryItem.name) + PlainList { + ForEach(basemapGalleryItems) { basemapGalleryItem in + BasemapGalleryItemRow(item: basemapGalleryItem) + .onTapGesture { + selectedBasemapGalleryItem = basemapGalleryItem + } } } + .esriBorder() } -// MapView( -// map: map, -// viewpoint: makeOverviewViewpoint(), -// graphicsOverlays: [graphicsOverlay] -// ) -// .attributionText(hidden: true) -// .interactionModes([]) -// .border(.black, width: 1) -// .onAppear(perform: { -// graphic.symbol = symbol -// }) -// .onChange(of: visibleArea, perform: { visibleArea in -// if let visibleArea = visibleArea { -// graphic.geometry = visibleArea -// } -// }) -// .onChange(of: viewpoint, perform: { viewpoint in -// if visibleArea == nil, -// let viewpoint = viewpoint, -// let point = viewpoint.targetGeometry as? Point { -// graphic.geometry = point -// } -// }) -// .onChange(of: symbol, perform: { -// graphic.symbol = $0 -// }) -// } + // MapView( + // map: map, + // viewpoint: makeOverviewViewpoint(), + // graphicsOverlays: [graphicsOverlay] + // ) + // .attributionText(hidden: true) + // .interactionModes([]) + // .border(.black, width: 1) + // .onAppear(perform: { + // graphic.symbol = symbol + // }) + // .onChange(of: visibleArea, perform: { visibleArea in + // if let visibleArea = visibleArea { + // graphic.geometry = visibleArea + // } + // }) + // .onChange(of: viewpoint, perform: { viewpoint in + // if visibleArea == nil, + // let viewpoint = viewpoint, + // let point = viewpoint.targetGeometry as? Point { + // graphic.geometry = point + // } + // }) + // .onChange(of: symbol, perform: { + // graphic.symbol = $0 + // }) + // } // MARK: Modifiers -// -// /// The `Map` displayed in the `OverviewMap`. -// /// - Parameter map: The new map. -// /// - Returns: The `OverviewMap`. -// public func map(_ map: Map) -> OverviewMap { -// var copy = self -// copy._map = StateObject(wrappedValue: map) -// return copy -// } -// -// /// 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`. -// /// - Parameter scaleFactor: The new scale factor. -// /// - Returns: The `OverviewMap`. -// public func scaleFactor(_ scaleFactor: Double) -> OverviewMap { -// var copy = self -// copy.scaleFactor = scaleFactor -// return copy -// } -// -// /// The `Symbol` used to display the main `GeoView` visible area. For `MapView`s, the symbol -// /// should be appropriate for visualizing a polygon, as it will be used to draw the visible area. For -// /// `SceneView`s, the symbol should be appropriate for visualizing a point, as it will be used to -// /// draw the current viewpoint's center. For `MapView`s, the default is a transparent -// /// `SimpleFillSymbol` with a red 1 point width outline; for `SceneView`s, the default is a -// /// red, crosshair `SimpleMarkerSymbol`. -// /// - Parameter symbol: The new symbol. -// /// - Returns: The `OverviewMap`. -// public func symbol(_ symbol: Symbol) -> OverviewMap { -// var copy = self -// copy.symbol = symbol -// return copy -// } + // + // /// The `Map` displayed in the `OverviewMap`. + // /// - Parameter map: The new map. + // /// - Returns: The `OverviewMap`. + // public func map(_ map: Map) -> OverviewMap { + // var copy = self + // copy._map = StateObject(wrappedValue: map) + // return copy + // } + // + // /// 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`. + // /// - Parameter scaleFactor: The new scale factor. + // /// - Returns: The `OverviewMap`. + // public func scaleFactor(_ scaleFactor: Double) -> OverviewMap { + // var copy = self + // copy.scaleFactor = scaleFactor + // return copy + // } + // + // /// The `Symbol` used to display the main `GeoView` visible area. For `MapView`s, the symbol + // /// should be appropriate for visualizing a polygon, as it will be used to draw the visible area. For + // /// `SceneView`s, the symbol should be appropriate for visualizing a point, as it will be used to + // /// draw the current viewpoint's center. For `MapView`s, the default is a transparent + // /// `SimpleFillSymbol` with a red 1 point width outline; for `SceneView`s, the default is a + // /// red, crosshair `SimpleMarkerSymbol`. + // /// - Parameter symbol: The new symbol. + // /// - Returns: The `OverviewMap`. + // public func symbol(_ symbol: Symbol) -> OverviewMap { + // var copy = self + // copy.symbol = symbol + // return copy + // } +} + +private struct BasemapGalleryItemRow: View { + var item: BasemapGalleryItem + var body: some View { + VStack { + if let thumbnail = item.thumbnail { + // TODO: thumbnail will have to be loaded. + Image(uiImage: thumbnail) + } + Text(item.name) + .font(.footnote) + } + } } // MARK: Extensions diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 4af27056f..37f508051 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -27,14 +27,26 @@ public struct BasemapGalleryItem { self.name = name self.description = description self.thumbnail = thumbnail +// self.thumbnailURL = thumbnailURL } - var basemap: Basemap - var name: String - var description: String? - var thumbnail: UIImage? + public var basemap: Basemap + public var name: String + public var description: String? +// public var thumbnailURL: URL? + public let thumbnail: UIImage? } -//extension DisplayableBasemap: Identifiable { -// public var id: ObjectIdentifier { ObjectIdentifier(self) } -//} +extension BasemapGalleryItem: Identifiable { + public var id: String { name } +} + +extension BasemapGalleryItem: Equatable { + public static func == (lhs: BasemapGalleryItem, rhs: BasemapGalleryItem) -> Bool { + lhs.basemap === rhs.basemap && + lhs.name == rhs.name && + lhs.description == rhs.description && + lhs.thumbnail === rhs.thumbnail + } + +} From 0765c947ec7728af367112cb3ce1602931dd1c40 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 Aug 2021 11:45:36 -0500 Subject: [PATCH 0154/1722] Fix indenting --- .../Utility/EsriDeleteTextButtonViewModifier.swift | 6 +++--- Sources/ArcGISToolkit/Utility/PlainList.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift index e1575f619..748b661b9 100644 --- a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift @@ -23,9 +23,9 @@ struct EsriDeleteTextButtonViewModifier: ViewModifier { Button( action: { self.text = "" }, label: { - Image(systemName: "xmark.circle.fill") - .foregroundColor(Color(.opaqueSeparator)) - } + Image(systemName: "xmark.circle.fill") + .foregroundColor(Color(.opaqueSeparator)) + } ) } } diff --git a/Sources/ArcGISToolkit/Utility/PlainList.swift b/Sources/ArcGISToolkit/Utility/PlainList.swift index df0fee803..ff1e1cfec 100644 --- a/Sources/ArcGISToolkit/Utility/PlainList.swift +++ b/Sources/ArcGISToolkit/Utility/PlainList.swift @@ -22,7 +22,7 @@ public struct PlainList : View where Content : View { public init(@ViewBuilder content: () -> Content) { self.content = content() } - + /// The content of the list. public var body: some View { List { From bd3d26d123680dbd85a453f0013f94374ddf3d73 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 Aug 2021 11:52:38 -0500 Subject: [PATCH 0155/1722] Move search button to the left side for clarity. --- .../Utility/EsriSearchButtonViewModifier.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index ced5a550d..af14f4cae 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -18,14 +18,14 @@ struct EsriSearchButtonViewModifier: ViewModifier { func body(content: Content) -> some View { HStack { - content Button( action: { self.performSearch = true }, label: { - Image(systemName: "magnifyingglass.circle.fill") - .foregroundColor(Color(.opaqueSeparator)) - } + Image(systemName: "magnifyingglass.circle.fill") + .foregroundColor(Color(.opaqueSeparator)) + } ) + content } } } From 1c03c4793b59b33cb57e35721852133577a2cb31 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 6 Aug 2021 09:16:01 -0500 Subject: [PATCH 0156/1722] Add test target and some initial tests. --- Package.swift | 10 +- .../SearchViewModelTests.swift | 145 ++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/SearchViewModelTests.swift diff --git a/Package.swift b/Package.swift index 36ada519c..69157f633 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,8 @@ let package = Package( products: [ .library( name: "ArcGISToolkit", - targets: ["ArcGISToolkit"]), + targets: ["ArcGISToolkit"] + ), ], dependencies: [ .package(name: "ArcGIS", path: "../swift") @@ -31,6 +32,11 @@ let package = Package( targets: [ .target( name: "ArcGISToolkit", - dependencies: ["ArcGIS"]), + dependencies: ["ArcGIS"] + ), + .testTarget( + name: "ArcGISToolkitTests", + dependencies: ["ArcGISToolkit"] + ) ] ) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift new file mode 100644 index 000000000..e1ba49f05 --- /dev/null +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -0,0 +1,145 @@ +// Copyright 2021 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 Foundation + +import XCTest +import ArcGIS +import ArcGISToolkit +import SwiftUI + +class SearchViewModelTests: XCTestCase { + func testIsEligibleForRequery() async { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // isEligibleForRequery defaults to `false`. + XCTAssertFalse(model.isEligibleForRequery) + + // There are no results, so setting `queryArea` has + // no effect on `isEligibleForRequery`. + model.queryArea = createPolygon() + XCTAssertFalse(model.isEligibleForRequery) + + // We have results and a new polygon, `isEligibleForRequery` is true. + model.currentQuery = "Magers & Quinn Bookseller" + await model.commitSearch(false) + model.queryArea = createPolygon() + XCTAssertTrue(model.isEligibleForRequery) + } + + func testCommitSearch() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // No search - results are nil. + try XCTAssertNil(model.results.get()) + + // Search with no results - result count is 0. + model.currentQuery = "No results found blah blah blah blah" + await model.commitSearch(false) + var results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 0) + + XCTAssertNil(model.selectedResult) + try XCTAssertNil(model.suggestions.get()) + + // Search with one result. + model.currentQuery = "Magers & Quinn Bookseller" + await model.commitSearch(false) + results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 1) + + // One results automatically populates `selectedResult`. + XCTAssertNotNil(model.selectedResult) + try XCTAssertNil(model.suggestions.get()) + + // Search with multiple results. + model.currentQuery = "Magers & Quinn" + await model.commitSearch(false) + results = try XCTUnwrap(model.results.get()) + XCTAssertGreaterThanOrEqual(results.count, 1) + + XCTAssertNil(model.selectedResult) + try XCTAssertNil(model.suggestions.get()) + } + + func testUpdateSuggestions() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // No currentQuery - suggestions are nil. + try XCTAssertNil(model.suggestions.get()) + + // UpdateSuggestions with no results - result count is 0. + model.currentQuery = "No results found blah blah blah blah" + await model.updateSuggestions() + var results = try XCTUnwrap(model.suggestions.get()) + XCTAssertEqual(results.count, 0) + + XCTAssertNil(model.selectedResult) + try XCTAssertNil(model.results.get()) + + // UpdateSuggestions with results. + model.currentQuery = "Magers & Quinn" + await model.updateSuggestions() + results = try XCTUnwrap(model.suggestions.get()) + XCTAssertGreaterThanOrEqual(results.count, 1) + + XCTAssertNil(model.selectedResult) + try XCTAssertNil(model.results.get()) + } + + func testAcceptSuggestion() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + model.currentQuery = "Magers & Quinn Bookseller" + await model.updateSuggestions() + let suggestionionResults = try XCTUnwrap(model.suggestions.get()) + let suggestion = try XCTUnwrap(suggestionionResults.first) + + await model.acceptSuggestion(suggestion) + let results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 1) + + // TODO: look into setting model.selectedResults in didSet of `results`. + XCTAssertNotNil(model.selectedResult) + try XCTAssertNil(model.suggestions.get()) + } +} + +// Move to new file +class LocatorSearchSourceTests: XCTestCase { + // Modify GeocodeParameters and SuggestParameters from example view + // Pass in custom locator (maybe from MMPK?) + // Set searchArea and/or preferredSearchLocation once (instead of every pan) + // +} + +// Move to new file +class SmartLocatorSearchSourceTests: XCTestCase { + // +} + +extension SearchViewModelTests { + func createPolygon() -> Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: .london) + let _ = builder.add(point: .paris) + let _ = builder.add(point: .rome) + return builder.toGeometry() as! ArcGIS.Polygon + } +} + +extension Point { + static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) + static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) + static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) +} From 1b24a8f3fb8be909c598855a3d11073cbd07394a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 6 Aug 2021 09:17:04 -0500 Subject: [PATCH 0157/1722] Dispatching to the main queue needs to be sync to maintain proper order of things. --- .../ArcGISToolkit/Components/Search/SearchViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index b9af50b0e..dd3e4fdcd 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -209,7 +209,7 @@ extension SearchViewModel { ) } - DispatchQueue.main.async { [weak self] in + DispatchQueue.main.sync { [weak self] in self?.isEligibleForRequery = false self?.suggestions = .success(nil) @@ -239,7 +239,7 @@ extension SearchViewModel { try await source.suggest(currentQuery) } - DispatchQueue.main.async { [weak self] in + DispatchQueue.main.sync { [weak self] in self?.results = .success(nil) self?.selectedResult = nil self?.isEligibleForRequery = false @@ -267,7 +267,7 @@ extension SearchViewModel { try await searchSuggestion.owningSource.search(searchSuggestion) } - DispatchQueue.main.async { [weak self] in + DispatchQueue.main.sync { [weak self] in var searchResults = [SearchResult]() var suggestError: Error? From 6034a37249342eec80ae5e4beaea423dc2e5f189 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 6 Aug 2021 09:57:15 -0500 Subject: [PATCH 0158/1722] Add new test files --- .../LocatorSearchSourceTests.swift | 145 ++++++++++++++++++ .../SearchViewModelTests.swift | 72 ++++----- .../SmartLocatorSearchSourceTests.swift | 136 ++++++++++++++++ 3 files changed, 310 insertions(+), 43 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift create mode 100644 Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift new file mode 100644 index 000000000..665465ffc --- /dev/null +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -0,0 +1,145 @@ +// Copyright 2021 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 Foundation + +import XCTest +import ArcGIS +import ArcGISToolkit +import SwiftUI + +class LocatorSearchSourceTests: XCTestCase { + // Modify GeocodeParameters and SuggestParameters from example view + // Pass in custom locator (maybe from MMPK?) + // Set searchArea and/or preferredSearchLocation once (instead of every pan) + // +} + +//class SearchViewModelTests: XCTestCase { +// func testAcceptSuggestion() async throws { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// model.currentQuery = "Magers & Quinn Bookseller" +// await model.updateSuggestions() +// let suggestionionResults = try XCTUnwrap(model.suggestions.get()) +// let suggestion = try XCTUnwrap(suggestionionResults.first) +// +// await model.acceptSuggestion(suggestion) +// let results = try XCTUnwrap(model.results.get()) +// XCTAssertEqual(results.count, 1) +// +// // TODO: look into setting model.selectedResults in didSet of `results`. +// XCTAssertNotNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// } +// +// func testCommitSearch() async throws { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// // No search - results are nil. +// try XCTAssertNil(model.results.get()) +// +// // Search with no results - result count is 0. +// model.currentQuery = "No results found blah blah blah blah" +// await model.commitSearch(false) +// var results = try XCTUnwrap(model.results.get()) +// XCTAssertEqual(results.count, 0) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// +// // Search with one result. +// model.currentQuery = "Magers & Quinn Bookseller" +// await model.commitSearch(false) +// results = try XCTUnwrap(model.results.get()) +// XCTAssertEqual(results.count, 1) +// +// // One results automatically populates `selectedResult`. +// XCTAssertNotNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// +// // Search with multiple results. +// model.currentQuery = "Magers & Quinn" +// await model.commitSearch(false) +// results = try XCTUnwrap(model.results.get()) +// XCTAssertGreaterThanOrEqual(results.count, 1) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// } +// +// func testIsEligibleForRequery() async { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// // isEligibleForRequery defaults to `false`. +// XCTAssertFalse(model.isEligibleForRequery) +// +// // There are no results, so setting `queryArea` has +// // no effect on `isEligibleForRequery`. +// model.queryArea = createPolygon() +// XCTAssertFalse(model.isEligibleForRequery) +// +// // We have results and a new polygon, `isEligibleForRequery` is true. +// model.currentQuery = "Magers & Quinn Bookseller" +// await model.commitSearch(false) +// model.queryArea = createPolygon() +// XCTAssertTrue(model.isEligibleForRequery) +// } +// +// func testUpdateSuggestions() async throws { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// // No currentQuery - suggestions are nil. +// try XCTAssertNil(model.suggestions.get()) +// +// // UpdateSuggestions with no results - result count is 0. +// model.currentQuery = "No results found blah blah blah blah" +// await model.updateSuggestions() +// var results = try XCTUnwrap(model.suggestions.get()) +// XCTAssertEqual(results.count, 0) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.results.get()) +// +// // UpdateSuggestions with results. +// model.currentQuery = "Magers & Quinn" +// await model.updateSuggestions() +// results = try XCTUnwrap(model.suggestions.get()) +// XCTAssertGreaterThanOrEqual(results.count, 1) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.results.get()) +// } +// +//} + +// Move to new file +class SmartLocatorSearchSourceTests: XCTestCase { + // +} + +extension SearchViewModelTests { + func createPolygon() -> Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: .london) + let _ = builder.add(point: .paris) + let _ = builder.add(point: .rome) + return builder.toGeometry() as! ArcGIS.Polygon + } +} + +extension Point { + static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) + static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) + static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) +} diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index e1ba49f05..13d3d1fc2 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -19,24 +19,21 @@ import ArcGISToolkit import SwiftUI class SearchViewModelTests: XCTestCase { - func testIsEligibleForRequery() async { + func testAcceptSuggestion() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) + + model.currentQuery = "Magers & Quinn Bookseller" + await model.updateSuggestions() + let suggestionionResults = try XCTUnwrap(model.suggestions.get()) + let suggestion = try XCTUnwrap(suggestionionResults.first) - // isEligibleForRequery defaults to `false`. - XCTAssertFalse(model.isEligibleForRequery) - - // There are no results, so setting `queryArea` has - // no effect on `isEligibleForRequery`. - model.queryArea = createPolygon() - XCTAssertFalse(model.isEligibleForRequery) + await model.acceptSuggestion(suggestion) + let results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 1) - // We have results and a new polygon, `isEligibleForRequery` is true. - model.currentQuery = "Magers & Quinn Bookseller" - await model.commitSearch(false) - model.queryArea = createPolygon() - XCTAssertTrue(model.isEligibleForRequery) + try XCTAssertNil(model.suggestions.get()) } - + func testCommitSearch() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -71,6 +68,24 @@ class SearchViewModelTests: XCTestCase { XCTAssertNil(model.selectedResult) try XCTAssertNil(model.suggestions.get()) } + + func testIsEligibleForRequery() async { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // isEligibleForRequery defaults to `false`. + XCTAssertFalse(model.isEligibleForRequery) + + // There are no results, so setting `queryArea` has + // no effect on `isEligibleForRequery`. + model.queryArea = createPolygon() + XCTAssertFalse(model.isEligibleForRequery) + + // We have results and a new polygon, `isEligibleForRequery` is true. + model.currentQuery = "Magers & Quinn Bookseller" + await model.commitSearch(false) + model.queryArea = createPolygon() + XCTAssertTrue(model.isEligibleForRequery) + } func testUpdateSuggestions() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -97,35 +112,6 @@ class SearchViewModelTests: XCTestCase { try XCTAssertNil(model.results.get()) } - func testAcceptSuggestion() async throws { - let model = SearchViewModel(sources: [LocatorSearchSource()]) - - model.currentQuery = "Magers & Quinn Bookseller" - await model.updateSuggestions() - let suggestionionResults = try XCTUnwrap(model.suggestions.get()) - let suggestion = try XCTUnwrap(suggestionionResults.first) - - await model.acceptSuggestion(suggestion) - let results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 1) - - // TODO: look into setting model.selectedResults in didSet of `results`. - XCTAssertNotNil(model.selectedResult) - try XCTAssertNil(model.suggestions.get()) - } -} - -// Move to new file -class LocatorSearchSourceTests: XCTestCase { - // Modify GeocodeParameters and SuggestParameters from example view - // Pass in custom locator (maybe from MMPK?) - // Set searchArea and/or preferredSearchLocation once (instead of every pan) - // -} - -// Move to new file -class SmartLocatorSearchSourceTests: XCTestCase { - // } extension SearchViewModelTests { diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift new file mode 100644 index 000000000..6c528459c --- /dev/null +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -0,0 +1,136 @@ +// Copyright 2021 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 Foundation + +import XCTest +import ArcGIS +import ArcGISToolkit +import SwiftUI + +class SmartLocatorSearchSourceTests: XCTestCase { +} + +//class SearchViewModelTests: XCTestCase { +// func testAcceptSuggestion() async throws { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// model.currentQuery = "Magers & Quinn Bookseller" +// await model.updateSuggestions() +// let suggestionionResults = try XCTUnwrap(model.suggestions.get()) +// let suggestion = try XCTUnwrap(suggestionionResults.first) +// +// await model.acceptSuggestion(suggestion) +// let results = try XCTUnwrap(model.results.get()) +// XCTAssertEqual(results.count, 1) +// +// // TODO: look into setting model.selectedResults in didSet of `results`. +// XCTAssertNotNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// } +// +// func testCommitSearch() async throws { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// // No search - results are nil. +// try XCTAssertNil(model.results.get()) +// +// // Search with no results - result count is 0. +// model.currentQuery = "No results found blah blah blah blah" +// await model.commitSearch(false) +// var results = try XCTUnwrap(model.results.get()) +// XCTAssertEqual(results.count, 0) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// +// // Search with one result. +// model.currentQuery = "Magers & Quinn Bookseller" +// await model.commitSearch(false) +// results = try XCTUnwrap(model.results.get()) +// XCTAssertEqual(results.count, 1) +// +// // One results automatically populates `selectedResult`. +// XCTAssertNotNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// +// // Search with multiple results. +// model.currentQuery = "Magers & Quinn" +// await model.commitSearch(false) +// results = try XCTUnwrap(model.results.get()) +// XCTAssertGreaterThanOrEqual(results.count, 1) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.suggestions.get()) +// } +// +// func testIsEligibleForRequery() async { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// // isEligibleForRequery defaults to `false`. +// XCTAssertFalse(model.isEligibleForRequery) +// +// // There are no results, so setting `queryArea` has +// // no effect on `isEligibleForRequery`. +// model.queryArea = createPolygon() +// XCTAssertFalse(model.isEligibleForRequery) +// +// // We have results and a new polygon, `isEligibleForRequery` is true. +// model.currentQuery = "Magers & Quinn Bookseller" +// await model.commitSearch(false) +// model.queryArea = createPolygon() +// XCTAssertTrue(model.isEligibleForRequery) +// } +// +// func testUpdateSuggestions() async throws { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// +// // No currentQuery - suggestions are nil. +// try XCTAssertNil(model.suggestions.get()) +// +// // UpdateSuggestions with no results - result count is 0. +// model.currentQuery = "No results found blah blah blah blah" +// await model.updateSuggestions() +// var results = try XCTUnwrap(model.suggestions.get()) +// XCTAssertEqual(results.count, 0) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.results.get()) +// +// // UpdateSuggestions with results. +// model.currentQuery = "Magers & Quinn" +// await model.updateSuggestions() +// results = try XCTUnwrap(model.suggestions.get()) +// XCTAssertGreaterThanOrEqual(results.count, 1) +// +// XCTAssertNil(model.selectedResult) +// try XCTAssertNil(model.results.get()) +// } +// +//} + +extension SearchViewModelTests { + func createPolygon() -> Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: .london) + let _ = builder.add(point: .paris) + let _ = builder.add(point: .rome) + return builder.toGeometry() as! ArcGIS.Polygon + } +} + +extension Point { + static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) + static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) + static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) +} From 5f47a970524513609af0e3aee615ba488cf79731 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 9 Aug 2021 15:54:29 -0500 Subject: [PATCH 0159/1722] Cleanup how selectedResult gets set; some code refactoring. --- Examples/Examples/SearchExampleView.swift | 12 +- .../Components/Search/SearchViewModel.swift | 147 +++++++++--------- 2 files changed, 76 insertions(+), 83 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index ac1e99eb1..ff8acb38c 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -76,7 +76,7 @@ struct SearchExampleView: View { searchViewModel.sources = [smartLocator] } - fileprivate func display(searchResults: Result<[SearchResult]?, SearchError>) { + private func display(searchResults: Result<[SearchResult]?, SearchError>) { switch searchResults { case .success(let results): var resultGraphics = [Graphic]() @@ -105,15 +105,9 @@ struct SearchExampleView: View { } } - fileprivate func display(selectedResult: SearchResult?) { - guard let selectedResult = selectedResult, - let graphic = selectedResult.geoElement as? Graphic else { return } - + private func display(selectedResult: SearchResult?) { + guard let selectedResult = selectedResult else { return } searchResultViewpoint = selectedResult.selectionViewpoint - graphic.updateGraphic(withResult: selectedResult) - - searchResultsOverlay.removeAllGraphics() - searchResultsOverlay.addGraphic(graphic) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index dd3e4fdcd..c5409f1ae 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -65,12 +65,10 @@ public class SearchViewModel: ObservableObject { @Published public var currentQuery: String = "" { didSet { - selectedResult = nil + results = .success(nil) if currentQuery.isEmpty { - results = .success(nil) suggestions = .success(nil) } - isEligibleForRequery = false } } @@ -104,7 +102,23 @@ public class SearchViewModel: ObservableObject { /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. @Published - public private(set) var results: Result<[SearchResult]?, SearchError> = .success(nil) + public private(set) var results: Result<[SearchResult]?, SearchError> = .success(nil) { + didSet { + switch results { + case .success(let results): + if results != nil && results?.count == 1 { + selectedResult = results?.first + } + else { + selectedResult = nil + } + case .failure(_): + selectedResult = nil + } + + isEligibleForRequery = false + } + } /// Tracks selection of results from the `results` collection. When there is only one result, /// that result is automatically assigned to this property. If there are multiple results, the view sets @@ -150,7 +164,8 @@ public class SearchViewModel: ObservableObject { source.searchArea = queryArea source.preferredSearchLocation = queryCenter - selectedResult = nil + + suggestions = .success(nil) currentTask?.cancel() currentTask = commitSearchTask( @@ -168,6 +183,8 @@ public class SearchViewModel: ObservableObject { source.searchArea = queryArea source.preferredSearchLocation = queryCenter + results = .success(nil) + currentTask?.cancel() currentTask = updateSuggestionsTask(source) await currentTask?.value @@ -183,6 +200,8 @@ public class SearchViewModel: ObservableObject { ) async -> Void { currentQuery = searchSuggestion.displayTitle + suggestions = .success(nil) + currentTask?.cancel() currentTask = acceptSuggestionTask(searchSuggestion) await currentTask?.value @@ -208,25 +227,7 @@ extension SearchViewModel { area: restrictToArea ? queryArea : nil ) } - - DispatchQueue.main.sync { [weak self] in - self?.isEligibleForRequery = false - self?.suggestions = .success(nil) - - switch searchResult { - case .success(let searchResults): - self?.results = .success(searchResults) - if searchResults.count == 1 { - self?.selectedResult = searchResults.first - } - case .failure(let error): - self?.results = .failure(SearchError(error)) - break - case .none: - self?.results = .success(nil) - break - } - } + processSearchResults(searchResult) }) return task } @@ -239,19 +240,15 @@ extension SearchViewModel { try await source.suggest(currentQuery) } - DispatchQueue.main.sync { [weak self] in - self?.results = .success(nil) - self?.selectedResult = nil - self?.isEligibleForRequery = false - + DispatchQueue.main.sync { switch suggestResult { case .success(let suggestResults): - self?.suggestions = .success(suggestResults) + suggestions = .success(suggestResults) case .failure(let error): - self?.suggestions = .failure(SearchError(error)) + suggestions = .failure(SearchError(error)) break case .none: - self?.suggestions = .success(nil) + suggestions = .success(nil) break } } @@ -267,55 +264,57 @@ extension SearchViewModel { try await searchSuggestion.owningSource.search(searchSuggestion) } - DispatchQueue.main.sync { [weak self] in - var searchResults = [SearchResult]() - var suggestError: Error? - - self?.suggestions = .success(nil) - self?.isEligibleForRequery = false - self?.selectedResult = nil - - switch searchResult { - case .success(let results): - switch (self?.resultMode) - { - case .single: + processSearchResults( + searchResult, + isCollection: searchSuggestion.suggestResult?.isCollection ?? true + ) + }) + return task + } + + private func processSearchResults( + _ result: Result<[SearchResult], Error>?, + isCollection: Bool = true + ) { + guard let result = result else { + results = .success([]) + return + } + + DispatchQueue.main.sync { + var searchResults = [SearchResult]() + var searchError: Error? + + switch result { + case .success(let results): + switch (resultMode) { + case .single: + if let firstResult = results.first { + searchResults = [firstResult] + } + case .multiple: + searchResults = results + case .automatic: + if isCollection { + searchResults = results + } else { if let firstResult = results.first { searchResults = [firstResult] } - case .multiple: - searchResults = results - case .automatic: - if searchSuggestion.suggestResult?.isCollection ?? true { - searchResults = results - } else { - if let firstResult = results.first { - searchResults = [firstResult] - } - } - case .none: - break - } - case .failure(let error): - suggestError = error - case .none: - break - } - - if let error = suggestError { - self?.results = .failure(SearchError(error)) - } - else { - self?.results = .success(searchResults) - if searchResults.count == 1 { - self?.selectedResult = searchResults.first } } + case .failure(let error): + searchError = error } - }) - return task + + if let error = searchError { + results = .failure(SearchError(error)) + } + else { + results = .success(searchResults) + } + } } - } extension SearchViewModel { From 5989ad7922faff00e98dc82eead89c18b6575bd2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 12 Aug 2021 11:06:52 -0500 Subject: [PATCH 0160/1722] Fixes and code cleanup. --- .../Components/Search/LocatorSearchSource.swift | 7 +------ Sources/ArcGISToolkit/Components/Search/SearchView.swift | 1 - .../Components/Search/SmartLocatorSearchSource.swift | 5 +++-- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index a577b01ef..277251f01 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -86,13 +86,8 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { public func search( _ queryString: String, - area: Geometry? = nil + area: Geometry? ) async throws -> [SearchResult] { - // TODO: think about this... - // This differs from the .NET approach; .NET only uses the - // center of `searchArea` for the `geocodeParameters.preferredSearchLocation` - // and only sets `geocodeParameters.searchArea` from the `area` argument. - // geocodeParameters.searchArea = (area != nil) ? area : searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 1af8f0aa9..68ed59da9 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -84,7 +84,6 @@ public struct SearchView: View { ) } } - // TODO: Not sure how to get the list to constrain itself if there's less than a screen full of rows. Spacer() .task(id: searchViewModel.currentQuery) { // User typed a new character diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 5884724e4..a61bdcb60 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -68,8 +68,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // First, peform super class search. var results = try await super.search(queryString, area: area) if results.count > repeatSearchResultThreshold || - area != nil || - geocodeParameters.searchArea == nil { + repeatSearchResultThreshold == 0 || + (area == nil && geocodeParameters.searchArea == nil) { // Result count meets threshold or there were no geographic // constraints on the search, so return results. return results @@ -139,6 +139,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ) async throws -> [SearchSuggestion] { var results = try await super.suggest(queryString) if results.count > repeatSuggestResultThreshold || + repeatSuggestResultThreshold == 0 || suggestParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. From 684bb7b3981944b3542b1c037e8be48ad48eb325 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 12 Aug 2021 11:07:07 -0500 Subject: [PATCH 0161/1722] More tests. --- .../LocatorSearchSourceTests.swift | 166 +++++------------- .../SearchViewModelTests.swift | 145 ++++++++++++++- .../SmartLocatorSearchSourceTests.swift | 156 +++++----------- 3 files changed, 227 insertions(+), 240 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 665465ffc..939c83269 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -19,127 +19,49 @@ import ArcGISToolkit import SwiftUI class LocatorSearchSourceTests: XCTestCase { - // Modify GeocodeParameters and SuggestParameters from example view - // Pass in custom locator (maybe from MMPK?) - // Set searchArea and/or preferredSearchLocation once (instead of every pan) - // -} - -//class SearchViewModelTests: XCTestCase { -// func testAcceptSuggestion() async throws { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// model.currentQuery = "Magers & Quinn Bookseller" -// await model.updateSuggestions() -// let suggestionionResults = try XCTUnwrap(model.suggestions.get()) -// let suggestion = try XCTUnwrap(suggestionionResults.first) -// -// await model.acceptSuggestion(suggestion) -// let results = try XCTUnwrap(model.results.get()) -// XCTAssertEqual(results.count, 1) -// -// // TODO: look into setting model.selectedResults in didSet of `results`. -// XCTAssertNotNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// } -// -// func testCommitSearch() async throws { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// // No search - results are nil. -// try XCTAssertNil(model.results.get()) -// -// // Search with no results - result count is 0. -// model.currentQuery = "No results found blah blah blah blah" -// await model.commitSearch(false) -// var results = try XCTUnwrap(model.results.get()) -// XCTAssertEqual(results.count, 0) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// -// // Search with one result. -// model.currentQuery = "Magers & Quinn Bookseller" -// await model.commitSearch(false) -// results = try XCTUnwrap(model.results.get()) -// XCTAssertEqual(results.count, 1) -// -// // One results automatically populates `selectedResult`. -// XCTAssertNotNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// -// // Search with multiple results. -// model.currentQuery = "Magers & Quinn" -// await model.commitSearch(false) -// results = try XCTUnwrap(model.results.get()) -// XCTAssertGreaterThanOrEqual(results.count, 1) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// } -// -// func testIsEligibleForRequery() async { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// // isEligibleForRequery defaults to `false`. -// XCTAssertFalse(model.isEligibleForRequery) -// -// // There are no results, so setting `queryArea` has -// // no effect on `isEligibleForRequery`. -// model.queryArea = createPolygon() -// XCTAssertFalse(model.isEligibleForRequery) -// -// // We have results and a new polygon, `isEligibleForRequery` is true. -// model.currentQuery = "Magers & Quinn Bookseller" -// await model.commitSearch(false) -// model.queryArea = createPolygon() -// XCTAssertTrue(model.isEligibleForRequery) -// } -// -// func testUpdateSuggestions() async throws { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// // No currentQuery - suggestions are nil. -// try XCTAssertNil(model.suggestions.get()) -// -// // UpdateSuggestions with no results - result count is 0. -// model.currentQuery = "No results found blah blah blah blah" -// await model.updateSuggestions() -// var results = try XCTUnwrap(model.suggestions.get()) -// XCTAssertEqual(results.count, 0) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.results.get()) -// -// // UpdateSuggestions with results. -// model.currentQuery = "Magers & Quinn" -// await model.updateSuggestions() -// results = try XCTUnwrap(model.suggestions.get()) -// XCTAssertGreaterThanOrEqual(results.count, 1) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.results.get()) -// } -// -//} - -// Move to new file -class SmartLocatorSearchSourceTests: XCTestCase { - // -} - -extension SearchViewModelTests { - func createPolygon() -> Polygon { - let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: .london) - let _ = builder.add(point: .paris) - let _ = builder.add(point: .rome) - return builder.toGeometry() as! ArcGIS.Polygon + func testMaximumResults() async throws { + let locator = LocatorSearchSource() + locator.maximumResults = 4 + XCTAssertEqual(locator.maximumResults, 4) + + var searchResults = try await locator.search("Coffee", area: nil) + var results = try XCTUnwrap(searchResults) + XCTAssertEqual(results.count, 4) + + locator.maximumResults = 12 + searchResults = try await locator.search("Coffee", area: nil) + results = try XCTUnwrap(searchResults) + XCTAssertEqual(results.count, 12) + + // Set property directly on geocode parameters. + locator.geocodeParameters.maxResults = 2 + XCTAssertEqual(Int(locator.geocodeParameters.maxResults), 2) + XCTAssertEqual(locator.maximumResults, 2) + searchResults = try await locator.search("Coffee", area: nil) + results = try XCTUnwrap(searchResults) + XCTAssertEqual(results.count, 2) + } + + func testMaximumSuggestions() async throws { + let locator = LocatorSearchSource() + locator.maximumSuggestions = 4 + XCTAssertEqual(locator.maximumSuggestions, 4) + + var suggestResults = try await locator.suggest("Coffee") + var results = try XCTUnwrap(suggestResults) + XCTAssertEqual(results.count, 4) + + locator.maximumSuggestions = 12 + suggestResults = try await locator.suggest("Coffee") + results = try XCTUnwrap(suggestResults) + XCTAssertEqual(results.count, 12) + + // Set property directly on suggest parameters. + locator.suggestParameters.maxResults = 2 + XCTAssertEqual(Int(locator.suggestParameters.maxResults), 2) + XCTAssertEqual(locator.maximumSuggestions, 2) + suggestResults = try await locator.suggest("Coffee") + results = try XCTUnwrap(suggestResults) + XCTAssertEqual(results.count, 2) } -} - -extension Point { - static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) - static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) - static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) } diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 13d3d1fc2..09a0790ce 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -22,7 +22,7 @@ class SearchViewModelTests: XCTestCase { func testAcceptSuggestion() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - model.currentQuery = "Magers & Quinn Bookseller" + model.currentQuery = "Magers & Quinn Booksellers" await model.updateSuggestions() let suggestionionResults = try XCTUnwrap(model.suggestions.get()) let suggestion = try XCTUnwrap(suggestionionResults.first) @@ -31,8 +31,30 @@ class SearchViewModelTests: XCTestCase { let results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 1) + // With only one results, model should set `selectedResult` property. + XCTAssertEqual(results.first!, model.selectedResult) + try XCTAssertNil(model.suggestions.get()) } + + func testActiveSource() async throws { + let activeSource = LocatorSearchSource() + activeSource.displayName = "Simple Locator" + + let model = SearchViewModel( + activeSource: activeSource, + sources: [LocatorSearchSource()] + ) + + model.currentQuery = "Magers & Quinn Booksellers" + await model.commitSearch(false) + let result = try XCTUnwrap(model.results.get()?.first) + XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) + + await model.updateSuggestions() + let suggestResult = try XCTUnwrap(model.suggestions.get()?.first) + XCTAssertEqual(suggestResult.owningSource.displayName, activeSource.displayName) + } func testCommitSearch() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -50,7 +72,7 @@ class SearchViewModelTests: XCTestCase { try XCTAssertNil(model.suggestions.get()) // Search with one result. - model.currentQuery = "Magers & Quinn Bookseller" + model.currentQuery = "Magers & Quinn Booksellers" await model.commitSearch(false) results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 1) @@ -63,11 +85,32 @@ class SearchViewModelTests: XCTestCase { model.currentQuery = "Magers & Quinn" await model.commitSearch(false) results = try XCTUnwrap(model.results.get()) - XCTAssertGreaterThanOrEqual(results.count, 1) + XCTAssertGreaterThan(results.count, 1) XCTAssertNil(model.selectedResult) try XCTAssertNil(model.suggestions.get()) } + + func testCurrentQuery() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // Empty `currentQuery` should produce nil results value. + model.currentQuery = "" + await model.commitSearch(false) + try XCTAssertNil(model.results.get()) + + // Empty `currentQuery` should produce nil suggestions value. + await model.updateSuggestions() + try XCTAssertNil(model.suggestions.get()) + + model.currentQuery = "Coffee" + await model.commitSearch(false) + try XCTAssertNotNil(model.results.get()) + + // Changing the `currentQuery` should set results to nil. + model.currentQuery = "Coffee in Portland" + try XCTAssertNil(model.results.get()) + } func testIsEligibleForRequery() async { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -81,12 +124,97 @@ class SearchViewModelTests: XCTestCase { XCTAssertFalse(model.isEligibleForRequery) // We have results and a new polygon, `isEligibleForRequery` is true. - model.currentQuery = "Magers & Quinn Bookseller" + model.currentQuery = "Magers & Quinn Booksellers" await model.commitSearch(false) + XCTAssertFalse(model.isEligibleForRequery) + model.queryArea = createPolygon() XCTAssertTrue(model.isEligibleForRequery) } + func testQueryCenter() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // Set queryCenter to Portland + model.queryCenter = .portland + model.currentQuery = "Coffee" + await model.commitSearch(false) + var resultPoint = try XCTUnwrap( + model.results.get()?.first?.geoElement?.geometry as? Point + ) + + var geodeticDistance = try XCTUnwrap ( + GeometryEngine.distanceGeodetic( + point1: .portland, + point2: resultPoint, + distanceUnit: .meters, + azimuthUnit: nil, + curveType: .geodesic + ) + ) + + // First result within 1500m of Portland. + XCTAssertLessThan(geodeticDistance.distance, 1500.0) + + // Set queryCenter to Edinburgh + model.queryCenter = .edinburgh + model.currentQuery = "Restaurants" + await model.commitSearch(false) + resultPoint = try XCTUnwrap( + model.results.get()?.first?.geoElement?.geometry as? Point + ) + + // Web Mercator distance between .edinburgh and first result. + geodeticDistance = try XCTUnwrap ( + GeometryEngine.distanceGeodetic( + point1: .edinburgh, + point2: resultPoint, + distanceUnit: .meters, + azimuthUnit: nil, + curveType: .geodesic + ) + ) + + // First result within 100m of Edinburgh. + XCTAssertLessThan(geodeticDistance.distance, 100) + } + + func testQueryArea() async throws { + // Coming soon, once the implementation details are + // finalized between .Net/Qt/iOS. + } + + func testSearchResultMode() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + XCTAssertEqual(model.resultMode, .automatic) + + model.resultMode = .single + model.currentQuery = "Magers & Quinn" + await model.commitSearch(false) + var results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 1) + + model.resultMode = .multiple + await model.commitSearch(false) + results = try XCTUnwrap(model.results.get()) + XCTAssertGreaterThan(results.count, 1) + + model.currentQuery = "Coffee" + await model.updateSuggestions() + let suggestResults = try XCTUnwrap(model.suggestions.get()) + let collectionSuggestion = try XCTUnwrap(suggestResults.filter { $0.isCollection }.first) + let singleSuggestion = try XCTUnwrap(suggestResults.filter { !$0.isCollection }.first) + + model.resultMode = .automatic + await model.acceptSuggestion(collectionSuggestion) + results = try XCTUnwrap(model.results.get()) + XCTAssertGreaterThan(results.count, 1) + + await model.acceptSuggestion(singleSuggestion) + results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 1) + } + func testUpdateSuggestions() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -99,9 +227,6 @@ class SearchViewModelTests: XCTestCase { var results = try XCTUnwrap(model.suggestions.get()) XCTAssertEqual(results.count, 0) - XCTAssertNil(model.selectedResult) - try XCTAssertNil(model.results.get()) - // UpdateSuggestions with results. model.currentQuery = "Magers & Quinn" await model.updateSuggestions() @@ -111,7 +236,6 @@ class SearchViewModelTests: XCTestCase { XCTAssertNil(model.selectedResult) try XCTAssertNil(model.results.get()) } - } extension SearchViewModelTests { @@ -125,7 +249,10 @@ extension SearchViewModelTests { } extension Point { + static let edinburgh = Point(x: -3.188267, y: 55.953251, spatialReference: .wgs84) + static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) + static let minneapolis = Point(x: -93.25813, y: 44.98665, spatialReference: .wgs84) static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) + static let portland = Point(x: -122.658722, y: 45.512230, spatialReference: .wgs84) static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) - static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) } diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 6c528459c..64581fb25 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -19,118 +19,56 @@ import ArcGISToolkit import SwiftUI class SmartLocatorSearchSourceTests: XCTestCase { -} + func testRepeatSearchResultThreshold() async throws { + let locator = SmartLocatorSearchSource() + locator.searchArea = Envelope.edinburgh -//class SearchViewModelTests: XCTestCase { -// func testAcceptSuggestion() async throws { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// model.currentQuery = "Magers & Quinn Bookseller" -// await model.updateSuggestions() -// let suggestionionResults = try XCTUnwrap(model.suggestions.get()) -// let suggestion = try XCTUnwrap(suggestionionResults.first) -// -// await model.acceptSuggestion(suggestion) -// let results = try XCTUnwrap(model.results.get()) -// XCTAssertEqual(results.count, 1) -// -// // TODO: look into setting model.selectedResults in didSet of `results`. -// XCTAssertNotNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// } -// -// func testCommitSearch() async throws { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// // No search - results are nil. -// try XCTAssertNil(model.results.get()) -// -// // Search with no results - result count is 0. -// model.currentQuery = "No results found blah blah blah blah" -// await model.commitSearch(false) -// var results = try XCTUnwrap(model.results.get()) -// XCTAssertEqual(results.count, 0) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// -// // Search with one result. -// model.currentQuery = "Magers & Quinn Bookseller" -// await model.commitSearch(false) -// results = try XCTUnwrap(model.results.get()) -// XCTAssertEqual(results.count, 1) -// -// // One results automatically populates `selectedResult`. -// XCTAssertNotNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// -// // Search with multiple results. -// model.currentQuery = "Magers & Quinn" -// await model.commitSearch(false) -// results = try XCTUnwrap(model.results.get()) -// XCTAssertGreaterThanOrEqual(results.count, 1) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.suggestions.get()) -// } -// -// func testIsEligibleForRequery() async { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// // isEligibleForRequery defaults to `false`. -// XCTAssertFalse(model.isEligibleForRequery) -// -// // There are no results, so setting `queryArea` has -// // no effect on `isEligibleForRequery`. -// model.queryArea = createPolygon() -// XCTAssertFalse(model.isEligibleForRequery) -// -// // We have results and a new polygon, `isEligibleForRequery` is true. -// model.currentQuery = "Magers & Quinn Bookseller" -// await model.commitSearch(false) -// model.queryArea = createPolygon() -// XCTAssertTrue(model.isEligibleForRequery) -// } -// -// func testUpdateSuggestions() async throws { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// -// // No currentQuery - suggestions are nil. -// try XCTAssertNil(model.suggestions.get()) -// -// // UpdateSuggestions with no results - result count is 0. -// model.currentQuery = "No results found blah blah blah blah" -// await model.updateSuggestions() -// var results = try XCTUnwrap(model.suggestions.get()) -// XCTAssertEqual(results.count, 0) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.results.get()) -// -// // UpdateSuggestions with results. -// model.currentQuery = "Magers & Quinn" -// await model.updateSuggestions() -// results = try XCTUnwrap(model.suggestions.get()) -// XCTAssertGreaterThanOrEqual(results.count, 1) -// -// XCTAssertNil(model.selectedResult) -// try XCTAssertNil(model.results.get()) -// } -// -//} + // Threshold of 0 means no re-query. + locator.repeatSearchResultThreshold = 0 + var searchResults = try await locator.search( + "Dunkin' Donuts", + area: nil + ) + var results = try XCTUnwrap(searchResults) + XCTAssertEqual(results.count, 0) + + // Threshold of 1+ means requery with fewer restrictions + locator.repeatSearchResultThreshold = 1 + searchResults = try await locator.search( + "Dunkin' Donuts", + area: nil + ) + results = try XCTUnwrap(searchResults) + XCTAssertGreaterThanOrEqual(results.count, 1) + } + + func testRepeatSuggestResultThreshold() async throws { + let locator = SmartLocatorSearchSource() + locator.searchArea = Envelope.edinburgh -extension SearchViewModelTests { - func createPolygon() -> Polygon { - let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: .london) - let _ = builder.add(point: .paris) - let _ = builder.add(point: .rome) - return builder.toGeometry() as! ArcGIS.Polygon + // Threshold of 0 means no re-query. + locator.repeatSuggestResultThreshold = 0 + var suggestResults = try await locator.suggest( + "Dunkin' Donuts" + ) + var results = try XCTUnwrap(suggestResults) + XCTAssertEqual(results.count, 0) + + // Threshold of 1 -> requery with fewer restrictions + locator.repeatSuggestResultThreshold = 1 + suggestResults = try await locator.suggest( + "Dunkin' Donuts" + ) + results = try XCTUnwrap(suggestResults) + XCTAssertGreaterThanOrEqual(results.count, 1) } } -extension Point { - static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) - static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) - static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) +extension Envelope { + static let edinburgh = Envelope( + xMin: -365155.60783391213, + yMin: 7536778.456812576, + xMax: -347494.47622280417, + yMax: 7559866.706991681 + ) } From 1bf1b1ffd4a3f1fb60629b4678b48270f35e7da1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 12 Aug 2021 16:27:14 -0500 Subject: [PATCH 0162/1722] Remove queryArea and isEligibleForRequery. --- .../Search/LocatorSearchSource.swift | 25 ++--------- .../Search/SearchSourceProtocol.swift | 9 +--- .../Components/Search/SearchView.swift | 2 +- .../Components/Search/SearchViewModel.swift | 44 +++---------------- .../Search/SmartLocatorSearchSource.swift | 7 ++- .../Utility/EsriBorderViewModifier.swift | 4 +- .../SearchViewModelTests.swift | 40 +++++------------ 7 files changed, 27 insertions(+), 104 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 277251f01..839dc45b7 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -84,11 +84,8 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { /// based on searches. public private(set) var suggestParameters: SuggestParameters = SuggestParameters() - public func search( - _ queryString: String, - area: Geometry? - ) async throws -> [SearchResult] { - geocodeParameters.searchArea = (area != nil) ? area : searchArea + public func search(_ queryString: String) async throws -> [SearchResult] { + geocodeParameters.searchArea = searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.geocode( @@ -105,22 +102,8 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { ) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] } - geocodeParameters.searchArea = nil - geocodeParameters.preferredSearchLocation = nil - if preferredSearchLocation == nil, - let area = searchArea { - if let point = searchArea as? Point { - geocodeParameters.preferredSearchLocation = point - geocodeParameters.searchArea = nil - } - else if !area.extent.isEmpty { - geocodeParameters.preferredSearchLocation = area.extent.center - geocodeParameters.searchArea = nil - } - } - else if preferredSearchLocation != nil { - geocodeParameters.preferredSearchLocation = preferredSearchLocation - } + geocodeParameters.searchArea = searchArea + geocodeParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.geocode( suggestResult: suggestResult, diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift index bffdb8bb7..7cf4c8d47 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift @@ -37,16 +37,11 @@ public protocol SearchSourceProtocol { /// - Returns: The array of suggestions. func suggest(_ queryString: String) async throws -> [SearchSuggestion] - /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the - /// `searchArea` property may be consulted but does not need to be used as a strict limit. + /// Gets search results. /// - Parameters: /// - queryString: Text to be used for query. - /// - area: Area to be used to constrain search results. /// - Returns: Array of `SearchResult`s - func search( - _ queryString: String, - area: Geometry? - ) async throws -> [SearchResult] + func search(_ queryString: String) async throws -> [SearchResult] /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the /// `searchArea` property may be consulted but does not need to be used as a strict limit. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 68ed59da9..e96412264 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -94,7 +94,7 @@ public struct SearchView: View { .task(id: shouldCommitSearch) { if shouldCommitSearch { // User committed changes (hit Enter/Search button) - await searchViewModel.commitSearch(false) + await searchViewModel.commitSearch() shouldCommitSearch.toggle() } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index c5409f1ae..8cacad5eb 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -74,21 +74,7 @@ public class SearchViewModel: ObservableObject { /// The search area to be used for the current query. This property should be updated /// as the user navigates the map/scene, or at minimum before calling `commitSearch`. - public var queryArea: Geometry? { - willSet { - var hasResults = false - switch results { - case .success(let results): - hasResults = results != nil - case .failure(_): - break; - } - - // When `queryArea` changes, the model is eligible for - // requery if there are previous results. - isEligibleForRequery = hasResults - } - } + public var queryArea: Geometry? = nil /// Defines the center for the search. For most use cases, this should be updated by the view /// every time the user navigates the map. @@ -115,8 +101,6 @@ public class SearchViewModel: ObservableObject { case .failure(_): selectedResult = nil } - - isEligibleForRequery = false } } @@ -139,13 +123,6 @@ public class SearchViewModel: ObservableObject { @Published public private(set) var suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) - /// `true` if the `queryArea` has changed since the `results` collection has been set. - /// This property is used by the view to enable 'Repeat search here' functionality. This property is - /// observable, and the view should use it to hide and show the 'repeat search' button. Changes to - /// this property are driven by changes to the `queryArea` property. - @Published - public private(set) var isEligibleForRequery: Bool = false - private var subscriptions = Set() /// The currently executing async task. `currentTask` should be cancelled @@ -154,11 +131,7 @@ public class SearchViewModel: ObservableObject { /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. - /// If `restrictToArea` is true, only results in the query area will be returned. - /// - Parameter restrictToArea: If true, the search is restricted to results within the extent - /// of the `queryArea` property. Behavior when called with `restrictToArea` set to true - /// when the `queryArea` property is null, a line, a point, or an empty geometry is undefined. - public func commitSearch(_ restrictToArea: Bool) async -> Void { + public func commitSearch() async -> Void { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } @@ -168,10 +141,7 @@ public class SearchViewModel: ObservableObject { suggestions = .success(nil) currentTask?.cancel() - currentTask = commitSearchTask( - source, - restrictToArea: restrictToArea - ) + currentTask = commitSearchTask(source) await currentTask?.value } @@ -217,15 +187,11 @@ public class SearchViewModel: ObservableObject { extension SearchViewModel { private func commitSearchTask( - _ source: SearchSourceProtocol, - restrictToArea: Bool + _ source: SearchSourceProtocol ) -> Task<(), Never> { let task = Task(operation: { let searchResult = await Result { - try await source.search( - currentQuery, - area: restrictToArea ? queryArea : nil - ) + try await source.search(currentQuery) } processSearchResults(searchResult) }) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index a61bdcb60..823f48e5c 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -62,14 +62,13 @@ public class SmartLocatorSearchSource: LocatorSearchSource { public var repeatSuggestResultThreshold: Int = 6 public override func search( - _ queryString: String, - area: Geometry? + _ queryString: String ) async throws -> [SearchResult] { // First, peform super class search. - var results = try await super.search(queryString, area: area) + var results = try await super.search(queryString) if results.count > repeatSearchResultThreshold || repeatSearchResultThreshold == 0 || - (area == nil && geocodeParameters.searchArea == nil) { + geocodeParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. return results diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 6e6a8ad53..f69b7d893 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -15,7 +15,7 @@ import SwiftUI struct EsriBorderViewModifier: ViewModifier { var edgeInsets: EdgeInsets - func body(content: Content) -> some View { + public func body(content: Content) -> some View { let roundedRect = RoundedRectangle(cornerRadius: 8) content .padding(edgeInsets) @@ -35,7 +35,7 @@ struct EsriBorderViewModifier: ViewModifier { } extension View { - func esriBorder( + public func esriBorder( edgeInsets: EdgeInsets = EdgeInsets( top: 8, leading: 12, diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 09a0790ce..670b47a90 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -47,7 +47,7 @@ class SearchViewModelTests: XCTestCase { ) model.currentQuery = "Magers & Quinn Booksellers" - await model.commitSearch(false) + await model.commitSearch() let result = try XCTUnwrap(model.results.get()?.first) XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) @@ -64,7 +64,7 @@ class SearchViewModelTests: XCTestCase { // Search with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" - await model.commitSearch(false) + await model.commitSearch() var results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 0) @@ -73,7 +73,7 @@ class SearchViewModelTests: XCTestCase { // Search with one result. model.currentQuery = "Magers & Quinn Booksellers" - await model.commitSearch(false) + await model.commitSearch() results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 1) @@ -83,7 +83,7 @@ class SearchViewModelTests: XCTestCase { // Search with multiple results. model.currentQuery = "Magers & Quinn" - await model.commitSearch(false) + await model.commitSearch() results = try XCTUnwrap(model.results.get()) XCTAssertGreaterThan(results.count, 1) @@ -96,7 +96,7 @@ class SearchViewModelTests: XCTestCase { // Empty `currentQuery` should produce nil results value. model.currentQuery = "" - await model.commitSearch(false) + await model.commitSearch() try XCTAssertNil(model.results.get()) // Empty `currentQuery` should produce nil suggestions value. @@ -104,33 +104,13 @@ class SearchViewModelTests: XCTestCase { try XCTAssertNil(model.suggestions.get()) model.currentQuery = "Coffee" - await model.commitSearch(false) + await model.commitSearch() try XCTAssertNotNil(model.results.get()) // Changing the `currentQuery` should set results to nil. model.currentQuery = "Coffee in Portland" try XCTAssertNil(model.results.get()) } - - func testIsEligibleForRequery() async { - let model = SearchViewModel(sources: [LocatorSearchSource()]) - - // isEligibleForRequery defaults to `false`. - XCTAssertFalse(model.isEligibleForRequery) - - // There are no results, so setting `queryArea` has - // no effect on `isEligibleForRequery`. - model.queryArea = createPolygon() - XCTAssertFalse(model.isEligibleForRequery) - - // We have results and a new polygon, `isEligibleForRequery` is true. - model.currentQuery = "Magers & Quinn Booksellers" - await model.commitSearch(false) - XCTAssertFalse(model.isEligibleForRequery) - - model.queryArea = createPolygon() - XCTAssertTrue(model.isEligibleForRequery) - } func testQueryCenter() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -138,7 +118,7 @@ class SearchViewModelTests: XCTestCase { // Set queryCenter to Portland model.queryCenter = .portland model.currentQuery = "Coffee" - await model.commitSearch(false) + await model.commitSearch() var resultPoint = try XCTUnwrap( model.results.get()?.first?.geoElement?.geometry as? Point ) @@ -159,7 +139,7 @@ class SearchViewModelTests: XCTestCase { // Set queryCenter to Edinburgh model.queryCenter = .edinburgh model.currentQuery = "Restaurants" - await model.commitSearch(false) + await model.commitSearch() resultPoint = try XCTUnwrap( model.results.get()?.first?.geoElement?.geometry as? Point ) @@ -190,12 +170,12 @@ class SearchViewModelTests: XCTestCase { model.resultMode = .single model.currentQuery = "Magers & Quinn" - await model.commitSearch(false) + await model.commitSearch() var results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 1) model.resultMode = .multiple - await model.commitSearch(false) + await model.commitSearch() results = try XCTUnwrap(model.results.get()) XCTAssertGreaterThan(results.count, 1) From 1564e3db85b45a4ec5d53fffee4b811900137ecc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 17 Aug 2021 11:15:51 -0500 Subject: [PATCH 0163/1722] Query Area design updates --- .../ArcGISToolkit/Components/Search/SearchViewModel.swift | 4 ++-- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 6 +++--- .../ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift | 6 ++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 8cacad5eb..f28291220 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -131,11 +131,11 @@ public class SearchViewModel: ObservableObject { /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. - public func commitSearch() async -> Void { + public func commitSearch(_ searchArea: Geometry? = nil) async -> Void { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } - source.searchArea = queryArea + source.searchArea = searchArea != nil ? searchArea : queryArea source.preferredSearchLocation = queryCenter suggestions = .success(nil) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 939c83269..b5ba3600f 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -24,12 +24,12 @@ class LocatorSearchSourceTests: XCTestCase { locator.maximumResults = 4 XCTAssertEqual(locator.maximumResults, 4) - var searchResults = try await locator.search("Coffee", area: nil) + var searchResults = try await locator.search("Coffee") var results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 4) locator.maximumResults = 12 - searchResults = try await locator.search("Coffee", area: nil) + searchResults = try await locator.search("Coffee") results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 12) @@ -37,7 +37,7 @@ class LocatorSearchSourceTests: XCTestCase { locator.geocodeParameters.maxResults = 2 XCTAssertEqual(Int(locator.geocodeParameters.maxResults), 2) XCTAssertEqual(locator.maximumResults, 2) - searchResults = try await locator.search("Coffee", area: nil) + searchResults = try await locator.search("Coffee") results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 2) } diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 64581fb25..41a0dab21 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -26,8 +26,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { // Threshold of 0 means no re-query. locator.repeatSearchResultThreshold = 0 var searchResults = try await locator.search( - "Dunkin' Donuts", - area: nil + "Dunkin' Donuts" ) var results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 0) @@ -35,8 +34,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { // Threshold of 1+ means requery with fewer restrictions locator.repeatSearchResultThreshold = 1 searchResults = try await locator.search( - "Dunkin' Donuts", - area: nil + "Dunkin' Donuts" ) results = try XCTUnwrap(searchResults) XCTAssertGreaterThanOrEqual(results.count, 1) From e227d83b21afa9d868065dc1541446e63dce8e4d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 Aug 2021 10:41:27 -0500 Subject: [PATCH 0164/1722] Move graphical display of search results into SearchView. --- Examples/Examples/SearchExampleView.swift | 83 ++++--------------- .../Components/Search/SearchView.swift | 80 +++++++++++++++++- .../Utility/EsriBorderViewModifier.swift | 1 + .../EsriDeleteTextButtonViewModifier.swift | 1 + .../EsriSearchButtonViewModifier.swift | 1 + .../EsriShowResultsButtonViewModifier.swift | 5 +- Sources/ArcGISToolkit/Utility/PlainList.swift | 2 +- 7 files changed, 103 insertions(+), 70 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index ff8acb38c..0c62b4736 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -17,17 +17,22 @@ import ArcGIS import ArcGISToolkit struct SearchExampleView: View { + /// The `SearchViewModel` used to define behavior of the `SearchView`. @ObservedObject var searchViewModel = SearchViewModel() let map = Map(basemapStyle: .arcGISImagery) + /// The map viewpoint used by the `SearchView` to pan/zoom the map + /// to the extent of the search results. @State var searchResultViewpoint: Viewpoint? = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), scale: 1000000 ) + /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. + @State var searchResultsOverlay = GraphicsOverlay() var body: some View { @@ -46,21 +51,20 @@ struct SearchExampleView: View { // redraw the map with the new viewpoint. searchResultViewpoint = nil } - .onVisibleAreaChanged { - searchViewModel.queryArea = $0 + .onVisibleAreaChanged { newValue in + // Setting `searchViewModel.queryArea` will limit the initial + // results to `queryArea`. + // searchViewModel.queryArea = newValue } - .overlay( - SearchView(searchViewModel: searchViewModel) + .overlay(alignment: .topTrailing) { + SearchView( + searchViewModel: searchViewModel, + viewpoint: $searchResultViewpoint, + resultsOverlay: $searchResultsOverlay + ) .frame(width: 360) - .padding(), - alignment: .topTrailing - ) - .onChange(of: searchViewModel.results, perform: { newValue in - display(searchResults: newValue) - }) - .onChange(of: searchViewModel.selectedResult, perform: { newValue in - display(selectedResult: newValue) - }) + .padding() + } .onAppear() { setupSearchViewModel() } @@ -69,61 +73,10 @@ struct SearchExampleView: View { /// Sets up any desired customization on `searchViewModel`. private func setupSearchViewModel() { let smartLocator = SmartLocatorSearchSource( - displayName: "Locator One", + displayName: "My locator", maximumResults: 16, maximumSuggestions: 16 ) searchViewModel.sources = [smartLocator] } - - private func display(searchResults: Result<[SearchResult]?, SearchError>) { - switch searchResults { - case .success(let results): - var resultGraphics = [Graphic]() - results?.forEach({ result in - if let graphic = result.geoElement as? Graphic { - graphic.updateGraphic(withResult: result) - resultGraphics.append(graphic) - } - }) - searchResultsOverlay.removeAllGraphics() - searchResultsOverlay.addGraphics(resultGraphics) - - if resultGraphics.count > 0, - let envelope = searchResultsOverlay.extent { - let builder = EnvelopeBuilder(envelope: envelope) - builder.expand(factor: 1.1) - searchResultViewpoint = Viewpoint( - targetExtent: builder.toGeometry() - ) - } - else { - searchResultViewpoint = nil - } - case .failure(_): - break - } - } - - private func display(selectedResult: SearchResult?) { - guard let selectedResult = selectedResult else { return } - searchResultViewpoint = selectedResult.selectionViewpoint - } -} - -private extension Symbol { - /// A search result marker symbol. - static let resultSymbol: MarkerSymbol = PictureMarkerSymbol( - image: UIImage(named: "MapPin")! - ) -} - -private extension Graphic { - func updateGraphic(withResult result: SearchResult) { - if symbol == nil { - symbol = .resultSymbol - } - setAttributeValue(result.displayTitle, forKey: "displayTitle") - setAttributeValue(result.displaySubtitle, forKey: "displaySubtitle") - } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e96412264..65a31b7d6 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -17,7 +17,14 @@ import ArcGIS /// SearchView presents a search experience, powered by an underlying SearchViewModel. public struct SearchView: View { - public init(searchViewModel: SearchViewModel? = nil) { + /// Creates a new `SearchView`. + /// - Parameters: + /// - searchViewModel: The view model used by `SearchView`. + /// - viewpoint: The `Viewpoint` used to zoom to results. + /// - resultsOverlay: The `GraphicsOverlay` used to display results. + public init(searchViewModel: SearchViewModel? = nil, + viewpoint: Binding? = nil, + resultsOverlay: Binding? = nil) { if let searchViewModel = searchViewModel { self.searchViewModel = searchViewModel } @@ -26,6 +33,8 @@ public struct SearchView: View { sources: [LocatorSearchSource()] ) } + self.resultsOverlay = resultsOverlay + self.viewpoint = viewpoint } /// The view model used by the view. The `SearchViewModel` manages state and handles the @@ -34,13 +43,21 @@ public struct SearchView: View { @ObservedObject var searchViewModel: SearchViewModel + /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. + private var viewpoint: Binding? = nil + + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. + private var resultsOverlay: Binding? = nil + /// Determines whether a built-in result view will be shown. Defaults to true. /// If false, the result display/selection list is not shown. Set to false if you want to hide the results /// or define a custom result list. You might use a custom result list to show results in a separate list, /// disconnected from the rest of the search view. + /// Note: this is set using the `enableResultListView` modifier. private var enableResultListView = true /// Message to show when there are no results or suggestions. Defaults to "No results found". + /// Note: this is set using the `noResultMessage` modifier. private var noResultMessage = "No results found" /// Indicates that the `SearchViewModel` should start a search. @@ -67,7 +84,7 @@ public struct SearchView: View { .esriDeleteTextButton(text: $searchViewModel.currentQuery) .esriSearchButton(performSearch: $shouldCommitSearch) .esriShowResultsButton( - isEnabled: !enableResultListView, + isEnabled: enableResultListView, isHidden: $isResultDisplayHidden ) .esriBorder() @@ -84,6 +101,12 @@ public struct SearchView: View { ) } } + .onChange(of: searchViewModel.results, perform: { newValue in + display(searchResults: newValue) + }) + .onChange(of: searchViewModel.selectedResult, perform: { newValue in + display(selectedResult: newValue) + }) Spacer() .task(id: searchViewModel.currentQuery) { // User typed a new character @@ -131,6 +154,42 @@ public struct SearchView: View { } } +extension SearchView { + private func display(searchResults: Result<[SearchResult]?, SearchError>) { + switch searchResults { + case .success(let results): + var resultGraphics = [Graphic]() + results?.forEach({ result in + if let graphic = result.geoElement as? Graphic { + graphic.updateGraphic(withResult: result) + resultGraphics.append(graphic) + } + }) + resultsOverlay?.wrappedValue.removeAllGraphics() + resultsOverlay?.wrappedValue.addGraphics(resultGraphics) + + if resultGraphics.count > 0, + let envelope = resultsOverlay?.wrappedValue.extent { + let builder = EnvelopeBuilder(envelope: envelope) + builder.expand(factor: 1.1) + viewpoint?.wrappedValue = Viewpoint( + targetExtent: builder.toGeometry() + ) + } + else { + viewpoint?.wrappedValue = nil + } + case .failure(_): + break + } + } + + private func display(selectedResult: SearchResult?) { + guard let selectedResult = selectedResult else { return } + viewpoint?.wrappedValue = selectedResult.selectionViewpoint + } +} + struct SearchResultList: View { var searchResults: Result<[SearchResult]?, SearchError> @Binding var selectedResult: SearchResult? @@ -249,3 +308,20 @@ struct ResultRow: View { } } } + +private extension Graphic { + func updateGraphic(withResult result: SearchResult) { + if symbol == nil { + symbol = .resultSymbol + } + setAttributeValue(result.displayTitle, forKey: "displayTitle") + setAttributeValue(result.displaySubtitle, forKey: "displaySubtitle") + } +} + +private extension Symbol { + /// A search result marker symbol. + static let resultSymbol: MarkerSymbol = PictureMarkerSymbol( + image: UIImage(named: "MapPin")! + ) +} diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index f69b7d893..d48c36f13 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -13,6 +13,7 @@ import SwiftUI +/// A modifier which displays a 2 pixel width border and a shadow around a view. struct EsriBorderViewModifier: ViewModifier { var edgeInsets: EdgeInsets public func body(content: Content) -> some View { diff --git a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift index 748b661b9..7b68dff9c 100644 --- a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift @@ -13,6 +13,7 @@ import SwiftUI +/// A modifier which adds a "delete text" button in a view, used to delete a given text string. struct EsriDeleteTextButtonViewModifier: ViewModifier { @Binding var text: String diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index af14f4cae..e640b8046 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -13,6 +13,7 @@ import SwiftUI +/// A modifier which adds a "search" button in a view, used to initiate an operation. struct EsriSearchButtonViewModifier: ViewModifier { @Binding var performSearch: Bool diff --git a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift index 73c2be830..bcf5d7728 100644 --- a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift @@ -13,6 +13,7 @@ import SwiftUI +/// A modifier which adds a "show results" button in a view, used to hide/show another view. struct EsriShowResultsButtonViewModifier: ViewModifier { var isEnabled: Bool @Binding var isHidden: Bool @@ -20,14 +21,14 @@ struct EsriShowResultsButtonViewModifier: ViewModifier { func body(content: Content) -> some View { HStack { content - if isEnabled { + if !isEnabled { EmptyView() } else { Button( action: { isHidden.toggle() }, label: { - Image(systemName: isHidden ? "eye.slash.fill" : "eye.fill") + Image(systemName: isHidden ? "eye.fill" : "eye.slash.fill") .foregroundColor(Color(.opaqueSeparator)) } ) diff --git a/Sources/ArcGISToolkit/Utility/PlainList.swift b/Sources/ArcGISToolkit/Utility/PlainList.swift index ff1e1cfec..d6130c029 100644 --- a/Sources/ArcGISToolkit/Utility/PlainList.swift +++ b/Sources/ArcGISToolkit/Utility/PlainList.swift @@ -13,7 +13,7 @@ import SwiftUI -/// A List with the `.plain` list style. +/// A `View` displaying a `List` with the `.plain` style. public struct PlainList : View where Content : View { let content: Content /// Creates a plain list with the given content. From 6d3901cfbd51afe7456e40f2d0385bb6cbde8c33 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 Aug 2021 11:53:18 -0500 Subject: [PATCH 0165/1722] variable rename. --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 65a31b7d6..0a4192292 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -70,7 +70,7 @@ public struct SearchView: View { /// Determines whether the results lists are displayed. @State - private var isResultDisplayHidden: Bool = false + private var isResultListViewHidden: Bool = false public var body: some View { VStack (alignment: .center) { @@ -85,10 +85,10 @@ public struct SearchView: View { .esriSearchButton(performSearch: $shouldCommitSearch) .esriShowResultsButton( isEnabled: enableResultListView, - isHidden: $isResultDisplayHidden + isHidden: $isResultListViewHidden ) .esriBorder() - if enableResultListView, !isResultDisplayHidden { + if enableResultListView, !isResultListViewHidden { SearchResultList( searchResults: searchViewModel.results, selectedResult: $searchViewModel.selectedResult, From 2d1b775c8d7588d3119e9caf218da89dfead5751 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 Aug 2021 16:20:33 -0500 Subject: [PATCH 0166/1722] Add remaining API; get styles to work, including .automatic. --- .../Examples/BasemapGalleryExampleView.swift | 13 +- .../BasemapGallery/BasemapGallery.swift | 232 ++++++++++-------- .../BasemapGallery/BasemapGalleryItem.swift | 10 +- 3 files changed, 147 insertions(+), 108 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 60de0a025..51973ddcd 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -70,9 +70,6 @@ struct BasemapGalleryExampleView: View { let map = Map(basemapStyle: .arcGISNova) - @State - var selectedBasemapGalleryItem: BasemapGalleryItem? - @State var showBasemapGallery: Bool = true // NOTE: Set to false when BasemapGallery is back in the navigation stack. @@ -84,11 +81,6 @@ struct BasemapGalleryExampleView: View { var body: some View { ZStack(alignment: .topTrailing, content: { MapView(map: map, viewpoint: initialViewpoint) - .onChange(of: selectedBasemapGalleryItem) { newValue in - if let selectedItem = selectedBasemapGalleryItem { - map.basemap = selectedItem.basemap - } - } .toolbar { ToolbarItem(placement: .primaryAction) { Button { @@ -100,9 +92,10 @@ struct BasemapGalleryExampleView: View { } if showBasemapGallery { BasemapGallery( - basemapGalleryItems: basemapGalleryItems, - selectedBasemapGalleryItem: $selectedBasemapGalleryItem + geoModel: map, + basemapGalleryItems: basemapGalleryItems ) + .basemapGalleryStyle(.automatic) .frame(width: 300) .padding() } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 96648fb01..455f4fa8f 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -14,130 +14,170 @@ import SwiftUI import ArcGIS -/// `OverviewMap` is a small, secondary `MapView` (sometimes called an "inset map"), superimposed -/// on an existing `GeoView`, which shows the visible extent of that `GeoView`. +/// The `BasemapGallery` tool displays a collection of images representing basemaps from +/// ArcGIS Online, a user-defined portal, or an array of `Basemap`s. +/// When a new basemap is selected from the `BasemapGallery` and the optional +/// `BasemapGallery.geoModel` property is set, then the basemap of the geoModel is replaced +/// with the basemap in the gallery. public struct BasemapGallery: View { + /// The view style of the gallery. + public enum BasemapGalleryStyle { + /// The `BasemapGallery` will display as a grid when there is appropriate + /// 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 + /// The `BasemapGallery` will display as a list. + case list + } + + /// Creates a `BasemapGallery`. Generates a list of appropriate, default basemaps. + /// The given default basemaps require either an API key or named-user to be signed into the app. + /// These basemaps are sourced from this PortalGroup: + /// https://www.arcgis.com/home/group.html?id=a25523e2241d4ff2bcc9182cc971c156). + /// `BasemapmapGallery.currentBasemap` is set to the basemap of the given + /// geoModel if not `nil`. + /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. + public init(geoModel: GeoModel? = nil) { + self.geoModel = geoModel + self.currentBasemap = geoModel?.basemap + } + + /// Creates a `BasemapGallery`. Uses the given `portal` to retrieve basemaps. + /// `BasemapmapGallery.currentBasemap` is set to the basemap of the given + /// geoModel if not `nil`. + /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. + /// - Parameter portal: The `GeoModel` we're selecting the basemap for. public init( - basemapGalleryItems: [BasemapGalleryItem] = [], - selectedBasemapGalleryItem: Binding + geoModel: GeoModel? = nil, + portal: Portal ) { + self.geoModel = geoModel + self.currentBasemap = geoModel?.basemap + self.portal = portal + } + + /// Creates a `BasemapGallery`. Uses the given list of basemap gallery items. + /// `BasemapmapGallery.currentBasemap` is set to the basemap of the given + /// geoModel if not `nil`. + /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. + /// - Parameter basemapGalleryItems: The `GeoModel` we're selecting the basemap for. + public init( + geoModel: GeoModel? = nil, + basemapGalleryItems: [BasemapGalleryItem] = [] + ) { + self.geoModel = geoModel + self.currentBasemap = geoModel?.basemap self.basemapGalleryItems = basemapGalleryItems - self._selectedBasemapGalleryItem = selectedBasemapGalleryItem } + /// If the `GeoModel` is not loaded when passed to the `BasemapGallery`, 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, the geoModel will have its + /// basemap replaced with the selected basemap. + public var geoModel: GeoModel? = nil + + @State + /// Currently applied basemap on the associated `GeoModel`. This may be a basemap + /// which does not exist in the gallery. + public var currentBasemap: Basemap? = nil + + /// The `Portal` object, if set in the constructor of the `BasemapGallery`. + public var portal: Portal? = nil + + /// The list of basemaps currently visible in the gallery. Items added or removed from this list will + /// update the gallery. public var basemapGalleryItems: [BasemapGalleryItem] = [] - @Binding - public var selectedBasemapGalleryItem: BasemapGalleryItem? + /// 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 `automatic`. + /// Set using the `basemapGalleryStyle` modifier. + private var style: BasemapGalleryStyle = .automatic + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + +// @Binding +// public var selectedBasemapGalleryItem: BasemapGalleryItem? public var body: some View { - PlainList { + switch style { + case .automatic: + if horizontalSizeClass == .regular { + GridView() + } + else { + ListView() + } + case .grid: + GridView() + case .list: + ListView() + } + Spacer() + } + + // MARK: Modifiers + + /// The style of the basemap gallery. Defaults to `.automatic`. + /// - Parameter style: The `BasemapGalleryStyle` to use. + /// - Returns: The `BasemapGallery`. + public func basemapGalleryStyle(_ style: BasemapGalleryStyle) -> BasemapGallery { + var copy = self + copy.style = style + return copy + } +} + +extension BasemapGallery { + private func GridView() -> some View { + let columns: [GridItem] = [ + .init(.flexible(), spacing: 8.0, alignment: .top), + .init(.flexible(), spacing: 8.0, alignment: .top), + .init(.flexible(), spacing: 8.0, alignment: .top) + ] + + return ScrollView { + LazyVGrid(columns: columns, spacing: 4) { + ForEach(basemapGalleryItems) { basemapGalleryItem in + BasemapGalleryItemRow(item: basemapGalleryItem) + .onTapGesture { + geoModel?.basemap = basemapGalleryItem.basemap + currentBasemap = basemapGalleryItem.basemap + } + } + } + } + .esriBorder() + } + + private func ListView() -> some View { + return PlainList { ForEach(basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow(item: basemapGalleryItem) .onTapGesture { - selectedBasemapGalleryItem = basemapGalleryItem + geoModel?.basemap = basemapGalleryItem.basemap + currentBasemap = basemapGalleryItem.basemap } } } .esriBorder() } - // MapView( - // map: map, - // viewpoint: makeOverviewViewpoint(), - // graphicsOverlays: [graphicsOverlay] - // ) - // .attributionText(hidden: true) - // .interactionModes([]) - // .border(.black, width: 1) - // .onAppear(perform: { - // graphic.symbol = symbol - // }) - // .onChange(of: visibleArea, perform: { visibleArea in - // if let visibleArea = visibleArea { - // graphic.geometry = visibleArea - // } - // }) - // .onChange(of: viewpoint, perform: { viewpoint in - // if visibleArea == nil, - // let viewpoint = viewpoint, - // let point = viewpoint.targetGeometry as? Point { - // graphic.geometry = point - // } - // }) - // .onChange(of: symbol, perform: { - // graphic.symbol = $0 - // }) - // } - - // MARK: Modifiers - // - // /// The `Map` displayed in the `OverviewMap`. - // /// - Parameter map: The new map. - // /// - Returns: The `OverviewMap`. - // public func map(_ map: Map) -> OverviewMap { - // var copy = self - // copy._map = StateObject(wrappedValue: map) - // return copy - // } - // - // /// 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`. - // /// - Parameter scaleFactor: The new scale factor. - // /// - Returns: The `OverviewMap`. - // public func scaleFactor(_ scaleFactor: Double) -> OverviewMap { - // var copy = self - // copy.scaleFactor = scaleFactor - // return copy - // } - // - // /// The `Symbol` used to display the main `GeoView` visible area. For `MapView`s, the symbol - // /// should be appropriate for visualizing a polygon, as it will be used to draw the visible area. For - // /// `SceneView`s, the symbol should be appropriate for visualizing a point, as it will be used to - // /// draw the current viewpoint's center. For `MapView`s, the default is a transparent - // /// `SimpleFillSymbol` with a red 1 point width outline; for `SceneView`s, the default is a - // /// red, crosshair `SimpleMarkerSymbol`. - // /// - Parameter symbol: The new symbol. - // /// - Returns: The `OverviewMap`. - // public func symbol(_ symbol: Symbol) -> OverviewMap { - // var copy = self - // copy.symbol = symbol - // return copy - // } + } private struct BasemapGalleryItemRow: View { var item: BasemapGalleryItem var body: some View { VStack { - if let thumbnail = item.thumbnail { + if let thumbnailImage = item.thumbnail { // TODO: thumbnail will have to be loaded. - Image(uiImage: thumbnail) + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) } Text(item.name) .font(.footnote) } } } - -// MARK: Extensions - -private extension Symbol { - /// The default marker symbol. - static let defaultMarker: MarkerSymbol = SimpleMarkerSymbol( - style: .cross, - color: .red, - size: 12.0 - ) - - /// The default fill symbol. - static let defaultFill: FillSymbol = SimpleFillSymbol( - style: .solid, - color: .clear, - outline: SimpleLineSymbol( - style: .solid, - color: .red, - width: 1.0 - ) - ) -} diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 37f508051..18fba0b2c 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -16,6 +16,7 @@ import ArcGIS import Foundation +/// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. public struct BasemapGalleryItem { public init( basemap: Basemap, @@ -27,13 +28,18 @@ public struct BasemapGalleryItem { self.name = name self.description = description self.thumbnail = thumbnail -// self.thumbnailURL = thumbnailURL } + /// The basemap this `BasemapGalleryItem` represents. public var basemap: Basemap + + /// The name of this `Basemap`. public var name: String + + /// The description which will be used in the gallery. public var description: String? -// public var thumbnailURL: URL? + + /// The thumbnail which will be displayed in the gallery. public let thumbnail: UIImage? } From febfee829122e76b0a38add5a61d493df1f2f506 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 19 Aug 2021 12:48:09 -0500 Subject: [PATCH 0167/1722] Support grid/list/automatic layouts; more doc and properties. --- .../BasemapGallery/BasemapGallery.swift | 72 +++++++++++++------ 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 455f4fa8f..d0ffc4d07 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -41,6 +41,7 @@ public struct BasemapGallery: View { public init(geoModel: GeoModel? = nil) { self.geoModel = geoModel self.currentBasemap = geoModel?.basemap + self.portal = Portal.arcGISOnline(loginRequired: false) } /// Creates a `BasemapGallery`. Uses the given `portal` to retrieve basemaps. @@ -68,7 +69,7 @@ public struct BasemapGallery: View { ) { self.geoModel = geoModel self.currentBasemap = geoModel?.basemap - self.basemapGalleryItems = basemapGalleryItems + self._basemapGalleryItems = State(wrappedValue: basemapGalleryItems) } /// If the `GeoModel` is not loaded when passed to the `BasemapGallery`, then the @@ -86,19 +87,20 @@ public struct BasemapGallery: View { /// The `Portal` object, if set in the constructor of the `BasemapGallery`. public var portal: Portal? = nil + @State /// The list of basemaps currently visible in the gallery. Items added or removed from this list will /// update the gallery. public var basemapGalleryItems: [BasemapGalleryItem] = [] - + + @State + private var fetchBasemapsResult: Result<[BasemapGalleryItem]?, Error>? = .success([]) + /// 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 `automatic`. /// Set using the `basemapGalleryStyle` modifier. private var style: BasemapGalleryStyle = .automatic @Environment(\.horizontalSizeClass) var horizontalSizeClass - -// @Binding -// public var selectedBasemapGalleryItem: BasemapGalleryItem? public var body: some View { switch style { @@ -115,6 +117,27 @@ public struct BasemapGallery: View { ListView() } Spacer() + .task { + let result = await Result { + try await portal?.fetchDeveloperBasemaps() + } + switch result { + case .success(let basemaps): + let items = basemaps.map { + $0.map { + BasemapGalleryItem(basemap: $0) + } + } + _fetchBasemapsResult = State(wrappedValue: .success(items)) +// _basemapGalleryItems = State(wrappedValue: items) + case .failure(let error): + self?.results = .failure(error) + break + case .none: + self?.results = .success(nil) + break + } + } } // MARK: Modifiers @@ -137,10 +160,25 @@ extension BasemapGallery { .init(.flexible(), spacing: 8.0, alignment: .top) ] + return GalleryView(columns) + } + + private func ListView() -> some View { + let columns: [GridItem] = [ + .init(.flexible(), spacing: 8.0, alignment: .top) + ] + + return GalleryView(columns) + } + + private func GalleryView(_ columns: [GridItem]) -> some View { return ScrollView { LazyVGrid(columns: columns, spacing: 4) { ForEach(basemapGalleryItems) { basemapGalleryItem in - BasemapGalleryItemRow(item: basemapGalleryItem) + BasemapGalleryItemRow( + basemapGalleryItem: basemapGalleryItem, + currentBasemap: currentBasemap + ) .onTapGesture { geoModel?.basemap = basemapGalleryItem.basemap currentBasemap = basemapGalleryItem.basemap @@ -150,33 +188,21 @@ extension BasemapGallery { } .esriBorder() } - - private func ListView() -> some View { - return PlainList { - ForEach(basemapGalleryItems) { basemapGalleryItem in - BasemapGalleryItemRow(item: basemapGalleryItem) - .onTapGesture { - geoModel?.basemap = basemapGalleryItem.basemap - currentBasemap = basemapGalleryItem.basemap - } - } - } - .esriBorder() - } - } private struct BasemapGalleryItemRow: View { - var item: BasemapGalleryItem + var basemapGalleryItem: BasemapGalleryItem + var currentBasemap: Basemap? = nil + var body: some View { VStack { - if let thumbnailImage = item.thumbnail { + if let thumbnailImage = basemapGalleryItem.thumbnail { // TODO: thumbnail will have to be loaded. Image(uiImage: thumbnailImage) .resizable() .aspectRatio(contentMode: .fit) } - Text(item.name) + Text(basemapGalleryItem.name) .font(.footnote) } } From 26f9b21e19a8e486ebc28139cd2d36e3979e1f41 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 24 Aug 2021 15:48:17 -0500 Subject: [PATCH 0168/1722] Use viewmodel --- .../Examples/BasemapGalleryExampleView.swift | 66 ++++++-- .../BasemapGallery/BasemapGallery.swift | 142 +++++------------- .../BasemapGalleryViewModel.swift | 134 +++++++++++++++++ 3 files changed, 224 insertions(+), 118 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 51973ddcd..ab5a22122 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -68,8 +68,20 @@ struct BasemapGalleryExampleView: View { ) ] + let newItem = BasemapGalleryItem( + basemap: Basemap(style: .arcGISMidcentury), + name: "ArcGIS Midcentury 2", + description: "A 2nd vector basemap inspired by the art and advertising of the 1950's that presents a unique design option to the ArcGIS basemaps.", + thumbnail: UIImage(named: "Midcentury") + // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/52d6a28f09704f04b33761ba7c4bf93f/info/thumbnail/thumbnail1607554184831.jpeg")! + ) + + let map = Map(basemapStyle: .arcGISNova) + @ObservedObject + var viewModel = BasemapGalleryViewModel() + @State var showBasemapGallery: Bool = true // NOTE: Set to false when BasemapGallery is back in the navigation stack. @@ -78,28 +90,54 @@ struct BasemapGalleryExampleView: View { scale: 1000000 ) + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + // TODO: figure out of viewmodel stuff is OK + // TODO: get Portal loading and working + + var galleryWidth: CGFloat { + get { + if horizontalSizeClass == .regular { + return 300.0 + } + else { + return 150.0 + } + } + } + var body: some View { - ZStack(alignment: .topTrailing, content: { + ZStack(alignment: .topTrailing) { MapView(map: map, viewpoint: initialViewpoint) - .toolbar { - ToolbarItem(placement: .primaryAction) { + .overlay( + VStack(alignment: .trailing) { Button { showBasemapGallery.toggle() } label: { - Image(systemName: "map.fill") + HStack(alignment: .center) { + Image(systemName: "map.fill") + Text(showBasemapGallery ? "Hide Basemaps" : "Show Basemaps") + } + } + if showBasemapGallery { + BasemapGallery(viewModel: viewModel) + .basemapGalleryStyle(.automatic) + .frame(width: galleryWidth) } } - } - if showBasemapGallery { - BasemapGallery( - geoModel: map, - basemapGalleryItems: basemapGalleryItems + .padding(), + alignment: .topTrailing ) - .basemapGalleryStyle(.automatic) - .frame(width: 300) - .padding() - } - }) + .onAppear() { + SetupViewModel() + } + } + } + + private func SetupViewModel() { + viewModel.geoModel = map + viewModel.basemapGalleryItems = basemapGalleryItems +// viewModel.portal = Portal.arcGISOnline(loginRequired: false) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index d0ffc4d07..36ae09146 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -31,112 +31,29 @@ public struct BasemapGallery: View { case list } - /// Creates a `BasemapGallery`. Generates a list of appropriate, default basemaps. - /// The given default basemaps require either an API key or named-user to be signed into the app. - /// These basemaps are sourced from this PortalGroup: - /// https://www.arcgis.com/home/group.html?id=a25523e2241d4ff2bcc9182cc971c156). - /// `BasemapmapGallery.currentBasemap` is set to the basemap of the given - /// geoModel if not `nil`. - /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. - public init(geoModel: GeoModel? = nil) { - self.geoModel = geoModel - self.currentBasemap = geoModel?.basemap - self.portal = Portal.arcGISOnline(loginRequired: false) - } - - /// Creates a `BasemapGallery`. Uses the given `portal` to retrieve basemaps. - /// `BasemapmapGallery.currentBasemap` is set to the basemap of the given - /// geoModel if not `nil`. - /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. - /// - Parameter portal: The `GeoModel` we're selecting the basemap for. - public init( - geoModel: GeoModel? = nil, - portal: Portal - ) { - self.geoModel = geoModel - self.currentBasemap = geoModel?.basemap - self.portal = portal - } - - /// Creates a `BasemapGallery`. Uses the given list of basemap gallery items. - /// `BasemapmapGallery.currentBasemap` is set to the basemap of the given - /// geoModel if not `nil`. - /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. - /// - Parameter basemapGalleryItems: The `GeoModel` we're selecting the basemap for. - public init( - geoModel: GeoModel? = nil, - basemapGalleryItems: [BasemapGalleryItem] = [] - ) { - self.geoModel = geoModel - self.currentBasemap = geoModel?.basemap - self._basemapGalleryItems = State(wrappedValue: basemapGalleryItems) + public init(viewModel: BasemapGalleryViewModel? = nil) { + if let viewModel = viewModel { + self.viewModel = viewModel + } + else { + self.viewModel = BasemapGalleryViewModel() + } } - /// If the `GeoModel` is not loaded when passed to the `BasemapGallery`, 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, the geoModel will have its - /// basemap replaced with the selected basemap. - public var geoModel: GeoModel? = nil + @ObservedObject + public var viewModel: BasemapGalleryViewModel - @State - /// Currently applied basemap on the associated `GeoModel`. This may be a basemap - /// which does not exist in the gallery. - public var currentBasemap: Basemap? = nil - - /// The `Portal` object, if set in the constructor of the `BasemapGallery`. - public var portal: Portal? = nil - - @State - /// The list of basemaps currently visible in the gallery. Items added or removed from this list will - /// update the gallery. - public var basemapGalleryItems: [BasemapGalleryItem] = [] - - @State - private var fetchBasemapsResult: Result<[BasemapGalleryItem]?, Error>? = .success([]) - /// 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 `automatic`. /// Set using the `basemapGalleryStyle` modifier. private var style: BasemapGalleryStyle = .automatic @Environment(\.horizontalSizeClass) var horizontalSizeClass - + public var body: some View { - switch style { - case .automatic: - if horizontalSizeClass == .regular { - GridView() - } - else { - ListView() - } - case .grid: - GridView() - case .list: - ListView() - } - Spacer() + GalleryView() .task { - let result = await Result { - try await portal?.fetchDeveloperBasemaps() - } - switch result { - case .success(let basemaps): - let items = basemaps.map { - $0.map { - BasemapGalleryItem(basemap: $0) - } - } - _fetchBasemapsResult = State(wrappedValue: .success(items)) -// _basemapGalleryItems = State(wrappedValue: items) - case .failure(let error): - self?.results = .failure(error) - break - case .none: - self?.results = .success(nil) - break - } + await viewModel.fetchBasemaps() } } @@ -153,6 +70,23 @@ public struct BasemapGallery: View { } extension BasemapGallery { + @ViewBuilder + private func GalleryView() -> some View { + switch style { + case .automatic: + if horizontalSizeClass == .regular { + GridView() + } + else { + ListView() + } + case .grid: + GridView() + case .list: + ListView() + } + } + private func GridView() -> some View { let columns: [GridItem] = [ .init(.flexible(), spacing: 8.0, alignment: .top), @@ -160,7 +94,7 @@ extension BasemapGallery { .init(.flexible(), spacing: 8.0, alignment: .top) ] - return GalleryView(columns) + return InternalGalleryView(columns) } private func ListView() -> some View { @@ -168,20 +102,18 @@ extension BasemapGallery { .init(.flexible(), spacing: 8.0, alignment: .top) ] - return GalleryView(columns) + return InternalGalleryView(columns) } - private func GalleryView(_ columns: [GridItem]) -> some View { + private func InternalGalleryView(_ columns: [GridItem]) -> some View { return ScrollView { LazyVGrid(columns: columns, spacing: 4) { - ForEach(basemapGalleryItems) { basemapGalleryItem in + ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow( - basemapGalleryItem: basemapGalleryItem, - currentBasemap: currentBasemap + basemapGalleryItem: basemapGalleryItem ) .onTapGesture { - geoModel?.basemap = basemapGalleryItem.basemap - currentBasemap = basemapGalleryItem.basemap + viewModel.geoModel?.basemap = basemapGalleryItem.basemap } } } @@ -192,7 +124,6 @@ extension BasemapGallery { private struct BasemapGalleryItemRow: View { var basemapGalleryItem: BasemapGalleryItem - var currentBasemap: Basemap? = nil var body: some View { VStack { @@ -205,5 +136,8 @@ private struct BasemapGalleryItemRow: View { Text(basemapGalleryItem.name) .font(.footnote) } +// if basemapGalleryItem == currentBasemap { +// .background(Color.accentColor) +// } } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift new file mode 100644 index 000000000..3215f7b85 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -0,0 +1,134 @@ +// Copyright 2021 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 Swift +import SwiftUI +import ArcGIS +import Combine + +/// Manages the state for a `BasemapGallery`. +public class BasemapGalleryViewModel: ObservableObject { + public init( + geoModel: GeoModel? = nil, + portal: Portal? = nil, + basemapGalleryItems: [BasemapGalleryItem] = [] + ) { + self.geoModel = geoModel + self.portal = portal + self.basemapGalleryItems = basemapGalleryItems + } + +// +// /// Creates a `BasemapGalleryViewModel`. Generates a list of appropriate, default basemaps. +// /// The given default basemaps require either an API key or named-user to be signed into the app. +// /// These basemaps are sourced from this PortalGroup: +// /// https://www.arcgis.com/home/group.html?id=a25523e2241d4ff2bcc9182cc971c156). +// /// `BasemapGalleryViewModel.currentBasemap` is set to the basemap of the given +// /// geoModel if not `nil`. +// /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. +// public init(geoModel: GeoModel? = nil) { +// self.geoModel = geoModel +// self.currentBasemap = geoModel?.basemap +// self.portal = Portal.arcGISOnline(loginRequired: false) +// } +// +// /// Creates a `BasemapGalleryViewModel`. Uses the given `portal` to retrieve basemaps. +// /// `BasemapGalleryViewModel.currentBasemap` is set to the basemap of the given +// /// geoModel if not `nil`. +// /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. +// /// - Parameter portal: The `GeoModel` we're selecting the basemap for. +// public init( +// geoModel: GeoModel? = nil, +// portal: Portal +// ) { +// self.geoModel = geoModel +// self.currentBasemap = geoModel?.basemap +// self.portal = portal +// } +// +// /// Creates a `BasemapGalleryViewModel`. Uses the given list of basemap gallery items. +// /// `BasemapGalleryViewModel.currentBasemap` is set to the basemap of the given +// /// geoModel if not `nil`. +// /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. +// /// - Parameter basemapGalleryItems: The `GeoModel` we're selecting the basemap for. +// public init( +// geoModel: GeoModel? = nil, +// basemapGalleryItems: [BasemapGalleryItem] = [] +// ) { +// self.geoModel = geoModel +// self.currentBasemap = geoModel?.basemap +// self.basemapGalleryItems = basemapGalleryItems +// } + + /// 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, + /// the geoModel will have its basemap replaced with the selected basemap. + public var geoModel: GeoModel? = nil + + /// The `Portal` object, if any. + public var portal: Portal? = nil + + @Published + /// The list of basemaps currently visible in the gallery. Items added or removed from this list will + /// update the gallery. + public var basemapGalleryItems: [BasemapGalleryItem] = [] + + /// Currently applied basemap on the associated `GeoModel`. This may be a basemap + /// which does not exist in the gallery. + public var currentBasemap: Basemap? { + get async throws { + guard let geoModel = geoModel else { return nil } + try await geoModel.load() + return geoModel.basemap + } + } + + /// The currently executing async task. `currentTask` should be cancelled + /// prior to starting another async task. + private var currentTask: Task? = nil + + /// Updates suggestions list asynchronously. + public func fetchBasemaps() async -> Void { + guard let portal = portal else { return } + + currentTask?.cancel() + currentTask = fetchBasemapsTask(portal) + await currentTask?.value + } +} + +extension BasemapGalleryViewModel { + private func fetchBasemapsTask(_ portal: Portal) -> Task<(), Never> { + let task = Task(operation: { + let basemapResults = await Result { + try await portal.fetchDeveloperBasemaps() + } + + DispatchQueue.main.async { [weak self] in + switch basemapResults { + case .success(let basemaps): + self?.basemapGalleryItems = basemaps.map { BasemapGalleryItem(basemap: $0) } + case .failure(_): + self?.basemapGalleryItems = [] + case .none: + self?.basemapGalleryItems = [] + } + } + }) + return task + } +} + +extension BasemapGalleryViewModel { +} From 74eb39beae6d45c29b686dbb5ddf6599aa617562 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 25 Aug 2021 16:01:22 -0500 Subject: [PATCH 0169/1722] QueryArea test; use Int32 to match GeocodeParameters. --- .../Search/LocatorSearchSource.swift | 16 +-- .../Search/SearchSourceProtocol.swift | 4 +- .../Search/SmartLocatorSearchSource.swift | 10 +- .../SearchViewModelTests.swift | 97 +++++++++++++------ 4 files changed, 83 insertions(+), 44 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 839dc45b7..c9b89f51a 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -31,8 +31,8 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" )! ), - maximumResults: Int = 6, - maximumSuggestions: Int = 6, + maximumResults: Int32 = 6, + maximumSuggestions: Int32 = 6, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil ) { @@ -48,22 +48,22 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { public var displayName: String /// The maximum results to return when performing a search. Most sources default to 6 - public var maximumResults: Int { + public var maximumResults: Int32 { get { - Int(geocodeParameters.maxResults) + geocodeParameters.maxResults } set { - geocodeParameters.maxResults = Int32(newValue) + geocodeParameters.maxResults = newValue } } /// The maximum suggestions to return. Most sources default to 6. - public var maximumSuggestions: Int { + public var maximumSuggestions: Int32 { get { - Int(suggestParameters.maxResults) + suggestParameters.maxResults } set { - suggestParameters.maxResults = Int32(newValue) + suggestParameters.maxResults = newValue } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift index 7cf4c8d47..ab06b18e4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift @@ -20,10 +20,10 @@ public protocol SearchSourceProtocol { var displayName: String { get set } /// The maximum results to return when performing a search. Most sources default to 6. - var maximumResults: Int { get set } + var maximumResults: Int32 { get set } /// The maximum suggestions to return. Most sources default to 6. - var maximumSuggestions: Int { get set } + var maximumSuggestions: Int32 { get set } /// Area to be used as a constraint for searches and suggestions. var searchArea: Geometry? { get set } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 823f48e5c..507426e45 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -30,8 +30,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. public init( displayName: String = "Smart Locator", - maximumResults: Int = 6, - maximumSuggestions: Int = 6, + maximumResults: Int32 = 6, + maximumSuggestions: Int32 = 6, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil, repeatSearchResultThreshold: Int = 1, @@ -90,7 +90,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Limit results to `maximumResults`. if allResults.count > maximumResults { - let dropCount = allResults.count - maximumResults + let dropCount = allResults.count - Int(maximumResults) allResults = allResults.dropLast(dropCount) } return allResults @@ -127,7 +127,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Limit results to `maximumResults`. if allResults.count > maximumResults { - let dropCount = allResults.count - maximumResults + let dropCount = allResults.count - Int(maximumResults) allResults = allResults.dropLast(dropCount) } return allResults @@ -161,7 +161,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Limit results to `maximumResults`. if allResults.count > maximumSuggestions { - let dropCount = allResults.count - maximumSuggestions + let dropCount = allResults.count - Int(maximumSuggestions) allResults = allResults.dropLast(dropCount) } return allResults diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 670b47a90..8f9220c83 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -21,7 +21,7 @@ import SwiftUI class SearchViewModelTests: XCTestCase { func testAcceptSuggestion() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - + model.currentQuery = "Magers & Quinn Booksellers" await model.updateSuggestions() let suggestionionResults = try XCTUnwrap(model.suggestions.get()) @@ -50,24 +50,24 @@ class SearchViewModelTests: XCTestCase { await model.commitSearch() let result = try XCTUnwrap(model.results.get()?.first) XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) - + await model.updateSuggestions() let suggestResult = try XCTUnwrap(model.suggestions.get()?.first) XCTAssertEqual(suggestResult.owningSource.displayName, activeSource.displayName) } - + func testCommitSearch() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) // No search - results are nil. try XCTAssertNil(model.results.get()) - + // Search with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" await model.commitSearch() var results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 0) - + XCTAssertNil(model.selectedResult) try XCTAssertNil(model.suggestions.get()) @@ -76,7 +76,7 @@ class SearchViewModelTests: XCTestCase { await model.commitSearch() results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 1) - + // One results automatically populates `selectedResult`. XCTAssertNotNil(model.selectedResult) try XCTAssertNil(model.suggestions.get()) @@ -86,19 +86,19 @@ class SearchViewModelTests: XCTestCase { await model.commitSearch() results = try XCTUnwrap(model.results.get()) XCTAssertGreaterThan(results.count, 1) - + XCTAssertNil(model.selectedResult) try XCTAssertNil(model.suggestions.get()) } func testCurrentQuery() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - + // Empty `currentQuery` should produce nil results value. model.currentQuery = "" await model.commitSearch() try XCTAssertNil(model.results.get()) - + // Empty `currentQuery` should produce nil suggestions value. await model.updateSuggestions() try XCTAssertNil(model.suggestions.get()) @@ -106,7 +106,7 @@ class SearchViewModelTests: XCTestCase { model.currentQuery = "Coffee" await model.commitSearch() try XCTAssertNotNil(model.results.get()) - + // Changing the `currentQuery` should set results to nil. model.currentQuery = "Coffee in Portland" try XCTAssertNil(model.results.get()) @@ -114,7 +114,7 @@ class SearchViewModelTests: XCTestCase { func testQueryCenter() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - + // Set queryCenter to Portland model.queryCenter = .portland model.currentQuery = "Coffee" @@ -135,7 +135,7 @@ class SearchViewModelTests: XCTestCase { // First result within 1500m of Portland. XCTAssertLessThan(geodeticDistance.distance, 1500.0) - + // Set queryCenter to Edinburgh model.queryCenter = .edinburgh model.currentQuery = "Restaurants" @@ -154,20 +154,52 @@ class SearchViewModelTests: XCTestCase { curveType: .geodesic ) ) - + // First result within 100m of Edinburgh. XCTAssertLessThan(geodeticDistance.distance, 100) } func testQueryArea() async throws { - // Coming soon, once the implementation details are - // finalized between .Net/Qt/iOS. - } + let source = LocatorSearchSource() + source.maximumResults = Int32.max + let model = SearchViewModel(sources: [source]) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.currentQuery = "Coffee" + await model.commitSearch() + + var results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 9) + + let resultGeometryUnion: Geometry = try XCTUnwrap( + GeometryEngine.union( + geometries: results.compactMap{ $0.geoElement?.geometry } + ) + ) + + XCTAssertTrue( + GeometryEngine.contains( + geometry1: model.queryArea!, + geometry2: resultGeometryUnion + ) + ) + + model.currentQuery = "Magers & Quinn Booksellers" + await model.commitSearch() + results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 0) + model.queryArea = Polygon.minneapolis + await model.commitSearch() + results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 1) + } + func testSearchResultMode() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) XCTAssertEqual(model.resultMode, .automatic) - + model.resultMode = .single model.currentQuery = "Magers & Quinn" await model.commitSearch() @@ -184,17 +216,17 @@ class SearchViewModelTests: XCTestCase { let suggestResults = try XCTUnwrap(model.suggestions.get()) let collectionSuggestion = try XCTUnwrap(suggestResults.filter { $0.isCollection }.first) let singleSuggestion = try XCTUnwrap(suggestResults.filter { !$0.isCollection }.first) - + model.resultMode = .automatic await model.acceptSuggestion(collectionSuggestion) results = try XCTUnwrap(model.results.get()) XCTAssertGreaterThan(results.count, 1) - + await model.acceptSuggestion(singleSuggestion) results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 1) } - + func testUpdateSuggestions() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -212,27 +244,34 @@ class SearchViewModelTests: XCTestCase { await model.updateSuggestions() results = try XCTUnwrap(model.suggestions.get()) XCTAssertGreaterThanOrEqual(results.count, 1) - + XCTAssertNil(model.selectedResult) try XCTAssertNil(model.results.get()) } } -extension SearchViewModelTests { - func createPolygon() -> Polygon { +extension Polygon { + static var chippewaFalls: Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) + return builder.toGeometry() as! ArcGIS.Polygon + } + + static var minneapolis: Polygon { let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: .london) - let _ = builder.add(point: .paris) - let _ = builder.add(point: .rome) + let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) return builder.toGeometry() as! ArcGIS.Polygon } } extension Point { static let edinburgh = Point(x: -3.188267, y: 55.953251, spatialReference: .wgs84) - static let london = Point(x: -0.1278, y: 51.5074, spatialReference: .wgs84) static let minneapolis = Point(x: -93.25813, y: 44.98665, spatialReference: .wgs84) - static let paris = Point(x: 2.23522, y: 48.8566, spatialReference: .wgs84) static let portland = Point(x: -122.658722, y: 45.512230, spatialReference: .wgs84) - static let rome = Point(x: 12.4964, y: 41.9028, spatialReference: .wgs84) } From 9a973210635cd60b2355072a6d2280d5146d2454 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 25 Aug 2021 17:25:55 -0500 Subject: [PATCH 0170/1722] Fix indenting. --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 2 +- Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 8f9220c83..75e282d51 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -189,7 +189,7 @@ class SearchViewModelTests: XCTestCase { await model.commitSearch() results = try XCTUnwrap(model.results.get()) XCTAssertEqual(results.count, 0) - + model.queryArea = Polygon.minneapolis await model.commitSearch() results = try XCTUnwrap(model.results.get()) diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 41a0dab21..0246d58c8 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -22,7 +22,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { func testRepeatSearchResultThreshold() async throws { let locator = SmartLocatorSearchSource() locator.searchArea = Envelope.edinburgh - + // Threshold of 0 means no re-query. locator.repeatSearchResultThreshold = 0 var searchResults = try await locator.search( @@ -43,7 +43,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { func testRepeatSuggestResultThreshold() async throws { let locator = SmartLocatorSearchSource() locator.searchArea = Envelope.edinburgh - + // Threshold of 0 means no re-query. locator.repeatSuggestResultThreshold = 0 var suggestResults = try await locator.suggest( From e9955a93f3fa73daf15df1e9ee303a619db31e83 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 10 Sep 2021 09:48:17 -0500 Subject: [PATCH 0171/1722] Test cleanup; thoughts on better concurrency in view model. --- .../Components/Search/SearchView.swift | 1 + .../Components/Search/SearchViewModel.swift | 4 + .../Extensions/GeocodeResult.swift | 4 - .../SearchViewModelTests.swift | 118 +++++++++--------- 4 files changed, 67 insertions(+), 60 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 0a4192292..03be8aa97 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -108,6 +108,7 @@ public struct SearchView: View { display(selectedResult: newValue) }) Spacer() + // TODO: Look at debouncing currentQuery to 1/4 second or so. .task(id: searchViewModel.currentQuery) { // User typed a new character if currentSuggestion == nil { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index f28291220..689e73778 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -131,6 +131,8 @@ public class SearchViewModel: ObservableObject { /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. + /// - Parameter searchArea: geometry used to constrain the results. If `nil`, the + /// `queryArea` property is used instead. If `queryArea` is `nil`, results are not constrained. public func commitSearch(_ searchArea: Geometry? = nil) async -> Void { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } @@ -146,6 +148,7 @@ public class SearchViewModel: ObservableObject { } /// Updates suggestions list asynchronously. + @MainActor // TODO: ???? yes or no or a better idea? Maybe model is an Actor and not a class public func updateSuggestions() async -> Void { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, var source = currentSource() else { return } @@ -155,6 +158,7 @@ public class SearchViewModel: ObservableObject { results = .success(nil) + // TODO: Not thread safe... (currentTask) currentTask?.cancel() currentTask = updateSuggestionsTask(source) await currentTask?.value diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift index 4180f0f87..6a069c4db 100644 --- a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift +++ b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift @@ -37,7 +37,3 @@ extension GeocodeResult { ) } } - -extension GeocodeResult: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(self) } -} diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 75e282d51..24014815e 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -78,7 +78,7 @@ class SearchViewModelTests: XCTestCase { XCTAssertEqual(results.count, 1) // One results automatically populates `selectedResult`. - XCTAssertNotNil(model.selectedResult) + XCTAssertEqual(results.first!, model.selectedResult) try XCTAssertNil(model.suggestions.get()) // Search with multiple results. @@ -110,8 +110,52 @@ class SearchViewModelTests: XCTestCase { // Changing the `currentQuery` should set results to nil. model.currentQuery = "Coffee in Portland" try XCTAssertNil(model.results.get()) + + await model.updateSuggestions() + try XCTAssertNotNil(model.suggestions.get()) + + // Changing the `currentQuery` should set results to nil. + model.currentQuery = "Coffee in Edinburgh" + try XCTAssertNil(model.suggestions.get()) } + func testQueryArea() async throws { + let source = LocatorSearchSource() + source.maximumResults = Int32.max + let model = SearchViewModel(sources: [source]) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.currentQuery = "Coffee" + await model.commitSearch() + + var results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 9) + + let resultGeometryUnion: Geometry = try XCTUnwrap( + GeometryEngine.union( + geometries: results.compactMap{ $0.geoElement?.geometry } + ) + ) + + XCTAssertTrue( + GeometryEngine.contains( + geometry1: model.queryArea!, + geometry2: resultGeometryUnion + ) + ) + + model.currentQuery = "Magers & Quinn Booksellers" + await model.commitSearch() + results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 0) + + model.queryArea = Polygon.minneapolis + await model.commitSearch() + results = try XCTUnwrap(model.results.get()) + XCTAssertEqual(results.count, 1) + } + func testQueryCenter() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -159,43 +203,6 @@ class SearchViewModelTests: XCTestCase { XCTAssertLessThan(geodeticDistance.distance, 100) } - func testQueryArea() async throws { - let source = LocatorSearchSource() - source.maximumResults = Int32.max - let model = SearchViewModel(sources: [source]) - - // Set queryArea to Chippewa Falls - model.queryArea = Polygon.chippewaFalls - model.currentQuery = "Coffee" - await model.commitSearch() - - var results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 9) - - let resultGeometryUnion: Geometry = try XCTUnwrap( - GeometryEngine.union( - geometries: results.compactMap{ $0.geoElement?.geometry } - ) - ) - - XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.queryArea!, - geometry2: resultGeometryUnion - ) - ) - - model.currentQuery = "Magers & Quinn Booksellers" - await model.commitSearch() - results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 0) - - model.queryArea = Polygon.minneapolis - await model.commitSearch() - results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 1) - } - func testSearchResultMode() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) XCTAssertEqual(model.resultMode, .automatic) @@ -251,27 +258,26 @@ class SearchViewModelTests: XCTestCase { } extension Polygon { - static var chippewaFalls: Polygon { - let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) - return builder.toGeometry() as! ArcGIS.Polygon - } - - static var minneapolis: Polygon { - let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - return builder.toGeometry() as! ArcGIS.Polygon - } +static var chippewaFalls: Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) + return builder.toGeometry() as! ArcGIS.Polygon +} + +static var minneapolis: Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) + return builder.toGeometry() as! ArcGIS.Polygon +} } extension Point { static let edinburgh = Point(x: -3.188267, y: 55.953251, spatialReference: .wgs84) - static let minneapolis = Point(x: -93.25813, y: 44.98665, spatialReference: .wgs84) static let portland = Point(x: -122.658722, y: 45.512230, spatialReference: .wgs84) } From 5bd0f38237780e1e08e2cfcc00f02f9915a7ce2e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 10 Sep 2021 17:02:14 -0500 Subject: [PATCH 0172/1722] Get all attributes from geocoder. --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index c9b89f51a..1aaa2c8aa 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -42,6 +42,8 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { self.maximumSuggestions = maximumSuggestions self.searchArea = searchArea self.preferredSearchLocation = preferredSearchLocation + + self.geocodeParameters.addResultAttributeName("*") } /// Name to show when presenting this source in the UI. From 0d12fe73988d2d456b5a2455e988e610ab70b268 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 10 Sep 2021 17:02:54 -0500 Subject: [PATCH 0173/1722] First pass at updating concurrency/Tasks. --- .../Components/Search/SearchView.swift | 103 ++++----- .../Components/Search/SearchViewModel.swift | 212 +++++++++--------- .../EsriSearchButtonViewModifier.swift | 8 +- 3 files changed, 150 insertions(+), 173 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 03be8aa97..fdbe99aeb 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -60,13 +60,10 @@ public struct SearchView: View { /// Note: this is set using the `noResultMessage` modifier. private var noResultMessage = "No results found" - /// Indicates that the `SearchViewModel` should start a search. - @State - private var shouldCommitSearch = false - /// The current suggestion selected by the user. - @State - private var currentSuggestion: SearchSuggestion? +// /// The current suggestion selected by the user. +// @State +// private var currentSuggestion: SearchSuggestion? /// Determines whether the results lists are displayed. @State @@ -79,56 +76,43 @@ public struct SearchView: View { text: $searchViewModel.currentQuery ) { _ in } onCommit: { - shouldCommitSearch = true + searchViewModel.commitSearch() } .esriDeleteTextButton(text: $searchViewModel.currentQuery) - .esriSearchButton(performSearch: $shouldCommitSearch) + .esriSearchButton { searchViewModel.commitSearch() } .esriShowResultsButton( isEnabled: enableResultListView, isHidden: $isResultListViewHidden ) .esriBorder() if enableResultListView, !isResultListViewHidden { - SearchResultList( - searchResults: searchViewModel.results, - selectedResult: $searchViewModel.selectedResult, - noResultMessage: noResultMessage - ) - SearchSuggestionList( - suggestionResults: searchViewModel.suggestions, - currentSuggestion: $currentSuggestion, - noResultMessage: noResultMessage - ) - } - } - .onChange(of: searchViewModel.results, perform: { newValue in - display(searchResults: newValue) - }) - .onChange(of: searchViewModel.selectedResult, perform: { newValue in - display(selectedResult: newValue) - }) - Spacer() - // TODO: Look at debouncing currentQuery to 1/4 second or so. - .task(id: searchViewModel.currentQuery) { - // User typed a new character - if currentSuggestion == nil { - await searchViewModel.updateSuggestions() - } - } - .task(id: shouldCommitSearch) { - if shouldCommitSearch { - // User committed changes (hit Enter/Search button) - await searchViewModel.commitSearch() - shouldCommitSearch.toggle() + if let results = searchViewModel.results { + SearchResultList( + searchResults: results, + selectedResult: $searchViewModel.selectedResult, + noResultMessage: noResultMessage + ) } - } - .task(id: currentSuggestion) { - if let suggestion = currentSuggestion { - // User selected a suggestion. - await searchViewModel.acceptSuggestion(suggestion) - currentSuggestion = nil + if let suggestions = searchViewModel.suggestions { + SearchSuggestionList( + suggestionResults: suggestions, + currentSuggestion: $searchViewModel.currentSuggestion, + noResultMessage: noResultMessage + ) } } + } + .onChange(of: searchViewModel.results) { + display(searchResults: $0) + } + .onChange(of: searchViewModel.selectedResult) { + display(selectedResult: $0) + } + .onReceive(searchViewModel.$currentQuery/*.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)*/) { _ in + searchViewModel.updateSuggestions() + } + + Spacer() } // MARK: Modifiers @@ -139,7 +123,7 @@ public struct SearchView: View { /// Defaults to `true`. /// - Parameter enableResultListView: The new value. /// - Returns: The `SearchView`. - public func enableResultListView(_ enableResultListView: Bool) -> SearchView { + public func enableResultListView(_ enableResultListView: Bool) -> Self { var copy = self copy.enableResultListView = enableResultListView return copy @@ -148,7 +132,7 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. Defaults to "No results found". /// - Parameter noResultMessage: The new value. /// - Returns: The `SearchView`. - public func noResultMessage(_ noResultMessage: String) -> SearchView { + public func noResultMessage(_ noResultMessage: String) -> Self { var copy = self copy.noResultMessage = noResultMessage return copy @@ -156,11 +140,11 @@ public struct SearchView: View { } extension SearchView { - private func display(searchResults: Result<[SearchResult]?, SearchError>) { + private func display(searchResults: Result<[SearchResult], SearchError>?) { switch searchResults { case .success(let results): var resultGraphics = [Graphic]() - results?.forEach({ result in + results.forEach({ result in if let graphic = result.geoElement as? Graphic { graphic.updateGraphic(withResult: result) resultGraphics.append(graphic) @@ -180,8 +164,9 @@ extension SearchView { else { viewpoint?.wrappedValue = nil } - case .failure(_): - break + default: + resultsOverlay?.wrappedValue.removeAllGraphics() + viewpoint?.wrappedValue = nil } } @@ -192,7 +177,7 @@ extension SearchView { } struct SearchResultList: View { - var searchResults: Result<[SearchResult]?, SearchError> + var searchResults: Result<[SearchResult], SearchError> @Binding var selectedResult: SearchResult? var noResultMessage: String @@ -200,8 +185,7 @@ struct SearchResultList: View { Group { switch searchResults { case .success(let results): - if let results = results, results.count > 0 { - if results.count > 1 { + if results.count > 1 { // Only show the list if we have more than one result. PlainList { ForEach(results) { result in @@ -210,10 +194,9 @@ struct SearchResultList: View { selectedResult = result } } - } } } - else if results != nil { + else if results.isEmpty { PlainList { Text(noResultMessage) } @@ -229,15 +212,15 @@ struct SearchResultList: View { } struct SearchSuggestionList: View { - var suggestionResults: Result<[SearchSuggestion]?, SearchError> + var suggestionResults: Result<[SearchSuggestion], SearchError> @Binding var currentSuggestion: SearchSuggestion? var noResultMessage: String var body: some View { Group { switch suggestionResults { - case .success(let results): - if let suggestions = results, suggestions.count > 0 { + case .success(let suggestions): + if !suggestions.isEmpty { PlainList { if suggestions.count > 0 { ForEach(suggestions) { suggestion in @@ -249,7 +232,7 @@ struct SearchSuggestionList: View { } } } - else if results != nil { + else { PlainList { Text(noResultMessage) } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 689e73778..b6c03f745 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -17,6 +17,7 @@ import ArcGIS import Combine /// Performs searches and manages search state for a Search, or optionally without a UI connection. +@MainActor public class SearchViewModel: ObservableObject { /// Defines how many results to return; one, many, or automatic based on circumstance. public enum SearchResultMode { @@ -65,9 +66,9 @@ public class SearchViewModel: ObservableObject { @Published public var currentQuery: String = "" { didSet { - results = .success(nil) + results = nil if currentQuery.isEmpty { - suggestions = .success(nil) + suggestions = nil } } } @@ -88,17 +89,14 @@ public class SearchViewModel: ObservableObject { /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. @Published - public private(set) var results: Result<[SearchResult]?, SearchError> = .success(nil) { + public private(set) var results: Result<[SearchResult], SearchError>? { didSet { switch results { case .success(let results): - if results != nil && results?.count == 1 { - selectedResult = results?.first + if results.count == 1 { + selectedResult = results.first } - else { - selectedResult = nil - } - case .failure(_): + default: selectedResult = nil } } @@ -121,7 +119,7 @@ public class SearchViewModel: ObservableObject { /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. @Published - public private(set) var suggestions: Result<[SearchSuggestion]?, SearchError> = .success(nil) + public private(set) var suggestions: Result<[SearchSuggestion], SearchError>? private var subscriptions = Set() @@ -129,39 +127,52 @@ public class SearchViewModel: ObservableObject { /// prior to starting another async task. private var currentTask: Task? + private func makeEffectiveSource( + with searchArea: Geometry?, + preferredSearchLocation: Point? + ) -> SearchSourceProtocol? { + guard var source = currentSource() else { return nil } + source.searchArea = searchArea ?? queryArea + source.preferredSearchLocation = preferredSearchLocation + return source + } + /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. /// - Parameter searchArea: geometry used to constrain the results. If `nil`, the /// `queryArea` property is used instead. If `queryArea` is `nil`, results are not constrained. - public func commitSearch(_ searchArea: Geometry? = nil) async -> Void { + public func commitSearch(_ searchArea: Geometry? = nil) { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - var source = currentSource() else { return } - - source.searchArea = searchArea != nil ? searchArea : queryArea - source.preferredSearchLocation = queryCenter + let source = makeEffectiveSource(with: searchArea, preferredSearchLocation: queryCenter) else { + return + } - suggestions = .success(nil) - - currentTask?.cancel() - currentTask = commitSearchTask(source) - await currentTask?.value + kickoffTask(commitSearchTask(source)) } /// Updates suggestions list asynchronously. @MainActor // TODO: ???? yes or no or a better idea? Maybe model is an Actor and not a class - public func updateSuggestions() async -> Void { + public func updateSuggestions() { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - var source = currentSource() else { return } - - source.searchArea = queryArea - source.preferredSearchLocation = queryCenter - - results = .success(nil) + let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { + return + } + guard currentSuggestion == nil else { + // don't update suggestions if currently searching for one + return + } - // TODO: Not thread safe... (currentTask) - currentTask?.cancel() - currentTask = updateSuggestionsTask(source) - await currentTask?.value + kickoffTask(updateSuggestionsTask(source)) + } + + @Published + public var currentSuggestion: SearchSuggestion? { + didSet { + if let currentSuggestion = currentSuggestion { + currentQuery = currentSuggestion.displayTitle + kickoffTask(acceptSuggestionTask(currentSuggestion)) + } + } } /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is @@ -174,13 +185,19 @@ public class SearchViewModel: ObservableObject { ) async -> Void { currentQuery = searchSuggestion.displayTitle - suggestions = .success(nil) + suggestions = nil currentTask?.cancel() currentTask = acceptSuggestionTask(searchSuggestion) await currentTask?.value } + private func kickoffTask(_ task: Task<(), Never>) { + suggestions = nil + currentTask?.cancel() + currentTask = task + } + /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, /// and reset the current query. public func clearSearch() { @@ -190,100 +207,77 @@ public class SearchViewModel: ObservableObject { } extension SearchViewModel { - private func commitSearchTask( - _ source: SearchSourceProtocol - ) -> Task<(), Never> { - let task = Task(operation: { - let searchResult = await Result { - try await source.search(currentQuery) + private func commitSearchTask(_ source: SearchSourceProtocol) -> Task<(), Never> { + Task { + do { + try await process(searchResults: source.search(currentQuery)) + } catch is CancellationError { + results = nil + } catch { + results = .failure(SearchError(error)) } - processSearchResults(searchResult) - }) - return task + } } - private func updateSuggestionsTask( - _ source: SearchSourceProtocol - ) -> Task<(), Never> { - let task = Task(operation: { + private func updateSuggestionsTask(_ source: SearchSourceProtocol) -> Task<(), Never> { + Task { let suggestResult = await Result { try await source.suggest(currentQuery) } - DispatchQueue.main.sync { - switch suggestResult { - case .success(let suggestResults): - suggestions = .success(suggestResults) - case .failure(let error): - suggestions = .failure(SearchError(error)) - break - case .none: - suggestions = .success(nil) - break - } + switch suggestResult { + case .success(let suggestResults): + suggestions = .success(suggestResults) + case .failure(let error): + suggestions = .failure(SearchError(error)) + break + case nil: + suggestions = nil + break } - }) - return task + } } - private func acceptSuggestionTask( - _ searchSuggestion: SearchSuggestion - ) -> Task<(), Never> { - let task = Task(operation: { - let searchResult = await Result { - try await searchSuggestion.owningSource.search(searchSuggestion) + private func acceptSuggestionTask(_ searchSuggestion: SearchSuggestion) -> Task<(), Never> { + Task { + do { + try await process(searchResults: searchSuggestion.owningSource.search(searchSuggestion)) + } catch is CancellationError { + results = nil + } catch { + results = .failure(SearchError(error)) } - - processSearchResults( - searchResult, - isCollection: searchSuggestion.suggestResult?.isCollection ?? true - ) - }) - return task + // once we are done searching for the suggestion, then reset it to nil + currentSuggestion = nil + } } - private func processSearchResults( - _ result: Result<[SearchResult], Error>?, - isCollection: Bool = true - ) { - guard let result = result else { - results = .success([]) - return - } + private func process(searchResults: [SearchResult], isCollection: Bool = true) { + let effectiveResults: [SearchResult] - DispatchQueue.main.sync { - var searchResults = [SearchResult]() - var searchError: Error? - - switch result { - case .success(let results): - switch (resultMode) { - case .single: - if let firstResult = results.first { - searchResults = [firstResult] - } - case .multiple: - searchResults = results - case .automatic: - if isCollection { - searchResults = results - } else { - if let firstResult = results.first { - searchResults = [firstResult] - } - } - } - case .failure(let error): - searchError = error + switch (resultMode) { + case .single: + if let firstResult = searchResults.first { + effectiveResults = [firstResult] + } else { + effectiveResults = [] } - - if let error = searchError { - results = .failure(SearchError(error)) - } - else { - results = .success(searchResults) + case .multiple: + effectiveResults = searchResults + case .automatic: + if isCollection { + effectiveResults = searchResults + } else { + if let firstResult = searchResults.first { + effectiveResults = [firstResult] + } + else { + effectiveResults = [] + } } } + + results = .success(effectiveResults) } } diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index e640b8046..dc1e6a888 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -15,12 +15,12 @@ import SwiftUI /// A modifier which adds a "search" button in a view, used to initiate an operation. struct EsriSearchButtonViewModifier: ViewModifier { - @Binding var performSearch: Bool + var action: () -> Void func body(content: Content) -> some View { HStack { Button( - action: { self.performSearch = true }, + action: action, label: { Image(systemName: "magnifyingglass.circle.fill") .foregroundColor(Color(.opaqueSeparator)) @@ -32,10 +32,10 @@ struct EsriSearchButtonViewModifier: ViewModifier { } extension View { - func esriSearchButton(performSearch: Binding) -> some View { + func esriSearchButton(_ action: @escaping () -> Void) -> some View { ModifiedContent( content: self, - modifier: EsriSearchButtonViewModifier(performSearch: performSearch) + modifier: EsriSearchButtonViewModifier(action: action) ) } } From 8dfc8302ad8824c75561ca7aaf8e904855c37ec4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 16 Sep 2021 16:22:14 -0500 Subject: [PATCH 0174/1722] Update tests; fix issue with search source constructor. --- .../Components/Search/SearchViewModel.swift | 6 ++---- .../Search/SmartLocatorSearchSource.swift | 6 ++++++ .../ArcGISToolkitTests/SearchViewModelTests.swift | 8 ++++++++ .../SmartLocatorSearchSourceTests.swift | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index b6c03f745..e2122cce7 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -65,11 +65,9 @@ public class SearchViewModel: ObservableObject { /// Tracks the current user-entered query. This property drives both suggestions and searches. @Published public var currentQuery: String = "" { - didSet { + willSet { results = nil - if currentQuery.isEmpty { - suggestions = nil - } + suggestions = nil } } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 507426e45..e5fa490c6 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -30,6 +30,11 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. public init( displayName: String = "Smart Locator", + locatorTask: LocatorTask = LocatorTask( + url: URL( + string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" + )! + ), maximumResults: Int32 = 6, maximumSuggestions: Int32 = 6, searchArea: Geometry? = nil, @@ -39,6 +44,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ) { super.init( displayName: displayName, + locatorTask: locatorTask, maximumResults: maximumResults, maximumSuggestions: maximumSuggestions, searchArea: searchArea, diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 24014815e..a13e85816 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -117,6 +117,14 @@ class SearchViewModelTests: XCTestCase { // Changing the `currentQuery` should set results to nil. model.currentQuery = "Coffee in Edinburgh" try XCTAssertNil(model.suggestions.get()) + + // Changing current query after search with 1 result + // should set `selectedResult` to nil + model.currentQuery = "Magers & Quinn Bookseller" + await model.commitSearch() + XCTAssertNotNil(model.selectedResult) + model.currentQuery = "Hotel" + XCTAssertNil(model.selectedResult) } func testQueryArea() async throws { diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 0246d58c8..0c7a65510 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -60,6 +60,20 @@ class SmartLocatorSearchSourceTests: XCTestCase { results = try XCTUnwrap(suggestResults) XCTAssertGreaterThanOrEqual(results.count, 1) } + + func testLocalLocatorTask() async throws { + let locatorTask = LocatorTask(url: URL(fileURLWithPath: "/Users/mark1113/san-diego/SanDiego_StreetAddress.loc/")); + let locator = SmartLocatorSearchSource(locatorTask: locatorTask) + + let suggestResults = try await locator.suggest("Coffee") + let suggestions = try XCTUnwrap(suggestResults) + XCTAssertEqual(suggestions.count, 3) + + let searchResults = try await locator.search("Hotel") + let results = try XCTUnwrap(searchResults) + XCTAssertEqual(results.count, 2) + } + } extension Envelope { From 6a36021377ff6ad7f029669d6bfdca9a9daee7e5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 30 Sep 2021 15:52:52 -0500 Subject: [PATCH 0175/1722] queryArea and repeat search behavior changes --- Examples/Examples/SearchExampleView.swift | 17 ++- .../Search/LocatorSearchSource.swift | 36 ++++-- .../Search/SearchSourceProtocol.swift | 7 ++ .../Components/Search/SearchView.swift | 115 +++++++++++------- .../Components/Search/SearchViewModel.swift | 97 +++++++++++++-- .../Search/SmartLocatorSearchSource.swift | 2 +- 6 files changed, 209 insertions(+), 65 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 0c62b4736..f543ac65d 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -35,12 +35,16 @@ struct SearchExampleView: View { @State var searchResultsOverlay = GraphicsOverlay() + @State + private var isNavigating: Bool = false + var body: some View { MapView( map: map, viewpoint: searchResultViewpoint, graphicsOverlays: [searchResultsOverlay] ) + .onNavigatingChanged { isNavigating = $0 } .onViewpointChanged(kind: .centerAndScale) { searchViewModel.queryCenter = $0.targetGeometry as? Point @@ -52,9 +56,15 @@ struct SearchExampleView: View { searchResultViewpoint = nil } .onVisibleAreaChanged { newValue in - // Setting `searchViewModel.queryArea` will limit the initial + // Setting `searchViewModel.queryArea` will limit the // results to `queryArea`. - // searchViewModel.queryArea = newValue + //searchViewModel.queryArea = newValue + + // For "Repeat Search Here" behavior, set the + // `searchViewModel.extent` property when navigating. + if isNavigating || searchViewModel.extent == nil { + searchViewModel.extent = newValue.extent + } } .overlay(alignment: .topTrailing) { SearchView( @@ -62,7 +72,7 @@ struct SearchExampleView: View { viewpoint: $searchResultViewpoint, resultsOverlay: $searchResultsOverlay ) - .frame(width: 360) + .searchBarWidth(360.0) .padding() } .onAppear() { @@ -77,6 +87,7 @@ struct SearchExampleView: View { maximumResults: 16, maximumSuggestions: 16 ) + searchViewModel.sources = [smartLocator] } } diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 1aaa2c8aa..8c5511a76 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -86,17 +86,15 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { /// based on searches. public private(set) var suggestParameters: SuggestParameters = SuggestParameters() + public func repeatSearch( + _ queryString: String, + queryExtent: Envelope + ) async throws -> [SearchResult] { + return try await internalSearch(queryString, queryArea: queryExtent) + } + public func search(_ queryString: String) async throws -> [SearchResult] { - geocodeParameters.searchArea = searchArea - geocodeParameters.preferredSearchLocation = preferredSearchLocation - - let geocodeResults = try await locatorTask.geocode( - searchText: queryString, - parameters: geocodeParameters - ) - - // Convert to SearchResults and return. - return geocodeResults.map{ $0.toSearchResult(searchSource: self) } + return try await internalSearch(queryString, queryArea: searchArea) } public func search( @@ -130,3 +128,21 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { return geocodeResults.map{ $0.toSearchSuggestion(searchSource: self) } } } + +extension LocatorSearchSource { + private func internalSearch( + _ queryString: String, + queryArea: Geometry? + ) async throws -> [SearchResult] { + geocodeParameters.searchArea = queryArea + geocodeParameters.preferredSearchLocation = preferredSearchLocation + + let geocodeResults = try await locatorTask.geocode( + searchText: queryString, + parameters: geocodeParameters + ) + + // Convert to SearchResults and return. + return geocodeResults.map{ $0.toSearchResult(searchSource: self) } + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift index ab06b18e4..607eb3569 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift @@ -52,4 +52,11 @@ public protocol SearchSourceProtocol { func search( _ searchSuggestion: SearchSuggestion ) async throws -> [SearchResult] + + /// Repeats the last search. + /// - Parameters: + /// - queryString: Text to be used for query. + /// - queryExtent: Extent used to limit the results. + /// - Returns: Array of `SearchResult`s + func repeatSearch(_ queryString: String, queryExtent: Envelope) async throws -> [SearchResult] } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index fdbe99aeb..51ea9c478 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -60,10 +60,10 @@ public struct SearchView: View { /// Note: this is set using the `noResultMessage` modifier. private var noResultMessage = "No results found" + public var searchBarWidth: CGFloat = 360.0 -// /// The current suggestion selected by the user. -// @State -// private var currentSuggestion: SearchSuggestion? + @State + private var shouldZoomToResults = true /// Determines whether the results lists are displayed. @State @@ -71,35 +71,49 @@ public struct SearchView: View { public var body: some View { VStack (alignment: .center) { - TextField( - searchViewModel.defaultPlaceholder, - text: $searchViewModel.currentQuery - ) { _ in - } onCommit: { - searchViewModel.commitSearch() - } - .esriDeleteTextButton(text: $searchViewModel.currentQuery) - .esriSearchButton { searchViewModel.commitSearch() } - .esriShowResultsButton( - isEnabled: enableResultListView, - isHidden: $isResultListViewHidden - ) - .esriBorder() - if enableResultListView, !isResultListViewHidden { - if let results = searchViewModel.results { - SearchResultList( - searchResults: results, - selectedResult: $searchViewModel.selectedResult, - noResultMessage: noResultMessage + HStack { + Spacer() + VStack (alignment: .center) { + TextField( + searchViewModel.defaultPlaceholder, + text: $searchViewModel.currentQuery + ) { _ in + } onCommit: { + searchViewModel.commitSearch() + } + .esriDeleteTextButton(text: $searchViewModel.currentQuery) + .esriSearchButton { searchViewModel.commitSearch() } + .esriShowResultsButton( + isEnabled: enableResultListView, + isHidden: $isResultListViewHidden ) + .esriBorder() + if enableResultListView, !isResultListViewHidden { + if let results = searchViewModel.results { + SearchResultList( + searchResults: results, + selectedResult: $searchViewModel.selectedResult, + noResultMessage: noResultMessage + ) + } + if let suggestions = searchViewModel.suggestions { + SearchSuggestionList( + suggestionResults: suggestions, + currentSuggestion: $searchViewModel.currentSuggestion, + noResultMessage: noResultMessage + ) + } + } + Spacer() } - if let suggestions = searchViewModel.suggestions { - SearchSuggestionList( - suggestionResults: suggestions, - currentSuggestion: $searchViewModel.currentSuggestion, - noResultMessage: noResultMessage - ) + .frame(width: searchBarWidth) + } + if searchViewModel.isEligibleForRequery { + Button("Repeat Search Here") { + shouldZoomToResults = false + searchViewModel.repeatSearch() } + .esriBorder() } } .onChange(of: searchViewModel.results) { @@ -137,6 +151,15 @@ public struct SearchView: View { copy.noResultMessage = noResultMessage return copy } + + /// The width of the search bar. + /// - Parameter searchBarWidth: The desired width of the search bar. + /// - Returns: The `SearchView`. + public func searchBarWidth(_ searchBarWidth: CGFloat) -> Self { + var copy = self + copy.searchBarWidth = searchBarWidth + return copy + } } extension SearchView { @@ -154,12 +177,15 @@ extension SearchView { resultsOverlay?.wrappedValue.addGraphics(resultGraphics) if resultGraphics.count > 0, - let envelope = resultsOverlay?.wrappedValue.extent { + let envelope = resultsOverlay?.wrappedValue.extent, + shouldZoomToResults { let builder = EnvelopeBuilder(envelope: envelope) builder.expand(factor: 1.1) + let targetExtent = builder.toGeometry() as! Envelope viewpoint?.wrappedValue = Viewpoint( - targetExtent: builder.toGeometry() + targetExtent: targetExtent ) + searchViewModel.lastSearchExtent = targetExtent } else { viewpoint?.wrappedValue = nil @@ -168,6 +194,8 @@ extension SearchView { resultsOverlay?.wrappedValue.removeAllGraphics() viewpoint?.wrappedValue = nil } + + if !shouldZoomToResults { shouldZoomToResults = true } } private func display(selectedResult: SearchResult?) { @@ -186,14 +214,14 @@ struct SearchResultList: View { switch searchResults { case .success(let results): if results.count > 1 { - // Only show the list if we have more than one result. - PlainList { - ForEach(results) { result in - SearchResultRow(result: result) - .onTapGesture { - selectedResult = result - } - } + // Only show the list if we have more than one result. + PlainList { + ForEach(results) { result in + SearchResultRow(result: result) + .onTapGesture { + selectedResult = result + } + } } } else if results.isEmpty { @@ -305,7 +333,10 @@ private extension Graphic { private extension Symbol { /// A search result marker symbol. - static let resultSymbol: MarkerSymbol = PictureMarkerSymbol( - image: UIImage(named: "MapPin")! - ) + static var resultSymbol: MarkerSymbol { + let image = UIImage(named: "MapPin")! + let symbol = PictureMarkerSymbol(image: image) + symbol.offsetY = Float(image.size.height / 2.0) + return symbol + } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index e2122cce7..4a1572403 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -68,13 +68,64 @@ public class SearchViewModel: ObservableObject { willSet { results = nil suggestions = nil + isEligibleForRequery = false } } - /// The search area to be used for the current query. This property should be updated - /// as the user navigates the map/scene, or at minimum before calling `commitSearch`. - public var queryArea: Geometry? = nil + /// The extent at the time of the last search. This is primarily set by the model, but in certain + /// circumstances can be set by an external client, for example after a view zooms programmatically + /// to an extent based on results of a search. + public var lastSearchExtent: Envelope? = nil { + didSet { + isEligibleForRequery = false + } + } + /// The current GeoView extent. Defaults to null. This should be updated as the user navigates + /// the map/scene. It will be used to determine the value of `IsEligibleForRequery` + /// for the 'Repeat search here' behavior. If that behavior is not wanted, it should be left `nil`. + public var extent: Envelope? = nil { + willSet { + guard !isEligibleForRequery, + !currentQuery.isEmpty, + let lastExtent = lastSearchExtent, + let newExtent = newValue + else { return } + + // Check extent difference. + let widthDiff = fabs(lastExtent.width - newExtent.width) + let heightDiff = fabs(lastExtent.height - newExtent.height) + + let widthThreshold = lastExtent.width * 0.25 + let heightThreshold = lastExtent.height * 0.25 + + isEligibleForRequery = widthDiff > widthThreshold || heightDiff > heightThreshold + guard !isEligibleForRequery else { return } + + // Check center difference. + let centerDiff = GeometryEngine.distance( + geometry1: lastExtent.center, + geometry2: newExtent.center + ) + let currentExtentAvg = (lastExtent.width + lastExtent.height / 2.0) + let threshold = currentExtentAvg * 0.25 + isEligibleForRequery = (centerDiff ?? 0.0) > threshold + } + } + + /// True if the Extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. + /// This property is used by the view to enable 'Repeat search here' functionality. This property is + /// observable, and the view should use it to hide and show the 'repeat search' button. + /// Changes to this property are driven by changes to the `Extent` property. This value will be + /// true if the extent center changes by more than 25% of the average of the extent's height and width + /// at the time of the last search or if the extent width/height changes by the same amount. + @Published + public private(set) var isEligibleForRequery: Bool = false + + /// The search area to be used for the current query. Results will be limited to those + /// within `QueryArea`. Defaults to `nil`. + public var queryArea: Geometry? = nil + /// Defines the center for the search. For most use cases, this should be updated by the view /// every time the user navigates the map. public var queryCenter: Point? @@ -130,7 +181,7 @@ public class SearchViewModel: ObservableObject { preferredSearchLocation: Point? ) -> SearchSourceProtocol? { guard var source = currentSource() else { return nil } - source.searchArea = searchArea ?? queryArea + source.searchArea = searchArea source.preferredSearchLocation = preferredSearchLocation return source } @@ -139,17 +190,27 @@ public class SearchViewModel: ObservableObject { /// asynchronously. Other query properties are read to define the parameters of the search. /// - Parameter searchArea: geometry used to constrain the results. If `nil`, the /// `queryArea` property is used instead. If `queryArea` is `nil`, results are not constrained. - public func commitSearch(_ searchArea: Geometry? = nil) { + public func commitSearch() { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = makeEffectiveSource(with: searchArea, preferredSearchLocation: queryCenter) else { + let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { return } - kickoffTask(commitSearchTask(source)) + kickoffTask(searchTask(source)) + } + + /// Repeats the last search, limiting results to the extent specified in `extent`. + public func repeatSearch() { + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, + let queryExtent = extent, + let source = makeEffectiveSource(with: extent, preferredSearchLocation: nil) else { + return + } + + kickoffTask(repeatSearchTask(source, extent: queryExtent)) } /// Updates suggestions list asynchronously. - @MainActor // TODO: ???? yes or no or a better idea? Maybe model is an Actor and not a class public func updateSuggestions() { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { @@ -205,9 +266,25 @@ public class SearchViewModel: ObservableObject { } extension SearchViewModel { - private func commitSearchTask(_ source: SearchSourceProtocol) -> Task<(), Never> { + private func repeatSearchTask(_ source: SearchSourceProtocol, extent: Envelope) -> Task<(), Never> { + Task { + do { + // User is performing a search, so set `lastSearchExtent`. + lastSearchExtent = extent + try await process(searchResults: source.repeatSearch(currentQuery, queryExtent: extent)) + } catch is CancellationError { + results = nil + } catch { + results = .failure(SearchError(error)) + } + } + } + + private func searchTask(_ source: SearchSourceProtocol) -> Task<(), Never> { Task { do { + // User is performing a search, so set `lastSearchExtent`. + lastSearchExtent = extent try await process(searchResults: source.search(currentQuery)) } catch is CancellationError { results = nil @@ -239,6 +316,8 @@ extension SearchViewModel { private func acceptSuggestionTask(_ searchSuggestion: SearchSuggestion) -> Task<(), Never> { Task { do { + // User is performing a search, so set `lastSearchExtent`. + lastSearchExtent = extent try await process(searchResults: searchSuggestion.owningSource.search(searchSuggestion)) } catch is CancellationError { results = nil diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index e5fa490c6..82652a9c5 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -66,7 +66,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search /// repeat behavior. public var repeatSuggestResultThreshold: Int = 6 - + public override func search( _ queryString: String ) async throws -> [SearchResult] { From 7801aafdcfb087ce1a96be7dc478566a089cfe0b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 5 Oct 2021 17:25:54 -0500 Subject: [PATCH 0176/1722] Test and view model concurrency updates (MainActor) --- .../Components/Search/SearchView.swift | 6 +- .../Components/Search/SearchViewModel.swift | 54 ++++++++++------- .../SearchViewModelTests.swift | 58 +++++++++++++++---- 3 files changed, 83 insertions(+), 35 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 51ea9c478..1ce8c7431 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -122,9 +122,9 @@ public struct SearchView: View { .onChange(of: searchViewModel.selectedResult) { display(selectedResult: $0) } - .onReceive(searchViewModel.$currentQuery/*.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)*/) { _ in - searchViewModel.updateSuggestions() - } +// .onReceive(searchViewModel.$currentQuery/*.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)*/) { _ in +// searchViewModel.updateSuggestions() +// } Spacer() } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 4a1572403..77b50575b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -53,6 +53,16 @@ public class SearchViewModel: ObservableObject { self.queryCenter = queryCenter self.resultMode = resultMode self.sources = sources + + $currentQuery.sink { [weak self] query in + self?.results = nil + self?.isEligibleForRequery = false + if query.isEmpty { + self?.suggestions = nil + } else { + self?.updateSuggestions() + } + }.store(in: &subscriptions) } /// The string shown in the search view when no user query is entered. @@ -64,13 +74,7 @@ public class SearchViewModel: ObservableObject { /// Tracks the current user-entered query. This property drives both suggestions and searches. @Published - public var currentQuery: String = "" { - willSet { - results = nil - suggestions = nil - isEligibleForRequery = false - } - } + public var currentQuery: String = "" /// The extent at the time of the last search. This is primarily set by the model, but in certain /// circumstances can be set by an external client, for example after a view zooms programmatically @@ -98,10 +102,10 @@ public class SearchViewModel: ObservableObject { let widthThreshold = lastExtent.width * 0.25 let heightThreshold = lastExtent.height * 0.25 - + isEligibleForRequery = widthDiff > widthThreshold || heightDiff > heightThreshold guard !isEligibleForRequery else { return } - + // Check center difference. let centerDiff = GeometryEngine.distance( geometry1: lastExtent.center, @@ -139,6 +143,11 @@ public class SearchViewModel: ObservableObject { /// were no results, and the view should show an appropriate 'no results' message. @Published public private(set) var results: Result<[SearchResult], SearchError>? { + willSet { + if newValue != nil { + suggestions = nil + } + } didSet { switch results { case .success(let results): @@ -168,7 +177,13 @@ public class SearchViewModel: ObservableObject { /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, /// a useful 'no results' message should be shown by the view. @Published - public private(set) var suggestions: Result<[SearchSuggestion], SearchError>? + public private(set) var suggestions: Result<[SearchSuggestion], SearchError>? { + willSet { + if newValue != nil { + results = nil + } + } + } private var subscriptions = Set() @@ -210,6 +225,10 @@ public class SearchViewModel: ObservableObject { kickoffTask(repeatSearchTask(source, extent: queryExtent)) } + // TODO: something's not right with concurrency; currently seeing both results and suggestions + // but that shouldn't be possible. What's up? Maybe because model methods are not async it's + // messing things up? But the model should account for that... Right? + /// Updates suggestions list asynchronously. public func updateSuggestions() { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, @@ -228,8 +247,7 @@ public class SearchViewModel: ObservableObject { public var currentSuggestion: SearchSuggestion? { didSet { if let currentSuggestion = currentSuggestion { - currentQuery = currentSuggestion.displayTitle - kickoffTask(acceptSuggestionTask(currentSuggestion)) + acceptSuggestion(currentSuggestion) } } } @@ -239,20 +257,12 @@ public class SearchViewModel: ObservableObject { /// `currentQuery` property. /// - Parameters: /// - searchSuggestion: The suggestion to use to commit the search. - public func acceptSuggestion( - _ searchSuggestion: SearchSuggestion - ) async -> Void { + public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) { currentQuery = searchSuggestion.displayTitle - - suggestions = nil - - currentTask?.cancel() - currentTask = acceptSuggestionTask(searchSuggestion) - await currentTask?.value + kickoffTask(acceptSuggestionTask(searchSuggestion)) } private func kickoffTask(_ task: Task<(), Never>) { - suggestions = nil currentTask?.cancel() currentTask = task } diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index a13e85816..dd3948338 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -17,26 +17,63 @@ import XCTest import ArcGIS import ArcGISToolkit import SwiftUI +import Combine class SearchViewModelTests: XCTestCase { + @MainActor func testAcceptSuggestion() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - model.currentQuery = "Magers & Quinn Booksellers" - await model.updateSuggestions() - let suggestionionResults = try XCTUnwrap(model.suggestions.get()) - let suggestion = try XCTUnwrap(suggestionionResults.first) - await model.acceptSuggestion(suggestion) - let results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 1) + var subscriptions = Set() + + // Get suggestion + let exp = expectation(description: "UpdateSuggestions") + var suggestion: SearchSuggestion? + model.$suggestions.dropFirst().first().sink { value in + do { + print("$suggestions: \(String(describing: value))") + suggestion = try XCTUnwrap(value?.get().first) + } catch { + XCTFail("Valid suggestion") + } + exp.fulfill() + } + .store(in: &subscriptions) + + // `model.updateSuggestions()` gets called, but the inner locatorSearchSource.suggest() + // is never called (it's wrapped in a Taks, which is never started. I'm wondering if + // `waitForExpectations` is blocking the main thread, which is where the Task is + // started (maybe?) because the model is marked as `@MainActor`.??? + model.updateSuggestions() + waitForExpectations(timeout: 5.0) + + // Get search result + guard let suggestion = suggestion else { return } + + let exp2 = expectation(description: "AcceptSuggestion") + model.$results.drop(while: { $0 == nil }).sink { value in + do { + print("$results: \(String(describing: value))") + let results = try XCTUnwrap(value?.get()) + XCTAssertEqual(results.count, 1) + + try XCTAssertNil(model.suggestions?.get()) + } catch { + XCTFail("Valid suggestion") + } + exp2.fulfill() + } + .store(in: &subscriptions) + + model.acceptSuggestion(suggestion) + waitForExpectations(timeout: 5.0) // With only one results, model should set `selectedResult` property. + let results = try XCTUnwrap(model.results?.get()) XCTAssertEqual(results.first!, model.selectedResult) - - try XCTAssertNil(model.suggestions.get()) } - +/* func testActiveSource() async throws { let activeSource = LocatorSearchSource() activeSource.displayName = "Simple Locator" @@ -263,6 +300,7 @@ class SearchViewModelTests: XCTestCase { XCTAssertNil(model.selectedResult) try XCTAssertNil(model.results.get()) } + */ } extension Polygon { From 80cd9346aa15e361e4ce9a1cecf110bec544640b Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Oct 2021 21:45:07 -0600 Subject: [PATCH 0177/1722] use publisher extension and async/await instead of xctexpectations --- Tests/ArcGISToolkitTests/Publisher.swift | 41 +++++++ .../SearchViewModelTests.swift | 109 +++++++++++------- 2 files changed, 110 insertions(+), 40 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/Publisher.swift diff --git a/Tests/ArcGISToolkitTests/Publisher.swift b/Tests/ArcGISToolkitTests/Publisher.swift new file mode 100644 index 000000000..01184dba4 --- /dev/null +++ b/Tests/ArcGISToolkitTests/Publisher.swift @@ -0,0 +1,41 @@ +// Copyright 2021 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 Foundation +import Combine + +extension Publisher { + /// Asynchronously returns the first value emitted from the publisher. + /// This property will return `nil` if this Publisher completes without an error before + /// it emits a value. + var first: Output? { + get async throws { + if let output = try await AsyncThrowingPublisher(self).first { + return output + } else if Task.isCancelled { + throw CancellationError() + } else { + return nil + } + } + } +} + +extension AsyncSequence { + /// The first element emitted by the async sequence. + var first: Element? { + get async throws { + try await first { _ in true } + } + } +} diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index dd3948338..a48f2a71f 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -24,55 +24,83 @@ class SearchViewModelTests: XCTestCase { func testAcceptSuggestion() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) model.currentQuery = "Magers & Quinn Booksellers" - - var subscriptions = Set() - // Get suggestion - let exp = expectation(description: "UpdateSuggestions") - var suggestion: SearchSuggestion? - model.$suggestions.dropFirst().first().sink { value in - do { - print("$suggestions: \(String(describing: value))") - suggestion = try XCTUnwrap(value?.get().first) - } catch { - XCTFail("Valid suggestion") - } - exp.fulfill() + Task { + model.updateSuggestions() } - .store(in: &subscriptions) - - // `model.updateSuggestions()` gets called, but the inner locatorSearchSource.suggest() - // is never called (it's wrapped in a Taks, which is never started. I'm wondering if - // `waitForExpectations` is blocking the main thread, which is where the Task is - // started (maybe?) because the model is marked as `@MainActor`.??? - model.updateSuggestions() - waitForExpectations(timeout: 5.0) - - // Get search result - guard let suggestion = suggestion else { return } - - let exp2 = expectation(description: "AcceptSuggestion") - model.$results.drop(while: { $0 == nil }).sink { value in - do { - print("$results: \(String(describing: value))") - let results = try XCTUnwrap(value?.get()) - XCTAssertEqual(results.count, 1) - - try XCTAssertNil(model.suggestions?.get()) - } catch { - XCTFail("Valid suggestion") - } - exp2.fulfill() + + // Get suggestion + let suggestionResult = try await model.$suggestions.dropFirst().compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestionResult?.get().first) + + Task { + model.acceptSuggestion(suggestion) } - .store(in: &subscriptions) - model.acceptSuggestion(suggestion) - waitForExpectations(timeout: 5.0) + let searchResultResult = try await model.$results.dropFirst().compactMap({$0}).first + let searchResults = try XCTUnwrap(searchResultResult?.get()) + XCTAssertEqual(searchResults.count, 1) + try XCTAssertNil(model.suggestions?.get()) // With only one results, model should set `selectedResult` property. let results = try XCTUnwrap(model.results?.get()) XCTAssertEqual(results.first!, model.selectedResult) } + + +// @MainActor +// func testAcceptSuggestion() async throws { +// let model = SearchViewModel(sources: [LocatorSearchSource()]) +// model.currentQuery = "Magers & Quinn Booksellers" +// +// var subscriptions = Set() +// +// // Get suggestion +// let exp = expectation(description: "UpdateSuggestions") +// var suggestion: SearchSuggestion? +// model.$suggestions.dropFirst().first().sink { value in +// do { +// print("$suggestions: \(String(describing: value))") +// suggestion = try XCTUnwrap(value?.get().first) +// } catch { +// XCTFail("Valid suggestion") +// } +// exp.fulfill() +// } +// .store(in: &subscriptions) +// +// // `model.updateSuggestions()` gets called, but the inner locatorSearchSource.suggest() +// // is never called (it's wrapped in a Taks, which is never started. I'm wondering if +// // `waitForExpectations` is blocking the main thread, which is where the Task is +// // started (maybe?) because the model is marked as `@MainActor`.??? +// model.updateSuggestions() +// waitForExpectations(timeout: 5.0) +// +// // Get search result +// guard let suggestion = suggestion else { return } +// +// let exp2 = expectation(description: "AcceptSuggestion") +// model.$results.drop(while: { $0 == nil }).sink { value in +// do { +// print("$results: \(String(describing: value))") +// let results = try XCTUnwrap(value?.get()) +// XCTAssertEqual(results.count, 1) +// +// try XCTAssertNil(model.suggestions?.get()) +// } catch { +// XCTFail("Valid suggestion") +// } +// exp2.fulfill() +// } +// .store(in: &subscriptions) +// +// model.acceptSuggestion(suggestion) +// waitForExpectations(timeout: 5.0) +// +// // With only one results, model should set `selectedResult` property. +// let results = try XCTUnwrap(model.results?.get()) +// XCTAssertEqual(results.first!, model.selectedResult) +// } /* func testActiveSource() async throws { let activeSource = LocatorSearchSource() @@ -327,3 +355,4 @@ 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) } + From 375162257fb5293292662cdef9cce68ed0a62d5d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Oct 2021 21:48:32 -0600 Subject: [PATCH 0178/1722] don't need dropFirst because using compactMap --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index a48f2a71f..737787196 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -30,14 +30,14 @@ class SearchViewModelTests: XCTestCase { } // Get suggestion - let suggestionResult = try await model.$suggestions.dropFirst().compactMap({$0}).first + let suggestionResult = try await model.$suggestions.compactMap({$0}).first let suggestion = try XCTUnwrap(suggestionResult?.get().first) Task { model.acceptSuggestion(suggestion) } - let searchResultResult = try await model.$results.dropFirst().compactMap({$0}).first + let searchResultResult = try await model.$results.compactMap({$0}).first let searchResults = try XCTUnwrap(searchResultResult?.get()) XCTAssertEqual(searchResults.count, 1) try XCTAssertNil(model.suggestions?.get()) From cc9304411ad028e803fce8f59b81b54d46e84d1b Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Oct 2021 21:53:40 -0600 Subject: [PATCH 0179/1722] variable rename --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 737787196..fbea140a5 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -30,15 +30,15 @@ class SearchViewModelTests: XCTestCase { } // Get suggestion - let suggestionResult = try await model.$suggestions.compactMap({$0}).first - let suggestion = try XCTUnwrap(suggestionResult?.get().first) + let suggestionsResult = try await model.$suggestions.compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestionsResult?.get().first) Task { model.acceptSuggestion(suggestion) } - let searchResultResult = try await model.$results.compactMap({$0}).first - let searchResults = try XCTUnwrap(searchResultResult?.get()) + let searchResultsResult = try await model.$results.compactMap({$0}).first + let searchResults = try XCTUnwrap(searchResultsResult?.get()) XCTAssertEqual(searchResults.count, 1) try XCTAssertNil(model.suggestions?.get()) From 330bb9e7bb5ca29187373d5d0373c2b3194ecb2b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 8 Oct 2021 14:53:15 -0500 Subject: [PATCH 0180/1722] Update tests for new concurrency methodology; minor model updates. --- .../Components/Search/SearchView.swift | 6 +- .../Components/Search/SearchViewModel.swift | 29 +- .../SearchViewModelTests.swift | 305 +++++++++--------- 3 files changed, 169 insertions(+), 171 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 1ce8c7431..b245b3412 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -122,9 +122,9 @@ public struct SearchView: View { .onChange(of: searchViewModel.selectedResult) { display(selectedResult: $0) } -// .onReceive(searchViewModel.$currentQuery/*.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)*/) { _ in -// searchViewModel.updateSuggestions() -// } + .onReceive(searchViewModel.$currentQuery) { _ in + searchViewModel.updateSuggestions() + } Spacer() } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 77b50575b..97b39b663 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -53,16 +53,6 @@ public class SearchViewModel: ObservableObject { self.queryCenter = queryCenter self.resultMode = resultMode self.sources = sources - - $currentQuery.sink { [weak self] query in - self?.results = nil - self?.isEligibleForRequery = false - if query.isEmpty { - self?.suggestions = nil - } else { - self?.updateSuggestions() - } - }.store(in: &subscriptions) } /// The string shown in the search view when no user query is entered. @@ -74,7 +64,13 @@ public class SearchViewModel: ObservableObject { /// Tracks the current user-entered query. This property drives both suggestions and searches. @Published - public var currentQuery: String = "" + public var currentQuery: String = "" { + willSet { + results = nil + suggestions = nil + isEligibleForRequery = false + } + } /// The extent at the time of the last search. This is primarily set by the model, but in certain /// circumstances can be set by an external client, for example after a view zooms programmatically @@ -184,9 +180,7 @@ public class SearchViewModel: ObservableObject { } } } - - private var subscriptions = Set() - + /// The currently executing async task. `currentTask` should be cancelled /// prior to starting another async task. private var currentTask: Task? @@ -198,6 +192,7 @@ public class SearchViewModel: ObservableObject { guard var source = currentSource() else { return nil } source.searchArea = searchArea source.preferredSearchLocation = preferredSearchLocation + return source } @@ -210,7 +205,7 @@ public class SearchViewModel: ObservableObject { let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { return } - + // TODO: do we want the above in the `searchTask()` method?? kickoffTask(searchTask(source)) } @@ -225,10 +220,6 @@ public class SearchViewModel: ObservableObject { kickoffTask(repeatSearchTask(source, extent: queryExtent)) } - // TODO: something's not right with concurrency; currently seeing both results and suggestions - // but that shouldn't be possible. What's up? Maybe because model methods are not async it's - // messing things up? But the model should account for that... Right? - /// Updates suggestions list asynchronously. public func updateSuggestions() { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index fbea140a5..079e2339e 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -19,179 +19,139 @@ import ArcGISToolkit import SwiftUI import Combine +@MainActor class SearchViewModelTests: XCTestCase { - @MainActor + // + // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/Search/Search_Test_Design.md + // func testAcceptSuggestion() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) model.currentQuery = "Magers & Quinn Booksellers" - Task { - model.updateSuggestions() - } + Task { model.updateSuggestions() } // Get suggestion - let suggestionsResult = try await model.$suggestions.compactMap({$0}).first - let suggestion = try XCTUnwrap(suggestionsResult?.get().first) + let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestions?.get().first) - Task { - model.acceptSuggestion(suggestion) - } + Task { model.acceptSuggestion(suggestion) } - let searchResultsResult = try await model.$results.compactMap({$0}).first - let searchResults = try XCTUnwrap(searchResultsResult?.get()) - XCTAssertEqual(searchResults.count, 1) - try XCTAssertNil(model.suggestions?.get()) + let results = try await model.$results.compactMap({$0}).first + let result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + XCTAssertNil(model.suggestions) // With only one results, model should set `selectedResult` property. - let results = try XCTUnwrap(model.results?.get()) - XCTAssertEqual(results.first!, model.selectedResult) + XCTAssertEqual(result.first!, model.selectedResult) } - - -// @MainActor -// func testAcceptSuggestion() async throws { -// let model = SearchViewModel(sources: [LocatorSearchSource()]) -// model.currentQuery = "Magers & Quinn Booksellers" -// -// var subscriptions = Set() -// -// // Get suggestion -// let exp = expectation(description: "UpdateSuggestions") -// var suggestion: SearchSuggestion? -// model.$suggestions.dropFirst().first().sink { value in -// do { -// print("$suggestions: \(String(describing: value))") -// suggestion = try XCTUnwrap(value?.get().first) -// } catch { -// XCTFail("Valid suggestion") -// } -// exp.fulfill() -// } -// .store(in: &subscriptions) -// -// // `model.updateSuggestions()` gets called, but the inner locatorSearchSource.suggest() -// // is never called (it's wrapped in a Taks, which is never started. I'm wondering if -// // `waitForExpectations` is blocking the main thread, which is where the Task is -// // started (maybe?) because the model is marked as `@MainActor`.??? -// model.updateSuggestions() -// waitForExpectations(timeout: 5.0) -// -// // Get search result -// guard let suggestion = suggestion else { return } -// -// let exp2 = expectation(description: "AcceptSuggestion") -// model.$results.drop(while: { $0 == nil }).sink { value in -// do { -// print("$results: \(String(describing: value))") -// let results = try XCTUnwrap(value?.get()) -// XCTAssertEqual(results.count, 1) -// -// try XCTAssertNil(model.suggestions?.get()) -// } catch { -// XCTFail("Valid suggestion") -// } -// exp2.fulfill() -// } -// .store(in: &subscriptions) -// -// model.acceptSuggestion(suggestion) -// waitForExpectations(timeout: 5.0) -// -// // With only one results, model should set `selectedResult` property. -// let results = try XCTUnwrap(model.results?.get()) -// XCTAssertEqual(results.first!, model.selectedResult) -// } -/* + func testActiveSource() async throws { let activeSource = LocatorSearchSource() activeSource.displayName = "Simple Locator" - let model = SearchViewModel( activeSource: activeSource, sources: [LocatorSearchSource()] ) model.currentQuery = "Magers & Quinn Booksellers" - await model.commitSearch() - let result = try XCTUnwrap(model.results.get()?.first) + + Task { model.commitSearch() } + + let results = try await model.$results.compactMap({$0}).first + let result = try XCTUnwrap(results?.get().first) XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) - await model.updateSuggestions() - let suggestResult = try XCTUnwrap(model.suggestions.get()?.first) - XCTAssertEqual(suggestResult.owningSource.displayName, activeSource.displayName) + Task { model.updateSuggestions() } + + let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestions?.get().first) + XCTAssertEqual(suggestion.owningSource.displayName, activeSource.displayName) } - + func testCommitSearch() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) // No search - results are nil. - try XCTAssertNil(model.results.get()) + XCTAssertNil(model.results) // Search with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" - await model.commitSearch() - var results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 0) + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) XCTAssertNil(model.selectedResult) - try XCTAssertNil(model.suggestions.get()) + XCTAssertNil(model.suggestions) // Search with one result. model.currentQuery = "Magers & Quinn Booksellers" - await model.commitSearch() - results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 1) + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) // One results automatically populates `selectedResult`. - XCTAssertEqual(results.first!, model.selectedResult) - try XCTAssertNil(model.suggestions.get()) + XCTAssertEqual(result.first!, model.selectedResult) + XCTAssertNil(model.suggestions) // Search with multiple results. model.currentQuery = "Magers & Quinn" - await model.commitSearch() - results = try XCTUnwrap(model.results.get()) - XCTAssertGreaterThan(results.count, 1) + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) XCTAssertNil(model.selectedResult) - try XCTAssertNil(model.suggestions.get()) + XCTAssertNil(model.suggestions) } - + func testCurrentQuery() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - // Empty `currentQuery` should produce nil results value. + // Empty `currentQuery` should produce nil results and suggestions. model.currentQuery = "" - await model.commitSearch() - try XCTAssertNil(model.results.get()) - - // Empty `currentQuery` should produce nil suggestions value. - await model.updateSuggestions() - try XCTAssertNil(model.suggestions.get()) + XCTAssertNil(model.results) + XCTAssertNil(model.suggestions) + // Valid `currentQuery` should produce non-nil results. model.currentQuery = "Coffee" - await model.commitSearch() - try XCTAssertNotNil(model.results.get()) + Task { model.commitSearch() } + + let results = try await model.$results.compactMap({$0}).first + XCTAssertNotNil(results) + // Changing the `currentQuery` should set results to nil. model.currentQuery = "Coffee in Portland" - try XCTAssertNil(model.results.get()) + XCTAssertNil(model.results) - await model.updateSuggestions() - try XCTAssertNotNil(model.suggestions.get()) + Task { model.updateSuggestions() } - // Changing the `currentQuery` should set results to nil. + let suggestions = try await model.$suggestions.compactMap({$0}).first + XCTAssertNotNil(suggestions) + + // Changing the `currentQuery` should set suggestions to nil. model.currentQuery = "Coffee in Edinburgh" - try XCTAssertNil(model.suggestions.get()) + XCTAssertNil(model.suggestions) // Changing current query after search with 1 result // should set `selectedResult` to nil model.currentQuery = "Magers & Quinn Bookseller" - await model.commitSearch() + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).first XCTAssertNotNil(model.selectedResult) model.currentQuery = "Hotel" XCTAssertNil(model.selectedResult) } - + func testQueryArea() async throws { let source = LocatorSearchSource() source.maximumResults = Int32.max @@ -200,14 +160,16 @@ class SearchViewModelTests: XCTestCase { // Set queryArea to Chippewa Falls model.queryArea = Polygon.chippewaFalls model.currentQuery = "Coffee" - await model.commitSearch() - var results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 9) + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) let resultGeometryUnion: Geometry = try XCTUnwrap( GeometryEngine.union( - geometries: results.compactMap{ $0.geoElement?.geometry } + geometries: result.compactMap{ $0.geoElement?.geometry } ) ) @@ -219,14 +181,28 @@ class SearchViewModelTests: XCTestCase { ) model.currentQuery = "Magers & Quinn Booksellers" - await model.commitSearch() - results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 0) + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) model.queryArea = Polygon.minneapolis - await model.commitSearch() - results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 1) + + Task { model.commitSearch() } + + // A note about the use of `.dropFirst()`: + // Because `model.results` is not changed between the previous call + // to `model.commitSearch()` and the one right above, the + // `try await model.$results...` call will return the last result + // received (from the first `model.commitSearch()` call), which is + // incorrect. Calling `.dropFirst()` will remove that one + // and will give us the next one, which is the correct one (the result + // from the second `model.commitSearch()` call). + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) } func testQueryCenter() async throws { @@ -235,9 +211,14 @@ class SearchViewModelTests: XCTestCase { // Set queryCenter to Portland model.queryCenter = .portland model.currentQuery = "Coffee" - await model.commitSearch() + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + var resultPoint = try XCTUnwrap( - model.results.get()?.first?.geoElement?.geometry as? Point + result.first?.geoElement?.geometry as? Point ) var geodeticDistance = try XCTUnwrap ( @@ -256,9 +237,15 @@ class SearchViewModelTests: XCTestCase { // Set queryCenter to Edinburgh model.queryCenter = .edinburgh model.currentQuery = "Restaurants" - await model.commitSearch() + + Task { model.commitSearch() } + + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + resultPoint = try XCTUnwrap( - model.results.get()?.first?.geoElement?.geometry as? Point + result.first?.geoElement?.geometry as? Point ) // Web Mercator distance between .edinburgh and first result. @@ -275,60 +262,80 @@ class SearchViewModelTests: XCTestCase { // First result within 100m of Edinburgh. XCTAssertLessThan(geodeticDistance.distance, 100) } - + func testSearchResultMode() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) XCTAssertEqual(model.resultMode, .automatic) model.resultMode = .single model.currentQuery = "Magers & Quinn" - await model.commitSearch() - var results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 1) + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) model.resultMode = .multiple - await model.commitSearch() - results = try XCTUnwrap(model.results.get()) - XCTAssertGreaterThan(results.count, 1) + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) model.currentQuery = "Coffee" - await model.updateSuggestions() - let suggestResults = try XCTUnwrap(model.suggestions.get()) - let collectionSuggestion = try XCTUnwrap(suggestResults.filter { $0.isCollection }.first) - let singleSuggestion = try XCTUnwrap(suggestResults.filter { !$0.isCollection }.first) + + Task { model.updateSuggestions() } + + let suggestionResults = try await model.$suggestions.compactMap({$0}).first + let suggestions = try XCTUnwrap(suggestionResults?.get()) + + let collectionSuggestion = try XCTUnwrap(suggestions.filter { $0.isCollection }.first) + let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) model.resultMode = .automatic - await model.acceptSuggestion(collectionSuggestion) - results = try XCTUnwrap(model.results.get()) - XCTAssertGreaterThan(results.count, 1) - await model.acceptSuggestion(singleSuggestion) - results = try XCTUnwrap(model.results.get()) - XCTAssertEqual(results.count, 1) + Task { model.acceptSuggestion(collectionSuggestion) } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + Task { model.acceptSuggestion(singleSuggestion) } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) } func testUpdateSuggestions() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) // No currentQuery - suggestions are nil. - try XCTAssertNil(model.suggestions.get()) + XCTAssertNil(model.suggestions) // UpdateSuggestions with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" - await model.updateSuggestions() - var results = try XCTUnwrap(model.suggestions.get()) - XCTAssertEqual(results.count, 0) + + Task { model.updateSuggestions() } + + var suggestionResults = try await model.$suggestions.compactMap({$0}).first + var suggestions = try XCTUnwrap(suggestionResults?.get()) + XCTAssertEqual(suggestions.count, 0) // UpdateSuggestions with results. model.currentQuery = "Magers & Quinn" - await model.updateSuggestions() - results = try XCTUnwrap(model.suggestions.get()) - XCTAssertGreaterThanOrEqual(results.count, 1) + + Task { model.updateSuggestions() } + + suggestionResults = try await model.$suggestions.compactMap({$0}).first + suggestions = try XCTUnwrap(suggestionResults?.get()) + XCTAssertGreaterThanOrEqual(suggestions.count, 1) XCTAssertNil(model.selectedResult) - try XCTAssertNil(model.results.get()) + XCTAssertNil(model.results) } - */ } extension Polygon { From 26bbd639445ab021b4395c249e15f23dd936bf01 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 8 Oct 2021 14:55:01 -0500 Subject: [PATCH 0181/1722] Fix indenting. --- .../Search/LocatorSearchSource.swift | 2 +- .../Components/Search/SearchViewModel.swift | 12 ++-- .../Search/SmartLocatorSearchSource.swift | 2 +- .../SearchViewModelTests.swift | 70 +++++++++---------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 8c5511a76..c4a2cf788 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -92,7 +92,7 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { ) async throws -> [SearchResult] { return try await internalSearch(queryString, queryArea: queryExtent) } - + public func search(_ queryString: String) async throws -> [SearchResult] { return try await internalSearch(queryString, queryArea: searchArea) } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 97b39b663..01ca4dd7a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -91,7 +91,7 @@ public class SearchViewModel: ObservableObject { let lastExtent = lastSearchExtent, let newExtent = newValue else { return } - + // Check extent difference. let widthDiff = fabs(lastExtent.width - newExtent.width) let heightDiff = fabs(lastExtent.height - newExtent.height) @@ -112,7 +112,7 @@ public class SearchViewModel: ObservableObject { isEligibleForRequery = (centerDiff ?? 0.0) > threshold } } - + /// True if the Extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. @@ -121,11 +121,11 @@ public class SearchViewModel: ObservableObject { /// at the time of the last search or if the extent width/height changes by the same amount. @Published public private(set) var isEligibleForRequery: Bool = false - + /// The search area to be used for the current query. Results will be limited to those /// within `QueryArea`. Defaults to `nil`. public var queryArea: Geometry? = nil - + /// Defines the center for the search. For most use cases, this should be updated by the view /// every time the user navigates the map. public var queryCenter: Point? @@ -180,7 +180,7 @@ public class SearchViewModel: ObservableObject { } } } - + /// The currently executing async task. `currentTask` should be cancelled /// prior to starting another async task. private var currentTask: Task? @@ -192,7 +192,7 @@ public class SearchViewModel: ObservableObject { guard var source = currentSource() else { return nil } source.searchArea = searchArea source.preferredSearchLocation = preferredSearchLocation - + return source } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 82652a9c5..e5fa490c6 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -66,7 +66,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search /// repeat behavior. public var repeatSuggestResultThreshold: Int = 6 - + public override func search( _ queryString: String ) async throws -> [SearchResult] { diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 079e2339e..18dcbd9d5 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -27,7 +27,7 @@ class SearchViewModelTests: XCTestCase { func testAcceptSuggestion() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) model.currentQuery = "Magers & Quinn Booksellers" - + Task { model.updateSuggestions() } // Get suggestion @@ -44,7 +44,7 @@ class SearchViewModelTests: XCTestCase { // With only one results, model should set `selectedResult` property. XCTAssertEqual(result.first!, model.selectedResult) } - + func testActiveSource() async throws { let activeSource = LocatorSearchSource() activeSource.displayName = "Simple Locator" @@ -67,7 +67,7 @@ class SearchViewModelTests: XCTestCase { let suggestion = try XCTUnwrap(suggestions?.get().first) XCTAssertEqual(suggestion.owningSource.displayName, activeSource.displayName) } - + func testCommitSearch() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -110,7 +110,7 @@ class SearchViewModelTests: XCTestCase { XCTAssertNil(model.selectedResult) XCTAssertNil(model.suggestions) } - + func testCurrentQuery() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -126,7 +126,7 @@ class SearchViewModelTests: XCTestCase { let results = try await model.$results.compactMap({$0}).first XCTAssertNotNil(results) - + // Changing the `currentQuery` should set results to nil. model.currentQuery = "Coffee in Portland" XCTAssertNil(model.results) @@ -151,7 +151,7 @@ class SearchViewModelTests: XCTestCase { model.currentQuery = "Hotel" XCTAssertNil(model.selectedResult) } - + func testQueryArea() async throws { let source = LocatorSearchSource() source.maximumResults = Int32.max @@ -189,9 +189,9 @@ class SearchViewModelTests: XCTestCase { XCTAssertEqual(result.count, 0) model.queryArea = Polygon.minneapolis - + Task { model.commitSearch() } - + // A note about the use of `.dropFirst()`: // Because `model.results` is not changed between the previous call // to `model.commitSearch()` and the one right above, the @@ -204,7 +204,7 @@ class SearchViewModelTests: XCTestCase { result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } - + func testQueryCenter() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) @@ -216,7 +216,7 @@ class SearchViewModelTests: XCTestCase { var results = try await model.$results.compactMap({$0}).first var result = try XCTUnwrap(results?.get()) - + var resultPoint = try XCTUnwrap( result.first?.geoElement?.geometry as? Point ) @@ -243,7 +243,7 @@ class SearchViewModelTests: XCTestCase { results = try await model.$results.compactMap({$0}).first result = try XCTUnwrap(results?.get()) - + resultPoint = try XCTUnwrap( result.first?.geoElement?.geometry as? Point ) @@ -262,7 +262,7 @@ class SearchViewModelTests: XCTestCase { // First result within 100m of Edinburgh. XCTAssertLessThan(geodeticDistance.distance, 100) } - + func testSearchResultMode() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) XCTAssertEqual(model.resultMode, .automatic) @@ -277,9 +277,9 @@ class SearchViewModelTests: XCTestCase { XCTAssertEqual(result.count, 1) model.resultMode = .multiple - + Task { model.commitSearch() } - + results = try await model.$results.compactMap({$0}).dropFirst().first result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) @@ -290,7 +290,7 @@ class SearchViewModelTests: XCTestCase { let suggestionResults = try await model.$suggestions.compactMap({$0}).first let suggestions = try XCTUnwrap(suggestionResults?.get()) - + let collectionSuggestion = try XCTUnwrap(suggestions.filter { $0.isCollection }.first) let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) @@ -317,9 +317,9 @@ class SearchViewModelTests: XCTestCase { // UpdateSuggestions with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" - + Task { model.updateSuggestions() } - + var suggestionResults = try await model.$suggestions.compactMap({$0}).first var suggestions = try XCTUnwrap(suggestionResults?.get()) XCTAssertEqual(suggestions.count, 0) @@ -328,7 +328,7 @@ class SearchViewModelTests: XCTestCase { model.currentQuery = "Magers & Quinn" Task { model.updateSuggestions() } - + suggestionResults = try await model.$suggestions.compactMap({$0}).first suggestions = try XCTUnwrap(suggestionResults?.get()) XCTAssertGreaterThanOrEqual(suggestions.count, 1) @@ -339,23 +339,23 @@ class SearchViewModelTests: XCTestCase { } extension Polygon { -static var chippewaFalls: Polygon { - let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) - return builder.toGeometry() as! ArcGIS.Polygon -} - -static var minneapolis: Polygon { - let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - return builder.toGeometry() as! ArcGIS.Polygon -} + static var chippewaFalls: Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) + return builder.toGeometry() as! ArcGIS.Polygon + } + + static var minneapolis: Polygon { + let builder = PolygonBuilder(spatialReference: .wgs84) + let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) + let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) + return builder.toGeometry() as! ArcGIS.Polygon + } } extension Point { From d87ff41284f4924d4b0de6c1eb81af18b1f8841d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 8 Oct 2021 16:53:31 -0500 Subject: [PATCH 0182/1722] Add tests for IsEligibleForRequery and RepeatSearch behavior. --- .../SearchViewModelTests.swift | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 18dcbd9d5..e87490e61 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -152,6 +152,63 @@ class SearchViewModelTests: XCTestCase { XCTAssertNil(model.selectedResult) } + func testIsEligibleForRequery() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.geoViewExtent = Polygon.chippewaFalls.extent + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).first + XCTAssertFalse(model.isEligibleForRequery) + + // Offset extent by 10% - isEligibleForRequery should still be `false`. + var builder = EnvelopeBuilder(envelope: model.geoViewExtent) + let tenPercentWidth = model.geoViewExtent!.width * 0.1 + builder.offsetBy(x: tenPercentWidth, y: 0.0) + var newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertFalse(model.isEligibleForRequery) + + // Offset extent by 50% - isEligibleForRequery should now be `true`. + builder = EnvelopeBuilder(envelope: model.geoViewExtent) + let fiftyPercentWidth = model.geoViewExtent!.width * 0.5 + builder.offsetBy(x: fiftyPercentWidth, y: 0.0) + newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertTrue(model.isEligibleForRequery) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.geoViewExtent = Polygon.chippewaFalls.extent + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).dropFirst().first + XCTAssertFalse(model.isEligibleForRequery) + + // Expand extent by 1.1x - isEligibleForRequery should still be `false`. + builder = EnvelopeBuilder(envelope: model.geoViewExtent) + builder.expand(factor: 1.1) + newExtent = builder.toGeometry() as! Envelope + + 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) + newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertTrue(model.isEligibleForRequery) + } + func testQueryArea() async throws { let source = LocatorSearchSource() source.maximumResults = Int32.max @@ -263,6 +320,49 @@ class SearchViewModelTests: XCTestCase { XCTAssertLessThan(geodeticDistance.distance, 100) } + func testRepeatSearch() async throws { + let model = SearchViewModel(sources: [LocatorSearchSource()]) + + // Set queryArea to Chippewa Falls + model.geoViewExtent = Polygon.chippewaFalls.extent + model.currentQuery = "Coffee" + + Task { model.repeatSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + let resultGeometryUnion: Geometry = try XCTUnwrap( + GeometryEngine.union( + geometries: result.compactMap{ $0.geoElement?.geometry } + ) + ) + + XCTAssertTrue( + GeometryEngine.contains( + geometry1: model.geoViewExtent!, + geometry2: resultGeometryUnion + ) + ) + + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.repeatSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) + + model.geoViewExtent = Polygon.minneapolis.extent + + Task { model.repeatSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + } + func testSearchResultMode() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) XCTAssertEqual(model.resultMode, .automatic) From a418c117ef0bd83dd4e12b1813da1f82ec1adb63 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 8 Oct 2021 16:54:40 -0500 Subject: [PATCH 0183/1722] rename model.extent to model.geoViewExtent; move precondition checks for search/suggestion methods into Tasks, so the model can correctly cancel tasks if necessary. --- Examples/Examples/SearchExampleView.swift | 4 +- .../Components/Search/SearchViewModel.swift | 57 ++++++++++--------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index f543ac65d..40ee448f2 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -62,8 +62,8 @@ struct SearchExampleView: View { // For "Repeat Search Here" behavior, set the // `searchViewModel.extent` property when navigating. - if isNavigating || searchViewModel.extent == nil { - searchViewModel.extent = newValue.extent + if isNavigating || searchViewModel.geoViewExtent == nil { + searchViewModel.geoViewExtent = newValue.extent } } .overlay(alignment: .topTrailing) { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 01ca4dd7a..d0b468e17 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -84,7 +84,7 @@ public class SearchViewModel: ObservableObject { /// The current GeoView extent. Defaults to null. This should be updated as the user navigates /// the map/scene. It will be used to determine the value of `IsEligibleForRequery` /// for the 'Repeat search here' behavior. If that behavior is not wanted, it should be left `nil`. - public var extent: Envelope? = nil { + public var geoViewExtent: Envelope? = nil { willSet { guard !isEligibleForRequery, !currentQuery.isEmpty, @@ -116,7 +116,7 @@ public class SearchViewModel: ObservableObject { /// True if the Extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. - /// Changes to this property are driven by changes to the `Extent` property. This value will be + /// Changes to this property are driven by changes to the `geoViewExtent` property. This value will be /// true if the extent center changes by more than 25% of the average of the extent's height and width /// at the time of the last search or if the extent width/height changes by the same amount. @Published @@ -201,37 +201,22 @@ public class SearchViewModel: ObservableObject { /// - Parameter searchArea: geometry used to constrain the results. If `nil`, the /// `queryArea` property is used instead. If `queryArea` is `nil`, results are not constrained. public func commitSearch() { - guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { - return - } - // TODO: do we want the above in the `searchTask()` method?? - kickoffTask(searchTask(source)) + kickoffTask(searchTask()) } - /// Repeats the last search, limiting results to the extent specified in `extent`. + /// Repeats the last search, limiting results to the extent specified in `geoViewExtent`. public func repeatSearch() { - guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let queryExtent = extent, - let source = makeEffectiveSource(with: extent, preferredSearchLocation: nil) else { - return - } - - kickoffTask(repeatSearchTask(source, extent: queryExtent)) + kickoffTask(repeatSearchTask()) } /// Updates suggestions list asynchronously. public func updateSuggestions() { - guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { - return - } guard currentSuggestion == nil else { // don't update suggestions if currently searching for one return } - kickoffTask(updateSuggestionsTask(source)) + kickoffTask(updateSuggestionsTask()) } @Published @@ -267,12 +252,18 @@ public class SearchViewModel: ObservableObject { } extension SearchViewModel { - private func repeatSearchTask(_ source: SearchSourceProtocol, extent: Envelope) -> Task<(), Never> { + private func repeatSearchTask() -> Task<(), Never> { Task { + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, + let queryExtent = geoViewExtent, + let source = makeEffectiveSource(with: queryExtent, preferredSearchLocation: nil) else { + return + } + do { // User is performing a search, so set `lastSearchExtent`. - lastSearchExtent = extent - try await process(searchResults: source.repeatSearch(currentQuery, queryExtent: extent)) + lastSearchExtent = geoViewExtent + try await process(searchResults: source.repeatSearch(currentQuery, queryExtent: queryExtent)) } catch is CancellationError { results = nil } catch { @@ -281,11 +272,16 @@ extension SearchViewModel { } } - private func searchTask(_ source: SearchSourceProtocol) -> Task<(), Never> { + private func searchTask() -> Task<(), Never> { Task { + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, + let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { + return + } + do { // User is performing a search, so set `lastSearchExtent`. - lastSearchExtent = extent + lastSearchExtent = geoViewExtent try await process(searchResults: source.search(currentQuery)) } catch is CancellationError { results = nil @@ -295,8 +291,13 @@ extension SearchViewModel { } } - private func updateSuggestionsTask(_ source: SearchSourceProtocol) -> Task<(), Never> { + private func updateSuggestionsTask() -> Task<(), Never> { Task { + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, + let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { + return + } + let suggestResult = await Result { try await source.suggest(currentQuery) } @@ -318,7 +319,7 @@ extension SearchViewModel { Task { do { // User is performing a search, so set `lastSearchExtent`. - lastSearchExtent = extent + lastSearchExtent = geoViewExtent try await process(searchResults: searchSuggestion.owningSource.search(searchSuggestion)) } catch is CancellationError { results = nil From 04c0d21de9a56c60cc43a3accfc7ab850d923192 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 11 Oct 2021 10:54:23 -0500 Subject: [PATCH 0184/1722] Add selected row indicator; add selectedResult test steps and fix to clear selected result on new results. --- .../Components/Search/SearchView.swift | 13 ++++++++++--- .../Components/Search/SearchViewModel.swift | 3 +++ Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 10 ++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index b245b3412..8acb16151 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -217,10 +217,17 @@ struct SearchResultList: View { // Only show the list if we have more than one result. PlainList { ForEach(results) { result in - SearchResultRow(result: result) - .onTapGesture { - selectedResult = result + HStack { + SearchResultRow(result: result) + .onTapGesture { + selectedResult = result + } + if result == selectedResult { + Spacer() + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) } + } } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index d0b468e17..6ac7fc30e 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -150,6 +150,9 @@ public class SearchViewModel: ObservableObject { if results.count == 1 { selectedResult = results.first } + else { + selectedResult = nil + } default: selectedResult = nil } diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index e87490e61..77bde66fb 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -109,6 +109,16 @@ class SearchViewModelTests: XCTestCase { XCTAssertNil(model.selectedResult) XCTAssertNil(model.suggestions) + + model.selectedResult = result.first! + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + XCTAssertNil(model.selectedResult) } func testCurrentQuery() async throws { From f0412e29e68c918d1504bdf812b60f214efb8053 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 12 Oct 2021 17:37:37 -0500 Subject: [PATCH 0185/1722] More gallery features; initial test files --- .../Examples/BasemapGalleryExampleView.swift | 32 +- .../basemap.imageset/Contents.json | 29 + .../basemap.imageset/basemap@1x.png | Bin 0 -> 446 bytes .../basemap.imageset/basemap@2x.png | Bin 0 -> 599 bytes .../basemap.imageset/basemap@3x.png | Bin 0 -> 1077 bytes .../BasemapGallery/BasemapGallery.swift | 35 +- .../BasemapGallery/BasemapGalleryItem.swift | 19 +- .../BasemapGalleryViewModel.swift | 172 +++--- .../BasemapGalleryItemTests.swift | 501 ++++++++++++++++++ .../BasemapGalleryViewModelTests.swift | 501 ++++++++++++++++++ 10 files changed, 1164 insertions(+), 125 deletions(-) create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@3x.png create mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift create mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index ab5a22122..742d180a1 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -28,7 +28,7 @@ struct BasemapGalleryExampleView: View { basemap: Basemap(style: .arcGISNova), name: "ArcGIS Nova", description: "A vector basemap for the world featuring a dark background with glowing blue symbology inspired by science-fiction and futuristic themes.", - thumbnail: UIImage(named: "Nova") + thumbnail: nil//UIImage(named: "Nova") // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/90f86b329f37499096d3715ac6e5ed1f/info/thumbnail/thumbnail1607555507609.jpeg")! ), BasemapGalleryItem( @@ -92,9 +92,6 @@ struct BasemapGalleryExampleView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass - // TODO: figure out of viewmodel stuff is OK - // TODO: get Portal loading and working - var galleryWidth: CGFloat { get { if horizontalSizeClass == .regular { @@ -111,14 +108,6 @@ struct BasemapGalleryExampleView: View { MapView(map: map, viewpoint: initialViewpoint) .overlay( VStack(alignment: .trailing) { - Button { - showBasemapGallery.toggle() - } label: { - HStack(alignment: .center) { - Image(systemName: "map.fill") - Text(showBasemapGallery ? "Hide Basemaps" : "Show Basemaps") - } - } if showBasemapGallery { BasemapGallery(viewModel: viewModel) .basemapGalleryStyle(.automatic) @@ -132,19 +121,20 @@ struct BasemapGalleryExampleView: View { SetupViewModel() } } + .navigationTitle("Basemap Gallery") + .navigationBarItems(trailing: Button { + showBasemapGallery.toggle() + } label: { + HStack(alignment: .center) { + Image(uiImage: UIImage(named: "basemap")!) + Text(showBasemapGallery ? "Hide Basemaps" : "Show Basemaps") + } + }) } private func SetupViewModel() { viewModel.geoModel = map viewModel.basemapGalleryItems = basemapGalleryItems -// viewModel.portal = Portal.arcGISOnline(loginRequired: false) + viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) } } - -struct BasemapGalleryExampleView_Previews: PreviewProvider { - static var previews: some View { - BasemapGalleryExampleView() - } -} - -// MARK: Extensions diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json new file mode 100644 index 000000000..800e56381 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json @@ -0,0 +1,29 @@ +{ + "images" : [ + { + "idiom" : "universal" + }, + { + "filename" : "basemap@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "basemap@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "basemap@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..3182e883959bc5e2d961ef1221366f47c71be440 GIT binary patch literal 446 zcmV;v0YUzWP)K7t?^Xkd1I05KN>UtsYkG0>o=$G*+X>?qvG0}VArRd=82sx$D`a`k+m*EVmg zNVrBH-Na7ORgGQDp;Oo?qJQ9ak2t8L^}KXD;wQynVH zlG^cr7vVo}T737hBB<$h!vQI}Dmp8^!IAK@NpX%#LHOEWr^*=_Jl7;GVZP-7X^Bci o1|97OE=8WxN&Ev{G_*l`2hI7upU5cWssI2007*qoM6N<$g1@NEYybcN literal 0 HcmV?d00001 diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..708fcaeca1d778a10b911a1ceb341f7823cbc4f4 GIT binary patch literal 599 zcmV-d0;v6oP)V0Y^(%(#nxM6;RV4)Q9)UB$6}b>W1NhMo7v64{1BMU{BzFvNOmSk0SIuff~Mbj zt~S>Jo2|?Pv@5^m&jgm6?tyinPe2@NynntWG>NU}x(z_oE(JsVhmTn4)Cb-ulWd271wvb zH1$7vA8@Gn7a9OCk07eapk91_1tf>GtDgz&_2GE_%nW_v-0k^=WK^)s$I)5NL8v!s$X6rHW l*ZMQRn9Fx$tybm%z5(?{11frOuf6~P002ovPDHLkV1iAm1S

If6+C<%(P#*K&Gg}3v1xBJb` z?9QzHhdE5W>h5|)cTIIo51d`WYaq>FC2d@rVc`Fo;-Cb@K?&AHWS`?$MYcafCx>c3 zb$PLJe-m~kbaIIGlNC7Y1lJf%1?f6EIgIp^yTqm|_Zw>(&^vM+bh>hXNz;aOm0Sn? zSh>HvX+ye7EJ(-r2GAI*+4J}cpEYSz4US1ChB?V(?L={o*CP7AVn@Qb zI%CZ-h_tIiPCp|$*dvSx1b)Qq$z%H;@MePq9#{q1rw7X4sy7jrDm>erLgU-jbcn;qQGh&YS zF@di${$y_%XNg^m_sowmCvYPkHyXTG1>?q`i!fP{&EQpR7h=;C{#?3g-5^y_+>D2@ ze(3kz!Yul}(ry}W&0gnXC$11SwHIK6_9}|4k_w#nM06f&X{;E7NV~Qe87pUrO+{o| z8#JoZow)IPX=uzVSPvxlUJER35tUf6bB_(XPJ|(3OTQ@bE<`fLZMJ7 z6bglgS);isUmbGJI!0dyFDGR;Uv~5LDGo|d9F!pDpo2p2ZsTf-w(L7t9jMHp1jRuK z>N;qBT4i-G&ikzi;f&%fxEg2hi*QD95}!0~-NLk$+(r}9RPuH+K8om7uUCZk?~Adj z62%R8SvW*~YsMdWLYPgLX6W{Yg&ed+IBNHl@gC1R@F}(}(XP%Ai$R<5p@on3p2X`y z47xj_w@o-Vz1!#!nO;&|cQX;$S^R`YgtI?$tQBI!zY+Z(aA9JsL56*UNV_`Z^fRIp zTVk;#bp+So4g51CpPB9SAz?-hvmJ*P=&v`V@}M(#6!+ryMfBdpgXTLA?i3DS^_ImP zG%e&khcf#*f_)MB0X?2YizSMC@b;MfP2zUkF(lhLd;h_heJz!0GFiJS84JjV>)T}_ z(%&gzMlNTN&PfE5W5t%(31Rls<+zY}53|Yqr(bV~Iq0y_ubju_g(bG{3~iM=jtYm% zdrzD7pgnk2`16y7#X$*TiQ+CR`#L&@ vI`+j9#jIp<#j16BUQsM4F(^rKP=fyeBxGdq<~Ib#00000NkvXXu0mjfI}r8H literal 0 HcmV?d00001 diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 36ae09146..25650e4f2 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -52,9 +52,9 @@ public struct BasemapGallery: View { public var body: some View { GalleryView() - .task { - await viewModel.fetchBasemaps() - } +// .onAppear { +// viewModel.fetchBasemaps() +// } } // MARK: Modifiers @@ -110,10 +110,11 @@ extension BasemapGallery { LazyVGrid(columns: columns, spacing: 4) { ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow( - basemapGalleryItem: basemapGalleryItem + basemapGalleryItem: basemapGalleryItem, + currentItem: viewModel.currentBasemapGalleryItem ) .onTapGesture { - viewModel.geoModel?.basemap = basemapGalleryItem.basemap + viewModel.currentBasemapGalleryItem = basemapGalleryItem } } } @@ -124,20 +125,22 @@ extension BasemapGallery { private struct BasemapGalleryItemRow: View { var basemapGalleryItem: BasemapGalleryItem + var currentItem: BasemapGalleryItem? var body: some View { - VStack { - if let thumbnailImage = basemapGalleryItem.thumbnail { - // TODO: thumbnail will have to be loaded. - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) + HStack (alignment: .center) { + VStack { + if let thumbnailImage = basemapGalleryItem.thumbnail { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .border( + basemapGalleryItem == currentItem ? Color.accentColor: Color.clear, + width: 3.0) + } + Text(basemapGalleryItem.name) + .font(.footnote) } - Text(basemapGalleryItem.name) - .font(.footnote) } -// if basemapGalleryItem == currentBasemap { -// .background(Color.accentColor) -// } } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 18fba0b2c..e686c1396 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -18,6 +18,10 @@ import Foundation /// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. public struct BasemapGalleryItem { + static var defaultThumbnail: UIImage { + return UIImage(named: "basemap")! + } + public init( basemap: Basemap, name: String = "", @@ -25,26 +29,27 @@ public struct BasemapGalleryItem { thumbnail: UIImage? = nil ) { self.basemap = basemap - self.name = name - self.description = description - self.thumbnail = thumbnail + self.name = name.isEmpty ? basemap.name : name + self.description = description ?? basemap.item?.description + self.thumbnail = thumbnail ?? + (basemap.item?.thumbnail?.image ?? BasemapGalleryItem.defaultThumbnail) } /// The basemap this `BasemapGalleryItem` represents. - public var basemap: Basemap + public private(set) var basemap: Basemap /// The name of this `Basemap`. - public var name: String + public private(set) var name: String /// The description which will be used in the gallery. - public var description: String? + public private(set) var description: String? /// The thumbnail which will be displayed in the gallery. public let thumbnail: UIImage? } extension BasemapGalleryItem: Identifiable { - public var id: String { name } + public var id: ObjectIdentifier { ObjectIdentifier(basemap) } } extension BasemapGalleryItem: Equatable { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 3215f7b85..dc291b955 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -17,7 +17,13 @@ import ArcGIS import Combine /// Manages the state for a `BasemapGallery`. +@MainActor public class BasemapGalleryViewModel: ObservableObject { + /// Creates a `BasemapGalleryViewModel`. + /// - Parameters: + /// - currentBasemap: The `Basemap` currently used by a `GeoModel`. + /// - portal: The `Portal` to load base maps from. + /// - basemapGalleryItems: A list of pre-defined base maps to display. public init( geoModel: GeoModel? = nil, portal: Portal? = nil, @@ -25,110 +31,114 @@ public class BasemapGalleryViewModel: ObservableObject { ) { self.geoModel = geoModel self.portal = portal - self.basemapGalleryItems = basemapGalleryItems + self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) + + loadGeoModel() + fetchBasemaps() } -// -// /// Creates a `BasemapGalleryViewModel`. Generates a list of appropriate, default basemaps. -// /// The given default basemaps require either an API key or named-user to be signed into the app. -// /// These basemaps are sourced from this PortalGroup: -// /// https://www.arcgis.com/home/group.html?id=a25523e2241d4ff2bcc9182cc971c156). -// /// `BasemapGalleryViewModel.currentBasemap` is set to the basemap of the given -// /// geoModel if not `nil`. -// /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. -// public init(geoModel: GeoModel? = nil) { -// self.geoModel = geoModel -// self.currentBasemap = geoModel?.basemap -// self.portal = Portal.arcGISOnline(loginRequired: false) -// } -// -// /// Creates a `BasemapGalleryViewModel`. Uses the given `portal` to retrieve basemaps. -// /// `BasemapGalleryViewModel.currentBasemap` is set to the basemap of the given -// /// geoModel if not `nil`. -// /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. -// /// - Parameter portal: The `GeoModel` we're selecting the basemap for. -// public init( -// geoModel: GeoModel? = nil, -// portal: Portal -// ) { -// self.geoModel = geoModel -// self.currentBasemap = geoModel?.basemap -// self.portal = portal -// } -// -// /// Creates a `BasemapGalleryViewModel`. Uses the given list of basemap gallery items. -// /// `BasemapGalleryViewModel.currentBasemap` is set to the basemap of the given -// /// geoModel if not `nil`. -// /// - Parameter geoModel: The `GeoModel` we're selecting the basemap for. -// /// - Parameter basemapGalleryItems: The `GeoModel` we're selecting the basemap for. -// public init( -// geoModel: GeoModel? = nil, -// basemapGalleryItems: [BasemapGalleryItem] = [] -// ) { -// self.geoModel = geoModel -// self.currentBasemap = geoModel?.basemap -// self.basemapGalleryItems = basemapGalleryItems -// } - /// 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, /// the geoModel will have its basemap replaced with the selected basemap. - public var geoModel: GeoModel? = nil + public var geoModel: GeoModel? = nil { + didSet { + loadGeoModel() + } + } + + /// The `Portal` object, if any. Setting the portal will automatically fetch it's base maps + /// and add them to the `basemapGalleryItems` array. + public var portal: Portal? = nil { + didSet { + fetchBasemaps() + } + } - /// The `Portal` object, if any. - public var portal: Portal? = nil - - @Published /// The list of basemaps currently visible in the gallery. Items added or removed from this list will /// update the gallery. + @Published public var basemapGalleryItems: [BasemapGalleryItem] = [] - /// Currently applied basemap on the associated `GeoModel`. This may be a basemap - /// which does not exist in the gallery. - public var currentBasemap: Basemap? { - get async throws { - guard let geoModel = geoModel else { return nil } - try await geoModel.load() - return geoModel.basemap + /// `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a + /// basemap which does not exist in the gallery. + @Published + public var currentBasemapGalleryItem: BasemapGalleryItem? = nil { + didSet { + guard let item = currentBasemapGalleryItem else { return } + geoModel?.basemap = item.basemap } } - - /// The currently executing async task. `currentTask` should be cancelled - /// prior to starting another async task. - private var currentTask: Task? = nil - /// Updates suggestions list asynchronously. - public func fetchBasemaps() async -> Void { - guard let portal = portal else { return } - - currentTask?.cancel() - currentTask = fetchBasemapsTask(portal) - await currentTask?.value + // TODO: write tests to check on loading stuff, setting portal and other props, etc. + // TODO: Change type of `Task` so I don't need to wrap operation in a Result. + + /// The currently executing async task for fetching basemaps from the portal. + /// `fetchBasemapTask` should be cancelled prior to starting another async task. + private var fetchBasemapTask: Task? = nil + + /// Fetches the basemaps from `portal`. + private func fetchBasemaps() { + fetchBasemapTask?.cancel() + fetchBasemapTask = fetchBasemapsTask(portal) + } + + /// The currently executing async task for loading `geoModel`. + /// `loadGeoModelTask` should be cancelled prior to starting another async task. + private var loadGeoModelTask: Task? = nil + + /// Loads `geoModel`. + private func loadGeoModel() { + loadGeoModelTask?.cancel() + loadGeoModelTask = loadGeoModelTask(geoModel) } } extension BasemapGalleryViewModel { - private func fetchBasemapsTask(_ portal: Portal) -> Task<(), Never> { - let task = Task(operation: { + private func fetchBasemapsTask(_ portal: Portal?) -> Task<(), Never>? { + guard let portal = portal else { return nil } + + return Task(operation: { let basemapResults = await Result { - try await portal.fetchDeveloperBasemaps() + try await portal.developerBasemaps } - DispatchQueue.main.async { [weak self] in - switch basemapResults { - case .success(let basemaps): - self?.basemapGalleryItems = basemaps.map { BasemapGalleryItem(basemap: $0) } - case .failure(_): - self?.basemapGalleryItems = [] - case .none: - self?.basemapGalleryItems = [] + switch basemapResults { + case .success(let basemaps): + basemaps.forEach { basemap in + Task { + try await basemap.load() + if let loadableImage = basemap.item?.thumbnail { + try await loadableImage.load() + } + basemapGalleryItems.append(BasemapGalleryItem(basemap: basemap)) + } } + case .failure(_), .none: + basemapGalleryItems = [] + } + }) + } + + private func loadGeoModelTask(_ geoModel: GeoModel?) -> Task<(), Never>? { + guard let geoModel = geoModel else { return nil } + + return Task(operation: { + let loadResult = await Result { + try await geoModel.load() + } + + switch loadResult { + case .success(_): + if let basemap = geoModel.basemap { + currentBasemapGalleryItem = BasemapGalleryItem(basemap: basemap) + } + else { + fallthrough + } + case .failure(_), .none: + currentBasemapGalleryItem = nil } }) - return task } -} - -extension BasemapGalleryViewModel { } diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift new file mode 100644 index 000000000..1fb428bbf --- /dev/null +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -0,0 +1,501 @@ +// Copyright 2021 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 Foundation + +import XCTest +import ArcGIS +import ArcGISToolkit +import SwiftUI +import Combine + +@MainActor +class BasemapGalleryItemTests: XCTestCase { + // + // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/Search/Search_Test_Design.md + // + func testInit() async throws { + + } + + func testPortal() async throws { + + } + + func testInitialBasemapItems() async throws { + + } + + func testGeoModel() async throws { + + } + + func testCurrentBasemapItem() async throws { + + } + + + /* + + + + func testAcceptSuggestion() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.updateSuggestions() } + + // Get suggestion + let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestions?.get().first) + + Task { model.acceptSuggestion(suggestion) } + + let results = try await model.$results.compactMap({$0}).first + let result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + XCTAssertNil(model.suggestions) + + // With only one results, model should set `selectedResult` property. + XCTAssertEqual(result.first!, model.selectedResult) + } + + func testActiveSource() async throws { + let activeSource = LocatorSearchSource() + activeSource.displayName = "Simple Locator" + let model = BasemapGalleryViewModel( + activeSource: activeSource, + sources: [LocatorSearchSource()] + ) + + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.commitSearch() } + + let results = try await model.$results.compactMap({$0}).first + let result = try XCTUnwrap(results?.get().first) + XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) + + Task { model.updateSuggestions() } + + let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestions?.get().first) + XCTAssertEqual(suggestion.owningSource.displayName, activeSource.displayName) + } + + func testCommitSearch() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // No search - results are nil. + XCTAssertNil(model.results) + + // Search with no results - result count is 0. + model.currentQuery = "No results found blah blah blah blah" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) + XCTAssertNil(model.selectedResult) + XCTAssertNil(model.suggestions) + + // Search with one result. + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + + // One results automatically populates `selectedResult`. + XCTAssertEqual(result.first!, model.selectedResult) + XCTAssertNil(model.suggestions) + + // Search with multiple results. + model.currentQuery = "Magers & Quinn" + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + XCTAssertNil(model.selectedResult) + XCTAssertNil(model.suggestions) + + model.selectedResult = result.first! + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + XCTAssertNil(model.selectedResult) + } + + func testCurrentQuery() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Empty `currentQuery` should produce nil results and suggestions. + model.currentQuery = "" + XCTAssertNil(model.results) + XCTAssertNil(model.suggestions) + + // Valid `currentQuery` should produce non-nil results. + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + let results = try await model.$results.compactMap({$0}).first + XCTAssertNotNil(results) + + // Changing the `currentQuery` should set results to nil. + model.currentQuery = "Coffee in Portland" + XCTAssertNil(model.results) + + Task { model.updateSuggestions() } + + let suggestions = try await model.$suggestions.compactMap({$0}).first + XCTAssertNotNil(suggestions) + + // Changing the `currentQuery` should set suggestions to nil. + model.currentQuery = "Coffee in Edinburgh" + XCTAssertNil(model.suggestions) + + // Changing current query after search with 1 result + // should set `selectedResult` to nil + model.currentQuery = "Magers & Quinn Bookseller" + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).first + XCTAssertNotNil(model.selectedResult) + model.currentQuery = "Hotel" + XCTAssertNil(model.selectedResult) + } + + func testIsEligibleForRequery() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.geoViewExtent = Polygon.chippewaFalls.extent + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).first + XCTAssertFalse(model.isEligibleForRequery) + + // Offset extent by 10% - isEligibleForRequery should still be `false`. + var builder = EnvelopeBuilder(envelope: model.geoViewExtent) + let tenPercentWidth = model.geoViewExtent!.width * 0.1 + builder.offsetBy(x: tenPercentWidth, y: 0.0) + var newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertFalse(model.isEligibleForRequery) + + // Offset extent by 50% - isEligibleForRequery should now be `true`. + builder = EnvelopeBuilder(envelope: model.geoViewExtent) + let fiftyPercentWidth = model.geoViewExtent!.width * 0.5 + builder.offsetBy(x: fiftyPercentWidth, y: 0.0) + newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertTrue(model.isEligibleForRequery) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.geoViewExtent = Polygon.chippewaFalls.extent + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).dropFirst().first + XCTAssertFalse(model.isEligibleForRequery) + + // Expand extent by 1.1x - isEligibleForRequery should still be `false`. + builder = EnvelopeBuilder(envelope: model.geoViewExtent) + builder.expand(factor: 1.1) + newExtent = builder.toGeometry() as! Envelope + + 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) + newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertTrue(model.isEligibleForRequery) + } + + func testQueryArea() async throws { + let source = LocatorSearchSource() + source.maximumResults = Int32.max + let model = BasemapGalleryViewModel(sources: [source]) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + let resultGeometryUnion: Geometry = try XCTUnwrap( + GeometryEngine.union( + geometries: result.compactMap{ $0.geoElement?.geometry } + ) + ) + + XCTAssertTrue( + GeometryEngine.contains( + geometry1: model.queryArea!, + geometry2: resultGeometryUnion + ) + ) + + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) + + model.queryArea = Polygon.minneapolis + + Task { model.commitSearch() } + + // A note about the use of `.dropFirst()`: + // Because `model.results` is not changed between the previous call + // to `model.commitSearch()` and the one right above, the + // `try await model.$results...` call will return the last result + // received (from the first `model.commitSearch()` call), which is + // incorrect. Calling `.dropFirst()` will remove that one + // and will give us the next one, which is the correct one (the result + // from the second `model.commitSearch()` call). + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + } + + func testQueryCenter() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Set queryCenter to Portland + model.queryCenter = .portland + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + + var resultPoint = try XCTUnwrap( + result.first?.geoElement?.geometry as? Point + ) + + var geodeticDistance = try XCTUnwrap ( + GeometryEngine.distanceGeodetic( + point1: .portland, + point2: resultPoint, + distanceUnit: .meters, + azimuthUnit: nil, + curveType: .geodesic + ) + ) + + // First result within 1500m of Portland. + XCTAssertLessThan(geodeticDistance.distance, 1500.0) + + // Set queryCenter to Edinburgh + model.queryCenter = .edinburgh + model.currentQuery = "Restaurants" + + Task { model.commitSearch() } + + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + + resultPoint = try XCTUnwrap( + result.first?.geoElement?.geometry as? Point + ) + + // Web Mercator distance between .edinburgh and first result. + geodeticDistance = try XCTUnwrap ( + GeometryEngine.distanceGeodetic( + point1: .edinburgh, + point2: resultPoint, + distanceUnit: .meters, + azimuthUnit: nil, + curveType: .geodesic + ) + ) + + // First result within 100m of Edinburgh. + XCTAssertLessThan(geodeticDistance.distance, 100) + } + + func testRepeatSearch() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Set queryArea to Chippewa Falls + model.geoViewExtent = Polygon.chippewaFalls.extent + model.currentQuery = "Coffee" + + Task { model.repeatSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + let resultGeometryUnion: Geometry = try XCTUnwrap( + GeometryEngine.union( + geometries: result.compactMap{ $0.geoElement?.geometry } + ) + ) + + XCTAssertTrue( + GeometryEngine.contains( + geometry1: model.geoViewExtent!, + geometry2: resultGeometryUnion + ) + ) + + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.repeatSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) + + model.geoViewExtent = Polygon.minneapolis.extent + + Task { model.repeatSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + } + + func testSearchResultMode() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + XCTAssertEqual(model.resultMode, .automatic) + + model.resultMode = .single + model.currentQuery = "Magers & Quinn" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + + model.resultMode = .multiple + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + model.currentQuery = "Coffee" + + Task { model.updateSuggestions() } + + let suggestionResults = try await model.$suggestions.compactMap({$0}).first + let suggestions = try XCTUnwrap(suggestionResults?.get()) + + let collectionSuggestion = try XCTUnwrap(suggestions.filter { $0.isCollection }.first) + let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) + + model.resultMode = .automatic + + Task { model.acceptSuggestion(collectionSuggestion) } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + Task { model.acceptSuggestion(singleSuggestion) } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + } + + func testUpdateSuggestions() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // No currentQuery - suggestions are nil. + XCTAssertNil(model.suggestions) + + // UpdateSuggestions with no results - result count is 0. + model.currentQuery = "No results found blah blah blah blah" + + Task { model.updateSuggestions() } + + var suggestionResults = try await model.$suggestions.compactMap({$0}).first + var suggestions = try XCTUnwrap(suggestionResults?.get()) + XCTAssertEqual(suggestions.count, 0) + + // UpdateSuggestions with results. + model.currentQuery = "Magers & Quinn" + + Task { model.updateSuggestions() } + + suggestionResults = try await model.$suggestions.compactMap({$0}).first + suggestions = try XCTUnwrap(suggestionResults?.get()) + XCTAssertGreaterThanOrEqual(suggestions.count, 1) + + XCTAssertNil(model.selectedResult) + XCTAssertNil(model.results) + } + */ +} + +//extension Polygon { +// static var chippewaFalls: Polygon { +// let builder = PolygonBuilder(spatialReference: .wgs84) +// let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) +// return builder.toGeometry() as! ArcGIS.Polygon +// } +// +// static var minneapolis: Polygon { +// let builder = PolygonBuilder(spatialReference: .wgs84) +// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) +// return builder.toGeometry() as! ArcGIS.Polygon +// } +//} +// +//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) +//} +// diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift new file mode 100644 index 000000000..8a43eb57f --- /dev/null +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -0,0 +1,501 @@ +// Copyright 2021 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 Foundation + +import XCTest +import ArcGIS +import ArcGISToolkit +import SwiftUI +import Combine + +@MainActor +class BasemapGalleryViewModelTests: XCTestCase { + // + // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/Search/Search_Test_Design.md + // + func testInit() async throws { + + } + + func testPortal() async throws { + + } + + func testInitialBasemapItems() async throws { + + } + + func testGeoModel() async throws { + + } + + func testCurrentBasemapItem() async throws { + + } + + + /* + + + + func testAcceptSuggestion() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.updateSuggestions() } + + // Get suggestion + let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestions?.get().first) + + Task { model.acceptSuggestion(suggestion) } + + let results = try await model.$results.compactMap({$0}).first + let result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + XCTAssertNil(model.suggestions) + + // With only one results, model should set `selectedResult` property. + XCTAssertEqual(result.first!, model.selectedResult) + } + + func testActiveSource() async throws { + let activeSource = LocatorSearchSource() + activeSource.displayName = "Simple Locator" + let model = BasemapGalleryViewModel( + activeSource: activeSource, + sources: [LocatorSearchSource()] + ) + + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.commitSearch() } + + let results = try await model.$results.compactMap({$0}).first + let result = try XCTUnwrap(results?.get().first) + XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) + + Task { model.updateSuggestions() } + + let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestion = try XCTUnwrap(suggestions?.get().first) + XCTAssertEqual(suggestion.owningSource.displayName, activeSource.displayName) + } + + func testCommitSearch() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // No search - results are nil. + XCTAssertNil(model.results) + + // Search with no results - result count is 0. + model.currentQuery = "No results found blah blah blah blah" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) + XCTAssertNil(model.selectedResult) + XCTAssertNil(model.suggestions) + + // Search with one result. + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + + // One results automatically populates `selectedResult`. + XCTAssertEqual(result.first!, model.selectedResult) + XCTAssertNil(model.suggestions) + + // Search with multiple results. + model.currentQuery = "Magers & Quinn" + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + XCTAssertNil(model.selectedResult) + XCTAssertNil(model.suggestions) + + model.selectedResult = result.first! + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + XCTAssertNil(model.selectedResult) + } + + func testCurrentQuery() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Empty `currentQuery` should produce nil results and suggestions. + model.currentQuery = "" + XCTAssertNil(model.results) + XCTAssertNil(model.suggestions) + + // Valid `currentQuery` should produce non-nil results. + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + let results = try await model.$results.compactMap({$0}).first + XCTAssertNotNil(results) + + // Changing the `currentQuery` should set results to nil. + model.currentQuery = "Coffee in Portland" + XCTAssertNil(model.results) + + Task { model.updateSuggestions() } + + let suggestions = try await model.$suggestions.compactMap({$0}).first + XCTAssertNotNil(suggestions) + + // Changing the `currentQuery` should set suggestions to nil. + model.currentQuery = "Coffee in Edinburgh" + XCTAssertNil(model.suggestions) + + // Changing current query after search with 1 result + // should set `selectedResult` to nil + model.currentQuery = "Magers & Quinn Bookseller" + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).first + XCTAssertNotNil(model.selectedResult) + model.currentQuery = "Hotel" + XCTAssertNil(model.selectedResult) + } + + func testIsEligibleForRequery() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.geoViewExtent = Polygon.chippewaFalls.extent + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).first + XCTAssertFalse(model.isEligibleForRequery) + + // Offset extent by 10% - isEligibleForRequery should still be `false`. + var builder = EnvelopeBuilder(envelope: model.geoViewExtent) + let tenPercentWidth = model.geoViewExtent!.width * 0.1 + builder.offsetBy(x: tenPercentWidth, y: 0.0) + var newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertFalse(model.isEligibleForRequery) + + // Offset extent by 50% - isEligibleForRequery should now be `true`. + builder = EnvelopeBuilder(envelope: model.geoViewExtent) + let fiftyPercentWidth = model.geoViewExtent!.width * 0.5 + builder.offsetBy(x: fiftyPercentWidth, y: 0.0) + newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertTrue(model.isEligibleForRequery) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.geoViewExtent = Polygon.chippewaFalls.extent + + Task { model.commitSearch() } + + _ = try await model.$results.compactMap({$0}).dropFirst().first + XCTAssertFalse(model.isEligibleForRequery) + + // Expand extent by 1.1x - isEligibleForRequery should still be `false`. + builder = EnvelopeBuilder(envelope: model.geoViewExtent) + builder.expand(factor: 1.1) + newExtent = builder.toGeometry() as! Envelope + + 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) + newExtent = builder.toGeometry() as! Envelope + + model.geoViewExtent = newExtent + XCTAssertTrue(model.isEligibleForRequery) + } + + func testQueryArea() async throws { + let source = LocatorSearchSource() + source.maximumResults = Int32.max + let model = BasemapGalleryViewModel(sources: [source]) + + // Set queryArea to Chippewa Falls + model.queryArea = Polygon.chippewaFalls + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + let resultGeometryUnion: Geometry = try XCTUnwrap( + GeometryEngine.union( + geometries: result.compactMap{ $0.geoElement?.geometry } + ) + ) + + XCTAssertTrue( + GeometryEngine.contains( + geometry1: model.queryArea!, + geometry2: resultGeometryUnion + ) + ) + + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) + + model.queryArea = Polygon.minneapolis + + Task { model.commitSearch() } + + // A note about the use of `.dropFirst()`: + // Because `model.results` is not changed between the previous call + // to `model.commitSearch()` and the one right above, the + // `try await model.$results...` call will return the last result + // received (from the first `model.commitSearch()` call), which is + // incorrect. Calling `.dropFirst()` will remove that one + // and will give us the next one, which is the correct one (the result + // from the second `model.commitSearch()` call). + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + } + + func testQueryCenter() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Set queryCenter to Portland + model.queryCenter = .portland + model.currentQuery = "Coffee" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + + var resultPoint = try XCTUnwrap( + result.first?.geoElement?.geometry as? Point + ) + + var geodeticDistance = try XCTUnwrap ( + GeometryEngine.distanceGeodetic( + point1: .portland, + point2: resultPoint, + distanceUnit: .meters, + azimuthUnit: nil, + curveType: .geodesic + ) + ) + + // First result within 1500m of Portland. + XCTAssertLessThan(geodeticDistance.distance, 1500.0) + + // Set queryCenter to Edinburgh + model.queryCenter = .edinburgh + model.currentQuery = "Restaurants" + + Task { model.commitSearch() } + + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + + resultPoint = try XCTUnwrap( + result.first?.geoElement?.geometry as? Point + ) + + // Web Mercator distance between .edinburgh and first result. + geodeticDistance = try XCTUnwrap ( + GeometryEngine.distanceGeodetic( + point1: .edinburgh, + point2: resultPoint, + distanceUnit: .meters, + azimuthUnit: nil, + curveType: .geodesic + ) + ) + + // First result within 100m of Edinburgh. + XCTAssertLessThan(geodeticDistance.distance, 100) + } + + func testRepeatSearch() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // Set queryArea to Chippewa Falls + model.geoViewExtent = Polygon.chippewaFalls.extent + model.currentQuery = "Coffee" + + Task { model.repeatSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + let resultGeometryUnion: Geometry = try XCTUnwrap( + GeometryEngine.union( + geometries: result.compactMap{ $0.geoElement?.geometry } + ) + ) + + XCTAssertTrue( + GeometryEngine.contains( + geometry1: model.geoViewExtent!, + geometry2: resultGeometryUnion + ) + ) + + model.currentQuery = "Magers & Quinn Booksellers" + + Task { model.repeatSearch() } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 0) + + model.geoViewExtent = Polygon.minneapolis.extent + + Task { model.repeatSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + } + + func testSearchResultMode() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + XCTAssertEqual(model.resultMode, .automatic) + + model.resultMode = .single + model.currentQuery = "Magers & Quinn" + + Task { model.commitSearch() } + + var results = try await model.$results.compactMap({$0}).first + var result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + + model.resultMode = .multiple + + Task { model.commitSearch() } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + model.currentQuery = "Coffee" + + Task { model.updateSuggestions() } + + let suggestionResults = try await model.$suggestions.compactMap({$0}).first + let suggestions = try XCTUnwrap(suggestionResults?.get()) + + let collectionSuggestion = try XCTUnwrap(suggestions.filter { $0.isCollection }.first) + let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) + + model.resultMode = .automatic + + Task { model.acceptSuggestion(collectionSuggestion) } + + results = try await model.$results.compactMap({$0}).first + result = try XCTUnwrap(results?.get()) + XCTAssertGreaterThan(result.count, 1) + + Task { model.acceptSuggestion(singleSuggestion) } + + results = try await model.$results.compactMap({$0}).dropFirst().first + result = try XCTUnwrap(results?.get()) + XCTAssertEqual(result.count, 1) + } + + func testUpdateSuggestions() async throws { + let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) + + // No currentQuery - suggestions are nil. + XCTAssertNil(model.suggestions) + + // UpdateSuggestions with no results - result count is 0. + model.currentQuery = "No results found blah blah blah blah" + + Task { model.updateSuggestions() } + + var suggestionResults = try await model.$suggestions.compactMap({$0}).first + var suggestions = try XCTUnwrap(suggestionResults?.get()) + XCTAssertEqual(suggestions.count, 0) + + // UpdateSuggestions with results. + model.currentQuery = "Magers & Quinn" + + Task { model.updateSuggestions() } + + suggestionResults = try await model.$suggestions.compactMap({$0}).first + suggestions = try XCTUnwrap(suggestionResults?.get()) + XCTAssertGreaterThanOrEqual(suggestions.count, 1) + + XCTAssertNil(model.selectedResult) + XCTAssertNil(model.results) + } + */ +} + +//extension Polygon { +// static var chippewaFalls: Polygon { +// let builder = PolygonBuilder(spatialReference: .wgs84) +// let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) +// return builder.toGeometry() as! ArcGIS.Polygon +// } +// +// static var minneapolis: Polygon { +// let builder = PolygonBuilder(spatialReference: .wgs84) +// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) +// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) +// return builder.toGeometry() as! ArcGIS.Polygon +// } +//} +// +//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) +//} +// From e3b057a44062faa4dd6939275477893d31e3c566 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 13 Oct 2021 13:35:49 -0500 Subject: [PATCH 0186/1722] Test gallery in sceneView --- .../Examples/BasemapGalleryExampleView.swift | 19 ++++++------------- .../BasemapGalleryViewModel.swift | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 742d180a1..1a144e428 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -68,17 +68,9 @@ struct BasemapGalleryExampleView: View { ) ] - let newItem = BasemapGalleryItem( - basemap: Basemap(style: .arcGISMidcentury), - name: "ArcGIS Midcentury 2", - description: "A 2nd vector basemap inspired by the art and advertising of the 1950's that presents a unique design option to the ArcGIS basemaps.", - thumbnail: UIImage(named: "Midcentury") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/52d6a28f09704f04b33761ba7c4bf93f/info/thumbnail/thumbnail1607554184831.jpeg")! - ) - - - let map = Map(basemapStyle: .arcGISNova) - +// let geoModel: GeoModel = Map(basemapStyle: .arcGISNova) + let geoModel: GeoModel = Scene(basemapStyle: .arcGISNova) + @ObservedObject var viewModel = BasemapGalleryViewModel() @@ -105,7 +97,8 @@ struct BasemapGalleryExampleView: View { var body: some View { ZStack(alignment: .topTrailing) { - MapView(map: map, viewpoint: initialViewpoint) +// MapView(map: geoModel as! Map, viewpoint: initialViewpoint) + SceneView(scene: geoModel as! ArcGIS.Scene, viewpoint: initialViewpoint) .overlay( VStack(alignment: .trailing) { if showBasemapGallery { @@ -133,7 +126,7 @@ struct BasemapGalleryExampleView: View { } private func SetupViewModel() { - viewModel.geoModel = map + viewModel.geoModel = geoModel viewModel.basemapGalleryItems = basemapGalleryItems viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index dc291b955..05c9f309f 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -71,7 +71,7 @@ public class BasemapGalleryViewModel: ObservableObject { } // TODO: write tests to check on loading stuff, setting portal and other props, etc. - // TODO: Change type of `Task` so I don't need to wrap operation in a Result. + // TODO: Change type of `Task` so I don't need to wrap operation in a Result.??? /// The currently executing async task for fetching basemaps from the portal. /// `fetchBasemapTask` should be cancelled prior to starting another async task. From 14c9eab63639c76ad5babe60c8dc46cd62fe80dc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 18 Oct 2021 10:02:01 -0500 Subject: [PATCH 0187/1722] Add selected item stuff; update concurrency/task code --- .../DefaultBasemap.imageset/Contents.json | 12 ++ .../generic_basemap_thumbnail.jpg | Bin 0 -> 8581 bytes .../basemap.imageset/Contents.json | 4 +- .../basemap.imageset/basemap@2x.png | Bin 599 -> 0 bytes .../basemap.imageset/basemap@3x.png | Bin 1077 -> 0 bytes .../BasemapGallery/BasemapGallery.swift | 28 +++- .../BasemapGallery/BasemapGalleryItem.swift | 110 ++++++++++++--- .../BasemapGalleryViewModel.swift | 127 +++++++++--------- 8 files changed, 191 insertions(+), 90 deletions(-) create mode 100644 Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/Contents.json create mode 100644 Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg delete mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@3x.png diff --git a/Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/Contents.json new file mode 100644 index 000000000..288c7a409 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "generic_basemap_thumbnail.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg b/Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d91d35911fc0df230996cc2f17aa61931244f690 GIT binary patch literal 8581 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 bsu%sO95Ym?Qu$Mx$ojr~KaT$=jK26Eg#m?7 literal 0 HcmV?d00001 diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json index 800e56381..ccc73709c 100644 --- a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json +++ b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json @@ -1,20 +1,18 @@ { "images" : [ { + "filename" : "basemap@1x.png", "idiom" : "universal" }, { - "filename" : "basemap@1x.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "basemap@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "basemap@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png deleted file mode 100644 index 708fcaeca1d778a10b911a1ceb341f7823cbc4f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 599 zcmV-d0;v6oP)V0Y^(%(#nxM6;RV4)Q9)UB$6}b>W1NhMo7v64{1BMU{BzFvNOmSk0SIuff~Mbj zt~S>Jo2|?Pv@5^m&jgm6?tyinPe2@NynntWG>NU}x(z_oE(JsVhmTn4)Cb-ulWd271wvb zH1$7vA8@Gn7a9OCk07eapk91_1tf>GtDgz&_2GE_%nW_v-0k^=WK^)s$I)5NL8v!s$X6rHW l*ZMQRn9Fx$tybm%z5(?{11frOuf6~P002ovPDHLkV1iAm1S
If6+C<%(P#*K&Gg}3v1xBJb` z?9QzHhdE5W>h5|)cTIIo51d`WYaq>FC2d@rVc`Fo;-Cb@K?&AHWS`?$MYcafCx>c3 zb$PLJe-m~kbaIIGlNC7Y1lJf%1?f6EIgIp^yTqm|_Zw>(&^vM+bh>hXNz;aOm0Sn? zSh>HvX+ye7EJ(-r2GAI*+4J}cpEYSz4US1ChB?V(?L={o*CP7AVn@Qb zI%CZ-h_tIiPCp|$*dvSx1b)Qq$z%H;@MePq9#{q1rw7X4sy7jrDm>erLgU-jbcn;qQGh&YS zF@di${$y_%XNg^m_sowmCvYPkHyXTG1>?q`i!fP{&EQpR7h=;C{#?3g-5^y_+>D2@ ze(3kz!Yul}(ry}W&0gnXC$11SwHIK6_9}|4k_w#nM06f&X{;E7NV~Qe87pUrO+{o| z8#JoZow)IPX=uzVSPvxlUJER35tUf6bB_(XPJ|(3OTQ@bE<`fLZMJ7 z6bglgS);isUmbGJI!0dyFDGR;Uv~5LDGo|d9F!pDpo2p2ZsTf-w(L7t9jMHp1jRuK z>N;qBT4i-G&ikzi;f&%fxEg2hi*QD95}!0~-NLk$+(r}9RPuH+K8om7uUCZk?~Adj z62%R8SvW*~YsMdWLYPgLX6W{Yg&ed+IBNHl@gC1R@F}(}(XP%Ai$R<5p@on3p2X`y z47xj_w@o-Vz1!#!nO;&|cQX;$S^R`YgtI?$tQBI!zY+Z(aA9JsL56*UNV_`Z^fRIp zTVk;#bp+So4g51CpPB9SAz?-hvmJ*P=&v`V@}M(#6!+ryMfBdpgXTLA?i3DS^_ImP zG%e&khcf#*f_)MB0X?2YizSMC@b;MfP2zUkF(lhLd;h_heJz!0GFiJS84JjV>)T}_ z(%&gzMlNTN&PfE5W5t%(31Rls<+zY}53|Yqr(bV~Iq0y_ubju_g(bG{3~iM=jtYm% zdrzD7pgnk2`16y7#X$*TiQ+CR`#L&@ vI`+j9#jIp<#j16BUQsM4F(^rKP=fyeBxGdq<~Ib#00000NkvXXu0mjfI}r8H diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 25650e4f2..87070bb18 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -111,7 +111,10 @@ extension BasemapGallery { ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow( basemapGalleryItem: basemapGalleryItem, - currentItem: viewModel.currentBasemapGalleryItem + isSelected: basemapGalleryItem == viewModel.currentBasemapGalleryItem, + isValid: basemapGalleryItem.isValid( + for: viewModel.currentSpatialReference + ) ) .onTapGesture { viewModel.currentBasemapGalleryItem = basemapGalleryItem @@ -124,23 +127,36 @@ extension BasemapGallery { } private struct BasemapGalleryItemRow: View { - var basemapGalleryItem: BasemapGalleryItem - var currentItem: BasemapGalleryItem? + @ObservedObject var basemapGalleryItem: BasemapGalleryItem + let isSelected: Bool + let isValid: Bool var body: some View { - HStack (alignment: .center) { + ZStack { VStack { - if let thumbnailImage = basemapGalleryItem.thumbnail { + if !basemapGalleryItem.isLoaded { + Spacer() + Text("Loading...") + .font(.caption) + Spacer() + } + else if let thumbnailImage = basemapGalleryItem.thumbnail { Image(uiImage: thumbnailImage) .resizable() .aspectRatio(contentMode: .fit) .border( - basemapGalleryItem == currentItem ? Color.accentColor: Color.clear, + isSelected ? Color.accentColor: Color.clear, width: 3.0) } Text(basemapGalleryItem.name) .font(.footnote) } + if !isValid { + Color(white: 0.5, opacity: 0.5) + .blur(radius: 0.0, opaque: true) + } + } + .allowsHitTesting(isValid) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index e686c1396..f9d546c00 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -17,39 +17,117 @@ import ArcGIS import Foundation /// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. -public struct BasemapGalleryItem { +public class BasemapGalleryItem: ObservableObject { static var defaultThumbnail: UIImage { - return UIImage(named: "basemap")! + return UIImage(named: "DefaultBasemap")! } public init( basemap: Basemap, - name: String = "", - description: String? = "", + name: String? = nil, + description: String? = nil, thumbnail: UIImage? = nil ) { self.basemap = basemap - self.name = name.isEmpty ? basemap.name : name - self.description = description ?? basemap.item?.description - self.thumbnail = thumbnail ?? - (basemap.item?.thumbnail?.image ?? BasemapGalleryItem.defaultThumbnail) + self.nameOverride = name + self.name = name ?? "" + self.descriptionOverride = description + self.description = description + self.thumbnailOverride = thumbnail + self.thumbnail = thumbnail + + loadBasemapTask = Task { await loadBasemap() } } +// deinit { +// loadBasemapTask.cancel() +// fetchBasemapTask.cancel() +// } + + @Published + public var loadBasemapsError: Error? = nil + + /// The currently executing async task for loading basemap. + private var loadBasemapTask: Task? = nil + /// The basemap this `BasemapGalleryItem` represents. public private(set) var basemap: Basemap - + + private var nameOverride: String? = nil /// The name of this `Basemap`. - public private(set) var name: String - + @Published + public private(set) var name: String = "" + + private var descriptionOverride: String? = nil /// The description which will be used in the gallery. - public private(set) var description: String? - + @Published + public private(set) var description: String? = nil + + private var thumbnailOverride: UIImage? = nil /// The thumbnail which will be displayed in the gallery. - public let thumbnail: UIImage? + @Published + public private(set) var thumbnail: UIImage? = nil + + public private(set) var spatialReference: SpatialReference? = nil + + @Published + public private(set) var isLoaded = false +} + +extension BasemapGalleryItem { + private func loadBasemap() async { + do { + print("pre-basemap.load()") + try await basemap.load() + print("basemap loaded!") + if let loadableImage = basemap.item?.thumbnail { + try await loadableImage.load() + } + + //TODO: use the item.spatialreferenceName to create a spatial reference instead of always loading the first base layer. + // Determine the spatial reference of the basemap +// if let item = basemap.item as? PortalItem { +// try await item.load() +// } + + print("sr = \(basemap.item?.spatialReferenceName ?? "no name"); item: \(String(describing: (basemap.item as? PortalItem)?.loadStatus))") + if let layer = basemap.baseLayers.first { + try await layer.load() + spatialReference = layer.spatialReference + } + + //TODO: Add sr checking and setting of sr to bmgi (isValid???); and what to do with errors... + + await update() + } catch { + loadBasemapsError = error + } + } + + @MainActor + func update() { + self.name = nameOverride ?? basemap.name + self.description = descriptionOverride ?? basemap.item?.description + self.thumbnail = thumbnailOverride ?? + (basemap.item?.thumbnail?.image ?? BasemapGalleryItem.defaultThumbnail) + + isLoaded = true + } + + /// Returns whether the basemap gallery item is valid and ok to use. + /// - Parameter item: item to match spatial references with. + /// - Returns: true if the item is loaded and either `item`'s spatial reference is nil + /// or matches `spatialReference`. + public func isValid(for otherSpatialReference: SpatialReference?) -> Bool { + print("name: \(name); isLoaded = \(isLoaded); loadStatus = \(basemap.loadStatus)") + guard isLoaded else { return false } + return otherSpatialReference == nil || otherSpatialReference == spatialReference +// return true + } } extension BasemapGalleryItem: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(basemap) } + public var id: ObjectIdentifier { ObjectIdentifier(self) } } extension BasemapGalleryItem: Equatable { @@ -59,5 +137,5 @@ extension BasemapGalleryItem: Equatable { lhs.description == rhs.description && lhs.thumbnail === rhs.thumbnail } - + } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 05c9f309f..42fa92bb2 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -21,40 +21,52 @@ import Combine public class BasemapGalleryViewModel: ObservableObject { /// Creates a `BasemapGalleryViewModel`. /// - Parameters: - /// - currentBasemap: The `Basemap` currently used by a `GeoModel`. + /// - geoModel: The `GeoModel`. /// - portal: The `Portal` to load base maps from. /// - basemapGalleryItems: A list of pre-defined base maps to display. public init( geoModel: GeoModel? = nil, portal: Portal? = nil, - basemapGalleryItems: [BasemapGalleryItem] = [] + basemapGalleryItems: [BasemapGalleryItem] = [] // TODO: Doc passing this in. ) { self.geoModel = geoModel self.portal = portal self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) - loadGeoModel() - fetchBasemaps() + loadGeoModelTask = Task { await loadGeoModel() } + fetchBasemapTask = Task { await fetchBasemaps() } } + deinit { + loadGeoModelTask?.cancel() + fetchBasemapTask?.cancel() + } + + @Published + public var fetchBasemapsError: Error? = nil + + @Published + public var loadGeoModelError: Error? = 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, /// the geoModel will have its basemap replaced with the selected basemap. - public var geoModel: GeoModel? = nil { + public var geoModel: GeoModel? { didSet { - loadGeoModel() + loadGeoModelTask?.cancel() + loadGeoModelTask = Task { await loadGeoModel() } } } /// The `Portal` object, if any. Setting the portal will automatically fetch it's base maps /// and add them to the `basemapGalleryItems` array. - public var portal: Portal? = nil { - didSet { - fetchBasemaps() - } - } - + public var portal: Portal? { + didSet { + fetchBasemapTask?.cancel() + fetchBasemapTask = Task { await fetchBasemaps() } + } + } /// The list of basemaps currently visible in the gallery. Items added or removed from this list will /// update the gallery. @Published @@ -70,17 +82,29 @@ public class BasemapGalleryViewModel: ObservableObject { } } + public var currentSpatialReference: SpatialReference? { + guard let scene = geoModel as? ArcGIS.Scene, + scene.sceneViewTilingScheme == .webMercator else { + return geoModel?.spatialReference + } + return .webMercator + } + // TODO: write tests to check on loading stuff, setting portal and other props, etc. - // TODO: Change type of `Task` so I don't need to wrap operation in a Result.??? /// The currently executing async task for fetching basemaps from the portal. /// `fetchBasemapTask` should be cancelled prior to starting another async task. private var fetchBasemapTask: Task? = nil /// Fetches the basemaps from `portal`. - private func fetchBasemaps() { - fetchBasemapTask?.cancel() - fetchBasemapTask = fetchBasemapsTask(portal) + private func fetchBasemaps() async { + guard let portal = portal else { return } + + do { + basemapGalleryItems += try await portal.developerBasemaps.map { BasemapGalleryItem.init(basemap: $0) } + } catch { + fetchBasemapsError = error + } } /// The currently executing async task for loading `geoModel`. @@ -88,57 +112,30 @@ public class BasemapGalleryViewModel: ObservableObject { private var loadGeoModelTask: Task? = nil /// Loads `geoModel`. - private func loadGeoModel() { - loadGeoModelTask?.cancel() - loadGeoModelTask = loadGeoModelTask(geoModel) - } -} - -extension BasemapGalleryViewModel { - private func fetchBasemapsTask(_ portal: Portal?) -> Task<(), Never>? { - guard let portal = portal else { return nil } - - return Task(operation: { - let basemapResults = await Result { - try await portal.developerBasemaps + private func loadGeoModel() async { + guard let geoModel = geoModel else { return } + do { + try await geoModel.load() + if let basemap = geoModel.basemap { + currentBasemapGalleryItem = BasemapGalleryItem(basemap: basemap) } - - switch basemapResults { - case .success(let basemaps): - basemaps.forEach { basemap in - Task { - try await basemap.load() - if let loadableImage = basemap.item?.thumbnail { - try await loadableImage.load() - } - basemapGalleryItems.append(BasemapGalleryItem(basemap: basemap)) - } - } - case .failure(_), .none: - basemapGalleryItems = [] - } - }) - } - - private func loadGeoModelTask(_ geoModel: GeoModel?) -> Task<(), Never>? { - guard let geoModel = geoModel else { return nil } - - return Task(operation: { - let loadResult = await Result { - try await geoModel.load() - } - - switch loadResult { - case .success(_): - if let basemap = geoModel.basemap { - currentBasemapGalleryItem = BasemapGalleryItem(basemap: basemap) - } - else { - fallthrough - } - case .failure(_), .none: + else { currentBasemapGalleryItem = nil } - }) + } catch { + loadGeoModelError = error + } } } + +//extension SpatialReference { +// public func effectivelyEquals(_ otherSpatialReference: SpatialReference?) -> Bool { +// guard let otherSR = otherSpatialReference else { false } +// +// guard let self != otherSpatialReference else { true } +// if self == otherSR { +// return true +// } +// else +// } +//} From 4114112ca5dc9e63936e20b59ebd8d767cd36a13 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 18 Oct 2021 10:21:31 -0500 Subject: [PATCH 0188/1722] PR review changes. --- Examples/Examples/SearchExampleView.swift | 2 +- .../Components/Search/LocatorSearchSource.swift | 10 +++++----- .../Components/Search/SearchResult.swift | 4 ++-- ...archSourceProtocol.swift => SearchSource.swift} | 8 ++++---- .../Components/Search/SearchSuggestion.swift | 4 ++-- .../Components/Search/SearchViewModel.swift | 14 +++++++------- .../Search/SmartLocatorSearchSource.swift | 6 +++--- .../ArcGISToolkit/Extensions/GeocodeResult.swift | 2 +- .../ArcGISToolkit/Extensions/SuggestResult.swift | 2 +- .../ArcGISToolkitTests/SearchViewModelTests.swift | 6 +++--- 10 files changed, 29 insertions(+), 29 deletions(-) rename Sources/ArcGISToolkit/Components/Search/{SearchSourceProtocol.swift => SearchSource.swift} (91%) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 40ee448f2..feb782337 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -83,7 +83,7 @@ struct SearchExampleView: View { /// Sets up any desired customization on `searchViewModel`. private func setupSearchViewModel() { let smartLocator = SmartLocatorSearchSource( - displayName: "My locator", + name: "My locator", maximumResults: 16, maximumSuggestions: 16 ) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index c4a2cf788..bfda5cfa9 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -16,16 +16,16 @@ import ArcGIS /// Uses a Locator to provide search and suggest results. Most configuration should be done on the /// `GeocodeParameters` directly. -public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { +public class LocatorSearchSource: ObservableObject, SearchSource { /// Creates a locator search source. /// - Parameters: - /// - displayName: Name to show when presenting this source in the UI. + /// - name: Name to show when presenting this source in the UI. /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. /// - searchArea: Area to be used as a constraint for searches and suggestions. /// - preferredSearchLocation: Point to be used as an input to searches and suggestions. public init( - displayName: String = "Locator", + name: String = "Locator", locatorTask: LocatorTask = LocatorTask( url: URL( string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" @@ -36,7 +36,7 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil ) { - self.displayName = displayName + self.name = name self.locatorTask = locatorTask self.maximumResults = maximumResults self.maximumSuggestions = maximumSuggestions @@ -47,7 +47,7 @@ public class LocatorSearchSource: ObservableObject, SearchSourceProtocol { } /// Name to show when presenting this source in the UI. - public var displayName: String + public var name: String /// The maximum results to return when performing a search. Most sources default to 6 public var maximumResults: Int32 { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 7634bd0da..102221b6f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -20,7 +20,7 @@ public class SearchResult { displayTitle: String, displaySubtitle: String? = nil, markerImage: UIImage? = nil, - owningSource: SearchSourceProtocol, + owningSource: SearchSource, geoElement: GeoElement? = nil, selectionViewpoint: Viewpoint? = nil ) { @@ -45,7 +45,7 @@ public class SearchResult { public var markerImage: UIImage? /// Reference to the search source that created this result. - public var owningSource: SearchSourceProtocol + public var owningSource: SearchSource /// For locator results, should be the graphic that was used to display the result on the map. /// For feature layer results, should be the result feature. Can be null depending on the type of the diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift similarity index 91% rename from Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift rename to Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 607eb3569..f945c099f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSourceProtocol.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -15,9 +15,9 @@ import ArcGIS import Foundation /// Defines the contract for a search result provider. -public protocol SearchSourceProtocol { +public protocol SearchSource { /// Name to show when presenting this source in the UI. - var displayName: String { get set } + var name: String { get set } /// The maximum results to return when performing a search. Most sources default to 6. var maximumResults: Int32 { get set } @@ -25,10 +25,10 @@ public protocol SearchSourceProtocol { /// The maximum suggestions to return. Most sources default to 6. var maximumSuggestions: Int32 { get set } - /// Area to be used as a constraint for searches and suggestions. + /// The area to be used as a constraint for searches and suggestions. var searchArea: Geometry? { get set } - /// Point to be used as an input to searches and suggestions. + /// The point to be used as an input to searches and suggestions. var preferredSearchLocation: Point? { get set } /// Gets suggestions. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index e9d642676..48b4ad410 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -18,7 +18,7 @@ public class SearchSuggestion { public init( displayTitle: String, displaySubtitle: String? = nil, - owningSource: SearchSourceProtocol, + owningSource: SearchSource, suggestResult: SuggestResult? = nil, isCollection: Bool ) { @@ -38,7 +38,7 @@ public class SearchSuggestion { /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the /// view model to be able to accept a suggestion, because a suggestion should only be used with the /// locator that created it. - public var owningSource: SearchSourceProtocol + public var owningSource: SearchSource /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 6ac7fc30e..21a8546ad 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -40,11 +40,11 @@ public class SearchViewModel: ObservableObject { /// - sources: Collection of search sources to be used. public convenience init( defaultPlaceholder: String = .defaultPlaceholder, - activeSource: SearchSourceProtocol? = nil, + activeSource: SearchSource? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, - sources: [SearchSourceProtocol] = [] + sources: [SearchSource] = [] ) { self.init() self.defaultPlaceholder = defaultPlaceholder @@ -60,7 +60,7 @@ public class SearchViewModel: ObservableObject { public var defaultPlaceholder: String = .defaultPlaceholder /// The active search source. If `nil`, the first item in `sources` is used. - public var activeSource: SearchSourceProtocol? + public var activeSource: SearchSource? /// Tracks the current user-entered query. This property drives both suggestions and searches. @Published @@ -170,7 +170,7 @@ public class SearchViewModel: ObservableObject { /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. /// NOTE: only the first source is currently used; multiple sources are not yet supported. - public var sources: [SearchSourceProtocol] = [] + public var sources: [SearchSource] = [] /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, @@ -191,7 +191,7 @@ public class SearchViewModel: ObservableObject { private func makeEffectiveSource( with searchArea: Geometry?, preferredSearchLocation: Point? - ) -> SearchSourceProtocol? { + ) -> SearchSource? { guard var source = currentSource() else { return nil } source.searchArea = searchArea source.preferredSearchLocation = preferredSearchLocation @@ -366,8 +366,8 @@ extension SearchViewModel { extension SearchViewModel { /// Returns the search source to be used in geocode operations. /// - Returns: The search source to use. - func currentSource() -> SearchSourceProtocol? { - var source: SearchSourceProtocol? + func currentSource() -> SearchSource? { + var source: SearchSource? if let activeSource = activeSource { source = activeSource } else { diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index e5fa490c6..54aae0a56 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -21,7 +21,7 @@ import ArcGIS public class SmartLocatorSearchSource: LocatorSearchSource { /// Creates a smart locator search source. /// - Parameters: - /// - displayName: Name to show when presenting this source in the UI. + /// - name: Name to show when presenting this source in the UI. /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. /// - searchArea: Area to be used as a constraint for searches and suggestions. @@ -29,7 +29,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// - repeatSearchResultThreshold: The minimum number of search results to attempt to return. /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. public init( - displayName: String = "Smart Locator", + name: String = "Smart Locator", locatorTask: LocatorTask = LocatorTask( url: URL( string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" @@ -43,7 +43,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { repeatSuggestResultThreshold: Int = 6 ) { super.init( - displayName: displayName, + name: name, locatorTask: locatorTask, maximumResults: maximumResults, maximumSuggestions: maximumSuggestions, diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift index 6a069c4db..334ca503f 100644 --- a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift +++ b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift @@ -17,7 +17,7 @@ extension GeocodeResult { /// Converts a `GeocodeResult` to a `SearchResult`. /// - Parameter searchSource: The search source generating the result. /// - Returns: The new `SearchResult`. - func toSearchResult(searchSource: SearchSourceProtocol) -> SearchResult { + func toSearchResult(searchSource: SearchSource) -> SearchResult { let subtitle = self.attributes["LongLabel"] as? String ?? "Match percent: \((self.score / 100.0).formatted(.percent))" var viewpoint: Viewpoint? = nil diff --git a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift index cb4c61e61..18239ef6f 100644 --- a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift +++ b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift @@ -17,7 +17,7 @@ extension SuggestResult { /// Converts a `SuggestResult` to a `SearchSuggestion`. /// - Parameter searchSource: The search source generating the result. /// - Returns: The new `SearchSuggestion`. - func toSearchSuggestion(searchSource: SearchSourceProtocol) -> SearchSuggestion { + func toSearchSuggestion(searchSource: SearchSource) -> SearchSuggestion { return SearchSuggestion(displayTitle: label, displaySubtitle: nil, owningSource: searchSource, diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 77bde66fb..2653abefb 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -47,7 +47,7 @@ class SearchViewModelTests: XCTestCase { func testActiveSource() async throws { let activeSource = LocatorSearchSource() - activeSource.displayName = "Simple Locator" + activeSource.name = "Simple Locator" let model = SearchViewModel( activeSource: activeSource, sources: [LocatorSearchSource()] @@ -59,13 +59,13 @@ class SearchViewModelTests: XCTestCase { let results = try await model.$results.compactMap({$0}).first let result = try XCTUnwrap(results?.get().first) - XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) + XCTAssertEqual(result.owningSource.name, activeSource.name) Task { model.updateSuggestions() } let suggestions = try await model.$suggestions.compactMap({$0}).first let suggestion = try XCTUnwrap(suggestions?.get().first) - XCTAssertEqual(suggestion.owningSource.displayName, activeSource.displayName) + XCTAssertEqual(suggestion.owningSource.name, activeSource.name) } func testCommitSearch() async throws { From 04caf53199bb2fdc847b8641741f12897192b279 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 18 Oct 2021 13:37:30 -0500 Subject: [PATCH 0189/1722] PR doc changes. --- .../Components/Search/SearchSource.swift | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index f945c099f..5f56ea258 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -12,7 +12,6 @@ // limitations under the License. import ArcGIS -import Foundation /// Defines the contract for a search result provider. public protocol SearchSource { @@ -31,10 +30,9 @@ public protocol SearchSource { /// The point to be used as an input to searches and suggestions. var preferredSearchLocation: Point? { get set } - /// Gets suggestions. - /// - Parameters: - /// - queryString: Text to be used for query. - /// - Returns: The array of suggestions. + /// Returns the search suggestions for the specified query. + /// - Parameter queryString: The query for which to provide search suggestions. + /// - Returns: An array of search suggestions. func suggest(_ queryString: String) async throws -> [SearchSuggestion] /// Gets search results. @@ -43,12 +41,10 @@ public protocol SearchSource { /// - Returns: Array of `SearchResult`s func search(_ queryString: String) async throws -> [SearchResult] - /// Gets search results. If `area` is not `nil`, search is restricted to that area. Otherwise, the - /// `searchArea` property may be consulted but does not need to be used as a strict limit. - /// - Parameters: - /// - searchSuggestion: Suggestion to be used as basis for search. - /// - area: Area to be used to constrain search results. - /// - Returns: Array of `SearchResult`s + /// Returns the search results for the specified search suggestion. + /// - Parameter searchSuggestion: The search suggestion for which to provide + /// search results. + /// - Returns: An array of search results. func search( _ searchSuggestion: SearchSuggestion ) async throws -> [SearchResult] From b79fec637dbce54fc36ab5a834107b06797411fe Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:24:33 -0500 Subject: [PATCH 0190/1722] PR review - set list style once and remove PlainList. --- .../Components/Search/SearchView.swift | 13 ++++---- Sources/ArcGISToolkit/Utility/PlainList.swift | 33 ------------------- 2 files changed, 7 insertions(+), 39 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Utility/PlainList.swift diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 8acb16151..7538e13c7 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -116,6 +116,7 @@ public struct SearchView: View { .esriBorder() } } + .listStyle(.plain) .onChange(of: searchViewModel.results) { display(searchResults: $0) } @@ -215,7 +216,7 @@ struct SearchResultList: View { case .success(let results): if results.count > 1 { // Only show the list if we have more than one result. - PlainList { + List { ForEach(results) { result in HStack { SearchResultRow(result: result) @@ -232,12 +233,12 @@ struct SearchResultList: View { } } else if results.isEmpty { - PlainList { + List { Text(noResultMessage) } } case .failure(let error): - PlainList { + List { Text(error.localizedDescription) } } @@ -256,7 +257,7 @@ struct SearchSuggestionList: View { switch suggestionResults { case .success(let suggestions): if !suggestions.isEmpty { - PlainList { + List { if suggestions.count > 0 { ForEach(suggestions) { suggestion in SuggestionResultRow(suggestion: suggestion) @@ -268,12 +269,12 @@ struct SearchSuggestionList: View { } } else { - PlainList { + List { Text(noResultMessage) } } case .failure(let error): - PlainList { + List { Text(error.errorDescription) } } diff --git a/Sources/ArcGISToolkit/Utility/PlainList.swift b/Sources/ArcGISToolkit/Utility/PlainList.swift deleted file mode 100644 index d6130c029..000000000 --- a/Sources/ArcGISToolkit/Utility/PlainList.swift +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021 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 `View` displaying a `List` with the `.plain` style. -public struct PlainList : View where Content : View { - let content: Content - /// Creates a plain list with the given content. - /// - Parameters: - /// - content: The content of the list. - public init(@ViewBuilder content: () -> Content) { - self.content = content() - } - - /// The content of the list. - public var body: some View { - List { - content - } - .listStyle(.plain) - } -} From cea0418e832bc19715e8e4b378a907c64ca82f09 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:26:38 -0500 Subject: [PATCH 0191/1722] Update Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift Co-authored-by: Philip Ridgeway --- .../Utility/EsriShowResultsButtonViewModifier.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift index bcf5d7728..ee9a56fb9 100644 --- a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift @@ -42,12 +42,9 @@ extension View { isEnabled: Bool, isHidden: Binding ) -> some View { - ModifiedContent( - content: self, - modifier: EsriShowResultsButtonViewModifier( - isEnabled: isEnabled, - isHidden: isHidden - ) - ) + modifier(EsriShowResultsButtonViewModifier( + isEnabled: isEnabled, + isHidden: isHidden + )) } } From adb506f07dce25ec23370bccc16736cc6f9cc64e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:26:53 -0500 Subject: [PATCH 0192/1722] Update Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift Co-authored-by: Philip Ridgeway --- .../Utility/EsriShowResultsButtonViewModifier.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift index ee9a56fb9..a69cc207b 100644 --- a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift @@ -21,14 +21,13 @@ struct EsriShowResultsButtonViewModifier: ViewModifier { func body(content: Content) -> some View { HStack { content - if !isEnabled { - EmptyView() - } - else { + if isEnabled { Button( action: { isHidden.toggle() }, label: { - Image(systemName: isHidden ? "eye.fill" : "eye.slash.fill") + Image(systemName: "eye") + .symbolVariant(isHidden ? .none : .slash) + .symbolVariant(.fill) .foregroundColor(Color(.opaqueSeparator)) } ) From b2663e20ea3c69d2c43aedac51822e0f4fcb3ea5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:27:03 -0500 Subject: [PATCH 0193/1722] Update Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index dc1e6a888..5fc7a19b3 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -23,7 +23,7 @@ struct EsriSearchButtonViewModifier: ViewModifier { action: action, label: { Image(systemName: "magnifyingglass.circle.fill") - .foregroundColor(Color(.opaqueSeparator)) + .foregroundColor(Color(uiColor: .opaqueSeparator)) } ) content From ee68cb8b7e02daaef0968d0141b90ddc354fdfcc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:27:17 -0500 Subject: [PATCH 0194/1722] Update Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift index 5fc7a19b3..91149e806 100644 --- a/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriSearchButtonViewModifier.swift @@ -33,9 +33,6 @@ struct EsriSearchButtonViewModifier: ViewModifier { extension View { func esriSearchButton(_ action: @escaping () -> Void) -> some View { - ModifiedContent( - content: self, - modifier: EsriSearchButtonViewModifier(action: action) - ) + modifier(EsriSearchButtonViewModifier(action: action)) } } From 208ab555c0253c9ffe12a207e17567fd1427baf9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:27:34 -0500 Subject: [PATCH 0195/1722] Update Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift Co-authored-by: Philip Ridgeway --- .../Utility/EsriDeleteTextButtonViewModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift index 7b68dff9c..562999747 100644 --- a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift @@ -22,7 +22,7 @@ struct EsriDeleteTextButtonViewModifier: ViewModifier { content if !text.isEmpty { Button( - action: { self.text = "" }, + action: { text = "" }, label: { Image(systemName: "xmark.circle.fill") .foregroundColor(Color(.opaqueSeparator)) From 99fd5c66e19005621842b90cd3885f0019d381d0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:28:33 -0500 Subject: [PATCH 0196/1722] Update Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Utility/EsriBorderViewModifier.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index d48c36f13..5037aa3fe 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -27,10 +27,11 @@ struct EsriBorderViewModifier: ViewModifier { .stroke(lineWidth: 2) .foregroundColor(Color(.separator)) ) - .shadow(color: Color.gray.opacity(0.4), - radius: 3, - x: 1, - y: 2 + .shadow( + color: .gray.opacity(0.4), + radius: 3, + x: 1, + y: 2 ) } } From 850942a79f60a3b91aa1b66ec3ba2f3fba5a5fa4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:28:57 -0500 Subject: [PATCH 0197/1722] Update Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 5037aa3fe..87c5cf981 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -36,8 +36,8 @@ struct EsriBorderViewModifier: ViewModifier { } } -extension View { - public func esriBorder( +public extension View { + func esriBorder( edgeInsets: EdgeInsets = EdgeInsets( top: 8, leading: 12, From 205c541d7e5763e6f4f29638307246e3023d4b26 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:29:07 -0500 Subject: [PATCH 0198/1722] Update Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 87c5cf981..dfbad4b68 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -45,9 +45,6 @@ public extension View { trailing: 12 ) ) -> some View { - return ModifiedContent( - content: self, - modifier: EsriBorderViewModifier(edgeInsets: edgeInsets) - ) + modifier(EsriBorderViewModifier(edgeInsets: edgeInsets)) } } From 53ff0f0e37f0b691aaaa3de38bba58f1d76fbe32 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:29:23 -0500 Subject: [PATCH 0199/1722] Update Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift Co-authored-by: Philip Ridgeway --- .../Utility/EsriDeleteTextButtonViewModifier.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift index 562999747..02644ce36 100644 --- a/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriDeleteTextButtonViewModifier.swift @@ -35,9 +35,6 @@ struct EsriDeleteTextButtonViewModifier: ViewModifier { extension View { func esriDeleteTextButton(text: Binding) -> some View { - ModifiedContent( - content: self, - modifier: EsriDeleteTextButtonViewModifier(text: text) - ) + modifier(EsriDeleteTextButtonViewModifier(text: text)) } } From 751b676e7c60cc55827d18ae6538b44bd01218e2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:29:53 -0500 Subject: [PATCH 0200/1722] Update Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index dfbad4b68..0802fe303 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -16,7 +16,8 @@ import SwiftUI /// A modifier which displays a 2 pixel width border and a shadow around a view. struct EsriBorderViewModifier: ViewModifier { var edgeInsets: EdgeInsets - public func body(content: Content) -> some View { + + func body(content: Content) -> some View { let roundedRect = RoundedRectangle(cornerRadius: 8) content .padding(edgeInsets) From 9659c1b2677502e5228552c923dd75ab1a484b87 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:30:07 -0500 Subject: [PATCH 0201/1722] Update Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 0802fe303..448a9ec52 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -13,7 +13,7 @@ import SwiftUI -/// A modifier which displays a 2 pixel width border and a shadow around a view. +/// A modifier which displays a 2 point width border and a shadow around a view. struct EsriBorderViewModifier: ViewModifier { var edgeInsets: EdgeInsets From fd0a2f94f0dc6b1500cf08169fe0fe808974f045 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:30:20 -0500 Subject: [PATCH 0202/1722] Update Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 448a9ec52..1a238bcb3 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -21,7 +21,7 @@ struct EsriBorderViewModifier: ViewModifier { let roundedRect = RoundedRectangle(cornerRadius: 8) content .padding(edgeInsets) - .background(Color(.systemBackground)) + .background(Color(uiColor: .systemBackground)) .clipShape(roundedRect) .overlay( roundedRect From 7bb105b2ba674f29db66832ac91aae2ab87cf2f9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:30:33 -0500 Subject: [PATCH 0203/1722] Update Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 1a238bcb3..c14f119a6 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -26,7 +26,7 @@ struct EsriBorderViewModifier: ViewModifier { .overlay( roundedRect .stroke(lineWidth: 2) - .foregroundColor(Color(.separator)) + .foregroundColor(Color(uiColor: .separator)) ) .shadow( color: .gray.opacity(0.4), From 0de8ec4128d37195c18b0edaff2da0bd55085a10 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:48:15 -0500 Subject: [PATCH 0204/1722] PR review changes. --- .../Components/Search/SearchView.swift | 12 ++++++------ .../Utility/EsriBorderViewModifier.swift | 8 ++++---- .../EsriShowResultsButtonViewModifier.swift | 18 +++++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 7538e13c7..8015c164f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -67,7 +67,7 @@ public struct SearchView: View { /// Determines whether the results lists are displayed. @State - private var isResultListViewHidden: Bool = false + private var showResultListView: Bool = true public var body: some View { VStack (alignment: .center) { @@ -84,11 +84,11 @@ public struct SearchView: View { .esriDeleteTextButton(text: $searchViewModel.currentQuery) .esriSearchButton { searchViewModel.commitSearch() } .esriShowResultsButton( - isEnabled: enableResultListView, - isHidden: $isResultListViewHidden + isHidden: !enableResultListView, + showResults: $showResultListView ) .esriBorder() - if enableResultListView, !isResultListViewHidden { + if enableResultListView, showResultListView { if let results = searchViewModel.results { SearchResultList( searchResults: results, @@ -243,7 +243,7 @@ struct SearchResultList: View { } } } - .esriBorder(edgeInsets: EdgeInsets()) + .esriBorder(padding: EdgeInsets()) } } @@ -279,7 +279,7 @@ struct SearchSuggestionList: View { } } } - .esriBorder(edgeInsets: EdgeInsets()) + .esriBorder(padding: EdgeInsets()) } } diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index c14f119a6..dacf71f82 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -15,12 +15,12 @@ import SwiftUI /// A modifier which displays a 2 point width border and a shadow around a view. struct EsriBorderViewModifier: ViewModifier { - var edgeInsets: EdgeInsets + var padding: EdgeInsets func body(content: Content) -> some View { let roundedRect = RoundedRectangle(cornerRadius: 8) content - .padding(edgeInsets) + .padding(padding) .background(Color(uiColor: .systemBackground)) .clipShape(roundedRect) .overlay( @@ -39,13 +39,13 @@ struct EsriBorderViewModifier: ViewModifier { public extension View { func esriBorder( - edgeInsets: EdgeInsets = EdgeInsets( + padding: EdgeInsets = EdgeInsets( top: 8, leading: 12, bottom: 8, trailing: 12 ) ) -> some View { - modifier(EsriBorderViewModifier(edgeInsets: edgeInsets)) + modifier(EsriBorderViewModifier(padding: padding)) } } diff --git a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift index a69cc207b..90a6da49c 100644 --- a/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriShowResultsButtonViewModifier.swift @@ -15,18 +15,18 @@ import SwiftUI /// A modifier which adds a "show results" button in a view, used to hide/show another view. struct EsriShowResultsButtonViewModifier: ViewModifier { - var isEnabled: Bool - @Binding var isHidden: Bool + var isHidden: Bool + @Binding var showResults: Bool func body(content: Content) -> some View { HStack { content - if isEnabled { + if !isHidden { Button( - action: { isHidden.toggle() }, + action: { showResults.toggle() }, label: { Image(systemName: "eye") - .symbolVariant(isHidden ? .none : .slash) + .symbolVariant(!showResults ? .none : .slash) .symbolVariant(.fill) .foregroundColor(Color(.opaqueSeparator)) } @@ -38,12 +38,12 @@ struct EsriShowResultsButtonViewModifier: ViewModifier { extension View { func esriShowResultsButton( - isEnabled: Bool, - isHidden: Binding + isHidden: Bool, + showResults: Binding ) -> some View { modifier(EsriShowResultsButtonViewModifier( - isEnabled: isEnabled, - isHidden: isHidden + isHidden: isHidden, + showResults: showResults )) } } From 2068a3a56e6499e41aa3e842448569404f693939 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Oct 2021 15:53:42 -0500 Subject: [PATCH 0205/1722] SearchError no longer inherits from LocalizedError, just Error --- Sources/ArcGISToolkit/Components/Search/SearchError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchError.swift b/Sources/ArcGISToolkit/Components/Search/SearchError.swift index d32d10617..5536d6473 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchError.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchError.swift @@ -14,7 +14,7 @@ import Foundation /// A value that represents an error that occurs while Searching. -public struct SearchError: LocalizedError { +public struct SearchError: Error { /// A basic description for the error. public let errorDescription: String } From 75031d50c268f797ed426398a6529fe1d64a21b8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Oct 2021 15:25:06 -0500 Subject: [PATCH 0206/1722] Finalize map pin images for search and suggestions. --- .../MapPin.imageset/Contents.json | 67 ------------------ .../MapPin.imageset/iOS8_MapPin_End108.png | Bin 1909 -> 0 bytes .../MapPin.imageset/iOS8_MapPin_End36.png | Bin 1256 -> 0 bytes .../MapPin.imageset/iOS8_MapPin_End72.png | Bin 1624 -> 0 bytes .../Components/Search/SearchView.swift | 22 ++++-- .../Resources/Images.xcassets/Contents.json | 6 ++ .../MapPin.imageset/Contents.json | 23 ++++++ .../MapPin.imageset/pin_blank_red@1x.png | Bin 0 -> 2136 bytes .../MapPin.imageset/pin_blank_red@2x.png | Bin 0 -> 3392 bytes .../MapPin.imageset/pin_blank_red@3x.png | Bin 0 -> 2866 bytes .../pin.imageset/Contents.json | 29 ++++++++ .../Images.xcassets/pin.imageset/pin@1x.png | Bin 0 -> 383 bytes .../Images.xcassets/pin.imageset/pin@2x.png | Bin 0 -> 748 bytes .../Images.xcassets/pin.imageset/pin@3x.png | Bin 0 -> 1016 bytes 14 files changed, 75 insertions(+), 72 deletions(-) delete mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End108.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End36.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End72.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Contents.json create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/Contents.json create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/pin_blank_red@1x.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/pin_blank_red@2x.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/pin_blank_red@3x.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/Contents.json create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/pin@1x.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/pin@2x.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/pin@3x.png diff --git a/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json deleted file mode 100644 index e4f68a3f0..000000000 --- a/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/Contents.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal" - }, - { - "filename" : "iOS8_MapPin_End36.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "iOS8_MapPin_End72.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "iOS8_MapPin_End108.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "idiom" : "iphone" - }, - { - "idiom" : "iphone", - "scale" : "1x" - }, - { - "idiom" : "iphone", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "scale" : "1x", - "subtype" : "retina4" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "subtype" : "retina4" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "subtype" : "retina4" - }, - { - "idiom" : "ipad" - }, - { - "idiom" : "ipad", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "scale" : "2x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End108.png b/Examples/ExamplesApp/Assets.xcassets/MapPin.imageset/iOS8_MapPin_End108.png deleted file mode 100644 index 462e1a52b9aad96381bbff52ab651d181d228d41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1909 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 Image { + suggestion.isCollection ? + Image(systemName: "magnifyingglass") : + Image(uiImage: UIImage(named: "pin", in: Bundle.module, with: nil)!) + } } struct ResultRow: View { @@ -342,9 +348,15 @@ private extension Graphic { private extension Symbol { /// A search result marker symbol. static var resultSymbol: MarkerSymbol { - let image = UIImage(named: "MapPin")! + let image = UIImage.mapPin let symbol = PictureMarkerSymbol(image: image) symbol.offsetY = Float(image.size.height / 2.0) return symbol } } + +extension UIImage { + static var mapPin: UIImage { + return UIImage(named: "MapPin", in: Bundle.module, with: nil)! + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/Contents.json new file mode 100644 index 000000000..85253d63b --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pin_blank_red@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pin_blank_red@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pin_blank_red@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/pin_blank_red@1x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/pin_blank_red@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..66d40a82760d3b20ac8cbf00ac664ba30d77893b GIT binary patch literal 2136 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 literal 0 HcmV?d00001 diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/pin_blank_red@3x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/MapPin.imageset/pin_blank_red@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..841ea5db12773e3ead8e46fcd65b5c25c23aa6f9 GIT binary patch literal 2866 zcmdT`d00~07C#80sT9|=XX(A3Y_eMl4mlv=1Oz53gg0$cf}>#x3OK#A1S>VsXM?4_ z1}*C~*kHGE$SF&y^l4_9mgSsjWoUz?*Lnvt?S0kj?|06(&t7}&_4}=1@AZ8r$=h=| zMr)21000;|jm&~)GCblm(eU?Ta6J~D5MtJHSKwToQ6IdBfqdzK3j6N;3Apft8z9hoqfjCgZxn`tZYym-QwE327pN4A0RWke zm}1ISn?#edk&|+qiplFhDmi_<=uguG$%od$J~Q%0$=+gFzq*x#1y@Ktd;z zV1+r4&m&mj$z&Ths7#n!@tVr7rFb+x0~lPzrV7M6G_m5BQWI#tFATDm|i;Rv|U z1TBAx*6L@pUqQd2B|{>91jM6?gdyX{7|IceAyr8oO}^Ch>l{^Sjvoqv|HF7JejLv? z(5ZN)n0Y@epykh)zk*INPgZv-)$w-td`ng3CAf(ojzlP83x%6WKWXW4w*LZrg`1S51$+9^@FXoK+>ihO&=fk^g&l=> z`M6%t#nwYBHx)1**4;@$X=BxP+`r&_G(J!v8$x$Xd$tt&q8&qhjlO`)&lp1KcvaNO z7I+lonO*>LcGGGu1&=Ix8dH!T{<_L@!{|eSWiQPq#%T26@Pn=`kI5qBN@h$`pX7Mo zz@3a=t&E=Rk_8whXr=Wg-po)m6BCcd&fJ;M;j>j4UcN2vZJAqLy%*q2XtQFzkzPuy ziyXUju;sS2Yy!HAvAH$`vqSH(JM z|JuPcK>5?}6YH)UGP`=bg>;S$aONd0kp>Qc!Pi#Ov!d zE5yH0&>uRgcGw^2@skb)Ezta}TbilOD`@PywDbBXYmYzg5DK`{vTBXlXz^_K!9NTZ zUC*idF#4jl%u{{WQCxkc^4j|Hwj<&0RxYGI$rWh(r}cCCPb_RZ$8bZz*vGBaqY-b}*lW%D@&rDnWU*#}tu5nzdRoK_{&R2p^1}x+oKTC* zP(6pUUmPz@2+sul3=IA@&|1h*B;-+(^R_KKL`%Nh&>SCTy00eGXKT~bLL-Yhq9f@_ z7pA}g(-WL~%<&@r$^Ko1TV9{=JJPlNwNO7#wZy8-7yys807#fy;68)VJ_QB(lbrx%zJhFNTy4FFB*<-icXYN2W z&#cB{gAL>?q%T7xBD-!k_SP>7Tqyl3Ehsy`^yOwwwL`VRAlzR zPHbsue#g1hCn0uCjz_6nMhdIC8e0Iqdzvgzv}NvFYl=O%SuSh2i5d8(PObyK!e-Wd zqv?YQt91+2>ql*uUwpl64RCv#WH_s-ytzVlGv#)3vvkcMq}5t}uraDKR*tD;o|dO#dsvKjL;yZn4YOV&Gj?a|JSjIUwkUL(}wkD<;aPziXFF{cqucFz#|A*DaEzbKHCecmXCcp|2LUFP7)^^tpbtUkO` zT2_omPCud%Z#;12(Yokj@lafWlnA^uj<$UFdN`u4fEF7A%)6*TveM$ayn!r|q)@%kU>; z_flATHx^ZUq!F0A;@($hXy=;mD*=51YB4Mg_utY^<3&<-iY1PgZcyZMnw+VT8!UGP zeR68mP9cxsY5Jy(&i$39>YauQv~xY>cR>>Ku5*92Qms>ahf7w465WZcQs--3UqbWS zXOK(Rc?Ck=z+G$V0+7N~av~lMFMNL*eAyTl1IoD{QoF5u+AzIicDkSR?qOn**tY0Y z|Jj;z>XT)$SO%xPIbGHcL^HE)oK&Ab!@beqva~i{_9?jKMut=@K6d!k;zRSc7GHkr z{qfe5bASH+p)EbUZsEY-~Mf@-JF%m*o%hmJBv=Ay6-q0_h7ngCe1C(D{ryR zLAQCJUFanQG&NV>xA97z) AMgRZ+ literal 0 HcmV?d00001 diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/Contents.json new file mode 100644 index 000000000..e3ded2e80 --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/Contents.json @@ -0,0 +1,29 @@ +{ + "images" : [ + { + "idiom" : "universal" + }, + { + "filename" : "pin@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pin@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pin@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/pin@1x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/pin.imageset/pin@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..b26fd32c57f1a87b7c77471330dbef6048843d83 GIT binary patch literal 383 zcmV-_0f7FAP)k~*$u}vY5ZY1Gt58#KRX8weMTXYH-!m|hWr*+xcKV; zRb1i?O`M0XiT7|;^NP;|ku8){>Sb&P&U7AwQ|w3Z4sepk;0f~)yahaF6$iRBmhl?m zVeH@;tq>1|+6zXqLDPO+grkpL?5tt^S~L0TIXbq1%x4AYqBMML_K%WL%mB{y*(c4B{E3V`#y%f zNi+%r9_l4BE*qQhSo4{z`6O#%nnh=_L7r!F%gw9ijHwwoE-6gsBGegWqZx7D>oi&(`^8o5ENfeyqd zBt6JH;_oRG#2WaGc%;1F;+n_6KB8NyGd>M~xD)jPhk<*DzY^aN&%gb^4#eYUp|tud zk(VH*P$85Cil#JBG^K%}`L7Ms9P@PzH4k-CGfA{-4&i2^Dr=~ElaAU_QD?DJoGO#~%g(O|KOLE=qRfC~gB zUXf%XPW+~&m6(jpL|cegG=V81u>2j?L1T`DI^Gg+&zm%WSG1{kMH6A7EyXK( e2^#pg+lUKbEh(f%%!c9%YM3B`0hTx&Pb*OngRX}w&6!j&QP%~BUz4_?wsp^@5 zKp+svkFp$dZv$$8qre_uSJVJzqc#DI0K>pS4mD;eyruZR{T;Xp99To_gTOUlRER$W zRIDL(W<003_}jQGz5=)-LPa*Fw`L^%p`cBPnaO_ZVDiVKUqmqFeK zTF!UmyS_`LndOC!@_*r%CKbRBEq8;f;9Fb;0?ujuUesHB+ROk?wa3o83hq)+oz{Qh zFt!OjmtM=NaVe-q>wj?=+k~D=uVr1JubB5{n0ud=d&X5@mq*YJt^dbjP8rYv^q_X_ z^U=H!pgVfb27RZ$({f&>=!>>W!}waDUzn%2exTYAt5TS^?pTSS7Sz2S+fC^dY8jD) zSAuF;nVh&yl6)k2BdCnz(`pd73*3tSmd5lUuadnH)FF(UL|)i+A|GjO!dPzv^$6q6 zmWbIUjPDl4c_V088}r&C?vOU;voIz#K`z%pHXPCVgBEl8wEiJSwOEn0ApJjU^HGjo zSFLYzTcT0xe{)oe6GN#4qdNs`oRF;|lF8;|6~NRnnKEvX(^j`T%NFKEVI(x#IW z(@gQUM3=N_<-{aOY9Yw^M?30;7mAodmeZb7jkzO9Dr|GeU(efABuNT#3eY75IRYIh mN%J`T)v|A#iCQ4w34Z}Gj#1H#dnqIU0000 Date: Mon, 25 Oct 2021 16:21:38 -0500 Subject: [PATCH 0207/1722] Use ProgressView instead of "loading..." text. --- .../BasemapGallery/BasemapGallery.swift | 28 +++++++++++-------- .../BasemapGallery/BasemapGalleryItem.swift | 6 ++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 87070bb18..93d147008 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -112,7 +112,7 @@ extension BasemapGallery { BasemapGalleryItemRow( basemapGalleryItem: basemapGalleryItem, isSelected: basemapGalleryItem == viewModel.currentBasemapGalleryItem, - isValid: basemapGalleryItem.isValid( + isValid: basemapGalleryItem.isLoaded && basemapGalleryItem.isValid( for: viewModel.currentSpatialReference ) ) @@ -126,6 +126,8 @@ extension BasemapGallery { } } +// Don't check spatial reference until user taps on it. + private struct BasemapGalleryItemRow: View { @ObservedObject var basemapGalleryItem: BasemapGalleryItem let isSelected: Bool @@ -136,20 +138,22 @@ private struct BasemapGalleryItemRow: View { VStack { if !basemapGalleryItem.isLoaded { Spacer() - Text("Loading...") - .font(.caption) + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) Spacer() } - else if let thumbnailImage = basemapGalleryItem.thumbnail { - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .border( - isSelected ? Color.accentColor: Color.clear, - width: 3.0) + else { + if let thumbnailImage = basemapGalleryItem.thumbnail { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .border( + isSelected ? Color.accentColor: Color.clear, + width: 3.0) + } + Text(basemapGalleryItem.name) + .font(.footnote) } - Text(basemapGalleryItem.name) - .font(.footnote) } if !isValid { Color(white: 0.5, opacity: 0.5) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index f9d546c00..63b757cea 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -47,9 +47,6 @@ public class BasemapGalleryItem: ObservableObject { @Published public var loadBasemapsError: Error? = nil - /// The currently executing async task for loading basemap. - private var loadBasemapTask: Task? = nil - /// The basemap this `BasemapGalleryItem` represents. public private(set) var basemap: Basemap @@ -72,6 +69,9 @@ public class BasemapGalleryItem: ObservableObject { @Published public private(set) var isLoaded = false + + /// The currently executing async task for loading basemap. + private var loadBasemapTask: Task? = nil } extension BasemapGalleryItem { From 33d4c0b3d0c8b24f44323a37aef6a829dd0a58cd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 25 Oct 2021 16:28:15 -0500 Subject: [PATCH 0208/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index bfda5cfa9..3c41ede63 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -111,7 +111,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { ) // Convert to SearchResults and return. - return geocodeResults.map{ $0.toSearchResult(searchSource: self) } + return geocodeResults.map { $0.toSearchResult(searchSource: self) } } public func suggest( From 5d77d92f7f142ad1665fde2a4ea64ab5cfd8837f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 25 Oct 2021 16:28:26 -0500 Subject: [PATCH 0209/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 3c41ede63..73d54e9a3 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -143,6 +143,6 @@ extension LocatorSearchSource { ) // Convert to SearchResults and return. - return geocodeResults.map{ $0.toSearchResult(searchSource: self) } + return geocodeResults.map { $0.toSearchResult(searchSource: self) } } } From 42402441c5b4748ad758694d64532be451bb28a3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 25 Oct 2021 16:28:36 -0500 Subject: [PATCH 0210/1722] Update Sources/ArcGISToolkit/Components/Search/SearchError.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchError.swift b/Sources/ArcGISToolkit/Components/Search/SearchError.swift index 5536d6473..aec2589fa 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchError.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchError.swift @@ -27,4 +27,4 @@ extension SearchError { } } -extension SearchError: Equatable { } +extension SearchError: Equatable {} From 21e5c47e1bc17d32d8b73d3b5e6e780fe8ab2432 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 25 Oct 2021 16:31:12 -0500 Subject: [PATCH 0211/1722] Update Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 48b4ad410..d17fa099a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -44,7 +44,7 @@ public class SearchSuggestion { /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. public var suggestResult: SuggestResult? - /// True if the search from this suggestion should be treated like a collection search, false if the + /// `true` if the search from this suggestion should be treated like a collection search, `false` if the /// search would return a single result. This property should be used to display a different icon /// in the UI depending on if this is a category search (like 'Coffee', 'Pizza', or 'Starbucks') and /// false if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). From 349aed263dc030c5cd5d26fa275327a522710644 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 25 Oct 2021 16:45:28 -0500 Subject: [PATCH 0212/1722] PR review changes. --- .../Components/Search/LocatorSearchSource.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index bfda5cfa9..4963bbd7f 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -20,6 +20,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// Creates a locator search source. /// - Parameters: /// - name: Name to show when presenting this source in the UI. + /// - locatorTask: The `LocatorTask` to use for searching. /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. /// - searchArea: Area to be used as a constraint for searches and suggestions. @@ -80,11 +81,11 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// Parameters used for geocoding. Some properties on parameters will be updated automatically /// based on searches. - public private(set) var geocodeParameters: GeocodeParameters = GeocodeParameters() + public let geocodeParameters: GeocodeParameters = GeocodeParameters() /// Parameters used for getting suggestions. Some properties will be updated automatically /// based on searches. - public private(set) var suggestParameters: SuggestParameters = SuggestParameters() + public let suggestParameters: SuggestParameters = SuggestParameters() public func repeatSearch( _ queryString: String, From cfd72089961d6b2ae96241d16e116234e96743b3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 25 Oct 2021 17:39:45 -0500 Subject: [PATCH 0213/1722] PR review changes; remove props on SearchSource -> move to methods. --- .../Search/LocatorSearchSource.swift | 46 ++++++++++--------- .../Components/Search/SearchResult.swift | 42 ++++++----------- .../Components/Search/SearchSource.swift | 42 +++++++++++------ .../Components/Search/SearchSuggestion.swift | 36 ++++++--------- .../Components/Search/SearchViewModel.swift | 46 +++++++++++-------- .../Search/SmartLocatorSearchSource.swift | 38 +++++++++------ .../LocatorSearchSourceTests.swift | 36 ++++++++++++--- .../SmartLocatorSearchSourceTests.swift | 34 ++++++-------- 8 files changed, 175 insertions(+), 145 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 2619be403..e75917746 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -23,8 +23,6 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// - locatorTask: The `LocatorTask` to use for searching. /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. - /// - searchArea: Area to be used as a constraint for searches and suggestions. - /// - preferredSearchLocation: Point to be used as an input to searches and suggestions. public init( name: String = "Locator", locatorTask: LocatorTask = LocatorTask( @@ -33,16 +31,12 @@ public class LocatorSearchSource: ObservableObject, SearchSource { )! ), maximumResults: Int32 = 6, - maximumSuggestions: Int32 = 6, - searchArea: Geometry? = nil, - preferredSearchLocation: Point? = nil + maximumSuggestions: Int32 = 6 ) { self.name = name self.locatorTask = locatorTask self.maximumResults = maximumResults self.maximumSuggestions = maximumSuggestions - self.searchArea = searchArea - self.preferredSearchLocation = preferredSearchLocation self.geocodeParameters.addResultAttributeName("*") } @@ -70,12 +64,6 @@ public class LocatorSearchSource: ObservableObject, SearchSource { } } - /// Area to be used as a constraint for searches and suggestions. - public var searchArea: Geometry? - - /// Point to be used as an input to searches and suggestions. - public var preferredSearchLocation: Point? - /// The locator used by this search source. public private(set) var locatorTask: LocatorTask @@ -89,17 +77,30 @@ public class LocatorSearchSource: ObservableObject, SearchSource { public func repeatSearch( _ queryString: String, - queryExtent: Envelope + searchExtent: Envelope ) async throws -> [SearchResult] { - return try await internalSearch(queryString, queryArea: queryExtent) + return try await internalSearch( + queryString, + searchArea: searchExtent + ) } - public func search(_ queryString: String) async throws -> [SearchResult] { - return try await internalSearch(queryString, queryArea: searchArea) + public func search( + _ queryString: String, + searchArea: Geometry?, + preferredSearchLocation: Point? + ) async throws -> [SearchResult] { + return try await internalSearch( + queryString, + searchArea: searchArea, + preferredSearchLocation: preferredSearchLocation + ) } public func search( - _ searchSuggestion: SearchSuggestion + _ searchSuggestion: SearchSuggestion, + searchArea: Geometry?, + preferredSearchLocation: Point? ) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] } @@ -116,7 +117,9 @@ public class LocatorSearchSource: ObservableObject, SearchSource { } public func suggest( - _ queryString: String + _ queryString: String, + searchArea: Geometry?, + preferredSearchLocation: Point? ) async throws -> [SearchSuggestion] { suggestParameters.searchArea = searchArea suggestParameters.preferredSearchLocation = preferredSearchLocation @@ -133,9 +136,10 @@ public class LocatorSearchSource: ObservableObject, SearchSource { extension LocatorSearchSource { private func internalSearch( _ queryString: String, - queryArea: Geometry? + searchArea: Geometry?, + preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { - geocodeParameters.searchArea = queryArea + geocodeParameters.searchArea = searchArea geocodeParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.geocode( diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 102221b6f..42f727382 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -15,7 +15,7 @@ import UIKit.UIImage import ArcGIS /// Wraps a search result for display. -public class SearchResult { +public struct SearchResult { public init( displayTitle: String, displaySubtitle: String? = nil, @@ -33,61 +33,45 @@ public class SearchResult { } /// Title that should be shown whenever a result is displayed. - public var displayTitle: String + public let displayTitle: String /// Should be shown as a subtitle wherever results are shown. - public var displaySubtitle: String? + public let displaySubtitle: String? /// Image, in the native platform's format, for the result. This should be the marker that would be /// shown on the map, and also shown in the UI. This property is available for convenience so the /// UI doesn't have to worry about whether the `GeoElement` is a graphic or a feature when displaying /// the icon in the UI. - public var markerImage: UIImage? + public let markerImage: UIImage? /// Reference to the search source that created this result. - public var owningSource: SearchSource + public let owningSource: SearchSource /// For locator results, should be the graphic that was used to display the result on the map. /// For feature layer results, should be the result feature. Can be null depending on the type of the /// result, and can have `GeoElement`s without a defined geometry. - public var geoElement: GeoElement? + public let geoElement: GeoElement? /// The viewpoint to be used when the view zooms to a selected result. This property can be `nil` /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. - public var selectionViewpoint: Viewpoint? + public let selectionViewpoint: Viewpoint? + + /// The stable identity of the entity associated with this instance. + public let id = UUID() } // MARK: Extensions -extension SearchResult: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(self) } -} +extension SearchResult: Identifiable { } extension SearchResult: Equatable { public static func == (lhs: SearchResult, rhs: SearchResult) -> Bool { - lhs.hashValue == rhs.hashValue + lhs.id == rhs.id } } extension SearchResult: Hashable { - /// Note: we're not hashing `geoElement.attributes` as results with the same title, - /// subtitle, geometry, and owningSource are considered identical for searching purposes. public func hash(into hasher: inout Hasher) { - hasher.combine(displayTitle) - hasher.combine(displaySubtitle) - - if let geometry = geoElement?.geometry { - hasher.combine(geometry) - } - - if let locatorSource = owningSource as? LocatorSearchSource { - hasher.combine(ObjectIdentifier(locatorSource)) - } - // If you define a custom type that does NOT inherit from - // `LocatorSearchSource`, you will need to add an `else if` check - // for your custom type. - // else if let customSearchSource = owningSource as? MyCustomSearchSource { - // hasher.combine(ObjectIdentifier(customSearchSource)) - // } + hasher.combine(id) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 5f56ea258..77787f757 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -24,35 +24,49 @@ public protocol SearchSource { /// The maximum suggestions to return. Most sources default to 6. var maximumSuggestions: Int32 { get set } - /// The area to be used as a constraint for searches and suggestions. - var searchArea: Geometry? { get set } - - /// The point to be used as an input to searches and suggestions. - var preferredSearchLocation: Point? { get set } - /// Returns the search suggestions for the specified query. - /// - Parameter queryString: The query for which to provide search suggestions. + /// - Parameters: + /// - queryString: The query for which to provide search suggestions. + /// - searchArea: The area used to limit results. + /// - preferredSearchLocation: The location used as a starting point for searches. /// - Returns: An array of search suggestions. - func suggest(_ queryString: String) async throws -> [SearchSuggestion] + func suggest( + _ queryString: String, + searchArea: Geometry?, + preferredSearchLocation: Point? + ) async throws -> [SearchSuggestion] /// Gets search results. /// - Parameters: /// - queryString: Text to be used for query. + /// - searchArea: The area used to limit results. + /// - preferredSearchLocation: The location used as a starting point for searches. /// - Returns: Array of `SearchResult`s - func search(_ queryString: String) async throws -> [SearchResult] + func search( + _ queryString: String, + searchArea: Geometry?, + preferredSearchLocation: Point? + ) async throws -> [SearchResult] /// Returns the search results for the specified search suggestion. - /// - Parameter searchSuggestion: The search suggestion for which to provide - /// search results. + /// - Parameters: + /// - searchSuggestion: The search suggestion for which to provide search results. + /// - searchArea: The area used to limit results. + /// - preferredSearchLocation: The location used as a starting point for searches. /// - Returns: An array of search results. func search( - _ searchSuggestion: SearchSuggestion + _ searchSuggestion: SearchSuggestion, + searchArea: Geometry?, + preferredSearchLocation: Point? ) async throws -> [SearchResult] /// Repeats the last search. /// - Parameters: /// - queryString: Text to be used for query. - /// - queryExtent: Extent used to limit the results. + /// - searchExtent: Extent used to limit the results. /// - Returns: Array of `SearchResult`s - func repeatSearch(_ queryString: String, queryExtent: Envelope) async throws -> [SearchResult] + func repeatSearch( + _ queryString: String, + searchExtent: Envelope + ) async throws -> [SearchResult] } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index d17fa099a..1b7d9e43d 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -11,10 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation import ArcGIS /// Wraps a suggestion for display. -public class SearchSuggestion { +public struct SearchSuggestion { public init( displayTitle: String, displaySubtitle: String? = nil, @@ -30,34 +31,35 @@ public class SearchSuggestion { } /// Title that should be used when displaying a suggestion. - public var displayTitle: String + public let displayTitle: String /// Optional subtitle that can be displayed when showing a suggestion. - public var displaySubtitle: String? + public let displaySubtitle: String? /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the /// view model to be able to accept a suggestion, because a suggestion should only be used with the /// locator that created it. - public var owningSource: SearchSource + public let owningSource: SearchSource /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. - public var suggestResult: SuggestResult? + public let suggestResult: SuggestResult? /// `true` if the search from this suggestion should be treated like a collection search, `false` if the /// search would return a single result. This property should be used to display a different icon /// in the UI depending on if this is a category search (like 'Coffee', 'Pizza', or 'Starbucks') and /// false if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). - public var isCollection: Bool + public let isCollection: Bool + + /// The stable identity of the entity associated with this instance. + public let id = UUID() } -extension SearchSuggestion: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(self) } -} +extension SearchSuggestion: Identifiable { } extension SearchSuggestion: Equatable { public static func == (lhs: SearchSuggestion, rhs: SearchSuggestion) -> Bool { - lhs.hashValue == rhs.hashValue + lhs.id == rhs.id } } @@ -66,18 +68,6 @@ extension SearchSuggestion: Hashable { /// a `SuggestResult` and `suggestResult` will be different for two sepate geocode /// operations even though they represent the same suggestion. public func hash(into hasher: inout Hasher) { - hasher.combine(displayTitle) - hasher.combine(displaySubtitle) - hasher.combine(isCollection) - - if let locatorSource = owningSource as? LocatorSearchSource { - hasher.combine(ObjectIdentifier(locatorSource)) - } - // If you define a custom type that does NOT inherit from - // `LocatorSearchSource`, you will need to add an `else if` check - // for your custom type. - // else if let customSearchSource = owningSource as? MyCustomSearchSource { - // hasher.combine(ObjectIdentifier(customSearchSource)) - // } + hasher.combine(id) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 21a8546ad..7e398484a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -188,17 +188,6 @@ public class SearchViewModel: ObservableObject { /// prior to starting another async task. private var currentTask: Task? - private func makeEffectiveSource( - with searchArea: Geometry?, - preferredSearchLocation: Point? - ) -> SearchSource? { - guard var source = currentSource() else { return nil } - source.searchArea = searchArea - source.preferredSearchLocation = preferredSearchLocation - - return source - } - /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. /// - Parameter searchArea: geometry used to constrain the results. If `nil`, the @@ -259,14 +248,19 @@ extension SearchViewModel { Task { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let queryExtent = geoViewExtent, - let source = makeEffectiveSource(with: queryExtent, preferredSearchLocation: nil) else { + let source = currentSource() else { return } do { // User is performing a search, so set `lastSearchExtent`. lastSearchExtent = geoViewExtent - try await process(searchResults: source.repeatSearch(currentQuery, queryExtent: queryExtent)) + try await process( + searchResults: source.repeatSearch( + currentQuery, + searchExtent: queryExtent + ) + ) } catch is CancellationError { results = nil } catch { @@ -278,14 +272,20 @@ extension SearchViewModel { private func searchTask() -> Task<(), Never> { Task { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { + let source = currentSource() else { return } do { // User is performing a search, so set `lastSearchExtent`. lastSearchExtent = geoViewExtent - try await process(searchResults: source.search(currentQuery)) + try await process( + searchResults: source.search( + currentQuery, + searchArea: queryArea, + preferredSearchLocation: queryCenter + ) + ) } catch is CancellationError { results = nil } catch { @@ -297,12 +297,16 @@ extension SearchViewModel { private func updateSuggestionsTask() -> Task<(), Never> { Task { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = makeEffectiveSource(with: queryArea, preferredSearchLocation: queryCenter) else { + let source = currentSource() else { return } let suggestResult = await Result { - try await source.suggest(currentQuery) + try await source.suggest( + currentQuery, + searchArea: queryArea, + preferredSearchLocation: queryCenter + ) } switch suggestResult { @@ -323,7 +327,13 @@ extension SearchViewModel { do { // User is performing a search, so set `lastSearchExtent`. lastSearchExtent = geoViewExtent - try await process(searchResults: searchSuggestion.owningSource.search(searchSuggestion)) + try await process( + searchResults: searchSuggestion.owningSource.search( + searchSuggestion, + searchArea: queryArea, + preferredSearchLocation: queryCenter + ) + ) } catch is CancellationError { results = nil } catch { diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 54aae0a56..cc18b4c2c 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -24,8 +24,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// - name: Name to show when presenting this source in the UI. /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. - /// - searchArea: Area to be used as a constraint for searches and suggestions. - /// - preferredSearchLocation: Point to be used as an input to searches and suggestions. /// - repeatSearchResultThreshold: The minimum number of search results to attempt to return. /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. public init( @@ -37,8 +35,6 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ), maximumResults: Int32 = 6, maximumSuggestions: Int32 = 6, - searchArea: Geometry? = nil, - preferredSearchLocation: Point? = nil, repeatSearchResultThreshold: Int = 1, repeatSuggestResultThreshold: Int = 6 ) { @@ -46,9 +42,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { name: name, locatorTask: locatorTask, maximumResults: maximumResults, - maximumSuggestions: maximumSuggestions, - searchArea: searchArea, - preferredSearchLocation: preferredSearchLocation + maximumSuggestions: maximumSuggestions ) self.repeatSearchResultThreshold = repeatSearchResultThreshold self.repeatSuggestResultThreshold = repeatSuggestResultThreshold @@ -68,10 +62,16 @@ public class SmartLocatorSearchSource: LocatorSearchSource { public var repeatSuggestResultThreshold: Int = 6 public override func search( - _ queryString: String + _ queryString: String, + searchArea: Geometry?, + preferredSearchLocation: Point? ) async throws -> [SearchResult] { // First, peform super class search. - var results = try await super.search(queryString) + var results = try await super.search( + queryString, + searchArea: searchArea, + preferredSearchLocation: preferredSearchLocation + ) if results.count > repeatSearchResultThreshold || repeatSearchResultThreshold == 0 || geocodeParameters.searchArea == nil { @@ -103,13 +103,19 @@ public class SmartLocatorSearchSource: LocatorSearchSource { } public override func search( - _ searchSuggestion: SearchSuggestion + _ searchSuggestion: SearchSuggestion, + searchArea: Geometry?, + preferredSearchLocation: Point? ) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] } - var results = try await super.search(searchSuggestion) + var results = try await super.search( + searchSuggestion, + searchArea: searchArea, + preferredSearchLocation: preferredSearchLocation + ) if results.count > repeatSearchResultThreshold || geocodeParameters.searchArea == nil { // Result count meets threshold or there were no geographic @@ -140,9 +146,15 @@ public class SmartLocatorSearchSource: LocatorSearchSource { } public override func suggest( - _ queryString: String + _ queryString: String, + searchArea: Geometry?, + preferredSearchLocation: Point? ) async throws -> [SearchSuggestion] { - var results = try await super.suggest(queryString) + var results = try await super.suggest( + queryString, + searchArea: searchArea, + preferredSearchLocation: preferredSearchLocation + ) if results.count > repeatSuggestResultThreshold || repeatSuggestResultThreshold == 0 || suggestParameters.searchArea == nil { diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index b5ba3600f..31b6c4b22 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -24,12 +24,20 @@ class LocatorSearchSourceTests: XCTestCase { locator.maximumResults = 4 XCTAssertEqual(locator.maximumResults, 4) - var searchResults = try await locator.search("Coffee") + var searchResults = try await locator.search( + "Coffee", + searchArea: nil, + preferredSearchLocation: nil + ) var results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 4) locator.maximumResults = 12 - searchResults = try await locator.search("Coffee") + searchResults = try await locator.search( + "Coffee", + searchArea: nil, + preferredSearchLocation: nil + ) results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 12) @@ -37,7 +45,11 @@ class LocatorSearchSourceTests: XCTestCase { locator.geocodeParameters.maxResults = 2 XCTAssertEqual(Int(locator.geocodeParameters.maxResults), 2) XCTAssertEqual(locator.maximumResults, 2) - searchResults = try await locator.search("Coffee") + searchResults = try await locator.search( + "Coffee", + searchArea: nil, + preferredSearchLocation: nil + ) results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 2) } @@ -47,12 +59,20 @@ class LocatorSearchSourceTests: XCTestCase { locator.maximumSuggestions = 4 XCTAssertEqual(locator.maximumSuggestions, 4) - var suggestResults = try await locator.suggest("Coffee") + var suggestResults = try await locator.suggest( + "Coffee", + searchArea: nil, + preferredSearchLocation: nil + ) var results = try XCTUnwrap(suggestResults) XCTAssertEqual(results.count, 4) locator.maximumSuggestions = 12 - suggestResults = try await locator.suggest("Coffee") + suggestResults = try await locator.suggest( + "Coffee", + searchArea: nil, + preferredSearchLocation: nil + ) results = try XCTUnwrap(suggestResults) XCTAssertEqual(results.count, 12) @@ -60,7 +80,11 @@ class LocatorSearchSourceTests: XCTestCase { locator.suggestParameters.maxResults = 2 XCTAssertEqual(Int(locator.suggestParameters.maxResults), 2) XCTAssertEqual(locator.maximumSuggestions, 2) - suggestResults = try await locator.suggest("Coffee") + suggestResults = try await locator.suggest( + "Coffee", + searchArea: nil, + preferredSearchLocation: nil + ) results = try XCTUnwrap(suggestResults) XCTAssertEqual(results.count, 2) } diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 0c7a65510..702806eff 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -21,12 +21,13 @@ import SwiftUI class SmartLocatorSearchSourceTests: XCTestCase { func testRepeatSearchResultThreshold() async throws { let locator = SmartLocatorSearchSource() - locator.searchArea = Envelope.edinburgh // Threshold of 0 means no re-query. locator.repeatSearchResultThreshold = 0 var searchResults = try await locator.search( - "Dunkin' Donuts" + "Dunkin' Donuts", + searchArea: Envelope.edinburgh, + preferredSearchLocation: nil ) var results = try XCTUnwrap(searchResults) XCTAssertEqual(results.count, 0) @@ -34,7 +35,9 @@ class SmartLocatorSearchSourceTests: XCTestCase { // Threshold of 1+ means requery with fewer restrictions locator.repeatSearchResultThreshold = 1 searchResults = try await locator.search( - "Dunkin' Donuts" + "Dunkin' Donuts", + searchArea: Envelope.edinburgh, + preferredSearchLocation: nil ) results = try XCTUnwrap(searchResults) XCTAssertGreaterThanOrEqual(results.count, 1) @@ -42,12 +45,13 @@ class SmartLocatorSearchSourceTests: XCTestCase { func testRepeatSuggestResultThreshold() async throws { let locator = SmartLocatorSearchSource() - locator.searchArea = Envelope.edinburgh - + // Threshold of 0 means no re-query. locator.repeatSuggestResultThreshold = 0 var suggestResults = try await locator.suggest( - "Dunkin' Donuts" + "Dunkin' Donuts", + searchArea: Envelope.edinburgh, + preferredSearchLocation: nil ) var results = try XCTUnwrap(suggestResults) XCTAssertEqual(results.count, 0) @@ -55,25 +59,13 @@ class SmartLocatorSearchSourceTests: XCTestCase { // Threshold of 1 -> requery with fewer restrictions locator.repeatSuggestResultThreshold = 1 suggestResults = try await locator.suggest( - "Dunkin' Donuts" + "Dunkin' Donuts", + searchArea: Envelope.edinburgh, + preferredSearchLocation: nil ) results = try XCTUnwrap(suggestResults) XCTAssertGreaterThanOrEqual(results.count, 1) } - - func testLocalLocatorTask() async throws { - let locatorTask = LocatorTask(url: URL(fileURLWithPath: "/Users/mark1113/san-diego/SanDiego_StreetAddress.loc/")); - let locator = SmartLocatorSearchSource(locatorTask: locatorTask) - - let suggestResults = try await locator.suggest("Coffee") - let suggestions = try XCTUnwrap(suggestResults) - XCTAssertEqual(suggestions.count, 3) - - let searchResults = try await locator.search("Hotel") - let results = try XCTUnwrap(searchResults) - XCTAssertEqual(results.count, 2) - } - } extension Envelope { From 572eb6a68498cd9a2c98b5eda0a830a4a50cc433 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 10:08:57 -0500 Subject: [PATCH 0214/1722] Update hardcoded maps. --- .../Examples/BasemapGalleryExampleView.swift | 86 +++++++++---------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 1a144e428..6276520d5 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -18,58 +18,54 @@ import ArcGISToolkit struct BasemapGalleryExampleView: View { var basemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( - basemap: Basemap(style: .arcGISLightGray), - name: "ArcGIS Light Gray", - description: "A vector basemap for the world featuring a light neutral background style with minimal colors as the base layer and labels as the reference layer.", - thumbnail: UIImage(named: "LightGray") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/0f74af7609054be8a29e0ba5f154f0a8/info/thumbnail/thumbnail1607388219207.jpeg")! + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ), + name: "OpenStreetMap (Blueprint)", + description: "OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. This vector basemap is based on the Daylight map distribution of OSM data and is hosted by Esri. It presents the map in a cartographic style is like a blueprint technical drawing.", + thumbnail: nil ), BasemapGalleryItem( - basemap: Basemap(style: .arcGISNova), - name: "ArcGIS Nova", - description: "A vector basemap for the world featuring a dark background with glowing blue symbology inspired by science-fiction and futuristic themes.", - thumbnail: nil//UIImage(named: "Nova") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/90f86b329f37499096d3715ac6e5ed1f/info/thumbnail/thumbnail1607555507609.jpeg")! + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! + )! + ), + name: "National Geographic Style Map", + description: "This vector web map provides a detailed view of the world featuring beautiful political boundaries, labeling, and background that highlights the differences in the physical characteristics of the land.", + thumbnail: nil ), BasemapGalleryItem( - basemap: Basemap(style: .arcGISNewspaper), - name: "ArcGIS Newspaper", - description: "A vector basemap in black & white design with halftone patterns, red highlights, and stylized fonts to depict a unique \"newspaper\" styled theme.", - thumbnail: UIImage(named: "Newspaper") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/e3c062eedf8b487b8bb5b9b08db1b7a9/info/thumbnail/thumbnail1607553292807.jpeg")! + basemap: Basemap( + url: URL( + string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3" + )! + )!, +// basemap: Basemap( +// item: PortalItem( +// url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! +// )! +// ), + name: "Firefly Imagery Hybrid", + description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer.", + thumbnail: nil ), BasemapGalleryItem( - basemap: Basemap(style: .arcGISNavigationNight), - name: "ArcGIS NavigationNight", - description: "A vector basemap for the world featuring a 'dark mode' version of the `Basemap.Style.arcGISNavigation` style, using the same content.", - thumbnail: UIImage(named: "NavigationNight") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/77073a29526046b89bb5622b6276e933/info/thumbnail/thumbnail1607386977674.jpeg")! + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! + )! + ), + name: "Human Geography Dark Label", + description: "This (v2) vector tile layer provides a detailed basemap for the world, featuring a dark monochromatic style with content adjusted to support Human Geography information. This map is designed for use with Human Geography Dark Detail and Base layers.", + thumbnail: nil ), - BasemapGalleryItem( - basemap: Basemap(style: .arcGISStreets), - name: "ArcGIS Streets", - description: "A vector basemap for the world featuring a classic Esri street map style.", - thumbnail: UIImage(named: "Streets") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/e3e6df1d2f6a485d8a70f28fdd3ce19e/info/thumbnail/thumbnail1607389307240.jpeg")! - ), - BasemapGalleryItem( - basemap: Basemap(style: .arcGISTerrain), - name: "ArcGIS Terrain", - description: "A composite basemap with elevation hillshade (raster), minimal map content like water and land fill, water lines and roads (vector) as the base layers and minimal map content like populated place names, admin and water labels with boundary lines (vector) as the reference layer.", - thumbnail: UIImage(named: "Terrain") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/2ef1306b93c9459ca7c7b4f872c070b9/info/thumbnail/thumbnail1607387869592.jpeg")! - ), - BasemapGalleryItem( - basemap: Basemap(style: .arcGISMidcentury), - name: "ArcGIS Midcentury", - description: "A vector basemap inspired by the art and advertising of the 1950's that presents a unique design option to the ArcGIS basemaps.", - thumbnail: UIImage(named: "Midcentury") - // thumbnailURL: URL(string: "https://www.arcgis.com/sharing/rest/content/items/52d6a28f09704f04b33761ba7c4bf93f/info/thumbnail/thumbnail1607554184831.jpeg")! - ) ] -// let geoModel: GeoModel = Map(basemapStyle: .arcGISNova) - let geoModel: GeoModel = Scene(basemapStyle: .arcGISNova) + let geoModel: GeoModel = Map(basemapStyle: .arcGISNova) +// let geoModel: GeoModel = Scene(basemapStyle: .arcGISNova) @ObservedObject var viewModel = BasemapGalleryViewModel() @@ -97,8 +93,8 @@ struct BasemapGalleryExampleView: View { var body: some View { ZStack(alignment: .topTrailing) { -// MapView(map: geoModel as! Map, viewpoint: initialViewpoint) - SceneView(scene: geoModel as! ArcGIS.Scene, viewpoint: initialViewpoint) + MapView(map: geoModel as! Map, viewpoint: initialViewpoint) +// SceneView(scene: geoModel as! ArcGIS.Scene, viewpoint: initialViewpoint) .overlay( VStack(alignment: .trailing) { if showBasemapGallery { From bc2c05237882ba896dd0f5eb609e24b001c43c69 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 10:09:55 -0500 Subject: [PATCH 0215/1722] Don't check spatial reference; update display of basemap items that have load errors. --- .../BasemapGallery/BasemapGallery.swift | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 93d147008..45bfe08ff 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -31,6 +31,8 @@ public struct BasemapGallery: View { case list } + /// Creates a `BasemapGallery`. + /// - Parameter viewModel: The view model used by the `BasemapGallery`. public init(viewModel: BasemapGalleryViewModel? = nil) { if let viewModel = viewModel { self.viewModel = viewModel @@ -40,6 +42,10 @@ public struct BasemapGallery: View { } } + /// 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 public var viewModel: BasemapGalleryViewModel @@ -48,13 +54,12 @@ public struct BasemapGallery: View { /// Set using the `basemapGalleryStyle` modifier. private var style: BasemapGalleryStyle = .automatic + /// The size class used to determine if the basemap items should dispaly in a list or grid. + /// If the size class is `.regular`, they display in a grid. If it is `.compact`, they display in a list. @Environment(\.horizontalSizeClass) var horizontalSizeClass public var body: some View { GalleryView() -// .onAppear { -// viewModel.fetchBasemaps() -// } } // MARK: Modifiers @@ -111,12 +116,12 @@ extension BasemapGallery { ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow( basemapGalleryItem: basemapGalleryItem, - isSelected: basemapGalleryItem == viewModel.currentBasemapGalleryItem, - isValid: basemapGalleryItem.isLoaded && basemapGalleryItem.isValid( - for: viewModel.currentSpatialReference - ) + isSelected: basemapGalleryItem == viewModel.currentBasemapGalleryItem ) .onTapGesture { + // Don't check spatial reference until user taps on it. + // At this point, we need to get errors from setting the basemap (in the model?). + // Error in the model, displayed in the gallery. viewModel.currentBasemapGalleryItem = basemapGalleryItem } } @@ -126,12 +131,9 @@ extension BasemapGallery { } } -// Don't check spatial reference until user taps on it. - private struct BasemapGalleryItemRow: View { @ObservedObject var basemapGalleryItem: BasemapGalleryItem let isSelected: Bool - let isValid: Bool var body: some View { ZStack { @@ -143,24 +145,32 @@ private struct BasemapGalleryItemRow: View { Spacer() } else { - if let thumbnailImage = basemapGalleryItem.thumbnail { - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .border( - isSelected ? Color.accentColor: Color.clear, - width: 3.0) + ZStack { + if let thumbnailImage = basemapGalleryItem.thumbnail { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .border( + isSelected ? Color.accentColor: Color.clear, + width: 3.0) + } + if basemapGalleryItem.loadBasemapsError != nil { + Spacer() + Image(systemName: "minus.circle.fill") + .font(.title) + .foregroundColor(.red) + Spacer() + } } - Text(basemapGalleryItem.name) - .font(.footnote) } + Text(basemapGalleryItem.name) + .font(.footnote) + .multilineTextAlignment(.center) } - if !isValid { - Color(white: 0.5, opacity: 0.5) - .blur(radius: 0.0, opaque: true) - } - } - .allowsHitTesting(isValid) + .allowsHitTesting( + basemapGalleryItem.isLoaded && + basemapGalleryItem.loadBasemapsError == nil + ) } } From da91557bebc06a84a23a5a9526b05f6f43a87de5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 10:11:07 -0500 Subject: [PATCH 0216/1722] Don't check spatial reference; code comments, errors, and cleanup. --- .../BasemapGallery/BasemapGalleryItem.swift | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 63b757cea..9ce2a568e 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -22,6 +22,12 @@ public class BasemapGalleryItem: ObservableObject { return UIImage(named: "DefaultBasemap")! } + /// Creates a `BasemapGalleryItem`. + /// - Parameters: + /// - basemap: The `Basemap` represented by the item. + /// - name: The item name. + /// - description: The item description. + /// - thumbnail: The thumbnail used to represent the item. public init( basemap: Basemap, name: String? = nil, @@ -39,34 +45,29 @@ public class BasemapGalleryItem: ObservableObject { loadBasemapTask = Task { await loadBasemap() } } -// deinit { -// loadBasemapTask.cancel() -// fetchBasemapTask.cancel() -// } - @Published public var loadBasemapsError: Error? = nil /// The basemap this `BasemapGalleryItem` represents. public private(set) var basemap: Basemap - private var nameOverride: String? = nil /// The name of this `Basemap`. @Published public private(set) var name: String = "" + private var nameOverride: String? = nil - private var descriptionOverride: String? = nil /// The description which will be used in the gallery. @Published public private(set) var description: String? = nil + private var descriptionOverride: String? = nil - private var thumbnailOverride: UIImage? = nil /// The thumbnail which will be displayed in the gallery. @Published public private(set) var thumbnail: UIImage? = nil + private var thumbnailOverride: UIImage? = nil - public private(set) var spatialReference: SpatialReference? = nil - + /// Denotes whether loading the `basemap` has been attempted. + /// If the loading of the item generates an error, `isLoaded` will be true. @Published public private(set) var isLoaded = false @@ -76,54 +77,27 @@ public class BasemapGalleryItem: ObservableObject { extension BasemapGalleryItem { private func loadBasemap() async { + var loadError: Error? = nil do { - print("pre-basemap.load()") try await basemap.load() - print("basemap loaded!") if let loadableImage = basemap.item?.thumbnail { try await loadableImage.load() } - - //TODO: use the item.spatialreferenceName to create a spatial reference instead of always loading the first base layer. - // Determine the spatial reference of the basemap -// if let item = basemap.item as? PortalItem { -// try await item.load() -// } - - print("sr = \(basemap.item?.spatialReferenceName ?? "no name"); item: \(String(describing: (basemap.item as? PortalItem)?.loadStatus))") - if let layer = basemap.baseLayers.first { - try await layer.load() - spatialReference = layer.spatialReference - } - - //TODO: Add sr checking and setting of sr to bmgi (isValid???); and what to do with errors... - - await update() } catch { - loadBasemapsError = error + loadError = error } + await update(error: loadError) } @MainActor - func update() { - self.name = nameOverride ?? basemap.name - self.description = descriptionOverride ?? basemap.item?.description - self.thumbnail = thumbnailOverride ?? + func update(error: Error?) { + name = nameOverride ?? basemap.name + description = descriptionOverride ?? basemap.item?.description + thumbnail = thumbnailOverride ?? (basemap.item?.thumbnail?.image ?? BasemapGalleryItem.defaultThumbnail) - + loadBasemapsError = error isLoaded = true } - - /// Returns whether the basemap gallery item is valid and ok to use. - /// - Parameter item: item to match spatial references with. - /// - Returns: true if the item is loaded and either `item`'s spatial reference is nil - /// or matches `spatialReference`. - public func isValid(for otherSpatialReference: SpatialReference?) -> Bool { - print("name: \(name); isLoaded = \(isLoaded); loadStatus = \(basemap.loadStatus)") - guard isLoaded else { return false } - return otherSpatialReference == nil || otherSpatialReference == spatialReference -// return true - } } extension BasemapGalleryItem: Identifiable { From e9475fd271089f21b6b13259fec8cdf091eae194 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 10:17:09 -0500 Subject: [PATCH 0217/1722] don't check spatial reference. --- .../BasemapGalleryViewModel.swift | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 42fa92bb2..bc95ba488 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -27,7 +27,7 @@ public class BasemapGalleryViewModel: ObservableObject { public init( geoModel: GeoModel? = nil, portal: Portal? = nil, - basemapGalleryItems: [BasemapGalleryItem] = [] // TODO: Doc passing this in. + basemapGalleryItems: [BasemapGalleryItem] = [] ) { self.geoModel = geoModel self.portal = portal @@ -67,6 +67,7 @@ public class BasemapGalleryViewModel: ObservableObject { fetchBasemapTask = Task { await fetchBasemaps() } } } + /// The list of basemaps currently visible in the gallery. Items added or removed from this list will /// update the gallery. @Published @@ -82,16 +83,6 @@ public class BasemapGalleryViewModel: ObservableObject { } } - public var currentSpatialReference: SpatialReference? { - guard let scene = geoModel as? ArcGIS.Scene, - scene.sceneViewTilingScheme == .webMercator else { - return geoModel?.spatialReference - } - return .webMercator - } - - // TODO: write tests to check on loading stuff, setting portal and other props, etc. - /// The currently executing async task for fetching basemaps from the portal. /// `fetchBasemapTask` should be cancelled prior to starting another async task. private var fetchBasemapTask: Task? = nil @@ -101,7 +92,9 @@ public class BasemapGalleryViewModel: ObservableObject { guard let portal = portal else { return } do { - basemapGalleryItems += try await portal.developerBasemaps.map { BasemapGalleryItem.init(basemap: $0) } + basemapGalleryItems += try await portal.developerBasemaps.map { + BasemapGalleryItem.init(basemap: $0) + } } catch { fetchBasemapsError = error } @@ -127,15 +120,3 @@ public class BasemapGalleryViewModel: ObservableObject { } } } - -//extension SpatialReference { -// public func effectivelyEquals(_ otherSpatialReference: SpatialReference?) -> Bool { -// guard let otherSR = otherSpatialReference else { false } -// -// guard let self != otherSpatialReference else { true } -// if self == otherSR { -// return true -// } -// else -// } -//} From a521e7defc17a882ad267b99957476ee74a5f06d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:13:57 -0500 Subject: [PATCH 0218/1722] Add todos. --- Examples/Examples/BasemapGalleryExampleView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 6276520d5..23dfd5953 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -15,6 +15,10 @@ import SwiftUI import ArcGIS import ArcGISToolkit +// TODO: once user taps on a map (or maybe in the view model setter for current item) +// TODO: then check if SRs match and don't set basemap if they don't. Figure out +// TODO: how to then gray out the item in the gallery. + struct BasemapGalleryExampleView: View { var basemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( From 5243cba2485386d8f4ebb65798fe43eb2f18b9fa Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:26:42 -0500 Subject: [PATCH 0219/1722] Update Sources/ArcGISToolkit/Components/Search/SearchResult.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchResult.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 42f727382..69123fc6e 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -62,7 +62,7 @@ public struct SearchResult { // MARK: Extensions -extension SearchResult: Identifiable { } +extension SearchResult: Identifiable {} extension SearchResult: Equatable { public static func == (lhs: SearchResult, rhs: SearchResult) -> Bool { From 04f8c11bee1178a6d6e9200d1845a70204252bc3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:26:59 -0500 Subject: [PATCH 0220/1722] Update Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 1b7d9e43d..67bdbba12 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -55,7 +55,7 @@ public struct SearchSuggestion { public let id = UUID() } -extension SearchSuggestion: Identifiable { } +extension SearchSuggestion: Identifiable {} extension SearchSuggestion: Equatable { public static func == (lhs: SearchSuggestion, rhs: SearchSuggestion) -> Bool { From 16aca7c81a782c93be5147c78a135a19c72e6a1d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:27:31 -0500 Subject: [PATCH 0221/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 93c06d599..2ab2041b2 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -22,9 +22,11 @@ public struct SearchView: View { /// - searchViewModel: The view model used by `SearchView`. /// - viewpoint: The `Viewpoint` used to zoom to results. /// - resultsOverlay: The `GraphicsOverlay` used to display results. - public init(searchViewModel: SearchViewModel? = nil, - viewpoint: Binding? = nil, - resultsOverlay: Binding? = nil) { + public init( + searchViewModel: SearchViewModel? = nil, + viewpoint: Binding? = nil, + resultsOverlay: Binding? = nil + ) { if let searchViewModel = searchViewModel { self.searchViewModel = searchViewModel } From 8ecb558bef0e9c6691dad83bfdf814ddd8a5086f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:27:48 -0500 Subject: [PATCH 0222/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 2ab2041b2..e9130abff 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -62,7 +62,7 @@ public struct SearchView: View { /// Note: this is set using the `noResultMessage` modifier. private var noResultMessage = "No results found" - public var searchBarWidth: CGFloat = 360.0 + private var searchBarWidth: CGFloat = 360.0 @State private var shouldZoomToResults = true From 4d73efaa1adb6e455931854b2a7f6f136e10271f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:28:12 -0500 Subject: [PATCH 0223/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e9130abff..ec0e568d4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -72,7 +72,7 @@ public struct SearchView: View { private var showResultListView: Bool = true public var body: some View { - VStack (alignment: .center) { + VStack { HStack { Spacer() VStack (alignment: .center) { From e68e5abe4fcf3a642158ab768b30e53bfddbf729 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:28:29 -0500 Subject: [PATCH 0224/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index ec0e568d4..badf53fb1 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -75,7 +75,7 @@ public struct SearchView: View { VStack { HStack { Spacer() - VStack (alignment: .center) { + VStack { TextField( searchViewModel.defaultPlaceholder, text: $searchViewModel.currentQuery From 75a452c34c120cbcdc83e51d0a908114600df242 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:29:10 -0500 Subject: [PATCH 0225/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index badf53fb1..88c901e4b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -79,10 +79,8 @@ public struct SearchView: View { TextField( searchViewModel.defaultPlaceholder, text: $searchViewModel.currentQuery - ) { _ in - } onCommit: { - searchViewModel.commitSearch() - } + ) + .onSubmit { searchViewModel.commitSearch() } .esriDeleteTextButton(text: $searchViewModel.currentQuery) .esriSearchButton { searchViewModel.commitSearch() } .esriShowResultsButton( From 4297c77df6758726a54a84e3f0c64918de86e214 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:29:41 -0500 Subject: [PATCH 0226/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 88c901e4b..e24e0fd23 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -95,8 +95,7 @@ public struct SearchView: View { selectedResult: $searchViewModel.selectedResult, noResultMessage: noResultMessage ) - } - if let suggestions = searchViewModel.suggestions { + } else if let suggestions = searchViewModel.suggestions { SearchSuggestionList( suggestionResults: suggestions, currentSuggestion: $searchViewModel.currentSuggestion, From 9fdb4657e5e78371be93a8ab4bbe1e6c3abfd382 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:30:25 -0500 Subject: [PATCH 0227/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e24e0fd23..e9398b87a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -116,12 +116,8 @@ public struct SearchView: View { } } .listStyle(.plain) - .onChange(of: searchViewModel.results) { - display(searchResults: $0) - } - .onChange(of: searchViewModel.selectedResult) { - display(selectedResult: $0) - } + .onChange(of: searchViewModel.results, perform: display(searchResults:)) + .onChange(of: searchViewModel.selectedResult, perform: display(selectedResult:)) .onReceive(searchViewModel.$currentQuery) { _ in searchViewModel.updateSuggestions() } From 51be14218c141b3f919884d06c61ef040c8f2add Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:31:01 -0500 Subject: [PATCH 0228/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e9398b87a..92d3b5a52 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -132,7 +132,7 @@ public struct SearchView: View { /// custom result list to show results in a separate list, disconnected from the rest of the search view. /// Defaults to `true`. /// - Parameter enableResultListView: The new value. - /// - Returns: The `SearchView`. + /// - Returns: A new `SearchView`. public func enableResultListView(_ enableResultListView: Bool) -> Self { var copy = self copy.enableResultListView = enableResultListView From 661b9b469b6a48a8c35c038b8146b612ae1c122a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:31:18 -0500 Subject: [PATCH 0229/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 92d3b5a52..a28277374 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -141,7 +141,7 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. Defaults to "No results found". /// - Parameter noResultMessage: The new value. - /// - Returns: The `SearchView`. + /// - Returns: A new `SearchView`. public func noResultMessage(_ noResultMessage: String) -> Self { var copy = self copy.noResultMessage = noResultMessage From 306d46c84b80f63ab5768482d78446e926f4a63a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:31:49 -0500 Subject: [PATCH 0230/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a28277374..1be07785a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -150,7 +150,7 @@ public struct SearchView: View { /// The width of the search bar. /// - Parameter searchBarWidth: The desired width of the search bar. - /// - Returns: The `SearchView`. + /// - Returns: A new `SearchView`. public func searchBarWidth(_ searchBarWidth: CGFloat) -> Self { var copy = self copy.searchBarWidth = searchBarWidth From cf7b7305718548087ab412f6d65cbb7cef25b30e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:33:02 -0500 Subject: [PATCH 0231/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/SearchView.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 1be07785a..0fbf3ebf3 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -162,13 +162,11 @@ extension SearchView { private func display(searchResults: Result<[SearchResult], SearchError>?) { switch searchResults { case .success(let results): - var resultGraphics = [Graphic]() - results.forEach({ result in - if let graphic = result.geoElement as? Graphic { - graphic.updateGraphic(withResult: result) - resultGraphics.append(graphic) - } - }) + let resultGraphics: [Graphic] = results.compactMap { result in + guard let graphic = result.geoElement as? Graphic else { return nil } + graphic.updateGraphic(withResult: result) + return graphic + } resultsOverlay?.wrappedValue.removeAllGraphics() resultsOverlay?.wrappedValue.addGraphics(resultGraphics) From f67d69bd51e26e0e254947d6c00be987ad559031 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:33:29 -0500 Subject: [PATCH 0232/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 0fbf3ebf3..e578474c4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -170,7 +170,7 @@ extension SearchView { resultsOverlay?.wrappedValue.removeAllGraphics() resultsOverlay?.wrappedValue.addGraphics(resultGraphics) - if resultGraphics.count > 0, + if !resultGraphics.isEmpty, let envelope = resultsOverlay?.wrappedValue.extent, shouldZoomToResults { let builder = EnvelopeBuilder(envelope: envelope) From 3bec8add4428c1122543ea456e5bc2fc1a09f39c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 13:33:51 -0500 Subject: [PATCH 0233/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e578474c4..2d3ccb3a8 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -180,8 +180,7 @@ extension SearchView { targetExtent: targetExtent ) searchViewModel.lastSearchExtent = targetExtent - } - else { + } else { viewpoint?.wrappedValue = nil } default: From ce024a1ded4ce8d8a11bbb1485dfaf4194ac78e6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 16:54:20 -0500 Subject: [PATCH 0234/1722] PR review changes. --- .../Components/Search/SearchResult.swift | 6 +- .../Components/Search/SearchSuggestion.swift | 6 +- .../Components/Search/SearchView.swift | 20 ++--- .../Extensions/SuggestResult.swift | 1 - .../ArcGISToolkit/Utility/SearchField.swift | 75 +++++++++++++++++++ 5 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 Sources/ArcGISToolkit/Utility/SearchField.swift diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 42f727382..b0cb024c3 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -16,9 +16,9 @@ import ArcGIS /// Wraps a search result for display. public struct SearchResult { - public init( + internal init( displayTitle: String, - displaySubtitle: String? = nil, + displaySubtitle: String = "", markerImage: UIImage? = nil, owningSource: SearchSource, geoElement: GeoElement? = nil, @@ -36,7 +36,7 @@ public struct SearchResult { public let displayTitle: String /// Should be shown as a subtitle wherever results are shown. - public let displaySubtitle: String? + public let displaySubtitle: String /// Image, in the native platform's format, for the result. This should be the marker that would be /// shown on the map, and also shown in the UI. This property is available for convenience so the diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 1b7d9e43d..8464d1576 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -16,9 +16,9 @@ import ArcGIS /// Wraps a suggestion for display. public struct SearchSuggestion { - public init( + internal init( displayTitle: String, - displaySubtitle: String? = nil, + displaySubtitle: String = "", owningSource: SearchSource, suggestResult: SuggestResult? = nil, isCollection: Bool @@ -34,7 +34,7 @@ public struct SearchSuggestion { public let displayTitle: String /// Optional subtitle that can be displayed when showing a suggestion. - public let displaySubtitle: String? + public let displaySubtitle: String /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the /// view model to be able to accept a suggestion, because a suggestion should only be used with the diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 93c06d599..34d25fd6b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -74,20 +74,14 @@ public struct SearchView: View { HStack { Spacer() VStack (alignment: .center) { - TextField( - searchViewModel.defaultPlaceholder, - text: $searchViewModel.currentQuery - ) { _ in - } onCommit: { - searchViewModel.commitSearch() - } - .esriDeleteTextButton(text: $searchViewModel.currentQuery) - .esriSearchButton { searchViewModel.commitSearch() } - .esriShowResultsButton( - isHidden: !enableResultListView, + SearchField( + defaultPlaceholder: searchViewModel.defaultPlaceholder, + currentQuery: $searchViewModel.currentQuery, + isShowResultsHidden: !enableResultListView, showResults: $showResultListView ) - .esriBorder() + .onSubmit { searchViewModel.commitSearch() } + if enableResultListView, showResultListView { if let results = searchViewModel.results { SearchResultList( @@ -126,8 +120,6 @@ public struct SearchView: View { .onReceive(searchViewModel.$currentQuery) { _ in searchViewModel.updateSuggestions() } - - Spacer() } // MARK: Modifiers diff --git a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift index 18239ef6f..ef634bb42 100644 --- a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift +++ b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift @@ -19,7 +19,6 @@ extension SuggestResult { /// - Returns: The new `SearchSuggestion`. func toSearchSuggestion(searchSource: SearchSource) -> SearchSuggestion { return SearchSuggestion(displayTitle: label, - displaySubtitle: nil, owningSource: searchSource, suggestResult: self, isCollection: isCollection) diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift new file mode 100644 index 000000000..abd726b93 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -0,0 +1,75 @@ +// +// SwiftUIView.swift +// +// +// Created by Mark Dostal on 10/27/21. +// + +import SwiftUI + +struct SearchField: View { + let defaultPlaceholder: String + var currentQuery: Binding + let isShowResultsHidden: Bool + var showResults: Binding + var onSubmit: () -> Void = {} + + internal init( + defaultPlaceholder: String = "", + currentQuery: Binding, + isShowResultsHidden: Bool, + showResults: Binding, + onSubmit: @escaping () -> Void = {} + ) { + self.defaultPlaceholder = defaultPlaceholder + self.currentQuery = currentQuery + self.isShowResultsHidden = isShowResultsHidden + self.showResults = showResults + self.onSubmit = onSubmit + } + + var body: some View { + HStack { + // Search button + Button( + action: onSubmit, + label: { + Image(systemName: "magnifyingglass.circle.fill") + .foregroundColor(Color(uiColor: .opaqueSeparator)) + } + ) + + // Search text field + TextField( + defaultPlaceholder, + text: currentQuery + ) + .onSubmit { onSubmit() } + + // Delete text button + if !currentQuery.wrappedValue.isEmpty { + Button( + action: { currentQuery.wrappedValue = "" }, + label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(Color(.opaqueSeparator)) + } + ) + } + + // Show Results button + if !isShowResultsHidden { + Button( + action: { showResults.wrappedValue.toggle() }, + label: { + Image(systemName: "eye") + .symbolVariant(!showResults.wrappedValue ? .none : .slash) + .symbolVariant(.fill) + .foregroundColor(Color(.opaqueSeparator)) + } + ) + } + } + .esriBorder() + } +} From 9334e7ed8e2e3eb981b08a9f354fe85ec777b23f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Oct 2021 18:06:35 -0500 Subject: [PATCH 0235/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- .../Components/Search/SearchView.swift | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index cbde77c3d..36213f60e 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -197,36 +197,29 @@ struct SearchResultList: View { var noResultMessage: String var body: some View { - Group { + List { switch searchResults { case .success(let results): if results.count > 1 { // Only show the list if we have more than one result. - List { - ForEach(results) { result in - HStack { - SearchResultRow(result: result) - .onTapGesture { - selectedResult = result - } - if result == selectedResult { - Spacer() - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) + ForEach(results) { result in + HStack { + SearchResultRow(result: result) + .onTapGesture { + selectedResult = result } + if result == selectedResult { + Spacer() + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) } } } - } - else if results.isEmpty { - List { - Text(noResultMessage) - } + } else if results.isEmpty { + Text(noResultMessage) } case .failure(let error): - List { - Text(error.localizedDescription) - } + Text(error.localizedDescription) } } .esriBorder(padding: EdgeInsets()) From 9d7742a04dae72953ba35bf55ea9bf0ddf128615 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 28 Oct 2021 12:59:51 -0500 Subject: [PATCH 0236/1722] Redo ResultRow and other PR review changes. --- .../Components/Search/SearchView.swift | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index cbde77c3d..36579c6b3 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -158,7 +158,7 @@ extension SearchView { case .success(let results): let resultGraphics: [Graphic] = results.compactMap { result in guard let graphic = result.geoElement as? Graphic else { return nil } - graphic.updateGraphic(withResult: result) + graphic.update(with: result) return graphic } resultsOverlay.removeAllGraphics() @@ -205,7 +205,7 @@ struct SearchResultList: View { List { ForEach(results) { result in HStack { - SearchResultRow(result: result) + ResultRow(searchResult: result) .onTapGesture { selectedResult = result } @@ -246,7 +246,7 @@ struct SearchSuggestionList: View { List { if suggestions.count > 0 { ForEach(suggestions) { suggestion in - SuggestionResultRow(suggestion: suggestion) + ResultRow(searchSuggestion: suggestion) .onTapGesture() { currentSuggestion = suggestion } @@ -269,62 +269,62 @@ struct SearchSuggestionList: View { } } -struct SearchResultRow: View { - var result: SearchResult - +struct ResultRow: View { + var title: String + var subtitle: String = "" + var image: AnyView + var body: some View { HStack { - Image(uiImage: UIImage.mapPin) - .scaleEffect(0.65) - ResultRow( - title: result.displayTitle, - subtitle: result.displaySubtitle - ) + image + VStack (alignment: .leading){ + Text(title) + .font(.callout) + if !subtitle.isEmpty { + Text(subtitle) + .font(.caption) + } + } } } } -struct SuggestionResultRow: View { - var suggestion: SearchSuggestion - - var body: some View { - HStack { - mapPinImage() - .foregroundColor(.secondary) - ResultRow( - title: suggestion.displayTitle, - subtitle: suggestion.displaySubtitle +extension ResultRow { + init(searchSuggestion: SearchSuggestion) { + self.init( + title: searchSuggestion.displayTitle, + subtitle: searchSuggestion.displaySubtitle, + image: AnyView( + (searchSuggestion.isCollection ? + Image(systemName: "magnifyingglass") : + Image( + uiImage: UIImage( + named: "pin", + in: Bundle.module, with: nil + )! + ) + ) + .foregroundColor(.secondary) ) - } + ) } - func mapPinImage() -> Image { - suggestion.isCollection ? - Image(systemName: "magnifyingglass") : - Image(uiImage: UIImage(named: "pin", in: Bundle.module, with: nil)!) - } -} - -struct ResultRow: View { - var title: String - var subtitle: String? - - var body: some View { - VStack (alignment: .leading){ - Text(title) - .font(.callout) - if let subtitle = subtitle { - Text(subtitle) - .font(.caption) - } - } + init(searchResult: SearchResult) { + self.init( + title: searchResult.displayTitle, + subtitle: searchResult.displaySubtitle, + image: AnyView( + Image(uiImage: UIImage.mapPin) + .scaleEffect(0.65) + ) + ) } } private extension Graphic { - func updateGraphic(withResult result: SearchResult) { + func update(with result: SearchResult) { if symbol == nil { - symbol = .resultSymbol + symbol = Symbol.searchResult() } setAttributeValue(result.displayTitle, forKey: "displayTitle") setAttributeValue(result.displaySubtitle, forKey: "displaySubtitle") @@ -333,7 +333,7 @@ private extension Graphic { private extension Symbol { /// A search result marker symbol. - static var resultSymbol: MarkerSymbol { + static func searchResult() -> MarkerSymbol { let image = UIImage.mapPin let symbol = PictureMarkerSymbol(image: image) symbol.offsetY = Float(image.size.height / 2.0) From 796555db08b703f51a9152deb8c43b8eeaa8ab46 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 28 Oct 2021 13:21:28 -0500 Subject: [PATCH 0237/1722] Revert Results list to a Group to allow not showing the list if there is only 1 result. --- .../Components/Search/SearchView.swift | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 8c382b1a9..36579c6b3 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -197,29 +197,36 @@ struct SearchResultList: View { var noResultMessage: String var body: some View { - List { + Group { switch searchResults { case .success(let results): if results.count > 1 { // Only show the list if we have more than one result. - ForEach(results) { result in - HStack { - ResultRow(searchResult: result) - .onTapGesture { - selectedResult = result + List { + ForEach(results) { result in + HStack { + ResultRow(searchResult: result) + .onTapGesture { + selectedResult = result + } + if result == selectedResult { + Spacer() + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) } - if result == selectedResult { - Spacer() - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) } } } - } else if results.isEmpty { - Text(noResultMessage) + } + else if results.isEmpty { + List { + Text(noResultMessage) + } } case .failure(let error): - Text(error.localizedDescription) + List { + Text(error.localizedDescription) + } } } .esriBorder(padding: EdgeInsets()) From 91b024cb9d4270dcfd89610df00f0ef68ebdcc31 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 28 Oct 2021 13:26:58 -0500 Subject: [PATCH 0238/1722] Undo change to SearchResults list to preserve behavior of not showing the list if there's only one result. --- .../Components/Search/SearchView.swift | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 36579c6b3..46e391ecc 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -239,30 +239,24 @@ struct SearchSuggestionList: View { var noResultMessage: String var body: some View { - Group { + List { switch suggestionResults { case .success(let suggestions): if !suggestions.isEmpty { - List { - if suggestions.count > 0 { - ForEach(suggestions) { suggestion in - ResultRow(searchSuggestion: suggestion) - .onTapGesture() { - currentSuggestion = suggestion - } - } + if suggestions.count > 0 { + ForEach(suggestions) { suggestion in + ResultRow(searchSuggestion: suggestion) + .onTapGesture() { + currentSuggestion = suggestion + } } } } else { - List { - Text(noResultMessage) - } + Text(noResultMessage) } case .failure(let error): - List { - Text(error.errorDescription) - } + Text(error.errorDescription) } } .esriBorder(padding: EdgeInsets()) From be80c767e2f1e9a85cee6577db0ea54348e2726a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 11:06:02 -0500 Subject: [PATCH 0239/1722] Clean up and add basemap with invalid spatial reference. --- .../Examples/BasemapGalleryExampleView.swift | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 23dfd5953..28ec15208 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -43,19 +43,24 @@ struct BasemapGalleryExampleView: View { ), BasemapGalleryItem( basemap: Basemap( - url: URL( - string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3" + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! )! - )!, -// basemap: Basemap( -// item: PortalItem( -// url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! -// )! -// ), + ), name: "Firefly Imagery Hybrid", description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer.", thumbnail: nil ), + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ), + name: nil, + description: "This web map features satellite imagery for the world and high-resolution aerial imagery for many areas. It uses WGS84 Geographic, version 2 tiling scheme.", + thumbnail: nil + ), BasemapGalleryItem( basemap: Basemap( item: PortalItem( From a61ec105e6b43c2c9aad036ebbedd32098d79ee1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 11:06:56 -0500 Subject: [PATCH 0240/1722] Error handling and displaying errors to user. --- .../BasemapGallery/BasemapGallery.swift | 79 +++++++++++++++++-- .../BasemapGallery/BasemapGalleryItem.swift | 54 ++++++++++++- 2 files changed, 124 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 45bfe08ff..e93cbcdbb 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -57,9 +57,18 @@ public struct BasemapGallery: View { /// The size class used to determine if the basemap items should dispaly in a list or grid. /// If the size class is `.regular`, they display in a grid. If it is `.compact`, they display in a list. @Environment(\.horizontalSizeClass) var horizontalSizeClass + + @State + private var alertItem: AlertItem? public var body: some View { GalleryView() + .alert(item: $alertItem) { alertItem in + Alert( + title: Text(alertItem.title), + message: Text(alertItem.message) + ) + } } // MARK: Modifiers @@ -122,7 +131,29 @@ extension BasemapGallery { // Don't check spatial reference until user taps on it. // At this point, we need to get errors from setting the basemap (in the model?). // Error in the model, displayed in the gallery. - viewModel.currentBasemapGalleryItem = basemapGalleryItem + + // TODO: this doesn't work until the basemap is tapped on once, then I assume + // TODO: it's loaded the basemap layers. Figure this out. (load base layers when bm loads?) + if let loadError = basemapGalleryItem.loadBasemapsError { + alertItem = AlertItem(error: loadError) + print("basemaps DON'T match (or error)!") + } + else { + // if !showingBasemapLoadError, + // basemapGalleryItem.matchesSpatialReference(viewModel.geoModel?.spatialReference) { + Task { + try await basemapGalleryItem.updateSpatialReferenceStatus(for: viewModel.geoModel?.spatialReference) + if basemapGalleryItem.spatialReferenceStatus == .match || + basemapGalleryItem.spatialReferenceStatus == .unknown { + print("basemap matches!") + viewModel.currentBasemapGalleryItem = basemapGalleryItem + } + else { + alertItem = AlertItem(geoModelSR: viewModel.geoModel?.spatialReference) + print("Task bm don't match") + } + } + } } } } @@ -143,9 +174,9 @@ private struct BasemapGalleryItemRow: View { ProgressView() .progressViewStyle(CircularProgressViewStyle()) Spacer() - } - else { - ZStack { + + } else { + ZStack(alignment: .center) { if let thumbnailImage = basemapGalleryItem.thumbnail { Image(uiImage: thumbnailImage) .resizable() @@ -154,12 +185,15 @@ private struct BasemapGalleryItemRow: View { isSelected ? Color.accentColor: Color.clear, width: 3.0) } + if basemapGalleryItem.loadBasemapsError != nil { - Spacer() Image(systemName: "minus.circle.fill") .font(.title) .foregroundColor(.red) - Spacer() + } else if basemapGalleryItem.spatialReferenceStatus == .noMatch { + Image(systemName: "x.circle.fill") + .font(.title) + .foregroundColor(.red) } } } @@ -169,8 +203,37 @@ private struct BasemapGalleryItemRow: View { } } .allowsHitTesting( - basemapGalleryItem.isLoaded && - basemapGalleryItem.loadBasemapsError == nil + basemapGalleryItem.isLoaded + ) + } +} + +struct AlertItem { + var title: String = "" + var message: String = "" +} + +extension AlertItem: Identifiable { + public var id: UUID { UUID() } +} + +// TODO: add SR for basemap, if possible (SR property on basemap?) Maybe that can speed up baselayer sr checking... +// TODO: Cleanup all .tapGesture code, alert code, old error/alert stuff +// TODO: Figure out common errors, so I don't need to rely on `Error` or `RuntimeError`. +// TODO: update item's spatialreferenceStatus on main thread. (method marked with @MainActor, the way `update()` is?) +// TODO: add basemap SR to init below. +extension AlertItem { + init(geoModelSR: SpatialReference?) { + self.init( + title: "Spatial Reference mismatch.", + message: "The spatial reference of the basemap does not match that of the geomodel (\(geoModelSR?.description ?? ""))." + ) + } + + init(error: Error) { + self.init( + title: "Error loading basemap.", + message: "\(error.localizedDescription)" ) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 9ce2a568e..1c6dcb297 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -22,6 +22,12 @@ public class BasemapGalleryItem: ObservableObject { return UIImage(named: "DefaultBasemap")! } + public enum SpatialReferenceStatus { + case unknown + case match + case noMatch + } + /// Creates a `BasemapGalleryItem`. /// - Parameters: /// - basemap: The `Basemap` represented by the item. @@ -70,9 +76,14 @@ public class BasemapGalleryItem: ObservableObject { /// If the loading of the item generates an error, `isLoaded` will be true. @Published public private(set) var isLoaded = false - + + @Published + public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown + /// The currently executing async task for loading basemap. private var loadBasemapTask: Task? = nil + + private var lastSpatialReference: SpatialReference? = nil } extension BasemapGalleryItem { @@ -95,6 +106,8 @@ extension BasemapGalleryItem { description = descriptionOverride ?? basemap.item?.description thumbnail = thumbnailOverride ?? (basemap.item?.thumbnail?.image ?? BasemapGalleryItem.defaultThumbnail) + + // TODO: include error messaging alert. loadBasemapsError = error isLoaded = true } @@ -111,5 +124,44 @@ extension BasemapGalleryItem: Equatable { lhs.description == rhs.description && lhs.thumbnail === rhs.thumbnail } +} + +extension BasemapGalleryItem { + public func updateSpatialReferenceStatus( + for referenceSpatialReference: SpatialReference? + ) async throws { + guard let spatialReference = referenceSpatialReference, + basemap.loadStatus == .loaded, + spatialReference != lastSpatialReference + else { return } + + lastSpatialReference = spatialReference + await withThrowingTaskGroup(of: Void.self, + returning: Void.self, + body: { taskGroup in + basemap.baseLayers.forEach { baseLayer in + taskGroup.addTask { + try await baseLayer.load() + return + } + } + }) + + spatialReferenceStatus = matches(spatialReference) ? .match : .noMatch + } + public func matches( + _ spatialReference: SpatialReference? + ) -> Bool { + guard let spatialReference = spatialReference else { return false } + + for baselayer in basemap.baseLayers { + if let baseLayerSR = baselayer.spatialReference, + baseLayerSR != spatialReference { + return false + } + } + + return true + } } From 95d51cefafadefcfba04cf4ce0a1144e53170e46 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 11:07:23 -0500 Subject: [PATCH 0241/1722] Clean up task/async stuff. --- .../BasemapGalleryViewModel.swift | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index bc95ba488..fc958d9bd 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -33,13 +33,13 @@ public class BasemapGalleryViewModel: ObservableObject { self.portal = portal self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) - loadGeoModelTask = Task { await loadGeoModel() } - fetchBasemapTask = Task { await fetchBasemaps() } - } - - deinit { - loadGeoModelTask?.cancel() - fetchBasemapTask?.cancel() + // Note that we don't want to store these tasks and cancel them + // before kicking off another operation becasue both of these + // operations could have been started elsewhere as well as here. + // Canceling them here would also cancel those other operations, + // which we don't want to do. + Task { await load(geoModel: geoModel) } + Task { await fetchBasemaps(from: portal) } } @Published @@ -54,8 +54,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// the geoModel will have its basemap replaced with the selected basemap. public var geoModel: GeoModel? { didSet { - loadGeoModelTask?.cancel() - loadGeoModelTask = Task { await loadGeoModel() } + Task { await load(geoModel: geoModel) } } } @@ -63,8 +62,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// and add them to the `basemapGalleryItems` array. public var portal: Portal? { didSet { - fetchBasemapTask?.cancel() - fetchBasemapTask = Task { await fetchBasemaps() } + Task { await fetchBasemaps(from: portal) } } } @@ -82,13 +80,11 @@ public class BasemapGalleryViewModel: ObservableObject { geoModel?.basemap = item.basemap } } - - /// The currently executing async task for fetching basemaps from the portal. - /// `fetchBasemapTask` should be cancelled prior to starting another async task. - private var fetchBasemapTask: Task? = nil + +// public func /// Fetches the basemaps from `portal`. - private func fetchBasemaps() async { + private func fetchBasemaps(from portal: Portal?) async { guard let portal = portal else { return } do { @@ -100,12 +96,8 @@ public class BasemapGalleryViewModel: ObservableObject { } } - /// The currently executing async task for loading `geoModel`. - /// `loadGeoModelTask` should be cancelled prior to starting another async task. - private var loadGeoModelTask: Task? = nil - /// Loads `geoModel`. - private func loadGeoModel() async { + private func load(geoModel: GeoModel?) async { guard let geoModel = geoModel else { return } do { try await geoModel.load() From 1ad109ff2ff98f5630c195eee1103e7859862e15 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:15:18 -0500 Subject: [PATCH 0242/1722] Update Sources/ArcGISToolkit/Components/Search/SearchResult.swift Co-authored-by: Philip Ridgeway --- .../Components/Search/SearchResult.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index e4d9edcb2..6f8efd022 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -16,22 +16,6 @@ import ArcGIS /// Wraps a search result for display. public struct SearchResult { - internal init( - displayTitle: String, - displaySubtitle: String = "", - markerImage: UIImage? = nil, - owningSource: SearchSource, - geoElement: GeoElement? = nil, - selectionViewpoint: Viewpoint? = nil - ) { - self.displayTitle = displayTitle - self.displaySubtitle = displaySubtitle - self.markerImage = markerImage - self.owningSource = owningSource - self.geoElement = geoElement - self.selectionViewpoint = selectionViewpoint - } - /// Title that should be shown whenever a result is displayed. public let displayTitle: String From 1ce1e5bc71796e161ad1ef8ba5afe92e8018d5bc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:15:32 -0500 Subject: [PATCH 0243/1722] Update Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 38162d81c..c90d33276 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -16,7 +16,7 @@ import ArcGIS /// Wraps a suggestion for display. public struct SearchSuggestion { - internal init( + init( displayTitle: String, displaySubtitle: String = "", owningSource: SearchSource, From ba535fad46a13526249dc479c7d751a3b6a681be Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:16:25 -0500 Subject: [PATCH 0244/1722] Update Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 702806eff..fd5fb8fef 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -64,7 +64,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { preferredSearchLocation: nil ) results = try XCTUnwrap(suggestResults) - XCTAssertGreaterThanOrEqual(results.count, 1) + XCTAssertNotEqual(suggestResults, []) } } From 0bb30ee0fb4a415c12268d93de5724915495d31b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:16:34 -0500 Subject: [PATCH 0245/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 2653abefb..9745a8334 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -414,7 +414,7 @@ class SearchViewModelTests: XCTestCase { Task { model.acceptSuggestion(singleSuggestion) } - results = try await model.$results.compactMap({$0}).dropFirst().first + results = try await model.$results.compactMap({ $0 }).dropFirst().first result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } From b6f1f2b969e4354f512d46577c0df21d48cfdc2c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:16:57 -0500 Subject: [PATCH 0246/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: Philip Ridgeway --- Examples/Examples/SearchExampleView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index a87b8a795..ce0035606 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -32,8 +32,7 @@ struct SearchExampleView: View { ) /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. - @State - var searchResultsOverlay = GraphicsOverlay() + let searchResultsOverlay = GraphicsOverlay() @State private var isNavigating: Bool = false From ddf17d1d591033ef2897b80f9465eeeddbec4df7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:18:14 -0500 Subject: [PATCH 0247/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 46e391ecc..e6f31c41f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -217,8 +217,7 @@ struct SearchResultList: View { } } } - } - else if results.isEmpty { + } else if results.isEmpty { List { Text(noResultMessage) } From 7fcca606c4d8ef406c73f289ea26b155a2033f7a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:42:58 -0500 Subject: [PATCH 0248/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e6f31c41f..85da340c9 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -270,7 +270,7 @@ struct ResultRow: View { var body: some View { HStack { image - VStack (alignment: .leading){ + VStack(alignment: .leading) { Text(title) .font(.callout) if !subtitle.isEmpty { From 2a11c6767bbdb7e874a90d3ec822b4869b65714e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:46:39 -0500 Subject: [PATCH 0249/1722] Update Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../Components/Search/SmartLocatorSearchSource.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index cc18b4c2c..32d337b40 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -92,14 +92,9 @@ public class SmartLocatorSearchSource: LocatorSearchSource { $0.toSearchResult(searchSource: self) } results.append(contentsOf: searchResults) - var allResults: [SearchResult] = Array(Set(results)) // Limit results to `maximumResults`. - if allResults.count > maximumResults { - let dropCount = allResults.count - Int(maximumResults) - allResults = allResults.dropLast(dropCount) - } - return allResults + return Array(results.prefix(Int(maximumResults))) } public override func search( From 8042da52b858aa01308d7f533fb8882aaa37cf11 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:47:36 -0500 Subject: [PATCH 0250/1722] Update Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../Components/Search/SmartLocatorSearchSource.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 32d337b40..59c7fb9be 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -130,14 +130,9 @@ public class SmartLocatorSearchSource: LocatorSearchSource { $0.toSearchResult(searchSource: self) } results.append(contentsOf: searchResults) - var allResults: [SearchResult] = Array(Set(results)) // Limit results to `maximumResults`. - if allResults.count > maximumResults { - let dropCount = allResults.count - Int(maximumResults) - allResults = allResults.dropLast(dropCount) - } - return allResults + return Array(results.prefix(Int(maximumResults))) } public override func suggest( From 9c9f55b40df7b14b4a3972bb2c7f60283a6acd2e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 14:48:50 -0500 Subject: [PATCH 0251/1722] Update Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../Components/Search/SmartLocatorSearchSource.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 59c7fb9be..8d8de319a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -165,13 +165,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { $0.toSearchSuggestion(searchSource: self) } results.append(contentsOf: suggestResults) - var allResults: [SearchSuggestion] = Array(Set(results)) - // Limit results to `maximumResults`. - if allResults.count > maximumSuggestions { - let dropCount = allResults.count - Int(maximumSuggestions) - allResults = allResults.dropLast(dropCount) - } - return allResults + // Limit results to `maximumSuggestions`. + return Array(results.prefix(Int(maximumSuggestions))) } } From 280e4232461cad48bd81888412cc62ee9b3b37c9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:18:34 -0500 Subject: [PATCH 0252/1722] Update Sources/ArcGISToolkit/Extensions/GeocodeResult.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Extensions/GeocodeResult.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift index 334ca503f..8786e9b76 100644 --- a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift +++ b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift @@ -20,10 +20,7 @@ extension GeocodeResult { func toSearchResult(searchSource: SearchSource) -> SearchResult { let subtitle = self.attributes["LongLabel"] as? String ?? "Match percent: \((self.score / 100.0).formatted(.percent))" - var viewpoint: Viewpoint? = nil - if let extent = self.extent { - viewpoint = Viewpoint(targetExtent: extent) - } + let viewpoint = extent.map(Viewpoint.init(targetExtent:)) return SearchResult( displayTitle: self.label, displaySubtitle: subtitle, From 8284eded6dd3bf9e66e3c4e52dde5b3105d5cf51 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:25:46 -0500 Subject: [PATCH 0253/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 31b6c4b22..7b8e58e62 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -11,12 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation - import XCTest import ArcGIS import ArcGISToolkit -import SwiftUI class LocatorSearchSourceTests: XCTestCase { func testMaximumResults() async throws { From dd0bebe09a66baa01a6099ac67e17bc30c342405 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:26:22 -0500 Subject: [PATCH 0254/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 7b8e58e62..e445b1ca7 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -15,7 +15,7 @@ import XCTest import ArcGIS import ArcGISToolkit -class LocatorSearchSourceTests: XCTestCase { +final class LocatorSearchSourceTests: XCTestCase { func testMaximumResults() async throws { let locator = LocatorSearchSource() locator.maximumResults = 4 From 6ae4544dcd6b3416878ccf6c799594c91279beef Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:27:03 -0500 Subject: [PATCH 0255/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index e445b1ca7..a6c0d5a7a 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -26,8 +26,7 @@ final class LocatorSearchSourceTests: XCTestCase { searchArea: nil, preferredSearchLocation: nil ) - var results = try XCTUnwrap(searchResults) - XCTAssertEqual(results.count, 4) + XCTAssertEqual(searchResults.count, 4) locator.maximumResults = 12 searchResults = try await locator.search( From 79df0725938d5a4a14621efbac79ea060388dfef Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:27:58 -0500 Subject: [PATCH 0256/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index a6c0d5a7a..3e1e1275e 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -34,8 +34,7 @@ final class LocatorSearchSourceTests: XCTestCase { searchArea: nil, preferredSearchLocation: nil ) - results = try XCTUnwrap(searchResults) - XCTAssertEqual(results.count, 12) + XCTAssertEqual(searchResults.count, 12) // Set property directly on geocode parameters. locator.geocodeParameters.maxResults = 2 From 8f90a316b4dfe45c3d92240f1a610bddeb6d6626 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:28:46 -0500 Subject: [PATCH 0257/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 3e1e1275e..7b44f2aa4 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -38,7 +38,7 @@ final class LocatorSearchSourceTests: XCTestCase { // Set property directly on geocode parameters. locator.geocodeParameters.maxResults = 2 - XCTAssertEqual(Int(locator.geocodeParameters.maxResults), 2) + XCTAssertEqual(locator.geocodeParameters.maxResults, 2) XCTAssertEqual(locator.maximumResults, 2) searchResults = try await locator.search( "Coffee", From 8b01a56d0a8ddf109276a418a84740b505386c86 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:29:23 -0500 Subject: [PATCH 0258/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 7b44f2aa4..787255fde 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -45,8 +45,7 @@ final class LocatorSearchSourceTests: XCTestCase { searchArea: nil, preferredSearchLocation: nil ) - results = try XCTUnwrap(searchResults) - XCTAssertEqual(results.count, 2) + XCTAssertEqual(searchResults.count, 2) } func testMaximumSuggestions() async throws { From ae259371d9529cec469c30804e847cc8db86209c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:30:10 -0500 Subject: [PATCH 0259/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 787255fde..c32f62016 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -58,8 +58,7 @@ final class LocatorSearchSourceTests: XCTestCase { searchArea: nil, preferredSearchLocation: nil ) - var results = try XCTUnwrap(suggestResults) - XCTAssertEqual(results.count, 4) + XCTAssertEqual(suggestResults.count, 4) locator.maximumSuggestions = 12 suggestResults = try await locator.suggest( From 5dcb932f6ca47c81e358b0aa30623b0b9275c6a4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:31:00 -0500 Subject: [PATCH 0260/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index c32f62016..a3170bb47 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -66,8 +66,7 @@ final class LocatorSearchSourceTests: XCTestCase { searchArea: nil, preferredSearchLocation: nil ) - results = try XCTUnwrap(suggestResults) - XCTAssertEqual(results.count, 12) + XCTAssertEqual(suggestResults.count, 12) // Set property directly on suggest parameters. locator.suggestParameters.maxResults = 2 From 11731ec092fcba74da4ab06c299814f2c2b543b2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:31:23 -0500 Subject: [PATCH 0261/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 9745a8334..14ec92c40 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -11,12 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation - import XCTest import ArcGIS import ArcGISToolkit -import SwiftUI import Combine @MainActor From 394e33d556900eefef4c289f0b8f272d6a465882 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:32:06 -0500 Subject: [PATCH 0262/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index a3170bb47..4cef54df6 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -70,7 +70,7 @@ final class LocatorSearchSourceTests: XCTestCase { // Set property directly on suggest parameters. locator.suggestParameters.maxResults = 2 - XCTAssertEqual(Int(locator.suggestParameters.maxResults), 2) + XCTAssertEqual(locator.suggestParameters.maxResults, 2) XCTAssertEqual(locator.maximumSuggestions, 2) suggestResults = try await locator.suggest( "Coffee", From a93b9060000e37e59f07437c748436fdd54d6118 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Nov 2021 15:33:01 -0500 Subject: [PATCH 0263/1722] Update Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 4cef54df6..567bd78d0 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -77,7 +77,6 @@ final class LocatorSearchSourceTests: XCTestCase { searchArea: nil, preferredSearchLocation: nil ) - results = try XCTUnwrap(suggestResults) - XCTAssertEqual(results.count, 2) + XCTAssertEqual(suggestResults.count, 2) } } From 83a12ace5540a84d5ba53c9344048fe82c5696dd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 2 Nov 2021 09:46:55 -0500 Subject: [PATCH 0264/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 7e398484a..8d6bf9616 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -93,8 +93,8 @@ public class SearchViewModel: ObservableObject { else { return } // Check extent difference. - let widthDiff = fabs(lastExtent.width - newExtent.width) - let heightDiff = fabs(lastExtent.height - newExtent.height) + let widthDiff = abs(lastExtent.width - newExtent.width) + let heightDiff = abs(lastExtent.height - newExtent.height) let widthThreshold = lastExtent.width * 0.25 let heightThreshold = lastExtent.height * 0.25 From e2d4ba51962821767bac5591f8822d60ecdb480c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 2 Nov 2021 09:47:43 -0500 Subject: [PATCH 0265/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 14ec92c40..42507aae5 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -296,7 +296,7 @@ class SearchViewModelTests: XCTestCase { ) // First result within 1500m of Portland. - XCTAssertLessThan(geodeticDistance.distance, 1500.0) + XCTAssertLessThan(geodeticDistance.distance, 1500) // Set queryCenter to Edinburgh model.queryCenter = .edinburgh From cecbee1eda4e5d7c050dfa1c2da156f6eb078f78 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 2 Nov 2021 09:48:56 -0500 Subject: [PATCH 0266/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 8d6bf9616..a1ecb247b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -107,7 +107,7 @@ public class SearchViewModel: ObservableObject { geometry1: lastExtent.center, geometry2: newExtent.center ) - let currentExtentAvg = (lastExtent.width + lastExtent.height / 2.0) + let currentExtentAvg = (lastExtent.width + lastExtent.height) / 2.0 let threshold = currentExtentAvg * 0.25 isEligibleForRequery = (centerDiff ?? 0.0) > threshold } From 9dd53f5dfd6645ab8fe8cdc52c8421a8948a847c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 2 Nov 2021 09:50:08 -0500 Subject: [PATCH 0267/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index a1ecb247b..e43467142 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -190,8 +190,6 @@ public class SearchViewModel: ObservableObject { /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. - /// - Parameter searchArea: geometry used to constrain the results. If `nil`, the - /// `queryArea` property is used instead. If `queryArea` is `nil`, results are not constrained. public func commitSearch() { kickoffTask(searchTask()) } From cded1ccd4ad07c67cc5255f6dadb927ceee15824 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 2 Nov 2021 10:00:59 -0500 Subject: [PATCH 0268/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index e43467142..07f98c954 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -375,7 +375,7 @@ extension SearchViewModel { /// Returns the search source to be used in geocode operations. /// - Returns: The search source to use. func currentSource() -> SearchSource? { - var source: SearchSource? + let source: SearchSource? if let activeSource = activeSource { source = activeSource } else { From 619ae09ed2cb48164a478c45fd3f171917290912 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 2 Nov 2021 14:02:02 -0500 Subject: [PATCH 0269/1722] PR review comments. --- Examples/Examples/SearchExampleView.swift | 22 +-- .../Search/LocatorSearchSource.swift | 12 +- .../Components/Search/SearchResult.swift | 64 +++++---- .../Components/Search/SearchSuggestion.swift | 39 +++--- .../Components/Search/SearchView.swift | 75 +++++----- .../Components/Search/SearchViewModel.swift | 47 ++----- .../Search/SmartLocatorSearchSource.swift | 7 +- .../Extensions/GeocodeResult.swift | 39 ------ .../Extensions/SuggestResult.swift | 26 ---- .../ArcGISToolkit/Utility/SearchField.swift | 41 ++---- .../SearchViewModelTests.swift | 131 +++++++++--------- .../SmartLocatorSearchSourceTests.swift | 11 +- 12 files changed, 205 insertions(+), 309 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/GeocodeResult.swift delete mode 100644 Sources/ArcGISToolkit/Extensions/SuggestResult.swift diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index a87b8a795..6543a32c5 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -19,7 +19,13 @@ import ArcGISToolkit struct SearchExampleView: View { /// The `SearchViewModel` used to define behavior of the `SearchView`. @ObservedObject - var searchViewModel = SearchViewModel() + var searchViewModel = SearchViewModel( + sources: [SmartLocatorSearchSource( + name: "My locator", + maximumResults: 16, + maximumSuggestions: 16 + )] + ) let map = Map(basemapStyle: .arcGISImagery) @@ -75,19 +81,5 @@ struct SearchExampleView: View { .searchBarWidth(360.0) .padding() } - .onAppear() { - setupSearchViewModel() - } - } - - /// Sets up any desired customization on `searchViewModel`. - private func setupSearchViewModel() { - let smartLocator = SmartLocatorSearchSource( - name: "My locator", - maximumResults: 16, - maximumSuggestions: 16 - ) - - searchViewModel.sources = [smartLocator] } } diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index e75917746..40bb4ebf9 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -113,7 +113,9 @@ public class LocatorSearchSource: ObservableObject, SearchSource { ) // Convert to SearchResults and return. - return geocodeResults.map { $0.toSearchResult(searchSource: self) } + return geocodeResults.map { + SearchResult(geocodeResult: $0, searchSource: self) + } } public func suggest( @@ -129,7 +131,9 @@ public class LocatorSearchSource: ObservableObject, SearchSource { parameters: suggestParameters ) // Convert to SearchSuggestions and return. - return geocodeResults.map{ $0.toSearchSuggestion(searchSource: self) } + return geocodeResults.map { + SearchSuggestion(suggestResult: $0, searchSource: self) + } } } @@ -148,6 +152,8 @@ extension LocatorSearchSource { ) // Convert to SearchResults and return. - return geocodeResults.map { $0.toSearchResult(searchSource: self) } + return geocodeResults.map { + SearchResult(geocodeResult: $0, searchSource: self) + } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index e4d9edcb2..663cc9a6d 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -15,46 +15,30 @@ import UIKit.UIImage import ArcGIS /// Wraps a search result for display. -public struct SearchResult { - internal init( - displayTitle: String, - displaySubtitle: String = "", - markerImage: UIImage? = nil, - owningSource: SearchSource, - geoElement: GeoElement? = nil, - selectionViewpoint: Viewpoint? = nil - ) { - self.displayTitle = displayTitle - self.displaySubtitle = displaySubtitle - self.markerImage = markerImage - self.owningSource = owningSource - self.geoElement = geoElement - self.selectionViewpoint = selectionViewpoint - } - +public struct SearchResult { /// Title that should be shown whenever a result is displayed. public let displayTitle: String - /// Should be shown as a subtitle wherever results are shown. - public let displaySubtitle: String - - /// Image, in the native platform's format, for the result. This should be the marker that would be - /// shown on the map, and also shown in the UI. This property is available for convenience so the - /// UI doesn't have to worry about whether the `GeoElement` is a graphic or a feature when displaying - /// the icon in the UI. - public let markerImage: UIImage? - /// Reference to the search source that created this result. public let owningSource: SearchSource + /// Should be shown as a subtitle wherever results are shown. + public var displaySubtitle: String = "" + /// For locator results, should be the graphic that was used to display the result on the map. /// For feature layer results, should be the result feature. Can be null depending on the type of the /// result, and can have `GeoElement`s without a defined geometry. - public let geoElement: GeoElement? + public var geoElement: GeoElement? = nil + /// Image, in the native platform's format, for the result. This should be the marker that would be + /// shown on the map, and also shown in the UI. This property is available for convenience so the + /// UI doesn't have to worry about whether the `GeoElement` is a graphic or a feature when displaying + /// the icon in the UI. + public var markerImage: UIImage? = nil + /// The viewpoint to be used when the view zooms to a selected result. This property can be `nil` /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. - public let selectionViewpoint: Viewpoint? + public var selectionViewpoint: Viewpoint? = nil /// The stable identity of the entity associated with this instance. public let id = UUID() @@ -75,3 +59,27 @@ extension SearchResult: Hashable { hasher.combine(id) } } + +extension SearchResult { + init( + geocodeResult: GeocodeResult, + searchSource: SearchSource + ) { + let subtitle = geocodeResult.attributes["LongLabel"] as? String ?? + "Match percent: \((geocodeResult.score / 100.0).formatted(.percent))" + + var viewpoint: Viewpoint? = nil + if let extent = geocodeResult.extent { + viewpoint = Viewpoint(targetExtent: extent) + } + + displayTitle = geocodeResult.label + owningSource = searchSource + displaySubtitle = subtitle + geoElement = Graphic( + geometry: geocodeResult.displayLocation, + attributes: geocodeResult.attributes + ) + selectionViewpoint = viewpoint + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 38162d81c..2c01d0d3a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -15,42 +15,28 @@ import Foundation import ArcGIS /// Wraps a suggestion for display. -public struct SearchSuggestion { - internal init( - displayTitle: String, - displaySubtitle: String = "", - owningSource: SearchSource, - suggestResult: SuggestResult? = nil, - isCollection: Bool - ) { - self.displayTitle = displayTitle - self.displaySubtitle = displaySubtitle - self.owningSource = owningSource - self.suggestResult = suggestResult - self.isCollection = isCollection - } - +public struct SearchSuggestion { /// Title that should be used when displaying a suggestion. public let displayTitle: String - /// Optional subtitle that can be displayed when showing a suggestion. - public let displaySubtitle: String - /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the /// view model to be able to accept a suggestion, because a suggestion should only be used with the /// locator that created it. public let owningSource: SearchSource - /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and - /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. - public let suggestResult: SuggestResult? - /// `true` if the search from this suggestion should be treated like a collection search, `false` if the /// search would return a single result. This property should be used to display a different icon /// in the UI depending on if this is a category search (like 'Coffee', 'Pizza', or 'Starbucks') and /// false if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). public let isCollection: Bool + /// Optional subtitle that can be displayed when showing a suggestion. + public var displaySubtitle: String = "" + + /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and + /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. + public var suggestResult: SuggestResult? = nil + /// The stable identity of the entity associated with this instance. public let id = UUID() } @@ -71,3 +57,12 @@ extension SearchSuggestion: Hashable { hasher.combine(id) } } + +extension SearchSuggestion { + init(suggestResult: SuggestResult, searchSource: SearchSource) { + displayTitle = suggestResult.label + owningSource = searchSource + isCollection = suggestResult.isCollection + self.suggestResult = suggestResult + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 46e391ecc..76e9b33ba 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -51,6 +51,11 @@ public struct SearchView: View { /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. private var resultsOverlay: GraphicsOverlay? = nil + /// The string shown in the search view when no user query is entered. + /// Defaults to "Find a place or address". Note: this is set using the + /// `defaultPlaceholder` modifier. + private var defaultPlaceholder: String = "Find a place or address" + /// Determines whether a built-in result view will be shown. Defaults to true. /// If false, the result display/selection list is not shown. Set to false if you want to hide the results /// or define a custom result list. You might use a custom result list to show results in a separate list, @@ -61,7 +66,7 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. Defaults to "No results found". /// Note: this is set using the `noResultMessage` modifier. private var noResultMessage = "No results found" - + private var searchBarWidth: CGFloat = 360.0 @State @@ -76,8 +81,8 @@ public struct SearchView: View { HStack { VStack (alignment: .center) { SearchField( - defaultPlaceholder: searchViewModel.defaultPlaceholder, currentQuery: $searchViewModel.currentQuery, + defaultPlaceholder: defaultPlaceholder, isShowResultsHidden: !enableResultListView, showResults: $showResultListView, onCommit: { searchViewModel.commitSearch() } @@ -111,7 +116,9 @@ public struct SearchView: View { } } .listStyle(.plain) - .onChange(of: searchViewModel.results, perform: display(searchResults:)) + .onChange(of: searchViewModel.results, perform: { newValue in + display(searchResults: (try? newValue?.get()) ?? []) + }) .onChange(of: searchViewModel.selectedResult, perform: display(selectedResult:)) .onReceive(searchViewModel.$currentQuery) { _ in searchViewModel.updateSuggestions() @@ -132,6 +139,16 @@ public struct SearchView: View { return copy } + /// The string shown in the search view when no user query is entered. + /// Defaults to "Find a place or address". + /// - Parameter defaultPlaceholder: The new value. + /// - Returns: A new `SearchView`. + public func defaultPlaceholder(_ defaultPlaceholder: String) -> Self { + var copy = self + copy.defaultPlaceholder = defaultPlaceholder + return copy + } + /// Message to show when there are no results or suggestions. Defaults to "No results found". /// - Parameter noResultMessage: The new value. /// - Returns: A new `SearchView`. @@ -140,7 +157,7 @@ public struct SearchView: View { copy.noResultMessage = noResultMessage return copy } - + /// The width of the search bar. /// - Parameter searchBarWidth: The desired width of the search bar. /// - Returns: A new `SearchView`. @@ -151,41 +168,35 @@ public struct SearchView: View { } } -extension SearchView { - private func display(searchResults: Result<[SearchResult], SearchError>?) { +private extension SearchView { + func display(searchResults: [SearchResult]) { guard let resultsOverlay = resultsOverlay else { return } - switch searchResults { - case .success(let results): - let resultGraphics: [Graphic] = results.compactMap { result in - guard let graphic = result.geoElement as? Graphic else { return nil } - graphic.update(with: result) - return graphic - } - resultsOverlay.removeAllGraphics() - resultsOverlay.addGraphics(resultGraphics) - - if !resultGraphics.isEmpty, - let envelope = resultsOverlay.extent, - shouldZoomToResults { - let builder = EnvelopeBuilder(envelope: envelope) - builder.expand(factor: 1.1) - let targetExtent = builder.toGeometry() as! Envelope - viewpoint?.wrappedValue = Viewpoint( - targetExtent: targetExtent - ) - searchViewModel.lastSearchExtent = targetExtent - } else { - viewpoint?.wrappedValue = nil - } - default: - resultsOverlay.removeAllGraphics() + let resultGraphics: [Graphic] = searchResults.compactMap { result in + guard let graphic = result.geoElement as? Graphic else { return nil } + graphic.update(with: result) + return graphic + } + resultsOverlay.removeAllGraphics() + resultsOverlay.addGraphics(resultGraphics) + + if !resultGraphics.isEmpty, + let envelope = resultsOverlay.extent, + shouldZoomToResults { + let builder = EnvelopeBuilder(envelope: envelope) + builder.expand(factor: 1.1) + let targetExtent = builder.toGeometry() as! Envelope + viewpoint?.wrappedValue = Viewpoint( + targetExtent: targetExtent + ) + searchViewModel.lastSearchExtent = targetExtent + } else { viewpoint?.wrappedValue = nil } if !shouldZoomToResults { shouldZoomToResults = true } } - private func display(selectedResult: SearchResult?) { + func display(selectedResult: SearchResult?) { guard let selectedResult = selectedResult else { return } viewpoint?.wrappedValue = selectedResult.selectionViewpoint } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 7e398484a..490ddf9de 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -32,14 +32,12 @@ public class SearchViewModel: ObservableObject { /// Creates a `SearchViewModel`. /// - Parameters: - /// - defaultPlaceholder: The string shown in the search view when no user query is entered. /// - activeSource: Tracks the currently active search source. /// - queryArea: The search area to be used for the current query. /// - queryCenter: Defines the center for the search. /// - resultMode: Defines how many results to return. /// - sources: Collection of search sources to be used. public convenience init( - defaultPlaceholder: String = .defaultPlaceholder, activeSource: SearchSource? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, @@ -47,7 +45,6 @@ public class SearchViewModel: ObservableObject { sources: [SearchSource] = [] ) { self.init() - self.defaultPlaceholder = defaultPlaceholder self.activeSource = activeSource self.queryArea = queryArea self.queryCenter = queryCenter @@ -55,10 +52,6 @@ public class SearchViewModel: ObservableObject { self.sources = sources } - /// The string shown in the search view when no user query is entered. - /// Default is "Find a place or address". - public var defaultPlaceholder: String = .defaultPlaceholder - /// The active search source. If `nil`, the first item in `sources` is used. public var activeSource: SearchSource? @@ -67,8 +60,10 @@ public class SearchViewModel: ObservableObject { public var currentQuery: String = "" { willSet { results = nil - suggestions = nil isEligibleForRequery = false + if currentQuery.isEmpty { + suggestions = nil + } } } @@ -193,12 +188,12 @@ public class SearchViewModel: ObservableObject { /// - Parameter searchArea: geometry used to constrain the results. If `nil`, the /// `queryArea` property is used instead. If `queryArea` is `nil`, results are not constrained. public func commitSearch() { - kickoffTask(searchTask()) + kickoffTask({ searchTask() }) } /// Repeats the last search, limiting results to the extent specified in `geoViewExtent`. public func repeatSearch() { - kickoffTask(repeatSearchTask()) + kickoffTask({ repeatSearchTask() }) } /// Updates suggestions list asynchronously. @@ -208,7 +203,7 @@ public class SearchViewModel: ObservableObject { return } - kickoffTask(updateSuggestionsTask()) + kickoffTask({ updateSuggestionsTask() }) } @Published @@ -227,19 +222,12 @@ public class SearchViewModel: ObservableObject { /// - searchSuggestion: The suggestion to use to commit the search. public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) { currentQuery = searchSuggestion.displayTitle - kickoffTask(acceptSuggestionTask(searchSuggestion)) + kickoffTask({ acceptSuggestionTask(searchSuggestion) }) } - private func kickoffTask(_ task: Task<(), Never>) { + private func kickoffTask(_ taskInit: () -> Task<(), Never>) { currentTask?.cancel() - currentTask = task - } - - /// Clears the search. This will set the results list to null, clear the result selection, clear suggestions, - /// and reset the current query. - public func clearSearch() { - // Setting currentQuery to "" will reset everything necessary. - currentQuery = "" + currentTask = taskInit() } } @@ -349,23 +337,14 @@ extension SearchViewModel { switch (resultMode) { case .single: - if let firstResult = searchResults.first { - effectiveResults = [firstResult] - } else { - effectiveResults = [] - } + effectiveResults = Array(searchResults.prefix(1)) case .multiple: effectiveResults = searchResults case .automatic: if isCollection { effectiveResults = searchResults } else { - if let firstResult = searchResults.first { - effectiveResults = [firstResult] - } - else { - effectiveResults = [] - } + effectiveResults = Array(searchResults.prefix(1)) } } @@ -386,7 +365,3 @@ extension SearchViewModel { return source } } - -public extension String { - static let defaultPlaceholder = "Find a place or address" -} diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index cc18b4c2c..ef05b51de 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -22,6 +22,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// Creates a smart locator search source. /// - Parameters: /// - name: Name to show when presenting this source in the UI. + /// - locatorTask: The `LocatorTask` to use for searching.. /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. /// - repeatSearchResultThreshold: The minimum number of search results to attempt to return. @@ -89,7 +90,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Union results and return. let searchResults = geocodeResults.map { - $0.toSearchResult(searchSource: self) + SearchResult(geocodeResult: $0, searchSource: self) } results.append(contentsOf: searchResults) var allResults: [SearchResult] = Array(Set(results)) @@ -132,7 +133,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Union results and return. let searchResults = geocodeResults.map { - $0.toSearchResult(searchSource: self) + SearchResult(geocodeResult: $0, searchSource: self) } results.append(contentsOf: searchResults) var allResults: [SearchResult] = Array(Set(results)) @@ -172,7 +173,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Union results and return. let suggestResults = geocodeResults.map { - $0.toSearchSuggestion(searchSource: self) + SearchSuggestion(suggestResult: $0, searchSource: self) } results.append(contentsOf: suggestResults) var allResults: [SearchSuggestion] = Array(Set(results)) diff --git a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift b/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift deleted file mode 100644 index 334ca503f..000000000 --- a/Sources/ArcGISToolkit/Extensions/GeocodeResult.swift +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2021 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 GeocodeResult { - /// Converts a `GeocodeResult` to a `SearchResult`. - /// - Parameter searchSource: The search source generating the result. - /// - Returns: The new `SearchResult`. - func toSearchResult(searchSource: SearchSource) -> SearchResult { - let subtitle = self.attributes["LongLabel"] as? String ?? - "Match percent: \((self.score / 100.0).formatted(.percent))" - var viewpoint: Viewpoint? = nil - if let extent = self.extent { - viewpoint = Viewpoint(targetExtent: extent) - } - return SearchResult( - displayTitle: self.label, - displaySubtitle: subtitle, - markerImage: nil, - owningSource: searchSource, - geoElement: Graphic( - geometry: self.displayLocation, - attributes: self.attributes - ), - selectionViewpoint: viewpoint - ) - } -} diff --git a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift b/Sources/ArcGISToolkit/Extensions/SuggestResult.swift deleted file mode 100644 index ef634bb42..000000000 --- a/Sources/ArcGISToolkit/Extensions/SuggestResult.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 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 SuggestResult { - /// Converts a `SuggestResult` to a `SearchSuggestion`. - /// - Parameter searchSource: The search source generating the result. - /// - Returns: The new `SearchSuggestion`. - func toSearchSuggestion(searchSource: SearchSource) -> SearchSuggestion { - return SearchSuggestion(displayTitle: label, - owningSource: searchSource, - suggestResult: self, - isCollection: isCollection) - } -} diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index 3c91d54bf..8ad8ad787 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -15,47 +15,24 @@ import SwiftUI /// A custom view implementing a SearchField. It contains a search button, text field, delete text button, /// and a button to allow users to hide/show the search results list. -struct SearchField: View { - /// The default placeholder displayed when `currentQuery` is empty. - let defaultPlaceholder: String - +public struct SearchField: View { /// The current search query. - var currentQuery: Binding + public var currentQuery: Binding + + /// The default placeholder displayed when `currentQuery` is empty. + public var defaultPlaceholder: String = "" /// The visibility of the `showResults` button. - let isShowResultsHidden: Bool + public var isShowResultsHidden: Bool = true /// Binding allowing the user to toggle the visibility of the results list. - var showResults: Binding? + public var showResults: Binding? = nil /// The handler executed when the user submits a search, either via the `TextField` /// or the Search button. - var onCommit: () -> Void - - /// Creates a new SearchField - /// - Parameters: - /// - defaultPlaceholder: The default placeholder displayed when `currentQuery` - /// is empty. - /// - currentQuery: The current search query. - /// - isShowResultsHidden: The visibility of the `showResults` button. - /// - showResults: Binding allowing the user to toggle the visibility of the results list. - /// - onCommit: The handler executed when the user submits a search, either via the - /// `TextField`or the Search button. - internal init( - defaultPlaceholder: String = "", - currentQuery: Binding, - isShowResultsHidden: Bool = true, - showResults: Binding? = nil, - onCommit: @escaping () -> Void = { } - ) { - self.defaultPlaceholder = defaultPlaceholder - self.currentQuery = currentQuery - self.isShowResultsHidden = isShowResultsHidden - self.showResults = showResults - self.onCommit = onCommit - } + public var onCommit: () -> Void = { } - var body: some View { + public var body: some View { HStack { // Search button Button { diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 2653abefb..c2ef7f7e1 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -28,21 +28,21 @@ class SearchViewModelTests: XCTestCase { let model = SearchViewModel(sources: [LocatorSearchSource()]) model.currentQuery = "Magers & Quinn Booksellers" - Task { model.updateSuggestions() } + model.updateSuggestions() // Get suggestion - let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestions = try await model.$suggestions.compactMap({ $0 }).first let suggestion = try XCTUnwrap(suggestions?.get().first) - Task { model.acceptSuggestion(suggestion) } + model.acceptSuggestion(suggestion) - let results = try await model.$results.compactMap({$0}).first + let results = try await model.$results.compactMap({ $0 }).first let result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) XCTAssertNil(model.suggestions) // With only one results, model should set `selectedResult` property. - XCTAssertEqual(result.first!, model.selectedResult) + XCTAssertEqual(result.first, model.selectedResult) } func testActiveSource() async throws { @@ -55,15 +55,15 @@ class SearchViewModelTests: XCTestCase { model.currentQuery = "Magers & Quinn Booksellers" - Task { model.commitSearch() } + model.commitSearch() - let results = try await model.$results.compactMap({$0}).first + let results = try await model.$results.compactMap({ $0 }).first let result = try XCTUnwrap(results?.get().first) XCTAssertEqual(result.owningSource.name, activeSource.name) - Task { model.updateSuggestions() } + model.updateSuggestions() - let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestions = try await model.$suggestions.compactMap({ $0 }).first let suggestion = try XCTUnwrap(suggestions?.get().first) XCTAssertEqual(suggestion.owningSource.name, activeSource.name) } @@ -77,33 +77,33 @@ class SearchViewModelTests: XCTestCase { // Search with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" - Task { model.commitSearch() } + model.commitSearch() - var results = try await model.$results.compactMap({$0}).first + var results = try await model.$results.compactMap({ $0 }).first var result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) + XCTAssert(result.count, []) XCTAssertNil(model.selectedResult) XCTAssertNil(model.suggestions) // Search with one result. model.currentQuery = "Magers & Quinn Booksellers" - Task { model.commitSearch() } + model.commitSearch() - results = try await model.$results.compactMap({$0}).first + results = try await model.$results.compactMap({ $0 }).first result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) // One results automatically populates `selectedResult`. - XCTAssertEqual(result.first!, model.selectedResult) + XCTAssertEqual(result.first, model.selectedResult) XCTAssertNil(model.suggestions) // Search with multiple results. model.currentQuery = "Magers & Quinn" - Task { model.commitSearch() } + model.commitSearch() - results = try await model.$results.compactMap({$0}).first + results = try await model.$results.compactMap({ $ 0}).first result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) @@ -112,9 +112,9 @@ class SearchViewModelTests: XCTestCase { model.selectedResult = result.first! - Task { model.commitSearch() } + model.commitSearch() - results = try await model.$results.compactMap({$0}).dropFirst().first + results = try await model.$results.compactMap({ $0 }).dropFirst().first result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) @@ -132,18 +132,18 @@ class SearchViewModelTests: XCTestCase { // Valid `currentQuery` should produce non-nil results. model.currentQuery = "Coffee" - Task { model.commitSearch() } + model.commitSearch() - let results = try await model.$results.compactMap({$0}).first + let results = try await model.$results.compactMap({ $0 }).first XCTAssertNotNil(results) // Changing the `currentQuery` should set results to nil. model.currentQuery = "Coffee in Portland" XCTAssertNil(model.results) - Task { model.updateSuggestions() } + model.updateSuggestions() - let suggestions = try await model.$suggestions.compactMap({$0}).first + let suggestions = try await model.$suggestions.compactMap({ $0 }).first XCTAssertNotNil(suggestions) // Changing the `currentQuery` should set suggestions to nil. @@ -154,9 +154,9 @@ class SearchViewModelTests: XCTestCase { // should set `selectedResult` to nil model.currentQuery = "Magers & Quinn Bookseller" - Task { model.commitSearch() } + model.commitSearch() - _ = try await model.$results.compactMap({$0}).first + _ = try await model.$results.compactMap({ $0 }).first XCTAssertNotNil(model.selectedResult) model.currentQuery = "Hotel" XCTAssertNil(model.selectedResult) @@ -170,9 +170,9 @@ class SearchViewModelTests: XCTestCase { model.geoViewExtent = Polygon.chippewaFalls.extent model.currentQuery = "Coffee" - Task { model.commitSearch() } + model.commitSearch() - _ = try await model.$results.compactMap({$0}).first + _ = try await model.$results.compactMap({ $0 }).first XCTAssertFalse(model.isEligibleForRequery) // Offset extent by 10% - isEligibleForRequery should still be `false`. @@ -197,9 +197,9 @@ class SearchViewModelTests: XCTestCase { model.queryArea = Polygon.chippewaFalls model.geoViewExtent = Polygon.chippewaFalls.extent - Task { model.commitSearch() } + model.commitSearch() - _ = try await model.$results.compactMap({$0}).dropFirst().first + _ = try await model.$results.compactMap({ $0 }).dropFirst().first XCTAssertFalse(model.isEligibleForRequery) // Expand extent by 1.1x - isEligibleForRequery should still be `false`. @@ -221,22 +221,22 @@ class SearchViewModelTests: XCTestCase { func testQueryArea() async throws { let source = LocatorSearchSource() - source.maximumResults = Int32.max + source.maximumResults = .max let model = SearchViewModel(sources: [source]) // Set queryArea to Chippewa Falls model.queryArea = Polygon.chippewaFalls model.currentQuery = "Coffee" - Task { model.commitSearch() } + model.commitSearch() - var results = try await model.$results.compactMap({$0}).first + var results = try await model.$results.compactMap({ $0 }).first var result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) let resultGeometryUnion: Geometry = try XCTUnwrap( GeometryEngine.union( - geometries: result.compactMap{ $0.geoElement?.geometry } + geometries: result.compactMap { $0.geoElement?.geometry } ) ) @@ -249,15 +249,15 @@ class SearchViewModelTests: XCTestCase { model.currentQuery = "Magers & Quinn Booksellers" - Task { model.commitSearch() } + model.commitSearch() - results = try await model.$results.compactMap({$0}).first + results = try await model.$results.compactMap({ $0 }).first result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 0) model.queryArea = Polygon.minneapolis - Task { model.commitSearch() } + model.commitSearch() // A note about the use of `.dropFirst()`: // Because `model.results` is not changed between the previous call @@ -267,7 +267,7 @@ class SearchViewModelTests: XCTestCase { // incorrect. Calling `.dropFirst()` will remove that one // and will give us the next one, which is the correct one (the result // from the second `model.commitSearch()` call). - results = try await model.$results.compactMap({$0}).dropFirst().first + results = try await model.$results.compactMap({ $0 }).dropFirst().first result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } @@ -279,9 +279,9 @@ class SearchViewModelTests: XCTestCase { model.queryCenter = .portland model.currentQuery = "Coffee" - Task { model.commitSearch() } + model.commitSearch() - var results = try await model.$results.compactMap({$0}).first + var results = try await model.$results.compactMap({ $0 }).first var result = try XCTUnwrap(results?.get()) var resultPoint = try XCTUnwrap( @@ -299,16 +299,15 @@ class SearchViewModelTests: XCTestCase { ) // First result within 1500m of Portland. - XCTAssertLessThan(geodeticDistance.distance, 1500.0) + XCTAssertLessThan(geodeticDistance.distance, 1500) // Set queryCenter to Edinburgh model.queryCenter = .edinburgh model.currentQuery = "Restaurants" - Task { model.commitSearch() } - + model.commitSearch() - results = try await model.$results.compactMap({$0}).first + results = try await model.$results.compactMap({ $0 }).first result = try XCTUnwrap(results?.get()) resultPoint = try XCTUnwrap( @@ -337,15 +336,15 @@ class SearchViewModelTests: XCTestCase { model.geoViewExtent = Polygon.chippewaFalls.extent model.currentQuery = "Coffee" - Task { model.repeatSearch() } + model.repeatSearch() - var results = try await model.$results.compactMap({$0}).first + var results = try await model.$results.compactMap({ $0 }).first var result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) let resultGeometryUnion: Geometry = try XCTUnwrap( GeometryEngine.union( - geometries: result.compactMap{ $0.geoElement?.geometry } + geometries: result.compactMap { $0.geoElement?.geometry } ) ) @@ -358,17 +357,17 @@ class SearchViewModelTests: XCTestCase { model.currentQuery = "Magers & Quinn Booksellers" - Task { model.repeatSearch() } + model.repeatSearch() - results = try await model.$results.compactMap({$0}).first + results = try await model.$results.compactMap({ $0 }).first result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 0) model.geoViewExtent = Polygon.minneapolis.extent - Task { model.repeatSearch() } + model.repeatSearch() - results = try await model.$results.compactMap({$0}).dropFirst().first + results = try await model.$results.compactMap({ $0 }).dropFirst().first result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } @@ -380,41 +379,41 @@ class SearchViewModelTests: XCTestCase { model.resultMode = .single model.currentQuery = "Magers & Quinn" - Task { model.commitSearch() } + model.commitSearch() - var results = try await model.$results.compactMap({$0}).first + var results = try await model.$results.compactMap({ $0 }).first var result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) model.resultMode = .multiple - Task { model.commitSearch() } + model.commitSearch() - results = try await model.$results.compactMap({$0}).dropFirst().first + results = try await model.$results.compactMap({ $0 }).dropFirst().first result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) model.currentQuery = "Coffee" - Task { model.updateSuggestions() } + model.updateSuggestions() - let suggestionResults = try await model.$suggestions.compactMap({$0}).first + let suggestionResults = try await model.$suggestions.compactMap({ $0 }).first let suggestions = try XCTUnwrap(suggestionResults?.get()) - let collectionSuggestion = try XCTUnwrap(suggestions.filter { $0.isCollection }.first) - let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) + let collectionSuggestion = try XCTUnwrap(suggestions.filter(\.isCollection).first) + let singleSuggestion = try XCTUnwrap(suggestions.filter(!\.isCollection).first) model.resultMode = .automatic - Task { model.acceptSuggestion(collectionSuggestion) } + model.acceptSuggestion(collectionSuggestion) - results = try await model.$results.compactMap({$0}).first + results = try await model.$results.compactMap({ $0 }).first result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) Task { model.acceptSuggestion(singleSuggestion) } - results = try await model.$results.compactMap({$0}).dropFirst().first + results = try await model.$results.compactMap({ $0 }).dropFirst().first result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } @@ -428,18 +427,18 @@ class SearchViewModelTests: XCTestCase { // UpdateSuggestions with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" - Task { model.updateSuggestions() } + model.updateSuggestions() - var suggestionResults = try await model.$suggestions.compactMap({$0}).first + var suggestionResults = try await model.$suggestions.compactMap({ $0 }).first var suggestions = try XCTUnwrap(suggestionResults?.get()) - XCTAssertEqual(suggestions.count, 0) + XCTAssertEqual(suggestions, []) // UpdateSuggestions with results. model.currentQuery = "Magers & Quinn" - Task { model.updateSuggestions() } + model.updateSuggestions() - suggestionResults = try await model.$suggestions.compactMap({$0}).first + suggestionResults = try await model.$suggestions.compactMap({ $0 }).first suggestions = try XCTUnwrap(suggestionResults?.get()) XCTAssertGreaterThanOrEqual(suggestions.count, 1) diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 702806eff..d48829ed7 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -11,14 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation - import XCTest import ArcGIS import ArcGISToolkit -import SwiftUI -class SmartLocatorSearchSourceTests: XCTestCase { +final class SmartLocatorSearchSourceTests: XCTestCase { func testRepeatSearchResultThreshold() async throws { let locator = SmartLocatorSearchSource() @@ -30,7 +27,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { preferredSearchLocation: nil ) var results = try XCTUnwrap(searchResults) - XCTAssertEqual(results.count, 0) + XCTAssertEqual(results, []) // Threshold of 1+ means requery with fewer restrictions locator.repeatSearchResultThreshold = 1 @@ -40,7 +37,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { preferredSearchLocation: nil ) results = try XCTUnwrap(searchResults) - XCTAssertGreaterThanOrEqual(results.count, 1) + XCTAssertNotEqual(results, []) } func testRepeatSuggestResultThreshold() async throws { @@ -54,7 +51,7 @@ class SmartLocatorSearchSourceTests: XCTestCase { preferredSearchLocation: nil ) var results = try XCTUnwrap(suggestResults) - XCTAssertEqual(results.count, 0) + XCTAssertEqual(results, []) // Threshold of 1 -> requery with fewer restrictions locator.repeatSuggestResultThreshold = 1 From a6682430cd7ec7a236cca12857a027042bdb46c3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 3 Nov 2021 16:22:00 -0500 Subject: [PATCH 0270/1722] Use SearchOutcome instead of suggestions and results. --- .../Components/Search/SearchView.swift | 29 ++-- .../Components/Search/SearchViewModel.swift | 161 ++++++++---------- .../SearchViewModelTests.swift | 128 ++++++++------ 3 files changed, 170 insertions(+), 148 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index fb5e85031..8e810ce33 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -55,7 +55,7 @@ public struct SearchView: View { /// Defaults to "Find a place or address". Note: this is set using the /// `defaultPlaceholder` modifier. private var defaultPlaceholder: String = "Find a place or address" - + /// Determines whether a built-in result view will be shown. Defaults to true. /// If false, the result display/selection list is not shown. Set to false if you want to hide the results /// or define a custom result list. You might use a custom result list to show results in a separate list, @@ -66,7 +66,7 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. Defaults to "No results found". /// Note: this is set using the `noResultMessage` modifier. private var noResultMessage = "No results found" - + private var searchBarWidth: CGFloat = 360.0 @State @@ -79,7 +79,8 @@ public struct SearchView: View { public var body: some View { VStack { HStack { - VStack (alignment: .center) { + Spacer() + VStack { SearchField( currentQuery: $searchViewModel.currentQuery, defaultPlaceholder: defaultPlaceholder, @@ -87,20 +88,23 @@ public struct SearchView: View { showResults: $showResultListView, onCommit: { searchViewModel.commitSearch() } ) - + if enableResultListView, showResultListView { - if let results = searchViewModel.results { + switch searchViewModel.searchOutcome { + case .results(let results): SearchResultList( searchResults: results, selectedResult: $searchViewModel.selectedResult, noResultMessage: noResultMessage ) - } else if let suggestions = searchViewModel.suggestions { + case .suggestions(let suggestions): SearchSuggestionList( suggestionResults: suggestions, currentSuggestion: $searchViewModel.currentSuggestion, noResultMessage: noResultMessage ) + case .none: + EmptyView() } } } @@ -116,8 +120,13 @@ public struct SearchView: View { } } .listStyle(.plain) - .onChange(of: searchViewModel.results, perform: { newValue in - display(searchResults: (try? newValue?.get()) ?? []) + .onChange(of: searchViewModel.searchOutcome, perform: { newValue in + switch newValue { + case .results(let results): + display(searchResults: (try? results.get()) ?? []) + default: + display(searchResults: []) + } }) .onChange(of: searchViewModel.selectedResult, perform: display(selectedResult:)) .onReceive(searchViewModel.$currentQuery) { _ in @@ -157,7 +166,7 @@ public struct SearchView: View { copy.noResultMessage = noResultMessage return copy } - + /// The width of the search bar. /// - Parameter searchBarWidth: The desired width of the search bar. /// - Returns: A new `SearchView`. @@ -277,7 +286,7 @@ struct ResultRow: View { var title: String var subtitle: String = "" var image: AnyView - + var body: some View { HStack { image diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 96521ed62..0e2620b36 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -30,6 +30,12 @@ public class SearchViewModel: ObservableObject { case automatic } + /// The outcome of a geocode operation (search or suggestion). + public enum SearchOutcome { + case results(result: Result<[SearchResult], SearchError>) + case suggestions(result: Result<[SearchSuggestion], SearchError>) + } + /// Creates a `SearchViewModel`. /// - Parameters: /// - activeSource: Tracks the currently active search source. @@ -59,10 +65,17 @@ public class SearchViewModel: ObservableObject { @Published public var currentQuery: String = "" { willSet { - results = nil isEligibleForRequery = false - if currentQuery.isEmpty { - suggestions = nil + + switch searchOutcome { + case .results(_): + searchOutcome = nil + case .suggestions(_): + if currentQuery.isEmpty { + searchOutcome = nil + } + case .none: + break } } } @@ -132,24 +145,30 @@ public class SearchViewModel: ObservableObject { /// Collection of results. `nil` means no query has been made. An empty array means there /// were no results, and the view should show an appropriate 'no results' message. + /// + /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there + /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, + /// a useful 'no results' message should be shown by the view. + @Published - public private(set) var results: Result<[SearchResult], SearchError>? { - willSet { - if newValue != nil { - suggestions = nil - } - } + public private(set) var searchOutcome: SearchOutcome? { didSet { - switch results { - case .success(let results): - if results.count == 1 { - selectedResult = results.first - } - else { + switch searchOutcome { + case .results(let results): + switch results { + case .success(let results): + if results.count == 1 { + selectedResult = results.first + } + else { + selectedResult = nil + } + default: selectedResult = nil } default: selectedResult = nil + break } } } @@ -167,18 +186,6 @@ public class SearchViewModel: ObservableObject { /// NOTE: only the first source is currently used; multiple sources are not yet supported. public var sources: [SearchSource] = [] - /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there - /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, - /// a useful 'no results' message should be shown by the view. - @Published - public private(set) var suggestions: Result<[SearchSuggestion], SearchError>? { - willSet { - if newValue != nil { - results = nil - } - } - } - /// The currently executing async task. `currentTask` should be cancelled /// prior to starting another async task. private var currentTask: Task? @@ -238,45 +245,27 @@ extension SearchViewModel { return } - do { - // User is performing a search, so set `lastSearchExtent`. - lastSearchExtent = geoViewExtent - try await process( - searchResults: source.repeatSearch( - currentQuery, - searchExtent: queryExtent - ) + await search(with: { + try await source.repeatSearch( + currentQuery, + searchExtent: queryExtent ) - } catch is CancellationError { - results = nil - } catch { - results = .failure(SearchError(error)) - } + } ) } } private func searchTask() -> Task<(), Never> { Task { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = currentSource() else { - return - } + let source = currentSource() else { return } - do { - // User is performing a search, so set `lastSearchExtent`. - lastSearchExtent = geoViewExtent - try await process( - searchResults: source.search( - currentQuery, - searchArea: queryArea, - preferredSearchLocation: queryCenter - ) + await search(with: { + try await source.search( + currentQuery, + searchArea: queryArea, + preferredSearchLocation: queryCenter ) - } catch is CancellationError { - results = nil - } catch { - results = .failure(SearchError(error)) - } + } ) } } @@ -286,50 +275,48 @@ extension SearchViewModel { let source = currentSource() else { return } - - let suggestResult = await Result { - try await source.suggest( + do { + let suggestions = try await source.suggest( currentQuery, searchArea: queryArea, preferredSearchLocation: queryCenter ) - } - - switch suggestResult { - case .success(let suggestResults): - suggestions = .success(suggestResults) - case .failure(let error): - suggestions = .failure(SearchError(error)) - break - case nil: - suggestions = nil - break + searchOutcome = .suggestions(result: .success(suggestions)) + } catch is CancellationError { + searchOutcome = nil + } catch { + searchOutcome = .suggestions(result: .failure(SearchError(error))) } } } private func acceptSuggestionTask(_ searchSuggestion: SearchSuggestion) -> Task<(), Never> { Task { - do { - // User is performing a search, so set `lastSearchExtent`. - lastSearchExtent = geoViewExtent - try await process( - searchResults: searchSuggestion.owningSource.search( - searchSuggestion, - searchArea: queryArea, - preferredSearchLocation: queryCenter - ) + await search(with: { + try await searchSuggestion.owningSource.search( + searchSuggestion, + searchArea: queryArea, + preferredSearchLocation: queryCenter ) - } catch is CancellationError { - results = nil - } catch { - results = .failure(SearchError(error)) - } + } ) + // once we are done searching for the suggestion, then reset it to nil currentSuggestion = nil } } + private func search(with action: () async throws -> [SearchResult]) async { + do { + // User is performing a search, so set `lastSearchExtent`. + lastSearchExtent = geoViewExtent + try await process(searchResults: action()) + } catch is CancellationError { + searchOutcome = nil + } catch { + searchOutcome = .results(result: .failure(SearchError(error))) + } + } + private func process(searchResults: [SearchResult], isCollection: Bool = true) { let effectiveResults: [SearchResult] @@ -346,7 +333,7 @@ extension SearchViewModel { } } - results = .success(effectiveResults) + searchOutcome = .results(result: .success(effectiveResults)) } } @@ -363,3 +350,5 @@ extension SearchViewModel { return source } } + +extension SearchViewModel.SearchOutcome: Equatable {} diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 2dcd368b0..1cd4e1100 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -28,17 +28,16 @@ class SearchViewModelTests: XCTestCase { model.updateSuggestions() // Get suggestion - let suggestions = try await model.$suggestions.compactMap({ $0 }).first + let suggestions = try await searchSuggestions(model) let suggestion = try XCTUnwrap(suggestions?.get().first) model.acceptSuggestion(suggestion) - let results = try await model.$results.compactMap({ $0 }).first + let results = try await searchResults(model, dropFirst: true) let result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) - XCTAssertNil(model.suggestions) - // With only one results, model should set `selectedResult` property. + // With only one result, model should set `selectedResult` property. XCTAssertEqual(result.first, model.selectedResult) } @@ -54,13 +53,13 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - let results = try await model.$results.compactMap({ $0 }).first + let results = try await searchResults(model) let result = try XCTUnwrap(results?.get().first) XCTAssertEqual(result.owningSource.name, activeSource.name) model.updateSuggestions() - let suggestions = try await model.$suggestions.compactMap({ $0 }).first + let suggestions = try await searchSuggestions(model, dropFirst: true) let suggestion = try XCTUnwrap(suggestions?.get().first) XCTAssertEqual(suggestion.owningSource.name, activeSource.name) } @@ -68,50 +67,47 @@ class SearchViewModelTests: XCTestCase { func testCommitSearch() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - // No search - results are nil. - XCTAssertNil(model.results) + // No search - searchOutcome is nil. + XCTAssertNil(model.searchOutcome) // Search with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" model.commitSearch() - var results = try await model.$results.compactMap({ $0 }).first + var results = try await searchResults(model) var result = try XCTUnwrap(results?.get()) - XCTAssert(result.count, []) + XCTAssertEqual(result, []) XCTAssertNil(model.selectedResult) - XCTAssertNil(model.suggestions) // Search with one result. model.currentQuery = "Magers & Quinn Booksellers" model.commitSearch() - results = try await model.$results.compactMap({ $0 }).first + results = try await searchResults(model) result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) // One results automatically populates `selectedResult`. XCTAssertEqual(result.first, model.selectedResult) - XCTAssertNil(model.suggestions) // Search with multiple results. model.currentQuery = "Magers & Quinn" model.commitSearch() - results = try await model.$results.compactMap({ $ 0}).first + results = try await searchResults(model) result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) XCTAssertNil(model.selectedResult) - XCTAssertNil(model.suggestions) model.selectedResult = result.first! model.commitSearch() - results = try await model.$results.compactMap({ $0 }).dropFirst().first + results = try await searchResults(model, dropFirst: true) result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) @@ -121,39 +117,34 @@ class SearchViewModelTests: XCTestCase { func testCurrentQuery() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) - // Empty `currentQuery` should produce nil results and suggestions. + // Empty `currentQuery` should produce nil searchOutcome. model.currentQuery = "" - XCTAssertNil(model.results) - XCTAssertNil(model.suggestions) + XCTAssertNil(model.searchOutcome) // Valid `currentQuery` should produce non-nil results. model.currentQuery = "Coffee" model.commitSearch() - let results = try await model.$results.compactMap({ $0 }).first + let results = try await searchResults(model) XCTAssertNotNil(results) - // Changing the `currentQuery` should set results to nil. + // Changing the `currentQuery` should set searchOutcome to nil. model.currentQuery = "Coffee in Portland" - XCTAssertNil(model.results) + XCTAssertNil(model.searchOutcome) model.updateSuggestions() - let suggestions = try await model.$suggestions.compactMap({ $0 }).first + let suggestions = try await searchSuggestions(model) XCTAssertNotNil(suggestions) - // Changing the `currentQuery` should set suggestions to nil. - model.currentQuery = "Coffee in Edinburgh" - XCTAssertNil(model.suggestions) - // Changing current query after search with 1 result // should set `selectedResult` to nil model.currentQuery = "Magers & Quinn Bookseller" model.commitSearch() - _ = try await model.$results.compactMap({ $0 }).first + _ = try await searchResults(model, dropFirst: true) XCTAssertNotNil(model.selectedResult) model.currentQuery = "Hotel" XCTAssertNil(model.selectedResult) @@ -169,7 +160,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - _ = try await model.$results.compactMap({ $0 }).first + _ = try await searchResults(model) XCTAssertFalse(model.isEligibleForRequery) // Offset extent by 10% - isEligibleForRequery should still be `false`. @@ -196,7 +187,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - _ = try await model.$results.compactMap({ $0 }).dropFirst().first + _ = try await searchResults(model, dropFirst: true) XCTAssertFalse(model.isEligibleForRequery) // Expand extent by 1.1x - isEligibleForRequery should still be `false`. @@ -227,7 +218,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - var results = try await model.$results.compactMap({ $0 }).first + var results = try await searchResults(model) var result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) @@ -248,23 +239,23 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - results = try await model.$results.compactMap({ $0 }).first + results = try await searchResults(model) result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) + XCTAssertEqual(result, []) model.queryArea = Polygon.minneapolis model.commitSearch() // A note about the use of `.dropFirst()`: - // Because `model.results` is not changed between the previous call + // Because `model.searchOutcome` is not changed between the previous call // to `model.commitSearch()` and the one right above, the - // `try await model.$results...` call will return the last result + // `try await model.searchOutCome...` call will return the last result // received (from the first `model.commitSearch()` call), which is // incorrect. Calling `.dropFirst()` will remove that one // and will give us the next one, which is the correct one (the result // from the second `model.commitSearch()` call). - results = try await model.$results.compactMap({ $0 }).dropFirst().first + results = try await searchResults(model, dropFirst: true) result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } @@ -278,7 +269,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - var results = try await model.$results.compactMap({ $0 }).first + var results = try await searchResults(model) var result = try XCTUnwrap(results?.get()) var resultPoint = try XCTUnwrap( @@ -304,7 +295,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - results = try await model.$results.compactMap({ $0 }).first + results = try await searchResults(model) result = try XCTUnwrap(results?.get()) resultPoint = try XCTUnwrap( @@ -335,7 +326,7 @@ class SearchViewModelTests: XCTestCase { model.repeatSearch() - var results = try await model.$results.compactMap({ $0 }).first + var results = try await searchResults(model) var result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) @@ -356,7 +347,7 @@ class SearchViewModelTests: XCTestCase { model.repeatSearch() - results = try await model.$results.compactMap({ $0 }).first + results = try await searchResults(model) result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 0) @@ -364,7 +355,7 @@ class SearchViewModelTests: XCTestCase { model.repeatSearch() - results = try await model.$results.compactMap({ $0 }).dropFirst().first + results = try await searchResults(model, dropFirst: true) result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } @@ -378,7 +369,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - var results = try await model.$results.compactMap({ $0 }).first + var results = try await searchResults(model) var result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) @@ -386,7 +377,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() - results = try await model.$results.compactMap({ $0 }).dropFirst().first + results = try await searchResults(model, dropFirst: true) result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) @@ -394,23 +385,23 @@ class SearchViewModelTests: XCTestCase { model.updateSuggestions() - let suggestionResults = try await model.$suggestions.compactMap({ $0 }).first + let suggestionResults = try await searchSuggestions(model) let suggestions = try XCTUnwrap(suggestionResults?.get()) let collectionSuggestion = try XCTUnwrap(suggestions.filter(\.isCollection).first) - let singleSuggestion = try XCTUnwrap(suggestions.filter(!\.isCollection).first) + let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) model.resultMode = .automatic model.acceptSuggestion(collectionSuggestion) - results = try await model.$results.compactMap({ $0 }).first + results = try await searchResults(model, dropFirst: true) result = try XCTUnwrap(results?.get()) XCTAssertGreaterThan(result.count, 1) - Task { model.acceptSuggestion(singleSuggestion) } + model.acceptSuggestion(singleSuggestion) - results = try await model.$results.compactMap({ $0 }).dropFirst().first + results = try await searchResults(model) result = try XCTUnwrap(results?.get()) XCTAssertEqual(result.count, 1) } @@ -419,14 +410,14 @@ class SearchViewModelTests: XCTestCase { let model = SearchViewModel(sources: [LocatorSearchSource()]) // No currentQuery - suggestions are nil. - XCTAssertNil(model.suggestions) + XCTAssertNil(model.searchOutcome) // UpdateSuggestions with no results - result count is 0. model.currentQuery = "No results found blah blah blah blah" model.updateSuggestions() - var suggestionResults = try await model.$suggestions.compactMap({ $0 }).first + var suggestionResults = try await searchSuggestions(model) var suggestions = try XCTUnwrap(suggestionResults?.get()) XCTAssertEqual(suggestions, []) @@ -435,12 +426,45 @@ class SearchViewModelTests: XCTestCase { model.updateSuggestions() - suggestionResults = try await model.$suggestions.compactMap({ $0 }).first + suggestionResults = try await searchSuggestions(model, dropFirst: true) suggestions = try XCTUnwrap(suggestionResults?.get()) XCTAssertGreaterThanOrEqual(suggestions.count, 1) XCTAssertNil(model.selectedResult) - XCTAssertNil(model.results) + } +} + +extension SearchViewModelTests { + func searchResults( + _ model: SearchViewModel, + dropFirst: Bool = false + ) async throws -> Result<[SearchResult], SearchError>? { + let searchOutcome = dropFirst ? + try await model.$searchOutcome.compactMap({ $0 }).dropFirst().first : + try await model.$searchOutcome.compactMap({ $0 }).first + + switch searchOutcome { + case .results(let results): + return results + default: + return nil + } + } + + func searchSuggestions( + _ model: SearchViewModel, + dropFirst: Bool = false + ) async throws -> Result<[SearchSuggestion], SearchError>? { + let searchOutcome = dropFirst ? + try await model.$searchOutcome.compactMap({ $0 }).dropFirst().first : + try await model.$searchOutcome.compactMap({ $0 }).first + + switch searchOutcome { + case .suggestions(let suggestions): + return suggestions + default: + return nil + } } } From a60baaf832bc497292c4f5cb846a272b07ea7fc6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 5 Nov 2021 12:14:11 -0500 Subject: [PATCH 0271/1722] SpatialReference checking/status/setting/loading --- .../BasemapGallery/BasemapGallery.swift | 15 ++-- .../BasemapGallery/BasemapGalleryItem.swift | 69 +++++++++++-------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index e93cbcdbb..54bb7961e 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -149,7 +149,10 @@ extension BasemapGallery { viewModel.currentBasemapGalleryItem = basemapGalleryItem } else { - alertItem = AlertItem(geoModelSR: viewModel.geoModel?.spatialReference) + alertItem = AlertItem( + basemapSR: basemapGalleryItem.spatialReference, + geoModelSR: viewModel.geoModel?.spatialReference + ) print("Task bm don't match") } } @@ -169,7 +172,7 @@ private struct BasemapGalleryItemRow: View { var body: some View { ZStack { VStack { - if !basemapGalleryItem.isLoaded { + if basemapGalleryItem.isLoading { Spacer() ProgressView() .progressViewStyle(CircularProgressViewStyle()) @@ -203,7 +206,7 @@ private struct BasemapGalleryItemRow: View { } } .allowsHitTesting( - basemapGalleryItem.isLoaded + !basemapGalleryItem.isLoading ) } } @@ -220,13 +223,11 @@ extension AlertItem: Identifiable { // TODO: add SR for basemap, if possible (SR property on basemap?) Maybe that can speed up baselayer sr checking... // TODO: Cleanup all .tapGesture code, alert code, old error/alert stuff // TODO: Figure out common errors, so I don't need to rely on `Error` or `RuntimeError`. -// TODO: update item's spatialreferenceStatus on main thread. (method marked with @MainActor, the way `update()` is?) -// TODO: add basemap SR to init below. extension AlertItem { - init(geoModelSR: SpatialReference?) { + init(basemapSR: SpatialReference?, geoModelSR: SpatialReference?) { self.init( title: "Spatial Reference mismatch.", - message: "The spatial reference of the basemap does not match that of the geomodel (\(geoModelSR?.description ?? ""))." + message: "The spatial reference of the basemap \(basemapSR?.description ?? "") does not match that of the GeoModel \(geoModelSR?.description ?? "")." ) } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 1c6dcb297..aeecd276e 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -53,37 +53,38 @@ public class BasemapGalleryItem: ObservableObject { @Published public var loadBasemapsError: Error? = nil - + /// The basemap this `BasemapGalleryItem` represents. public private(set) var basemap: Basemap - + /// The name of this `Basemap`. @Published public private(set) var name: String = "" private var nameOverride: String? = nil - + /// The description which will be used in the gallery. @Published public private(set) var description: String? = nil private var descriptionOverride: String? = nil - + /// The thumbnail which will be displayed in the gallery. @Published public private(set) var thumbnail: UIImage? = nil private var thumbnailOverride: UIImage? = nil - /// Denotes whether loading the `basemap` has been attempted. - /// If the loading of the item generates an error, `isLoaded` will be true. + /// Denotes whether the `basemap` or it's base layers are being loaded. @Published - public private(set) var isLoaded = false + public private(set) var isLoading = true @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown + /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's + /// baseLayers have been loaded in `updateSpatialReferenceStatus`. + public private(set) var spatialReference: SpatialReference? = nil + /// The currently executing async task for loading basemap. private var loadBasemapTask: Task? = nil - - private var lastSpatialReference: SpatialReference? = nil } extension BasemapGalleryItem { @@ -106,10 +107,16 @@ extension BasemapGalleryItem { description = descriptionOverride ?? basemap.item?.description thumbnail = thumbnailOverride ?? (basemap.item?.thumbnail?.image ?? BasemapGalleryItem.defaultThumbnail) - - // TODO: include error messaging alert. + loadBasemapsError = error - isLoaded = true + isLoading = false + } + + @MainActor + func update(with referenceSpatialReference: SpatialReference) { + spatialReference = basemap.baseLayers.first?.spatialReference + spatialReferenceStatus = matches(referenceSpatialReference) ? .match : .noMatch + isLoading = false } } @@ -132,29 +139,31 @@ extension BasemapGalleryItem { ) async throws { guard let spatialReference = referenceSpatialReference, basemap.loadStatus == .loaded, - spatialReference != lastSpatialReference + self.spatialReference == nil else { return } - - lastSpatialReference = spatialReference - await withThrowingTaskGroup(of: Void.self, - returning: Void.self, - body: { taskGroup in - basemap.baseLayers.forEach { baseLayer in - taskGroup.addTask { - try await baseLayer.load() - return + + isLoading = true + await withThrowingTaskGroup( + of: Void.self, + returning: Void.self, + body: { taskGroup in + basemap.baseLayers.forEach { baseLayer in + taskGroup.addTask { + try await baseLayer.load() + return + } } } - }) + ) - spatialReferenceStatus = matches(spatialReference) ? .match : .noMatch + await update(with: spatialReference) } - - public func matches( - _ spatialReference: SpatialReference? - ) -> Bool { - guard let spatialReference = spatialReference else { return false } - + + /// Determines if the basemap spatial reference matches `spatialReference`. + /// - Parameter spatialReference: The `SpatialReference` to match against. + /// - Returns: `true` if the basemap spatial reference matches `spatialReference`, + /// `false` if they don't match. + private func matches(_ spatialReference: SpatialReference) -> Bool { for baselayer in basemap.baseLayers { if let baseLayerSR = baselayer.spatialReference, baseLayerSR != spatialReference { From 24ee0ab130bc67d78824556424ad4af19d864e64 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 5 Nov 2021 16:53:30 -0500 Subject: [PATCH 0272/1722] Cleanup. --- .../Examples/BasemapGalleryExampleView.swift | 2 +- .../BasemapGallery/BasemapGallery.swift | 58 ++++++++-------- .../BasemapGallery/BasemapGalleryItem.swift | 68 +++++++++++-------- .../BasemapGalleryViewModel.swift | 45 +++++++----- 4 files changed, 97 insertions(+), 76 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 28ec15208..ef3eed252 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -80,7 +80,7 @@ struct BasemapGalleryExampleView: View { var viewModel = BasemapGalleryViewModel() @State - var showBasemapGallery: Bool = true // NOTE: Set to false when BasemapGallery is back in the navigation stack. + var showBasemapGallery: Bool = true let initialViewpoint: Viewpoint? = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 54bb7961e..7051e075d 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -60,7 +60,7 @@ public struct BasemapGallery: View { @State private var alertItem: AlertItem? - + public var body: some View { GalleryView() .alert(item: $alertItem) { alertItem in @@ -107,7 +107,7 @@ extension BasemapGallery { .init(.flexible(), spacing: 8.0, alignment: .top), .init(.flexible(), spacing: 8.0, alignment: .top) ] - + return InternalGalleryView(columns) } @@ -115,7 +115,7 @@ extension BasemapGallery { let columns: [GridItem] = [ .init(.flexible(), spacing: 8.0, alignment: .top) ] - + return InternalGalleryView(columns) } @@ -172,32 +172,32 @@ private struct BasemapGalleryItemRow: View { var body: some View { ZStack { VStack { - if basemapGalleryItem.isLoading { - Spacer() - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - Spacer() - - } else { - ZStack(alignment: .center) { - if let thumbnailImage = basemapGalleryItem.thumbnail { - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .border( - isSelected ? Color.accentColor: Color.clear, - width: 3.0) - } + ZStack(alignment: .center) { + if let thumbnailImage = basemapGalleryItem.thumbnail { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .border( + isSelected ? Color.accentColor: Color.clear, + width: 3.0) + } + + if basemapGalleryItem.loadBasemapsError != nil { + Image(systemName: "minus.circle.fill") + .font(.title) + .foregroundColor(.red) + } else if basemapGalleryItem.spatialReferenceStatus == .noMatch { + Image(systemName: "x.circle.fill") + .font(.title) + .foregroundColor(.red) + } + if basemapGalleryItem.isLoading { + Spacer() + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .esriBorder() + Spacer() - if basemapGalleryItem.loadBasemapsError != nil { - Image(systemName: "minus.circle.fill") - .font(.title) - .foregroundColor(.red) - } else if basemapGalleryItem.spatialReferenceStatus == .noMatch { - Image(systemName: "x.circle.fill") - .font(.title) - .foregroundColor(.red) - } } } Text(basemapGalleryItem.name) @@ -230,7 +230,7 @@ extension AlertItem { message: "The spatial reference of the basemap \(basemapSR?.description ?? "") does not match that of the GeoModel \(geoModelSR?.description ?? "")." ) } - + init(error: Error) { self.init( title: "Error loading basemap.", diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index aeecd276e..19ce48491 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -18,22 +18,25 @@ import Foundation /// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. public class BasemapGalleryItem: ObservableObject { - static var defaultThumbnail: UIImage { - return UIImage(named: "DefaultBasemap")! - } - + /// Indicates the status of the basemap's spatial reference in relation to a reference spatial reference. public enum SpatialReferenceStatus { + /// The basemap's spatial reference status is unknown, either because the basemap's + /// base layers haven't been loaded yet or the status has yet to be updated. case unknown + /// The basemap's spatial reference matches the reference spatial reference. case match + /// The basemap's spatial reference does not match the reference spatial reference. case noMatch } /// Creates a `BasemapGalleryItem`. /// - Parameters: /// - basemap: The `Basemap` represented by the item. - /// - name: The item name. - /// - description: The item description. - /// - thumbnail: The thumbnail used to represent the item. + /// - name: The item name. If `nil`, `Basemap.name` is used, if available.. + /// - description: The item description. If `nil`, `Basemap.Item.description` + /// is used, if available. + /// - thumbnail: The thumbnail used to represent the item. If `nil`, + /// `Basemap.Item.thumbnail` is used, if available. public init( basemap: Basemap, name: String? = nil, @@ -51,23 +54,24 @@ public class BasemapGalleryItem: ObservableObject { loadBasemapTask = Task { await loadBasemap() } } + /// The error generated loading the basemap, if any. @Published public var loadBasemapsError: Error? = nil /// The basemap this `BasemapGalleryItem` represents. public private(set) var basemap: Basemap - /// The name of this `Basemap`. + /// The name of the `basemap`. @Published public private(set) var name: String = "" private var nameOverride: String? = nil - /// The description which will be used in the gallery. + /// The description of the `basemap`. @Published public private(set) var description: String? = nil private var descriptionOverride: String? = nil - /// The thumbnail which will be displayed in the gallery. + /// The thumbnail used to represent the `basemap`. @Published public private(set) var thumbnail: UIImage? = nil private var thumbnailOverride: UIImage? = nil @@ -76,19 +80,21 @@ public class BasemapGalleryItem: ObservableObject { @Published public private(set) var isLoading = true + /// The `SpatialReferenceStatus` of the item. This is set via a call to + /// `updateSpatialReferenceStatus(for:)` @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's - /// baseLayers have been loaded in `updateSpatialReferenceStatus`. + /// baseLayers have been loaded in `updateSpatialReferenceStatus(for:)`. public private(set) var spatialReference: SpatialReference? = nil - - /// The currently executing async task for loading basemap. + + /// The currently executing `Task` for loading the basemap. private var loadBasemapTask: Task? = nil } -extension BasemapGalleryItem { - private func loadBasemap() async { +private extension BasemapGalleryItem { + func loadBasemap() async { var loadError: Error? = nil do { try await basemap.load() @@ -102,17 +108,23 @@ extension BasemapGalleryItem { } @MainActor + /// Updates the item in response to basemap loading completion. + /// - Parameter error: The basemap load error, if any. func update(error: Error?) { name = nameOverride ?? basemap.name description = descriptionOverride ?? basemap.item?.description thumbnail = thumbnailOverride ?? - (basemap.item?.thumbnail?.image ?? BasemapGalleryItem.defaultThumbnail) + (basemap.item?.thumbnail?.image ?? UIImage.defaultThumbnail()) loadBasemapsError = error isLoading = false } @MainActor + /// Updates the item's `spatialReference` and `spatialReferenceStatus` properties. + /// - Parameter referenceSpatialReference: The `SpatialReference` used to + /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` + /// `SpatialReference`. func update(with referenceSpatialReference: SpatialReference) { spatialReference = basemap.baseLayers.first?.spatialReference spatialReferenceStatus = matches(referenceSpatialReference) ? .match : .noMatch @@ -134,6 +146,10 @@ extension BasemapGalleryItem: Equatable { } extension BasemapGalleryItem { + /// Loads the first base layer of `basemap` and determines if it matches + /// `referenceSpatialReference`, setting the `spatialReferenceStatus` + /// property appropriately. + /// - Parameter referenceSpatialReference: The `SpatialReference to match to`. public func updateSpatialReferenceStatus( for referenceSpatialReference: SpatialReference? ) async throws { @@ -143,18 +159,7 @@ extension BasemapGalleryItem { else { return } isLoading = true - await withThrowingTaskGroup( - of: Void.self, - returning: Void.self, - body: { taskGroup in - basemap.baseLayers.forEach { baseLayer in - taskGroup.addTask { - try await baseLayer.load() - return - } - } - } - ) + try await basemap.baseLayers.first?.load() await update(with: spatialReference) } @@ -174,3 +179,10 @@ extension BasemapGalleryItem { return true } } + +private extension UIImage { + /// A search result marker symbol. + static func defaultThumbnail() -> UIImage { + return UIImage(named: "DefaultBasemap")! + } +} diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index fc958d9bd..c000bb09e 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -43,17 +43,19 @@ public class BasemapGalleryViewModel: ObservableObject { } @Published + /// The error generated by fetching the `Basemaps` from the `Portal`. public var fetchBasemapsError: Error? = nil - @Published - public var loadGeoModelError: Error? = 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, /// the geoModel will have its basemap replaced with the selected basemap. public var geoModel: GeoModel? { didSet { + // Note that we don't want to store this task and cancel it + // before kicking off another operation because it could have been + // started elsewhere as well as here. Canceling it here would also + // cancel those other operations, which we don't want to do. Task { await load(geoModel: geoModel) } } } @@ -61,13 +63,21 @@ public class BasemapGalleryViewModel: ObservableObject { /// The `Portal` object, if any. Setting the portal will automatically fetch it's base maps /// and add them to the `basemapGalleryItems` array. public var portal: Portal? { - didSet { - Task { await fetchBasemaps(from: portal) } - } - } + didSet { + // Note that we don't want to store this task and cancel it + // before kicking off another operation because it could have been + // started elsewhere as well as here. Canceling it here would also + // cancel those other operations, which we don't want to do. + Task { await fetchBasemaps(from: portal) } + } + } + + // TODO: what does this mean that it is a public var? + // TODO: what happens if the user sets this after things are loaded + // TODO: test this... - /// The list of basemaps currently visible in the gallery. Items added or removed from this list will - /// update the gallery. + /// The list of basemaps currently visible in the gallery. It is comprised of items passed into + /// the `BasemapGalleryItem` constructor and items loaded from the `Portal`. @Published public var basemapGalleryItems: [BasemapGalleryItem] = [] @@ -80,25 +90,24 @@ public class BasemapGalleryViewModel: ObservableObject { geoModel?.basemap = item.basemap } } +} -// public func - - /// Fetches the basemaps from `portal`. - private func fetchBasemaps(from portal: Portal?) async { +private extension BasemapGalleryViewModel { + func fetchBasemaps(from portal: Portal?) async { guard let portal = portal else { return } - + do { basemapGalleryItems += try await portal.developerBasemaps.map { - BasemapGalleryItem.init(basemap: $0) + BasemapGalleryItem(basemap: $0) } } catch { fetchBasemapsError = error } } - /// Loads `geoModel`. - private func load(geoModel: GeoModel?) async { + func load(geoModel: GeoModel?) async { guard let geoModel = geoModel else { return } + do { try await geoModel.load() if let basemap = geoModel.basemap { @@ -108,7 +117,7 @@ public class BasemapGalleryViewModel: ObservableObject { currentBasemapGalleryItem = nil } } catch { - loadGeoModelError = error + // We don't need any errors, as that should be handled by the app. } } } From 70247bbc32a6fccb106719b35c27b0cc3123e41f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:49:50 -0600 Subject: [PATCH 0273/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: Philip Ridgeway --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 090b94831..fd281b2f1 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -32,7 +32,7 @@ struct SearchExampleView: View { /// The map viewpoint used by the `SearchView` to pan/zoom the map /// to the extent of the search results. @State - var searchResultViewpoint: Viewpoint? = Viewpoint( + private var searchResultViewpoint: Viewpoint? = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), scale: 1000000 ) From a5d79d25c0de80248f3a95db4e9241a53d185ed1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:49:59 -0600 Subject: [PATCH 0274/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 40bb4ebf9..dc8f13708 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -19,7 +19,7 @@ import ArcGIS public class LocatorSearchSource: ObservableObject, SearchSource { /// Creates a locator search source. /// - Parameters: - /// - name: Name to show when presenting this source in the UI. + /// - name: The name to show when presenting this source in the UI. /// - locatorTask: The `LocatorTask` to use for searching. /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. From f98a9b22f496e50af8c23359cbf82b0545e0b6bb Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:50:06 -0600 Subject: [PATCH 0275/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index dc8f13708..9d0dddb62 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -41,7 +41,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { self.geocodeParameters.addResultAttributeName("*") } - /// Name to show when presenting this source in the UI. + /// The name to show when presenting this source in the UI. public var name: String /// The maximum results to return when performing a search. Most sources default to 6 From 8c9a5a8c143736e11bb00a3c6095b233c0d42b27 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:53:11 -0600 Subject: [PATCH 0276/1722] Update Sources/ArcGISToolkit/Utility/SearchField.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/SearchField.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index 8ad8ad787..c72e16045 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -44,8 +44,9 @@ public struct SearchField: View { // Search text field TextField( - defaultPlaceholder, - text: currentQuery + "Search Query", + text: currentQuery, + prompt: Text(defaultPlaceholder) ) .onSubmit { onCommit() } From 7a9b332a48acd79caa7fa40ddf45bc55e4213d6f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:53:27 -0600 Subject: [PATCH 0277/1722] Update Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index ea2d459b9..171bec82a 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -65,7 +65,7 @@ final class SmartLocatorSearchSourceTests: XCTestCase { } } -extension Envelope { +private extension Envelope { static let edinburgh = Envelope( xMin: -365155.60783391213, yMin: 7536778.456812576, From 3131495b30515a63ff642adfab423174dd1aec01 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:54:38 -0600 Subject: [PATCH 0278/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 9d0dddb62..407d860cd 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -79,7 +79,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { _ queryString: String, searchExtent: Envelope ) async throws -> [SearchResult] { - return try await internalSearch( + try await internalSearch( queryString, searchArea: searchExtent ) From 61e804494e6a88261222dbd1d382b524f53456c8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:55:04 -0600 Subject: [PATCH 0279/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 407d860cd..4543d7f01 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -90,7 +90,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { searchArea: Geometry?, preferredSearchLocation: Point? ) async throws -> [SearchResult] { - return try await internalSearch( + try await internalSearch( queryString, searchArea: searchArea, preferredSearchLocation: preferredSearchLocation From f74319a1b977aee6933674a4091db0595da68991 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 14:55:54 -0600 Subject: [PATCH 0280/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- .../ArcGISToolkit/Components/Search/SearchView.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 8e810ce33..a59ae98fb 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -27,14 +27,9 @@ public struct SearchView: View { viewpoint: Binding? = nil, resultsOverlay: GraphicsOverlay? = nil ) { - if let searchViewModel = searchViewModel { - self.searchViewModel = searchViewModel - } - else { - self.searchViewModel = SearchViewModel( - sources: [LocatorSearchSource()] - ) - } + self.searchViewModel = searchViewModel ?? SearchViewModel( + sources: [LocatorSearchSource()] + ) self.resultsOverlay = resultsOverlay self.viewpoint = viewpoint } From eec47d80ffb4f6b604a4a38334d2c323d2d393f4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 15:04:08 -0600 Subject: [PATCH 0281/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a59ae98fb..7db74fabd 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -131,7 +131,7 @@ public struct SearchView: View { // MARK: Modifiers - /// Determines whether a built-in result view will be shown. If `false`, the result display/selection + /// Specifies whether a built-in result view will be shown. If `false`, the result display/selection /// list is not shown. Set to `false` if you want to define a custom result list. You might use a /// custom result list to show results in a separate list, disconnected from the rest of the search view. /// Defaults to `true`. From 7079252bd709d428054f45e01303b087c6cf3c67 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Nov 2021 15:20:25 -0600 Subject: [PATCH 0282/1722] PR review changes. --- .../Search/LocatorSearchSource.swift | 2 +- .../Components/Search/SearchView.swift | 61 ++++++++++--------- .../ArcGISToolkit/Utility/SearchField.swift | 4 +- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 40bb4ebf9..f1090f0e5 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -65,7 +65,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { } /// The locator used by this search source. - public private(set) var locatorTask: LocatorTask + public let locatorTask: LocatorTask /// Parameters used for geocoding. Some properties on parameters will be updated automatically /// based on searches. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 8e810ce33..5d4e98552 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -53,8 +53,8 @@ public struct SearchView: View { /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". Note: this is set using the - /// `defaultPlaceholder` modifier. - private var defaultPlaceholder: String = "Find a place or address" + /// `searchFieldPrompt` modifier. + private var searchFieldPrompt: String = "Find a place or address" /// Determines whether a built-in result view will be shown. Defaults to true. /// If false, the result display/selection list is not shown. Set to false if you want to hide the results @@ -64,8 +64,8 @@ public struct SearchView: View { private var enableResultListView = true /// Message to show when there are no results or suggestions. Defaults to "No results found". - /// Note: this is set using the `noResultMessage` modifier. - private var noResultMessage = "No results found" + /// Note: this is set using the `noResultsMessage` modifier. + private var noResultsMessage = "No results found" private var searchBarWidth: CGFloat = 360.0 @@ -83,27 +83,30 @@ public struct SearchView: View { VStack { SearchField( currentQuery: $searchViewModel.currentQuery, - defaultPlaceholder: defaultPlaceholder, + searchFieldPrompt: searchFieldPrompt, isShowResultsHidden: !enableResultListView, showResults: $showResultListView, onCommit: { searchViewModel.commitSearch() } ) if enableResultListView, showResultListView { - switch searchViewModel.searchOutcome { - case .results(let results): - SearchResultList( - searchResults: results, - selectedResult: $searchViewModel.selectedResult, - noResultMessage: noResultMessage - ) - case .suggestions(let suggestions): - SearchSuggestionList( - suggestionResults: suggestions, - currentSuggestion: $searchViewModel.currentSuggestion, - noResultMessage: noResultMessage - ) - case .none: + if let searchOutcome = searchViewModel.searchOutcome { + switch searchOutcome { + case .results(let results): + SearchResultList( + searchResults: results, + selectedResult: $searchViewModel.selectedResult, + noResultsMessage: noResultsMessage + ) + case .suggestions(let suggestions): + SearchSuggestionList( + suggestionResults: suggestions, + currentSuggestion: $searchViewModel.currentSuggestion, + noResultsMessage: noResultsMessage + ) + } + } + else { EmptyView() } } @@ -150,20 +153,20 @@ public struct SearchView: View { /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". - /// - Parameter defaultPlaceholder: The new value. + /// - Parameter searchFieldPrompt: The new value. /// - Returns: A new `SearchView`. - public func defaultPlaceholder(_ defaultPlaceholder: String) -> Self { + public func searchFieldPrompt(_ searchFieldPrompt: String) -> Self { var copy = self - copy.defaultPlaceholder = defaultPlaceholder + copy.searchFieldPrompt = searchFieldPrompt return copy } /// Message to show when there are no results or suggestions. Defaults to "No results found". - /// - Parameter noResultMessage: The new value. + /// - Parameter noResultsMessage: The new value. /// - Returns: A new `SearchView`. - public func noResultMessage(_ noResultMessage: String) -> Self { + public func noResultsMessage(_ noResultsMessage: String) -> Self { var copy = self - copy.noResultMessage = noResultMessage + copy.noResultsMessage = noResultsMessage return copy } @@ -214,7 +217,7 @@ private extension SearchView { struct SearchResultList: View { var searchResults: Result<[SearchResult], SearchError> @Binding var selectedResult: SearchResult? - var noResultMessage: String + var noResultsMessage: String var body: some View { Group { @@ -239,7 +242,7 @@ struct SearchResultList: View { } } else if results.isEmpty { List { - Text(noResultMessage) + Text(noResultsMessage) } } case .failure(let error): @@ -255,7 +258,7 @@ struct SearchResultList: View { struct SearchSuggestionList: View { var suggestionResults: Result<[SearchSuggestion], SearchError> @Binding var currentSuggestion: SearchSuggestion? - var noResultMessage: String + var noResultsMessage: String var body: some View { List { @@ -272,7 +275,7 @@ struct SearchSuggestionList: View { } } else { - Text(noResultMessage) + Text(noResultsMessage) } case .failure(let error): Text(error.errorDescription) diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index 8ad8ad787..9bccab89e 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -20,7 +20,7 @@ public struct SearchField: View { public var currentQuery: Binding /// The default placeholder displayed when `currentQuery` is empty. - public var defaultPlaceholder: String = "" + public var searchFieldPrompt: String = "" /// The visibility of the `showResults` button. public var isShowResultsHidden: Bool = true @@ -44,7 +44,7 @@ public struct SearchField: View { // Search text field TextField( - defaultPlaceholder, + searchFieldPrompt, text: currentQuery ) .onSubmit { onCommit() } From 4d6896cb3c609f6d5e47bebb21c02007d0b8b991 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Nov 2021 09:19:02 -0600 Subject: [PATCH 0283/1722] PR review comments. --- Examples/Examples/SearchExampleView.swift | 1 - .../Search/LocatorSearchSource.swift | 12 +- .../Components/Search/SearchView.swift | 116 +++++++++--------- .../Search/SmartLocatorSearchSource.swift | 12 +- .../ArcGISToolkit/Utility/SearchField.swift | 41 ++++--- .../LocatorSearchSourceTests.swift | 36 +----- .../SmartLocatorSearchSourceTests.swift | 12 +- 7 files changed, 103 insertions(+), 127 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index fd281b2f1..a0ca4c666 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -77,7 +77,6 @@ struct SearchExampleView: View { viewpoint: $searchResultViewpoint, resultsOverlay: searchResultsOverlay ) - .searchBarWidth(360.0) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 1e492bbd6..4440377fd 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -87,8 +87,8 @@ public class LocatorSearchSource: ObservableObject, SearchSource { public func search( _ queryString: String, - searchArea: Geometry?, - preferredSearchLocation: Point? + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { try await internalSearch( queryString, @@ -99,8 +99,8 @@ public class LocatorSearchSource: ObservableObject, SearchSource { public func search( _ searchSuggestion: SearchSuggestion, - searchArea: Geometry?, - preferredSearchLocation: Point? + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] } @@ -120,8 +120,8 @@ public class LocatorSearchSource: ObservableObject, SearchSource { public func suggest( _ queryString: String, - searchArea: Geometry?, - preferredSearchLocation: Point? + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil ) async throws -> [SearchSuggestion] { suggestParameters.searchArea = searchArea suggestParameters.preferredSearchLocation = preferredSearchLocation diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 28a2b57da..6d4c10fea 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -62,7 +62,15 @@ public struct SearchView: View { /// Note: this is set using the `noResultsMessage` modifier. private var noResultsMessage = "No results found" - private var searchBarWidth: CGFloat = 360.0 + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? + + @Environment(\.verticalSizeClass) + private var verticalSizeClass: UserInterfaceSizeClass? + + private var searchBarWidth: CGFloat? { + horizontalSizeClass == .compact && verticalSizeClass == .regular ? nil : 360 + } @State private var shouldZoomToResults = true @@ -77,31 +85,34 @@ public struct SearchView: View { Spacer() VStack { SearchField( - currentQuery: $searchViewModel.currentQuery, + query: $searchViewModel.currentQuery, searchFieldPrompt: searchFieldPrompt, isShowResultsHidden: !enableResultListView, - showResults: $showResultListView, - onCommit: { searchViewModel.commitSearch() } + showResults: $showResultListView ) + .onSubmit { searchViewModel.commitSearch() } + .submitLabel(.search) if enableResultListView, showResultListView { if let searchOutcome = searchViewModel.searchOutcome { - switch searchOutcome { - case .results(let results): - SearchResultList( - searchResults: results, - selectedResult: $searchViewModel.selectedResult, - noResultsMessage: noResultsMessage - ) - case .suggestions(let suggestions): - SearchSuggestionList( - suggestionResults: suggestions, - currentSuggestion: $searchViewModel.currentSuggestion, - noResultsMessage: noResultsMessage - ) + Group { + switch searchOutcome { + case .results(let results): + SearchResultList( + searchResults: results, + selectedResult: $searchViewModel.selectedResult, + noResultsMessage: noResultsMessage + ) + case .suggestions(let suggestions): + SearchSuggestionList( + suggestionResults: suggestions, + currentSuggestion: $searchViewModel.currentSuggestion, + noResultsMessage: noResultsMessage + ) + } } - } - else { + .esriBorder(padding: EdgeInsets()) + } else { EmptyView() } } @@ -156,7 +167,9 @@ public struct SearchView: View { return copy } - /// Message to show when there are no results or suggestions. Defaults to "No results found". + /// Sets the message to show when there are no results or suggestions. + /// + /// The default message is "No results found". /// - Parameter noResultsMessage: The new value. /// - Returns: A new `SearchView`. public func noResultsMessage(_ noResultsMessage: String) -> Self { @@ -164,15 +177,6 @@ public struct SearchView: View { copy.noResultsMessage = noResultsMessage return copy } - - /// The width of the search bar. - /// - Parameter searchBarWidth: The desired width of the search bar. - /// - Returns: A new `SearchView`. - public func searchBarWidth(_ searchBarWidth: CGFloat) -> Self { - var copy = self - copy.searchBarWidth = searchBarWidth - return copy - } } private extension SearchView { @@ -186,18 +190,21 @@ private extension SearchView { resultsOverlay.removeAllGraphics() resultsOverlay.addGraphics(resultGraphics) + // Make sure we have a viewpoint to zoom to. + guard let viewpoint = viewpoint else { return } + if !resultGraphics.isEmpty, let envelope = resultsOverlay.extent, shouldZoomToResults { let builder = EnvelopeBuilder(envelope: envelope) builder.expand(factor: 1.1) let targetExtent = builder.toGeometry() as! Envelope - viewpoint?.wrappedValue = Viewpoint( + viewpoint.wrappedValue = Viewpoint( targetExtent: targetExtent ) searchViewModel.lastSearchExtent = targetExtent } else { - viewpoint?.wrappedValue = nil + viewpoint.wrappedValue = nil } if !shouldZoomToResults { shouldZoomToResults = true } @@ -218,27 +225,27 @@ struct SearchResultList: View { Group { switch searchResults { case .success(let results): - if results.count > 1 { - // Only show the list if we have more than one result. + if results.count != 1 { List { - ForEach(results) { result in - HStack { - ResultRow(searchResult: result) - .onTapGesture { - selectedResult = result + if results.count > 1 { + // Only show the list if we have more than one result. + ForEach(results) { result in + HStack { + ResultRow(searchResult: result) + .onTapGesture { + selectedResult = result + } + if result == selectedResult { + Spacer() + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) } - if result == selectedResult { - Spacer() - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) } } + } else if results.isEmpty { + Text(noResultsMessage) } } - } else if results.isEmpty { - List { - Text(noResultsMessage) - } } case .failure(let error): List { @@ -246,7 +253,6 @@ struct SearchResultList: View { } } } - .esriBorder(padding: EdgeInsets()) } } @@ -260,23 +266,19 @@ struct SearchSuggestionList: View { switch suggestionResults { case .success(let suggestions): if !suggestions.isEmpty { - if suggestions.count > 0 { - ForEach(suggestions) { suggestion in - ResultRow(searchSuggestion: suggestion) - .onTapGesture() { - currentSuggestion = suggestion - } - } + ForEach(suggestions) { suggestion in + ResultRow(searchSuggestion: suggestion) + .onTapGesture() { + currentSuggestion = suggestion + } } - } - else { + } else { Text(noResultsMessage) } case .failure(let error): Text(error.errorDescription) } } - .esriBorder(padding: EdgeInsets()) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 2e944b905..2c77e1b40 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -64,8 +64,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { public override func search( _ queryString: String, - searchArea: Geometry?, - preferredSearchLocation: Point? + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { // First, peform super class search. var results = try await super.search( @@ -100,8 +100,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { public override func search( _ searchSuggestion: SearchSuggestion, - searchArea: Geometry?, - preferredSearchLocation: Point? + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { guard let suggestResult = searchSuggestion.suggestResult else { return [] @@ -138,8 +138,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { public override func suggest( _ queryString: String, - searchArea: Geometry?, - preferredSearchLocation: Point? + searchArea: Geometry? = nil, + preferredSearchLocation: Point? = nil ) async throws -> [SearchSuggestion] { var results = try await super.suggest( queryString, diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index a8b6fbbd8..b90bf4fe1 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -16,44 +16,47 @@ import SwiftUI /// A custom view implementing a SearchField. It contains a search button, text field, delete text button, /// and a button to allow users to hide/show the search results list. public struct SearchField: View { + internal init( + query: Binding, + searchFieldPrompt: String = "", + isShowResultsHidden: Bool = true, + showResults: Binding? = nil + ) { + self.query = query + self.searchFieldPrompt = searchFieldPrompt + self.isShowResultsHidden = isShowResultsHidden + self.showResults = showResults + } + /// The current search query. - public var currentQuery: Binding + private var query: Binding /// The default placeholder displayed when `currentQuery` is empty. - public var searchFieldPrompt: String = "" + private let searchFieldPrompt: String /// The visibility of the `showResults` button. - public var isShowResultsHidden: Bool = true + private let isShowResultsHidden: Bool /// Binding allowing the user to toggle the visibility of the results list. - public var showResults: Binding? = nil - - /// The handler executed when the user submits a search, either via the `TextField` - /// or the Search button. - public var onCommit: () -> Void = { } + private var showResults: Binding? public var body: some View { HStack { - // Search button - Button { - onCommit() - } label: { - Image(systemName: "magnifyingglass.circle.fill") - .foregroundColor(Color(uiColor: .opaqueSeparator)) - } + // Search icon + Image(systemName: "magnifyingglass.circle.fill") + .foregroundColor(Color(uiColor: .opaqueSeparator)) // Search text field TextField( "Search Query", - text: currentQuery, + text: query, prompt: Text(searchFieldPrompt) ) - .onSubmit { onCommit() } // Delete text button - if !currentQuery.wrappedValue.isEmpty { + if !query.wrappedValue.isEmpty { Button { - currentQuery.wrappedValue = "" + query.wrappedValue = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundColor(Color(.opaqueSeparator)) diff --git a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift index 567bd78d0..e87737464 100644 --- a/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/LocatorSearchSourceTests.swift @@ -21,30 +21,18 @@ final class LocatorSearchSourceTests: XCTestCase { locator.maximumResults = 4 XCTAssertEqual(locator.maximumResults, 4) - var searchResults = try await locator.search( - "Coffee", - searchArea: nil, - preferredSearchLocation: nil - ) + var searchResults = try await locator.search("Coffee") XCTAssertEqual(searchResults.count, 4) locator.maximumResults = 12 - searchResults = try await locator.search( - "Coffee", - searchArea: nil, - preferredSearchLocation: nil - ) + searchResults = try await locator.search("Coffee") XCTAssertEqual(searchResults.count, 12) // Set property directly on geocode parameters. locator.geocodeParameters.maxResults = 2 XCTAssertEqual(locator.geocodeParameters.maxResults, 2) XCTAssertEqual(locator.maximumResults, 2) - searchResults = try await locator.search( - "Coffee", - searchArea: nil, - preferredSearchLocation: nil - ) + searchResults = try await locator.search("Coffee") XCTAssertEqual(searchResults.count, 2) } @@ -53,30 +41,18 @@ final class LocatorSearchSourceTests: XCTestCase { locator.maximumSuggestions = 4 XCTAssertEqual(locator.maximumSuggestions, 4) - var suggestResults = try await locator.suggest( - "Coffee", - searchArea: nil, - preferredSearchLocation: nil - ) + var suggestResults = try await locator.suggest("Coffee") XCTAssertEqual(suggestResults.count, 4) locator.maximumSuggestions = 12 - suggestResults = try await locator.suggest( - "Coffee", - searchArea: nil, - preferredSearchLocation: nil - ) + suggestResults = try await locator.suggest("Coffee") XCTAssertEqual(suggestResults.count, 12) // Set property directly on suggest parameters. locator.suggestParameters.maxResults = 2 XCTAssertEqual(locator.suggestParameters.maxResults, 2) XCTAssertEqual(locator.maximumSuggestions, 2) - suggestResults = try await locator.suggest( - "Coffee", - searchArea: nil, - preferredSearchLocation: nil - ) + suggestResults = try await locator.suggest("Coffee") XCTAssertEqual(suggestResults.count, 2) } } diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 171bec82a..0e5dda0b8 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -23,8 +23,7 @@ final class SmartLocatorSearchSourceTests: XCTestCase { locator.repeatSearchResultThreshold = 0 var searchResults = try await locator.search( "Dunkin' Donuts", - searchArea: Envelope.edinburgh, - preferredSearchLocation: nil + searchArea: Envelope.edinburgh ) var results = try XCTUnwrap(searchResults) XCTAssertEqual(results, []) @@ -33,8 +32,7 @@ final class SmartLocatorSearchSourceTests: XCTestCase { locator.repeatSearchResultThreshold = 1 searchResults = try await locator.search( "Dunkin' Donuts", - searchArea: Envelope.edinburgh, - preferredSearchLocation: nil + searchArea: Envelope.edinburgh ) results = try XCTUnwrap(searchResults) XCTAssertNotEqual(results, []) @@ -47,8 +45,7 @@ final class SmartLocatorSearchSourceTests: XCTestCase { locator.repeatSuggestResultThreshold = 0 var suggestResults = try await locator.suggest( "Dunkin' Donuts", - searchArea: Envelope.edinburgh, - preferredSearchLocation: nil + searchArea: Envelope.edinburgh ) var results = try XCTUnwrap(suggestResults) XCTAssertEqual(results, []) @@ -57,8 +54,7 @@ final class SmartLocatorSearchSourceTests: XCTestCase { locator.repeatSuggestResultThreshold = 1 suggestResults = try await locator.suggest( "Dunkin' Donuts", - searchArea: Envelope.edinburgh, - preferredSearchLocation: nil + searchArea: Envelope.edinburgh ) results = try XCTUnwrap(suggestResults) XCTAssertNotEqual(suggestResults, []) From 60832ee39de15de3d1a57bc8f0e5127384ff16bd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Nov 2021 10:07:49 -0600 Subject: [PATCH 0284/1722] PR review changes. --- .../Components/Search/SearchViewModel.swift | 33 ++++++++++--------- .../Search/SmartLocatorSearchSource.swift | 24 +++++++------- .../SmartLocatorSearchSourceTests.swift | 8 ++--- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 0e2620b36..dca738197 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -43,14 +43,13 @@ public class SearchViewModel: ObservableObject { /// - queryCenter: Defines the center for the search. /// - resultMode: Defines how many results to return. /// - sources: Collection of search sources to be used. - public convenience init( + public init( activeSource: SearchSource? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, sources: [SearchSource] = [] ) { - self.init() self.activeSource = activeSource self.queryArea = queryArea self.queryCenter = queryCenter @@ -89,9 +88,11 @@ public class SearchViewModel: ObservableObject { } } - /// The current GeoView extent. Defaults to null. This should be updated as the user navigates - /// the map/scene. It will be used to determine the value of `IsEligibleForRequery` - /// for the 'Repeat search here' behavior. If that behavior is not wanted, it should be left `nil`. + /// The current map/scene view extent. Defaults to `nil`. + /// + /// This should be updated as the user navigates the map/scene. It will be + /// used to determine the value of `isEligibleForRequery` for the 'Repeat + /// search here' behavior. If that behavior is not wanted, it should be left `nil`. public var geoViewExtent: Envelope? = nil { willSet { guard !isEligibleForRequery, @@ -121,7 +122,7 @@ public class SearchViewModel: ObservableObject { } } - /// True if the Extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. + /// `true` if the extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. /// Changes to this property are driven by changes to the `geoViewExtent` property. This value will be @@ -130,7 +131,7 @@ public class SearchViewModel: ObservableObject { @Published public private(set) var isEligibleForRequery: Bool = false - /// The search area to be used for the current query. Results will be limited to those + /// The search area to be used for the current query. Results will be limited to those /// within `QueryArea`. Defaults to `nil`. public var queryArea: Geometry? = nil @@ -140,7 +141,7 @@ public class SearchViewModel: ObservableObject { /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate /// number of results is returned based on the type of suggestion chosen - /// (driven by the IsCollection property). + /// (driven by the `isCollection` property). public var resultMode: SearchResultMode = .automatic /// Collection of results. `nil` means no query has been made. An empty array means there @@ -236,8 +237,8 @@ public class SearchViewModel: ObservableObject { } } -extension SearchViewModel { - private func repeatSearchTask() -> Task<(), Never> { +private extension SearchViewModel { + func repeatSearchTask() -> Task<(), Never> { Task { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let queryExtent = geoViewExtent, @@ -254,7 +255,7 @@ extension SearchViewModel { } } - private func searchTask() -> Task<(), Never> { + func searchTask() -> Task<(), Never> { Task { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let source = currentSource() else { return } @@ -269,7 +270,7 @@ extension SearchViewModel { } } - private func updateSuggestionsTask() -> Task<(), Never> { + func updateSuggestionsTask() -> Task<(), Never> { Task { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let source = currentSource() else { @@ -290,7 +291,7 @@ extension SearchViewModel { } } - private func acceptSuggestionTask(_ searchSuggestion: SearchSuggestion) -> Task<(), Never> { + func acceptSuggestionTask(_ searchSuggestion: SearchSuggestion) -> Task<(), Never> { Task { await search(with: { try await searchSuggestion.owningSource.search( @@ -305,7 +306,7 @@ extension SearchViewModel { } } - private func search(with action: () async throws -> [SearchResult]) async { + func search(with action: () async throws -> [SearchResult]) async { do { // User is performing a search, so set `lastSearchExtent`. lastSearchExtent = geoViewExtent @@ -317,10 +318,10 @@ extension SearchViewModel { } } - private func process(searchResults: [SearchResult], isCollection: Bool = true) { + func process(searchResults: [SearchResult], isCollection: Bool = true) { let effectiveResults: [SearchResult] - switch (resultMode) { + switch resultMode { case .single: effectiveResults = Array(searchResults.prefix(1)) case .multiple: diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 2c77e1b40..5ecd6af27 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -36,8 +36,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ), maximumResults: Int32 = 6, maximumSuggestions: Int32 = 6, - repeatSearchResultThreshold: Int = 1, - repeatSuggestResultThreshold: Int = 6 + repeatSearchResultThreshold: Int? = 1, + repeatSuggestResultThreshold: Int? = 6 ) { super.init( name: name, @@ -52,15 +52,15 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// The minimum number of results to attempt to return. If there are too few results, the search is /// repeated with loosened parameters until enough results are accumulated. If no search is /// successful, it is still possible to have a total number of results less than this threshold. Does not - /// apply to repeated search with area constraint. Set to zero to disable search repeat behavior. - public var repeatSearchResultThreshold: Int = 1 + /// apply to repeated search with area constraint. Set to `nil` to disable search repeat behavior. + public var repeatSearchResultThreshold: Int? = 1 /// The minimum number of suggestions to attempt to return. If there are too few suggestions, /// request is repeated with loosened constraints until enough suggestions are accumulated. /// If no search is successful, it is still possible to have a total number of results less than this - /// threshold. Does not apply to repeated search with area constraint. Set to zero to disable search + /// threshold. Does not apply to repeated search with area constraint. Set to `nil` to disable search /// repeat behavior. - public var repeatSuggestResultThreshold: Int = 6 + public var repeatSuggestResultThreshold: Int? = 6 public override func search( _ queryString: String, @@ -73,8 +73,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { searchArea: searchArea, preferredSearchLocation: preferredSearchLocation ) - if results.count > repeatSearchResultThreshold || - repeatSearchResultThreshold == 0 || + if repeatSearchResultThreshold == nil || + results.count > repeatSearchResultThreshold! || geocodeParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. @@ -112,7 +112,9 @@ public class SmartLocatorSearchSource: LocatorSearchSource { searchArea: searchArea, preferredSearchLocation: preferredSearchLocation ) - if results.count > repeatSearchResultThreshold || + + if repeatSearchResultThreshold == nil || + results.count > repeatSearchResultThreshold! || geocodeParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. @@ -146,8 +148,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { searchArea: searchArea, preferredSearchLocation: preferredSearchLocation ) - if results.count > repeatSuggestResultThreshold || - repeatSuggestResultThreshold == 0 || + if repeatSuggestResultThreshold == nil || + results.count > repeatSuggestResultThreshold! || suggestParameters.searchArea == nil { // Result count meets threshold or there were no geographic // constraints on the search, so return results. diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 0e5dda0b8..e63a8be5d 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -19,8 +19,8 @@ final class SmartLocatorSearchSourceTests: XCTestCase { func testRepeatSearchResultThreshold() async throws { let locator = SmartLocatorSearchSource() - // Threshold of 0 means no re-query. - locator.repeatSearchResultThreshold = 0 + // Threshold of nil means no re-query. + locator.repeatSearchResultThreshold = nil var searchResults = try await locator.search( "Dunkin' Donuts", searchArea: Envelope.edinburgh @@ -41,8 +41,8 @@ final class SmartLocatorSearchSourceTests: XCTestCase { func testRepeatSuggestResultThreshold() async throws { let locator = SmartLocatorSearchSource() - // Threshold of 0 means no re-query. - locator.repeatSuggestResultThreshold = 0 + // Threshold of nil means no re-query. + locator.repeatSuggestResultThreshold = nil var suggestResults = try await locator.suggest( "Dunkin' Donuts", searchArea: Envelope.edinburgh From 8d7335e9628c1dd234253467e6784979198eec02 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Nov 2021 13:12:02 -0600 Subject: [PATCH 0285/1722] PR review changes - redo SearchOutcome to include third .failure option --- .../Components/Search/SearchView.swift | 70 ++++++++----------- .../Components/Search/SearchViewModel.swift | 53 ++++++-------- .../SearchViewModelTests.swift | 50 ++++++------- 3 files changed, 76 insertions(+), 97 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 6d4c10fea..f21daceab 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -109,6 +109,10 @@ public struct SearchView: View { currentSuggestion: $searchViewModel.currentSuggestion, noResultsMessage: noResultsMessage ) + case .failure(let error): + List { + Text(error.errorDescription) + } } } .esriBorder(padding: EdgeInsets()) @@ -132,7 +136,7 @@ public struct SearchView: View { .onChange(of: searchViewModel.searchOutcome, perform: { newValue in switch newValue { case .results(let results): - display(searchResults: (try? results.get()) ?? []) + display(searchResults: results) default: display(searchResults: []) } @@ -217,39 +221,30 @@ private extension SearchView { } struct SearchResultList: View { - var searchResults: Result<[SearchResult], SearchError> + var searchResults: [SearchResult] @Binding var selectedResult: SearchResult? var noResultsMessage: String var body: some View { - Group { - switch searchResults { - case .success(let results): - if results.count != 1 { - List { - if results.count > 1 { - // Only show the list if we have more than one result. - ForEach(results) { result in - HStack { - ResultRow(searchResult: result) - .onTapGesture { - selectedResult = result - } - if result == selectedResult { - Spacer() - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) - } + if searchResults.count != 1 { + List { + if searchResults.count > 1 { + // Only show the list if we have more than one result. + ForEach(searchResults) { result in + HStack { + ResultRow(searchResult: result) + .onTapGesture { + selectedResult = result } + if result == selectedResult { + Spacer() + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) } - } else if results.isEmpty { - Text(noResultsMessage) } } - } - case .failure(let error): - List { - Text(error.localizedDescription) + } else if searchResults.isEmpty { + Text(noResultsMessage) } } } @@ -257,26 +252,21 @@ struct SearchResultList: View { } struct SearchSuggestionList: View { - var suggestionResults: Result<[SearchSuggestion], SearchError> + var suggestionResults: [SearchSuggestion] @Binding var currentSuggestion: SearchSuggestion? var noResultsMessage: String var body: some View { List { - switch suggestionResults { - case .success(let suggestions): - if !suggestions.isEmpty { - ForEach(suggestions) { suggestion in - ResultRow(searchSuggestion: suggestion) - .onTapGesture() { - currentSuggestion = suggestion - } - } - } else { - Text(noResultsMessage) + if !suggestionResults.isEmpty { + ForEach(suggestionResults) { suggestion in + ResultRow(searchSuggestion: suggestion) + .onTapGesture() { + currentSuggestion = suggestion + } } - case .failure(let error): - Text(error.errorDescription) + } else { + Text(noResultsMessage) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index dca738197..ac798a9f8 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -31,9 +31,15 @@ public class SearchViewModel: ObservableObject { } /// The outcome of a geocode operation (search or suggestion). + /// + /// An empty results or suggestions array means there were no results. + /// + /// The `failure` case contains the error (if any) generated by the last search or suggestion + /// operation. public enum SearchOutcome { - case results(result: Result<[SearchResult], SearchError>) - case suggestions(result: Result<[SearchSuggestion], SearchError>) + case results([SearchResult]) + case suggestions([SearchSuggestion]) + case failure(SearchError) } /// Creates a `SearchViewModel`. @@ -66,14 +72,14 @@ public class SearchViewModel: ObservableObject { willSet { isEligibleForRequery = false + guard let searchOutcome = searchOutcome else { return } switch searchOutcome { - case .results(_): - searchOutcome = nil case .suggestions(_): if currentQuery.isEmpty { - searchOutcome = nil + self.searchOutcome = nil } - case .none: + default: + self.searchOutcome = nil break } } @@ -144,32 +150,15 @@ public class SearchViewModel: ObservableObject { /// (driven by the `isCollection` property). public var resultMode: SearchResultMode = .automatic - /// Collection of results. `nil` means no query has been made. An empty array means there - /// were no results, and the view should show an appropriate 'no results' message. - /// - /// Collection of suggestion results. Defaults to `nil`. This collection will be set to empty when there - /// are no suggestions, `nil` when no suggestions have been requested. If the list is empty, - /// a useful 'no results' message should be shown by the view. - + /// The collection of search and suggestion results. A `nil` value means no query has been made. @Published public private(set) var searchOutcome: SearchOutcome? { didSet { - switch searchOutcome { - case .results(let results): - switch results { - case .success(let results): - if results.count == 1 { - selectedResult = results.first - } - else { - selectedResult = nil - } - default: - selectedResult = nil - } - default: + if case let .results(results) = searchOutcome, + results.count == 1 { + selectedResult = results.first + } else { selectedResult = nil - break } } } @@ -282,11 +271,11 @@ private extension SearchViewModel { searchArea: queryArea, preferredSearchLocation: queryCenter ) - searchOutcome = .suggestions(result: .success(suggestions)) + searchOutcome = .suggestions(suggestions) } catch is CancellationError { searchOutcome = nil } catch { - searchOutcome = .suggestions(result: .failure(SearchError(error))) + searchOutcome = .failure(SearchError(error)) } } } @@ -314,7 +303,7 @@ private extension SearchViewModel { } catch is CancellationError { searchOutcome = nil } catch { - searchOutcome = .results(result: .failure(SearchError(error))) + searchOutcome = .failure(SearchError(error)) } } @@ -334,7 +323,7 @@ private extension SearchViewModel { } } - searchOutcome = .results(result: .success(effectiveResults)) + searchOutcome = .results(effectiveResults) } } diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 1cd4e1100..c6526d636 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -29,12 +29,12 @@ class SearchViewModelTests: XCTestCase { // Get suggestion let suggestions = try await searchSuggestions(model) - let suggestion = try XCTUnwrap(suggestions?.get().first) + let suggestion = try XCTUnwrap(suggestions?.first) model.acceptSuggestion(suggestion) let results = try await searchResults(model, dropFirst: true) - let result = try XCTUnwrap(results?.get()) + let result = try XCTUnwrap(results) XCTAssertEqual(result.count, 1) // With only one result, model should set `selectedResult` property. @@ -54,13 +54,13 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() let results = try await searchResults(model) - let result = try XCTUnwrap(results?.get().first) + let result = try XCTUnwrap(results?.first) XCTAssertEqual(result.owningSource.name, activeSource.name) model.updateSuggestions() let suggestions = try await searchSuggestions(model, dropFirst: true) - let suggestion = try XCTUnwrap(suggestions?.get().first) + let suggestion = try XCTUnwrap(suggestions?.first) XCTAssertEqual(suggestion.owningSource.name, activeSource.name) } @@ -76,7 +76,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() var results = try await searchResults(model) - var result = try XCTUnwrap(results?.get()) + var result = try XCTUnwrap(results) XCTAssertEqual(result, []) XCTAssertNil(model.selectedResult) @@ -86,7 +86,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() results = try await searchResults(model) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertEqual(result.count, 1) // One results automatically populates `selectedResult`. @@ -98,7 +98,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() results = try await searchResults(model) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertGreaterThan(result.count, 1) XCTAssertNil(model.selectedResult) @@ -108,7 +108,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() results = try await searchResults(model, dropFirst: true) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertGreaterThan(result.count, 1) XCTAssertNil(model.selectedResult) @@ -219,7 +219,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() var results = try await searchResults(model) - var result = try XCTUnwrap(results?.get()) + var result = try XCTUnwrap(results) XCTAssertGreaterThan(result.count, 1) let resultGeometryUnion: Geometry = try XCTUnwrap( @@ -240,7 +240,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() results = try await searchResults(model) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertEqual(result, []) model.queryArea = Polygon.minneapolis @@ -256,7 +256,7 @@ class SearchViewModelTests: XCTestCase { // and will give us the next one, which is the correct one (the result // from the second `model.commitSearch()` call). results = try await searchResults(model, dropFirst: true) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertEqual(result.count, 1) } @@ -270,7 +270,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() var results = try await searchResults(model) - var result = try XCTUnwrap(results?.get()) + var result = try XCTUnwrap(results) var resultPoint = try XCTUnwrap( result.first?.geoElement?.geometry as? Point @@ -296,7 +296,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() results = try await searchResults(model) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) resultPoint = try XCTUnwrap( result.first?.geoElement?.geometry as? Point @@ -327,7 +327,7 @@ class SearchViewModelTests: XCTestCase { model.repeatSearch() var results = try await searchResults(model) - var result = try XCTUnwrap(results?.get()) + var result = try XCTUnwrap(results) XCTAssertGreaterThan(result.count, 1) let resultGeometryUnion: Geometry = try XCTUnwrap( @@ -348,7 +348,7 @@ class SearchViewModelTests: XCTestCase { model.repeatSearch() results = try await searchResults(model) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertEqual(result.count, 0) model.geoViewExtent = Polygon.minneapolis.extent @@ -356,7 +356,7 @@ class SearchViewModelTests: XCTestCase { model.repeatSearch() results = try await searchResults(model, dropFirst: true) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertEqual(result.count, 1) } @@ -370,7 +370,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() var results = try await searchResults(model) - var result = try XCTUnwrap(results?.get()) + var result = try XCTUnwrap(results) XCTAssertEqual(result.count, 1) model.resultMode = .multiple @@ -378,7 +378,7 @@ class SearchViewModelTests: XCTestCase { model.commitSearch() results = try await searchResults(model, dropFirst: true) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertGreaterThan(result.count, 1) model.currentQuery = "Coffee" @@ -386,7 +386,7 @@ class SearchViewModelTests: XCTestCase { model.updateSuggestions() let suggestionResults = try await searchSuggestions(model) - let suggestions = try XCTUnwrap(suggestionResults?.get()) + let suggestions = try XCTUnwrap(suggestionResults) let collectionSuggestion = try XCTUnwrap(suggestions.filter(\.isCollection).first) let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) @@ -396,13 +396,13 @@ class SearchViewModelTests: XCTestCase { model.acceptSuggestion(collectionSuggestion) results = try await searchResults(model, dropFirst: true) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertGreaterThan(result.count, 1) model.acceptSuggestion(singleSuggestion) results = try await searchResults(model) - result = try XCTUnwrap(results?.get()) + result = try XCTUnwrap(results) XCTAssertEqual(result.count, 1) } @@ -418,7 +418,7 @@ class SearchViewModelTests: XCTestCase { model.updateSuggestions() var suggestionResults = try await searchSuggestions(model) - var suggestions = try XCTUnwrap(suggestionResults?.get()) + var suggestions = try XCTUnwrap(suggestionResults) XCTAssertEqual(suggestions, []) // UpdateSuggestions with results. @@ -427,7 +427,7 @@ class SearchViewModelTests: XCTestCase { model.updateSuggestions() suggestionResults = try await searchSuggestions(model, dropFirst: true) - suggestions = try XCTUnwrap(suggestionResults?.get()) + suggestions = try XCTUnwrap(suggestionResults) XCTAssertGreaterThanOrEqual(suggestions.count, 1) XCTAssertNil(model.selectedResult) @@ -438,7 +438,7 @@ extension SearchViewModelTests { func searchResults( _ model: SearchViewModel, dropFirst: Bool = false - ) async throws -> Result<[SearchResult], SearchError>? { + ) async throws -> [SearchResult]? { let searchOutcome = dropFirst ? try await model.$searchOutcome.compactMap({ $0 }).dropFirst().first : try await model.$searchOutcome.compactMap({ $0 }).first @@ -454,7 +454,7 @@ extension SearchViewModelTests { func searchSuggestions( _ model: SearchViewModel, dropFirst: Bool = false - ) async throws -> Result<[SearchSuggestion], SearchError>? { + ) async throws -> [SearchSuggestion]? { let searchOutcome = dropFirst ? try await model.$searchOutcome.compactMap({ $0 }).dropFirst().first : try await model.$searchOutcome.compactMap({ $0 }).first From ddb769ab2b9c79b7267e6bb679fec5f06b94b356 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Nov 2021 17:48:17 -0600 Subject: [PATCH 0286/1722] PR review changes. --- .../Components/Search/SearchResult.swift | 22 ++-- .../Components/Search/SearchSuggestion.swift | 10 +- .../Components/Search/SearchView.swift | 96 ++++++++------ .../Components/Search/SearchViewModel.swift | 122 ++++++++---------- 4 files changed, 125 insertions(+), 125 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 301217220..8a96eaa63 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -40,7 +40,6 @@ public struct SearchResult { /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. public var selectionViewpoint: Viewpoint? = nil - /// The stable identity of the entity associated with this instance. public let id = UUID() } @@ -68,18 +67,15 @@ extension SearchResult { let subtitle = geocodeResult.attributes["LongLabel"] as? String ?? "Match percent: \((geocodeResult.score / 100.0).formatted(.percent))" - var viewpoint: Viewpoint? = nil - if let extent = geocodeResult.extent { - viewpoint = Viewpoint(targetExtent: extent) - } - - displayTitle = geocodeResult.label - owningSource = searchSource - displaySubtitle = subtitle - geoElement = Graphic( - geometry: geocodeResult.displayLocation, - attributes: geocodeResult.attributes + self.init( + displayTitle: geocodeResult.label, + owningSource: searchSource, + displaySubtitle: subtitle, + geoElement: Graphic( + geometry: geocodeResult.displayLocation, + attributes: geocodeResult.attributes + ), + selectionViewpoint: geocodeResult.extent.map(Viewpoint.init) ) - selectionViewpoint = viewpoint } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 2c01d0d3a..b6766c093 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -60,9 +60,11 @@ extension SearchSuggestion: Hashable { extension SearchSuggestion { init(suggestResult: SuggestResult, searchSource: SearchSource) { - displayTitle = suggestResult.label - owningSource = searchSource - isCollection = suggestResult.isCollection - self.suggestResult = suggestResult + self.init( + displayTitle: suggestResult.label, + owningSource: searchSource, + isCollection: suggestResult.isCollection, + suggestResult: suggestResult + ) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index f21daceab..a910d1137 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -93,32 +93,30 @@ public struct SearchView: View { .onSubmit { searchViewModel.commitSearch() } .submitLabel(.search) - if enableResultListView, showResultListView { - if let searchOutcome = searchViewModel.searchOutcome { - Group { - switch searchOutcome { - case .results(let results): - SearchResultList( - searchResults: results, - selectedResult: $searchViewModel.selectedResult, - noResultsMessage: noResultsMessage - ) - case .suggestions(let suggestions): - SearchSuggestionList( - suggestionResults: suggestions, - currentSuggestion: $searchViewModel.currentSuggestion, - noResultsMessage: noResultsMessage - ) - case .failure(let error): - List { - Text(error.errorDescription) - } + if enableResultListView, + showResultListView, + let searchOutcome = searchViewModel.searchOutcome { + Group { + switch searchOutcome { + case .results(let results): + SearchResultList( + searchResults: results, + selectedResult: $searchViewModel.selectedResult, + noResultsMessage: noResultsMessage + ) + case .suggestions(let suggestions): + SearchSuggestionList( + suggestionResults: suggestions, + currentSuggestion: $searchViewModel.currentSuggestion, + noResultsMessage: noResultsMessage + ) + case .failure(let error): + List { + Text(error.errorDescription) } } - .esriBorder(padding: EdgeInsets()) - } else { - EmptyView() } + .esriBorder(padding: EdgeInsets()) } } .frame(width: searchBarWidth) @@ -146,39 +144,40 @@ public struct SearchView: View { searchViewModel.updateSuggestions() } } - - // MARK: Modifiers - +} + +// MARK: Modifiers +extension SearchView { /// Specifies whether a built-in result view will be shown. If `false`, the result display/selection /// list is not shown. Set to `false` if you want to define a custom result list. You might use a /// custom result list to show results in a separate list, disconnected from the rest of the search view. /// Defaults to `true`. - /// - Parameter enableResultListView: The new value. + /// - Parameter newEnableResultListView: The new value. /// - Returns: A new `SearchView`. - public func enableResultListView(_ enableResultListView: Bool) -> Self { + public func enableResultListView(_ newEnableResultListView: Bool) -> Self { var copy = self - copy.enableResultListView = enableResultListView + copy.enableResultListView = newEnableResultListView return copy } /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". - /// - Parameter searchFieldPrompt: The new value. + /// - Parameter newSearchFieldPrompt: The new value. /// - Returns: A new `SearchView`. - public func searchFieldPrompt(_ searchFieldPrompt: String) -> Self { + public func searchFieldPrompt(_ newSearchFieldPrompt: String) -> Self { var copy = self - copy.searchFieldPrompt = searchFieldPrompt + copy.searchFieldPrompt = newSearchFieldPrompt return copy } /// Sets the message to show when there are no results or suggestions. /// /// The default message is "No results found". - /// - Parameter noResultsMessage: The new value. + /// - Parameter newNoResultsMessage: The new value. /// - Returns: A new `SearchView`. - public func noResultsMessage(_ noResultsMessage: String) -> Self { + public func noResultsMessage(_ newNoResultsMessage: String) -> Self { var copy = self - copy.noResultsMessage = noResultsMessage + copy.noResultsMessage = newNoResultsMessage return copy } } @@ -227,8 +226,8 @@ struct SearchResultList: View { var body: some View { if searchResults.count != 1 { - List { - if searchResults.count > 1 { + if searchResults.count > 1 { + List { // Only show the list if we have more than one result. ForEach(searchResults) { result in HStack { @@ -243,9 +242,9 @@ struct SearchResultList: View { } } } - } else if searchResults.isEmpty { - Text(noResultsMessage) } + } else if searchResults.isEmpty { + NoResultsView(message: noResultsMessage) } } } @@ -257,17 +256,28 @@ struct SearchSuggestionList: View { var noResultsMessage: String var body: some View { - List { - if !suggestionResults.isEmpty { + if !suggestionResults.isEmpty { + List { ForEach(suggestionResults) { suggestion in ResultRow(searchSuggestion: suggestion) .onTapGesture() { currentSuggestion = suggestion } } - } else { - Text(noResultsMessage) } + } else { + NoResultsView(message: noResultsMessage) + } + } +} + +struct NoResultsView: View { + var message: String + + var body: some View { + LazyVStack { + Text(message) + .padding() } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index ac798a9f8..af6ec0ca1 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -183,12 +183,12 @@ public class SearchViewModel: ObservableObject { /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. public func commitSearch() { - kickoffTask({ searchTask() }) + kickoffTask { await self.doSearch() } } /// Repeats the last search, limiting results to the extent specified in `geoViewExtent`. public func repeatSearch() { - kickoffTask({ repeatSearchTask() }) + kickoffTask { await self.doRepeatSearch() } } /// Updates suggestions list asynchronously. @@ -198,7 +198,7 @@ public class SearchViewModel: ObservableObject { return } - kickoffTask({ updateSuggestionsTask() }) + kickoffTask { await self.doUpdateSuggestions() } } @Published @@ -217,82 +217,74 @@ public class SearchViewModel: ObservableObject { /// - searchSuggestion: The suggestion to use to commit the search. public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) { currentQuery = searchSuggestion.displayTitle - kickoffTask({ acceptSuggestionTask(searchSuggestion) }) + kickoffTask { await self.doAcceptSuggestion(searchSuggestion) } } - private func kickoffTask(_ taskInit: () -> Task<(), Never>) { + private func kickoffTask(_ taskInit: @escaping () async -> Void) { currentTask?.cancel() - currentTask = taskInit() + currentTask = Task { await taskInit() } } } private extension SearchViewModel { - func repeatSearchTask() -> Task<(), Never> { - Task { - guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let queryExtent = geoViewExtent, - let source = currentSource() else { - return - } - - await search(with: { - try await source.repeatSearch( - currentQuery, - searchExtent: queryExtent - ) - } ) - } + func doRepeatSearch() async { + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, + let queryExtent = geoViewExtent, + let source = currentSource() else { + return + } + + await search(with: { + try await source.repeatSearch( + currentQuery, + searchExtent: queryExtent + ) + }) } - func searchTask() -> Task<(), Never> { - Task { - guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = currentSource() else { return } - - await search(with: { - try await source.search( - currentQuery, - searchArea: queryArea, - preferredSearchLocation: queryCenter - ) - } ) - } + func doSearch() async { + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, + let source = currentSource() else { return } + + await search(with: { + try await source.search( + currentQuery, + searchArea: queryArea, + preferredSearchLocation: queryCenter + ) + } ) } - func updateSuggestionsTask() -> Task<(), Never> { - Task { - guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = currentSource() else { - return - } - do { - let suggestions = try await source.suggest( - currentQuery, - searchArea: queryArea, - preferredSearchLocation: queryCenter - ) - searchOutcome = .suggestions(suggestions) - } catch is CancellationError { - searchOutcome = nil - } catch { - searchOutcome = .failure(SearchError(error)) - } + func doUpdateSuggestions() async { + guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, + let source = currentSource() else { + return + } + do { + let suggestions = try await source.suggest( + currentQuery, + searchArea: queryArea, + preferredSearchLocation: queryCenter + ) + searchOutcome = .suggestions(suggestions) + } catch is CancellationError { + // Do nothing if user cancelled and let next task set searchOutcome. + } catch { + searchOutcome = .failure(SearchError(error)) } } - func acceptSuggestionTask(_ searchSuggestion: SearchSuggestion) -> Task<(), Never> { - Task { - await search(with: { - try await searchSuggestion.owningSource.search( - searchSuggestion, - searchArea: queryArea, - preferredSearchLocation: queryCenter - ) - } ) - - // once we are done searching for the suggestion, then reset it to nil - currentSuggestion = nil - } + func doAcceptSuggestion(_ searchSuggestion: SearchSuggestion) async { + await search(with: { + try await searchSuggestion.owningSource.search( + searchSuggestion, + searchArea: queryArea, + preferredSearchLocation: queryCenter + ) + } ) + + // once we are done searching for the suggestion, then reset it to nil + currentSuggestion = nil } func search(with action: () async throws -> [SearchResult]) async { From ba5b438a3d2a19a5cca6c2f4b0b67cd9d34ab360 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Nov 2021 11:02:00 -0600 Subject: [PATCH 0287/1722] Push more logic to model; cleanup --- .../Examples/BasemapGalleryExampleView.swift | 12 +- .../BasemapGallery/BasemapGallery.swift | 143 ++++++++---------- .../BasemapGallery/BasemapGalleryItem.swift | 105 ++++++------- .../BasemapGalleryViewModel.swift | 57 ++++--- .../SpatialReferenceMismatchError.swift | 26 ++++ 5 files changed, 186 insertions(+), 157 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index ef3eed252..8a267e9a1 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -15,10 +15,6 @@ import SwiftUI import ArcGIS import ArcGISToolkit -// TODO: once user taps on a map (or maybe in the view model setter for current item) -// TODO: then check if SRs match and don't set basemap if they don't. Figure out -// TODO: how to then gray out the item in the gallery. - struct BasemapGalleryExampleView: View { var basemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( @@ -74,8 +70,8 @@ struct BasemapGalleryExampleView: View { ] let geoModel: GeoModel = Map(basemapStyle: .arcGISNova) -// let geoModel: GeoModel = Scene(basemapStyle: .arcGISNova) - + // let geoModel: GeoModel = Scene(basemapStyle: .arcGISNova) + @ObservedObject var viewModel = BasemapGalleryViewModel() @@ -103,12 +99,12 @@ struct BasemapGalleryExampleView: View { var body: some View { ZStack(alignment: .topTrailing) { MapView(map: geoModel as! Map, viewpoint: initialViewpoint) -// SceneView(scene: geoModel as! ArcGIS.Scene, viewpoint: initialViewpoint) + // SceneView(scene: geoModel as! ArcGIS.Scene, viewpoint: initialViewpoint) .overlay( VStack(alignment: .trailing) { if showBasemapGallery { BasemapGallery(viewModel: viewModel) - .basemapGalleryStyle(.automatic) + .style(.automatic) .frame(width: galleryWidth) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 7051e075d..b8c209df7 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -21,7 +21,7 @@ import ArcGIS /// with the basemap in the gallery. public struct BasemapGallery: View { /// The view style of the gallery. - public enum BasemapGalleryStyle { + public enum Style { /// The `BasemapGallery` will display as a grid when there is appropriate /// width available for the gallery to do so. Otherwise the gallery will display as a list. case automatic @@ -51,8 +51,8 @@ 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 `automatic`. - /// Set using the `basemapGalleryStyle` modifier. - private var style: BasemapGalleryStyle = .automatic + /// Set using the `style` modifier. + private var style: Style = .automatic /// The size class used to determine if the basemap items should dispaly in a list or grid. /// If the size class is `.regular`, they display in a grid. If it is `.compact`, they display in a list. @@ -63,6 +63,13 @@ public struct BasemapGallery: View { public var body: some View { GalleryView() + .onReceive(viewModel.$spatialReferenceMismatchError, perform: { error in + guard let error = error else { return } + alertItem = AlertItem( + basemapSR: error.basemapSR, + geoModelSR: error.geoModelSR + ) + }) .alert(item: $alertItem) { alertItem in Alert( title: Text(alertItem.title), @@ -70,22 +77,26 @@ public struct BasemapGallery: View { ) } } - - // MARK: Modifiers - +} + +// MARK: Modifiers + +public extension BasemapGallery { /// The style of the basemap gallery. Defaults to `.automatic`. - /// - Parameter style: The `BasemapGalleryStyle` to use. + /// - Parameter style: The `Style` to use. /// - Returns: The `BasemapGallery`. - public func basemapGalleryStyle(_ style: BasemapGalleryStyle) -> BasemapGallery { + func style( + _ newStyle: Style + ) -> BasemapGallery { var copy = self - copy.style = style + copy.style = newStyle return copy } } -extension BasemapGallery { +private extension BasemapGallery { @ViewBuilder - private func GalleryView() -> some View { + func GalleryView() -> some View { switch style { case .automatic: if horizontalSizeClass == .regular { @@ -101,7 +112,7 @@ extension BasemapGallery { } } - private func GridView() -> some View { + func GridView() -> some View { let columns: [GridItem] = [ .init(.flexible(), spacing: 8.0, alignment: .top), .init(.flexible(), spacing: 8.0, alignment: .top), @@ -111,7 +122,7 @@ extension BasemapGallery { return InternalGalleryView(columns) } - private func ListView() -> some View { + func ListView() -> some View { let columns: [GridItem] = [ .init(.flexible(), spacing: 8.0, alignment: .top) ] @@ -119,7 +130,7 @@ extension BasemapGallery { return InternalGalleryView(columns) } - private func InternalGalleryView(_ columns: [GridItem]) -> some View { + func InternalGalleryView(_ columns: [GridItem]) -> some View { return ScrollView { LazyVGrid(columns: columns, spacing: 4) { ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in @@ -128,34 +139,11 @@ extension BasemapGallery { isSelected: basemapGalleryItem == viewModel.currentBasemapGalleryItem ) .onTapGesture { - // Don't check spatial reference until user taps on it. - // At this point, we need to get errors from setting the basemap (in the model?). - // Error in the model, displayed in the gallery. - - // TODO: this doesn't work until the basemap is tapped on once, then I assume - // TODO: it's loaded the basemap layers. Figure this out. (load base layers when bm loads?) if let loadError = basemapGalleryItem.loadBasemapsError { - alertItem = AlertItem(error: loadError) - print("basemaps DON'T match (or error)!") + alertItem = AlertItem(loadBasemapError: loadError) } else { - // if !showingBasemapLoadError, - // basemapGalleryItem.matchesSpatialReference(viewModel.geoModel?.spatialReference) { - Task { - try await basemapGalleryItem.updateSpatialReferenceStatus(for: viewModel.geoModel?.spatialReference) - if basemapGalleryItem.spatialReferenceStatus == .match || - basemapGalleryItem.spatialReferenceStatus == .unknown { - print("basemap matches!") - viewModel.currentBasemapGalleryItem = basemapGalleryItem - } - else { - alertItem = AlertItem( - basemapSR: basemapGalleryItem.spatialReference, - geoModelSR: viewModel.geoModel?.spatialReference - ) - print("Task bm don't match") - } - } + viewModel.updateCurrentBasemapGalleryItem(basemapGalleryItem) } } } @@ -170,40 +158,38 @@ private struct BasemapGalleryItemRow: View { let isSelected: Bool var body: some View { - ZStack { - VStack { - ZStack(alignment: .center) { - if let thumbnailImage = basemapGalleryItem.thumbnail { - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .border( - isSelected ? Color.accentColor: Color.clear, - width: 3.0) - } + VStack { + ZStack(alignment: .center) { + if let thumbnailImage = basemapGalleryItem.thumbnail { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .border( + isSelected ? Color.accentColor: Color.clear, + width: 3.0) + } + + if basemapGalleryItem.loadBasemapsError != nil { + Image(systemName: "minus.circle.fill") + .font(.title) + .foregroundColor(.red) + } else if basemapGalleryItem.spatialReferenceStatus == .noMatch { + Image(systemName: "x.circle.fill") + .font(.title) + .foregroundColor(.red) + } + if basemapGalleryItem.isLoading { + Spacer() + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .esriBorder() + Spacer() - if basemapGalleryItem.loadBasemapsError != nil { - Image(systemName: "minus.circle.fill") - .font(.title) - .foregroundColor(.red) - } else if basemapGalleryItem.spatialReferenceStatus == .noMatch { - Image(systemName: "x.circle.fill") - .font(.title) - .foregroundColor(.red) - } - if basemapGalleryItem.isLoading { - Spacer() - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .esriBorder() - Spacer() - - } } - Text(basemapGalleryItem.name) - .font(.footnote) - .multilineTextAlignment(.center) } + Text(basemapGalleryItem.name) + .font(.footnote) + .multilineTextAlignment(.center) } .allowsHitTesting( !basemapGalleryItem.isLoading @@ -220,21 +206,24 @@ extension AlertItem: Identifiable { public var id: UUID { UUID() } } -// TODO: add SR for basemap, if possible (SR property on basemap?) Maybe that can speed up baselayer sr checking... -// TODO: Cleanup all .tapGesture code, alert code, old error/alert stuff -// TODO: Figure out common errors, so I don't need to rely on `Error` or `RuntimeError`. extension AlertItem { + /// Creates an alert item based on a spatial reference mismatch. + /// - Parameters: + /// - basemapSR: The basemap's spatial reference. + /// - geoModelSR: The geomodel's spatial reference. init(basemapSR: SpatialReference?, geoModelSR: SpatialReference?) { self.init( - title: "Spatial Reference mismatch.", - message: "The spatial reference of the basemap \(basemapSR?.description ?? "") does not match that of the GeoModel \(geoModelSR?.description ?? "")." + title: "Spatial reference mismatch.", + message: "The spatial reference of the basemap: \(basemapSR?.description ?? "") does not match that of the geomodel: \(geoModelSR?.description ?? "")." ) } - init(error: Error) { + /// Creates an alert item based on an error generated loading a basemap. + /// - Parameter loadBasemapError: The load basemap error. + init(loadBasemapError: RuntimeError) { self.init( title: "Error loading basemap.", - message: "\(error.localizedDescription)" + message: "\(loadBasemapError.failureReason ?? "")" ) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 19ce48491..3537604a1 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -51,66 +51,63 @@ public class BasemapGalleryItem: ObservableObject { self.thumbnailOverride = thumbnail self.thumbnail = thumbnail - loadBasemapTask = Task { await loadBasemap() } + Task { await loadBasemap() } } - /// The error generated loading the basemap, if any. @Published - public var loadBasemapsError: Error? = nil + /// The error generated loading the basemap, if any. + public private(set) var loadBasemapsError: RuntimeError? = nil /// The basemap this `BasemapGalleryItem` represents. public private(set) var basemap: Basemap - /// The name of the `basemap`. @Published + /// The name of the `basemap`. public private(set) var name: String = "" private var nameOverride: String? = nil - /// The description of the `basemap`. @Published + /// The description of the `basemap`. public private(set) var description: String? = nil private var descriptionOverride: String? = nil - /// The thumbnail used to represent the `basemap`. @Published + /// The thumbnail used to represent the `basemap`. public private(set) var thumbnail: UIImage? = nil private var thumbnailOverride: UIImage? = nil - /// Denotes whether the `basemap` or it's base layers are being loaded. @Published + /// Denotes whether the `basemap` or it's base layers are being loaded. public private(set) var isLoading = true - /// The `SpatialReferenceStatus` of the item. This is set via a call to - /// `updateSpatialReferenceStatus(for:)` @Published + /// The `SpatialReferenceStatus` of the item. This is set via a call to + /// `updateSpatialReferenceStatus()` public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's - /// baseLayers have been loaded in `updateSpatialReferenceStatus(for:)`. + /// baseLayers have been loaded in `updateSpatialReferenceStatus()`. public private(set) var spatialReference: SpatialReference? = nil - - /// The currently executing `Task` for loading the basemap. - private var loadBasemapTask: Task? = nil } private extension BasemapGalleryItem { func loadBasemap() async { - var loadError: Error? = nil + var loadError: RuntimeError? = nil do { try await basemap.load() if let loadableImage = basemap.item?.thumbnail { try await loadableImage.load() } - } catch { - loadError = error + } catch { + loadError = error as? RuntimeError } - await update(error: loadError) + await finalizeLoading(error: loadError) } @MainActor /// Updates the item in response to basemap loading completion. /// - Parameter error: The basemap load error, if any. - func update(error: Error?) { + func finalizeLoading(error: RuntimeError?) { name = nameOverride ?? basemap.name description = descriptionOverride ?? basemap.item?.description thumbnail = thumbnailOverride ?? @@ -125,58 +122,56 @@ private extension BasemapGalleryItem { /// - Parameter referenceSpatialReference: The `SpatialReference` used to /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` /// `SpatialReference`. - func update(with referenceSpatialReference: SpatialReference) { + func finalizeUpdateSpatialReferenceStatus( + with referenceSpatialReference: SpatialReference? + ) { spatialReference = basemap.baseLayers.first?.spatialReference - spatialReferenceStatus = matches(referenceSpatialReference) ? .match : .noMatch + if referenceSpatialReference == nil { + spatialReferenceStatus = .unknown + } + else if spatialReference == referenceSpatialReference { + spatialReferenceStatus = .match + } + else { + spatialReferenceStatus = .noMatch + } isLoading = false } } -extension BasemapGalleryItem: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(self) } -} - -extension BasemapGalleryItem: Equatable { - public static func == (lhs: BasemapGalleryItem, rhs: BasemapGalleryItem) -> Bool { - lhs.basemap === rhs.basemap && - lhs.name == rhs.name && - lhs.description == rhs.description && - lhs.thumbnail === rhs.thumbnail - } -} - -extension BasemapGalleryItem { +public extension BasemapGalleryItem { /// Loads the first base layer of `basemap` and determines if it matches /// `referenceSpatialReference`, setting the `spatialReferenceStatus` /// property appropriately. /// - Parameter referenceSpatialReference: The `SpatialReference to match to`. - public func updateSpatialReferenceStatus( - for referenceSpatialReference: SpatialReference? + func updateSpatialReferenceStatus( + _ spatialReference: SpatialReference? ) async throws { - guard let spatialReference = referenceSpatialReference, - basemap.loadStatus == .loaded, + guard basemap.loadStatus == .loaded, self.spatialReference == nil - else { return } + else { + return + } - isLoading = true + await MainActor.run { + isLoading = true + } try await basemap.baseLayers.first?.load() - await update(with: spatialReference) + await finalizeUpdateSpatialReferenceStatus(with: spatialReference) } - - /// Determines if the basemap spatial reference matches `spatialReference`. - /// - Parameter spatialReference: The `SpatialReference` to match against. - /// - Returns: `true` if the basemap spatial reference matches `spatialReference`, - /// `false` if they don't match. - private func matches(_ spatialReference: SpatialReference) -> Bool { - for baselayer in basemap.baseLayers { - if let baseLayerSR = baselayer.spatialReference, - baseLayerSR != spatialReference { - return false - } - } - - return true +} + +extension BasemapGalleryItem: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(self) } +} + +extension BasemapGalleryItem: Equatable { + public static func == (lhs: BasemapGalleryItem, rhs: BasemapGalleryItem) -> Bool { + lhs.basemap === rhs.basemap && + lhs.name == rhs.name && + lhs.description == rhs.description && + lhs.thumbnail === rhs.thumbnail } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index c000bb09e..92455f33f 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -52,30 +52,18 @@ public class BasemapGalleryViewModel: ObservableObject { /// the geoModel will have its basemap replaced with the selected basemap. public var geoModel: GeoModel? { didSet { - // Note that we don't want to store this task and cancel it - // before kicking off another operation because it could have been - // started elsewhere as well as here. Canceling it here would also - // cancel those other operations, which we don't want to do. Task { await load(geoModel: geoModel) } } } - /// The `Portal` object, if any. Setting the portal will automatically fetch it's base maps + /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps /// and add them to the `basemapGalleryItems` array. public var portal: Portal? { didSet { - // Note that we don't want to store this task and cancel it - // before kicking off another operation because it could have been - // started elsewhere as well as here. Canceling it here would also - // cancel those other operations, which we don't want to do. Task { await fetchBasemaps(from: portal) } } } - // TODO: what does this mean that it is a public var? - // TODO: what happens if the user sets this after things are loaded - // TODO: test this... - /// The list of basemaps currently visible in the gallery. It is comprised of items passed into /// the `BasemapGalleryItem` constructor and items loaded from the `Portal`. @Published @@ -84,12 +72,49 @@ public class BasemapGalleryViewModel: ObservableObject { /// `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a /// basemap which does not exist in the gallery. @Published - public var currentBasemapGalleryItem: BasemapGalleryItem? = nil { + public private(set) var currentBasemapGalleryItem: BasemapGalleryItem? = nil { didSet { guard let item = currentBasemapGalleryItem else { return } geoModel?.basemap = item.basemap } } + + @Published + /// The error signifying the spatial reference of the GeoModel and that of a potential + /// current `BasemapGalleryItem` do not match. + public private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil + + /// This attempts to set `currentBasemapGalleryItem`. `currentBasemapGalleryItem` + /// will be set if it's spatialReference matches that of the current geoModel. If the spatialReferences + /// do not match, `currentBasemapGalleryItem` will be unchanged. + /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. + public func updateCurrentBasemapGalleryItem(_ basemapGalleryItem: BasemapGalleryItem) { + Task { + try await basemapGalleryItem.updateSpatialReferenceStatus( + geoModel?.actualSpatialReference + ) + await MainActor.run { + if basemapGalleryItem.spatialReferenceStatus == .match || + basemapGalleryItem.spatialReferenceStatus == .unknown { + currentBasemapGalleryItem = basemapGalleryItem + } + else { + spatialReferenceMismatchError = SpatialReferenceMismatchError( + basemapSR: basemapGalleryItem.spatialReference, + geoModelSR: geoModel?.actualSpatialReference + ) + } + } + } + } +} + +private extension GeoModel { + var actualSpatialReference: SpatialReference? { + (self as? ArcGIS.Scene)?.sceneViewTilingScheme == .webMercator ? + SpatialReference.webMercator : + spatialReference + } } private extension BasemapGalleryViewModel { @@ -116,8 +141,6 @@ private extension BasemapGalleryViewModel { else { currentBasemapGalleryItem = nil } - } catch { - // We don't need any errors, as that should be handled by the app. - } + } catch { } } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift new file mode 100644 index 000000000..edfa4197f --- /dev/null +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift @@ -0,0 +1,26 @@ +// Copyright 2021 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 Foundation +import ArcGIS + +/// A value that represents a SpatialReference mismatch. +public struct SpatialReferenceMismatchError: Error { + /// The basemap's spatial reference + public let basemapSR: SpatialReference? + + /// The geomodel's spatial reference + public let geoModelSR: SpatialReference? +} + +extension SpatialReferenceMismatchError: Equatable {} From fdb825664c04293b5298305f3c20dad5fbe3cee5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Nov 2021 11:08:32 -0600 Subject: [PATCH 0288/1722] Cleanup; move modifiers after property/method doc. --- .../BasemapGallery/BasemapGalleryItem.swift | 16 ++++++------ .../BasemapGalleryViewModel.swift | 15 +++++++++-- .../SpatialReferenceMismatchError.swift | 26 ------------------- 3 files changed, 21 insertions(+), 36 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 3537604a1..a0a83f865 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -54,35 +54,35 @@ public class BasemapGalleryItem: ObservableObject { Task { await loadBasemap() } } - @Published /// The error generated loading the basemap, if any. + @Published public private(set) var loadBasemapsError: RuntimeError? = nil /// The basemap this `BasemapGalleryItem` represents. public private(set) var basemap: Basemap - @Published /// The name of the `basemap`. + @Published public private(set) var name: String = "" private var nameOverride: String? = nil - @Published /// The description of the `basemap`. + @Published public private(set) var description: String? = nil private var descriptionOverride: String? = nil - @Published /// The thumbnail used to represent the `basemap`. + @Published public private(set) var thumbnail: UIImage? = nil private var thumbnailOverride: UIImage? = nil - @Published /// Denotes whether the `basemap` or it's base layers are being loaded. + @Published public private(set) var isLoading = true - @Published /// The `SpatialReferenceStatus` of the item. This is set via a call to /// `updateSpatialReferenceStatus()` + @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's @@ -104,9 +104,9 @@ private extension BasemapGalleryItem { await finalizeLoading(error: loadError) } - @MainActor /// Updates the item in response to basemap loading completion. /// - Parameter error: The basemap load error, if any. + @MainActor func finalizeLoading(error: RuntimeError?) { name = nameOverride ?? basemap.name description = descriptionOverride ?? basemap.item?.description @@ -117,11 +117,11 @@ private extension BasemapGalleryItem { isLoading = false } - @MainActor /// Updates the item's `spatialReference` and `spatialReferenceStatus` properties. /// - Parameter referenceSpatialReference: The `SpatialReference` used to /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` /// `SpatialReference`. + @MainActor func finalizeUpdateSpatialReferenceStatus( with referenceSpatialReference: SpatialReference? ) { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 92455f33f..4971ac000 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -42,8 +42,8 @@ public class BasemapGalleryViewModel: ObservableObject { Task { await fetchBasemaps(from: portal) } } - @Published /// The error generated by fetching the `Basemaps` from the `Portal`. + @Published public var fetchBasemapsError: Error? = nil /// If the `GeoModel` is not loaded when passed to the `BasemapGalleryViewModel`, then @@ -79,9 +79,9 @@ public class BasemapGalleryViewModel: ObservableObject { } } - @Published /// The error signifying the spatial reference of the GeoModel and that of a potential /// current `BasemapGalleryItem` do not match. + @Published public private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil /// This attempts to set `currentBasemapGalleryItem`. `currentBasemapGalleryItem` @@ -144,3 +144,14 @@ private extension BasemapGalleryViewModel { } catch { } } } + +/// A value that represents a SpatialReference mismatch. +public struct SpatialReferenceMismatchError: Error { + /// The basemap's spatial reference + public let basemapSR: SpatialReference? + + /// The geomodel's spatial reference + public let geoModelSR: SpatialReference? +} + +extension SpatialReferenceMismatchError: Equatable {} diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift deleted file mode 100644 index edfa4197f..000000000 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/SpatialReferenceMismatchError.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 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 Foundation -import ArcGIS - -/// A value that represents a SpatialReference mismatch. -public struct SpatialReferenceMismatchError: Error { - /// The basemap's spatial reference - public let basemapSR: SpatialReference? - - /// The geomodel's spatial reference - public let geoModelSR: SpatialReference? -} - -extension SpatialReferenceMismatchError: Equatable {} From 8a7be7948fd9895331a8d857d9a51213321c286a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Nov 2021 16:32:39 -0600 Subject: [PATCH 0289/1722] updates for displaying error alert --- .../Components/BasemapGallery/BasemapGallery.swift | 2 +- .../BasemapGallery/BasemapGalleryViewModel.swift | 2 ++ .../DefaultBasemap.imageset/Contents.json | 0 .../generic_basemap_thumbnail.jpg | Bin 4 files changed, 3 insertions(+), 1 deletion(-) rename {Examples/ExamplesApp/Assets.xcassets => Sources/ArcGISToolkit/Resources/Images.xcassets}/DefaultBasemap.imageset/Contents.json (100%) rename {Examples/ExamplesApp/Assets.xcassets => Sources/ArcGISToolkit/Resources/Images.xcassets}/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg (100%) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index b8c209df7..dd2f96f20 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -63,7 +63,7 @@ public struct BasemapGallery: View { public var body: some View { GalleryView() - .onReceive(viewModel.$spatialReferenceMismatchError, perform: { error in + .onReceive(viewModel.$spatialReferenceMismatchError.dropFirst(), perform: { error in guard let error = error else { return } alertItem = AlertItem( basemapSR: error.basemapSR, diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 4971ac000..3e82282df 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -90,6 +90,8 @@ public class BasemapGalleryViewModel: ObservableObject { /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. public func updateCurrentBasemapGalleryItem(_ basemapGalleryItem: BasemapGalleryItem) { Task { + // Reset the mismatch error. + spatialReferenceMismatchError = nil try await basemapGalleryItem.updateSpatialReferenceStatus( geoModel?.actualSpatialReference ) diff --git a/Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/Contents.json similarity index 100% rename from Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/Contents.json rename to Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/Contents.json diff --git a/Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg b/Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg similarity index 100% rename from Examples/ExamplesApp/Assets.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg rename to Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg From 3cb4be03207c6ffe990334ae13432a8477f86a18 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 09:22:01 -0600 Subject: [PATCH 0290/1722] Cleanup and more tests. --- .../Examples/BasemapGalleryExampleView.swift | 3 - .../BasemapGallery/BasemapGalleryItem.swift | 2 +- .../BasemapGalleryItemTests.swift | 70 ++++++++++++++++--- .../BasemapGalleryViewModelTests.swift | 58 ++++++++++++++- 4 files changed, 119 insertions(+), 14 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 8a267e9a1..620ab57b0 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -25,7 +25,6 @@ struct BasemapGalleryExampleView: View { ), name: "OpenStreetMap (Blueprint)", description: "OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. This vector basemap is based on the Daylight map distribution of OSM data and is hosted by Esri. It presents the map in a cartographic style is like a blueprint technical drawing.", - thumbnail: nil ), BasemapGalleryItem( basemap: Basemap( @@ -35,7 +34,6 @@ struct BasemapGalleryExampleView: View { ), name: "National Geographic Style Map", description: "This vector web map provides a detailed view of the world featuring beautiful political boundaries, labeling, and background that highlights the differences in the physical characteristics of the land.", - thumbnail: nil ), BasemapGalleryItem( basemap: Basemap( @@ -45,7 +43,6 @@ struct BasemapGalleryExampleView: View { ), name: "Firefly Imagery Hybrid", description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer.", - thumbnail: nil ), BasemapGalleryItem( basemap: Basemap( diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index a0a83f865..77f2a7229 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -178,6 +178,6 @@ extension BasemapGalleryItem: Equatable { private extension UIImage { /// A search result marker symbol. static func defaultThumbnail() -> UIImage { - return UIImage(named: "DefaultBasemap")! + return UIImage(named: "DefaultBasemap", in: Bundle.module, with: nil)! } } diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 1fb428bbf..73524ebcc 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -22,25 +22,79 @@ import Combine @MainActor class BasemapGalleryItemTests: XCTestCase { // - // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/Search/Search_Test_Design.md + // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md // func testInit() async throws { + let basemap = Basemap.lightGrayCanvas() + var item = BasemapGalleryItem(basemap: basemap) + + var isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first + var loading = try XCTUnwrap(isLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertTrue(item.basemap === basemap) + XCTAssertEqual(item.name, "Light Gray Canvas") + XCTAssertNil(item.description) + XCTAssertNotNil(item.thumbnail) + XCTAssertNil(item.loadBasemapsError) + + // Test with overrides. + let thumbnail = UIImage(systemName: "magnifyingglass") + XCTAssertNotNil(thumbnail) + item = BasemapGalleryItem( + basemap: basemap, + name: "My Basemap", + description: "Basemap description", + thumbnail: thumbnail + ) - } - - func testPortal() async throws { + isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first + loading = try XCTUnwrap(isLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertEqual(item.name, "My Basemap") + XCTAssertEqual(item.description, "Basemap description") + XCTAssertEqual(item.thumbnail, thumbnail) + XCTAssertNil(item.loadBasemapsError) + + // Test with portal item. + item = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ) + ) + isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first + loading = try XCTUnwrap(isLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertEqual(item.name, "OpenStreetMap Blueprint") + XCTAssertEqual(item.description, "

This web map presents a vector basemap of OpenStreetMap (OSM) data hosted by Esri. Esri created this vector tile basemap from the Daylight map distribution of OSM data, which is supported by Facebook and supplemented with additional data from Microsoft. It presents the map in a cartographic style is like a blueprint technical drawing. The OSM Daylight map will be updated every month with the latest version of OSM Daylight data. 

OpenStreetMap is an open collaborative project to create a free editable map of the world. Volunteers gather location data using GPS, local knowledge, and other free sources of information and upload it. The resulting free map can be viewed and downloaded from the OpenStreetMap site: www.OpenStreetMap.org. Esri is a supporter of the OSM project and is excited to make this enhanced vector basemap available to the ArcGIS user and developer communities.
") + XCTAssertNotNil(item.thumbnail) + XCTAssertNil(item.loadBasemapsError) } - func testInitialBasemapItems() async throws { - + func testLoadBasemapError() async throws { + let item = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! + )! + ) + ) + + let isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first + let loading = try XCTUnwrap(isLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertNotNil(item.loadBasemapsError) } - func testGeoModel() async throws { + func testSpatialReferenceStatus() async throws { + let basemap = Basemap.lightGrayCanvas() + let item = BasemapGalleryItem(basemap: basemap) } - func testCurrentBasemapItem() async throws { + func testSpatialReference() async throws { } diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 8a43eb57f..164cef379 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -21,11 +21,65 @@ import Combine @MainActor class BasemapGalleryViewModelTests: XCTestCase { + var basemapGalleryItems: [BasemapGalleryItem] = [ + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ), + name: "OpenStreetMap (Blueprint)", + description: "OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. This vector basemap is based on the Daylight map distribution of OSM data and is hosted by Esri. It presents the map in a cartographic style is like a blueprint technical drawing.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! + )! + ), + name: "National Geographic Style Map", + description: "This vector web map provides a detailed view of the world featuring beautiful political boundaries, labeling, and background that highlights the differences in the physical characteristics of the land.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! + )! + ), + name: "Firefly Imagery Hybrid", + description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ), + name: nil, + description: "This web map features satellite imagery for the world and high-resolution aerial imagery for many areas. It uses WGS84 Geographic, version 2 tiling scheme.", + thumbnail: nil + ), + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! + )! + ), + name: "Human Geography Dark Label", + description: "This (v2) vector tile layer provides a detailed basemap for the world, featuring a dark monochromatic style with content adjusted to support Human Geography information. This map is designed for use with Human Geography Dark Detail and Base layers.", + thumbnail: nil + ), + ] + + + // - // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/Search/Search_Test_Design.md + // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md // func testInit() async throws { - } func testPortal() async throws { From 0d5d445f9c9f3d582603cc1ae48ab2fced79b177 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:21:14 -0600 Subject: [PATCH 0291/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a910d1137..9ae89eb30 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -41,7 +41,7 @@ public struct SearchView: View { var searchViewModel: SearchViewModel /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. - private var viewpoint: Binding? = nil + private let viewpoint: Binding? /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. private var resultsOverlay: GraphicsOverlay? = nil From eae963e3109791b0c19b9e1c702f5c788a7f013d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:21:26 -0600 Subject: [PATCH 0292/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 9ae89eb30..d4567b97a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -44,7 +44,7 @@ public struct SearchView: View { private let viewpoint: Binding? /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. - private var resultsOverlay: GraphicsOverlay? = nil + private let resultsOverlay: GraphicsOverlay? /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". Note: this is set using the From dad61ecf7ee83440ab55455330749a280bd730c4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:21:36 -0600 Subject: [PATCH 0293/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index af6ec0ca1..1174e0a97 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -80,7 +80,6 @@ public class SearchViewModel: ObservableObject { } default: self.searchOutcome = nil - break } } } From 49a637827b25ba44e20ca27c7799df57e1213eac Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:21:46 -0600 Subject: [PATCH 0294/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 1174e0a97..bb71f8964 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -280,7 +280,7 @@ private extension SearchViewModel { searchArea: queryArea, preferredSearchLocation: queryCenter ) - } ) + }) // once we are done searching for the suggestion, then reset it to nil currentSuggestion = nil From 2491afac8cae35dc35d7f09a31216b63495daa09 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:22:01 -0600 Subject: [PATCH 0295/1722] Update Sources/ArcGISToolkit/Utility/SearchField.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Utility/SearchField.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index b90bf4fe1..50325cadf 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -16,7 +16,7 @@ import SwiftUI /// A custom view implementing a SearchField. It contains a search button, text field, delete text button, /// and a button to allow users to hide/show the search results list. public struct SearchField: View { - internal init( + init( query: Binding, searchFieldPrompt: String = "", isShowResultsHidden: Bool = true, From 69d0a8f524c1bbebfacbd0d3c93f1ba53e33f0c1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:22:38 -0600 Subject: [PATCH 0296/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index c6526d636..1a77ae871 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -439,9 +439,10 @@ extension SearchViewModelTests { _ model: SearchViewModel, dropFirst: Bool = false ) async throws -> [SearchResult]? { - let searchOutcome = dropFirst ? - try await model.$searchOutcome.compactMap({ $0 }).dropFirst().first : - try await model.$searchOutcome.compactMap({ $0 }).first + let searchOutcome = try await model.$searchOutcome + .compactMap { $0 } + .dropFirst(dropFirst ? 1 : 0) + .first switch searchOutcome { case .results(let results): From f4b38a2397d5920107b8f796db152db0ff13e198 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:22:47 -0600 Subject: [PATCH 0297/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 1a77ae871..2413d2118 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -456,9 +456,10 @@ extension SearchViewModelTests { _ model: SearchViewModel, dropFirst: Bool = false ) async throws -> [SearchSuggestion]? { - let searchOutcome = dropFirst ? - try await model.$searchOutcome.compactMap({ $0 }).dropFirst().first : - try await model.$searchOutcome.compactMap({ $0 }).first + let searchOutcome = try await model.$searchOutcome + .compactMap { $0 } + .dropFirst(dropFirst ? 1 : 0) + .first switch searchOutcome { case .suggestions(let suggestions): From f6d770e08ca804265b2b00a0429a8dae666c4c8d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:23:04 -0600 Subject: [PATCH 0298/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 2413d2118..ca52dac5c 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -471,14 +471,14 @@ extension SearchViewModelTests { } extension Polygon { - static var chippewaFalls: Polygon { + static let chippewaFalls: Polygon = { let builder = PolygonBuilder(spatialReference: .wgs84) let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) return builder.toGeometry() as! ArcGIS.Polygon - } + }() static var minneapolis: Polygon { let builder = PolygonBuilder(spatialReference: .wgs84) From 7a16f55aed2f0dec8da6248e051d28a74a15a120 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:23:14 -0600 Subject: [PATCH 0299/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index ca52dac5c..a2137bdc1 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -480,14 +480,14 @@ extension Polygon { return builder.toGeometry() as! ArcGIS.Polygon }() - static var minneapolis: Polygon { + static let minneapolis: Polygon = { let builder = PolygonBuilder(spatialReference: .wgs84) let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) return builder.toGeometry() as! ArcGIS.Polygon - } + }() } extension Point { From ada9679747d1d5de9949d42e73a8f82b6a4b24b8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:23:23 -0600 Subject: [PATCH 0300/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Philip Ridgeway --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index a2137bdc1..11f05a3b5 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -494,4 +494,3 @@ 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) } - From 5dab3126207b321f28496adeead3081c98225cd7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 10:43:17 -0600 Subject: [PATCH 0301/1722] replace "let _" with just "_". --- .../SearchViewModelTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 11f05a3b5..fc67d0747 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -473,19 +473,19 @@ extension SearchViewModelTests { extension Polygon { static let chippewaFalls: Polygon = { let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) return builder.toGeometry() as! ArcGIS.Polygon }() static let minneapolis: Polygon = { let builder = PolygonBuilder(spatialReference: .wgs84) - let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) + _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) return builder.toGeometry() as! ArcGIS.Polygon }() } From de40d97ce074636e41ab82d4725202db4b3f3633 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Nov 2021 15:40:05 -0600 Subject: [PATCH 0302/1722] Update tests. --- .../Examples/BasemapGalleryExampleView.swift | 8 +- .../BasemapGallery/BasemapGalleryItem.swift | 16 +- .../BasemapGalleryViewModel.swift | 5 +- .../BasemapGalleryItemTests.swift | 507 ++------------- .../BasemapGalleryViewModelTests.swift | 577 +++--------------- 5 files changed, 153 insertions(+), 960 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 620ab57b0..bacd88836 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -24,7 +24,7 @@ struct BasemapGalleryExampleView: View { )! ), name: "OpenStreetMap (Blueprint)", - description: "OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. This vector basemap is based on the Daylight map distribution of OSM data and is hosted by Esri. It presents the map in a cartographic style is like a blueprint technical drawing.", + description: "OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. This vector basemap is based on the Daylight map distribution of OSM data and is hosted by Esri. It presents the map in a cartographic style is like a blueprint technical drawing." ), BasemapGalleryItem( basemap: Basemap( @@ -33,7 +33,7 @@ struct BasemapGalleryExampleView: View { )! ), name: "National Geographic Style Map", - description: "This vector web map provides a detailed view of the world featuring beautiful political boundaries, labeling, and background that highlights the differences in the physical characteristics of the land.", + description: "This vector web map provides a detailed view of the world featuring beautiful political boundaries, labeling, and background that highlights the differences in the physical characteristics of the land." ), BasemapGalleryItem( basemap: Basemap( @@ -42,7 +42,7 @@ struct BasemapGalleryExampleView: View { )! ), name: "Firefly Imagery Hybrid", - description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer.", + description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer." ), BasemapGalleryItem( basemap: Basemap( @@ -63,7 +63,7 @@ struct BasemapGalleryExampleView: View { name: "Human Geography Dark Label", description: "This (v2) vector tile layer provides a detailed basemap for the world, featuring a dark monochromatic style with content adjusted to support Human Geography information. This map is designed for use with Human Geography Dark Detail and Base layers.", thumbnail: nil - ), + ) ] let geoModel: GeoModel = Map(basemapStyle: .arcGISNova) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 77f2a7229..c4c201408 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -147,16 +147,14 @@ public extension BasemapGalleryItem { func updateSpatialReferenceStatus( _ spatialReference: SpatialReference? ) async throws { - guard basemap.loadStatus == .loaded, - self.spatialReference == nil - else { - return - } - - await MainActor.run { - isLoading = true + guard basemap.loadStatus == .loaded else { return } + + if self.spatialReference == nil { + await MainActor.run { + isLoading = true + } + try await basemap.baseLayers.first?.load() } - try await basemap.baseLayers.first?.load() await finalizeUpdateSpatialReferenceStatus(with: spatialReference) } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 3e82282df..9a1c98fe0 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -90,6 +90,9 @@ public class BasemapGalleryViewModel: ObservableObject { /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. public func updateCurrentBasemapGalleryItem(_ basemapGalleryItem: BasemapGalleryItem) { Task { + // Ensure the geoModel is loaded. + try await geoModel?.load() + // Reset the mismatch error. spatialReferenceMismatchError = nil try await basemapGalleryItem.updateSpatialReferenceStatus( @@ -111,7 +114,7 @@ public class BasemapGalleryViewModel: ObservableObject { } } -private extension GeoModel { +internal extension GeoModel { var actualSpatialReference: SpatialReference? { (self as? ArcGIS.Scene)?.sceneViewTilingScheme == .webMercator ? SpatialReference.webMercator : diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 73524ebcc..3040f2366 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -88,468 +88,63 @@ class BasemapGalleryItemTests: XCTestCase { XCTAssertNotNil(item.loadBasemapsError) } - func testSpatialReferenceStatus() async throws { + func testSpatialReferenceAndStatus() async throws { let basemap = Basemap.lightGrayCanvas() let item = BasemapGalleryItem(basemap: basemap) - - } - - func testSpatialReference() async throws { - - } - - /* + var isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first + var loading = try XCTUnwrap(isLoading) + XCTAssertFalse(loading, "Item is not loading.") - - - func testAcceptSuggestion() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.updateSuggestions() } - - // Get suggestion - let suggestions = try await model.$suggestions.compactMap({$0}).first - let suggestion = try XCTUnwrap(suggestions?.get().first) - - Task { model.acceptSuggestion(suggestion) } - - let results = try await model.$results.compactMap({$0}).first - let result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - XCTAssertNil(model.suggestions) - - // With only one results, model should set `selectedResult` property. - XCTAssertEqual(result.first!, model.selectedResult) - } - - func testActiveSource() async throws { - let activeSource = LocatorSearchSource() - activeSource.displayName = "Simple Locator" - let model = BasemapGalleryViewModel( - activeSource: activeSource, - sources: [LocatorSearchSource()] - ) - - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.commitSearch() } - - let results = try await model.$results.compactMap({$0}).first - let result = try XCTUnwrap(results?.get().first) - XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) - - Task { model.updateSuggestions() } - - let suggestions = try await model.$suggestions.compactMap({$0}).first - let suggestion = try XCTUnwrap(suggestions?.get().first) - XCTAssertEqual(suggestion.owningSource.displayName, activeSource.displayName) - } - - func testCommitSearch() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // No search - results are nil. - XCTAssertNil(model.results) - - // Search with no results - result count is 0. - model.currentQuery = "No results found blah blah blah blah" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) - XCTAssertNil(model.selectedResult) - XCTAssertNil(model.suggestions) - - // Search with one result. - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - - // One results automatically populates `selectedResult`. - XCTAssertEqual(result.first!, model.selectedResult) - XCTAssertNil(model.suggestions) - - // Search with multiple results. - model.currentQuery = "Magers & Quinn" - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - XCTAssertNil(model.selectedResult) - XCTAssertNil(model.suggestions) - - model.selectedResult = result.first! - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - XCTAssertNil(model.selectedResult) - } - - func testCurrentQuery() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Empty `currentQuery` should produce nil results and suggestions. - model.currentQuery = "" - XCTAssertNil(model.results) - XCTAssertNil(model.suggestions) - - // Valid `currentQuery` should produce non-nil results. - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - let results = try await model.$results.compactMap({$0}).first - XCTAssertNotNil(results) - - // Changing the `currentQuery` should set results to nil. - model.currentQuery = "Coffee in Portland" - XCTAssertNil(model.results) - - Task { model.updateSuggestions() } - - let suggestions = try await model.$suggestions.compactMap({$0}).first - XCTAssertNotNil(suggestions) - - // Changing the `currentQuery` should set suggestions to nil. - model.currentQuery = "Coffee in Edinburgh" - XCTAssertNil(model.suggestions) - - // Changing current query after search with 1 result - // should set `selectedResult` to nil - model.currentQuery = "Magers & Quinn Bookseller" - - Task { model.commitSearch() } - - _ = try await model.$results.compactMap({$0}).first - XCTAssertNotNil(model.selectedResult) - model.currentQuery = "Hotel" - XCTAssertNil(model.selectedResult) - } - - func testIsEligibleForRequery() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Set queryArea to Chippewa Falls - model.queryArea = Polygon.chippewaFalls - model.geoViewExtent = Polygon.chippewaFalls.extent - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - _ = try await model.$results.compactMap({$0}).first - XCTAssertFalse(model.isEligibleForRequery) - - // Offset extent by 10% - isEligibleForRequery should still be `false`. - var builder = EnvelopeBuilder(envelope: model.geoViewExtent) - let tenPercentWidth = model.geoViewExtent!.width * 0.1 - builder.offsetBy(x: tenPercentWidth, y: 0.0) - var newExtent = builder.toGeometry() as! Envelope - - model.geoViewExtent = newExtent - XCTAssertFalse(model.isEligibleForRequery) - - // Offset extent by 50% - isEligibleForRequery should now be `true`. - builder = EnvelopeBuilder(envelope: model.geoViewExtent) - let fiftyPercentWidth = model.geoViewExtent!.width * 0.5 - builder.offsetBy(x: fiftyPercentWidth, y: 0.0) - newExtent = builder.toGeometry() as! Envelope - - model.geoViewExtent = newExtent - XCTAssertTrue(model.isEligibleForRequery) - - // Set queryArea to Chippewa Falls - model.queryArea = Polygon.chippewaFalls - model.geoViewExtent = Polygon.chippewaFalls.extent - - Task { model.commitSearch() } - - _ = try await model.$results.compactMap({$0}).dropFirst().first - XCTAssertFalse(model.isEligibleForRequery) - - // Expand extent by 1.1x - isEligibleForRequery should still be `false`. - builder = EnvelopeBuilder(envelope: model.geoViewExtent) - builder.expand(factor: 1.1) - newExtent = builder.toGeometry() as! Envelope - - 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) - newExtent = builder.toGeometry() as! Envelope - - model.geoViewExtent = newExtent - XCTAssertTrue(model.isEligibleForRequery) - } - - func testQueryArea() async throws { - let source = LocatorSearchSource() - source.maximumResults = Int32.max - let model = BasemapGalleryViewModel(sources: [source]) - - // Set queryArea to Chippewa Falls - model.queryArea = Polygon.chippewaFalls - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - let resultGeometryUnion: Geometry = try XCTUnwrap( - GeometryEngine.union( - geometries: result.compactMap{ $0.geoElement?.geometry } - ) - ) - - XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.queryArea!, - geometry2: resultGeometryUnion - ) - ) - - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) - - model.queryArea = Polygon.minneapolis - - Task { model.commitSearch() } - - // A note about the use of `.dropFirst()`: - // Because `model.results` is not changed between the previous call - // to `model.commitSearch()` and the one right above, the - // `try await model.$results...` call will return the last result - // received (from the first `model.commitSearch()` call), which is - // incorrect. Calling `.dropFirst()` will remove that one - // and will give us the next one, which is the correct one (the result - // from the second `model.commitSearch()` call). - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - } - - func testQueryCenter() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Set queryCenter to Portland - model.queryCenter = .portland - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - - var resultPoint = try XCTUnwrap( - result.first?.geoElement?.geometry as? Point - ) - - var geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .portland, - point2: resultPoint, - distanceUnit: .meters, - azimuthUnit: nil, - curveType: .geodesic - ) - ) - - // First result within 1500m of Portland. - XCTAssertLessThan(geodeticDistance.distance, 1500.0) - - // Set queryCenter to Edinburgh - model.queryCenter = .edinburgh - model.currentQuery = "Restaurants" - - Task { model.commitSearch() } - - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - - resultPoint = try XCTUnwrap( - result.first?.geoElement?.geometry as? Point - ) - - // Web Mercator distance between .edinburgh and first result. - geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .edinburgh, - point2: resultPoint, - distanceUnit: .meters, - azimuthUnit: nil, - curveType: .geodesic - ) - ) - - // First result within 100m of Edinburgh. - XCTAssertLessThan(geodeticDistance.distance, 100) - } - - func testRepeatSearch() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Set queryArea to Chippewa Falls - model.geoViewExtent = Polygon.chippewaFalls.extent - model.currentQuery = "Coffee" - - Task { model.repeatSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - let resultGeometryUnion: Geometry = try XCTUnwrap( - GeometryEngine.union( - geometries: result.compactMap{ $0.geoElement?.geometry } - ) - ) - - XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.geoViewExtent!, - geometry2: resultGeometryUnion + XCTAssertEqual(item.spatialReferenceStatus, .unknown) + + // Test if basemap matches. Use a Task here so we can catch and test + // the change to `item.isLoading` during the loading of the base layers. + Task { + try await item.updateSpatialReferenceStatus(SpatialReference.webMercator) + } + + // Check if `isLoading` is set to true during first call + // to updateSpatialReferenceStatus. + isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first + loading = try XCTUnwrap(isLoading) + XCTAssertTrue(loading, "Item base layers are loading.") + + var srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).dropFirst().first + var status = try XCTUnwrap(srStatus) + XCTAssertEqual(status, .match) + XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) + XCTAssertFalse(item.isLoading) + + // Test if basemap doesn't match. + try await item.updateSpatialReferenceStatus(SpatialReference.wgs84) + + // Since we've already called `updateSpatialReferenceStatus` once, + // we should no longer internally need to load the baselayers. + isLoading = try await item.$isLoading.compactMap({ $0 }).first + loading = try XCTUnwrap(isLoading) + XCTAssertFalse(loading, "Item base layers are not loading.") + + srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).first + status = try XCTUnwrap(srStatus) + XCTAssertEqual(status, .noMatch) + XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) + + // Test WGS84 basemap. + let otherItem = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! ) ) - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.repeatSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) - - model.geoViewExtent = Polygon.minneapolis.extent - - Task { model.repeatSearch() } - - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - } - - func testSearchResultMode() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - XCTAssertEqual(model.resultMode, .automatic) - - model.resultMode = .single - model.currentQuery = "Magers & Quinn" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - - model.resultMode = .multiple - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - model.currentQuery = "Coffee" - - Task { model.updateSuggestions() } - - let suggestionResults = try await model.$suggestions.compactMap({$0}).first - let suggestions = try XCTUnwrap(suggestionResults?.get()) - - let collectionSuggestion = try XCTUnwrap(suggestions.filter { $0.isCollection }.first) - let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) - - model.resultMode = .automatic - - Task { model.acceptSuggestion(collectionSuggestion) } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - Task { model.acceptSuggestion(singleSuggestion) } - - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - } - - func testUpdateSuggestions() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // No currentQuery - suggestions are nil. - XCTAssertNil(model.suggestions) - - // UpdateSuggestions with no results - result count is 0. - model.currentQuery = "No results found blah blah blah blah" - - Task { model.updateSuggestions() } - - var suggestionResults = try await model.$suggestions.compactMap({$0}).first - var suggestions = try XCTUnwrap(suggestionResults?.get()) - XCTAssertEqual(suggestions.count, 0) - - // UpdateSuggestions with results. - model.currentQuery = "Magers & Quinn" - - Task { model.updateSuggestions() } - - suggestionResults = try await model.$suggestions.compactMap({$0}).first - suggestions = try XCTUnwrap(suggestionResults?.get()) - XCTAssertGreaterThanOrEqual(suggestions.count, 1) - - XCTAssertNil(model.selectedResult) - XCTAssertNil(model.results) + _ = try await otherItem.$isLoading.compactMap({ $0 }).dropFirst().first + + try await otherItem.updateSpatialReferenceStatus(SpatialReference.wgs84) + srStatus = try await otherItem.$spatialReferenceStatus.compactMap({ $0 }).first + status = try XCTUnwrap(srStatus) + XCTAssertEqual(status, .match) + XCTAssertEqual(otherItem.spatialReference, SpatialReference.wgs84) } - */ } - -//extension Polygon { -// static var chippewaFalls: Polygon { -// let builder = PolygonBuilder(spatialReference: .wgs84) -// let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) -// return builder.toGeometry() as! ArcGIS.Polygon -// } -// -// static var minneapolis: Polygon { -// let builder = PolygonBuilder(spatialReference: .wgs84) -// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) -// return builder.toGeometry() as! ArcGIS.Polygon -// } -//} -// -//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) -//} -// diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 164cef379..55d5faeaf 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -15,541 +15,138 @@ import Foundation import XCTest import ArcGIS -import ArcGISToolkit +@testable import ArcGISToolkit import SwiftUI import Combine @MainActor class BasemapGalleryViewModelTests: XCTestCase { - var basemapGalleryItems: [BasemapGalleryItem] = [ + let basemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( basemap: Basemap( item: PortalItem( url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! )! - ), - name: "OpenStreetMap (Blueprint)", - description: "OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. This vector basemap is based on the Daylight map distribution of OSM data and is hosted by Esri. It presents the map in a cartographic style is like a blueprint technical drawing.", - thumbnail: nil + ) ), BasemapGalleryItem( basemap: Basemap( item: PortalItem( url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! )! - ), - name: "National Geographic Style Map", - description: "This vector web map provides a detailed view of the world featuring beautiful political boundaries, labeling, and background that highlights the differences in the physical characteristics of the land.", - thumbnail: nil - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! - )! - ), - name: "Firefly Imagery Hybrid", - description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer.", - thumbnail: nil - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ), - name: nil, - description: "This web map features satellite imagery for the world and high-resolution aerial imagery for many areas. It uses WGS84 Geographic, version 2 tiling scheme.", - thumbnail: nil - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! - )! - ), - name: "Human Geography Dark Label", - description: "This (v2) vector tile layer provides a detailed basemap for the world, featuring a dark monochromatic style with content adjusted to support Human Geography information. This map is designed for use with Human Geography Dark Detail and Base layers.", - thumbnail: nil - ), + ) + ) ] - - // // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md // func testInit() async throws { - } - - func testPortal() async throws { - - } - - func testInitialBasemapItems() async throws { - - } - - func testGeoModel() async throws { - - } - - func testCurrentBasemapItem() async throws { - - } + // Note: this is a good candidate for mocking portal data. + // This would allow the test to check for a specific number of items + // in both the "Portal" and "Both" sections. - - /* + // Portal. + let portalViewModel = BasemapGalleryViewModel( + portal: Portal.arcGISOnline(isLoginRequired: false) + ) - - - func testAcceptSuggestion() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.updateSuggestions() } + var bgItems = try await portalViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first + var items = try XCTUnwrap(bgItems) - // Get suggestion - let suggestions = try await model.$suggestions.compactMap({$0}).first - let suggestion = try XCTUnwrap(suggestions?.get().first) - - Task { model.acceptSuggestion(suggestion) } - - let results = try await model.$results.compactMap({$0}).first - let result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - XCTAssertNil(model.suggestions) - - // With only one results, model should set `selectedResult` property. - XCTAssertEqual(result.first!, model.selectedResult) - } - - func testActiveSource() async throws { - let activeSource = LocatorSearchSource() - activeSource.displayName = "Simple Locator" - let model = BasemapGalleryViewModel( - activeSource: activeSource, - sources: [LocatorSearchSource()] + // There will be greater than 10 basemaps in the portal. + XCTAssertGreaterThan(items.count, 10) + + // BasemapGalleryItems. + let itemsViewModel = BasemapGalleryViewModel( + basemapGalleryItems: basemapGalleryItems ) - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.commitSearch() } - - let results = try await model.$results.compactMap({$0}).first - let result = try XCTUnwrap(results?.get().first) - XCTAssertEqual(result.owningSource.displayName, activeSource.displayName) - - Task { model.updateSuggestions() } - - let suggestions = try await model.$suggestions.compactMap({$0}).first - let suggestion = try XCTUnwrap(suggestions?.get().first) - XCTAssertEqual(suggestion.owningSource.displayName, activeSource.displayName) - } - - func testCommitSearch() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // No search - results are nil. - XCTAssertNil(model.results) - - // Search with no results - result count is 0. - model.currentQuery = "No results found blah blah blah blah" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) - XCTAssertNil(model.selectedResult) - XCTAssertNil(model.suggestions) - - // Search with one result. - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - - // One results automatically populates `selectedResult`. - XCTAssertEqual(result.first!, model.selectedResult) - XCTAssertNil(model.suggestions) - - // Search with multiple results. - model.currentQuery = "Magers & Quinn" - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - XCTAssertNil(model.selectedResult) - XCTAssertNil(model.suggestions) - - model.selectedResult = result.first! - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - XCTAssertNil(model.selectedResult) - } - - func testCurrentQuery() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Empty `currentQuery` should produce nil results and suggestions. - model.currentQuery = "" - XCTAssertNil(model.results) - XCTAssertNil(model.suggestions) - - // Valid `currentQuery` should produce non-nil results. - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - let results = try await model.$results.compactMap({$0}).first - XCTAssertNotNil(results) - - // Changing the `currentQuery` should set results to nil. - model.currentQuery = "Coffee in Portland" - XCTAssertNil(model.results) - - Task { model.updateSuggestions() } - - let suggestions = try await model.$suggestions.compactMap({$0}).first - XCTAssertNotNil(suggestions) - - // Changing the `currentQuery` should set suggestions to nil. - model.currentQuery = "Coffee in Edinburgh" - XCTAssertNil(model.suggestions) - - // Changing current query after search with 1 result - // should set `selectedResult` to nil - model.currentQuery = "Magers & Quinn Bookseller" - - Task { model.commitSearch() } - - _ = try await model.$results.compactMap({$0}).first - XCTAssertNotNil(model.selectedResult) - model.currentQuery = "Hotel" - XCTAssertNil(model.selectedResult) - } - - func testIsEligibleForRequery() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Set queryArea to Chippewa Falls - model.queryArea = Polygon.chippewaFalls - model.geoViewExtent = Polygon.chippewaFalls.extent - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - _ = try await model.$results.compactMap({$0}).first - XCTAssertFalse(model.isEligibleForRequery) - - // Offset extent by 10% - isEligibleForRequery should still be `false`. - var builder = EnvelopeBuilder(envelope: model.geoViewExtent) - let tenPercentWidth = model.geoViewExtent!.width * 0.1 - builder.offsetBy(x: tenPercentWidth, y: 0.0) - var newExtent = builder.toGeometry() as! Envelope - - model.geoViewExtent = newExtent - XCTAssertFalse(model.isEligibleForRequery) - - // Offset extent by 50% - isEligibleForRequery should now be `true`. - builder = EnvelopeBuilder(envelope: model.geoViewExtent) - let fiftyPercentWidth = model.geoViewExtent!.width * 0.5 - builder.offsetBy(x: fiftyPercentWidth, y: 0.0) - newExtent = builder.toGeometry() as! Envelope - - model.geoViewExtent = newExtent - XCTAssertTrue(model.isEligibleForRequery) - - // Set queryArea to Chippewa Falls - model.queryArea = Polygon.chippewaFalls - model.geoViewExtent = Polygon.chippewaFalls.extent - - Task { model.commitSearch() } - - _ = try await model.$results.compactMap({$0}).dropFirst().first - XCTAssertFalse(model.isEligibleForRequery) - - // Expand extent by 1.1x - isEligibleForRequery should still be `false`. - builder = EnvelopeBuilder(envelope: model.geoViewExtent) - builder.expand(factor: 1.1) - newExtent = builder.toGeometry() as! Envelope - - 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) - newExtent = builder.toGeometry() as! Envelope - - model.geoViewExtent = newExtent - XCTAssertTrue(model.isEligibleForRequery) - } - - func testQueryArea() async throws { - let source = LocatorSearchSource() - source.maximumResults = Int32.max - let model = BasemapGalleryViewModel(sources: [source]) - - // Set queryArea to Chippewa Falls - model.queryArea = Polygon.chippewaFalls - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - let resultGeometryUnion: Geometry = try XCTUnwrap( - GeometryEngine.union( - geometries: result.compactMap{ $0.geoElement?.geometry } - ) + // The item counts should match. + XCTAssertEqual( + itemsViewModel.basemapGalleryItems.count, + basemapGalleryItems.count ) - XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.queryArea!, - geometry2: resultGeometryUnion - ) + // Both Portal and BasemapGalleryItems. + let viewModel = BasemapGalleryViewModel( + portal: Portal.arcGISOnline(isLoginRequired: false), + basemapGalleryItems: basemapGalleryItems ) + + bgItems = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first + items = try XCTUnwrap(bgItems) - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) - - model.queryArea = Polygon.minneapolis - - Task { model.commitSearch() } - - // A note about the use of `.dropFirst()`: - // Because `model.results` is not changed between the previous call - // to `model.commitSearch()` and the one right above, the - // `try await model.$results...` call will return the last result - // received (from the first `model.commitSearch()` call), which is - // incorrect. Calling `.dropFirst()` will remove that one - // and will give us the next one, which is the correct one (the result - // from the second `model.commitSearch()` call). - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) + // Count will be greater than the number of hardcoded items. + XCTAssertGreaterThan(items.count, basemapGalleryItems.count) } - func testQueryCenter() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Set queryCenter to Portland - model.queryCenter = .portland - model.currentQuery = "Coffee" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) + func testGeoModelActualSpatialReference() async throws { + // Map with .webMercator basemap. + let geoModel = Map(basemap: Basemap.streets()) + try await geoModel.load() + XCTAssertEqual(geoModel.actualSpatialReference, .webMercator) - var resultPoint = try XCTUnwrap( - result.first?.geoElement?.geometry as? Point - ) - - var geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .portland, - point2: resultPoint, - distanceUnit: .meters, - azimuthUnit: nil, - curveType: .geodesic - ) - ) - - // First result within 1500m of Portland. - XCTAssertLessThan(geodeticDistance.distance, 1500.0) - - // Set queryCenter to Edinburgh - model.queryCenter = .edinburgh - model.currentQuery = "Restaurants" - - Task { model.commitSearch() } - - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - - resultPoint = try XCTUnwrap( - result.first?.geoElement?.geometry as? Point - ) - - // Web Mercator distance between .edinburgh and first result. - geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .edinburgh, - point2: resultPoint, - distanceUnit: .meters, - azimuthUnit: nil, - curveType: .geodesic + // Map with .wgs84 basemap. + let geoModel2 = Map( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! ) ) - - // First result within 100m of Edinburgh. - XCTAssertLessThan(geodeticDistance.distance, 100) + + try await geoModel2.load() + XCTAssertEqual(geoModel2.actualSpatialReference, .wgs84) + + // Test with Scene that has a tiling scheme of .webMercator + // but a different spatial reference... } - func testRepeatSearch() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // Set queryArea to Chippewa Falls - model.geoViewExtent = Polygon.chippewaFalls.extent - model.currentQuery = "Coffee" - - Task { model.repeatSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - let resultGeometryUnion: Geometry = try XCTUnwrap( - GeometryEngine.union( - geometries: result.compactMap{ $0.geoElement?.geometry } - ) - ) + func testCurrentBasemapItem() async throws { + let geoModel = Map(basemap: Basemap.streets()) + + let viewModel = BasemapGalleryViewModel(geoModel: geoModel) - XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.geoViewExtent!, - geometry2: resultGeometryUnion + // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). + let validItem = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! ) ) - model.currentQuery = "Magers & Quinn Booksellers" - - Task { model.repeatSearch() } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 0) - - model.geoViewExtent = Polygon.minneapolis.extent - - Task { model.repeatSearch() } - - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - } - - func testSearchResultMode() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - XCTAssertEqual(model.resultMode, .automatic) - - model.resultMode = .single - model.currentQuery = "Magers & Quinn" - - Task { model.commitSearch() } - - var results = try await model.$results.compactMap({$0}).first - var result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - - model.resultMode = .multiple - - Task { model.commitSearch() } - - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - model.currentQuery = "Coffee" - - Task { model.updateSuggestions() } - - let suggestionResults = try await model.$suggestions.compactMap({$0}).first - let suggestions = try XCTUnwrap(suggestionResults?.get()) - - let collectionSuggestion = try XCTUnwrap(suggestions.filter { $0.isCollection }.first) - let singleSuggestion = try XCTUnwrap(suggestions.filter { !$0.isCollection }.first) - - model.resultMode = .automatic - - Task { model.acceptSuggestion(collectionSuggestion) } - - results = try await model.$results.compactMap({$0}).first - result = try XCTUnwrap(results?.get()) - XCTAssertGreaterThan(result.count, 1) - - Task { model.acceptSuggestion(singleSuggestion) } + // Wait until it loads. + _ = try await validItem.$isLoading.compactMap({ $0 }).dropFirst().first + + viewModel.updateCurrentBasemapGalleryItem(validItem) - results = try await model.$results.compactMap({$0}).dropFirst().first - result = try XCTUnwrap(results?.get()) - XCTAssertEqual(result.count, 1) - } + // Wait until the `currentBasemapGalleryItem` is updated. + let tmpItem = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).dropFirst().first + let currentItem = try XCTUnwrap(tmpItem) + XCTAssertEqual(currentItem, validItem) - func testUpdateSuggestions() async throws { - let model = BasemapGalleryViewModel(sources: [LocatorSearchSource()]) - - // No currentQuery - suggestions are nil. - XCTAssertNil(model.suggestions) - - // UpdateSuggestions with no results - result count is 0. - model.currentQuery = "No results found blah blah blah blah" - - Task { model.updateSuggestions() } - - var suggestionResults = try await model.$suggestions.compactMap({$0}).first - var suggestions = try XCTUnwrap(suggestionResults?.get()) - XCTAssertEqual(suggestions.count, 0) - - // UpdateSuggestions with results. - model.currentQuery = "Magers & Quinn" - - Task { model.updateSuggestions() } + // Test WGS84 basemap item (Imagery (WGS84)). + let invalidItem = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ) + ) - suggestionResults = try await model.$suggestions.compactMap({$0}).first - suggestions = try XCTUnwrap(suggestionResults?.get()) - XCTAssertGreaterThanOrEqual(suggestions.count, 1) + // Wait until it loads. + _ = try await invalidItem.$isLoading.compactMap({ $0 }).dropFirst().first + + viewModel.updateCurrentBasemapGalleryItem(invalidItem) + + // The update will fail, so wait until the + // `$spatialReferenceMismatchError` is updated. + let error = try await viewModel.$spatialReferenceMismatchError.compactMap({ $0 }).first + XCTAssertNotNil(error, "Error is not nil.") - XCTAssertNil(model.selectedResult) - XCTAssertNil(model.results) + // Make sure the current item is still equal to the valid item. + XCTAssertEqual(currentItem, validItem) } - */ } - -//extension Polygon { -// static var chippewaFalls: Polygon { -// let builder = PolygonBuilder(spatialReference: .wgs84) -// let _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) -// return builder.toGeometry() as! ArcGIS.Polygon -// } -// -// static var minneapolis: Polygon { -// let builder = PolygonBuilder(spatialReference: .wgs84) -// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) -// let _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) -// return builder.toGeometry() as! ArcGIS.Polygon -// } -//} -// -//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) -//} -// From 5d2f70cc3090c8d550bf0fad6a669d43eadd99fb Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 20 Nov 2021 16:44:41 -0600 Subject: [PATCH 0303/1722] Update tests for more coverage. --- .../BasemapGalleryViewModel.swift | 78 ++++++++++++-- .../BasemapGalleryItemTests.swift | 11 +- .../BasemapGalleryViewModelTests.swift | 101 +++++++++++++----- 3 files changed, 152 insertions(+), 38 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 9a1c98fe0..b49bf74a4 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -20,6 +20,9 @@ import Combine @MainActor public class BasemapGalleryViewModel: ObservableObject { /// Creates a `BasemapGalleryViewModel`. + /// - Remark: If `portal` is non-nil, the portal's basemaps will be loaded. If `portal` is + /// `nil`, ArcGISOnline's developer basemaps will be loaded. In both cases, the basemaps + /// will be added to `basemapGalleryItems`. /// - Parameters: /// - geoModel: The `GeoModel`. /// - portal: The `Portal` to load base maps from. @@ -33,13 +36,30 @@ public class BasemapGalleryViewModel: ObservableObject { self.portal = portal self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) - // Note that we don't want to store these tasks and cancel them - // before kicking off another operation becasue both of these - // operations could have been started elsewhere as well as here. + // Note that we don't want to store this tasks and cancel it + // before kicking off another operation because these operations + // could have been started elsewhere as well as here. // Canceling them here would also cancel those other operations, // which we don't want to do. - Task { await load(geoModel: geoModel) } - Task { await fetchBasemaps(from: portal) } + Task { + // Load the geomodel. + await load(geoModel: geoModel) + + // If we have a portal or no basemapGalleryItems were supplied, + // then load the default basemaps from the portal, if any, or AGOL. + if portal != nil || basemapGalleryItems.isEmpty { + var thePortal = portal + var useDeveloperBasemaps = false + if portal == nil { + thePortal = Portal.arcGISOnline(isLoginRequired: false) + useDeveloperBasemaps = true + } + await fetchBasemaps( + from: thePortal, + useDeveloperBasemaps: useDeveloperBasemaps + ) + } + } } /// The error generated by fetching the `Basemaps` from the `Portal`. @@ -57,15 +77,16 @@ public class BasemapGalleryViewModel: ObservableObject { } /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps - /// and add them to the `basemapGalleryItems` array. + /// and replace the`basemapGalleryItems` array with the fetched basemaps. public var portal: Portal? { didSet { - Task { await fetchBasemaps(from: portal) } + Task { await fetchBasemaps(from: portal, append: false) } } } /// The list of basemaps currently visible in the gallery. It is comprised of items passed into - /// the `BasemapGalleryItem` constructor and items loaded from the `Portal`. + /// the `BasemapGalleryItem` constructor property and items loaded either from `portal` or + /// from ArcGISOnline if `portal` is `nil`. @Published public var basemapGalleryItems: [BasemapGalleryItem] = [] @@ -123,12 +144,47 @@ internal extension GeoModel { } private extension BasemapGalleryViewModel { - func fetchBasemaps(from portal: Portal?) async { + /// Fetches the basemaps from the given portal and populates `basemapGalleryItems` with + /// items created from the fetched basemaps. + /// - Parameters: + /// - portal: Portal to fetch basemaps from + /// - useDeveloperBasemaps: If `true`, will always use the portal's developer basemaps. If + /// `false`, it will use either the portal's basemaps or vector basemaps, depending on the value of + /// `portal.portalInfo.useVectorBasemaps`. + /// - append: If `true`, will appended fetched basemaps to `basemapGalleryItems`. + /// If `false`, it will clear `basemapGalleryItems` before adding the fetched basemaps. + func fetchBasemaps( + from portal: Portal?, + useDeveloperBasemaps: Bool = false, + append: Bool = true + ) async { guard let portal = portal else { return } do { - basemapGalleryItems += try await portal.developerBasemaps.map { - BasemapGalleryItem(basemap: $0) + try await portal.load() + + var tmpItems = [BasemapGalleryItem]() + + if useDeveloperBasemaps { + tmpItems += try await portal.developerBasemaps.map { + BasemapGalleryItem(basemap: $0) + } + } else if let portalInfo = portal.portalInfo, + portalInfo.useVectorBasemaps { + tmpItems += try await portal.vectorBasemaps.map { + BasemapGalleryItem(basemap: $0) + } + } else { + tmpItems += try await portal.basemaps.map { + BasemapGalleryItem(basemap: $0) + } + } + + if append { + basemapGalleryItems += tmpItems + } + else { + basemapGalleryItems = tmpItems } } catch { fetchBasemapsError = error diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 3040f2366..9ef0ee1c9 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -19,11 +19,16 @@ import ArcGISToolkit import SwiftUI import Combine +// +// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md +// +// Note: the iOS implementation uses the MVVM approach and SwiftUI. This +// required a bit more properties/logic in the `BasemapGalleryItem` (such +// as the `loadBasemapError` and `spatialReferenceStatus` properties than +// the `BasemapGallery` design specifies. Tests not present in the +// test design have been added to accomodate those differences. @MainActor class BasemapGalleryItemTests: XCTestCase { - // - // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md - // func testInit() async throws { let basemap = Basemap.lightGrayCanvas() var item = BasemapGalleryItem(basemap: basemap) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 55d5faeaf..5075ff46b 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -19,9 +19,17 @@ import ArcGIS import SwiftUI import Combine +// +// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md +// +// Note: the iOS implementation uses the MVVM approach and SwiftUI. This +// required a bit more properties/logic in the `BasemapGalleryViewModel` (such +// as `geoModel.actualSpatialReference`) than the `BasemapGallery` design +// specifies. Tests not present in the test design have been added to +// accomodate those differences. @MainActor class BasemapGalleryViewModelTests: XCTestCase { - let basemapGalleryItems: [BasemapGalleryItem] = [ + let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( basemap: Basemap( item: PortalItem( @@ -37,48 +45,60 @@ class BasemapGalleryViewModelTests: XCTestCase { ) ) ] - - // - // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md - // + func testInit() async throws { // Note: this is a good candidate for mocking portal data. - // This would allow the test to check for a specific number of items - // in both the "Portal" and "Both" sections. - + // This would allow the test to check for a specific number of items. + + // GeoModel. + let geoModel = Map(basemap: Basemap.streets()) + let geoModelViewModel = BasemapGalleryViewModel(geoModel: geoModel) + + // With no portal, `basemapGalleryItems` are fetched from AGOL's + // list of developer basemaps. + var items = try await geoModelViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first + var basemapGalleryItems = try XCTUnwrap(items) + XCTAssertTrue(basemapGalleryItems.isEmpty) + + let developerBasemapItems = basemapGalleryItems + // Portal. let portalViewModel = BasemapGalleryViewModel( portal: Portal.arcGISOnline(isLoginRequired: false) ) - var bgItems = try await portalViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - var items = try XCTUnwrap(bgItems) - - // There will be greater than 10 basemaps in the portal. - XCTAssertGreaterThan(items.count, 10) + // With a portal, `basemapGalleryItems` are fetched from either the + // portal's vector basemaps or regular basemaps. + items = try await portalViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first + basemapGalleryItems = try XCTUnwrap(items) + XCTAssertFalse(basemapGalleryItems.isEmpty) + + let sortedItems = basemapGalleryItems.sorted { $0.name < $1.name } + let sortedDeveloperItems = developerBasemapItems.sorted { $0.name < $1.name } + XCTAssertNotEqual(sortedItems, sortedDeveloperItems) // BasemapGalleryItems. let itemsViewModel = BasemapGalleryViewModel( - basemapGalleryItems: basemapGalleryItems + basemapGalleryItems: defaultBasemapGalleryItems ) // The item counts should match. XCTAssertEqual( itemsViewModel.basemapGalleryItems.count, - basemapGalleryItems.count + defaultBasemapGalleryItems.count ) // Both Portal and BasemapGalleryItems. let viewModel = BasemapGalleryViewModel( portal: Portal.arcGISOnline(isLoginRequired: false), - basemapGalleryItems: basemapGalleryItems + basemapGalleryItems: defaultBasemapGalleryItems ) - bgItems = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - items = try XCTUnwrap(bgItems) + items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first + basemapGalleryItems = try XCTUnwrap(items) // Count will be greater than the number of hardcoded items. - XCTAssertGreaterThan(items.count, basemapGalleryItems.count) + XCTAssertGreaterThan(basemapGalleryItems.count, defaultBasemapGalleryItems.count) } func testGeoModelActualSpatialReference() async throws { @@ -99,15 +119,21 @@ class BasemapGalleryViewModelTests: XCTestCase { try await geoModel2.load() XCTAssertEqual(geoModel2.actualSpatialReference, .wgs84) - // Test with Scene that has a tiling scheme of .webMercator + // TODO: Test with Scene that has a tiling scheme of .webMercator // but a different spatial reference... } - func testCurrentBasemapItem() async throws { - let geoModel = Map(basemap: Basemap.streets()) + func testCurrentBasemapGalleryItem() async throws { + let basemap = Basemap.streets() + let geoModel = Map(basemap: basemap) let viewModel = BasemapGalleryViewModel(geoModel: geoModel) + // Verify current item is equal to map's basemap. + var item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).first + var currentItem = try XCTUnwrap(item) + XCTAssertTrue(currentItem.basemap === basemap) + // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). let validItem = BasemapGalleryItem( basemap: Basemap( @@ -123,8 +149,8 @@ class BasemapGalleryViewModelTests: XCTestCase { viewModel.updateCurrentBasemapGalleryItem(validItem) // Wait until the `currentBasemapGalleryItem` is updated. - let tmpItem = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).dropFirst().first - let currentItem = try XCTUnwrap(tmpItem) + item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).dropFirst().first + currentItem = try XCTUnwrap(item) XCTAssertEqual(currentItem, validItem) // Test WGS84 basemap item (Imagery (WGS84)). @@ -149,4 +175,31 @@ class BasemapGalleryViewModelTests: XCTestCase { // Make sure the current item is still equal to the valid item. XCTAssertEqual(currentItem, validItem) } + + func testUpdatePortal() async throws { + // Note: this is a good candidate for mocking portal data. + // This would allow the test to check for a specific number of items. + + let viewModel = BasemapGalleryViewModel( + basemapGalleryItems: defaultBasemapGalleryItems + ) + + var items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).first + var basemapGalleryItems = try XCTUnwrap(items) + + // There are exactly two default items. + XCTAssertEqual(basemapGalleryItems.count, 2) + + viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) + + items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first + basemapGalleryItems = try XCTUnwrap(items) + + // There should be no default items in the basemap gallery. + let foundDefaultItem = basemapGalleryItems.first(where: { + $0 == defaultBasemapGalleryItems[0] || + $0 == defaultBasemapGalleryItems[1] + }) + XCTAssertNil(foundDefaultItem) + } } From 47c5bda98a2263355d134eeda657557c9ef6a845 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 14:50:57 -0600 Subject: [PATCH 0304/1722] Test updates. --- .../BasemapGalleryViewModel.swift | 4 +--- .../BasemapGalleryViewModelTests.swift | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index b49bf74a4..ad2474045 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -50,7 +50,7 @@ public class BasemapGalleryViewModel: ObservableObject { if portal != nil || basemapGalleryItems.isEmpty { var thePortal = portal var useDeveloperBasemaps = false - if portal == nil { + if thePortal == nil { thePortal = Portal.arcGISOnline(isLoginRequired: false) useDeveloperBasemaps = true } @@ -164,7 +164,6 @@ private extension BasemapGalleryViewModel { try await portal.load() var tmpItems = [BasemapGalleryItem]() - if useDeveloperBasemaps { tmpItems += try await portal.developerBasemaps.map { BasemapGalleryItem(basemap: $0) @@ -193,7 +192,6 @@ private extension BasemapGalleryViewModel { func load(geoModel: GeoModel?) async { guard let geoModel = geoModel else { return } - do { try await geoModel.load() if let basemap = geoModel.basemap { diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 5075ff46b..e04985bfd 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -58,7 +58,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // list of developer basemaps. var items = try await geoModelViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first var basemapGalleryItems = try XCTUnwrap(items) - XCTAssertTrue(basemapGalleryItems.isEmpty) + XCTAssertFalse(basemapGalleryItems.isEmpty) let developerBasemapItems = basemapGalleryItems @@ -73,11 +73,13 @@ class BasemapGalleryViewModelTests: XCTestCase { basemapGalleryItems = try XCTUnwrap(items) XCTAssertFalse(basemapGalleryItems.isEmpty) + // Sort the developer items from the "GeoModel" test above and the + // items from the portal and make sure they are not equal. let sortedItems = basemapGalleryItems.sorted { $0.name < $1.name } let sortedDeveloperItems = developerBasemapItems.sorted { $0.name < $1.name } XCTAssertNotEqual(sortedItems, sortedDeveloperItems) - // BasemapGalleryItems. + // BasemapGalleryItems. No basemaps are fetched from a portal. let itemsViewModel = BasemapGalleryViewModel( basemapGalleryItems: defaultBasemapGalleryItems ) @@ -88,7 +90,8 @@ class BasemapGalleryViewModelTests: XCTestCase { defaultBasemapGalleryItems.count ) - // Both Portal and BasemapGalleryItems. + // Both Portal and BasemapGalleryItems. Basemaps are fetched from + // the portal and appended to the list of basemapGalleryItems. let viewModel = BasemapGalleryViewModel( portal: Portal.arcGISOnline(isLoginRequired: false), basemapGalleryItems: defaultBasemapGalleryItems @@ -119,8 +122,17 @@ class BasemapGalleryViewModelTests: XCTestCase { try await geoModel2.load() XCTAssertEqual(geoModel2.actualSpatialReference, .wgs84) - // TODO: Test with Scene that has a tiling scheme of .webMercator - // but a different spatial reference... + // Test with Scene that has a tiling scheme of .webMercator + let geoModel3 = Scene( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ) + ) + + try await geoModel3.load() + XCTAssertEqual(geoModel3.actualSpatialReference, .webMercator) } func testCurrentBasemapGalleryItem() async throws { From 022e71f56cfe451ae14d21f09b72cfebb1fd38ac Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:18:25 -0600 Subject: [PATCH 0305/1722] Update Sources/ArcGISToolkit/Components/Search/SearchError.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchError.swift b/Sources/ArcGISToolkit/Components/Search/SearchError.swift index aec2589fa..153258ffa 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchError.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchError.swift @@ -13,7 +13,7 @@ import Foundation -/// A value that represents an error that occurs while Searching. +/// A value that represents an error that occurs while searching. public struct SearchError: Error { /// A basic description for the error. public let errorDescription: String From 1335c0b090fd10b1f0e6ad64a7c03a6c4f27048a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:18:37 -0600 Subject: [PATCH 0306/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/SearchExampleView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index a0ca4c666..633ffe5b4 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -12,7 +12,6 @@ // limitations under the License. import SwiftUI -import Combine import ArcGIS import ArcGISToolkit From 9d85f9c2617900d9c1305ca172708bdc53e44e25 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:18:46 -0600 Subject: [PATCH 0307/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Zachary A Kline --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 4440377fd..09fd10039 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -21,7 +21,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// - Parameters: /// - name: The name to show when presenting this source in the UI. /// - locatorTask: The `LocatorTask` to use for searching. - /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. + /// - maximumResults: The maximum results to return when performing a search. Most sources default to `6`. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. public init( name: String = "Locator", From f31780a37d12ade0c85a7aa10ee7afda72b46fff Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:18:53 -0600 Subject: [PATCH 0308/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Zachary A Kline --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 09fd10039..a587cd48a 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -22,7 +22,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// - name: The name to show when presenting this source in the UI. /// - locatorTask: The `LocatorTask` to use for searching. /// - maximumResults: The maximum results to return when performing a search. Most sources default to `6`. - /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. + /// - maximumSuggestions: The maximum suggestions to return. Most sources default to `6`. public init( name: String = "Locator", locatorTask: LocatorTask = LocatorTask( From 653a74c507997d1a14b6baceaaad1936f6c7d603 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:19:00 -0600 Subject: [PATCH 0309/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Zachary A Kline --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index a587cd48a..36023b535 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -44,7 +44,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// The name to show when presenting this source in the UI. public var name: String - /// The maximum results to return when performing a search. Most sources default to 6 + /// The maximum results to return when performing a search. Most sources default to `6`. public var maximumResults: Int32 { get { geocodeParameters.maxResults From f1b45427b141703dba0ec6fb2eedd5b47c883b06 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:19:10 -0600 Subject: [PATCH 0310/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index bb71f8964..0d0637227 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -212,8 +212,7 @@ public class SearchViewModel: ObservableObject { /// Commits a search from a specific suggestion. Results will be set asynchronously. Behavior is /// generally the same as `commitSearch`, except `searchSuggestion` is used instead of the /// `currentQuery` property. - /// - Parameters: - /// - searchSuggestion: The suggestion to use to commit the search. + /// - Parameter searchSuggestion: The suggestion to use to commit the search. public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) { currentQuery = searchSuggestion.displayTitle kickoffTask { await self.doAcceptSuggestion(searchSuggestion) } From 98a704c1202c8f77261d17fab6d5403f80f587e4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:19:17 -0600 Subject: [PATCH 0311/1722] Update Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift Co-authored-by: Zachary A Kline --- .../Components/Search/SmartLocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 5ecd6af27..e56dc0a87 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -23,7 +23,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// - Parameters: /// - name: Name to show when presenting this source in the UI. /// - locatorTask: The `LocatorTask` to use for searching.. - /// - maximumResults: The maximum results to return when performing a search. Most sources default to 6. + /// - maximumResults: The maximum results to return when performing a search. Most sources default to `6`. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. /// - repeatSearchResultThreshold: The minimum number of search results to attempt to return. /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. From e889c3b7f1103623a7ac7f49615cde0cddaa157b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:19:25 -0600 Subject: [PATCH 0312/1722] Update Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift Co-authored-by: Zachary A Kline --- .../Components/Search/SmartLocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index e56dc0a87..1da01b24c 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -24,7 +24,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { /// - name: Name to show when presenting this source in the UI. /// - locatorTask: The `LocatorTask` to use for searching.. /// - maximumResults: The maximum results to return when performing a search. Most sources default to `6`. - /// - maximumSuggestions: The maximum suggestions to return. Most sources default to 6. + /// - maximumSuggestions: The maximum suggestions to return. Most sources default to `6`. /// - repeatSearchResultThreshold: The minimum number of search results to attempt to return. /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. public init( From 978c6387b5d25b711d800bc9eb433272c8e17b1f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:19:33 -0600 Subject: [PATCH 0313/1722] Update Tests/ArcGISToolkitTests/Publisher.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/Publisher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/Publisher.swift b/Tests/ArcGISToolkitTests/Publisher.swift index 01184dba4..c9b373cc6 100644 --- a/Tests/ArcGISToolkitTests/Publisher.swift +++ b/Tests/ArcGISToolkitTests/Publisher.swift @@ -16,7 +16,7 @@ import Combine extension Publisher { /// Asynchronously returns the first value emitted from the publisher. - /// This property will return `nil` if this Publisher completes without an error before + /// This property will return `nil` if this publisher completes without an error before /// it emits a value. var first: Output? { get async throws { From 7a5bae222ac4661c727bc663982d0e29901c75ce Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:19:40 -0600 Subject: [PATCH 0314/1722] Update Tests/ArcGISToolkitTests/SearchViewModelTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index fc67d0747..3d3c1fc32 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -14,7 +14,6 @@ import XCTest import ArcGIS import ArcGISToolkit -import Combine @MainActor class SearchViewModelTests: XCTestCase { From 6b0a179588816dc422c2d5d5c957be5a34ba6a84 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:19:53 -0600 Subject: [PATCH 0315/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: Zachary A Kline --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 36023b535..c451324b7 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -54,7 +54,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { } } - /// The maximum suggestions to return. Most sources default to 6. + /// The maximum suggestions to return. Most sources default to `6`. public var maximumSuggestions: Int32 { get { suggestParameters.maxResults From 3b6fd0a11279b3f28f0cd49b60261869bd57779e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:20:28 -0600 Subject: [PATCH 0316/1722] Update Sources/ArcGISToolkit/Components/Search/SearchResult.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchResult.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 8a96eaa63..7de13c814 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -26,7 +26,7 @@ public struct SearchResult { public var displaySubtitle: String = "" /// For locator results, should be the graphic that was used to display the result on the map. - /// For feature layer results, should be the result feature. Can be null depending on the type of the + /// For feature layer results, should be the resulting feature. Can be `nil` depending on the type of the /// result, and can have `GeoElement`s without a defined geometry. public var geoElement: GeoElement? = nil From c6e19a9dfdf1392be1ee22387abe7a07635da22c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:20:59 -0600 Subject: [PATCH 0317/1722] Update Sources/ArcGISToolkit/Components/Search/SearchSource.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 77787f757..1e0f87c75 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -18,7 +18,7 @@ public protocol SearchSource { /// Name to show when presenting this source in the UI. var name: String { get set } - /// The maximum results to return when performing a search. Most sources default to 6. + /// The maximum results to return when performing a search. Most sources default to `6`. var maximumResults: Int32 { get set } /// The maximum suggestions to return. Most sources default to 6. From 27f953b5579efe26359f6704a9f8dec028095104 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:21:38 -0600 Subject: [PATCH 0318/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index d4567b97a..92ea6483d 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -15,7 +15,7 @@ import SwiftUI import Combine import ArcGIS -/// SearchView presents a search experience, powered by an underlying SearchViewModel. +/// `SearchView` presents a search experience, powered by an underlying `SearchViewModel`. public struct SearchView: View { /// Creates a new `SearchView`. /// - Parameters: From 08d3146eef1b13e63a25ebf218fe5663677de683 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:21:59 -0600 Subject: [PATCH 0319/1722] Update Sources/ArcGISToolkit/Components/Search/SearchSource.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 1e0f87c75..1e07f0828 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -21,7 +21,7 @@ public protocol SearchSource { /// The maximum results to return when performing a search. Most sources default to `6`. var maximumResults: Int32 { get set } - /// The maximum suggestions to return. Most sources default to 6. + /// The maximum suggestions to return. Most sources default to `6`. var maximumSuggestions: Int32 { get set } /// Returns the search suggestions for the specified query. From 41e8118548227dd7cf814332a2961544d44cc140 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:22:28 -0600 Subject: [PATCH 0320/1722] Update Sources/ArcGISToolkit/Components/Search/SearchSource.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 1e07f0828..76ab2ce0a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -41,7 +41,7 @@ public protocol SearchSource { /// - queryString: Text to be used for query. /// - searchArea: The area used to limit results. /// - preferredSearchLocation: The location used as a starting point for searches. - /// - Returns: Array of `SearchResult`s + /// - Returns: An array of search results. func search( _ queryString: String, searchArea: Geometry?, From 2d987879d919a614913e9abab44c9c58b6a9b117 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:22:49 -0600 Subject: [PATCH 0321/1722] Update Sources/ArcGISToolkit/Components/Search/SearchSource.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 76ab2ce0a..0ed123792 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -64,7 +64,7 @@ public protocol SearchSource { /// - Parameters: /// - queryString: Text to be used for query. /// - searchExtent: Extent used to limit the results. - /// - Returns: Array of `SearchResult`s + /// - Returns: An array of search results. func repeatSearch( _ queryString: String, searchExtent: Envelope From b7dae1ac75d8c529d4bdc8eb6d0bff319c83be15 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:23:11 -0600 Subject: [PATCH 0322/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 92ea6483d..7e6fd5d17 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -12,7 +12,6 @@ // limitations under the License. import SwiftUI -import Combine import ArcGIS /// `SearchView` presents a search experience, powered by an underlying `SearchViewModel`. From 771235765a647f921cb84abeaf6bbfc605d6f5cf Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:23:33 -0600 Subject: [PATCH 0323/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 7e6fd5d17..c55ce6292 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -57,7 +57,7 @@ public struct SearchView: View { /// Note: this is set using the `enableResultListView` modifier. private var enableResultListView = true - /// Message to show when there are no results or suggestions. Defaults to "No results found". + /// Message to show when there are no results or suggestions. Defaults to "No results found". /// Note: this is set using the `noResultsMessage` modifier. private var noResultsMessage = "No results found" From 8074f07e4f1cec3918fa1a451b59739ae17ede5a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:24:54 -0600 Subject: [PATCH 0324/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 0d0637227..94e38b347 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -16,7 +16,7 @@ import SwiftUI import ArcGIS import Combine -/// Performs searches and manages search state for a Search, or optionally without a UI connection. +/// Performs searches and manages search state for a search, or optionally without a UI connection. @MainActor public class SearchViewModel: ObservableObject { /// Defines how many results to return; one, many, or automatic based on circumstance. From 091a7fc1562dfcd660889fa52d515741a3abac75 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Nov 2021 15:27:58 -0600 Subject: [PATCH 0325/1722] Update Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift Co-authored-by: R Olson --- .../Components/Search/LocatorSearchSource.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index c451324b7..737f98ad8 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -56,12 +56,8 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// The maximum suggestions to return. Most sources default to `6`. public var maximumSuggestions: Int32 { - get { - suggestParameters.maxResults - } - set { - suggestParameters.maxResults = newValue - } + get { suggestParameters.maxResults } + set { suggestParameters.maxResults = newValue } } /// The locator used by this search source. From 8e3080f6dacf5322ee8c72c4be97cf528d7a869b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 24 Nov 2021 16:40:19 -0600 Subject: [PATCH 0326/1722] PR review comments. --- .../Search/LocatorSearchSource.swift | 24 ++--- .../Components/Search/SearchError.swift | 30 ------ .../Components/Search/SearchResult.swift | 40 ++++++-- .../Components/Search/SearchSource.swift | 12 +-- .../Components/Search/SearchSuggestion.swift | 30 +++++- .../Components/Search/SearchView.swift | 96 +++++++++++-------- .../Components/Search/SearchViewModel.swift | 44 ++++----- .../Search/SmartLocatorSearchSource.swift | 33 +++---- .../ArcGISToolkit/Utility/SearchField.swift | 36 ++++--- 9 files changed, 188 insertions(+), 157 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Search/SearchError.swift diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 737f98ad8..85daf547a 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -46,12 +46,8 @@ public class LocatorSearchSource: ObservableObject, SearchSource { /// The maximum results to return when performing a search. Most sources default to `6`. public var maximumResults: Int32 { - get { - geocodeParameters.maxResults - } - set { - geocodeParameters.maxResults = newValue - } + get { geocodeParameters.maxResults } + set { geocodeParameters.maxResults = newValue } } /// The maximum suggestions to return. Most sources default to `6`. @@ -72,22 +68,22 @@ public class LocatorSearchSource: ObservableObject, SearchSource { public let suggestParameters: SuggestParameters = SuggestParameters() public func repeatSearch( - _ queryString: String, + _ query: String, searchExtent: Envelope ) async throws -> [SearchResult] { try await internalSearch( - queryString, + query, searchArea: searchExtent ) } public func search( - _ queryString: String, + _ query: String, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { try await internalSearch( - queryString, + query, searchArea: searchArea, preferredSearchLocation: preferredSearchLocation ) @@ -115,7 +111,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { } public func suggest( - _ queryString: String, + _ query: String, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil ) async throws -> [SearchSuggestion] { @@ -123,7 +119,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { suggestParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.suggest( - searchText: queryString, + searchText: query, parameters: suggestParameters ) // Convert to SearchSuggestions and return. @@ -135,7 +131,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { extension LocatorSearchSource { private func internalSearch( - _ queryString: String, + _ query: String, searchArea: Geometry?, preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { @@ -143,7 +139,7 @@ extension LocatorSearchSource { geocodeParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.geocode( - searchText: queryString, + searchText: query, parameters: geocodeParameters ) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchError.swift b/Sources/ArcGISToolkit/Components/Search/SearchError.swift deleted file mode 100644 index 153258ffa..000000000 --- a/Sources/ArcGISToolkit/Components/Search/SearchError.swift +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021 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 Foundation - -/// A value that represents an error that occurs while searching. -public struct SearchError: Error { - /// A basic description for the error. - public let errorDescription: String -} - -extension SearchError { - init(_ error: Error) { - self.init( - errorDescription: error.localizedDescription - ) - } -} - -extension SearchError: Equatable {} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index 7de13c814..dc938e538 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -16,30 +16,56 @@ import ArcGIS /// Wraps a search result for display. public struct SearchResult { - /// Title that should be shown whenever a result is displayed. + /// Creates a `SearchResult`. + /// - Parameters: + /// - displayTitle: The string to be shown whenever a result is displayed. + /// - owningSource: Reference to the search source that created this result. + /// - displaySubtitle: The string to be shown as a subtitle wherever results are shown. + /// - geoElement: The graphic that was used to display the result on the map. + /// - markerImage: The marker that would be shown on the map for the result. + /// - selectionViewpoint: The viewpoint to be used when the view zooms to a selected result. + public init( + displayTitle: String, + owningSource: SearchSource, + displaySubtitle: String = "", + geoElement: GeoElement? = nil, + markerImage: UIImage? = nil, + selectionViewpoint: Viewpoint? = nil + ) { + self.displayTitle = displayTitle + self.owningSource = owningSource + self.displaySubtitle = displaySubtitle + self.geoElement = geoElement + self.markerImage = markerImage + self.selectionViewpoint = selectionViewpoint + } + + /// The string to be shown whenever a result is displayed. public let displayTitle: String /// Reference to the search source that created this result. public let owningSource: SearchSource - /// Should be shown as a subtitle wherever results are shown. - public var displaySubtitle: String = "" + /// The string to be shown as a subtitle wherever results are shown. + public let displaySubtitle: String /// For locator results, should be the graphic that was used to display the result on the map. /// For feature layer results, should be the resulting feature. Can be `nil` depending on the type of the /// result, and can have `GeoElement`s without a defined geometry. - public var geoElement: GeoElement? = nil + public let geoElement: GeoElement? /// Image, in the native platform's format, for the result. This should be the marker that would be /// shown on the map, and also shown in the UI. This property is available for convenience so the /// UI doesn't have to worry about whether the `GeoElement` is a graphic or a feature when displaying /// the icon in the UI. - public var markerImage: UIImage? = nil + public let markerImage: UIImage? /// The viewpoint to be used when the view zooms to a selected result. This property can be `nil` /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. - public var selectionViewpoint: Viewpoint? = nil - + public let selectionViewpoint: Viewpoint? + + /// The stable identity of the entity associated with this instance. + /// Required by `Identifiable` protocol. public let id = UUID() } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift index 0ed123792..4423e2d61 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSource.swift @@ -26,24 +26,24 @@ public protocol SearchSource { /// Returns the search suggestions for the specified query. /// - Parameters: - /// - queryString: The query for which to provide search suggestions. + /// - query: The query for which to provide search suggestions. /// - searchArea: The area used to limit results. /// - preferredSearchLocation: The location used as a starting point for searches. /// - Returns: An array of search suggestions. func suggest( - _ queryString: String, + _ query: String, searchArea: Geometry?, preferredSearchLocation: Point? ) async throws -> [SearchSuggestion] /// Gets search results. /// - Parameters: - /// - queryString: Text to be used for query. + /// - query: Text to be used for query. /// - searchArea: The area used to limit results. /// - preferredSearchLocation: The location used as a starting point for searches. /// - Returns: An array of search results. func search( - _ queryString: String, + _ query: String, searchArea: Geometry?, preferredSearchLocation: Point? ) async throws -> [SearchResult] @@ -62,11 +62,11 @@ public protocol SearchSource { /// Repeats the last search. /// - Parameters: - /// - queryString: Text to be used for query. + /// - query: Text to be used for query. /// - searchExtent: Extent used to limit the results. /// - Returns: An array of search results. func repeatSearch( - _ queryString: String, + _ query: String, searchExtent: Envelope ) async throws -> [SearchResult] } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index b6766c093..e310e61c2 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -15,8 +15,29 @@ import Foundation import ArcGIS /// Wraps a suggestion for display. -public struct SearchSuggestion { - /// Title that should be used when displaying a suggestion. +public struct SearchSuggestion { + /// Creates a `SearchSuggestion`. + /// - Parameters: + /// - displayTitle: The string to be used when displaying a suggestion. + /// - owningSource: Reference to the `SearchSourceProtocol` that created this suggestion. + /// - isCollection: `true` if the search from this suggestion should be treated like a collection search, `false` if the search would return a single result. + /// - displaySubtitle: Optional subtitle that can be displayed when showing a suggestion. + /// - suggestResult: Underlying suggest result if this suggestion was created by a LocatorTask. + public init( + displayTitle: String, + owningSource: SearchSource, + isCollection: Bool = false, + displaySubtitle: String = "", + suggestResult: SuggestResult? = nil + ) { + self.displayTitle = displayTitle + self.owningSource = owningSource + self.isCollection = isCollection + self.displaySubtitle = displaySubtitle + self.suggestResult = suggestResult + } + + /// The string to be used when displaying a suggestion. public let displayTitle: String /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the @@ -31,13 +52,14 @@ public struct SearchSuggestion { public let isCollection: Bool /// Optional subtitle that can be displayed when showing a suggestion. - public var displaySubtitle: String = "" + public let displaySubtitle: String /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. - public var suggestResult: SuggestResult? = nil + public let suggestResult: SuggestResult? /// The stable identity of the entity associated with this instance. + /// Required by `Identifiable` protocol. public let id = UUID() } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index c55ce6292..22c25aae7 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -47,8 +47,8 @@ public struct SearchView: View { /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". Note: this is set using the - /// `searchFieldPrompt` modifier. - private var searchFieldPrompt: String = "Find a place or address" + /// `prompt` modifier. + private var prompt: String = "Find a place or address" /// Determines whether a built-in result view will be shown. Defaults to true. /// If false, the result display/selection list is not shown. Set to false if you want to hide the results @@ -67,58 +67,74 @@ public struct SearchView: View { @Environment(\.verticalSizeClass) private var verticalSizeClass: UserInterfaceSizeClass? + /// The width of the search bar, taking into account the horizontal and vertical size classes + /// of the device. This will cause the search field to display full-width on an iPhone in portrait + /// orientation (and certain iPad multitasking configurations) and limit the width to `360` in other cases. private var searchBarWidth: CGFloat? { horizontalSizeClass == .compact && verticalSizeClass == .regular ? nil : 360 } + /// If `true`, will draw the results list view at half height, exposing a portion of the + /// underlying map below the list on an iPhone in portrait orientation (and certain iPad multitasking + /// configurations). If `false`, will draw the results list view full size. + private var useHalfHeightResults: Bool { + horizontalSizeClass == .compact && verticalSizeClass == .regular + } + + /// If `true`, will set the viewpoint to the extent of the results, plus a little buffer, which will + /// cause the geoView to zoom to the extent of the results. If `false`, + /// no setting of the viewpoint will occur. @State private var shouldZoomToResults = true /// Determines whether the results lists are displayed. @State - private var showResultListView: Bool = true + private var isResultListHidden: Bool = false public var body: some View { VStack { - HStack { - Spacer() - VStack { - SearchField( - query: $searchViewModel.currentQuery, - searchFieldPrompt: searchFieldPrompt, - isShowResultsHidden: !enableResultListView, - showResults: $showResultListView - ) - .onSubmit { searchViewModel.commitSearch() } - .submitLabel(.search) - - if enableResultListView, - showResultListView, - let searchOutcome = searchViewModel.searchOutcome { - Group { - switch searchOutcome { - case .results(let results): - SearchResultList( - searchResults: results, - selectedResult: $searchViewModel.selectedResult, - noResultsMessage: noResultsMessage - ) - case .suggestions(let suggestions): - SearchSuggestionList( - suggestionResults: suggestions, - currentSuggestion: $searchViewModel.currentSuggestion, - noResultsMessage: noResultsMessage - ) - case .failure(let error): - List { - Text(error.errorDescription) + GeometryReader { geometry in + HStack { + Spacer() + VStack { + SearchField( + query: $searchViewModel.currentQuery, + prompt: prompt, + isResultsButtonHidden: !enableResultListView, + isResultListHidden: $isResultListHidden + ) + .onSubmit { searchViewModel.commitSearch() } + .submitLabel(.search) + .esriBorder(padding: EdgeInsets()) + if enableResultListView, + !isResultListHidden, + let searchOutcome = searchViewModel.searchOutcome { + Group { + switch searchOutcome { + case .results(let results): + SearchResultList( + searchResults: results, + selectedResult: $searchViewModel.selectedResult, + noResultsMessage: noResultsMessage + ) + .frame(height: useHalfHeightResults ? geometry.size.height / 2 : nil) + case .suggestions(let suggestions): + SearchSuggestionList( + suggestionResults: suggestions, + currentSuggestion: $searchViewModel.currentSuggestion, + noResultsMessage: noResultsMessage + ) + case .failure(let errorString): + List { + Text(errorString) + } } } + .esriBorder(padding: EdgeInsets()) } - .esriBorder(padding: EdgeInsets()) } + .frame(width: searchBarWidth) } - .frame(width: searchBarWidth) } Spacer() if searchViewModel.isEligibleForRequery { @@ -161,11 +177,11 @@ extension SearchView { /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". - /// - Parameter newSearchFieldPrompt: The new value. + /// - Parameter newPrompt: The new value. /// - Returns: A new `SearchView`. - public func searchFieldPrompt(_ newSearchFieldPrompt: String) -> Self { + public func prompt(_ newPrompt: String) -> Self { var copy = self - copy.searchFieldPrompt = newSearchFieldPrompt + copy.prompt = newPrompt return copy } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 94e38b347..6430560b4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -39,7 +39,7 @@ public class SearchViewModel: ObservableObject { public enum SearchOutcome { case results([SearchResult]) case suggestions([SearchSuggestion]) - case failure(SearchError) + case failure(String) } /// Creates a `SearchViewModel`. @@ -175,29 +175,34 @@ public class SearchViewModel: ObservableObject { /// NOTE: only the first source is currently used; multiple sources are not yet supported. public var sources: [SearchSource] = [] - /// The currently executing async task. `currentTask` should be cancelled + /// The currently executing async task. `currentTask` will be cancelled /// prior to starting another async task. - private var currentTask: Task? + private var currentTask: Task? { + willSet { + currentTask?.cancel() + } + } /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. public func commitSearch() { - kickoffTask { await self.doSearch() } + currentTask = Task { await self.doSearch() } } /// Repeats the last search, limiting results to the extent specified in `geoViewExtent`. public func repeatSearch() { - kickoffTask { await self.doRepeatSearch() } + currentTask = Task { await self.doRepeatSearch() } } /// Updates suggestions list asynchronously. public func updateSuggestions() { - guard currentSuggestion == nil else { + guard currentSuggestion == nil + else { // don't update suggestions if currently searching for one return } - kickoffTask { await self.doUpdateSuggestions() } + currentTask = Task { await self.doUpdateSuggestions() } } @Published @@ -215,12 +220,7 @@ public class SearchViewModel: ObservableObject { /// - Parameter searchSuggestion: The suggestion to use to commit the search. public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) { currentQuery = searchSuggestion.displayTitle - kickoffTask { await self.doAcceptSuggestion(searchSuggestion) } - } - - private func kickoffTask(_ taskInit: @escaping () async -> Void) { - currentTask?.cancel() - currentTask = Task { await taskInit() } + currentTask = Task { await self.doAcceptSuggestion(searchSuggestion) } } } @@ -228,9 +228,8 @@ private extension SearchViewModel { func doRepeatSearch() async { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let queryExtent = geoViewExtent, - let source = currentSource() else { - return - } + let source = currentSource() + else { return } await search(with: { try await source.repeatSearch( @@ -242,7 +241,8 @@ private extension SearchViewModel { func doSearch() async { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = currentSource() else { return } + let source = currentSource() + else { return } await search(with: { try await source.search( @@ -255,9 +255,9 @@ private extension SearchViewModel { func doUpdateSuggestions() async { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, - let source = currentSource() else { - return - } + let source = currentSource() + else { return } + do { let suggestions = try await source.suggest( currentQuery, @@ -268,7 +268,7 @@ private extension SearchViewModel { } catch is CancellationError { // Do nothing if user cancelled and let next task set searchOutcome. } catch { - searchOutcome = .failure(SearchError(error)) + searchOutcome = .failure(error.localizedDescription) } } @@ -293,7 +293,7 @@ private extension SearchViewModel { } catch is CancellationError { searchOutcome = nil } catch { - searchOutcome = .failure(SearchError(error)) + searchOutcome = .failure(error.localizedDescription) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 1da01b24c..053f7c227 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -15,25 +15,17 @@ import SwiftUI import ArcGIS /// Extends `LocatorSearchSource` with intelligent search behaviors; adds support for features like -/// type-specific placemarks, repeated search, and more. Advanced functionality requires knowledge of the -/// underlying locator to be used well; this class implements behaviors that make assumptions about the -/// locator being the world geocode service. +/// type-specific placemarks, repeated search, and more on the world geocode service. public class SmartLocatorSearchSource: LocatorSearchSource { /// Creates a smart locator search source. /// - Parameters: /// - name: Name to show when presenting this source in the UI. - /// - locatorTask: The `LocatorTask` to use for searching.. /// - maximumResults: The maximum results to return when performing a search. Most sources default to `6`. /// - maximumSuggestions: The maximum suggestions to return. Most sources default to `6`. /// - repeatSearchResultThreshold: The minimum number of search results to attempt to return. /// - repeatSuggestResultThreshold: The minimum number of suggestions to attempt to return. public init( name: String = "Smart Locator", - locatorTask: LocatorTask = LocatorTask( - url: URL( - string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" - )! - ), maximumResults: Int32 = 6, maximumSuggestions: Int32 = 6, repeatSearchResultThreshold: Int? = 1, @@ -41,7 +33,11 @@ public class SmartLocatorSearchSource: LocatorSearchSource { ) { super.init( name: name, - locatorTask: locatorTask, + locatorTask: LocatorTask( + url: URL( + string: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" + )! + ), maximumResults: maximumResults, maximumSuggestions: maximumSuggestions ) @@ -63,13 +59,13 @@ public class SmartLocatorSearchSource: LocatorSearchSource { public var repeatSuggestResultThreshold: Int? = 6 public override func search( - _ queryString: String, + _ query: String, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { // First, peform super class search. var results = try await super.search( - queryString, + query, searchArea: searchArea, preferredSearchLocation: preferredSearchLocation ) @@ -84,7 +80,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil let geocodeResults = try await locatorTask.geocode( - searchText: queryString, + searchText: query, parameters: geocodeParameters ) @@ -103,9 +99,8 @@ public class SmartLocatorSearchSource: LocatorSearchSource { searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil ) async throws -> [SearchResult] { - guard let suggestResult = searchSuggestion.suggestResult else { - return [] - } + guard let suggestResult = searchSuggestion.suggestResult + else { return [] } var results = try await super.search( searchSuggestion, @@ -139,12 +134,12 @@ public class SmartLocatorSearchSource: LocatorSearchSource { } public override func suggest( - _ queryString: String, + _ query: String, searchArea: Geometry? = nil, preferredSearchLocation: Point? = nil ) async throws -> [SearchSuggestion] { var results = try await super.suggest( - queryString, + query, searchArea: searchArea, preferredSearchLocation: preferredSearchLocation ) @@ -159,7 +154,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. suggestParameters.searchArea = nil let geocodeResults = try await locatorTask.suggest( - searchText: queryString, + searchText: query, parameters: suggestParameters ) diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index 50325cadf..e5fc90740 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -16,29 +16,35 @@ import SwiftUI /// A custom view implementing a SearchField. It contains a search button, text field, delete text button, /// and a button to allow users to hide/show the search results list. public struct SearchField: View { + /// Creates a `SearchField`. + /// - Parameters: + /// - query: The current search query. + /// - prompt: The default placeholder displayed when `currentQuery` is empty. + /// - isResultsButtonHidden: The visibility of the button used to toggle visibility of the results list. + /// - isResultListHidden: Binding allowing the user to toggle the visibility of the results list. init( query: Binding, - searchFieldPrompt: String = "", - isShowResultsHidden: Bool = true, - showResults: Binding? = nil + prompt: String = "", + isResultsButtonHidden: Bool = false, + isResultListHidden: Binding? = nil ) { self.query = query - self.searchFieldPrompt = searchFieldPrompt - self.isShowResultsHidden = isShowResultsHidden - self.showResults = showResults + self.prompt = prompt + self.isResultsButtonHidden = isResultsButtonHidden + self.isResultListHidden = isResultListHidden } /// The current search query. private var query: Binding /// The default placeholder displayed when `currentQuery` is empty. - private let searchFieldPrompt: String + private let prompt: String - /// The visibility of the `showResults` button. - private let isShowResultsHidden: Bool + /// The visibility of the button used to toggle visibility of the results list. + private let isResultsButtonHidden: Bool /// Binding allowing the user to toggle the visibility of the results list. - private var showResults: Binding? + private var isResultListHidden: Binding? public var body: some View { HStack { @@ -50,7 +56,7 @@ public struct SearchField: View { TextField( "Search Query", text: query, - prompt: Text(searchFieldPrompt) + prompt: Text(prompt) ) // Delete text button @@ -64,14 +70,14 @@ public struct SearchField: View { } // Show Results button - if !isShowResultsHidden { + if !isResultsButtonHidden { Button { - showResults?.wrappedValue.toggle() + isResultListHidden?.wrappedValue.toggle() } label: { Image(systemName: "eye") - .symbolVariant(!(showResults?.wrappedValue ?? false) ? .none : .slash) + .symbolVariant((isResultListHidden?.wrappedValue ?? false) ? .none : .slash) .symbolVariant(.fill) - .foregroundColor(Color(.opaqueSeparator)) + .foregroundColor(.accentColor) } } } From a83fa89dd79654ec43fba61f3a750deb7b0ed22a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Nov 2021 08:49:04 -0600 Subject: [PATCH 0327/1722] Move graphic and requery logic from view to model. Other PR review changes. --- Examples/Examples/SearchExampleView.swift | 21 ++-- .../Components/Search/SearchSuggestion.swift | 6 +- .../Components/Search/SearchView.swift | 119 ++++-------------- .../Components/Search/SearchViewModel.swift | 111 ++++++++++++++-- .../ArcGISToolkit/Utility/SearchField.swift | 2 +- 5 files changed, 138 insertions(+), 121 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 633ffe5b4..f313c124a 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -38,9 +38,6 @@ struct SearchExampleView: View { /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. let searchResultsOverlay = GraphicsOverlay() - - @State - private var isNavigating: Bool = false var body: some View { MapView( @@ -48,7 +45,7 @@ struct SearchExampleView: View { viewpoint: searchResultViewpoint, graphicsOverlays: [searchResultsOverlay] ) - .onNavigatingChanged { isNavigating = $0 } + .onNavigatingChanged { searchViewModel.isGeoViewNavigating = $0 } .onViewpointChanged(kind: .centerAndScale) { searchViewModel.queryCenter = $0.targetGeometry as? Point @@ -65,18 +62,16 @@ struct SearchExampleView: View { //searchViewModel.queryArea = newValue // For "Repeat Search Here" behavior, set the - // `searchViewModel.extent` property when navigating. - if isNavigating || searchViewModel.geoViewExtent == nil { - searchViewModel.geoViewExtent = newValue.extent - } + // `searchViewModel.geoViewExtent` property when navigating. + searchViewModel.geoViewExtent = newValue.extent } .overlay(alignment: .topTrailing) { - SearchView( - searchViewModel: searchViewModel, - viewpoint: $searchResultViewpoint, - resultsOverlay: searchResultsOverlay - ) + SearchView(searchViewModel: searchViewModel) .padding() } + .onAppear { + searchViewModel.viewpoint = $searchResultViewpoint + searchViewModel.resultsOverlay = searchResultsOverlay + } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index e310e61c2..7154867d2 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -41,14 +41,14 @@ public struct SearchSuggestion { public let displayTitle: String /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the - /// view model to be able to accept a suggestion, because a suggestion should only be used with the + /// view model to be able to accept a suggestion because a suggestion should only be used with the /// locator that created it. public let owningSource: SearchSource /// `true` if the search from this suggestion should be treated like a collection search, `false` if the /// search would return a single result. This property should be used to display a different icon /// in the UI depending on if this is a category search (like 'Coffee', 'Pizza', or 'Starbucks') and - /// false if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). + /// `false` if it is a search for a specific result (e.g. '380 New York St. Redlands CA'). public let isCollection: Bool /// Optional subtitle that can be displayed when showing a suggestion. @@ -72,7 +72,7 @@ extension SearchSuggestion: Equatable { } extension SearchSuggestion: Hashable { - /// Note: we're not hashing `suggestResult` as `SearchSuggestion` is created from + /// Note: We're not hashing `suggestResult` as `SearchSuggestion` is created from /// a `SuggestResult` and `suggestResult` will be different for two sepate geocode /// operations even though they represent the same suggestion. public func hash(into hasher: inout Hasher) { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 22c25aae7..81330bec4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -19,18 +19,10 @@ public struct SearchView: View { /// Creates a new `SearchView`. /// - Parameters: /// - searchViewModel: The view model used by `SearchView`. - /// - viewpoint: The `Viewpoint` used to zoom to results. - /// - resultsOverlay: The `GraphicsOverlay` used to display results. - public init( - searchViewModel: SearchViewModel? = nil, - viewpoint: Binding? = nil, - resultsOverlay: GraphicsOverlay? = nil - ) { + public init(searchViewModel: SearchViewModel? = nil) { self.searchViewModel = searchViewModel ?? SearchViewModel( sources: [LocatorSearchSource()] ) - self.resultsOverlay = resultsOverlay - self.viewpoint = viewpoint } /// The view model used by the view. The `SearchViewModel` manages state and handles the @@ -39,12 +31,6 @@ public struct SearchView: View { @ObservedObject var searchViewModel: SearchViewModel - /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. - private let viewpoint: Binding? - - /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. - private let resultsOverlay: GraphicsOverlay? - /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". Note: this is set using the /// `prompt` modifier. @@ -81,12 +67,6 @@ public struct SearchView: View { horizontalSizeClass == .compact && verticalSizeClass == .regular } - /// If `true`, will set the viewpoint to the extent of the results, plus a little buffer, which will - /// cause the geoView to zoom to the extent of the results. If `false`, - /// no setting of the viewpoint will occur. - @State - private var shouldZoomToResults = true - /// Determines whether the results lists are displayed. @State private var isResultListHidden: Bool = false @@ -105,7 +85,6 @@ public struct SearchView: View { ) .onSubmit { searchViewModel.commitSearch() } .submitLabel(.search) - .esriBorder(padding: EdgeInsets()) if enableResultListView, !isResultListHidden, let searchOutcome = searchViewModel.searchOutcome { @@ -139,22 +118,12 @@ public struct SearchView: View { Spacer() if searchViewModel.isEligibleForRequery { Button("Repeat Search Here") { - shouldZoomToResults = false searchViewModel.repeatSearch() } .esriBorder() } } .listStyle(.plain) - .onChange(of: searchViewModel.searchOutcome, perform: { newValue in - switch newValue { - case .results(let results): - display(searchResults: results) - default: - display(searchResults: []) - } - }) - .onChange(of: searchViewModel.selectedResult, perform: display(selectedResult:)) .onReceive(searchViewModel.$currentQuery) { _ in searchViewModel.updateSuggestions() } @@ -197,43 +166,6 @@ extension SearchView { } } -private extension SearchView { - func display(searchResults: [SearchResult]) { - guard let resultsOverlay = resultsOverlay else { return } - let resultGraphics: [Graphic] = searchResults.compactMap { result in - guard let graphic = result.geoElement as? Graphic else { return nil } - graphic.update(with: result) - return graphic - } - resultsOverlay.removeAllGraphics() - resultsOverlay.addGraphics(resultGraphics) - - // Make sure we have a viewpoint to zoom to. - guard let viewpoint = viewpoint else { return } - - if !resultGraphics.isEmpty, - let envelope = resultsOverlay.extent, - shouldZoomToResults { - let builder = EnvelopeBuilder(envelope: envelope) - builder.expand(factor: 1.1) - let targetExtent = builder.toGeometry() as! Envelope - viewpoint.wrappedValue = Viewpoint( - targetExtent: targetExtent - ) - searchViewModel.lastSearchExtent = targetExtent - } else { - viewpoint.wrappedValue = nil - } - - if !shouldZoomToResults { shouldZoomToResults = true } - } - - func display(selectedResult: SearchResult?) { - guard let selectedResult = selectedResult else { return } - viewpoint?.wrappedValue = selectedResult.selectionViewpoint - } -} - struct SearchResultList: View { var searchResults: [SearchResult] @Binding var selectedResult: SearchResult? @@ -250,12 +182,9 @@ struct SearchResultList: View { .onTapGesture { selectedResult = result } - if result == selectedResult { - Spacer() - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) - } + Spacer() } + .selected(result == selectedResult) } } } else if searchResults.isEmpty { @@ -349,28 +278,32 @@ extension ResultRow { } } -private extension Graphic { - func update(with result: SearchResult) { - if symbol == nil { - symbol = Symbol.searchResult() - } - setAttributeValue(result.displayTitle, forKey: "displayTitle") - setAttributeValue(result.displaySubtitle, forKey: "displaySubtitle") - } -} +/// A modifier which displays a 2 point width border and a shadow around a view. +struct SelectedModifier: ViewModifier { + var isSelected: Bool -private extension Symbol { - /// A search result marker symbol. - static func searchResult() -> MarkerSymbol { - let image = UIImage.mapPin - let symbol = PictureMarkerSymbol(image: image) - symbol.offsetY = Float(image.size.height / 2.0) - return symbol + func body(content: Content) -> some View { + let roundedRect = RoundedRectangle(cornerRadius: 4) + Group { + if isSelected { + content + .background(Color.accentColor.opacity(0.8)) + .clipShape(roundedRect) + .shadow( + color: Color.accentColor.opacity(0.8), + radius: 2 + ) + } else { + content + } + } } } -extension UIImage { - static var mapPin: UIImage { - return UIImage(named: "MapPin", in: Bundle.module, with: nil)! +extension View { + func selected( + _ isSelected: Bool = false + ) -> some View { + modifier(SelectedModifier(isSelected: isSelected)) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 6430560b4..7ad5ad19f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -84,10 +84,8 @@ public class SearchViewModel: ObservableObject { } } - /// The extent at the time of the last search. This is primarily set by the model, but in certain - /// circumstances can be set by an external client, for example after a view zooms programmatically - /// to an extent based on results of a search. - public var lastSearchExtent: Envelope? = nil { + /// The extent at the time of the last search. + private var lastSearchExtent: Envelope? = nil { didSet { isEligibleForRequery = false } @@ -100,11 +98,14 @@ public class SearchViewModel: ObservableObject { /// search here' behavior. If that behavior is not wanted, it should be left `nil`. public var geoViewExtent: Envelope? = nil { willSet { - guard !isEligibleForRequery, + guard isGeoViewNavigating, + !isEligibleForRequery, !currentQuery.isEmpty, let lastExtent = lastSearchExtent, let newExtent = newValue else { return } + + viewpoint?.wrappedValue = nil // Check extent difference. let widthDiff = abs(lastExtent.width - newExtent.width) @@ -127,6 +128,21 @@ public class SearchViewModel: ObservableObject { } } + /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. + public var isGeoViewNavigating: Bool = false + + /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. + public var viewpoint: Binding? = nil + + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. + public var resultsOverlay: GraphicsOverlay? = nil + + /// If `true`, will set the viewpoint to the extent of the results, plus a little buffer, which will + /// cause the geoView to zoom to the extent of the results. If `false`, + /// no setting of the viewpoint will occur. + @Published + private var shouldZoomToResults = true + /// `true` if the extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. @@ -153,11 +169,11 @@ public class SearchViewModel: ObservableObject { @Published public private(set) var searchOutcome: SearchOutcome? { didSet { - if case let .results(results) = searchOutcome, - results.count == 1 { - selectedResult = results.first + if case let .results(results) = searchOutcome { + display(searchResults: results) + selectedResult = results.count == 1 ? results.first : nil } else { - selectedResult = nil + display(searchResults: []) } } } @@ -167,7 +183,15 @@ public class SearchViewModel: ObservableObject { /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. @Published - public var selectedResult: SearchResult? + public var selectedResult: SearchResult? { + willSet { + (selectedResult?.geoElement as? Graphic)?.isSelected = false + } + didSet { + (selectedResult?.geoElement as? Graphic)?.isSelected = true + display(selectedResult: selectedResult) + } + } /// Collection of search sources to be used. This list is maintained over time and is not nullable. /// The view should observe this list for changes. Consumers should add and remove sources from @@ -230,7 +254,9 @@ private extension SearchViewModel { let queryExtent = geoViewExtent, let source = currentSource() else { return } - + + // We're repeating a search, don't zoom to results. + shouldZoomToResults = false await search(with: { try await source.repeatSearch( currentQuery, @@ -331,4 +357,67 @@ extension SearchViewModel { } } +private extension SearchViewModel { + func display(searchResults: [SearchResult]) { + guard let resultsOverlay = resultsOverlay else { return } + let resultGraphics: [Graphic] = searchResults.compactMap { result in + guard let graphic = result.geoElement as? Graphic else { return nil } + graphic.update(with: result) + return graphic + } + resultsOverlay.removeAllGraphics() + resultsOverlay.addGraphics(resultGraphics) + + // Make sure we have a viewpoint to zoom to. + guard let viewpoint = viewpoint else { return } + + if !resultGraphics.isEmpty, + let envelope = resultsOverlay.extent, + shouldZoomToResults { + let builder = EnvelopeBuilder(envelope: envelope) + builder.expand(factor: 1.1) + let targetExtent = builder.toGeometry() as! Envelope + viewpoint.wrappedValue = Viewpoint( + targetExtent: targetExtent + ) + lastSearchExtent = targetExtent + } else { + viewpoint.wrappedValue = nil + } + + if !shouldZoomToResults { shouldZoomToResults = true } + } + + func display(selectedResult: SearchResult?) { + guard let selectedResult = selectedResult else { return } + viewpoint?.wrappedValue = selectedResult.selectionViewpoint + } +} + extension SearchViewModel.SearchOutcome: Equatable {} + +private extension Graphic { + func update(with result: SearchResult) { + if symbol == nil { + symbol = Symbol.searchResult() + } + setAttributeValue(result.displayTitle, forKey: "displayTitle") + setAttributeValue(result.displaySubtitle, forKey: "displaySubtitle") + } +} + +private extension Symbol { + /// A search result marker symbol. + static func searchResult() -> MarkerSymbol { + let image = UIImage.mapPin + let symbol = PictureMarkerSymbol(image: image) + symbol.offsetY = Float(image.size.height / 2.0) + return symbol + } +} + +extension UIImage { + static var mapPin: UIImage { + return UIImage(named: "MapPin", in: Bundle.module, with: nil)! + } +} diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index e5fc90740..355f89de7 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -75,7 +75,7 @@ public struct SearchField: View { isResultListHidden?.wrappedValue.toggle() } label: { Image(systemName: "eye") - .symbolVariant((isResultListHidden?.wrappedValue ?? false) ? .none : .slash) + .symbolVariant(!(isResultListHidden?.wrappedValue ?? false) ? .none : .slash) .symbolVariant(.fill) .foregroundColor(.accentColor) } From a19ca521bef70122bb199dbe9cba28a4f05196d4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Nov 2021 09:36:07 -0600 Subject: [PATCH 0328/1722] Change doc reference to `SearchSourceProtocol` to `SearchSource`, as the protocol name changed. --- .../ArcGISToolkit/Components/Search/SearchSuggestion.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 7154867d2..24f0acd50 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -19,7 +19,7 @@ public struct SearchSuggestion { /// Creates a `SearchSuggestion`. /// - Parameters: /// - displayTitle: The string to be used when displaying a suggestion. - /// - owningSource: Reference to the `SearchSourceProtocol` that created this suggestion. + /// - owningSource: Reference to the `SearchSource` that created this suggestion. /// - isCollection: `true` if the search from this suggestion should be treated like a collection search, `false` if the search would return a single result. /// - displaySubtitle: Optional subtitle that can be displayed when showing a suggestion. /// - suggestResult: Underlying suggest result if this suggestion was created by a LocatorTask. @@ -40,7 +40,7 @@ public struct SearchSuggestion { /// The string to be used when displaying a suggestion. public let displayTitle: String - /// Reference to the `SearchSourceProtocol` that created this suggestion. This property is necessary for the + /// Reference to the `SearchSource` that created this suggestion. This property is necessary for the /// view model to be able to accept a suggestion because a suggestion should only be used with the /// locator that created it. public let owningSource: SearchSource @@ -55,7 +55,7 @@ public struct SearchSuggestion { public let displaySubtitle: String /// Underlying suggest result if this suggestion was created by a LocatorTask. This can be `nil`, and - /// is likely to be `nil` when using custom `SearchSourceProtocol` implementations. + /// is likely to be `nil` when using custom `SearchSource` implementations. public let suggestResult: SuggestResult? /// The stable identity of the entity associated with this instance. From 37c6e36edcc27452aad2b1e5e369a1fb12825fa2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Nov 2021 09:47:04 -0600 Subject: [PATCH 0329/1722] Fix indenting. --- .../ArcGISToolkit/Components/Search/SearchResult.swift | 4 ++-- .../ArcGISToolkit/Components/Search/SearchViewModel.swift | 6 +++--- Sources/ArcGISToolkit/Utility/SearchField.swift | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index dc938e538..d89b5e1fd 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -48,7 +48,7 @@ public struct SearchResult { /// The string to be shown as a subtitle wherever results are shown. public let displaySubtitle: String - + /// For locator results, should be the graphic that was used to display the result on the map. /// For feature layer results, should be the resulting feature. Can be `nil` depending on the type of the /// result, and can have `GeoElement`s without a defined geometry. @@ -59,7 +59,7 @@ public struct SearchResult { /// UI doesn't have to worry about whether the `GeoElement` is a graphic or a feature when displaying /// the icon in the UI. public let markerImage: UIImage? - + /// The viewpoint to be used when the view zooms to a selected result. This property can be `nil` /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. public let selectionViewpoint: Viewpoint? diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 7ad5ad19f..e59c005b2 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -104,7 +104,7 @@ public class SearchViewModel: ObservableObject { let lastExtent = lastSearchExtent, let newExtent = newValue else { return } - + viewpoint?.wrappedValue = nil // Check extent difference. @@ -142,7 +142,7 @@ public class SearchViewModel: ObservableObject { /// no setting of the viewpoint will occur. @Published private var shouldZoomToResults = true - + /// `true` if the extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. /// This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. @@ -254,7 +254,7 @@ private extension SearchViewModel { let queryExtent = geoViewExtent, let source = currentSource() else { return } - + // We're repeating a search, don't zoom to results. shouldZoomToResults = false await search(with: { diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index 355f89de7..55839bc6b 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -33,16 +33,16 @@ public struct SearchField: View { self.isResultsButtonHidden = isResultsButtonHidden self.isResultListHidden = isResultListHidden } - + /// The current search query. private var query: Binding - + /// The default placeholder displayed when `currentQuery` is empty. private let prompt: String - + /// The visibility of the button used to toggle visibility of the results list. private let isResultsButtonHidden: Bool - + /// Binding allowing the user to toggle the visibility of the results list. private var isResultListHidden: Binding? From 03246b60228ec58a2a99121572e49d41914f4562 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Nov 2021 09:47:30 -0600 Subject: [PATCH 0330/1722] Remove unnecessary `Group` from modifier. --- .../Components/Search/SearchView.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 81330bec4..2bbeab53f 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -281,21 +281,19 @@ extension ResultRow { /// A modifier which displays a 2 point width border and a shadow around a view. struct SelectedModifier: ViewModifier { var isSelected: Bool - + func body(content: Content) -> some View { let roundedRect = RoundedRectangle(cornerRadius: 4) - Group { - if isSelected { - content - .background(Color.accentColor.opacity(0.8)) - .clipShape(roundedRect) - .shadow( - color: Color.accentColor.opacity(0.8), - radius: 2 - ) - } else { - content - } + if isSelected { + content + .background(Color.accentColor.opacity(0.8)) + .clipShape(roundedRect) + .shadow( + color: Color.accentColor.opacity(0.8), + radius: 2 + ) + } else { + content } } } From 28f5d1110e3a406b302920632dfb2b8b55f04e03 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Nov 2021 16:57:15 -0600 Subject: [PATCH 0331/1722] update colors. --- .../Components/Search/SearchView.swift | 5 +++-- Sources/ArcGISToolkit/Utility/SearchField.swift | 14 ++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 2bbeab53f..70c9a0a87 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -243,6 +243,7 @@ struct ResultRow: View { } } } + .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) } } @@ -286,10 +287,10 @@ struct SelectedModifier: ViewModifier { let roundedRect = RoundedRectangle(cornerRadius: 4) if isSelected { content - .background(Color.accentColor.opacity(0.8)) + .background(Color.secondary.opacity(0.8)) .clipShape(roundedRect) .shadow( - color: Color.accentColor.opacity(0.8), + color: Color.secondary.opacity(0.8), radius: 2 ) } else { diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index 55839bc6b..6987779cc 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -50,7 +50,7 @@ public struct SearchField: View { HStack { // Search icon Image(systemName: "magnifyingglass.circle.fill") - .foregroundColor(Color(uiColor: .opaqueSeparator)) + .foregroundColor(Color.secondary) // Search text field TextField( @@ -65,7 +65,7 @@ public struct SearchField: View { query.wrappedValue = "" } label: { Image(systemName: "xmark.circle.fill") - .foregroundColor(Color(.opaqueSeparator)) + .foregroundColor(Color.secondary) } } @@ -74,10 +74,12 @@ public struct SearchField: View { Button { isResultListHidden?.wrappedValue.toggle() } label: { - Image(systemName: "eye") - .symbolVariant(!(isResultListHidden?.wrappedValue ?? false) ? .none : .slash) - .symbolVariant(.fill) - .foregroundColor(.accentColor) + Image( + systemName: (isResultListHidden?.wrappedValue ?? false) ? + "chevron.down" : + "chevron.up" + ) + .foregroundColor(Color.secondary) } } } From 362c741696e95491a53a8db5dec546730329071e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Nov 2021 17:54:34 -0600 Subject: [PATCH 0332/1722] Remove ActiveSource from model and tests. --- .../Components/Search/SearchViewModel.swift | 5 +--- .../SearchViewModelTests.swift | 23 ------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index e59c005b2..76f503838 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -44,19 +44,16 @@ public class SearchViewModel: ObservableObject { /// Creates a `SearchViewModel`. /// - Parameters: - /// - activeSource: Tracks the currently active search source. /// - queryArea: The search area to be used for the current query. /// - queryCenter: Defines the center for the search. /// - resultMode: Defines how many results to return. /// - sources: Collection of search sources to be used. public init( - activeSource: SearchSource? = nil, queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, sources: [SearchSource] = [] ) { - self.activeSource = activeSource self.queryArea = queryArea self.queryCenter = queryCenter self.resultMode = resultMode @@ -64,7 +61,7 @@ public class SearchViewModel: ObservableObject { } /// The active search source. If `nil`, the first item in `sources` is used. - public var activeSource: SearchSource? + private var activeSource: SearchSource? = nil /// Tracks the current user-entered query. This property drives both suggestions and searches. @Published diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 3d3c1fc32..7791aa89f 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -40,29 +40,6 @@ class SearchViewModelTests: XCTestCase { XCTAssertEqual(result.first, model.selectedResult) } - func testActiveSource() async throws { - let activeSource = LocatorSearchSource() - activeSource.name = "Simple Locator" - let model = SearchViewModel( - activeSource: activeSource, - sources: [LocatorSearchSource()] - ) - - model.currentQuery = "Magers & Quinn Booksellers" - - model.commitSearch() - - let results = try await searchResults(model) - let result = try XCTUnwrap(results?.first) - XCTAssertEqual(result.owningSource.name, activeSource.name) - - model.updateSuggestions() - - let suggestions = try await searchSuggestions(model, dropFirst: true) - let suggestion = try XCTUnwrap(suggestions?.first) - XCTAssertEqual(suggestion.owningSource.name, activeSource.name) - } - func testCommitSearch() async throws { let model = SearchViewModel(sources: [LocatorSearchSource()]) From 7129dae82ef143ac900bffb451c7cc1d95e1b9b5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Nov 2021 18:00:55 -0600 Subject: [PATCH 0333/1722] Removed the doc for id as it will be inherited from the protocol. --- Sources/ArcGISToolkit/Components/Search/SearchResult.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift index d89b5e1fd..bed37359a 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchResult.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchResult.swift @@ -64,8 +64,6 @@ public struct SearchResult { /// because not all valid results will have a geometry. E.g. feature results from non-spatial features. public let selectionViewpoint: Viewpoint? - /// The stable identity of the entity associated with this instance. - /// Required by `Identifiable` protocol. public let id = UUID() } From 3809231706e0a983fcbf955dd7cbbe6b77c54d01 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Nov 2021 13:52:22 -0600 Subject: [PATCH 0334/1722] PR review changes; fix tests. --- Examples/Examples/SearchExampleView.swift | 2 +- .../Search/LocatorSearchSource.swift | 8 +- .../Components/Search/SearchSuggestion.swift | 4 +- .../Components/Search/SearchView.swift | 30 ++++++- .../Components/Search/SearchViewModel.swift | 83 +++++++++++-------- .../ArcGISToolkit/Utility/SearchField.swift | 2 +- .../SearchViewModelTests.swift | 3 + 7 files changed, 86 insertions(+), 46 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index f313c124a..538978cb2 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -51,7 +51,7 @@ struct SearchExampleView: View { // Reset `searchResultViewpoint` here when the user pans/zooms // the map, so if the user commits the same search with the - // same result, the Map will pan/zoom to the result. Otherwise + // same result, the Map will pan/zoom to the result. Otherwise, // `searchResultViewpoint` doesn't change which doesn't // redraw the map with the new viewpoint. searchResultViewpoint = nil diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 85daf547a..39607f2c3 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -104,7 +104,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { parameters: geocodeParameters ) - // Convert to SearchResults and return. + // Convert to an array of `SearchResult` objects and return. return geocodeResults.map { SearchResult(geocodeResult: $0, searchSource: self) } @@ -122,15 +122,15 @@ public class LocatorSearchSource: ObservableObject, SearchSource { searchText: query, parameters: suggestParameters ) - // Convert to SearchSuggestions and return. + // Convert to an array of `SearchSuggestion` objects and return. return geocodeResults.map { SearchSuggestion(suggestResult: $0, searchSource: self) } } } -extension LocatorSearchSource { - private func internalSearch( +private extension LocatorSearchSource { + func internalSearch( _ query: String, searchArea: Geometry?, preferredSearchLocation: Point? = nil diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index 24f0acd50..fd4805aaf 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -58,8 +58,6 @@ public struct SearchSuggestion { /// is likely to be `nil` when using custom `SearchSource` implementations. public let suggestResult: SuggestResult? - /// The stable identity of the entity associated with this instance. - /// Required by `Identifiable` protocol. public let id = UUID() } @@ -72,7 +70,7 @@ extension SearchSuggestion: Equatable { } extension SearchSuggestion: Hashable { - /// Note: We're not hashing `suggestResult` as `SearchSuggestion` is created from + /// Note: We're not hashing `suggestResult` as `SearchSuggestion` is created from /// a `SuggestResult` and `suggestResult` will be different for two sepate geocode /// operations even though they represent the same suggestion. public func hash(into hasher: inout Hasher) { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 70c9a0a87..bd6a0d158 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -36,8 +36,8 @@ public struct SearchView: View { /// `prompt` modifier. private var prompt: String = "Find a place or address" - /// Determines whether a built-in result view will be shown. Defaults to true. - /// If false, the result display/selection list is not shown. Set to false if you want to hide the results + /// Determines whether a built-in result view will be shown. Defaults to `true`. + /// If `false`, the result display/selection list is not shown. Set to false if you want to hide the results /// or define a custom result list. You might use a custom result list to show results in a separate list, /// disconnected from the rest of the search view. /// Note: this is set using the `enableResultListView` modifier. @@ -54,7 +54,7 @@ public struct SearchView: View { private var verticalSizeClass: UserInterfaceSizeClass? /// The width of the search bar, taking into account the horizontal and vertical size classes - /// of the device. This will cause the search field to display full-width on an iPhone in portrait + /// of the device. This will cause the search field to display full-width on an iPhone in portrait /// orientation (and certain iPad multitasking configurations) and limit the width to `360` in other cases. private var searchBarWidth: CGFloat? { horizontalSizeClass == .compact && verticalSizeClass == .regular ? nil : 360 @@ -166,9 +166,13 @@ extension SearchView { } } +/// A View displaying the list of search results. struct SearchResultList: View { + /// The array of search results to display. var searchResults: [SearchResult] + /// The result the user selects. @Binding var selectedResult: SearchResult? + /// The message to display when there are no results. var noResultsMessage: String var body: some View { @@ -194,9 +198,13 @@ struct SearchResultList: View { } } +/// A View displaying the list of search suggestion results. struct SearchSuggestionList: View { + /// The array of suggestion results to display. var suggestionResults: [SearchSuggestion] + /// The suggestion the user selects. @Binding var currentSuggestion: SearchSuggestion? + /// The message to display when there are no results. var noResultsMessage: String var body: some View { @@ -215,7 +223,9 @@ struct SearchSuggestionList: View { } } +/// A View displaying the "no results" message when there are no search or suggestion results. struct NoResultsView: View { + /// The message to display when there are no results. var message: String var body: some View { @@ -226,9 +236,13 @@ struct NoResultsView: View { } } +/// A view representing a row containing one search or suggestion result. struct ResultRow: View { + /// The title of the result. var title: String + /// Additional result information, if available. var subtitle: String = "" + /// The image to display for the result. var image: AnyView var body: some View { @@ -248,6 +262,8 @@ struct ResultRow: View { } extension ResultRow { + /// Creates a `ResultRow` from a search suggestion. + /// - Parameter searchSuggestion: The search suggestion displayed in the row. init(searchSuggestion: SearchSuggestion) { self.init( title: searchSuggestion.displayTitle, @@ -267,6 +283,8 @@ extension ResultRow { ) } + /// Creates a `ResultRow` from a search result. + /// - Parameter searchResult: The search result displayed in the view. init(searchResult: SearchResult) { self.init( title: searchResult.displayTitle, @@ -279,8 +297,9 @@ extension ResultRow { } } -/// A modifier which displays a 2 point width border and a shadow around a view. +/// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { + /// `true` if the view should display as selected, `false` otherwise. var isSelected: Bool func body(content: Content) -> some View { @@ -300,6 +319,9 @@ struct SelectedModifier: ViewModifier { } extension View { + /// View modifier used to denote the view is selected. + /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. + /// - Returns: The view being modified. func selected( _ isSelected: Bool = false ) -> some View { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 76f503838..eea0ef1d8 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -11,10 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Swift import SwiftUI import ArcGIS -import Combine /// Performs searches and manages search state for a search, or optionally without a UI connection. @MainActor @@ -125,32 +123,32 @@ public class SearchViewModel: ObservableObject { } } - /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. + /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. public var isGeoViewNavigating: Bool = false - /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. + /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. public var viewpoint: Binding? = nil - /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. public var resultsOverlay: GraphicsOverlay? = nil /// If `true`, will set the viewpoint to the extent of the results, plus a little buffer, which will - /// cause the geoView to zoom to the extent of the results. If `false`, + /// cause the geoView to zoom to the extent of the results. If `false`, /// no setting of the viewpoint will occur. @Published private var shouldZoomToResults = true - /// `true` if the extent has changed by a set amount after a `Search` or `AcceptSuggestion` call. - /// This property is used by the view to enable 'Repeat search here' functionality. This property is + /// `true` if the extent has changed by a set amount after a `Search` or `AcceptSuggestion` + /// call. This property is used by the view to enable 'Repeat search here' functionality. This property is /// observable, and the view should use it to hide and show the 'repeat search' button. - /// Changes to this property are driven by changes to the `geoViewExtent` property. This value will be - /// true if the extent center changes by more than 25% of the average of the extent's height and width + /// Changes to this property are driven by changes to the `geoViewExtent` property. This value will be + /// `true` if the extent center changes by more than 25% of the average of the extent's height and width /// at the time of the last search or if the extent width/height changes by the same amount. @Published public private(set) var isEligibleForRequery: Bool = false /// The search area to be used for the current query. Results will be limited to those - /// within `QueryArea`. Defaults to `nil`. + /// within `QueryArea`. Defaults to `nil`. public var queryArea: Geometry? = nil /// Defines the center for the search. For most use cases, this should be updated by the view @@ -162,7 +160,7 @@ public class SearchViewModel: ObservableObject { /// (driven by the `isCollection` property). public var resultMode: SearchResultMode = .automatic - /// The collection of search and suggestion results. A `nil` value means no query has been made. + /// The collection of search and suggestion results. A `nil` value means no query has been made. @Published public private(set) var searchOutcome: SearchOutcome? { didSet { @@ -171,6 +169,7 @@ public class SearchViewModel: ObservableObject { selectedResult = results.count == 1 ? results.first : nil } else { display(searchResults: []) + selectedResult = nil } } } @@ -193,10 +192,10 @@ public class SearchViewModel: ObservableObject { /// Collection of search sources to be used. This list is maintained over time and is not nullable. /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. - /// NOTE: only the first source is currently used; multiple sources are not yet supported. + /// NOTE: Only the first source is currently used; multiple sources are not yet supported. public var sources: [SearchSource] = [] - /// The currently executing async task. `currentTask` will be cancelled + /// The currently executing async task. `currentTask` will be cancelled /// prior to starting another async task. private var currentTask: Task? { willSet { @@ -219,7 +218,7 @@ public class SearchViewModel: ObservableObject { public func updateSuggestions() { guard currentSuggestion == nil else { - // don't update suggestions if currently searching for one + // Don't update suggestions if currently searching for one. return } @@ -227,6 +226,7 @@ public class SearchViewModel: ObservableObject { } @Published + /// The suggestion currently selected by the user. public var currentSuggestion: SearchSuggestion? { didSet { if let currentSuggestion = currentSuggestion { @@ -246,6 +246,7 @@ public class SearchViewModel: ObservableObject { } private extension SearchViewModel { + /// Method to execute an async `repeatSearch` operation. func doRepeatSearch() async { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let queryExtent = geoViewExtent, @@ -262,6 +263,7 @@ private extension SearchViewModel { }) } + /// Method to execute an async `search` operation. func doSearch() async { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let source = currentSource() @@ -276,6 +278,7 @@ private extension SearchViewModel { } ) } + /// Method to execute an async `suggest` operation. func doUpdateSuggestions() async { guard !currentQuery.trimmingCharacters(in: .whitespaces).isEmpty, let source = currentSource() @@ -295,32 +298,46 @@ private extension SearchViewModel { } } + /// Method to execute an async `search` operation using a search suggestion.. + /// - Parameter searchSuggestion: The suggestion to search for. func doAcceptSuggestion(_ searchSuggestion: SearchSuggestion) async { - await search(with: { - try await searchSuggestion.owningSource.search( - searchSuggestion, - searchArea: queryArea, - preferredSearchLocation: queryCenter - ) - }) + await search( + with: { + try await searchSuggestion.owningSource.search( + searchSuggestion, + searchArea: queryArea, + preferredSearchLocation: queryCenter + ) + }, + isCollection: searchSuggestion.isCollection + ) // once we are done searching for the suggestion, then reset it to nil currentSuggestion = nil } - func search(with action: () async throws -> [SearchResult]) async { - do { - // User is performing a search, so set `lastSearchExtent`. - lastSearchExtent = geoViewExtent - try await process(searchResults: action()) - } catch is CancellationError { - searchOutcome = nil - } catch { - searchOutcome = .failure(error.localizedDescription) + /// Method to execut a search action and process the results. + /// - Parameter action: The action to perform prior to processing results. + /// - Parameter isCollection: `true` if the results are based on a collection search. + func search( + with action: () async throws -> [SearchResult], + isCollection: Bool = true) async { + do { + // User is performing a search, so set `lastSearchExtent`. + lastSearchExtent = geoViewExtent + try await process(searchResults: action(), isCollection: isCollection) + } catch is CancellationError { + searchOutcome = nil + } catch { + searchOutcome = .failure(error.localizedDescription) + } } - } - func process(searchResults: [SearchResult], isCollection: Bool = true) { + /// Method to process search results based on the current `resultMode`. + /// - Parameters: + /// - searchResults: The array of search results to process. + /// - isCollection: `true` if the results are based on a collection search. + func process(searchResults: [SearchResult], isCollection: Bool) { let effectiveResults: [SearchResult] switch resultMode { diff --git a/Sources/ArcGISToolkit/Utility/SearchField.swift b/Sources/ArcGISToolkit/Utility/SearchField.swift index 6987779cc..cde200103 100644 --- a/Sources/ArcGISToolkit/Utility/SearchField.swift +++ b/Sources/ArcGISToolkit/Utility/SearchField.swift @@ -13,7 +13,7 @@ import SwiftUI -/// A custom view implementing a SearchField. It contains a search button, text field, delete text button, +/// A custom view implementing a SearchField. It contains a search button, text field, delete text button, /// and a button to allow users to hide/show the search results list. public struct SearchField: View { /// Creates a `SearchField`. diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 7791aa89f..f10e9d631 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -134,6 +134,9 @@ class SearchViewModelTests: XCTestCase { model.geoViewExtent = Polygon.chippewaFalls.extent model.currentQuery = "Coffee" + // This is necessary for the model to compute `isEligibleForRequery`. + model.isGeoViewNavigating = true + model.commitSearch() _ = try await searchResults(model) From 7643d98f944fc2a0ad990311901e752f64c357e7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Nov 2021 13:58:31 -0600 Subject: [PATCH 0335/1722] Eliminate double space after period. --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index bd6a0d158..196d7e785 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -297,7 +297,7 @@ extension ResultRow { } } -/// A modifier which displays a background and shadow for a view. Used to represent a selected view. +/// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { /// `true` if the view should display as selected, `false` otherwise. var isSelected: Bool From cfb57ee1ba13fdd75db7cb34f9a0f6e95290fafd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Nov 2021 14:47:43 -0600 Subject: [PATCH 0336/1722] Minor Search doc changes. --- .../ArcGISToolkit/Components/Search/SearchSuggestion.swift | 7 ++++--- .../ArcGISToolkit/Components/Search/SearchViewModel.swift | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift index fd4805aaf..49d9fe39e 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchSuggestion.swift @@ -70,10 +70,11 @@ extension SearchSuggestion: Equatable { } extension SearchSuggestion: Hashable { - /// Note: We're not hashing `suggestResult` as `SearchSuggestion` is created from - /// a `SuggestResult` and `suggestResult` will be different for two sepate geocode - /// operations even though they represent the same suggestion. public func hash(into hasher: inout Hasher) { + // Note: We're not hashing `suggestResult` as `SearchSuggestion` is + // created from a `SuggestResult` and `suggestResult` will be different + // for two sepate geocode operations even though they represent the + // same suggestion. hasher.combine(id) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index eea0ef1d8..2b17d2789 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -225,8 +225,8 @@ public class SearchViewModel: ObservableObject { currentTask = Task { await self.doUpdateSuggestions() } } - @Published /// The suggestion currently selected by the user. + @Published public var currentSuggestion: SearchSuggestion? { didSet { if let currentSuggestion = currentSuggestion { @@ -316,7 +316,7 @@ private extension SearchViewModel { currentSuggestion = nil } - /// Method to execut a search action and process the results. + /// Method to execute a search action and process the results. /// - Parameter action: The action to perform prior to processing results. /// - Parameter isCollection: `true` if the results are based on a collection search. func search( From 17c415b9b2462639177da84bde94ab82879e4011 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 1 Dec 2021 18:12:04 -0600 Subject: [PATCH 0337/1722] Cleanup; remove second space after `.`. --- .../Examples/BasemapGalleryExampleView.swift | 110 +++++------------ .../LightGray.imageset/Contents.json | 21 ---- .../LightGray.imageset/Image-1.png | Bin 21045 -> 0 bytes .../Midcentury.imageset/Contents.json | 21 ---- .../Midcentury.imageset/Image.png | Bin 44133 -> 0 bytes .../NavigationNight.imageset/Contents.json | 21 ---- .../NavigationNight.imageset/Image.png | Bin 52142 -> 0 bytes .../Newspaper.imageset/Contents.json | 21 ---- .../Newspaper.imageset/Image.png | Bin 41376 -> 0 bytes .../Nova.imageset/Contents.json | 21 ---- .../Assets.xcassets/Nova.imageset/Image.png | Bin 43542 -> 0 bytes .../Streets.imageset/Contents.json | 21 ---- .../Streets.imageset/Image.png | Bin 58688 -> 0 bytes .../Terrain.imageset/Contents.json | 21 ---- .../Terrain.imageset/Image.png | Bin 52275 -> 0 bytes .../BasemapGallery/BasemapGallery.swift | 110 +++++++++++------ .../BasemapGallery/BasemapGalleryItem.swift | 23 ++-- .../BasemapGalleryViewModel.swift | 115 ++++++++---------- .../BasemapGalleryItemTests.swift | 6 +- .../BasemapGalleryViewModelTests.swift | 8 +- 20 files changed, 171 insertions(+), 348 deletions(-) delete mode 100644 Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Image-1.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Image.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Image.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Image.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Nova.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Nova.imageset/Image.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Streets.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Streets.imageset/Image.png delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json delete mode 100644 Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Image.png diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index bacd88836..db6f29b49 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -16,98 +16,32 @@ import ArcGIS import ArcGISToolkit struct BasemapGalleryExampleView: View { - var basemapGalleryItems: [BasemapGalleryItem] = [ - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ), - name: "OpenStreetMap (Blueprint)", - description: "OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. This vector basemap is based on the Daylight map distribution of OSM data and is hosted by Esri. It presents the map in a cartographic style is like a blueprint technical drawing." - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! - )! - ), - name: "National Geographic Style Map", - description: "This vector web map provides a detailed view of the world featuring beautiful political boundaries, labeling, and background that highlights the differences in the physical characteristics of the land." - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! - )! - ), - name: "Firefly Imagery Hybrid", - description: "This map features an alternative view of the World Imagery map designed to be used as a neutral imagery basemap, with de-saturated colors, that is useful for overlaying other brightly styled layers. The map also includes a reference layer." - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ), - name: nil, - description: "This web map features satellite imagery for the world and high-resolution aerial imagery for many areas. It uses WGS84 Geographic, version 2 tiling scheme.", - thumbnail: nil - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! - )! - ), - name: "Human Geography Dark Label", - description: "This (v2) vector tile layer provides a detailed basemap for the world, featuring a dark monochromatic style with content adjusted to support Human Geography information. This map is designed for use with Human Geography Dark Detail and Base layers.", - thumbnail: nil - ) - ] - - let geoModel: GeoModel = Map(basemapStyle: .arcGISNova) - // let geoModel: GeoModel = Scene(basemapStyle: .arcGISNova) + /// The map displayed in the map view. + let map = Map(basemapStyle: .arcGISImagery) + /// The view model for the basemap gallery. @ObservedObject var viewModel = BasemapGalleryViewModel() + /// `true` if the basemap gallery should be displayed; `false` otherwise. @State var showBasemapGallery: Bool = true + /// The initial viewpoint of the map. let initialViewpoint: Viewpoint? = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), scale: 1000000 ) - @Environment(\.horizontalSizeClass) var horizontalSizeClass - - var galleryWidth: CGFloat { - get { - if horizontalSizeClass == .regular { - return 300.0 - } - else { - return 150.0 - } - } - } - var body: some View { ZStack(alignment: .topTrailing) { - MapView(map: geoModel as! Map, viewpoint: initialViewpoint) - // SceneView(scene: geoModel as! ArcGIS.Scene, viewpoint: initialViewpoint) - .overlay( - VStack(alignment: .trailing) { - if showBasemapGallery { - BasemapGallery(viewModel: viewModel) - .style(.automatic) - .frame(width: galleryWidth) - } + MapView(map: map, viewpoint: initialViewpoint) + .overlay(alignment: .topTrailing) { + if showBasemapGallery { + BasemapGallery(viewModel: viewModel) + .style(.automatic) } - .padding(), - alignment: .topTrailing - ) + } .onAppear() { SetupViewModel() } @@ -124,8 +58,24 @@ struct BasemapGalleryExampleView: View { } private func SetupViewModel() { - viewModel.geoModel = geoModel - viewModel.basemapGalleryItems = basemapGalleryItems - viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) + viewModel.geoModel = map + viewModel.basemapGalleryItems.append( + contentsOf: BasemapGalleryExampleView.initialBasemaps() + ) + } + + // TODO: remove last one (bad url) before PR + static private func initialBasemaps() -> [BasemapGalleryItem] { + let itemURLs: [URL] = [ + URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")!, + URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")!, + URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")!, + URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")!, + URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! + ] + + return itemURLs.map { + BasemapGalleryItem(basemap: Basemap(item: PortalItem(url: $0)!)) + } } } diff --git a/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json deleted file mode 100644 index 8c132e32f..000000000 --- a/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "Image-1.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Image-1.png b/Examples/ExamplesApp/Assets.xcassets/LightGray.imageset/Image-1.png deleted file mode 100644 index 537f371c95dc1b797aeaa4a1b6dd3b9681b2410a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21045 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 A!2kdN diff --git a/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json deleted file mode 100644 index 17f6fd948..000000000 --- a/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "Image.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/Midcentury.imageset/Image.png deleted file mode 100644 index 48735a706b22b9eecc3852842636b76d7ce760cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44133 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πFEQjbXBkQ5(*yIuIuoLt&gkQw8G;qmOST~y{E!A-C!}8S>0Fj7NPMQ1 zN-B8ptzfcqb=4Vl{tT~uz$7`H>y_rC8@k^=LKOAmDmC3E`m`3uyJ4K)@KyVVhP!5U z7blrsqVWRkcGBi>RgcY5yyTS>Q2RIb4|9~*$I1ai6*>d8M61|CaFSLzR1PO% zBW!a_jBxKuazh0TUMjGaM$C z(MG(H3M4UR+gV;O%bOtqKocVVkpLlH?ib zsOeQnUcy8!wZW~udI+b`sS;L2<=M3xI{2mZp6;&3cnU%>xZJB({89(2blNuv2B)TS zJ9q73SNc2OxdNikKH#v&o_Kmk-yl~B8XKEYzK}|Hf`>eVB#fa+A(2#L*vs5R7+=$If#Mv!GP#aOAk2^sA#p{dhEk#g{mlO(8n$7nLrvdYSj{js^`Dg=~@BcldN38;h&LY=V5u90lZ zVdS!*z)odHTeGx(CWmoXDzczQAAl){tIP`>h>T7E|6AYHaPuveY&@F}P=#7oAtFdo?pT zb;Qv}jgO3O*tBuq#Yi4ogq0k|RzEO=1O&?zd80$Evlora(xkCVWIoq(F<06E=QYS--wn z`vgWhj+ueZ1<--CV*(ib2Nu(lX$@t;F>5u72OB!8;Dlk%&A=DBXF> zXhQ2K&?M(rW4W>C6i?f`X3)NYb37PMpW~V}`v9mscC=)K_=3#e=-%p}u#i{$*mlu7 zNx?jCmeLP+5SMdd0bk$f7?+s&<+!<>?E@nt4Y6b{lb;;wYe+S*Ex&(tD~{8A_(RXUuxj0gbvt+NJo0Ua z&*_HtL+_IHJ)IsVpW2NLZ#)ZW$r<-fQ+mNC%*vC%Ushlrd)j2EPM46F$PA71T0K0sz#Vd!UQZY zz|;i{L|Ua(7uR)EL#ZnFp~Ew70%-;oSY*~X=LuF`(_q(6Tp3i)WkP$@sHZR-=)r|@ zW5F6cI56Y*hp>@I&(w4Vd&#LQqm`kdh`C? zQD8}fX+o)(9!Y0VHmr#akB;p(e*yEn?XPYh*uDGhC%qjcEOl~gjfn!L$}M-?*45p! zduVvsL5JLQ(@nSj@i$josoiTZGdM3l{)CO2ws7QT#h-7x_~MIy``cgNd-q+xxasGY zeCm^@oP09J+|Y@xKAI~SqeWP?N)fa%%tg;(O|oZ6C(#sA&uwxFh<(NcXM9x(tqC`{ z#P8;o{0th_BSE?l>uUJSO_4N>xg7f7byhnErty_OX(v`U*c4%Ifq2lckj=0NVt&9H z0;mS1us`D%Ut36nRJ9KC$7wdPF_R?E0YDf?iva++u}Jg5Ns?tG#&CKu1cLu?$)_Pg zvo_%>t2BUHh`I-mON)6Z*Y4Sf^u)j6<}I7F)C_jWnB*PK&X)$5Zs4s{37!M3m17v-GlR!8p!l?--Wc<}cGNzLZdX8C( z?bL-;B2*3!))KZCH)P_oB*B&_xCKOWX<;0*TsP3gbb2C;6>A8>LBXiX*mm%vh*8f) zMRYIM=mLp`P%Jf335*wg*|Di-UtFzwXM+PwRF^JYdf1`czxUslH#Rqa{*q5ETD*vD z)DM2(!oD3lSgq*p>>TG;4uZk{!9jR=;L@c#`uc+9e2V?^Na(9y`M1Hrfkg}E4Gj%& zdPMe{MrV;E&927Na6sMpWIBmY6djgcmg9MPM1=DoJa8C^0n0-fdZ=2pDzHL^Nx-Ct zvn4U!d({y%-92ItijLvu_Y|?ubPQ4hpU&C9D}G#ry&YbXiYK`4ndOLCx!lapg1BF% z&GUn844-+PLr1UaZB^r;HJSPJaRL@^GGV^OlVr{61!!2yXg;axGq#IWmnS}5QSyTC z`NHR52fYWrKsV5|mz5_Jd^ibLOFHq?NRMcWn{sF@{rUMlFdcse14(LhWJBHAr;QiE zi0_C11Zf$@O;`|kX(QH&;56U}n$KV0q=Sq$odZ*U*PDS9jmSHGqwbM0A}9b7s(}de zMOuJ`ghDazOfSq2#I&KQ<>7}PUA1b>p5Y;Wi1C$m>t22Jl_THwwnh6aIPT~p_TPW; zKJ(^fvg2&@^8h_zE+wVMM+kG#sI#-By}6-(pf4KqHDRL1qFmKwuEmq|_ToHq8!g9i z)I_rcaSaMxllcF4TXzEY=~2~*{{Qa#)_bSBlkRjU=>!6S03iVehb<%+4Ip0bovWZ+ zF6hP48OCAY$~eO3dYusm9h608L}r*l5G4W$k$`AI5(pt7`$D$f-`?JRU+4R)df%6* zQ+f0B^RK6tQ|FvIb?VfqDkE9IE1n%?f|0q7-`u1188NP!`K9-~=aEMrzxJA+wCN>& zDD>Ke`6ONsk`%OA5`b~mlTUy8(?s*$}-c= zQjxzoq~$3;G53P2{z=Eu{I|dQSM3Y)*T3;6hq}7>-3|=)bhxW>Vd~hC$E|RmzkBpSPz)8?SkY33 z49t=q325DlIu7l2sOvbRqvOBxJ0JO#_x$QdKJt4L)06G4d7qz-g*8GQXLc+$ExUvL z?AejefBt{$-Md%I?z-!)x4rG>$UuCYKBd|+v5sIP(@hbVR(9^%vu(#Y?oFSX7+rNq z^Rmmjx(Go7a)k{9m9q84Q%ijUvk>?)XoXVb+(w$p2A;yR==kHJ-k>BB!Ldi;8oyJ2*)w_N@ZBz7eDaxrTmycPZ+v!b&sO zI=uu%bhq^OxQlw>^y#w)$(7dD*Sz-X$%$!7SZ}wxUb#+{@woPfBqMu-|>!jX2n8}YBXE3YVOTt zwrn#K#omV9kw3r`nu0Juu8}>h5Naf<)lZdbg#q%acJY30WUTxoW+QLS;-^m$)Ja`f zk~ujYZho$2u1{YDWCE0!8BLe7NUwCDa8W``N6AnSY9P>4i)G7{x0c{ZeHLmtGJ+^v zklg7jg^QY@jPPT)Qkag6hgjFxAeH7mP5@&Y#0_(@3DRfP8@C~4o1>Ru+1208O+$md z_HnE%%s=;e&%b|q%t*-rg9fv6>n2s0CvM)n+2+{nAmMB@J3BQujXZnz?r~4Z$cZC* zVtY3a930z89@Qz5+ z?C1%nz?o#c{&lZ^;=u>>G!_>70c?63ak~io}C=` zO(O22>~%ia0O&8<;iZcd$=o4`3rvmKjyQ@GnW2-C?uMqUtxy=Jvrxd#|%@ zW|xWYM~3DUgHGJ|?4yV>?v`@9DvWc+5*(UAqN#(v5KXLUfCHMdb6YwcdPA`&6QGVJ z3*XcZO*Ok_z`}{fptPiEw#ltOt!>@ei^!pSb!D2P4z;MUa8?c0V;?(mc-sXR4==U# zb(-OIZW%djm1&@B>(Y#^M_npgom=T>bj&w{vvGFhwCzOA11`nxp0rJ*tJOIaPSxPJ z(ByW7#g%@2z!u+(-f@0#WNR+5aor60-Axo!nknZk!&O?(XUD%=B&W z!6$|v{Mcvz)xY}S9Y1*J(T5&Jq+9Q3fAgE){Gkth=WIWH{75Gil77+J!XJ)@9F{nB>Wp5L0b2D{?U*Ar!AYez5Cto z)^Ho08>=}s0h5@2cp*+u$)~lERSRXt-B_}+p4p;8kr2AY)X1Q(aAk_%p@jvOV~b+K z>vV>5OS6Ge;412Lqy@(SHC8CC&Qgk6p8+ret?<1ChYTP%jv0q;x3W01x-ipl-v)Bp z+hMOpTf7|A!LxM9hM*=!Kq7!p){Gc=%*vvU;ZCAr-a$+a8)6LrFcN1B^MK_g7WP@F zo0~mx=G55CRE}zLSJ}$al*1rf8?%n|ZNX6X8s%Pqn<%H z))KzpRwMoKVr}j0qABWBkf~8RBrwcFFnwsySS1391SOJ-x7~JIUtcfW-gMKAZccyS zufO-e`FsEJOJDqdf8+hfj~)5BpZmGbeeQF1JvauO>IOMB*ph8p@m1PkYukC}?Rn&( zhb#kX;Aj8jGq3uIS8f{Y+p}}W@CjSf`p?<6**xRai9_wH%m4NR|K_VV-gw@wofBiD zxBl(lxsBHNJ}|h6JBGFE4}9?7e(lDa80!A^w(tCZzx_LGX@`y;qXza54F2eeCxS)x7emms@D}NAP18&HrD9me>9AyD&`z zQVHHMjj+L0vTa1-Gd|I9X{yQ|=Z2xSr4?d{hh{f-{c>uX@qr>Rvp74oFgv+8I}PDv zXO4i!%Te8}qag|w;oEKj-bSK!TVCBXF+c5Pg+Cb&S{{`s9Gf4>oAl1ZF(3DgB9X|Y z;-J5Rp?MC_|tEAe$lkJ#LK#MVKExP((**^gJumWi9-ITq-Hg;YmX1xJ0KlM3qtf+h4aB3--6W064)v9lb-8tdblEyIxTW9Ki|xHF z?S0LyJ&f1WGuEUCU~Fo+yUsbJJlej@c`y?bE)Xfi{2-oF+!`ShGt%gtu!!$E?PqfQ za7%~vVGRsA+uHBF=e|Gt!k4bS?x%Eg?YjN8J2b)U;T2b2@!-P`dvTHE$jFFM{NRNb z(aY_n(Yl*&{)UC47hL|LJHPi`ThPc7$28vm;6qnjam63}!AGTcIGK5?DXR1C4j+1Y z-@bikPoJ`8Z;QY^cmH7bxqE;%Jv(#Y{Qc1Iw|{r*Uw`vk|LkqQ_=g|=)GJ^0YO?2? z-RGJS|I=5#;+AjRd|=;R$EUkB@dtN(?@e#K{;A>PM~)nEM)KvCU;e}sPuL!O(M1_rm>`o_F4k!JfwS_==N798Bsq zL(94VS)AkWN#e!j+x4mz$mu=?7!Z+~y!o1&dJVtX=!!S7CC6z4x^5O8RH4lqnPHft z8lcUpoN;Oeue}4mo^|#L2diw~=(2BYjv}14nv)i#JLVzWZHh#yQw?pGqFMZL0 z^Upi5@4O%0_k#=f?^{`#@9OR%?6+>-WM`GOnqr!(UUJDn1DVdlJ;!z_mcO2!?gJO> z|1Y2Z&$P`;4j#Dnb+74ZZyD%nJ$dx$ZJT-*r^Yr9^cWpJLwCULKA=3-)05+Fi#v7f zneNS7Hnn$NcJM-8E@stgC#B5k92hY*J2yp5%9iWZO`CA>W(t?1FO3d```e*s9MnMeSxoo@K0EfrZ0sXYu>WXo`>wsg05 z_H=Yb;w~SdC$4<1k(lRueYG^mE_4WSTQ+e!a9#+N>w*g| zpz^=%=YQeO@814__y5KfFMaXTPaSe*@lXHs^`H2}Cw}uc|DBAldChA+`q7V?$zFBU zRd?Qb=XEc8E+qfsKzfIeLg6R_4JweMF$HmIDQ_8Z!dunO0HKyL_@@!X3v*deiLhF6 z*u#Qbe&=U3mkGdthTiJiW97fCmE1Ok@$Jmy$~3XEx->gJ7Q=x7Mb4SIxCX`sV=XO8 zo*hO`z-&0P5`Q@ab7^_^?mb;SeRO*4$?q-}IJ2(R)|o6}V}a}h0YCvdg0}PyMHOpz z#4X!J;=6M|j`)nuVhGupMTQ8|jMbT`31_bM_w^pwcRq4iMLu@=^vu*u1U3N{s~HgG zG&{`JazlAfTg#6A{!OrG2(?M3CfSv7=2nU#yW^LT6^ED!89{1;PzghmRan-LBnxD0Z8M`X?tw$HzvT!hrnl zO5U+`@YK}U&Yk;54nNHm+YUkG8aVg7(KF*io3}ptqh}5epPF7=by=w$a^L=&Z<-J^ zmgZmcikJN7KmO$RzI(ePVV#n_vN-$BU-&q*2on5Ak=(VuLd4h@2gOjg&3({@B0_D-z!^8%mAS_9)a=acLT=?7>K_>BchZp+kf!Ig zWGxb&nK8x}=1-1|otd3naQdV!o{g#w{%#Dlw(s4xy^k&L4t{8CQ_!@9Vvxzb{w8pMmX1pbOZLJeeIFO!*Dz0+z1MVn(i)o$S6D2#s zNF$uTOW@7UW{;#rV!LGBDkE9na+c0TdWRMka$Idf8(TV`mgU8LTeoiS>g{I(;akhZ z)Sulf5>teGdU{Q=Zoc{EtFOLV2|Tx~&n(y>H#@_o;aXa?xAzWZPC_4ZR2g$gybIGO zbrdA7tW&Q*B-{&o>L1cK}X%v-8P%I+~Z~W=6c!7@iT`Hp*`1~ zp6<>=hn{vM@zK+#N%(=M-L}ebP?sB2N&Jb&SX~xRcDF5M(&A zJy(_D$7Sl&mhQd__Us*?ytK7%8aXbnfyYV#f6&S0rK4jb$3{om2M4J4W`>T%+S=2% z)e%)RC`&%eakH^lM5@D*Gf2*kPmCL3Nl^AAMPs@+F9zqb2xY(^tyu#X=DVHL-dLh$EzM1hojKXZJkj1c zM+Io_`O3}TeDvwVtF1i_@WYNTzj)VWm%Y#tU+gI;%PJC~o2_zLsiWBGlJW3aWb*AC z=BzRT>b3s1o36?bJg*#ro|< zdnFZW(vYd}T#YY1y^w@z1sPZ>P6cIB9{qszp)pUSoE_gXu(_jcr7Ig}ios#oc2n2V zw;wof-+32Y^n=G9kw;&ho?B|;lWd`wQs3kX+E%vu>4n9~**P>Bot(m3I4mpttO1zF zU~M|!UwpsPSctBiW@#NbO+Ziy%mft#<&+4F6rC+5JS-G66t03DXo6@6_O1!`&Y6s6 z$;J#+kul%4IckyeZz71|@TguN!gA8Mf^d1cTL)`2^-fLqZr=$Yr6UsL$59W0tWz{tcb(;t)AC$59XNS5z z!Q$7{+DV)n8>j+Tzy8{TC(qn{-^0g$aPM3H_kXs(dx49X^I_OEVpGxOf}zM;2RE?d zAbfXQVBC-G{mx%S z0teIa{VF}iZ|yHi6>(rbN*}f!q_NEYS@Ee7YrUdR(spnro-cd)e{)>l$y-JoE?Kd@ z=}A%gandVa@v?jGyUzi9{nJOH4lc4k2JkdER4`Ar)|7oTfXJMBmD`~6*k-#|dy!E^ z;Tl)%7F`uYoh>U?gQXMUn|A|&_&F}T;jDlLc>S@k%4$YLZ?XDdn(`ff1=uNt$Hj#%=4Vo;n*tKs%+ER}J&|=-m9#Bgc=9k1;Si zqXtw#rTH|+7lRDQ`e@^%T!zj$iW6Ba2`X`a3jpDtV7^E!?}+V}*kH?=l7X*m)eMwn z4KV{ePAK$|axrF%1cE006xt>=Cn*(U=GtBV8OdI*hQP5REd^j&OCbPmIP1BF$NAjkomx|LN~( zF7{U!u^Q@;QA*Az6x)OX^qrhAl|~l*z|w^!kZs$x9Nd52>g<$Z*Pur2QLuuLS~BIh zQJx@&i*zhoMQlQ}qo;Y*Qbm}rA_PWC&}#ItaZ|i{z^hAO?qCCWN?7-|gO0cXFmtNR zXKUt?ODft z5-T2qK*9jfE|7LHkHoElqEgPQFoIy7*t9DAMM}kYm4G1n3q}2?!Y{plf%YxCA)B4j zgn<=zkxy`jc#<}QhzeCigv4W=ngY?q2w|;YWO8N`RaOXFDqr6fm$& zro(WEq&<40*>Hs=V8(O;$B~4&A_PlC5lj=q(DKY{0Gm(TYKv!PtiPG(Do`q>5JSiE zk?;uE!_w!%PBN3lQ$!-SAEypAqCJlKp7Qe4v01j7ld?04y7 zc`d3@O7a3oRa>q=HVBGd2n^g+#-^IcGc7CJYK(^kl1$gB=(%Tc9 zhLeBfEsP~;Bpb^G7LA|aqT#1HU*lV)b#qfNiW-*rrqxk5qarO^%R7pasTauPqNz4_ z)xsA65WaBz0=XCTG}!PYs3Zv)@*z+L;bj6LhlT+(O38YNTnzlcGyJQabY1Fe=xl5o zkVJMRBx5vjRF4wI4}e-?_`BfT9Z+I)EW!>-Lfy0-?Xir+*p~E*lHsF9WWAX`M4&1u z;nF*XK>+KhFf86Wp`Yr-@fu+~O|@Zu|A98geE=v!VJ)RL2Z$TmRSKnV7NI9z0x_W@ z*_IVySg73up|NU*#T;7KOomk}Db~r;569=1o9Cy4I6^nq99aL!0*NI*MJRMR_KO^o zRK}o2_d;fI{8LDu)Nagy)~w|vfEW&xrYT{MSP_#f6Sjz6dJi5T`ZMR!Pqob&f~2L> zhq-!8jF2l_rDOnjW)0lv2g69qgv%%wYd$zy+xo1=VNXS}6P2?q(s^TF6pI01Z zPLo6-lNKGEZwMSLOwYFDkk4v@gZQh6zIOP@>C`eD=@d5JJ{%y{$~y)4wAS z<7gL2E&T`!2ExMLQj=#XJmVY2)sxF4K*?4_NR9QN3TUiDoTay%u9;3iw1&KlTnaE$ zEf~m&Y-3HNo{p48t`00_*ROQx8l__qCn1%_8!s&Gu;>*zPp4C3_fkxbCLV&H%BdYf zBMLU7=f&CxB%|!g@=RAbjd+E}M8h@cak9W8cBEJvF5S8I&?t}v+Y6l?z*%b&Nf~RS z?pI`6?%QKXZ0j1iJ=0 zO)ARjGsN>GH|i^p)1R}jn5q1GdVKVWA!2#CYilOgKZ)|RQ)xxF5)1*MsSh2r&cE)*KTVjr~ z6O%)}Yrt1|{kX>gZfzql(~aPWmabmoiiX(-5rc@`zzdB<$0j&(ETP#AB>2aa8a-*= z#XAX1T^(H-E24Dl^xAYb+{Ta2Gu}`z+R|xrW4Ga@(7vEJNRK9?A4lyEF};TYVKYz> zekBqE`Ii6F>BCzk#6d+*3c#?FfxpUFK0z6!peMjY$OIb^B{2nh_d#J|#7YPZ+7--# zTf?LXOP2na$mk>qMI7a)^y>GlFW$@d>?8G(UR?A@@3mflDI#4cteMnlS#wjT15}Fz zUpc_>rJ}W)gP!h78fOsG@0dq3Zy=Z*fS6VGA`vZ;PR>k2Ku>FbxWHUlM+Y$ovx(kS zkeQc33aJf%f)WoJ4dI$$B_9Ksn$wg{9fn-X;91aK;7OW{YY8=8(%`14$>G}$w6%1G z(K%`QD!x2p3PQ4UxZ>6m_nWv!WXjYD4YzulmOn8TqvOg zFqc_s@Qx4x9}FeWvK{~cE4N8RK~!2)Qmmz8S^yBhY)zh@FY0+l7tD^&lsX-(gE9!r zGhAqhP_+Y|V+kSExf42EKgJTyr;V<-iYX#oLNcy$N>&cV&r>#YtPsd#C5Z^0wM};g zTeUM20dXsi)M2YFUCaR)Bm%7d>!t7$Jc&tU2?cd2pMx^4(q!+5SUaPaBBcljxzn@i zEJm&p6`TO2mgWiXoDEf3J7J9vIpWY1RYLjF03el9a zbtNQ9fY3NTu$5(3d*#|Bw`+yUB@K-zQ!HoM>r5$}+{!eyoNvfMLhqtT@fXO%1nU*u zGmeTZ4>WZ%@i^5|uofL5X8_WF5ys@aQMS()7(zg0re*LoKNr=6q7!i+Ox93vSU@kp zd#hj7p6!KYf3tDffG97zWI_@;QkYV{LS(6kDmYPD8GH*$jjc+nrH9sQT(YK71<0T( zB?u&0NkVSn&a78?5m!|0#+o5i%PZWvqb_o-% z2kfP!JWEebbowJE3a9vmrLvw?q>EakdeQEq;IA>FV#SmiG8B4|#bLW>BcOZ=F@%xV zXJydGacO@l!cy!qG%0rK|;?O=}BC1CE@_Xivh0Gg(!vgmPIoB zs<0FgfIzOfey0?@rUYaP3Zp@!WIZrFr>O7%BLXFsq*h;qM;KKW!dpWvEQZ4()WBO9 zt6;xOyNVnFjiMp9GFGZD%`h^gzCunl)_Blsop>*DRZEf*Izv|GTn2adCr0;*9iW7g zCE1Ay{}JBS6NpQ*4ic9-6*DQxQmCFpB~%!m&0E1YL|}@b0@{j>5())n%zd7Pqsly` zj6@i*4m?T|BoH*;lie<>QRN+k>Z~oQ%$(F|=^8mVUV=ruRen2)cZ{2o8y*_`YUqZo zGfOKoHvQAFTAELvIz2x-Cuwg_&p>x?pRM@RPm1=$C_1J_eW_?O=wigh<%vvM8(pr?tv)YL!TluZqo3SdbWTmm*-wTQOfD8_*?CqoklvSGYJ0 zOBDhJ2C@JGxy=8RkV23!ds$sI9fMmfhp+%z>2nf;GzC;H!dUdTl#uybPNPb_C8MI! zu+&ym?(*V+k(ELG)quj`MFUm=K%`zPROJQ$fq`wy*8Un&fBK(jRp{#|AuAY2O*QIT z$xWi77#o)0je6*`f)qG6Ij^K6<==aA5QHZQZ>+OU+$p$48H!9yvQcCNtZ< zKGWp_neow~?%rM7ckbS7lUlSQG*DX1V@1j;JKesFTBXs&DuC?dN4aPS2y3la|3@Ka z(3${#`mO|Gyp-u(AzJ*dBdrS=(J|;^iSSd8E^H?m5rF?Y!QQ*jISBQR=%@kFA(Rnl8r2Kq^TR%1$CPumCYlNT$ zO;1Tslqtt^x)G&bYo&}pNA?^Fm1fsMylN#Gi-MB%Dy}8h+)3cBC{Nkp3*x=hP~TE3 ztc0Y6MZ;%Nw85bqOD-AG3UlC+RGyiK{AzPM8L&@>?jxvb>Qn zy(rBOHPQ{WRC_ffBa2wQq_20Zv3l(2;l<DO7h81Bg;lL=;dxYoupM7A$#3RKO#j7rmvcH!CRV z;%TO$SQSEr=rd+1ngjCe1%bW{J?_&90{y8`9%PDCD@cOJf@En69|Sn2vSLTd8Z8Jb zVA|61ns>b24+Vj#j#X=93(tz?u445fq=GmVr7I;!9#To*t0q}!(jE~oGVg3|$Ly8i z(xI?IQWw;yQae2>7Sh`FP}8i;m947D6nm;5r2sYwWlSi!YI+`VzTyVX<8}DUV#?wo zr%(wipat&0?&mtS)@v;|@*`a0YaIbY`}7k*&x<0CFmWV^fei0j>ed;&_~g$|fAPc@ z(b6(&bK~mr#L~jp!u-VI!nEb3rp6o-JTj;v!Xdso);@RyUDCx+yvpfbMiSD&-{sH- zcJN3Kgat!zulrFtttgja1bA8vVAu{#3UiCBtqR;F##iSbDJg1+&2$30XcXd{W5a*Y zCa_bd&u-10oE*!uYxizfi>TMddec+WT|GVe(CGMhUvJ;U6j!=shw|yLyf&2ft#AL$ zIp^$BZqUl<*IdLRZv>BHTbZ}Y^+Q#qjrc3ln2+hRfwLL@J@esc_AgSJfid41;#M6`tDw zfa{j@H7I%gHHRr6H4QPAbq$J|XX28DXNjUPm;pzx(eey54?KO?DL0!tayV~G+oHn+ z;`~l$S&T1IWQ1+LMPV=UB4Dp0;P3qlyfw6x1mFQbBHCer68Xxz1|6frH0$&?ef_U* zzvIqdde5(X^=mhGcK0%U+1m88Kl`)ipSx*jXzNplp5YyO_}J0GzW(hy_e@Vr_7843 z^wiVlx0?nAogeh2FMM%gddhukTl)K934zE*E2ssGh|=_R(uFVvF)E3IXrF`D@IyGF z^cecSB+>4mV{Jyh##=$LR;7Q?BT63<0iif6{v)OLS2|VtiJ@T1qQJs7Yho<@GQx>V z83#D)a1oUu2V-^FWRXHQLU`H7R4q^f5TCfhSjvq41-z&N=^mL%M56MNuS006*37^R z>NGkU9>ByqOIgo57?}f*>M=58XwT3DjU1PnEnUTlE=8r6bNnGW6dQV!K2gVFF@n-l zB}j^zuhJ^6%D<7UiYROsDJ8q1$`hN0iy}D-zZzNMOKIn7^jsVypAu?wFkHw}WK0HS z2#_g4NDDHpt}))jm0{<2eXV7OG|6RBQ5gs9K1)N&=qaw?Mp>(~Z5FQDo;ou+;BFO{ zlt!#ll^j~s!N-r+H>k63F)J}@SZ=oJ9mWkcJnKskvNk3pK3!$K6>S|kLdR-P4p?4Y zoSK;%7<34zV-z}xoc$LaL}G0>DQ05z^5wt$in-)@d-vaZ>#c_mAAbAW-+sdlH^^=i z$BSQb<(AFcZ@J}`2OoO$yWhL}+H0>JKFKlunEe449lU_f!sJi}`E<^fH~jLu!BnT{ zRaQZq#sE&U1HJ$AZ>Z>@MFdpolZj6Qv5y6ZqS+X^g(ME)h!k$GMOO=vrBx$rh7i;R z#EN|dg~|lQS4qNLQNZzEk)4@zr1QyCYL8B%Fp&yVc~q9tM=f`%I=zCVQDSi_kuhFI zG>od}p%j0k0z`mZYf(zW_4rX45mA`{M4wfnrA9w14Yj|pkZSnIgi&NMxJVX5j()&y zRdA&Sd2lmNAKuyM`zp}6eEk%)D+8WLv1k!T5jUU zlkvjd*v0sU6y+U+S^yqkAfU>{2x(m5xzauqiik{AjTR=3uiyL)+p_VPdDa)c@WtQy zcOU-x%{TY;54gVSi(mM{^De#ghOd0(u^&Bg;KGZXctfyE&&+-HzkY6Vdd9`lz5N4j zI@Zygv*P)#cm2|T`OKf%GiMLk#g`le9-3ec^o(a)g$w}<6rN$S^$<=dpodx3HH=V2 z!-|TKx6(&(p@u0+`iw#78d^p3Hv=}JF+%)=LZjD)K zEoujh5M?>+sPu3H4l~=Lka-5e(gX6T1XwnFpl*bf4uYR;DK08DFX}?(6iA#(p!rg6 zWK@X8X_vhyj()Ic!yFa!U}+S$3nOo|A&wb(l)v(>FR}vpi;xdGi_RPjF-I10 z3rjkb9UQG~ORhKX=vip#7@J=@G<^E#>9dpbi?iBeI1M(eI%;`Ztw2Z)+AIIAGZ3r; zwFd8+E2XDaaGr`$B%RAXlE$NtK7Q)VS*K~(n`z6?@TuV^pL}x1&TYh%#H7@P7aSZu zar)%&X*-pE@fZHDJMXyb{Cx*r_u6Z|dCT7%J#vC`Tz6OB;X_AH96xp2cka0Np8JiS zTefU{^pVF^2dc^j(Af0x2k*^4j`ZaNSL8wY>UqWrNtq&J z_0vy3{pBzJ)vI6o+KVnaXsD#HUvlxKcinxrgSbweI^{|;jdJKDZWtWupPrgEz>SYh zPEAjoyJzp{*xAvMaSqDv)avQ&0$N9B+u1Xtd$$cd^w0ylcAbMeySmyI7H3)0GV%T7 z!}oyzD+H`EBXfX`vDVKp4!NSi_yw$mu~^ZABg__wp1=W4u+A?HhDv-?3uh@1;q7hH zW4?+kX@C5vSddJp?}`;hqDv5^(XMoRF~Ep;i@H-pQKCXo)q6Vgs+R8=hFxc@p04iN zAEGH2#zNJk;mU%*MYfR{f==ZbGoQH_m%hbC2h@N;Jz^+5(knK3*F&wvuNq%}O7F$t zM_4MxFi%=T^rT32z-W9nKjJNezOi2ELRWtV#D@F&oT+7TWMOGye06q)cjW$EyY}ze z>3}9IWeJn7l_pQck2H)0w_Y|Ft3sl02=KYVSj#Ov;3Salz4zX`@4m-Z+vHvK_=Y!J zXU~OUpN2$w?b)^KWVu4z4l+ey08OA;?btRnd}@>iCep!OoO3))HbbLjE?@2Qqr=LX zoE(c9?TDd8TiIi}Y5Kh%ey?J%Hq*Nh1Qpkdjh}#nmpnVdI!GlTa6)=uRlFi#w3+{q z(kc^86l?EBb}xeT8wu)rI$-gR(H@;o&jC6{fg=f$CkpRcsMXdV{Y7P}WnvWR^S@_V z5<$p>B7@7cpwZG9|FaM~eI|m$Fbx=Lb%7uqom;a>YXG2J_ZIVy`jfb$^GL;MiY<}VU}`=`0=7piInD3Whsy< znDy4vyw%s_D(nfPetIWWM7s7NTv-m=sqovm9{>sqN~{u4<1z!MbyHR4XH1j%jofhBDzu8#P8N13ni>*;P(FG;$tfMlMps!D3{!O`4B9_UM6~ z+gkhka$Q|CcNe^4c_yPOf-{b!GsP2E#hKC)hK7z*mRb9X6Aq)~EG*g}f6ouT|E4#+ z$!!mS^eF;Cs-m7mlIk;m&!|#Ttvw_Q-k>EB>%4`+&;{*+7YU@(C)d^r{Fd0WrqFhb zA1CU@mQt~BWpZZFz%}_;VGsfvMOj##!_t9LS_GeFMN`v&?#Tm~5ONEu2puJuz@`zwSX}bf+_?PxPmCVJ)0d zq=&N-W4VdFD(j3ei(fGrUN*H1%b9-m^>5j}b<0H;9I!HOMAAo;nduXiNI8MiY&CLJ z2y$W@62O;b@nFhdFs8hauO`D!h4PX3n(V7jQH+u>8A7@G?t?@=$>6C3n1B>2SHR9Bpw2_W(*VI7d|jlMcLR7O^fdGAx0GqnV{9XN zf;E9xj>MXf+YHLGm70yGGtoWDBp}5Ss;UE+0WQZaLjy5Ygr4@NJ{NX{?<~$1bzAA_ zOH0cp$mv>LJ(x$;j~_a6?#^u_&3sFnKU~cEm8(HB7vKPmr%0u+K0L%LJ;PwUyRd)w zDkgxXiIKCHKIanW{ZT*}31gEC4T$Tcji}EC`ZTbj4zB{mdo@xzPQ`j8&s5NxVa@Y+hizk_W!L5OyMK5ngKWGhCSym(xw_FyvE+jTW#eFo=R3 zj0UC*PGPe2RmS>aTC9wv7*)%qUV#+~NUA19QbXlMFTqEiGg+ezPT&KJM&|oZdQ|}x zCrMRMK%hvuHU%kgYfi;faA8$P2{4@lLY()X!#Tj;k-2_`IqTv)zm*WPgzal{GoG3& zy)jCSD$}~tPP>fTYr1X0Uziwj=p8XAF+@<@Ee59{nHXjA1Fzns!%TE6L#PV}R=fPv zv7EPT3ay#P#wO3(Gc>j^HM`Q>&EKpV;_*{9uKk4Vqhad$wGI7k+)e4!9iNr z)75psf%EyuLyf(cWE7eZX_A1s@V9)SLr`rBo@WS)fN&S@ebxX>TL}sm&COGe7+dej zL8B}sq@!1666M22z#Klooy=auOD`o^QZok;CHN_%Z6&}N_i{&zWm#8`AOwAxl_OD%Mh&ZN}wXZ z3u$Lj1r#OJyc(se3-Yc_*NgnFReOS2Que1h;MDk7&zqW`d(p){O-wkH<<#8t6b(`0 zfq|}L<0NakIl)vUwH82Cv-432%QeQz`2;>21YwooJJ%ls5Gc(=Sg(tBhPO;9`~U`9 zWmC%|M~?35Ze_KbM;YgIrhCf9n~+0Jnv%Yo?&UwDSBhi4!`KLtEzyleUw2PdFKt^R zX(QuH$1YPDShiwn8c~tMK8?33-3xic9uU9wwfrMS_w%el9NeOpXR#3k)d^s%K{_<@ ztfkbXH5=?CMC3p~Va7tH@Ds9zMun3io3c@4(P-IUBBAR6|0w{N2x9SsC0T@govt+hLnFYVrq*>PspnbUBO5r*V?qv2xPZG zHXS3_L1~l!f*O!8|2Sf|4E`B0*I0J3iE4W4>ZHby~ikB zWp&s&sx$Nbd@C6ATbi6{%UK{5Mf_y_2_I<+l+gNcNed6&qQRPAYjfD#z#bH-JfOg7 zw7i^E7F5E*Is${DjS#XXtbMI~;&+Y)D}uj#t(ebiRH%*M_Xac7)D@NK?4`R)S&L3q zEL4aL5dy)ZGMpq0ya-hoxY+I4xfg+C}di1)_RA9;t|uT^!#X+;wphF zd#SP3>m^EWVX4X-z)AH5iYln;@TWxr+63GZyro5IbLa&}fN0#AGbVRgj2A;exB1hc zoW5E|Q-?9MwwyXKJbp6EwQbA=L$Vv+jG}AhrwWGra8k(~a zSKH*A>!I;rk(32a#8XT3sZxuI3R@;pO$HYg)%RL^^be9V+nNJmTAS~sZ?fO8OP1xD zfUd;MEJ=Wz1wPa_FgH0lJ2tj`XrQa9dE4N?9%n*s-Q~bCJHs5wWIv-*yh#FD6XTAY zDLu%}5E%IMpS4?UiSsh;O1%NFG$rlTwz$s&j2KH6Rt={aIHkj0{w!|d42>#^WlXOT z7Mhd&M471Y+A^^;opg*R7_9iQuun}o^DRmN#s`Rz!!ki86HF(WLxi!QSbQMFsm`Hj zZyy~U?HlN4`lU&GY64-Lslz(wq;VBV5PHoozsnCz0kdZfVuI^J0Pi(J&2QRPLpf3* zucg%?h?I4p3OuBd1r0X|T7NAi*K#T?J*zBCjuMc+_85mxVhT+u0x_ckCX;@0Y6|>T ziYQ@V#aj-Lhq5xEPN^AwN~q1?1qudLi^GK}umr8B4GAZLIs(h6kSZ!9o;pm@l&^v*WP~cCWV-GDgmRiFA znPZhfO39FiSE4V38&nOdN)^-vVrn3WTYSuX%D02Vj{Yl6%hh8!fRk@Z>0ka1cQ z)2)y-tZDgHt__rWsp*yPYZ9%8T07eyS)bF{QMzdTG9V{J({S~bqHq@J4d*(s#jE*@2t$n>t_v<9_ zGx%u$u1Zywrg!h%_t;~PZQr^L^Sj6ir%a4Ffs8=X3ZkyX4P+5sP&k)m9K);;5o(Qj zOg~`LlP%frIA_;`4?g7j_paW)C!Tz2XmHTtGad;{>j6Q%)@RFW-}R35`1~tYfMpHa zh!?Z*9{z@$bfL9Sazx0Z(3;>Otq4_7PK=#%u9A6LBwHjwSRlE*E40MKMv(8(sg~ND z6RiJ)s1bDPNS2iqvnoGIKh5SW@0sa22xUI|^b{C@&K6J<1y39%SCvw7<;CwT74m6} zeo+2Wa!6PDOxjb$U2Rf%I&0AP0F*}{ij1QpV^Gq=Ar#$Rn+6BdF>?g21m+2x8QRF* zsRbJj7ainmUEPuk=lN7!eHa6eDK$=8&Apf7sQDlmug}jig!88ONxB83RMyXb|B|i%+wy&myAAODBf3W zQ8<!m}VXPvG?m8OkZCGBWze zV~=m$vhA@ao){Y))BG!6`Z7Id`_`Sa(<3l~cPe{?jwM8ct{;4RHcOF;HC|V*J-%kp zn>Mr=t%Vx76h}ySA+)?JiszdyDsNNnAW3aiP^JF#zHn_MUx6ol3ejbXj!Il~HO@Rd zW4%+{V%mwpHpP~iROleF%v^Y>1f||G_9kTofE}&^oM&O{=g>SY2R10$!Fp2el!_GV7iII{34D@M~7|MJUQckX`P3tv1kG5y9jzU8mJcB3dpPwkI7B%vqqt@zzgcc8Q{H0V0oNQEC z+`E)QWRGMS01HDG;RncS_pdAxmXfTnlEQ_u){&RmJwmL)_W?fi$i9O`Vu&h}(WT9b zDTwHsY~eYHTLRU}!sjjaj9H|VrA16hIk4s2 zwx9Oa9u#ul0Withp1uJW;J6BE@A>;RO+Cj?p2E6(L9f30>OFh*eEL&={4+oErpumx z*>C>Vhu`q}Yc_2g8Xp;Zyd~H-jNJvoL@hS^@~)vx zE450>YM#JY=^KU5YMI*LrLuBIcREb8RXklS&ZS+Bc@K0YzDh^Qn6E6$iL7i~OdhT` zR}=}fNO)7^@~4A2G{A7jUNz(BDg@->(vtIW9nv2mzS{2K;#I3HIR-PweKwGqv6P*x1r(fT35qz2-IY3@YoZ;3KR)4boEEom z;B_YoG&i|@sef?j`~w%9K0C5y%jWOibI&E0Jon?D{FK{JjvPIH(~URp+H=x4%6+JpB6Czy84o9=`wn``_?W*FAIO@b2B`e)z*5&RsqC z+~a(2u3ZoqWG+k>{H;Ae6m)H+7_kKWAq!mmP;fI%p7Xz84yaVsDGUl>KszliXFO}Tf13znp_Xj%FdSNbDEZSH#g30UOlg+>50s_ z>>M&1UzxUv4p-tCTva-Pf8f2tbtX-2VruVqJKWN$LzrDM<9J)4&Wm zF%fbvo@=(nj;)9{0g+?8$_XCUO>J>%cZLyr2*JlP22D?Muxa z2B+55w%+EBzSZ{rM%$1(99NqAR+@V3|4>dWg3<1b_xxdu=X#G0K~!R>7WZ!E+}->| z#6tB-OMA}6a&+zr_&b+dI#@S3(U%1X!|qIL^L$%}y91AoOfR8B2hYF!%9Cd&ckDiYe0t$K-@W^q>)!Zxx8L>r1A8Jh z^(+KS2S>XMdM!WoEAOc+hRetX0VfJ|gv##>Tj4=3o)IAaDF)2Q63T}h*a#;^2TG7# zl2pP2GxMbwlrmzKj0*CXI{Xe>re4zBpYMjC#84W_Xqk~C<3o)o^G0(lr4f*+sd2o_ z1=LGWxdEUG8)X-uVg#rfJ^Cp;<*v6S%_%jCP6bMnr#>q%Wu!(q(KRu^3doV-B`Rf+ zS26*h2$Wo>ke01=(KTZpFrtYIh2O!D0!xSn05PbMHU{o|3p^>puK-zMQdx2Zx0wNF z+|HPhS!+}56E2-woS${Okz1dhdhADJ*ueuAEH2DmddVe!`Q^Xp>+T*M8#(;U)BoaM z{OS{rJ$CcWH*s^=vSpL^2OoN1ZhCrT^z5Ohp0e(L-5aib;_=5u&z{-8Z||w$6ZhQx z{lWe|Zt&m#-gjw%K21-$8)jB^+;rs&4k9mz!01WHrp;nL{n*2rBfw4_O1$J}VUE)3 zEWy?wr;D>ZgH4ID!dWj^V`R!ne>JZJTBqUND3r5nl2XDb z$JujB$qfqD$mew}Wb$Z#gqda}r=+S6fOe4v^2ptE4}vSPFt_Pq zGEw`G5M*Gnd(wwJ>|@*Kb*xoLC;68tP~t=<2!R;4ZgN zbL+F624!)Bs&NK~RX-JoMGz~MmR-v6Gg3N>d^!3-yJIz-pZ65wCv0no^wLB=xdyP< zW24s=fs%PEHu(61_emjW9?Z%nzh`ovInA-qmV0FEI_t2M^Gq_7l(5@1=IP!;=RX6%izQg$U$ zno$cZvs&pxmBP@WYiUy~_oZ#}`MMFIjSUFIH*s86L>`A~_DXnHHT?>Q?3DSzh z^)tb0TZ~oobX--|S&E#lj8&7=me#6iS*7&gpPE?ZR!FdpQg~ady*4jLKm>Non5wDh z1%ST6fr-&E_p!T^+o8?hx%r!0y1PiMi-*?k6<4)vo@PJFSD4ukT3|B#fi1j;dw36) ziM(fpFu;^KvPM<(ilKNEFqo{Z8=7O4h0d}p7Ba7_{7>P`b?WUCQgi?S002ovPDHLk FV1lYE_9XxS diff --git a/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json deleted file mode 100644 index 17f6fd948..000000000 --- a/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "Image.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/NavigationNight.imageset/Image.png deleted file mode 100644 index 2f67475c133f9656f1c96ae31db1811f60713a6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52142 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 LNkvXXu0mjf8jjP< diff --git a/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json deleted file mode 100644 index 17f6fd948..000000000 --- a/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "Image.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/Newspaper.imageset/Image.png deleted file mode 100644 index e69d7979f94d74d90360dff0a9c9988d606d1c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41376 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

`$G1(;r~^2=jgs8PSN^F~(!)?fKK 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 diff --git a/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json deleted file mode 100644 index 17f6fd948..000000000 --- a/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "Image.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Image.png deleted file mode 100644 index a719c929c15a27a026b242d71e3d84ee8b1f73d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52275 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

SI6So=t`BdDf>ZvUk8(khY=u$vLRf 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 BasemapGallery { - var copy = self - copy.style = newStyle - return copy - } -} - private extension BasemapGallery { + /// The gallery view, either displayed as a grid or a list depending on `BasemapGallery.style`. + /// - Returns: A view representing the basemap gallery. @ViewBuilder func GalleryView() -> some View { switch style { case .automatic: - if horizontalSizeClass == .regular { + if isRegularWidth { GridView() } else { @@ -112,26 +113,33 @@ private extension BasemapGallery { } } + /// The gallery view, displayed as a grid. + /// - Returns: A view representing the basemap gallery grid. func GridView() -> some View { - let columns: [GridItem] = [ - .init(.flexible(), spacing: 8.0, alignment: .top), - .init(.flexible(), spacing: 8.0, alignment: .top), - .init(.flexible(), spacing: 8.0, alignment: .top) - ] - - return InternalGalleryView(columns) + InternalGalleryView( + [ + .init(.flexible(), spacing: 8.0, alignment: .top), + .init(.flexible(), spacing: 8.0, alignment: .top), + .init(.flexible(), spacing: 8.0, alignment: .top) + ] + ) } + /// The gallery view, displayed as a list. + /// - Returns: A view representing the basemap gallery list. func ListView() -> some View { - let columns: [GridItem] = [ - .init(.flexible(), spacing: 8.0, alignment: .top) - ] - - return InternalGalleryView(columns) + InternalGalleryView( + [ + .init(.flexible(), spacing: 8.0, alignment: .top) + ] + ) } + /// The gallery view, displayed in the specified columns. + /// - Parameter columns: The columns used to display the basemap items. + /// - Returns: A view representing the basemap gallery with the specified columns. func InternalGalleryView(_ columns: [GridItem]) -> some View { - return ScrollView { + ScrollView { LazyVGrid(columns: columns, spacing: 4) { ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow( @@ -153,6 +161,7 @@ private extension BasemapGallery { } } +/// A row or grid element representing a basemap gallery item. private struct BasemapGalleryItemRow: View { @ObservedObject var basemapGalleryItem: BasemapGalleryItem let isSelected: Bool @@ -160,6 +169,7 @@ private struct BasemapGalleryItemRow: View { var body: some View { VStack { ZStack(alignment: .center) { + // Display the thumbnail, if available. if let thumbnailImage = basemapGalleryItem.thumbnail { Image(uiImage: thumbnailImage) .resizable() @@ -169,6 +179,8 @@ private struct BasemapGalleryItemRow: View { width: 3.0) } + // Display an image representing either a load basemap error + // or a spatial reference mismatch error. if basemapGalleryItem.loadBasemapsError != nil { Image(systemName: "minus.circle.fill") .font(.title) @@ -178,6 +190,8 @@ private struct BasemapGalleryItemRow: View { .font(.title) .foregroundColor(.red) } + + // Display a progress view if the item is loading. if basemapGalleryItem.isLoading { Spacer() ProgressView() @@ -187,6 +201,8 @@ private struct BasemapGalleryItemRow: View { } } + + // Display the name of the item. Text(basemapGalleryItem.name) .font(.footnote) .multilineTextAlignment(.center) @@ -197,6 +213,24 @@ private struct BasemapGalleryItemRow: View { } } +// MARK: Modifiers + +public extension BasemapGallery { + /// The style of the basemap gallery. Defaults to `.automatic`. + /// - Parameter style: The `Style` to use. + /// - Returns: The `BasemapGallery`. + func style( + _ newStyle: Style + ) -> BasemapGallery { + var copy = self + copy.style = newStyle + return copy + } +} + +// MARK: AlertItem + +/// An item used to populate a displayed alert. struct AlertItem { var title: String = "" var message: String = "" diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index c4c201408..c5dba627e 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -11,10 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI -import ArcGIS - import Foundation +import UIKit.UIImage +import ArcGIS /// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. public class BasemapGalleryItem: ObservableObject { @@ -32,10 +31,10 @@ public class BasemapGalleryItem: ObservableObject { /// Creates a `BasemapGalleryItem`. /// - Parameters: /// - basemap: The `Basemap` represented by the item. - /// - name: The item name. If `nil`, `Basemap.name` is used, if available.. - /// - description: The item description. If `nil`, `Basemap.Item.description` + /// - name: The item name. If `nil`, `Basemap.name` is used, if available.. + /// - description: The item description. If `nil`, `Basemap.Item.description` /// is used, if available. - /// - thumbnail: The thumbnail used to represent the item. If `nil`, + /// - thumbnail: The thumbnail used to represent the item. If `nil`, /// `Basemap.Item.thumbnail` is used, if available. public init( basemap: Basemap, @@ -58,7 +57,7 @@ public class BasemapGalleryItem: ObservableObject { @Published public private(set) var loadBasemapsError: RuntimeError? = nil - /// The basemap this `BasemapGalleryItem` represents. + /// The basemap represented by `BasemapGalleryItem`. public private(set) var basemap: Basemap /// The name of the `basemap`. @@ -80,17 +79,18 @@ public class BasemapGalleryItem: ObservableObject { @Published public private(set) var isLoading = true - /// The `SpatialReferenceStatus` of the item. This is set via a call to - /// `updateSpatialReferenceStatus()` + /// The `SpatialReferenceStatus` of the item. This is set via a call to + /// `updateSpatialReferenceStatus()`. @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown - /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's + /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's /// baseLayers have been loaded in `updateSpatialReferenceStatus()`. public private(set) var spatialReference: SpatialReference? = nil } private extension BasemapGalleryItem { + /// Loads the basemap and the item's thumbnail, if available. func loadBasemap() async { var loadError: RuntimeError? = nil do { @@ -174,7 +174,8 @@ extension BasemapGalleryItem: Equatable { } private extension UIImage { - /// A search result marker symbol. + /// A default thumbnail image. + /// - Returns: The default thumbnail. static func defaultThumbnail() -> UIImage { return UIImage(named: "DefaultBasemap", in: Bundle.module, with: nil)! } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index ad2474045..a03877912 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -11,57 +11,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Swift import SwiftUI import ArcGIS -import Combine /// Manages the state for a `BasemapGallery`. @MainActor public class BasemapGalleryViewModel: ObservableObject { /// Creates a `BasemapGalleryViewModel`. - /// - Remark: If `portal` is non-nil, the portal's basemaps will be loaded. If `portal` is - /// `nil`, ArcGISOnline's developer basemaps will be loaded. In both cases, the basemaps - /// will be added to `basemapGalleryItems`. + /// - Remark: If `basemapGalleryItems` is empty, ArcGISOnline's developer basemaps will + /// be loaded and added to `basemapGalleryItems`. /// - Parameters: - /// - geoModel: The `GeoModel`. - /// - portal: The `Portal` to load base maps from. /// - basemapGalleryItems: A list of pre-defined base maps to display. - public init( - geoModel: GeoModel? = nil, - portal: Portal? = nil, - basemapGalleryItems: [BasemapGalleryItem] = [] - ) { - self.geoModel = geoModel - self.portal = portal + public init(_ basemapGalleryItems: [BasemapGalleryItem] = []) { self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) - // Note that we don't want to store this tasks and cancel it - // before kicking off another operation because these operations - // could have been started elsewhere as well as here. - // Canceling them here would also cancel those other operations, - // which we don't want to do. - Task { - // Load the geomodel. - await load(geoModel: geoModel) - - // If we have a portal or no basemapGalleryItems were supplied, - // then load the default basemaps from the portal, if any, or AGOL. - if portal != nil || basemapGalleryItems.isEmpty { - var thePortal = portal - var useDeveloperBasemaps = false - if thePortal == nil { - thePortal = Portal.arcGISOnline(isLoginRequired: false) - useDeveloperBasemaps = true - } + if basemapGalleryItems.isEmpty { + // We have no basemap items, so fetch the + //developer basemaps from AGOL. + fetchBasemapsTask = Task { await fetchBasemaps( - from: thePortal, - useDeveloperBasemaps: useDeveloperBasemaps + from: Portal.arcGISOnline(isLoginRequired: false), + useDeveloperBasemaps: true ) } } } + private var fetchBasemapsTask: Task? { + willSet { + fetchBasemapsTask?.cancel() + } + } + /// The error generated by fetching the `Basemaps` from the `Portal`. @Published public var fetchBasemapsError: Error? = nil @@ -76,21 +57,23 @@ public class BasemapGalleryViewModel: ObservableObject { } } - /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps - /// and replace the`basemapGalleryItems` array with the fetched basemaps. + /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps + /// and replace the`basemapGalleryItems` array items with the fetched basemaps. public var portal: Portal? { didSet { - Task { await fetchBasemaps(from: portal, append: false) } + // Remove all items from `basemapGalleryItems`. + basemapGalleryItems.removeAll() + fetchBasemapsTask = Task { await fetchBasemaps(from: portal) } } } - /// The list of basemaps currently visible in the gallery. It is comprised of items passed into + /// The list of basemaps currently visible in the gallery. It is comprised of items passed into /// the `BasemapGalleryItem` constructor property and items loaded either from `portal` or /// from ArcGISOnline if `portal` is `nil`. @Published public var basemapGalleryItems: [BasemapGalleryItem] = [] - /// `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a + /// The `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a /// basemap which does not exist in the gallery. @Published public private(set) var currentBasemapGalleryItem: BasemapGalleryItem? = nil { @@ -106,25 +89,30 @@ public class BasemapGalleryViewModel: ObservableObject { public private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil /// This attempts to set `currentBasemapGalleryItem`. `currentBasemapGalleryItem` - /// will be set if it's spatialReference matches that of the current geoModel. If the spatialReferences + /// will be set if it's spatial reference matches that of the current geoModel. If the spatial references /// do not match, `currentBasemapGalleryItem` will be unchanged. /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. - public func updateCurrentBasemapGalleryItem(_ basemapGalleryItem: BasemapGalleryItem) { + public func updateCurrentBasemapGalleryItem( + _ basemapGalleryItem: BasemapGalleryItem + ) { Task { // Ensure the geoModel is loaded. try await geoModel?.load() // Reset the mismatch error. spatialReferenceMismatchError = nil + + // Update the basemap gallery item's `spatialReferenceStatus`. try await basemapGalleryItem.updateSpatialReferenceStatus( geoModel?.actualSpatialReference ) + + // Update @State on the main thread. await MainActor.run { - if basemapGalleryItem.spatialReferenceStatus == .match || - basemapGalleryItem.spatialReferenceStatus == .unknown { + switch basemapGalleryItem.spatialReferenceStatus { + case .match, .unknown: currentBasemapGalleryItem = basemapGalleryItem - } - else { + case .noMatch: spatialReferenceMismatchError = SpatialReferenceMismatchError( basemapSR: basemapGalleryItem.spatialReference, geoModelSR: geoModel?.actualSpatialReference @@ -136,6 +124,10 @@ public class BasemapGalleryViewModel: ObservableObject { } internal extension GeoModel { + /// The actual spatial reference of the geoModel. For `Map`s, this is the map's + /// `spatialReference`. For `Scene`s, if the `sceneViewTilingScheme` is + /// `.webMercator`, the `actualSpatialReference` is `.webMercator`, otherwise + /// it is the `spatialReference` of the scene. var actualSpatialReference: SpatialReference? { (self as? ArcGIS.Scene)?.sceneViewTilingScheme == .webMercator ? SpatialReference.webMercator : @@ -144,52 +136,44 @@ internal extension GeoModel { } private extension BasemapGalleryViewModel { - /// Fetches the basemaps from the given portal and populates `basemapGalleryItems` with + /// Fetches the basemaps from the given portal and appends `basemapGalleryItems` with /// items created from the fetched basemaps. /// - Parameters: /// - portal: Portal to fetch basemaps from - /// - useDeveloperBasemaps: If `true`, will always use the portal's developer basemaps. If + /// - useDeveloperBasemaps: If `true`, will always use the portal's developer basemaps. If /// `false`, it will use either the portal's basemaps or vector basemaps, depending on the value of /// `portal.portalInfo.useVectorBasemaps`. - /// - append: If `true`, will appended fetched basemaps to `basemapGalleryItems`. - /// If `false`, it will clear `basemapGalleryItems` before adding the fetched basemaps. func fetchBasemaps( from portal: Portal?, - useDeveloperBasemaps: Bool = false, - append: Bool = true + useDeveloperBasemaps: Bool = false ) async { guard let portal = portal else { return } do { try await portal.load() - - var tmpItems = [BasemapGalleryItem]() + if useDeveloperBasemaps { - tmpItems += try await portal.developerBasemaps.map { + basemapGalleryItems += try await portal.developerBasemaps.map { BasemapGalleryItem(basemap: $0) } } else if let portalInfo = portal.portalInfo, portalInfo.useVectorBasemaps { - tmpItems += try await portal.vectorBasemaps.map { + basemapGalleryItems += try await portal.vectorBasemaps.map { BasemapGalleryItem(basemap: $0) } } else { - tmpItems += try await portal.basemaps.map { + basemapGalleryItems += try await portal.basemaps.map { BasemapGalleryItem(basemap: $0) } } - - if append { - basemapGalleryItems += tmpItems - } - else { - basemapGalleryItems = tmpItems - } } catch { fetchBasemapsError = error } } + /// Loads the given `GeoModel` then sets `currentBasemapGalleryItem` to an item + /// created with the geoModel's basemap. + /// - Parameter geoModel: The `GeoModel` to load. func load(geoModel: GeoModel?) async { guard let geoModel = geoModel else { return } do { @@ -204,11 +188,12 @@ private extension BasemapGalleryViewModel { } } -/// A value that represents a SpatialReference mismatch. +/// A value that represents an error ocurring because of a SpatialReference mismatch between +/// a geomodel and a basemap. public struct SpatialReferenceMismatchError: Error { /// The basemap's spatial reference public let basemapSR: SpatialReference? - + /// The geomodel's spatial reference public let geoModelSR: SpatialReference? } diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 9ef0ee1c9..1fbf28f05 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -22,10 +22,10 @@ import Combine // // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md // -// Note: the iOS implementation uses the MVVM approach and SwiftUI. This +// Note: the iOS implementation uses the MVVM approach and SwiftUI. This // required a bit more properties/logic in the `BasemapGalleryItem` (such // as the `loadBasemapError` and `spatialReferenceStatus` properties than -// the `BasemapGallery` design specifies. Tests not present in the +// the `BasemapGallery` design specifies. Tests not present in the // test design have been added to accomodate those differences. @MainActor class BasemapGalleryItemTests: XCTestCase { @@ -103,7 +103,7 @@ class BasemapGalleryItemTests: XCTestCase { XCTAssertEqual(item.spatialReferenceStatus, .unknown) - // Test if basemap matches. Use a Task here so we can catch and test + // Test if basemap matches. Use a Task here so we can catch and test // the change to `item.isLoading` during the loading of the base layers. Task { try await item.updateSpatialReferenceStatus(SpatialReference.webMercator) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index e04985bfd..d73c59779 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -22,10 +22,10 @@ import Combine // // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md // -// Note: the iOS implementation uses the MVVM approach and SwiftUI. This +// Note: the iOS implementation uses the MVVM approach and SwiftUI. This // required a bit more properties/logic in the `BasemapGalleryViewModel` (such // as `geoModel.actualSpatialReference`) than the `BasemapGallery` design -// specifies. Tests not present in the test design have been added to +// specifies. Tests not present in the test design have been added to // accomodate those differences. @MainActor class BasemapGalleryViewModelTests: XCTestCase { @@ -79,7 +79,7 @@ class BasemapGalleryViewModelTests: XCTestCase { let sortedDeveloperItems = developerBasemapItems.sorted { $0.name < $1.name } XCTAssertNotEqual(sortedItems, sortedDeveloperItems) - // BasemapGalleryItems. No basemaps are fetched from a portal. + // BasemapGalleryItems. No basemaps are fetched from a portal. let itemsViewModel = BasemapGalleryViewModel( basemapGalleryItems: defaultBasemapGalleryItems ) @@ -90,7 +90,7 @@ class BasemapGalleryViewModelTests: XCTestCase { defaultBasemapGalleryItems.count ) - // Both Portal and BasemapGalleryItems. Basemaps are fetched from + // Both Portal and BasemapGalleryItems. Basemaps are fetched from // the portal and appended to the list of basemapGalleryItems. let viewModel = BasemapGalleryViewModel( portal: Portal.arcGISOnline(isLoginRequired: false), From 9917b02841ad1c79b7414971bedc02f9ab9895ce Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 2 Dec 2021 16:02:53 -0600 Subject: [PATCH 0338/1722] Add additional constructors. --- .../BasemapGalleryViewModel.swift | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index a03877912..f68b7180a 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -17,12 +17,25 @@ import ArcGIS /// Manages the state for a `BasemapGallery`. @MainActor public class BasemapGalleryViewModel: ObservableObject { - /// Creates a `BasemapGalleryViewModel`. + /// Creates a `BasemapGalleryViewModel` + /// - Remark: The ArcGISOnline's developer basemaps will + /// be loaded and added to `basemapGalleryItems`. + /// - Parameters: + /// - geoModel: The `GeoModel`. + public convenience init(_ geoModel: GeoModel? = nil) { + self.init(geoModel, basemapGalleryItems: []) + } + + /// Creates a `BasemapGalleryViewModel`. Uses the given list of basemap gallery items. /// - Remark: If `basemapGalleryItems` is empty, ArcGISOnline's developer basemaps will /// be loaded and added to `basemapGalleryItems`. /// - Parameters: + /// - geoModel: The `GeoModel`. /// - basemapGalleryItems: A list of pre-defined base maps to display. - public init(_ basemapGalleryItems: [BasemapGalleryItem] = []) { + public init( + _ geoModel: GeoModel? = nil, + basemapGalleryItems: [BasemapGalleryItem] + ) { self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) if basemapGalleryItems.isEmpty { @@ -35,8 +48,28 @@ public class BasemapGalleryViewModel: ObservableObject { ) } } + + defer { + // Using `defer` allows the property `didSet` observers to be called. + self.geoModel = geoModel + } } + /// Creates a `BasemapGalleryViewModel`. Uses the given `Portal` to retrieve basemaps. + /// - Parameters: + /// - geoModel: The `GeoModel`. + /// - portal: The `Portal` used to load basemaps. + public init( + _ geoModel: GeoModel? = nil, + portal: Portal + ) { + defer { + // Using `defer` allows the property `didSet` observers to be called. + self.geoModel = geoModel + self.portal = portal + } + } + private var fetchBasemapsTask: Task? { willSet { fetchBasemapsTask?.cancel() @@ -53,6 +86,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// the geoModel will have its basemap replaced with the selected basemap. public var geoModel: GeoModel? { didSet { + guard let geoModel = geoModel else { return } Task { await load(geoModel: geoModel) } } } @@ -63,6 +97,8 @@ public class BasemapGalleryViewModel: ObservableObject { didSet { // Remove all items from `basemapGalleryItems`. basemapGalleryItems.removeAll() + + guard let portal = portal else { return } fetchBasemapsTask = Task { await fetchBasemaps(from: portal) } } } From 8d4b57c597eab7cbdcd7b0fdf20957e6d01e181f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 2 Dec 2021 16:03:19 -0600 Subject: [PATCH 0339/1722] Cleanup and augment tests. --- .../BasemapGalleryItemTests.swift | 1 + .../BasemapGalleryViewModelTests.swift | 86 +++++++++++++------ 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 1fbf28f05..0da03bc79 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -79,6 +79,7 @@ class BasemapGalleryItemTests: XCTestCase { } func testLoadBasemapError() async throws { + // Create item with bad portal item URL. let item = BasemapGalleryItem( basemap: Basemap( item: PortalItem( diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index d73c59779..b65d33eb6 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -45,14 +45,18 @@ class BasemapGalleryViewModelTests: XCTestCase { ) ) ] - + + /// Test the various constructor methods. func testInit() async throws { // Note: this is a good candidate for mocking portal data. // This would allow the test to check for a specific number of items. + // // GeoModel. - let geoModel = Map(basemap: Basemap.streets()) - let geoModelViewModel = BasemapGalleryViewModel(geoModel: geoModel) + // + var geoModel = Map(basemap: Basemap.lightGrayCanvas()) + let geoModelViewModel = BasemapGalleryViewModel(geoModel) + XCTAssertTrue(geoModel === geoModelViewModel.geoModel) // With no portal, `basemapGalleryItems` are fetched from AGOL's // list of developer basemaps. @@ -60,12 +64,22 @@ class BasemapGalleryViewModelTests: XCTestCase { var basemapGalleryItems = try XCTUnwrap(items) XCTAssertFalse(basemapGalleryItems.isEmpty) + // GeoModel should be loaded. + XCTAssertEqual(geoModel.loadStatus, .loaded) + XCTAssertTrue(geoModel.basemap === geoModelViewModel.currentBasemapGalleryItem?.basemap) + + // Save the array of developer basemap items from AGOL. let developerBasemapItems = basemapGalleryItems + // // Portal. - let portalViewModel = BasemapGalleryViewModel( - portal: Portal.arcGISOnline(isLoginRequired: false) - ) + // + geoModel = Map(basemap: Basemap.lightGrayCanvas()) + let portal = Portal.arcGISOnline(isLoginRequired: false) + let portalViewModel = BasemapGalleryViewModel(geoModel, portal: portal) + + XCTAssertTrue(geoModel === portalViewModel.geoModel) + XCTAssertTrue(portal === portalViewModel.portal) // With a portal, `basemapGalleryItems` are fetched from either the // portal's vector basemaps or regular basemaps. @@ -73,40 +87,35 @@ class BasemapGalleryViewModelTests: XCTestCase { basemapGalleryItems = try XCTUnwrap(items) XCTAssertFalse(basemapGalleryItems.isEmpty) + XCTAssertEqual(geoModel.loadStatus, .loaded) + XCTAssertTrue(geoModel.basemap === portalViewModel.currentBasemapGalleryItem?.basemap) + // Sort the developer items from the "GeoModel" test above and the // items from the portal and make sure they are not equal. let sortedItems = basemapGalleryItems.sorted { $0.name < $1.name } let sortedDeveloperItems = developerBasemapItems.sorted { $0.name < $1.name } XCTAssertNotEqual(sortedItems, sortedDeveloperItems) + // // BasemapGalleryItems. No basemaps are fetched from a portal. + // + geoModel = Map(basemap: Basemap.lightGrayCanvas()) let itemsViewModel = BasemapGalleryViewModel( + geoModel, basemapGalleryItems: defaultBasemapGalleryItems ) - + // The item counts should match. XCTAssertEqual( itemsViewModel.basemapGalleryItems.count, defaultBasemapGalleryItems.count ) - - // Both Portal and BasemapGalleryItems. Basemaps are fetched from - // the portal and appended to the list of basemapGalleryItems. - let viewModel = BasemapGalleryViewModel( - portal: Portal.arcGISOnline(isLoginRequired: false), - basemapGalleryItems: defaultBasemapGalleryItems - ) - - items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - basemapGalleryItems = try XCTUnwrap(items) - - // Count will be greater than the number of hardcoded items. - XCTAssertGreaterThan(basemapGalleryItems.count, defaultBasemapGalleryItems.count) } + /// Test the `GeoModel.actualSpatialReference` extension property. func testGeoModelActualSpatialReference() async throws { // Map with .webMercator basemap. - let geoModel = Map(basemap: Basemap.streets()) + let geoModel = Map(basemap: Basemap.lightGrayCanvas()) try await geoModel.load() XCTAssertEqual(geoModel.actualSpatialReference, .webMercator) @@ -133,13 +142,25 @@ class BasemapGalleryViewModelTests: XCTestCase { try await geoModel3.load() XCTAssertEqual(geoModel3.actualSpatialReference, .webMercator) + + // Test with Scene that has a tiling scheme of .wgs84 + let geoModel4 = Scene( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ) + ) + try await geoModel4.load() + XCTAssertEqual(geoModel4.actualSpatialReference, .wgs84) } + /// Test the `currentBasemapGalleryItem` property including valid and invalid basemaps. func testCurrentBasemapGalleryItem() async throws { let basemap = Basemap.streets() let geoModel = Map(basemap: basemap) - let viewModel = BasemapGalleryViewModel(geoModel: geoModel) + let viewModel = BasemapGalleryViewModel(geoModel) // Verify current item is equal to map's basemap. var item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).first @@ -158,14 +179,19 @@ class BasemapGalleryViewModelTests: XCTestCase { // Wait until it loads. _ = try await validItem.$isLoading.compactMap({ $0 }).dropFirst().first + // Update the item on the model. viewModel.updateCurrentBasemapGalleryItem(validItem) // Wait until the `currentBasemapGalleryItem` is updated. item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).dropFirst().first currentItem = try XCTUnwrap(item) + + // Items should equal, meaning the `validItem` was set properly. XCTAssertEqual(currentItem, validItem) - // Test WGS84 basemap item (Imagery (WGS84)). + // Test WGS84 basemap item (Imagery (WGS84)). This item is in a + // different spatial reference than the geoModel, so it should + // not be set as the current item. let invalidItem = BasemapGalleryItem( basemap: Basemap( item: PortalItem( @@ -177,6 +203,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // Wait until it loads. _ = try await invalidItem.$isLoading.compactMap({ $0 }).dropFirst().first + // Update the item on the model. viewModel.updateCurrentBasemapGalleryItem(invalidItem) // The update will fail, so wait until the @@ -188,10 +215,9 @@ class BasemapGalleryViewModelTests: XCTestCase { XCTAssertEqual(currentItem, validItem) } + /// Test setting the portal after the model has been created. func testUpdatePortal() async throws { - // Note: this is a good candidate for mocking portal data. - // This would allow the test to check for a specific number of items. - + // Create a model with a default list of items. let viewModel = BasemapGalleryViewModel( basemapGalleryItems: defaultBasemapGalleryItems ) @@ -202,8 +228,16 @@ class BasemapGalleryViewModelTests: XCTestCase { // There are exactly two default items. XCTAssertEqual(basemapGalleryItems.count, 2) + // Set a portal on the model. This should clear out the existing + // array of items and load basemaps from the portal. viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) + // The items should be cleared prior to loading those from the portal. + items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).first + basemapGalleryItems = try XCTUnwrap(items) + XCTAssertTrue(basemapGalleryItems.isEmpty) + + // Wait for the portal basemaps to load. items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first basemapGalleryItems = try XCTUnwrap(items) From bb865c88c8ce94e738f0e3c17429b46f87b6bda4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 14 Dec 2021 15:25:12 -0600 Subject: [PATCH 0340/1722] Cleanup --- Examples/Examples/BasemapGalleryExampleView.swift | 5 +---- .../Components/BasemapGallery/BasemapGallery.swift | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index db6f29b49..0a8216a50 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -64,14 +64,11 @@ struct BasemapGalleryExampleView: View { ) } - // TODO: remove last one (bad url) before PR static private func initialBasemaps() -> [BasemapGalleryItem] { let itemURLs: [URL] = [ URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")!, URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")!, - URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")!, - URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")!, - URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! + URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! ] return itemURLs.map { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index b4f9ce673..fa5a9def5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -14,8 +14,8 @@ import SwiftUI import ArcGIS -/// The `BasemapGallery` tool displays a collection basemaps from -/// ArcGIS Online, a user-defined portal, or an array of `Basemap`s. +/// The `BasemapGallery` tool displays a collection of basemaps from either +/// ArcGIS Online, a user-defined portal, or an array of `BasemapGalleryItem`s. /// When a new basemap is selected from the `BasemapGallery` and the optional /// `BasemapGallery.geoModel` property is set, then the basemap of the geoModel is replaced /// with the basemap in the gallery. From 8bff380f185c314f78a322bc444810f502df33fd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 15 Dec 2021 16:46:54 -0600 Subject: [PATCH 0341/1722] Move methods into same extension. --- .../BasemapGallery/BasemapGalleryItem.swift | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index c5dba627e..1cb021494 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -116,6 +116,27 @@ private extension BasemapGalleryItem { loadBasemapsError = error isLoading = false } +} + +public extension BasemapGalleryItem { + /// Loads the first base layer of `basemap` and determines if it matches + /// `referenceSpatialReference`, setting the `spatialReferenceStatus` + /// property appropriately. + /// - Parameter referenceSpatialReference: The `SpatialReference to match to`. + func updateSpatialReferenceStatus( + _ spatialReference: SpatialReference? + ) async throws { + guard basemap.loadStatus == .loaded else { return } + + if self.spatialReference == nil { + await MainActor.run { + isLoading = true + } + try await basemap.baseLayers.first?.load() + } + + await finalizeUpdateSpatialReferenceStatus(with: spatialReference) + } /// Updates the item's `spatialReference` and `spatialReferenceStatus` properties. /// - Parameter referenceSpatialReference: The `SpatialReference` used to @@ -137,27 +158,8 @@ private extension BasemapGalleryItem { } isLoading = false } -} -public extension BasemapGalleryItem { - /// Loads the first base layer of `basemap` and determines if it matches - /// `referenceSpatialReference`, setting the `spatialReferenceStatus` - /// property appropriately. - /// - Parameter referenceSpatialReference: The `SpatialReference to match to`. - func updateSpatialReferenceStatus( - _ spatialReference: SpatialReference? - ) async throws { - guard basemap.loadStatus == .loaded else { return } - if self.spatialReference == nil { - await MainActor.run { - isLoading = true - } - try await basemap.baseLayers.first?.load() - } - - await finalizeUpdateSpatialReferenceStatus(with: spatialReference) - } } extension BasemapGalleryItem: Identifiable { From 65e3adc0e8f7866857e27ed12791342ce0cd9cc7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 15 Dec 2021 16:48:39 -0600 Subject: [PATCH 0342/1722] Remove extra empty lines. --- .../Components/BasemapGallery/BasemapGalleryItem.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 1cb021494..b9de3b96c 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -158,8 +158,6 @@ public extension BasemapGalleryItem { } isLoading = false } - - } extension BasemapGalleryItem: Identifiable { From a0d44f7c238613f068814ede7197b1ddd1924a73 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 15 Dec 2021 16:58:38 -0600 Subject: [PATCH 0343/1722] PR Prep --- .../BasemapGallery/BasemapGallery.swift | 27 +- .../BasemapGallery/BasemapGalleryItem.swift | 61 +---- .../BasemapGalleryViewModel.swift | 53 +--- .../BasemapGalleryItemTests.swift | 156 ----------- .../BasemapGalleryViewModelTests.swift | 251 ------------------ 5 files changed, 3 insertions(+), 545 deletions(-) delete mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift delete mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index fa5a9def5..7011cdb5f 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -75,15 +75,6 @@ public struct BasemapGallery: View { public var body: some View { GalleryView() .frame(width: galleryWidth) - .onReceive( - viewModel.$spatialReferenceMismatchError.dropFirst(), - perform: { error in - guard let error = error else { return } - alertItem = AlertItem( - basemapSR: error.basemapSR, - geoModelSR: error.geoModelSR - ) - }) .alert(item: $alertItem) { alertItem in Alert( title: Text(alertItem.title), @@ -151,7 +142,7 @@ private extension BasemapGallery { alertItem = AlertItem(loadBasemapError: loadError) } else { - viewModel.updateCurrentBasemapGalleryItem(basemapGalleryItem) + viewModel.currentBasemapGalleryItem = basemapGalleryItem } } } @@ -185,10 +176,6 @@ private struct BasemapGalleryItemRow: View { Image(systemName: "minus.circle.fill") .font(.title) .foregroundColor(.red) - } else if basemapGalleryItem.spatialReferenceStatus == .noMatch { - Image(systemName: "x.circle.fill") - .font(.title) - .foregroundColor(.red) } // Display a progress view if the item is loading. @@ -198,7 +185,6 @@ private struct BasemapGalleryItemRow: View { .progressViewStyle(CircularProgressViewStyle()) .esriBorder() Spacer() - } } @@ -241,17 +227,6 @@ extension AlertItem: Identifiable { } extension AlertItem { - /// Creates an alert item based on a spatial reference mismatch. - /// - Parameters: - /// - basemapSR: The basemap's spatial reference. - /// - geoModelSR: The geomodel's spatial reference. - init(basemapSR: SpatialReference?, geoModelSR: SpatialReference?) { - self.init( - title: "Spatial reference mismatch.", - message: "The spatial reference of the basemap: \(basemapSR?.description ?? "") does not match that of the geomodel: \(geoModelSR?.description ?? "")." - ) - } - /// Creates an alert item based on an error generated loading a basemap. /// - Parameter loadBasemapError: The load basemap error. init(loadBasemapError: RuntimeError) { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index b9de3b96c..7776efd1d 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -17,17 +17,6 @@ import ArcGIS /// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. public class BasemapGalleryItem: ObservableObject { - /// Indicates the status of the basemap's spatial reference in relation to a reference spatial reference. - public enum SpatialReferenceStatus { - /// The basemap's spatial reference status is unknown, either because the basemap's - /// base layers haven't been loaded yet or the status has yet to be updated. - case unknown - /// The basemap's spatial reference matches the reference spatial reference. - case match - /// The basemap's spatial reference does not match the reference spatial reference. - case noMatch - } - /// Creates a `BasemapGalleryItem`. /// - Parameters: /// - basemap: The `Basemap` represented by the item. @@ -79,13 +68,7 @@ public class BasemapGalleryItem: ObservableObject { @Published public private(set) var isLoading = true - /// The `SpatialReferenceStatus` of the item. This is set via a call to - /// `updateSpatialReferenceStatus()`. - @Published - public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown - - /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's - /// baseLayers have been loaded in `updateSpatialReferenceStatus()`. + /// The `SpatialReference` of `basemap`. public private(set) var spatialReference: SpatialReference? = nil } @@ -118,48 +101,6 @@ private extension BasemapGalleryItem { } } -public extension BasemapGalleryItem { - /// Loads the first base layer of `basemap` and determines if it matches - /// `referenceSpatialReference`, setting the `spatialReferenceStatus` - /// property appropriately. - /// - Parameter referenceSpatialReference: The `SpatialReference to match to`. - func updateSpatialReferenceStatus( - _ spatialReference: SpatialReference? - ) async throws { - guard basemap.loadStatus == .loaded else { return } - - if self.spatialReference == nil { - await MainActor.run { - isLoading = true - } - try await basemap.baseLayers.first?.load() - } - - await finalizeUpdateSpatialReferenceStatus(with: spatialReference) - } - - /// Updates the item's `spatialReference` and `spatialReferenceStatus` properties. - /// - Parameter referenceSpatialReference: The `SpatialReference` used to - /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` - /// `SpatialReference`. - @MainActor - func finalizeUpdateSpatialReferenceStatus( - with referenceSpatialReference: SpatialReference? - ) { - spatialReference = basemap.baseLayers.first?.spatialReference - if referenceSpatialReference == nil { - spatialReferenceStatus = .unknown - } - else if spatialReference == referenceSpatialReference { - spatialReferenceStatus = .match - } - else { - spatialReferenceStatus = .noMatch - } - isLoading = false - } -} - extension BasemapGalleryItem: Identifiable { public var id: ObjectIdentifier { ObjectIdentifier(self) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index f68b7180a..6536da274 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -112,51 +112,12 @@ public class BasemapGalleryViewModel: ObservableObject { /// The `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a /// basemap which does not exist in the gallery. @Published - public private(set) var currentBasemapGalleryItem: BasemapGalleryItem? = nil { + public var currentBasemapGalleryItem: BasemapGalleryItem? = nil { didSet { guard let item = currentBasemapGalleryItem else { return } geoModel?.basemap = item.basemap } } - - /// The error signifying the spatial reference of the GeoModel and that of a potential - /// current `BasemapGalleryItem` do not match. - @Published - public private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil - - /// This attempts to set `currentBasemapGalleryItem`. `currentBasemapGalleryItem` - /// will be set if it's spatial reference matches that of the current geoModel. If the spatial references - /// do not match, `currentBasemapGalleryItem` will be unchanged. - /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. - public func updateCurrentBasemapGalleryItem( - _ basemapGalleryItem: BasemapGalleryItem - ) { - Task { - // Ensure the geoModel is loaded. - try await geoModel?.load() - - // Reset the mismatch error. - spatialReferenceMismatchError = nil - - // Update the basemap gallery item's `spatialReferenceStatus`. - try await basemapGalleryItem.updateSpatialReferenceStatus( - geoModel?.actualSpatialReference - ) - - // Update @State on the main thread. - await MainActor.run { - switch basemapGalleryItem.spatialReferenceStatus { - case .match, .unknown: - currentBasemapGalleryItem = basemapGalleryItem - case .noMatch: - spatialReferenceMismatchError = SpatialReferenceMismatchError( - basemapSR: basemapGalleryItem.spatialReference, - geoModelSR: geoModel?.actualSpatialReference - ) - } - } - } - } } internal extension GeoModel { @@ -223,15 +184,3 @@ private extension BasemapGalleryViewModel { } catch { } } } - -/// A value that represents an error ocurring because of a SpatialReference mismatch between -/// a geomodel and a basemap. -public struct SpatialReferenceMismatchError: Error { - /// The basemap's spatial reference - public let basemapSR: SpatialReference? - - /// The geomodel's spatial reference - public let geoModelSR: SpatialReference? -} - -extension SpatialReferenceMismatchError: Equatable {} diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift deleted file mode 100644 index 0da03bc79..000000000 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2021 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 Foundation - -import XCTest -import ArcGIS -import ArcGISToolkit -import SwiftUI -import Combine - -// -// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md -// -// Note: the iOS implementation uses the MVVM approach and SwiftUI. This -// required a bit more properties/logic in the `BasemapGalleryItem` (such -// as the `loadBasemapError` and `spatialReferenceStatus` properties than -// the `BasemapGallery` design specifies. Tests not present in the -// test design have been added to accomodate those differences. -@MainActor -class BasemapGalleryItemTests: XCTestCase { - func testInit() async throws { - let basemap = Basemap.lightGrayCanvas() - var item = BasemapGalleryItem(basemap: basemap) - - var isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - var loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertTrue(item.basemap === basemap) - XCTAssertEqual(item.name, "Light Gray Canvas") - XCTAssertNil(item.description) - XCTAssertNotNil(item.thumbnail) - XCTAssertNil(item.loadBasemapsError) - - // Test with overrides. - let thumbnail = UIImage(systemName: "magnifyingglass") - XCTAssertNotNil(thumbnail) - item = BasemapGalleryItem( - basemap: basemap, - name: "My Basemap", - description: "Basemap description", - thumbnail: thumbnail - ) - - isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertEqual(item.name, "My Basemap") - XCTAssertEqual(item.description, "Basemap description") - XCTAssertEqual(item.thumbnail, thumbnail) - XCTAssertNil(item.loadBasemapsError) - - // Test with portal item. - item = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ) - - isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertEqual(item.name, "OpenStreetMap Blueprint") - XCTAssertEqual(item.description, "

This web map presents a vector basemap of OpenStreetMap (OSM) data hosted by Esri. Esri created this vector tile basemap from the Daylight map distribution of OSM data, which is supported by Facebook and supplemented with additional data from Microsoft. It presents the map in a cartographic style is like a blueprint technical drawing. The OSM Daylight map will be updated every month with the latest version of OSM Daylight data. 

OpenStreetMap is an open collaborative project to create a free editable map of the world. Volunteers gather location data using GPS, local knowledge, and other free sources of information and upload it. The resulting free map can be viewed and downloaded from the OpenStreetMap site: www.OpenStreetMap.org. Esri is a supporter of the OSM project and is excited to make this enhanced vector basemap available to the ArcGIS user and developer communities.
") - XCTAssertNotNil(item.thumbnail) - XCTAssertNil(item.loadBasemapsError) - } - - func testLoadBasemapError() async throws { - // Create item with bad portal item URL. - let item = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! - )! - ) - ) - - let isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - let loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertNotNil(item.loadBasemapsError) - } - - func testSpatialReferenceAndStatus() async throws { - let basemap = Basemap.lightGrayCanvas() - let item = BasemapGalleryItem(basemap: basemap) - - var isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - var loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - - XCTAssertEqual(item.spatialReferenceStatus, .unknown) - - // Test if basemap matches. Use a Task here so we can catch and test - // the change to `item.isLoading` during the loading of the base layers. - Task { - try await item.updateSpatialReferenceStatus(SpatialReference.webMercator) - } - - // Check if `isLoading` is set to true during first call - // to updateSpatialReferenceStatus. - isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isLoading) - XCTAssertTrue(loading, "Item base layers are loading.") - - var srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).dropFirst().first - var status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .match) - XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) - XCTAssertFalse(item.isLoading) - - // Test if basemap doesn't match. - try await item.updateSpatialReferenceStatus(SpatialReference.wgs84) - - // Since we've already called `updateSpatialReferenceStatus` once, - // we should no longer internally need to load the baselayers. - isLoading = try await item.$isLoading.compactMap({ $0 }).first - loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item base layers are not loading.") - - srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).first - status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .noMatch) - XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) - - // Test WGS84 basemap. - let otherItem = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - - _ = try await otherItem.$isLoading.compactMap({ $0 }).dropFirst().first - - try await otherItem.updateSpatialReferenceStatus(SpatialReference.wgs84) - srStatus = try await otherItem.$spatialReferenceStatus.compactMap({ $0 }).first - status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .match) - XCTAssertEqual(otherItem.spatialReference, SpatialReference.wgs84) - } -} diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift deleted file mode 100644 index b65d33eb6..000000000 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2021 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 Foundation - -import XCTest -import ArcGIS -@testable import ArcGISToolkit -import SwiftUI -import Combine - -// -// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md -// -// Note: the iOS implementation uses the MVVM approach and SwiftUI. This -// required a bit more properties/logic in the `BasemapGalleryViewModel` (such -// as `geoModel.actualSpatialReference`) than the `BasemapGallery` design -// specifies. Tests not present in the test design have been added to -// accomodate those differences. -@MainActor -class BasemapGalleryViewModelTests: XCTestCase { - let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! - )! - ) - ) - ] - - /// Test the various constructor methods. - func testInit() async throws { - // Note: this is a good candidate for mocking portal data. - // This would allow the test to check for a specific number of items. - - // - // GeoModel. - // - var geoModel = Map(basemap: Basemap.lightGrayCanvas()) - let geoModelViewModel = BasemapGalleryViewModel(geoModel) - XCTAssertTrue(geoModel === geoModelViewModel.geoModel) - - // With no portal, `basemapGalleryItems` are fetched from AGOL's - // list of developer basemaps. - var items = try await geoModelViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - var basemapGalleryItems = try XCTUnwrap(items) - XCTAssertFalse(basemapGalleryItems.isEmpty) - - // GeoModel should be loaded. - XCTAssertEqual(geoModel.loadStatus, .loaded) - XCTAssertTrue(geoModel.basemap === geoModelViewModel.currentBasemapGalleryItem?.basemap) - - // Save the array of developer basemap items from AGOL. - let developerBasemapItems = basemapGalleryItems - - // - // Portal. - // - geoModel = Map(basemap: Basemap.lightGrayCanvas()) - let portal = Portal.arcGISOnline(isLoginRequired: false) - let portalViewModel = BasemapGalleryViewModel(geoModel, portal: portal) - - XCTAssertTrue(geoModel === portalViewModel.geoModel) - XCTAssertTrue(portal === portalViewModel.portal) - - // With a portal, `basemapGalleryItems` are fetched from either the - // portal's vector basemaps or regular basemaps. - items = try await portalViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - basemapGalleryItems = try XCTUnwrap(items) - XCTAssertFalse(basemapGalleryItems.isEmpty) - - XCTAssertEqual(geoModel.loadStatus, .loaded) - XCTAssertTrue(geoModel.basemap === portalViewModel.currentBasemapGalleryItem?.basemap) - - // Sort the developer items from the "GeoModel" test above and the - // items from the portal and make sure they are not equal. - let sortedItems = basemapGalleryItems.sorted { $0.name < $1.name } - let sortedDeveloperItems = developerBasemapItems.sorted { $0.name < $1.name } - XCTAssertNotEqual(sortedItems, sortedDeveloperItems) - - // - // BasemapGalleryItems. No basemaps are fetched from a portal. - // - geoModel = Map(basemap: Basemap.lightGrayCanvas()) - let itemsViewModel = BasemapGalleryViewModel( - geoModel, - basemapGalleryItems: defaultBasemapGalleryItems - ) - - // The item counts should match. - XCTAssertEqual( - itemsViewModel.basemapGalleryItems.count, - defaultBasemapGalleryItems.count - ) - } - - /// Test the `GeoModel.actualSpatialReference` extension property. - func testGeoModelActualSpatialReference() async throws { - // Map with .webMercator basemap. - let geoModel = Map(basemap: Basemap.lightGrayCanvas()) - try await geoModel.load() - XCTAssertEqual(geoModel.actualSpatialReference, .webMercator) - - // Map with .wgs84 basemap. - let geoModel2 = Map( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - - try await geoModel2.load() - XCTAssertEqual(geoModel2.actualSpatialReference, .wgs84) - - // Test with Scene that has a tiling scheme of .webMercator - let geoModel3 = Scene( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ) - - try await geoModel3.load() - XCTAssertEqual(geoModel3.actualSpatialReference, .webMercator) - - // Test with Scene that has a tiling scheme of .wgs84 - let geoModel4 = Scene( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - try await geoModel4.load() - XCTAssertEqual(geoModel4.actualSpatialReference, .wgs84) - } - - /// Test the `currentBasemapGalleryItem` property including valid and invalid basemaps. - func testCurrentBasemapGalleryItem() async throws { - let basemap = Basemap.streets() - let geoModel = Map(basemap: basemap) - - let viewModel = BasemapGalleryViewModel(geoModel) - - // Verify current item is equal to map's basemap. - var item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).first - var currentItem = try XCTUnwrap(item) - XCTAssertTrue(currentItem.basemap === basemap) - - // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). - let validItem = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ) - - // Wait until it loads. - _ = try await validItem.$isLoading.compactMap({ $0 }).dropFirst().first - - // Update the item on the model. - viewModel.updateCurrentBasemapGalleryItem(validItem) - - // Wait until the `currentBasemapGalleryItem` is updated. - item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).dropFirst().first - currentItem = try XCTUnwrap(item) - - // Items should equal, meaning the `validItem` was set properly. - XCTAssertEqual(currentItem, validItem) - - // Test WGS84 basemap item (Imagery (WGS84)). This item is in a - // different spatial reference than the geoModel, so it should - // not be set as the current item. - let invalidItem = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - - // Wait until it loads. - _ = try await invalidItem.$isLoading.compactMap({ $0 }).dropFirst().first - - // Update the item on the model. - viewModel.updateCurrentBasemapGalleryItem(invalidItem) - - // The update will fail, so wait until the - // `$spatialReferenceMismatchError` is updated. - let error = try await viewModel.$spatialReferenceMismatchError.compactMap({ $0 }).first - XCTAssertNotNil(error, "Error is not nil.") - - // Make sure the current item is still equal to the valid item. - XCTAssertEqual(currentItem, validItem) - } - - /// Test setting the portal after the model has been created. - func testUpdatePortal() async throws { - // Create a model with a default list of items. - let viewModel = BasemapGalleryViewModel( - basemapGalleryItems: defaultBasemapGalleryItems - ) - - var items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).first - var basemapGalleryItems = try XCTUnwrap(items) - - // There are exactly two default items. - XCTAssertEqual(basemapGalleryItems.count, 2) - - // Set a portal on the model. This should clear out the existing - // array of items and load basemaps from the portal. - viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) - - // The items should be cleared prior to loading those from the portal. - items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).first - basemapGalleryItems = try XCTUnwrap(items) - XCTAssertTrue(basemapGalleryItems.isEmpty) - - // Wait for the portal basemaps to load. - items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - basemapGalleryItems = try XCTUnwrap(items) - - // There should be no default items in the basemap gallery. - let foundDefaultItem = basemapGalleryItems.first(where: { - $0 == defaultBasemapGalleryItems[0] || - $0 == defaultBasemapGalleryItems[1] - }) - XCTAssertNil(foundDefaultItem) - } -} From d1d58aa6f4929f22907a6e6e24580613ba9dc618 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 15 Dec 2021 17:02:15 -0600 Subject: [PATCH 0344/1722] Remove unneeded SR property. --- .../Components/BasemapGallery/BasemapGalleryItem.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 7776efd1d..20273757a 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -67,9 +67,6 @@ public class BasemapGalleryItem: ObservableObject { /// Denotes whether the `basemap` or it's base layers are being loaded. @Published public private(set) var isLoading = true - - /// The `SpatialReference` of `basemap`. - public private(set) var spatialReference: SpatialReference? = nil } private extension BasemapGalleryItem { From 6be42a4375b15cb23e2df17b89d403a21b8cd19d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 15 Dec 2021 17:11:14 -0600 Subject: [PATCH 0345/1722] PR prep --- .../Examples/BasemapGalleryExampleView.swift | 1 - .../BasemapGallery/BasemapGallery.swift | 93 ++----------------- 2 files changed, 6 insertions(+), 88 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 0a8216a50..e1949d129 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -39,7 +39,6 @@ struct BasemapGalleryExampleView: View { .overlay(alignment: .topTrailing) { if showBasemapGallery { BasemapGallery(viewModel: viewModel) - .style(.automatic) } } .onAppear() { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 7011cdb5f..2edadd5a9 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -20,17 +20,6 @@ import ArcGIS /// `BasemapGallery.geoModel` property is set, then the basemap of the geoModel is replaced /// with the basemap in the gallery. public struct BasemapGallery: View { - /// The view style of the gallery. - public enum Style { - /// The `BasemapGallery` will display as a grid when there is appropriate - /// 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 - /// The `BasemapGallery` will display as a list. - case list - } - /// Creates a `BasemapGallery`. /// - Parameter viewModel: The view model used by the `BasemapGallery`. public init(viewModel: BasemapGalleryViewModel? = nil) { @@ -48,25 +37,6 @@ public struct BasemapGallery: View { /// user action. @ObservedObject public 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 `automatic`. - /// Set using the `style` modifier. - private var style: Style = .automatic - - /// The size class used to determine if the basemap items should dispaly in a list or grid. - /// If the size class is `.regular`, they display in a grid. If it is `.compact`, they display in a list. - @Environment(\.horizontalSizeClass) var horizontalSizeClass - - /// `true` if the horizontal size class is `.regular`, `false` if it's `.compact`. - private var isRegularWidth: Bool { - horizontalSizeClass == .regular - } - - /// The width of the gallery, taking into account the horizontal size class of the device. - private var galleryWidth: CGFloat? { - isRegularWidth ? 300 : 150 - } /// The current alert item to display. @State @@ -74,7 +44,7 @@ public struct BasemapGallery: View { public var body: some View { GalleryView() - .frame(width: galleryWidth) + .frame(width: 300) .alert(item: $alertItem) { alertItem in Alert( title: Text(alertItem.title), @@ -85,52 +55,16 @@ public struct BasemapGallery: View { } private extension BasemapGallery { - /// The gallery view, either displayed as a grid or a list depending on `BasemapGallery.style`. - /// - Returns: A view representing the basemap gallery. - @ViewBuilder + /// The gallery view, displayed in the specified columns. + /// - Parameter columns: The columns used to display the basemap items. + /// - Returns: A view representing the basemap gallery with the specified columns. func GalleryView() -> some View { - switch style { - case .automatic: - if isRegularWidth { - GridView() - } - else { - ListView() - } - case .grid: - GridView() - case .list: - ListView() - } - } - - /// The gallery view, displayed as a grid. - /// - Returns: A view representing the basemap gallery grid. - func GridView() -> some View { - InternalGalleryView( - [ + ScrollView { + let columns: [GridItem] = [ .init(.flexible(), spacing: 8.0, alignment: .top), .init(.flexible(), spacing: 8.0, alignment: .top), .init(.flexible(), spacing: 8.0, alignment: .top) ] - ) - } - - /// The gallery view, displayed as a list. - /// - Returns: A view representing the basemap gallery list. - func ListView() -> some View { - InternalGalleryView( - [ - .init(.flexible(), spacing: 8.0, alignment: .top) - ] - ) - } - - /// The gallery view, displayed in the specified columns. - /// - Parameter columns: The columns used to display the basemap items. - /// - Returns: A view representing the basemap gallery with the specified columns. - func InternalGalleryView(_ columns: [GridItem]) -> some View { - ScrollView { LazyVGrid(columns: columns, spacing: 4) { ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow( @@ -199,21 +133,6 @@ private struct BasemapGalleryItemRow: View { } } -// MARK: Modifiers - -public extension BasemapGallery { - /// The style of the basemap gallery. Defaults to `.automatic`. - /// - Parameter style: The `Style` to use. - /// - Returns: The `BasemapGallery`. - func style( - _ newStyle: Style - ) -> BasemapGallery { - var copy = self - copy.style = newStyle - return copy - } -} - // MARK: AlertItem /// An item used to populate a displayed alert. From 566037db7889cad00857cfc7bde1a7a2da87758c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 15 Dec 2021 17:18:34 -0600 Subject: [PATCH 0346/1722] PR prep --- .../Examples/BasemapGalleryExampleView.swift | 3 +- .../BasemapGalleryViewModel.swift | 98 +------------------ 2 files changed, 2 insertions(+), 99 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index e1949d129..548450ac9 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -66,8 +66,7 @@ struct BasemapGalleryExampleView: View { static private func initialBasemaps() -> [BasemapGalleryItem] { let itemURLs: [URL] = [ URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")!, - URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")!, - URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=9e557abc61ce41c9b8ec8b15800c20d3")! + URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! ] return itemURLs.map { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 6536da274..41d206479 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -38,48 +38,12 @@ public class BasemapGalleryViewModel: ObservableObject { ) { self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) - if basemapGalleryItems.isEmpty { - // We have no basemap items, so fetch the - //developer basemaps from AGOL. - fetchBasemapsTask = Task { - await fetchBasemaps( - from: Portal.arcGISOnline(isLoginRequired: false), - useDeveloperBasemaps: true - ) - } - } - defer { // Using `defer` allows the property `didSet` observers to be called. self.geoModel = geoModel } } - /// Creates a `BasemapGalleryViewModel`. Uses the given `Portal` to retrieve basemaps. - /// - Parameters: - /// - geoModel: The `GeoModel`. - /// - portal: The `Portal` used to load basemaps. - public init( - _ geoModel: GeoModel? = nil, - portal: Portal - ) { - defer { - // Using `defer` allows the property `didSet` observers to be called. - self.geoModel = geoModel - self.portal = portal - } - } - - private var fetchBasemapsTask: Task? { - willSet { - fetchBasemapsTask?.cancel() - } - } - - /// The error generated by fetching the `Basemaps` from the `Portal`. - @Published - public var fetchBasemapsError: Error? = 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, @@ -91,21 +55,8 @@ public class BasemapGalleryViewModel: ObservableObject { } } - /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps - /// and replace the`basemapGalleryItems` array items with the fetched basemaps. - public var portal: Portal? { - didSet { - // Remove all items from `basemapGalleryItems`. - basemapGalleryItems.removeAll() - - guard let portal = portal else { return } - fetchBasemapsTask = Task { await fetchBasemaps(from: portal) } - } - } - /// The list of basemaps currently visible in the gallery. It is comprised of items passed into - /// the `BasemapGalleryItem` constructor property and items loaded either from `portal` or - /// from ArcGISOnline if `portal` is `nil`. + /// the `BasemapGalleryItem` constructor property. @Published public var basemapGalleryItems: [BasemapGalleryItem] = [] @@ -120,54 +71,7 @@ public class BasemapGalleryViewModel: ObservableObject { } } -internal extension GeoModel { - /// The actual spatial reference of the geoModel. For `Map`s, this is the map's - /// `spatialReference`. For `Scene`s, if the `sceneViewTilingScheme` is - /// `.webMercator`, the `actualSpatialReference` is `.webMercator`, otherwise - /// it is the `spatialReference` of the scene. - var actualSpatialReference: SpatialReference? { - (self as? ArcGIS.Scene)?.sceneViewTilingScheme == .webMercator ? - SpatialReference.webMercator : - spatialReference - } -} - private extension BasemapGalleryViewModel { - /// Fetches the basemaps from the given portal and appends `basemapGalleryItems` with - /// items created from the fetched basemaps. - /// - Parameters: - /// - portal: Portal to fetch basemaps from - /// - useDeveloperBasemaps: If `true`, will always use the portal's developer basemaps. If - /// `false`, it will use either the portal's basemaps or vector basemaps, depending on the value of - /// `portal.portalInfo.useVectorBasemaps`. - func fetchBasemaps( - from portal: Portal?, - useDeveloperBasemaps: Bool = false - ) async { - guard let portal = portal else { return } - - do { - try await portal.load() - - if useDeveloperBasemaps { - basemapGalleryItems += try await portal.developerBasemaps.map { - BasemapGalleryItem(basemap: $0) - } - } else if let portalInfo = portal.portalInfo, - portalInfo.useVectorBasemaps { - basemapGalleryItems += try await portal.vectorBasemaps.map { - BasemapGalleryItem(basemap: $0) - } - } else { - basemapGalleryItems += try await portal.basemaps.map { - BasemapGalleryItem(basemap: $0) - } - } - } catch { - fetchBasemapsError = error - } - } - /// Loads the given `GeoModel` then sets `currentBasemapGalleryItem` to an item /// created with the geoModel's basemap. /// - Parameter geoModel: The `GeoModel` to load. From e47b3a5a9749ecce9b62cef1de4b33bc61e1e226 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 15 Dec 2021 17:53:57 -0600 Subject: [PATCH 0347/1722] PR prep --- .../BasemapGalleryItemTests.swift | 156 ----------- .../BasemapGalleryViewModelTests.swift | 251 ------------------ 2 files changed, 407 deletions(-) delete mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift delete mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift deleted file mode 100644 index 0da03bc79..000000000 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2021 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 Foundation - -import XCTest -import ArcGIS -import ArcGISToolkit -import SwiftUI -import Combine - -// -// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md -// -// Note: the iOS implementation uses the MVVM approach and SwiftUI. This -// required a bit more properties/logic in the `BasemapGalleryItem` (such -// as the `loadBasemapError` and `spatialReferenceStatus` properties than -// the `BasemapGallery` design specifies. Tests not present in the -// test design have been added to accomodate those differences. -@MainActor -class BasemapGalleryItemTests: XCTestCase { - func testInit() async throws { - let basemap = Basemap.lightGrayCanvas() - var item = BasemapGalleryItem(basemap: basemap) - - var isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - var loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertTrue(item.basemap === basemap) - XCTAssertEqual(item.name, "Light Gray Canvas") - XCTAssertNil(item.description) - XCTAssertNotNil(item.thumbnail) - XCTAssertNil(item.loadBasemapsError) - - // Test with overrides. - let thumbnail = UIImage(systemName: "magnifyingglass") - XCTAssertNotNil(thumbnail) - item = BasemapGalleryItem( - basemap: basemap, - name: "My Basemap", - description: "Basemap description", - thumbnail: thumbnail - ) - - isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertEqual(item.name, "My Basemap") - XCTAssertEqual(item.description, "Basemap description") - XCTAssertEqual(item.thumbnail, thumbnail) - XCTAssertNil(item.loadBasemapsError) - - // Test with portal item. - item = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ) - - isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertEqual(item.name, "OpenStreetMap Blueprint") - XCTAssertEqual(item.description, "

This web map presents a vector basemap of OpenStreetMap (OSM) data hosted by Esri. Esri created this vector tile basemap from the Daylight map distribution of OSM data, which is supported by Facebook and supplemented with additional data from Microsoft. It presents the map in a cartographic style is like a blueprint technical drawing. The OSM Daylight map will be updated every month with the latest version of OSM Daylight data. 

OpenStreetMap is an open collaborative project to create a free editable map of the world. Volunteers gather location data using GPS, local knowledge, and other free sources of information and upload it. The resulting free map can be viewed and downloaded from the OpenStreetMap site: www.OpenStreetMap.org. Esri is a supporter of the OSM project and is excited to make this enhanced vector basemap available to the ArcGIS user and developer communities.
") - XCTAssertNotNil(item.thumbnail) - XCTAssertNil(item.loadBasemapsError) - } - - func testLoadBasemapError() async throws { - // Create item with bad portal item URL. - let item = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! - )! - ) - ) - - let isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - let loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertNotNil(item.loadBasemapsError) - } - - func testSpatialReferenceAndStatus() async throws { - let basemap = Basemap.lightGrayCanvas() - let item = BasemapGalleryItem(basemap: basemap) - - var isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - var loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item is not loading.") - - XCTAssertEqual(item.spatialReferenceStatus, .unknown) - - // Test if basemap matches. Use a Task here so we can catch and test - // the change to `item.isLoading` during the loading of the base layers. - Task { - try await item.updateSpatialReferenceStatus(SpatialReference.webMercator) - } - - // Check if `isLoading` is set to true during first call - // to updateSpatialReferenceStatus. - isLoading = try await item.$isLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isLoading) - XCTAssertTrue(loading, "Item base layers are loading.") - - var srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).dropFirst().first - var status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .match) - XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) - XCTAssertFalse(item.isLoading) - - // Test if basemap doesn't match. - try await item.updateSpatialReferenceStatus(SpatialReference.wgs84) - - // Since we've already called `updateSpatialReferenceStatus` once, - // we should no longer internally need to load the baselayers. - isLoading = try await item.$isLoading.compactMap({ $0 }).first - loading = try XCTUnwrap(isLoading) - XCTAssertFalse(loading, "Item base layers are not loading.") - - srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).first - status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .noMatch) - XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) - - // Test WGS84 basemap. - let otherItem = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - - _ = try await otherItem.$isLoading.compactMap({ $0 }).dropFirst().first - - try await otherItem.updateSpatialReferenceStatus(SpatialReference.wgs84) - srStatus = try await otherItem.$spatialReferenceStatus.compactMap({ $0 }).first - status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .match) - XCTAssertEqual(otherItem.spatialReference, SpatialReference.wgs84) - } -} diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift deleted file mode 100644 index b65d33eb6..000000000 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2021 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 Foundation - -import XCTest -import ArcGIS -@testable import ArcGISToolkit -import SwiftUI -import Combine - -// -// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md -// -// Note: the iOS implementation uses the MVVM approach and SwiftUI. This -// required a bit more properties/logic in the `BasemapGalleryViewModel` (such -// as `geoModel.actualSpatialReference`) than the `BasemapGallery` design -// specifies. Tests not present in the test design have been added to -// accomodate those differences. -@MainActor -class BasemapGalleryViewModelTests: XCTestCase { - let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ), - BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! - )! - ) - ) - ] - - /// Test the various constructor methods. - func testInit() async throws { - // Note: this is a good candidate for mocking portal data. - // This would allow the test to check for a specific number of items. - - // - // GeoModel. - // - var geoModel = Map(basemap: Basemap.lightGrayCanvas()) - let geoModelViewModel = BasemapGalleryViewModel(geoModel) - XCTAssertTrue(geoModel === geoModelViewModel.geoModel) - - // With no portal, `basemapGalleryItems` are fetched from AGOL's - // list of developer basemaps. - var items = try await geoModelViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - var basemapGalleryItems = try XCTUnwrap(items) - XCTAssertFalse(basemapGalleryItems.isEmpty) - - // GeoModel should be loaded. - XCTAssertEqual(geoModel.loadStatus, .loaded) - XCTAssertTrue(geoModel.basemap === geoModelViewModel.currentBasemapGalleryItem?.basemap) - - // Save the array of developer basemap items from AGOL. - let developerBasemapItems = basemapGalleryItems - - // - // Portal. - // - geoModel = Map(basemap: Basemap.lightGrayCanvas()) - let portal = Portal.arcGISOnline(isLoginRequired: false) - let portalViewModel = BasemapGalleryViewModel(geoModel, portal: portal) - - XCTAssertTrue(geoModel === portalViewModel.geoModel) - XCTAssertTrue(portal === portalViewModel.portal) - - // With a portal, `basemapGalleryItems` are fetched from either the - // portal's vector basemaps or regular basemaps. - items = try await portalViewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - basemapGalleryItems = try XCTUnwrap(items) - XCTAssertFalse(basemapGalleryItems.isEmpty) - - XCTAssertEqual(geoModel.loadStatus, .loaded) - XCTAssertTrue(geoModel.basemap === portalViewModel.currentBasemapGalleryItem?.basemap) - - // Sort the developer items from the "GeoModel" test above and the - // items from the portal and make sure they are not equal. - let sortedItems = basemapGalleryItems.sorted { $0.name < $1.name } - let sortedDeveloperItems = developerBasemapItems.sorted { $0.name < $1.name } - XCTAssertNotEqual(sortedItems, sortedDeveloperItems) - - // - // BasemapGalleryItems. No basemaps are fetched from a portal. - // - geoModel = Map(basemap: Basemap.lightGrayCanvas()) - let itemsViewModel = BasemapGalleryViewModel( - geoModel, - basemapGalleryItems: defaultBasemapGalleryItems - ) - - // The item counts should match. - XCTAssertEqual( - itemsViewModel.basemapGalleryItems.count, - defaultBasemapGalleryItems.count - ) - } - - /// Test the `GeoModel.actualSpatialReference` extension property. - func testGeoModelActualSpatialReference() async throws { - // Map with .webMercator basemap. - let geoModel = Map(basemap: Basemap.lightGrayCanvas()) - try await geoModel.load() - XCTAssertEqual(geoModel.actualSpatialReference, .webMercator) - - // Map with .wgs84 basemap. - let geoModel2 = Map( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - - try await geoModel2.load() - XCTAssertEqual(geoModel2.actualSpatialReference, .wgs84) - - // Test with Scene that has a tiling scheme of .webMercator - let geoModel3 = Scene( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ) - - try await geoModel3.load() - XCTAssertEqual(geoModel3.actualSpatialReference, .webMercator) - - // Test with Scene that has a tiling scheme of .wgs84 - let geoModel4 = Scene( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - try await geoModel4.load() - XCTAssertEqual(geoModel4.actualSpatialReference, .wgs84) - } - - /// Test the `currentBasemapGalleryItem` property including valid and invalid basemaps. - func testCurrentBasemapGalleryItem() async throws { - let basemap = Basemap.streets() - let geoModel = Map(basemap: basemap) - - let viewModel = BasemapGalleryViewModel(geoModel) - - // Verify current item is equal to map's basemap. - var item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).first - var currentItem = try XCTUnwrap(item) - XCTAssertTrue(currentItem.basemap === basemap) - - // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). - let validItem = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! - )! - ) - ) - - // Wait until it loads. - _ = try await validItem.$isLoading.compactMap({ $0 }).dropFirst().first - - // Update the item on the model. - viewModel.updateCurrentBasemapGalleryItem(validItem) - - // Wait until the `currentBasemapGalleryItem` is updated. - item = try await viewModel.$currentBasemapGalleryItem.compactMap({ $0 }).dropFirst().first - currentItem = try XCTUnwrap(item) - - // Items should equal, meaning the `validItem` was set properly. - XCTAssertEqual(currentItem, validItem) - - // Test WGS84 basemap item (Imagery (WGS84)). This item is in a - // different spatial reference than the geoModel, so it should - // not be set as the current item. - let invalidItem = BasemapGalleryItem( - basemap: Basemap( - item: PortalItem( - url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! - )! - ) - ) - - // Wait until it loads. - _ = try await invalidItem.$isLoading.compactMap({ $0 }).dropFirst().first - - // Update the item on the model. - viewModel.updateCurrentBasemapGalleryItem(invalidItem) - - // The update will fail, so wait until the - // `$spatialReferenceMismatchError` is updated. - let error = try await viewModel.$spatialReferenceMismatchError.compactMap({ $0 }).first - XCTAssertNotNil(error, "Error is not nil.") - - // Make sure the current item is still equal to the valid item. - XCTAssertEqual(currentItem, validItem) - } - - /// Test setting the portal after the model has been created. - func testUpdatePortal() async throws { - // Create a model with a default list of items. - let viewModel = BasemapGalleryViewModel( - basemapGalleryItems: defaultBasemapGalleryItems - ) - - var items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).first - var basemapGalleryItems = try XCTUnwrap(items) - - // There are exactly two default items. - XCTAssertEqual(basemapGalleryItems.count, 2) - - // Set a portal on the model. This should clear out the existing - // array of items and load basemaps from the portal. - viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) - - // The items should be cleared prior to loading those from the portal. - items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).first - basemapGalleryItems = try XCTUnwrap(items) - XCTAssertTrue(basemapGalleryItems.isEmpty) - - // Wait for the portal basemaps to load. - items = try await viewModel.$basemapGalleryItems.compactMap({ $0 }).dropFirst().first - basemapGalleryItems = try XCTUnwrap(items) - - // There should be no default items in the basemap gallery. - let foundDefaultItem = basemapGalleryItems.first(where: { - $0 == defaultBasemapGalleryItems[0] || - $0 == defaultBasemapGalleryItems[1] - }) - XCTAssertNil(foundDefaultItem) - } -} From 74af5887aa95dad5dae86e85ec3cb59cf90ef6af Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 17 Dec 2021 09:47:35 -0600 Subject: [PATCH 0348/1722] Initial framework for Floating Panel. --- Examples/Examples.xcodeproj/project.pbxproj | 4 ++ .../Examples/FloatingPanelExampleView.swift | 24 +++++++++ Examples/ExamplesApp/Examples.swift | 10 +++- .../FloatingPanel/FloatingPanel.swift | 49 +++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 Examples/Examples/FloatingPanelExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 65d3ad5ee..919ab6d20 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ E48A73452658227100F5C118 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733D2658227000F5C118 /* Examples.swift */; }; E48A73462658227100F5C118 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73422658227100F5C118 /* ExampleView.swift */; }; E48A73472658227100F5C118 /* ExampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733E2658227100F5C118 /* ExampleList.swift */; }; + E4AA9316276BF5ED000E6289 /* FloatingPanelExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */; }; E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = E4E57DC5265D8EB00077A093 /* ArcGISToolkit */; }; E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */; }; /* End PBXBuildFile section */ @@ -50,6 +51,7 @@ E48A73402658227100F5C118 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; E48A73412658227100F5C118 /* AnyExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyExample.swift; sourceTree = ""; }; E48A73422658227100F5C118 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; + E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelExampleView.swift; sourceTree = ""; }; E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewMapExampleView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -71,6 +73,7 @@ children = ( E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, E42BFBE82672BF9500159107 /* SearchExampleView.swift */, + E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -212,6 +215,7 @@ E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, + E4AA9316276BF5ED000E6289 /* FloatingPanelExampleView.swift in Sources */, E48A73442658227100F5C118 /* AnyExample.swift in Sources */, E48A73472658227100F5C118 /* ExampleList.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift new file mode 100644 index 000000000..2952eb35a --- /dev/null +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -0,0 +1,24 @@ +// +// FloatingPanelExampleView.swift +// Examples +// +// Created by Mark Dostal on 12/16/21. +// + +import SwiftUI +import ArcGISToolkit +import ArcGIS + +struct FloatingPanelExampleView: View { + let map = Map(basemapStyle: .arcGISImagery) + + var body: some View { + FloatingPanel(content: MapView(map: map)) + } +} + +struct FloatingPanelExampleView_Previews: PreviewProvider { + static var previews: some View { + FloatingPanelExampleView() + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 5bbf43128..56600fc26 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -16,7 +16,8 @@ import SwiftUI struct Examples: View { /// The list of example lists. Allows for a hierarchical navigation model for examples. let lists: [ExampleList] = [ - .geoview + .geoview, + .views ] var body: some View { @@ -38,4 +39,11 @@ extension ExampleList { AnyExample("Search", content: SearchExampleView()) ] ) + + static let views = Self( + name: "Views", + examples: [ + AnyExample("FloatingPanel", content: FloatingPanelExampleView()) + ] + ) } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift new file mode 100644 index 000000000..e1164624f --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -0,0 +1,49 @@ +// Copyright 2021 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 + +public struct FloatingPanel : View where Content : View { + /// The content that is to be housed in the floating panel. + let content: Content + + public init( + content: Content + ) { + self.content = content + } + + public var body: some View { + VStack(alignment: .center) { + Rectangle().foregroundColor(.blue) //content +// Rectangle() +// .foregroundColor(.secondary) +// .frame(height: 1.0) + Rectangle() + .foregroundColor(.secondary) + .frame(width: 100, height: 8.0) + .cornerRadius(4.0) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + } + .frame(width: 300) + .esriBorder(padding: EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .padding() + } +} + +struct FloatingPanel_Previews: PreviewProvider { + static var previews: some View { + FloatingPanel(content: Rectangle().foregroundColor(.blue)) + } +} From 54cb49368a7f8c6d464c031cc860cbf0e358ccb0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 17 Dec 2021 10:07:56 -0600 Subject: [PATCH 0349/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- .../Examples/BasemapGalleryExampleView.swift | 11 ++++------ .../BasemapGallery/BasemapGallery.swift | 20 ++++--------------- .../BasemapGallery/BasemapGalleryItem.swift | 14 ++++++------- .../BasemapGalleryViewModel.swift | 5 ++--- 4 files changed, 17 insertions(+), 33 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 548450ac9..680b0c91a 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -25,12 +25,12 @@ struct BasemapGalleryExampleView: View { /// `true` if the basemap gallery should be displayed; `false` otherwise. @State - var showBasemapGallery: Bool = true + private var showBasemapGallery: Bool = true /// The initial viewpoint of the map. - let initialViewpoint: Viewpoint? = Viewpoint( + let initialViewpoint = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), - scale: 1000000 + scale: 1_000_000 ) var body: some View { @@ -49,10 +49,7 @@ struct BasemapGalleryExampleView: View { .navigationBarItems(trailing: Button { showBasemapGallery.toggle() } label: { - HStack(alignment: .center) { - Image(uiImage: UIImage(named: "basemap")!) - Text(showBasemapGallery ? "Hide Basemaps" : "Show Basemaps") - } + Image("basemap", label: Text("Basemaps")) }) } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 2edadd5a9..806662ba3 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -23,12 +23,7 @@ public struct BasemapGallery: View { /// Creates a `BasemapGallery`. /// - Parameter viewModel: The view model used by the `BasemapGallery`. public init(viewModel: BasemapGalleryViewModel? = nil) { - if let viewModel = viewModel { - self.viewModel = viewModel - } - else { - self.viewModel = BasemapGalleryViewModel() - } + self.viewModel = viewModel ?? BasemapGalleryViewModel() } /// The view model used by the view. The `BasemapGalleryViewModel` manages the state @@ -60,12 +55,8 @@ private extension BasemapGallery { /// - Returns: A view representing the basemap gallery with the specified columns. func GalleryView() -> some View { ScrollView { - let columns: [GridItem] = [ - .init(.flexible(), spacing: 8.0, alignment: .top), - .init(.flexible(), spacing: 8.0, alignment: .top), - .init(.flexible(), spacing: 8.0, alignment: .top) - ] - LazyVGrid(columns: columns, spacing: 4) { + let columns = Array(repeating: GridItem(.flexible(), alignment: .top), count: 3) + LazyVGrid(columns: columns) { ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in BasemapGalleryItemRow( basemapGalleryItem: basemapGalleryItem, @@ -74,8 +65,7 @@ private extension BasemapGallery { .onTapGesture { if let loadError = basemapGalleryItem.loadBasemapsError { alertItem = AlertItem(loadBasemapError: loadError) - } - else { + } else { viewModel.currentBasemapGalleryItem = basemapGalleryItem } } @@ -114,11 +104,9 @@ private struct BasemapGalleryItemRow: View { // Display a progress view if the item is loading. if basemapGalleryItem.isLoading { - Spacer() ProgressView() .progressViewStyle(CircularProgressViewStyle()) .esriBorder() - Spacer() } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 20273757a..75e10d195 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -39,7 +39,9 @@ public class BasemapGalleryItem: ObservableObject { self.thumbnailOverride = thumbnail self.thumbnail = thumbnail - Task { await loadBasemap() } + if basemap.loadStatus != .loaded { + Task { await loadBasemap() } + } } /// The error generated loading the basemap, if any. @@ -47,7 +49,7 @@ public class BasemapGalleryItem: ObservableObject { public private(set) var loadBasemapsError: RuntimeError? = nil /// The basemap represented by `BasemapGalleryItem`. - public private(set) var basemap: Basemap + public let basemap: Basemap /// The name of the `basemap`. @Published @@ -64,7 +66,7 @@ public class BasemapGalleryItem: ObservableObject { public private(set) var thumbnail: UIImage? = nil private var thumbnailOverride: UIImage? = nil - /// Denotes whether the `basemap` or it's base layers are being loaded. + /// A Boolean value indicating whether the `basemap` or it's base layers are being loaded. @Published public private(set) var isLoading = true } @@ -98,9 +100,7 @@ private extension BasemapGalleryItem { } } -extension BasemapGalleryItem: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(self) } -} +extension BasemapGalleryItem: Identifiable {} extension BasemapGalleryItem: Equatable { public static func == (lhs: BasemapGalleryItem, rhs: BasemapGalleryItem) -> Bool { @@ -115,6 +115,6 @@ private extension UIImage { /// A default thumbnail image. /// - Returns: The default thumbnail. static func defaultThumbnail() -> UIImage { - return UIImage(named: "DefaultBasemap", in: Bundle.module, with: nil)! + return UIImage(named: "DefaultBasemap", in: .module, with: nil)! } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 41d206479..a6d2c86e9 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -20,13 +20,12 @@ public class BasemapGalleryViewModel: ObservableObject { /// Creates a `BasemapGalleryViewModel` /// - Remark: The ArcGISOnline's developer basemaps will /// be loaded and added to `basemapGalleryItems`. - /// - Parameters: - /// - geoModel: The `GeoModel`. + /// - Parameter geoModel: The `GeoModel`. public convenience init(_ geoModel: GeoModel? = nil) { self.init(geoModel, basemapGalleryItems: []) } - /// Creates a `BasemapGalleryViewModel`. Uses the given list of basemap gallery items. + /// Creates a `BasemapGalleryViewModel`. Uses the given array of basemap gallery items. /// - Remark: If `basemapGalleryItems` is empty, ArcGISOnline's developer basemaps will /// be loaded and added to `basemapGalleryItems`. /// - Parameters: From 5f6b8204f2b7672168d3f9def0585a0f1def31e9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 17 Dec 2021 11:23:27 -0600 Subject: [PATCH 0350/1722] Partial PR review changes. --- .../Examples/BasemapGalleryExampleView.swift | 16 ++++--- .../BasemapGallery/BasemapGallery.swift | 45 ++++++++++--------- .../BasemapGalleryViewModel.swift | 16 +++---- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 680b0c91a..c8ec85cf8 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -46,16 +46,20 @@ struct BasemapGalleryExampleView: View { } } .navigationTitle("Basemap Gallery") - .navigationBarItems(trailing: Button { - showBasemapGallery.toggle() - } label: { - Image("basemap", label: Text("Basemaps")) - }) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + showBasemapGallery.toggle() + } label: { + Image("basemap", label: Text("Basemaps")) + } + } + } } private func SetupViewModel() { viewModel.geoModel = map - viewModel.basemapGalleryItems.append( + viewModel.items.append( contentsOf: BasemapGalleryExampleView.initialBasemaps() ) } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 806662ba3..64d1001e6 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -33,19 +33,23 @@ public struct BasemapGallery: View { @ObservedObject public var viewModel: BasemapGalleryViewModel + /// A Boolean value indicating whether the error alert should be shown. + @State + private var showErrorAlert = false + /// The current alert item to display. @State private var alertItem: AlertItem? public var body: some View { - GalleryView() + makeGalleryView() .frame(width: 300) - .alert(item: $alertItem) { alertItem in - Alert( - title: Text(alertItem.title), - message: Text(alertItem.message) - ) - } + .alert( + alertItem?.title ?? "", + isPresented: $showErrorAlert, + presenting: alertItem) { item in + Text(item.message) + } } } @@ -53,20 +57,21 @@ private extension BasemapGallery { /// The gallery view, displayed in the specified columns. /// - Parameter columns: The columns used to display the basemap items. /// - Returns: A view representing the basemap gallery with the specified columns. - func GalleryView() -> some View { + func makeGalleryView() -> some View { ScrollView { let columns = Array(repeating: GridItem(.flexible(), alignment: .top), count: 3) LazyVGrid(columns: columns) { - ForEach(viewModel.basemapGalleryItems) { basemapGalleryItem in + ForEach(viewModel.items) { item in BasemapGalleryItemRow( - basemapGalleryItem: basemapGalleryItem, - isSelected: basemapGalleryItem == viewModel.currentBasemapGalleryItem + item: item, + isSelected: item == viewModel.currentBasemapGalleryItem ) .onTapGesture { - if let loadError = basemapGalleryItem.loadBasemapsError { + if let loadError = item.loadBasemapsError { alertItem = AlertItem(loadBasemapError: loadError) + showErrorAlert = true } else { - viewModel.currentBasemapGalleryItem = basemapGalleryItem + viewModel.currentBasemapGalleryItem = item } } } @@ -78,14 +83,14 @@ private extension BasemapGallery { /// A row or grid element representing a basemap gallery item. private struct BasemapGalleryItemRow: View { - @ObservedObject var basemapGalleryItem: BasemapGalleryItem + @ObservedObject var item: BasemapGalleryItem let isSelected: Bool var body: some View { VStack { ZStack(alignment: .center) { // Display the thumbnail, if available. - if let thumbnailImage = basemapGalleryItem.thumbnail { + if let thumbnailImage = item.thumbnail { Image(uiImage: thumbnailImage) .resizable() .aspectRatio(contentMode: .fit) @@ -96,14 +101,14 @@ private struct BasemapGalleryItemRow: View { // Display an image representing either a load basemap error // or a spatial reference mismatch error. - if basemapGalleryItem.loadBasemapsError != nil { + if item.loadBasemapsError != nil { Image(systemName: "minus.circle.fill") .font(.title) .foregroundColor(.red) } // Display a progress view if the item is loading. - if basemapGalleryItem.isLoading { + if item.isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle()) .esriBorder() @@ -111,12 +116,12 @@ private struct BasemapGalleryItemRow: View { } // Display the name of the item. - Text(basemapGalleryItem.name) + Text(item.name) .font(.footnote) .multilineTextAlignment(.center) } .allowsHitTesting( - !basemapGalleryItem.isLoading + !item.isLoading ) } } @@ -139,7 +144,7 @@ extension AlertItem { init(loadBasemapError: RuntimeError) { self.init( title: "Error loading basemap.", - message: "\(loadBasemapError.failureReason ?? "")" + message: "\(loadBasemapError.failureReason ?? "The basemap failed to load for an unknown reason.")" ) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index a6d2c86e9..7e809d22d 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -19,23 +19,23 @@ import ArcGIS public class BasemapGalleryViewModel: ObservableObject { /// Creates a `BasemapGalleryViewModel` /// - Remark: The ArcGISOnline's developer basemaps will - /// be loaded and added to `basemapGalleryItems`. + /// be loaded and added to `items`. /// - Parameter geoModel: The `GeoModel`. public convenience init(_ geoModel: GeoModel? = nil) { - self.init(geoModel, basemapGalleryItems: []) + self.init(geoModel, items: []) } /// Creates a `BasemapGalleryViewModel`. Uses the given array of basemap gallery items. - /// - Remark: If `basemapGalleryItems` is empty, ArcGISOnline's developer basemaps will - /// be loaded and added to `basemapGalleryItems`. + /// - Remark: If `items` is empty, ArcGISOnline's developer basemaps will + /// be loaded and added to `items`. /// - Parameters: /// - geoModel: The `GeoModel`. - /// - basemapGalleryItems: A list of pre-defined base maps to display. + /// - items: A list of pre-defined base maps to display. public init( _ geoModel: GeoModel? = nil, - basemapGalleryItems: [BasemapGalleryItem] + items: [BasemapGalleryItem] ) { - self.basemapGalleryItems.append(contentsOf: basemapGalleryItems) + self.items.append(contentsOf: items) defer { // Using `defer` allows the property `didSet` observers to be called. @@ -57,7 +57,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// The list of basemaps currently visible in the gallery. It is comprised of items passed into /// the `BasemapGalleryItem` constructor property. @Published - public var basemapGalleryItems: [BasemapGalleryItem] = [] + public var items: [BasemapGalleryItem] = [] /// The `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a /// basemap which does not exist in the gallery. From fe70f6e3fe1e9a3324c9fe96db124c259bfda144 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 20 Dec 2021 09:50:02 -0600 Subject: [PATCH 0351/1722] PR review feedback. --- .../BasemapGallery/BasemapGallery.swift | 68 +++---------------- .../BasemapGallery/BasemapGalleryCell.swift | 64 +++++++++++++++++ .../BasemapGallery/BasemapGalleryItem.swift | 34 ++++------ .../BasemapGalleryViewModel.swift | 33 +++++---- 4 files changed, 108 insertions(+), 91 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 64d1001e6..df4099269 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -62,18 +62,17 @@ private extension BasemapGallery { let columns = Array(repeating: GridItem(.flexible(), alignment: .top), count: 3) LazyVGrid(columns: columns) { ForEach(viewModel.items) { item in - BasemapGalleryItemRow( + BasemapGalleryCell( item: item, - isSelected: item == viewModel.currentBasemapGalleryItem - ) - .onTapGesture { - if let loadError = item.loadBasemapsError { - alertItem = AlertItem(loadBasemapError: loadError) - showErrorAlert = true - } else { - viewModel.currentBasemapGalleryItem = item - } + isSelected: item == viewModel.currentItem + ) { + if let loadError = item.loadBasemapsError { + alertItem = AlertItem(loadBasemapError: loadError) + showErrorAlert = true + } else { + viewModel.currentItem = item } + } } } } @@ -81,51 +80,6 @@ private extension BasemapGallery { } } -/// A row or grid element representing a basemap gallery item. -private struct BasemapGalleryItemRow: View { - @ObservedObject var item: BasemapGalleryItem - let isSelected: Bool - - var body: some View { - VStack { - ZStack(alignment: .center) { - // Display the thumbnail, if available. - if let thumbnailImage = item.thumbnail { - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .border( - isSelected ? Color.accentColor: Color.clear, - width: 3.0) - } - - // Display an image representing either a load basemap error - // or a spatial reference mismatch error. - if item.loadBasemapsError != nil { - Image(systemName: "minus.circle.fill") - .font(.title) - .foregroundColor(.red) - } - - // Display a progress view if the item is loading. - if item.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .esriBorder() - } - } - - // Display the name of the item. - Text(item.name) - .font(.footnote) - .multilineTextAlignment(.center) - } - .allowsHitTesting( - !item.isLoading - ) - } -} - // MARK: AlertItem /// An item used to populate a displayed alert. @@ -141,10 +95,10 @@ extension AlertItem: Identifiable { extension AlertItem { /// Creates an alert item based on an error generated loading a basemap. /// - Parameter loadBasemapError: The load basemap error. - init(loadBasemapError: RuntimeError) { + init(loadBasemapError: Error) { self.init( title: "Error loading basemap.", - message: "\(loadBasemapError.failureReason ?? "The basemap failed to load for an unknown reason.")" + message: "\((loadBasemapError as? RuntimeError)?.failureReason ?? "The basemap failed to load for an unknown reason.")" ) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift new file mode 100644 index 000000000..d99b0ddf3 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -0,0 +1,64 @@ +// +// SwiftUIView.swift +// +// +// Created by Mark Dostal on 12/17/21. +// + +import SwiftUI + +/// A row or grid element representing a basemap gallery item. +struct BasemapGalleryCell: View { + /// The displayed item. + @ObservedObject var item: BasemapGalleryItem + + /// A Boolean specifying if the item should be displayed ais selected. + let isSelected: Bool + + /// The action executed when the item is selected. + var onSelection: () -> Void + + var body: some View { + VStack { + Button(action: { + onSelection() + }, label: { + VStack { + ZStack(alignment: .center) { + // Display the thumbnail, if available. + if let thumbnailImage = item.thumbnail { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .border( + isSelected ? Color.accentColor: Color.clear, + width: 3.0) + } + + // Display an image representing either a load basemap error + // or a spatial reference mismatch error. + if item.loadBasemapsError != nil { + Image(systemName: "minus.circle.fill") + .font(.title) + .foregroundColor(.red) + } + + // Display a progress view if the item is loading. + if item.isBasemapLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .esriBorder() + } + } + + // Display the name of the item. + Text(item.name ?? "") + .font(.footnote) + .multilineTextAlignment(.center) + .foregroundColor(.primary) + } + }) + .disabled(item.basemap.loadStatus != .loaded) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 75e10d195..9513bae49 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -32,11 +32,8 @@ public class BasemapGalleryItem: ObservableObject { thumbnail: UIImage? = nil ) { self.basemap = basemap - self.nameOverride = name - self.name = name ?? "" - self.descriptionOverride = description + self.name = name self.description = description - self.thumbnailOverride = thumbnail self.thumbnail = thumbnail if basemap.loadStatus != .loaded { @@ -46,42 +43,39 @@ public class BasemapGalleryItem: ObservableObject { /// The error generated loading the basemap, if any. @Published - public private(set) var loadBasemapsError: RuntimeError? = nil + private(set) var loadBasemapsError: Error? = nil /// The basemap represented by `BasemapGalleryItem`. public let basemap: Basemap /// The name of the `basemap`. @Published - public private(set) var name: String = "" - private var nameOverride: String? = nil - + public private(set) var name: String? + /// The description of the `basemap`. @Published - public private(set) var description: String? = nil - private var descriptionOverride: String? = nil + public private(set) var description: String? /// The thumbnail used to represent the `basemap`. @Published - public private(set) var thumbnail: UIImage? = nil - private var thumbnailOverride: UIImage? = nil + public private(set) var thumbnail: UIImage? /// A Boolean value indicating whether the `basemap` or it's base layers are being loaded. @Published - public private(set) var isLoading = true + private(set) var isBasemapLoading = true } private extension BasemapGalleryItem { /// Loads the basemap and the item's thumbnail, if available. func loadBasemap() async { - var loadError: RuntimeError? = nil + var loadError: Error? = nil do { try await basemap.load() if let loadableImage = basemap.item?.thumbnail { try await loadableImage.load() } } catch { - loadError = error as? RuntimeError + loadError = error } await finalizeLoading(error: loadError) } @@ -89,14 +83,14 @@ private extension BasemapGalleryItem { /// Updates the item in response to basemap loading completion. /// - Parameter error: The basemap load error, if any. @MainActor - func finalizeLoading(error: RuntimeError?) { - name = nameOverride ?? basemap.name - description = descriptionOverride ?? basemap.item?.description - thumbnail = thumbnailOverride ?? + func finalizeLoading(error: Error?) { + name = name ?? basemap.name + description = description ?? basemap.item?.description + thumbnail = thumbnail ?? (basemap.item?.thumbnail?.image ?? UIImage.defaultThumbnail()) loadBasemapsError = error - isLoading = false + isBasemapLoading = false } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 7e809d22d..99ea4d4e4 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -21,8 +21,8 @@ public class BasemapGalleryViewModel: ObservableObject { /// - Remark: The ArcGISOnline's developer basemaps will /// be loaded and added to `items`. /// - Parameter geoModel: The `GeoModel`. - public convenience init(_ geoModel: GeoModel? = nil) { - self.init(geoModel, items: []) + public convenience init(geoModel: GeoModel? = nil) { + self.init(geoModel: geoModel, items: []) } /// Creates a `BasemapGalleryViewModel`. Uses the given array of basemap gallery items. @@ -32,15 +32,12 @@ public class BasemapGalleryViewModel: ObservableObject { /// - geoModel: The `GeoModel`. /// - items: A list of pre-defined base maps to display. public init( - _ geoModel: GeoModel? = nil, + geoModel: GeoModel? = nil, items: [BasemapGalleryItem] ) { self.items.append(contentsOf: items) - defer { - // Using `defer` allows the property `didSet` observers to be called. - self.geoModel = geoModel - } + geoModelDidChange(nil) } /// If the `GeoModel` is not loaded when passed to the `BasemapGalleryViewModel`, then @@ -49,8 +46,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// the geoModel will have its basemap replaced with the selected basemap. public var geoModel: GeoModel? { didSet { - guard let geoModel = geoModel else { return } - Task { await load(geoModel: geoModel) } + geoModelDidChange(oldValue) } } @@ -62,16 +58,25 @@ public class BasemapGalleryViewModel: ObservableObject { /// The `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a /// basemap which does not exist in the gallery. @Published - public var currentBasemapGalleryItem: BasemapGalleryItem? = nil { + public var currentItem: BasemapGalleryItem? = nil { didSet { - guard let item = currentBasemapGalleryItem else { return } + guard let item = currentItem else { return } geoModel?.basemap = item.basemap } } + + /// Handles changes to the `geoModel` property. + /// - Parameter previousGeoModel: The previously set `geoModel`. + func geoModelDidChange(_ previousGeoModel: GeoModel?) { + guard let geoModel = geoModel else { return } + if geoModel.loadStatus != .loaded { + Task { await load(geoModel: geoModel) } + } + } } private extension BasemapGalleryViewModel { - /// Loads the given `GeoModel` then sets `currentBasemapGalleryItem` to an item + /// Loads the given `GeoModel` then sets `currentItem` to an item /// created with the geoModel's basemap. /// - Parameter geoModel: The `GeoModel` to load. func load(geoModel: GeoModel?) async { @@ -79,10 +84,10 @@ private extension BasemapGalleryViewModel { do { try await geoModel.load() if let basemap = geoModel.basemap { - currentBasemapGalleryItem = BasemapGalleryItem(basemap: basemap) + currentItem = BasemapGalleryItem(basemap: basemap) } else { - currentBasemapGalleryItem = nil + currentItem = nil } } catch { } } From 7371012efe3b13c330626a96a338de2c76a53f37 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 20 Dec 2021 11:23:35 -0600 Subject: [PATCH 0352/1722] Use.svg images for default basemap --- .../Examples/BasemapGalleryExampleView.swift | 15 +++++++++------ .../basemap.imageset/Contents.json | 2 +- .../basemap.imageset/basemap-64.svg | 1 + .../basemap.imageset/basemap@1x.png | Bin 446 -> 0 bytes .../BasemapGallery/BasemapGalleryItem.swift | 2 +- .../Contents.json | 2 +- .../Basemap.imageset/basemap-64.svg | 1 + .../generic_basemap_thumbnail.jpg | Bin 8581 -> 0 bytes 8 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg delete mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png rename Sources/ArcGISToolkit/Resources/Images.xcassets/{DefaultBasemap.imageset => Basemap.imageset}/Contents.json (70%) create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg delete mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index c8ec85cf8..ceaa06ff9 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -36,11 +36,6 @@ struct BasemapGalleryExampleView: View { var body: some View { ZStack(alignment: .topTrailing) { MapView(map: map, viewpoint: initialViewpoint) - .overlay(alignment: .topTrailing) { - if showBasemapGallery { - BasemapGallery(viewModel: viewModel) - } - } .onAppear() { SetupViewModel() } @@ -51,10 +46,18 @@ struct BasemapGalleryExampleView: View { Button { showBasemapGallery.toggle() } label: { - Image("basemap", label: Text("Basemaps")) + Image("basemap") + .resizable() + .aspectRatio(contentMode: .fit) + .background(Color(white: 1.0, opacity: 0.8)) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + .popover(isPresented: $showBasemapGallery) { + BasemapGallery(viewModel: viewModel) } } } + } private func SetupViewModel() { diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json index ccc73709c..8f4b02651 100644 --- a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json +++ b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "basemap@1x.png", + "filename" : "basemap-64.svg", "idiom" : "universal" }, { diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg new file mode 100644 index 000000000..d180daa17 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png deleted file mode 100644 index 3182e883959bc5e2d961ef1221366f47c71be440..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmV;v0YUzWP)K7t?^Xkd1I05KN>UtsYkG0>o=$G*+X>?qvG0}VArRd=82sx$D`a`k+m*EVmg zNVrBH-Na7ORgGQDp;Oo?qJQ9ak2t8L^}KXD;wQynVH zlG^cr7vVo}T737hBB<$h!vQI}Dmp8^!IAK@NpX%#LHOEWr^*=_Jl7;GVZP-7X^Bci o1|97OE=8WxN&Ev{G_*l`2hI7upU5cWssI2007*qoM6N<$g1@NEYybcN diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 9513bae49..bb6f69f4d 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -109,6 +109,6 @@ private extension UIImage { /// A default thumbnail image. /// - Returns: The default thumbnail. static func defaultThumbnail() -> UIImage { - return UIImage(named: "DefaultBasemap", in: .module, with: nil)! + return UIImage(named: "basemap", in: .module, with: nil)! } } diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/Contents.json similarity index 70% rename from Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/Contents.json rename to Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/Contents.json index 288c7a409..fc093a16e 100644 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/Contents.json +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "generic_basemap_thumbnail.jpg", + "filename" : "basemap-64.svg", "idiom" : "universal" } ], diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg new file mode 100644 index 000000000..d180daa17 --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg b/Sources/ArcGISToolkit/Resources/Images.xcassets/DefaultBasemap.imageset/generic_basemap_thumbnail.jpg deleted file mode 100644 index d91d35911fc0df230996cc2f17aa61931244f690..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8581 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 bsu%sO95Ym?Qu$Mx$ojr~KaT$=jK26Eg#m?7 From 1e3374b068952eb49aa3bdb8e73737c331dfed4b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 21 Dec 2021 10:53:38 -0600 Subject: [PATCH 0353/1722] Start implementation of drag gesture. --- .../Examples/FloatingPanelExampleView.swift | 2 + .../FloatingPanel/FloatingPanel.swift | 49 ++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 2952eb35a..9aef892da 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -14,6 +14,8 @@ struct FloatingPanelExampleView: View { var body: some View { FloatingPanel(content: MapView(map: map)) +// FloatingPanel(content: Rectangle().foregroundColor(.blue)) + .padding() } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index e1164624f..bce95dbef 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -24,21 +24,58 @@ public struct FloatingPanel : View where Content : View { self.content = content } + @State + var handleColor: Color = .secondary + + var drag: some Gesture { + DragGesture() + // .onChanged { self.heightOffset = $0.translation.height} + .onChanged { + self.handleColor = .red + self.heightOffset = $0.translation.height + lastHeight = originalHeight + heightOffset + } + .onEnded { _ in + self.handleColor = .secondary + self.originalHeight = lastHeight + } + } + + @State + private var heightOffset: CGFloat = 0 + + @State + private var originalHeight: CGFloat = 0 + @State + private var lastHeight: CGFloat = 0 + + // private var currentHeight: CGFloat = 0 + public var body: some View { VStack(alignment: .center) { - Rectangle().foregroundColor(.blue) //content -// Rectangle() -// .foregroundColor(.secondary) -// .frame(height: 1.0) + GeometryReader { geometry in + content + .frame(height: originalHeight + heightOffset) + .onAppear { + if originalHeight == 0 { + originalHeight = geometry.size.height + } + } + } + // Rectangle() + // .foregroundColor(.secondary) + // .frame(height: 1.0) Rectangle() - .foregroundColor(.secondary) + .foregroundColor(handleColor) .frame(width: 100, height: 8.0) .cornerRadius(4.0) .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + .gesture(drag) + Text("\(self.heightOffset)") } .frame(width: 300) .esriBorder(padding: EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .padding() + Spacer() } } From 51d70ab617f33d7480530169bbd191e42bf8355d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 21 Dec 2021 16:27:54 -0600 Subject: [PATCH 0354/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- Examples/Examples/BasemapGalleryExampleView.swift | 2 +- .../Components/BasemapGallery/BasemapGallery.swift | 4 ---- .../BasemapGallery/BasemapGalleryCell.swift | 2 +- .../BasemapGallery/BasemapGalleryItem.swift | 13 +++++++++---- .../BasemapGallery/BasemapGalleryViewModel.swift | 5 ++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index ceaa06ff9..7aeb17a01 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -25,7 +25,7 @@ struct BasemapGalleryExampleView: View { /// `true` if the basemap gallery should be displayed; `false` otherwise. @State - private var showBasemapGallery: Bool = true + private var showBasemapGallery: Bool = false /// The initial viewpoint of the map. let initialViewpoint = Viewpoint( diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index df4099269..5055f445d 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -88,10 +88,6 @@ struct AlertItem { var message: String = "" } -extension AlertItem: Identifiable { - public var id: UUID { UUID() } -} - extension AlertItem { /// Creates an alert item based on an error generated loading a basemap. /// - Parameter loadBasemapError: The load basemap error. diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index d99b0ddf3..a2bc775f0 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -16,7 +16,7 @@ struct BasemapGalleryCell: View { let isSelected: Bool /// The action executed when the item is selected. - var onSelection: () -> Void + let onSelection: () -> Void var body: some View { VStack { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index bb6f69f4d..1390cad62 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -84,10 +84,15 @@ private extension BasemapGalleryItem { /// - Parameter error: The basemap load error, if any. @MainActor func finalizeLoading(error: Error?) { - name = name ?? basemap.name - description = description ?? basemap.item?.description - thumbnail = thumbnail ?? - (basemap.item?.thumbnail?.image ?? UIImage.defaultThumbnail()) + if name == nil { + name = basemap.name + } + if description == nil { + description = basemap.item?.description + } + if thumbnail == nil { + thumbnail = basemap.item?.thumbnail?.image ?? .defaultThumbnail() + } loadBasemapsError = error isBasemapLoading = false diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 99ea4d4e4..3829d01b2 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -85,10 +85,9 @@ private extension BasemapGalleryViewModel { try await geoModel.load() if let basemap = geoModel.basemap { currentItem = BasemapGalleryItem(basemap: basemap) - } - else { + } else { currentItem = nil } - } catch { } + } catch {} } } From a9782f3c1e478bedfb0d06b58141ef24a9a713c7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 22 Dec 2021 10:11:20 -0600 Subject: [PATCH 0355/1722] PR review update. --- .../Examples/BasemapGalleryExampleView.swift | 51 ++++++++---------- .../basemap.imageset/Contents.json | 4 +- .../basemap.imageset/basemap-64.svg | 1 - .../basemap.imageset/basemap@1x.png | Bin 0 -> 446 bytes .../basemap.imageset/basemap@2x.png | Bin 0 -> 599 bytes .../basemap.imageset/basemap@3x.png | Bin 0 -> 1077 bytes .../BasemapGallery/BasemapGalleryCell.swift | 20 ++++--- .../BasemapGallery/BasemapGalleryItem.swift | 14 +++-- .../BasemapGalleryViewModel.swift | 16 ++---- .../Basemap.imageset/Contents.json | 12 ----- .../Basemap.imageset/basemap-64.svg | 1 - .../defaultthumbnail.imageset/Contents.json | 26 +++++++++ .../defaultthumbnail.imageset/basemap@1x.png | Bin 0 -> 3072 bytes .../defaultthumbnail.imageset/basemap@2x.png | Bin 0 -> 5488 bytes .../defaultthumbnail.imageset/basemap@3x.png | Bin 0 -> 7247 bytes 15 files changed, 76 insertions(+), 69 deletions(-) delete mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png create mode 100644 Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@3x.png delete mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/Contents.json delete mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@1x.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@2x.png create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@3x.png diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 7aeb17a01..a796b1dc6 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct BasemapGalleryExampleView: View { /// The map displayed in the map view. - let map = Map(basemapStyle: .arcGISImagery) + let map: Map /// The view model for the basemap gallery. @ObservedObject @@ -33,38 +33,29 @@ struct BasemapGalleryExampleView: View { scale: 1_000_000 ) + init() { + self.map = Map(basemapStyle: .arcGISImagery) + self.viewModel = BasemapGalleryViewModel( + geoModel: self.map, + items: Self.initialBasemaps() + ) + } + var body: some View { - ZStack(alignment: .topTrailing) { - MapView(map: map, viewpoint: initialViewpoint) - .onAppear() { - SetupViewModel() - } - } - .navigationTitle("Basemap Gallery") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - showBasemapGallery.toggle() - } label: { - Image("basemap") - .resizable() - .aspectRatio(contentMode: .fit) - .background(Color(white: 1.0, opacity: 0.8)) - .clipShape(RoundedRectangle(cornerRadius: 4)) - } - .popover(isPresented: $showBasemapGallery) { - BasemapGallery(viewModel: viewModel) + MapView(map: map, viewpoint: initialViewpoint) + .navigationTitle("Basemap Gallery") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + showBasemapGallery.toggle() + } label: { + Image("basemap") + } + .popover(isPresented: $showBasemapGallery) { + BasemapGallery(viewModel: viewModel) + } } } - } - - } - - private func SetupViewModel() { - viewModel.geoModel = map - viewModel.items.append( - contentsOf: BasemapGalleryExampleView.initialBasemaps() - ) } static private func initialBasemaps() -> [BasemapGalleryItem] { diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json index 8f4b02651..800e56381 100644 --- a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json +++ b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json @@ -1,18 +1,20 @@ { "images" : [ { - "filename" : "basemap-64.svg", "idiom" : "universal" }, { + "filename" : "basemap@1x.png", "idiom" : "universal", "scale" : "1x" }, { + "filename" : "basemap@2x.png", "idiom" : "universal", "scale" : "2x" }, { + "filename" : "basemap@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg deleted file mode 100644 index d180daa17..000000000 --- a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap-64.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..3182e883959bc5e2d961ef1221366f47c71be440 GIT binary patch literal 446 zcmV;v0YUzWP)K7t?^Xkd1I05KN>UtsYkG0>o=$G*+X>?qvG0}VArRd=82sx$D`a`k+m*EVmg zNVrBH-Na7ORgGQDp;Oo?qJQ9ak2t8L^}KXD;wQynVH zlG^cr7vVo}T737hBB<$h!vQI}Dmp8^!IAK@NpX%#LHOEWr^*=_Jl7;GVZP-7X^Bci o1|97OE=8WxN&Ev{G_*l`2hI7upU5cWssI2007*qoM6N<$g1@NEYybcN literal 0 HcmV?d00001 diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/basemap@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..708fcaeca1d778a10b911a1ceb341f7823cbc4f4 GIT binary patch literal 599 zcmV-d0;v6oP)V0Y^(%(#nxM6;RV4)Q9)UB$6}b>W1NhMo7v64{1BMU{BzFvNOmSk0SIuff~Mbj zt~S>Jo2|?Pv@5^m&jgm6?tyinPe2@NynntWG>NU}x(z_oE(JsVhmTn4)Cb-ulWd271wvb zH1$7vA8@Gn7a9OCk07eapk91_1tf>GtDgz&_2GE_%nW_v-0k^=WK^)s$I)5NL8v!s$X6rHW l*ZMQRn9Fx$tybm%z5(?{11frOuf6~P002ovPDHLkV1iAm1S
If6+C<%(P#*K&Gg}3v1xBJb` z?9QzHhdE5W>h5|)cTIIo51d`WYaq>FC2d@rVc`Fo;-Cb@K?&AHWS`?$MYcafCx>c3 zb$PLJe-m~kbaIIGlNC7Y1lJf%1?f6EIgIp^yTqm|_Zw>(&^vM+bh>hXNz;aOm0Sn? zSh>HvX+ye7EJ(-r2GAI*+4J}cpEYSz4US1ChB?V(?L={o*CP7AVn@Qb zI%CZ-h_tIiPCp|$*dvSx1b)Qq$z%H;@MePq9#{q1rw7X4sy7jrDm>erLgU-jbcn;qQGh&YS zF@di${$y_%XNg^m_sowmCvYPkHyXTG1>?q`i!fP{&EQpR7h=;C{#?3g-5^y_+>D2@ ze(3kz!Yul}(ry}W&0gnXC$11SwHIK6_9}|4k_w#nM06f&X{;E7NV~Qe87pUrO+{o| z8#JoZow)IPX=uzVSPvxlUJER35tUf6bB_(XPJ|(3OTQ@bE<`fLZMJ7 z6bglgS);isUmbGJI!0dyFDGR;Uv~5LDGo|d9F!pDpo2p2ZsTf-w(L7t9jMHp1jRuK z>N;qBT4i-G&ikzi;f&%fxEg2hi*QD95}!0~-NLk$+(r}9RPuH+K8om7uUCZk?~Adj z62%R8SvW*~YsMdWLYPgLX6W{Yg&ed+IBNHl@gC1R@F}(}(XP%Ai$R<5p@on3p2X`y z47xj_w@o-Vz1!#!nO;&|cQX;$S^R`YgtI?$tQBI!zY+Z(aA9JsL56*UNV_`Z^fRIp zTVk;#bp+So4g51CpPB9SAz?-hvmJ*P=&v`V@}M(#6!+ryMfBdpgXTLA?i3DS^_ImP zG%e&khcf#*f_)MB0X?2YizSMC@b;MfP2zUkF(lhLd;h_heJz!0GFiJS84JjV>)T}_ z(%&gzMlNTN&PfE5W5t%(31Rls<+zY}53|Yqr(bV~Iq0y_ubju_g(bG{3~iM=jtYm% zdrzD7pgnk2`16y7#X$*TiQ+CR`#L&@ vI`+j9#jIp<#j16BUQsM4F(^rKP=fyeBxGdq<~Ib#00000NkvXXu0mjfI}r8H literal 0 HcmV?d00001 diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index a2bc775f0..8e71285c5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -1,9 +1,15 @@ -// -// SwiftUIView.swift -// -// -// Created by Mark Dostal on 12/17/21. -// +// Copyright 2021 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 @@ -58,7 +64,7 @@ struct BasemapGalleryCell: View { .foregroundColor(.primary) } }) - .disabled(item.basemap.loadStatus != .loaded) + .disabled(item.isBasemapLoading) } } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 1390cad62..fcacd7be7 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -36,8 +36,12 @@ public class BasemapGalleryItem: ObservableObject { self.description = description self.thumbnail = thumbnail - if basemap.loadStatus != .loaded { - Task { await loadBasemap() } + Task { + if basemap.loadStatus != .loaded { + await loadBasemap() + } else { + await finalizeLoading() + } } } @@ -51,7 +55,7 @@ public class BasemapGalleryItem: ObservableObject { /// The name of the `basemap`. @Published public private(set) var name: String? - + /// The description of the `basemap`. @Published public private(set) var description: String? @@ -83,7 +87,7 @@ private extension BasemapGalleryItem { /// Updates the item in response to basemap loading completion. /// - Parameter error: The basemap load error, if any. @MainActor - func finalizeLoading(error: Error?) { + func finalizeLoading(error: Error? = nil) { if name == nil { name = basemap.name } @@ -114,6 +118,6 @@ private extension UIImage { /// A default thumbnail image. /// - Returns: The default thumbnail. static func defaultThumbnail() -> UIImage { - return UIImage(named: "basemap", in: .module, with: nil)! + return UIImage(named: "defaultthumbnail", in: .module, with: nil)! } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 3829d01b2..876553f14 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -17,14 +17,6 @@ import ArcGIS /// Manages the state for a `BasemapGallery`. @MainActor public class BasemapGalleryViewModel: ObservableObject { - /// Creates a `BasemapGalleryViewModel` - /// - Remark: The ArcGISOnline's developer basemaps will - /// be loaded and added to `items`. - /// - Parameter geoModel: The `GeoModel`. - public convenience init(geoModel: GeoModel? = nil) { - self.init(geoModel: geoModel, items: []) - } - /// Creates a `BasemapGalleryViewModel`. Uses the given array of basemap gallery items. /// - Remark: If `items` is empty, ArcGISOnline's developer basemaps will /// be loaded and added to `items`. @@ -33,10 +25,10 @@ public class BasemapGalleryViewModel: ObservableObject { /// - items: A list of pre-defined base maps to display. public init( geoModel: GeoModel? = nil, - items: [BasemapGalleryItem] + items: [BasemapGalleryItem] = [] ) { - self.items.append(contentsOf: items) - + self.items = items + self.geoModel = geoModel geoModelDidChange(nil) } @@ -53,7 +45,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// The list of basemaps currently visible in the gallery. It is comprised of items passed into /// the `BasemapGalleryItem` constructor property. @Published - public var items: [BasemapGalleryItem] = [] + public var items: [BasemapGalleryItem] /// The `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a /// basemap which does not exist in the gallery. diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/Contents.json deleted file mode 100644 index fc093a16e..000000000 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "basemap-64.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg deleted file mode 100644 index d180daa17..000000000 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/Basemap.imageset/basemap-64.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json new file mode 100644 index 000000000..24873226e --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal" + }, + { + "filename" : "basemap@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "basemap@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "basemap@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@1x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..95bf6fe6618bceb99241ae79bf317c92c58f38bb GIT binary patch literal 3072 zcmZ`*2{=@H8$V+hvWv3Dm?2wcA;h(19m|M`d&d%knbBy>Ff(N=VcarVatlTHM6}SY z+)`-~nzBVjMY)QxyWFI)CFz@~ZuRxt@0{m<-uJxk@AoeM|NEThWcvEJ%SmfU0{|fB zfpNhJrKND`OF@Lsb&?vB001(eI6M39advm6Q|XMIvEh-V?L>M63Fq#Fwzl2|0ER~+ z2?QKwi(y;%dIF)X%fv{U5sy1`CJ)N1z4LBCt4uu(mz~d<*MUCtK^Pi`69}EaR-7?eRs4CnBTS49eOC?7(M^;_UkuUG}!CFzPEGHsLF+wU$ zu>{`SG}KtY;fyIbbygK}!a4F_@JIgxMH-J;p#6}H{meYC?2hsll$p9YN^uwr{0#Su z<>aG<8wapScn>lb3m6M^DL?|W9smn9knjdT8i2$n9RNH*nqPGs$nXnB3;?(kK>Q0P zNGL@YM!1CRFH)>r>?^{u9Q;++7eRN=ZFU!mb+MR0CIHANixvndE>!~nu~LdZo`uJH z+eFf15aFU@M6hFGMJ&LUZ6j1;NUU%uJBCVQ+OX|lpAa@eU1UbWpr0VDJ$5iW))(qb zXON(l2owSZvzLZKp|%Vn*#_t0_JuBdvxDtsv0`nI$oTkpM7%kI&WJ(^XT}D!aoQ3Cyon=8Ofl;vM6*KR1`Nng1(n!2ZM5;tFJ*mi`wlPa&c>I*CQ}9hd1{OT}!? zk1I53M8dt(`p1XCMyRvbs_Ic8YpN9i0ReBv9;gO}hHlIe$c@>RbR9=B=!P8=f}Wlp z*sfi>7%Z0F?5l<+uexkuYM>Z|1ORrvq-eAw2$~|cPOS3A;xYjU9a%5>~ zX>55JfAiL@gZn)YsqYuDe>Nm7sfZu1cwXAPy0UU}etv$^b~f$z>S{muQO*_olNT?l zr{(44i4=;*k@n@2SFY4p$sS4Mt*J~qf{NtaCg;{J1Yh@8@}ova=XG^q4`(H)paC9R zi@|2&^Fo4yO{{1GzO%~MSB6{X5koMJm4L+ROPU$qhq!epS-{+G z#q!fb?j#3p+_-VHyVAXwoKkZ)l9KIbYre?mNxpdb^4Yok;l4{@$hp9K{1J6Y!y8xq zj4V+qfzInZJudw52O4|Ys}M1jVy?Xc1(o`P_0-cUF?~00+K}`k(xh?vf!no{9Kte< zFS;pJgNs-;rm43RDrc#yd4<@Wz1n^BL(*ZPb7@0Vsty?-0@!gf4@7ru@IP82_H zdUN??kfFZa?7$89f}ETg8eNeQt}-ep#%V^qe(svVj3N?UvT?uW$NjWFI4C%H?<|c< zEik4PZ3(+**XrZiBUK6d6_yq_I5>!;hdgz2cOT>PxINw7{M<}2$DO4J_gEiP^?CT~6Iq2_5Ev^BrFDrFbJSA7HA{oXK+NKk8D zyNic|AMBN?Ep|xUVBS2no7=?b`73s`5;=ZV0q6tjDTm z!Y>2f-``)CUbbPEIRbZ&FfLOvEq8r6cTv85T>4qJxI~idnUJ!$ zF_tsd>v+WujDwctyAYM2-J4c&48W$Jt9U& zd(szNGQ^xmW(1XBq@1>w6_{KYF|J-2SeGmrAU5)wE98vyK;X5822+7Ruwv?3rS@oI zAZEZKM_o(nX(VV?aDGFhnLutrQv#~dowkJpA~>Gl9!b^Cl9lDRyWpN=GY^9EKM*xb z5^|ciUBu^rbHR4IwOTZNr4`-p3P0gfXSb!TAzUFDx+)U(Q0vytkvrYwEmN)Apda3O zwwUekWSUphh!tqcUtYd8GMHd!hEn#?14w-^aHJqb2a*-Hr!AUB>lJf3&<^Q&InE<4 zCmP$eg2(OQZw$-qd(Oi_CJ`R>AGcfcdvklPY&=y*^wq^Ye7S0Z zfgfZ4Y;74<_wdwCqYFpcHKqfH9R#%L1bKMxkxiu~8ffgXG~Mv*)Og!0&O|RCh3_{A fp{#|i>CA_f6*r-kjw=cOF9E>A)yL(c6CwS-l6d$^ literal 0 HcmV?d00001 diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@2x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..89e04b686eada81472e07630f3aa722048930f8d GIT binary patch literal 5488 zcmZ`-2{=^k+n+&5B1_CzhoV7QMr1c-jqI{zXY6AvgNbY{gehzGHAJ=~TlVZbA+n8a zls#nE@QvQS_kF+r_5aSfp7Y%2zVF|2ug`N`=Y(l%s$8UEp#lH^7w@Vn>Jn!s;!vR^ zCq92L3~3m-bVE8JU69m7Q`1?{rP&q$nJ~-cN44!ul&^(WANMxL zbzG>{)s2V^-r5j0cEdx)T!WW9FPZB`B;0+0oKewc1l|m@cl5?8iS?S zr9Jl2o%)4GVx!s_6=- z!OJ}ZZXm9fIT~wsfqu{UvM4pi3!NJ~hoY z#svp2(%$LKNeH$IrXeLg)qk4EGKdEHlZW_w#;8YhXSNH&z@oyJ=1BqE?rH|w9!+3k z<}e7OyLK8H0DdA(2{;F&2apmeAn^tOSpesL(*S@Pko9j`7YO~MLjnMVIs(Z4=$H`a zvrCmYi0D5vNhZnP8cvy{f72>ws`}LNWyI;co2s!V0C0ifYybhtsUQG=B-K&h2yLWs zPs#>`6tp@kqaX(9c7_7TV5Ep75`ngYV301Zo>CZD?%x_xMEZ;^#0~kaf_9eWHqy|B zD4;wL5OG0aL1At=DhLE3<6&zjrK_m?2TpvG<#s@$-K2zse0+QaeMALO9`-`So{snc(ywI}T+-HXVzW(wP z?P&LpC0EZsVG#oqI+F;A2nq}RHy8rr`2WDpB!9tv$Mu&vnX|>DjCB#7C>O6Yx8!cY zWPV%tKg@sp`wQ@o{(VEajC+#E|Rj{8Dy_X2gd2 zw9I&sIwpR~=-__&Vt^Vu3)$O@xHs%}<2j{Tk+<8>;zttCzJ6blSaZdf)ruyak7Ww) zefQNG+c4pxgX25Z)>@*JFkQ?_*-TB0d*SWv$8mYd@tVS2BTZzOx4e?z;fBl5nr3~E zc1XZ;^O`WzwjkL;lK|I(`%WZ`Jc6dx!g`OAHrH++iVPvErbj=Bph^Rqkxt`k^qdJ@$*P53z=ua2N>-0Z?cE6Fwua~usWnoFly z_%7#ebY081j_|U1^EsHCO{w==_^0$ zoS4;08A~3F@Ci_9cf4d5(7<1{N^rKdYM1EK2Kxew_dSPQoE5H%jGd^wU{C4`KA+VAi)OD?$MGb=sPTJZ}$id81Tt)1{_%J zPy5X9#c!!w;W;&eUfVLBC98I!BXR(%Uzy`rJAS;B9m$kgBv%d?O$d%r`M%jJd*#6m z1B;<3WkUTv?tx$-?=OYOP^kow&p6KkStZ$&ZDdt~=dVNBRo9}+C%7xyzWvnwvwcE;|g|-MJca8ywHqv6=D-zvZxbd%tKt5gOXl_E-fg`? z4Ot+HhQ_yWo#Y?(&+F^!`?1Wxi9F3z36b%!>dfHPaf3OJ@C}KQow98y1?QvEr{6jKcXZB=gLPH=Fb*`N5Kp)%|z7=wcxzZoyjX)GkaXW_-+X5qj41uSs$N8L}&Bw>8l$^{@Z$h=FPHPvZq z$m(ew5+iq6O7!8YI)@;HophBaYT~~2o=5Eyd9T?UQr#SJ z&%ngrtlI58TQtu;TSx%iOsC)1y5(?soF;WmQexa<3>%LKc7Z2h`>kFcZ{8*UyUj|0Z|+sOApSYutCf+XO~b_`KDqQ55wj@WC(K`2N8%plTb$Z zeEyuTK>t_bamK&sp(a1wvvX0~As4@B_xqH9m*l1U7OV-SNA}w8`}z4zr}^MMzPE0& z)^_cDOTtWfT{3-KAs!rl9ec9bLYtr$IT3Q;;b~9=yG7Ak-3ACzxjTGaMmMr#%y+$2 zy%G777p_FvOxkX4-EMO3m*j%4937|?vit!WDKa==Q!Nt&+^eIC$|P?s3(cfI~}tK^skh1Qpox7S_aJ<{63xsSvWELE`H33IcC9(lx_m=m7z}HV2bP{ zRH$kMg+ErBHFupE+f_+H@JL8|nvz~U9wnaTGxb~TtRGM;)SFSBI!=Sd9-(Dm zX=L>za|T?!4E>lZxL2I%&q6Z$nkxT_Xg@Mw3p{mitf94XJVmmRGsn zpB%NqJ%7DH#@6)p(St+k6g{Ys0}<*M`^vV4)2E#2qO(W3kHshodMk047Jt5T)Nche z3 zu0kbrIFbh57v_6p(B%#~A*KZW)CZ=cV&@of2MfjTnZ^-rKVYOK2&*j2d(Xp$DJWyS z85Aikw1T*V_LmE_bXRz$m^Cq~f4Znjx4OZKv-j5}ireErH`Rs~@VZ4eO(Ve9+;ub<*Sw zFsqk;=m%Ix|3+Dm0^%8P0yfv%Uk8Sl#f@8dapr=+c-)C{h)$y6J!J|ilxVNJ29^O) z;7)61+A6)!sljv&RIG;=e(5eM(#}yu++{92n=n%0#6rac0~=I1psHxVXt!O=;NpZ) zT}vKV+u%N9r=`s|u^(~Zo)$0c=#ZO&*^;xVVZ;a-w6Lr2McvJ-7_7lw+G;Ez4?6P0 zipMm)U)6Vyq5NvO?&Bw6bm*m30Y8us=RU>N&$t2k()D$6 z5`i+tamrh-Tzn(@5s@B>@d3wEv=%Jv&~R94i{a<^W2VO{UZUeqH&xKi-nazoxdY$b3&Nn^)b%|mxfaWr$4)DUm;15bL z*M=gbBv%Ts4NA>>&&M>5;i<@rwu1)U?NT;3rxR-Xn1iTcKFV2>tF$8xDHc~-j26U5 z`9DNtrRG)u!mY}5VL0vg0O)o2z(6wpo{eK z+QsMhtR5jJab)=#PxC+Vk!z{SdGVxd8EGK6sXws{Cnh;WHxo()k)eGk7VRcUkDbom zDiu0E8bisOZWC2~TW>#shodlGet9+)V^aoiQ4|!Vs?mo76*dwI{j+C3wgfDAg=!xi z_;*~;sK1>c|3Xg|PjhIX(0h4nv!IP@y;8>l&a(=;G;%y8C6zmndg;p4O^WXGBy-tN zc-@ojDz0Bn8s=6i%%77YEj81mzD)aTo%)-dQpksIUsiP}?)11LY$g&xokOE7r)RT< zjFrP{490|s;%y!)HCjy??e_(TljxXijCbt&H+u)hwBKS}3I3)BLvnDh%Hwqeuz3+c zy^=}2_ThUolam4h;aQ(iF+*b_aX)bWPnO2+hsq54pAz;LjcF|QZ|Hhvmc6d#SMK@V zX&Pbr`s5EQ%P6f4CU$e&m zd3~G=Vt?+zim&4L4GLH$_=*^?fC+nSY1=IfV&(!WOlxlqj#sVeG~A;{AH1NNofvD@ zo8|J@sl8kAer4;GVZ}wqhk!y0D%HZHnL+W!{LnSpM>A|t0q%?lW3ae+Vbf~R{jg}j z)|BVKD~p6J3YnMeDUY1{C7L`AWhMfcvP~T=-DO0aEGa3}SSo(p({f23~FT9O~252%=J@ z?(+hwB=vl-K=6<~_fj^}sFLS9QYu9xyFf`81x-q0?%UpDc-qT*A3fm_^tm$yaCw>d zEod$tcpN>hcIdSs=zm(w1|o^OHS@@mNrQz|nD;WExbjPEZhblNAlH=@{O~0foK(42 zfRrN{`^E~V%Q)=>Wy;j{nf~~8T_w+jxJeOlC9AF#FbGrd?Y)tiwezEF`7m{qm@WDC zbrLQPOStc_YYORorMhfXX9~-QH-$ZuCbIsqrzyDClG?AKI76MTB-#%0mgT1;K&uw=Xb7Z6w#!Ca7nElSEt0y9X8$pGVl=AeTEEr zM>@}wo^I^+*RZUE5dU1?A&(j7w>1OvD{Pl@X;~Z_QPeH$;B0SoXoa8LkT}?I-8vvQ zi?hh>+Mw;MTuQY(ZN096Uc08|*n|ete01R24n=bIDd#wJ2_i09BtBxay};hkZ72K0 z?%qS&{AFF7@=M<$B?62WY-+F%AQxOin6XVqp6RR~f zsUrCM1ec<6L*%m&+$aPwm2H>7^PsthoRrY9?Z)(6{Z#&!`B)=9{u}+-&$YWsnu;ZN HEFb?5&3gQK literal 0 HcmV?d00001 diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@3x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6342f9c17e7df1d1d3caef1b66c636281f5101b5 GIT binary patch literal 7247 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 Date: Wed, 22 Dec 2021 12:30:17 -0600 Subject: [PATCH 0356/1722] Moves styling and size out of BasemapGallery and into client. Fixes popover display on iPhone. --- Examples/Examples/BasemapGalleryExampleView.swift | 1 + .../Components/BasemapGallery/BasemapGallery.swift | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index a796b1dc6..156e17bdc 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -53,6 +53,7 @@ struct BasemapGalleryExampleView: View { } .popover(isPresented: $showBasemapGallery) { BasemapGallery(viewModel: viewModel) + .padding() } } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 5055f445d..a7cbc5923 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -43,7 +43,6 @@ public struct BasemapGallery: View { public var body: some View { makeGalleryView() - .frame(width: 300) .alert( alertItem?.title ?? "", isPresented: $showErrorAlert, @@ -59,7 +58,13 @@ private extension BasemapGallery { /// - Returns: A view representing the basemap gallery with the specified columns. func makeGalleryView() -> some View { ScrollView { - let columns = Array(repeating: GridItem(.flexible(), alignment: .top), count: 3) + let columns = Array( + repeating: GridItem( + .flexible(minimum: 75, maximum: 100), + alignment: .top + ), + count: 3 + ) LazyVGrid(columns: columns) { ForEach(viewModel.items) { item in BasemapGalleryCell( @@ -76,7 +81,6 @@ private extension BasemapGallery { } } } - .esriBorder() } } From 4b602dd3cddd348c6c3b2717aead3c12be68cffe Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 23 Dec 2021 15:37:37 -0600 Subject: [PATCH 0357/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- Examples/Examples/BasemapGalleryExampleView.swift | 11 ++++++----- .../Assets.xcassets/basemap.imageset/Contents.json | 4 +--- .../BasemapGallery/BasemapGalleryCell.swift | 3 ++- .../defaultthumbnail.imageset/Contents.json | 3 --- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 156e17bdc..e2786e590 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -60,13 +60,14 @@ struct BasemapGalleryExampleView: View { } static private func initialBasemaps() -> [BasemapGalleryItem] { - let itemURLs: [URL] = [ - URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")!, - URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! + let identifiers = [ + "46a87c20f09e4fc48fa3c38081e0cae6", + "f33a34de3a294590ab48f246e99958c9" ] - return itemURLs.map { - BasemapGalleryItem(basemap: Basemap(item: PortalItem(url: $0)!)) + return identifiers.map { identifier in + let url = URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=\(identifier)")! + return BasemapGalleryItem(basemap: Basemap(item: PortalItem(url: url)!)) } } } diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json index 800e56381..2f50ec9d4 100644 --- a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json +++ b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json @@ -1,8 +1,5 @@ { "images" : [ - { - "idiom" : "universal" - }, { "filename" : "basemap@1x.png", "idiom" : "universal", @@ -24,6 +21,7 @@ "version" : 1 }, "properties" : { + "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index 8e71285c5..0e9ec31c5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -38,7 +38,8 @@ struct BasemapGalleryCell: View { .aspectRatio(contentMode: .fit) .border( isSelected ? Color.accentColor: Color.clear, - width: 3.0) + width: 3 + ) } // Display an image representing either a load basemap error diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json index 24873226e..a00b75d2f 100644 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/Contents.json @@ -1,8 +1,5 @@ { "images" : [ - { - "idiom" : "universal" - }, { "filename" : "basemap@1x.png", "idiom" : "universal", From 9b5490fdfee047b78c507b6746195396e09e105e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 23 Dec 2021 15:49:05 -0600 Subject: [PATCH 0358/1722] PR review changes. --- .../Examples/BasemapGalleryExampleView.swift | 17 +++- .../BasemapGallery/BasemapGalleryCell.swift | 80 +++++++++---------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index e2786e590..d654af352 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -52,8 +52,21 @@ struct BasemapGalleryExampleView: View { Image("basemap") } .popover(isPresented: $showBasemapGallery) { - BasemapGallery(viewModel: viewModel) - .padding() + if UIDevice.current.userInterfaceIdiom == .phone { + NavigationView { + BasemapGallery(viewModel: viewModel) + .navigationTitle("Basemaps") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { showBasemapGallery = false } + } + } + } + } else { + BasemapGallery(viewModel: viewModel) + .padding() + } } } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index 0e9ec31c5..10ea35da6 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -17,55 +17,53 @@ import SwiftUI struct BasemapGalleryCell: View { /// The displayed item. @ObservedObject var item: BasemapGalleryItem - + /// A Boolean specifying if the item should be displayed ais selected. let isSelected: Bool /// The action executed when the item is selected. let onSelection: () -> Void - + var body: some View { - VStack { - Button(action: { - onSelection() - }, label: { - VStack { - ZStack(alignment: .center) { - // Display the thumbnail, if available. - if let thumbnailImage = item.thumbnail { - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .border( - isSelected ? Color.accentColor: Color.clear, - width: 3 - ) - } - - // Display an image representing either a load basemap error - // or a spatial reference mismatch error. - if item.loadBasemapsError != nil { - Image(systemName: "minus.circle.fill") - .font(.title) - .foregroundColor(.red) - } - - // Display a progress view if the item is loading. - if item.isBasemapLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .esriBorder() - } + Button(action: { + onSelection() + }, label: { + VStack { + ZStack(alignment: .center) { + // Display the thumbnail, if available. + if let thumbnailImage = item.thumbnail { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .border( + isSelected ? Color.accentColor: Color.clear, + width: 3 + ) } - // Display the name of the item. - Text(item.name ?? "") - .font(.footnote) - .multilineTextAlignment(.center) - .foregroundColor(.primary) + // Display an image representing either a load basemap error + // or a spatial reference mismatch error. + if item.loadBasemapsError != nil { + Image(systemName: "minus.circle.fill") + .font(.title) + .foregroundColor(.red) + } + + // Display a progress view if the item is loading. + if item.isBasemapLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .esriBorder() + } } - }) - .disabled(item.isBasemapLoading) - } + + // Display the name of the item. + Text(item.name ?? "") + .font(.footnote) + .multilineTextAlignment(.center) + .foregroundColor(.primary) + } + }) + .disabled(item.isBasemapLoading) } } From 0d6edd258e9ed6cf43a430978a06b2e09346a032 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 28 Dec 2021 09:16:48 -0600 Subject: [PATCH 0359/1722] Update Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json Co-authored-by: Philip Ridgeway --- .../ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json index 2f50ec9d4..df4b99862 100644 --- a/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json +++ b/Examples/ExamplesApp/Assets.xcassets/basemap.imageset/Contents.json @@ -21,7 +21,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } From b0b118035c7e79104d19b9a312f29c9e9bc51213 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 28 Dec 2021 14:19:12 -0600 Subject: [PATCH 0360/1722] Perfect(?) drag resizing. --- .../FloatingPanel/FloatingPanel.swift | 62 +++++-------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index bce95dbef..a93a6b4c1 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -12,15 +12,12 @@ // limitations under the License. import SwiftUI -//import ArcGIS public struct FloatingPanel : View where Content : View { /// The content that is to be housed in the floating panel. let content: Content - public init( - content: Content - ) { + public init(content: Content) { self.content = content } @@ -29,58 +26,33 @@ public struct FloatingPanel : View where Content : View { var drag: some Gesture { DragGesture() - // .onChanged { self.heightOffset = $0.translation.height} - .onChanged { + .onChanged { value in self.handleColor = .red - self.heightOffset = $0.translation.height - lastHeight = originalHeight + heightOffset + height = max(minHeight, (height ?? 0) + value.translation.height) } .onEnded { _ in self.handleColor = .secondary - self.originalHeight = lastHeight } } - - @State - private var heightOffset: CGFloat = 0 - - @State - private var originalHeight: CGFloat = 0 - @State - private var lastHeight: CGFloat = 0 - // private var currentHeight: CGFloat = 0 + private let minHeight: CGFloat = 44 + + @State var height: CGFloat? = nil public var body: some View { - VStack(alignment: .center) { - GeometryReader { geometry in + VStack { + VStack { content - .frame(height: originalHeight + heightOffset) - .onAppear { - if originalHeight == 0 { - originalHeight = geometry.size.height - } - } + .frame(minHeight: minHeight, maxHeight: height) + Rectangle() + .foregroundColor(handleColor) + .frame(width: 100, height: 8.0) + .cornerRadius(4.0) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + .gesture(drag) } - // Rectangle() - // .foregroundColor(.secondary) - // .frame(height: 1.0) - Rectangle() - .foregroundColor(handleColor) - .frame(width: 100, height: 8.0) - .cornerRadius(4.0) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) - .gesture(drag) - Text("\(self.heightOffset)") + .esriBorder(padding: EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + Spacer() } - .frame(width: 300) - .esriBorder(padding: EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - Spacer() - } -} - -struct FloatingPanel_Previews: PreviewProvider { - static var previews: some View { - FloatingPanel(content: Rectangle().foregroundColor(.blue)) } } From 8c52eccc15b3894e42dd91024f55bc9bf8990f62 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 28 Dec 2021 15:15:07 -0600 Subject: [PATCH 0361/1722] Update FloatingPanel example with Search view. --- .../Examples/FloatingPanelExampleView.swift | 71 ++++++++++++++++++- .../FloatingPanel/FloatingPanel.swift | 27 +++---- .../Components/Search/SearchView.swift | 24 +++++-- 3 files changed, 102 insertions(+), 20 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 9aef892da..6ce430be3 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -10,15 +10,80 @@ import ArcGISToolkit import ArcGIS struct FloatingPanelExampleView: View { + /// The `SearchViewModel` used to define behavior of the `SearchView`. + @ObservedObject + var searchViewModel = SearchViewModel( + sources: [SmartLocatorSearchSource( + name: "My locator", + maximumResults: 16, + maximumSuggestions: 16 + )] + ) + let map = Map(basemapStyle: .arcGISImagery) + + /// The map viewpoint used by the `SearchView` to pan/zoom the map + /// to the extent of the search results. + @State + private var searchResultViewpoint: Viewpoint? = Viewpoint( + center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), + scale: 1000000 + ) + + /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. + let searchResultsOverlay = GraphicsOverlay() var body: some View { - FloatingPanel(content: MapView(map: map)) -// FloatingPanel(content: Rectangle().foregroundColor(.blue)) - .padding() + MapView( + map: map, + viewpoint: searchResultViewpoint, + graphicsOverlays: [searchResultsOverlay] + ) + .onNavigatingChanged { searchViewModel.isGeoViewNavigating = $0 } + .onViewpointChanged(kind: .centerAndScale) { + searchViewModel.queryCenter = $0.targetGeometry as? Point + + // Reset `searchResultViewpoint` here when the user pans/zooms + // the map, so if the user commits the same search with the + // same result, the Map will pan/zoom to the result. Otherwise, + // `searchResultViewpoint` doesn't change which doesn't + // redraw the map with the new viewpoint. + searchResultViewpoint = nil + } + .onVisibleAreaChanged { newValue in + // Setting `searchViewModel.queryArea` will limit the + // results to `queryArea`. + //searchViewModel.queryArea = newValue + + // For "Repeat Search Here" behavior, set the + // `searchViewModel.geoViewExtent` property when navigating. + searchViewModel.geoViewExtent = newValue.extent + } + .overlay(alignment: .topTrailing) { + FloatingPanel( + content: + SearchView(searchViewModel: searchViewModel) + .padding() + ) + .frame(width: 360) + } + .onAppear { + searchViewModel.viewpoint = $searchResultViewpoint + searchViewModel.resultsOverlay = searchResultsOverlay + } } } +//struct FloatingPanelExampleView: View { +// let map = Map(basemapStyle: .arcGISImagery) +// +// var body: some View { +// FloatingPanel(content: MapView(map: map)) +//// FloatingPanel(content: Rectangle().foregroundColor(.blue)) +// .padding() +// } +//} + struct FloatingPanelExampleView_Previews: PreviewProvider { static var previews: some View { FloatingPanelExampleView() diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index a93a6b4c1..625ddee42 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -23,21 +23,11 @@ public struct FloatingPanel : View where Content : View { @State var handleColor: Color = .secondary - - var drag: some Gesture { - DragGesture() - .onChanged { value in - self.handleColor = .red - height = max(minHeight, (height ?? 0) + value.translation.height) - } - .onEnded { _ in - self.handleColor = .secondary - } - } - private let minHeight: CGFloat = 44 + @State + var height: CGFloat? = nil - @State var height: CGFloat? = nil + private let minHeight: CGFloat = 66 public var body: some View { VStack { @@ -55,4 +45,15 @@ public struct FloatingPanel : View where Content : View { Spacer() } } + + var drag: some Gesture { + DragGesture() + .onChanged { value in + self.handleColor = .red + height = max(minHeight, (height ?? 0) + value.translation.height) + } + .onEnded { _ in + self.handleColor = .secondary + } + } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 196d7e785..90e425722 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -56,10 +56,12 @@ public struct SearchView: View { /// The width of the search bar, taking into account the horizontal and vertical size classes /// of the device. This will cause the search field to display full-width on an iPhone in portrait /// orientation (and certain iPad multitasking configurations) and limit the width to `360` in other cases. - private var searchBarWidth: CGFloat? { - horizontalSizeClass == .compact && verticalSizeClass == .regular ? nil : 360 + private var internalSearchBarWidth: CGFloat? { + horizontalSizeClass == .compact && verticalSizeClass == .regular ? nil : searchBarWidth } - + + private var searchBarWidth: CGFloat? + /// If `true`, will draw the results list view at half height, exposing a portion of the /// underlying map below the list on an iPhone in portrait orientation (and certain iPad multitasking /// configurations). If `false`, will draw the results list view full size. @@ -109,7 +111,8 @@ public struct SearchView: View { } } } - .esriBorder(padding: EdgeInsets()) +// .padding() +// .esriBorder(padding: EdgeInsets()) } } .frame(width: searchBarWidth) @@ -328,3 +331,16 @@ extension View { modifier(SelectedModifier(isSelected: isSelected)) } } + +extension SearchView { + /// 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`. + /// - Parameter scaleFactor: The new scale factor. + /// - Returns: The `OverviewMap`. + public func searchBarWidth(_ newWidth: CGFloat?) -> SearchView { + var copy = self + copy.searchBarWidth = newWidth + return copy + } +} From 023d84c0c398357594a67d49f445858608318827 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 29 Dec 2021 15:52:00 -0600 Subject: [PATCH 0362/1722] Update arcgis package location. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 69157f633..f35cc529e 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( ), ], dependencies: [ - .package(name: "ArcGIS", path: "../swift") + .package(name: "ArcGIS", path: "../swift/ArcGIS") ], targets: [ .target( From d05a0273774d7e97644231446db6310b158103c9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 29 Dec 2021 17:23:04 -0600 Subject: [PATCH 0363/1722] Apply suggestions from code review Co-authored-by: Nimesh Jarecha --- .../Examples/BasemapGalleryExampleView.swift | 2 +- .../BasemapGallery/BasemapGallery.swift | 6 ++--- .../BasemapGallery/BasemapGalleryCell.swift | 7 +++--- .../BasemapGalleryViewModel.swift | 23 ++++++++----------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index d654af352..db610740a 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -23,7 +23,7 @@ struct BasemapGalleryExampleView: View { @ObservedObject var viewModel = BasemapGalleryViewModel() - /// `true` if the basemap gallery should be displayed; `false` otherwise. + /// A Boolean value indicating whether to show the basemap gallery. @State private var showBasemapGallery: Bool = false diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index a7cbc5923..203b870b5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -17,8 +17,8 @@ import ArcGIS /// The `BasemapGallery` tool displays a collection of basemaps from either /// ArcGIS Online, a user-defined portal, or an array of `BasemapGalleryItem`s. /// When a new basemap is selected from the `BasemapGallery` and the optional -/// `BasemapGallery.geoModel` property is set, then the basemap of the geoModel is replaced -/// with the basemap in the gallery. +/// `BasemapGalleryViewModel.geoModel` property is set, then the basemap of the +/// `geoModel` is replaced with the basemap in the gallery. public struct BasemapGallery: View { /// Creates a `BasemapGallery`. /// - Parameter viewModel: The view model used by the `BasemapGallery`. @@ -33,7 +33,7 @@ public struct BasemapGallery: View { @ObservedObject public var viewModel: BasemapGalleryViewModel - /// A Boolean value indicating whether the error alert should be shown. + /// A Boolean value indicating whether to show an error alert. @State private var showErrorAlert = false diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index 10ea35da6..ab8a85f28 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -18,7 +18,7 @@ struct BasemapGalleryCell: View { /// The displayed item. @ObservedObject var item: BasemapGalleryItem - /// A Boolean specifying if the item should be displayed ais selected. + /// A Boolean value indicating whether the item should be displayed is selected. let isSelected: Bool /// The action executed when the item is selected. @@ -36,7 +36,7 @@ struct BasemapGalleryCell: View { .resizable() .aspectRatio(contentMode: .fit) .border( - isSelected ? Color.accentColor: Color.clear, + isSelected ? Color.accentColor : Color.clear, width: 3 ) } @@ -63,7 +63,6 @@ struct BasemapGalleryCell: View { .multilineTextAlignment(.center) .foregroundColor(.primary) } - }) - .disabled(item.isBasemapLoading) + }).disabled(item.isBasemapLoading) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 876553f14..30a3a9eb8 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -18,7 +18,7 @@ import ArcGIS @MainActor public class BasemapGalleryViewModel: ObservableObject { /// Creates a `BasemapGalleryViewModel`. Uses the given array of basemap gallery items. - /// - Remark: If `items` is empty, ArcGISOnline's developer basemaps will + /// - Remark: If `items` is empty, ArcGIS Online's developer basemaps will /// be loaded and added to `items`. /// - Parameters: /// - geoModel: The `GeoModel`. @@ -47,7 +47,7 @@ public class BasemapGalleryViewModel: ObservableObject { @Published public var items: [BasemapGalleryItem] - /// The `BasemapGalleryItem` representing the `GeoModel`'s current base map. This may be a + /// The `BasemapGalleryItem` representing the `GeoModel`'s current basemap. This may be a /// basemap which does not exist in the gallery. @Published public var currentItem: BasemapGalleryItem? = nil { @@ -58,7 +58,7 @@ public class BasemapGalleryViewModel: ObservableObject { } /// Handles changes to the `geoModel` property. - /// - Parameter previousGeoModel: The previously set `geoModel`. + /// - Parameter previousGeoModel: The previously set `GeoModel`. func geoModelDidChange(_ previousGeoModel: GeoModel?) { guard let geoModel = geoModel else { return } if geoModel.loadStatus != .loaded { @@ -71,15 +71,12 @@ private extension BasemapGalleryViewModel { /// Loads the given `GeoModel` then sets `currentItem` to an item /// created with the geoModel's basemap. /// - Parameter geoModel: The `GeoModel` to load. - func load(geoModel: GeoModel?) async { - guard let geoModel = geoModel else { return } - do { - try await geoModel.load() - if let basemap = geoModel.basemap { - currentItem = BasemapGalleryItem(basemap: basemap) - } else { - currentItem = nil - } - } catch {} + func load(geoModel: GeoModel) async { + try? await geoModel.load() + if let basemap = geoModel.basemap { + currentItem = BasemapGalleryItem(basemap: basemap) + } else { + currentItem = nil + } } } From 0b58cca0f4567d21e3269c89147e5c020ba20b2a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 29 Dec 2021 17:26:35 -0600 Subject: [PATCH 0364/1722] Don't create BasemapGalleryViewModel twice. --- 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 db610740a..a0634c270 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -21,7 +21,7 @@ struct BasemapGalleryExampleView: View { /// The view model for the basemap gallery. @ObservedObject - var viewModel = BasemapGalleryViewModel() + var viewModel: BasemapGalleryViewModel /// A Boolean value indicating whether to show the basemap gallery. @State From d1fa94cad8b7c90289f25d3bdb66b83ba194de0f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 30 Dec 2021 10:32:25 -0600 Subject: [PATCH 0365/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift Co-authored-by: Nimesh Jarecha --- .../Components/BasemapGallery/BasemapGalleryItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index fcacd7be7..3177203d9 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -20,7 +20,7 @@ public class BasemapGalleryItem: ObservableObject { /// Creates a `BasemapGalleryItem`. /// - Parameters: /// - basemap: The `Basemap` represented by the item. - /// - name: The item name. If `nil`, `Basemap.name` is used, if available.. + /// - name: The item name. If `nil`, `Basemap.name` is used, if available. /// - description: The item description. If `nil`, `Basemap.Item.description` /// is used, if available. /// - thumbnail: The thumbnail used to represent the item. If `nil`, From efdddf3aec51a9663084e6fd2eff8508aca3de3f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 30 Dec 2021 10:51:15 -0600 Subject: [PATCH 0366/1722] `loadBasemapsError` -> `loadBasemapError` --- .../Components/BasemapGallery/BasemapGallery.swift | 2 +- .../Components/BasemapGallery/BasemapGalleryCell.swift | 2 +- .../Components/BasemapGallery/BasemapGalleryItem.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 203b870b5..bb235d2f7 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -71,7 +71,7 @@ private extension BasemapGallery { item: item, isSelected: item == viewModel.currentItem ) { - if let loadError = item.loadBasemapsError { + if let loadError = item.loadBasemapError { alertItem = AlertItem(loadBasemapError: loadError) showErrorAlert = true } else { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index ab8a85f28..711db3ca4 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -43,7 +43,7 @@ struct BasemapGalleryCell: View { // Display an image representing either a load basemap error // or a spatial reference mismatch error. - if item.loadBasemapsError != nil { + if item.loadBasemapError != nil { Image(systemName: "minus.circle.fill") .font(.title) .foregroundColor(.red) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 3177203d9..3ce1a7d75 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -47,7 +47,7 @@ public class BasemapGalleryItem: ObservableObject { /// The error generated loading the basemap, if any. @Published - private(set) var loadBasemapsError: Error? = nil + private(set) var loadBasemapError: Error? = nil /// The basemap represented by `BasemapGalleryItem`. public let basemap: Basemap @@ -98,7 +98,7 @@ private extension BasemapGalleryItem { thumbnail = basemap.item?.thumbnail?.image ?? .defaultThumbnail() } - loadBasemapsError = error + loadBasemapError = error isBasemapLoading = false } } From 407dfc5106c4146044e3aaad51b8399123ff4c37 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 Dec 2021 14:58:18 -0600 Subject: [PATCH 0367/1722] Add Portal basemaps --- .../Examples/BasemapGalleryExampleView.swift | 5 +- .../BasemapGalleryViewModel.swift | 93 ++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index a0634c270..0a546415a 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -36,8 +36,9 @@ struct BasemapGalleryExampleView: View { init() { self.map = Map(basemapStyle: .arcGISImagery) self.viewModel = BasemapGalleryViewModel( - geoModel: self.map, - items: Self.initialBasemaps() + geoModel: self.map + // You can add your own basemaps by passing them in here: + //items: Self.initialBasemaps() ) } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 30a3a9eb8..1227477c3 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -30,8 +30,45 @@ public class BasemapGalleryViewModel: ObservableObject { self.items = items self.geoModel = geoModel geoModelDidChange(nil) + + if items.isEmpty { + // We have no basemap items, so fetch the + //developer basemaps from AGOL. + fetchBasemapsTask = Task { + await fetchBasemaps( + from: Portal.arcGISOnline(isLoginRequired: false), + useDeveloperBasemaps: true + ) + } + } + } + + /// Creates a `BasemapGalleryViewModel`. Uses the given `Portal` to retrieve basemaps. + /// - Parameters: + /// - geoModel: The `GeoModel`. + /// - portal: The `Portal` used to load basemaps. + public init( + _ geoModel: GeoModel? = nil, + portal: Portal + ) { + items = [] + self.geoModel = geoModel + geoModelDidChange(nil) + + self.portal = portal + portalDidChange(portal) + } + + private var fetchBasemapsTask: Task? { + willSet { + fetchBasemapsTask?.cancel() + } } + /// The error generated by fetching the `Basemaps` from the `Portal`. + @Published + public var fetchBasemapsError: Error? = 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, @@ -42,8 +79,17 @@ public class BasemapGalleryViewModel: ObservableObject { } } + /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps + /// and replace the`basemapGalleryItems` array items with the fetched basemaps. + public var portal: Portal? { + didSet { + portalDidChange(oldValue) + } + } + /// The list of basemaps currently visible in the gallery. It is comprised of items passed into - /// the `BasemapGalleryItem` constructor property. + /// the `BasemapGalleryItem` constructor property and items loaded either from `portal` or + /// from ArcGISOnline if `portal` is `nil`. @Published public var items: [BasemapGalleryItem] @@ -65,9 +111,54 @@ public class BasemapGalleryViewModel: ObservableObject { Task { await load(geoModel: geoModel) } } } + + /// Handles changes to the `portal` property. + /// - Parameter previousPortal: The previously set `Portal`. + func portalDidChange(_ previousPortal: Portal?) { + // Remove all items from `basemapGalleryItems`. + items.removeAll() + + guard let portal = portal else { return } + fetchBasemapsTask = Task { await fetchBasemaps(from: portal) } + } } private extension BasemapGalleryViewModel { + /// Fetches the basemaps from the given portal and appends `basemapGalleryItems` with + /// items created from the fetched basemaps. + /// - Parameters: + /// - portal: Portal to fetch basemaps from + /// - useDeveloperBasemaps: If `true`, will always use the portal's developer basemaps. If + /// `false`, it will use either the portal's basemaps or vector basemaps, depending on the value of + /// `portal.portalInfo.useVectorBasemaps`. + func fetchBasemaps( + from portal: Portal?, + useDeveloperBasemaps: Bool = false + ) async { + guard let portal = portal else { return } + + do { + try await portal.load() + + if useDeveloperBasemaps { + items += try await portal.developerBasemaps.map { + BasemapGalleryItem(basemap: $0) + } + } else if let portalInfo = portal.info, + portalInfo.useVectorBasemaps { + items += try await portal.vectorBasemaps.map { + BasemapGalleryItem(basemap: $0) + } + } else { + items += try await portal.basemaps.map { + BasemapGalleryItem(basemap: $0) + } + } + } catch { + fetchBasemapsError = error + } + } + /// Loads the given `GeoModel` then sets `currentItem` to an item /// created with the geoModel's basemap. /// - Parameter geoModel: The `GeoModel` to load. From a45f518feae5155eee0150d0b0068bf3c10dd2ad Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Tue, 4 Jan 2022 12:50:42 -0800 Subject: [PATCH 0368/1722] Address unsatisfiable constraints warnings --- Examples/Examples/BasemapGalleryExampleView.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index a0634c270..5834e9e5e 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -59,10 +59,16 @@ struct BasemapGalleryExampleView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { showBasemapGallery = false } + Button { + showBasemapGallery = false + } label: { + Text("Done") + .bold() + } } } } +// .navigationViewStyle(.stack) } else { BasemapGallery(viewModel: viewModel) .padding() From 66b8f50722a309a66a2e8283e361ea48caf7421c Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Tue, 4 Jan 2022 12:51:31 -0800 Subject: [PATCH 0369/1722] 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 5834e9e5e..dbba770ba 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -68,7 +68,7 @@ struct BasemapGalleryExampleView: View { } } } -// .navigationViewStyle(.stack) + .navigationViewStyle(.stack) } else { BasemapGallery(viewModel: viewModel) .padding() From 868946aff0ce168b5ece7bdbc9b06ca6b8b49e18 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 5 Jan 2022 10:09:03 -0600 Subject: [PATCH 0370/1722] Cleanup. --- .../FloatingPanel/FloatingPanel.swift | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 625ddee42..500139207 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -13,19 +13,31 @@ import SwiftUI +/// A floating panel is a view that overlays a view and supplies view-related +/// content. For a map view, for instance, it could display a legend, bookmarks, search results, etc.. +/// Apple Maps, Google Maps, Windows 10, and Collector have floating panel +/// implementations, sometimes referred to as a "bottom sheet". +/// +/// Floating Panels are non-modal and can be transient, only displaying +/// information for a short period of time like identify results, +/// or persistent, where the information is always displayed, for example a +/// dedicated search panel. They will also be primarily simple containers +/// that clients will fill with their own content. public struct FloatingPanel : View where Content : View { /// The content that is to be housed in the floating panel. let content: Content + /// Creates a `FloatingPanel` + /// - Parameter content: The view shown in the floating panel. public init(content: Content) { self.content = content } @State - var handleColor: Color = .secondary + private var handleColor: Color = defaultHandleColor @State - var height: CGFloat? = nil + private var height: CGFloat? = nil private let minHeight: CGFloat = 66 @@ -34,14 +46,10 @@ public struct FloatingPanel : View where Content : View { VStack { content .frame(minHeight: minHeight, maxHeight: height) - Rectangle() - .foregroundColor(handleColor) - .frame(width: 100, height: 8.0) - .cornerRadius(4.0) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + Handle(color: handleColor) .gesture(drag) } - .esriBorder(padding: EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .esriBorder() Spacer() } } @@ -49,11 +57,27 @@ public struct FloatingPanel : View where Content : View { var drag: some Gesture { DragGesture() .onChanged { value in - self.handleColor = .red + self.handleColor = Self.activeHandleColor height = max(minHeight, (height ?? 0) + value.translation.height) } .onEnded { _ in - self.handleColor = .secondary + self.handleColor = Self.defaultHandleColor } } } + +private extension FloatingPanel { + static var defaultHandleColor: Color { .secondary } + static var activeHandleColor: Color { .primary } +} + +private struct Handle: View { + var color: Color + var body: some View { + Rectangle() + .foregroundColor(color) + .frame(width: 100, height: 8.0) + .cornerRadius(4.0) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + } +} From 1106af60398f34f1b323cd83648452da06aee2bf Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 5 Jan 2022 13:23:36 -0600 Subject: [PATCH 0371/1722] Final cleanup --- .../Examples/FloatingPanelExampleView.swift | 87 +++++-------------- .../FloatingPanel/FloatingPanel.swift | 25 +++--- 2 files changed, 32 insertions(+), 80 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 6ce430be3..28e6597bb 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -1,91 +1,44 @@ -// -// FloatingPanelExampleView.swift -// Examples -// -// Created by Mark Dostal on 12/16/21. -// +// 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 ArcGISToolkit import ArcGIS struct FloatingPanelExampleView: View { - /// The `SearchViewModel` used to define behavior of the `SearchView`. - @ObservedObject - var searchViewModel = SearchViewModel( - sources: [SmartLocatorSearchSource( - name: "My locator", - maximumResults: 16, - maximumSuggestions: 16 - )] - ) - let map = Map(basemapStyle: .arcGISImagery) - /// The map viewpoint used by the `SearchView` to pan/zoom the map - /// to the extent of the search results. - @State - private var searchResultViewpoint: Viewpoint? = Viewpoint( + private var initialViewpoint: Viewpoint? = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), scale: 1000000 ) - /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. - let searchResultsOverlay = GraphicsOverlay() - var body: some View { MapView( map: map, - viewpoint: searchResultViewpoint, - graphicsOverlays: [searchResultsOverlay] + viewpoint: initialViewpoint ) - .onNavigatingChanged { searchViewModel.isGeoViewNavigating = $0 } - .onViewpointChanged(kind: .centerAndScale) { - searchViewModel.queryCenter = $0.targetGeometry as? Point - - // Reset `searchResultViewpoint` here when the user pans/zooms - // the map, so if the user commits the same search with the - // same result, the Map will pan/zoom to the result. Otherwise, - // `searchResultViewpoint` doesn't change which doesn't - // redraw the map with the new viewpoint. - searchResultViewpoint = nil - } - .onVisibleAreaChanged { newValue in - // Setting `searchViewModel.queryArea` will limit the - // results to `queryArea`. - //searchViewModel.queryArea = newValue - - // For "Repeat Search Here" behavior, set the - // `searchViewModel.geoViewExtent` property when navigating. - searchViewModel.geoViewExtent = newValue.extent - } .overlay(alignment: .topTrailing) { - FloatingPanel( - content: - SearchView(searchViewModel: searchViewModel) - .padding() - ) + FloatingPanel(content: SampleContent()) + .padding() .frame(width: 360) } - .onAppear { - searchViewModel.viewpoint = $searchResultViewpoint - searchViewModel.resultsOverlay = searchResultsOverlay - } } } -//struct FloatingPanelExampleView: View { -// let map = Map(basemapStyle: .arcGISImagery) -// -// var body: some View { -// FloatingPanel(content: MapView(map: map)) -//// FloatingPanel(content: Rectangle().foregroundColor(.blue)) -// .padding() -// } -//} - -struct FloatingPanelExampleView_Previews: PreviewProvider { - static var previews: some View { - FloatingPanelExampleView() +struct SampleContent: View { + var body: some View { + List(1..<21) { Text("\($0)") } + .listStyle(.plain) } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 500139207..03ca49b55 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Esri. +// 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. @@ -18,13 +18,13 @@ import SwiftUI /// Apple Maps, Google Maps, Windows 10, and Collector have floating panel /// implementations, sometimes referred to as a "bottom sheet". /// -/// Floating Panels are non-modal and can be transient, only displaying +/// Floating panels are non-modal and can be transient, only displaying /// information for a short period of time like identify results, /// or persistent, where the information is always displayed, for example a /// dedicated search panel. They will also be primarily simple containers /// that clients will fill with their own content. public struct FloatingPanel : View where Content : View { - /// The content that is to be housed in the floating panel. + /// The content shown in the floating panel. let content: Content /// Creates a `FloatingPanel` @@ -35,7 +35,7 @@ public struct FloatingPanel : View where Content : View { @State private var handleColor: Color = defaultHandleColor - + @State private var height: CGFloat? = nil @@ -43,15 +43,13 @@ public struct FloatingPanel : View where Content : View { public var body: some View { VStack { - VStack { - content - .frame(minHeight: minHeight, maxHeight: height) - Handle(color: handleColor) - .gesture(drag) - } - .esriBorder() - Spacer() + content + .frame(minHeight: minHeight, maxHeight: height) + Divider() + Handle(color: handleColor) + .gesture(drag) } + .esriBorder() } var drag: some Gesture { @@ -71,13 +69,14 @@ private extension FloatingPanel { static var activeHandleColor: Color { .primary } } +/// The "Handle" view of the floating panel. private struct Handle: View { + /// The color of the handle. var color: Color var body: some View { Rectangle() .foregroundColor(color) .frame(width: 100, height: 8.0) .cornerRadius(4.0) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) } } From 120f729f64e99e6d71dc90ee1c09cc015aa3e906 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 5 Jan 2022 13:38:27 -0600 Subject: [PATCH 0372/1722] Revert temporary SearchView changes. --- .../Components/Search/SearchView.swift | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 90e425722..196d7e785 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -56,12 +56,10 @@ public struct SearchView: View { /// The width of the search bar, taking into account the horizontal and vertical size classes /// of the device. This will cause the search field to display full-width on an iPhone in portrait /// orientation (and certain iPad multitasking configurations) and limit the width to `360` in other cases. - private var internalSearchBarWidth: CGFloat? { - horizontalSizeClass == .compact && verticalSizeClass == .regular ? nil : searchBarWidth + private var searchBarWidth: CGFloat? { + horizontalSizeClass == .compact && verticalSizeClass == .regular ? nil : 360 } - - private var searchBarWidth: CGFloat? - + /// If `true`, will draw the results list view at half height, exposing a portion of the /// underlying map below the list on an iPhone in portrait orientation (and certain iPad multitasking /// configurations). If `false`, will draw the results list view full size. @@ -111,8 +109,7 @@ public struct SearchView: View { } } } -// .padding() -// .esriBorder(padding: EdgeInsets()) + .esriBorder(padding: EdgeInsets()) } } .frame(width: searchBarWidth) @@ -331,16 +328,3 @@ extension View { modifier(SelectedModifier(isSelected: isSelected)) } } - -extension SearchView { - /// 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`. - /// - Parameter scaleFactor: The new scale factor. - /// - Returns: The `OverviewMap`. - public func searchBarWidth(_ newWidth: CGFloat?) -> SearchView { - var copy = self - copy.searchBarWidth = newWidth - return copy - } -} From e1596a586a5a679ac85241b50f45e82f6042359d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 5 Jan 2022 14:31:25 -0600 Subject: [PATCH 0373/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- .../BasemapGalleryViewModel.swift | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 1227477c3..051a3cd0a 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -43,10 +43,10 @@ public class BasemapGalleryViewModel: ObservableObject { } } - /// Creates a `BasemapGalleryViewModel`. Uses the given `Portal` to retrieve basemaps. + /// Creates an instance using the given portal to retrieve basemaps. /// - Parameters: - /// - geoModel: The `GeoModel`. - /// - portal: The `Portal` used to load basemaps. + /// - geoModel: A geo model. + /// - portal: The portal to use to load basemaps. public init( _ geoModel: GeoModel? = nil, portal: Portal @@ -88,7 +88,7 @@ public class BasemapGalleryViewModel: ObservableObject { } /// The list of basemaps currently visible in the gallery. It is comprised of items passed into - /// the `BasemapGalleryItem` constructor property and items loaded either from `portal` or + /// the `BasemapGalleryItem` initializer and items loaded either from `portal` or /// from ArcGISOnline if `portal` is `nil`. @Published public var items: [BasemapGalleryItem] @@ -132,28 +132,22 @@ private extension BasemapGalleryViewModel { /// `false`, it will use either the portal's basemaps or vector basemaps, depending on the value of /// `portal.portalInfo.useVectorBasemaps`. func fetchBasemaps( - from portal: Portal?, + from portal: Portal, useDeveloperBasemaps: Bool = false ) async { - guard let portal = portal else { return } - do { try await portal.load() + let basemaps: [Basemap] if useDeveloperBasemaps { - items += try await portal.developerBasemaps.map { - BasemapGalleryItem(basemap: $0) - } + basemaps = try await portal.developerBasemaps } else if let portalInfo = portal.info, portalInfo.useVectorBasemaps { - items += try await portal.vectorBasemaps.map { - BasemapGalleryItem(basemap: $0) - } + basemaps = try await portal.vectorBasemaps } else { - items += try await portal.basemaps.map { - BasemapGalleryItem(basemap: $0) - } + basemaps = try await portal.basemaps } + items += basemaps.map { BasemapGalleryItem(basemap: $0) } } catch { fetchBasemapsError = error } From b97522f53d82414f971360a000d09cfe7b90a3d7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 5 Jan 2022 14:51:15 -0600 Subject: [PATCH 0374/1722] Task cleanup/PR review changes. --- .../BasemapGalleryViewModel.swift | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 051a3cd0a..3460626d2 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -21,7 +21,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// - Remark: If `items` is empty, ArcGIS Online's developer basemaps will /// be loaded and added to `items`. /// - Parameters: - /// - geoModel: The `GeoModel`. + /// - geoModel: A geo model. /// - items: A list of pre-defined base maps to display. public init( geoModel: GeoModel? = nil, @@ -34,12 +34,10 @@ public class BasemapGalleryViewModel: ObservableObject { if items.isEmpty { // We have no basemap items, so fetch the //developer basemaps from AGOL. - fetchBasemapsTask = Task { - await fetchBasemaps( - from: Portal.arcGISOnline(isLoginRequired: false), - useDeveloperBasemaps: true - ) - } + fetchBasemaps( + from: Portal.arcGISOnline(isLoginRequired: false), + useDeveloperBasemaps: true + ) } } @@ -59,11 +57,7 @@ public class BasemapGalleryViewModel: ObservableObject { portalDidChange(portal) } - private var fetchBasemapsTask: Task? { - willSet { - fetchBasemapsTask?.cancel() - } - } + private var fetchBasemapsTask: Task? /// The error generated by fetching the `Basemaps` from the `Portal`. @Published @@ -119,7 +113,7 @@ public class BasemapGalleryViewModel: ObservableObject { items.removeAll() guard let portal = portal else { return } - fetchBasemapsTask = Task { await fetchBasemaps(from: portal) } + fetchBasemaps(from: portal) } } @@ -134,22 +128,25 @@ private extension BasemapGalleryViewModel { func fetchBasemaps( from portal: Portal, useDeveloperBasemaps: Bool = false - ) async { - do { - try await portal.load() - - let basemaps: [Basemap] - if useDeveloperBasemaps { - basemaps = try await portal.developerBasemaps - } else if let portalInfo = portal.info, - portalInfo.useVectorBasemaps { - basemaps = try await portal.vectorBasemaps - } else { - basemaps = try await portal.basemaps + ) { + fetchBasemapsTask?.cancel() + fetchBasemapsTask = Task { + do { + try await portal.load() + + let basemaps: [Basemap] + if useDeveloperBasemaps { + basemaps = try await portal.developerBasemaps + } else if let portalInfo = portal.info, + portalInfo.useVectorBasemaps { + basemaps = try await portal.vectorBasemaps + } else { + basemaps = try await portal.basemaps + } + items += basemaps.map { BasemapGalleryItem(basemap: $0) } + } catch { + fetchBasemapsError = error } - items += basemaps.map { BasemapGalleryItem(basemap: $0) } - } catch { - fetchBasemapsError = error } } From 4647b155e1d2fbddd8916573917a1e5177d1b3d8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 6 Jan 2022 12:07:00 -0600 Subject: [PATCH 0375/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift Co-authored-by: Philip Ridgeway --- .../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 3460626d2..775874783 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -33,7 +33,7 @@ public class BasemapGalleryViewModel: ObservableObject { if items.isEmpty { // We have no basemap items, so fetch the - //developer basemaps from AGOL. + // developer basemaps from AGOL. fetchBasemaps( from: Portal.arcGISOnline(isLoginRequired: false), useDeveloperBasemaps: true From bdbefbff0a4ada102064980bfe4f9081bffb14a4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 7 Jan 2022 10:43:31 -0600 Subject: [PATCH 0376/1722] Apply suggestions from code review Co-authored-by: Nimesh Jarecha --- .../BasemapGallery/BasemapGalleryViewModel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 775874783..1d741789b 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -74,7 +74,7 @@ public class BasemapGalleryViewModel: ObservableObject { } /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps - /// and replace the`basemapGalleryItems` array items with the fetched basemaps. + /// and replace the ``items`` array with the fetched basemaps. public var portal: Portal? { didSet { portalDidChange(oldValue) @@ -82,8 +82,8 @@ public class BasemapGalleryViewModel: ObservableObject { } /// The list of basemaps currently visible in the gallery. It is comprised of items passed into - /// the `BasemapGalleryItem` initializer and items loaded either from `portal` or - /// from ArcGISOnline if `portal` is `nil`. + /// the ``init(geoModel:items:)`` and items loaded either from `portal` or + /// from ArcGIS Online if `portal` is `nil`. @Published public var items: [BasemapGalleryItem] @@ -118,7 +118,7 @@ public class BasemapGalleryViewModel: ObservableObject { } private extension BasemapGalleryViewModel { - /// Fetches the basemaps from the given portal and appends `basemapGalleryItems` with + /// Fetches the basemaps from the given portal and appends `items` with /// items created from the fetched basemaps. /// - Parameters: /// - portal: Portal to fetch basemaps from From 9d35a5a9635b5257e5e9fe4ea798d17004da3f52 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 7 Jan 2022 13:14:45 -0600 Subject: [PATCH 0377/1722] PR review comments. --- .../BasemapGallery/BasemapGalleryViewModel.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 1d741789b..bd8e76c58 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -56,8 +56,6 @@ public class BasemapGalleryViewModel: ObservableObject { self.portal = portal portalDidChange(portal) } - - private var fetchBasemapsTask: Task? /// The error generated by fetching the `Basemaps` from the `Portal`. @Published @@ -81,9 +79,7 @@ public class BasemapGalleryViewModel: ObservableObject { } } - /// The list of basemaps currently visible in the gallery. It is comprised of items passed into - /// the ``init(geoModel:items:)`` and items loaded either from `portal` or - /// from ArcGIS Online if `portal` is `nil`. + /// The list of basemaps shown in the gallery. @Published public var items: [BasemapGalleryItem] @@ -97,6 +93,8 @@ public class BasemapGalleryViewModel: ObservableObject { } } + private var fetchBasemapsTask: Task? + /// Handles changes to the `geoModel` property. /// - Parameter previousGeoModel: The previously set `GeoModel`. func geoModelDidChange(_ previousGeoModel: GeoModel?) { From 9144f1f27b6ee6f50c098cffd3757ac29efb8c89 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 7 Jan 2022 13:21:08 -0600 Subject: [PATCH 0378/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- Examples/Examples/FloatingPanelExampleView.swift | 4 ++-- Examples/ExamplesApp/Examples.swift | 2 +- .../Components/FloatingPanel/FloatingPanel.swift | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 28e6597bb..ac4d65af6 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -18,9 +18,9 @@ import ArcGIS struct FloatingPanelExampleView: View { let map = Map(basemapStyle: .arcGISImagery) - private var initialViewpoint: Viewpoint? = Viewpoint( + private let initialViewpoint = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), - scale: 1000000 + scale: 1_000_000 ) var body: some View { diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index ffc5b00ac..aacf22bc5 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -44,7 +44,7 @@ extension ExampleList { static let views = Self( name: "Views", examples: [ - AnyExample("FloatingPanel", content: FloatingPanelExampleView()) + AnyExample("Floating Panel", content: FloatingPanelExampleView()) ] ) } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 03ca49b55..fea83bea7 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -23,7 +23,7 @@ import SwiftUI /// or persistent, where the information is always displayed, for example a /// dedicated search panel. They will also be primarily simple containers /// that clients will fill with their own content. -public struct FloatingPanel : View where Content : View { +public struct FloatingPanel: View where Content: View { /// The content shown in the floating panel. let content: Content @@ -53,7 +53,7 @@ public struct FloatingPanel : View where Content : View { } var drag: some Gesture { - DragGesture() + DragGesture(minimumDistance: 0) .onChanged { value in self.handleColor = Self.activeHandleColor height = max(minHeight, (height ?? 0) + value.translation.height) @@ -73,6 +73,7 @@ private extension FloatingPanel { private struct Handle: View { /// The color of the handle. var color: Color + var body: some View { Rectangle() .foregroundColor(color) From f83199c75f7c7185208386a302fdb9a38afaa38c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 7 Jan 2022 14:35:31 -0600 Subject: [PATCH 0379/1722] PR review changes --- .../Examples/FloatingPanelExampleView.swift | 4 +- .../FloatingPanel/FloatingPanel.swift | 45 ++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index ac4d65af6..3de93e6f4 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -29,7 +29,9 @@ struct FloatingPanelExampleView: View { viewpoint: initialViewpoint ) .overlay(alignment: .topTrailing) { - FloatingPanel(content: SampleContent()) + FloatingPanel { + SampleContent() + } .padding() .frame(width: 360) } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index fea83bea7..71cc2636c 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -24,27 +24,29 @@ import SwiftUI /// dedicated search panel. They will also be primarily simple containers /// that clients will fill with their own content. public struct FloatingPanel: View where Content: View { + // Note: instead of the FloatingPanel being a view, it might be preferable + // to have it be a view modifier, similar to how SwiftUI doesn't have a + // SheetView, but a modifier that presents a sheet. + /// The content shown in the floating panel. let content: Content /// Creates a `FloatingPanel` /// - Parameter content: The view shown in the floating panel. - public init(content: Content) { - self.content = content + public init(@ViewBuilder content: () -> Content) { + self.content = content() } @State - private var handleColor: Color = defaultHandleColor + private var handleColor: Color = .defaultHandleColor @State - private var height: CGFloat? = nil - - private let minHeight: CGFloat = 66 + private var height: CGFloat? public var body: some View { VStack { content - .frame(minHeight: minHeight, maxHeight: height) + .frame(minHeight: .minHeight, maxHeight: height) Divider() Handle(color: handleColor) .gesture(drag) @@ -55,29 +57,38 @@ public struct FloatingPanel: View where Content: View { var drag: some Gesture { DragGesture(minimumDistance: 0) .onChanged { value in - self.handleColor = Self.activeHandleColor - height = max(minHeight, (height ?? 0) + value.translation.height) + handleColor = .activeHandleColor + // Note: There is a bug here where `height` can be set + // larger than the displayed height. This occurs by continuing + // to drag down on the handle after the panel reaches it's max + // height. When that happens subsequent "drag up" operations + // don't cause the panel to shrink immediately, but will + // ultimately snap to the correct height. + height = max(.minHeight, (height ?? 0) + value.translation.height) } .onEnded { _ in - self.handleColor = Self.defaultHandleColor + handleColor = .defaultHandleColor } } } -private extension FloatingPanel { - static var defaultHandleColor: Color { .secondary } - static var activeHandleColor: Color { .primary } -} - /// The "Handle" view of the floating panel. private struct Handle: View { /// The color of the handle. var color: Color var body: some View { - Rectangle() + RoundedRectangle(cornerRadius: 4.0) .foregroundColor(color) .frame(width: 100, height: 8.0) - .cornerRadius(4.0) } } + +private extension CGFloat { + static let minHeight: CGFloat = 66 +} + +private extension Color { + static var defaultHandleColor: Color { .secondary } + static var activeHandleColor: Color { .primary } +} From e30f7b524a331a811cbcf8ee11d14f34ff777d2d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 7 Jan 2022 15:57:01 -0600 Subject: [PATCH 0380/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift Co-authored-by: Nimesh Jarecha --- .../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 bd8e76c58..552cbd744 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -107,7 +107,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// Handles changes to the `portal` property. /// - Parameter previousPortal: The previously set `Portal`. func portalDidChange(_ previousPortal: Portal?) { - // Remove all items from `basemapGalleryItems`. + // Remove all items from `items`. items.removeAll() guard let portal = portal else { return } From 8b585c3922a86811f229868746aed5c25f683e45 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 10 Jan 2022 14:19:34 -0600 Subject: [PATCH 0381/1722] Add BasemapGallery.style for grid/list/automatic display. --- .../Examples/BasemapGalleryExampleView.swift | 46 +++----- .../BasemapGallery/BasemapGallery.swift | 110 +++++++++++++++--- 2 files changed, 111 insertions(+), 45 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 10da9d241..2426b8d81 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -44,39 +44,23 @@ struct BasemapGalleryExampleView: View { var body: some View { MapView(map: map, viewpoint: initialViewpoint) - .navigationTitle("Basemap Gallery") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - showBasemapGallery.toggle() - } label: { - Image("basemap") - } - .popover(isPresented: $showBasemapGallery) { - if UIDevice.current.userInterfaceIdiom == .phone { - NavigationView { - BasemapGallery(viewModel: viewModel) - .navigationTitle("Basemaps") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - showBasemapGallery = false - } label: { - Text("Done") - .bold() - } - } - } - } - .navigationViewStyle(.stack) - } else { - BasemapGallery(viewModel: viewModel) - .padding() - } - } + .overlay(alignment: .topTrailing) { + if showBasemapGallery { + BasemapGallery(viewModel: viewModel) + .style(.automatic) + .esriBorder() + .padding() } } + .navigationTitle("Basemap Gallery") + .navigationBarItems(trailing: Button { + showBasemapGallery.toggle() + } label: { + HStack(alignment: .center) { + Image(uiImage: UIImage(named: "basemap")!) + Text(showBasemapGallery ? "Hide Basemaps" : "Show Basemaps") + } + }) } static private func initialBasemaps() -> [BasemapGalleryItem] { diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index bb235d2f7..e9ed115bb 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -20,6 +20,17 @@ import ArcGIS /// `BasemapGalleryViewModel.geoModel` property is set, then the basemap of the /// `geoModel` is replaced with the basemap in the gallery. public struct BasemapGallery: View { + /// The view style of the gallery. + public enum Style { + /// The `BasemapGallery` will display as a grid when there is appropriate + /// 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 + /// The `BasemapGallery` will display as a list. + case list + } + /// Creates a `BasemapGallery`. /// - Parameter viewModel: The view model used by the `BasemapGallery`. public init(viewModel: BasemapGalleryViewModel? = nil) { @@ -32,6 +43,25 @@ public struct BasemapGallery: View { /// user action. @ObservedObject public 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 `automatic`. + /// Set using the `style` modifier. + private var style: Style = .automatic + + /// The size class used to determine if the basemap items should dispaly in a list or grid. + /// If the size class is `.regular`, they display in a grid. If it is `.compact`, they display in a list. + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + /// `true` if the horizontal size class is `.regular`, `false` if it's not. + private var isRegularWidth: Bool { + horizontalSizeClass == .regular + } + + /// The width of the gallery, taking into account the horizontal size class of the device. + private var galleryWidth: CGFloat? { + isRegularWidth ? 300 : 150 + } /// A Boolean value indicating whether to show an error alert. @State @@ -43,6 +73,7 @@ public struct BasemapGallery: View { public var body: some View { makeGalleryView() + .frame(width: galleryWidth) .alert( alertItem?.title ?? "", isPresented: $showErrorAlert, @@ -58,25 +89,61 @@ private extension BasemapGallery { /// - Returns: A view representing the basemap gallery with the specified columns. func makeGalleryView() -> some View { ScrollView { - let columns = Array( + switch style { + case .automatic: + if isRegularWidth { + makeGridView() + } + else { + makeListView() + } + case .grid: + makeGridView() + case .list: + makeListView() + } + } + } + + /// The gallery view, displayed as a grid. + /// - Returns: A view representing the basemap gallery grid. + func makeGridView() -> some View { + internalMakeGalleryView( + Array( repeating: GridItem( - .flexible(minimum: 75, maximum: 100), + .flexible(), alignment: .top ), count: 3 ) - LazyVGrid(columns: columns) { - ForEach(viewModel.items) { item in - BasemapGalleryCell( - item: item, - isSelected: item == viewModel.currentItem - ) { - if let loadError = item.loadBasemapError { - alertItem = AlertItem(loadBasemapError: loadError) - showErrorAlert = true - } else { - viewModel.currentItem = item - } + ) + } + + /// The gallery view, displayed as a list. + /// - Returns: A view representing the basemap gallery list. + func makeListView() -> some View { + internalMakeGalleryView( + [ + .init( + .flexible(), + alignment: .top + ) + ] + ) + } + + func internalMakeGalleryView(_ columns: [GridItem]) -> some View { + LazyVGrid(columns: columns) { + ForEach(viewModel.items) { item in + BasemapGalleryCell( + item: item, + isSelected: item == viewModel.currentItem + ) { + if let loadError = item.loadBasemapError { + alertItem = AlertItem(loadBasemapError: loadError) + showErrorAlert = true + } else { + viewModel.currentItem = item } } } @@ -84,6 +151,21 @@ private extension BasemapGallery { } } +// MARK: Modifiers + +public extension BasemapGallery { + /// The style of the basemap gallery. Defaults to `.automatic`. + /// - Parameter style: The `Style` to use. + /// - Returns: The `BasemapGallery`. + func style( + _ newStyle: Style + ) -> BasemapGallery { + var copy = self + copy.style = newStyle + return copy + } +} + // MARK: AlertItem /// An item used to populate a displayed alert. From af52672964feb99ef23852c77463d606beec12c1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jan 2022 09:31:26 -0600 Subject: [PATCH 0382/1722] Apply suggestions from code review Co-authored-by: Zachary A Kline --- .../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 e9ed115bb..d21861a5b 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -22,8 +22,8 @@ import ArcGIS public struct BasemapGallery: View { /// The view style of the gallery. public enum Style { - /// The `BasemapGallery` will display as a grid when there is appropriate - /// width available for the gallery to do so. Otherwise the gallery will display as a list. + /// 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 /// The `BasemapGallery` will display as a grid. case grid @@ -45,7 +45,7 @@ public struct BasemapGallery: View { public 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 `automatic`. + /// switch between the two based on-screen real estate. Defaults to ``BasemapGallery/Style/automatic``. /// Set using the `style` modifier. private var style: Style = .automatic From b68f463271c59f23890cbb9a9ec143f7f65ab19f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jan 2022 09:32:38 -0600 Subject: [PATCH 0383/1722] Start of FloorFilter implementation --- Examples/Examples.xcodeproj/project.pbxproj | 4 + .../Examples/FloorFilterExampleView.swift | 75 +++++++++ Examples/ExamplesApp/Examples.swift | 1 + .../Components/FloorFilter/FloorFilter.swift | 30 ++++ .../FloorFilter/FloorFilterViewModel.swift | 154 ++++++++++++++++++ .../Extensions/FloorFacility.swift | 21 +++ .../ArcGISToolkit/Extensions/FloorSite.swift | 21 +++ .../Site.imageset/Contents.json | 26 +++ .../Site.imageset/business_black_18dp (1).svg | 1 + .../business_black_24dp (1)-1.svg | 1 + .../Site.imageset/business_black_24dp (1).svg | 1 + 11 files changed, 335 insertions(+) create mode 100644 Examples/Examples/FloorFilterExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift create mode 100644 Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift create mode 100644 Sources/ArcGISToolkit/Extensions/FloorFacility.swift create mode 100644 Sources/ArcGISToolkit/Extensions/FloorSite.swift create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg create mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 437bc78b7..37a702e2b 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; + E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */; }; E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; @@ -40,6 +41,7 @@ /* Begin PBXFileReference section */ E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; + E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloorFilterExampleView.swift; sourceTree = ""; }; E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -77,6 +79,7 @@ E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, E42BFBE82672BF9500159107 /* SearchExampleView.swift */, E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, + E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -221,6 +224,7 @@ E4AA9316276BF5ED000E6289 /* FloatingPanelExampleView.swift in Sources */, E48A73442658227100F5C118 /* AnyExample.swift in Sources */, E48A73472658227100F5C118 /* ExampleList.swift in Sources */, + E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */, E4C389D526B8A12C002BC255 /* BasemapGalleryExampleView.swift in Sources */, diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift new file mode 100644 index 000000000..30e61ef5b --- /dev/null +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -0,0 +1,75 @@ +// 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 ArcGISToolkit +import ArcGIS + +struct FloorFilterExampleView: View { + private let map: Map + + @State + private var viewpoint = Viewpoint( + center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), + scale: 1_000_000 + ) + + @State + private var floorFilterViewModel: FloorFilterViewModel? = nil + + init() { + // Create the map from a portal item and assign to the mapView. + let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") + map = Map(item: portalItem) + } + + private let floorFilterPadding: CGFloat = 48 + + var body: some View { + MapView( + map: map, + viewpoint: viewpoint + ) + .overlay(alignment: .bottomLeading) { + if let viewModel = floorFilterViewModel { + FloorFilter(viewModel) + .esriBorder() + .padding(floorFilterPadding) + } + else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .esriBorder() + .padding(floorFilterPadding) + } + } + .task { + do { + try await map.load() + guard let floorManager = map.floorManager else { return } + floorFilterViewModel = FloorFilterViewModel( + viewpoint: $viewpoint, + floorManager: floorManager + ) + } catch { } + } + } +} +// +//struct SampleContent: View { +// var body: some View { +// List(1..<21) { Text("\($0)") } +// .listStyle(.plain) +// } +//} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index aacf22bc5..3b3b4cec8 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -36,6 +36,7 @@ extension ExampleList { name: "GeoView", examples: [ AnyExample("Basemap Gallery", content: BasemapGalleryExampleView()), + AnyExample("Floor Filter", content: FloorFilterExampleView()), AnyExample("Overview Map", content: OverviewMapExampleView()), AnyExample("Search", content: SearchExampleView()) ] diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift new file mode 100644 index 000000000..008557dc7 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -0,0 +1,30 @@ +// 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 + +public struct FloorFilter: View { + /// Creates a `FloorFilter` + /// - Parameter content: The view shown in the floating panel. + public init(_ floorFilterViewModel: FloorFilterViewModel) { + self.floorFilterViewModel = floorFilterViewModel + } + + private let floorFilterViewModel: FloorFilterViewModel + + public var body: some View { + TODO: turn this into a button... + Image(uiImage: .site) + } +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift new file mode 100644 index 000000000..69109e162 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -0,0 +1,154 @@ +// 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 + +/* + + // method that gets/sets the facility in the floor filter using the facility ID + string selectedFacilityId() + void setSelectedFacilityId(string facilityId) + + // method that gets/sets the level in the floor filter using the level ID + string selectedLevelId() + void setSelectedtLevelId(String levelId) + + // method that gets/sets the site in the floor filter using the site ID + string selectedSiteId() + void setSelectedSiteId(String siteId) + + */ + +/// View Model class that contains the Data Model of the Floor Filter +/// Also contains the business logic to filter and change the map extent based on selected site/level/facility +public class FloorFilterViewModel { + public init( + viewpoint: Binding? = nil, + floorManager: FloorManager + ) { + self.viewpoint = viewpoint + self.floorManager = floorManager + } + + /// The `Viewpoint` used to pan/zoom to the floor level. If `nil`, there will be no zooming. + public var viewpoint: Binding? = nil + + public var floorManager: FloorManager + + public var sites: [FloorSite] { + return floorManager.sites + } + + /// Facilities in the selected site + /// If no site is selected then the list is empty + /// If the sites data does not exist in the map, then use all the facilities in the map + public var facilities: [FloorFacility] { + return sites.isEmpty ? floorManager.facilities : floorManager.facilities.filter { + $0.site == selectedSite + } + } + + /// Levels that are visible in the expanded Floor Filter levels table view + /// Reverse the order of the levels to make it in ascending order + public var visibleLevelsInExpandedList: [FloorLevel] { + return facilities.isEmpty ? floorManager.levels : floorManager.levels.filter { + $0.facility == selectedFacility + }.reversed() + } + + /// All the levels in the map + public var allLevels: [FloorLevel] { + return floorManager.levels + } + + /// The site, facility, and level that are selected by the user + public var selectedSite: FloorSite? = nil + public var selectedFacility: FloorFacility? = nil + public var selectedLevel: FloorLevel? = nil + + /// The default vertical order is 0 according to Runtime 100.12 update for FloorManager + public let defaultVerticalOrder: Int32 = 0 + + public func reset() { + selectedSite = nil + selectedFacility = nil + selectedLevel = nil + } + + public func getDefaultLevelForFacility(facility: FloorFacility?) -> FloorLevel? { + let candidateLevels = allLevels.filter {$0.facility == facility} + return candidateLevels.first {$0.verticalOrder == 0} ?? getLowestLevel(levels: candidateLevels) + } + + /// Returns the FloorLevel with the lowest verticalOrder. + private func getLowestLevel(levels: [FloorLevel]) -> FloorLevel? { + var lowestLevel: FloorLevel? = nil + allLevels.forEach { + if ($0.verticalOrder != Int.min && $0.verticalOrder != Int.max) { + let lowestVerticalOrder = lowestLevel?.verticalOrder + if (lowestVerticalOrder == nil || lowestVerticalOrder ?? defaultVerticalOrder > $0.verticalOrder) { + lowestLevel = $0 + } + } + } + return lowestLevel + } + + /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level + public func filterMapToSelectedLevel() { + guard let selectedLevel = selectedLevel else { return } + allLevels.forEach { + $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder + } + } + + /// Zooms to the facility if there is a selected facility, otherwise zooms to the site. + public func zoomToSelection() { + if selectedFacility != nil { + zoomToFacility() + } else if selectedSite != nil { + zoomToSite() + } + } + + private func zoomToSite() { + zoomToExtent(extent: selectedSite?.geometry?.extent) + } + + private func zoomToFacility() { + zoomToExtent(extent: selectedFacility?.geometry?.extent) + } + + private func zoomToExtent(extent: Envelope?) { + // Make sure we have an extent and viewpoint to zoom to. + guard let extent = extent, + let viewpoint = viewpoint + else { return } + + let builder = EnvelopeBuilder(envelope: extent) + builder.expand(factor: 1.5) + let targetExtent = builder.toGeometry() as! Envelope + if !targetExtent.isEmpty { + viewpoint.wrappedValue = Viewpoint( + targetExtent: targetExtent + ) + } + } +} + +extension UIImage { + static var site: UIImage { + return UIImage(named: "Site", in: Bundle.module, with: nil)! + } +} diff --git a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift new file mode 100644 index 000000000..79bf5c685 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift @@ -0,0 +1,21 @@ +// 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 + +extension FloorFacility: Equatable { + public static func == (lhs: FloorFacility, rhs: FloorFacility) -> Bool { + lhs.facilityId == rhs.facilityId + } +} diff --git a/Sources/ArcGISToolkit/Extensions/FloorSite.swift b/Sources/ArcGISToolkit/Extensions/FloorSite.swift new file mode 100644 index 000000000..fda6bcf55 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/FloorSite.swift @@ -0,0 +1,21 @@ +// 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 + +extension FloorSite: Equatable { + public static func == (lhs: FloorSite, rhs: FloorSite) -> Bool { + lhs.siteId == rhs.siteId + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json new file mode 100644 index 000000000..972dad443 --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "business_black_18dp (1).svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "business_black_24dp (1).svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "business_black_24dp (1)-1.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg new file mode 100644 index 000000000..c91f34b5f --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg new file mode 100644 index 000000000..541e8376f --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg new file mode 100644 index 000000000..541e8376f --- /dev/null +++ b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg @@ -0,0 +1 @@ + \ No newline at end of file From 8883b4d39af31f8961fce0036c007225ced03b43 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jan 2022 09:45:23 -0600 Subject: [PATCH 0384/1722] PR review changes. --- Examples/Examples/BasemapGalleryExampleView.swift | 7 +++++-- .../Components/BasemapGallery/BasemapGallery.swift | 8 +++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 2426b8d81..11e4d60bc 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -57,8 +57,11 @@ struct BasemapGalleryExampleView: View { showBasemapGallery.toggle() } label: { HStack(alignment: .center) { - Image(uiImage: UIImage(named: "basemap")!) - Text(showBasemapGallery ? "Hide Basemaps" : "Show Basemaps") + if showBasemapGallery { + Image(systemName: "xmark.circle") + } else { + Image(uiImage: UIImage(named: "basemap")!) + } } }) } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index d21861a5b..b8552a94b 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -84,9 +84,8 @@ public struct BasemapGallery: View { } private extension BasemapGallery { - /// The gallery view, displayed in the specified columns. - /// - Parameter columns: The columns used to display the basemap items. - /// - Returns: A view representing the basemap gallery with the specified columns. + /// The gallery view. + /// - Returns: A view representing the basemap gallery. func makeGalleryView() -> some View { ScrollView { switch style { @@ -132,6 +131,9 @@ private extension BasemapGallery { ) } + /// The gallery view, displayed in the specified columns. + /// - Parameter columns: The columns used to display the basemap items. + /// - Returns: A view representing the basemap gallery with the specified columns. func internalMakeGalleryView(_ columns: [GridItem]) -> some View { LazyVGrid(columns: columns) { ForEach(viewModel.items) { item in From 4b4b8887f782afa086de816fc56ccfb2c09afbd0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jan 2022 12:53:35 -0600 Subject: [PATCH 0385/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift Co-authored-by: Zachary A Kline --- .../Components/BasemapGallery/BasemapGallery.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index b8552a94b..99820ef98 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -156,7 +156,7 @@ private extension BasemapGallery { // MARK: Modifiers public extension BasemapGallery { - /// The style of the basemap gallery. Defaults to `.automatic`. + /// The style of the basemap gallery. Defaults to ``Style/automatic``. /// - Parameter style: The `Style` to use. /// - Returns: The `BasemapGallery`. func style( From 873f250fb94c1d317b50ef558e5199d9dea418df Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jan 2022 13:32:34 -0600 Subject: [PATCH 0386/1722] Add SiteSelector and change up the loading. --- .../Examples/FloorFilterExampleView.swift | 18 +---- .../Components/FloorFilter/FloorFilter.swift | 36 ++++++++- .../FloorFilter/FloorFilterViewModel.swift | 29 ++++--- .../Components/FloorFilter/SiteSelector.swift | 77 +++++++++++++++++++ .../Extensions/FloorFacility.swift | 2 + .../ArcGISToolkit/Extensions/FloorSite.swift | 2 + 6 files changed, 135 insertions(+), 29 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 30e61ef5b..552f90d3f 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -44,13 +44,6 @@ struct FloorFilterExampleView: View { .overlay(alignment: .bottomLeading) { if let viewModel = floorFilterViewModel { FloorFilter(viewModel) - .esriBorder() - .padding(floorFilterPadding) - } - else { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .esriBorder() .padding(floorFilterPadding) } } @@ -59,17 +52,10 @@ struct FloorFilterExampleView: View { try await map.load() guard let floorManager = map.floorManager else { return } floorFilterViewModel = FloorFilterViewModel( - viewpoint: $viewpoint, - floorManager: floorManager + floorManager: floorManager, + viewpoint: $viewpoint ) } catch { } } } } -// -//struct SampleContent: View { -// var body: some View { -// List(1..<21) { Text("\($0)") } -// .listStyle(.plain) -// } -//} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 008557dc7..37cd06b9a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -18,13 +18,41 @@ public struct FloorFilter: View { /// Creates a `FloorFilter` /// - Parameter content: The view shown in the floating panel. public init(_ floorFilterViewModel: FloorFilterViewModel) { - self.floorFilterViewModel = floorFilterViewModel + self.viewModel = floorFilterViewModel } - private let floorFilterViewModel: FloorFilterViewModel + @ObservedObject + private(set) var viewModel: FloorFilterViewModel + + @State + private var showSiteSelector: Bool = false public var body: some View { - TODO: turn this into a button... - Image(uiImage: .site) + HStack(alignment: .bottom) { + if viewModel.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .esriBorder() + } else { + VStack { + // + // show levels and close button here + // + Button { + showSiteSelector.toggle() + } label: { + Image(uiImage: .site) + } + } + .esriBorder() + if showSiteSelector { + SiteSelector( + viewModel, + showSiteSelector: $showSiteSelector + ) + .frame(width: 200) + } + } + } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 69109e162..33cc39686 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -30,31 +30,42 @@ import ArcGIS */ +@MainActor /// View Model class that contains the Data Model of the Floor Filter /// Also contains the business logic to filter and change the map extent based on selected site/level/facility -public class FloorFilterViewModel { +public class FloorFilterViewModel: ObservableObject { public init( - viewpoint: Binding? = nil, - floorManager: FloorManager + floorManager: FloorManager, + viewpoint: Binding? = nil ) { self.viewpoint = viewpoint self.floorManager = floorManager + Task { + do { + try await floorManager.load() + isLoading = false + } catch { } + } } /// The `Viewpoint` used to pan/zoom to the floor level. If `nil`, there will be no zooming. - public var viewpoint: Binding? = nil + var viewpoint: Binding? = nil - public var floorManager: FloorManager + var floorManager: FloorManager public var sites: [FloorSite] { - return floorManager.sites + floorManager.sites } + /// `true` if the model is loading it's properties, `false` if not loading. + @Published + public var isLoading = true + /// Facilities in the selected site /// If no site is selected then the list is empty /// If the sites data does not exist in the map, then use all the facilities in the map public var facilities: [FloorFacility] { - return sites.isEmpty ? floorManager.facilities : floorManager.facilities.filter { + sites.isEmpty ? floorManager.facilities : floorManager.facilities.filter { $0.site == selectedSite } } @@ -62,14 +73,14 @@ public class FloorFilterViewModel { /// Levels that are visible in the expanded Floor Filter levels table view /// Reverse the order of the levels to make it in ascending order public var visibleLevelsInExpandedList: [FloorLevel] { - return facilities.isEmpty ? floorManager.levels : floorManager.levels.filter { + facilities.isEmpty ? floorManager.levels : floorManager.levels.filter { $0.facility == selectedFacility }.reversed() } /// All the levels in the map public var allLevels: [FloorLevel] { - return floorManager.levels + floorManager.levels } /// The site, facility, and level that are selected by the user diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift new file mode 100644 index 000000000..04b627353 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift @@ -0,0 +1,77 @@ +// 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 + +struct SiteSelector: View { + /// Creates a `FloorFilter` + /// - Parameter content: The view shown in the floating panel. + public init( + _ floorFilterViewModel: FloorFilterViewModel, + showSiteSelector: Binding + ) { + self.viewModel = floorFilterViewModel + self.showSiteList = showSiteSelector + } + + private let viewModel: FloorFilterViewModel + + /// Binding allowing the user to toggle the visibility of the results list. + private var showSiteList: Binding + + // TODO: refactor if-else below. + var body: some View { + if !viewModel.sites.isEmpty { + LazyVStack { + HStack { + Text("Select a site...") + .bold() + Spacer() + Button { + showSiteList.wrappedValue.toggle() + } label: { + Image(systemName: "xmark.circle") + } + } + Rectangle() + .frame(height:1) + .foregroundColor(.secondary) + ForEach(viewModel.sites) { site in + Text("\(site.name)") + } + } + .esriBorder() + } else { + LazyVStack { + HStack { + Text("Select a facility...") + .bold() + Spacer() + Button { + showSiteList.wrappedValue.toggle() + } label: { + Image(systemName: "xmark.circle") + } + } + Rectangle() + .frame(height:1) + .foregroundColor(.secondary) + ForEach(viewModel.facilities) { facility in + Text("\(facility.name)") + } + } + .esriBorder() + } + } +} diff --git a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift index 79bf5c685..48a492c99 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift @@ -19,3 +19,5 @@ extension FloorFacility: Equatable { lhs.facilityId == rhs.facilityId } } + +extension FloorFacility: Identifiable { } diff --git a/Sources/ArcGISToolkit/Extensions/FloorSite.swift b/Sources/ArcGISToolkit/Extensions/FloorSite.swift index fda6bcf55..3fcc5d196 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorSite.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorSite.swift @@ -19,3 +19,5 @@ extension FloorSite: Equatable { lhs.siteId == rhs.siteId } } + +extension FloorSite: Identifiable { } From 9db128a35ffe6f56f01a5f1007dd3e12631a632c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jan 2022 14:17:36 -0600 Subject: [PATCH 0387/1722] Fix list/grid widths. --- .../Examples/BasemapGalleryExampleView.swift | 2 +- .../BasemapGallery/BasemapGallery.swift | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 11e4d60bc..dc9d7b37c 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -47,7 +47,7 @@ struct BasemapGalleryExampleView: View { .overlay(alignment: .topTrailing) { if showBasemapGallery { BasemapGallery(viewModel: viewModel) - .style(.automatic) + .style(.automatic()) .esriBorder() .padding() } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 99820ef98..9151e3271 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -24,11 +24,12 @@ 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 - /// The `BasemapGallery` will display as a grid. - case grid - /// The `BasemapGallery` will display as a list. - case list + /// Defaults to 125 when displayed as a list, 300 when displayed as a grid. + case automatic(listWidth: CGFloat = 125, gridWidth: CGFloat = 300) + /// The `BasemapGallery` will display as a grid. Defaults to 300. + case grid(width: CGFloat = 300) + /// The `BasemapGallery` will display as a list. Defaults to 125. + case list(width: CGFloat = 125) } /// Creates a `BasemapGallery`. @@ -47,7 +48,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() /// The size class used to determine if the basemap items should dispaly in a list or grid. /// If the size class is `.regular`, they display in a grid. If it is `.compact`, they display in a list. @@ -59,8 +60,15 @@ public struct BasemapGallery: View { } /// The width of the gallery, taking into account the horizontal size class of the device. - private var galleryWidth: CGFloat? { - isRegularWidth ? 300 : 150 + 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. From e5bd732b631ab7242733142ebdb87e948ff8dfe9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jan 2022 14:36:41 -0600 Subject: [PATCH 0388/1722] fix doc for default values. --- .../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 9151e3271..e640c3793 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -24,11 +24,11 @@ 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. + /// Defaults to `125` when displayed as a list, `300` when displayed as a grid. case automatic(listWidth: CGFloat = 125, gridWidth: CGFloat = 300) - /// The `BasemapGallery` will display as a grid. Defaults to 300. + /// The `BasemapGallery` will display as a grid. Defaults to `300`. case grid(width: CGFloat = 300) - /// The `BasemapGallery` will display as a list. Defaults to 125. + /// The `BasemapGallery` will display as a list. Defaults to `125`. case list(width: CGFloat = 125) } From aa50309fbe171e640d129d487e1804c5d6814f52 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 12 Jan 2022 13:32:57 -0600 Subject: [PATCH 0389/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- .../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 e640c3793..e149d6985 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -92,7 +92,7 @@ public struct BasemapGallery: View { } private extension BasemapGallery { - /// The gallery view. + /// Creates a gallery view. /// - Returns: A view representing the basemap gallery. func makeGalleryView() -> some View { ScrollView { @@ -100,8 +100,7 @@ private extension BasemapGallery { case .automatic: if isRegularWidth { makeGridView() - } - else { + } else { makeListView() } case .grid: From 39837b16321d6de034e3c0e457a309a29419df06 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 12 Jan 2022 13:38:46 -0600 Subject: [PATCH 0390/1722] Update Examples/Examples/BasemapGalleryExampleView.swift Co-authored-by: Philip Ridgeway --- Examples/Examples/BasemapGalleryExampleView.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index dc9d7b37c..3244c9fe8 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -53,16 +53,8 @@ struct BasemapGalleryExampleView: View { } } .navigationTitle("Basemap Gallery") - .navigationBarItems(trailing: Button { - showBasemapGallery.toggle() - } label: { - HStack(alignment: .center) { - if showBasemapGallery { - Image(systemName: "xmark.circle") - } else { - Image(uiImage: UIImage(named: "basemap")!) - } - } + .navigationBarItems(trailing: Toggle(isOn: $showBasemapGallery) { + Image("basemap", label: Text("Show base map")) }) } From 8ab7febc73150601f8a553ccf9de761ff45b2548 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 12 Jan 2022 13:44:02 -0600 Subject: [PATCH 0391/1722] Updates from PR review for regular/compact widths. --- .../BasemapGallery/BasemapGallery.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index e149d6985..63d853763 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -50,16 +50,16 @@ public struct BasemapGallery: View { /// Set using the `style` modifier. private var style: Style = .automatic() - /// The size class used to determine if the basemap items should dispaly in a list or grid. - /// If the size class is `.regular`, they display in a grid. If it is `.compact`, they display in a list. @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass - /// `true` if the horizontal size class is `.regular`, `false` if it's not. + /// If `true`, the gallery will display as if the device is in a regular-width orientation. + /// If `false`, the gallery will display as if the device is in a compact-width orientation. private var isRegularWidth: Bool { - horizontalSizeClass == .regular + !(horizontalSizeClass == .compact && verticalSizeClass == .regular) } - - /// The width of the gallery, taking into account the horizontal size class of the device. + + /// 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): @@ -70,7 +70,7 @@ public struct BasemapGallery: View { return isRegularWidth ? gridWidth : listWidth } } - + /// A Boolean value indicating whether to show an error alert. @State private var showErrorAlert = false @@ -115,7 +115,7 @@ private extension BasemapGallery { /// - Returns: A view representing the basemap gallery grid. func makeGridView() -> some View { internalMakeGalleryView( - Array( + columns: Array( repeating: GridItem( .flexible(), alignment: .top @@ -129,7 +129,7 @@ private extension BasemapGallery { /// - Returns: A view representing the basemap gallery list. func makeListView() -> some View { internalMakeGalleryView( - [ + columns: [ .init( .flexible(), alignment: .top @@ -141,7 +141,7 @@ private extension BasemapGallery { /// The gallery view, displayed in the specified columns. /// - Parameter columns: The columns used to display the basemap items. /// - Returns: A view representing the basemap gallery with the specified columns. - func internalMakeGalleryView(_ columns: [GridItem]) -> some View { + func internalMakeGalleryView(columns: [GridItem]) -> some View { LazyVGrid(columns: columns) { ForEach(viewModel.items) { item in BasemapGalleryCell( From 7c01b6ccd68f086776c76ad6d1045ccb45bfa877 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 13 Jan 2022 11:38:05 -0600 Subject: [PATCH 0392/1722] Add Spatial Reference checking. --- .../BasemapGallery/BasemapGallery.swift | 24 ++++++- .../BasemapGallery/BasemapGalleryCell.swift | 4 ++ .../BasemapGallery/BasemapGalleryItem.swift | 63 +++++++++++++++++++ .../BasemapGalleryViewModel.swift | 52 ++++++++++++++- 4 files changed, 141 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 63d853763..540a91004 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -82,6 +82,12 @@ public struct BasemapGallery: View { public var body: some View { makeGalleryView() .frame(width: galleryWidth) + .onReceive( + viewModel.$spatialReferenceMismatchError.dropFirst(), + perform: { error in + guard let error = error else { return } + alertItem = AlertItem(spatialReferenceMismatchError: error) + }) .alert( alertItem?.title ?? "", isPresented: $showErrorAlert, @@ -152,7 +158,7 @@ private extension BasemapGallery { alertItem = AlertItem(loadBasemapError: loadError) showErrorAlert = true } else { - viewModel.currentItem = item + viewModel.updateCurrentItem(item) } } } @@ -192,4 +198,20 @@ extension AlertItem { message: "\((loadBasemapError as? RuntimeError)?.failureReason ?? "The basemap failed to load for an unknown reason.")" ) } + + /// Creates an alert item based on a spatial reference mismatch. + /// - Parameters: + /// - basemapSR: The basemap's spatial reference. + /// - geoModelSR: The geomodel's spatial reference. + init(spatialReferenceMismatchError: SpatialReferenceMismatchError) { + self.init( + title: "Spatial reference mismatch.", + message: + """ + The spatial reference of the basemap: + \(spatialReferenceMismatchError.basemapSR?.description ?? "") does not match that of the geomodel: + \(spatialReferenceMismatchError.geoModelSR?.description ?? ""). +""" + ) + } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index 711db3ca4..d69046df2 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -47,6 +47,10 @@ struct BasemapGalleryCell: View { Image(systemName: "minus.circle.fill") .font(.title) .foregroundColor(.red) + } else if item.spatialReferenceStatus == .noMatch { + Image(systemName: "x.circle.fill") + .font(.title) + .foregroundColor(.red) } // Display a progress view if the item is loading. diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 3ce1a7d75..06e3360e4 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -17,6 +17,17 @@ import ArcGIS /// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. public class BasemapGalleryItem: ObservableObject { + /// Indicates the status of the basemap's spatial reference in relation to a reference spatial reference. + public enum SpatialReferenceStatus { + /// The basemap's spatial reference status is unknown, either because the basemap's + /// base layers haven't been loaded yet or the status has yet to be updated. + case unknown + /// The basemap's spatial reference matches the reference spatial reference. + case match + /// The basemap's spatial reference does not match the reference spatial reference. + case noMatch + } + /// Creates a `BasemapGalleryItem`. /// - Parameters: /// - basemap: The `Basemap` represented by the item. @@ -67,6 +78,15 @@ public class BasemapGalleryItem: ObservableObject { /// A Boolean value indicating whether the `basemap` or it's base layers are being loaded. @Published private(set) var isBasemapLoading = true + + /// The `SpatialReferenceStatus` of the item. This is set via a call to + /// ``updateSpatialReferenceStatus()``. + @Published + public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown + + /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's + /// baseLayers have been loaded in ``updateSpatialReferenceStatus()``. + public private(set) var spatialReference: SpatialReference? = nil } private extension BasemapGalleryItem { @@ -114,6 +134,49 @@ extension BasemapGalleryItem: Equatable { } } +public extension BasemapGalleryItem { + /// Updats the `spatialReferenceStatus` by loading the first base layer of `basemap` + /// and determining if it matches `referenceSpatialReference`. + /// - Parameter referenceSpatialReference: The `SpatialReference` to match to. + func updateSpatialReferenceStatus( + _ referenceSpatialReference: SpatialReference? + ) async throws { + guard basemap.loadStatus == .loaded else { return } + + if spatialReference == nil { + await MainActor.run { + isBasemapLoading = true + } + try await basemap.baseLayers.first?.load() + } + + await finalizeUpdateSpatialReferenceStatus( + with: referenceSpatialReference + ) + } + + /// Updates the item's `spatialReference` and `spatialReferenceStatus` properties. + /// - Parameter referenceSpatialReference: The `SpatialReference` used to + /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` + /// `SpatialReference`. + @MainActor + func finalizeUpdateSpatialReferenceStatus( + with referenceSpatialReference: SpatialReference? + ) { + spatialReference = basemap.baseLayers.first?.spatialReference + if referenceSpatialReference == nil { + spatialReferenceStatus = .unknown + } + else if spatialReference == referenceSpatialReference { + spatialReferenceStatus = .match + } + else { + spatialReferenceStatus = .noMatch + } + isBasemapLoading = false + } +} + private extension UIImage { /// A default thumbnail image. /// - Returns: The default thumbnail. diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 552cbd744..c5ba09f61 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -60,6 +60,11 @@ public class BasemapGalleryViewModel: ObservableObject { /// The error generated by fetching the `Basemaps` from the `Portal`. @Published public var fetchBasemapsError: Error? = nil + + /// The error signifying the spatial reference of the GeoModel and that of a potential + /// current `BasemapGalleryItem` do not match. + @Published + public 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 @@ -86,7 +91,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// The `BasemapGalleryItem` representing the `GeoModel`'s current basemap. This may be a /// basemap which does not exist in the gallery. @Published - public var currentItem: BasemapGalleryItem? = nil { + public private(set) var currentItem: BasemapGalleryItem? = nil { didSet { guard let item = currentItem else { return } geoModel?.basemap = item.basemap @@ -113,6 +118,40 @@ public class BasemapGalleryViewModel: ObservableObject { guard let portal = portal else { return } fetchBasemaps(from: portal) } + + /// This attempts to set `currentItem`. `currentItem` will be set if it's spatial reference + /// matches that of the `geoModel`. If the spatial references do not match, `currentItem` + /// will be unchanged. + /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. + public func updateCurrentItem( + _ basemapGalleryItem: BasemapGalleryItem + ) { + Task { + // Ensure the geoModel is loaded. + try await geoModel?.load() + + // Reset the mismatch error. + spatialReferenceMismatchError = nil + + // Update the basemap gallery item's `spatialReferenceStatus`. + try await basemapGalleryItem.updateSpatialReferenceStatus( + geoModel?.actualSpatialReference + ) + + // Update @State on the main thread. + await MainActor.run { + switch basemapGalleryItem.spatialReferenceStatus { + case .match, .unknown: + currentBasemapGalleryItem = basemapGalleryItem + case .noMatch: + spatialReferenceMismatchError = SpatialReferenceMismatchError( + basemapSR: basemapGalleryItem.spatialReference, + geoModelSR: geoModel?.actualSpatialReference + ) + } + } + } + } } private extension BasemapGalleryViewModel { @@ -160,3 +199,14 @@ private extension BasemapGalleryViewModel { } } } + +/// An error describing a SpatialReference mismatch between a geomodel and a basemap. +public struct SpatialReferenceMismatchError: Error { + /// The basemap's spatial reference. + public let basemapSR: SpatialReference? + + /// The geomodel's spatial reference. + public let geoModelSR: SpatialReference? +} + +extension SpatialReferenceMismatchError: Equatable {} From 3ef1fc3171b7fd0ec782711e07f838ce085d0ba3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 13 Jan 2022 13:55:07 -0600 Subject: [PATCH 0393/1722] Add SpatialReference checking --- .../Examples/BasemapGalleryExampleView.swift | 8 +++++--- .../BasemapGallery/BasemapGallery.swift | 16 +++++++--------- .../BasemapGallery/BasemapGalleryViewModel.swift | 14 +++++++++++++- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 3244c9fe8..cb88b6740 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -36,9 +36,9 @@ struct BasemapGalleryExampleView: View { init() { self.map = Map(basemapStyle: .arcGISImagery) self.viewModel = BasemapGalleryViewModel( - geoModel: self.map + geoModel: self.map, // You can add your own basemaps by passing them in here: - //items: Self.initialBasemaps() + items: Self.initialBasemaps() ) } @@ -61,7 +61,9 @@ struct BasemapGalleryExampleView: View { static private func initialBasemaps() -> [BasemapGalleryItem] { let identifiers = [ "46a87c20f09e4fc48fa3c38081e0cae6", - "f33a34de3a294590ab48f246e99958c9" + "f33a34de3a294590ab48f246e99958c9", + "52bdc7ab7fb044d98add148764eaa30a", //<<== mismatched spatial reference + "3a8d410a4a034a2ba9738bb0860d68c4" //<<== incorrect portal item type ] return identifiers.map { identifier in diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 540a91004..8c1a6cbce 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -87,13 +87,16 @@ public struct BasemapGallery: View { perform: { error in guard let error = error else { return } alertItem = AlertItem(spatialReferenceMismatchError: error) + showErrorAlert = true }) .alert( alertItem?.title ?? "", isPresented: $showErrorAlert, - presenting: alertItem) { item in - Text(item.message) - } + presenting: alertItem + ) { _ in + } message: { item in + Text(item.message) + } } } @@ -206,12 +209,7 @@ extension AlertItem { init(spatialReferenceMismatchError: SpatialReferenceMismatchError) { self.init( title: "Spatial reference mismatch.", - message: - """ - The spatial reference of the basemap: - \(spatialReferenceMismatchError.basemapSR?.description ?? "") does not match that of the geomodel: - \(spatialReferenceMismatchError.geoModelSR?.description ?? ""). -""" + message: "The spatial reference of the basemap: \(spatialReferenceMismatchError.basemapSR?.description ?? "") does not match that of the geomodel: \(spatialReferenceMismatchError.geoModelSR?.description ?? "")." ) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index c5ba09f61..b82d60027 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -142,7 +142,7 @@ public class BasemapGalleryViewModel: ObservableObject { await MainActor.run { switch basemapGalleryItem.spatialReferenceStatus { case .match, .unknown: - currentBasemapGalleryItem = basemapGalleryItem + currentItem = basemapGalleryItem case .noMatch: spatialReferenceMismatchError = SpatialReferenceMismatchError( basemapSR: basemapGalleryItem.spatialReference, @@ -210,3 +210,15 @@ public struct SpatialReferenceMismatchError: Error { } extension SpatialReferenceMismatchError: Equatable {} + +extension GeoModel { + /// The actual spatial reference of the geoModel. For `Map`s, this is the map's + /// `spatialReference`. For `Scene`s, if the `sceneViewTilingScheme` is + /// `.webMercator`, the `actualSpatialReference` is `.webMercator`, otherwise + /// it is the `spatialReference` of the scene. + var actualSpatialReference: SpatialReference? { + (self as? ArcGIS.Scene)?.sceneViewTilingScheme == .webMercator ? + SpatialReference.webMercator : + spatialReference + } +} From 0c0d73310b6ead211a374cc0ab0d763c446b21ff Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 13 Jan 2022 14:14:21 -0600 Subject: [PATCH 0394/1722] Fix doc. --- .../Components/BasemapGallery/BasemapGallery.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 8c1a6cbce..2ddf24364 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -203,9 +203,7 @@ extension AlertItem { } /// Creates an alert item based on a spatial reference mismatch. - /// - Parameters: - /// - basemapSR: The basemap's spatial reference. - /// - geoModelSR: The geomodel's spatial reference. + /// - Parameter spatialReferenceMismatchError: The error associated with the mismatch. init(spatialReferenceMismatchError: SpatialReferenceMismatchError) { self.init( title: "Spatial reference mismatch.", From e30a5f46a09dcf1a32d911ee78be49cb2d714d8b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 14 Jan 2022 14:59:30 -0600 Subject: [PATCH 0395/1722] Apply suggestions from code review Co-authored-by: Nimesh Jarecha --- .../BasemapGallery/BasemapGallery.swift | 5 +++-- .../BasemapGallery/BasemapGalleryItem.swift | 20 +++++++------------ .../BasemapGalleryViewModel.swift | 20 +++++++++---------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 2ddf24364..105769da5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -88,7 +88,8 @@ public struct BasemapGallery: View { guard let error = error else { return } alertItem = AlertItem(spatialReferenceMismatchError: error) showErrorAlert = true - }) + } + ) .alert( alertItem?.title ?? "", isPresented: $showErrorAlert, @@ -202,7 +203,7 @@ extension AlertItem { ) } - /// Creates an alert item based on a spatial reference mismatch. + /// Creates an alert item based on a spatial reference mismatch error. /// - Parameter spatialReferenceMismatchError: The error associated with the mismatch. init(spatialReferenceMismatchError: SpatialReferenceMismatchError) { self.init( diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 06e3360e4..b5f6d8e82 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -84,7 +84,7 @@ public class BasemapGalleryItem: ObservableObject { @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown - /// The `SpatialReference` of `basemap`. This will be `nil` until the basemap's + /// The `SpatialReference` of ``basemap``. This will be `nil` until the basemap's /// baseLayers have been loaded in ``updateSpatialReferenceStatus()``. public private(set) var spatialReference: SpatialReference? = nil } @@ -135,8 +135,8 @@ extension BasemapGalleryItem: Equatable { } public extension BasemapGalleryItem { - /// Updats the `spatialReferenceStatus` by loading the first base layer of `basemap` - /// and determining if it matches `referenceSpatialReference`. + /// Updates the ``spatialReferenceStatus-swift.property`` by loading the first base layer of + /// the ``basemap`` and determining if it matches with the given `SpatialReference`. /// - Parameter referenceSpatialReference: The `SpatialReference` to match to. func updateSpatialReferenceStatus( _ referenceSpatialReference: SpatialReference? @@ -155,7 +155,7 @@ public extension BasemapGalleryItem { ) } - /// Updates the item's `spatialReference` and `spatialReferenceStatus` properties. + /// Updates the item's ``spatialReference`` and ``spatialReferenceStatus-swift.property`` properties. /// - Parameter referenceSpatialReference: The `SpatialReference` used to /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` /// `SpatialReference`. @@ -164,15 +164,9 @@ public extension BasemapGalleryItem { with referenceSpatialReference: SpatialReference? ) { spatialReference = basemap.baseLayers.first?.spatialReference - if referenceSpatialReference == nil { - spatialReferenceStatus = .unknown - } - else if spatialReference == referenceSpatialReference { - spatialReferenceStatus = .match - } - else { - spatialReferenceStatus = .noMatch - } + spatialReferenceStatus = referenceSpatialReference != nil ? + (spatialReference == referenceSpatialReference ? .match : .noMatch) + : .unknown isBasemapLoading = false } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index b82d60027..c9b1018a7 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -61,8 +61,8 @@ public class BasemapGalleryViewModel: ObservableObject { @Published public var fetchBasemapsError: Error? = nil - /// The error signifying the spatial reference of the GeoModel and that of a potential - /// current `BasemapGalleryItem` do not match. + /// The error signifying the spatial reference of the ``geoModel`` and the spatial reference of + /// the ``currentItem`` do not match. @Published public private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil @@ -119,9 +119,8 @@ public class BasemapGalleryViewModel: ObservableObject { fetchBasemaps(from: portal) } - /// This attempts to set `currentItem`. `currentItem` will be set if it's spatial reference - /// matches that of the `geoModel`. If the spatial references do not match, `currentItem` - /// will be unchanged. + /// This attempts to set the ``currentItem`` and it will be set only if it's spatial reference + /// matches with the ``geoModel``'s spatial reference. Otherwise ``currentItem`` will be unchanged. /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. public func updateCurrentItem( _ basemapGalleryItem: BasemapGalleryItem @@ -200,7 +199,7 @@ private extension BasemapGalleryViewModel { } } -/// An error describing a SpatialReference mismatch between a geomodel and a basemap. +/// An error describing a `SpatialReference` mismatch between a `GeoModel` and a `Basemap`. public struct SpatialReferenceMismatchError: Error { /// The basemap's spatial reference. public let basemapSR: SpatialReference? @@ -212,10 +211,11 @@ public struct SpatialReferenceMismatchError: Error { extension SpatialReferenceMismatchError: Equatable {} extension GeoModel { - /// The actual spatial reference of the geoModel. For `Map`s, this is the map's - /// `spatialReference`. For `Scene`s, if the `sceneViewTilingScheme` is - /// `.webMercator`, the `actualSpatialReference` is `.webMercator`, otherwise - /// it is the `spatialReference` of the scene. + /// The actual spatial reference of the `GeoModel`. + /// - Remark: + /// - For `Map`, it is map's `spatialReference`. + /// - For `Scene`, if the `sceneViewTilingScheme` is `webMercator`, then `actualSpatialReference` + /// is `webMercator`. Otherwise scene's `spatialReference`. var actualSpatialReference: SpatialReference? { (self as? ArcGIS.Scene)?.sceneViewTilingScheme == .webMercator ? SpatialReference.webMercator : From 6b243d35b7537c2c6c7fd0fb029f7bdda5d63a0c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 15 Jan 2022 14:55:45 -0600 Subject: [PATCH 0396/1722] Updates for new map and list refactoring. --- .../Examples/FloorFilterExampleView.swift | 40 ++++-- .../FloorFilter/FloorFilterViewModel.swift | 11 +- .../Components/FloorFilter/SiteSelector.swift | 123 ++++++++++++------ 3 files changed, 122 insertions(+), 52 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 552f90d3f..a292aa6d7 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -16,7 +16,8 @@ import ArcGISToolkit import ArcGIS struct FloorFilterExampleView: View { - private let map: Map + @State + private var map: Map @State private var viewpoint = Viewpoint( @@ -30,8 +31,30 @@ struct FloorFilterExampleView: View { init() { // Create the map from a portal item and assign to the mapView. let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") + let portalItem = PortalItem(portal: portal, itemId: "49520a67773842f1858602735ef538b5") //<= multiple sites/facilities +// let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") //<= single site/facility map = Map(item: portalItem) +// +// Task { +// do { +// let map2 = Map(item: portalItem) +// try await map2.load() +// print("map2.loadStatus = \(map2.loadStatus)") +// } +// catch { +// print("error: \(error)") +// } +// } + +// Task { +// // Create the map from a portal item and assign to the mapView. +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, itemId: "49520a67773842f1858602735ef538b5") //<= multiple sites/facilities +// // let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") //<= single site/facility +// await ArcGISURLSession.credentialStore.add(try await .indoors) +// map = Map(item: portalItem) +// } + } private let floorFilterPadding: CGFloat = 48 @@ -48,14 +71,15 @@ struct FloorFilterExampleView: View { } } .task { + var floorManager: FloorManager? do { try await map.load() - guard let floorManager = map.floorManager else { return } - floorFilterViewModel = FloorFilterViewModel( - floorManager: floorManager, - viewpoint: $viewpoint - ) - } catch { } + floorManager = map.floorManager + try await floorManager?.load() + } catch { + print("FloorManager.loadStatus = \(String(describing: floorManager?.loadStatus))") + print("load error: \(error)") + } } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 33cc39686..2ce76787d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -43,8 +43,15 @@ public class FloorFilterViewModel: ObservableObject { Task { do { try await floorManager.load() - isLoading = false - } catch { } + if sites.count == 1 { + // If we have only one site, select it. + selectedSite = sites.first + } +// viewpoint?.wrappedValue = floorManager + } catch { + print("error: \(error)") + } + isLoading = false } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift index 04b627353..2947702c5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift @@ -22,56 +22,95 @@ struct SiteSelector: View { showSiteSelector: Binding ) { self.viewModel = floorFilterViewModel - self.showSiteList = showSiteSelector + self.showSiteSelector = showSiteSelector } - + private let viewModel: FloorFilterViewModel /// Binding allowing the user to toggle the visibility of the results list. - private var showSiteList: Binding - - // TODO: refactor if-else below. + private var showSiteSelector: Binding + var body: some View { - if !viewModel.sites.isEmpty { - LazyVStack { - HStack { - Text("Select a site...") - .bold() - Spacer() - Button { - showSiteList.wrappedValue.toggle() - } label: { - Image(systemName: "xmark.circle") - } - } - Rectangle() - .frame(height:1) - .foregroundColor(.secondary) - ForEach(viewModel.sites) { site in - Text("\(site.name)") - } - } - .esriBorder() + if viewModel.sites.count > 1 && !(viewModel.selectedSite == nil) { + // Only show site list if there is more than one site + // and the user has not yet selected a site. + FloorFilterList( + "Select a site...", + sites: viewModel.sites, + showSiteSelector: showSiteSelector + ) } else { - LazyVStack { - HStack { - Text("Select a facility...") - .bold() - Spacer() - Button { - showSiteList.wrappedValue.toggle() - } label: { - Image(systemName: "xmark.circle") + FloorFilterList( + "Select a facility...", + facilities: viewModel.facilities, + showSiteSelector: showSiteSelector + ) + } + } + + struct FloorFilterList: View { + private let title: String + private let sites: [FloorSite]? + private let facilities: [FloorFacility]? + + /// Binding allowing the user to toggle the visibility of the results list. + private var showSiteSelector: Binding + + init( + _ title: String, + sites: [FloorSite], + showSiteSelector: Binding + ) { + self.title = title + self.sites = sites + facilities = [] + self.showSiteSelector = showSiteSelector + } + + init( + _ title: String, + facilities: [FloorFacility], + showSiteSelector: Binding + ) { + self.title = title + self.facilities = facilities + sites = nil + self.showSiteSelector = showSiteSelector + } + + var body: some View { +// NavigationView { +// TODO: figure this navigation stuff out or at least get to a demo-able point + LazyVStack { + HStack { + Text(title) + .bold() + Spacer() + Button { + showSiteSelector.wrappedValue.toggle() + } label: { + Image(systemName: "xmark.circle") + } + } + Rectangle() + .frame(height:1) + .foregroundColor(.secondary) + ForEach(sites ?? []) { site in +// NavigationLink( +// destination: EmptyView()) { + Text(site.name) +// } + } + ForEach(facilities ?? []) { facility in +// NavigationLink( +// destination: EmptyView()) { + Text(facility.name) +// } } } - Rectangle() - .frame(height:1) - .foregroundColor(.secondary) - ForEach(viewModel.facilities) { facility in - Text("\(facility.name)") - } +// .navigationBarTitle("Mountain Airport") + .esriBorder() } - .esriBorder() } - } +// } } From f74ab299d5ddb0f6b0d6fba979f8999348e71bce Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 15 Jan 2022 15:11:28 -0600 Subject: [PATCH 0397/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift Co-authored-by: Philip Ridgeway --- .../BasemapGallery/BasemapGalleryViewModel.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index c9b1018a7..ac2592ee8 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -217,8 +217,11 @@ extension GeoModel { /// - For `Scene`, if the `sceneViewTilingScheme` is `webMercator`, then `actualSpatialReference` /// is `webMercator`. Otherwise scene's `spatialReference`. var actualSpatialReference: SpatialReference? { - (self as? ArcGIS.Scene)?.sceneViewTilingScheme == .webMercator ? - SpatialReference.webMercator : - spatialReference + if let scene = self as? ArcGIS.Scene, + scene.sceneViewTilingScheme == .webMercator { + return .webMercator + } else { + return spatialReference + } } } From c9234c95fc3c26e6b0f180e0ea718f9486f64c65 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 15 Jan 2022 15:25:52 -0600 Subject: [PATCH 0398/1722] PR review changes. --- .../BasemapGallery/BasemapGallery.swift | 10 +++- .../BasemapGallery/BasemapGalleryCell.swift | 15 +++--- .../BasemapGallery/BasemapGalleryItem.swift | 11 ++--- .../BasemapGalleryViewModel.swift | 46 ++++++++++--------- 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 105769da5..731fb6564 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -162,7 +162,7 @@ private extension BasemapGallery { alertItem = AlertItem(loadBasemapError: loadError) showErrorAlert = true } else { - viewModel.updateCurrentItem(item) + viewModel.setCurrentItem(item) } } } @@ -206,9 +206,15 @@ extension AlertItem { /// Creates an alert item based on a spatial reference mismatch error. /// - Parameter spatialReferenceMismatchError: The error associated with the mismatch. init(spatialReferenceMismatchError: SpatialReferenceMismatchError) { + let message: String + if spatialReferenceMismatchError.basemapSpatialReference == nil { + message = "The basemap does not have a spatial reference." + } else { + message = "The spatial reference of the basemap: \(spatialReferenceMismatchError.basemapSpatialReference?.description ?? "") does not match that of the geomodel: \(spatialReferenceMismatchError.geoModelSpatialReference?.description ?? "")." + } self.init( title: "Spatial reference mismatch.", - message: "The spatial reference of the basemap: \(spatialReferenceMismatchError.basemapSR?.description ?? "") does not match that of the geomodel: \(spatialReferenceMismatchError.geoModelSR?.description ?? "")." + message: message ) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index d69046df2..d8a19ce80 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -44,13 +44,9 @@ struct BasemapGalleryCell: View { // Display an image representing either a load basemap error // or a spatial reference mismatch error. if item.loadBasemapError != nil { - Image(systemName: "minus.circle.fill") - .font(.title) - .foregroundColor(.red) + makeErrorImage(systemName: "minus") } else if item.spatialReferenceStatus == .noMatch { - Image(systemName: "x.circle.fill") - .font(.title) - .foregroundColor(.red) + makeErrorImage(systemName: "x") } // Display a progress view if the item is loading. @@ -69,4 +65,11 @@ struct BasemapGalleryCell: View { } }).disabled(item.isBasemapLoading) } + + private func makeErrorImage(systemName: String) -> some View { + Image(systemName: systemName) + .symbolVariant(.circle.fill) + .font(.title) + .foregroundColor(.red) + } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index b5f6d8e82..59d6dd3cd 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -27,7 +27,7 @@ public class BasemapGalleryItem: ObservableObject { /// The basemap's spatial reference does not match the reference spatial reference. case noMatch } - + /// Creates a `BasemapGalleryItem`. /// - Parameters: /// - basemap: The `Basemap` represented by the item. @@ -138,19 +138,18 @@ public extension BasemapGalleryItem { /// Updates the ``spatialReferenceStatus-swift.property`` by loading the first base layer of /// the ``basemap`` and determining if it matches with the given `SpatialReference`. /// - Parameter referenceSpatialReference: The `SpatialReference` to match to. + @MainActor func updateSpatialReferenceStatus( _ referenceSpatialReference: SpatialReference? ) async throws { guard basemap.loadStatus == .loaded else { return } if spatialReference == nil { - await MainActor.run { - isBasemapLoading = true - } + isBasemapLoading = true try await basemap.baseLayers.first?.load() } - await finalizeUpdateSpatialReferenceStatus( + finalizeUpdateSpatialReferenceStatus( with: referenceSpatialReference ) } @@ -160,7 +159,7 @@ public extension BasemapGalleryItem { /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` /// `SpatialReference`. @MainActor - func finalizeUpdateSpatialReferenceStatus( + private func finalizeUpdateSpatialReferenceStatus( with referenceSpatialReference: SpatialReference? ) { spatialReference = basemap.baseLayers.first?.spatialReference diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index c9b1018a7..52a9d82da 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -119,36 +119,40 @@ public class BasemapGalleryViewModel: ObservableObject { fetchBasemaps(from: portal) } - /// This attempts to set the ``currentItem`` and it will be set only if it's spatial reference - /// matches with the ``geoModel``'s spatial reference. Otherwise ``currentItem`` will be unchanged. + /// This attempts to set ``currentItem``; it will be set only if it's spatial reference + /// matches with the ``geoModel``'s spatial reference. Otherwise ``currentItem`` + /// will be unchanged. /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. - public func updateCurrentItem( + @MainActor + func setCurrentItem( _ basemapGalleryItem: BasemapGalleryItem ) { - Task { - // Ensure the geoModel is loaded. - try await geoModel?.load() - - // Reset the mismatch error. - spatialReferenceMismatchError = nil - - // Update the basemap gallery item's `spatialReferenceStatus`. - try await basemapGalleryItem.updateSpatialReferenceStatus( - geoModel?.actualSpatialReference - ) - - // Update @State on the main thread. - await MainActor.run { + // Reset the mismatch error. + spatialReferenceMismatchError = nil + + if let geoModel = geoModel { + Task { + // Ensure the geoModel is loaded. + try await geoModel.load() + + // Update the basemap gallery item's `spatialReferenceStatus`. + try await basemapGalleryItem.updateSpatialReferenceStatus( + geoModel.actualSpatialReference + ) + switch basemapGalleryItem.spatialReferenceStatus { case .match, .unknown: currentItem = basemapGalleryItem case .noMatch: spatialReferenceMismatchError = SpatialReferenceMismatchError( - basemapSR: basemapGalleryItem.spatialReference, - geoModelSR: geoModel?.actualSpatialReference + basemapSpatialReference: basemapGalleryItem.spatialReference, + geoModelSpatialReference: geoModel.actualSpatialReference ) } } + } else { + // No geoModel so no SR checking possible; just set `currentItem`. + currentItem = basemapGalleryItem } } } @@ -202,10 +206,10 @@ private extension BasemapGalleryViewModel { /// An error describing a `SpatialReference` mismatch between a `GeoModel` and a `Basemap`. public struct SpatialReferenceMismatchError: Error { /// The basemap's spatial reference. - public let basemapSR: SpatialReference? + public let basemapSpatialReference: SpatialReference? /// The geomodel's spatial reference. - public let geoModelSR: SpatialReference? + public let geoModelSpatialReference: SpatialReference? } extension SpatialReferenceMismatchError: Equatable {} From 71058afe8a8c3640425f52c31846fd9ba2c88453 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 18 Jan 2022 10:41:15 -0600 Subject: [PATCH 0399/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- .../BasemapGallery/BasemapGalleryItem.swift | 21 ++++++++++--------- .../BasemapGalleryViewModel.swift | 16 +++++++------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 59d6dd3cd..6668048de 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -17,7 +17,7 @@ import ArcGIS /// The `BasemapGalleryItem` encompasses an element in a `BasemapGallery`. public class BasemapGalleryItem: ObservableObject { - /// Indicates the status of the basemap's spatial reference in relation to a reference spatial reference. + /// The status of a basemap's spatial reference in relation to a reference spatial reference. public enum SpatialReferenceStatus { /// The basemap's spatial reference status is unknown, either because the basemap's /// base layers haven't been loaded yet or the status has yet to be updated. @@ -79,13 +79,14 @@ public class BasemapGalleryItem: ObservableObject { @Published private(set) var isBasemapLoading = true - /// The `SpatialReferenceStatus` of the item. This is set via a call to - /// ``updateSpatialReferenceStatus()``. + /// The spatial reference status of the item. This is set via a call to + /// ``updateSpatialReferenceStatus(_:)``. @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown - /// The `SpatialReference` of ``basemap``. This will be `nil` until the basemap's - /// baseLayers have been loaded in ``updateSpatialReferenceStatus()``. + /// The spatial reference of ``basemap``. This will be `nil` until the + /// basemap's base layers have been loaded by + /// ``updateSpatialReferenceStatus(_:)``. public private(set) var spatialReference: SpatialReference? = nil } @@ -136,8 +137,8 @@ extension BasemapGalleryItem: Equatable { public extension BasemapGalleryItem { /// Updates the ``spatialReferenceStatus-swift.property`` by loading the first base layer of - /// the ``basemap`` and determining if it matches with the given `SpatialReference`. - /// - Parameter referenceSpatialReference: The `SpatialReference` to match to. + /// ``basemap`` and determining if it matches with the given spatial reference. + /// - Parameter referenceSpatialReference: The spatial reference to match to. @MainActor func updateSpatialReferenceStatus( _ referenceSpatialReference: SpatialReference? @@ -155,9 +156,9 @@ public extension BasemapGalleryItem { } /// Updates the item's ``spatialReference`` and ``spatialReferenceStatus-swift.property`` properties. - /// - Parameter referenceSpatialReference: The `SpatialReference` used to - /// compare to the `basemap`'s `SpatialReference`, represented by the first base layer's` - /// `SpatialReference`. + /// - Parameter referenceSpatialReference: The spatial reference used to + /// compare to the `basemap`'s spatial reference, represented by the first base layer's + /// spatial reference. @MainActor private func finalizeUpdateSpatialReferenceStatus( with referenceSpatialReference: SpatialReference? diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 44f8d9efa..004622de8 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -61,8 +61,8 @@ public class BasemapGalleryViewModel: ObservableObject { @Published public var fetchBasemapsError: Error? = nil - /// The error signifying the spatial reference of the ``geoModel`` and the spatial reference of - /// the ``currentItem`` do not match. + /// The error signifying the spatial reference of ``geoModel`` and the spatial reference of + /// ``currentItem`` do not match. @Published public private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil @@ -119,10 +119,10 @@ public class BasemapGalleryViewModel: ObservableObject { fetchBasemaps(from: portal) } - /// This attempts to set ``currentItem``; it will be set only if it's spatial reference - /// matches with the ``geoModel``'s spatial reference. Otherwise ``currentItem`` + /// This attempts to set ``currentItem``; it will be set only if its spatial reference + /// matches with ``geoModel``'s spatial reference. Otherwise ``currentItem`` /// will be unchanged. - /// - Parameter basemapGalleryItem: The new, potential, `BasemapGalleryItem`. + /// - Parameter basemapGalleryItem: The new, potential, basemap gallery item. @MainActor func setCurrentItem( _ basemapGalleryItem: BasemapGalleryItem @@ -203,19 +203,19 @@ private extension BasemapGalleryViewModel { } } -/// An error describing a `SpatialReference` mismatch between a `GeoModel` and a `Basemap`. +/// An error describing a spatial reference mismatch between a geo model and a basemap. public struct SpatialReferenceMismatchError: Error { /// The basemap's spatial reference. public let basemapSpatialReference: SpatialReference? - /// The geomodel's spatial reference. + /// The geo model's spatial reference. public let geoModelSpatialReference: SpatialReference? } extension SpatialReferenceMismatchError: Equatable {} extension GeoModel { - /// The actual spatial reference of the `GeoModel`. + /// The actual spatial reference of the geo model. /// - Remark: /// - For `Map`, it is map's `spatialReference`. /// - For `Scene`, if the `sceneViewTilingScheme` is `webMercator`, then `actualSpatialReference` From 8bfc39a509bb33553bd6b053941a91d315d7e12d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 18 Jan 2022 10:57:47 -0600 Subject: [PATCH 0400/1722] PR review comments. --- .../BasemapGallery/BasemapGallery.swift | 11 ++++++++--- .../BasemapGallery/BasemapGalleryItem.swift | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 731fb6564..7398e763e 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -207,11 +207,16 @@ extension AlertItem { /// - Parameter spatialReferenceMismatchError: The error associated with the mismatch. init(spatialReferenceMismatchError: SpatialReferenceMismatchError) { let message: String - if spatialReferenceMismatchError.basemapSpatialReference == nil { + + switch (spatialReferenceMismatchError.basemapSpatialReference, spatialReferenceMismatchError.geoModelSpatialReference) { + case (.some(let basemapSpatialReference), .some(let geoModelSpatialReference)): + message = "The spatial reference of the basemap: \(basemapSpatialReference.description) does not match that of the geomodel: \(geoModelSpatialReference.description)." + case (_, .none): + message = "The geo model does not have a spatial reference." + case (.none, _): message = "The basemap does not have a spatial reference." - } else { - message = "The spatial reference of the basemap: \(spatialReferenceMismatchError.basemapSpatialReference?.description ?? "") does not match that of the geomodel: \(spatialReferenceMismatchError.geoModelSpatialReference?.description ?? "")." } + self.init( title: "Spatial reference mismatch.", message: message diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 6668048de..38179640c 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -157,16 +157,24 @@ public extension BasemapGalleryItem { /// Updates the item's ``spatialReference`` and ``spatialReferenceStatus-swift.property`` properties. /// - Parameter referenceSpatialReference: The spatial reference used to - /// compare to the `basemap`'s spatial reference, represented by the first base layer's + /// compare to `basemap`'s spatial reference, represented by the first base layer's /// spatial reference. @MainActor private func finalizeUpdateSpatialReferenceStatus( with referenceSpatialReference: SpatialReference? ) { spatialReference = basemap.baseLayers.first?.spatialReference - spatialReferenceStatus = referenceSpatialReference != nil ? - (spatialReference == referenceSpatialReference ? .match : .noMatch) - : .unknown + + if referenceSpatialReference != nil { + spatialReferenceStatus = .unknown + } + else if spatialReference == referenceSpatialReference { + spatialReferenceStatus = .match + } + else { + spatialReferenceStatus = .noMatch + } + isBasemapLoading = false } } From b86e4ba432bd203b0bbc10d96a5f707c1ea3da2a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 18 Jan 2022 13:01:04 -0600 Subject: [PATCH 0401/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift Co-authored-by: Philip Ridgeway --- .../Components/BasemapGallery/BasemapGalleryItem.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 38179640c..0ab746f12 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -167,8 +167,7 @@ public extension BasemapGalleryItem { if referenceSpatialReference != nil { spatialReferenceStatus = .unknown - } - else if spatialReference == referenceSpatialReference { + } else if spatialReference == referenceSpatialReference { spatialReferenceStatus = .match } else { From e408ddaf13e554d59ab3098392382338aaf5128a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 18 Jan 2022 13:01:09 -0600 Subject: [PATCH 0402/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift Co-authored-by: Philip Ridgeway --- .../Components/BasemapGallery/BasemapGalleryItem.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 0ab746f12..fdc9004e1 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -169,8 +169,7 @@ public extension BasemapGalleryItem { spatialReferenceStatus = .unknown } else if spatialReference == referenceSpatialReference { spatialReferenceStatus = .match - } - else { + } else { spatialReferenceStatus = .noMatch } From 400617d73fef13569e2af793b7afb9cd6653bc9a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 20 Jan 2022 12:05:57 -0600 Subject: [PATCH 0403/1722] Fix loading of floormanager --- .../Examples/FloorFilterExampleView.swift | 45 ++++++++----------- .../FloorFilter/FloorFilterViewModel.swift | 19 ++++++-- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index a292aa6d7..d8b90aabe 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -30,31 +30,20 @@ struct FloorFilterExampleView: View { init() { // Create the map from a portal item and assign to the mapView. + + // Multiple sites/facilities: Esri IST map with all buildings. +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, itemId: "49520a67773842f1858602735ef538b5") + + // Redlands Campus map. +// let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, itemId: "7687805bd42549f5ba41237443d0c60a") //<= another multiple sites/facilities + + // Single site (ESRI Redlands Main) and facility (Building L). let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, itemId: "49520a67773842f1858602735ef538b5") //<= multiple sites/facilities -// let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") //<= single site/facility + let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") + map = Map(item: portalItem) -// -// Task { -// do { -// let map2 = Map(item: portalItem) -// try await map2.load() -// print("map2.loadStatus = \(map2.loadStatus)") -// } -// catch { -// print("error: \(error)") -// } -// } - -// Task { -// // Create the map from a portal item and assign to the mapView. -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, itemId: "49520a67773842f1858602735ef538b5") //<= multiple sites/facilities -// // let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") //<= single site/facility -// await ArcGISURLSession.credentialStore.add(try await .indoors) -// map = Map(item: portalItem) -// } - } private let floorFilterPadding: CGFloat = 48 @@ -71,13 +60,15 @@ struct FloorFilterExampleView: View { } } .task { - var floorManager: FloorManager? do { try await map.load() - floorManager = map.floorManager - try await floorManager?.load() + if let floorManager = map.floorManager { + floorFilterViewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: $viewpoint + ) + } } catch { - print("FloorManager.loadStatus = \(String(describing: floorManager?.loadStatus))") print("load error: \(error)") } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 2ce76787d..841ec96e9 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -47,7 +47,6 @@ public class FloorFilterViewModel: ObservableObject { // If we have only one site, select it. selectedSite = sites.first } -// viewpoint?.wrappedValue = floorManager } catch { print("error: \(error)") } @@ -58,6 +57,7 @@ public class FloorFilterViewModel: ObservableObject { /// The `Viewpoint` used to pan/zoom to the floor level. If `nil`, there will be no zooming. var viewpoint: Binding? = nil + /// The `FloorManager` containing the site, floor, and level information. var floorManager: FloorManager public var sites: [FloorSite] { @@ -91,8 +91,21 @@ public class FloorFilterViewModel: ObservableObject { } /// The site, facility, and level that are selected by the user - public var selectedSite: FloorSite? = nil - public var selectedFacility: FloorFacility? = nil + @Published + public var selectedSite: FloorSite? = nil { + didSet { + zoomToSite() + } + } + + @Published + public var selectedFacility: FloorFacility? = nil { + didSet { + zoomToFacility() + } + } + + @Published public var selectedLevel: FloorLevel? = nil /// The default vertical order is 0 according to Runtime 100.12 update for FloorManager From cd06253e968dd1f6244f3e18428a52ed4d034c62 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 20 Jan 2022 13:33:47 -0600 Subject: [PATCH 0404/1722] Add tests. --- .../BasemapGallery/BasemapGalleryItem.swift | 2 +- .../BasemapGalleryItemTests.swift | 156 +++++++++++ .../BasemapGalleryViewModelTests.swift | 257 ++++++++++++++++++ 3 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift create mode 100644 Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index fdc9004e1..2caf5be92 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -165,7 +165,7 @@ public extension BasemapGalleryItem { ) { spatialReference = basemap.baseLayers.first?.spatialReference - if referenceSpatialReference != nil { + if referenceSpatialReference == nil { spatialReferenceStatus = .unknown } else if spatialReference == referenceSpatialReference { spatialReferenceStatus = .match diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift new file mode 100644 index 000000000..df97793c9 --- /dev/null +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -0,0 +1,156 @@ +// 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 Foundation + +import XCTest +import ArcGIS +@testable import ArcGISToolkit +import SwiftUI +import Combine + +// +// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md +// +// Note: the iOS implementation uses the MVVM approach and SwiftUI. This +// required a bit more properties/logic in the `BasemapGalleryItem` (such +// as the `loadBasemapError` and `spatialReferenceStatus` properties than +// the `BasemapGallery` design specifies. Tests not present in the +// test design have been added to accomodate those differences. +@MainActor +class BasemapGalleryItemTests: XCTestCase { + func testInit() async throws { + let basemap = Basemap.lightGrayCanvas() + var item = BasemapGalleryItem(basemap: basemap) + + var isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + var loading = try XCTUnwrap(isBasemapLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertTrue(item.basemap === basemap) + XCTAssertEqual(item.name, "Light Gray Canvas") + XCTAssertNil(item.description) + XCTAssertNotNil(item.thumbnail) + XCTAssertNil(item.loadBasemapError) + + // Test with overrides. + let thumbnail = UIImage(systemName: "magnifyingglass") + XCTAssertNotNil(thumbnail) + item = BasemapGalleryItem( + basemap: basemap, + name: "My Basemap", + description: "Basemap description", + thumbnail: thumbnail + ) + + isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + loading = try XCTUnwrap(isBasemapLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertEqual(item.name, "My Basemap") + XCTAssertEqual(item.description, "Basemap description") + XCTAssertEqual(item.thumbnail, thumbnail) + XCTAssertNil(item.loadBasemapError) + + // Test with portal item. + item = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ) + ) + + isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + loading = try XCTUnwrap(isBasemapLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertEqual(item.name, "OpenStreetMap Blueprint") + XCTAssertEqual(item.description, "

This web map presents a vector basemap of OpenStreetMap (OSM) data hosted by Esri. Esri created this vector tile basemap from the Daylight map distribution of OSM data, which is supported by Facebook and supplemented with additional data from Microsoft. It presents the map in a cartographic style is like a blueprint technical drawing. The OSM Daylight map will be updated every month with the latest version of OSM Daylight data. 

OpenStreetMap is an open collaborative project to create a free editable map of the world. Volunteers gather location data using GPS, local knowledge, and other free sources of information and upload it. The resulting free map can be viewed and downloaded from the OpenStreetMap site: www.OpenStreetMap.org. Esri is a supporter of the OSM project and is excited to make this enhanced vector basemap available to the ArcGIS user and developer communities.
") + XCTAssertNotNil(item.thumbnail) + XCTAssertNil(item.loadBasemapError) + } + + func testLoadBasemapError() async throws { + // Create item with bad portal item URL. + let item = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=4a3922d6d15f405d8c2b7a448a7fbad2")! + )! + ) + ) + + let isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + let loading = try XCTUnwrap(isBasemapLoading) + XCTAssertFalse(loading, "Item is not loading.") + XCTAssertNotNil(item.loadBasemapError) + } + + func testSpatialReferenceAndStatus() async throws { + let basemap = Basemap.lightGrayCanvas() + let item = BasemapGalleryItem(basemap: basemap) + + var isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + var loading = try XCTUnwrap(isBasemapLoading) + XCTAssertFalse(loading, "Item is not loading.") + + XCTAssertEqual(item.spatialReferenceStatus, .unknown) + + // Test if basemap matches. Use a Task here so we can catch and test + // the change to `item.isBasemapLoading` during the loading of the base layers. + Task { + try await item.updateSpatialReferenceStatus(SpatialReference.webMercator) + } + + // Check if `isBasemapLoading` is set to true during first call + // to updateSpatialReferenceStatus. + isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + loading = try XCTUnwrap(isBasemapLoading) + XCTAssertTrue(loading, "Item base layers are loading.") + + var srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).dropFirst().first + var status = try XCTUnwrap(srStatus) + XCTAssertEqual(status, .match) + XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) + XCTAssertFalse(item.isBasemapLoading) + + // Test if basemap doesn't match. + try await item.updateSpatialReferenceStatus(SpatialReference.wgs84) + + // Since we've already called `updateSpatialReferenceStatus` once, + // we should no longer internally need to load the baselayers. + isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).first + loading = try XCTUnwrap(isBasemapLoading) + XCTAssertFalse(loading, "Item base layers are not loading.") + + srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).first + status = try XCTUnwrap(srStatus) + XCTAssertEqual(status, .noMatch) + XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) + + // Test WGS84 basemap. + let otherItem = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ) + ) + + _ = try await otherItem.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + + try await otherItem.updateSpatialReferenceStatus(SpatialReference.wgs84) + srStatus = try await otherItem.$spatialReferenceStatus.compactMap({ $0 }).first + status = try XCTUnwrap(srStatus) + XCTAssertEqual(status, .match) + XCTAssertEqual(otherItem.spatialReference, SpatialReference.wgs84) + } +} diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift new file mode 100644 index 000000000..0a5bc9337 --- /dev/null +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -0,0 +1,257 @@ +// 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 Foundation + +import XCTest +import ArcGIS +@testable import ArcGISToolkit +import SwiftUI +import Combine + +// +// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md +// +// Note: the iOS implementation uses the MVVM approach and SwiftUI. This +// required a bit more properties/logic in the `BasemapGalleryViewModel` (such +// as `geoModel.actualSpatialReference`) than the `BasemapGallery` design +// specifies. Tests not present in the test design have been added to +// accomodate those differences. +@MainActor +class BasemapGalleryViewModelTests: XCTestCase { + let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ) + ), + BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=f33a34de3a294590ab48f246e99958c9")! + )! + ) + ) + ] + + /// Test the various constructor methods. + func testInit() async throws { + // Note: this is a good candidate for mocking portal data. + // This would allow the test to check for a specific number of items. + + // + // GeoModel. + // + var geoModel = Map(basemap: Basemap.lightGrayCanvas()) + let geoModelViewModel = BasemapGalleryViewModel(geoModel: geoModel) + XCTAssertTrue(geoModel === geoModelViewModel.geoModel) + + // With no portal, `basemapGalleryItems` are fetched from AGOL's + // list of developer basemaps. + var items = try await geoModelViewModel.$items.compactMap({ $0 }).dropFirst().first + var basemapGalleryItems = try XCTUnwrap(items) + XCTAssertFalse(basemapGalleryItems.isEmpty) + + // GeoModel should be loaded. + XCTAssertEqual(geoModel.loadStatus, .loaded) + XCTAssertTrue(geoModel.basemap === geoModelViewModel.currentItem?.basemap) + + // Save the array of developer basemap items from AGOL. + let developerBasemapItems = basemapGalleryItems + + // + // Portal. + // + geoModel = Map(basemap: Basemap.lightGrayCanvas()) + let portal = Portal.arcGISOnline(isLoginRequired: false) + let portalViewModel = BasemapGalleryViewModel(geoModel, portal: portal) + + XCTAssertTrue(geoModel === portalViewModel.geoModel) + XCTAssertTrue(portal === portalViewModel.portal) + + // With a portal, `basemapGalleryItems` are fetched from either the + // portal's vector basemaps or regular basemaps. + items = try await portalViewModel.$items.compactMap({ $0 }).dropFirst().first + basemapGalleryItems = try XCTUnwrap(items) + XCTAssertFalse(basemapGalleryItems.isEmpty) + + XCTAssertEqual(geoModel.loadStatus, .loaded) + XCTAssertTrue(geoModel.basemap === portalViewModel.currentItem?.basemap) + + // Sort the developer items from the "GeoModel" test above and the + // items from the portal and make sure they are not equal. + let sortedItems = basemapGalleryItems.sorted { + guard let name0 = $0.name, let name1 = $1.name else { return false } + return name0 < name1 + } + let sortedDeveloperItems = developerBasemapItems.sorted { + guard let name0 = $0.name, let name1 = $1.name else { return false } + return name0 < name1 + } + XCTAssertNotEqual(sortedItems, sortedDeveloperItems) + + // + // BasemapGalleryItems. No basemaps are fetched from a portal. + // + geoModel = Map(basemap: Basemap.lightGrayCanvas()) + let itemsViewModel = BasemapGalleryViewModel( + geoModel: geoModel, + items: defaultBasemapGalleryItems + ) + + // The item counts should match. + XCTAssertEqual( + itemsViewModel.items.count, + defaultBasemapGalleryItems.count + ) + } + + /// Test the `GeoModel.actualSpatialReference` extension property. + func testGeoModelActualSpatialReference() async throws { + // Map with .webMercator basemap. + let geoModel = Map(basemap: Basemap.lightGrayCanvas()) + try await geoModel.load() + XCTAssertEqual(geoModel.actualSpatialReference, .webMercator) + + // Map with .wgs84 basemap. + let geoModel2 = Map( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ) + ) + + try await geoModel2.load() + XCTAssertEqual(geoModel2.actualSpatialReference, .wgs84) + + // Test with Scene that has a tiling scheme of .webMercator + let geoModel3 = Scene( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ) + ) + + try await geoModel3.load() + XCTAssertEqual(geoModel3.actualSpatialReference, .webMercator) + + // Test with Scene that has a tiling scheme of .wgs84 + let geoModel4 = Scene( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ) + ) + try await geoModel4.load() + XCTAssertEqual(geoModel4.actualSpatialReference, .wgs84) + } + + /// Test the `currentItem` property including valid and invalid basemaps. + func testcurrentItem() async throws { + let basemap = Basemap.streets() + let geoModel = Map(basemap: basemap) + + let viewModel = BasemapGalleryViewModel(geoModel: geoModel) + + // Verify current item is equal to map's basemap. + var item = try await viewModel.$currentItem.compactMap({ $0 }).first + var currentItem = try XCTUnwrap(item) + XCTAssertTrue(currentItem.basemap === basemap) + + // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). + let validItem = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! + )! + ) + ) + + // Wait until it loads. + _ = try await validItem.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + + // Update the item on the model. + viewModel.setCurrentItem(validItem) + + // Wait until the `currentItem` is updated. + item = try await viewModel.$currentItem.compactMap({ $0 }).dropFirst().first + currentItem = try XCTUnwrap(item) + + // Items should equal, meaning the `validItem` was set properly. + XCTAssertEqual(currentItem, validItem) + + // Test WGS84 basemap item (Imagery (WGS84)). This item is in a + // different spatial reference than the geoModel, so it should + // not be set as the current item. + let invalidItem = BasemapGalleryItem( + basemap: Basemap( + item: PortalItem( + url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=52bdc7ab7fb044d98add148764eaa30a")! + )! + ) + ) + + // Wait until it loads. + _ = try await invalidItem.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + + // Update the item on the model. + viewModel.setCurrentItem(invalidItem) + + // The update will fail, so wait until the + // `$spatialReferenceMismatchError` is updated. + let error = try await viewModel.$spatialReferenceMismatchError.compactMap({ $0 }).first + XCTAssertNotNil(error, "Error is not nil.") + + // Make sure the current item is still equal to the valid item. + XCTAssertEqual(currentItem, validItem) + } + + /// Test setting the portal after the model has been created. + func testUpdatePortal() async throws { + // Create a model with a default list of items. + let viewModel = BasemapGalleryViewModel( + items: defaultBasemapGalleryItems + ) + + var items = try await viewModel.$items.compactMap({ $0 }).first + var basemapGalleryItems = try XCTUnwrap(items) + + // There are exactly two default items. + XCTAssertEqual(basemapGalleryItems.count, 2) + + // Set a portal on the model. This should clear out the existing + // array of items and load basemaps from the portal. + viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) + + // The items should be cleared prior to loading those from the portal. + items = try await viewModel.$items.compactMap({ $0 }).first + basemapGalleryItems = try XCTUnwrap(items) + XCTAssertTrue(basemapGalleryItems.isEmpty) + + // Wait for the portal basemaps to load. + items = try await viewModel.$items.compactMap({ $0 }).dropFirst().first + basemapGalleryItems = try XCTUnwrap(items) + + // There should be no default items in the basemap gallery. + let foundDefaultItem = basemapGalleryItems.first(where: { + $0 == defaultBasemapGalleryItems[0] || + $0 == defaultBasemapGalleryItems[1] + }) + XCTAssertNil(foundDefaultItem) + } +} From 85c80ad3140cd0f86584771859c433e012969f3e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 21 Jan 2022 10:02:35 -0600 Subject: [PATCH 0405/1722] Update documentation, remove dead code, general cleanup --- .../Components/FloorFilter/FloorFilter.swift | 16 +++- .../FloorFilter/FloorFilterViewModel.swift | 87 ++++--------------- .../Components/FloorFilter/SiteSelector.swift | 68 +++++++-------- 3 files changed, 64 insertions(+), 107 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 37cd06b9a..c3748c074 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -14,16 +14,21 @@ 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 `GeoView` +/// to a site, a facility (building) in the site, or a floor in the facility. public struct FloorFilter: View { /// Creates a `FloorFilter` - /// - Parameter content: The view shown in the floating panel. + /// - Parameter floorFilterViewModel: The view model used by the `BasemapGallery`. public init(_ floorFilterViewModel: FloorFilterViewModel) { self.viewModel = floorFilterViewModel } + /// The view model used by the `FloorFilter`. @ObservedObject private(set) var viewModel: FloorFilterViewModel + /// Allows the user to toggle the visibility of the site selector. @State private var showSiteSelector: Bool = false @@ -35,9 +40,6 @@ public struct FloorFilter: View { .esriBorder() } else { VStack { - // - // show levels and close button here - // Button { showSiteSelector.toggle() } label: { @@ -56,3 +58,9 @@ public struct FloorFilter: View { } } } + +extension UIImage { + static var site: UIImage { + return UIImage(named: "Site", in: Bundle.module, with: nil)! + } +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 841ec96e9..90e830ac3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -14,26 +14,13 @@ import SwiftUI import ArcGIS -/* - - // method that gets/sets the facility in the floor filter using the facility ID - string selectedFacilityId() - void setSelectedFacilityId(string facilityId) - - // method that gets/sets the level in the floor filter using the level ID - string selectedLevelId() - void setSelectedtLevelId(String levelId) - - // method that gets/sets the site in the floor filter using the site ID - string selectedSiteId() - void setSelectedSiteId(String siteId) - - */ - @MainActor -/// View Model class that contains the Data Model of the Floor Filter -/// Also contains the business logic to filter and change the map extent based on selected site/level/facility +/// Manages the state for a `FloorFilter`. public class FloorFilterViewModel: ObservableObject { + /// Creates a `FloorFilterViewModel`. + /// - Parameters: + /// - floorManager: A floor manager. + /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( floorManager: FloorManager, viewpoint: Binding? = nil @@ -54,12 +41,14 @@ public class FloorFilterViewModel: ObservableObject { } } - /// The `Viewpoint` used to pan/zoom to the floor level. If `nil`, there will be no zooming. + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// If `nil`, there will be automatic no pan/zoom operations. var viewpoint: Binding? = nil /// The `FloorManager` containing the site, floor, and level information. var floorManager: FloorManager + /// The floor manager sites. public var sites: [FloorSite] { floorManager.sites } @@ -68,18 +57,18 @@ public class FloorFilterViewModel: ObservableObject { @Published public var isLoading = true - /// Facilities in the selected site - /// If no site is selected then the list is empty - /// If the sites data does not exist in the map, then use all the facilities in the map + /// Facilities in the selected site. If no site is selected then the list is empty. + /// If the sites list is empty, all facilities will be returned. public var facilities: [FloorFacility] { sites.isEmpty ? floorManager.facilities : floorManager.facilities.filter { $0.site == selectedSite } } - /// Levels that are visible in the expanded Floor Filter levels table view - /// Reverse the order of the levels to make it in ascending order - public var visibleLevelsInExpandedList: [FloorLevel] { + /// Levels in the selected facility. If no facility is selected then the list is empty. + /// If the facilities list is empty, all levels will be returned. + /// The levels are returned in ascending order. + public var levels: [FloorLevel] { facilities.isEmpty ? floorManager.levels : floorManager.levels.filter { $0.facility == selectedFacility }.reversed() @@ -90,7 +79,7 @@ public class FloorFilterViewModel: ObservableObject { floorManager.levels } - /// The site, facility, and level that are selected by the user + /// The selected site. @Published public var selectedSite: FloorSite? = nil { didSet { @@ -98,6 +87,7 @@ public class FloorFilterViewModel: ObservableObject { } } + /// The selected facility. @Published public var selectedFacility: FloorFacility? = nil { didSet { @@ -105,46 +95,11 @@ public class FloorFilterViewModel: ObservableObject { } } + /// The selected level. @Published public var selectedLevel: FloorLevel? = nil - /// The default vertical order is 0 according to Runtime 100.12 update for FloorManager - public let defaultVerticalOrder: Int32 = 0 - - public func reset() { - selectedSite = nil - selectedFacility = nil - selectedLevel = nil - } - - public func getDefaultLevelForFacility(facility: FloorFacility?) -> FloorLevel? { - let candidateLevels = allLevels.filter {$0.facility == facility} - return candidateLevels.first {$0.verticalOrder == 0} ?? getLowestLevel(levels: candidateLevels) - } - - /// Returns the FloorLevel with the lowest verticalOrder. - private func getLowestLevel(levels: [FloorLevel]) -> FloorLevel? { - var lowestLevel: FloorLevel? = nil - allLevels.forEach { - if ($0.verticalOrder != Int.min && $0.verticalOrder != Int.max) { - let lowestVerticalOrder = lowestLevel?.verticalOrder - if (lowestVerticalOrder == nil || lowestVerticalOrder ?? defaultVerticalOrder > $0.verticalOrder) { - lowestLevel = $0 - } - } - } - return lowestLevel - } - - /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level - public func filterMapToSelectedLevel() { - guard let selectedLevel = selectedLevel else { return } - allLevels.forEach { - $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder - } - } - - /// Zooms to the facility if there is a selected facility, otherwise zooms to the site. + /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. public func zoomToSelection() { if selectedFacility != nil { zoomToFacility() @@ -177,9 +132,3 @@ public class FloorFilterViewModel: ObservableObject { } } } - -extension UIImage { - static var site: UIImage { - return UIImage(named: "Site", in: Bundle.module, with: nil)! - } -} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift index 2947702c5..3bbcedfb2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift @@ -14,9 +14,11 @@ import SwiftUI import ArcGIS +/// A view which allows selection of sites and facilities represented in a `FloorManager`. struct SiteSelector: View { - /// Creates a `FloorFilter` - /// - Parameter content: The view shown in the floating panel. + /// Creates a `SiteSelector` + /// - Parameter floorFilterViewModel: The view model used by the `SiteSelector`. + /// - Parameter showSiteSelector: A binding used to dismiss the site selector. public init( _ floorFilterViewModel: FloorFilterViewModel, showSiteSelector: Binding @@ -25,9 +27,11 @@ struct SiteSelector: View { self.showSiteSelector = showSiteSelector } - private let viewModel: FloorFilterViewModel + /// The view model used by the `SiteSelector`. + @ObservedObject + private var viewModel: FloorFilterViewModel - /// Binding allowing the user to toggle the visibility of the results list. + /// Allows the user to toggle the visibility of the site selector. private var showSiteSelector: Binding var body: some View { @@ -48,6 +52,7 @@ struct SiteSelector: View { } } + /// A view displaying either the sites or facilities contained in a `FloorManager`. struct FloorFilterList: View { private let title: String private let sites: [FloorSite]? @@ -56,6 +61,11 @@ struct SiteSelector: View { /// Binding allowing the user to toggle the visibility of the results list. private var showSiteSelector: Binding + /// Creates a `FloorFilterList` + /// - Parameters: + /// - title: The title of the list. + /// - sites: The sites to display. + /// - showSiteSelector: A binding used to dismiss the site selector. init( _ title: String, sites: [FloorSite], @@ -79,38 +89,28 @@ struct SiteSelector: View { } var body: some View { -// NavigationView { -// TODO: figure this navigation stuff out or at least get to a demo-able point - LazyVStack { - HStack { - Text(title) - .bold() - Spacer() - Button { - showSiteSelector.wrappedValue.toggle() - } label: { - Image(systemName: "xmark.circle") - } - } - Rectangle() - .frame(height:1) - .foregroundColor(.secondary) - ForEach(sites ?? []) { site in -// NavigationLink( -// destination: EmptyView()) { - Text(site.name) -// } - } - ForEach(facilities ?? []) { facility in -// NavigationLink( -// destination: EmptyView()) { - Text(facility.name) -// } + LazyVStack(alignment: .leading) { + HStack { + Text(title) + .bold() + Spacer() + Button { + showSiteSelector.wrappedValue.toggle() + } label: { + Image(systemName: "xmark.circle") } } -// .navigationBarTitle("Mountain Airport") - .esriBorder() + Rectangle() + .frame(height:1) + .foregroundColor(.secondary) + ForEach(sites ?? []) { site in + Text(site.name) + } + ForEach(facilities ?? []) { facility in + Text(facility.name) + } } + .esriBorder() } -// } + } } From 659672329fd10698dc404bc0dceb05085f95f24b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 21 Jan 2022 15:39:02 -0600 Subject: [PATCH 0406/1722] Update for new Item.id constructor --- Examples/Examples/FloorFilterExampleView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index d8b90aabe..138a06e64 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -33,15 +33,15 @@ struct FloorFilterExampleView: View { // Multiple sites/facilities: Esri IST map with all buildings. // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, itemId: "49520a67773842f1858602735ef538b5") +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) // Redlands Campus map. // let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, itemId: "7687805bd42549f5ba41237443d0c60a") //<= another multiple sites/facilities +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities // Single site (ESRI Redlands Main) and facility (Building L). let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, itemId: "f133a698536f44c8884ad81f80b6cfc7") + let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) map = Map(item: portalItem) } From c084919998a6f7046f3e590c3735136436f4e5e3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 22 Jan 2022 11:47:45 -0600 Subject: [PATCH 0407/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway Co-authored-by: Nimesh Jarecha --- .../BasemapGalleryItemTests.swift | 21 +++++----- .../BasemapGalleryViewModelTests.swift | 40 +++++++++---------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index df97793c9..21ba7052f 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -23,12 +23,12 @@ import Combine // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md // // Note: the iOS implementation uses the MVVM approach and SwiftUI. This -// required a bit more properties/logic in the `BasemapGalleryItem` (such -// as the `loadBasemapError` and `spatialReferenceStatus` properties than -// the `BasemapGallery` design specifies. Tests not present in the +// required a bit more properties/logic in the 'BasemapGalleryItem' (such +// as the 'loadBasemapError' and 'spatialReferenceStatus' properties than +// the 'BasemapGallery' design specifies. Tests not present in the // test design have been added to accomodate those differences. @MainActor -class BasemapGalleryItemTests: XCTestCase { +final class BasemapGalleryItemTests: XCTestCase { func testInit() async throws { let basemap = Basemap.lightGrayCanvas() var item = BasemapGalleryItem(basemap: basemap) @@ -36,15 +36,14 @@ class BasemapGalleryItemTests: XCTestCase { var isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first var loading = try XCTUnwrap(isBasemapLoading) XCTAssertFalse(loading, "Item is not loading.") - XCTAssertTrue(item.basemap === basemap) + XCTAssertIdentical(item.basemap, basemap) XCTAssertEqual(item.name, "Light Gray Canvas") XCTAssertNil(item.description) XCTAssertNotNil(item.thumbnail) XCTAssertNil(item.loadBasemapError) // Test with overrides. - let thumbnail = UIImage(systemName: "magnifyingglass") - XCTAssertNotNil(thumbnail) + let thumbnail = UIImage(systemName: "magnifyingglass")! item = BasemapGalleryItem( basemap: basemap, name: "My Basemap", @@ -123,7 +122,7 @@ class BasemapGalleryItemTests: XCTestCase { XCTAssertFalse(item.isBasemapLoading) // Test if basemap doesn't match. - try await item.updateSpatialReferenceStatus(SpatialReference.wgs84) + try await item.updateSpatialReferenceStatus(.wgs84) // Since we've already called `updateSpatialReferenceStatus` once, // we should no longer internally need to load the baselayers. @@ -134,7 +133,7 @@ class BasemapGalleryItemTests: XCTestCase { srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).first status = try XCTUnwrap(srStatus) XCTAssertEqual(status, .noMatch) - XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) + XCTAssertEqual(item.spatialReference, .webMercator) // Test WGS84 basemap. let otherItem = BasemapGalleryItem( @@ -147,10 +146,10 @@ class BasemapGalleryItemTests: XCTestCase { _ = try await otherItem.$isBasemapLoading.compactMap({ $0 }).dropFirst().first - try await otherItem.updateSpatialReferenceStatus(SpatialReference.wgs84) + try await otherItem.updateSpatialReferenceStatus(.wgs84) srStatus = try await otherItem.$spatialReferenceStatus.compactMap({ $0 }).first status = try XCTUnwrap(srStatus) XCTAssertEqual(status, .match) - XCTAssertEqual(otherItem.spatialReference, SpatialReference.wgs84) + XCTAssertEqual(otherItem.spatialReference, .wgs84) } } diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 0a5bc9337..8a619a556 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -23,8 +23,8 @@ import Combine // Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md // // Note: the iOS implementation uses the MVVM approach and SwiftUI. This -// required a bit more properties/logic in the `BasemapGalleryViewModel` (such -// as `geoModel.actualSpatialReference`) than the `BasemapGallery` design +// required a bit more properties/logic in the 'BasemapGalleryViewModel' (such +// as 'geoModel.actualSpatialReference') than the 'BasemapGallery' design // specifies. Tests not present in the test design have been added to // accomodate those differences. @MainActor @@ -54,9 +54,9 @@ class BasemapGalleryViewModelTests: XCTestCase { // // GeoModel. // - var geoModel = Map(basemap: Basemap.lightGrayCanvas()) + var geoModel = Map(basemap: .lightGrayCanvas()) let geoModelViewModel = BasemapGalleryViewModel(geoModel: geoModel) - XCTAssertTrue(geoModel === geoModelViewModel.geoModel) + XCTAssertIdentical(geoModelViewModel.geoModel, geoModel) // With no portal, `basemapGalleryItems` are fetched from AGOL's // list of developer basemaps. @@ -66,7 +66,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // GeoModel should be loaded. XCTAssertEqual(geoModel.loadStatus, .loaded) - XCTAssertTrue(geoModel.basemap === geoModelViewModel.currentItem?.basemap) + XCTAssertIdentical(geoModelViewModel.currentItem?.basemap, geoModel.basemap) // Save the array of developer basemap items from AGOL. let developerBasemapItems = basemapGalleryItems @@ -74,12 +74,12 @@ class BasemapGalleryViewModelTests: XCTestCase { // // Portal. // - geoModel = Map(basemap: Basemap.lightGrayCanvas()) + geoModel = Map(basemap: .lightGrayCanvas()) let portal = Portal.arcGISOnline(isLoginRequired: false) let portalViewModel = BasemapGalleryViewModel(geoModel, portal: portal) - XCTAssertTrue(geoModel === portalViewModel.geoModel) - XCTAssertTrue(portal === portalViewModel.portal) + XCTAssertIdentical(portalViewModel.geoModel, geoModel) + XCTAssertIdentical(portalViewModel.portal, portal) // With a portal, `basemapGalleryItems` are fetched from either the // portal's vector basemaps or regular basemaps. @@ -88,7 +88,7 @@ class BasemapGalleryViewModelTests: XCTestCase { XCTAssertFalse(basemapGalleryItems.isEmpty) XCTAssertEqual(geoModel.loadStatus, .loaded) - XCTAssertTrue(geoModel.basemap === portalViewModel.currentItem?.basemap) + XCTAssertIdentical(portalViewModel.currentItem?.basemap, geoModel.basemap) // Sort the developer items from the "GeoModel" test above and the // items from the portal and make sure they are not equal. @@ -105,7 +105,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // // BasemapGalleryItems. No basemaps are fetched from a portal. // - geoModel = Map(basemap: Basemap.lightGrayCanvas()) + geoModel = Map(basemap: .lightGrayCanvas()) let itemsViewModel = BasemapGalleryViewModel( geoModel: geoModel, items: defaultBasemapGalleryItems @@ -120,12 +120,12 @@ class BasemapGalleryViewModelTests: XCTestCase { /// Test the `GeoModel.actualSpatialReference` extension property. func testGeoModelActualSpatialReference() async throws { - // Map with .webMercator basemap. - let geoModel = Map(basemap: Basemap.lightGrayCanvas()) + // Map with Web Mercator basemap. + let geoModel = Map(basemap: .lightGrayCanvas()) try await geoModel.load() XCTAssertEqual(geoModel.actualSpatialReference, .webMercator) - // Map with .wgs84 basemap. + // Map with WGS 84 basemap. let geoModel2 = Map( basemap: Basemap( item: PortalItem( @@ -171,7 +171,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // Verify current item is equal to map's basemap. var item = try await viewModel.$currentItem.compactMap({ $0 }).first var currentItem = try XCTUnwrap(item) - XCTAssertTrue(currentItem.basemap === basemap) + XCTAssertIdentical(currentItem.basemap, basemap) // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). let validItem = BasemapGalleryItem( @@ -213,7 +213,7 @@ class BasemapGalleryViewModelTests: XCTestCase { viewModel.setCurrentItem(invalidItem) // The update will fail, so wait until the - // `$spatialReferenceMismatchError` is updated. + // `spatialReferenceMismatchError` is updated. let error = try await viewModel.$spatialReferenceMismatchError.compactMap({ $0 }).first XCTAssertNotNil(error, "Error is not nil.") @@ -234,7 +234,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // There are exactly two default items. XCTAssertEqual(basemapGalleryItems.count, 2) - // Set a portal on the model. This should clear out the existing + // Set a portal on the model. This should clear out the existing // array of items and load basemaps from the portal. viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) @@ -248,10 +248,8 @@ class BasemapGalleryViewModelTests: XCTestCase { basemapGalleryItems = try XCTUnwrap(items) // There should be no default items in the basemap gallery. - let foundDefaultItem = basemapGalleryItems.first(where: { - $0 == defaultBasemapGalleryItems[0] || - $0 == defaultBasemapGalleryItems[1] - }) - XCTAssertNil(foundDefaultItem) + XCTAssertFalse(basemapGalleryItems.contains(where: { item in + defaultBasemapGalleryItems.contains(item) + })) } } From 22957463e497be09e93eb80b4551129c7aef80cc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 22 Jan 2022 11:48:48 -0600 Subject: [PATCH 0408/1722] Remove internal dest design url --- Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index df97793c9..26a419ac0 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -19,9 +19,6 @@ import ArcGIS import SwiftUI import Combine -// -// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md -// // Note: the iOS implementation uses the MVVM approach and SwiftUI. This // required a bit more properties/logic in the `BasemapGalleryItem` (such // as the `loadBasemapError` and `spatialReferenceStatus` properties than From 49579bc01ab4ec0a893cdef21d4225a57dafdd21 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 24 Jan 2022 10:41:21 -0600 Subject: [PATCH 0409/1722] Fix for Geometry Envelope initializer API changes. --- .../ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index e63a8be5d..8ba6c97bb 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -63,9 +63,7 @@ final class SmartLocatorSearchSourceTests: XCTestCase { private extension Envelope { static let edinburgh = Envelope( - xMin: -365155.60783391213, - yMin: 7536778.456812576, - xMax: -347494.47622280417, - yMax: 7559866.706991681 + xRange: -365155.60783391213...(-347494.47622280417), + yRange: 7536778.456812576...7559866.706991681 ) } From 7699433e1f8118c12b463b7ef60ba9c57566ac4f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 24 Jan 2022 11:45:51 -0600 Subject: [PATCH 0410/1722] Update Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift index 8ba6c97bb..99e422af2 100644 --- a/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift +++ b/Tests/ArcGISToolkitTests/SmartLocatorSearchSourceTests.swift @@ -63,7 +63,7 @@ final class SmartLocatorSearchSourceTests: XCTestCase { private extension Envelope { static let edinburgh = Envelope( - xRange: -365155.60783391213...(-347494.47622280417), + xRange: -365155.60783391213 ... -347494.47622280417, yRange: 7536778.456812576...7559866.706991681 ) } From 43ba7d2bb8e23870351aa2ccb75289041d0288f7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 24 Jan 2022 14:24:23 -0600 Subject: [PATCH 0411/1722] Remove unnecessary `compactMap` and use `let` instead of `var`. --- .../BasemapGalleryItemTests.swift | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 40eb28e14..34eee2e0d 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -28,10 +28,10 @@ import Combine final class BasemapGalleryItemTests: XCTestCase { func testInit() async throws { let basemap = Basemap.lightGrayCanvas() - var item = BasemapGalleryItem(basemap: basemap) + let item = BasemapGalleryItem(basemap: basemap) - var isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first - var loading = try XCTUnwrap(isBasemapLoading) + let isBasemapLoading = try await item.$isBasemapLoading.dropFirst().first + let loading = try XCTUnwrap(isBasemapLoading) XCTAssertFalse(loading, "Item is not loading.") XCTAssertIdentical(item.basemap, basemap) XCTAssertEqual(item.name, "Light Gray Canvas") @@ -41,23 +41,24 @@ final class BasemapGalleryItemTests: XCTestCase { // Test with overrides. let thumbnail = UIImage(systemName: "magnifyingglass")! - item = BasemapGalleryItem( - basemap: basemap, + let basemap2 = Basemap.lightGrayCanvas() + let item2 = BasemapGalleryItem( + basemap: basemap2, name: "My Basemap", description: "Basemap description", thumbnail: thumbnail ) - isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isBasemapLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertEqual(item.name, "My Basemap") - XCTAssertEqual(item.description, "Basemap description") - XCTAssertEqual(item.thumbnail, thumbnail) - XCTAssertNil(item.loadBasemapError) + let isBasemapLoading2 = try await item2.$isBasemapLoading.dropFirst().first + let loading2 = try XCTUnwrap(isBasemapLoading2) + XCTAssertFalse(loading2, "Item is not loading.") + XCTAssertEqual(item2.name, "My Basemap") + XCTAssertEqual(item2.description, "Basemap description") + XCTAssertEqual(item2.thumbnail, thumbnail) + XCTAssertNil(item2.loadBasemapError) // Test with portal item. - item = BasemapGalleryItem( + let item3 = BasemapGalleryItem( basemap: Basemap( item: PortalItem( url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=46a87c20f09e4fc48fa3c38081e0cae6")! @@ -65,13 +66,13 @@ final class BasemapGalleryItemTests: XCTestCase { ) ) - isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isBasemapLoading) - XCTAssertFalse(loading, "Item is not loading.") - XCTAssertEqual(item.name, "OpenStreetMap Blueprint") - XCTAssertEqual(item.description, "

This web map presents a vector basemap of OpenStreetMap (OSM) data hosted by Esri. Esri created this vector tile basemap from the Daylight map distribution of OSM data, which is supported by Facebook and supplemented with additional data from Microsoft. It presents the map in a cartographic style is like a blueprint technical drawing. The OSM Daylight map will be updated every month with the latest version of OSM Daylight data. 

OpenStreetMap is an open collaborative project to create a free editable map of the world. Volunteers gather location data using GPS, local knowledge, and other free sources of information and upload it. The resulting free map can be viewed and downloaded from the OpenStreetMap site: www.OpenStreetMap.org. Esri is a supporter of the OSM project and is excited to make this enhanced vector basemap available to the ArcGIS user and developer communities.
") - XCTAssertNotNil(item.thumbnail) - XCTAssertNil(item.loadBasemapError) + let isBasemapLoading3 = try await item3.$isBasemapLoading.dropFirst().first + let loading3 = try XCTUnwrap(isBasemapLoading3) + XCTAssertFalse(loading3, "Item is not loading.") + XCTAssertEqual(item3.name, "OpenStreetMap Blueprint") + XCTAssertEqual(item3.description, "

This web map presents a vector basemap of OpenStreetMap (OSM) data hosted by Esri. Esri created this vector tile basemap from the Daylight map distribution of OSM data, which is supported by Facebook and supplemented with additional data from Microsoft. It presents the map in a cartographic style is like a blueprint technical drawing. The OSM Daylight map will be updated every month with the latest version of OSM Daylight data. 

OpenStreetMap is an open collaborative project to create a free editable map of the world. Volunteers gather location data using GPS, local knowledge, and other free sources of information and upload it. The resulting free map can be viewed and downloaded from the OpenStreetMap site: www.OpenStreetMap.org. Esri is a supporter of the OSM project and is excited to make this enhanced vector basemap available to the ArcGIS user and developer communities.
") + XCTAssertNotNil(item3.thumbnail) + XCTAssertNil(item3.loadBasemapError) } func testLoadBasemapError() async throws { @@ -84,7 +85,7 @@ final class BasemapGalleryItemTests: XCTestCase { ) ) - let isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + let isBasemapLoading = try await item.$isBasemapLoading.dropFirst().first let loading = try XCTUnwrap(isBasemapLoading) XCTAssertFalse(loading, "Item is not loading.") XCTAssertNotNil(item.loadBasemapError) @@ -94,8 +95,8 @@ final class BasemapGalleryItemTests: XCTestCase { let basemap = Basemap.lightGrayCanvas() let item = BasemapGalleryItem(basemap: basemap) - var isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first - var loading = try XCTUnwrap(isBasemapLoading) + let isBasemapLoading = try await item.$isBasemapLoading.dropFirst().first + let loading = try XCTUnwrap(isBasemapLoading) XCTAssertFalse(loading, "Item is not loading.") XCTAssertEqual(item.spatialReferenceStatus, .unknown) @@ -108,12 +109,12 @@ final class BasemapGalleryItemTests: XCTestCase { // Check if `isBasemapLoading` is set to true during first call // to updateSpatialReferenceStatus. - isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).dropFirst().first - loading = try XCTUnwrap(isBasemapLoading) - XCTAssertTrue(loading, "Item base layers are loading.") + let isBasemapLoading2 = try await item.$isBasemapLoading.dropFirst().first + let loading2 = try XCTUnwrap(isBasemapLoading2) + XCTAssertTrue(loading2, "Item base layers are loading.") - var srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).dropFirst().first - var status = try XCTUnwrap(srStatus) + let srStatus = try await item.$spatialReferenceStatus.dropFirst().first + let status = try XCTUnwrap(srStatus) XCTAssertEqual(status, .match) XCTAssertEqual(item.spatialReference, SpatialReference.webMercator) XCTAssertFalse(item.isBasemapLoading) @@ -123,13 +124,13 @@ final class BasemapGalleryItemTests: XCTestCase { // Since we've already called `updateSpatialReferenceStatus` once, // we should no longer internally need to load the baselayers. - isBasemapLoading = try await item.$isBasemapLoading.compactMap({ $0 }).first - loading = try XCTUnwrap(isBasemapLoading) - XCTAssertFalse(loading, "Item base layers are not loading.") + let isBasemapLoading3 = try await item.$isBasemapLoading.first + let loading3 = try XCTUnwrap(isBasemapLoading3) + XCTAssertFalse(loading3, "Item base layers are not loading.") - srStatus = try await item.$spatialReferenceStatus.compactMap({ $0 }).first - status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .noMatch) + let srStatus2 = try await item.$spatialReferenceStatus.first + let status2 = try XCTUnwrap(srStatus2) + XCTAssertEqual(status2, .noMatch) XCTAssertEqual(item.spatialReference, .webMercator) // Test WGS84 basemap. @@ -141,12 +142,12 @@ final class BasemapGalleryItemTests: XCTestCase { ) ) - _ = try await otherItem.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + _ = try await otherItem.$isBasemapLoading.dropFirst().first try await otherItem.updateSpatialReferenceStatus(.wgs84) - srStatus = try await otherItem.$spatialReferenceStatus.compactMap({ $0 }).first - status = try XCTUnwrap(srStatus) - XCTAssertEqual(status, .match) + let srStatus3 = try await otherItem.$spatialReferenceStatus.first + let status3 = try XCTUnwrap(srStatus3) + XCTAssertEqual(status3, .match) XCTAssertEqual(otherItem.spatialReference, .wgs84) } } From 3e38d79659197cba380e374a7e21c5ca504c4fa5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 24 Jan 2022 14:32:55 -0600 Subject: [PATCH 0412/1722] Remove unnecessary `compactMap` and use let instead of var. --- .../BasemapGalleryViewModelTests.swift | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 8a619a556..440864399 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -54,14 +54,14 @@ class BasemapGalleryViewModelTests: XCTestCase { // // GeoModel. // - var geoModel = Map(basemap: .lightGrayCanvas()) + let geoModel = Map(basemap: .lightGrayCanvas()) let geoModelViewModel = BasemapGalleryViewModel(geoModel: geoModel) XCTAssertIdentical(geoModelViewModel.geoModel, geoModel) // With no portal, `basemapGalleryItems` are fetched from AGOL's // list of developer basemaps. - var items = try await geoModelViewModel.$items.compactMap({ $0 }).dropFirst().first - var basemapGalleryItems = try XCTUnwrap(items) + let items = try await geoModelViewModel.$items.dropFirst().first + let basemapGalleryItems = try XCTUnwrap(items) XCTAssertFalse(basemapGalleryItems.isEmpty) // GeoModel should be loaded. @@ -74,25 +74,25 @@ class BasemapGalleryViewModelTests: XCTestCase { // // Portal. // - geoModel = Map(basemap: .lightGrayCanvas()) + let geoModel2 = Map(basemap: .lightGrayCanvas()) let portal = Portal.arcGISOnline(isLoginRequired: false) - let portalViewModel = BasemapGalleryViewModel(geoModel, portal: portal) + let portalViewModel = BasemapGalleryViewModel(geoModel2, portal: portal) - XCTAssertIdentical(portalViewModel.geoModel, geoModel) + XCTAssertIdentical(portalViewModel.geoModel, geoModel2) XCTAssertIdentical(portalViewModel.portal, portal) // With a portal, `basemapGalleryItems` are fetched from either the // portal's vector basemaps or regular basemaps. - items = try await portalViewModel.$items.compactMap({ $0 }).dropFirst().first - basemapGalleryItems = try XCTUnwrap(items) - XCTAssertFalse(basemapGalleryItems.isEmpty) + let items2 = try await portalViewModel.$items.dropFirst().first + let basemapGalleryItems2 = try XCTUnwrap(items2) + XCTAssertFalse(basemapGalleryItems2.isEmpty) - XCTAssertEqual(geoModel.loadStatus, .loaded) - XCTAssertIdentical(portalViewModel.currentItem?.basemap, geoModel.basemap) + XCTAssertEqual(geoModel2.loadStatus, .loaded) + XCTAssertIdentical(portalViewModel.currentItem?.basemap, geoModel2.basemap) // Sort the developer items from the "GeoModel" test above and the // items from the portal and make sure they are not equal. - let sortedItems = basemapGalleryItems.sorted { + let sortedItems = basemapGalleryItems2.sorted { guard let name0 = $0.name, let name1 = $1.name else { return false } return name0 < name1 } @@ -105,9 +105,9 @@ class BasemapGalleryViewModelTests: XCTestCase { // // BasemapGalleryItems. No basemaps are fetched from a portal. // - geoModel = Map(basemap: .lightGrayCanvas()) + let geoModel3 = Map(basemap: .lightGrayCanvas()) let itemsViewModel = BasemapGalleryViewModel( - geoModel: geoModel, + geoModel: geoModel3, items: defaultBasemapGalleryItems ) @@ -169,8 +169,8 @@ class BasemapGalleryViewModelTests: XCTestCase { let viewModel = BasemapGalleryViewModel(geoModel: geoModel) // Verify current item is equal to map's basemap. - var item = try await viewModel.$currentItem.compactMap({ $0 }).first - var currentItem = try XCTUnwrap(item) + let item = try await viewModel.$currentItem.compactMap({ $0 }).first + let currentItem = try XCTUnwrap(item) XCTAssertIdentical(currentItem.basemap, basemap) // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). @@ -183,17 +183,17 @@ class BasemapGalleryViewModelTests: XCTestCase { ) // Wait until it loads. - _ = try await validItem.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + _ = try await validItem.$isBasemapLoading.dropFirst().first // Update the item on the model. viewModel.setCurrentItem(validItem) // Wait until the `currentItem` is updated. - item = try await viewModel.$currentItem.compactMap({ $0 }).dropFirst().first - currentItem = try XCTUnwrap(item) + let item2 = try await viewModel.$currentItem.compactMap({ $0 }).dropFirst().first + let currentItem2 = try XCTUnwrap(item2) // Items should equal, meaning the `validItem` was set properly. - XCTAssertEqual(currentItem, validItem) + XCTAssertEqual(currentItem2, validItem) // Test WGS84 basemap item (Imagery (WGS84)). This item is in a // different spatial reference than the geoModel, so it should @@ -207,18 +207,18 @@ class BasemapGalleryViewModelTests: XCTestCase { ) // Wait until it loads. - _ = try await invalidItem.$isBasemapLoading.compactMap({ $0 }).dropFirst().first + _ = try await invalidItem.$isBasemapLoading.dropFirst().first // Update the item on the model. viewModel.setCurrentItem(invalidItem) // The update will fail, so wait until the // `spatialReferenceMismatchError` is updated. - let error = try await viewModel.$spatialReferenceMismatchError.compactMap({ $0 }).first + let error = try await viewModel.$spatialReferenceMismatchError.first XCTAssertNotNil(error, "Error is not nil.") // Make sure the current item is still equal to the valid item. - XCTAssertEqual(currentItem, validItem) + XCTAssertEqual(currentItem2, validItem) } /// Test setting the portal after the model has been created. @@ -228,8 +228,8 @@ class BasemapGalleryViewModelTests: XCTestCase { items: defaultBasemapGalleryItems ) - var items = try await viewModel.$items.compactMap({ $0 }).first - var basemapGalleryItems = try XCTUnwrap(items) + let items = try await viewModel.$items.first + let basemapGalleryItems = try XCTUnwrap(items) // There are exactly two default items. XCTAssertEqual(basemapGalleryItems.count, 2) @@ -239,16 +239,16 @@ class BasemapGalleryViewModelTests: XCTestCase { viewModel.portal = Portal.arcGISOnline(isLoginRequired: false) // The items should be cleared prior to loading those from the portal. - items = try await viewModel.$items.compactMap({ $0 }).first - basemapGalleryItems = try XCTUnwrap(items) - XCTAssertTrue(basemapGalleryItems.isEmpty) + let items2 = try await viewModel.$items.first + let basemapGalleryItems2 = try XCTUnwrap(items2) + XCTAssertTrue(basemapGalleryItems2.isEmpty) // Wait for the portal basemaps to load. - items = try await viewModel.$items.compactMap({ $0 }).dropFirst().first - basemapGalleryItems = try XCTUnwrap(items) + let items3 = try await viewModel.$items.dropFirst().first + let basemapGalleryItems3 = try XCTUnwrap(items3) // There should be no default items in the basemap gallery. - XCTAssertFalse(basemapGalleryItems.contains(where: { item in + XCTAssertFalse(basemapGalleryItems3.contains(where: { item in defaultBasemapGalleryItems.contains(item) })) } From 8e60f7381029c67c38a100651a7334a609924a69 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 24 Jan 2022 17:17:59 -0600 Subject: [PATCH 0413/1722] Apply suggestions from code review Co-authored-by: Nimesh Jarecha --- Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift | 4 ++-- .../BasemapGalleryViewModelTests.swift | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 34eee2e0d..b43f5da36 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -108,7 +108,7 @@ final class BasemapGalleryItemTests: XCTestCase { } // Check if `isBasemapLoading` is set to true during first call - // to updateSpatialReferenceStatus. + // to `updateSpatialReferenceStatus`. let isBasemapLoading2 = try await item.$isBasemapLoading.dropFirst().first let loading2 = try XCTUnwrap(isBasemapLoading2) XCTAssertTrue(loading2, "Item base layers are loading.") @@ -123,7 +123,7 @@ final class BasemapGalleryItemTests: XCTestCase { try await item.updateSpatialReferenceStatus(.wgs84) // Since we've already called `updateSpatialReferenceStatus` once, - // we should no longer internally need to load the baselayers. + // we should no longer internally need to load the base layers. let isBasemapLoading3 = try await item.$isBasemapLoading.first let loading3 = try XCTUnwrap(isBasemapLoading3) XCTAssertFalse(loading3, "Item base layers are not loading.") diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 440864399..d4f0a5718 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -19,9 +19,6 @@ import ArcGIS import SwiftUI import Combine -// -// Test Design: https://devtopia.esri.com/runtime/common-toolkit/blob/master/designs/BasemapGallery/BasemapGallery_Test_Design.md -// // Note: the iOS implementation uses the MVVM approach and SwiftUI. This // required a bit more properties/logic in the 'BasemapGalleryViewModel' (such // as 'geoModel.actualSpatialReference') than the 'BasemapGallery' design @@ -137,7 +134,7 @@ class BasemapGalleryViewModelTests: XCTestCase { try await geoModel2.load() XCTAssertEqual(geoModel2.actualSpatialReference, .wgs84) - // Test with Scene that has a tiling scheme of .webMercator + // Test with Scene that has a tiling scheme of Web Mercator. let geoModel3 = Scene( basemap: Basemap( item: PortalItem( @@ -149,7 +146,7 @@ class BasemapGalleryViewModelTests: XCTestCase { try await geoModel3.load() XCTAssertEqual(geoModel3.actualSpatialReference, .webMercator) - // Test with Scene that has a tiling scheme of .wgs84 + // Test with Scene that has a tiling scheme of WGS 84. let geoModel4 = Scene( basemap: Basemap( item: PortalItem( @@ -162,7 +159,7 @@ class BasemapGalleryViewModelTests: XCTestCase { } /// Test the `currentItem` property including valid and invalid basemaps. - func testcurrentItem() async throws { + func testCurrentItem() async throws { let basemap = Basemap.streets() let geoModel = Map(basemap: basemap) From 72f64ecfd7f5085d15cbce21639dbd180c62197a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 24 Jan 2022 17:37:57 -0600 Subject: [PATCH 0414/1722] PR review changes --- .../BasemapGalleryViewModelTests.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 440864399..ae8323b6e 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -169,9 +169,9 @@ class BasemapGalleryViewModelTests: XCTestCase { let viewModel = BasemapGalleryViewModel(geoModel: geoModel) // Verify current item is equal to map's basemap. - let item = try await viewModel.$currentItem.compactMap({ $0 }).first + let item = try await viewModel.$currentItem.dropFirst().first let currentItem = try XCTUnwrap(item) - XCTAssertIdentical(currentItem.basemap, basemap) + XCTAssertIdentical(currentItem?.basemap, basemap) // Test valid basemap item (OpenStreetMap Vector Basemap (Blueprint)). let validItem = BasemapGalleryItem( @@ -189,7 +189,7 @@ class BasemapGalleryViewModelTests: XCTestCase { viewModel.setCurrentItem(validItem) // Wait until the `currentItem` is updated. - let item2 = try await viewModel.$currentItem.compactMap({ $0 }).dropFirst().first + let item2 = try await viewModel.$currentItem.dropFirst().first let currentItem2 = try XCTUnwrap(item2) // Items should equal, meaning the `validItem` was set properly. @@ -214,8 +214,9 @@ class BasemapGalleryViewModelTests: XCTestCase { // The update will fail, so wait until the // `spatialReferenceMismatchError` is updated. - let error = try await viewModel.$spatialReferenceMismatchError.first - XCTAssertNotNil(error, "Error is not nil.") + let error = try await viewModel.$spatialReferenceMismatchError.dropFirst().first + let unwrappedError = try XCTUnwrap(error) + XCTAssertNotNil(unwrappedError, "Error is not nil.") // Make sure the current item is still equal to the valid item. XCTAssertEqual(currentItem2, validItem) From 5b487affff7a5b42b9653ad31832dc58e13a5c48 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 26 Jan 2022 12:49:11 -0600 Subject: [PATCH 0415/1722] Apply suggestions from code review Co-authored-by: Phil Gruenler --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 2 +- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index c3748c074..7561797c7 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -19,7 +19,7 @@ import ArcGIS /// to a site, a facility (building) in the site, or a floor in the facility. public struct FloorFilter: View { /// Creates a `FloorFilter` - /// - Parameter floorFilterViewModel: The view model used by the `BasemapGallery`. + /// - Parameter floorFilterViewModel: The view model used by the `FloorFilter`. public init(_ floorFilterViewModel: FloorFilterViewModel) { self.viewModel = floorFilterViewModel } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 90e830ac3..0552c521b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -42,7 +42,7 @@ public class FloorFilterViewModel: ObservableObject { } /// The `Viewpoint` used to pan/zoom to the selected site/facilty. - /// If `nil`, there will be automatic no pan/zoom operations. + /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding? = nil /// The `FloorManager` containing the site, floor, and level information. From ff80102eb493771d8de97c134df55dbd198b7a67 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 26 Jan 2022 12:55:18 -0600 Subject: [PATCH 0416/1722] PR review changes. --- .../Components/FloorFilter/SiteSelector.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift index 3bbcedfb2..884bf302b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift @@ -55,8 +55,8 @@ struct SiteSelector: View { /// A view displaying either the sites or facilities contained in a `FloorManager`. struct FloorFilterList: View { private let title: String - private let sites: [FloorSite]? - private let facilities: [FloorFacility]? + private let sites: [FloorSite] + private let facilities: [FloorFacility] /// Binding allowing the user to toggle the visibility of the results list. private var showSiteSelector: Binding @@ -84,7 +84,7 @@ struct SiteSelector: View { ) { self.title = title self.facilities = facilities - sites = nil + sites = [] self.showSiteSelector = showSiteSelector } @@ -103,10 +103,10 @@ struct SiteSelector: View { Rectangle() .frame(height:1) .foregroundColor(.secondary) - ForEach(sites ?? []) { site in + ForEach(sites) { site in Text(site.name) } - ForEach(facilities ?? []) { facility in + ForEach(facilities) { facility in Text(facility.name) } } From 4bd47d21d5bfdbdd61c915e7a91c24f4ad88d42b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 26 Jan 2022 12:57:38 -0600 Subject: [PATCH 0417/1722] Removed unnecessary ArcGIS framework. --- Examples/Examples.xcodeproj/project.pbxproj | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 437bc78b7..89e1e6311 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -11,8 +11,6 @@ E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */; }; E47ABE4B2652FE0C00FD2FE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE4A2652FE0C00FD2FE3 /* Preview Assets.xcassets */; }; - E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; }; - E47ABE5D2652FF1200FD2FE3 /* ArcGIS in Embed Frameworks */ = {isa = PBXBuildFile; productRef = E47ABE5B2652FF1200FD2FE3 /* ArcGIS */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; E48A73432658227100F5C118 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73402658227100F5C118 /* Example.swift */; }; E48A73442658227100F5C118 /* AnyExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A73412658227100F5C118 /* AnyExample.swift */; }; E48A73452658227100F5C118 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48A733D2658227000F5C118 /* Examples.swift */; }; @@ -31,7 +29,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - E47ABE5D2652FF1200FD2FE3 /* ArcGIS in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -63,7 +60,6 @@ buildActionMask = 2147483647; files = ( E4E57DC6265D8EB00077A093 /* ArcGISToolkit in Frameworks */, - E47ABE5C2652FF1200FD2FE3 /* ArcGIS in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -158,7 +154,6 @@ ); name = Examples; packageProductDependencies = ( - E47ABE5B2652FF1200FD2FE3 /* ArcGIS */, E4E57DC5265D8EB00077A093 /* ArcGISToolkit */, ); productName = Examples; @@ -418,10 +413,6 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - E47ABE5B2652FF1200FD2FE3 /* ArcGIS */ = { - isa = XCSwiftPackageProductDependency; - productName = ArcGIS; - }; E4E57DC5265D8EB00077A093 /* ArcGISToolkit */ = { isa = XCSwiftPackageProductDependency; productName = ArcGISToolkit; From 1f7d5ad6df65e067b4f1479a0c6334255154178e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 26 Jan 2022 13:18:24 -0600 Subject: [PATCH 0418/1722] Make class "final" --- .../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 0552c521b..42dddbd5b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -16,7 +16,7 @@ import ArcGIS @MainActor /// Manages the state for a `FloorFilter`. -public class FloorFilterViewModel: ObservableObject { +final public class FloorFilterViewModel: ObservableObject { /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - floorManager: A floor manager. From 8cae45978b5fd6a0703443379c23e3a4509f8616 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 31 Jan 2022 15:40:50 -0600 Subject: [PATCH 0419/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- .../ArcGISToolkit/Components/FloorFilter/SiteSelector.swift | 4 ++-- Sources/ArcGISToolkit/Extensions/FloorFacility.swift | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 42dddbd5b..7e05eedb7 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -14,8 +14,8 @@ import SwiftUI import ArcGIS -@MainActor /// Manages the state for a `FloorFilter`. +@MainActor final public class FloorFilterViewModel: ObservableObject { /// Creates a `FloorFilterViewModel`. /// - Parameters: diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift index 884bf302b..57d08a040 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift @@ -39,7 +39,7 @@ struct SiteSelector: View { // Only show site list if there is more than one site // and the user has not yet selected a site. FloorFilterList( - "Select a site...", + "Select a site…", sites: viewModel.sites, showSiteSelector: showSiteSelector ) @@ -101,7 +101,7 @@ struct SiteSelector: View { } } Rectangle() - .frame(height:1) + .frame(height: 1) .foregroundColor(.secondary) ForEach(sites) { site in Text(site.name) diff --git a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift index 48a492c99..b7efe890e 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift @@ -20,4 +20,6 @@ extension FloorFacility: Equatable { } } -extension FloorFacility: Identifiable { } +extension FloorFacility: Identifiable { + public var id: String { facilityId } +} From 042946f7b25229c04add903bc8f5d1bf29b86f60 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 31 Jan 2022 15:41:25 -0600 Subject: [PATCH 0420/1722] PR review changes. --- .../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 7561797c7..2f0aefdaa 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -19,9 +19,9 @@ import ArcGIS /// to a site, a facility (building) in the site, or a floor in the facility. public struct FloorFilter: View { /// Creates a `FloorFilter` - /// - Parameter floorFilterViewModel: The view model used by the `FloorFilter`. - public init(_ floorFilterViewModel: FloorFilterViewModel) { - self.viewModel = floorFilterViewModel + /// - Parameter viewModel: The view model used by the `FloorFilter`. + public init(_ viewModel: FloorFilterViewModel) { + self.viewModel = viewModel } /// The view model used by the `FloorFilter`. @@ -43,7 +43,7 @@ public struct FloorFilter: View { Button { showSiteSelector.toggle() } label: { - Image(uiImage: .site) + Image.init("Site", bundle: Bundle.module) } } .esriBorder() @@ -58,9 +58,3 @@ public struct FloorFilter: View { } } } - -extension UIImage { - static var site: UIImage { - return UIImage(named: "Site", in: Bundle.module, with: nil)! - } -} From 1377c7271fced3dc48aae14d069f88be86909a38 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 31 Jan 2022 17:29:35 -0600 Subject: [PATCH 0421/1722] PR review changes. --- .../Components/FloorFilter/FloorFilter.swift | 8 +++--- .../FloorFilter/FloorFilterViewModel.swift | 14 +++++++--- .../Components/FloorFilter/SiteSelector.swift | 26 +++++++++---------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 2f0aefdaa..6ca703c1f 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -30,7 +30,7 @@ public struct FloorFilter: View { /// Allows the user to toggle the visibility of the site selector. @State - private var showSiteSelector: Bool = false + private var isSelectorVisible: Bool = false public var body: some View { HStack(alignment: .bottom) { @@ -41,16 +41,16 @@ public struct FloorFilter: View { } else { VStack { Button { - showSiteSelector.toggle() + isSelectorVisible.toggle() } label: { Image.init("Site", bundle: Bundle.module) } } .esriBorder() - if showSiteSelector { + if isSelectorVisible { SiteSelector( viewModel, - showSiteSelector: $showSiteSelector + isSelectorVisible: $isSelectorVisible ) .frame(width: 200) } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 7e05eedb7..c88967802 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -55,7 +55,7 @@ final public class FloorFilterViewModel: ObservableObject { /// `true` if the model is loading it's properties, `false` if not loading. @Published - public var isLoading = true + public private(set) var isLoading = true /// Facilities in the selected site. If no site is selected then the list is empty. /// If the sites list is empty, all facilities will be returned. @@ -67,11 +67,19 @@ final public class FloorFilterViewModel: ObservableObject { /// Levels in the selected facility. If no facility is selected then the list is empty. /// If the facilities list is empty, all levels will be returned. - /// The levels are returned in ascending order. public var levels: [FloorLevel] { facilities.isEmpty ? floorManager.levels : floorManager.levels.filter { $0.facility == selectedFacility - }.reversed() + } + } + + /// Levels in the selected facility. If no facility is selected then the list is empty. + /// If the facilities list is empty, all levels will be returned. + /// The levels are sorted by `verticalOrder` in descending order. + public var sortedLevels: [FloorLevel] { + levels.sorted { + $0.verticalOrder > $1.verticalOrder + } } /// All the levels in the map diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift index 57d08a040..dec8cc77d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift @@ -18,13 +18,13 @@ import ArcGIS struct SiteSelector: View { /// Creates a `SiteSelector` /// - Parameter floorFilterViewModel: The view model used by the `SiteSelector`. - /// - Parameter showSiteSelector: A binding used to dismiss the site selector. + /// - Parameter isSelectorVisible: A binding used to dismiss the site selector. public init( _ floorFilterViewModel: FloorFilterViewModel, - showSiteSelector: Binding + isSelectorVisible: Binding ) { self.viewModel = floorFilterViewModel - self.showSiteSelector = showSiteSelector + self.isSelectorVisible = isSelectorVisible } /// The view model used by the `SiteSelector`. @@ -32,7 +32,7 @@ struct SiteSelector: View { private var viewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site selector. - private var showSiteSelector: Binding + private var isSelectorVisible: Binding var body: some View { if viewModel.sites.count > 1 && !(viewModel.selectedSite == nil) { @@ -41,13 +41,13 @@ struct SiteSelector: View { FloorFilterList( "Select a site…", sites: viewModel.sites, - showSiteSelector: showSiteSelector + isSelectorVisible: isSelectorVisible ) } else { FloorFilterList( "Select a facility...", facilities: viewModel.facilities, - showSiteSelector: showSiteSelector + isSelectorVisible: isSelectorVisible ) } } @@ -59,33 +59,33 @@ struct SiteSelector: View { private let facilities: [FloorFacility] /// Binding allowing the user to toggle the visibility of the results list. - private var showSiteSelector: Binding + private var isSelectorVisible: Binding /// Creates a `FloorFilterList` /// - Parameters: /// - title: The title of the list. /// - sites: The sites to display. - /// - showSiteSelector: A binding used to dismiss the site selector. + /// - isSelectorVisible: A binding used to dismiss the site selector. init( _ title: String, sites: [FloorSite], - showSiteSelector: Binding + isSelectorVisible: Binding ) { self.title = title self.sites = sites facilities = [] - self.showSiteSelector = showSiteSelector + self.isSelectorVisible = isSelectorVisible } init( _ title: String, facilities: [FloorFacility], - showSiteSelector: Binding + isSelectorVisible: Binding ) { self.title = title self.facilities = facilities sites = [] - self.showSiteSelector = showSiteSelector + self.isSelectorVisible = isSelectorVisible } var body: some View { @@ -95,7 +95,7 @@ struct SiteSelector: View { .bold() Spacer() Button { - showSiteSelector.wrappedValue.toggle() + isSelectorVisible.wrappedValue.toggle() } label: { Image(systemName: "xmark.circle") } From fa17305427cdac82260cd725c71cede33f71b36b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 1 Feb 2022 07:03:57 -0600 Subject: [PATCH 0422/1722] PR review changes. --- .../Components/FloorFilter/FloorFilter.swift | 4 +-- .../FloorFilter/FloorFilterViewModel.swift | 2 +- .../Components/FloorFilter/SiteSelector.swift | 26 +++++++++---------- .../ArcGISToolkit/Extensions/FloorSite.swift | 4 ++- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 6ca703c1f..28e3da151 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -43,14 +43,14 @@ public struct FloorFilter: View { Button { isSelectorVisible.toggle() } label: { - Image.init("Site", bundle: Bundle.module) + Image.init("Site", bundle: Bundle.module, label: Text("Site")) } } .esriBorder() if isSelectorVisible { SiteSelector( viewModel, - isSelectorVisible: $isSelectorVisible + isVisible: $isSelectorVisible ) .frame(width: 200) } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index c88967802..7f35f7355 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -55,7 +55,7 @@ final public class FloorFilterViewModel: ObservableObject { /// `true` if the model is loading it's properties, `false` if not loading. @Published - public private(set) var isLoading = true + private(set) var isLoading = true /// Facilities in the selected site. If no site is selected then the list is empty. /// If the sites list is empty, all facilities will be returned. diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift index dec8cc77d..817741acb 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift @@ -18,13 +18,13 @@ import ArcGIS struct SiteSelector: View { /// Creates a `SiteSelector` /// - Parameter floorFilterViewModel: The view model used by the `SiteSelector`. - /// - Parameter isSelectorVisible: A binding used to dismiss the site selector. + /// - Parameter isVisible: A binding used to dismiss the site selector. public init( _ floorFilterViewModel: FloorFilterViewModel, - isSelectorVisible: Binding + isVisible: Binding ) { self.viewModel = floorFilterViewModel - self.isSelectorVisible = isSelectorVisible + self.isVisible = isVisible } /// The view model used by the `SiteSelector`. @@ -32,7 +32,7 @@ struct SiteSelector: View { private var viewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site selector. - private var isSelectorVisible: Binding + private var isVisible: Binding var body: some View { if viewModel.sites.count > 1 && !(viewModel.selectedSite == nil) { @@ -41,13 +41,13 @@ struct SiteSelector: View { FloorFilterList( "Select a site…", sites: viewModel.sites, - isSelectorVisible: isSelectorVisible + isVisible: isVisible ) } else { FloorFilterList( "Select a facility...", facilities: viewModel.facilities, - isSelectorVisible: isSelectorVisible + isVisible: isVisible ) } } @@ -59,33 +59,33 @@ struct SiteSelector: View { private let facilities: [FloorFacility] /// Binding allowing the user to toggle the visibility of the results list. - private var isSelectorVisible: Binding + private var isVisible: Binding /// Creates a `FloorFilterList` /// - Parameters: /// - title: The title of the list. /// - sites: The sites to display. - /// - isSelectorVisible: A binding used to dismiss the site selector. + /// - isVisible: A binding used to dismiss the site selector. init( _ title: String, sites: [FloorSite], - isSelectorVisible: Binding + isVisible: Binding ) { self.title = title self.sites = sites facilities = [] - self.isSelectorVisible = isSelectorVisible + self.isVisible = isVisible } init( _ title: String, facilities: [FloorFacility], - isSelectorVisible: Binding + isVisible: Binding ) { self.title = title self.facilities = facilities sites = [] - self.isSelectorVisible = isSelectorVisible + self.isVisible = isVisible } var body: some View { @@ -95,7 +95,7 @@ struct SiteSelector: View { .bold() Spacer() Button { - isSelectorVisible.wrappedValue.toggle() + isVisible.wrappedValue.toggle() } label: { Image(systemName: "xmark.circle") } diff --git a/Sources/ArcGISToolkit/Extensions/FloorSite.swift b/Sources/ArcGISToolkit/Extensions/FloorSite.swift index 3fcc5d196..2cf858aad 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorSite.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorSite.swift @@ -20,4 +20,6 @@ extension FloorSite: Equatable { } } -extension FloorSite: Identifiable { } +extension FloorSite: Identifiable { + public var id: String { siteId } +} From 92140e8d576c6609005068a591147b536b3a3a48 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 1 Feb 2022 09:20:55 -0600 Subject: [PATCH 0423/1722] PR review changes. --- .../Components/FloorFilter/FloorFilter.swift | 2 +- .../FloorFilter/FloorFilterViewModel.swift | 77 +++++++++++++------ ...or.swift => SiteAndFacilitySelector.swift} | 10 +-- 3 files changed, 60 insertions(+), 29 deletions(-) rename Sources/ArcGISToolkit/Components/FloorFilter/{SiteSelector.swift => SiteAndFacilitySelector.swift} (94%) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 28e3da151..b974dce63 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -48,7 +48,7 @@ public struct FloorFilter: View { } .esriBorder() if isSelectorVisible { - SiteSelector( + SiteAndFacilitySelector( viewModel, isVisible: $isSelectorVisible ) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 7f35f7355..a61de6611 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -17,6 +17,16 @@ import ArcGIS /// Manages the state for a `FloorFilter`. @MainActor final public class FloorFilterViewModel: ObservableObject { + /// The current selection. + enum Selection { + /// The selected site. + case site(FloorSite) + /// The selected facility. + case facility(FloorFacility) + /// The selected level. + case level(FloorLevel) + } + /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - floorManager: A floor manager. @@ -87,41 +97,62 @@ final public class FloorFilterViewModel: ObservableObject { floorManager.levels } - /// The selected site. - @Published - public var selectedSite: FloorSite? = nil { + /// The selected site, floor, or level. + public var selection: Selection? { didSet { - zoomToSite() + zoomToSelection() } } - - /// The selected facility. + + /// The selected site. @Published - public var selectedFacility: FloorFacility? = nil { - didSet { - zoomToFacility() + var selectedSite: FloorSite? { + guard let selection = selection else { + return nil + } + + switch selection { + case .site(let site): + return site + case .facility(let facility): + return facility.site + case .level(let level): + return level.facility?.site } } - - /// The selected level. - @Published - public var selectedLevel: FloorLevel? = nil - /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. - public func zoomToSelection() { - if selectedFacility != nil { - zoomToFacility() - } else if selectedSite != nil { - zoomToSite() + /// The selected facility. + var selectedFacility: FloorFacility? { + guard let selection = selection else { + return nil + } + + switch selection { + case .site: + return nil + case .facility(let facility): + return facility + case .level(let level): + return level.facility } } - private func zoomToSite() { - zoomToExtent(extent: selectedSite?.geometry?.extent) + /// The selected level. + var selectedLevel: FloorLevel? { + if case let .level(level) = selection { + return level + } else { + return nil + } } - private func zoomToFacility() { - zoomToExtent(extent: selectedFacility?.geometry?.extent) + /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. + public func zoomToSelection() { + if let selectedFacility = selectedFacility { + zoomToExtent(extent: selectedFacility.geometry?.extent) + } else if let selectedSite = selectedSite { + zoomToExtent(extent: selectedSite.geometry?.extent) + } } private func zoomToExtent(extent: Envelope?) { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift similarity index 94% rename from Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift rename to Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 817741acb..4de16ae71 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -15,9 +15,9 @@ import SwiftUI import ArcGIS /// A view which allows selection of sites and facilities represented in a `FloorManager`. -struct SiteSelector: View { - /// Creates a `SiteSelector` - /// - Parameter floorFilterViewModel: The view model used by the `SiteSelector`. +struct SiteAndFacilitySelector: View { + /// Creates a `SiteAndFacilitySelector` + /// - Parameter floorFilterViewModel: The view model used by the `SiteAndFacilitySelector`. /// - Parameter isVisible: A binding used to dismiss the site selector. public init( _ floorFilterViewModel: FloorFilterViewModel, @@ -27,7 +27,7 @@ struct SiteSelector: View { self.isVisible = isVisible } - /// The view model used by the `SiteSelector`. + /// The view model used by the `SiteAndFacilitySelector`. @ObservedObject private var viewModel: FloorFilterViewModel @@ -45,7 +45,7 @@ struct SiteSelector: View { ) } else { FloorFilterList( - "Select a facility...", + "Select a facility…", facilities: viewModel.facilities, isVisible: isVisible ) From c9d3ea3e995162a64e384914f3eacdc4e9e3dadd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 1 Feb 2022 09:50:51 -0600 Subject: [PATCH 0424/1722] PR review changes; updates to Site/Facility selector. --- .../FloorFilter/FloorFilterViewModel.swift | 6 +- .../FloorFilter/SiteAndFacilitySelector.swift | 104 ++++++++---------- 2 files changed, 47 insertions(+), 63 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index a61de6611..9cfec9bac 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -18,7 +18,7 @@ import ArcGIS @MainActor final public class FloorFilterViewModel: ObservableObject { /// The current selection. - enum Selection { + public enum Selection { /// The selected site. case site(FloorSite) /// The selected facility. @@ -42,7 +42,7 @@ final public class FloorFilterViewModel: ObservableObject { try await floorManager.load() if sites.count == 1 { // If we have only one site, select it. - selectedSite = sites.first + selection = .site(sites.first!) } } catch { print("error: \(error)") @@ -98,6 +98,7 @@ final public class FloorFilterViewModel: ObservableObject { } /// The selected site, floor, or level. + @Published public var selection: Selection? { didSet { zoomToSelection() @@ -105,7 +106,6 @@ final public class FloorFilterViewModel: ObservableObject { } /// The selected site. - @Published var selectedSite: FloorSite? { guard let selection = selection else { return nil diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 4de16ae71..46ab2b0e2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -31,81 +31,43 @@ struct SiteAndFacilitySelector: View { @ObservedObject private var viewModel: FloorFilterViewModel - /// Allows the user to toggle the visibility of the site selector. + /// Allows the user to toggle the visibility of the site and facility selector. private var isVisible: Binding var body: some View { if viewModel.sites.count > 1 && !(viewModel.selectedSite == nil) { // Only show site list if there is more than one site // and the user has not yet selected a site. - FloorFilterList( - "Select a site…", - sites: viewModel.sites, - isVisible: isVisible - ) + Sites(sites: viewModel.sites, isVisible: isVisible) } else { - FloorFilterList( - "Select a facility…", - facilities: viewModel.facilities, - isVisible: isVisible - ) + Facilities(facilities: viewModel.facilities, isVisible: isVisible) } } - /// A view displaying either the sites or facilities contained in a `FloorManager`. - struct FloorFilterList: View { - private let title: String - private let sites: [FloorSite] - private let facilities: [FloorFacility] - - /// Binding allowing the user to toggle the visibility of the results list. - private var isVisible: Binding - - /// Creates a `FloorFilterList` - /// - Parameters: - /// - title: The title of the list. - /// - sites: The sites to display. - /// - isVisible: A binding used to dismiss the site selector. - init( - _ title: String, - sites: [FloorSite], - isVisible: Binding - ) { - self.title = title - self.sites = sites - facilities = [] - self.isVisible = isVisible - } - - init( - _ title: String, - facilities: [FloorFacility], - isVisible: Binding - ) { - self.title = title - self.facilities = facilities - sites = [] - self.isVisible = isVisible - } - + /// A view displaying the sites contained in a `FloorManager`. + struct Sites: View { + let sites: [FloorSite] + var isVisible: Binding + var body: some View { LazyVStack(alignment: .leading) { - HStack { - Text(title) - .bold() - Spacer() - Button { - isVisible.wrappedValue.toggle() - } label: { - Image(systemName: "xmark.circle") - } - } - Rectangle() - .frame(height: 1) - .foregroundColor(.secondary) + Header(title: "Select a site…", isVisible: isVisible) ForEach(sites) { site in Text(site.name) } + } + .esriBorder() + } + } + + /// A view displaying the facilities contained in a `FloorManager`. + struct Facilities: View { + let facilities: [FloorFacility] + var isVisible: Binding + + var body: some View { + LazyVStack(alignment: .leading) { + Header(title: "Select a facility…", isVisible: isVisible) ForEach(facilities) { facility in Text(facility.name) } @@ -113,4 +75,26 @@ struct SiteAndFacilitySelector: View { .esriBorder() } } + + /// The header for a site or facility selector. + struct Header: View { + let title: String + var isVisible: Binding + + var body: some View { + HStack { + Text(title) + .bold() + Spacer() + Button { + isVisible.wrappedValue.toggle() + } label: { + Image(systemName: "xmark.circle") + } + } + Rectangle() + .frame(height: 1) + .foregroundColor(.secondary) + } + } } From 366c9d362ccb99b9aafac9f7a87d52abf79d4c45 Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Thu, 3 Feb 2022 10:03:02 -0800 Subject: [PATCH 0425/1722] update manifest to use new Swift API package name --- Package.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index f35cc529e..34e523cff 100644 --- a/Package.swift +++ b/Package.swift @@ -27,12 +27,14 @@ let package = Package( ), ], dependencies: [ - .package(name: "ArcGIS", path: "../swift/ArcGIS") + .package(name: "arcgis-runtime-swift", path: "../swift/ArcGIS") ], targets: [ .target( name: "ArcGISToolkit", - dependencies: ["ArcGIS"] + dependencies: [ + .product(name: "ArcGIS", package: "arcgis-runtime-swift") + ] ), .testTarget( name: "ArcGISToolkitTests", From a3c102b18b293d939c283df7939bd695faac74ab Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 4 Feb 2022 06:48:52 -0600 Subject: [PATCH 0426/1722] Apply suggestions from code review Co-authored-by: Philip Ridgeway --- .../Components/FloorFilter/FloorFilter.swift | 26 +++++++++--------- .../FloorFilter/FloorFilterViewModel.swift | 27 ++++++++++++------- .../FloorFilter/SiteAndFacilitySelector.swift | 6 ++--- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index b974dce63..a4a3c5b8c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -15,12 +15,12 @@ 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 `GeoView` +/// 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 { /// Creates a `FloorFilter` /// - Parameter viewModel: The view model used by the `FloorFilter`. - public init(_ viewModel: FloorFilterViewModel) { + public init(viewModel: FloorFilterViewModel) { self.viewModel = viewModel } @@ -33,18 +33,16 @@ public struct FloorFilter: View { private var isSelectorVisible: Bool = false public var body: some View { - HStack(alignment: .bottom) { - if viewModel.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .esriBorder() - } else { - VStack { - Button { - isSelectorVisible.toggle() - } label: { - Image.init("Site", bundle: Bundle.module, label: Text("Site")) - } + if viewModel.isLoading { + ProgressView() + .progressViewStyle(.circular) + .esriBorder() + } else { + HStack(alignment: .bottom) { + Button { + isSelectorVisible.toggle() + } label: { + Image("Site", bundle: .module, label: Text("Site")) } .esriBorder() if isSelectorVisible { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 9cfec9bac..554accba0 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -17,13 +17,13 @@ import ArcGIS /// Manages the state for a `FloorFilter`. @MainActor final public class FloorFilterViewModel: ObservableObject { - /// The current selection. + /// A selected site, floor, or level. public enum Selection { - /// The selected site. + /// A selected site. case site(FloorSite) - /// The selected facility. + /// A selected facility. case facility(FloorFacility) - /// The selected level. + /// A selected level. case level(FloorLevel) } @@ -53,10 +53,10 @@ final public class FloorFilterViewModel: ObservableObject { /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. - var viewpoint: Binding? = nil + let viewpoint: Binding? /// The `FloorManager` containing the site, floor, and level information. - var floorManager: FloorManager + let floorManager: FloorManager /// The floor manager sites. public var sites: [FloorSite] { @@ -148,10 +148,17 @@ final public class FloorFilterViewModel: ObservableObject { /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. public func zoomToSelection() { - if let selectedFacility = selectedFacility { - zoomToExtent(extent: selectedFacility.geometry?.extent) - } else if let selectedSite = selectedSite { - zoomToExtent(extent: selectedSite.geometry?.extent) + guard let selection = selection else { + return nil + } + + switch selection { + case .site(let site): + zoomToExtent(extent: site.geometry?.extent) + case .facility(let facility): + zoomToExtent(extent: facility.geometry?.extent) + case .level: + break } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 46ab2b0e2..300de0f62 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -19,8 +19,8 @@ struct SiteAndFacilitySelector: View { /// Creates a `SiteAndFacilitySelector` /// - Parameter floorFilterViewModel: The view model used by the `SiteAndFacilitySelector`. /// - Parameter isVisible: A binding used to dismiss the site selector. - public init( - _ floorFilterViewModel: FloorFilterViewModel, + init( + floorFilterViewModel: FloorFilterViewModel, isVisible: Binding ) { self.viewModel = floorFilterViewModel @@ -89,7 +89,7 @@ struct SiteAndFacilitySelector: View { Button { isVisible.wrappedValue.toggle() } label: { - Image(systemName: "xmark.circle") + Image(systemName: "xmark.circle", label: Text("Close")) } } Rectangle() From 88360e2623f06241c9240a3d53609b317653f853 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 4 Feb 2022 10:28:38 -0600 Subject: [PATCH 0427/1722] PR review changes --- .../FloorFilter/FloorFilterViewModel.swift | 34 ++++--------------- .../FloorFilter/SiteAndFacilitySelector.swift | 26 +++++++------- 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 9cfec9bac..f3b85bddd 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -63,39 +63,19 @@ final public class FloorFilterViewModel: ObservableObject { floorManager.sites } - /// `true` if the model is loading it's properties, `false` if not loading. - @Published - private(set) var isLoading = true - - /// Facilities in the selected site. If no site is selected then the list is empty. - /// If the sites list is empty, all facilities will be returned. + /// The floor manager facilities. public var facilities: [FloorFacility] { - sites.isEmpty ? floorManager.facilities : floorManager.facilities.filter { - $0.site == selectedSite - } + floorManager.facilities } - /// Levels in the selected facility. If no facility is selected then the list is empty. - /// If the facilities list is empty, all levels will be returned. + /// The floor manager levels. public var levels: [FloorLevel] { - facilities.isEmpty ? floorManager.levels : floorManager.levels.filter { - $0.facility == selectedFacility - } - } - - /// Levels in the selected facility. If no facility is selected then the list is empty. - /// If the facilities list is empty, all levels will be returned. - /// The levels are sorted by `verticalOrder` in descending order. - public var sortedLevels: [FloorLevel] { - levels.sorted { - $0.verticalOrder > $1.verticalOrder - } - } - - /// All the levels in the map - public var allLevels: [FloorLevel] { floorManager.levels } + + /// `true` if the model is loading it's properties, `false` if not loading. + @Published + private(set) var isLoading = true /// The selected site, floor, or level. @Published diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 46ab2b0e2..93e56180e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -23,24 +23,27 @@ struct SiteAndFacilitySelector: View { _ floorFilterViewModel: FloorFilterViewModel, isVisible: Binding ) { - self.viewModel = floorFilterViewModel + self.floorFilterViewModel = floorFilterViewModel self.isVisible = isVisible } /// The view model used by the `SiteAndFacilitySelector`. @ObservedObject - private var viewModel: FloorFilterViewModel + private var floorFilterViewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site and facility selector. private var isVisible: Binding var body: some View { - if viewModel.sites.count > 1 && !(viewModel.selectedSite == nil) { - // Only show site list if there is more than one site - // and the user has not yet selected a site. - Sites(sites: viewModel.sites, isVisible: isVisible) + if let selectedSite = floorFilterViewModel.selectedSite { + Facilities(facilities: selectedSite.facilities, isVisible: isVisible) + } else if floorFilterViewModel.sites.count == 1 { + Facilities( + facilities: floorFilterViewModel.sites.first!.facilities, + isVisible: isVisible + ) } else { - Facilities(facilities: viewModel.facilities, isVisible: isVisible) + Sites(sites: floorFilterViewModel.sites, isVisible: isVisible) } } @@ -50,8 +53,9 @@ struct SiteAndFacilitySelector: View { var isVisible: Binding var body: some View { - LazyVStack(alignment: .leading) { + VStack(alignment: .leading) { Header(title: "Select a site…", isVisible: isVisible) + Divider() ForEach(sites) { site in Text(site.name) } @@ -66,8 +70,9 @@ struct SiteAndFacilitySelector: View { var isVisible: Binding var body: some View { - LazyVStack(alignment: .leading) { + VStack(alignment: .leading) { Header(title: "Select a facility…", isVisible: isVisible) + Divider() ForEach(facilities) { facility in Text(facility.name) } @@ -92,9 +97,6 @@ struct SiteAndFacilitySelector: View { Image(systemName: "xmark.circle") } } - Rectangle() - .frame(height: 1) - .foregroundColor(.secondary) } } } From 057a7b9a7e74fce131dc0b78c3e9c8dc4735f48b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 4 Feb 2022 10:35:03 -0600 Subject: [PATCH 0428/1722] compile fixes after code review changes. --- Examples/Examples/FloorFilterExampleView.swift | 2 +- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 2 +- .../Components/FloorFilter/FloorFilterViewModel.swift | 2 +- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 138a06e64..6f8cda1e1 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -55,7 +55,7 @@ struct FloorFilterExampleView: View { ) .overlay(alignment: .bottomLeading) { if let viewModel = floorFilterViewModel { - FloorFilter(viewModel) + FloorFilter(viewModel: viewModel) .padding(floorFilterPadding) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index a4a3c5b8c..e87138eaa 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -47,7 +47,7 @@ public struct FloorFilter: View { .esriBorder() if isSelectorVisible { SiteAndFacilitySelector( - viewModel, + floorFilterViewModel: viewModel, isVisible: $isSelectorVisible ) .frame(width: 200) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index f10f35d40..b61d1b831 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -129,7 +129,7 @@ final public class FloorFilterViewModel: ObservableObject { /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. public func zoomToSelection() { guard let selection = selection else { - return nil + return } switch selection { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index cbe4dfa3f..a72744dbe 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -94,7 +94,7 @@ struct SiteAndFacilitySelector: View { Button { isVisible.wrappedValue.toggle() } label: { - Image(systemName: "xmark.circle", label: Text("Close")) + Image(systemName: "xmark.circle") } } } From 0716e5be9f6f3101021ac3f00ef91ec2236e48b7 Mon Sep 17 00:00:00 2001 From: Zachary A Kline Date: Fri, 4 Feb 2022 09:11:04 -0800 Subject: [PATCH 0429/1722] Update Package.swift Co-authored-by: Mark Dostal --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index 34e523cff..f79a824ed 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,7 @@ let package = Package( ), ], dependencies: [ + // To use a daily build of the Swift API, change the path below to point to the daily build's `output` folder. .package(name: "arcgis-runtime-swift", path: "../swift/ArcGIS") ], targets: [ From 185565c6f32b300870355592ad580fbc9f956071 Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Fri, 4 Feb 2022 10:57:34 -0800 Subject: [PATCH 0430/1722] update xcscheme --- .../arcgis-runtime-toolkit-swift.xcscheme | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme index f1bfb0c72..ec08c65b0 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/arcgis-runtime-toolkit-swift.xcscheme @@ -74,19 +74,9 @@ skipped = "NO"> - - - - From c55035cb083f54907ac280813cdb96bcc74e0e25 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 4 Feb 2022 17:15:40 -0600 Subject: [PATCH 0431/1722] Add initial compass implementation --- Examples/Examples.xcodeproj/project.pbxproj | 4 + Examples/Examples/CompassExampleView.swift | 44 +++++++++ Examples/ExamplesApp/Examples.swift | 3 +- .../Components/Compass/Compass.swift | 96 +++++++++++++++++++ .../Components/Compass/CompassBody.swift | 31 ++++++ .../Components/Compass/Needle/Needle.swift | 54 +++++++++++ .../Compass/Needle/NeedleCenter.swift | 24 +++++ .../Compass/Needle/NeedleQuadrant.swift | 31 ++++++ 8 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 Examples/Examples/CompassExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/Compass/Compass.swift create mode 100644 Sources/ArcGISToolkit/Components/Compass/CompassBody.swift create mode 100644 Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift create mode 100644 Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift create mode 100644 Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 89e1e6311..146babe4f 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75657E4727ABAC8400EE865B /* CompassExampleView.swift */; }; E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; E47ABE482652FE0C00FD2FE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E47ABE472652FE0C00FD2FE3 /* Assets.xcassets */; }; @@ -36,6 +37,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 75657E4727ABAC8400EE865B /* CompassExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExampleView.swift; sourceTree = ""; }; E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; @@ -73,6 +75,7 @@ E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, E42BFBE82672BF9500159107 /* SearchExampleView.swift */, E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, + 75657E4727ABAC8400EE865B /* CompassExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -209,6 +212,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */, E48A73452658227100F5C118 /* Examples.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift new file mode 100644 index 000000000..6bf33ec22 --- /dev/null +++ b/Examples/Examples/CompassExampleView.swift @@ -0,0 +1,44 @@ +// 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 +import ArcGISToolkit +import SwiftUI + +struct CompassExampleView: View { + /// The map displayed in the map view. + private let map = Map(basemapStyle: .arcGISImagery) + + /// Allows for communication between the Compass and MapView. + @State + private var viewpoint: Viewpoint + + init() { + _viewpoint = State(initialValue: Viewpoint( + center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), + scale: 10_000, + rotation: 0.0 + )) + } + + var body: some View { + MapView(map: map, viewpoint: viewpoint) + .snapToNorthThreshold(0) + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: .topTrailing) { + Compass(viewpoint: $viewpoint, size: 75, autoHide: true) + .padding() + } + } +} + diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index aacf22bc5..cb3e6e83f 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -37,7 +37,8 @@ extension ExampleList { examples: [ AnyExample("Basemap Gallery", content: BasemapGalleryExampleView()), AnyExample("Overview Map", content: OverviewMapExampleView()), - AnyExample("Search", content: SearchExampleView()) + AnyExample("Search", content: SearchExampleView()), + AnyExample("Compass", content: CompassExampleView()) ] ) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift new file mode 100644 index 000000000..7adad1808 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -0,0 +1,96 @@ +// 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 +import SwiftUI + +public struct Compass: View { + var autoHide: Bool + + @Binding var viewpoint: Viewpoint + + @State var opacity: Double + + @State public var height: Double + + @State public var width: Double + + private var heading: String { + "Compass, heading " + + Int(viewpoint.rotation.rounded()).description + + " degrees " + + Int(viewpoint.rotation.rounded()).asCardinalOrIntercardinal + } + + public init( + viewpoint: Binding, + size: Double = 30.0, + autoHide: Bool = true + ) { + self._viewpoint = viewpoint + self.autoHide = autoHide + height = size + width = size + opacity = viewpoint.wrappedValue.rotation.isZero ? 0 : 1 + } + + public var body: some View { + ZStack { + CompassBody() + Needle() + .rotationEffect(Angle(degrees: viewpoint.rotation)) + } + .frame(width: width, height: height) + .opacity(opacity) + .onTapGesture { + viewpoint = Viewpoint( + center: viewpoint.targetGeometry.extent.center, + scale: viewpoint.targetScale, + rotation: 0.0 + ) + } + .onChange(of: viewpoint, perform: { _ in + let hide = viewpoint.rotation.isZero && autoHide + withAnimation(.default.delay(hide ? 0.25 : 0)) { + opacity = hide ? 0 : 1 + } + }) + .accessibilityLabel(heading) + } +} + +extension Color { + init(red: Int, green: Int, blue: Int) { + self.init( + red: Double(red)/255, + green: Double(green)/255, + blue: Double(blue)/255 + ) + } +} + +extension Int { + var asCardinalOrIntercardinal: String { + switch self { + case 0...22, 338...360: return "north" + case 23...67: return "northeast" + case 68...112: return "east" + case 113...157: return "southeast" + case 158...202: return "south" + case 203...247: return "southwest" + case 248...290: return "west" + case 291...337: return "northwest" + default: return "" + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift new file mode 100644 index 000000000..25811a988 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift @@ -0,0 +1,31 @@ +// 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 + +struct CompassBody: View { + private let fill = Color(red: 228, green: 240, blue: 244) + + private let outline = Color(red: 127, green: 127, blue: 127) + + var body: some View { + GeometryReader { geometry in + let borderWidth = geometry.size.width * 0.025 + Circle() + .inset(by: borderWidth / 2.0) + .stroke(lineWidth: borderWidth) + .foregroundColor(outline) + .background(Circle().foregroundColor(fill)) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift new file mode 100644 index 000000000..d4dcacaa8 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift @@ -0,0 +1,54 @@ +// 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 + +struct Needle: View { + private let grayDark = Color(red: 128, green: 128, blue: 128) + + private let grayLight = Color(red: 169, green: 168, blue: 168) + + private let redDark = Color(red: 124, green: 22, blue: 13) + + private let redLight = Color(red: 233, green: 51, blue: 35) + + var body: some View { + ZStack { + VStack(spacing: 0) { + HStack(spacing: 0) { + NeedleQuadrant(color: redLight) + NeedleQuadrant(color: redDark) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 0, y: 1, z: 0)) + } + HStack(spacing: 0) { + NeedleQuadrant(color: grayLight) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 1, y: 0, z: 0)) + NeedleQuadrant(color: grayDark) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 0, y: 1, z: 0)) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 1, y: 0, z: 0)) + } + } + NeedleCenter() + } + .aspectRatio(1.0/3.0, contentMode: .fit) + .scaleEffect(0.6) + } +} diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift new file mode 100644 index 000000000..bffe4141c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift @@ -0,0 +1,24 @@ +// 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 + +struct NeedleCenter: View { + private let color = Color(red: 241, green: 169, blue: 59) + + var body: some View { + Circle() + .scale(0.25) + .foregroundColor(color) + } +} diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift new file mode 100644 index 000000000..0261f4c45 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift @@ -0,0 +1,31 @@ +// 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 + +struct NeedleQuadrant: View { + let color: Color + + var body: some View { + GeometryReader { geometry in + Path { path in + let width = geometry.size.width + let height = geometry.size.height + path.move(to: CGPoint(x: 0, y: height)) + path.addLine(to: CGPoint(x: width, y: 0)) + path.addLine(to: CGPoint(x: width, y: height)) + } + .fill(color) + } + } +} From d28d139fedad1b9637a544563adb52b747757f90 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 11:15:36 -0600 Subject: [PATCH 0432/1722] Fix rotation direction, adjust access control --- Examples/Examples/CompassExampleView.swift | 1 - .../Components/Compass/Compass.swift | 24 ++++++++----------- Sources/ArcGISToolkit/Extensions/Color.swift | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Color.swift diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 6bf33ec22..2550fc671 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -41,4 +41,3 @@ struct CompassExampleView: View { } } } - diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 7adad1808..943b9d6fd 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -27,9 +27,9 @@ public struct Compass: View { private var heading: String { "Compass, heading " - + Int(viewpoint.rotation.rounded()).description + + Int(viewpoint.adjustedRotation.rounded()).description + " degrees " - + Int(viewpoint.rotation.rounded()).asCardinalOrIntercardinal + + Int(viewpoint.adjustedRotation.rounded()).asCardinalOrIntercardinal } public init( @@ -48,7 +48,7 @@ public struct Compass: View { ZStack { CompassBody() Needle() - .rotationEffect(Angle(degrees: viewpoint.rotation)) + .rotationEffect(Angle(degrees: viewpoint.adjustedRotation)) } .frame(width: width, height: height) .opacity(opacity) @@ -69,17 +69,7 @@ public struct Compass: View { } } -extension Color { - init(red: Int, green: Int, blue: Int) { - self.init( - red: Double(red)/255, - green: Double(green)/255, - blue: Double(blue)/255 - ) - } -} - -extension Int { +fileprivate extension Int { var asCardinalOrIntercardinal: String { switch self { case 0...22, 338...360: return "north" @@ -94,3 +84,9 @@ extension Int { } } } + +fileprivate extension Viewpoint { + var adjustedRotation: Double { + self.rotation == 0 ? self.rotation : 360 - self.rotation + } +} diff --git a/Sources/ArcGISToolkit/Extensions/Color.swift b/Sources/ArcGISToolkit/Extensions/Color.swift new file mode 100644 index 000000000..e5f493993 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Color.swift @@ -0,0 +1,24 @@ +// 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 + +extension Color { + init(red: Int, green: Int, blue: Int) { + self.init( + red: Double(red)/255, + green: Double(green)/255, + blue: Double(blue)/255 + ) + } +} From 18f4ec5ea866153609bd2f3189a58beae4724bdf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 15:19:03 -0600 Subject: [PATCH 0433/1722] Add inline documentation --- Examples/Examples/CompassExampleView.swift | 4 ++-- .../ArcGISToolkit/Components/Compass/Compass.swift | 14 ++++++++++++-- .../Components/Compass/CompassBody.swift | 3 +++ .../Components/Compass/Needle/Needle.swift | 5 +++++ .../Components/Compass/Needle/NeedleCenter.swift | 2 ++ .../Components/Compass/Needle/NeedleQuadrant.swift | 2 ++ 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 2550fc671..2315ed6b1 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -27,7 +27,7 @@ struct CompassExampleView: View { _viewpoint = State(initialValue: Viewpoint( center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), scale: 10_000, - rotation: 0.0 + rotation: 0 )) } @@ -36,7 +36,7 @@ struct CompassExampleView: View { .snapToNorthThreshold(0) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint, size: 75, autoHide: true) + Compass(viewpoint: $viewpoint, size: 75) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 943b9d6fd..af9319c98 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -14,17 +14,25 @@ import ArcGIS import SwiftUI +/// A Compass (alias North arrow) shows where north is in a MapView or SceneView. public struct Compass: View { - var autoHide: Bool - + /// Acts as link between the compass and the parent map or scene view. @Binding var viewpoint: Viewpoint + /// Controls visibility of the compass such for when `autoHide` is enabled. @State var opacity: Double + /// Determines if the compass should automatically hide/show itself when the parent view is oriented + /// north. + @State public var autoHide: Bool + + /// The height of the compass. @State public var height: Double + /// The width of the compass. @State public var width: Double + /// A text description of the current heading, sutiable for accessibility voiceover. private var heading: String { "Compass, heading " + Int(viewpoint.adjustedRotation.rounded()).description @@ -70,6 +78,7 @@ public struct Compass: View { } fileprivate extension Int { + /// A representation of an integer's associated cardinal or intercardinal direction. var asCardinalOrIntercardinal: String { switch self { case 0...22, 338...360: return "north" @@ -86,6 +95,7 @@ fileprivate extension Int { } fileprivate extension Viewpoint { + /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. var adjustedRotation: Double { self.rotation == 0 ? self.rotation : 360 - self.rotation } diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift index 25811a988..a9502ceec 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift @@ -13,9 +13,12 @@ import SwiftUI +/// Represents the circular housing which encompasses the spinning needle at the center of the compass. struct CompassBody: View { + /// The background color of the compass housing. private let fill = Color(red: 228, green: 240, blue: 244) + /// The outline color of the compass housing. private let outline = Color(red: 127, green: 127, blue: 127) var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift index d4dcacaa8..1e9b50704 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift @@ -13,13 +13,18 @@ import SwiftUI +/// Represents the spinning needle at the center of the compass. struct Needle: View { + /// The dark gray color of the compass needle. private let grayDark = Color(red: 128, green: 128, blue: 128) + /// The light gray color of the compass needle. private let grayLight = Color(red: 169, green: 168, blue: 168) + /// The dark red color of the compass needle. private let redDark = Color(red: 124, green: 22, blue: 13) + /// The light red color of the compass needle. private let redLight = Color(red: 233, green: 51, blue: 35) var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift index bffe4141c..c3a1f88ee 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift @@ -13,7 +13,9 @@ import SwiftUI +/// Represents the center of the spinning needle at the center of the compass. struct NeedleCenter: View { + /// The bronze color component of the compass needle. private let color = Color(red: 241, green: 169, blue: 59) var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift index 0261f4c45..2ab8f4292 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift @@ -13,7 +13,9 @@ import SwiftUI +/// Represents 1/4 (one triangle) of the spinning needle at the center of the compass. struct NeedleQuadrant: View { + /// The color of this needle quadrant. let color: Color var body: some View { From c690adcddaa66d9d086664a9c07ff981a22a18a4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 17:16:41 -0600 Subject: [PATCH 0434/1722] Add documentation --- Documentation/Compass/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Documentation/Compass/README.md diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md new file mode 100644 index 000000000..4267ffe0e --- /dev/null +++ b/Documentation/Compass/README.md @@ -0,0 +1,24 @@ +# Compass + +The Compass (alias North arrow) shows where north is in the MapView or SceneView. The Compass supports reset rotation. + +The ArcGIS Runtime SDK currently supports rotating the map with 2-finger gesture on MapView and SceneView interactively by default and while the map will snap to north when rotating using gestures, the compass provides an easier way. The Compass Toolkit component will appear when the map is rotated and, when tapped, re-orientates the map back to north and hides the compass icon (note that the MapView auto-snaps back to north when it's within a threshold of north, and in that case the compass also auto hides). + +### Compass Behavior: + +Whenever the map is not orientated North (non-zero bearing) the compass appears. When reset to north, it disappears. A property allows you to disable the auto-hide feature so that it always shows. + +When the compass is tapped, the map orients back to north (zero bearing), the default orientation and the compass fades away, or after a short duration disappears. + +### Usage + +```swift +MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: .topTrailing) { + Compass(viewpoint: $viewpoint) + .padding() + } +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [CompassExampleView.swift](../../Examples/Examples/CompassExampleView.swift) in the project. \ No newline at end of file From 5ca669a814e763b92213a522b20d7ad0be01ff59 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 17:20:29 -0600 Subject: [PATCH 0435/1722] Update CompassExampleView.swift --- Examples/Examples/CompassExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 2315ed6b1..e6ec1a37e 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -19,7 +19,7 @@ struct CompassExampleView: View { /// The map displayed in the map view. private let map = Map(basemapStyle: .arcGISImagery) - /// Allows for communication between the Compass and MapView. + /// Allows for communication between the Compass and MapView or SceneView. @State private var viewpoint: Viewpoint From 66b674d1046823490cce09b54ad94f8227ffad68 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 17:21:01 -0600 Subject: [PATCH 0436/1722] Update README.md --- Documentation/Compass/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Compass/README.md b/Documentation/Compass/README.md index 4267ffe0e..b4713d91d 100644 --- a/Documentation/Compass/README.md +++ b/Documentation/Compass/README.md @@ -21,4 +21,4 @@ MapView(map: map, viewpoint: viewpoint) } ``` -To see it in action, try out the [Examples](../../Examples) and refer to [CompassExampleView.swift](../../Examples/Examples/CompassExampleView.swift) in the project. \ No newline at end of file +To see it in action, try out the [Examples](../../Examples/Examples) and refer to [CompassExampleView.swift](../../Examples/Examples/CompassExampleView.swift) in the project. From bf748e3f2096d48e7b787768a987df195002e7a0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 17:25:03 -0600 Subject: [PATCH 0437/1722] Pull extra spacing --- 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 af9319c98..b689e2ccc 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -95,7 +95,7 @@ fileprivate extension Int { } fileprivate extension Viewpoint { - /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. + /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. var adjustedRotation: Double { self.rotation == 0 ? self.rotation : 360 - self.rotation } From eff3e242c4f05457ab3954149a52c47ea1a11f7b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 17:28:08 -0600 Subject: [PATCH 0438/1722] Fix `asCardinalOrIntercardinal` intervals --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index b689e2ccc..0e2e9cb9f 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -87,8 +87,8 @@ fileprivate extension Int { case 113...157: return "southeast" case 158...202: return "south" case 203...247: return "southwest" - case 248...290: return "west" - case 291...337: return "northwest" + case 248...292: return "west" + case 293...337: return "northwest" default: return "" } } From 5e004841fb9fbce6e2fc91f048b19b8490841501 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Feb 2022 17:34:59 -0600 Subject: [PATCH 0439/1722] Documentation fix --- .../ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift index c3a1f88ee..38285d7f3 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift @@ -15,7 +15,7 @@ import SwiftUI /// Represents the center of the spinning needle at the center of the compass. struct NeedleCenter: View { - /// The bronze color component of the compass needle. + /// The bronze color of the center of the compass needle. private let color = Color(red: 241, green: 169, blue: 59) var body: some View { From 599ca4058435a382b405aa8a072d38373a31c8e7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 12:10:08 -0600 Subject: [PATCH 0440/1722] Add color extension initializer documentation and preconditions --- Sources/ArcGISToolkit/Extensions/Color.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ArcGISToolkit/Extensions/Color.swift b/Sources/ArcGISToolkit/Extensions/Color.swift index e5f493993..a8191af5d 100644 --- a/Sources/ArcGISToolkit/Extensions/Color.swift +++ b/Sources/ArcGISToolkit/Extensions/Color.swift @@ -14,7 +14,12 @@ import SwiftUI extension Color { + /// Initializes a new color with RGB integer values between 0 and 255 inclusive. init(red: Int, green: Int, blue: Int) { + let validRange = 0...255 + precondition(validRange.contains(red)) + precondition(validRange.contains(green)) + precondition(validRange.contains(blue)) self.init( red: Double(red)/255, green: Double(green)/255, From ab848a50c95aaab57c07dcae923e20c2855c0615 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 12:17:20 -0600 Subject: [PATCH 0441/1722] Move colors to static members in private extensions. --- .../Components/Compass/CompassBody.swift | 18 +++++----- .../Components/Compass/Needle/Needle.swift | 34 ++++++++++--------- .../Compass/Needle/NeedleCenter.swift | 10 +++--- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift index a9502ceec..9dc1ea035 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift @@ -15,20 +15,22 @@ import SwiftUI /// Represents the circular housing which encompasses the spinning needle at the center of the compass. struct CompassBody: View { - /// The background color of the compass housing. - private let fill = Color(red: 228, green: 240, blue: 244) - - /// The outline color of the compass housing. - private let outline = Color(red: 127, green: 127, blue: 127) - var body: some View { GeometryReader { geometry in let borderWidth = geometry.size.width * 0.025 Circle() .inset(by: borderWidth / 2.0) .stroke(lineWidth: borderWidth) - .foregroundColor(outline) - .background(Circle().foregroundColor(fill)) + .foregroundColor(Color.outline) + .background(Circle().foregroundColor(Color.fill)) } } } + +private extension Color { + /// The background color of the compass housing. + static let fill = Color(red: 228, green: 240, blue: 244) + + /// The outline color of the compass housing. + static let outline = Color(red: 127, green: 127, blue: 127) +} diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift index 1e9b50704..78192d5d7 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift @@ -15,34 +15,22 @@ import SwiftUI /// Represents the spinning needle at the center of the compass. struct Needle: View { - /// The dark gray color of the compass needle. - private let grayDark = Color(red: 128, green: 128, blue: 128) - - /// The light gray color of the compass needle. - private let grayLight = Color(red: 169, green: 168, blue: 168) - - /// The dark red color of the compass needle. - private let redDark = Color(red: 124, green: 22, blue: 13) - - /// The light red color of the compass needle. - private let redLight = Color(red: 233, green: 51, blue: 35) - var body: some View { ZStack { VStack(spacing: 0) { HStack(spacing: 0) { - NeedleQuadrant(color: redLight) - NeedleQuadrant(color: redDark) + NeedleQuadrant(color: Color.redLight) + NeedleQuadrant(color: Color.redDark) .rotation3DEffect( Angle(degrees: 180), axis: (x: 0, y: 1, z: 0)) } HStack(spacing: 0) { - NeedleQuadrant(color: grayLight) + NeedleQuadrant(color: Color.grayLight) .rotation3DEffect( Angle(degrees: 180), axis: (x: 1, y: 0, z: 0)) - NeedleQuadrant(color: grayDark) + NeedleQuadrant(color: Color.grayDark) .rotation3DEffect( Angle(degrees: 180), axis: (x: 0, y: 1, z: 0)) @@ -57,3 +45,17 @@ struct Needle: View { .scaleEffect(0.6) } } + +private extension Color { + /// The dark gray color of the compass needle. + static let grayDark = Color(red: 128, green: 128, blue: 128) + + /// The light gray color of the compass needle. + static let grayLight = Color(red: 169, green: 168, blue: 168) + + /// The dark red color of the compass needle. + static let redDark = Color(red: 124, green: 22, blue: 13) + + /// The light red color of the compass needle. + static let redLight = Color(red: 233, green: 51, blue: 35) +} diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift index 38285d7f3..17ccd708f 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift @@ -15,12 +15,14 @@ import SwiftUI /// Represents the center of the spinning needle at the center of the compass. struct NeedleCenter: View { - /// The bronze color of the center of the compass needle. - private let color = Color(red: 241, green: 169, blue: 59) - var body: some View { Circle() .scale(0.25) - .foregroundColor(color) + .foregroundColor(Color.bronze) } } + +private extension Color { + /// The bronze color of the center of the compass needle. + static let bronze = Color(red: 241, green: 169, blue: 59) +} From 97357d21f0f4609782529e5a0f1a73f719dc95af Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 12:19:12 -0600 Subject: [PATCH 0442/1722] Move Needle elements out of Needle folder --- .../ArcGISToolkit/Components/Compass/{Needle => }/Needle.swift | 0 .../Components/Compass/{Needle => }/NeedleCenter.swift | 0 .../Components/Compass/{Needle => }/NeedleQuadrant.swift | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Sources/ArcGISToolkit/Components/Compass/{Needle => }/Needle.swift (100%) rename Sources/ArcGISToolkit/Components/Compass/{Needle => }/NeedleCenter.swift (100%) rename Sources/ArcGISToolkit/Components/Compass/{Needle => }/NeedleQuadrant.swift (100%) diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Compass/Needle/Needle.swift rename to Sources/ArcGISToolkit/Components/Compass/Needle.swift diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift b/Sources/ArcGISToolkit/Components/Compass/NeedleCenter.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Compass/Needle/NeedleCenter.swift rename to Sources/ArcGISToolkit/Components/Compass/NeedleCenter.swift diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift b/Sources/ArcGISToolkit/Components/Compass/NeedleQuadrant.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Compass/Needle/NeedleQuadrant.swift rename to Sources/ArcGISToolkit/Components/Compass/NeedleQuadrant.swift From 8db53830dd43549b43a6f8f942119ca3958ca782 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 12:28:03 -0600 Subject: [PATCH 0443/1722] Modify access control --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 0e2e9cb9f..e6dc595cd 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -77,7 +77,7 @@ public struct Compass: View { } } -fileprivate extension Int { +private extension Int { /// A representation of an integer's associated cardinal or intercardinal direction. var asCardinalOrIntercardinal: String { switch self { @@ -94,7 +94,7 @@ fileprivate extension Int { } } -fileprivate extension Viewpoint { +private extension Viewpoint { /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. var adjustedRotation: Double { self.rotation == 0 ? self.rotation : 360 - self.rotation From 8b679f99672ba585a357e7c54f5551439c141b6d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 19:20:44 -0600 Subject: [PATCH 0444/1722] Switch to ViewModel --- Examples/Examples/CompassExampleView.swift | 15 ++-- .../Components/Compass/Compass.swift | 78 +++------------- .../Components/Compass/CompassViewModel.swift | 88 +++++++++++++++++++ .../CompassViewModelTests.swift | 79 +++++++++++++++++ 4 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift create mode 100644 Tests/ArcGISToolkitTests/CompassViewModelTests.swift diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index e6ec1a37e..8049ce72c 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -21,19 +21,14 @@ struct CompassExampleView: View { /// Allows for communication between the Compass and MapView or SceneView. @State - private var viewpoint: Viewpoint - - init() { - _viewpoint = State(initialValue: Viewpoint( - center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), - scale: 10_000, - rotation: 0 - )) - } + private var viewpoint = Viewpoint( + center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), + scale: 10_000, + rotation: 0 + ) var body: some View { MapView(map: map, viewpoint: viewpoint) - .snapToNorthThreshold(0) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { Compass(viewpoint: $viewpoint, size: 75) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index e6dc595cd..1f1219313 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -16,87 +16,37 @@ import SwiftUI /// A Compass (alias North arrow) shows where north is in a MapView or SceneView. public struct Compass: View { - /// Acts as link between the compass and the parent map or scene view. - @Binding var viewpoint: Viewpoint - - /// Controls visibility of the compass such for when `autoHide` is enabled. - @State var opacity: Double - - /// Determines if the compass should automatically hide/show itself when the parent view is oriented - /// north. - @State public var autoHide: Bool - - /// The height of the compass. - @State public var height: Double - - /// The width of the compass. - @State public var width: Double - - /// A text description of the current heading, sutiable for accessibility voiceover. - private var heading: String { - "Compass, heading " - + Int(viewpoint.adjustedRotation.rounded()).description - + " degrees " - + Int(viewpoint.adjustedRotation.rounded()).asCardinalOrIntercardinal - } + @ObservedObject + public var viewModel: CompassViewModel public init( viewpoint: Binding, size: Double = 30.0, autoHide: Bool = true ) { - self._viewpoint = viewpoint - self.autoHide = autoHide - height = size - width = size - opacity = viewpoint.wrappedValue.rotation.isZero ? 0 : 1 + self.viewModel = CompassViewModel( + viewpoint: viewpoint, + size: size, + autoHide: autoHide) } public var body: some View { ZStack { CompassBody() Needle() - .rotationEffect(Angle(degrees: viewpoint.adjustedRotation)) + .rotationEffect(Angle(degrees: viewModel.viewpoint.adjustedRotation)) } - .frame(width: width, height: height) - .opacity(opacity) + .frame(width: viewModel.width, height: viewModel.height) + .opacity(viewModel.opacity) .onTapGesture { - viewpoint = Viewpoint( - center: viewpoint.targetGeometry.extent.center, - scale: viewpoint.targetScale, - rotation: 0.0 - ) + viewModel.resetHeading() } - .onChange(of: viewpoint, perform: { _ in - let hide = viewpoint.rotation.isZero && autoHide + .onChange(of: viewModel.viewpoint, perform: { _ in + let hide = viewModel.viewpoint.rotation.isZero && viewModel.autoHide withAnimation(.default.delay(hide ? 0.25 : 0)) { - opacity = hide ? 0 : 1 + viewModel.opacity = hide ? 0 : 1 } }) - .accessibilityLabel(heading) - } -} - -private extension Int { - /// A representation of an integer's associated cardinal or intercardinal direction. - var asCardinalOrIntercardinal: String { - switch self { - case 0...22, 338...360: return "north" - case 23...67: return "northeast" - case 68...112: return "east" - case 113...157: return "southeast" - case 158...202: return "south" - case 203...247: return "southwest" - case 248...292: return "west" - case 293...337: return "northwest" - default: return "" - } - } -} - -private extension Viewpoint { - /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. - var adjustedRotation: Double { - self.rotation == 0 ? self.rotation : 360 - self.rotation + .accessibilityLabel(viewModel.viewpoint.heading) } } diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift new file mode 100644 index 000000000..e14bea09c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -0,0 +1,88 @@ +// 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 +import SwiftUI + +/// Manages the state for a `Compass` +@MainActor +public class CompassViewModel: ObservableObject { + /// Acts as link between the compass and the parent map or scene view. + @Binding var viewpoint: Viewpoint + + /// Controls visibility of the compass such for when `autoHide` is enabled. + @Published var opacity: Double + + /// Determines if the compass should automatically hide/show itself when the parent view is oriented + /// north. + @Published public var autoHide: Bool + + /// The height of the compass. + @Published public var height: Double + + /// The width of the compass. + @Published public var width: Double + + public init( + viewpoint: Binding, + size: Double = 30.0, + autoHide: Bool = true + ) { + self._viewpoint = viewpoint + self.autoHide = autoHide + height = size + width = size + opacity = viewpoint.wrappedValue.rotation.isZero ? 0 : 1 + } + + /// Resets the viewpoints `rotation` to zero. + public func resetHeading() { + self.viewpoint = Viewpoint( + center: viewpoint.targetGeometry.extent.center, + scale: viewpoint.targetScale, + rotation: 0.0 + ) + } +} + +internal extension Int { + /// A representation of an integer's associated cardinal or intercardinal direction. + var asCardinalOrIntercardinal: String { + switch self { + case 0...22, 338...360: return "north" + case 23...67: return "northeast" + case 68...112: return "east" + case 113...157: return "southeast" + case 158...202: return "south" + case 203...247: return "southwest" + case 248...292: return "west" + case 293...337: return "northwest" + default: return "" + } + } +} + +internal extension Viewpoint { + /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. + var adjustedRotation: Double { + self.rotation == 0 ? self.rotation : 360 - self.rotation + } + + /// A text description of the current heading, sutiable for accessibility voiceover. + var heading: String { + "Compass, heading " + + Int(self.adjustedRotation.rounded()).description + + " degrees " + + Int(self.adjustedRotation.rounded()).asCardinalOrIntercardinal + } +} diff --git a/Tests/ArcGISToolkitTests/CompassViewModelTests.swift b/Tests/ArcGISToolkitTests/CompassViewModelTests.swift new file mode 100644 index 000000000..eb2f6225f --- /dev/null +++ b/Tests/ArcGISToolkitTests/CompassViewModelTests.swift @@ -0,0 +1,79 @@ +// 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 +import SwiftUI +import XCTest +@testable import ArcGISToolkit + +@MainActor +class CompassViewModelTests: XCTestCase { + func testInitWithViewpoint() { + var viewpoint = viewpoint(0.0) + let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) + let viewModel = CompassViewModel(viewpoint: _viewpoint) + XCTAssertTrue(viewModel.viewpoint.rotation.isZero) + } + + func testInitWithViewpointAndSize() { + let initialRotationValue = 90.0 + let initialSizeValue = 90.0 + var viewpoint = viewpoint(initialRotationValue) + let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) + let viewModel = CompassViewModel( + viewpoint: _viewpoint, + size: initialSizeValue + ) + XCTAssertTrue(viewModel.width.isEqual(to: initialSizeValue)) + XCTAssertTrue(viewModel.height.isEqual(to: initialSizeValue)) + } + + func testInitWithViewpointAndAutoHide() { + let rotationValue = 90.0 + let sizeValue = 90.0 + let autoHide = false + var viewpoint = viewpoint(rotationValue) + let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) + let viewModel = CompassViewModel( + viewpoint: _viewpoint, + size: sizeValue, + autoHide: autoHide + ) + XCTAssertTrue(viewModel.autoHide == autoHide) + } + + func testResetHeading() { + let initialValue = 0.5 + let finalValue = 0.0 + var viewpoint = viewpoint(initialValue) + let binding = Binding(get: { viewpoint }, set: { viewpoint = $0 }) + let viewModel = CompassViewModel(viewpoint: binding) + XCTAssertTrue(viewModel.viewpoint.rotation.isEqual(to: initialValue)) + viewModel.resetHeading() + XCTAssertTrue(viewModel.viewpoint.rotation.isEqual(to: finalValue)) + } +} + +extension CompassViewModelTests { + var point: Point { + Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) + } + + var scale: Double { + 10_000.00 + } + + func viewpoint(_ rotation: Double) -> Viewpoint { + return Viewpoint(center: point, scale: scale, rotation: rotation) + } +} From 47dc3139c4df92d35e0fa423c5ca7263c7a73fae Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 19:43:41 -0600 Subject: [PATCH 0445/1722] Fix animations post ViewModel transition --- .../Components/Compass/Compass.swift | 18 +++++++++--------- .../Components/Compass/CompassViewModel.swift | 10 ++++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 1f1219313..dc8a6620d 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -19,6 +19,9 @@ public struct Compass: View { @ObservedObject public var viewModel: CompassViewModel + /// Controls the visibility of the compass. + @State private var opacity = 0.0 + public init( viewpoint: Binding, size: Double = 30.0, @@ -37,16 +40,13 @@ public struct Compass: View { .rotationEffect(Angle(degrees: viewModel.viewpoint.adjustedRotation)) } .frame(width: viewModel.width, height: viewModel.height) - .opacity(viewModel.opacity) - .onTapGesture { - viewModel.resetHeading() - } - .onChange(of: viewModel.viewpoint, perform: { _ in - let hide = viewModel.viewpoint.rotation.isZero && viewModel.autoHide - withAnimation(.default.delay(hide ? 0.25 : 0)) { - viewModel.opacity = hide ? 0 : 1 + .opacity(opacity) + .onTapGesture { viewModel.resetHeading() } + .onChange(of: viewModel.viewpoint) { _ in + withAnimation(.default.delay(viewModel.hidden ? 0.25 : 0)) { + opacity = viewModel.hidden ? 0 : 1 } - }) + } .accessibilityLabel(viewModel.viewpoint.heading) } } diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index e14bea09c..02f53d378 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -20,9 +20,6 @@ public class CompassViewModel: ObservableObject { /// Acts as link between the compass and the parent map or scene view. @Binding var viewpoint: Viewpoint - /// Controls visibility of the compass such for when `autoHide` is enabled. - @Published var opacity: Double - /// Determines if the compass should automatically hide/show itself when the parent view is oriented /// north. @Published public var autoHide: Bool @@ -33,6 +30,12 @@ public class CompassViewModel: ObservableObject { /// The width of the compass. @Published public var width: Double + /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and + /// autoHide preference. + public var hidden: Bool { + viewpoint.rotation.isZero && autoHide + } + public init( viewpoint: Binding, size: Double = 30.0, @@ -42,7 +45,6 @@ public class CompassViewModel: ObservableObject { self.autoHide = autoHide height = size width = size - opacity = viewpoint.wrappedValue.rotation.isZero ? 0 : 1 } /// Resets the viewpoints `rotation` to zero. From a7e3623d3d1ebfd63c56e12f202184e78e077dab Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 20:30:50 -0600 Subject: [PATCH 0446/1722] Update tests --- .../Components/Compass/Compass.swift | 4 +- .../CompassViewModelTests.swift | 119 ++++++++++++++++-- 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index dc8a6620d..82e37bc9b 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -37,7 +37,9 @@ public struct Compass: View { ZStack { CompassBody() Needle() - .rotationEffect(Angle(degrees: viewModel.viewpoint.adjustedRotation)) + .rotationEffect( + Angle(degrees: viewModel.viewpoint.adjustedRotation) + ) } .frame(width: viewModel.width, height: viewModel.height) .opacity(opacity) diff --git a/Tests/ArcGISToolkitTests/CompassViewModelTests.swift b/Tests/ArcGISToolkitTests/CompassViewModelTests.swift index eb2f6225f..037928e72 100644 --- a/Tests/ArcGISToolkitTests/CompassViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CompassViewModelTests.swift @@ -18,62 +18,155 @@ import XCTest @MainActor class CompassViewModelTests: XCTestCase { + /// Asserts that accessibility labels are properly generated. + func testCardinalAndIntercardinals() { + var viewpoint = getViewpoint(0.0) + let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) + let viewModel = CompassViewModel(viewpoint: _viewpoint) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 0 degrees north" + ) + viewpoint = getViewpoint(23) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 337 degrees northwest" + ) + viewpoint = getViewpoint(68) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 292 degrees west" + ) + viewpoint = getViewpoint(113) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 247 degrees southwest" + ) + viewpoint = getViewpoint(158) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 202 degrees south" + ) + viewpoint = getViewpoint(203) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 157 degrees southeast" + ) + viewpoint = getViewpoint(248) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 112 degrees east" + ) + viewpoint = getViewpoint(293) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 67 degrees northeast" + ) + viewpoint = getViewpoint(293) + XCTAssertEqual( + viewModel.viewpoint.heading, + "Compass, heading 67 degrees northeast" + ) + } + + /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is + /// disabled. + func testHiddenWithAutoHideOff() { + let initialValue = 0.0 + let finalValue = 90.0 + var viewpoint = getViewpoint(initialValue) + let binding = Binding(get: { viewpoint }, set: { viewpoint = $0 }) + let viewModel = CompassViewModel(viewpoint: binding, autoHide: false) + XCTAssertFalse(viewModel.hidden) + viewpoint = getViewpoint(finalValue) + XCTAssertFalse(viewModel.hidden) + } + + /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is + /// enabled. + func testHiddenWithAutoHideOn() { + let initialValue = 0.0 + let finalValue = 90.0 + var viewpoint = getViewpoint(initialValue) + let binding = Binding(get: { viewpoint }, set: { viewpoint = $0 }) + let viewModel = CompassViewModel(viewpoint: binding) + XCTAssertTrue(viewModel.hidden) + viewpoint = getViewpoint(finalValue) + XCTAssertFalse(viewModel.hidden) + } + + /// Asserts that the model correctly initializes when given only a viewpoint. func testInitWithViewpoint() { - var viewpoint = viewpoint(0.0) + var viewpoint = getViewpoint(0.0) let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) let viewModel = CompassViewModel(viewpoint: _viewpoint) XCTAssertTrue(viewModel.viewpoint.rotation.isZero) + XCTAssertEqual(viewModel.height, 30.0) + XCTAssertEqual(viewModel.width, 30.0) + XCTAssertEqual(viewModel.autoHide, true) } + /// Asserts that the model correctly initializes when given a viewpoint and size. func testInitWithViewpointAndSize() { - let initialRotationValue = 90.0 + let initialRotationValue = 0.0 let initialSizeValue = 90.0 - var viewpoint = viewpoint(initialRotationValue) + var viewpoint = getViewpoint(initialRotationValue) let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) let viewModel = CompassViewModel( viewpoint: _viewpoint, size: initialSizeValue ) - XCTAssertTrue(viewModel.width.isEqual(to: initialSizeValue)) - XCTAssertTrue(viewModel.height.isEqual(to: initialSizeValue)) + XCTAssertTrue(viewModel.viewpoint.rotation.isZero) + XCTAssertEqual(viewModel.width, initialSizeValue) + XCTAssertEqual(viewModel.height, initialSizeValue) + XCTAssertEqual(viewModel.autoHide, true) } - func testInitWithViewpointAndAutoHide() { - let rotationValue = 90.0 + /// Asserts that the model correctly initializes when given only a viewpoint. + func testInitWithViewpointAndSizeAndAutoHide() { + let rotationValue = 0.0 let sizeValue = 90.0 let autoHide = false - var viewpoint = viewpoint(rotationValue) + var viewpoint = getViewpoint(rotationValue) let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) let viewModel = CompassViewModel( viewpoint: _viewpoint, size: sizeValue, autoHide: autoHide ) - XCTAssertTrue(viewModel.autoHide == autoHide) + XCTAssertTrue(viewModel.viewpoint.rotation.isZero) + XCTAssertEqual(viewModel.width, sizeValue) + XCTAssertEqual(viewModel.height, sizeValue) + XCTAssertEqual(viewModel.autoHide, autoHide) } func testResetHeading() { let initialValue = 0.5 let finalValue = 0.0 - var viewpoint = viewpoint(initialValue) + var viewpoint = getViewpoint(initialValue) let binding = Binding(get: { viewpoint }, set: { viewpoint = $0 }) let viewModel = CompassViewModel(viewpoint: binding) - XCTAssertTrue(viewModel.viewpoint.rotation.isEqual(to: initialValue)) + XCTAssertEqual(viewModel.viewpoint.rotation, initialValue) viewModel.resetHeading() - XCTAssertTrue(viewModel.viewpoint.rotation.isEqual(to: finalValue)) + XCTAssertEqual(viewModel.viewpoint.rotation, finalValue) } } extension CompassViewModelTests { + /// 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 } - func viewpoint(_ rotation: Double) -> Viewpoint { + /// Builds viewpoints to use for tests. + /// - Parameter rotation: The rotation to use for the resulting viewpoint. + /// - Returns: A viewpoint object for tests. + func getViewpoint(_ rotation: Double) -> Viewpoint { return Viewpoint(center: point, scale: scale, rotation: rotation) } } From ebf1d9fde0df88116ae74c90674ef96420f12b07 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 20:32:43 -0600 Subject: [PATCH 0447/1722] Documentation --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 82e37bc9b..c0e2fdf76 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -16,6 +16,8 @@ import SwiftUI /// A Compass (alias North arrow) shows where north is in a MapView or SceneView. public struct Compass: View { + + /// The view model for the compass. @ObservedObject public var viewModel: CompassViewModel From a7e2d7118a34645f2faf6c8541af5db7b63f2052 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 20:33:24 -0600 Subject: [PATCH 0448/1722] Spacing --- 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 c0e2fdf76..e7d91897b 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -16,7 +16,6 @@ import SwiftUI /// A Compass (alias North arrow) shows where north is in a MapView or SceneView. public struct Compass: View { - /// The view model for the compass. @ObservedObject public var viewModel: CompassViewModel From 12277c5a0a058704ee6097e4187a73f74f0d9e05 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 8 Feb 2022 20:33:51 -0600 Subject: [PATCH 0449/1722] Formatting --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index e7d91897b..ababafddc 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -21,7 +21,8 @@ public struct Compass: View { public var viewModel: CompassViewModel /// Controls the visibility of the compass. - @State private var opacity = 0.0 + @State + private var opacity = 0.0 public init( viewpoint: Binding, From efea2a71d05bcd4f6f3a70a9f4a27ffdb6745b2b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 9 Feb 2022 11:45:05 -0600 Subject: [PATCH 0450/1722] Mark CompassViewModel as private --- Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index 02f53d378..04ab339fd 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -16,7 +16,7 @@ import SwiftUI /// Manages the state for a `Compass` @MainActor -public class CompassViewModel: ObservableObject { +final public class CompassViewModel: ObservableObject { /// Acts as link between the compass and the parent map or scene view. @Binding var viewpoint: Viewpoint From 8cc1d4345bfc6d5d16be5474fc53103c0a88c1dc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 9 Feb 2022 15:08:41 -0600 Subject: [PATCH 0451/1722] Implement `CompassDirection` enum --- .../Components/Compass/CompassDirection.swift | 64 +++++++++++++++++++ .../Components/Compass/CompassViewModel.swift | 19 +----- 2 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift new file mode 100644 index 000000000..0162d734a --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift @@ -0,0 +1,64 @@ +// 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 Foundation + +/// Represents a cardinal or intercardinal direction +internal enum CompassDirection: String { + case north + case northeast + case east + case southeast + case south + case southwest + case west + case northwest +} + +internal extension CompassDirection { + /// Initializes a `CompassDirection` from a given degree value. All values will be normalized + /// between 0° and 360°. + init(_ degrees: Double) { + let angle = CompassDirection.normalizedAngle(degrees) + switch angle { + case 0..<23, 338..<360: + self = .north + case 23..<68: + self = .northeast + case 68..<113: + self = .east + case 113..<158: + self = .southeast + case 158..<203: + self = .south + case 203..<248: + self = .southwest + case 248..<293: + self = .west + case 293..<338: + self = .northwest + default: + fatalError() + } + } + + /// Normalizes degree values between 0° and 360°. + static private func normalizedAngle(_ degrees: Double) -> Double { + let normalizded = degrees.truncatingRemainder(dividingBy: 360) + if normalizded < 0 { + return normalizded + 360 + } else { + return abs(normalizded) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index 04ab339fd..44261440d 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -57,23 +57,6 @@ final public class CompassViewModel: ObservableObject { } } -internal extension Int { - /// A representation of an integer's associated cardinal or intercardinal direction. - var asCardinalOrIntercardinal: String { - switch self { - case 0...22, 338...360: return "north" - case 23...67: return "northeast" - case 68...112: return "east" - case 113...157: return "southeast" - case 158...202: return "south" - case 203...247: return "southwest" - case 248...292: return "west" - case 293...337: return "northwest" - default: return "" - } - } -} - internal extension Viewpoint { /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. var adjustedRotation: Double { @@ -85,6 +68,6 @@ internal extension Viewpoint { "Compass, heading " + Int(self.adjustedRotation.rounded()).description + " degrees " - + Int(self.adjustedRotation.rounded()).asCardinalOrIntercardinal + + CompassDirection(self.adjustedRotation).rawValue } } From b3dd3285292ff2ad6c76d00821cfe6c8e733e276 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 9 Feb 2022 15:14:44 -0600 Subject: [PATCH 0452/1722] FloorFilterViewModel updates --- .../Examples/FloorFilterExampleView.swift | 18 +++-- .../Components/FloorFilter/FloorFilter.swift | 65 ++++++++++++------- .../FloorFilter/FloorFilterViewModel.swift | 53 +++++++-------- 3 files changed, 71 insertions(+), 65 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 6f8cda1e1..927789e51 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -26,8 +26,8 @@ struct FloorFilterExampleView: View { ) @State - private var floorFilterViewModel: FloorFilterViewModel? = nil - + private var floorManager: FloorManager? = nil + init() { // Create the map from a portal item and assign to the mapView. @@ -54,20 +54,18 @@ struct FloorFilterExampleView: View { viewpoint: viewpoint ) .overlay(alignment: .bottomLeading) { - if let viewModel = floorFilterViewModel { - FloorFilter(viewModel: viewModel) + if let floorManager = floorManager { + FloorFilter( + floorManager: floorManager, + viewpoint: $viewpoint + ) .padding(floorFilterPadding) } } .task { do { try await map.load() - if let floorManager = map.floorManager { - floorFilterViewModel = FloorFilterViewModel( - floorManager: floorManager, - viewpoint: $viewpoint - ) - } + floorManager = map.floorManager } catch { print("load error: \(error)") } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index e87138eaa..81ee50f1d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -19,40 +19,57 @@ import ArcGIS /// to a site, a facility (building) in the site, or a floor in the facility. public struct FloorFilter: View { /// Creates a `FloorFilter` - /// - Parameter viewModel: The view model used by the `FloorFilter`. - public init(viewModel: FloorFilterViewModel) { - self.viewModel = viewModel + /// - Parameter floorManager: The floor manager used by the `FloorFilter`. + /// Creates a `FloorFilter` + /// - Parameters: + /// - floorManager: The floor manager used by the `FloorFilter`. + /// - viewpoint: Viewpoint updated when the selected site or facility changes. + public init( + floorManager: FloorManager, + viewpoint: Binding? = nil + ) { + self.floorManager = floorManager + self.viewpoint = viewpoint } - /// The view model used by the `FloorFilter`. - @ObservedObject - private(set) var viewModel: FloorFilterViewModel + let floorManager: FloorManager + let viewpoint: Binding? + /// The view model used by the `FloorFilter`. + @StateObject + private var viewModel = FloorFilterViewModel() + /// Allows the user to toggle the visibility of the site selector. @State private var isSelectorVisible: Bool = false public var body: some View { - if viewModel.isLoading { - ProgressView() - .progressViewStyle(.circular) - .esriBorder() - } else { - HStack(alignment: .bottom) { - Button { - isSelectorVisible.toggle() - } label: { - Image("Site", bundle: .module, label: Text("Site")) - } - .esriBorder() - if isSelectorVisible { - SiteAndFacilitySelector( - floorFilterViewModel: viewModel, - isVisible: $isSelectorVisible - ) - .frame(width: 200) + Group { + if viewModel.isLoading { + ProgressView() + .progressViewStyle(.circular) + .esriBorder() + } else { + HStack(alignment: .bottom) { + Button { + isSelectorVisible.toggle() + } label: { + Image("Site", bundle: .module, label: Text("Site")) + } + .esriBorder() + if isSelectorVisible { + SiteAndFacilitySelector( + floorFilterViewModel: viewModel, + isVisible: $isSelectorVisible + ) + .frame(width: 200) + } } } } + .onAppear { + viewModel.floorManager = floorManager + viewModel.viewpoint = viewpoint + } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index b61d1b831..b422ae3d1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -18,7 +18,7 @@ import ArcGIS @MainActor final public class FloorFilterViewModel: ObservableObject { /// A selected site, floor, or level. - public enum Selection { + enum Selection { /// A selected site. case site(FloorSite) /// A selected facility. @@ -26,51 +26,42 @@ final public class FloorFilterViewModel: ObservableObject { /// A selected level. case level(FloorLevel) } - - /// Creates a `FloorFilterViewModel`. - /// - Parameters: - /// - floorManager: A floor manager. - /// - viewpoint: Viewpoint updated when the selected site or facility changes. - public init( - floorManager: FloorManager, - viewpoint: Binding? = nil - ) { - self.viewpoint = viewpoint - self.floorManager = floorManager - Task { - do { - try await floorManager.load() - if sites.count == 1 { - // If we have only one site, select it. - selection = .site(sites.first!) - } - } catch { - print("error: \(error)") - } - isLoading = false - } - } /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. - let viewpoint: Binding? + var viewpoint: Binding? /// The `FloorManager` containing the site, floor, and level information. - let floorManager: FloorManager + var floorManager: FloorManager? = nil { + didSet { + Task { + do { + try await floorManager?.load() + if sites.count == 1 { + // If we have only one site, select it. + selection = .site(sites.first!) + } + } catch { + print("error: \(error)") + } + isLoading = false + } + } + } /// The floor manager sites. public var sites: [FloorSite] { - floorManager.sites + floorManager?.sites ?? [] } /// The floor manager facilities. public var facilities: [FloorFacility] { - floorManager.facilities + floorManager?.facilities ?? [] } /// The floor manager levels. public var levels: [FloorLevel] { - floorManager.levels + floorManager?.levels ?? [] } /// `true` if the model is loading it's properties, `false` if not loading. @@ -79,7 +70,7 @@ final public class FloorFilterViewModel: ObservableObject { /// The selected site, floor, or level. @Published - public var selection: Selection? { + var selection: Selection? { didSet { zoomToSelection() } From aa4f959bb4b08172a9b6bef6cbf67d205f7bfd93 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 9 Feb 2022 15:29:09 -0600 Subject: [PATCH 0453/1722] Punctuation --- Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift index 0162d734a..9db3f1c7c 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift @@ -13,7 +13,7 @@ import Foundation -/// Represents a cardinal or intercardinal direction +/// Represents a cardinal or intercardinal direction. internal enum CompassDirection: String { case north case northeast From cedc5fa98ea0fb0727326aca3b3ef15459465f5c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 9 Feb 2022 15:37:07 -0600 Subject: [PATCH 0454/1722] Merge all Needle components into Needle.swift --- .../Components/Compass/Needle.swift | 31 +++++++++++++++++ .../Components/Compass/NeedleCenter.swift | 28 ---------------- .../Components/Compass/NeedleQuadrant.swift | 33 ------------------- 3 files changed, 31 insertions(+), 61 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Compass/NeedleCenter.swift delete mode 100644 Sources/ArcGISToolkit/Components/Compass/NeedleQuadrant.swift diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index 78192d5d7..4adcb0282 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -46,7 +46,38 @@ struct Needle: View { } } +/// Represents the center of the spinning needle at the center of the compass. +struct NeedleCenter: View { + var body: some View { + Circle() + .scale(0.25) + .foregroundColor(Color.bronze) + } +} + +/// Represents 1/4 (one triangle) of the spinning needle at the center of the compass. +struct NeedleQuadrant: View { + /// The color of this needle quadrant. + let color: Color + + var body: some View { + GeometryReader { geometry in + Path { path in + let width = geometry.size.width + let height = geometry.size.height + path.move(to: CGPoint(x: 0, y: height)) + path.addLine(to: CGPoint(x: width, y: 0)) + path.addLine(to: CGPoint(x: width, y: height)) + } + .fill(color) + } + } +} + private extension Color { + /// The bronze color of the center of the compass needle. + static let bronze = Color(red: 241, green: 169, blue: 59) + /// The dark gray color of the compass needle. static let grayDark = Color(red: 128, green: 128, blue: 128) diff --git a/Sources/ArcGISToolkit/Components/Compass/NeedleCenter.swift b/Sources/ArcGISToolkit/Components/Compass/NeedleCenter.swift deleted file mode 100644 index 17ccd708f..000000000 --- a/Sources/ArcGISToolkit/Components/Compass/NeedleCenter.swift +++ /dev/null @@ -1,28 +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 - -/// Represents the center of the spinning needle at the center of the compass. -struct NeedleCenter: View { - var body: some View { - Circle() - .scale(0.25) - .foregroundColor(Color.bronze) - } -} - -private extension Color { - /// The bronze color of the center of the compass needle. - static let bronze = Color(red: 241, green: 169, blue: 59) -} diff --git a/Sources/ArcGISToolkit/Components/Compass/NeedleQuadrant.swift b/Sources/ArcGISToolkit/Components/Compass/NeedleQuadrant.swift deleted file mode 100644 index 2ab8f4292..000000000 --- a/Sources/ArcGISToolkit/Components/Compass/NeedleQuadrant.swift +++ /dev/null @@ -1,33 +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 - -/// Represents 1/4 (one triangle) of the spinning needle at the center of the compass. -struct NeedleQuadrant: View { - /// The color of this needle quadrant. - let color: Color - - var body: some View { - GeometryReader { geometry in - Path { path in - let width = geometry.size.width - let height = geometry.size.height - path.move(to: CGPoint(x: 0, y: height)) - path.addLine(to: CGPoint(x: width, y: 0)) - path.addLine(to: CGPoint(x: width, y: height)) - } - .fill(color) - } - } -} From ae5b6a19cf586332f36db5997a5b3b9c76b56280 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Feb 2022 09:38:14 -0600 Subject: [PATCH 0455/1722] Auto-indenting; remove duplicate lines. --- Examples/Examples/FloorFilterExampleView.swift | 16 ++++++++-------- .../Components/FloorFilter/FloorFilter.swift | 4 +--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 927789e51..c0ef6c2bb 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -27,22 +27,22 @@ struct FloorFilterExampleView: View { @State private var floorManager: FloorManager? = nil - + init() { // Create the map from a portal item and assign to the mapView. // Multiple sites/facilities: Esri IST map with all buildings. -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - + // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) + // Redlands Campus map. -// let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities - + // let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities + // Single site (ESRI Redlands Main) and facility (Building L). let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - + map = Map(item: portalItem) } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 81ee50f1d..acc15d721 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -18,8 +18,6 @@ import ArcGIS /// 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 { - /// Creates a `FloorFilter` - /// - Parameter floorManager: The floor manager used by the `FloorFilter`. /// Creates a `FloorFilter` /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilter`. @@ -38,7 +36,7 @@ public struct FloorFilter: View { /// The view model used by the `FloorFilter`. @StateObject private var viewModel = FloorFilterViewModel() - + /// Allows the user to toggle the visibility of the site selector. @State private var isSelectorVisible: Bool = false From daef5f10097768c49dd3d7f42df0783dd95e36fd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:08:58 -0600 Subject: [PATCH 0456/1722] Put Compass in proper alphabetical order --- Examples/ExamplesApp/Examples.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index a33287c4e..bb40609ae 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -36,10 +36,10 @@ extension ExampleList { name: "GeoView", examples: [ AnyExample("Basemap Gallery", content: BasemapGalleryExampleView()), + AnyExample("Compass", content: CompassExampleView()), AnyExample("Floor Filter", content: FloorFilterExampleView()), AnyExample("Overview Map", content: OverviewMapExampleView()), - AnyExample("Search", content: SearchExampleView()), - AnyExample("Compass", content: CompassExampleView()) + AnyExample("Search", content: SearchExampleView()) ] ) From 99d7056b56519bef99e60b67b0016ee6770d3fdb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:09:25 -0600 Subject: [PATCH 0457/1722] Start demo off with rotation so that Compass is displayed by default --- Examples/Examples/CompassExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 8049ce72c..b4e6ce604 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -24,7 +24,7 @@ struct CompassExampleView: View { private var viewpoint = Viewpoint( center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), scale: 10_000, - rotation: 0 + rotation: -45 ) var body: some View { From b574af42437d4deaf6d62d2fd4c22ca656357470 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:11:07 -0600 Subject: [PATCH 0458/1722] Update Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index 44261440d..a6b5f7462 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -14,7 +14,7 @@ import ArcGIS import SwiftUI -/// Manages the state for a `Compass` +/// Manages the state for a `Compass`. @MainActor final public class CompassViewModel: ObservableObject { /// Acts as link between the compass and the parent map or scene view. From 5983f83a813c9992d90957717625276f9f309b19 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:26:14 -0600 Subject: [PATCH 0459/1722] Add `precondition` doc tag. --- Sources/ArcGISToolkit/Extensions/Color.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/Color.swift b/Sources/ArcGISToolkit/Extensions/Color.swift index a8191af5d..f85f61306 100644 --- a/Sources/ArcGISToolkit/Extensions/Color.swift +++ b/Sources/ArcGISToolkit/Extensions/Color.swift @@ -14,7 +14,8 @@ import SwiftUI extension Color { - /// Initializes a new color with RGB integer values between 0 and 255 inclusive. + /// Initializes a new color with RGB integer values. + /// - Precondition: `red`, `blue` and `green` are values between 0 and 255 inclusive. init(red: Int, green: Int, blue: Int) { let validRange = 0...255 precondition(validRange.contains(red)) From 7d2750cfd3966b81b9a4249585003c219d9a6fa2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:29:47 -0600 Subject: [PATCH 0460/1722] Switch `CompassViewModel` from `public` to `internal` --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 2 +- Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index ababafddc..11258f519 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -18,7 +18,7 @@ import SwiftUI public struct Compass: View { /// The view model for the compass. @ObservedObject - public var viewModel: CompassViewModel + var viewModel: CompassViewModel /// Controls the visibility of the compass. @State diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index a6b5f7462..921815530 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -16,7 +16,7 @@ import SwiftUI /// Manages the state for a `Compass`. @MainActor -final public class CompassViewModel: ObservableObject { +final internal class CompassViewModel: ObservableObject { /// Acts as link between the compass and the parent map or scene view. @Binding var viewpoint: Viewpoint From 75e7e01f463890c7a5dd0be2dc47464bb537436a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:34:13 -0600 Subject: [PATCH 0461/1722] Update Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index 921815530..d09bee357 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -52,7 +52,7 @@ final internal class CompassViewModel: ObservableObject { self.viewpoint = Viewpoint( center: viewpoint.targetGeometry.extent.center, scale: viewpoint.targetScale, - rotation: 0.0 + rotation: .zero ) } } From 3674c051a30deb27f2dbadd369a9a10f40997985 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:34:35 -0600 Subject: [PATCH 0462/1722] Update Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index d09bee357..8c5b2b82e 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -60,7 +60,7 @@ final internal class CompassViewModel: ObservableObject { internal extension Viewpoint { /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. var adjustedRotation: Double { - self.rotation == 0 ? self.rotation : 360 - self.rotation + rotation.isZero ? .zero : 360 - rotation } /// A text description of the current heading, sutiable for accessibility voiceover. From 758fc3399ca229cdc5fff5b66218c3f11a9d550b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:34:50 -0600 Subject: [PATCH 0463/1722] Update Sources/ArcGISToolkit/Components/Compass/Needle.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Compass/Needle.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index 4adcb0282..37dc28d84 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -19,8 +19,8 @@ struct Needle: View { ZStack { VStack(spacing: 0) { HStack(spacing: 0) { - NeedleQuadrant(color: Color.redLight) - NeedleQuadrant(color: Color.redDark) + NeedleQuadrant(color: .redLight) + NeedleQuadrant(color: .redDark) .rotation3DEffect( Angle(degrees: 180), axis: (x: 0, y: 1, z: 0)) From 5e4c227797d3f8a95a4fd78e80c593a1e63d59ae Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:34:57 -0600 Subject: [PATCH 0464/1722] Update Sources/ArcGISToolkit/Components/Compass/Needle.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Compass/Needle.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index 37dc28d84..db585b976 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -26,7 +26,7 @@ struct Needle: View { axis: (x: 0, y: 1, z: 0)) } HStack(spacing: 0) { - NeedleQuadrant(color: Color.grayLight) + NeedleQuadrant(color: .grayLight) .rotation3DEffect( Angle(degrees: 180), axis: (x: 1, y: 0, z: 0)) From b55bbb10857daea4b2f4511f89cb111df5d69446 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:35:09 -0600 Subject: [PATCH 0465/1722] Update Sources/ArcGISToolkit/Components/Compass/Needle.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Compass/Needle.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index db585b976..00c24bd3f 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -30,7 +30,7 @@ struct Needle: View { .rotation3DEffect( Angle(degrees: 180), axis: (x: 1, y: 0, z: 0)) - NeedleQuadrant(color: Color.grayDark) + NeedleQuadrant(color: .grayDark) .rotation3DEffect( Angle(degrees: 180), axis: (x: 0, y: 1, z: 0)) From 2bb47ea9ee32be174e6063f3e60eceed96352d15 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:37:41 -0600 Subject: [PATCH 0466/1722] Switch to use use of implicit `Color` types --- Sources/ArcGISToolkit/Components/Compass/CompassBody.swift | 4 ++-- Sources/ArcGISToolkit/Components/Compass/Needle.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift index 9dc1ea035..adb0e15bc 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift @@ -21,8 +21,8 @@ struct CompassBody: View { Circle() .inset(by: borderWidth / 2.0) .stroke(lineWidth: borderWidth) - .foregroundColor(Color.outline) - .background(Circle().foregroundColor(Color.fill)) + .foregroundColor(.outline) + .background(Circle().foregroundColor(.fill)) } } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index 00c24bd3f..fe7911043 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -51,7 +51,7 @@ struct NeedleCenter: View { var body: some View { Circle() .scale(0.25) - .foregroundColor(Color.bronze) + .foregroundColor(.bronze) } } From dc85943b15d49884bc67969d662398fda49e8a7a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:49:36 -0600 Subject: [PATCH 0467/1722] Add `init` documentation for both compass and its view model --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 8 +++++++- .../Components/Compass/CompassViewModel.swift | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 11258f519..8f759894f 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -20,10 +20,16 @@ public struct Compass: View { @ObservedObject var viewModel: CompassViewModel - /// Controls the visibility of the compass. + /// Controls the current opacity of the compass. @State private var opacity = 0.0 + /// Creates a `Compass` + /// - Parameters: + /// - viewpoint: Acts a communication link between the MapView or SceneView and the compass. + /// - size: Enables a custom size configuuration for the compass. Default is 30. + /// - autoHide: Determines if the compass automatically hides itself when the MapView or + /// SceneView is oriented north. public init( viewpoint: Binding, size: Double = 30.0, diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index 8c5b2b82e..6b5fa5d5e 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -36,6 +36,12 @@ final internal class CompassViewModel: ObservableObject { viewpoint.rotation.isZero && autoHide } + /// Creates a `CompassViewModel` + /// - Parameters: + /// - viewpoint: Acts a communication link between the MapView or SceneView and the compass. + /// - size: Enables a custom size configuuration for the compass. Default is 30. + /// - autoHide: Determines if the compass automatically hides itself when the MapView or + /// SceneView is oriented north. public init( viewpoint: Binding, size: Double = 30.0, From 55a3c1610bed17d33f6db8f1cc53cb1f54481f36 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Feb 2022 11:57:59 -0600 Subject: [PATCH 0468/1722] Switch default size to 40 --- Examples/Examples/CompassExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index b4e6ce604..8e2694339 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -31,7 +31,7 @@ struct CompassExampleView: View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint, size: 75) + Compass(viewpoint: $viewpoint, size: 40) .padding() } } From 906fe8021daf338b5a5ef40bba38d3e2d9d45db6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Feb 2022 16:29:38 -0600 Subject: [PATCH 0469/1722] Update how the model is created. --- .../Components/FloorFilter/FloorFilter.swift | 15 +++----- .../FloorFilter/FloorFilterViewModel.swift | 37 ++++++++++++++----- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index acc15d721..e79878343 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -26,16 +26,15 @@ public struct FloorFilter: View { floorManager: FloorManager, viewpoint: Binding? = nil ) { - self.floorManager = floorManager - self.viewpoint = viewpoint + _viewModel = StateObject(wrappedValue: FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + )) } - let floorManager: FloorManager - let viewpoint: Binding? - /// The view model used by the `FloorFilter`. @StateObject - private var viewModel = FloorFilterViewModel() + private var viewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site selector. @State @@ -65,9 +64,5 @@ public struct FloorFilter: View { } } } - .onAppear { - viewModel.floorManager = floorManager - viewModel.viewpoint = viewpoint - } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index b422ae3d1..b0d36199d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -27,6 +27,19 @@ final public class FloorFilterViewModel: ObservableObject { case level(FloorLevel) } + /// Creates a `FloorFilterViewModel` + /// - Parameters: + /// - floorManager: The floor manager used by the `FloorFilterViewModel`. + /// - viewpoint: Viewpoint updated when the selected site or facility changes. + init( + floorManager: FloorManager, + viewpoint: Binding? = nil + ) { + self.floorManager = floorManager + self.viewpoint = viewpoint + floorManagerDidChange() + } + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding? @@ -34,18 +47,22 @@ final public class FloorFilterViewModel: ObservableObject { /// The `FloorManager` containing the site, floor, and level information. var floorManager: FloorManager? = nil { didSet { - Task { - do { - try await floorManager?.load() - if sites.count == 1 { - // If we have only one site, select it. - selection = .site(sites.first!) - } - } catch { - print("error: \(error)") + floorManagerDidChange() + } + } + + func floorManagerDidChange() { + Task { + do { + try await floorManager?.load() + if sites.count == 1 { + // If we have only one site, select it. + selection = .site(sites.first!) } - isLoading = false + } catch { + print("error: \(error)") } + isLoading = false } } From da2db1fe1704125c5d238f0bbdca6e33615837e6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Feb 2022 09:33:53 -0600 Subject: [PATCH 0470/1722] Update CompassDirection.swift --- .../Components/Compass/CompassDirection.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift index 9db3f1c7c..54e2d9b15 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift @@ -31,21 +31,21 @@ internal extension CompassDirection { init(_ degrees: Double) { let angle = CompassDirection.normalizedAngle(degrees) switch angle { - case 0..<23, 338..<360: + case 0..<22.5, 337.5..<360: self = .north - case 23..<68: + case 22.5..<67.5: self = .northeast - case 68..<113: + case 67.5..<112.5: self = .east - case 113..<158: + case 112.5..<157.5: self = .southeast - case 158..<203: + case 157.5..<202.5: self = .south - case 203..<248: + case 202.5..<247.5: self = .southwest - case 248..<293: + case 247.5..<292.5: self = .west - case 293..<338: + case 292.5..<337.5: self = .northwest default: fatalError() From a0550cb38bc71d231914c0c03318e76f4b6a544a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Feb 2022 09:55:05 -0600 Subject: [PATCH 0471/1722] Pull abs() from CompassDirection.normalizedAngle and add CompassDirection test --- .../Components/Compass/CompassDirection.swift | 4 +- .../CompassDirectionTests.swift | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/CompassDirectionTests.swift diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift index 54e2d9b15..e5c6f2e76 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift @@ -53,12 +53,12 @@ internal extension CompassDirection { } /// Normalizes degree values between 0° and 360°. - static private func normalizedAngle(_ degrees: Double) -> Double { + static func normalizedAngle(_ degrees: Double) -> Double { let normalizded = degrees.truncatingRemainder(dividingBy: 360) if normalizded < 0 { return normalizded + 360 } else { - return abs(normalizded) + return normalizded } } } diff --git a/Tests/ArcGISToolkitTests/CompassDirectionTests.swift b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift new file mode 100644 index 000000000..7af3be076 --- /dev/null +++ b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift @@ -0,0 +1,48 @@ +// 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 XCTest +@testable import ArcGISToolkit + +class CompassDirectionTests: XCTestCase { + /// Tests the behvaior of `CompassDirection.init(_ : Double)` + func testInitCompassDirection() { + XCTAssertEqual(CompassDirection(-405), .northwest) + XCTAssertEqual(CompassDirection(-360), .north) + XCTAssertEqual(CompassDirection(0), .north) + XCTAssertEqual(CompassDirection(22.4), .north) + XCTAssertEqual(CompassDirection(22.5), .northeast) + XCTAssertEqual(CompassDirection(45), .northeast) + XCTAssertEqual(CompassDirection(90), .east) + XCTAssertEqual(CompassDirection(135), .southeast) + XCTAssertEqual(CompassDirection(180), .south) + XCTAssertEqual(CompassDirection(225), .southwest) + XCTAssertEqual(CompassDirection(270), .west) + XCTAssertEqual(CompassDirection(315), .northwest) + XCTAssertEqual(CompassDirection(359), .north) + XCTAssertEqual(CompassDirection(450), .east) + } + + /// Tests the behvaior of `CompassDirection.normalizedAngle(_ : Double)` + func testNormalizedAngle() { + XCTAssertEqual(CompassDirection.normalizedAngle(-361.0), 359) + XCTAssertEqual(CompassDirection.normalizedAngle(-360.0), 0) + XCTAssertEqual(CompassDirection.normalizedAngle(-180.0), 180) + XCTAssertEqual(CompassDirection.normalizedAngle(-0), 0) + XCTAssertEqual(CompassDirection.normalizedAngle(0), 0) + XCTAssertEqual(CompassDirection.normalizedAngle(45), 45) + XCTAssertEqual(CompassDirection.normalizedAngle(180), 180) + XCTAssertEqual(CompassDirection.normalizedAngle(360), 0) + XCTAssertEqual(CompassDirection.normalizedAngle(405), 45) + } +} From 454ccebb7d16d27d3f4b42b75e93c9b0850b3fb7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Feb 2022 10:41:22 -0600 Subject: [PATCH 0472/1722] Move control of compass sizing to consumer --- Examples/Examples/CompassExampleView.swift | 3 +- .../Components/Compass/Compass.swift | 21 ++++---- .../Components/Compass/CompassViewModel.swift | 9 ---- .../Components/Compass/Needle.swift | 54 ++++++++++--------- 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 8e2694339..5d54c858e 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -31,7 +31,8 @@ struct CompassExampleView: View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { - Compass(viewpoint: $viewpoint, size: 40) + Compass(viewpoint: $viewpoint) + .frame(width: 44, height: 44) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 8f759894f..dc489fa1a 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -32,24 +32,27 @@ public struct Compass: View { /// SceneView is oriented north. public init( viewpoint: Binding, - size: Double = 30.0, autoHide: Bool = true ) { self.viewModel = CompassViewModel( viewpoint: viewpoint, - size: size, autoHide: autoHide) } public var body: some View { - ZStack { - CompassBody() - Needle() - .rotationEffect( - Angle(degrees: viewModel.viewpoint.adjustedRotation) - ) + GeometryReader { geometry in + ZStack { + CompassBody() + Needle() + .rotationEffect( + Angle(degrees: viewModel.viewpoint.adjustedRotation) + ) + } + .frame( + width: min(geometry.size.width, geometry.size.height), + height: min(geometry.size.width, geometry.size.height) + ) } - .frame(width: viewModel.width, height: viewModel.height) .opacity(opacity) .onTapGesture { viewModel.resetHeading() } .onChange(of: viewModel.viewpoint) { _ in diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index 6b5fa5d5e..0ba5ff739 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -24,12 +24,6 @@ final internal class CompassViewModel: ObservableObject { /// north. @Published public var autoHide: Bool - /// The height of the compass. - @Published public var height: Double - - /// The width of the compass. - @Published public var width: Double - /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and /// autoHide preference. public var hidden: Bool { @@ -44,13 +38,10 @@ final internal class CompassViewModel: ObservableObject { /// SceneView is oriented north. public init( viewpoint: Binding, - size: Double = 30.0, autoHide: Bool = true ) { self._viewpoint = viewpoint self.autoHide = autoHide - height = size - width = size } /// Resets the viewpoints `rotation` to zero. diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index fe7911043..9d0f729a6 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -16,33 +16,39 @@ import SwiftUI /// Represents the spinning needle at the center of the compass. struct Needle: View { var body: some View { - ZStack { - VStack(spacing: 0) { - HStack(spacing: 0) { - NeedleQuadrant(color: .redLight) - NeedleQuadrant(color: .redDark) - .rotation3DEffect( - Angle(degrees: 180), - axis: (x: 0, y: 1, z: 0)) - } - HStack(spacing: 0) { - NeedleQuadrant(color: .grayLight) - .rotation3DEffect( - Angle(degrees: 180), - axis: (x: 1, y: 0, z: 0)) - NeedleQuadrant(color: .grayDark) - .rotation3DEffect( - Angle(degrees: 180), - axis: (x: 0, y: 1, z: 0)) - .rotation3DEffect( - Angle(degrees: 180), - axis: (x: 1, y: 0, z: 0)) + GeometryReader { geometry in + ZStack { + VStack(spacing: 0) { + HStack(spacing: 0) { + NeedleQuadrant(color: .redLight) + NeedleQuadrant(color: .redDark) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 0, y: 1, z: 0)) + } + HStack(spacing: 0) { + NeedleQuadrant(color: .grayLight) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 1, y: 0, z: 0)) + NeedleQuadrant(color: .grayDark) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 0, y: 1, z: 0)) + .rotation3DEffect( + Angle(degrees: 180), + axis: (x: 1, y: 0, z: 0)) + } } + NeedleCenter() } - NeedleCenter() + .aspectRatio(1/3, contentMode: .fit) + .frame( + width: min(geometry.size.width, geometry.size.height), + height: min(geometry.size.width, geometry.size.height) + ) + .scaleEffect(0.6) } - .aspectRatio(1.0/3.0, contentMode: .fit) - .scaleEffect(0.6) } } From 7ae66056c468a983818b93bd710e3f493a4be04a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Feb 2022 13:41:23 -0600 Subject: [PATCH 0473/1722] Move setting the initial opacity value within the constructor --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index dc489fa1a..e04314a69 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -22,7 +22,7 @@ public struct Compass: View { /// Controls the current opacity of the compass. @State - private var opacity = 0.0 + private var opacity: Double /// Creates a `Compass` /// - Parameters: @@ -34,9 +34,11 @@ public struct Compass: View { viewpoint: Binding, autoHide: Bool = true ) { - self.viewModel = CompassViewModel( + let viewModel = CompassViewModel( viewpoint: viewpoint, autoHide: autoHide) + self.viewModel = viewModel + opacity = viewModel.hidden ? 0.0 : 1.0 } public var body: some View { From e5df850faf0a3309a3a466589840650465348be4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Feb 2022 13:41:30 -0600 Subject: [PATCH 0474/1722] Formatting --- .../ArcGISToolkit/Components/Compass/CompassViewModel.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift index 0ba5ff739..3c886f444 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift @@ -18,11 +18,13 @@ import SwiftUI @MainActor final internal class CompassViewModel: ObservableObject { /// Acts as link between the compass and the parent map or scene view. - @Binding var viewpoint: Viewpoint + @Binding + var viewpoint: Viewpoint /// Determines if the compass should automatically hide/show itself when the parent view is oriented /// north. - @Published public var autoHide: Bool + @Published + public var autoHide: Bool /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and /// autoHide preference. From 6db15c4a2edc6bcfd50338a9ed3120a1c42d4c2b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 14 Feb 2022 13:05:20 -0800 Subject: [PATCH 0475/1722] Undo view model implementation --- .../Components/Compass/Compass.swift | 62 +++++-- .../Components/Compass/CompassViewModel.swift | 72 -------- Tests/ArcGISToolkitTests/CompassTests.swift | 149 +++++++++++++++ .../CompassViewModelTests.swift | 172 ------------------ 4 files changed, 196 insertions(+), 259 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift create mode 100644 Tests/ArcGISToolkitTests/CompassTests.swift delete mode 100644 Tests/ArcGISToolkitTests/CompassViewModelTests.swift diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index e04314a69..566800912 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -16,29 +16,36 @@ import SwiftUI /// A Compass (alias North arrow) shows where north is in a MapView or SceneView. public struct Compass: View { - /// The view model for the compass. - @ObservedObject - var viewModel: CompassViewModel + /// Determines if the compass should automatically hide/show itself when the parent view is oriented + /// north. + public let autoHide: Bool + + /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and + /// autoHide preference. + public var isHidden: Bool { + viewpoint.rotation.isZero && autoHide + } /// Controls the current opacity of the compass. @State private var opacity: Double + /// Acts as link between the compass and the parent map or scene view. + @Binding + private(set) var viewpoint: Viewpoint + /// Creates a `Compass` /// - Parameters: /// - viewpoint: Acts a communication link between the MapView or SceneView and the compass. - /// - size: Enables a custom size configuuration for the compass. Default is 30. /// - autoHide: Determines if the compass automatically hides itself when the MapView or /// SceneView is oriented north. public init( viewpoint: Binding, autoHide: Bool = true ) { - let viewModel = CompassViewModel( - viewpoint: viewpoint, - autoHide: autoHide) - self.viewModel = viewModel - opacity = viewModel.hidden ? 0.0 : 1.0 + _viewpoint = viewpoint + _opacity = State(initialValue: .zero) + self.autoHide = autoHide } public var body: some View { @@ -47,7 +54,7 @@ public struct Compass: View { CompassBody() Needle() .rotationEffect( - Angle(degrees: viewModel.viewpoint.adjustedRotation) + Angle(degrees: viewpoint.adjustedRotation) ) } .frame( @@ -56,12 +63,37 @@ public struct Compass: View { ) } .opacity(opacity) - .onTapGesture { viewModel.resetHeading() } - .onChange(of: viewModel.viewpoint) { _ in - withAnimation(.default.delay(viewModel.hidden ? 0.25 : 0)) { - opacity = viewModel.hidden ? 0 : 1 + .onTapGesture { resetHeading() } + .onChange(of: viewpoint) { _ in + withAnimation(.default.delay(isHidden ? 0.25 : 0)) { + opacity = isHidden ? 0 : 1 } } - .accessibilityLabel(viewModel.viewpoint.heading) + .onAppear { opacity = isHidden ? 0 : 1 } + .accessibilityLabel(viewpoint.heading) + } + + /// Resets the viewpoints `rotation` to zero. + func resetHeading() { + self.viewpoint = Viewpoint( + center: viewpoint.targetGeometry.extent.center, + scale: viewpoint.targetScale, + rotation: .zero + ) + } +} + +internal extension Viewpoint { + /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. + var adjustedRotation: Double { + rotation.isZero ? .zero : 360 - rotation + } + + /// A text description of the current heading, sutiable for accessibility voiceover. + var heading: String { + "Compass, heading " + + Int(self.adjustedRotation.rounded()).description + + " degrees " + + CompassDirection(self.adjustedRotation).rawValue } } diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift b/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift deleted file mode 100644 index 3c886f444..000000000 --- a/Sources/ArcGISToolkit/Components/Compass/CompassViewModel.swift +++ /dev/null @@ -1,72 +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 -import SwiftUI - -/// Manages the state for a `Compass`. -@MainActor -final internal class CompassViewModel: ObservableObject { - /// Acts as link between the compass and the parent map or scene view. - @Binding - var viewpoint: Viewpoint - - /// Determines if the compass should automatically hide/show itself when the parent view is oriented - /// north. - @Published - public var autoHide: Bool - - /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and - /// autoHide preference. - public var hidden: Bool { - viewpoint.rotation.isZero && autoHide - } - - /// Creates a `CompassViewModel` - /// - Parameters: - /// - viewpoint: Acts a communication link between the MapView or SceneView and the compass. - /// - size: Enables a custom size configuuration for the compass. Default is 30. - /// - autoHide: Determines if the compass automatically hides itself when the MapView or - /// SceneView is oriented north. - public init( - viewpoint: Binding, - autoHide: Bool = true - ) { - self._viewpoint = viewpoint - self.autoHide = autoHide - } - - /// Resets the viewpoints `rotation` to zero. - public func resetHeading() { - self.viewpoint = Viewpoint( - center: viewpoint.targetGeometry.extent.center, - scale: viewpoint.targetScale, - rotation: .zero - ) - } -} - -internal extension Viewpoint { - /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. - var adjustedRotation: Double { - rotation.isZero ? .zero : 360 - rotation - } - - /// A text description of the current heading, sutiable for accessibility voiceover. - var heading: String { - "Compass, heading " - + Int(self.adjustedRotation.rounded()).description - + " degrees " - + CompassDirection(self.adjustedRotation).rawValue - } -} diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift new file mode 100644 index 000000000..333d408b5 --- /dev/null +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -0,0 +1,149 @@ +// 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 +import SwiftUI +import XCTest +@testable import ArcGISToolkit + +class CompassTests: XCTestCase { + /// Asserts that accessibility labels are properly generated. + func testCardinalAndIntercardinals() { + var _viewpoint = getViewpoint(0.0) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 0 degrees north" + ) + _viewpoint = getViewpoint(23) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 337 degrees northwest" + ) + _viewpoint = getViewpoint(68) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 292 degrees west" + ) + _viewpoint = getViewpoint(113) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 247 degrees southwest" + ) + _viewpoint = getViewpoint(158) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 202 degrees south" + ) + _viewpoint = getViewpoint(203) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 157 degrees southeast" + ) + _viewpoint = getViewpoint(248) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 112 degrees east" + ) + _viewpoint = getViewpoint(293) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 67 degrees northeast" + ) + _viewpoint = getViewpoint(293) + XCTAssertEqual( + compass.viewpoint.heading, + "Compass, heading 67 degrees northeast" + ) + } + + /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is + /// disabled. + func testHiddenWithAutoHideOff() { + let initialValue = 0.0 + let finalValue = 90.0 + var _viewpoint = getViewpoint(initialValue) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint, autoHide: false) + XCTAssertFalse(compass.isHidden) + _viewpoint = getViewpoint(finalValue) + XCTAssertFalse(compass.isHidden) + } + + /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is + /// enabled. + func testHiddenWithAutoHideOn() { + let initialValue = 0.0 + let finalValue = 90.0 + var _viewpoint = getViewpoint(initialValue) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertTrue(compass.isHidden) + _viewpoint = getViewpoint(finalValue) + XCTAssertFalse(compass.isHidden) + } + + /// Asserts that the model correctly initializes when given only a viewpoint. + func testInitWithViewpoint() { + var _viewpoint = getViewpoint(0.0) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertTrue(compass.viewpoint.rotation.isZero) + XCTAssertEqual(compass.autoHide, true) + } + + /// Asserts that the model correctly initializes when given only a viewpoint. + func testInitWithViewpointAndAutoHide() { + let rotationValue = 0.0 + let autoHide = false + var _viewpoint = getViewpoint(rotationValue) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass( + viewpoint: viewpoint, + autoHide: autoHide + ) + XCTAssertTrue(compass.viewpoint.rotation.isZero) + XCTAssertEqual(compass.autoHide, autoHide) + } + + func testResetHeading() { + let initialValue = 0.5 + let finalValue = 0.0 + var _viewpoint = getViewpoint(initialValue) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertEqual(compass.viewpoint.rotation, initialValue) + compass.resetHeading() + XCTAssertEqual(compass.viewpoint.rotation, finalValue) + } +} + +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 + } + + /// Builds viewpoints to use for tests. + /// - Parameter rotation: The rotation to use for the resulting viewpoint. + /// - Returns: A viewpoint object for tests. + func getViewpoint(_ rotation: Double) -> Viewpoint { + return Viewpoint(center: point, scale: scale, rotation: rotation) + } +} diff --git a/Tests/ArcGISToolkitTests/CompassViewModelTests.swift b/Tests/ArcGISToolkitTests/CompassViewModelTests.swift deleted file mode 100644 index 037928e72..000000000 --- a/Tests/ArcGISToolkitTests/CompassViewModelTests.swift +++ /dev/null @@ -1,172 +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 -import SwiftUI -import XCTest -@testable import ArcGISToolkit - -@MainActor -class CompassViewModelTests: XCTestCase { - /// Asserts that accessibility labels are properly generated. - func testCardinalAndIntercardinals() { - var viewpoint = getViewpoint(0.0) - let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) - let viewModel = CompassViewModel(viewpoint: _viewpoint) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 0 degrees north" - ) - viewpoint = getViewpoint(23) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 337 degrees northwest" - ) - viewpoint = getViewpoint(68) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 292 degrees west" - ) - viewpoint = getViewpoint(113) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 247 degrees southwest" - ) - viewpoint = getViewpoint(158) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 202 degrees south" - ) - viewpoint = getViewpoint(203) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 157 degrees southeast" - ) - viewpoint = getViewpoint(248) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 112 degrees east" - ) - viewpoint = getViewpoint(293) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 67 degrees northeast" - ) - viewpoint = getViewpoint(293) - XCTAssertEqual( - viewModel.viewpoint.heading, - "Compass, heading 67 degrees northeast" - ) - } - - /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is - /// disabled. - func testHiddenWithAutoHideOff() { - let initialValue = 0.0 - let finalValue = 90.0 - var viewpoint = getViewpoint(initialValue) - let binding = Binding(get: { viewpoint }, set: { viewpoint = $0 }) - let viewModel = CompassViewModel(viewpoint: binding, autoHide: false) - XCTAssertFalse(viewModel.hidden) - viewpoint = getViewpoint(finalValue) - XCTAssertFalse(viewModel.hidden) - } - - /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is - /// enabled. - func testHiddenWithAutoHideOn() { - let initialValue = 0.0 - let finalValue = 90.0 - var viewpoint = getViewpoint(initialValue) - let binding = Binding(get: { viewpoint }, set: { viewpoint = $0 }) - let viewModel = CompassViewModel(viewpoint: binding) - XCTAssertTrue(viewModel.hidden) - viewpoint = getViewpoint(finalValue) - XCTAssertFalse(viewModel.hidden) - } - - /// Asserts that the model correctly initializes when given only a viewpoint. - func testInitWithViewpoint() { - var viewpoint = getViewpoint(0.0) - let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) - let viewModel = CompassViewModel(viewpoint: _viewpoint) - XCTAssertTrue(viewModel.viewpoint.rotation.isZero) - XCTAssertEqual(viewModel.height, 30.0) - XCTAssertEqual(viewModel.width, 30.0) - XCTAssertEqual(viewModel.autoHide, true) - } - - /// Asserts that the model correctly initializes when given a viewpoint and size. - func testInitWithViewpointAndSize() { - let initialRotationValue = 0.0 - let initialSizeValue = 90.0 - var viewpoint = getViewpoint(initialRotationValue) - let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) - let viewModel = CompassViewModel( - viewpoint: _viewpoint, - size: initialSizeValue - ) - XCTAssertTrue(viewModel.viewpoint.rotation.isZero) - XCTAssertEqual(viewModel.width, initialSizeValue) - XCTAssertEqual(viewModel.height, initialSizeValue) - XCTAssertEqual(viewModel.autoHide, true) - } - - /// Asserts that the model correctly initializes when given only a viewpoint. - func testInitWithViewpointAndSizeAndAutoHide() { - let rotationValue = 0.0 - let sizeValue = 90.0 - let autoHide = false - var viewpoint = getViewpoint(rotationValue) - let _viewpoint = Binding(get: { viewpoint }, set: { viewpoint = $0 }) - let viewModel = CompassViewModel( - viewpoint: _viewpoint, - size: sizeValue, - autoHide: autoHide - ) - XCTAssertTrue(viewModel.viewpoint.rotation.isZero) - XCTAssertEqual(viewModel.width, sizeValue) - XCTAssertEqual(viewModel.height, sizeValue) - XCTAssertEqual(viewModel.autoHide, autoHide) - } - - func testResetHeading() { - let initialValue = 0.5 - let finalValue = 0.0 - var viewpoint = getViewpoint(initialValue) - let binding = Binding(get: { viewpoint }, set: { viewpoint = $0 }) - let viewModel = CompassViewModel(viewpoint: binding) - XCTAssertEqual(viewModel.viewpoint.rotation, initialValue) - viewModel.resetHeading() - XCTAssertEqual(viewModel.viewpoint.rotation, finalValue) - } -} - -extension CompassViewModelTests { - /// 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 - } - - /// Builds viewpoints to use for tests. - /// - Parameter rotation: The rotation to use for the resulting viewpoint. - /// - Returns: A viewpoint object for tests. - func getViewpoint(_ rotation: Double) -> Viewpoint { - return Viewpoint(center: point, scale: scale, rotation: rotation) - } -} From f2bce9e41e13995c01923ac0122f6dbc6e785e9c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Feb 2022 11:27:03 -0800 Subject: [PATCH 0476/1722] Update Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift Co-authored-by: Mark Dostal --- .../Components/Compass/CompassDirection.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift index e5c6f2e76..c25e9b344 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift @@ -54,11 +54,11 @@ internal extension CompassDirection { /// Normalizes degree values between 0° and 360°. static func normalizedAngle(_ degrees: Double) -> Double { - let normalizded = degrees.truncatingRemainder(dividingBy: 360) - if normalizded < 0 { - return normalizded + 360 + let normalized = degrees.truncatingRemainder(dividingBy: 360) + if normalized < 0 { + return normalized + 360 } else { - return normalizded + return normalized } } } From 1f95b3cf0bb922a43648cacf8e1faad226165c55 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Feb 2022 11:28:30 -0800 Subject: [PATCH 0477/1722] Update Tests/ArcGISToolkitTests/CompassTests.swift Co-authored-by: Mark Dostal --- Tests/ArcGISToolkitTests/CompassTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 333d408b5..5723a2b2e 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -105,9 +105,8 @@ class CompassTests: XCTestCase { /// Asserts that the model correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { - let rotationValue = 0.0 let autoHide = false - var _viewpoint = getViewpoint(rotationValue) + var _viewpoint = getViewpoint(0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass( viewpoint: viewpoint, From 9175657ef86da5b8640f99e48f38a0ff0b7224fd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Feb 2022 11:29:14 -0800 Subject: [PATCH 0478/1722] 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 566800912..be310ab89 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -18,7 +18,7 @@ import SwiftUI public struct Compass: View { /// Determines if the compass should automatically hide/show itself when the parent view is oriented /// north. - public let autoHide: Bool + private let autoHide: Bool /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and /// autoHide preference. From 44b48db79d63b5c28edcb34403f444059d7d6d7d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 08:02:12 -0800 Subject: [PATCH 0479/1722] Fix tests --- Tests/ArcGISToolkitTests/CompassTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 5723a2b2e..ffad46967 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -100,7 +100,7 @@ class CompassTests: XCTestCase { let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) XCTAssertTrue(compass.viewpoint.rotation.isZero) - XCTAssertEqual(compass.autoHide, true) + XCTAssertEqual(compass.isHidden, true) } /// Asserts that the model correctly initializes when given only a viewpoint. @@ -113,7 +113,7 @@ class CompassTests: XCTestCase { autoHide: autoHide ) XCTAssertTrue(compass.viewpoint.rotation.isZero) - XCTAssertEqual(compass.autoHide, autoHide) + XCTAssertEqual(compass.isHidden, false) } func testResetHeading() { From 4b4c7e2786632d64b9e54d40e5966560e17fd4cb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 08:09:08 -0800 Subject: [PATCH 0480/1722] Set Compass.viewpoint to private --- 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 be310ab89..ed4c13a8d 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -32,7 +32,7 @@ public struct Compass: View { /// Acts as link between the compass and the parent map or scene view. @Binding - private(set) var viewpoint: Viewpoint + private var viewpoint: Viewpoint /// Creates a `Compass` /// - Parameters: From 0814d9734c326204a51a19cc764aff81ff91456b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 08:09:36 -0800 Subject: [PATCH 0481/1722] Remove Compass.viewpoint from tests --- Tests/ArcGISToolkitTests/CompassTests.swift | 44 ++++++++++----------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index ffad46967..55cb2dcaa 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -19,51 +19,49 @@ import XCTest class CompassTests: XCTestCase { /// Asserts that accessibility labels are properly generated. func testCardinalAndIntercardinals() { - var _viewpoint = getViewpoint(0.0) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) + var viewpoint = getViewpoint(0.0) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 0 degrees north" ) - _viewpoint = getViewpoint(23) + viewpoint = getViewpoint(23) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 337 degrees northwest" ) - _viewpoint = getViewpoint(68) + viewpoint = getViewpoint(68) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 292 degrees west" ) - _viewpoint = getViewpoint(113) + viewpoint = getViewpoint(113) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 247 degrees southwest" ) - _viewpoint = getViewpoint(158) + viewpoint = getViewpoint(158) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 202 degrees south" ) - _viewpoint = getViewpoint(203) + viewpoint = getViewpoint(203) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 157 degrees southeast" ) - _viewpoint = getViewpoint(248) + viewpoint = getViewpoint(248) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 112 degrees east" ) - _viewpoint = getViewpoint(293) + viewpoint = getViewpoint(293) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 67 degrees northeast" ) - _viewpoint = getViewpoint(293) + viewpoint = getViewpoint(293) XCTAssertEqual( - compass.viewpoint.heading, + viewpoint.heading, "Compass, heading 67 degrees northeast" ) } @@ -99,7 +97,6 @@ class CompassTests: XCTestCase { var _viewpoint = getViewpoint(0.0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - XCTAssertTrue(compass.viewpoint.rotation.isZero) XCTAssertEqual(compass.isHidden, true) } @@ -112,7 +109,6 @@ class CompassTests: XCTestCase { viewpoint: viewpoint, autoHide: autoHide ) - XCTAssertTrue(compass.viewpoint.rotation.isZero) XCTAssertEqual(compass.isHidden, false) } @@ -122,9 +118,9 @@ class CompassTests: XCTestCase { var _viewpoint = getViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(compass.viewpoint.rotation, initialValue) + XCTAssertEqual(_viewpoint.rotation, initialValue) compass.resetHeading() - XCTAssertEqual(compass.viewpoint.rotation, finalValue) + XCTAssertEqual(_viewpoint.rotation, finalValue) } } From 30fbc337daa9220135dea1259f587c86a57f1592 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 10:44:18 -0800 Subject: [PATCH 0482/1722] Allow a viewpoint to be nil --- Examples/Examples/CompassExampleView.swift | 2 +- .../Components/Compass/Compass.swift | 15 +++++--- Tests/ArcGISToolkitTests/CompassTests.swift | 38 +++++++++++++------ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 5d54c858e..f22a1b3ce 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -21,7 +21,7 @@ struct CompassExampleView: View { /// Allows for communication between the Compass and MapView or SceneView. @State - private var viewpoint = Viewpoint( + private var viewpoint: Viewpoint? = Viewpoint( center: Point(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 ed4c13a8d..215348581 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -23,7 +23,8 @@ public struct Compass: View { /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and /// autoHide preference. public var isHidden: Bool { - viewpoint.rotation.isZero && autoHide + guard let viewpoint = viewpoint else { return autoHide } + return viewpoint.rotation.isZero && autoHide } /// Controls the current opacity of the compass. @@ -32,15 +33,16 @@ public struct Compass: View { /// Acts as link between the compass and the parent map or scene view. @Binding - private var viewpoint: Viewpoint + private var viewpoint: Viewpoint? /// Creates a `Compass` /// - Parameters: - /// - viewpoint: Acts a communication link between the MapView or SceneView and the compass. + /// - viewpoint: Acts a communication link between the MapView or SceneView and the + /// compass. /// - autoHide: Determines if the compass automatically hides itself when the MapView or /// SceneView is oriented north. public init( - viewpoint: Binding, + viewpoint: Binding, autoHide: Bool = true ) { _viewpoint = viewpoint @@ -54,7 +56,7 @@ public struct Compass: View { CompassBody() Needle() .rotationEffect( - Angle(degrees: viewpoint.adjustedRotation) + Angle(degrees: viewpoint?.adjustedRotation ?? .zero) ) } .frame( @@ -70,11 +72,12 @@ public struct Compass: View { } } .onAppear { opacity = isHidden ? 0 : 1 } - .accessibilityLabel(viewpoint.heading) + .accessibilityLabel(viewpoint?.heading ?? "Compass") } /// Resets the viewpoints `rotation` to zero. func resetHeading() { + guard let viewpoint = viewpoint else { return } self.viewpoint = Viewpoint( center: viewpoint.targetGeometry.extent.center, scale: viewpoint.targetScale, diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 55cb2dcaa..c2ac2e12c 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -66,12 +66,12 @@ class CompassTests: XCTestCase { ) } - /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is + /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide is /// disabled. func testHiddenWithAutoHideOff() { let initialValue = 0.0 let finalValue = 90.0 - var _viewpoint = getViewpoint(initialValue) + var _viewpoint: Viewpoint? = getViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint, autoHide: false) XCTAssertFalse(compass.isHidden) @@ -79,12 +79,12 @@ class CompassTests: XCTestCase { XCTAssertFalse(compass.isHidden) } - /// Asserts that the model accurately indicates when the compass should be hidden when autoHide is + /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide is /// enabled. func testHiddenWithAutoHideOn() { let initialValue = 0.0 let finalValue = 90.0 - var _viewpoint = getViewpoint(initialValue) + var _viewpoint: Viewpoint? = getViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) XCTAssertTrue(compass.isHidden) @@ -92,18 +92,34 @@ class CompassTests: XCTestCase { XCTAssertFalse(compass.isHidden) } - /// Asserts that the model correctly initializes when given only a viewpoint. + /// Asserts that the compass correctly initializes when given a nil viewpoint + func testInit() { + var _viewpoint: Viewpoint? = nil + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertEqual(compass.isHidden, true) + } + + /// Asserts that the compass correctly initializes when given a nil viewpoint, and autoHide is disabled. + func testInitNoAutoHide() { + var _viewpoint: Viewpoint? = nil + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint, autoHide: false) + XCTAssertEqual(compass.isHidden, false) + } + + /// Asserts that the compass correctly initializes when given only a viewpoint. func testInitWithViewpoint() { - var _viewpoint = getViewpoint(0.0) + var _viewpoint: Viewpoint? = getViewpoint(0.0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) XCTAssertEqual(compass.isHidden, true) } - /// Asserts that the model correctly initializes when given only a viewpoint. + /// Asserts that the compass correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { let autoHide = false - var _viewpoint = getViewpoint(0) + var _viewpoint: Viewpoint? = getViewpoint(0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass( viewpoint: viewpoint, @@ -115,12 +131,12 @@ class CompassTests: XCTestCase { func testResetHeading() { let initialValue = 0.5 let finalValue = 0.0 - var _viewpoint = getViewpoint(initialValue) + var _viewpoint: Viewpoint? = getViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(_viewpoint.rotation, initialValue) + XCTAssertEqual(_viewpoint?.rotation, initialValue) compass.resetHeading() - XCTAssertEqual(_viewpoint.rotation, finalValue) + XCTAssertEqual(_viewpoint?.rotation, finalValue) } } From 2e7b012b6e3e425ebf32a491fc65a3c92e490c47 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 11:30:06 -0800 Subject: [PATCH 0483/1722] Update Sources/ArcGISToolkit/Components/Compass/Compass.swift Co-authored-by: R Olson --- 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 215348581..1570c2fc1 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -20,7 +20,7 @@ public struct Compass: View { /// north. private let autoHide: Bool - /// Indicates if the compass should be hidden or visible based on the current viewpoint rotation and + /// Indicates if the compass is hidden based on the current viewpoint rotation and /// autoHide preference. public var isHidden: Bool { guard let viewpoint = viewpoint else { return autoHide } From f6fda85ec00d410cb8e60dbafb28fd20a8301c82 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 10 Feb 2022 16:36:55 -0600 Subject: [PATCH 0484/1722] Fix inadvertent commit --- .../Examples/FloorFilterExampleView.swift | 10 ++--- .../Components/FloorFilter/FloorFilter.swift | 38 ++++++++--------- .../FloorFilter/FloorFilterViewModel.swift | 41 ++++++++----------- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index c0ef6c2bb..a63877d98 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -16,8 +16,7 @@ import ArcGISToolkit import ArcGIS struct FloorFilterExampleView: View { - @State - private var map: Map + private let map: Map @State private var viewpoint = Viewpoint( @@ -26,7 +25,7 @@ struct FloorFilterExampleView: View { ) @State - private var floorManager: FloorManager? = nil + private var isMapLoaded: Bool = false init() { // Create the map from a portal item and assign to the mapView. @@ -54,7 +53,8 @@ struct FloorFilterExampleView: View { viewpoint: viewpoint ) .overlay(alignment: .bottomLeading) { - if let floorManager = floorManager { + if isMapLoaded, + let floorManager = map.floorManager { FloorFilter( floorManager: floorManager, viewpoint: $viewpoint @@ -65,7 +65,7 @@ struct FloorFilterExampleView: View { .task { do { try await map.load() - floorManager = map.floorManager + isMapLoaded = true } catch { print("load error: \(error)") } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index e79878343..4a63780cd 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -41,26 +41,24 @@ public struct FloorFilter: View { private var isSelectorVisible: Bool = false public var body: some View { - Group { - if viewModel.isLoading { - ProgressView() - .progressViewStyle(.circular) - .esriBorder() - } else { - HStack(alignment: .bottom) { - Button { - isSelectorVisible.toggle() - } label: { - Image("Site", bundle: .module, label: Text("Site")) - } - .esriBorder() - if isSelectorVisible { - SiteAndFacilitySelector( - floorFilterViewModel: viewModel, - isVisible: $isSelectorVisible - ) - .frame(width: 200) - } + if viewModel.isLoading { + ProgressView() + .progressViewStyle(.circular) + .esriBorder() + } else { + HStack(alignment: .bottom) { + Button { + isSelectorVisible.toggle() + } label: { + Image("Site", bundle: .module, label: Text("Site")) + } + .esriBorder() + if isSelectorVisible { + SiteAndFacilitySelector( + floorFilterViewModel: viewModel, + isVisible: $isSelectorVisible + ) + .frame(width: 200) } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index b0d36199d..43b31ade3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -16,7 +16,7 @@ import ArcGIS /// Manages the state for a `FloorFilter`. @MainActor -final public class FloorFilterViewModel: ObservableObject { +final class FloorFilterViewModel: ObservableObject { /// A selected site, floor, or level. enum Selection { /// A selected site. @@ -27,7 +27,7 @@ final public class FloorFilterViewModel: ObservableObject { case level(FloorLevel) } - /// Creates a `FloorFilterViewModel` + /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilterViewModel`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. @@ -37,24 +37,10 @@ final public class FloorFilterViewModel: ObservableObject { ) { self.floorManager = floorManager self.viewpoint = viewpoint - floorManagerDidChange() - } - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. - /// If `nil`, there will be no automatic pan/zoom operations. - var viewpoint: Binding? - - /// The `FloorManager` containing the site, floor, and level information. - var floorManager: FloorManager? = nil { - didSet { - floorManagerDidChange() - } - } - - func floorManagerDidChange() { Task { do { - try await floorManager?.load() + try await floorManager.load() if sites.count == 1 { // If we have only one site, select it. selection = .site(sites.first!) @@ -65,20 +51,27 @@ final public class FloorFilterViewModel: ObservableObject { isLoading = false } } + + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// If `nil`, there will be no automatic pan/zoom operations. + let viewpoint: Binding? + + /// The `FloorManager` containing the site, floor, and level information. + let floorManager: FloorManager /// The floor manager sites. - public var sites: [FloorSite] { - floorManager?.sites ?? [] + var sites: [FloorSite] { + floorManager.sites } /// The floor manager facilities. - public var facilities: [FloorFacility] { - floorManager?.facilities ?? [] + var facilities: [FloorFacility] { + floorManager.facilities } /// The floor manager levels. - public var levels: [FloorLevel] { - floorManager?.levels ?? [] + var levels: [FloorLevel] { + floorManager.levels } /// `true` if the model is loading it's properties, `false` if not loading. @@ -135,7 +128,7 @@ final public class FloorFilterViewModel: ObservableObject { } /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. - public func zoomToSelection() { + func zoomToSelection() { guard let selection = selection else { return } From cdbd74955faf2c2bbf63027f97dfefb17a0fb115 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 16 Feb 2022 15:56:32 -0600 Subject: [PATCH 0485/1722] Site/Facility/Level work --- .../Examples/FloorFilterExampleView.swift | 15 +- .../Components/FloorFilter/FloorFilter.swift | 134 +++++++++++++++--- .../FloorFilter/FloorFilterViewModel.swift | 38 ++++- .../FloorFilter/SiteAndFacilitySelector.swift | 116 +++++++++++---- .../Components/Search/SearchView.swift | 32 ----- .../ArcGISToolkit/Extensions/FloorLevel.swift | 23 +++ Sources/ArcGISToolkit/Extensions/View.swift | 46 ++++++ .../Site.imageset/Contents.json | 26 ---- .../Site.imageset/business_black_18dp (1).svg | 1 - .../business_black_24dp (1)-1.svg | 1 - .../Site.imageset/business_black_24dp (1).svg | 1 - 11 files changed, 318 insertions(+), 115 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/FloorLevel.swift create mode 100644 Sources/ArcGISToolkit/Extensions/View.swift delete mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json delete mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg delete mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg delete mode 100644 Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index a63877d98..480e3d390 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -26,7 +26,7 @@ struct FloorFilterExampleView: View { @State private var isMapLoaded: Bool = false - + init() { // Create the map from a portal item and assign to the mapView. @@ -35,13 +35,13 @@ struct FloorFilterExampleView: View { // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) // Redlands Campus map. - // let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities - + let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) + let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities + // Single site (ESRI Redlands Main) and facility (Building L). - let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) + map = Map(item: portalItem) } @@ -59,6 +59,7 @@ struct FloorFilterExampleView: View { floorManager: floorManager, viewpoint: $viewpoint ) + .frame(idealHeight: 300) .padding(floorFilterPadding) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 4a63780cd..31ee1ed18 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -36,31 +36,131 @@ public struct FloorFilter: View { @StateObject private var viewModel: FloorFilterViewModel - /// Allows the user to toggle the visibility of the site selector. + /// A Boolean value that indicates whether the site/facility selector is hidden. @State - private var isSelectorVisible: Bool = false + private var isSelectorHidden: Bool = true + + /// A Boolean value that indicates whether the levels view is currently collapsed. + @State + private var isLevelsViewCollapsed: Bool = false public var body: some View { - if viewModel.isLoading { - ProgressView() - .progressViewStyle(.circular) - .esriBorder() + Group { + if viewModel.isLoading { + ProgressView() + .progressViewStyle(.circular) + .padding(12) + } else { + HStack(alignment: .bottom) { + VStack { + LevelsView(isCollapsed: $isLevelsViewCollapsed) + Button { + isSelectorHidden.toggle() + } label: { + Image(systemName: "building.2") + } + } + .esriBorder() + if !isSelectorHidden { + SiteAndFacilitySelector(isHidden: $isSelectorHidden) + .esriBorder() + } + } + } + } + .environmentObject(viewModel) + } +} + +struct LevelsView: View { + /// Allows the user to toggle the visibility of the site and facility selector. + var isCollapsed: Binding + + /// The view model used by the `LevelsView`. + @EnvironmentObject var viewModel: FloorFilterViewModel + + private var sortedLevels: [FloorLevel] { + viewModel.selectedFacility?.levels.sorted() { + $0.verticalOrder > $1.verticalOrder + } ?? [] + } + + public var body: some View { + if viewModel.selectedFacility == nil || + viewModel.selectedFacility!.levels.isEmpty{ + EmptyView() } else { - HStack(alignment: .bottom) { + VStack { + if !isCollapsed.wrappedValue { + CollapseButton(isCollapsed: isCollapsed) + Divider() + .frame(width: 30) + if let levels = sortedLevels { + if levels.count > 3 { + ScrollView { + LevelsStack(levels: levels) + } + } else { + LevelsStack(levels: levels) + } + } + } + else if isCollapsed.wrappedValue { + // Button for the selected level. + Button { + isCollapsed.wrappedValue.toggle() + } label: { + Text(viewModel.selectedLevel?.shortName ?? (sortedLevels.first?.shortName ?? "None")) + } + .padding(8) + .selected(true) + } + + Divider() + .frame(width: 30) + } + } + } +} + +/// A vertical list of floor levels. +struct LevelsStack: View { + let levels: [FloorLevel] + + /// The view model used by the `LevelsView`. + @EnvironmentObject var viewModel: FloorFilterViewModel + + var body: some View { + VStack { + ForEach(levels) { level in Button { - isSelectorVisible.toggle() + viewModel.selection = .level(level) } label: { - Image("Site", bundle: .module, label: Text("Site")) - } - .esriBorder() - if isSelectorVisible { - SiteAndFacilitySelector( - floorFilterViewModel: viewModel, - isVisible: $isSelectorVisible - ) - .frame(width: 200) + Text(level.shortName) } + .padding(8) + .selected(level == viewModel.selectedLevel) } } } } + +/// A button used to collapse the floor level list. +struct CollapseButton: View { + /// Allows the user to toggle the visibility of the site and facility selector. + var isCollapsed: Binding + + var body: some View { + Button { + isCollapsed.wrappedValue.toggle() + } label: { + Image(systemName: "xmark") + } + .padding(EdgeInsets( + top: 2, + leading: 4, + bottom: 2, + trailing: 4 + )) + } +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 43b31ade3..04663f624 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -54,7 +54,7 @@ final class FloorFilterViewModel: ObservableObject { /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. - let viewpoint: Binding? + var viewpoint: Binding? /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager @@ -78,10 +78,34 @@ final class FloorFilterViewModel: ObservableObject { @Published private(set) var isLoading = true + /// Gets the default level for a facility. Uses level with vertical order 0 otherwise gets the lowest level. + public func getDefaultLevelForFacility( + facility: FloorFacility? + ) -> FloorLevel? { + let candidateLevels = levels.filter { $0.facility == facility } + return candidateLevels.first { $0.verticalOrder == 0 } ?? getLowestLevel() + } + + /// Returns the AGSFloorLevel with the lowest verticalOrder. + private func getLowestLevel() -> FloorLevel? { + let sortedLevels = levels.sorted { + $0.verticalOrder < $1.verticalOrder + } + return sortedLevels.first { + $0.verticalOrder != .min && $0.verticalOrder != .max + } + } + /// The selected site, floor, or level. @Published var selection: Selection? { didSet { + if case let .facility(facility) = selection, + let level = getDefaultLevelForFacility(facility: facility) { + selection = .level(level) + } else { + filterMapToSelectedLevel() + } zoomToSelection() } } @@ -138,8 +162,8 @@ final class FloorFilterViewModel: ObservableObject { zoomToExtent(extent: site.geometry?.extent) case .facility(let facility): zoomToExtent(extent: facility.geometry?.extent) - case .level: - break + case .level(let level): + zoomToExtent(extent: level.facility?.geometry?.extent) } } @@ -158,4 +182,12 @@ final class FloorFilterViewModel: ObservableObject { ) } } + + /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. + public func filterMapToSelectedLevel() { + guard let selectedLevel = selectedLevel else { return } + levels.forEach { + $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder + } + } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index a72744dbe..6e269c3fa 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -17,74 +17,136 @@ import ArcGIS /// A view which allows selection of sites and facilities represented in a `FloorManager`. struct SiteAndFacilitySelector: View { /// Creates a `SiteAndFacilitySelector` - /// - Parameter floorFilterViewModel: The view model used by the `SiteAndFacilitySelector`. - /// - Parameter isVisible: A binding used to dismiss the site selector. - init( - floorFilterViewModel: FloorFilterViewModel, - isVisible: Binding - ) { - self.floorFilterViewModel = floorFilterViewModel - self.isVisible = isVisible + /// - Parameter isHidden: A binding used to dismiss the site selector. + init(isHidden: Binding) { + self.isHidden = isHidden } /// The view model used by the `SiteAndFacilitySelector`. - @ObservedObject - private var floorFilterViewModel: FloorFilterViewModel + @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site and facility selector. - private var isVisible: Binding + private var isHidden: Binding var body: some View { if let selectedSite = floorFilterViewModel.selectedSite { - Facilities(facilities: selectedSite.facilities, isVisible: isVisible) + Facilities(facilities: selectedSite.facilities, isHidden: isHidden) } else if floorFilterViewModel.sites.count == 1 { Facilities( facilities: floorFilterViewModel.sites.first!.facilities, - isVisible: isVisible + isHidden: isHidden ) } else { - Sites(sites: floorFilterViewModel.sites, isVisible: isVisible) + Sites(sites: floorFilterViewModel.sites, isHidden: isHidden) } } /// A view displaying the sites contained in a `FloorManager`. struct Sites: View { let sites: [FloorSite] - var isVisible: Binding + var isHidden: Binding + + @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel var body: some View { - VStack(alignment: .leading) { - Header(title: "Select a site…", isVisible: isVisible) + VStack(alignment: .center) { + Header(title: "Select a site…", isHidden: isHidden) + Divider() + + if sites.count > 5 { + ScrollView { + VStack { + ForEach(sites) { site in + HStack { + Text(site.name) + Spacer() + } + .onTapGesture { + floorFilterViewModel.selection = .site(site) + } + .padding(4) + .selected(floorFilterViewModel.selectedSite == site) + } + } + } + } else { + VStack { + ForEach(sites) { site in + HStack { + Text(site.name) + Spacer() + } + .onTapGesture { + floorFilterViewModel.selection = .site(site) + } + .padding(4) + .selected(floorFilterViewModel.selectedSite == site) + } + } + } + Divider() - ForEach(sites) { site in - Text(site.name) + Button { + // Set something so we display all facilities + } label: { + Text("All sites") } } - .esriBorder() } } /// A view displaying the facilities contained in a `FloorManager`. struct Facilities: View { let facilities: [FloorFacility] - var isVisible: Binding + var isHidden: Binding + @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + var body: some View { VStack(alignment: .leading) { - Header(title: "Select a facility…", isVisible: isVisible) + Header(title: "Select a facility…", isHidden: isHidden) Divider() - ForEach(facilities) { facility in - Text(facility.name) + if facilities.count > 5 { + ScrollView { + VStack { + ForEach(facilities) { facility in + HStack { + Text(facility.name) + Spacer() + } + .onTapGesture { + floorFilterViewModel.selection = .facility(facility) + isHidden.wrappedValue = true + } + .padding(4 ) + .selected(floorFilterViewModel.selectedFacility == facility) + } + } + } + } else { + VStack { + ForEach(facilities) { facility in + HStack { + Text(facility.name) + Spacer() + } + .onTapGesture { + floorFilterViewModel.selection = .facility(facility) + isHidden.wrappedValue = true + } + .padding(4 ) + .selected(floorFilterViewModel.selectedFacility == facility) + } + } } } - .esriBorder() } } /// The header for a site or facility selector. struct Header: View { let title: String - var isVisible: Binding + var isHidden: Binding var body: some View { HStack { @@ -92,7 +154,7 @@ struct SiteAndFacilitySelector: View { .bold() Spacer() Button { - isVisible.wrappedValue.toggle() + isHidden.wrappedValue.toggle() } label: { Image(systemName: "xmark.circle") } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 196d7e785..71ea823b0 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -296,35 +296,3 @@ extension ResultRow { ) } } - -/// A modifier which displays a background and shadow for a view. Used to represent a selected view. -struct SelectedModifier: ViewModifier { - /// `true` if the view should display as selected, `false` otherwise. - var isSelected: Bool - - func body(content: Content) -> some View { - let roundedRect = RoundedRectangle(cornerRadius: 4) - if isSelected { - content - .background(Color.secondary.opacity(0.8)) - .clipShape(roundedRect) - .shadow( - color: Color.secondary.opacity(0.8), - radius: 2 - ) - } else { - content - } - } -} - -extension View { - /// View modifier used to denote the view is selected. - /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: The view being modified. - func selected( - _ isSelected: Bool = false - ) -> some View { - modifier(SelectedModifier(isSelected: isSelected)) - } -} diff --git a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift new file mode 100644 index 000000000..c98d5e85b --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift @@ -0,0 +1,23 @@ +// 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 + +extension FloorLevel: Equatable { + public static func == (lhs: FloorLevel, rhs: FloorLevel) -> Bool { + lhs.levelId == rhs.levelId + } +} + +extension FloorLevel: Identifiable { } diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift new file mode 100644 index 000000000..6bd7b0bf9 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -0,0 +1,46 @@ +// 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 modifier which displays a background and shadow for a view. Used to represent a selected view. +struct SelectedModifier: ViewModifier { + /// `true` if the view should display as selected, `false` otherwise. + var isSelected: Bool + + func body(content: Content) -> some View { + let roundedRect = RoundedRectangle(cornerRadius: 4) + if isSelected { + content + .background(Color.secondary.opacity(0.8)) + .clipShape(roundedRect) + .shadow( + color: Color.secondary.opacity(0.8), + radius: 2 + ) + } else { + content + } + } +} + +extension View { + /// View modifier used to denote the view is selected. + /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. + /// - Returns: The view being modified. + func selected( + _ isSelected: Bool = false + ) -> some View { + modifier(SelectedModifier(isSelected: isSelected)) + } +} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json deleted file mode 100644 index 972dad443..000000000 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "business_black_18dp (1).svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "business_black_24dp (1).svg", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "business_black_24dp (1)-1.svg", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg deleted file mode 100644 index c91f34b5f..000000000 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_18dp (1).svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg deleted file mode 100644 index 541e8376f..000000000 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1)-1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg b/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg deleted file mode 100644 index 541e8376f..000000000 --- a/Sources/ArcGISToolkit/Resources/Images.xcassets/Site.imageset/business_black_24dp (1).svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From d4e3aaca848e651b158ff520f380652ec31f8e32 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 14:48:07 -0800 Subject: [PATCH 0486/1722] Enclose type/member references in backticks --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 1570c2fc1..198da76ea 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` or `SceneView`. public struct Compass: View { /// Determines if the compass should automatically hide/show itself when the parent view is oriented /// north. @@ -37,10 +37,10 @@ public struct Compass: View { /// Creates a `Compass` /// - Parameters: - /// - viewpoint: Acts a communication link between the MapView or SceneView and the + /// - viewpoint: Acts a communication link between the `MapView` or `SceneView` and the /// compass. - /// - autoHide: Determines if the compass automatically hides itself when the MapView or - /// SceneView is oriented north. + /// - autoHide: Determines if the compass automatically hides itself when the `MapView` or + /// `SceneView` is oriented north. public init( viewpoint: Binding, autoHide: Bool = true From b5c3135b5d6d23cbd2ee405c28f0f72d14cf7e88 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 14:53:01 -0800 Subject: [PATCH 0487/1722] Rename heading to compassHeadingDescription --- .../Components/Compass/Compass.swift | 4 +-- Tests/ArcGISToolkitTests/CompassTests.swift | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 198da76ea..62cf92bfa 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -72,7 +72,7 @@ public struct Compass: View { } } .onAppear { opacity = isHidden ? 0 : 1 } - .accessibilityLabel(viewpoint?.heading ?? "Compass") + .accessibilityLabel(viewpoint?.compassHeadingDescription ?? "Compass") } /// Resets the viewpoints `rotation` to zero. @@ -93,7 +93,7 @@ internal extension Viewpoint { } /// A text description of the current heading, sutiable for accessibility voiceover. - var heading: String { + var compassHeadingDescription: String { "Compass, heading " + Int(self.adjustedRotation.rounded()).description + " degrees " diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index c2ac2e12c..9db3201a1 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -21,53 +21,53 @@ class CompassTests: XCTestCase { func testCardinalAndIntercardinals() { var viewpoint = getViewpoint(0.0) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 0 degrees north" ) viewpoint = getViewpoint(23) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 337 degrees northwest" ) viewpoint = getViewpoint(68) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 292 degrees west" ) viewpoint = getViewpoint(113) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 247 degrees southwest" ) viewpoint = getViewpoint(158) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 202 degrees south" ) viewpoint = getViewpoint(203) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 157 degrees southeast" ) viewpoint = getViewpoint(248) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 112 degrees east" ) viewpoint = getViewpoint(293) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 67 degrees northeast" ) viewpoint = getViewpoint(293) XCTAssertEqual( - viewpoint.heading, + viewpoint.compassHeadingDescription, "Compass, heading 67 degrees northeast" ) } - /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide is - /// disabled. + /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide + /// is disabled. func testHiddenWithAutoHideOff() { let initialValue = 0.0 let finalValue = 90.0 @@ -79,8 +79,8 @@ class CompassTests: XCTestCase { XCTAssertFalse(compass.isHidden) } - /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide is - /// enabled. + /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide + /// is enabled. func testHiddenWithAutoHideOn() { let initialValue = 0.0 let finalValue = 90.0 From 2778ebe89786614d63ccd9b77e9c67cd99a961d1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 14:56:32 -0800 Subject: [PATCH 0488/1722] Color renaming --- .../Components/Compass/Needle.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index 9d0f729a6..e19f71186 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -20,18 +20,18 @@ struct Needle: View { ZStack { VStack(spacing: 0) { HStack(spacing: 0) { - NeedleQuadrant(color: .redLight) - NeedleQuadrant(color: .redDark) + NeedleQuadrant(color: .lightRed) + NeedleQuadrant(color: .darkRed) .rotation3DEffect( Angle(degrees: 180), axis: (x: 0, y: 1, z: 0)) } HStack(spacing: 0) { - NeedleQuadrant(color: .grayLight) + NeedleQuadrant(color: .lightGray) .rotation3DEffect( Angle(degrees: 180), axis: (x: 1, y: 0, z: 0)) - NeedleQuadrant(color: .grayDark) + NeedleQuadrant(color: .darkGray) .rotation3DEffect( Angle(degrees: 180), axis: (x: 0, y: 1, z: 0)) @@ -85,14 +85,14 @@ private extension Color { static let bronze = Color(red: 241, green: 169, blue: 59) /// The dark gray color of the compass needle. - static let grayDark = Color(red: 128, green: 128, blue: 128) - - /// The light gray color of the compass needle. - static let grayLight = Color(red: 169, green: 168, blue: 168) + static let darkGray = Color(red: 128, green: 128, blue: 128) /// The dark red color of the compass needle. - static let redDark = Color(red: 124, green: 22, blue: 13) + static let darkRed = Color(red: 124, green: 22, blue: 13) + + /// The light gray color of the compass needle. + static let lightGray = Color(red: 169, green: 168, blue: 168) /// The light red color of the compass needle. - static let redLight = Color(red: 233, green: 51, blue: 35) + static let lightRed = Color(red: 233, green: 51, blue: 35) } From b4017a1cead085f4e5f53bc29c5f1bb752c4b22b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 14:59:58 -0800 Subject: [PATCH 0489/1722] Test naming updates `testCardinalAndIntercardinals` -> `testCompassHeadingDescription` --- Tests/ArcGISToolkitTests/CompassTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 9db3201a1..ddd1503fc 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -18,7 +18,7 @@ import XCTest class CompassTests: XCTestCase { /// Asserts that accessibility labels are properly generated. - func testCardinalAndIntercardinals() { + func testCompassHeadingDescription() { var viewpoint = getViewpoint(0.0) XCTAssertEqual( viewpoint.compassHeadingDescription, From 22388a8cb37e699576bbc71e68bc6d6aa6e95688 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 15:38:52 -0800 Subject: [PATCH 0490/1722] Guard against unneeded animation calls --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 62cf92bfa..2fa80046c 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -67,8 +67,10 @@ public struct Compass: View { .opacity(opacity) .onTapGesture { resetHeading() } .onChange(of: viewpoint) { _ in + let newOpacity: Double = isHidden ? .zero : 1 + guard opacity != newOpacity else { return } withAnimation(.default.delay(isHidden ? 0.25 : 0)) { - opacity = isHidden ? 0 : 1 + opacity = newOpacity } } .onAppear { opacity = isHidden ? 0 : 1 } From 7562956368374e249ca322ce4753df09bebc68fc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 16:10:22 -0800 Subject: [PATCH 0491/1722] Clarify the necessity of `adjustedRotation` in documentation --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 2fa80046c..eaa92268b 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -89,7 +89,8 @@ public struct Compass: View { } internal extension Viewpoint { - /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. + /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. This is + /// needed because the default viewpoint rotation is opposite the natural direction of a compass needle. var adjustedRotation: Double { rotation.isZero ? .zero : 360 - rotation } From 84c85bd4646c9f140a4013a81aed5db1b84a74e5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 16:17:34 -0800 Subject: [PATCH 0492/1722] Add tests for adjustedRotation --- Tests/ArcGISToolkitTests/CompassTests.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index ddd1503fc..6c6734833 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -17,6 +17,19 @@ import XCTest @testable import ArcGISToolkit class CompassTests: XCTestCase { + /// Asserts that viewpoint rotations are correctly adjusted to provide for natural compass rotation + /// behavior. + func testAdjustedRotation() { + var viewpoint = getViewpoint(.zero) + XCTAssertEqual(viewpoint.adjustedRotation, .zero) + viewpoint = getViewpoint(45) + XCTAssertEqual(viewpoint.adjustedRotation, 315) + viewpoint = getViewpoint(315) + XCTAssertEqual(viewpoint.adjustedRotation, 45) + viewpoint = getViewpoint(180) + XCTAssertEqual(viewpoint.adjustedRotation, 180) + } + /// Asserts that accessibility labels are properly generated. func testCompassHeadingDescription() { var viewpoint = getViewpoint(0.0) From 6b668993d59629e0e48a6ab07b8042fe338c3541 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 17:54:42 -0800 Subject: [PATCH 0493/1722] Refactor angle normalization --- .../Components/Compass/CompassDirection.swift | 14 ++------- Sources/ArcGISToolkit/Extensions/Angle.swift | 26 ++++++++++++++++ Tests/ArcGISToolkitTests/AngleTests.swift | 31 +++++++++++++++++++ .../CompassDirectionTests.swift | 13 -------- 4 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Angle.swift create mode 100644 Tests/ArcGISToolkitTests/AngleTests.swift diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift index c25e9b344..5ce21cfe5 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation +import SwiftUI /// Represents a cardinal or intercardinal direction. internal enum CompassDirection: String { @@ -29,7 +29,7 @@ internal extension CompassDirection { /// Initializes a `CompassDirection` from a given degree value. All values will be normalized /// between 0° and 360°. init(_ degrees: Double) { - let angle = CompassDirection.normalizedAngle(degrees) + let angle = Angle(degrees: degrees).normalized switch angle { case 0..<22.5, 337.5..<360: self = .north @@ -51,14 +51,4 @@ internal extension CompassDirection { fatalError() } } - - /// Normalizes degree values between 0° and 360°. - static func normalizedAngle(_ degrees: Double) -> Double { - let normalized = degrees.truncatingRemainder(dividingBy: 360) - if normalized < 0 { - return normalized + 360 - } else { - return normalized - } - } } diff --git a/Sources/ArcGISToolkit/Extensions/Angle.swift b/Sources/ArcGISToolkit/Extensions/Angle.swift new file mode 100644 index 000000000..740e1797a --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Angle.swift @@ -0,0 +1,26 @@ +// 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 + +extension Angle { + /// This angle's degree value normalized between 0° and 360°. + var normalized: Double { + let normalized = self.degrees.truncatingRemainder(dividingBy: 360) + if normalized < 0 { + return normalized + 360 + } else { + return normalized + } + } +} diff --git a/Tests/ArcGISToolkitTests/AngleTests.swift b/Tests/ArcGISToolkitTests/AngleTests.swift new file mode 100644 index 000000000..35ad8d3fe --- /dev/null +++ b/Tests/ArcGISToolkitTests/AngleTests.swift @@ -0,0 +1,31 @@ +// 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 XCTest +@testable import ArcGISToolkit + +class AngleTests: XCTestCase { + /// Tests the behvaior of `Angle`'s normalized member. + func testNormalizedAngle() { + XCTAssertEqual(Angle(degrees: -361.0).normalized, 359) + XCTAssertEqual(Angle(degrees: -360.0).normalized, 0) + XCTAssertEqual(Angle(degrees: -180.0).normalized, 180) + XCTAssertEqual(Angle(degrees: -0).normalized, 0) + XCTAssertEqual(Angle(degrees: 0).normalized, 0) + XCTAssertEqual(Angle(degrees: 45).normalized, 45) + XCTAssertEqual(Angle(degrees: 180).normalized, 180) + XCTAssertEqual(Angle(degrees: 360).normalized, 0) + XCTAssertEqual(Angle(degrees: 405).normalized, 45) + } +} diff --git a/Tests/ArcGISToolkitTests/CompassDirectionTests.swift b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift index 7af3be076..34219d2ee 100644 --- a/Tests/ArcGISToolkitTests/CompassDirectionTests.swift +++ b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift @@ -32,17 +32,4 @@ class CompassDirectionTests: XCTestCase { XCTAssertEqual(CompassDirection(359), .north) XCTAssertEqual(CompassDirection(450), .east) } - - /// Tests the behvaior of `CompassDirection.normalizedAngle(_ : Double)` - func testNormalizedAngle() { - XCTAssertEqual(CompassDirection.normalizedAngle(-361.0), 359) - XCTAssertEqual(CompassDirection.normalizedAngle(-360.0), 0) - XCTAssertEqual(CompassDirection.normalizedAngle(-180.0), 180) - XCTAssertEqual(CompassDirection.normalizedAngle(-0), 0) - XCTAssertEqual(CompassDirection.normalizedAngle(0), 0) - XCTAssertEqual(CompassDirection.normalizedAngle(45), 45) - XCTAssertEqual(CompassDirection.normalizedAngle(180), 180) - XCTAssertEqual(CompassDirection.normalizedAngle(360), 0) - XCTAssertEqual(CompassDirection.normalizedAngle(405), 45) - } } From 4862bd697eb7f186c903e4aeba30d647bf436f49 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Feb 2022 18:33:25 -0800 Subject: [PATCH 0494/1722] Rename `isHidden` for `shouldHide`. Remove `public` access level --- .../Components/Compass/Compass.swift | 20 +++++++++---------- Tests/ArcGISToolkitTests/CompassTests.swift | 16 +++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index eaa92268b..568b45752 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -20,17 +20,17 @@ public struct Compass: View { /// north. private let autoHide: Bool - /// Indicates if the compass is hidden based on the current viewpoint rotation and - /// autoHide preference. - public var isHidden: Bool { - guard let viewpoint = viewpoint else { return autoHide } - return viewpoint.rotation.isZero && autoHide - } - /// Controls the current opacity of the compass. @State private var opacity: Double + /// Indicates if the compass should hide based on the current viewpoint rotation and autoHide + /// preference. + var shouldHide: Bool { + guard let viewpoint = viewpoint else { return autoHide } + return viewpoint.rotation.isZero && autoHide + } + /// Acts as link between the compass and the parent map or scene view. @Binding private var viewpoint: Viewpoint? @@ -67,13 +67,13 @@ public struct Compass: View { .opacity(opacity) .onTapGesture { resetHeading() } .onChange(of: viewpoint) { _ in - let newOpacity: Double = isHidden ? .zero : 1 + let newOpacity: Double = shouldHide ? .zero : 1 guard opacity != newOpacity else { return } - withAnimation(.default.delay(isHidden ? 0.25 : 0)) { + withAnimation(.default.delay(shouldHide ? 0.25 : 0)) { opacity = newOpacity } } - .onAppear { opacity = isHidden ? 0 : 1 } + .onAppear { opacity = shouldHide ? 0 : 1 } .accessibilityLabel(viewpoint?.compassHeadingDescription ?? "Compass") } diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 6c6734833..5939afb53 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -87,9 +87,9 @@ class CompassTests: XCTestCase { var _viewpoint: Viewpoint? = getViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint, autoHide: false) - XCTAssertFalse(compass.isHidden) + XCTAssertFalse(compass.shouldHide) _viewpoint = getViewpoint(finalValue) - XCTAssertFalse(compass.isHidden) + XCTAssertFalse(compass.shouldHide) } /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide @@ -100,9 +100,9 @@ class CompassTests: XCTestCase { var _viewpoint: Viewpoint? = getViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - XCTAssertTrue(compass.isHidden) + XCTAssertTrue(compass.shouldHide) _viewpoint = getViewpoint(finalValue) - XCTAssertFalse(compass.isHidden) + XCTAssertFalse(compass.shouldHide) } /// Asserts that the compass correctly initializes when given a nil viewpoint @@ -110,7 +110,7 @@ class CompassTests: XCTestCase { var _viewpoint: Viewpoint? = nil let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(compass.isHidden, true) + XCTAssertEqual(compass.shouldHide, true) } /// Asserts that the compass correctly initializes when given a nil viewpoint, and autoHide is disabled. @@ -118,7 +118,7 @@ class CompassTests: XCTestCase { var _viewpoint: Viewpoint? = nil let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint, autoHide: false) - XCTAssertEqual(compass.isHidden, false) + XCTAssertEqual(compass.shouldHide, false) } /// Asserts that the compass correctly initializes when given only a viewpoint. @@ -126,7 +126,7 @@ class CompassTests: XCTestCase { var _viewpoint: Viewpoint? = getViewpoint(0.0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(compass.isHidden, true) + XCTAssertEqual(compass.shouldHide, true) } /// Asserts that the compass correctly initializes when given only a viewpoint. @@ -138,7 +138,7 @@ class CompassTests: XCTestCase { viewpoint: viewpoint, autoHide: autoHide ) - XCTAssertEqual(compass.isHidden, false) + XCTAssertEqual(compass.shouldHide, false) } func testResetHeading() { From 89225ecc954fda1df5be3916d155159d30bad9e4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Feb 2022 10:04:21 -0800 Subject: [PATCH 0495/1722] Rename `adjustedRotation` to `compassHeading` --- .../ArcGISToolkit/Components/Compass/Compass.swift | 12 ++++++------ Tests/ArcGISToolkitTests/CompassTests.swift | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 568b45752..18fafb0e6 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -56,7 +56,7 @@ public struct Compass: View { CompassBody() Needle() .rotationEffect( - Angle(degrees: viewpoint?.adjustedRotation ?? .zero) + Angle(degrees: viewpoint?.compassHeading ?? .zero) ) } .frame( @@ -89,17 +89,17 @@ public struct Compass: View { } internal extension Viewpoint { - /// The viewpoint's `rotation` adjusted to offset any rotation applied to the parent view. This is - /// needed because the default viewpoint rotation is opposite the natural direction of a compass needle. - var adjustedRotation: Double { + /// The heading appropriate for displaying a compass. + /// - Remark: The viewpoint rotation is opposite of the direction of a compass needle. + var compassHeading: Double { rotation.isZero ? .zero : 360 - rotation } /// A text description of the current heading, sutiable for accessibility voiceover. var compassHeadingDescription: String { "Compass, heading " - + Int(self.adjustedRotation.rounded()).description + + Int(self.compassHeading.rounded()).description + " degrees " - + CompassDirection(self.adjustedRotation).rawValue + + CompassDirection(self.compassHeading).rawValue } } diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 5939afb53..fc7944dc8 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -21,13 +21,13 @@ class CompassTests: XCTestCase { /// behavior. func testAdjustedRotation() { var viewpoint = getViewpoint(.zero) - XCTAssertEqual(viewpoint.adjustedRotation, .zero) + XCTAssertEqual(viewpoint.compassHeading, .zero) viewpoint = getViewpoint(45) - XCTAssertEqual(viewpoint.adjustedRotation, 315) + XCTAssertEqual(viewpoint.compassHeading, 315) viewpoint = getViewpoint(315) - XCTAssertEqual(viewpoint.adjustedRotation, 45) + XCTAssertEqual(viewpoint.compassHeading, 45) viewpoint = getViewpoint(180) - XCTAssertEqual(viewpoint.adjustedRotation, 180) + XCTAssertEqual(viewpoint.compassHeading, 180) } /// Asserts that accessibility labels are properly generated. From 7c75d19db916889f9a1a10a2dc993046f650fa48 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Feb 2022 10:07:17 -0800 Subject: [PATCH 0496/1722] Renames `normalized` to `normalizedDegrees` --- .../Components/Compass/CompassDirection.swift | 2 +- Sources/ArcGISToolkit/Extensions/Angle.swift | 2 +- Tests/ArcGISToolkitTests/AngleTests.swift | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift index 5ce21cfe5..656f454d1 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassDirection.swift @@ -29,7 +29,7 @@ internal extension CompassDirection { /// Initializes a `CompassDirection` from a given degree value. All values will be normalized /// between 0° and 360°. init(_ degrees: Double) { - let angle = Angle(degrees: degrees).normalized + let angle = Angle(degrees: degrees).normalizedDegrees switch angle { case 0..<22.5, 337.5..<360: self = .north diff --git a/Sources/ArcGISToolkit/Extensions/Angle.swift b/Sources/ArcGISToolkit/Extensions/Angle.swift index 740e1797a..b6dd016de 100644 --- a/Sources/ArcGISToolkit/Extensions/Angle.swift +++ b/Sources/ArcGISToolkit/Extensions/Angle.swift @@ -15,7 +15,7 @@ import SwiftUI extension Angle { /// This angle's degree value normalized between 0° and 360°. - var normalized: Double { + var normalizedDegrees: Double { let normalized = self.degrees.truncatingRemainder(dividingBy: 360) if normalized < 0 { return normalized + 360 diff --git a/Tests/ArcGISToolkitTests/AngleTests.swift b/Tests/ArcGISToolkitTests/AngleTests.swift index 35ad8d3fe..1a1863575 100644 --- a/Tests/ArcGISToolkitTests/AngleTests.swift +++ b/Tests/ArcGISToolkitTests/AngleTests.swift @@ -18,14 +18,14 @@ import XCTest class AngleTests: XCTestCase { /// Tests the behvaior of `Angle`'s normalized member. func testNormalizedAngle() { - XCTAssertEqual(Angle(degrees: -361.0).normalized, 359) - XCTAssertEqual(Angle(degrees: -360.0).normalized, 0) - XCTAssertEqual(Angle(degrees: -180.0).normalized, 180) - XCTAssertEqual(Angle(degrees: -0).normalized, 0) - XCTAssertEqual(Angle(degrees: 0).normalized, 0) - XCTAssertEqual(Angle(degrees: 45).normalized, 45) - XCTAssertEqual(Angle(degrees: 180).normalized, 180) - XCTAssertEqual(Angle(degrees: 360).normalized, 0) - XCTAssertEqual(Angle(degrees: 405).normalized, 45) + XCTAssertEqual(Angle(degrees: -361.0).normalizedDegrees, 359) + XCTAssertEqual(Angle(degrees: -360.0).normalizedDegrees, 0) + XCTAssertEqual(Angle(degrees: -180.0).normalizedDegrees, 180) + XCTAssertEqual(Angle(degrees: -0).normalizedDegrees, 0) + XCTAssertEqual(Angle(degrees: 0).normalizedDegrees, 0) + XCTAssertEqual(Angle(degrees: 45).normalizedDegrees, 45) + XCTAssertEqual(Angle(degrees: 180).normalizedDegrees, 180) + XCTAssertEqual(Angle(degrees: 360).normalizedDegrees, 0) + XCTAssertEqual(Angle(degrees: 405).normalizedDegrees, 45) } } From 422ec335b5409869723c2f9fb90f4d50ba7ee115 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Feb 2022 11:42:14 -0800 Subject: [PATCH 0497/1722] Add Bookmarks example entry --- Examples/Examples.xcodeproj/project.pbxproj | 4 ++++ Examples/Examples/BookmarksExampleView.swift | 20 ++++++++++++++++++++ Examples/ExamplesApp/Examples.swift | 1 + 3 files changed, 25 insertions(+) create mode 100644 Examples/Examples/BookmarksExampleView.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index a3c03372b..4968854ac 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */; }; E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; @@ -37,6 +38,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksExampleView.swift; sourceTree = ""; }; E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloorFilterExampleView.swift; sourceTree = ""; }; E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -72,6 +74,7 @@ isa = PBXGroup; children = ( E4C389D426B8A12C002BC255 /* BasemapGalleryExampleView.swift */, + 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */, E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, E42BFBE82672BF9500159107 /* SearchExampleView.swift */, E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, @@ -213,6 +216,7 @@ buildActionMask = 2147483647; files = ( E48A73452658227100F5C118 /* Examples.swift in Sources */, + 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift new file mode 100644 index 000000000..237bb8f78 --- /dev/null +++ b/Examples/Examples/BookmarksExampleView.swift @@ -0,0 +1,20 @@ +// 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 + +struct BookmarksExampleView: View { + var body: some View { + EmptyView() + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 3b3b4cec8..83a40d5bf 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -36,6 +36,7 @@ extension ExampleList { name: "GeoView", examples: [ AnyExample("Basemap Gallery", content: BasemapGalleryExampleView()), + AnyExample("Bookmarks", content: BookmarksExampleView()), AnyExample("Floor Filter", content: FloorFilterExampleView()), AnyExample("Overview Map", content: OverviewMapExampleView()), AnyExample("Search", content: SearchExampleView()) From 359965c6d4ec38bf020bfda5f87eee37318034e7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Feb 2022 17:23:49 -0800 Subject: [PATCH 0498/1722] Initial bookmarks implementation --- Examples/Examples/BookmarksExampleView.swift | 111 +++++++++++++++++- .../ArcGISToolkit/Components/Bookmarks.swift | 64 ++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 Sources/ArcGISToolkit/Components/Bookmarks.swift diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 237bb8f78..125b1b5ed 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -11,10 +11,119 @@ // See the License for the specific language governing permissions and // limitations under the License. +import ArcGIS +import ArcGISToolkit import SwiftUI struct BookmarksExampleView: View { + /// The map displayed in the map view. + private let map = Map(basemapStyle: .arcGISImagery) + + /// Indicates if the bookmarks list is shown or not. + /// - Remark: This allows a developer to control how the bookmarks menu is shown/hidden, + /// whether that be in a group of options or a standalone button. + @State + var showingBookmarks = false + + @State + var viewpoint: Viewpoint? = nil + var body: some View { - EmptyView() + MapView(map: map, viewpoint: viewpoint) + .onTapGesture { + showingBookmarks = false + } + // Show the bookmarks control as button + .overlay(alignment: .topLeading) { + Button { + showingBookmarks.toggle() + } label: { + Image( + systemName: + showingBookmarks ? "bookmark.fill" : "bookmark" + ) + } + .buttonStyle(.bordered) + .padding( + EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 0) + ) + } + // Show the bookmarks control as an option within a group + .toolbar { + ToolbarItem(placement: .primaryAction) { + Menu { + Button { + showingBookmarks.toggle() + } label: { + Label( + showingBookmarks ? + "Hide Bookmarks" : "Show Bookmarks", + systemImage: showingBookmarks ? + "bookmark.fill" : "bookmark" + ) + } + Text("More Options") + } + label: { + Label("Options", systemImage: "ellipsis") + } + } + } + .overlay { + Bookmarks(sampleBookmarks, isPresented: $showingBookmarks) + .onSelectionChanged { + print($0.name) + viewpoint = $0.viewpoint + } + } } } + +extension BookmarksExampleView { + var sampleBookmarks: [Bookmark] {[ + Bookmark( + name: "Yosemite National Park", + viewpoint: Viewpoint( + center: Point( + x: -119.538330, + y: 37.865101, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Zion National Park", + viewpoint: Viewpoint( + center: Point( + x: -113.028770, + y: 37.297817, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Yellowstone National Park", + viewpoint: Viewpoint( + center: Point( + x: -110.584663, + y: 44.429764, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ), + Bookmark( + name: "Grand Canyon National Park", + viewpoint: Viewpoint( + center: Point( + x: -112.1129, + y: 36.1069, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ), + ]} +} diff --git a/Sources/ArcGISToolkit/Components/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks.swift new file mode 100644 index 000000000..d4d8f2b66 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Bookmarks.swift @@ -0,0 +1,64 @@ +// 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 +import SwiftUI + +/// The bookmarks component allows for a user to select and navigate to defined set of "bookmarked" +/// locations. +public struct Bookmarks: View { + private var bookmarks: [Bookmark] + + /// Determines if the bookmarks list is currently shown or not. + @Binding + private var isPresented: Bool + + private var viewpoint: Binding? + + /// Creates a `Bookmarks` component. + public init(_ bookmarks: [Bookmark], isPresented: Binding, viewpoint: Binding? = nil) { + self.bookmarks = bookmarks + self.viewpoint = viewpoint + _isPresented = isPresented + } + + var selectionChangedActions: ((Bookmark) -> Void)? = nil + + /// Sets a closure to perform when the viewpoint of the map view changes. + /// - Parameters: + /// - kind: The kind of viewpoint passed to the `action` closure. + /// - action: The closure to perform when the viewpoint has changed. + public func onSelectionChanged(perform action: @escaping (Bookmark) -> Void) -> Bookmarks { + var copy = self + copy.selectionChangedActions = action + return copy + } + + public var body: some View { + if isPresented { + FloatingPanel { + ForEach(bookmarks, id: \.viewpoint) { bookmark in + Button { + viewpoint?.wrappedValue = bookmark.viewpoint + selectionChangedActions?(bookmark) + } label: { + Text(bookmark.name) + } + } + } + .frame( + width: 300, height: 600, alignment: .bottomLeading + ) + } + } +} From 2a0ed7f13c73cf506c27b080295506136a8f96be Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 19 Feb 2022 12:00:02 -0600 Subject: [PATCH 0499/1722] Improve level button look. Constraint scroll view to content height. --- .../Examples/FloorFilterExampleView.swift | 6 +- .../Components/FloorFilter/FloorFilter.swift | 129 +++++++++++------- Sources/ArcGISToolkit/Extensions/Button.swift | 43 ++++++ 3 files changed, 126 insertions(+), 52 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Button.swift diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 480e3d390..55580035d 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -45,8 +45,6 @@ struct FloorFilterExampleView: View { map = Map(item: portalItem) } - private let floorFilterPadding: CGFloat = 48 - var body: some View { MapView( map: map, @@ -59,8 +57,8 @@ struct FloorFilterExampleView: View { floorManager: floorManager, viewpoint: $viewpoint ) - .frame(idealHeight: 300) - .padding(floorFilterPadding) + .frame(height: 300) + .padding(36) } } .task { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 31ee1ed18..a7b19dcf5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -44,23 +44,52 @@ public struct FloorFilter: View { @State private var isLevelsViewCollapsed: Bool = false + /// 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. + private var hasLevelsToDisplay: Bool { + !(viewModel.selectedFacility == nil || + viewModel.selectedFacility!.levels.isEmpty) + } + + /// The selected facility's levels, sorted by `level.verticalOrder`. + private var sortedLevels: [FloorLevel] { + viewModel.selectedFacility?.levels.sorted() { + $0.verticalOrder > $1.verticalOrder + } ?? [] + } + public var body: some View { Group { if viewModel.isLoading { - ProgressView() - .progressViewStyle(.circular) - .padding(12) + VStack { + Spacer() + ProgressView() + .progressViewStyle(.circular) + .padding(12) + } } else { HStack(alignment: .bottom) { VStack { - LevelsView(isCollapsed: $isLevelsViewCollapsed) - Button { - isSelectorHidden.toggle() - } label: { - Image(systemName: "building.2") + Spacer() + VStack { + if hasLevelsToDisplay { + LevelsView( + levels: sortedLevels, + isCollapsed: $isLevelsViewCollapsed + ) + Divider() + .frame(width: 30) + } + // Site button. + Button { + isSelectorHidden.toggle() + } label: { + Image(systemName: "building.2") + } + .padding(4) } + .esriBorder() } - .esriBorder() if !isSelectorHidden { SiteAndFacilitySelector(isHidden: $isSelectorHidden) .esriBorder() @@ -72,52 +101,55 @@ public struct FloorFilter: View { } } +/// A view displaying the levels in the selected facility. struct LevelsView: View { - /// Allows the user to toggle the visibility of the site and facility selector. - var isCollapsed: Binding + /// The levels to display. + let levels: [FloorLevel] + + /// 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. + @Binding + var isCollapsed: Bool /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel - private var sortedLevels: [FloorLevel] { - viewModel.selectedFacility?.levels.sorted() { - $0.verticalOrder > $1.verticalOrder - } ?? [] - } + /// The height of the scroll view's content. + @State + private var scrollViewContentHeight: CGFloat = .zero public var body: some View { - if viewModel.selectedFacility == nil || - viewModel.selectedFacility!.levels.isEmpty{ - EmptyView() - } else { - VStack { - if !isCollapsed.wrappedValue { - CollapseButton(isCollapsed: isCollapsed) - Divider() - .frame(width: 30) - if let levels = sortedLevels { - if levels.count > 3 { - ScrollView { - LevelsStack(levels: levels) + VStack { + if !isCollapsed, + levels.count > 1 { + CollapseButton(isCollapsed: $isCollapsed) + Divider() + .frame(width: 30) + ScrollView { + LevelsStack(levels: levels) + .background( + GeometryReader { geometry -> Color in + DispatchQueue.main.async { + scrollViewContentHeight = geometry.size.height + } + return .clear } - } else { - LevelsStack(levels: levels) - } - } + ) } - else if isCollapsed.wrappedValue { - // Button for the selected level. - Button { - isCollapsed.wrappedValue.toggle() - } label: { - Text(viewModel.selectedLevel?.shortName ?? (sortedLevels.first?.shortName ?? "None")) + .frame(maxHeight: scrollViewContentHeight) + } + else { + // Button for the selected level. + Button { + if levels.count > 1 { + isCollapsed.toggle() } - .padding(8) - .selected(true) + } label: { + Text(viewModel.selectedLevel?.shortName ?? (levels.first?.shortName ?? "None")) } - - Divider() - .frame(width: 30) + .buttonSelected(true) + .padding(4) } } } @@ -138,8 +170,8 @@ struct LevelsStack: View { } label: { Text(level.shortName) } - .padding(8) - .selected(level == viewModel.selectedLevel) + .buttonSelected(level == viewModel.selectedLevel) + .padding(4) } } } @@ -148,11 +180,12 @@ struct LevelsStack: View { /// A button used to collapse the floor level list. struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. - var isCollapsed: Binding + @Binding + var isCollapsed: Bool var body: some View { Button { - isCollapsed.wrappedValue.toggle() + isCollapsed.toggle() } label: { Image(systemName: "xmark") } diff --git a/Sources/ArcGISToolkit/Extensions/Button.swift b/Sources/ArcGISToolkit/Extensions/Button.swift new file mode 100644 index 000000000..28b5c5214 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Button.swift @@ -0,0 +1,43 @@ +// 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 modifier which "selects" a button. If selected the button will display with the +/// `BorderedProminentButtonStyle`. If not selected, the button will display with the +/// `BorderedButtonStyle`. +struct ButtonSelectedModifier: ViewModifier { + /// `true` if the view should display as selected, `false` otherwise. + var isSelected: Bool + + func body(content: Content) -> some View { + if isSelected { + content + .buttonStyle(BorderedProminentButtonStyle()) + } else { + content + .buttonStyle(BorderedButtonStyle()) + } + } +} + +extension Button { + /// Button modifier used to denote the button is selected. + /// - Parameter isSelected: `true` if the button is selected, `false` otherwise. + /// - Returns: The button being modified. + func buttonSelected( + _ isSelected: Bool = false + ) -> some View { + modifier(ButtonSelectedModifier(isSelected: isSelected)) + } +} From ca0ed944704c22046b2758bb4be38fd687f33074 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 19 Feb 2022 12:29:22 -0600 Subject: [PATCH 0500/1722] Update view model with changes from Obj-C toolkit; SiteAndFacilityView scroll view improvements. --- .../FloorFilter/FloorFilterViewModel.swift | 20 ++--- .../FloorFilter/SiteAndFacilitySelector.swift | 75 ++++++++----------- 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 04663f624..4a6a084c4 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -78,16 +78,18 @@ final class FloorFilterViewModel: ObservableObject { @Published private(set) var isLoading = true - /// Gets the default level for a facility. Uses level with vertical order 0 otherwise gets the lowest level. - public func getDefaultLevelForFacility( - facility: FloorFacility? - ) -> FloorLevel? { - let candidateLevels = levels.filter { $0.facility == facility } - return candidateLevels.first { $0.verticalOrder == 0 } ?? getLowestLevel() + /// Gets the default level for a facility. + /// - Parameter facility: The facility to get the default level for. + /// - Returns: The default level for the facility, which is the level with vertical order 0; + /// if there's no level with vertical order of 0, it returns the lowest level. + func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { + return levels.first(where: { level in + level.facility == facility && level.verticalOrder == .zero + }) ?? lowestLevel() } - /// Returns the AGSFloorLevel with the lowest verticalOrder. - private func getLowestLevel() -> FloorLevel? { + /// Returns the level with the lowest vertical order. + private func lowestLevel() -> FloorLevel? { let sortedLevels = levels.sorted { $0.verticalOrder < $1.verticalOrder } @@ -101,7 +103,7 @@ final class FloorFilterViewModel: ObservableObject { var selection: Selection? { didSet { if case let .facility(facility) = selection, - let level = getDefaultLevelForFacility(facility: facility) { + let level = defaultLevel(for: facility) { selection = .level(level) } else { filterMapToSelectedLevel() diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 6e269c3fa..640a4d17d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -48,28 +48,15 @@ struct SiteAndFacilitySelector: View { @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + /// The height of the scroll view's content. + @State + private var scrollViewContentHeight: CGFloat = .zero + var body: some View { VStack(alignment: .center) { Header(title: "Select a site…", isHidden: isHidden) Divider() - - if sites.count > 5 { - ScrollView { - VStack { - ForEach(sites) { site in - HStack { - Text(site.name) - Spacer() - } - .onTapGesture { - floorFilterViewModel.selection = .site(site) - } - .padding(4) - .selected(floorFilterViewModel.selectedSite == site) - } - } - } - } else { + ScrollView { VStack { ForEach(sites) { site in HStack { @@ -83,47 +70,36 @@ struct SiteAndFacilitySelector: View { .selected(floorFilterViewModel.selectedSite == site) } } + .background( + GeometryReader { geometry -> Color in + DispatchQueue.main.async { + scrollViewContentHeight = geometry.size.height + } + return .clear + } + ) } - - Divider() - Button { - // Set something so we display all facilities - } label: { - Text("All sites") - } + .frame(maxHeight: scrollViewContentHeight) } } } - + /// A view displaying the facilities contained in a `FloorManager`. struct Facilities: View { let facilities: [FloorFacility] var isHidden: Binding @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel - + + /// The height of the scroll view's content. + @State + private var scrollViewContentHeight: CGFloat = .zero + var body: some View { VStack(alignment: .leading) { Header(title: "Select a facility…", isHidden: isHidden) Divider() - if facilities.count > 5 { - ScrollView { - VStack { - ForEach(facilities) { facility in - HStack { - Text(facility.name) - Spacer() - } - .onTapGesture { - floorFilterViewModel.selection = .facility(facility) - isHidden.wrappedValue = true - } - .padding(4 ) - .selected(floorFilterViewModel.selectedFacility == facility) - } - } - } - } else { + ScrollView { VStack { ForEach(facilities) { facility in HStack { @@ -138,7 +114,16 @@ struct SiteAndFacilitySelector: View { .selected(floorFilterViewModel.selectedFacility == facility) } } + .background( + GeometryReader { geometry -> Color in + DispatchQueue.main.async { + scrollViewContentHeight = geometry.size.height + } + return .clear + } + ) } + .frame(maxHeight: scrollViewContentHeight) } } } From 98590a96e9c5756fd2fe92642cbb5445db82aa5a Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Mon, 21 Feb 2022 15:18:47 -0800 Subject: [PATCH 0501/1722] remove type casting as it is no longer needed --- .../FloorFilter/FloorFilterViewModel.swift | 2 +- .../Components/Search/SearchViewModel.swift | 2 +- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index b61d1b831..971c50696 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -150,7 +150,7 @@ final public class FloorFilterViewModel: ObservableObject { let builder = EnvelopeBuilder(envelope: extent) builder.expand(factor: 1.5) - let targetExtent = builder.toGeometry() as! Envelope + let targetExtent = builder.toGeometry() if !targetExtent.isEmpty { viewpoint.wrappedValue = Viewpoint( targetExtent: targetExtent diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 2b17d2789..1c8324617 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -390,7 +390,7 @@ private extension SearchViewModel { shouldZoomToResults { let builder = EnvelopeBuilder(envelope: envelope) builder.expand(factor: 1.1) - let targetExtent = builder.toGeometry() as! Envelope + let targetExtent = builder.toGeometry() viewpoint.wrappedValue = Viewpoint( targetExtent: targetExtent ) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index f10e9d631..d015eb12c 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -146,7 +146,7 @@ class SearchViewModelTests: XCTestCase { var builder = EnvelopeBuilder(envelope: model.geoViewExtent) let tenPercentWidth = model.geoViewExtent!.width * 0.1 builder.offsetBy(x: tenPercentWidth, y: 0.0) - var newExtent = builder.toGeometry() as! Envelope + var newExtent = builder.toGeometry() model.geoViewExtent = newExtent XCTAssertFalse(model.isEligibleForRequery) @@ -155,7 +155,7 @@ class SearchViewModelTests: XCTestCase { builder = EnvelopeBuilder(envelope: model.geoViewExtent) let fiftyPercentWidth = model.geoViewExtent!.width * 0.5 builder.offsetBy(x: fiftyPercentWidth, y: 0.0) - newExtent = builder.toGeometry() as! Envelope + newExtent = builder.toGeometry() model.geoViewExtent = newExtent XCTAssertTrue(model.isEligibleForRequery) @@ -172,7 +172,7 @@ class SearchViewModelTests: XCTestCase { // Expand extent by 1.1x - isEligibleForRequery should still be `false`. builder = EnvelopeBuilder(envelope: model.geoViewExtent) builder.expand(factor: 1.1) - newExtent = builder.toGeometry() as! Envelope + newExtent = builder.toGeometry() model.geoViewExtent = newExtent XCTAssertFalse(model.isEligibleForRequery) @@ -180,7 +180,7 @@ class SearchViewModelTests: XCTestCase { // Expand extent by 1.5x - isEligibleForRequery should now be `true`. builder = EnvelopeBuilder(envelope: model.geoViewExtent) builder.expand(factor: 1.5) - newExtent = builder.toGeometry() as! Envelope + newExtent = builder.toGeometry() model.geoViewExtent = newExtent XCTAssertTrue(model.isEligibleForRequery) @@ -456,7 +456,7 @@ extension Polygon { _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, spatialReference: .wgs84)) - return builder.toGeometry() as! ArcGIS.Polygon + return builder.toGeometry() }() static let minneapolis: Polygon = { @@ -465,7 +465,7 @@ extension Polygon { _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - return builder.toGeometry() as! ArcGIS.Polygon + return builder.toGeometry() }() } From 976cc2287f6ea9c91fd03711b931b1de8dfeee71 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 11:57:12 -0800 Subject: [PATCH 0502/1722] Ignore generated schemes --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f23d8f015..4238dc30b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store /.build /Packages +xcshareddata/ xcuserdata/ /*.xcodeproj DerivedData/ From 739afdd967a4551a22777991db242f466b5a7a20 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 12:24:17 -0800 Subject: [PATCH 0503/1722] Remove logging --- Examples/Examples/BookmarksExampleView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 125b1b5ed..996e8f127 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -72,7 +72,6 @@ struct BookmarksExampleView: View { .overlay { Bookmarks(sampleBookmarks, isPresented: $showingBookmarks) .onSelectionChanged { - print($0.name) viewpoint = $0.viewpoint } } From 6ef42b764732e020e9d1e7967cbdda53db080abf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 13:44:05 -0800 Subject: [PATCH 0504/1722] Formatting --- Sources/ArcGISToolkit/Components/Bookmarks.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks.swift index d4d8f2b66..ae54d824c 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks.swift @@ -26,7 +26,11 @@ public struct Bookmarks: View { private var viewpoint: Binding? /// Creates a `Bookmarks` component. - public init(_ bookmarks: [Bookmark], isPresented: Binding, viewpoint: Binding? = nil) { + public init( + _ bookmarks: [Bookmark], + isPresented: Binding, + viewpoint: Binding? = nil + ) { self.bookmarks = bookmarks self.viewpoint = viewpoint _isPresented = isPresented @@ -38,7 +42,9 @@ public struct Bookmarks: View { /// - Parameters: /// - kind: The kind of viewpoint passed to the `action` closure. /// - action: The closure to perform when the viewpoint has changed. - public func onSelectionChanged(perform action: @escaping (Bookmark) -> Void) -> Bookmarks { + public func onSelectionChanged( + perform action: @escaping (Bookmark) -> Void + ) -> Bookmarks { var copy = self copy.selectionChangedActions = action return copy @@ -49,6 +55,7 @@ public struct Bookmarks: View { FloatingPanel { ForEach(bookmarks, id: \.viewpoint) { bookmark in Button { + isPresented = false viewpoint?.wrappedValue = bookmark.viewpoint selectionChangedActions?(bookmark) } label: { From a3797090246e4b2b4f6958d3a928d125a3b3d50e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 14:30:09 -0800 Subject: [PATCH 0505/1722] Provide viewpoint change flexibility --- Examples/Examples/BookmarksExampleView.swift | 32 ++++++++++++++----- .../ArcGISToolkit/Components/Bookmarks.swift | 10 +++--- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 996e8f127..bf19f8305 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -30,6 +30,9 @@ struct BookmarksExampleView: View { var body: some View { MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } .onTapGesture { showingBookmarks = false } @@ -64,16 +67,29 @@ struct BookmarksExampleView: View { } Text("More Options") } - label: { - Label("Options", systemImage: "ellipsis") - } + label: { Label("Options", systemImage: "ellipsis") } } } - .overlay { - Bookmarks(sampleBookmarks, isPresented: $showingBookmarks) - .onSelectionChanged { - viewpoint = $0.viewpoint - } + .overlay(alignment: .topTrailing) { + // Let the bookmarks component control viewpoint changes: + Bookmarks( + sampleBookmarks, + isPresented: $showingBookmarks, + viewpoint: $viewpoint + ) + .padding() + .frame(width: 200) + + // Or control viewpoint changes yourself: +// Bookmarks( +// sampleBookmarks, +// isPresented: $showingBookmarks +// ) +// .onSelectionChanged { +// viewpoint = $0.viewpoint +// } +// .padding() +// .frame(width: 200) } } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks.swift index ae54d824c..a1cf8c814 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks.swift @@ -56,16 +56,16 @@ public struct Bookmarks: View { ForEach(bookmarks, id: \.viewpoint) { bookmark in Button { isPresented = false - viewpoint?.wrappedValue = bookmark.viewpoint - selectionChangedActions?(bookmark) + if let viewpoint = viewpoint { + viewpoint.wrappedValue = bookmark.viewpoint + } else { + selectionChangedActions?(bookmark) + } } label: { Text(bookmark.name) } } } - .frame( - width: 300, height: 600, alignment: .bottomLeading - ) } } } From 90a5f5c89db46352c513d6a45a887d30508a8323 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 15:28:06 -0800 Subject: [PATCH 0506/1722] Add documentation for `selectionChangedActions` --- Sources/ArcGISToolkit/Components/Bookmarks.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks.swift index a1cf8c814..26dbbac78 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks.swift @@ -36,6 +36,7 @@ public struct Bookmarks: View { _isPresented = isPresented } + /// Actions to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil /// Sets a closure to perform when the viewpoint of the map view changes. From 11215d6f87c8b3d4e863a767bc0028e9841523eb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 16:44:17 -0800 Subject: [PATCH 0507/1722] Refactor underlying bookmark display --- Examples/Examples/BookmarksExampleView.swift | 9 ++--- .../ArcGISToolkit/Components/Bookmarks.swift | 33 ++++++++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index bf19f8305..02259fee4 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -33,9 +33,6 @@ struct BookmarksExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .onTapGesture { - showingBookmarks = false - } // Show the bookmarks control as button .overlay(alignment: .topLeading) { Button { @@ -59,10 +56,8 @@ struct BookmarksExampleView: View { showingBookmarks.toggle() } label: { Label( - showingBookmarks ? - "Hide Bookmarks" : "Show Bookmarks", - systemImage: showingBookmarks ? - "bookmark.fill" : "bookmark" + "Show Bookmarks", + systemImage: "bookmark" ) } Text("More Options") diff --git a/Sources/ArcGISToolkit/Components/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks.swift index 26dbbac78..a280b210c 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks.swift @@ -17,12 +17,14 @@ import SwiftUI /// The bookmarks component allows for a user to select and navigate to defined set of "bookmarked" /// locations. public struct Bookmarks: View { + /// A list of bookmarks that will be displayed private var bookmarks: [Bookmark] /// Determines if the bookmarks list is currently shown or not. @Binding private var isPresented: Bool + /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. private var viewpoint: Binding? /// Creates a `Bookmarks` component. @@ -52,21 +54,28 @@ public struct Bookmarks: View { } public var body: some View { - if isPresented { - FloatingPanel { - ForEach(bookmarks, id: \.viewpoint) { bookmark in - Button { - isPresented = false - if let viewpoint = viewpoint { - viewpoint.wrappedValue = bookmark.viewpoint - } else { - selectionChangedActions?(bookmark) + EmptyView() + .sheet(isPresented: $isPresented) { + List { + ForEach(bookmarks, id: \.viewpoint) { bookmark in + Button { + isPresented = false + if let viewpoint = viewpoint { + viewpoint.wrappedValue = bookmark.viewpoint + } else if let actions = selectionChangedActions { + actions(bookmark) + } else { + fatalError("No viewpoint or action provided") + } + } label: { + Text(bookmark.name) } - } label: { - Text(bookmark.name) } } + Button("Dismiss") { + isPresented = false + } + .padding() } - } } } From 9c2566bd8e00157671ce2566f13cf2ddce0cd5de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 16:47:05 -0800 Subject: [PATCH 0508/1722] Grammar --- Sources/ArcGISToolkit/Components/Bookmarks.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks.swift index a280b210c..17c4c24f3 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks.swift @@ -14,7 +14,7 @@ import ArcGIS import SwiftUI -/// The bookmarks component allows for a user to select and navigate to defined set of "bookmarked" +/// The bookmarks component allows for a user to select and navigate to a defined set of "bookmarked" /// locations. public struct Bookmarks: View { /// A list of bookmarks that will be displayed From 1c05d249e7260a35f1141a0af3511572223bf2b1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 17:25:21 -0800 Subject: [PATCH 0509/1722] Add header --- .../{ => Bookmarks}/Bookmarks.swift | 8 ++-- .../Bookmarks/BookmarksHeader.swift | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) rename Sources/ArcGISToolkit/Components/{ => Bookmarks}/Bookmarks.swift (94%) create mode 100644 Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift diff --git a/Sources/ArcGISToolkit/Components/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift similarity index 94% rename from Sources/ArcGISToolkit/Components/Bookmarks.swift rename to Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 17c4c24f3..e1bb2a5bd 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -22,7 +22,7 @@ public struct Bookmarks: View { /// Determines if the bookmarks list is currently shown or not. @Binding - private var isPresented: Bool + var isPresented: Bool /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. private var viewpoint: Binding? @@ -56,6 +56,8 @@ public struct Bookmarks: View { public var body: some View { EmptyView() .sheet(isPresented: $isPresented) { + BookmarksHeader(isPresented: $isPresented) + Divider() List { ForEach(bookmarks, id: \.viewpoint) { bookmark in Button { @@ -72,10 +74,6 @@ public struct Bookmarks: View { } } } - Button("Dismiss") { - isPresented = false - } - .padding() } } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift new file mode 100644 index 000000000..51d297a0d --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -0,0 +1,46 @@ +// 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 + +/// The header displayed at the top of the bookmarks menu. +struct BookmarksHeader: View { + /// Determines if the bookmarks list is currently shown or not. + @Binding + var isPresented: Bool + + public var body: some View { + VStack { + HStack { + Label { + Text("Bookmarks") + } icon: { + Image(systemName: "bookmark") + } + .font(.title2) + Spacer() + Button { + isPresented = false + } label: { + Image(systemName: "xmark.circle") + } + } + Text("Select a bookmark") + .font(.subheadline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding([.top], 5) + } + .padding() + } +} From b4fb33eae2bf87fd1c4fcfcf80e630193f3ed84d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 17:37:12 -0800 Subject: [PATCH 0510/1722] Move list into BookmarksList --- .../Components/Bookmarks/Bookmarks.swift | 28 ++++------- .../Components/Bookmarks/BookmarksList.swift | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index e1bb2a5bd..177de4354 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -24,6 +24,9 @@ public struct Bookmarks: View { @Binding var isPresented: Bool + /// Actions to be performed when a bookmark is selected. + var selectionChangedActions: ((Bookmark) -> Void)? = nil + /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. private var viewpoint: Binding? @@ -38,9 +41,6 @@ public struct Bookmarks: View { _isPresented = isPresented } - /// Actions to be performed when a bookmark is selected. - var selectionChangedActions: ((Bookmark) -> Void)? = nil - /// Sets a closure to perform when the viewpoint of the map view changes. /// - Parameters: /// - kind: The kind of viewpoint passed to the `action` closure. @@ -58,22 +58,12 @@ public struct Bookmarks: View { .sheet(isPresented: $isPresented) { BookmarksHeader(isPresented: $isPresented) Divider() - List { - ForEach(bookmarks, id: \.viewpoint) { bookmark in - Button { - isPresented = false - if let viewpoint = viewpoint { - viewpoint.wrappedValue = bookmark.viewpoint - } else if let actions = selectionChangedActions { - actions(bookmark) - } else { - fatalError("No viewpoint or action provided") - } - } label: { - Text(bookmark.name) - } - } - } + BookmarksList( + bookmarks: bookmarks, + isPresented: $isPresented, + selectionChangedActions: selectionChangedActions, + viewpoint: viewpoint + ) } } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift new file mode 100644 index 000000000..d9d2c88c2 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -0,0 +1,49 @@ +// 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 +import SwiftUI + +struct BookmarksList: View { + /// A list of bookmarks that will be displayed + var bookmarks: [Bookmark] + + /// Determines if the bookmarks list is currently shown or not. + @Binding + var isPresented: Bool + + /// Actions to be performed when a bookmark is selected. + var selectionChangedActions: ((Bookmark) -> Void)? = nil + + /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. + var viewpoint: Binding? + + var body: some View { + List { + ForEach(bookmarks, id: \.viewpoint) { bookmark in + Button { + isPresented = false + if let viewpoint = viewpoint { + viewpoint.wrappedValue = bookmark.viewpoint + } else if let actions = selectionChangedActions { + actions(bookmark) + } else { + fatalError("No viewpoint or action provided") + } + } label: { + Text(bookmark.name) + } + } + } + } +} From 9dc3b00b6140d8a875a74c05d561947626cb416e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 17:39:49 -0800 Subject: [PATCH 0511/1722] Remove unneeded frame and padding --- Examples/Examples/BookmarksExampleView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 02259fee4..75f3322c5 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -72,8 +72,6 @@ struct BookmarksExampleView: View { isPresented: $showingBookmarks, viewpoint: $viewpoint ) - .padding() - .frame(width: 200) // Or control viewpoint changes yourself: // Bookmarks( @@ -83,8 +81,6 @@ struct BookmarksExampleView: View { // .onSelectionChanged { // viewpoint = $0.viewpoint // } -// .padding() -// .frame(width: 200) } } } From f285d5a6b0cc38c3cc21fc96a17904932db0cd49 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 17:42:57 -0800 Subject: [PATCH 0512/1722] Refactor padding --- Examples/Examples/BookmarksExampleView.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 75f3322c5..ece5dad89 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -44,9 +44,7 @@ struct BookmarksExampleView: View { ) } .buttonStyle(.bordered) - .padding( - EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 0) - ) + .padding([.top, .leading], 10) } // Show the bookmarks control as an option within a group .toolbar { From d10bb58943c201781dd1baae0f3153b144959960 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 17:55:26 -0800 Subject: [PATCH 0513/1722] Refactor documentation --- .../Components/Bookmarks/Bookmarks.swift | 33 +++++++++---------- .../Components/Bookmarks/BookmarksList.swift | 7 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 177de4354..5397231d1 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -14,21 +14,21 @@ import ArcGIS import SwiftUI -/// The bookmarks component allows for a user to select and navigate to a defined set of "bookmarked" -/// locations. +/// `Bookmarks` allows for a user to view and select from a set of bookmarks. public struct Bookmarks: View { - /// A list of bookmarks that will be displayed - private var bookmarks: [Bookmark] - /// Determines if the bookmarks list is currently shown or not. @Binding var isPresented: Bool - /// Actions to be performed when a bookmark is selected. - var selectionChangedActions: ((Bookmark) -> Void)? = nil + /// User defined actions to be performed when a bookmark is selected. + var selectionChangedActions: ((Bookmark) -> Void)? = nil { + didSet { + bookmarksList.selectionChangedActions = selectionChangedActions + } + } - /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. - private var viewpoint: Binding? + /// A list that displays bookmarks. + private var bookmarksList: BookmarksList /// Creates a `Bookmarks` component. public init( @@ -36,8 +36,12 @@ public struct Bookmarks: View { isPresented: Binding, viewpoint: Binding? = nil ) { - self.bookmarks = bookmarks - self.viewpoint = viewpoint + bookmarksList = BookmarksList( + bookmarks: bookmarks, + isPresented: isPresented, + selectionChangedActions: selectionChangedActions, + viewpoint: viewpoint + ) _isPresented = isPresented } @@ -58,12 +62,7 @@ public struct Bookmarks: View { .sheet(isPresented: $isPresented) { BookmarksHeader(isPresented: $isPresented) Divider() - BookmarksList( - bookmarks: bookmarks, - isPresented: $isPresented, - selectionChangedActions: selectionChangedActions, - viewpoint: viewpoint - ) + bookmarksList } } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index d9d2c88c2..c6773ed7f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -14,15 +14,16 @@ import ArcGIS import SwiftUI +/// `BookmarksList` displays a list of selectable bookmarks. struct BookmarksList: View { - /// A list of bookmarks that will be displayed + /// A list of selectable bookmarks. var bookmarks: [Bookmark] - /// Determines if the bookmarks list is currently shown or not. + /// Determines if the list is currently shown or not. @Binding var isPresented: Bool - /// Actions to be performed when a bookmark is selected. + /// User defined actions to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. From eafcd0c8413ef016913c4a77a871e6b3924d4f2c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Feb 2022 18:32:33 -0800 Subject: [PATCH 0514/1722] Add support for webmaps --- Examples/Examples/BookmarksExampleView.swift | 9 ++++++--- .../Components/Bookmarks/Bookmarks.swift | 6 +++++- .../Components/Bookmarks/BookmarksList.swift | 18 ++++++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index ece5dad89..fa007b2e2 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -19,6 +19,9 @@ struct BookmarksExampleView: View { /// The map displayed in the map view. private let map = Map(basemapStyle: .arcGISImagery) + /// A web map with predefined bookmarks + private let webMap = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + /// Indicates if the bookmarks list is shown or not. /// - Remark: This allows a developer to control how the bookmarks menu is shown/hidden, /// whether that be in a group of options or a standalone button. @@ -66,15 +69,15 @@ struct BookmarksExampleView: View { .overlay(alignment: .topTrailing) { // Let the bookmarks component control viewpoint changes: Bookmarks( - sampleBookmarks, isPresented: $showingBookmarks, + bookmarks: sampleBookmarks, viewpoint: $viewpoint ) // Or control viewpoint changes yourself: // Bookmarks( -// sampleBookmarks, -// isPresented: $showingBookmarks +// isPresented: $showingBookmarks, +// map: webMap // ) // .onSelectionChanged { // viewpoint = $0.viewpoint diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 5397231d1..ee7b457f2 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -31,14 +31,18 @@ public struct Bookmarks: View { private var bookmarksList: BookmarksList /// Creates a `Bookmarks` component. + /// - precondition: `bookmarks` or `map` is non-nil. public init( - _ bookmarks: [Bookmark], isPresented: Binding, + bookmarks: [Bookmark]? = nil, + map: Map? = nil, viewpoint: Binding? = nil ) { + precondition((bookmarks != nil) || (map != nil)) bookmarksList = BookmarksList( bookmarks: bookmarks, isPresented: isPresented, + map: map, selectionChangedActions: selectionChangedActions, viewpoint: viewpoint ) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index c6773ed7f..5c16e428d 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -17,12 +17,26 @@ import SwiftUI /// `BookmarksList` displays a list of selectable bookmarks. struct BookmarksList: View { /// A list of selectable bookmarks. - var bookmarks: [Bookmark] + var bookmarks: [Bookmark]? + + /// A list of bookmarks derived either directly from `bookmarks` or from `map`. + private var definedBookmarks: [Bookmark] { + if let bookmarks = bookmarks { + return bookmarks + } else if let map = map { + return map.bookmarks + } else { + return [] + } + } /// Determines if the list is currently shown or not. @Binding var isPresented: Bool + /// A map containing bookmarks + var map: Map? + /// User defined actions to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil @@ -31,7 +45,7 @@ struct BookmarksList: View { var body: some View { List { - ForEach(bookmarks, id: \.viewpoint) { bookmark in + ForEach(definedBookmarks, id: \.viewpoint) { bookmark in Button { isPresented = false if let viewpoint = viewpoint { From 4d97a5a7b77f18429215a79b70c65941fadce688 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 10:10:55 -0800 Subject: [PATCH 0515/1722] Fix web map support --- .../Components/Bookmarks/BookmarksList.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 5c16e428d..b3f2ad5f2 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -37,6 +37,9 @@ struct BookmarksList: View { /// A map containing bookmarks var map: Map? + /// Indicates if bookmarks have loaded and are ready for display. + @State var mapisLoaded = false + /// User defined actions to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil @@ -44,6 +47,19 @@ struct BookmarksList: View { var viewpoint: Binding? var body: some View { + if map == nil { + list + } else { + if mapisLoaded { + list + } else { + loading + } + } + } + + /// A list that is shown once bookmarks have been loaded. + var list: some View { List { ForEach(definedBookmarks, id: \.viewpoint) { bookmark in Button { @@ -61,4 +77,25 @@ struct BookmarksList: View { } } } + + /// A view that is shown while a web map is being loaded. + var loading: some View { + VStack { + Spacer() + HStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .padding() + Text("Loading") + }.task { + do { + try await map?.load() + mapisLoaded = true + } catch { + print(error.localizedDescription) + } + } + Spacer() + } + } } From d27128c0eada4ae9450065698ea5c99de6b0645c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 11:32:19 -0800 Subject: [PATCH 0516/1722] Add tests, improve organization --- .../Components/Bookmarks/Bookmarks.swift | 6 +- .../Components/Bookmarks/BookmarksList.swift | 32 ++- .../BookmarksListTest.swift | 208 ++++++++++++++++++ 3 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/BookmarksListTest.swift diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index ee7b457f2..d893bad0f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -16,6 +16,9 @@ import SwiftUI /// `Bookmarks` allows for a user to view and select from a set of bookmarks. public struct Bookmarks: View { + /// A list that displays bookmarks. + private var bookmarksList: BookmarksList + /// Determines if the bookmarks list is currently shown or not. @Binding var isPresented: Bool @@ -27,9 +30,6 @@ public struct Bookmarks: View { } } - /// A list that displays bookmarks. - private var bookmarksList: BookmarksList - /// Creates a `Bookmarks` component. /// - precondition: `bookmarks` or `map` is non-nil. public init( diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index b3f2ad5f2..caf4bbe62 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -34,7 +34,7 @@ struct BookmarksList: View { @Binding var isPresented: Bool - /// A map containing bookmarks + /// A map containing bookmarks. var map: Map? /// Indicates if bookmarks have loaded and are ready for display. @@ -46,6 +46,21 @@ struct BookmarksList: View { /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. var viewpoint: Binding? + /// Performs the necessary actions when a bookmark is selected. This includes indicating that + /// bookmarks should be set to a hidden state, and changing the viewpoint if the user provided a + /// viewpoint or calling actions if the user used implemented a modifier. + /// - Parameter bookmark: The bookmark that was selected. + func makeSelection(_ bookmark: Bookmark) { + isPresented = false + if let viewpoint = viewpoint { + viewpoint.wrappedValue = bookmark.viewpoint + } else if let actions = selectionChangedActions { + actions(bookmark) + } else { + fatalError("No viewpoint or action provided") + } + } + var body: some View { if map == nil { list @@ -57,20 +72,15 @@ struct BookmarksList: View { } } } +} - /// A list that is shown once bookmarks have been loaded. +extension BookmarksList { + /// A list that is shown once bookmarks have loaded. var list: some View { List { ForEach(definedBookmarks, id: \.viewpoint) { bookmark in Button { - isPresented = false - if let viewpoint = viewpoint { - viewpoint.wrappedValue = bookmark.viewpoint - } else if let actions = selectionChangedActions { - actions(bookmark) - } else { - fatalError("No viewpoint or action provided") - } + makeSelection(bookmark) } label: { Text(bookmark.name) } @@ -78,7 +88,7 @@ struct BookmarksList: View { } } - /// A view that is shown while a web map is being loaded. + /// A view that is shown while a web map is loading. var loading: some View { VStack { Spacer() diff --git a/Tests/ArcGISToolkitTests/BookmarksListTest.swift b/Tests/ArcGISToolkitTests/BookmarksListTest.swift new file mode 100644 index 000000000..4ab3720c9 --- /dev/null +++ b/Tests/ArcGISToolkitTests/BookmarksListTest.swift @@ -0,0 +1,208 @@ +// 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 +import SwiftUI +import XCTest +@testable import ArcGISToolkit + +class BookmarksListTest: XCTestCase { + /// Assert that the list properly handles a selction when provided a modifier. + func testMakeSelectionWithModifier() { + let expectation = XCTestExpectation( + description: "Modifier action was performed" + ) + let bookmarks = sampleBookmarks + var _isPresented = true + let action: ((Bookmark) -> Void) = { + expectation.fulfill() + XCTAssertEqual($0.viewpoint, bookmarks.first?.viewpoint) + } + let isPresented = Binding( + get: { _isPresented }, + set: {_isPresented = $0 } + ) + let bookmarksList = BookmarksList( + bookmarks: bookmarks, + isPresented: isPresented, + mapisLoaded: true, + selectionChangedActions: action + ) + XCTAssertTrue(_isPresented) + bookmarksList.makeSelection(bookmarks.first!) + XCTAssertFalse(_isPresented) + wait(for: [expectation], timeout: 1.0) + } + + /// Assert that the list properly handles a selction when provided a modifier and web map. + func testMakeSelectionWithModifierAndWebMap() async { + let expectation = XCTestExpectation( + description: "Modifier action was performed" + ) + let webMap = webMap + do { + try await webMap.load() + } catch { + XCTFail("Web map failed to load \(error.localizedDescription)") + } + var _isPresented = true + let isPresented = Binding( + get: { _isPresented }, + set: {_isPresented = $0 } + ) + let action: ((Bookmark) -> Void) = { + expectation.fulfill() + XCTAssertEqual($0.viewpoint, webMap.bookmarks.first?.viewpoint) + } + let bookmarksList = BookmarksList( + isPresented: isPresented, + map: webMap, + mapisLoaded: true, + selectionChangedActions: action + ) + XCTAssertTrue(_isPresented) + bookmarksList.makeSelection(webMap.bookmarks.first!) + XCTAssertFalse(_isPresented) + wait(for: [expectation], timeout: 1.0) + } + + /// Assert that the list properly handles a selction when provided a viewpoint. + func testMakeSelectionWithViewpoint() { + let bookmarks = sampleBookmarks + var _isPresented = true + let isPresented = Binding( + get: { _isPresented }, + set: {_isPresented = $0 } + ) + var _viewpoint: Viewpoint? = getViewpoint(0) + let viewpoint = Binding( + get: { _viewpoint }, + set: { _viewpoint = $0 } + ) + let bookmarksList = BookmarksList( + bookmarks: bookmarks, + isPresented: isPresented, + mapisLoaded: true, + viewpoint: viewpoint + ) + XCTAssertTrue(_isPresented) + XCTAssertNotEqual(_viewpoint, bookmarks.first?.viewpoint) + bookmarksList.makeSelection(bookmarks.first!) + XCTAssertFalse(_isPresented) + XCTAssertEqual(_viewpoint, bookmarks.first?.viewpoint) + } + + /// Assert that the list properly handles a selction when provided a viewpoint and web map. + func testMakeSelectionWithViewpointAndWebMap() async { + let webMap = webMap + do { + try await webMap.load() + } catch { + XCTFail("Web map failed to load \(error.localizedDescription)") + } + var _isPresented = true + let isPresented = Binding( + get: { _isPresented }, + set: {_isPresented = $0 } + ) + var _viewpoint: Viewpoint? = getViewpoint(0) + let viewpoint = Binding( + get: { _viewpoint }, + set: { _viewpoint = $0 } + ) + let bookmarksList = BookmarksList( + isPresented: isPresented, + map: webMap, + mapisLoaded: true, + viewpoint: viewpoint + ) + XCTAssertTrue(_isPresented) + XCTAssertNotEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) + bookmarksList.makeSelection(webMap.bookmarks.first!) + XCTAssertFalse(_isPresented) + XCTAssertEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) + } +} + +extension BookmarksListTest { + /// An arbitrary point to use for testing. + var point: Point { + Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) + } + + /// A list of sample bookmarks for testing + var sampleBookmarks: [Bookmark] {[ + Bookmark( + name: "Yosemite National Park", + viewpoint: Viewpoint( + center: Point( + x: -119.538330, + y: 37.865101, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Zion National Park", + viewpoint: Viewpoint( + center: Point( + x: -113.028770, + y: 37.297817, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Yellowstone National Park", + viewpoint: Viewpoint( + center: Point( + x: -110.584663, + y: 44.429764, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ), + Bookmark( + name: "Grand Canyon National Park", + viewpoint: Viewpoint( + center: Point( + x: -112.1129, + y: 36.1069, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ), + ]} + + /// An arbitrary scale to use for testing. + var scale: Double { + 10_000.00 + } + + /// A web map authored with bookmarks for testing. + var webMap: Map { + return Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + } + + /// Builds viewpoints to use for tests. + /// - Parameter rotation: The rotation to use for the resulting viewpoint. + /// - Returns: A viewpoint object for tests. + func getViewpoint(_ rotation: Double) -> Viewpoint { + return Viewpoint(center: point, scale: scale, rotation: rotation) + } +} From d39ea88fc6e325b225180d3908240af902fe88bf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 11:47:18 -0800 Subject: [PATCH 0517/1722] Documentation and formatting --- .../Components/Bookmarks/Bookmarks.swift | 14 +++++++++++++- .../Components/Bookmarks/BookmarksList.swift | 7 ++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index d893bad0f..99f1f59b8 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -23,7 +23,9 @@ public struct Bookmarks: View { @Binding var isPresented: Bool - /// User defined actions to be performed when a bookmark is selected. + /// User defined action to be performed when a bookmark is selected. Use this when you prefer to + /// self-manage the response to a bookmark selection. Use either `onSelectionChangedActions` + /// or `viewpoint` exclusively. var selectionChangedActions: ((Bookmark) -> Void)? = nil { didSet { bookmarksList.selectionChangedActions = selectionChangedActions @@ -32,6 +34,16 @@ public struct Bookmarks: View { /// Creates a `Bookmarks` component. /// - precondition: `bookmarks` or `map` is non-nil. + /// - precondition: `viewpoint` is non-nil or the `selectionChangedActions` modifier is + /// implemented. + /// - Parameters: + /// - isPresented: Determines if the bookmarks list is presented. + /// - bookmarks: A list of bookmarks. Use this when displaying bookmarks defined at run-time. + /// Use either `bookmarks` or `map` exclusively. + /// - map: A web map authored with pre-existing bookmarks. Use either `map` or `bookmarks` + /// exclusively. + /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use + /// either `viewpoint` or `selectionChangedActions` exclusively. public init( isPresented: Binding, bookmarks: [Bookmark]? = nil, diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index caf4bbe62..9507087cf 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -38,9 +38,10 @@ struct BookmarksList: View { var map: Map? /// Indicates if bookmarks have loaded and are ready for display. - @State var mapisLoaded = false + @State + var mapisLoaded = false - /// User defined actions to be performed when a bookmark is selected. + /// User defined action to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. @@ -48,7 +49,7 @@ struct BookmarksList: View { /// Performs the necessary actions when a bookmark is selected. This includes indicating that /// bookmarks should be set to a hidden state, and changing the viewpoint if the user provided a - /// viewpoint or calling actions if the user used implemented a modifier. + /// viewpoint or calling actions if the user implemented the `selectionChangedActions` modifier. /// - Parameter bookmark: The bookmark that was selected. func makeSelection(_ bookmark: Bookmark) { isPresented = false From a555949fc273955ca13de3df86570719c2cffa16 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 11:51:17 -0800 Subject: [PATCH 0518/1722] Mark privates --- .../ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 9507087cf..8cf6a1051 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -77,7 +77,7 @@ struct BookmarksList: View { extension BookmarksList { /// A list that is shown once bookmarks have loaded. - var list: some View { + private var list: some View { List { ForEach(definedBookmarks, id: \.viewpoint) { bookmark in Button { @@ -90,7 +90,7 @@ extension BookmarksList { } /// A view that is shown while a web map is loading. - var loading: some View { + private var loading: some View { VStack { Spacer() HStack { From eece168f75abcc29a90c21d73c8bc1d243cb991b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 11:53:31 -0800 Subject: [PATCH 0519/1722] Mark privates --- 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 99f1f59b8..c395a83e4 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -21,7 +21,7 @@ public struct Bookmarks: View { /// Determines if the bookmarks list is currently shown or not. @Binding - var isPresented: Bool + private var isPresented: Bool /// User defined action to be performed when a bookmark is selected. Use this when you prefer to /// self-manage the response to a bookmark selection. Use either `onSelectionChangedActions` From ee3c5f8c31019b70a3961c1fe9ea11dbf4761203 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 14:40:31 -0800 Subject: [PATCH 0520/1722] Add `ScalebarExampleView` --- Examples/Examples/ScalebarExampleView.swift | 21 +++++++++++++++++++++ Examples/ExamplesApp/Examples.swift | 1 + 2 files changed, 22 insertions(+) create mode 100644 Examples/Examples/ScalebarExampleView.swift diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift new file mode 100644 index 000000000..8df426f69 --- /dev/null +++ b/Examples/Examples/ScalebarExampleView.swift @@ -0,0 +1,21 @@ +// Copyright 2021 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 ArcGISToolkit +import SwiftUI + +struct ScalebarExampleView: View { + var body: some View { + EmptyView() + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index bb40609ae..4e529b7c5 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -39,6 +39,7 @@ extension ExampleList { AnyExample("Compass", content: CompassExampleView()), AnyExample("Floor Filter", content: FloorFilterExampleView()), AnyExample("Overview Map", content: OverviewMapExampleView()), + AnyExample("Scalebar", content: ScalebarExampleView()), AnyExample("Search", content: SearchExampleView()) ] ) From b36871215c79b484e051f7832277ea5ad624b25f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 14:55:33 -0800 Subject: [PATCH 0521/1722] Fix build error --- Examples/Examples.xcodeproj/project.pbxproj | 4 ++++ Examples/Examples/ScalebarExampleView.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 10e7932c1..8ba30c736 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75657E4727ABAC8400EE865B /* CompassExampleView.swift */; }; + 75D41B2B27C6F21400624D7C /* ScalebarExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D41B2A27C6F21400624D7C /* ScalebarExampleView.swift */; }; E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; @@ -39,6 +40,7 @@ /* Begin PBXFileReference section */ 75657E4727ABAC8400EE865B /* CompassExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExampleView.swift; sourceTree = ""; }; + 75D41B2A27C6F21400624D7C /* ScalebarExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalebarExampleView.swift; sourceTree = ""; }; E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloorFilterExampleView.swift; sourceTree = ""; }; E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -75,6 +77,7 @@ children = ( E4C389D426B8A12C002BC255 /* BasemapGalleryExampleView.swift */, E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, + 75D41B2A27C6F21400624D7C /* ScalebarExampleView.swift */, E42BFBE82672BF9500159107 /* SearchExampleView.swift */, E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, 75657E4727ABAC8400EE865B /* CompassExampleView.swift */, @@ -227,6 +230,7 @@ E48A73462658227100F5C118 /* ExampleView.swift in Sources */, E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */, E4C389D526B8A12C002BC255 /* BasemapGalleryExampleView.swift in Sources */, + 75D41B2B27C6F21400624D7C /* ScalebarExampleView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 8df426f69..121ed042a 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Esri. +// 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. From a5f1fe9922789f780030d910826653f29b11508f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:05:34 -0800 Subject: [PATCH 0522/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index fa007b2e2..a8219fae7 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -19,7 +19,7 @@ struct BookmarksExampleView: View { /// The map displayed in the map view. private let map = Map(basemapStyle: .arcGISImagery) - /// A web map with predefined bookmarks + /// A web map with predefined bookmarks. private let webMap = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! /// Indicates if the bookmarks list is shown or not. From 66fe7bd176d0aa8ce42ff8c59179aad7f7f70a40 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:05:49 -0800 Subject: [PATCH 0523/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index a8219fae7..4c8e17f91 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -29,7 +29,7 @@ struct BookmarksExampleView: View { var showingBookmarks = false @State - var viewpoint: Viewpoint? = nil + var viewpoint: Viewpoint? = nil var body: some View { MapView(map: map, viewpoint: viewpoint) From 9f0449e0ba3ea25f73b7dd101cf7edf2259eb04d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:05:57 -0800 Subject: [PATCH 0524/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 4c8e17f91..0aa24e5fa 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -36,7 +36,7 @@ struct BookmarksExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - // Show the bookmarks control as button + // Show the bookmarks control as a button. .overlay(alignment: .topLeading) { Button { showingBookmarks.toggle() From 2ed50d2d615bcdc56bc1dd9e3a04085947f27eb5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:08:37 -0800 Subject: [PATCH 0525/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 0aa24e5fa..26aa5e5d5 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -86,7 +86,7 @@ struct BookmarksExampleView: View { } } -extension BookmarksExampleView { +private extension BookmarksExampleView { var sampleBookmarks: [Bookmark] {[ Bookmark( name: "Yosemite National Park", From ddd6d751fd8425c81c8550255f6cf64424d0a55a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:09:50 -0800 Subject: [PATCH 0526/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/BookmarksExampleView.swift | 94 ++++++++++---------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 26aa5e5d5..7565af45b 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -87,50 +87,52 @@ struct BookmarksExampleView: View { } private extension BookmarksExampleView { - var sampleBookmarks: [Bookmark] {[ - Bookmark( - name: "Yosemite National Park", - viewpoint: Viewpoint( - center: Point( - x: -119.538330, - y: 37.865101, - spatialReference: .wgs84 - ), - scale: 250_000 - ) - ), - Bookmark( - name: "Zion National Park", - viewpoint: Viewpoint( - center: Point( - x: -113.028770, - y: 37.297817, - spatialReference: .wgs84 - ), - scale: 250_000 - ) - ), - Bookmark( - name: "Yellowstone National Park", - viewpoint: Viewpoint( - center: Point( - x: -110.584663, - y: 44.429764, - spatialReference: .wgs84 - ), - scale: 375_000 - ) - ), - Bookmark( - name: "Grand Canyon National Park", - viewpoint: Viewpoint( - center: Point( - x: -112.1129, - y: 36.1069, - spatialReference: .wgs84 - ), - scale: 375_000 - ) - ), - ]} + var sampleBookmarks: [Bookmark] { + [ + Bookmark( + name: "Yosemite National Park", + viewpoint: Viewpoint( + center: Point( + x: -119.538330, + y: 37.865101, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Zion National Park", + viewpoint: Viewpoint( + center: Point( + x: -113.028770, + y: 37.297817, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Yellowstone National Park", + viewpoint: Viewpoint( + center: Point( + x: -110.584663, + y: 44.429764, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ), + Bookmark( + name: "Grand Canyon National Park", + viewpoint: Viewpoint( + center: Point( + x: -112.1129, + y: 36.1069, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ), + ] + } } From 989148fcaeec849fba6be7e20a11b0e27550356d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:11:47 -0800 Subject: [PATCH 0527/1722] Move `onSelectionChanged` down --- .../Components/Bookmarks/Bookmarks.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index c395a83e4..5d0a93225 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -61,6 +61,15 @@ public struct Bookmarks: View { _isPresented = isPresented } + public var body: some View { + EmptyView() + .sheet(isPresented: $isPresented) { + BookmarksHeader(isPresented: $isPresented) + Divider() + bookmarksList + } + } + /// Sets a closure to perform when the viewpoint of the map view changes. /// - Parameters: /// - kind: The kind of viewpoint passed to the `action` closure. @@ -72,13 +81,4 @@ public struct Bookmarks: View { copy.selectionChangedActions = action return copy } - - public var body: some View { - EmptyView() - .sheet(isPresented: $isPresented) { - BookmarksHeader(isPresented: $isPresented) - Divider() - bookmarksList - } - } } From 5eb18ff50d941d7be8485bee1182fae8ba224a07 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:13:23 -0800 Subject: [PATCH 0528/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 8cf6a1051..93a4726db 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -39,7 +39,7 @@ struct BookmarksList: View { /// Indicates if bookmarks have loaded and are ready for display. @State - var mapisLoaded = false + var mapIsLoaded = false /// User defined action to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil From 9d200b31817931e6a953a559eb7e36509c458e2a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:15:30 -0800 Subject: [PATCH 0529/1722] Fix build error --- .../ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 93a4726db..5f062b9c1 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -66,7 +66,7 @@ struct BookmarksList: View { if map == nil { list } else { - if mapisLoaded { + if mapIsLoaded { list } else { loading @@ -101,7 +101,7 @@ extension BookmarksList { }.task { do { try await map?.load() - mapisLoaded = true + mapIsLoaded = true } catch { print(error.localizedDescription) } From 49001a3fcc85d179292e9e4f4977971359445414 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:15:51 -0800 Subject: [PATCH 0530/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 5f062b9c1..d3b88bc49 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -44,7 +44,7 @@ struct BookmarksList: View { /// User defined action to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil - /// If *non-nil*, this viewpoint is updated when a bookmark is pressed. + /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. var viewpoint: Binding? /// Performs the necessary actions when a bookmark is selected. This includes indicating that From 1b79daee76c1b75d99dd002694af82d901a94489 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:18:22 -0800 Subject: [PATCH 0531/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index d3b88bc49..03eee2f20 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -75,7 +75,7 @@ struct BookmarksList: View { } } -extension BookmarksList { +private extension BookmarksList { /// A list that is shown once bookmarks have loaded. private var list: some View { List { From 13cae66ff1618d49d7cfdf2a3fcc5ba05322feb6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Feb 2022 17:27:43 -0800 Subject: [PATCH 0532/1722] `BookmarksList.list` -> `bookmarkList` --- .../ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 03eee2f20..d4628844d 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -64,10 +64,10 @@ struct BookmarksList: View { var body: some View { if map == nil { - list + bookmarkList } else { if mapIsLoaded { - list + bookmarkList } else { loading } @@ -77,7 +77,7 @@ struct BookmarksList: View { private extension BookmarksList { /// A list that is shown once bookmarks have loaded. - private var list: some View { + private var bookmarkList: some View { List { ForEach(definedBookmarks, id: \.viewpoint) { bookmark in Button { From e5cb1cb27df3422114e1d82ace4ec2f87bf1ccf9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 10:47:04 -0800 Subject: [PATCH 0533/1722] Split `Bookmarks` init into two --- .../Components/Bookmarks/Bookmarks.swift | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 5d0a93225..f4625fc2f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -33,29 +33,42 @@ public struct Bookmarks: View { } /// Creates a `Bookmarks` component. - /// - precondition: `bookmarks` or `map` is non-nil. /// - precondition: `viewpoint` is non-nil or the `selectionChangedActions` modifier is /// implemented. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. /// - bookmarks: A list of bookmarks. Use this when displaying bookmarks defined at run-time. - /// Use either `bookmarks` or `map` exclusively. - /// - map: A web map authored with pre-existing bookmarks. Use either `map` or `bookmarks` - /// exclusively. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use /// either `viewpoint` or `selectionChangedActions` exclusively. public init( isPresented: Binding, - bookmarks: [Bookmark]? = nil, - map: Map? = nil, + bookmarks: [Bookmark], viewpoint: Binding? = nil ) { - precondition((bookmarks != nil) || (map != nil)) bookmarksList = BookmarksList( bookmarks: bookmarks, + isPresented: isPresented, + viewpoint: viewpoint + ) + _isPresented = isPresented + } + + /// Creates a `Bookmarks` component. + /// - precondition: `viewpoint` is non-nil or the `selectionChangedActions` modifier is + /// implemented. + /// - Parameters: + /// - isPresented: Determines if the bookmarks list is presented. + /// - map: A web map authored with pre-existing bookmarks. + /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use + /// either `viewpoint` or `selectionChangedActions` exclusively. + public init( + isPresented: Binding, + map: Map, + viewpoint: Binding? = nil + ) { + bookmarksList = BookmarksList( isPresented: isPresented, map: map, - selectionChangedActions: selectionChangedActions, viewpoint: viewpoint ) _isPresented = isPresented From 2e84d53d75053255c94a5a6874cc2bcd99f96700 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 10:50:37 -0800 Subject: [PATCH 0534/1722] Documentation formatting --- .../Components/Bookmarks/BookmarksList.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index d4628844d..f43d20b7e 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -47,9 +47,11 @@ struct BookmarksList: View { /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. var viewpoint: Binding? - /// Performs the necessary actions when a bookmark is selected. This includes indicating that - /// bookmarks should be set to a hidden state, and changing the viewpoint if the user provided a - /// viewpoint or calling actions if the user implemented the `selectionChangedActions` modifier. + /// Performs the necessary actions when a bookmark is selected. + /// + /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint + /// if the user provided a viewpoint or calling actions if the user implemented the + /// `selectionChangedActions` modifier. /// - Parameter bookmark: The bookmark that was selected. func makeSelection(_ bookmark: Bookmark) { isPresented = false From f229f6d40279cfa213580cf5f920d27e8ea8c0d7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 10:58:29 -0800 Subject: [PATCH 0535/1722] Rename `makeSelection` for `selectBookmark` --- .../Components/Bookmarks/BookmarksList.swift | 4 ++-- .../BookmarksListTest.swift | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index f43d20b7e..0e591b872 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -53,7 +53,7 @@ struct BookmarksList: View { /// if the user provided a viewpoint or calling actions if the user implemented the /// `selectionChangedActions` modifier. /// - Parameter bookmark: The bookmark that was selected. - func makeSelection(_ bookmark: Bookmark) { + func selectBookmark(_ bookmark: Bookmark) { isPresented = false if let viewpoint = viewpoint { viewpoint.wrappedValue = bookmark.viewpoint @@ -83,7 +83,7 @@ private extension BookmarksList { List { ForEach(definedBookmarks, id: \.viewpoint) { bookmark in Button { - makeSelection(bookmark) + selectBookmark(bookmark) } label: { Text(bookmark.name) } diff --git a/Tests/ArcGISToolkitTests/BookmarksListTest.swift b/Tests/ArcGISToolkitTests/BookmarksListTest.swift index 4ab3720c9..98c4ebf2d 100644 --- a/Tests/ArcGISToolkitTests/BookmarksListTest.swift +++ b/Tests/ArcGISToolkitTests/BookmarksListTest.swift @@ -19,7 +19,7 @@ import XCTest class BookmarksListTest: XCTestCase { /// Assert that the list properly handles a selction when provided a modifier. - func testMakeSelectionWithModifier() { + func testSelectBookmarkWithModifier() { let expectation = XCTestExpectation( description: "Modifier action was performed" ) @@ -36,17 +36,17 @@ class BookmarksListTest: XCTestCase { let bookmarksList = BookmarksList( bookmarks: bookmarks, isPresented: isPresented, - mapisLoaded: true, + mapIsLoaded: true, selectionChangedActions: action ) XCTAssertTrue(_isPresented) - bookmarksList.makeSelection(bookmarks.first!) + bookmarksList.selectBookmark(bookmarks.first!) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } /// Assert that the list properly handles a selction when provided a modifier and web map. - func testMakeSelectionWithModifierAndWebMap() async { + func testSelectBookmarkWithModifierAndWebMap() async { let expectation = XCTestExpectation( description: "Modifier action was performed" ) @@ -68,17 +68,17 @@ class BookmarksListTest: XCTestCase { let bookmarksList = BookmarksList( isPresented: isPresented, map: webMap, - mapisLoaded: true, + mapIsLoaded: true, selectionChangedActions: action ) XCTAssertTrue(_isPresented) - bookmarksList.makeSelection(webMap.bookmarks.first!) + bookmarksList.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } /// Assert that the list properly handles a selction when provided a viewpoint. - func testMakeSelectionWithViewpoint() { + func testSelectBookmarkWithViewpoint() { let bookmarks = sampleBookmarks var _isPresented = true let isPresented = Binding( @@ -93,18 +93,18 @@ class BookmarksListTest: XCTestCase { let bookmarksList = BookmarksList( bookmarks: bookmarks, isPresented: isPresented, - mapisLoaded: true, + mapIsLoaded: true, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) XCTAssertNotEqual(_viewpoint, bookmarks.first?.viewpoint) - bookmarksList.makeSelection(bookmarks.first!) + bookmarksList.selectBookmark(bookmarks.first!) XCTAssertFalse(_isPresented) XCTAssertEqual(_viewpoint, bookmarks.first?.viewpoint) } /// Assert that the list properly handles a selction when provided a viewpoint and web map. - func testMakeSelectionWithViewpointAndWebMap() async { + func testSelectBookmarkWithViewpointAndWebMap() async { let webMap = webMap do { try await webMap.load() @@ -124,12 +124,12 @@ class BookmarksListTest: XCTestCase { let bookmarksList = BookmarksList( isPresented: isPresented, map: webMap, - mapisLoaded: true, + mapIsLoaded: true, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) XCTAssertNotEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) - bookmarksList.makeSelection(webMap.bookmarks.first!) + bookmarksList.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) XCTAssertEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) } From 8a379d939f028108740702790defb27ba4dafa05 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 11:13:34 -0800 Subject: [PATCH 0536/1722] Formatting --- Tests/ArcGISToolkitTests/BookmarksListTest.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksListTest.swift b/Tests/ArcGISToolkitTests/BookmarksListTest.swift index 98c4ebf2d..e18528ee2 100644 --- a/Tests/ArcGISToolkitTests/BookmarksListTest.swift +++ b/Tests/ArcGISToolkitTests/BookmarksListTest.swift @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. - import ArcGIS import SwiftUI import XCTest From 39d99b7c409ebc42f58387281404a124aac013b5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 11:21:47 -0800 Subject: [PATCH 0537/1722] Remove xcshareddata addition to gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4238dc30b..f23d8f015 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .DS_Store /.build /Packages -xcshareddata/ xcuserdata/ /*.xcodeproj DerivedData/ From 8352ca5d6e64fab9afac7b7c778fa2cd1cbd8984 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 11:22:57 -0800 Subject: [PATCH 0538/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/BookmarksExampleView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 7565af45b..69e2ae171 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -42,8 +42,7 @@ struct BookmarksExampleView: View { showingBookmarks.toggle() } label: { Image( - systemName: - showingBookmarks ? "bookmark.fill" : "bookmark" + systemName: showingBookmarks ? "bookmark.fill" : "bookmark" ) } .buttonStyle(.bordered) From 99a5be110b808c58a274dfa9bbe6d40db61b25e4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 11:50:27 -0800 Subject: [PATCH 0539/1722] Apply PR feedback --- Examples/Examples/BookmarksExampleView.swift | 2 +- .../Components/Bookmarks/Bookmarks.swift | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 69e2ae171..cdea0c3c2 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -65,7 +65,7 @@ struct BookmarksExampleView: View { label: { Label("Options", systemImage: "ellipsis") } } } - .overlay(alignment: .topTrailing) { + .sheet(isPresented: $showingBookmarks) { // Let the bookmarks component control viewpoint changes: Bookmarks( isPresented: $showingBookmarks, diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index f4625fc2f..4d32e5fbf 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -75,12 +75,11 @@ public struct Bookmarks: View { } public var body: some View { - EmptyView() - .sheet(isPresented: $isPresented) { - BookmarksHeader(isPresented: $isPresented) - Divider() - bookmarksList - } + Group { + BookmarksHeader(isPresented: $isPresented) + Divider() + bookmarksList + } } /// Sets a closure to perform when the viewpoint of the map view changes. From 6dbaee05d00f1434163abba977ec52374d2b2ee6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 17:21:38 -0800 Subject: [PATCH 0540/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 4d32e5fbf..d9dd2d358 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -14,7 +14,7 @@ import ArcGIS import SwiftUI -/// `Bookmarks` allows for a user to view and select from a set of bookmarks. +/// `Bookmarks` allows a user to view and select from a set of bookmarks. public struct Bookmarks: View { /// A list that displays bookmarks. private var bookmarksList: BookmarksList From 4cbc7cb2c652f977d52bcc1dbac56c83431d0ffc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Feb 2022 17:21:46 -0800 Subject: [PATCH 0541/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index d9dd2d358..b94d3abe3 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -84,8 +84,7 @@ public struct Bookmarks: View { /// Sets a closure to perform when the viewpoint of the map view changes. /// - Parameters: - /// - kind: The kind of viewpoint passed to the `action` closure. - /// - action: The closure to perform when the viewpoint has changed. + /// - action: The closure to perform when the bookmark selection has changed. public func onSelectionChanged( perform action: @escaping (Bookmark) -> Void ) -> Bookmarks { From 9afe9d69ceb8a451a3541119e50b2a2d03403f59 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 13:54:49 -0800 Subject: [PATCH 0542/1722] Implement popover --- Examples/Examples/BookmarksExampleView.swift | 64 +++++++------------ .../Components/Bookmarks/Bookmarks.swift | 1 - .../Bookmarks/BookmarksHeader.swift | 35 +++++----- .../Components/Bookmarks/BookmarksList.swift | 43 ++++++++++--- 4 files changed, 73 insertions(+), 70 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index cdea0c3c2..c8f54c48f 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -36,52 +36,34 @@ struct BookmarksExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - // Show the bookmarks control as a button. - .overlay(alignment: .topLeading) { - Button { - showingBookmarks.toggle() - } label: { - Image( - systemName: showingBookmarks ? "bookmark.fill" : "bookmark" - ) - } - .buttonStyle(.bordered) - .padding([.top, .leading], 10) - } - // Show the bookmarks control as an option within a group .toolbar { ToolbarItem(placement: .primaryAction) { - Menu { - Button { - showingBookmarks.toggle() - } label: { - Label( - "Show Bookmarks", - systemImage: "bookmark" - ) + Button { + showingBookmarks.toggle() + } label: { + Label( + "Show Bookmarks", + systemImage: "bookmark" + ) + } + .popover(isPresented: $showingBookmarks) { + // Let the bookmarks component control viewpoint changes: +// Bookmarks( +// isPresented: $showingBookmarks, +// bookmarks: sampleBookmarks, +// viewpoint: $viewpoint +// ) + // Or control viewpoint changes yourself: + Bookmarks( + isPresented: $showingBookmarks, + map: webMap + ) + .onSelectionChanged { + viewpoint = $0.viewpoint } - Text("More Options") } - label: { Label("Options", systemImage: "ellipsis") } } } - .sheet(isPresented: $showingBookmarks) { - // Let the bookmarks component control viewpoint changes: - Bookmarks( - isPresented: $showingBookmarks, - bookmarks: sampleBookmarks, - viewpoint: $viewpoint - ) - - // Or control viewpoint changes yourself: -// Bookmarks( -// isPresented: $showingBookmarks, -// map: webMap -// ) -// .onSelectionChanged { -// viewpoint = $0.viewpoint -// } - } } } @@ -131,7 +113,7 @@ private extension BookmarksExampleView { ), scale: 375_000 ) - ), + ) ] } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index b94d3abe3..7a7a908a9 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -77,7 +77,6 @@ public struct Bookmarks: View { public var body: some View { Group { BookmarksHeader(isPresented: $isPresented) - Divider() bookmarksList } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift index 51d297a0d..f2acc65f6 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -20,26 +20,23 @@ struct BookmarksHeader: View { var isPresented: Bool public var body: some View { - VStack { - HStack { - Label { - Text("Bookmarks") - } icon: { - Image(systemName: "bookmark") - } - .font(.title2) - Spacer() - Button { - isPresented = false - } label: { - Image(systemName: "xmark.circle") - } + HStack { + Image(systemName: "bookmark") + VStack { + Text("Bookmarks") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + Text("Select a bookmark") + .font(.subheadline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + Spacer() + Button { + isPresented = false + } label: { + Image(systemName: "xmark.circle") } - Text("Select a bookmark") - .font(.subheadline) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding([.top], 5) } .padding() } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 0e591b872..9fca39cdd 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -21,13 +21,15 @@ struct BookmarksList: View { /// A list of bookmarks derived either directly from `bookmarks` or from `map`. private var definedBookmarks: [Bookmark] { + var result: [Bookmark] = [] if let bookmarks = bookmarks { - return bookmarks + result = bookmarks } else if let map = map { - return map.bookmarks + result = map.bookmarks } else { return [] } + return result.sorted { $0.name < $1.name } } /// Determines if the list is currently shown or not. @@ -78,17 +80,40 @@ struct BookmarksList: View { } private extension BookmarksList { + /// The minimum height of a bookmark list item. + /// + /// This number will be larger when them item's name consumes 2+ lines of text. + private var minimumRowHeight: Double { + 44 + } + /// A list that is shown once bookmarks have loaded. private var bookmarkList: some View { List { - ForEach(definedBookmarks, id: \.viewpoint) { bookmark in - Button { - selectBookmark(bookmark) - } label: { - Text(bookmark.name) + if bookmarks?.isEmpty ?? true { + Label { + Text("No bookmarks") + } icon: { + Image(systemName: "bookmark.slash") + } + .foregroundColor(.primary) + } else { + ForEach(definedBookmarks, id: \.viewpoint) { bookmark in + Button { + selectBookmark(bookmark) + } label: { + Text(bookmark.name) + } } } } + .listStyle(.plain) + .frame( + minHeight: minimumRowHeight, + idealHeight: minimumRowHeight * Double( + max(1, definedBookmarks.count)), + maxHeight: .infinity + ) } /// A view that is shown while a web map is loading. @@ -97,8 +122,7 @@ private extension BookmarksList { Spacer() HStack { ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .padding() + .padding([.trailing], 5) Text("Loading") }.task { do { @@ -110,5 +134,6 @@ private extension BookmarksList { } Spacer() } + .padding() } } From 5b88f41fd3c20210603974bd80b01480878dc4a0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 13:57:44 -0800 Subject: [PATCH 0543/1722] Switch back sample mode --- Examples/Examples/BookmarksExampleView.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index c8f54c48f..47d85efe6 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -48,19 +48,19 @@ struct BookmarksExampleView: View { } .popover(isPresented: $showingBookmarks) { // Let the bookmarks component control viewpoint changes: -// Bookmarks( -// isPresented: $showingBookmarks, -// bookmarks: sampleBookmarks, -// viewpoint: $viewpoint -// ) - // Or control viewpoint changes yourself: Bookmarks( isPresented: $showingBookmarks, - map: webMap + bookmarks: sampleBookmarks, + viewpoint: $viewpoint ) - .onSelectionChanged { - viewpoint = $0.viewpoint - } + // Or control viewpoint changes yourself: +// Bookmarks( +// isPresented: $showingBookmarks, +// map: webMap +// ) +// .onSelectionChanged { +// viewpoint = $0.viewpoint +// } } } } From bf9853afaa557e992945d16d54b745725f17a0e1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 14:11:40 -0800 Subject: [PATCH 0544/1722] Fix regression --- Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 9fca39cdd..7e3c7ea16 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -90,7 +90,7 @@ private extension BookmarksList { /// A list that is shown once bookmarks have loaded. private var bookmarkList: some View { List { - if bookmarks?.isEmpty ?? true { + if definedBookmarks.isEmpty { Label { Text("No bookmarks") } icon: { @@ -127,6 +127,7 @@ private extension BookmarksList { }.task { do { try await map?.load() + print(map?.bookmarks.count) mapIsLoaded = true } catch { print(error.localizedDescription) From 6ebc1211a73499475a204f8da2eb711d5ba3910c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 14:48:44 -0800 Subject: [PATCH 0545/1722] Genericize map to geomodel for scene support --- Examples/Examples/BookmarksExampleView.swift | 116 ++++++++++-------- .../Components/Bookmarks/Bookmarks.swift | 8 +- .../Components/Bookmarks/BookmarksList.swift | 23 ++-- 3 files changed, 80 insertions(+), 67 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 47d85efe6..ff45a2fd1 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -16,9 +16,62 @@ import ArcGISToolkit import SwiftUI struct BookmarksExampleView: View { + /// Represents the number of bookmarks added during runtime. + @State + private var bookmarksAdded = 0 + /// The map displayed in the map view. private let map = Map(basemapStyle: .arcGISImagery) + /// Sample bookmarks for demonstration. + @State + var sampleBookmarks: [Bookmark] = [ + Bookmark( + name: "Yosemite National Park", + viewpoint: Viewpoint( + center: Point( + x: -119.538330, + y: 37.865101, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Zion National Park", + viewpoint: Viewpoint( + center: Point( + x: -113.028770, + y: 37.297817, + spatialReference: .wgs84 + ), + scale: 250_000 + ) + ), + Bookmark( + name: "Yellowstone National Park", + viewpoint: Viewpoint( + center: Point( + x: -110.584663, + y: 44.429764, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ), + Bookmark( + name: "Grand Canyon National Park", + viewpoint: Viewpoint( + center: Point( + x: -112.1129, + y: 36.1069, + spatialReference: .wgs84 + ), + scale: 375_000 + ) + ) + ] + /// A web map with predefined bookmarks. private let webMap = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! @@ -36,6 +89,18 @@ struct BookmarksExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .onSingleTapGesture { _, point in + let newBookmark = Bookmark( + name: "Added bookmark \(bookmarksAdded + 1)", + viewpoint: Viewpoint( + center: point, + scale: viewpoint?.targetScale ?? 10_000 + ) + ) + webMap.addBookmark(newBookmark) + sampleBookmarks.append(newBookmark) + bookmarksAdded += 1 + } .toolbar { ToolbarItem(placement: .primaryAction) { Button { @@ -66,54 +131,3 @@ struct BookmarksExampleView: View { } } } - -private extension BookmarksExampleView { - var sampleBookmarks: [Bookmark] { - [ - Bookmark( - name: "Yosemite National Park", - viewpoint: Viewpoint( - center: Point( - x: -119.538330, - y: 37.865101, - spatialReference: .wgs84 - ), - scale: 250_000 - ) - ), - Bookmark( - name: "Zion National Park", - viewpoint: Viewpoint( - center: Point( - x: -113.028770, - y: 37.297817, - spatialReference: .wgs84 - ), - scale: 250_000 - ) - ), - Bookmark( - name: "Yellowstone National Park", - viewpoint: Viewpoint( - center: Point( - x: -110.584663, - y: 44.429764, - spatialReference: .wgs84 - ), - scale: 375_000 - ) - ), - Bookmark( - name: "Grand Canyon National Park", - viewpoint: Viewpoint( - center: Point( - x: -112.1129, - y: 36.1069, - spatialReference: .wgs84 - ), - scale: 375_000 - ) - ) - ] - } -} diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 7a7a908a9..550e0f012 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -58,17 +58,17 @@ public struct Bookmarks: View { /// implemented. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. - /// - map: A web map authored with pre-existing bookmarks. + /// - mapOrScene: A `GeoModel` authored with pre-existing bookmarks. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use /// either `viewpoint` or `selectionChangedActions` exclusively. public init( isPresented: Binding, - map: Map, + mapOrScene: GeoModel, viewpoint: Binding? = nil ) { bookmarksList = BookmarksList( isPresented: isPresented, - map: map, + mapOrScene: mapOrScene, viewpoint: viewpoint ) _isPresented = isPresented @@ -81,7 +81,7 @@ public struct Bookmarks: View { } } - /// Sets a closure to perform when the viewpoint of the map view changes. + /// Sets a closure to perform when the viewpoint of the `MapView` or `SceneView` changes. /// - Parameters: /// - action: The closure to perform when the bookmark selection has changed. public func onSelectionChanged( diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 7e3c7ea16..a550332ff 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -19,13 +19,13 @@ struct BookmarksList: View { /// A list of selectable bookmarks. var bookmarks: [Bookmark]? - /// A list of bookmarks derived either directly from `bookmarks` or from `map`. + /// A list of bookmarks derived either directly from `bookmarks` or from `mapOrScene`. private var definedBookmarks: [Bookmark] { var result: [Bookmark] = [] if let bookmarks = bookmarks { result = bookmarks - } else if let map = map { - result = map.bookmarks + } else if let geoModel = mapOrScene { + result = geoModel.bookmarks } else { return [] } @@ -36,12 +36,12 @@ struct BookmarksList: View { @Binding var isPresented: Bool - /// A map containing bookmarks. - var map: Map? + /// A map or scene containing bookmarks. + var mapOrScene: GeoModel? /// Indicates if bookmarks have loaded and are ready for display. @State - var mapIsLoaded = false + var geoModelIsLoaded = false /// User defined action to be performed when a bookmark is selected. var selectionChangedActions: ((Bookmark) -> Void)? = nil @@ -67,10 +67,10 @@ struct BookmarksList: View { } var body: some View { - if map == nil { + if mapOrScene == nil { bookmarkList } else { - if mapIsLoaded { + if geoModelIsLoaded { bookmarkList } else { loading @@ -116,7 +116,7 @@ private extension BookmarksList { ) } - /// A view that is shown while a web map is loading. + /// A view that is shown while a `GeoModel` is loading. private var loading: some View { VStack { Spacer() @@ -126,9 +126,8 @@ private extension BookmarksList { Text("Loading") }.task { do { - try await map?.load() - print(map?.bookmarks.count) - mapIsLoaded = true + try await mapOrScene?.load() + geoModelIsLoaded = true } catch { print(error.localizedDescription) } From de0a0205ed8effc4ae6b8963be8677404b3195c6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 14:50:40 -0800 Subject: [PATCH 0546/1722] `map` -> `mapOrScene` --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index ff45a2fd1..719841ef7 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -121,7 +121,7 @@ struct BookmarksExampleView: View { // Or control viewpoint changes yourself: // Bookmarks( // isPresented: $showingBookmarks, -// map: webMap +// mapOrScene: webMap // ) // .onSelectionChanged { // viewpoint = $0.viewpoint From fc98cb1974d843f6acb0d1eb4eafee08dcc73504 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 14:51:28 -0800 Subject: [PATCH 0547/1722] Alphabetical ordering --- Examples/Examples/BookmarksExampleView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 719841ef7..2342d7e16 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -72,9 +72,6 @@ struct BookmarksExampleView: View { ) ] - /// A web map with predefined bookmarks. - private let webMap = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! - /// Indicates if the bookmarks list is shown or not. /// - Remark: This allows a developer to control how the bookmarks menu is shown/hidden, /// whether that be in a group of options or a standalone button. @@ -84,6 +81,9 @@ struct BookmarksExampleView: View { @State var viewpoint: Viewpoint? = nil + /// A web map with predefined bookmarks. + private let webMap = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + var body: some View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { From 65fe8d848c3b5238f9c49c976f0c6e5add0dfd64 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 16:40:19 -0800 Subject: [PATCH 0548/1722] Moves work from `BookmarksList`->`Bookmarks` per PR feedback --- .../Components/Bookmarks/Bookmarks.swift | 106 +++++++++++++----- .../Components/Bookmarks/BookmarksList.swift | 106 +++--------------- .../ArcGISToolkit/Extensions/Binding.swift | 27 +++++ 3 files changed, 124 insertions(+), 115 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Binding.swift diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 550e0f012..d565118a4 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -16,19 +16,61 @@ import SwiftUI /// `Bookmarks` allows a user to view and select from a set of bookmarks. public struct Bookmarks: View { - /// A list that displays bookmarks. - private var bookmarksList: BookmarksList + /// A list of selectable bookmarks. + @State + private var bookmarks: [Bookmark] = [] + + /// A map or scene containing bookmarks. + private var geoModel: GeoModel? + + /// Indicates if bookmarks have loaded and are ready for display. + @State + var geoModelIsLoaded = false /// Determines if the bookmarks list is currently shown or not. @Binding private var isPresented: Bool + /// A bookmark that was selected. + /// + /// Used to listen for a selection. + @State + var selectedBookmark: Bookmark? = nil + /// User defined action to be performed when a bookmark is selected. Use this when you prefer to /// self-manage the response to a bookmark selection. Use either `onSelectionChangedActions` /// or `viewpoint` exclusively. - var selectionChangedActions: ((Bookmark) -> Void)? = nil { - didSet { - bookmarksList.selectionChangedActions = selectionChangedActions + var selectionChangedActions: ((Bookmark) -> Void)? = nil + + /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. + var viewpoint: Binding? + + /// Sets a closure to perform when the viewpoint of the `MapView` or `SceneView` changes. + /// - Parameters: + /// - action: The closure to perform when the bookmark selection has changed. + public func onSelectionChanged( + perform action: @escaping (Bookmark) -> Void + ) -> Bookmarks { + var copy = self + copy.selectionChangedActions = action + return copy + } + + /// Performs the necessary actions when a bookmark is selected. + /// + /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint + /// if the user provided a viewpoint or calling actions if the user implemented the + /// `selectionChangedActions` modifier. + /// - Parameter bookmark: The bookmark that was selected. + func selectBookmark(_ bookmark: Bookmark?) { + guard bookmark != nil else { return } + isPresented = false + if let viewpoint = viewpoint { + viewpoint.wrappedValue = bookmark!.viewpoint + } else if let actions = selectionChangedActions { + actions(bookmark!) + } else { + fatalError("No viewpoint or action provided") } } @@ -45,11 +87,8 @@ public struct Bookmarks: View { bookmarks: [Bookmark], viewpoint: Binding? = nil ) { - bookmarksList = BookmarksList( - bookmarks: bookmarks, - isPresented: isPresented, - viewpoint: viewpoint - ) + self.bookmarks = bookmarks + self.viewpoint = viewpoint _isPresented = isPresented } @@ -66,29 +105,46 @@ public struct Bookmarks: View { mapOrScene: GeoModel, viewpoint: Binding? = nil ) { - bookmarksList = BookmarksList( - isPresented: isPresented, - mapOrScene: mapOrScene, - viewpoint: viewpoint - ) + geoModel = mapOrScene + self.viewpoint = viewpoint _isPresented = isPresented } public var body: some View { Group { BookmarksHeader(isPresented: $isPresented) - bookmarksList + if geoModel == nil || geoModelIsLoaded { + BookmarksList( + bookmarks: bookmarks, + selectedBookmark: $selectedBookmark.onChange { + selectBookmark($0) + } + ) + } else { + loading + } } } - /// Sets a closure to perform when the viewpoint of the `MapView` or `SceneView` changes. - /// - Parameters: - /// - action: The closure to perform when the bookmark selection has changed. - public func onSelectionChanged( - perform action: @escaping (Bookmark) -> Void - ) -> Bookmarks { - var copy = self - copy.selectionChangedActions = action - return copy + /// A view that is shown while a `GeoModel` is loading. + private var loading: some View { + VStack { + Spacer() + HStack { + ProgressView() + .padding([.trailing], 5) + Text("Loading") + }.task { + do { + try await geoModel?.load() + geoModelIsLoaded = true + bookmarks = geoModel?.bookmarks ?? [] + } catch { + print(error.localizedDescription) + } + } + Spacer() + } + .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index a550332ff..f765340ac 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -16,70 +16,9 @@ import SwiftUI /// `BookmarksList` displays a list of selectable bookmarks. struct BookmarksList: View { - /// A list of selectable bookmarks. - var bookmarks: [Bookmark]? + /// A list of bookmarks for display. + var bookmarks: [Bookmark] - /// A list of bookmarks derived either directly from `bookmarks` or from `mapOrScene`. - private var definedBookmarks: [Bookmark] { - var result: [Bookmark] = [] - if let bookmarks = bookmarks { - result = bookmarks - } else if let geoModel = mapOrScene { - result = geoModel.bookmarks - } else { - return [] - } - return result.sorted { $0.name < $1.name } - } - - /// Determines if the list is currently shown or not. - @Binding - var isPresented: Bool - - /// A map or scene containing bookmarks. - var mapOrScene: GeoModel? - - /// Indicates if bookmarks have loaded and are ready for display. - @State - var geoModelIsLoaded = false - - /// User defined action to be performed when a bookmark is selected. - var selectionChangedActions: ((Bookmark) -> Void)? = nil - - /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. - var viewpoint: Binding? - - /// Performs the necessary actions when a bookmark is selected. - /// - /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint - /// if the user provided a viewpoint or calling actions if the user implemented the - /// `selectionChangedActions` modifier. - /// - Parameter bookmark: The bookmark that was selected. - func selectBookmark(_ bookmark: Bookmark) { - isPresented = false - if let viewpoint = viewpoint { - viewpoint.wrappedValue = bookmark.viewpoint - } else if let actions = selectionChangedActions { - actions(bookmark) - } else { - fatalError("No viewpoint or action provided") - } - } - - var body: some View { - if mapOrScene == nil { - bookmarkList - } else { - if geoModelIsLoaded { - bookmarkList - } else { - loading - } - } - } -} - -private extension BookmarksList { /// The minimum height of a bookmark list item. /// /// This number will be larger when them item's name consumes 2+ lines of text. @@ -87,10 +26,15 @@ private extension BookmarksList { 44 } - /// A list that is shown once bookmarks have loaded. - private var bookmarkList: some View { + /// A bookmark that was selected. + /// + /// Indicates to the parent that a selection was made. + @Binding + var selectedBookmark: Bookmark? + + var body: some View { List { - if definedBookmarks.isEmpty { + if bookmarks.isEmpty { Label { Text("No bookmarks") } icon: { @@ -98,9 +42,12 @@ private extension BookmarksList { } .foregroundColor(.primary) } else { - ForEach(definedBookmarks, id: \.viewpoint) { bookmark in + ForEach( + bookmarks.sorted { $0.name < $1.name }, + id: \.viewpoint + ) { bookmark in Button { - selectBookmark(bookmark) + selectedBookmark = bookmark } label: { Text(bookmark.name) } @@ -111,29 +58,8 @@ private extension BookmarksList { .frame( minHeight: minimumRowHeight, idealHeight: minimumRowHeight * Double( - max(1, definedBookmarks.count)), + max(1, bookmarks.count)), maxHeight: .infinity ) } - - /// A view that is shown while a `GeoModel` is loading. - private var loading: some View { - VStack { - Spacer() - HStack { - ProgressView() - .padding([.trailing], 5) - Text("Loading") - }.task { - do { - try await mapOrScene?.load() - geoModelIsLoaded = true - } catch { - print(error.localizedDescription) - } - } - Spacer() - } - .padding() - } } diff --git a/Sources/ArcGISToolkit/Extensions/Binding.swift b/Sources/ArcGISToolkit/Extensions/Binding.swift new file mode 100644 index 000000000..cccf28372 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Binding.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 + +extension Binding { + /// Provides a way to listen for changes on bound parameters. + func onChange(_ handler: @escaping (Value) -> Void) -> Binding { + Binding( + get: { self.wrappedValue }, + set: { newValue in + self.wrappedValue = newValue + handler(newValue) + } + ) + } +} From 62a8e89048fbac88ce0c370b1e79871d3b86ff7d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 16:49:22 -0800 Subject: [PATCH 0549/1722] Update tests --- .../BookmarksListTest.swift | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksListTest.swift b/Tests/ArcGISToolkitTests/BookmarksListTest.swift index e18528ee2..42fc089a3 100644 --- a/Tests/ArcGISToolkitTests/BookmarksListTest.swift +++ b/Tests/ArcGISToolkitTests/BookmarksListTest.swift @@ -32,14 +32,13 @@ class BookmarksListTest: XCTestCase { get: { _isPresented }, set: {_isPresented = $0 } ) - let bookmarksList = BookmarksList( - bookmarks: bookmarks, + var bookmarksManager = Bookmarks( isPresented: isPresented, - mapIsLoaded: true, - selectionChangedActions: action + bookmarks: bookmarks ) + bookmarksManager.selectionChangedActions = action XCTAssertTrue(_isPresented) - bookmarksList.selectBookmark(bookmarks.first!) + bookmarksManager.selectBookmark(bookmarks.first!) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } @@ -64,14 +63,13 @@ class BookmarksListTest: XCTestCase { expectation.fulfill() XCTAssertEqual($0.viewpoint, webMap.bookmarks.first?.viewpoint) } - let bookmarksList = BookmarksList( + var bookmarksManager = Bookmarks( isPresented: isPresented, - map: webMap, - mapIsLoaded: true, - selectionChangedActions: action + mapOrScene: webMap ) + bookmarksManager.selectionChangedActions = action XCTAssertTrue(_isPresented) - bookmarksList.selectBookmark(webMap.bookmarks.first!) + bookmarksManager.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } @@ -89,15 +87,14 @@ class BookmarksListTest: XCTestCase { get: { _viewpoint }, set: { _viewpoint = $0 } ) - let bookmarksList = BookmarksList( - bookmarks: bookmarks, + let bookmarksManager = Bookmarks( isPresented: isPresented, - mapIsLoaded: true, + bookmarks: bookmarks, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) XCTAssertNotEqual(_viewpoint, bookmarks.first?.viewpoint) - bookmarksList.selectBookmark(bookmarks.first!) + bookmarksManager.selectBookmark(bookmarks.first!) XCTAssertFalse(_isPresented) XCTAssertEqual(_viewpoint, bookmarks.first?.viewpoint) } @@ -120,15 +117,14 @@ class BookmarksListTest: XCTestCase { get: { _viewpoint }, set: { _viewpoint = $0 } ) - let bookmarksList = BookmarksList( + let bookmarksManager = Bookmarks( isPresented: isPresented, - map: webMap, - mapIsLoaded: true, + mapOrScene: webMap, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) XCTAssertNotEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) - bookmarksList.selectBookmark(webMap.bookmarks.first!) + bookmarksManager.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) XCTAssertEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) } From 6331ab6e2acf0565da2dd60dff813113ab034e58 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 17:31:55 -0800 Subject: [PATCH 0550/1722] Add `ScalebarAlignment` --- .../Scalebar/ScalebarAlignment.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift new file mode 100644 index 000000000..436bb338e --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift @@ -0,0 +1,20 @@ +// 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 Foundation + +public enum ScalebarAlignment { + case left + case right + case center +} From 7878413b5d14baa3fef1aa2d60af608a330a8775 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 17:32:40 -0800 Subject: [PATCH 0551/1722] Add `ScalebarUnits` --- .../Components/Scalebar/ScalebarUnits.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift new file mode 100644 index 000000000..e9107c7f2 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -0,0 +1,19 @@ +// 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 Foundation + +public enum ScalebarUnits { + case imperial + case metric +} From 35ce8fafe837d58165e564f033a4d4d4741ba4e4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 17:33:46 -0800 Subject: [PATCH 0552/1722] Add `ScalebarStyle` --- .../Components/Scalebar/ScalebarStyle.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift new file mode 100644 index 000000000..302a67586 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift @@ -0,0 +1,22 @@ +// 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 Foundation + +public enum ScalebarStyle { + case line + case bar + case graduatedLine + case alternatingBar + case dualUnitLine +} From 66f2a172d9e79e55713be99d34763f5394359929 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 17:38:45 -0800 Subject: [PATCH 0553/1722] Add `Scalebar` --- .../Components/Scalebar/Scalebar.swift | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift new file mode 100644 index 000000000..ccf24882c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -0,0 +1,45 @@ +// 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 +import SwiftUI + +/// Displays the current scale on-screen +public struct Scalebar: View { + /// Acts as a data provider of the current scale. + private var scale: Double? + + /// Acts as a data provider of the current scale. + private var viewpoint: Viewpoint? + + /// Acts as a data provider of the current scale. + var visibleArea: Polygon? + + public init( + _ scale: Double?, + _ viewpoint: Viewpoint?, + _ visibleArea: Polygon? + ) { + self.scale = scale + self.viewpoint = viewpoint + self.visibleArea = visibleArea + } + + public var body: some View { + VStack { + Text(scale?.description ?? "N/A") + Text(viewpoint?.targetScale.description ?? "N/A") + Text(visibleArea?.extent.width.description ?? "N/A") + } + } +} From 62c6c9c89e9f07253c47fdd93cc10f81b357e968 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 17:39:08 -0800 Subject: [PATCH 0554/1722] Expand starter example --- Examples/Examples/ScalebarExampleView.swift | 32 ++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 121ed042a..26274ee37 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -11,11 +11,41 @@ // See the License for the specific language governing permissions and // limitations under the License. +import ArcGIS import ArcGISToolkit import SwiftUI struct ScalebarExampleView: View { + /// The map displayed in the map view. + private let map = Map(basemapStyle: .arcGISImagery) + + @State + /// Allows for communication between the Scalebar and MapView or SceneView. + private var scale: Double? + + /// Allows for communication between the Scalebar and MapView or SceneView. + @State + private var viewpoint: Viewpoint? = Viewpoint( + center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), + scale: 10_000 + ) + + /// Allows for communication between the Scalebar and MapView or SceneView. + @State + private var visibleArea: Polygon? + var body: some View { - EmptyView() + MapView(map: map, viewpoint: viewpoint) + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .onVisibleAreaChanged { visibleArea = $0 } + .onScaleChanged { scale = $0 } + .overlay(alignment: .bottomLeading) { + Scalebar(scale, viewpoint, visibleArea) + .border(.red, width: 2) + .padding([.leading], 10) + .border(.green, width: 2) + .padding([.bottom], 30) + .border(.blue, width: 2) + } } } From a56b9eded565b5958f10c4bc0996971d59abbe91 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Feb 2022 17:40:59 -0800 Subject: [PATCH 0555/1722] Private --- 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 ccf24882c..1ba148837 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -23,7 +23,7 @@ public struct Scalebar: View { private var viewpoint: Viewpoint? /// Acts as a data provider of the current scale. - var visibleArea: Polygon? + private var visibleArea: Polygon? public init( _ scale: Double?, From f555fac3fa0052f2979eb5dbd8bbd699215f6d36 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 28 Feb 2022 18:16:17 -0800 Subject: [PATCH 0556/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Mark Dostal --- Examples/Examples/BookmarksExampleView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 2342d7e16..6633aed38 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -112,7 +112,9 @@ struct BookmarksExampleView: View { ) } .popover(isPresented: $showingBookmarks) { - // Let the bookmarks component control viewpoint changes: + // Display the Bookmarks view with a pre-defined list of bookmarks. + // Passing in a viewpoint binding will allow the Bookmarks + // view to handle bookmark selection. Bookmarks( isPresented: $showingBookmarks, bookmarks: sampleBookmarks, From 0b708cf1da017cd9912ff9ae914fcdb507858f3c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 08:46:37 -0800 Subject: [PATCH 0557/1722] Pull extra sample map and bookmarks --- Examples/Examples/BookmarksExampleView.swift | 63 ++------------------ 1 file changed, 5 insertions(+), 58 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 2342d7e16..838be6616 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -20,57 +20,8 @@ struct BookmarksExampleView: View { @State private var bookmarksAdded = 0 - /// The map displayed in the map view. - private let map = Map(basemapStyle: .arcGISImagery) - - /// Sample bookmarks for demonstration. - @State - var sampleBookmarks: [Bookmark] = [ - Bookmark( - name: "Yosemite National Park", - viewpoint: Viewpoint( - center: Point( - x: -119.538330, - y: 37.865101, - spatialReference: .wgs84 - ), - scale: 250_000 - ) - ), - Bookmark( - name: "Zion National Park", - viewpoint: Viewpoint( - center: Point( - x: -113.028770, - y: 37.297817, - spatialReference: .wgs84 - ), - scale: 250_000 - ) - ), - Bookmark( - name: "Yellowstone National Park", - viewpoint: Viewpoint( - center: Point( - x: -110.584663, - y: 44.429764, - spatialReference: .wgs84 - ), - scale: 375_000 - ) - ), - Bookmark( - name: "Grand Canyon National Park", - viewpoint: Viewpoint( - center: Point( - x: -112.1129, - y: 36.1069, - spatialReference: .wgs84 - ), - scale: 375_000 - ) - ) - ] + /// A web map with predefined bookmarks. + private let map = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! /// Indicates if the bookmarks list is shown or not. /// - Remark: This allows a developer to control how the bookmarks menu is shown/hidden, @@ -81,9 +32,6 @@ struct BookmarksExampleView: View { @State var viewpoint: Viewpoint? = nil - /// A web map with predefined bookmarks. - private let webMap = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! - var body: some View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { @@ -97,8 +45,7 @@ struct BookmarksExampleView: View { scale: viewpoint?.targetScale ?? 10_000 ) ) - webMap.addBookmark(newBookmark) - sampleBookmarks.append(newBookmark) + map.addBookmark(newBookmark) bookmarksAdded += 1 } .toolbar { @@ -115,13 +62,13 @@ struct BookmarksExampleView: View { // Let the bookmarks component control viewpoint changes: Bookmarks( isPresented: $showingBookmarks, - bookmarks: sampleBookmarks, + mapOrScene: map, viewpoint: $viewpoint ) // Or control viewpoint changes yourself: // Bookmarks( // isPresented: $showingBookmarks, -// mapOrScene: webMap +// mapOrScene: map // ) // .onSelectionChanged { // viewpoint = $0.viewpoint From 79df8d221f3bc449897266aa04df2b6f8d5222dc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 08:47:12 -0800 Subject: [PATCH 0558/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Mark Dostal --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index e7716933e..fc1e67508 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -67,7 +67,7 @@ struct BookmarksExampleView: View { mapOrScene: map, viewpoint: $viewpoint ) - // Or control viewpoint changes yourself: + // Display the Bookmarks view with the list of bookmarks in a map. // Bookmarks( // isPresented: $showingBookmarks, // mapOrScene: map From c3748a4f894500acc18e4efe9acb1fac9217aa27 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 08:48:55 -0800 Subject: [PATCH 0559/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Mark Dostal --- Examples/Examples/BookmarksExampleView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index fc1e67508..afae6c0e5 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -72,6 +72,8 @@ struct BookmarksExampleView: View { // isPresented: $showingBookmarks, // mapOrScene: map // ) + // In order to handle bookmark selection changes manually, + // use `.onSelectionChanged`. // .onSelectionChanged { // viewpoint = $0.viewpoint // } From d397bec654f5810e89804fc7cd389b11f3d87767 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 08:49:58 -0800 Subject: [PATCH 0560/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 d565118a4..ed8c4a170 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -38,7 +38,7 @@ public struct Bookmarks: View { var selectedBookmark: Bookmark? = nil /// User defined action to be performed when a bookmark is selected. Use this when you prefer to - /// self-manage the response to a bookmark selection. Use either `onSelectionChangedActions` + /// self-manage the response to a bookmark selection. Use either `onSelectionChanged` /// or `viewpoint` exclusively. var selectionChangedActions: ((Bookmark) -> Void)? = nil From cd8a095cf572b55e42530ae1e0f6be81f886c0f3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 08:53:38 -0800 Subject: [PATCH 0561/1722] `selectionChangedActions` -> `onSelectionChanged` --- .../Components/Bookmarks/Bookmarks.swift | 25 ++++++++++--------- .../BookmarksListTest.swift | 4 +-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index ed8c4a170..73bc5954a 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -31,17 +31,18 @@ public struct Bookmarks: View { @Binding private var isPresented: Bool + /// User defined action to be performed when a bookmark is selected. + /// + /// Use this when you prefer to self-manage the response to a bookmark selection. Use either + /// `onSelectionChanged` or `viewpoint` exclusively. + var onSelectionChanged: ((Bookmark) -> Void)? = nil + /// A bookmark that was selected. /// /// Used to listen for a selection. @State var selectedBookmark: Bookmark? = nil - /// User defined action to be performed when a bookmark is selected. Use this when you prefer to - /// self-manage the response to a bookmark selection. Use either `onSelectionChanged` - /// or `viewpoint` exclusively. - var selectionChangedActions: ((Bookmark) -> Void)? = nil - /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. var viewpoint: Binding? @@ -52,7 +53,7 @@ public struct Bookmarks: View { perform action: @escaping (Bookmark) -> Void ) -> Bookmarks { var copy = self - copy.selectionChangedActions = action + copy.onSelectionChanged = action return copy } @@ -60,14 +61,14 @@ public struct Bookmarks: View { /// /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint /// if the user provided a viewpoint or calling actions if the user implemented the - /// `selectionChangedActions` modifier. + /// `onSelectionChanged` modifier. /// - Parameter bookmark: The bookmark that was selected. func selectBookmark(_ bookmark: Bookmark?) { guard bookmark != nil else { return } isPresented = false if let viewpoint = viewpoint { viewpoint.wrappedValue = bookmark!.viewpoint - } else if let actions = selectionChangedActions { + } else if let actions = onSelectionChanged { actions(bookmark!) } else { fatalError("No viewpoint or action provided") @@ -75,13 +76,13 @@ public struct Bookmarks: View { } /// Creates a `Bookmarks` component. - /// - precondition: `viewpoint` is non-nil or the `selectionChangedActions` modifier is + /// - precondition: `viewpoint` is non-nil or the `onSelectionChanged` modifier is /// implemented. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. /// - bookmarks: A list of bookmarks. Use this when displaying bookmarks defined at run-time. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use - /// either `viewpoint` or `selectionChangedActions` exclusively. + /// either `viewpoint` or `onSelectionChanged` exclusively. public init( isPresented: Binding, bookmarks: [Bookmark], @@ -93,13 +94,13 @@ public struct Bookmarks: View { } /// Creates a `Bookmarks` component. - /// - precondition: `viewpoint` is non-nil or the `selectionChangedActions` modifier is + /// - precondition: `viewpoint` is non-nil or the `onSelectionChanged` modifier is /// implemented. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. /// - mapOrScene: A `GeoModel` authored with pre-existing bookmarks. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use - /// either `viewpoint` or `selectionChangedActions` exclusively. + /// either `viewpoint` or `onSelectionChanged` exclusively. public init( isPresented: Binding, mapOrScene: GeoModel, diff --git a/Tests/ArcGISToolkitTests/BookmarksListTest.swift b/Tests/ArcGISToolkitTests/BookmarksListTest.swift index 42fc089a3..d9a8639c6 100644 --- a/Tests/ArcGISToolkitTests/BookmarksListTest.swift +++ b/Tests/ArcGISToolkitTests/BookmarksListTest.swift @@ -36,7 +36,7 @@ class BookmarksListTest: XCTestCase { isPresented: isPresented, bookmarks: bookmarks ) - bookmarksManager.selectionChangedActions = action + bookmarksManager.onSelectionChanged = action XCTAssertTrue(_isPresented) bookmarksManager.selectBookmark(bookmarks.first!) XCTAssertFalse(_isPresented) @@ -67,7 +67,7 @@ class BookmarksListTest: XCTestCase { isPresented: isPresented, mapOrScene: webMap ) - bookmarksManager.selectionChangedActions = action + bookmarksManager.onSelectionChanged = action XCTAssertTrue(_isPresented) bookmarksManager.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) From 73a11547406f3e7702a3cf1dfa736668da74b865 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 08:59:12 -0800 Subject: [PATCH 0562/1722] Update tests --- ...rksListTest.swift => BookmarksTests.swift} | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) rename Tests/ArcGISToolkitTests/{BookmarksListTest.swift => BookmarksTests.swift} (86%) diff --git a/Tests/ArcGISToolkitTests/BookmarksListTest.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift similarity index 86% rename from Tests/ArcGISToolkitTests/BookmarksListTest.swift rename to Tests/ArcGISToolkitTests/BookmarksTests.swift index d9a8639c6..3cb481064 100644 --- a/Tests/ArcGISToolkitTests/BookmarksListTest.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -16,29 +16,29 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -class BookmarksListTest: XCTestCase { +class BookmarksTests: XCTestCase { /// Assert that the list properly handles a selction when provided a modifier. func testSelectBookmarkWithModifier() { let expectation = XCTestExpectation( description: "Modifier action was performed" ) - let bookmarks = sampleBookmarks + let sampleBookmarks = sampleBookmarks var _isPresented = true let action: ((Bookmark) -> Void) = { expectation.fulfill() - XCTAssertEqual($0.viewpoint, bookmarks.first?.viewpoint) + XCTAssertEqual($0.viewpoint, sampleBookmarks.first?.viewpoint) } let isPresented = Binding( get: { _isPresented }, set: {_isPresented = $0 } ) - var bookmarksManager = Bookmarks( + var bookmarks = Bookmarks( isPresented: isPresented, - bookmarks: bookmarks + bookmarks: sampleBookmarks ) - bookmarksManager.onSelectionChanged = action + bookmarks.onSelectionChanged = action XCTAssertTrue(_isPresented) - bookmarksManager.selectBookmark(bookmarks.first!) + bookmarks.selectBookmark(sampleBookmarks.first!) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } @@ -63,20 +63,20 @@ class BookmarksListTest: XCTestCase { expectation.fulfill() XCTAssertEqual($0.viewpoint, webMap.bookmarks.first?.viewpoint) } - var bookmarksManager = Bookmarks( + var bookmarks = Bookmarks( isPresented: isPresented, mapOrScene: webMap ) - bookmarksManager.onSelectionChanged = action + bookmarks.onSelectionChanged = action XCTAssertTrue(_isPresented) - bookmarksManager.selectBookmark(webMap.bookmarks.first!) + bookmarks.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } /// Assert that the list properly handles a selction when provided a viewpoint. func testSelectBookmarkWithViewpoint() { - let bookmarks = sampleBookmarks + let sampleBookmarks = sampleBookmarks var _isPresented = true let isPresented = Binding( get: { _isPresented }, @@ -87,16 +87,16 @@ class BookmarksListTest: XCTestCase { get: { _viewpoint }, set: { _viewpoint = $0 } ) - let bookmarksManager = Bookmarks( + let bookmarks = Bookmarks( isPresented: isPresented, - bookmarks: bookmarks, + bookmarks: sampleBookmarks, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) - XCTAssertNotEqual(_viewpoint, bookmarks.first?.viewpoint) - bookmarksManager.selectBookmark(bookmarks.first!) + XCTAssertNotEqual(_viewpoint, sampleBookmarks.first?.viewpoint) + bookmarks.selectBookmark(sampleBookmarks.first!) XCTAssertFalse(_isPresented) - XCTAssertEqual(_viewpoint, bookmarks.first?.viewpoint) + XCTAssertEqual(_viewpoint, sampleBookmarks.first?.viewpoint) } /// Assert that the list properly handles a selction when provided a viewpoint and web map. @@ -117,20 +117,20 @@ class BookmarksListTest: XCTestCase { get: { _viewpoint }, set: { _viewpoint = $0 } ) - let bookmarksManager = Bookmarks( + let bookmarks = Bookmarks( isPresented: isPresented, mapOrScene: webMap, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) XCTAssertNotEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) - bookmarksManager.selectBookmark(webMap.bookmarks.first!) + bookmarks.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) XCTAssertEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) } } -extension BookmarksListTest { +extension BookmarksTests { /// An arbitrary point to use for testing. var point: Point { Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) From 9767432d7a197bfa4a145b0cfb59403d018940aa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 08:59:45 -0800 Subject: [PATCH 0563/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 73bc5954a..514a19452 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -25,7 +25,7 @@ public struct Bookmarks: View { /// Indicates if bookmarks have loaded and are ready for display. @State - var geoModelIsLoaded = false + private var geoModelIsLoaded = false /// Determines if the bookmarks list is currently shown or not. @Binding From c0103a660ab4a43857abccd4793fc14738240625 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:00:43 -0800 Subject: [PATCH 0564/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 514a19452..b8c5741ce 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -41,7 +41,7 @@ public struct Bookmarks: View { /// /// Used to listen for a selection. @State - var selectedBookmark: Bookmark? = nil + private var selectedBookmark: Bookmark? = nil /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. var viewpoint: Binding? From 2eed9663f54664b15b7c3a410d2f5be18fe4fdde Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:01:07 -0800 Subject: [PATCH 0565/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 b8c5741ce..4dfe627d5 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -44,7 +44,7 @@ public struct Bookmarks: View { private var selectedBookmark: Bookmark? = nil /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. - var viewpoint: Binding? + private var viewpoint: Binding? /// Sets a closure to perform when the viewpoint of the `MapView` or `SceneView` changes. /// - Parameters: From 63b1c152ff3a4377121f8d484a7a064474d713da Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:01:48 -0800 Subject: [PATCH 0566/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 4dfe627d5..db132e8d0 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -46,7 +46,7 @@ public struct Bookmarks: View { /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. private var viewpoint: Binding? - /// Sets a closure to perform when the viewpoint of the `MapView` or `SceneView` changes. + /// Sets a closure to perform when the bookmark selection changes. /// - Parameters: /// - action: The closure to perform when the bookmark selection has changed. public func onSelectionChanged( From 9b0d2dc92ec95fa3ebe5e39d82a41f055113a05f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:02:43 -0800 Subject: [PATCH 0567/1722] Remove `fatalError` --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index db132e8d0..46be1790f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -70,8 +70,6 @@ public struct Bookmarks: View { viewpoint.wrappedValue = bookmark!.viewpoint } else if let actions = onSelectionChanged { actions(bookmark!) - } else { - fatalError("No viewpoint or action provided") } } From 70a9b18053baf375f4490a3fc86fb4174efc4906 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:05:08 -0800 Subject: [PATCH 0568/1722] Update doc. for `selectBookmark` --- 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 46be1790f..106fae76b 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -60,7 +60,7 @@ public struct Bookmarks: View { /// Performs the necessary actions when a bookmark is selected. /// /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint - /// if the user provided a viewpoint or calling actions if the user implemented the + /// binding (if provided) or calling the closure provided by the `onSelectionChanged` modifier. /// `onSelectionChanged` modifier. /// - Parameter bookmark: The bookmark that was selected. func selectBookmark(_ bookmark: Bookmark?) { From b66c593d147d29e9fdde240b34b628fc220e9530 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:06:49 -0800 Subject: [PATCH 0569/1722] Update Bookmarks.swift --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 106fae76b..b49488bcb 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -68,8 +68,8 @@ public struct Bookmarks: View { isPresented = false if let viewpoint = viewpoint { viewpoint.wrappedValue = bookmark!.viewpoint - } else if let actions = onSelectionChanged { - actions(bookmark!) + } else if let onSelectionChanged = onSelectionChanged { + onSelectionChanged(bookmark!) } } From 9e419527a48c9ce19199ea99780382f060dd0d51 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:07:39 -0800 Subject: [PATCH 0570/1722] Pull precondition doc. --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index b49488bcb..59e74d7e4 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -74,8 +74,6 @@ public struct Bookmarks: View { } /// Creates a `Bookmarks` component. - /// - precondition: `viewpoint` is non-nil or the `onSelectionChanged` modifier is - /// implemented. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. /// - bookmarks: A list of bookmarks. Use this when displaying bookmarks defined at run-time. From 347345b4daedad01af4f10415da887ee48aa33dd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:09:51 -0800 Subject: [PATCH 0571/1722] Update doc. --- .../ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 59e74d7e4..28db9e58d 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -77,8 +77,8 @@ public struct Bookmarks: View { /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. /// - bookmarks: A list of bookmarks. Use this when displaying bookmarks defined at run-time. - /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use - /// either `viewpoint` or `onSelectionChanged` exclusively. + /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. + /// Alternately, you can use the `.onSelectionChanged` modifier to handle bookmark selection. public init( isPresented: Binding, bookmarks: [Bookmark], @@ -95,8 +95,8 @@ public struct Bookmarks: View { /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. /// - mapOrScene: A `GeoModel` authored with pre-existing bookmarks. - /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. Use - /// either `viewpoint` or `onSelectionChanged` exclusively. + /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. + /// Alternately, you can use the `.onSelectionChanged` modifier to handle bookmark selection. public init( isPresented: Binding, mapOrScene: GeoModel, From 9d1c98ae86ab32b553e307b4a22a35d86b7252ae Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:10:23 -0800 Subject: [PATCH 0572/1722] Update doc. --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 28db9e58d..045c8f53f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -90,8 +90,6 @@ public struct Bookmarks: View { } /// Creates a `Bookmarks` component. - /// - precondition: `viewpoint` is non-nil or the `onSelectionChanged` modifier is - /// implemented. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. /// - mapOrScene: A `GeoModel` authored with pre-existing bookmarks. From e6e8b9fe372a06004817156e783646fcb89f9256 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:12:01 -0800 Subject: [PATCH 0573/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 045c8f53f..b881b828e 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -43,7 +43,7 @@ public struct Bookmarks: View { @State private var selectedBookmark: Bookmark? = nil - /// If non-`nil`, this viewpoint is updated when a bookmark is pressed. + /// If non-`nil`, this viewpoint is updated when a bookmark is selected. private var viewpoint: Binding? /// Sets a closure to perform when the bookmark selection changes. From 2d0e894a0bc8e561215244850ff39d96fc5936b7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 09:24:59 -0800 Subject: [PATCH 0574/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift Co-authored-by: Mark Dostal --- .../ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift index f2acc65f6..692dbf0ee 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -20,7 +20,7 @@ struct BookmarksHeader: View { var isPresented: Bool public var body: some View { - HStack { + HStack(alignment: .top) { Image(systemName: "bookmark") VStack { Text("Bookmarks") From da880f570ab6dd25266a313aa02b853baec70fc0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 10:00:24 -0800 Subject: [PATCH 0575/1722] Redo dynamic height implementation --- .../Components/Bookmarks/BookmarksList.swift | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index f765340ac..3c539f712 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -16,16 +16,25 @@ import SwiftUI /// `BookmarksList` displays a list of selectable bookmarks. struct BookmarksList: View { + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? + + @Environment(\.verticalSizeClass) + private var verticalSizeClass: UserInterfaceSizeClass? + /// A list of bookmarks for display. var bookmarks: [Bookmark] - /// The minimum height of a bookmark list item. - /// - /// This number will be larger when them item's name consumes 2+ lines of text. - private var minimumRowHeight: Double { - 44 + /// If `true`, the device is in a compact-width or compact-height orientation. + /// If `false`, the device is in a regular-width and regular-height orientation. + private var isCompact: Bool { + horizontalSizeClass == .compact || verticalSizeClass == .compact } + /// The height of the scroll view's content. + @State + private var scrollViewContentHeight: CGFloat = .zero + /// A bookmark that was selected. /// /// Indicates to the parent that a selection was made. @@ -33,7 +42,7 @@ struct BookmarksList: View { var selectedBookmark: Bookmark? var body: some View { - List { + Group { if bookmarks.isEmpty { Label { Text("No bookmarks") @@ -42,24 +51,36 @@ struct BookmarksList: View { } .foregroundColor(.primary) } else { - ForEach( - bookmarks.sorted { $0.name < $1.name }, - id: \.viewpoint - ) { bookmark in - Button { - selectedBookmark = bookmark - } label: { - Text(bookmark.name) + ScrollView { + VStack(alignment: .leading) { + ForEach( + bookmarks.sorted { $0.name < $1.name }, + id: \.viewpoint + ) { bookmark in + Button { + selectedBookmark = bookmark + } label: { + Text(bookmark.name) + .foregroundColor(.primary) + } + .padding(4) + Divider() + } } + .padding() + .background( + GeometryReader { geometry -> Color in + DispatchQueue.main.async { + scrollViewContentHeight = geometry.size.height + } + return .clear + } + ) } + .frame( + maxHeight: isCompact ? .infinity : scrollViewContentHeight + ) } } - .listStyle(.plain) - .frame( - minHeight: minimumRowHeight, - idealHeight: minimumRowHeight * Double( - max(1, bookmarks.count)), - maxHeight: .infinity - ) } } From 75ee0c119b9760cb0c6c9755ba9e914afa400860 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 10:23:22 -0800 Subject: [PATCH 0576/1722] Apply PR suggestions, remove `onChange` --- .../Components/Bookmarks/Bookmarks.swift | 10 +++---- .../Components/Bookmarks/BookmarksList.swift | 20 +++++++++----- .../ArcGISToolkit/Extensions/Binding.swift | 27 ------------------- 3 files changed, 18 insertions(+), 39 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/Binding.swift diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index b881b828e..28d73b253 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -109,12 +109,10 @@ public struct Bookmarks: View { Group { BookmarksHeader(isPresented: $isPresented) if geoModel == nil || geoModelIsLoaded { - BookmarksList( - bookmarks: bookmarks, - selectedBookmark: $selectedBookmark.onChange { - selectBookmark($0) - } - ) + BookmarksList(bookmarks: bookmarks) + .onSelectionChanged { + selectBookmark($0) + } } else { loading } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 3c539f712..d856bbc4e 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -31,15 +31,23 @@ struct BookmarksList: View { horizontalSizeClass == .compact || verticalSizeClass == .compact } + /// Action to be performed when a bookmark is selected. + var onSelectionChanged: ((Bookmark) -> Void)? = nil + /// The height of the scroll view's content. @State private var scrollViewContentHeight: CGFloat = .zero - /// A bookmark that was selected. - /// - /// Indicates to the parent that a selection was made. - @Binding - var selectedBookmark: Bookmark? + /// Sets a closure to perform when the bookmark selection changes. + /// - Parameters: + /// - action: The closure to perform when the bookmark selection has changed. + public func onSelectionChanged( + perform action: @escaping (Bookmark) -> Void + ) -> BookmarksList { + var copy = self + copy.onSelectionChanged = action + return copy + } var body: some View { Group { @@ -58,7 +66,7 @@ struct BookmarksList: View { id: \.viewpoint ) { bookmark in Button { - selectedBookmark = bookmark + onSelectionChanged?(bookmark) } label: { Text(bookmark.name) .foregroundColor(.primary) diff --git a/Sources/ArcGISToolkit/Extensions/Binding.swift b/Sources/ArcGISToolkit/Extensions/Binding.swift deleted file mode 100644 index cccf28372..000000000 --- a/Sources/ArcGISToolkit/Extensions/Binding.swift +++ /dev/null @@ -1,27 +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 - -extension Binding { - /// Provides a way to listen for changes on bound parameters. - func onChange(_ handler: @escaping (Value) -> Void) -> Binding { - Binding( - get: { self.wrappedValue }, - set: { newValue in - self.wrappedValue = newValue - handler(newValue) - } - ) - } -} From 0ad573027aaafb0dcd8d013d6432e93121cc3e8b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 10:31:00 -0800 Subject: [PATCH 0577/1722] Renames `loading` to `loadingView` --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 28d73b253..7890f58b3 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -114,13 +114,13 @@ public struct Bookmarks: View { selectBookmark($0) } } else { - loading + loadingView } } } /// A view that is shown while a `GeoModel` is loading. - private var loading: some View { + private var loadingView: some View { VStack { Spacer() HStack { From 3a73db2ac1636dd6e27e90d1ed67f406a64c75ac Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Tue, 1 Mar 2022 12:01:56 -0800 Subject: [PATCH 0578/1722] fix tests --- Tests/ArcGISToolkitTests/SearchViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index d015eb12c..4146e619a 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -266,7 +266,7 @@ class SearchViewModelTests: XCTestCase { ) // First result within 1500m of Portland. - XCTAssertLessThan(geodeticDistance.distance, 1500) + XCTAssertLessThan(geodeticDistance.distance.value, 1500) // Set queryCenter to Edinburgh model.queryCenter = .edinburgh @@ -293,7 +293,7 @@ class SearchViewModelTests: XCTestCase { ) // First result within 100m of Edinburgh. - XCTAssertLessThan(geodeticDistance.distance, 100) + XCTAssertLessThan(geodeticDistance.distance.value, 100) } func testRepeatSearch() async throws { From 2aed6eca6d46d13beec7c356f0e0f182c2a03f07 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 12:23:03 -0800 Subject: [PATCH 0579/1722] Implement `onSizeChange` --- .../Components/Bookmarks/BookmarksList.swift | 17 ++++------- Sources/ArcGISToolkit/Extensions/View.swift | 30 +++++++++++++++++++ .../Utility/SizePreferenceKey.swift | 20 +++++++++++++ 3 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/View.swift create mode 100644 Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index d856bbc4e..f3acee07f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -34,9 +34,9 @@ struct BookmarksList: View { /// Action to be performed when a bookmark is selected. var onSelectionChanged: ((Bookmark) -> Void)? = nil - /// The height of the scroll view's content. + /// The height of the list content. @State - private var scrollViewContentHeight: CGFloat = .zero + private var listContentHeight: CGFloat = .zero /// Sets a closure to perform when the bookmark selection changes. /// - Parameters: @@ -76,17 +76,12 @@ struct BookmarksList: View { } } .padding() - .background( - GeometryReader { geometry -> Color in - DispatchQueue.main.async { - scrollViewContentHeight = geometry.size.height - } - return .clear - } - ) + .onSizeChange { + listContentHeight = $0.height + } } .frame( - maxHeight: isCompact ? .infinity : scrollViewContentHeight + maxHeight: isCompact ? .infinity : listContentHeight ) } } diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift new file mode 100644 index 000000000..74e66ebc3 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -0,0 +1,30 @@ +// 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 + +extension View { + /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. + /// - Parameter perform: The closure to be executed when the content size of the receiver + /// changes. + /// - Returns: A new `View`. + func onSizeChange(perform: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometry in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometry.size) + } + ) + .onPreferenceChange(SizePreferenceKey.self, perform: perform) + } +} diff --git a/Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift b/Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift new file mode 100644 index 000000000..bb2e71c78 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift @@ -0,0 +1,20 @@ +// 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 `PreferenceKey` that specifies a size. +struct SizePreferenceKey: PreferenceKey { + static let defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} +} From 6c123a2977ab54c6743ef0b2e8c53ecc0a19a194 Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Tue, 1 Mar 2022 16:54:06 -0800 Subject: [PATCH 0580/1722] fix build and tests b/c of new geometry engine method names --- .../Components/Search/SearchViewModel.swift | 4 +-- .../SearchViewModelTests.swift | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 1c8324617..17aabf8da 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -114,8 +114,8 @@ public class SearchViewModel: ObservableObject { // Check center difference. let centerDiff = GeometryEngine.distance( - geometry1: lastExtent.center, - geometry2: newExtent.center + from: lastExtent.center, + to: newExtent.center ) let currentExtentAvg = (lastExtent.width + lastExtent.height) / 2.0 let threshold = currentExtentAvg * 0.25 diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 4146e619a..887883528 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -203,14 +203,14 @@ class SearchViewModelTests: XCTestCase { let resultGeometryUnion: Geometry = try XCTUnwrap( GeometryEngine.union( - geometries: result.compactMap { $0.geoElement?.geometry } + of: result.compactMap { $0.geoElement?.geometry } ) ) XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.queryArea!, - geometry2: resultGeometryUnion + GeometryEngine.doesGeometry( + model.queryArea!, + contain: resultGeometryUnion ) ) @@ -256,9 +256,9 @@ class SearchViewModelTests: XCTestCase { ) var geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .portland, - point2: resultPoint, + GeometryEngine.geodeticDistance( + from: .portland, + to: resultPoint, distanceUnit: .meters, azimuthUnit: nil, curveType: .geodesic @@ -283,9 +283,9 @@ class SearchViewModelTests: XCTestCase { // Web Mercator distance between .edinburgh and first result. geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .edinburgh, - point2: resultPoint, + GeometryEngine.geodeticDistance( + from: .edinburgh, + to: resultPoint, distanceUnit: .meters, azimuthUnit: nil, curveType: .geodesic @@ -311,14 +311,14 @@ class SearchViewModelTests: XCTestCase { let resultGeometryUnion: Geometry = try XCTUnwrap( GeometryEngine.union( - geometries: result.compactMap { $0.geoElement?.geometry } + of: result.compactMap { $0.geoElement?.geometry } ) ) XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.geoViewExtent!, - geometry2: resultGeometryUnion + GeometryEngine.doesGeometry( + model.geoViewExtent!, + contain: resultGeometryUnion ) ) From 174348bbd1152c90624335a9e05142ca5bb3c380 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 19:02:17 -0800 Subject: [PATCH 0581/1722] Remove add bookmark sample feature --- Examples/Examples/BookmarksExampleView.swift | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index afae6c0e5..5774a8df5 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -16,10 +16,6 @@ import ArcGISToolkit import SwiftUI struct BookmarksExampleView: View { - /// Represents the number of bookmarks added during runtime. - @State - private var bookmarksAdded = 0 - /// A web map with predefined bookmarks. private let map = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! @@ -37,17 +33,6 @@ struct BookmarksExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .onSingleTapGesture { _, point in - let newBookmark = Bookmark( - name: "Added bookmark \(bookmarksAdded + 1)", - viewpoint: Viewpoint( - center: point, - scale: viewpoint?.targetScale ?? 10_000 - ) - ) - map.addBookmark(newBookmark) - bookmarksAdded += 1 - } .toolbar { ToolbarItem(placement: .primaryAction) { Button { From 6178368414622ae0f52559605ca503307001dbff Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 1 Mar 2022 19:03:03 -0800 Subject: [PATCH 0582/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 7890f58b3..c28e36f42 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -38,8 +38,6 @@ public struct Bookmarks: View { var onSelectionChanged: ((Bookmark) -> Void)? = nil /// A bookmark that was selected. - /// - /// Used to listen for a selection. @State private var selectedBookmark: Bookmark? = nil From 279ee67bc61f0ced3165f68aafceb528e2bdd9be Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 2 Mar 2022 10:33:27 -0800 Subject: [PATCH 0583/1722] Correct determination of sheet v popover --- Examples/Examples/BookmarksExampleView.swift | 10 ++++++++ .../Bookmarks/BookmarksHeader.swift | 25 +++++++++++++++---- .../Components/Bookmarks/BookmarksList.swift | 12 ++++----- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 5774a8df5..3ca92fb7e 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -16,6 +16,12 @@ import ArcGISToolkit import SwiftUI struct BookmarksExampleView: View { + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? + + @Environment(\.verticalSizeClass) + private var verticalSizeClass: UserInterfaceSizeClass? + /// A web map with predefined bookmarks. private let map = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! @@ -52,6 +58,8 @@ struct BookmarksExampleView: View { mapOrScene: map, viewpoint: $viewpoint ) + .environment(\.horizontalSizeClass, horizontalSizeClass) + .environment(\.verticalSizeClass, verticalSizeClass) // Display the Bookmarks view with the list of bookmarks in a map. // Bookmarks( // isPresented: $showingBookmarks, @@ -62,6 +70,8 @@ struct BookmarksExampleView: View { // .onSelectionChanged { // viewpoint = $0.viewpoint // } +// .environment(\.horizontalSizeClass, horizontalSizeClass) +// .environment(\.verticalSizeClass, verticalSizeClass) } } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift index 692dbf0ee..dcffecc61 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -15,6 +15,18 @@ import SwiftUI /// The header displayed at the top of the bookmarks menu. struct BookmarksHeader: View { + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? + + @Environment(\.verticalSizeClass) + private var verticalSizeClass: UserInterfaceSizeClass? + + /// If `true`, the bookmarks will display as sheet. + /// If `false`, the bookmarks will display as a popover. + private var isCompact: Bool { + return horizontalSizeClass == .compact || verticalSizeClass == .compact + } + /// Determines if the bookmarks list is currently shown or not. @Binding var isPresented: Bool @@ -31,11 +43,14 @@ struct BookmarksHeader: View { .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } - Spacer() - Button { - isPresented = false - } label: { - Image(systemName: "xmark.circle") + if isCompact { + Spacer() + Button { + isPresented = false + } label: { + Text("Cancel") + } + .foregroundColor(.red) } } .padding() diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index f3acee07f..52b40c959 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -25,19 +25,19 @@ struct BookmarksList: View { /// A list of bookmarks for display. var bookmarks: [Bookmark] - /// If `true`, the device is in a compact-width or compact-height orientation. - /// If `false`, the device is in a regular-width and regular-height orientation. + /// If `true`, the bookmarks will display as sheet. + /// If `false`, the bookmarks will display as a popover. private var isCompact: Bool { - horizontalSizeClass == .compact || verticalSizeClass == .compact + return horizontalSizeClass == .compact || verticalSizeClass == .compact } - /// Action to be performed when a bookmark is selected. - var onSelectionChanged: ((Bookmark) -> Void)? = nil - /// The height of the list content. @State private var listContentHeight: CGFloat = .zero + /// Action to be performed when a bookmark is selected. + var onSelectionChanged: ((Bookmark) -> Void)? = nil + /// Sets a closure to perform when the bookmark selection changes. /// - Parameters: /// - action: The closure to perform when the bookmark selection has changed. From 233be428b651f4ab9ba7bd14b7f61852620c564c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 2 Mar 2022 10:46:18 -0800 Subject: [PATCH 0584/1722] Add doc. for viewpoint --- Examples/Examples/BookmarksExampleView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 3ca92fb7e..2ca51478e 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -31,6 +31,7 @@ struct BookmarksExampleView: View { @State var showingBookmarks = false + /// Allows for communication between the Bookmarks and MapView or SceneView. @State var viewpoint: Viewpoint? = nil From d13fe477bd3ba8b6358054d8e3bd9a9b10d81654 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 2 Mar 2022 10:48:25 -0800 Subject: [PATCH 0585/1722] Mark bookmark non optional --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index c28e36f42..5bf5fecc9 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -61,13 +61,12 @@ public struct Bookmarks: View { /// binding (if provided) or calling the closure provided by the `onSelectionChanged` modifier. /// `onSelectionChanged` modifier. /// - Parameter bookmark: The bookmark that was selected. - func selectBookmark(_ bookmark: Bookmark?) { - guard bookmark != nil else { return } + func selectBookmark(_ bookmark: Bookmark) { isPresented = false if let viewpoint = viewpoint { - viewpoint.wrappedValue = bookmark!.viewpoint + viewpoint.wrappedValue = bookmark.viewpoint } else if let onSelectionChanged = onSelectionChanged { - onSelectionChanged(bookmark!) + onSelectionChanged(bookmark) } } From 8784924b7f5b0e4bee49cd86ecee21aedaa06af9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 2 Mar 2022 10:48:50 -0800 Subject: [PATCH 0586/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- 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 5bf5fecc9..a765160f6 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -73,7 +73,7 @@ public struct Bookmarks: View { /// Creates a `Bookmarks` component. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. - /// - bookmarks: A list of bookmarks. Use this when displaying bookmarks defined at run-time. + /// - bookmarks: An array of bookmarks. Use this when displaying bookmarks defined at run-time. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. /// Alternately, you can use the `.onSelectionChanged` modifier to handle bookmark selection. public init( From 0407effae6cdb0732816846eb1619064cec928ef Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 2 Mar 2022 10:49:41 -0800 Subject: [PATCH 0587/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 52b40c959..7d227e98b 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -39,8 +39,7 @@ struct BookmarksList: View { var onSelectionChanged: ((Bookmark) -> Void)? = nil /// Sets a closure to perform when the bookmark selection changes. - /// - Parameters: - /// - action: The closure to perform when the bookmark selection has changed. + /// - Parameter action: The closure to perform when the bookmark selection has changed. public func onSelectionChanged( perform action: @escaping (Bookmark) -> Void ) -> BookmarksList { From 379cebdcb17ab9a187fe7cc18f356ead0cec4b58 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 2 Mar 2022 11:31:02 -0800 Subject: [PATCH 0588/1722] Simplify loadingView --- .../Components/Bookmarks/Bookmarks.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index a765160f6..1c9647ae8 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -118,13 +118,9 @@ public struct Bookmarks: View { /// A view that is shown while a `GeoModel` is loading. private var loadingView: some View { - VStack { - Spacer() - HStack { - ProgressView() - .padding([.trailing], 5) - Text("Loading") - }.task { + ProgressView() + .padding() + .task { do { try await geoModel?.load() geoModelIsLoaded = true @@ -133,8 +129,5 @@ public struct Bookmarks: View { print(error.localizedDescription) } } - Spacer() - } - .padding() } } From eacdde04e3d687b00c061ffea9da3de3ae029385 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 2 Mar 2022 20:56:04 -0800 Subject: [PATCH 0589/1722] Rough initial bar --- Examples/Examples/ScalebarExampleView.swift | 19 +- .../Components/Scalebar/Scalebar.swift | 174 +++++++++++++++++- .../Components/Scalebar/ScalebarUnits.swift | 54 ++++++ 3 files changed, 231 insertions(+), 16 deletions(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 26274ee37..084a9f71b 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -17,18 +17,19 @@ import SwiftUI struct ScalebarExampleView: View { /// The map displayed in the map view. - private let map = Map(basemapStyle: .arcGISImagery) + private let map = Map(basemapStyle: .arcGISTopographic) @State /// Allows for communication between the Scalebar and MapView or SceneView. private var scale: Double? + @State + /// Allows for communication between the Scalebar and MapView or SceneView. + private var spatialReference: SpatialReference? + /// Allows for communication between the Scalebar and MapView or SceneView. @State - private var viewpoint: Viewpoint? = Viewpoint( - center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), - scale: 10_000 - ) + private var viewpoint: Viewpoint? /// Allows for communication between the Scalebar and MapView or SceneView. @State @@ -39,13 +40,9 @@ struct ScalebarExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .onScaleChanged { scale = $0 } + .onSpatialReferenceChanged { spatialReference = $0 } .overlay(alignment: .bottomLeading) { - Scalebar(scale, viewpoint, visibleArea) - .border(.red, width: 2) - .padding([.leading], 10) - .border(.green, width: 2) - .padding([.bottom], 30) - .border(.blue, width: 2) + Scalebar(scale, spatialReference, 175, viewpoint, visibleArea, units: .imperial) } } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 1ba148837..e29b3cfd2 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -16,9 +16,52 @@ import SwiftUI /// Displays the current scale on-screen public struct Scalebar: View { + private var alignment: ScalebarAlignment + + private var alternateFillColor = Color.black + + @State + private var displayLength: CGFloat = .zero + + @State + private var extentWidth: CGFloat = .zero + + private var fillColor = Color(uiColor: .lightGray).opacity(0.5) + + private var font: Font + + private var geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic + + private var lineColor = Color.white + + @State + private var mapLengthString: String = "none" + /// Acts as a data provider of the current scale. private var scale: Double? + private var shadowColor = Color(uiColor: .black).opacity(0.65) + + private var spatialReference: SpatialReference? + + private var style: ScalebarStyle + + private var targetWidth: Double + + private var textColor = Color.black + + private var textShadowColor = Color.white + + /// Unit of measure for the scalebar. + private var units: ScalebarUnits + + private var unitsPerPoint: Double { + (visibleArea?.extent.width ?? .zero) / extentWidth + } + + /// Allows a user to toggle geodetic calculations. + private var useGeodeticCalculations: Bool + /// Acts as a data provider of the current scale. private var viewpoint: Viewpoint? @@ -27,19 +70,140 @@ public struct Scalebar: View { public init( _ scale: Double?, + _ spatialReference: SpatialReference? = .wgs84, + _ targetWidth: Double, _ viewpoint: Viewpoint?, - _ visibleArea: Polygon? + _ visibleArea: Polygon?, + + alignment: ScalebarAlignment = .left, + font: Font = .system(size: 9.0, weight: .semibold), + style: ScalebarStyle = .line, + units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, + useGeodeticCalculations: Bool = true ) { self.scale = scale + self.targetWidth = targetWidth self.viewpoint = viewpoint self.visibleArea = visibleArea + + self.alignment = alignment + self.font = font + self.spatialReference = spatialReference + self.style = style + self.units = units + self.useGeodeticCalculations = useGeodeticCalculations } public var body: some View { - VStack { - Text(scale?.description ?? "N/A") - Text(viewpoint?.targetScale.description ?? "N/A") - Text(visibleArea?.extent.width.description ?? "N/A") + GeometryReader { geometryProxy in + VStack { + Rectangle() + .fill(.white) + .border(.red) + .frame(width: displayLength, height: 5, alignment: .leading) + Button { + updateScaleDisplay(forceRedraw: false) + } label: { + Text("Force Update") + } + Text(mapLengthString) + + } + .onChange(of: scale, perform: { newValue in + updateScaleDisplay(forceRedraw: false) + }) + .onChange(of: geometryProxy.size) { + extentWidth = $0.width + updateScaleDisplay(forceRedraw: false) + } + .onAppear { + extentWidth = geometryProxy.size.width + updateScaleDisplay(forceRedraw: false) + } + } + .frame(height: 100, alignment: .bottomLeading) + } + + internal static let labelYPad: CGFloat = 2.0 + internal static let labelXPad: CGFloat = 4.0 + internal static let tickHeight: CGFloat = 6.0 + internal static let tick2Height: CGFloat = 4.5 + internal static let notchHeight: CGFloat = 6.0 + internal static var numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + numberFormatter.formatterBehavior = .behavior10_4 + numberFormatter.maximumFractionDigits = 2 + numberFormatter.minimumFractionDigits = 0 + return numberFormatter + }() + + internal static let lineCap = CGLineCap.round + + internal var fontHeight: CGFloat = 0 + internal var zeroStringWidth: CGFloat = 0 + internal var maxRightUnitsPad: CGFloat = 0 + + /// Set a minScale if you only want the scalebar to appear when you reach a large enough scale maybe + /// something like 10_000_000. This could be useful because the scalebar is really only accurate for + /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will + /// always be visible + private let minScale: Double = 0 + + private func updateScaleDisplay(forceRedraw: Bool) { + guard let scale = scale else { + return + } + guard minScale <= 0 || scale < minScale else { + return } + guard let visibleArea = visibleArea else { + return + } + guard let sr = spatialReference else { + return + } + let totalWidthAvailable = targetWidth + + // TODO: - Removal of hardcoded sample renderer property (~16 - 2) derived from sample renderer + let maxLength = totalWidthAvailable - 16.30243742465973 - 2 + + let lineMapLength: Double + let displayUnit: LinearUnit + let mapCenter = visibleArea.extent.center + let lineDisplayLength: CGFloat + + if useGeodeticCalculations || spatialReference?.unit is AngularUnit { + let maxLengthPlanar = unitsPerPoint * Double(maxLength) + let p1 = Point(x: mapCenter.x - (maxLengthPlanar * 0.5), y: mapCenter.y, spatialReference: sr) + let p2 = Point(x: mapCenter.x + (maxLengthPlanar * 0.5), y: mapCenter.y, spatialReference: sr) + let polyline = Polyline(points: [p1, p2], spatialReference: spatialReference) + let baseUnits = units.baseUnits() + let maxLengthGeodetic = GeometryEngine.lengthGeodetic(geometry: polyline, lengthUnit: baseUnits, curveType: geodeticCurveType) + let roundNumberDistance = units.closestDistanceWithoutGoingOver(to: maxLengthGeodetic, units: baseUnits) + let planarToGeodeticFactor = maxLengthPlanar / maxLengthGeodetic + lineDisplayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) + displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) + lineMapLength = baseUnits.convert(to: displayUnit, value: roundNumberDistance) + } else { + guard let srUnit = sr.unit as? LinearUnit else { + return + } + let unitsPerPoint = unitsPerPoint + let baseUnits = units.baseUnits() + let lenAvail = srUnit.convert(to: baseUnits, value: unitsPerPoint * Double(maxLength)) + let closestLen = units.closestDistanceWithoutGoingOver(to: lenAvail, units: baseUnits) + lineDisplayLength = CGFloat(baseUnits.convert(to: srUnit, value: closestLen) / unitsPerPoint) + displayUnit = units.linearUnitsForDistance(distance: closestLen) + lineMapLength = baseUnits.convert(to: displayUnit, value: closestLen) + } + + guard lineDisplayLength.isFinite, !lineDisplayLength.isNaN else { + return + } + + displayLength = lineDisplayLength + + mapLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index e9107c7f2..37ab7d9e0 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -11,9 +11,63 @@ // See the License for the specific language governing permissions and // limitations under the License. +import ArcGIS import Foundation public enum ScalebarUnits { case imperial case metric + + internal func baseUnits() -> LinearUnit { + return self == .imperial ? LinearUnit.feet : LinearUnit.meters + } + + private static func multiplierAndMagnitudeForDistance(distance: Double) -> (multiplier: Double, magnitude: Double) { + // get multiplier + + let magnitude = pow(10, floor(log10(distance))) + let residual = distance / Double(magnitude) + let multiplier: Double = ScalebarUnits.roundNumberMultipliers.filter { $0 <= residual }.last ?? 0 + return (multiplier, magnitude) + } + + internal func closestDistanceWithoutGoingOver(to distance: Double, units: LinearUnit) -> Double { + let mm = ScalebarUnits.multiplierAndMagnitudeForDistance(distance: distance) + let roundNumber = mm.multiplier * mm.magnitude + + // because feet and miles are not relationally multiples of 10 with each other, + // we have to convert to miles if we are dealing in miles + if units == .feet { + let displayUnits = linearUnitsForDistance(distance: roundNumber) + if units != displayUnits { +// let displayDistance = closestDistanceWithoutGoingOver(to: units.convert(value: distance, to: displayUnits), units: displayUnits) + let displayDistance = closestDistanceWithoutGoingOver(to: units.convert(to: displayUnits, value: distance), units: displayUnits) +// return displayUnits.convert(displayDistance, to: units) + return displayUnits.convert(to: units, value: displayDistance) + } + } + + return roundNumber + } +// +// // this table must begin with 1 and end with 10 + private static let roundNumberMultipliers: [Double] = [1, 1.2, 1.25, 1.5, 1.75, 2, 2.4, 2.5, 3, 3.75, 4, 5, 6, 7.5, 8, 9, 10] + + internal func linearUnitsForDistance(distance: Double) -> LinearUnit { + switch self { + case .imperial: + + if distance >= 2640 { + return LinearUnit.miles + } + return LinearUnit.feet + + case .metric: + + if distance >= 1000 { + return LinearUnit.kilometers + } + return LinearUnit.meters + } + } } From 7cc0e2accd6ae80adf633800dc5381e69eb16003 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 3 Mar 2022 18:59:04 -0800 Subject: [PATCH 0590/1722] Implement `onSizeChange` --- .../FloorFilter/SiteAndFacilitySelector.swift | 22 +++++-------------- Sources/ArcGISToolkit/Extensions/View.swift | 14 ++++++++++++ .../Utility/SizePreferenceKey.swift | 17 ++++++++++++++ 3 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 640a4d17d..0422b78f6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -70,14 +70,9 @@ struct SiteAndFacilitySelector: View { .selected(floorFilterViewModel.selectedSite == site) } } - .background( - GeometryReader { geometry -> Color in - DispatchQueue.main.async { - scrollViewContentHeight = geometry.size.height - } - return .clear - } - ) + .onSizeChange { + scrollViewContentHeight = $0.height + } } .frame(maxHeight: scrollViewContentHeight) } @@ -114,14 +109,9 @@ struct SiteAndFacilitySelector: View { .selected(floorFilterViewModel.selectedFacility == facility) } } - .background( - GeometryReader { geometry -> Color in - DispatchQueue.main.async { - scrollViewContentHeight = geometry.size.height - } - return .clear - } - ) + .onSizeChange { + scrollViewContentHeight = $0.height + } } .frame(maxHeight: scrollViewContentHeight) } diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 6bd7b0bf9..d32e35fc9 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -35,6 +35,20 @@ struct SelectedModifier: ViewModifier { } extension View { + /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. + /// - Parameter perform: The closure to be executed when the content size of the receiver + /// changes. + /// - Returns: A new `View`. + func onSizeChange(perform: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometry in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometry.size) + } + ) + .onPreferenceChange(SizePreferenceKey.self, perform: perform) + } + /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. /// - Returns: The view being modified. diff --git a/Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift b/Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift new file mode 100644 index 000000000..2adfeef6a --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/SizePreferenceKey.swift @@ -0,0 +1,17 @@ +// 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 `PreferenceKey` that specifies a size. +struct SizePreferenceKey: PreferenceKey { + static let defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} +} From 221bf4ddfe0efd62981520001c049b6471618f44 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 4 Mar 2022 08:38:09 -0800 Subject: [PATCH 0591/1722] Match commented line spacing --- Examples/Examples/FloorFilterExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 55580035d..44ae22af1 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -31,8 +31,8 @@ struct FloorFilterExampleView: View { // Create the map from a portal item and assign to the mapView. // Multiple sites/facilities: Esri IST map with all buildings. - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) // Redlands Campus map. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) From 4a2e03e92d2f12a20dd9047cd1245ad58545c273 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 4 Mar 2022 08:58:56 -0800 Subject: [PATCH 0592/1722] Remove circular style Based on screen to screen comparison, the default style is `.circular` --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index a7b19dcf5..c02fc01d1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -64,7 +64,6 @@ public struct FloorFilter: View { VStack { Spacer() ProgressView() - .progressViewStyle(.circular) .padding(12) } } else { From 12265f72e57424fe4b2b75562c213488eb2883cd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 4 Mar 2022 17:11:24 -0800 Subject: [PATCH 0593/1722] Implement nav view --- .../FloorFilter/SiteAndFacilitySelector.swift | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 0422b78f6..a25f39ada 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -27,17 +27,24 @@ struct SiteAndFacilitySelector: View { /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding + + @State + var text: String = "" var body: some View { - if let selectedSite = floorFilterViewModel.selectedSite { - Facilities(facilities: selectedSite.facilities, isHidden: isHidden) - } else if floorFilterViewModel.sites.count == 1 { - Facilities( - facilities: floorFilterViewModel.sites.first!.facilities, - isHidden: isHidden - ) - } else { - Sites(sites: floorFilterViewModel.sites, isHidden: isHidden) + VStack { + TextField("Enter site name", text: $text) + .border(.gray) + if let selectedSite = floorFilterViewModel.selectedSite { + Facilities(facilities: selectedSite.facilities, isHidden: isHidden) + } else if floorFilterViewModel.sites.count == 1 { + Facilities( + facilities: floorFilterViewModel.sites.first!.facilities, + isHidden: isHidden + ) + } else { + Sites(sites: floorFilterViewModel.sites, isHidden: isHidden) + } } } @@ -53,28 +60,39 @@ struct SiteAndFacilitySelector: View { private var scrollViewContentHeight: CGFloat = .zero var body: some View { - VStack(alignment: .center) { - Header(title: "Select a site…", isHidden: isHidden) - Divider() - ScrollView { - VStack { - ForEach(sites) { site in - HStack { - Text(site.name) - Spacer() - } - .onTapGesture { - floorFilterViewModel.selection = .site(site) - } - .padding(4) - .selected(floorFilterViewModel.selectedSite == site) - } + NavigationView { + VStack { + List(sites) { (site) in + NavigationLink( + site.name, + destination: Facilities( + facilities: site.facilities, + isHidden: isHidden + ) + ) +// .onTapGesture { +// floorFilterViewModel.selection = .site(site) +// } +// .selected(floorFilterViewModel.selectedSite == site) } - .onSizeChange { - scrollViewContentHeight = $0.height + .listStyle(.plain) + NavigationLink( + "All sites", + destination: Facilities( + facilities: sites.first!.facilities, + isHidden: isHidden + ) + ) + .padding([.top, .bottom], 4) + } + .navigationBarTitle(Text("Select a site…"), displayMode: .inline) + .toolbar { + Button { + isHidden.wrappedValue.toggle() + } label: { + Image(systemName: "xmark.circle") } } - .frame(maxHeight: scrollViewContentHeight) } } } @@ -91,30 +109,20 @@ struct SiteAndFacilitySelector: View { private var scrollViewContentHeight: CGFloat = .zero var body: some View { - VStack(alignment: .leading) { - Header(title: "Select a facility…", isHidden: isHidden) - Divider() - ScrollView { - VStack { - ForEach(facilities) { facility in - HStack { - Text(facility.name) - Spacer() - } - .onTapGesture { - floorFilterViewModel.selection = .facility(facility) - isHidden.wrappedValue = true - } - .padding(4 ) - .selected(floorFilterViewModel.selectedFacility == facility) - } - } - .onSizeChange { - scrollViewContentHeight = $0.height - } + List(facilities) { facility in + Button { + print(facility.name) + floorFilterViewModel.selection = .facility(facility) + isHidden.wrappedValue = true + } label: { + Text(facility.name) + .fontWeight( + floorFilterViewModel.selectedFacility == facility ? .bold : .regular + ) } - .frame(maxHeight: scrollViewContentHeight) } + .navigationTitle("Select a facility") + .listStyle(.plain) } } From 91268322584c7d90cbcd3b326afb036267469580 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Mar 2022 12:59:56 -0800 Subject: [PATCH 0594/1722] site / facility selection, all sitea --- .../Components/FloorFilter/FloorFilter.swift | 7 ++- .../FloorFilter/SiteAndFacilitySelector.swift | 48 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index c02fc01d1..437f88f01 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -89,10 +89,9 @@ public struct FloorFilter: View { } .esriBorder() } - if !isSelectorHidden { - SiteAndFacilitySelector(isHidden: $isSelectorHidden) - .esriBorder() - } + SiteAndFacilitySelector(isHidden: $isSelectorHidden) + .esriBorder() + .opacity(isSelectorHidden ? .zero : 1) } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index a25f39ada..f4d2f2c56 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -35,12 +35,11 @@ struct SiteAndFacilitySelector: View { VStack { TextField("Enter site name", text: $text) .border(.gray) - if let selectedSite = floorFilterViewModel.selectedSite { - Facilities(facilities: selectedSite.facilities, isHidden: isHidden) - } else if floorFilterViewModel.sites.count == 1 { + if floorFilterViewModel.sites.count == 1 { Facilities( facilities: floorFilterViewModel.sites.first!.facilities, - isHidden: isHidden + isHidden: isHidden, + showSites: true ) } else { Sites(sites: floorFilterViewModel.sites, isHidden: isHidden) @@ -70,29 +69,19 @@ struct SiteAndFacilitySelector: View { isHidden: isHidden ) ) -// .onTapGesture { -// floorFilterViewModel.selection = .site(site) -// } -// .selected(floorFilterViewModel.selectedSite == site) } .listStyle(.plain) NavigationLink( "All sites", destination: Facilities( - facilities: sites.first!.facilities, - isHidden: isHidden + facilities: sites.flatMap({ $0.facilities }), + isHidden: isHidden, + showSites: true ) ) .padding([.top, .bottom], 4) } - .navigationBarTitle(Text("Select a site…"), displayMode: .inline) - .toolbar { - Button { - isHidden.wrappedValue.toggle() - } label: { - Image(systemName: "xmark.circle") - } - } + .navigationBarTitle(Text("Select a site"), displayMode: .inline) } } } @@ -100,7 +89,10 @@ struct SiteAndFacilitySelector: View { /// A view displaying the facilities contained in a `FloorManager`. struct Facilities: View { let facilities: [FloorFacility] + var isHidden: Binding + + var showSites: Bool = false @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel @@ -113,16 +105,24 @@ struct SiteAndFacilitySelector: View { Button { print(facility.name) floorFilterViewModel.selection = .facility(facility) - isHidden.wrappedValue = true + isHidden.wrappedValue.toggle() } label: { - Text(facility.name) - .fontWeight( - floorFilterViewModel.selectedFacility == facility ? .bold : .regular - ) + VStack { + Text(facility.name) + .fontWeight( + floorFilterViewModel.selectedFacility == facility ? .bold : .regular + ) + .frame(maxWidth: .infinity, alignment: .leading) + if showSites, let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + .frame(maxWidth: .infinity, alignment: .leading) + } + } } } - .navigationTitle("Select a facility") .listStyle(.plain) + .navigationBarTitle("Select a facility") } } From 903c3567bc90d0ac54e40446d045500e7300578f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Mar 2022 16:31:28 -0800 Subject: [PATCH 0595/1722] Add text filtering --- .../Examples/FloorFilterExampleView.swift | 2 +- .../Components/FloorFilter/FloorFilter.swift | 2 + .../FloorFilter/SiteAndFacilitySelector.swift | 120 ++++++++++-------- 3 files changed, 70 insertions(+), 54 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 44ae22af1..db2a2c857 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -57,7 +57,7 @@ struct FloorFilterExampleView: View { floorManager: floorManager, viewpoint: $viewpoint ) - .frame(height: 300) + .frame(maxHeight: 300) .padding(36) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 437f88f01..ae2fc8c85 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -95,6 +95,8 @@ public struct FloorFilter: View { } } } + // Ensure space for filter text field on small screens in landscape + .frame(minHeight: 100) .environmentObject(viewModel) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index f4d2f2c56..9b39f18e1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -27,14 +27,9 @@ struct SiteAndFacilitySelector: View { /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding - - @State - var text: String = "" var body: some View { VStack { - TextField("Enter site name", text: $text) - .border(.gray) if floorFilterViewModel.sites.count == 1 { Facilities( facilities: floorFilterViewModel.sites.first!.facilities, @@ -42,18 +37,36 @@ struct SiteAndFacilitySelector: View { showSites: true ) } else { - Sites(sites: floorFilterViewModel.sites, isHidden: isHidden) + Sites(isHidden: isHidden, sites: floorFilterViewModel.sites) } } } /// A view displaying the sites contained in a `FloorManager`. struct Sites: View { - let sites: [FloorSite] - var isHidden: Binding - + /// The view model used by this selector. @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + /// Allows the user to toggle the visibility of the site and facility selector. + var isHidden: Binding + + /// A subset of `sites` that contain `searchPhrase`. + var matchingSites: [FloorSite] { + if searchPhrase.isEmpty { + return sites + } + return sites.filter { floorSite in + floorSite.name.lowercased().contains(searchPhrase.lowercased()) + } + } + + /// A site filtering phrase entered by the user. + @State + var searchPhrase: String = "" + + /// Sites contained in a `FloorManager`. + let sites: [FloorSite] + /// The height of the scroll view's content. @State private var scrollViewContentHeight: CGFloat = .zero @@ -61,7 +74,8 @@ struct SiteAndFacilitySelector: View { var body: some View { NavigationView { VStack { - List(sites) { (site) in + TextField("Filter sites", text: $searchPhrase) + List(matchingSites) { (site) in NavigationLink( site.name, destination: Facilities( @@ -88,59 +102,59 @@ struct SiteAndFacilitySelector: View { /// A view displaying the facilities contained in a `FloorManager`. struct Facilities: View { + /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] + /// The view model used by this selector. + @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + + /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding - var showSites: Bool = false - - @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel - - /// The height of the scroll view's content. + /// A subset of `facilities` that contain `searchPhrase`. + var matchingFacilities: [FloorFacility] { + if searchPhrase.isEmpty { + return facilities + } + return facilities.filter { floorFacility in + floorFacility.name.lowercased().contains(searchPhrase.lowercased()) + } + } + + /// A facility filtering phrase entered by the user. @State - private var scrollViewContentHeight: CGFloat = .zero - + var searchPhrase: String = "" + + /// Indicates if site names should be shown as subtitles to the facility. + /// + /// Used when the user selects "All sites". + var showSites: Bool = false + var body: some View { - List(facilities) { facility in - Button { - print(facility.name) - floorFilterViewModel.selection = .facility(facility) - isHidden.wrappedValue.toggle() - } label: { - VStack { - Text(facility.name) - .fontWeight( - floorFilterViewModel.selectedFacility == facility ? .bold : .regular - ) - .frame(maxWidth: .infinity, alignment: .leading) - if showSites, let siteName = facility.site?.name { - Text(siteName) - .fontWeight(.ultraLight) + VStack { + TextField("Filter facilities", text: $searchPhrase) + List(matchingFacilities) { facility in + Button { + print(facility.name) + floorFilterViewModel.selection = .facility(facility) + isHidden.wrappedValue.toggle() + } label: { + VStack { + Text(facility.name) + .fontWeight( + floorFilterViewModel.selectedFacility == facility ? .bold : .regular + ) .frame(maxWidth: .infinity, alignment: .leading) + if showSites, let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + .frame(maxWidth: .infinity, alignment: .leading) + } } } } - } - .listStyle(.plain) - .navigationBarTitle("Select a facility") - } - } - - /// The header for a site or facility selector. - struct Header: View { - let title: String - var isHidden: Binding - - var body: some View { - HStack { - Text(title) - .bold() - Spacer() - Button { - isHidden.wrappedValue.toggle() - } label: { - Image(systemName: "xmark.circle") - } + .listStyle(.plain) + .navigationBarTitle("Select a facility") } } } From 9ec20862dc73f42f3f173420bd6f38aba3c39993 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 7 Mar 2022 16:46:39 -0800 Subject: [PATCH 0596/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index ae2fc8c85..00bba46d7 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -197,3 +197,11 @@ struct CollapseButton: View { )) } } + +/// Defines selection behavior. +enum AutomaticSelectionMode { + /// The selection is automatically synchronzied. + case Always + /// The selection is manually synchronzied. + case Never +} From 82ed6c6b4e317b35bb0108773b931a2afa3b5868 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Mar 2022 13:25:50 -0800 Subject: [PATCH 0597/1722] Define `AutomaticSelectionMode.AlwaysNotClearing` --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 00bba46d7..018c69b9b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -202,6 +202,8 @@ struct CollapseButton: View { enum AutomaticSelectionMode { /// The selection is automatically synchronzied. case Always + /// N/A. + case AlwaysNotClearing /// The selection is manually synchronzied. case Never } From 5af3b81a500523f9ad0c5ccde4d307248d3fe8ad Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 10 Mar 2022 13:32:54 -0800 Subject: [PATCH 0598/1722] Lowercase enum values --- .../Components/FloorFilter/FloorFilter.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 018c69b9b..d4936abf6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -31,6 +31,9 @@ public struct FloorFilter: View { viewpoint: viewpoint )) } + + /// The selection behavior of the floor filter. + var automaticSelectionMode: AutomaticSelectionMode = .always /// The view model used by the `FloorFilter`. @StateObject @@ -201,9 +204,9 @@ struct CollapseButton: View { /// Defines selection behavior. enum AutomaticSelectionMode { /// The selection is automatically synchronzied. - case Always + case always /// N/A. - case AlwaysNotClearing + case alwaysNotClearing /// The selection is manually synchronzied. - case Never + case never } From 1e87212ef2262b2078e09b5226a9faf4293b1aad Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 10:52:27 -0800 Subject: [PATCH 0599/1722] Get viewpoint changes, default to Redlands --- Examples/Examples/FloorFilterExampleView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index db2a2c857..5644a8ec6 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -20,8 +20,8 @@ struct FloorFilterExampleView: View { @State private var viewpoint = Viewpoint( - center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), - scale: 1_000_000 + center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), + scale: 100_000 ) @State @@ -50,6 +50,9 @@ struct FloorFilterExampleView: View { map: map, viewpoint: viewpoint ) + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } .overlay(alignment: .bottomLeading) { if isMapLoaded, let floorManager = map.floorManager { From a33aa628dd4c82aaf21acbc3ed316625fefab315 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 13:08:28 -0800 Subject: [PATCH 0600/1722] Initial automaticSelectionMode implementation --- .../Components/FloorFilter/FloorFilter.swift | 137 +++++++++++++++--- 1 file changed, 119 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index d4936abf6..997a02bc6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -21,23 +21,30 @@ public struct FloorFilter: View { /// Creates a `FloorFilter` /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilter`. + /// - automaticSelectionMode: The selection behavior of the floor filter. /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( floorManager: FloorManager, + automaticSelectionMode: AutomaticSelectionMode = .always, viewpoint: Binding? = nil ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint )) + self.automaticSelectionMode = automaticSelectionMode + self.viewpoint = viewpoint } /// The selection behavior of the floor filter. - var automaticSelectionMode: AutomaticSelectionMode = .always - - /// The view model used by the `FloorFilter`. - @StateObject - private var viewModel: FloorFilterViewModel + private let automaticSelectionMode: AutomaticSelectionMode + + /// 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. + private var hasLevelsToDisplay: Bool { + !(viewModel.selectedFacility == nil || + viewModel.selectedFacility!.levels.isEmpty) + } /// A Boolean value that indicates whether the site/facility selector is hidden. @State @@ -46,13 +53,14 @@ public struct FloorFilter: View { /// A Boolean value that indicates whether the levels view is currently collapsed. @State private var isLevelsViewCollapsed: Bool = false - - /// 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. - private var hasLevelsToDisplay: Bool { - !(viewModel.selectedFacility == nil || - viewModel.selectedFacility!.levels.isEmpty) - } + + /// Indicates the implicity selected facility based on the current viewpoint. + @State + private var selectedFacilityID: String? = nil + + /// Indicates the implicity selected site based on the current viewpoint. + @State + private var selectedSiteID: String? = nil /// The selected facility's levels, sorted by `level.verticalOrder`. private var sortedLevels: [FloorLevel] { @@ -60,6 +68,14 @@ public struct FloorFilter: View { $0.verticalOrder > $1.verticalOrder } ?? [] } + + /// The view model used by the `FloorFilter`. + @StateObject + private var viewModel: FloorFilterViewModel + + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. + private var viewpoint: Binding? public var body: some View { Group { @@ -92,9 +108,16 @@ public struct FloorFilter: View { } .esriBorder() } - SiteAndFacilitySelector(isHidden: $isSelectorHidden) + SiteAndFacilitySelector( + isHidden: $isSelectorHidden, + $selectedSiteID, + $selectedFacilityID + ) .esriBorder() .opacity(isSelectorHidden ? .zero : 1) + .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in + updateSelection() + } } } } @@ -102,6 +125,82 @@ public struct FloorFilter: View { .frame(minHeight: 100) .environmentObject(viewModel) } + + /// Updates `selectedFacilityID` and `selectedSiteID` based on the most recent + /// viewpoint. + private func updateSelection() { + guard let viewpoint = viewpoint?.wrappedValue, + viewpoint.targetScale != .zero, + automaticSelectionMode != .never else { + return + } + + // Only take action if viewpoint is within minimum scale. Default + // minscale is 4300 or less (~zoom level 17 or greater) + var targetScale = viewModel.floorManager.siteLayer?.minScale ?? .zero + if targetScale.isZero { + targetScale = 4300 + } + + // If viewpoint is out of range, reset selection (if not non-clearing) + // and return + if viewpoint.targetScale > targetScale { + if automaticSelectionMode == .always { + selectedSiteID = nil + selectedFacilityID = nil + } + // Assumption: if too zoomed out to see sites, also too zoomed out + // to see facilities + return + } + + // If the centerpoint is within a site's geometry, select that site. + // This code gracefully skips selection if there are no sites or no + // matching sites + let siteResult = viewModel.floorManager.sites.first { site in + guard let siteExtent = site.geometry?.extent else { + return false + } + return GeometryEngine.intersects( + geometry1: siteExtent, + geometry2: viewpoint.targetGeometry + ) + } + + if let siteResult = siteResult { + selectedSiteID = siteResult.siteId + } else if automaticSelectionMode == .always { + selectedSiteID = nil + } + + // Move on to facility selection. Default to map-authored Facility + // MinScale. If MinScale not specified or is 0, default to 1500. + targetScale = viewModel.floorManager.facilityLayer?.minScale ?? .zero + if targetScale.isZero { + targetScale = 1500 + } + + // If out of scale, stop here + if viewpoint.targetScale > targetScale { + return + } + + let facilityResult = viewModel.floorManager.facilities.first { facility in + guard let facilityExtent = facility.geometry?.extent else { + return false + } + return GeometryEngine.intersects( + geometry1: facilityExtent, + geometry2: viewpoint.targetGeometry + ) + } + + if let facilityResult = facilityResult { + selectedFacilityID = facilityResult.facilityId + } else if automaticSelectionMode == .always { + selectedFacilityID = nil + } + } } /// A view displaying the levels in the selected facility. @@ -201,12 +300,14 @@ struct CollapseButton: View { } } -/// Defines selection behavior. -enum AutomaticSelectionMode { - /// The selection is automatically synchronzied. +/// Defines automatic selection behavior. +public enum AutomaticSelectionMode { + /// Always update selection based on the current viewpoint; clear the selection when the user + /// navigates away. case always - /// N/A. + /// 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 - /// The selection is manually synchronzied. + /// Never update selection based on the GeoView's current viewpoint. case never } From 91a6e243cf8e00944ce99d1c9620375012c22a90 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 13:39:13 -0800 Subject: [PATCH 0601/1722] Initial automaticSelectionMode implementation pt 2 --- .../Components/FloorFilter/FloorFilter.swift | 4 +- .../FloorFilter/SiteAndFacilitySelector.swift | 67 +++++++++++++++---- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 997a02bc6..5bea72966 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -110,8 +110,8 @@ public struct FloorFilter: View { } SiteAndFacilitySelector( isHidden: $isSelectorHidden, - $selectedSiteID, - $selectedFacilityID + $selectedFacilityID, + $selectedSiteID ) .esriBorder() .opacity(isSelectorHidden ? .zero : 1) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 9b39f18e1..a9362e796 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -18,26 +18,50 @@ import ArcGIS struct SiteAndFacilitySelector: View { /// Creates a `SiteAndFacilitySelector` /// - Parameter isHidden: A binding used to dismiss the site selector. - init(isHidden: Binding) { + /// - Parameter selectedFacilityID: Indicates the implicity selected facility based on the + /// current viewpoint. + /// - Parameter selectedSiteID: Indicates the implicity selected site based on the current + /// viewpoint. + init( + isHidden: Binding, + _ selectedFacilityID: Binding, + _ selectedSiteID: Binding + ) { self.isHidden = isHidden + _selectedFacilityID = selectedFacilityID + _selectedSiteID = selectedSiteID } - + /// The view model used by the `SiteAndFacilitySelector`. @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding - + + /// Indicates the implicity selected facility based on the current viewpoint. + @Binding + private var selectedFacilityID: String? + + /// Indicates the implicity selected site based on the current viewpoint. + @Binding + private var selectedSiteID: String? + var body: some View { VStack { if floorFilterViewModel.sites.count == 1 { Facilities( facilities: floorFilterViewModel.sites.first!.facilities, isHidden: isHidden, + selectedFacilityID: $selectedFacilityID, showSites: true ) } else { - Sites(isHidden: isHidden, sites: floorFilterViewModel.sites) + Sites( + isHidden: isHidden, + selectedFacilityID: $selectedFacilityID, + selectedSiteID: $selectedSiteID, + sites: floorFilterViewModel.sites + ) } } } @@ -64,6 +88,14 @@ struct SiteAndFacilitySelector: View { @State var searchPhrase: String = "" + /// Indicates the implicity selected facility based on the current viewpoint. + @Binding + var selectedFacilityID: String? + + /// Indicates the implicity selected site based on the current viewpoint. + @Binding + var selectedSiteID: String? + /// Sites contained in a `FloorManager`. let sites: [FloorSite] @@ -78,21 +110,24 @@ struct SiteAndFacilitySelector: View { List(matchingSites) { (site) in NavigationLink( site.name, - destination: Facilities( - facilities: site.facilities, - isHidden: isHidden - ) - ) + tag: site.siteId, + selection: $selectedSiteID) { + Facilities( + facilities: site.facilities, + isHidden: isHidden, + selectedFacilityID: $selectedFacilityID + ) + } } .listStyle(.plain) - NavigationLink( - "All sites", - destination: Facilities( + NavigationLink("All sites") { + Facilities( facilities: sites.flatMap({ $0.facilities }), isHidden: isHidden, + selectedFacilityID: $selectedFacilityID, showSites: true ) - ) + } .padding([.top, .bottom], 4) } .navigationBarTitle(Text("Select a site"), displayMode: .inline) @@ -125,6 +160,10 @@ struct SiteAndFacilitySelector: View { @State var searchPhrase: String = "" + /// Indicates the implicity selected facility based on the current viewpoint. + @Binding + var selectedFacilityID: String? + /// Indicates if site names should be shown as subtitles to the facility. /// /// Used when the user selects "All sites". @@ -142,7 +181,7 @@ struct SiteAndFacilitySelector: View { VStack { Text(facility.name) .fontWeight( - floorFilterViewModel.selectedFacility == facility ? .bold : .regular + selectedFacilityID == facility.facilityId ? .bold : .regular ) .frame(maxWidth: .infinity, alignment: .leading) if showSites, let siteName = facility.site?.name { From 52e919633839db8ed38c71881cc7f35e4b3c5b94 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 13:51:08 -0800 Subject: [PATCH 0602/1722] Improve selection clarity --- .../FloorFilter/SiteAndFacilitySelector.swift | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index a9362e796..563f90854 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -178,16 +178,19 @@ struct SiteAndFacilitySelector: View { floorFilterViewModel.selection = .facility(facility) isHidden.wrappedValue.toggle() } label: { - VStack { - Text(facility.name) - .fontWeight( - selectedFacilityID == facility.facilityId ? .bold : .regular - ) - .frame(maxWidth: .infinity, alignment: .leading) - if showSites, let siteName = facility.site?.name { - Text(siteName) - .fontWeight(.ultraLight) + HStack { + Image( + systemName: selectedFacilityID == facility.facilityId ? "circle.fill" : "circle" + ) + VStack { + Text(facility.name) + .fontWeight(.regular) .frame(maxWidth: .infinity, alignment: .leading) + if showSites, let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + .frame(maxWidth: .infinity, alignment: .leading) + } } } } From 22201df8f0102cfbab6347096ae7f7a3f5f244ae Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 13:59:55 -0800 Subject: [PATCH 0603/1722] Hides sites browser if only 1 site defined --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 563f90854..610f2f8b6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -197,6 +197,9 @@ struct SiteAndFacilitySelector: View { } .listStyle(.plain) .navigationBarTitle("Select a facility") + .navigationBarBackButtonHidden( + floorFilterViewModel.sites.count == 1 + ) } } } From 3ee9f36de1d4da65b943077d14e0b46480ddb9e6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 14:02:38 -0800 Subject: [PATCH 0604/1722] Remove unneeded --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 610f2f8b6..563f90854 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -197,9 +197,6 @@ struct SiteAndFacilitySelector: View { } .listStyle(.plain) .navigationBarTitle("Select a facility") - .navigationBarBackButtonHidden( - floorFilterViewModel.sites.count == 1 - ) } } } From 2feae7438a07de3c7c73e9567747b868b76bf1c6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 17:12:47 -0800 Subject: [PATCH 0605/1722] Scroll to selected facility --- .../FloorFilter/SiteAndFacilitySelector.swift | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 563f90854..651d7eebc 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -171,33 +171,52 @@ struct SiteAndFacilitySelector: View { var body: some View { VStack { + if !showSites, let siteName = facilities.first?.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + } TextField("Filter facilities", text: $searchPhrase) - List(matchingFacilities) { facility in - Button { - print(facility.name) - floorFilterViewModel.selection = .facility(facility) - isHidden.wrappedValue.toggle() - } label: { - HStack { - Image( - systemName: selectedFacilityID == facility.facilityId ? "circle.fill" : "circle" - ) - VStack { - Text(facility.name) - .fontWeight(.regular) - .frame(maxWidth: .infinity, alignment: .leading) - if showSites, let siteName = facility.site?.name { - Text(siteName) - .fontWeight(.ultraLight) - .frame(maxWidth: .infinity, alignment: .leading) + ScrollViewReader { proxy in + List(matchingFacilities, id: \.facilityId) { facility in + Button { + print(facility.name) + floorFilterViewModel.selection = .facility(facility) + isHidden.wrappedValue.toggle() + } label: { + HStack { + Image( + systemName: selectedFacilityID == facility.facilityId ? "circle.fill" : "circle" + ) + VStack { + Text(facility.name) + .fontWeight(.regular) + .frame(maxWidth: .infinity, alignment: .leading) + if showSites, let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + .frame(maxWidth: .infinity, alignment: .leading) + } + } } } } - } + .listStyle(.plain) + .onChange(of: selectedFacilityID) { facilityID in + guard let fac = floorFilterViewModel.facilities.first(where: { facility in + facility.facilityId == facilityID + }) else { + return + } + withAnimation { + proxy.scrollTo( + fac.facilityId, + anchor: .center + ) + } + } } - .listStyle(.plain) - .navigationBarTitle("Select a facility") } + .navigationBarTitle("Select a facility") } } } From 5adc781508a70ac8de59577a351388eafbc9a8cf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 11 Mar 2022 17:14:10 -0800 Subject: [PATCH 0606/1722] Refactor --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 651d7eebc..b6b3248af 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -202,14 +202,14 @@ struct SiteAndFacilitySelector: View { } .listStyle(.plain) .onChange(of: selectedFacilityID) { facilityID in - guard let fac = floorFilterViewModel.facilities.first(where: { facility in + guard let facility = floorFilterViewModel.facilities.first(where: { facility in facility.facilityId == facilityID }) else { return } withAnimation { proxy.scrollTo( - fac.facilityId, + facility.facilityId, anchor: .center ) } From 9bc4d2927b2385c359b16da29d762703e53afeb2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 14 Mar 2022 11:36:31 -0700 Subject: [PATCH 0607/1722] 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 b6b3248af..8ade5a47b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -132,6 +132,7 @@ struct SiteAndFacilitySelector: View { } .navigationBarTitle(Text("Select a site"), displayMode: .inline) } + .navigationViewStyle(.stack) } } From a7457720e9608a9aa4ecc92abd9564850b6fa9b1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 14 Mar 2022 11:54:33 -0700 Subject: [PATCH 0608/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 8ade5a47b..57013e5f3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -107,6 +107,8 @@ struct SiteAndFacilitySelector: View { NavigationView { VStack { TextField("Filter sites", text: $searchPhrase) + .keyboardType(.alphabet) + .disableAutocorrection(true) List(matchingSites) { (site) in NavigationLink( site.name, @@ -177,44 +179,46 @@ struct SiteAndFacilitySelector: View { .fontWeight(.ultraLight) } TextField("Filter facilities", text: $searchPhrase) - ScrollViewReader { proxy in - List(matchingFacilities, id: \.facilityId) { facility in - Button { - print(facility.name) - floorFilterViewModel.selection = .facility(facility) - isHidden.wrappedValue.toggle() - } label: { - HStack { - Image( - systemName: selectedFacilityID == facility.facilityId ? "circle.fill" : "circle" - ) - VStack { - Text(facility.name) - .fontWeight(.regular) + .keyboardType(.alphabet) + .disableAutocorrection(true) + ScrollViewReader { proxy in + List(matchingFacilities, id: \.facilityId) { facility in + Button { + print(facility.name) + floorFilterViewModel.selection = .facility(facility) + isHidden.wrappedValue.toggle() + } label: { + HStack { + Image( + systemName: selectedFacilityID == facility.facilityId ? "circle.fill" : "circle" + ) + VStack { + Text(facility.name) + .fontWeight(.regular) + .frame(maxWidth: .infinity, alignment: .leading) + if showSites, let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) .frame(maxWidth: .infinity, alignment: .leading) - if showSites, let siteName = facility.site?.name { - Text(siteName) - .fontWeight(.ultraLight) - .frame(maxWidth: .infinity, alignment: .leading) - } } } } } - .listStyle(.plain) - .onChange(of: selectedFacilityID) { facilityID in - guard let facility = floorFilterViewModel.facilities.first(where: { facility in - facility.facilityId == facilityID - }) else { - return - } - withAnimation { - proxy.scrollTo( - facility.facilityId, - anchor: .center - ) - } + } + .listStyle(.plain) + .onChange(of: selectedFacilityID) { facilityID in + guard let facility = floorFilterViewModel.facilities.first(where: { facility in + facility.facilityId == facilityID + }) else { + return } + withAnimation { + proxy.scrollTo( + facility.facilityId, + anchor: .center + ) + } + } } } .navigationBarTitle("Select a facility") From 904bc87f56983cb3397dfeb19412404e5accd3f1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 14 Mar 2022 16:45:00 -0700 Subject: [PATCH 0609/1722] Update Result.swift --- Sources/ArcGISToolkit/Extensions/Result.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index 0503e5195..d4e837b1e 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -12,6 +12,11 @@ // limitations under the License. extension Result where Failure == Error { + /// Creates a result based on the outcome of the given task. If the task + /// succeeds, the result is `success`. If the task fails, the result is + /// `failure`. + /// + /// Returns `nil` in the event that the task was cancelled. init?(awaiting task: () async throws -> Success) async { do { self = .success(try await task()) From 8ea506942150dcc6eaf8b056175c5a449f0e4faa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 14 Mar 2022 20:26:03 -0700 Subject: [PATCH 0610/1722] Apply PR suggesionts --- .../Examples/FloorFilterExampleView.swift | 89 +++++++------ .../Components/FloorFilter/FloorFilter.swift | 63 ++++------ .../FloorFilter/FloorFilterViewModel.swift | 119 ++++++++++-------- .../FloorFilter/SiteAndFacilitySelector.swift | 38 +++--- Sources/ArcGISToolkit/Extensions/Button.swift | 12 +- Sources/ArcGISToolkit/Extensions/View.swift | 15 +-- 6 files changed, 180 insertions(+), 156 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 55580035d..a42bead69 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -16,58 +16,67 @@ import ArcGISToolkit import ArcGIS struct FloorFilterExampleView: View { - private let map: Map - - @State - private var viewpoint = Viewpoint( - center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), - scale: 1_000_000 - ) - - @State - private var isMapLoaded: Bool = false - - init() { - // Create the map from a portal item and assign to the mapView. - + /// Make a map from a portal item. + static func makeMap() -> Map { // Multiple sites/facilities: Esri IST map with all buildings. - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) + // Redlands Campus map. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities // Single site (ESRI Redlands Main) and facility (Building L). -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - map = Map(item: portalItem) + return Map(item: portalItem) } - + + private var map = makeMap() + + @State + private var mapLoadResult: Result? + + @State + private var viewpoint = Viewpoint( + center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), + scale: 100_000 + ) + var body: some View { - MapView( - map: map, - viewpoint: viewpoint - ) - .overlay(alignment: .bottomLeading) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - floorManager: floorManager, - viewpoint: $viewpoint + Group { + if let mapLoadResult = mapLoadResult { + switch mapLoadResult { + case .success(let value): + MapView( + map: value, + viewpoint: viewpoint ) - .frame(height: 300) - .padding(36) + case .failure(let error): + Text("Error loading map: \(error.localizedDescription)") + .padding() } + } else { + ProgressView() } - .task { - do { - try await map.load() - isMapLoaded = true - } catch { - print("load error: \(error)") - } + } + .task { + mapLoadResult = await Result { + try await map.load() + return map + } + } + .overlay(alignment: .bottomLeading) { + if map.loadStatus == .loaded, + let floorManager = map.floorManager { + FloorFilter( + floorManager: floorManager, + viewpoint: $viewpoint + ) + .frame(height: 300) + .padding(36) } + } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index c02fc01d1..a08aeea86 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -18,7 +18,7 @@ import ArcGIS /// 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 { - /// Creates a `FloorFilter` + /// Creates a `FloorFilter`. /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilter`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. @@ -31,33 +31,19 @@ public struct FloorFilter: View { viewpoint: viewpoint )) } - + /// The view model used by the `FloorFilter`. @StateObject private var viewModel: FloorFilterViewModel - - /// A Boolean value that indicates whether the site/facility selector is hidden. + + /// A Boolean value that indicates whether the site or facility selector is hidden. @State private var isSelectorHidden: Bool = true - + /// A Boolean value that indicates whether the levels view is currently collapsed. @State private var isLevelsViewCollapsed: Bool = false - - /// 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. - private var hasLevelsToDisplay: Bool { - !(viewModel.selectedFacility == nil || - viewModel.selectedFacility!.levels.isEmpty) - } - - /// The selected facility's levels, sorted by `level.verticalOrder`. - private var sortedLevels: [FloorLevel] { - viewModel.selectedFacility?.levels.sorted() { - $0.verticalOrder > $1.verticalOrder - } ?? [] - } - + public var body: some View { Group { if viewModel.isLoading { @@ -71,9 +57,9 @@ public struct FloorFilter: View { VStack { Spacer() VStack { - if hasLevelsToDisplay { + if viewModel.hasLevelsToDisplay { LevelsView( - levels: sortedLevels, + levels: viewModel.sortedLevels, isCollapsed: $isLevelsViewCollapsed ) Divider() @@ -104,20 +90,21 @@ public struct FloorFilter: View { struct LevelsView: View { /// The levels to display. let levels: [FloorLevel] - - /// 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. + + /// A Boolean value that indicates whether the view shows only the selected level or all levels. + /// If the value is `false`, the view will display all levels. Otherwise, display only the + /// selected level. @Binding var isCollapsed: Bool - + /// The view model used by the `LevelsView`. - @EnvironmentObject var viewModel: FloorFilterViewModel - + @EnvironmentObject + var viewModel: FloorFilterViewModel + /// The height of the scroll view's content. @State private var scrollViewContentHeight: CGFloat = .zero - + public var body: some View { VStack { if !isCollapsed, @@ -137,8 +124,7 @@ struct LevelsView: View { ) } .frame(maxHeight: scrollViewContentHeight) - } - else { + } else { // Button for the selected level. Button { if levels.count > 1 { @@ -147,7 +133,7 @@ struct LevelsView: View { } label: { Text(viewModel.selectedLevel?.shortName ?? (levels.first?.shortName ?? "None")) } - .buttonSelected(true) + .selected(true) .padding(4) } } @@ -157,10 +143,11 @@ struct LevelsView: View { /// A vertical list of floor levels. struct LevelsStack: View { let levels: [FloorLevel] - + /// The view model used by the `LevelsView`. - @EnvironmentObject var viewModel: FloorFilterViewModel - + @EnvironmentObject + var viewModel: FloorFilterViewModel + var body: some View { VStack { ForEach(levels) { level in @@ -169,7 +156,7 @@ struct LevelsStack: View { } label: { Text(level.shortName) } - .buttonSelected(level == viewModel.selectedLevel) + .selected(level == viewModel.selectedLevel) .padding(4) } } @@ -181,7 +168,7 @@ struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. @Binding var isCollapsed: Bool - + var body: some View { Button { isCollapsed.toggle() diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 3997ab872..8c985b45d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -26,7 +26,7 @@ final class FloorFilterViewModel: ObservableObject { /// A selected level. case level(FloorLevel) } - + /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilterViewModel`. @@ -41,63 +41,44 @@ final class FloorFilterViewModel: ObservableObject { Task { do { try await floorManager.load() - if sites.count == 1 { + if sites.count == 1, let firstSite = sites.first { // If we have only one site, select it. - selection = .site(sites.first!) + selection = .site(firstSite) } } catch { + // Note: Should user get to know about this error? print("error: \(error)") } isLoading = false } } - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// The `Viewpoint` used to pan/zoom to the selected site or facility. /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding? - + /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager - + + /// A Boolean value that indicates whether the model is loading. + @Published + private(set) var isLoading = true + /// The floor manager sites. var sites: [FloorSite] { floorManager.sites } - + /// The floor manager facilities. var facilities: [FloorFacility] { floorManager.facilities } - + /// The floor manager levels. var levels: [FloorLevel] { floorManager.levels } - /// `true` if the model is loading it's properties, `false` if not loading. - @Published - private(set) var isLoading = true - - /// Gets the default level for a facility. - /// - Parameter facility: The facility to get the default level for. - /// - Returns: The default level for the facility, which is the level with vertical order 0; - /// if there's no level with vertical order of 0, it returns the lowest level. - func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { - return levels.first(where: { level in - level.facility == facility && level.verticalOrder == .zero - }) ?? lowestLevel() - } - - /// Returns the level with the lowest vertical order. - private func lowestLevel() -> FloorLevel? { - let sortedLevels = levels.sorted { - $0.verticalOrder < $1.verticalOrder - } - return sortedLevels.first { - $0.verticalOrder != .min && $0.verticalOrder != .max - } - } - /// The selected site, floor, or level. @Published var selection: Selection? { @@ -111,13 +92,13 @@ final class FloorFilterViewModel: ObservableObject { zoomToSelection() } } - + /// The selected site. var selectedSite: FloorSite? { guard let selection = selection else { return nil } - + switch selection { case .site(let site): return site @@ -127,13 +108,13 @@ final class FloorFilterViewModel: ObservableObject { return level.facility?.site } } - + /// The selected facility. var selectedFacility: FloorFacility? { guard let selection = selection else { return nil } - + switch selection { case .site: return nil @@ -143,7 +124,7 @@ final class FloorFilterViewModel: ObservableObject { return level.facility } } - + /// The selected level. var selectedLevel: FloorLevel? { if case let .level(level) = selection { @@ -152,29 +133,48 @@ final class FloorFilterViewModel: ObservableObject { return nil } } - + + /// 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 { + guard let selectedFacility = selectedFacility else { + return false + } + return !selectedFacility.levels.isEmpty + } + + /// The selected facility's levels, sorted by `level.verticalOrder`. + var sortedLevels: [FloorLevel] { + selectedFacility?.levels.sorted() { + $0.verticalOrder > $1.verticalOrder + } ?? [] + } + + // Mark: Private Functions + /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. - func zoomToSelection() { + private func zoomToSelection() { guard let selection = selection else { return } - + switch selection { case .site(let site): - zoomToExtent(extent: site.geometry?.extent) + zoomToExtent(site.geometry?.extent) case .facility(let facility): - zoomToExtent(extent: facility.geometry?.extent) + zoomToExtent(facility.geometry?.extent) case .level(let level): - zoomToExtent(extent: level.facility?.geometry?.extent) + zoomToExtent(level.facility?.geometry?.extent) } } - - private func zoomToExtent(extent: Envelope?) { + + /// Zoom to given extent. + private func zoomToExtent(_ extent: Envelope?) { // Make sure we have an extent and viewpoint to zoom to. guard let extent = extent, let viewpoint = viewpoint else { return } - + let builder = EnvelopeBuilder(envelope: extent) builder.expand(factor: 1.5) let targetExtent = builder.toGeometry() @@ -184,12 +184,33 @@ final class FloorFilterViewModel: ObservableObject { ) } } - - /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. - public func filterMapToSelectedLevel() { + + /// Sets the visibility of all the levels on the map based on the vertical order of the current + /// selected level. + private func filterMapToSelectedLevel() { guard let selectedLevel = selectedLevel else { return } levels.forEach { $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder } } + + /// Gets the default level for a facility. + /// - Parameter facility: The facility to get the default level for. + /// - Returns: The default level for the facility, which is the level with vertical order 0; + /// if there's no level with vertical order of 0, it returns the lowest level. + private func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { + return levels.first(where: { level in + level.facility == facility && level.verticalOrder == .zero + }) ?? lowestLevel() + } + + /// Returns the level with the lowest vertical order. + private func lowestLevel() -> FloorLevel? { + let sortedLevels = levels.sorted { + $0.verticalOrder < $1.verticalOrder + } + return sortedLevels.first { + $0.verticalOrder != .min && $0.verticalOrder != .max + } + } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 0422b78f6..93e2474fe 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -16,18 +16,19 @@ import ArcGIS /// A view which allows selection of sites and facilities represented in a `FloorManager`. struct SiteAndFacilitySelector: View { - /// Creates a `SiteAndFacilitySelector` + /// Creates a `SiteAndFacilitySelector`. /// - Parameter isHidden: A binding used to dismiss the site selector. init(isHidden: Binding) { self.isHidden = isHidden } - + /// The view model used by the `SiteAndFacilitySelector`. - @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel - + @EnvironmentObject + var floorFilterViewModel: FloorFilterViewModel + /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding - + var body: some View { if let selectedSite = floorFilterViewModel.selectedSite { Facilities(facilities: selectedSite.facilities, isHidden: isHidden) @@ -40,13 +41,18 @@ struct SiteAndFacilitySelector: View { Sites(sites: floorFilterViewModel.sites, isHidden: isHidden) } } - + /// A view displaying the sites contained in a `FloorManager`. struct Sites: View { + /// The floor sites. let sites: [FloorSite] + + /// Allows the user to toggle the visibility of the sites. var isHidden: Binding - - @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + + /// The view model used by the `Sites`. + @EnvironmentObject + var floorFilterViewModel: FloorFilterViewModel /// The height of the scroll view's content. @State @@ -54,7 +60,7 @@ struct SiteAndFacilitySelector: View { var body: some View { VStack(alignment: .center) { - Header(title: "Select a site…", isHidden: isHidden) + Header(title: "Select a site", isHidden: isHidden) Divider() ScrollView { VStack { @@ -78,21 +84,21 @@ struct SiteAndFacilitySelector: View { } } } - + /// A view displaying the facilities contained in a `FloorManager`. struct Facilities: View { let facilities: [FloorFacility] var isHidden: Binding - + @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel - + /// The height of the scroll view's content. @State private var scrollViewContentHeight: CGFloat = .zero - + var body: some View { VStack(alignment: .leading) { - Header(title: "Select a facility…", isHidden: isHidden) + Header(title: "Select a facility", isHidden: isHidden) Divider() ScrollView { VStack { @@ -117,12 +123,12 @@ struct SiteAndFacilitySelector: View { } } } - + /// The header for a site or facility selector. struct Header: View { let title: String var isHidden: Binding - + var body: some View { HStack { Text(title) diff --git a/Sources/ArcGISToolkit/Extensions/Button.swift b/Sources/ArcGISToolkit/Extensions/Button.swift index 28b5c5214..ed7cd9f21 100644 --- a/Sources/ArcGISToolkit/Extensions/Button.swift +++ b/Sources/ArcGISToolkit/Extensions/Button.swift @@ -13,13 +13,13 @@ import SwiftUI -/// A modifier which "selects" a button. If selected the button will display with the -/// `BorderedProminentButtonStyle`. If not selected, the button will display with the +/// A modifier which "selects" a button. If selected, the button will be displayed with the +/// `BorderedProminentButtonStyle`. Otherwise, the button will be displayed with the /// `BorderedButtonStyle`. struct ButtonSelectedModifier: ViewModifier { - /// `true` if the view should display as selected, `false` otherwise. + /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool - + func body(content: Content) -> some View { if isSelected { content @@ -34,8 +34,8 @@ struct ButtonSelectedModifier: ViewModifier { extension Button { /// Button modifier used to denote the button is selected. /// - Parameter isSelected: `true` if the button is selected, `false` otherwise. - /// - Returns: The button being modified. - func buttonSelected( + /// - Returns: The modified button. + func selected( _ isSelected: Bool = false ) -> some View { modifier(ButtonSelectedModifier(isSelected: isSelected)) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index d32e35fc9..8b5b4e39a 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -15,17 +15,16 @@ import SwiftUI /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { - /// `true` if the view should display as selected, `false` otherwise. + /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool - + func body(content: Content) -> some View { - let roundedRect = RoundedRectangle(cornerRadius: 4) if isSelected { content .background(Color.secondary.opacity(0.8)) - .clipShape(roundedRect) + .clipShape(RoundedRectangle(cornerRadius: 4)) .shadow( - color: Color.secondary.opacity(0.8), + color: .secondary.opacity(0.8), radius: 2 ) } else { @@ -43,7 +42,9 @@ extension View { background( GeometryReader { geometry in Color.clear - .preference(key: SizePreferenceKey.self, value: geometry.size) + .preference( + key: SizePreferenceKey.self, value: geometry.size + ) } ) .onPreferenceChange(SizePreferenceKey.self, perform: perform) @@ -51,7 +52,7 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: The view being modified. + /// - Returns: The modified view. func selected( _ isSelected: Bool = false ) -> some View { From 38cc0462bf4847c63199d3a397ff5550749f7922 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 10:36:11 -0700 Subject: [PATCH 0611/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 5644a8ec6..47af3c6f2 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -21,7 +21,7 @@ struct FloorFilterExampleView: View { @State private var viewpoint = Viewpoint( center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), - scale: 100_000 + scale: 10_000 ) @State @@ -60,7 +60,7 @@ struct FloorFilterExampleView: View { floorManager: floorManager, viewpoint: $viewpoint ) - .frame(maxHeight: 300) + .frame(maxWidth: 300, maxHeight: 300) .padding(36) } } From 46be9e8d27831eb7ed03cf3828ddea7c4b92abc1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 11:05:43 -0700 Subject: [PATCH 0612/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 47af3c6f2..9d8737f1f 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -27,6 +27,8 @@ struct FloorFilterExampleView: View { @State private var isMapLoaded: Bool = false + private let filterAlignment = Alignment.bottomLeading + init() { // Create the map from a portal item and assign to the mapView. @@ -53,10 +55,11 @@ struct FloorFilterExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .overlay(alignment: .bottomLeading) { + .overlay(alignment: filterAlignment) { if isMapLoaded, let floorManager = map.floorManager { FloorFilter( + alignment: filterAlignment, floorManager: floorManager, viewpoint: $viewpoint ) From 094c6dbd7a7af44744513700260ba7323a651667 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 11:05:51 -0700 Subject: [PATCH 0613/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 109 +++++++++++++----- 1 file changed, 77 insertions(+), 32 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 5bea72966..d91403360 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -24,18 +24,23 @@ public struct FloorFilter: View { /// - automaticSelectionMode: The selection behavior of the floor filter. /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( - floorManager: FloorManager, + alignment: Alignment, automaticSelectionMode: AutomaticSelectionMode = .always, + floorManager: FloorManager, viewpoint: Binding? = nil ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint )) + self.alignment = alignment self.automaticSelectionMode = automaticSelectionMode self.viewpoint = viewpoint } + /// The alignment configuration. + private let alignment: Alignment + /// The selection behavior of the floor filter. private let automaticSelectionMode: AutomaticSelectionMode @@ -54,6 +59,46 @@ public struct FloorFilter: View { @State private var isLevelsViewCollapsed: Bool = false + /// A view that allows selecting between levels. + private var levelSelectorView: some View { + VStack { + if !topAligned { + Spacer() + } + VStack { + if hasLevelsToDisplay { + LevelsView( + levels: sortedLevels, + isCollapsed: $isLevelsViewCollapsed + ) + Divider() + .frame(width: 30) + } + // Site button. + Button { + isSelectorHidden.toggle() + } label: { + Image(systemName: "building.2") + } + .padding(4) + } + .esriBorder() + if topAligned { + Spacer() + } + } + } + + /// Indicates that the selector should be presented with a right oriented aligment configuration. + private var rightAligned: Bool { + switch alignment { + case .topTrailing, .trailing, .bottomTrailing: + return true + default: + return false + } + } + /// Indicates the implicity selected facility based on the current viewpoint. @State private var selectedFacilityID: String? = nil @@ -61,6 +106,20 @@ public struct FloorFilter: View { /// Indicates the implicity selected site based on the current viewpoint. @State private var selectedSiteID: String? = nil + + /// A configured SiteAndFacilitySelector view. + private var siteAndFacilitySelectorView: some View { + SiteAndFacilitySelector( + isHidden: $isSelectorHidden, + $selectedFacilityID, + $selectedSiteID + ) + .esriBorder() + .opacity(isSelectorHidden ? .zero : 1) + .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in + updateSelection() + } + } /// The selected facility's levels, sorted by `level.verticalOrder`. private var sortedLevels: [FloorLevel] { @@ -69,6 +128,16 @@ public struct FloorFilter: View { } ?? [] } + /// Indicates that the selector should be presented with a top oriented aligment configuration. + private var topAligned: Bool { + switch alignment { + case .topLeading, .top, .topTrailing: + return true + default: + return false + } + } + /// The view model used by the `FloorFilter`. @StateObject private var viewModel: FloorFilterViewModel @@ -76,7 +145,7 @@ public struct FloorFilter: View { /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. private var viewpoint: Binding? - + public var body: some View { Group { if viewModel.isLoading { @@ -87,37 +156,13 @@ public struct FloorFilter: View { } } else { HStack(alignment: .bottom) { - VStack { - Spacer() - VStack { - if hasLevelsToDisplay { - LevelsView( - levels: sortedLevels, - isCollapsed: $isLevelsViewCollapsed - ) - Divider() - .frame(width: 30) - } - // Site button. - Button { - isSelectorHidden.toggle() - } label: { - Image(systemName: "building.2") - } - .padding(4) - } - .esriBorder() + if rightAligned { + siteAndFacilitySelectorView + levelSelectorView + } else { + levelSelectorView + siteAndFacilitySelectorView } - SiteAndFacilitySelector( - isHidden: $isSelectorHidden, - $selectedFacilityID, - $selectedSiteID - ) - .esriBorder() - .opacity(isSelectorHidden ? .zero : 1) - .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in - updateSelection() - } } } } From ca295f2059816d9b00429f2954834c53eb31cf92 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 11:08:23 -0700 Subject: [PATCH 0614/1722] Update FloorFilter.swift --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index d91403360..7f544c46c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -20,8 +20,9 @@ import ArcGIS public struct FloorFilter: View { /// Creates a `FloorFilter` /// - Parameters: - /// - floorManager: The floor manager used by the `FloorFilter`. + /// - alignment: Determines the display configuration of Floor Filter elements. /// - automaticSelectionMode: The selection behavior of the floor filter. + /// - floorManager: The floor manager used by the `FloorFilter`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( alignment: Alignment, From a72d7d337f6a9e07e37643eed5e8b94d6ab16362 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 12:39:55 -0700 Subject: [PATCH 0615/1722] Update FloorFilter.swift --- 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 7f544c46c..83777488a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -108,7 +108,7 @@ public struct FloorFilter: View { @State private var selectedSiteID: String? = nil - /// A configured SiteAndFacilitySelector view. + /// A configured `SiteAndFacilitySelector` view. private var siteAndFacilitySelectorView: some View { SiteAndFacilitySelector( isHidden: $isSelectorHidden, From e7b380cbee5687e7617221eb62d31d078e642621 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 12:40:00 -0700 Subject: [PATCH 0616/1722] Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 57013e5f3..83f836347 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -120,6 +120,10 @@ struct SiteAndFacilitySelector: View { selectedFacilityID: $selectedFacilityID ) } + .onTapGesture { + selectedSiteID = site.siteId + floorFilterViewModel.selection = .site(site) + } } .listStyle(.plain) NavigationLink("All sites") { From 64e68199587e7b153f155add9daa027618784bc9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 12:50:58 -0700 Subject: [PATCH 0617/1722] Update FloorFilter.swift --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 83777488a..aee11b0a3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -333,7 +333,9 @@ struct CollapseButton: View { var body: some View { Button { - isCollapsed.toggle() + withAnimation { + isCollapsed.toggle() + } } label: { Image(systemName: "xmark") } From bed55d2b6873450c6a699c997d9f73f6cf08b9d9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 13:00:42 -0700 Subject: [PATCH 0618/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index a08aeea86..400a612bc 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -44,6 +44,13 @@ public struct FloorFilter: View { @State private var isLevelsViewCollapsed: Bool = false + /// The selected facility's levels, sorted by `level.verticalOrder`. + private var sortedLevels: [FloorLevel] { + viewModel.selectedFacility?.levels.sorted() { + $0.verticalOrder > $1.verticalOrder + } ?? [] + } + public var body: some View { Group { if viewModel.isLoading { @@ -59,7 +66,7 @@ public struct FloorFilter: View { VStack { if viewModel.hasLevelsToDisplay { LevelsView( - levels: viewModel.sortedLevels, + levels: sortedLevels, isCollapsed: $isLevelsViewCollapsed ) Divider() From ea6f26e4271a99b4a4ff79f517f342ecb1f4c030 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 13:00:46 -0700 Subject: [PATCH 0619/1722] Update FloorFilterViewModel.swift --- .../Components/FloorFilter/FloorFilterViewModel.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 8c985b45d..b0c954446 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -143,13 +143,6 @@ final class FloorFilterViewModel: ObservableObject { return !selectedFacility.levels.isEmpty } - /// The selected facility's levels, sorted by `level.verticalOrder`. - var sortedLevels: [FloorLevel] { - selectedFacility?.levels.sorted() { - $0.verticalOrder > $1.verticalOrder - } ?? [] - } - // Mark: Private Functions /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. From 9521456f128e0537e4ae8a2d431c79cc0615789e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 13:06:32 -0700 Subject: [PATCH 0620/1722] Update FloorFilter.swift --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index aee11b0a3..1b440cf65 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -291,7 +291,9 @@ struct LevelsView: View { // Button for the selected level. Button { if levels.count > 1 { - isCollapsed.toggle() + withAnimation { + isCollapsed.toggle() + } } } label: { Text(viewModel.selectedLevel?.shortName ?? (levels.first?.shortName ?? "None")) From 3230f0f56db1bdbcc13990097faad56709c9eb28 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 13:07:45 -0700 Subject: [PATCH 0621/1722] Update SiteAndFacilitySelector.swift --- .../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 83f836347..77c0abd39 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -188,7 +188,6 @@ struct SiteAndFacilitySelector: View { ScrollViewReader { proxy in List(matchingFacilities, id: \.facilityId) { facility in Button { - print(facility.name) floorFilterViewModel.selection = .facility(facility) isHidden.wrappedValue.toggle() } label: { From 8356a6c1dd734324697e7865b17edb872f6ebd90 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 15 Mar 2022 16:25:09 -0700 Subject: [PATCH 0622/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 1b440cf65..01cf304aa 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -107,7 +107,7 @@ public struct FloorFilter: View { /// Indicates the implicity selected site based on the current viewpoint. @State private var selectedSiteID: String? = nil - + /// A configured `SiteAndFacilitySelector` view. private var siteAndFacilitySelectorView: some View { SiteAndFacilitySelector( @@ -115,18 +115,26 @@ public struct FloorFilter: View { $selectedFacilityID, $selectedSiteID ) - .esriBorder() - .opacity(isSelectorHidden ? .zero : 1) - .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in - updateSelection() - } + .esriBorder() + .opacity(isSelectorHidden ? .zero : 1) + .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in + updateSelection() + } } /// The selected facility's levels, sorted by `level.verticalOrder`. private var sortedLevels: [FloorLevel] { - viewModel.selectedFacility?.levels.sorted() { + var levels: [FloorLevel] + if selectedFacilityID != nil { + levels = viewModel.facilities.first { facility in + facility.id == selectedFacilityID + }?.levels ?? [] + } else { + levels = viewModel.selectedFacility?.levels ?? [] + } + return levels.sorted() { $0.verticalOrder > $1.verticalOrder - } ?? [] + } } /// Indicates that the selector should be presented with a top oriented aligment configuration. From 4f7ac693cc9ad62377c1f4242ac7025122e688b7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 10:17:12 -0700 Subject: [PATCH 0623/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 77 ++++++------------- 1 file changed, 25 insertions(+), 52 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 01cf304aa..ccfcfba85 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -99,22 +99,10 @@ public struct FloorFilter: View { return false } } - - /// Indicates the implicity selected facility based on the current viewpoint. - @State - private var selectedFacilityID: String? = nil - - /// Indicates the implicity selected site based on the current viewpoint. - @State - private var selectedSiteID: String? = nil /// A configured `SiteAndFacilitySelector` view. private var siteAndFacilitySelectorView: some View { - SiteAndFacilitySelector( - isHidden: $isSelectorHidden, - $selectedFacilityID, - $selectedSiteID - ) + SiteAndFacilitySelector(isHidden: $isSelectorHidden) .esriBorder() .opacity(isSelectorHidden ? .zero : 1) .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in @@ -124,15 +112,8 @@ public struct FloorFilter: View { /// The selected facility's levels, sorted by `level.verticalOrder`. private var sortedLevels: [FloorLevel] { - var levels: [FloorLevel] - if selectedFacilityID != nil { - levels = viewModel.facilities.first { facility in - facility.id == selectedFacilityID - }?.levels ?? [] - } else { - levels = viewModel.selectedFacility?.levels ?? [] - } - return levels.sorted() { + let levels = viewModel.selectedFacility?.levels ?? [] + return levels.sorted { $0.verticalOrder > $1.verticalOrder } } @@ -184,7 +165,7 @@ public struct FloorFilter: View { /// viewpoint. private func updateSelection() { guard let viewpoint = viewpoint?.wrappedValue, - viewpoint.targetScale != .zero, + !viewpoint.targetScale.isZero, automaticSelectionMode != .never else { return } @@ -200,59 +181,51 @@ public struct FloorFilter: View { // and return if viewpoint.targetScale > targetScale { if automaticSelectionMode == .always { - selectedSiteID = nil - selectedFacilityID = nil + viewModel.setSite(nil) + viewModel.setFacility(nil) + viewModel.setLevel(nil) } // Assumption: if too zoomed out to see sites, also too zoomed out // to see facilities return } - // If the centerpoint is within a site's geometry, select that site. - // This code gracefully skips selection if there are no sites or no - // matching sites - let siteResult = viewModel.floorManager.sites.first { site in - guard let siteExtent = site.geometry?.extent else { + + let facilityResult = viewModel.floorManager.facilities.first { facility in + guard let facilityExtent = facility.geometry?.extent else { return false } return GeometryEngine.intersects( - geometry1: siteExtent, + geometry1: facilityExtent, geometry2: viewpoint.targetGeometry ) } - if let siteResult = siteResult { - selectedSiteID = siteResult.siteId + if let facilityResult = facilityResult { + viewModel.setFacility(facilityResult) + return } else if automaticSelectionMode == .always { - selectedSiteID = nil + viewModel.setFacility(nil) } - // Move on to facility selection. Default to map-authored Facility - // MinScale. If MinScale not specified or is 0, default to 1500. - targetScale = viewModel.floorManager.facilityLayer?.minScale ?? .zero - if targetScale.isZero { - targetScale = 1500 - } - // If out of scale, stop here - if viewpoint.targetScale > targetScale { - return - } - - let facilityResult = viewModel.floorManager.facilities.first { facility in - guard let facilityExtent = facility.geometry?.extent else { + // If the centerpoint is within a site's geometry, select that site. + // This code gracefully skips selection if there are no sites or no + // matching sites + let siteResult = viewModel.floorManager.sites.first { site in + guard let siteExtent = site.geometry?.extent else { return false } return GeometryEngine.intersects( - geometry1: facilityExtent, + geometry1: siteExtent, geometry2: viewpoint.targetGeometry ) } - if let facilityResult = facilityResult { - selectedFacilityID = facilityResult.facilityId + if let siteResult = siteResult { + viewModel.setSite(siteResult) } else if automaticSelectionMode == .always { - selectedFacilityID = nil + viewModel.setSite(nil) } } } @@ -324,7 +297,7 @@ struct LevelsStack: View { VStack { ForEach(levels) { level in Button { - viewModel.selection = .level(level) + viewModel.setLevel(level) } label: { Text(level.shortName) } From ebc8cad3796c1b42196d470dbf86892356dea773 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 10:17:34 -0700 Subject: [PATCH 0624/1722] Update FloorFilterViewModel.swift --- .../FloorFilter/FloorFilterViewModel.swift | 136 ++++++++---------- 1 file changed, 58 insertions(+), 78 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 3997ab872..77cdabb33 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -17,16 +17,6 @@ import ArcGIS /// Manages the state for a `FloorFilter`. @MainActor final class FloorFilterViewModel: ObservableObject { - /// A selected site, floor, or level. - enum Selection { - /// A selected site. - case site(FloorSite) - /// A selected facility. - case facility(FloorFacility) - /// A selected level. - case level(FloorLevel) - } - /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilterViewModel`. @@ -43,7 +33,7 @@ final class FloorFilterViewModel: ObservableObject { try await floorManager.load() if sites.count == 1 { // If we have only one site, select it. - selection = .site(sites.first!) + setSite(sites.first!, zoomTo: true) } } catch { print("error: \(error)") @@ -97,78 +87,61 @@ final class FloorFilterViewModel: ObservableObject { $0.verticalOrder != .min && $0.verticalOrder != .max } } - - /// The selected site, floor, or level. + @Published - var selection: Selection? { - didSet { - if case let .facility(facility) = selection, - let level = defaultLevel(for: facility) { - selection = .level(level) - } else { - filterMapToSelectedLevel() - } - zoomToSelection() - } - } - - /// The selected site. - var selectedSite: FloorSite? { - guard let selection = selection else { - return nil - } - - switch selection { - 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 selectedFacility: FloorFacility? { - guard let selection = selection else { - return nil - } - - switch selection { - case .site: - return nil - case .facility(let facility): - return facility - case .level(let level): - return level.facility + var selectedSite: FloorSite? + + @Published + var selectedFacility: FloorFacility? + + @Published + private(set) var selectedLevel: FloorLevel? + + // MARK: Set selectionmethods + + /// Updates the selected site, facility, and level based on a newly selected site. + /// - Parameters: + /// - floorSite: The selected site. + /// - zoomTo: The viewpoint should be updated to show to the extent of this site. + func setSite( + _ floorSite: FloorSite?, + zoomTo: Bool = false + ) { + selectedSite = floorSite + selectedFacility = nil + selectedLevel = nil + if zoomTo { + zoomToExtent(extent: floorSite?.geometry?.extent) } } - - /// The selected level. - var selectedLevel: FloorLevel? { - if case let .level(level) = selection { - return level - } else { - return nil + + /// Updates the selected site, facility, and level based on a newly selected facility. + /// - Parameters: + /// - floorFacility: The selected facility. + /// - zoomTo: The viewpoint should be updated to show to the extent of this facility. + func setFacility( + _ floorFacility: FloorFacility?, + zoomTo: Bool = false + ) { + selectedSite = floorFacility?.site + selectedFacility = floorFacility + selectedLevel = defaultLevel(for: floorFacility) + if zoomTo { + zoomToExtent(extent: floorFacility?.geometry?.extent) } } - - /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. - func zoomToSelection() { - guard let selection = selection else { - return - } - - switch selection { - case .site(let site): - zoomToExtent(extent: site.geometry?.extent) - case .facility(let facility): - zoomToExtent(extent: facility.geometry?.extent) - case .level(let level): - zoomToExtent(extent: level.facility?.geometry?.extent) - } + + /// Updates the selected site, facility, and level based on a newly selected level. + /// - Parameter floorLevel: The selected level. + func setLevel(_ floorLevel: FloorLevel?) { + selectedSite = floorLevel?.facility?.site + selectedFacility = floorLevel?.facility + selectedLevel = floorLevel + filterMapToSelectedLevel() } - + + /// Updates the viewpoint to display a given extent. + /// - Parameter extent: The new extent to be shown. private func zoomToExtent(extent: Envelope?) { // Make sure we have an extent and viewpoint to zoom to. guard let extent = extent, @@ -186,10 +159,17 @@ final class FloorFilterViewModel: ObservableObject { } /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. - public func filterMapToSelectedLevel() { + private func filterMapToSelectedLevel() { guard let selectedLevel = selectedLevel else { return } levels.forEach { $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder } } } + +extension FloorSite: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.siteId) + hasher.combine(self.name) + } +} From 848bd852123567dd9c661ad645e61b6b25cc1f4f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 10:17:44 -0700 Subject: [PATCH 0625/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 71 +++++++------------ 1 file changed, 26 insertions(+), 45 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 77c0abd39..ddded101d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -22,14 +22,8 @@ struct SiteAndFacilitySelector: View { /// current viewpoint. /// - Parameter selectedSiteID: Indicates the implicity selected site based on the current /// viewpoint. - init( - isHidden: Binding, - _ selectedFacilityID: Binding, - _ selectedSiteID: Binding - ) { + init(isHidden: Binding) { self.isHidden = isHidden - _selectedFacilityID = selectedFacilityID - _selectedSiteID = selectedSiteID } /// The view model used by the `SiteAndFacilitySelector`. @@ -38,28 +32,17 @@ struct SiteAndFacilitySelector: View { /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding - /// Indicates the implicity selected facility based on the current viewpoint. - @Binding - private var selectedFacilityID: String? - - /// Indicates the implicity selected site based on the current viewpoint. - @Binding - private var selectedSiteID: String? - var body: some View { VStack { if floorFilterViewModel.sites.count == 1 { Facilities( facilities: floorFilterViewModel.sites.first!.facilities, isHidden: isHidden, - selectedFacilityID: $selectedFacilityID, showSites: true ) } else { Sites( isHidden: isHidden, - selectedFacilityID: $selectedFacilityID, - selectedSiteID: $selectedSiteID, sites: floorFilterViewModel.sites ) } @@ -88,14 +71,6 @@ struct SiteAndFacilitySelector: View { @State var searchPhrase: String = "" - /// Indicates the implicity selected facility based on the current viewpoint. - @Binding - var selectedFacilityID: String? - - /// Indicates the implicity selected site based on the current viewpoint. - @Binding - var selectedSiteID: String? - /// Sites contained in a `FloorManager`. let sites: [FloorSite] @@ -112,17 +87,18 @@ struct SiteAndFacilitySelector: View { List(matchingSites) { (site) in NavigationLink( site.name, - tag: site.siteId, - selection: $selectedSiteID) { + tag: site, + selection: $floorFilterViewModel.selectedSite) { Facilities( facilities: site.facilities, - isHidden: isHidden, - selectedFacilityID: $selectedFacilityID + isHidden: isHidden ) } .onTapGesture { - selectedSiteID = site.siteId - floorFilterViewModel.selection = .site(site) + floorFilterViewModel.setSite( + site, + zoomTo: true + ) } } .listStyle(.plain) @@ -130,7 +106,6 @@ struct SiteAndFacilitySelector: View { Facilities( facilities: sites.flatMap({ $0.facilities }), isHidden: isHidden, - selectedFacilityID: $selectedFacilityID, showSites: true ) } @@ -167,10 +142,6 @@ struct SiteAndFacilitySelector: View { @State var searchPhrase: String = "" - /// Indicates the implicity selected facility based on the current viewpoint. - @Binding - var selectedFacilityID: String? - /// Indicates if site names should be shown as subtitles to the facility. /// /// Used when the user selects "All sites". @@ -188,30 +159,40 @@ struct SiteAndFacilitySelector: View { ScrollViewReader { proxy in List(matchingFacilities, id: \.facilityId) { facility in Button { - floorFilterViewModel.selection = .facility(facility) + floorFilterViewModel.setFacility( + facility, + zoomTo: true + ) isHidden.wrappedValue.toggle() } label: { HStack { Image( - systemName: selectedFacilityID == facility.facilityId ? "circle.fill" : "circle" + systemName: + floorFilterViewModel.selectedFacility?.facilityId == facility.facilityId ? "circle.fill" : "circle" ) VStack { Text(facility.name) - .fontWeight(.regular) - .frame(maxWidth: .infinity, alignment: .leading) + .fontWeight(.regular) + .frame( + maxWidth: .infinity, + alignment: .leading + ) if showSites, let siteName = facility.site?.name { Text(siteName) - .fontWeight(.ultraLight) - .frame(maxWidth: .infinity, alignment: .leading) + .fontWeight(.ultraLight) + .frame( + maxWidth: .infinity, + alignment: .leading + ) } } } } } .listStyle(.plain) - .onChange(of: selectedFacilityID) { facilityID in + .onChange(of: floorFilterViewModel.selectedFacility) { selectedFacility in guard let facility = floorFilterViewModel.facilities.first(where: { facility in - facility.facilityId == facilityID + facility.facilityId == selectedFacility?.facilityId }) else { return } From d1064dfad1c358f260439300c099a0a93aca74de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 10:40:15 -0700 Subject: [PATCH 0626/1722] Revert to original method of map loading --- .../Examples/FloorFilterExampleView.swift | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index a42bead69..f59dd93bc 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -33,6 +33,9 @@ struct FloorFilterExampleView: View { return Map(item: portalItem) } + @State + private var isMapLoaded: Bool = false + private var map = makeMap() @State @@ -45,31 +48,16 @@ struct FloorFilterExampleView: View { ) var body: some View { - Group { - if let mapLoadResult = mapLoadResult { - switch mapLoadResult { - case .success(let value): - MapView( - map: value, - viewpoint: viewpoint - ) - case .failure(let error): - Text("Error loading map: \(error.localizedDescription)") - .padding() - } - } else { - ProgressView() - } - } - .task { - mapLoadResult = await Result { - try await map.load() - return map - } + MapView( + map: map, + viewpoint: viewpoint + ) + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 } .overlay(alignment: .bottomLeading) { - if map.loadStatus == .loaded, - let floorManager = map.floorManager { + if isMapLoaded, + let floorManager = map.floorManager { FloorFilter( floorManager: floorManager, viewpoint: $viewpoint @@ -78,5 +66,13 @@ struct FloorFilterExampleView: View { .padding(36) } } + .task { + do { + try await map.load() + isMapLoaded = true + } catch { + print("load error: \(error)") + } + } } } From 9c2af94207a89e804a8f93158577b4df3f2009d6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 10:40:58 -0700 Subject: [PATCH 0627/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index f59dd93bc..9ec8a1351 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -38,9 +38,6 @@ struct FloorFilterExampleView: View { private var map = makeMap() - @State - private var mapLoadResult: Result? - @State private var viewpoint = Viewpoint( center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), From 168a8fcd610cd5674ecf61ff443408b45ffa3d22 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 10:43:52 -0700 Subject: [PATCH 0628/1722] Update Result.swift --- Sources/ArcGISToolkit/Extensions/Result.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index 0503e5195..d4e837b1e 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -12,6 +12,11 @@ // limitations under the License. extension Result where Failure == Error { + /// Creates a result based on the outcome of the given task. If the task + /// succeeds, the result is `success`. If the task fails, the result is + /// `failure`. + /// + /// Returns `nil` in the event that the task was cancelled. init?(awaiting task: () async throws -> Success) async { do { self = .success(try await task()) From 56113a41fe427767924eb71aeb4cfc752ff2d5cf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 10:44:02 -0700 Subject: [PATCH 0629/1722] Update View.swift --- Sources/ArcGISToolkit/Extensions/View.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index d32e35fc9..8b5b4e39a 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -15,17 +15,16 @@ import SwiftUI /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { - /// `true` if the view should display as selected, `false` otherwise. + /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool - + func body(content: Content) -> some View { - let roundedRect = RoundedRectangle(cornerRadius: 4) if isSelected { content .background(Color.secondary.opacity(0.8)) - .clipShape(roundedRect) + .clipShape(RoundedRectangle(cornerRadius: 4)) .shadow( - color: Color.secondary.opacity(0.8), + color: .secondary.opacity(0.8), radius: 2 ) } else { @@ -43,7 +42,9 @@ extension View { background( GeometryReader { geometry in Color.clear - .preference(key: SizePreferenceKey.self, value: geometry.size) + .preference( + key: SizePreferenceKey.self, value: geometry.size + ) } ) .onPreferenceChange(SizePreferenceKey.self, perform: perform) @@ -51,7 +52,7 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: The view being modified. + /// - Returns: The modified view. func selected( _ isSelected: Bool = false ) -> some View { From 8e63062dab2f0d35eee2a3ff8e5f236a25917c7c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 11:24:18 -0700 Subject: [PATCH 0630/1722] Patch breaking changes This reverts commit 56113a41fe427767924eb71aeb4cfc752ff2d5cf. --- .../Components/FloorFilter/FloorFilter.swift | 26 +++++++++---------- .../FloorFilter/FloorFilterViewModel.swift | 16 ++++++------ .../FloorFilter/SiteAndFacilitySelector.swift | 6 ++--- Sources/ArcGISToolkit/Extensions/Result.swift | 5 ---- Sources/ArcGISToolkit/Extensions/View.swift | 15 +++++------ 5 files changed, 31 insertions(+), 37 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index ccfcfba85..baf738342 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -51,11 +51,11 @@ public struct FloorFilter: View { !(viewModel.selectedFacility == nil || viewModel.selectedFacility!.levels.isEmpty) } - + /// A Boolean value that indicates whether the site/facility selector is hidden. @State private var isSelectorHidden: Bool = true - + /// A Boolean value that indicates whether the levels view is currently collapsed. @State private var isLevelsViewCollapsed: Bool = false @@ -99,7 +99,7 @@ public struct FloorFilter: View { return false } } - + /// A configured `SiteAndFacilitySelector` view. private var siteAndFacilitySelectorView: some View { SiteAndFacilitySelector(isHidden: $isSelectorHidden) @@ -109,7 +109,7 @@ public struct FloorFilter: View { updateSelection() } } - + /// The selected facility's levels, sorted by `level.verticalOrder`. private var sortedLevels: [FloorLevel] { let levels = viewModel.selectedFacility?.levels ?? [] @@ -234,20 +234,20 @@ public struct FloorFilter: View { struct LevelsView: View { /// The levels to display. let levels: [FloorLevel] - + /// 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. @Binding var isCollapsed: Bool - + /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel - + /// The height of the scroll view's content. @State private var scrollViewContentHeight: CGFloat = .zero - + public var body: some View { VStack { if !isCollapsed, @@ -279,7 +279,7 @@ struct LevelsView: View { } label: { Text(viewModel.selectedLevel?.shortName ?? (levels.first?.shortName ?? "None")) } - .buttonSelected(true) + .selected(true) .padding(4) } } @@ -289,10 +289,10 @@ struct LevelsView: View { /// A vertical list of floor levels. struct LevelsStack: View { let levels: [FloorLevel] - + /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel - + var body: some View { VStack { ForEach(levels) { level in @@ -301,7 +301,7 @@ struct LevelsStack: View { } label: { Text(level.shortName) } - .buttonSelected(level == viewModel.selectedLevel) + .selected(level == viewModel.selectedLevel) .padding(4) } } @@ -313,7 +313,7 @@ struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. @Binding var isCollapsed: Bool - + var body: some View { Button { withAnimation { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 77cdabb33..805d25e7c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -45,20 +45,20 @@ final class FloorFilterViewModel: ObservableObject { /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding? - + /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager - + /// The floor manager sites. var sites: [FloorSite] { floorManager.sites } - + /// The floor manager facilities. var facilities: [FloorFacility] { floorManager.facilities } - + /// The floor manager levels. var levels: [FloorLevel] { floorManager.levels @@ -67,7 +67,7 @@ final class FloorFilterViewModel: ObservableObject { /// `true` if the model is loading it's properties, `false` if not loading. @Published private(set) var isLoading = true - + /// Gets the default level for a facility. /// - Parameter facility: The facility to get the default level for. /// - Returns: The default level for the facility, which is the level with vertical order 0; @@ -77,7 +77,7 @@ final class FloorFilterViewModel: ObservableObject { level.facility == facility && level.verticalOrder == .zero }) ?? lowestLevel() } - + /// Returns the level with the lowest vertical order. private func lowestLevel() -> FloorLevel? { let sortedLevels = levels.sorted { @@ -147,7 +147,7 @@ final class FloorFilterViewModel: ObservableObject { guard let extent = extent, let viewpoint = viewpoint else { return } - + let builder = EnvelopeBuilder(envelope: extent) builder.expand(factor: 1.5) let targetExtent = builder.toGeometry() @@ -157,7 +157,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() { guard let selectedLevel = selectedLevel else { return } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index ddded101d..24ec1de27 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -28,7 +28,7 @@ struct SiteAndFacilitySelector: View { /// The view model used by the `SiteAndFacilitySelector`. @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel - + /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding @@ -48,7 +48,7 @@ struct SiteAndFacilitySelector: View { } } } - + /// A view displaying the sites contained in a `FloorManager`. struct Sites: View { /// The view model used by this selector. @@ -116,7 +116,7 @@ struct SiteAndFacilitySelector: View { .navigationViewStyle(.stack) } } - + /// A view displaying the facilities contained in a `FloorManager`. struct Facilities: View { /// `FloorFacility`s to be displayed by this view. diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index d4e837b1e..0503e5195 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -12,11 +12,6 @@ // limitations under the License. extension Result where Failure == Error { - /// Creates a result based on the outcome of the given task. If the task - /// succeeds, the result is `success`. If the task fails, the result is - /// `failure`. - /// - /// Returns `nil` in the event that the task was cancelled. init?(awaiting task: () async throws -> Success) async { do { self = .success(try await task()) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 8b5b4e39a..d32e35fc9 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -15,16 +15,17 @@ import SwiftUI /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { - /// A Boolean value that indicates whether view should display as selected. + /// `true` if the view should display as selected, `false` otherwise. var isSelected: Bool - + func body(content: Content) -> some View { + let roundedRect = RoundedRectangle(cornerRadius: 4) if isSelected { content .background(Color.secondary.opacity(0.8)) - .clipShape(RoundedRectangle(cornerRadius: 4)) + .clipShape(roundedRect) .shadow( - color: .secondary.opacity(0.8), + color: Color.secondary.opacity(0.8), radius: 2 ) } else { @@ -42,9 +43,7 @@ extension View { background( GeometryReader { geometry in Color.clear - .preference( - key: SizePreferenceKey.self, value: geometry.size - ) + .preference(key: SizePreferenceKey.self, value: geometry.size) } ) .onPreferenceChange(SizePreferenceKey.self, perform: perform) @@ -52,7 +51,7 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: The modified view. + /// - Returns: The view being modified. func selected( _ isSelected: Bool = false ) -> some View { From 94315c69563da03a4067604d3cad3d2d4efe72ec Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 11:40:06 -0700 Subject: [PATCH 0631/1722] Update FloorFilterExampleView.swift --- .../Examples/FloorFilterExampleView.swift | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 9d8737f1f..694e33224 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -52,28 +52,28 @@ struct FloorFilterExampleView: View { map: map, viewpoint: viewpoint ) - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } + .overlay(alignment: filterAlignment) { + if isMapLoaded, + let floorManager = map.floorManager { + FloorFilter( + alignment: filterAlignment, + floorManager: floorManager, + viewpoint: $viewpoint + ) + .frame(maxWidth: 300, maxHeight: 300) + .padding(36) } - .overlay(alignment: filterAlignment) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - alignment: filterAlignment, - floorManager: floorManager, - viewpoint: $viewpoint - ) - .frame(maxWidth: 300, maxHeight: 300) - .padding(36) - } - } - .task { - do { - try await map.load() - isMapLoaded = true - } catch { - print("load error: \(error)") - } + } + .task { + do { + try await map.load() + isMapLoaded = true + } catch { + print("load error: \(error)") } + } } } From cca6a480df9525472658c0f81a54899614605eaa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 11:47:43 -0700 Subject: [PATCH 0632/1722] Update FloorFilterExampleView.swift Match FF II --- .../Examples/FloorFilterExampleView.swift | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 694e33224..4704ecc34 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -16,26 +16,12 @@ import ArcGISToolkit import ArcGIS struct FloorFilterExampleView: View { - private let map: Map - - @State - private var viewpoint = Viewpoint( - center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), - scale: 10_000 - ) - - @State - private var isMapLoaded: Bool = false - - private let filterAlignment = Alignment.bottomLeading - - init() { - // Create the map from a portal item and assign to the mapView. - + /// Make a map from a portal item. + static func makeMap() -> Map { // Multiple sites/facilities: Esri IST map with all buildings. // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - + // Redlands Campus map. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities @@ -44,9 +30,22 @@ struct FloorFilterExampleView: View { // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - map = Map(item: portalItem) + return Map(item: portalItem) } - + + private let filterAlignment = Alignment.bottomLeading + + @State + private var isMapLoaded: Bool = false + + private var map = makeMap() + + @State + private var viewpoint = Viewpoint( + center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), + scale: 100_000 + ) + var body: some View { MapView( map: map, @@ -63,8 +62,8 @@ struct FloorFilterExampleView: View { floorManager: floorManager, viewpoint: $viewpoint ) - .frame(maxWidth: 300, maxHeight: 300) - .padding(36) + .frame(maxWidth: 300, maxHeight: 300) + .padding(36) } } .task { From 8789c8c79e967a6e795a4c65350be74bc57e9133 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 11:56:37 -0700 Subject: [PATCH 0633/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 4704ecc34..b553fc32d 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -22,9 +22,9 @@ struct FloorFilterExampleView: View { // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - // Redlands Campus map. + // Redlands Campus map with multiple sites and facilities let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities + let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) // Single site (ESRI Redlands Main) and facility (Building L). // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) From a58c8ea08db1493f4076b9ee3fccdda7cba2ab3d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 12:06:29 -0700 Subject: [PATCH 0634/1722] Update FloorFilterViewModel.swift --- .../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 805d25e7c..538fc9cd2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -35,7 +35,7 @@ final class FloorFilterViewModel: ObservableObject { // If we have only one site, select it. setSite(sites.first!, zoomTo: true) } - } catch { + } catch { print("error: \(error)") } isLoading = false From 3d93fcd23b5a0048b7158f07c798c9f2cef74404 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 12:15:20 -0700 Subject: [PATCH 0635/1722] Create FloorFilterTests.swift --- .../ArcGISToolkitTests/FloorFilterTests.swift | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Tests/ArcGISToolkitTests/FloorFilterTests.swift diff --git a/Tests/ArcGISToolkitTests/FloorFilterTests.swift b/Tests/ArcGISToolkitTests/FloorFilterTests.swift new file mode 100644 index 000000000..1dadcd5db --- /dev/null +++ b/Tests/ArcGISToolkitTests/FloorFilterTests.swift @@ -0,0 +1,45 @@ +// 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 +import SwiftUI +import XCTest +@testable import ArcGISToolkit + +class FloorFilterTests: XCTestCase { + /// Tests general behavior of `FloorFilterViewModel`. + func testFloorFilterViewModel() async { + let map = FloorFilterTests.makeMap() + // FIXME: Credentials required + try? await map.load() + print(map.loadStatus) + } + + /// Get a map constructed from an ArcGIS portal item. + /// - Returns: A map constructed from an ArcGIS portal item. + static func makeMap() -> Map { + // Multiple sites/facilities: Esri IST map with all buildings. +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) + + // Redlands Campus map with multiple sites and facilities. + let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) + let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) + + // Single site (ESRI Redlands Main) and facility (Building L). +// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) +// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) + + return Map(item: portalItem) + } +} From 9d4d44dafb4076ad9c831ecdc7c9b58674c1f61f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 17:57:23 -0700 Subject: [PATCH 0636/1722] Update FloorFilterTests.swift --- .../ArcGISToolkitTests/FloorFilterTests.swift | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterTests.swift b/Tests/ArcGISToolkitTests/FloorFilterTests.swift index 1dadcd5db..a3525051f 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterTests.swift @@ -12,22 +12,45 @@ // limitations under the License. import ArcGIS +import Combine import SwiftUI import XCTest @testable import ArcGISToolkit class FloorFilterTests: XCTestCase { + /// Applies credentials necessary to run tests. + override func setUp() async throws { + await addCredentials() + } + + private var cancellables = Set() + /// Tests general behavior of `FloorFilterViewModel`. func testFloorFilterViewModel() async { - let map = FloorFilterTests.makeMap() - // FIXME: Credentials required - try? await map.load() - print(map.loadStatus) + guard let map = await makeMap() else { + return + } + guard let floorManager = map.floorManager else { + XCTFail("No FloorManager available") + return + } + let viewModel = await FloorFilterViewModel(floorManager: floorManager) + let expectation = XCTestExpectation( + description: "View model successfully initialized" + ) + await viewModel.$isLoading + .sink { loading in + if !loading { + expectation.fulfill() + } + } + .store(in: &cancellables) + wait(for: [expectation], timeout: 10.0) } /// Get a map constructed from an ArcGIS portal item. /// - Returns: A map constructed from an ArcGIS portal item. - static func makeMap() -> Map { + private func makeMap() async -> Map? { // Multiple sites/facilities: Esri IST map with all buildings. // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) @@ -40,6 +63,17 @@ class FloorFilterTests: XCTestCase { // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - return Map(item: portalItem) + let map = Map(item: portalItem) + do { + try await map.load() + } catch { + XCTFail("\(#fileID), \(#function), \(#line), \(error.localizedDescription)") + return nil + } + guard map.loadStatus == .loaded else { + XCTFail("\(#fileID), \(#function), \(#line), Map not loaded") + return nil + } + return map } } From c5d5234f697704dc7c5f094882c691e79b6df2ad Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 18:50:53 -0700 Subject: [PATCH 0637/1722] Update FloorFilterTests.swift --- .../ArcGISToolkitTests/FloorFilterTests.swift | 157 +++++++++++++++--- 1 file changed, 136 insertions(+), 21 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterTests.swift b/Tests/ArcGISToolkitTests/FloorFilterTests.swift index a3525051f..95da50b7b 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterTests.swift @@ -23,29 +23,113 @@ class FloorFilterTests: XCTestCase { await addCredentials() } - private var cancellables = Set() + /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` + func testInitFloorFilterViewModelWithFloorManager() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { + return + } + let viewModel = await FloorFilterViewModel(floorManager: floorManager) + await verifyInitialization(viewModel) + let sites = await viewModel.sites + let facilities = await viewModel.facilities + let levels = await viewModel.levels + XCTAssertFalse(sites.isEmpty) + XCTAssertFalse(facilities.isEmpty) + XCTAssertFalse(levels.isEmpty) + } - /// Tests general behavior of `FloorFilterViewModel`. - func testFloorFilterViewModel() async { - guard let map = await makeMap() else { + /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager` and + /// `Binding?`.` + func testInitFloorFilterViewModelWithFloorManagerAndViewpoint() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { return } - guard let floorManager = map.floorManager else { - XCTFail("No FloorManager available") + var _viewpoint: Viewpoint = getViewpoint(.zero) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) + await verifyInitialization(viewModel) + let sites = await viewModel.sites + let facilities = await viewModel.facilities + let levels = await viewModel.levels + let vmViewpoint = await viewModel.viewpoint + XCTAssertFalse(sites.isEmpty) + XCTAssertFalse(facilities.isEmpty) + XCTAssertFalse(levels.isEmpty) + XCTAssertNotNil(vmViewpoint) + } + + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testSetSite() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { return } - let viewModel = await FloorFilterViewModel(floorManager: floorManager) - let expectation = XCTestExpectation( - description: "View model successfully initialized" + var _viewpoint: Viewpoint = getViewpoint(.zero) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) + await verifyInitialization(viewModel) + let sites = await viewModel.sites + await viewModel.setSite(sites.first) + let selectedSite = await viewModel.selectedSite + let selectedFacility = await viewModel.selectedFacility + let selectedLevel = await viewModel.selectedLevel + var vmViewpoint = await viewModel.viewpoint + XCTAssertEqual(selectedSite, sites.first) + XCTAssertNil(selectedFacility) + XCTAssertNil(selectedLevel) + XCTAssertEqual( + _viewpoint.targetGeometry.extent.center.x, + vmViewpoint?.wrappedValue.targetGeometry.extent.center.x + ) + await viewModel.setSite(sites.first, zoomTo: true) + vmViewpoint = await viewModel.viewpoint + XCTAssertEqual( + selectedSite?.geometry?.extent.center.x, + vmViewpoint?.wrappedValue.targetGeometry.extent.center.x + ) + } + + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testSetFacility() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { + return + } + var _viewpoint: Viewpoint = getViewpoint(.zero) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) + await verifyInitialization(viewModel) + let facilities = await viewModel.facilities + await viewModel.setFacility(facilities.first) + let selectedSite = await viewModel.selectedSite + let selectedFacility = await viewModel.selectedFacility + let selectedLevel = await viewModel.selectedLevel + let defaultLevel = await viewModel.defaultLevel(for: selectedFacility) + var vmViewpoint = await viewModel.viewpoint + XCTAssertEqual(selectedSite, selectedFacility?.site) + XCTAssertEqual(selectedFacility, facilities.first) + XCTAssertEqual(selectedLevel, defaultLevel) + XCTAssertEqual( + _viewpoint.targetGeometry.extent.center.x, + vmViewpoint?.wrappedValue.targetGeometry.extent.center.x + ) + await viewModel.setFacility(facilities.first, zoomTo: true) + vmViewpoint = await viewModel.viewpoint + XCTAssertEqual( + selectedFacility?.geometry?.extent.center.x, + vmViewpoint?.wrappedValue.targetGeometry.extent.center.x ) - await viewModel.$isLoading - .sink { loading in - if !loading { - expectation.fulfill() - } - } - .store(in: &cancellables) - wait(for: [expectation], timeout: 10.0) } /// Get a map constructed from an ArcGIS portal item. @@ -70,10 +154,41 @@ class FloorFilterTests: XCTestCase { XCTFail("\(#fileID), \(#function), \(#line), \(error.localizedDescription)") return nil } - guard map.loadStatus == .loaded else { - XCTFail("\(#fileID), \(#function), \(#line), Map not loaded") - return nil - } return map } + + /// Verifies that the `FloorFilterViewModel` has succesfully initialized. + /// - Parameter viewModel: The view model to analyze. + private func verifyInitialization(_ viewModel: FloorFilterViewModel) async { + let expectation = XCTestExpectation( + description: "View model successfully initialized" + ) + let subscription = await viewModel.$isLoading + .sink { loading in + if !loading { + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 10.0) + subscription.cancel() + } +} + +extension FloorFilterTests { + /// 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 + } + + /// Builds viewpoints to use for tests. + /// - Parameter rotation: The rotation to use for the resulting viewpoint. + /// - Returns: A viewpoint object for tests. + func getViewpoint(_ rotation: Double) -> Viewpoint { + return Viewpoint(center: point, scale: scale, rotation: rotation) + } } From 0639797085aed2c51f13103d5531cd4c1ff238b3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 16 Mar 2022 19:02:28 -0700 Subject: [PATCH 0638/1722] Update FloorFilterTests.swift --- .../ArcGISToolkitTests/FloorFilterTests.swift | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterTests.swift b/Tests/ArcGISToolkitTests/FloorFilterTests.swift index 95da50b7b..fc10be282 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterTests.swift @@ -69,7 +69,8 @@ class FloorFilterTests: XCTestCase { let floorManager = map.floorManager else { return } - var _viewpoint: Viewpoint = getViewpoint(.zero) + let initialViewpoint = getViewpoint(.zero) + var _viewpoint = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( floorManager: floorManager, @@ -81,19 +82,17 @@ class FloorFilterTests: XCTestCase { let selectedSite = await viewModel.selectedSite let selectedFacility = await viewModel.selectedFacility let selectedLevel = await viewModel.selectedLevel - var vmViewpoint = await viewModel.viewpoint XCTAssertEqual(selectedSite, sites.first) XCTAssertNil(selectedFacility) XCTAssertNil(selectedLevel) XCTAssertEqual( _viewpoint.targetGeometry.extent.center.x, - vmViewpoint?.wrappedValue.targetGeometry.extent.center.x + initialViewpoint.targetGeometry.extent.center.x ) await viewModel.setSite(sites.first, zoomTo: true) - vmViewpoint = await viewModel.viewpoint XCTAssertEqual( - selectedSite?.geometry?.extent.center.x, - vmViewpoint?.wrappedValue.targetGeometry.extent.center.x + _viewpoint.targetGeometry.extent.center.x, + selectedSite?.geometry?.extent.center.x ) } @@ -103,7 +102,8 @@ class FloorFilterTests: XCTestCase { let floorManager = map.floorManager else { return } - var _viewpoint: Viewpoint = getViewpoint(.zero) + let initialViewpoint = getViewpoint(.zero) + var _viewpoint = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( floorManager: floorManager, @@ -116,19 +116,17 @@ class FloorFilterTests: XCTestCase { let selectedFacility = await viewModel.selectedFacility let selectedLevel = await viewModel.selectedLevel let defaultLevel = await viewModel.defaultLevel(for: selectedFacility) - var vmViewpoint = await viewModel.viewpoint XCTAssertEqual(selectedSite, selectedFacility?.site) XCTAssertEqual(selectedFacility, facilities.first) XCTAssertEqual(selectedLevel, defaultLevel) XCTAssertEqual( _viewpoint.targetGeometry.extent.center.x, - vmViewpoint?.wrappedValue.targetGeometry.extent.center.x + initialViewpoint.targetGeometry.extent.center.x ) await viewModel.setFacility(facilities.first, zoomTo: true) - vmViewpoint = await viewModel.viewpoint XCTAssertEqual( - selectedFacility?.geometry?.extent.center.x, - vmViewpoint?.wrappedValue.targetGeometry.extent.center.x + _viewpoint.targetGeometry.extent.center.x, + selectedFacility?.geometry?.extent.center.x ) } From ff26552a4f30abeb82a52d61872e3fe24cc884ee Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 09:44:59 -0700 Subject: [PATCH 0639/1722] Update FloorFilterTests.swift --- .../ArcGISToolkitTests/FloorFilterTests.swift | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterTests.swift b/Tests/ArcGISToolkitTests/FloorFilterTests.swift index fc10be282..38ffbef16 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterTests.swift @@ -77,19 +77,19 @@ class FloorFilterTests: XCTestCase { viewpoint: viewpoint ) await verifyInitialization(viewModel) - let sites = await viewModel.sites - await viewModel.setSite(sites.first) + let site = await viewModel.sites.first + await viewModel.setSite(site) let selectedSite = await viewModel.selectedSite let selectedFacility = await viewModel.selectedFacility let selectedLevel = await viewModel.selectedLevel - XCTAssertEqual(selectedSite, sites.first) + XCTAssertEqual(selectedSite, site) XCTAssertNil(selectedFacility) XCTAssertNil(selectedLevel) XCTAssertEqual( _viewpoint.targetGeometry.extent.center.x, initialViewpoint.targetGeometry.extent.center.x ) - await viewModel.setSite(sites.first, zoomTo: true) + await viewModel.setSite(site, zoomTo: true) XCTAssertEqual( _viewpoint.targetGeometry.extent.center.x, selectedSite?.geometry?.extent.center.x @@ -110,26 +110,62 @@ class FloorFilterTests: XCTestCase { viewpoint: viewpoint ) await verifyInitialization(viewModel) - let facilities = await viewModel.facilities - await viewModel.setFacility(facilities.first) + let facility = await viewModel.facilities.first + await viewModel.setFacility(facility) let selectedSite = await viewModel.selectedSite let selectedFacility = await viewModel.selectedFacility let selectedLevel = await viewModel.selectedLevel let defaultLevel = await viewModel.defaultLevel(for: selectedFacility) XCTAssertEqual(selectedSite, selectedFacility?.site) - XCTAssertEqual(selectedFacility, facilities.first) + XCTAssertEqual(selectedFacility, facility) XCTAssertEqual(selectedLevel, defaultLevel) XCTAssertEqual( _viewpoint.targetGeometry.extent.center.x, initialViewpoint.targetGeometry.extent.center.x ) - await viewModel.setFacility(facilities.first, zoomTo: true) + await viewModel.setFacility(facility, zoomTo: true) XCTAssertEqual( _viewpoint.targetGeometry.extent.center.x, selectedFacility?.geometry?.extent.center.x ) } + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testSetLevel() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { + return + } + let initialViewpoint = getViewpoint(.zero) + var _viewpoint = initialViewpoint + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) + await verifyInitialization(viewModel) + let levels = await viewModel.levels + let level = levels.first + await viewModel.setLevel(level) + let selectedSite = await viewModel.selectedSite + let selectedFacility = await viewModel.selectedFacility + let selectedLevel = await viewModel.selectedLevel + XCTAssertEqual(selectedSite, selectedFacility?.site) + XCTAssertEqual(selectedFacility, level?.facility) + XCTAssertEqual(selectedLevel, level) + XCTAssertEqual( + _viewpoint.targetGeometry.extent.center.x, + initialViewpoint.targetGeometry.extent.center.x + ) + levels.forEach { level in + if level.verticalOrder == selectedLevel?.verticalOrder { + XCTAssertTrue(level.isVisible) + } else { + XCTAssertFalse(level.isVisible) + } + } + } + /// Get a map constructed from an ArcGIS portal item. /// - Returns: A map constructed from an ArcGIS portal item. private func makeMap() async -> Map? { From f337fb0e60d69a517e5e55c8cbe0b3d6bf169096 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 10:31:25 -0700 Subject: [PATCH 0640/1722] Update FloorFilterViewModelTests.swift --- ...FloorFilterTests.swift => FloorFilterViewModelTests.swift} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename Tests/ArcGISToolkitTests/{FloorFilterTests.swift => FloorFilterViewModelTests.swift} (99%) diff --git a/Tests/ArcGISToolkitTests/FloorFilterTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift similarity index 99% rename from Tests/ArcGISToolkitTests/FloorFilterTests.swift rename to Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 38ffbef16..2843a6c7a 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -17,7 +17,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -class FloorFilterTests: XCTestCase { +class FloorFilterViewModelTests: XCTestCase { /// Applies credentials necessary to run tests. override func setUp() async throws { await addCredentials() @@ -208,7 +208,7 @@ class FloorFilterTests: XCTestCase { } } -extension FloorFilterTests { +extension FloorFilterViewModelTests { /// An arbitrary point to use for testing. var point: Point { Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) From 0ec71efb40066825e93353f80a8a77d0a0962b4f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 10:48:38 -0700 Subject: [PATCH 0641/1722] Update FloorFilterViewModel.swift --- .../FloorFilter/FloorFilterViewModel.swift | 106 ++++++++++++++++-- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 538fc9cd2..ed8749a20 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -19,21 +19,24 @@ import ArcGIS final class FloorFilterViewModel: ObservableObject { /// Creates a `FloorFilterViewModel`. /// - Parameters: + /// - automaticSelectionMode: The selection behavior of the floor filter. /// - floorManager: The floor manager used by the `FloorFilterViewModel`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. init( + automaticSelectionMode: AutomaticSelectionMode = .always, floorManager: FloorManager, viewpoint: Binding? = nil ) { + self.automaticSelectionMode = automaticSelectionMode self.floorManager = floorManager self.viewpoint = viewpoint - Task { do { try await floorManager.load() - if sites.count == 1 { + if sites.count == 1, + let firstSite = sites.first { // If we have only one site, select it. - setSite(sites.first!, zoomTo: true) + setSite(firstSite, zoomTo: true) } } catch { print("error: \(error)") @@ -42,6 +45,9 @@ final class FloorFilterViewModel: ObservableObject { } } + /// The selection behavior of the floor filter. + private let automaticSelectionMode: AutomaticSelectionMode + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding? @@ -140,6 +146,80 @@ final class FloorFilterViewModel: ObservableObject { filterMapToSelectedLevel() } + /// Updates `selectedFacilityID` and `selectedSiteID` based on the most recent + /// viewpoint. + func updateSelection() { + guard let viewpoint = viewpoint?.wrappedValue, + !viewpoint.targetScale.isZero, + automaticSelectionMode != .never else { + return + } + + // Only take action if viewpoint is within minimum scale. Default + // minscale is 4300 or less (~zoom level 17 or greater) + var targetScale = floorManager.siteLayer?.minScale ?? .zero + if targetScale.isZero { + targetScale = 4300 + } + + // If viewpoint is out of range, reset selection (if not non-clearing) + // and return + if viewpoint.targetScale > targetScale { + if automaticSelectionMode == .always { + setSite(nil) + setFacility(nil) + setLevel(nil) + } + // Assumption: if too zoomed out to see sites, also too zoomed out + // to see facilities + return + } + + let facilityResult = floorManager.facilities.first { facility in + guard let facilityExtent = facility.geometry?.extent else { + return false + } + return GeometryEngine.intersects( + geometry1: facilityExtent, + geometry2: viewpoint.targetGeometry + ) + } + + if let facilityResult = facilityResult { + setFacility(facilityResult) + return + } else if automaticSelectionMode == .always { + setFacility(nil) + } + + // If the centerpoint is within a site's geometry, select that site. + // This code gracefully skips selection if there are no sites or no + // matching sites + let siteResult = floorManager.sites.first { site in + guard let siteExtent = site.geometry?.extent else { + return false + } + return GeometryEngine.intersects( + geometry1: siteExtent, + geometry2: viewpoint.targetGeometry + ) + } + + if let siteResult = siteResult { + setSite(siteResult) + } else if automaticSelectionMode == .always { + setSite(nil) + } + } + + /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. + private func filterMapToSelectedLevel() { + guard let selectedLevel = selectedLevel else { return } + levels.forEach { + $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder + } + } + /// Updates the viewpoint to display a given extent. /// - Parameter extent: The new extent to be shown. private func zoomToExtent(extent: Envelope?) { @@ -157,14 +237,6 @@ 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() { - guard let selectedLevel = selectedLevel else { return } - levels.forEach { - $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder - } - } } extension FloorSite: Hashable { @@ -173,3 +245,15 @@ extension FloorSite: Hashable { hasher.combine(self.name) } } + +/// Defines automatic selection behavior. +public enum AutomaticSelectionMode { + /// 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. + case alwaysNotClearing + /// Never update selection based on the GeoView's current viewpoint. + case never +} From 30a47b8869e4aae1a96c2d555bd2db41836714de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 10:48:44 -0700 Subject: [PATCH 0642/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 87 +------------------ 1 file changed, 2 insertions(+), 85 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index baf738342..fc551c624 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -31,20 +31,17 @@ public struct FloorFilter: View { viewpoint: Binding? = nil ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( + automaticSelectionMode: automaticSelectionMode, floorManager: floorManager, viewpoint: viewpoint )) self.alignment = alignment - self.automaticSelectionMode = automaticSelectionMode self.viewpoint = viewpoint } /// The alignment configuration. private let alignment: Alignment - /// The selection behavior of the floor filter. - private let automaticSelectionMode: AutomaticSelectionMode - /// 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. private var hasLevelsToDisplay: Bool { @@ -106,7 +103,7 @@ public struct FloorFilter: View { .esriBorder() .opacity(isSelectorHidden ? .zero : 1) .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in - updateSelection() + viewModel.updateSelection() } } @@ -160,74 +157,6 @@ public struct FloorFilter: View { .frame(minHeight: 100) .environmentObject(viewModel) } - - /// Updates `selectedFacilityID` and `selectedSiteID` based on the most recent - /// viewpoint. - private func updateSelection() { - guard let viewpoint = viewpoint?.wrappedValue, - !viewpoint.targetScale.isZero, - automaticSelectionMode != .never else { - return - } - - // Only take action if viewpoint is within minimum scale. Default - // minscale is 4300 or less (~zoom level 17 or greater) - var targetScale = viewModel.floorManager.siteLayer?.minScale ?? .zero - if targetScale.isZero { - targetScale = 4300 - } - - // If viewpoint is out of range, reset selection (if not non-clearing) - // and return - if viewpoint.targetScale > targetScale { - if automaticSelectionMode == .always { - viewModel.setSite(nil) - viewModel.setFacility(nil) - viewModel.setLevel(nil) - } - // Assumption: if too zoomed out to see sites, also too zoomed out - // to see facilities - return - } - - - let facilityResult = viewModel.floorManager.facilities.first { facility in - guard let facilityExtent = facility.geometry?.extent else { - return false - } - return GeometryEngine.intersects( - geometry1: facilityExtent, - geometry2: viewpoint.targetGeometry - ) - } - - if let facilityResult = facilityResult { - viewModel.setFacility(facilityResult) - return - } else if automaticSelectionMode == .always { - viewModel.setFacility(nil) - } - - - // If the centerpoint is within a site's geometry, select that site. - // This code gracefully skips selection if there are no sites or no - // matching sites - let siteResult = viewModel.floorManager.sites.first { site in - guard let siteExtent = site.geometry?.extent else { - return false - } - return GeometryEngine.intersects( - geometry1: siteExtent, - geometry2: viewpoint.targetGeometry - ) - } - - if let siteResult = siteResult { - viewModel.setSite(siteResult) - } else if automaticSelectionMode == .always { - viewModel.setSite(nil) - } - } } /// A view displaying the levels in the selected facility. @@ -330,15 +259,3 @@ struct CollapseButton: View { )) } } - -/// Defines automatic selection behavior. -public enum AutomaticSelectionMode { - /// 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. - case alwaysNotClearing - /// Never update selection based on the GeoView's current viewpoint. - case never -} From e9addf7a1d0794068a45015d86f98ec3920ae2cd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 10:57:25 -0700 Subject: [PATCH 0643/1722] Update FloorFilter.swift --- .../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 fc551c624..3427f20be 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -177,6 +177,18 @@ struct LevelsView: View { @State private var scrollViewContentHeight: CGFloat = .zero + /// Returns the short name of the currently selected level, the first level or "None" if none of the listed + /// are available. + private var selectedLevelName: String { + if let shortName = viewModel.selectedLevel?.shortName { + return shortName + } else if let firstLevelShortName = levels.first?.shortName { + return firstLevelShortName + } else { + return "None" + } + } + public var body: some View { VStack { if !isCollapsed, @@ -196,8 +208,7 @@ struct LevelsView: View { ) } .frame(maxHeight: scrollViewContentHeight) - } - else { + } else { // Button for the selected level. Button { if levels.count > 1 { @@ -206,7 +217,7 @@ struct LevelsView: View { } } } label: { - Text(viewModel.selectedLevel?.shortName ?? (levels.first?.shortName ?? "None")) + Text(selectedLevelName) } .selected(true) .padding(4) From d437deb252785e0633212fb53877511f2d9a2e44 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 11:17:45 -0700 Subject: [PATCH 0644/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index b553fc32d..3090869c5 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -33,13 +33,17 @@ struct FloorFilterExampleView: View { return Map(item: portalItem) } + /// Determines the arrangement of the inner `FloorFilter` UI componenets. private let filterAlignment = Alignment.bottomLeading + /// Determines the appropriate time to initialize the `FloorFilter`. @State private var isMapLoaded: Bool = false + /// The `Map` that will be provided to the `MapView`. private var map = makeMap() + /// The initial viewpoint of the map. @State private var viewpoint = Viewpoint( center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), From 7ab012d5f03768d89979942b1957b926710b7aaa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 11:18:04 -0700 Subject: [PATCH 0645/1722] Update FloorFilterViewModel.swift --- .../Components/FloorFilter/FloorFilterViewModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index ed8749a20..a2cc3557c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -146,8 +146,7 @@ final class FloorFilterViewModel: ObservableObject { filterMapToSelectedLevel() } - /// Updates `selectedFacilityID` and `selectedSiteID` based on the most recent - /// viewpoint. + /// Updates `selectedSite` and `selectedFacility` based on the latest viewpoint position. func updateSelection() { guard let viewpoint = viewpoint?.wrappedValue, !viewpoint.targetScale.isZero, From 2524084db76b0cc934cb5e6527fc516c941783e6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 11:26:11 -0700 Subject: [PATCH 0646/1722] Update FloorFilterViewModel.swift --- .../FloorFilter/FloorFilterViewModel.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index a2cc3557c..8b286394f 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -25,7 +25,7 @@ final class FloorFilterViewModel: ObservableObject { init( automaticSelectionMode: AutomaticSelectionMode = .always, floorManager: FloorManager, - viewpoint: Binding? = nil + viewpoint: Binding ) { self.automaticSelectionMode = automaticSelectionMode self.floorManager = floorManager @@ -50,7 +50,7 @@ final class FloorFilterViewModel: ObservableObject { /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. - var viewpoint: Binding? + var viewpoint: Binding /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager @@ -148,7 +148,7 @@ final class FloorFilterViewModel: ObservableObject { /// Updates `selectedSite` and `selectedFacility` based on the latest viewpoint position. func updateSelection() { - guard let viewpoint = viewpoint?.wrappedValue, + guard let viewpoint = viewpoint.wrappedValue, !viewpoint.targetScale.isZero, automaticSelectionMode != .never else { return @@ -223,9 +223,9 @@ final class FloorFilterViewModel: ObservableObject { /// - Parameter extent: The new extent to be shown. private func zoomToExtent(extent: Envelope?) { // Make sure we have an extent and viewpoint to zoom to. - guard let extent = extent, - let viewpoint = viewpoint - else { return } + guard let extent = extent else { + return + } let builder = EnvelopeBuilder(envelope: extent) builder.expand(factor: 1.5) From cd7130716ab9baf8131ebe0a675c4f4e755a5f9f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 11:26:18 -0700 Subject: [PATCH 0647/1722] Update FloorFilter.swift --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 3427f20be..6a832b84e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -28,7 +28,7 @@ public struct FloorFilter: View { alignment: Alignment, automaticSelectionMode: AutomaticSelectionMode = .always, floorManager: FloorManager, - viewpoint: Binding? = nil + viewpoint: Binding ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( automaticSelectionMode: automaticSelectionMode, @@ -102,7 +102,7 @@ public struct FloorFilter: View { SiteAndFacilitySelector(isHidden: $isSelectorHidden) .esriBorder() .opacity(isSelectorHidden ? .zero : 1) - .onChange(of: viewpoint?.wrappedValue.targetGeometry) { _ in + .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in viewModel.updateSelection() } } @@ -131,7 +131,7 @@ public struct FloorFilter: View { /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. - private var viewpoint: Binding? + private var viewpoint: Binding public var body: some View { Group { From c5a9a5302f05619c962fbc676e6d2f6099e9ad05 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 11:26:56 -0700 Subject: [PATCH 0648/1722] 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 3090869c5..08a6e3982 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -45,7 +45,7 @@ struct FloorFilterExampleView: View { /// The initial viewpoint of the map. @State - private var viewpoint = Viewpoint( + private var viewpoint: Viewpoint? = Viewpoint( center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), scale: 100_000 ) From 745208bbbfa94de40e558e25415018dd791452be Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 11:49:15 -0700 Subject: [PATCH 0649/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 24ec1de27..5a166a54b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -147,6 +147,14 @@ struct SiteAndFacilitySelector: View { /// Used when the user selects "All sites". var showSites: Bool = false + /// Determines if a given site is the one marked as selected in the view model. + /// - Parameter facility: The facility of interest + /// - Returns: `true` if the facility is marked as selected in the view model. + func facilityIsSelected(_ facility: FloorFacility) -> Bool { + return facility.facilityId == + floorFilterViewModel.selectedFacility?.facilityId + } + var body: some View { VStack { if !showSites, let siteName = facilities.first?.site?.name { @@ -168,7 +176,7 @@ struct SiteAndFacilitySelector: View { HStack { Image( systemName: - floorFilterViewModel.selectedFacility?.facilityId == facility.facilityId ? "circle.fill" : "circle" + facilityIsSelected(facility) ? "circle.fill" : "circle" ) VStack { Text(facility.name) @@ -190,10 +198,8 @@ struct SiteAndFacilitySelector: View { } } .listStyle(.plain) - .onChange(of: floorFilterViewModel.selectedFacility) { selectedFacility in - guard let facility = floorFilterViewModel.facilities.first(where: { facility in - facility.facilityId == selectedFacility?.facilityId - }) else { + .onChange(of: floorFilterViewModel.selectedFacility) { + guard let facility = $0 else { return } withAnimation { From a6753def9fa94c23440bb07ab5a9714a2c092f67 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 11:54:01 -0700 Subject: [PATCH 0650/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 5a166a54b..424437af7 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -27,23 +27,23 @@ struct SiteAndFacilitySelector: View { } /// The view model used by the `SiteAndFacilitySelector`. - @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + @EnvironmentObject var viewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding var body: some View { VStack { - if floorFilterViewModel.sites.count == 1 { + if viewModel.sites.count == 1 { Facilities( - facilities: floorFilterViewModel.sites.first!.facilities, + facilities: viewModel.sites.first!.facilities, isHidden: isHidden, showSites: true ) } else { Sites( isHidden: isHidden, - sites: floorFilterViewModel.sites + sites: viewModel.sites ) } } @@ -52,7 +52,7 @@ struct SiteAndFacilitySelector: View { /// A view displaying the sites contained in a `FloorManager`. struct Sites: View { /// The view model used by this selector. - @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + @EnvironmentObject var viewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding @@ -88,14 +88,14 @@ struct SiteAndFacilitySelector: View { NavigationLink( site.name, tag: site, - selection: $floorFilterViewModel.selectedSite) { + selection: $viewModel.selectedSite) { Facilities( facilities: site.facilities, isHidden: isHidden ) } .onTapGesture { - floorFilterViewModel.setSite( + viewModel.setSite( site, zoomTo: true ) @@ -123,7 +123,7 @@ struct SiteAndFacilitySelector: View { let facilities: [FloorFacility] /// The view model used by this selector. - @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + @EnvironmentObject var viewModel: FloorFilterViewModel /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding @@ -152,7 +152,7 @@ struct SiteAndFacilitySelector: View { /// - Returns: `true` if the facility is marked as selected in the view model. func facilityIsSelected(_ facility: FloorFacility) -> Bool { return facility.facilityId == - floorFilterViewModel.selectedFacility?.facilityId + viewModel.selectedFacility?.facilityId } var body: some View { @@ -167,7 +167,7 @@ struct SiteAndFacilitySelector: View { ScrollViewReader { proxy in List(matchingFacilities, id: \.facilityId) { facility in Button { - floorFilterViewModel.setFacility( + viewModel.setFacility( facility, zoomTo: true ) @@ -198,7 +198,7 @@ struct SiteAndFacilitySelector: View { } } .listStyle(.plain) - .onChange(of: floorFilterViewModel.selectedFacility) { + .onChange(of: viewModel.selectedFacility) { guard let facility = $0 else { return } From 601548e8bd89652ddfe35537537453808ef237f9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 15:04:34 -0700 Subject: [PATCH 0651/1722] Update FloorFilterExampleView.swift --- .../Examples/FloorFilterExampleView.swift | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 08a6e3982..02416eaa8 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -55,28 +55,28 @@ struct FloorFilterExampleView: View { map: map, viewpoint: viewpoint ) - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 - } - .overlay(alignment: filterAlignment) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - alignment: filterAlignment, - floorManager: floorManager, - viewpoint: $viewpoint - ) - .frame(maxWidth: 300, maxHeight: 300) - .padding(36) + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 } - } - .task { - do { - try await map.load() - isMapLoaded = true - } catch { - print("load error: \(error)") + .overlay(alignment: filterAlignment) { + if isMapLoaded, + let floorManager = map.floorManager { + FloorFilter( + alignment: filterAlignment, + floorManager: floorManager, + viewpoint: $viewpoint + ) + .frame(maxWidth: 300, maxHeight: 300) + .padding(36) + } + } + .task { + do { + try await map.load() + isMapLoaded = true + } catch { + print("load error: \(error)") + } } - } } } From 5d8f0bfc15911a07317613e7e2734b3eef7e60e5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 15:04:42 -0700 Subject: [PATCH 0652/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 6a832b84e..96e1db065 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -78,7 +78,7 @@ public struct FloorFilter: View { } label: { Image(systemName: "building.2") } - .padding(4) + .padding(4) } .esriBorder() if topAligned { @@ -100,11 +100,11 @@ public struct FloorFilter: View { /// A configured `SiteAndFacilitySelector` view. private var siteAndFacilitySelectorView: some View { SiteAndFacilitySelector(isHidden: $isSelectorHidden) - .esriBorder() - .opacity(isSelectorHidden ? .zero : 1) - .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in - viewModel.updateSelection() - } + .esriBorder() + .opacity(isSelectorHidden ? .zero : 1) + .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in + viewModel.updateSelection() + } } /// The selected facility's levels, sorted by `level.verticalOrder`. @@ -153,9 +153,9 @@ public struct FloorFilter: View { } } } - // Ensure space for filter text field on small screens in landscape - .frame(minHeight: 100) - .environmentObject(viewModel) + // Ensure space for filter text field on small screens in landscape + .frame(minHeight: 100) + .environmentObject(viewModel) } } @@ -207,7 +207,7 @@ struct LevelsView: View { } ) } - .frame(maxHeight: scrollViewContentHeight) + .frame(maxHeight: scrollViewContentHeight) } else { // Button for the selected level. Button { @@ -219,8 +219,8 @@ struct LevelsView: View { } label: { Text(selectedLevelName) } - .selected(true) - .padding(4) + .selected(true) + .padding(4) } } } @@ -241,8 +241,8 @@ struct LevelsStack: View { } label: { Text(level.shortName) } - .selected(level == viewModel.selectedLevel) - .padding(4) + .selected(level == viewModel.selectedLevel) + .padding(4) } } } @@ -262,11 +262,11 @@ struct CollapseButton: View { } label: { Image(systemName: "xmark") } - .padding(EdgeInsets( - top: 2, - leading: 4, - bottom: 2, - trailing: 4 - )) + .padding(EdgeInsets( + top: 2, + leading: 4, + bottom: 2, + trailing: 4 + )) } } From 85fcec9e16e46cdafd6fa8b771cc540835b8f6db Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 15:04:51 -0700 Subject: [PATCH 0653/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 424437af7..8751bbda2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -88,12 +88,13 @@ struct SiteAndFacilitySelector: View { NavigationLink( site.name, tag: site, - selection: $viewModel.selectedSite) { - Facilities( - facilities: site.facilities, - isHidden: isHidden - ) - } + selection: $viewModel.selectedSite + ) { + Facilities( + facilities: site.facilities, + isHidden: isHidden + ) + } .onTapGesture { viewModel.setSite( site, @@ -101,7 +102,7 @@ struct SiteAndFacilitySelector: View { ) } } - .listStyle(.plain) + .listStyle(.plain) NavigationLink("All sites") { Facilities( facilities: sites.flatMap({ $0.facilities }), @@ -109,11 +110,11 @@ struct SiteAndFacilitySelector: View { showSites: true ) } - .padding([.top, .bottom], 4) + .padding([.top, .bottom], 4) } - .navigationBarTitle(Text("Select a site"), displayMode: .inline) + .navigationBarTitle(Text("Select a site"), displayMode: .inline) } - .navigationViewStyle(.stack) + .navigationViewStyle(.stack) } } @@ -197,21 +198,21 @@ struct SiteAndFacilitySelector: View { } } } - .listStyle(.plain) - .onChange(of: viewModel.selectedFacility) { - guard let facility = $0 else { - return - } - withAnimation { - proxy.scrollTo( - facility.facilityId, - anchor: .center - ) + .listStyle(.plain) + .onChange(of: viewModel.selectedFacility) { + guard let facility = $0 else { + return + } + withAnimation { + proxy.scrollTo( + facility.facilityId, + anchor: .center + ) + } } - } } } - .navigationBarTitle("Select a facility") + .navigationBarTitle("Select a facility") } } } From 2585a3e24bdb5fe8aa545cb2d563f84e0bcd912e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 16:04:39 -0700 Subject: [PATCH 0654/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 96e1db065..e4922eb94 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -57,6 +57,37 @@ public struct FloorFilter: View { @State private var isLevelsViewCollapsed: Bool = false + /// Displays the available levels. + private var levelsAndDividerView: some View { + Group { + if hasLevelsToDisplay { + if topAligned { + Divider() + .frame(width: 30) + } + LevelsView( + topAligned: topAligned, + levels: sortedLevels, + isCollapsed: $isLevelsViewCollapsed + ) + if !topAligned { + Divider() + .frame(width: 30) + } + } + } + } + + /// Button to open and close the site and facility selector. + private var facilityButtonView: some View { + Button { + isSelectorHidden.toggle() + } label: { + Image(systemName: "building.2") + } + .padding(4) + } + /// A view that allows selecting between levels. private var levelSelectorView: some View { VStack { @@ -64,21 +95,13 @@ public struct FloorFilter: View { Spacer() } VStack { - if hasLevelsToDisplay { - LevelsView( - levels: sortedLevels, - isCollapsed: $isLevelsViewCollapsed - ) - Divider() - .frame(width: 30) - } - // Site button. - Button { - isSelectorHidden.toggle() - } label: { - Image(systemName: "building.2") + if topAligned { + facilityButtonView + levelsAndDividerView + } else { + levelsAndDividerView + facilityButtonView } - .padding(4) } .esriBorder() if topAligned { @@ -161,6 +184,9 @@ public struct FloorFilter: View { /// A view displaying the levels in the selected facility. struct LevelsView: View { + /// The alignment configuration. + var topAligned: Bool + /// The levels to display. let levels: [FloorLevel] @@ -192,10 +218,12 @@ struct LevelsView: View { public var body: some View { VStack { if !isCollapsed, - levels.count > 1 { - CollapseButton(isCollapsed: $isCollapsed) - Divider() - .frame(width: 30) + levels.count > 1 { + if !topAligned { + CollapseButton(isCollapsed: $isCollapsed) + Divider() + .frame(width: 30) + } ScrollView { LevelsStack(levels: levels) .background( @@ -208,6 +236,11 @@ struct LevelsView: View { ) } .frame(maxHeight: scrollViewContentHeight) + if topAligned { + Divider() + .frame(width: 30) + CollapseButton(isCollapsed: $isCollapsed) + } } else { // Button for the selected level. Button { From 8a33f793a3419b082a968eaf433d3e267c8d4a46 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 18:06:07 -0700 Subject: [PATCH 0655/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 73 +++++++++++++++---- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 8751bbda2..434a739ce 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -38,7 +38,7 @@ struct SiteAndFacilitySelector: View { Facilities( facilities: viewModel.sites.first!.facilities, isHidden: isHidden, - showSites: true + presentationStyle: .singleSite ) } else { Sites( @@ -92,7 +92,8 @@ struct SiteAndFacilitySelector: View { ) { Facilities( facilities: site.facilities, - isHidden: isHidden + isHidden: isHidden, + presentationStyle: .standard ) } .onTapGesture { @@ -107,12 +108,17 @@ struct SiteAndFacilitySelector: View { Facilities( facilities: sites.flatMap({ $0.facilities }), isHidden: isHidden, - showSites: true + presentationStyle: .allSites ) } .padding([.top, .bottom], 4) } .navigationBarTitle(Text("Select a site"), displayMode: .inline) + .navigationBarItems(trailing: Button(action: { + isHidden.wrappedValue.toggle() + }, label: { + Image(systemName: "xmark.circle") + })) } .navigationViewStyle(.stack) } @@ -120,6 +126,16 @@ struct SiteAndFacilitySelector: View { /// A view displaying the facilities contained in a `FloorManager`. struct Facilities: View { + /// Presentation styles for the facility list. + enum PresentationStyle { + /// A specific site was selected and the body is presented within a navigation view. + case standard + /// The all sites button was selcted and the body is presented within a navigation view. + case allSites + /// Only one site exists and the body is not presented within a navigation view. + case singleSite + } + /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] @@ -139,15 +155,13 @@ struct SiteAndFacilitySelector: View { } } + /// The selected presentation style for the facility list. + let presentationStyle: PresentationStyle + /// A facility filtering phrase entered by the user. @State var searchPhrase: String = "" - /// Indicates if site names should be shown as subtitles to the facility. - /// - /// Used when the user selects "All sites". - var showSites: Bool = false - /// Determines if a given site is the one marked as selected in the view model. /// - Parameter facility: The facility of interest /// - Returns: `true` if the facility is marked as selected in the view model. @@ -157,10 +171,43 @@ struct SiteAndFacilitySelector: View { } var body: some View { + if presentationStyle == .standard || + presentationStyle == .allSites { + facilityFilterAndListView + // Only apply navigation modifiers if this is displayed + // within a navigation view + .navigationBarTitle("Select a facility") + .navigationBarItems(trailing: closeButtonView) + } else { + facilityFilterAndListView + } + } + + /// Closese the site and facility selector. + var closeButtonView: some View { + Button(action: { + isHidden.wrappedValue.toggle() + }, label: { + Image(systemName: "xmark.circle") + }) + } + + /// A view containing a label for the site name, a filter-via-name bar and a list of the facility names. + var facilityFilterAndListView: some View { VStack { - if !showSites, let siteName = facilities.first?.site?.name { - Text(siteName) - .fontWeight(.ultraLight) + HStack { + if presentationStyle == .standard { + Text(facilities.first?.site?.name ?? "N/A") + .fontWeight(.ultraLight) + } else if presentationStyle == .allSites { + Text("All sites") + .fontWeight(.ultraLight) + } else if presentationStyle == .singleSite { + Text(facilities.first?.site?.name ?? "N/A") + .fontWeight(.ultraLight) + Spacer() + closeButtonView + } } TextField("Filter facilities", text: $searchPhrase) .keyboardType(.alphabet) @@ -186,7 +233,8 @@ struct SiteAndFacilitySelector: View { maxWidth: .infinity, alignment: .leading ) - if showSites, let siteName = facility.site?.name { + if presentationStyle == .allSites, + let siteName = facility.site?.name { Text(siteName) .fontWeight(.ultraLight) .frame( @@ -212,7 +260,6 @@ struct SiteAndFacilitySelector: View { } } } - .navigationBarTitle("Select a facility") } } } From aa879b412e11d65f82588dea969c59413c9a5a27 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 17 Mar 2022 19:05:52 -0700 Subject: [PATCH 0656/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index e4922eb94..35b432e4e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -108,6 +108,7 @@ public struct FloorFilter: View { Spacer() } } + .frame(width: 55) } /// Indicates that the selector should be presented with a right oriented aligment configuration. @@ -243,17 +244,18 @@ struct LevelsView: View { } } else { // Button for the selected level. - Button { - if levels.count > 1 { + Text(selectedLevelName) + .lineLimit(1) + .frame(maxWidth: 50) + .padding([.top, .bottom], 2) + .background(Color(uiColor: .systemBlue)) + .cornerRadius(4) + .onTapGesture { withAnimation { isCollapsed.toggle() } } - } label: { - Text(selectedLevelName) - } - .selected(true) - .padding(4) + .padding([.top, .bottom], 3) } } } @@ -269,13 +271,20 @@ struct LevelsStack: View { var body: some View { VStack { ForEach(levels) { level in - Button { - viewModel.setLevel(level) - } label: { - Text(level.shortName) - } - .selected(level == viewModel.selectedLevel) - .padding(4) + Text(level.shortName) + .lineLimit(1) + .frame(maxWidth: 50) + .padding([.top, .bottom], 2) + .background( + level == viewModel.selectedLevel ? + Color(uiColor: .systemBlue) : + Color(uiColor: .systemGray2) + ) + .cornerRadius(4) + .onTapGesture { + viewModel.setLevel(level) + } + .padding([.top, .bottom], 3) } } } From 96f17bc063a0566607ce330a6943e309d7208f83 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 08:37:19 -0700 Subject: [PATCH 0657/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 35b432e4e..5e56db924 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -244,10 +244,7 @@ struct LevelsView: View { } } else { // Button for the selected level. - Text(selectedLevelName) - .lineLimit(1) - .frame(maxWidth: 50) - .padding([.top, .bottom], 2) + LevelLabel(text: selectedLevelName) .background(Color(uiColor: .systemBlue)) .cornerRadius(4) .onTapGesture { @@ -261,6 +258,19 @@ struct LevelsView: View { } } +/// A label that display text in a view with a confined static width. +struct LevelLabel: View { + /// The text to be displayed in the label. + var text: String + + var body: some View { + Text(text) + .lineLimit(1) + .frame(maxWidth: 50) + .padding([.top, .bottom], 2) + } +} + /// A vertical list of floor levels. struct LevelsStack: View { let levels: [FloorLevel] @@ -271,10 +281,7 @@ struct LevelsStack: View { var body: some View { VStack { ForEach(levels) { level in - Text(level.shortName) - .lineLimit(1) - .frame(maxWidth: 50) - .padding([.top, .bottom], 2) + LevelLabel(text: level.shortName) .background( level == viewModel.selectedLevel ? Color(uiColor: .systemBlue) : From b417fb6fca76e30304a970a994528efb5b29533f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 08:42:07 -0700 Subject: [PATCH 0658/1722] Update FloorFilterViewModel.swift --- .../Components/FloorFilter/FloorFilterViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 8b286394f..cc9268058 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -101,9 +101,9 @@ final class FloorFilterViewModel: ObservableObject { var selectedFacility: FloorFacility? @Published - private(set) var selectedLevel: FloorLevel? + var selectedLevel: FloorLevel? - // MARK: Set selectionmethods + // MARK: Set selection methods /// Updates the selected site, facility, and level based on a newly selected site. /// - Parameters: From e88a7cadb0ccb3d04bb7f9e5fcc3c4af1e673278 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 13:22:03 -0700 Subject: [PATCH 0659/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 02416eaa8..066c7ac07 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -78,5 +78,7 @@ struct FloorFilterExampleView: View { print("load error: \(error)") } } + /// Preserve the current viewpoint when a keyboard is presented in landscape. + .padding([.bottom], 0.1) } } From 13b7232958f7e67fd85dc9ca17b6fa3bb2974961 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 17:03:44 -0700 Subject: [PATCH 0660/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 066c7ac07..2d94c5b20 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -58,11 +58,14 @@ struct FloorFilterExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + /// Preserve the current viewpoint when a keyboard is presented in landscape. + .ignoresSafeArea(.keyboard, edges: .bottom) .overlay(alignment: filterAlignment) { if isMapLoaded, let floorManager = map.floorManager { FloorFilter( alignment: filterAlignment, + automaticSelectionMode: .always, floorManager: floorManager, viewpoint: $viewpoint ) @@ -78,7 +81,5 @@ struct FloorFilterExampleView: View { print("load error: \(error)") } } - /// Preserve the current viewpoint when a keyboard is presented in landscape. - .padding([.bottom], 0.1) } } From 161bbac08deda48830398f67a8fde2c5a6dff9ce Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 17:17:21 -0700 Subject: [PATCH 0661/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 5e56db924..375d502d1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -160,11 +160,12 @@ public struct FloorFilter: View { public var body: some View { Group { if viewModel.isLoading { - VStack { - Spacer() ProgressView() - .padding(12) - } + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: alignment + ) } else { HStack(alignment: .bottom) { if rightAligned { From a1946aafd3228580dd8c71d1c47eed41d598449c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 17:17:38 -0700 Subject: [PATCH 0662/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 375d502d1..0cbbb4fef 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -160,12 +160,12 @@ public struct FloorFilter: View { public var body: some View { Group { if viewModel.isLoading { - ProgressView() - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: alignment - ) + ProgressView() + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: alignment + ) } else { HStack(alignment: .bottom) { if rightAligned { From bf515d7f7e14ab579a45f9d5fec639e3d7166a02 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 17:42:14 -0700 Subject: [PATCH 0663/1722] Update FloorFilterExampleView.swift Display error on map load failure --- .../Examples/FloorFilterExampleView.swift | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 9ec8a1351..35931be70 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -38,6 +38,9 @@ struct FloorFilterExampleView: View { private var map = makeMap() + @State + private var mapLoadError: Bool = false + @State private var viewpoint = Viewpoint( center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), @@ -49,27 +52,38 @@ struct FloorFilterExampleView: View { map: map, viewpoint: viewpoint ) - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 - } - .overlay(alignment: .bottomLeading) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - floorManager: floorManager, - viewpoint: $viewpoint - ) - .frame(height: 300) - .padding(36) + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } + .overlay(alignment: .bottomLeading) { + if isMapLoaded, + let floorManager = map.floorManager { + FloorFilter( + floorManager: floorManager, + viewpoint: $viewpoint + ) + .frame(height: 300) + .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 { - print("load error: \(error)") + .task { + do { + try await map.load() + isMapLoaded = true + } catch { + mapLoadError = true + } } - } } } From 1d81aec0c22d09e59a56b3b3e6eb306a4841a972 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 17:51:25 -0700 Subject: [PATCH 0664/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 2d94c5b20..736dcb679 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -43,6 +43,9 @@ struct FloorFilterExampleView: View { /// The `Map` that will be provided to the `MapView`. private var map = makeMap() + @State + private var mapLoadError: Bool = false + /// The initial viewpoint of the map. @State private var viewpoint: Viewpoint? = Viewpoint( @@ -71,6 +74,17 @@ struct FloorFilterExampleView: View { ) .frame(maxWidth: 300, maxHeight: 300) .padding(36) + } else if mapLoadError { + Label( + "Map load error!", + systemImage: "exclamationmark.triangle" + ) + .foregroundColor(.red) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .center + ) } } .task { @@ -78,7 +92,7 @@ struct FloorFilterExampleView: View { try await map.load() isMapLoaded = true } catch { - print("load error: \(error)") + mapLoadError = true } } } From ebd6e30674b568f34fd4b2bb3f13d4bc6d435e2e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 18:49:08 -0700 Subject: [PATCH 0665/1722] Update FloorFilterViewModel.swift --- .../FloorFilter/FloorFilterViewModel.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index cc9268058..1231a165e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -155,22 +155,19 @@ final class FloorFilterViewModel: ObservableObject { } // Only take action if viewpoint is within minimum scale. Default - // minscale is 4300 or less (~zoom level 17 or greater) - var targetScale = floorManager.siteLayer?.minScale ?? .zero - if targetScale.isZero { - targetScale = 4300 + // minScale is 4300 or less. + var minScale = floorManager.siteLayer?.minScale ?? .zero + if minScale.isZero { + minScale = 4300 } - // If viewpoint is out of range, reset selection (if not non-clearing) - // and return - if viewpoint.targetScale > targetScale { + // If viewpoint is out of range, reset selection and return. + if viewpoint.targetScale > minScale { if automaticSelectionMode == .always { setSite(nil) setFacility(nil) setLevel(nil) } - // Assumption: if too zoomed out to see sites, also too zoomed out - // to see facilities return } From 7b63abcd8ad7363bdd0cda1fec0eaa955d0c7bcf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 20:44:19 -0700 Subject: [PATCH 0666/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 736dcb679..559857736 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -68,7 +68,6 @@ struct FloorFilterExampleView: View { let floorManager = map.floorManager { FloorFilter( alignment: filterAlignment, - automaticSelectionMode: .always, floorManager: floorManager, viewpoint: $viewpoint ) From 8207895fafa8b6a9c08700524c8d0120ed079879 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 20:44:24 -0700 Subject: [PATCH 0667/1722] Update FloorFilterViewModel.swift --- .../FloorFilter/FloorFilterViewModel.swift | 99 ++++++++++++------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 1231a165e..9605de39c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -114,8 +114,6 @@ final class FloorFilterViewModel: ObservableObject { zoomTo: Bool = false ) { selectedSite = floorSite - selectedFacility = nil - selectedLevel = nil if zoomTo { zoomToExtent(extent: floorSite?.geometry?.extent) } @@ -129,9 +127,15 @@ final class FloorFilterViewModel: ObservableObject { _ floorFacility: FloorFacility?, zoomTo: Bool = false ) { - selectedSite = floorFacility?.site selectedFacility = floorFacility - selectedLevel = defaultLevel(for: floorFacility) + if let currentVerticalOrder = selectedLevel?.verticalOrder, + let newLevel = floorFacility?.levels.first(where: { level in + level.verticalOrder == currentVerticalOrder + }) { + setLevel(newLevel) + } else { + setLevel(defaultLevel(for: floorFacility)) + } if zoomTo { zoomToExtent(extent: floorFacility?.geometry?.extent) } @@ -140,8 +144,6 @@ final class FloorFilterViewModel: ObservableObject { /// Updates the selected site, facility, and level based on a newly selected level. /// - Parameter floorLevel: The selected level. func setLevel(_ floorLevel: FloorLevel?) { - selectedSite = floorLevel?.facility?.site - selectedFacility = floorLevel?.facility selectedLevel = floorLevel filterMapToSelectedLevel() } @@ -149,62 +151,89 @@ final class FloorFilterViewModel: ObservableObject { /// Updates `selectedSite` and `selectedFacility` based on the latest viewpoint position. func updateSelection() { guard let viewpoint = viewpoint.wrappedValue, - !viewpoint.targetScale.isZero, - automaticSelectionMode != .never else { + !viewpoint.targetScale.isZero, + automaticSelectionMode != .never else { return - } + } + if updateSelectedSite() { + updateSelectedFacility() + } + } - // Only take action if viewpoint is within minimum scale. Default - // minScale is 4300 or less. - var minScale = floorManager.siteLayer?.minScale ?? .zero - if minScale.isZero { - minScale = 4300 + /// Updates `selectedSite` if a good selection exists. + /// - Returns: `true` if a selection was made, `false` otherwise. + private func updateSelectedSite() -> Bool { + // Only select a facility if it is within minimum scale. Default at 4300. + let siteMinScale: Double + if let minScale = floorManager.siteLayer?.minScale, + minScale != .zero { + siteMinScale = minScale + } else { + siteMinScale = 4300 } - // If viewpoint is out of range, reset selection and return. - if viewpoint.targetScale > minScale { + // If viewpoint is out of range, reset selection and return early. + if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { if automaticSelectionMode == .always { setSite(nil) setFacility(nil) setLevel(nil) } - return + return false } - let facilityResult = floorManager.facilities.first { facility in - guard let facilityExtent = facility.geometry?.extent else { + // If the centerpoint is within a site's geometry, select that site. + let siteResult = floorManager.sites.first { site in + guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, + let extent2 = site.geometry?.extent else { return false } return GeometryEngine.intersects( - geometry1: facilityExtent, - geometry2: viewpoint.targetGeometry + geometry1: extent1, + geometry2: extent2 ) } - if let facilityResult = facilityResult { - setFacility(facilityResult) - return + if let siteResult = siteResult { + setSite(siteResult) + return true } else if automaticSelectionMode == .always { - setFacility(nil) + setSite(nil) } + return false + } - // If the centerpoint is within a site's geometry, select that site. - // This code gracefully skips selection if there are no sites or no - // matching sites - let siteResult = floorManager.sites.first { site in - guard let siteExtent = site.geometry?.extent else { + /// Updates `selectedFacility` if a good selection exists. + private func updateSelectedFacility() { + // Only select a facility if it is within minimum scale. Default at 1500. + let facilityMinScale: Double + if let minScale = floorManager.facilityLayer?.minScale, + minScale != .zero { + facilityMinScale = minScale + } else { + facilityMinScale = 1500 + } + + if viewpoint.wrappedValue?.targetScale ?? .zero > facilityMinScale { + return + } + + // If the centerpoint is within a facilities' geometry, select that site. + let facilityResult = floorManager.facilities.first { facility in + guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, + let extent2 = facility.geometry?.extent else { return false } return GeometryEngine.intersects( - geometry1: siteExtent, - geometry2: viewpoint.targetGeometry + geometry1: extent1, + geometry2: extent2 ) } - if let siteResult = siteResult { - setSite(siteResult) + if let facilityResult = facilityResult { + setFacility(facilityResult) } else if automaticSelectionMode == .always { - setSite(nil) + setFacility(nil) } } From 9c97f8037765aa4ab1f326be800a1e73546f743d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 20:53:30 -0700 Subject: [PATCH 0668/1722] Update FloorFilterViewModel.swift --- .../FloorFilter/FloorFilterViewModel.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 9605de39c..fad2a3f1a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -155,14 +155,12 @@ final class FloorFilterViewModel: ObservableObject { automaticSelectionMode != .never else { return } - if updateSelectedSite() { - updateSelectedFacility() - } + updateSelectedSite() + updateSelectedFacility() } /// Updates `selectedSite` if a good selection exists. - /// - Returns: `true` if a selection was made, `false` otherwise. - private func updateSelectedSite() -> Bool { + private func updateSelectedSite() { // Only select a facility if it is within minimum scale. Default at 4300. let siteMinScale: Double if let minScale = floorManager.siteLayer?.minScale, @@ -179,7 +177,7 @@ final class FloorFilterViewModel: ObservableObject { setFacility(nil) setLevel(nil) } - return false + return } // If the centerpoint is within a site's geometry, select that site. @@ -196,11 +194,9 @@ final class FloorFilterViewModel: ObservableObject { if let siteResult = siteResult { setSite(siteResult) - return true } else if automaticSelectionMode == .always { setSite(nil) } - return false } /// Updates `selectedFacility` if a good selection exists. From 94dcf82beab7d74dd90e5d4ecfb7d8ebd026e1fd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 21:24:13 -0700 Subject: [PATCH 0669/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 118 ++++++++++-------- 1 file changed, 67 insertions(+), 51 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 434a739ce..bccb0c2f3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -84,26 +84,34 @@ struct SiteAndFacilitySelector: View { TextField("Filter sites", text: $searchPhrase) .keyboardType(.alphabet) .disableAutocorrection(true) - List(matchingSites) { (site) in - NavigationLink( - site.name, - tag: site, - selection: $viewModel.selectedSite - ) { - Facilities( - facilities: site.facilities, - isHidden: isHidden, - presentationStyle: .standard - ) + if matchingSites.isEmpty { + VStack { + Spacer() + Text("No matches found") + Spacer() } - .onTapGesture { - viewModel.setSite( - site, - zoomTo: true + } else { + List(matchingSites) { site in + NavigationLink( + site.name, + tag: site, + selection: $viewModel.selectedSite + ) { + Facilities( + facilities: site.facilities, + isHidden: isHidden, + presentationStyle: .standard ) } + .onTapGesture { + viewModel.setSite( + site, + zoomTo: true + ) + } + } + .listStyle(.plain) } - .listStyle(.plain) NavigationLink("All sites") { Facilities( facilities: sites.flatMap({ $0.facilities }), @@ -212,52 +220,60 @@ struct SiteAndFacilitySelector: View { TextField("Filter facilities", text: $searchPhrase) .keyboardType(.alphabet) .disableAutocorrection(true) - ScrollViewReader { proxy in - List(matchingFacilities, id: \.facilityId) { facility in - Button { - viewModel.setFacility( - facility, - zoomTo: true - ) - isHidden.wrappedValue.toggle() - } label: { - HStack { - Image( - systemName: - facilityIsSelected(facility) ? "circle.fill" : "circle" + if matchingFacilities.isEmpty { + VStack { + Spacer() + Text("No matches found") + Spacer() + } + } else { + ScrollViewReader { proxy in + List(matchingFacilities, id: \.facilityId) { facility in + Button { + viewModel.setFacility( + facility, + zoomTo: true ) - VStack { - Text(facility.name) - .fontWeight(.regular) - .frame( - maxWidth: .infinity, - alignment: .leading + isHidden.wrappedValue.toggle() + } label: { + HStack { + Image( + systemName: + facilityIsSelected(facility) ? "circle.fill" : "circle" ) - if presentationStyle == .allSites, - let siteName = facility.site?.name { - Text(siteName) - .fontWeight(.ultraLight) + VStack { + Text(facility.name) + .fontWeight(.regular) .frame( maxWidth: .infinity, alignment: .leading ) + if presentationStyle == .allSites, + let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + .frame( + maxWidth: .infinity, + alignment: .leading + ) + } } } } } - } - .listStyle(.plain) - .onChange(of: viewModel.selectedFacility) { - guard let facility = $0 else { - return - } - withAnimation { - proxy.scrollTo( - facility.facilityId, - anchor: .center - ) + .listStyle(.plain) + .onChange(of: viewModel.selectedFacility) { + guard let facility = $0 else { + return + } + withAnimation { + proxy.scrollTo( + facility.facilityId, + anchor: .center + ) + } } - } + } } } } From add59cffb6763a84a357daf032280bed458562f1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 18 Mar 2022 21:57:41 -0700 Subject: [PATCH 0670/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 52 ++++++------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 0cbbb4fef..1f2280f68 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -85,7 +85,7 @@ public struct FloorFilter: View { } label: { Image(systemName: "building.2") } - .padding(4) + .frame(maxWidth: .infinity) } /// A view that allows selecting between levels. @@ -108,7 +108,7 @@ public struct FloorFilter: View { Spacer() } } - .frame(width: 55) + .frame(width: 75) } /// Indicates that the selector should be presented with a right oriented aligment configuration. @@ -244,34 +244,20 @@ struct LevelsView: View { CollapseButton(isCollapsed: $isCollapsed) } } else { - // Button for the selected level. - LevelLabel(text: selectedLevelName) - .background(Color(uiColor: .systemBlue)) - .cornerRadius(4) - .onTapGesture { - withAnimation { - isCollapsed.toggle() - } + Button { + if levels.count > 1 { + isCollapsed.toggle() } - .padding([.top, .bottom], 3) + } label: { + Text(selectedLevelName) + .lineLimit(1) + } + .selected(true) } } } } -/// A label that display text in a view with a confined static width. -struct LevelLabel: View { - /// The text to be displayed in the label. - var text: String - - var body: some View { - Text(text) - .lineLimit(1) - .frame(maxWidth: 50) - .padding([.top, .bottom], 2) - } -} - /// A vertical list of floor levels. struct LevelsStack: View { let levels: [FloorLevel] @@ -282,17 +268,13 @@ struct LevelsStack: View { var body: some View { VStack { ForEach(levels) { level in - LevelLabel(text: level.shortName) - .background( - level == viewModel.selectedLevel ? - Color(uiColor: .systemBlue) : - Color(uiColor: .systemGray2) - ) - .cornerRadius(4) - .onTapGesture { - viewModel.setLevel(level) - } - .padding([.top, .bottom], 3) + Button { + viewModel.setLevel(level) + } label: { + Text(level.shortName) + .lineLimit(1) + } + .selected(level == viewModel.selectedLevel) } } } From fbccab033cf6ac2d94fa2605ea25b26643f70d99 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 11:08:50 -0700 Subject: [PATCH 0671/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index bccb0c2f3..e371de2e3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -57,6 +57,10 @@ struct SiteAndFacilitySelector: View { /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding + /// Indicates that the keyboard is animating and some views may require reload. + @State + private var keyboardAnimating = false + /// A subset of `sites` that contain `searchPhrase`. var matchingSites: [FloorSite] { if searchPhrase.isEmpty { @@ -74,10 +78,6 @@ struct SiteAndFacilitySelector: View { /// Sites contained in a `FloorManager`. let sites: [FloorSite] - /// The height of the scroll view's content. - @State - private var scrollViewContentHeight: CGFloat = .zero - var body: some View { NavigationView { VStack { @@ -128,7 +128,26 @@ struct SiteAndFacilitySelector: View { Image(systemName: "xmark.circle") })) } + .opacity(keyboardAnimating ? 0.99 : 1.0) .navigationViewStyle(.stack) + .onReceive( + NotificationCenter.default.publisher( + for: UIResponder.keyboardWillChangeFrameNotification + ) + ) { _ in + withAnimation { + keyboardAnimating = true + } + } + .onReceive( + NotificationCenter.default.publisher( + for: UIResponder.keyboardDidChangeFrameNotification + ) + ) { _ in + withAnimation { + keyboardAnimating = false + } + } } } @@ -206,13 +225,10 @@ struct SiteAndFacilitySelector: View { HStack { if presentationStyle == .standard { Text(facilities.first?.site?.name ?? "N/A") - .fontWeight(.ultraLight) } else if presentationStyle == .allSites { Text("All sites") - .fontWeight(.ultraLight) } else if presentationStyle == .singleSite { Text(facilities.first?.site?.name ?? "N/A") - .fontWeight(.ultraLight) Spacer() closeButtonView } From e92717b46859a7b0b96c5d3d8f71b23f92c81259 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 11:15:40 -0700 Subject: [PATCH 0672/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index e371de2e3..713572758 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -33,19 +33,17 @@ struct SiteAndFacilitySelector: View { private var isHidden: Binding var body: some View { - VStack { - if viewModel.sites.count == 1 { - Facilities( - facilities: viewModel.sites.first!.facilities, - isHidden: isHidden, - presentationStyle: .singleSite - ) - } else { - Sites( - isHidden: isHidden, - sites: viewModel.sites - ) - } + if viewModel.sites.count == 1 { + Facilities( + facilities: viewModel.sites.first!.facilities, + isHidden: isHidden, + presentationStyle: .singleSite + ) + } else { + Sites( + isHidden: isHidden, + sites: viewModel.sites + ) } } From a3e4ad86758bdfb8675f719a70adb77c2cf943f4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 11:19:02 -0700 Subject: [PATCH 0673/1722] 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 713572758..471c44cea 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -117,7 +117,7 @@ struct SiteAndFacilitySelector: View { presentationStyle: .allSites ) } - .padding([.top, .bottom], 4) + .padding([.vertical], 4) } .navigationBarTitle(Text("Select a site"), displayMode: .inline) .navigationBarItems(trailing: Button(action: { From fc8f7fb5cf949c1b4b6633a3eb4c1fcefac8c205 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 11:20:46 -0700 Subject: [PATCH 0674/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 471c44cea..f0d38cd96 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -119,12 +119,17 @@ struct SiteAndFacilitySelector: View { } .padding([.vertical], 4) } - .navigationBarTitle(Text("Select a site"), displayMode: .inline) - .navigationBarItems(trailing: Button(action: { - isHidden.wrappedValue.toggle() - }, label: { - Image(systemName: "xmark.circle") - })) + .navigationBarTitle( + Text("Select a site"), + displayMode: .inline + ) + .navigationBarItems(trailing: + Button(action: { + isHidden.wrappedValue.toggle() + }, label: { + Image(systemName: "xmark.circle") + }) + ) } .opacity(keyboardAnimating ? 0.99 : 1.0) .navigationViewStyle(.stack) From 9c3e34c4a6e2d10ed91b7f329367d4f31714bdd3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 12:25:40 -0700 Subject: [PATCH 0675/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 291 ++++++++++-------- 1 file changed, 162 insertions(+), 129 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index f0d38cd96..2cec54bf7 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -34,13 +34,13 @@ struct SiteAndFacilitySelector: View { var body: some View { if viewModel.sites.count == 1 { - Facilities( + FacilitiesList( facilities: viewModel.sites.first!.facilities, isHidden: isHidden, presentationStyle: .singleSite ) } else { - Sites( + SitesList( isHidden: isHidden, sites: viewModel.sites ) @@ -48,7 +48,7 @@ struct SiteAndFacilitySelector: View { } /// A view displaying the sites contained in a `FloorManager`. - struct Sites: View { + struct SitesList: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel @@ -59,13 +59,14 @@ struct SiteAndFacilitySelector: View { @State private var keyboardAnimating = false - /// A subset of `sites` that contain `searchPhrase`. + /// A subset of `sites` with names containing `searchPhrase` or all `sites` if + /// `searchPhrase` is empty. var matchingSites: [FloorSite] { if searchPhrase.isEmpty { return sites } - return sites.filter { floorSite in - floorSite.name.lowercased().contains(searchPhrase.lowercased()) + return sites.filter { + $0.name.lowercased().contains(searchPhrase.lowercased()) } } @@ -77,41 +78,43 @@ struct SiteAndFacilitySelector: View { let sites: [FloorSite] var body: some View { + siteListAndFilterView + .opacity(keyboardAnimating ? 0.99 : 1.0) + .navigationViewStyle(.stack) + .onReceive( + NotificationCenter.default.publisher( + for: UIResponder.keyboardWillChangeFrameNotification + ) + ) { _ in + withAnimation { + keyboardAnimating = true + } + } + .onReceive( + NotificationCenter.default.publisher( + for: UIResponder.keyboardDidChangeFrameNotification + ) + ) { _ in + withAnimation { + keyboardAnimating = false + } + } + } + + /// A view containing a filter-via-name field, a list of the site names and an "All sites" button. + var siteListAndFilterView: some View { NavigationView { VStack { TextField("Filter sites", text: $searchPhrase) .keyboardType(.alphabet) .disableAutocorrection(true) if matchingSites.isEmpty { - VStack { - Spacer() - Text("No matches found") - Spacer() - } + NoMatchesView() } else { - List(matchingSites) { site in - NavigationLink( - site.name, - tag: site, - selection: $viewModel.selectedSite - ) { - Facilities( - facilities: site.facilities, - isHidden: isHidden, - presentationStyle: .standard - ) - } - .onTapGesture { - viewModel.setSite( - site, - zoomTo: true - ) - } - } - .listStyle(.plain) + siteListView } NavigationLink("All sites") { - Facilities( + FacilitiesList( facilities: sites.flatMap({ $0.facilities }), isHidden: isHidden, presentationStyle: .allSites @@ -124,38 +127,41 @@ struct SiteAndFacilitySelector: View { displayMode: .inline ) .navigationBarItems(trailing: - Button(action: { - isHidden.wrappedValue.toggle() - }, label: { - Image(systemName: "xmark.circle") - }) + CloseButton { isHidden.wrappedValue.toggle() } ) } - .opacity(keyboardAnimating ? 0.99 : 1.0) - .navigationViewStyle(.stack) - .onReceive( - NotificationCenter.default.publisher( - for: UIResponder.keyboardWillChangeFrameNotification + } + + /// A view containing a list of the site names. + /// + /// If `AutomaticSelectionMode` mode is in use, items will automatically be + /// selected/deselected. + var siteListView: some View { + List(matchingSites) { site in + NavigationLink( + site.name, + tag: site, + selection: $viewModel.selectedSite + ) { + FacilitiesList( + facilities: site.facilities, + isHidden: isHidden, + presentationStyle: .standard ) - ) { _ in - withAnimation { - keyboardAnimating = true - } } - .onReceive( - NotificationCenter.default.publisher( - for: UIResponder.keyboardDidChangeFrameNotification - ) - ) { _ in - withAnimation { - keyboardAnimating = false + .onTapGesture { + viewModel.setSite( + site, + zoomTo: true + ) } - } + } + .listStyle(.plain) } } /// A view displaying the facilities contained in a `FloorManager`. - struct Facilities: View { + struct FacilitiesList: View { /// Presentation styles for the facility list. enum PresentationStyle { /// A specific site was selected and the body is presented within a navigation view. @@ -172,16 +178,29 @@ struct SiteAndFacilitySelector: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel + /// Determines the SF Symbols image name to represent selection/non-selection of a facility. + /// - Parameter facility: The facility of interest + /// - Returns: "circle.fill" if the facility is marked selected or "cirlce" if the facility is not selected + /// in the view model. + func imageFor(_ facility: FloorFacility) -> String { + if facility.facilityId == viewModel.selectedFacility?.facilityId { + return "circle.fill" + } else { + return "circle" + } + } + /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding - /// A subset of `facilities` that contain `searchPhrase`. + /// A subset of `facilities` with names containing `searchPhrase` or all + /// `facilities` if `searchPhrase` is empty. var matchingFacilities: [FloorFacility] { if searchPhrase.isEmpty { return facilities } - return facilities.filter { floorFacility in - floorFacility.name.lowercased().contains(searchPhrase.lowercased()) + return facilities.filter { + $0.name.lowercased().contains(searchPhrase.lowercased()) } } @@ -192,38 +211,22 @@ struct SiteAndFacilitySelector: View { @State var searchPhrase: String = "" - /// Determines if a given site is the one marked as selected in the view model. - /// - Parameter facility: The facility of interest - /// - Returns: `true` if the facility is marked as selected in the view model. - func facilityIsSelected(_ facility: FloorFacility) -> Bool { - return facility.facilityId == - viewModel.selectedFacility?.facilityId - } - var body: some View { - if presentationStyle == .standard || - presentationStyle == .allSites { - facilityFilterAndListView + if presentationStyle == .singleSite { + facilityListAndFilterView + } else { + facilityListAndFilterView // Only apply navigation modifiers if this is displayed // within a navigation view .navigationBarTitle("Select a facility") - .navigationBarItems(trailing: closeButtonView) - } else { - facilityFilterAndListView + .navigationBarItems(trailing: + CloseButton { isHidden.wrappedValue.toggle() } + ) } } - /// Closese the site and facility selector. - var closeButtonView: some View { - Button(action: { - isHidden.wrappedValue.toggle() - }, label: { - Image(systemName: "xmark.circle") - }) - } - /// A view containing a label for the site name, a filter-via-name bar and a list of the facility names. - var facilityFilterAndListView: some View { + var facilityListAndFilterView: some View { VStack { HStack { if presentationStyle == .standard { @@ -233,68 +236,98 @@ struct SiteAndFacilitySelector: View { } else if presentationStyle == .singleSite { Text(facilities.first?.site?.name ?? "N/A") Spacer() - closeButtonView + CloseButton { isHidden.wrappedValue.toggle() } } } TextField("Filter facilities", text: $searchPhrase) .keyboardType(.alphabet) .disableAutocorrection(true) if matchingFacilities.isEmpty { - VStack { - Spacer() - Text("No matches found") - Spacer() - } + NoMatchesView() } else { - ScrollViewReader { proxy in - List(matchingFacilities, id: \.facilityId) { facility in - Button { - viewModel.setFacility( - facility, - zoomTo: true + facilityListView + } + } + } + + /// 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 slighlty different + /// appearance. + /// + /// If `AutomaticSelectionMode` mode is in use, this list will automatically scroll to the + /// selected item. + var facilityListView: some View { + ScrollViewReader { proxy in + List(matchingFacilities, id: \.facilityId) { facility in + Button { + viewModel.setFacility( + facility, + zoomTo: true + ) + isHidden.wrappedValue.toggle() + } label: { + HStack { + Image(systemName: imageFor(facility)) + VStack { + Text(facility.name) + .fontWeight(.regular) + .frame( + maxWidth: .infinity, + alignment: .leading ) - isHidden.wrappedValue.toggle() - } label: { - HStack { - Image( - systemName: - facilityIsSelected(facility) ? "circle.fill" : "circle" + if presentationStyle == .allSites, + let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + .frame( + maxWidth: .infinity, + alignment: .leading ) - VStack { - Text(facility.name) - .fontWeight(.regular) - .frame( - maxWidth: .infinity, - alignment: .leading - ) - if presentationStyle == .allSites, - let siteName = facility.site?.name { - Text(siteName) - .fontWeight(.ultraLight) - .frame( - maxWidth: .infinity, - alignment: .leading - ) - } - } } } } - .listStyle(.plain) - .onChange(of: viewModel.selectedFacility) { - guard let facility = $0 else { - return - } - withAnimation { - proxy.scrollTo( - facility.facilityId, - anchor: .center - ) - } - } } } + .listStyle(.plain) + .onChange(of: viewModel.selectedFacility) { + guard let facility = $0 else { + return + } + withAnimation { + proxy.scrollTo( + facility.facilityId, + anchor: .center + ) + } + } } } } } + +/// Displays text "No matches found". +struct NoMatchesView: View { + var body: some View { + VStack { + Spacer() + Text("No matches found") + Spacer() + } + } +} + +/// A custom button with an "X" enclosed within a circle to be used as a "close" button. +struct CloseButton: View { + /// The button's action to be performed when tapped. + var action: (() -> Void) + + var body: some View { + Button { + action() + } label: { + Image(systemName: "xmark.circle") + } + } +} From dc469a570f21ca013d7e3fac61ee6b3032e87fcb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 12:33:28 -0700 Subject: [PATCH 0676/1722] Update SiteAndFacilitySelector.swift Ordering --- .../FloorFilter/SiteAndFacilitySelector.swift | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 2cec54bf7..b8bb3dd00 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -18,10 +18,6 @@ import ArcGIS struct SiteAndFacilitySelector: View { /// Creates a `SiteAndFacilitySelector` /// - Parameter isHidden: A binding used to dismiss the site selector. - /// - Parameter selectedFacilityID: Indicates the implicity selected facility based on the - /// current viewpoint. - /// - Parameter selectedSiteID: Indicates the implicity selected site based on the current - /// viewpoint. init(isHidden: Binding) { self.isHidden = isHidden } @@ -52,13 +48,20 @@ struct SiteAndFacilitySelector: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel - /// Allows the user to toggle the visibility of the site and facility selector. - var isHidden: Binding - /// Indicates that the keyboard is animating and some views may require reload. @State private var keyboardAnimating = false + /// A site name filter phrase entered by the user. + @State + private var searchPhrase: String = "" + + /// Sites contained in a `FloorManager`. + let sites: [FloorSite] + + /// Allows the user to toggle the visibility of the site and facility selector. + var isHidden: Binding + /// A subset of `sites` with names containing `searchPhrase` or all `sites` if /// `searchPhrase` is empty. var matchingSites: [FloorSite] { @@ -70,15 +73,10 @@ struct SiteAndFacilitySelector: View { } } - /// A site filtering phrase entered by the user. - @State - var searchPhrase: String = "" - - /// Sites contained in a `FloorManager`. - let sites: [FloorSite] - var body: some View { siteListAndFilterView + // Trigger a reload on keyboard frame changes for proper layout + // across all devices. .opacity(keyboardAnimating ? 0.99 : 1.0) .navigationViewStyle(.stack) .onReceive( @@ -162,6 +160,9 @@ struct SiteAndFacilitySelector: View { /// A view displaying the facilities contained in a `FloorManager`. struct FacilitiesList: View { + /// The view model used by this selector. + @EnvironmentObject var viewModel: FloorFilterViewModel + /// Presentation styles for the facility list. enum PresentationStyle { /// A specific site was selected and the body is presented within a navigation view. @@ -172,23 +173,15 @@ struct SiteAndFacilitySelector: View { case singleSite } + /// A facility name filter phrase entered by the user. + @State + var searchPhrase: String = "" + /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] - /// The view model used by this selector. - @EnvironmentObject var viewModel: FloorFilterViewModel - - /// Determines the SF Symbols image name to represent selection/non-selection of a facility. - /// - Parameter facility: The facility of interest - /// - Returns: "circle.fill" if the facility is marked selected or "cirlce" if the facility is not selected - /// in the view model. - func imageFor(_ facility: FloorFacility) -> String { - if facility.facilityId == viewModel.selectedFacility?.facilityId { - return "circle.fill" - } else { - return "circle" - } - } + /// The selected presentation style for the facility list. + let presentationStyle: PresentationStyle /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding @@ -204,12 +197,17 @@ struct SiteAndFacilitySelector: View { } } - /// The selected presentation style for the facility list. - let presentationStyle: PresentationStyle - - /// A facility filtering phrase entered by the user. - @State - var searchPhrase: String = "" + /// Determines the SF Symbols image name to represent selection/non-selection of a facility. + /// - Parameter facility: The facility of interest + /// - Returns: "circle.fill" if the facility is marked selected or "cirlce" if the facility is not selected + /// in the view model. + func imageFor(_ facility: FloorFacility) -> String { + if facility.facilityId == viewModel.selectedFacility?.facilityId { + return "circle.fill" + } else { + return "circle" + } + } var body: some View { if presentationStyle == .singleSite { From f8594088d2de196d25409de4e8a4e417950772f7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 12:34:49 -0700 Subject: [PATCH 0677/1722] Update SiteAndFacilitySelector.swift Ordering --- .../FloorFilter/SiteAndFacilitySelector.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index b8bb3dd00..97cb748e5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -32,13 +32,13 @@ struct SiteAndFacilitySelector: View { if viewModel.sites.count == 1 { FacilitiesList( facilities: viewModel.sites.first!.facilities, - isHidden: isHidden, - presentationStyle: .singleSite + presentationStyle: .singleSite, + isHidden: isHidden ) } else { SitesList( - isHidden: isHidden, - sites: viewModel.sites + sites: viewModel.sites, + isHidden: isHidden ) } } @@ -114,8 +114,8 @@ struct SiteAndFacilitySelector: View { NavigationLink("All sites") { FacilitiesList( facilities: sites.flatMap({ $0.facilities }), - isHidden: isHidden, - presentationStyle: .allSites + presentationStyle: .allSites, + isHidden: isHidden ) } .padding([.vertical], 4) @@ -143,8 +143,8 @@ struct SiteAndFacilitySelector: View { ) { FacilitiesList( facilities: site.facilities, - isHidden: isHidden, - presentationStyle: .standard + presentationStyle: .standard, + isHidden: isHidden ) } .onTapGesture { From 602f5a9e044d8b09414245d740618c05dc3e44c9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 12:44:50 -0700 Subject: [PATCH 0678/1722] Create LevelSelector.swift --- .../FloorFilter/LevelSelector.swift | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift new file mode 100644 index 000000000..428c4324f --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -0,0 +1,134 @@ +// 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 view which allows selection of levels represented in `FloorFacility`. +struct LevelSelector: View { + /// The alignment configuration. + var topAligned: Bool + + /// The levels to display. + let levels: [FloorLevel] + + /// 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. + @Binding + var isCollapsed: Bool + + /// The view model used by the `LevelsView`. + @EnvironmentObject var viewModel: FloorFilterViewModel + + /// The height of the scroll view's content. + @State + private var scrollViewContentHeight: CGFloat = .zero + + /// Returns the short name of the currently selected level, the first level or "None" if none of the listed + /// are available. + private var selectedLevelName: String { + if let shortName = viewModel.selectedLevel?.shortName { + return shortName + } else if let firstLevelShortName = levels.first?.shortName { + return firstLevelShortName + } else { + return "None" + } + } + + public var body: some View { + VStack { + if !isCollapsed, + levels.count > 1 { + if !topAligned { + CollapseButton(isCollapsed: $isCollapsed) + Divider() + .frame(width: 30) + } + ScrollView { + LevelsStack(levels: levels) + .background( + GeometryReader { geometry -> Color in + DispatchQueue.main.async { + scrollViewContentHeight = geometry.size.height + } + return .clear + } + ) + } + .frame(maxHeight: scrollViewContentHeight) + if topAligned { + Divider() + .frame(width: 30) + CollapseButton(isCollapsed: $isCollapsed) + } + } else { + Button { + if levels.count > 1 { + isCollapsed.toggle() + } + } label: { + Text(selectedLevelName) + .lineLimit(1) + } + .selected(true) + } + } + } +} + +/// A vertical list of floor levels. +struct LevelsStack: View { + let levels: [FloorLevel] + + /// The view model used by the `LevelsView`. + @EnvironmentObject var viewModel: FloorFilterViewModel + + var body: some View { + VStack { + ForEach(levels) { level in + Button { + viewModel.setLevel(level) + } label: { + Text(level.shortName) + .lineLimit(1) + } + .selected(level == viewModel.selectedLevel) + } + } + } +} + +/// A button used to collapse the floor level list. +struct CollapseButton: View { + /// Allows the user to toggle the visibility of the site and facility selector. + @Binding + var isCollapsed: Bool + + var body: some View { + Button { + withAnimation { + isCollapsed.toggle() + } + } label: { + Image(systemName: "xmark") + } + .padding(EdgeInsets( + top: 2, + leading: 4, + bottom: 2, + trailing: 4 + )) + } +} From b7985f3ce3c0ad8d2bbbaeb78f0d5b52bbfd6e61 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 12:44:53 -0700 Subject: [PATCH 0679/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 137 ++---------------- 1 file changed, 9 insertions(+), 128 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 1f2280f68..165ce0570 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -39,6 +39,14 @@ public struct FloorFilter: View { self.viewpoint = viewpoint } + /// A Boolean value that indicates whether the levels view is currently collapsed. + @State + private var isLevelsViewCollapsed: Bool = false + + /// A Boolean value that indicates whether the site/facility selector is hidden. + @State + private var isSelectorHidden: Bool = true + /// The alignment configuration. private let alignment: Alignment @@ -49,14 +57,6 @@ public struct FloorFilter: View { viewModel.selectedFacility!.levels.isEmpty) } - /// A Boolean value that indicates whether the site/facility selector is hidden. - @State - private var isSelectorHidden: Bool = true - - /// A Boolean value that indicates whether the levels view is currently collapsed. - @State - private var isLevelsViewCollapsed: Bool = false - /// Displays the available levels. private var levelsAndDividerView: some View { Group { @@ -65,7 +65,7 @@ public struct FloorFilter: View { Divider() .frame(width: 30) } - LevelsView( + LevelSelector( topAligned: topAligned, levels: sortedLevels, isCollapsed: $isLevelsViewCollapsed @@ -183,122 +183,3 @@ public struct FloorFilter: View { .environmentObject(viewModel) } } - -/// A view displaying the levels in the selected facility. -struct LevelsView: View { - /// The alignment configuration. - var topAligned: Bool - - /// The levels to display. - let levels: [FloorLevel] - - /// 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. - @Binding - var isCollapsed: Bool - - /// The view model used by the `LevelsView`. - @EnvironmentObject var viewModel: FloorFilterViewModel - - /// The height of the scroll view's content. - @State - private var scrollViewContentHeight: CGFloat = .zero - - /// Returns the short name of the currently selected level, the first level or "None" if none of the listed - /// are available. - private var selectedLevelName: String { - if let shortName = viewModel.selectedLevel?.shortName { - return shortName - } else if let firstLevelShortName = levels.first?.shortName { - return firstLevelShortName - } else { - return "None" - } - } - - public var body: some View { - VStack { - if !isCollapsed, - levels.count > 1 { - if !topAligned { - CollapseButton(isCollapsed: $isCollapsed) - Divider() - .frame(width: 30) - } - ScrollView { - LevelsStack(levels: levels) - .background( - GeometryReader { geometry -> Color in - DispatchQueue.main.async { - scrollViewContentHeight = geometry.size.height - } - return .clear - } - ) - } - .frame(maxHeight: scrollViewContentHeight) - if topAligned { - Divider() - .frame(width: 30) - CollapseButton(isCollapsed: $isCollapsed) - } - } else { - Button { - if levels.count > 1 { - isCollapsed.toggle() - } - } label: { - Text(selectedLevelName) - .lineLimit(1) - } - .selected(true) - } - } - } -} - -/// A vertical list of floor levels. -struct LevelsStack: View { - let levels: [FloorLevel] - - /// The view model used by the `LevelsView`. - @EnvironmentObject var viewModel: FloorFilterViewModel - - var body: some View { - VStack { - ForEach(levels) { level in - Button { - viewModel.setLevel(level) - } label: { - Text(level.shortName) - .lineLimit(1) - } - .selected(level == viewModel.selectedLevel) - } - } - } -} - -/// A button used to collapse the floor level list. -struct CollapseButton: View { - /// Allows the user to toggle the visibility of the site and facility selector. - @Binding - var isCollapsed: Bool - - var body: some View { - Button { - withAnimation { - isCollapsed.toggle() - } - } label: { - Image(systemName: "xmark") - } - .padding(EdgeInsets( - top: 2, - leading: 4, - bottom: 2, - trailing: 4 - )) - } -} From 46185a4474d031a803fba477d8a34e045941a631 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 12:52:05 -0700 Subject: [PATCH 0680/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 165ce0570..180e92dd7 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -39,6 +39,10 @@ public struct FloorFilter: View { self.viewpoint = viewpoint } + /// 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: Bool = false @@ -50,15 +54,25 @@ public struct FloorFilter: View { /// The alignment configuration. private let alignment: Alignment - /// 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. + /// Button to open and close the site and facility selector. + private var facilityButtonView: some View { + Button { + isSelectorHidden.toggle() + } label: { + Image(systemName: "building.2") + } + .frame(maxWidth: .infinity) + } + + /// 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. private var hasLevelsToDisplay: Bool { !(viewModel.selectedFacility == nil || viewModel.selectedFacility!.levels.isEmpty) } /// Displays the available levels. - private var levelsAndDividerView: some View { + private var levelsSelectorView: some View { Group { if hasLevelsToDisplay { if topAligned { @@ -78,16 +92,6 @@ public struct FloorFilter: View { } } - /// Button to open and close the site and facility selector. - private var facilityButtonView: some View { - Button { - isSelectorHidden.toggle() - } label: { - Image(systemName: "building.2") - } - .frame(maxWidth: .infinity) - } - /// A view that allows selecting between levels. private var levelSelectorView: some View { VStack { @@ -97,9 +101,9 @@ public struct FloorFilter: View { VStack { if topAligned { facilityButtonView - levelsAndDividerView + levelsSelectorView } else { - levelsAndDividerView + levelsSelectorView facilityButtonView } } @@ -149,10 +153,6 @@ public struct FloorFilter: View { } } - /// The view model used by the `FloorFilter`. - @StateObject - private var viewModel: FloorFilterViewModel - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. private var viewpoint: Binding From 452216db11d01d115e99c8b2dda94a2e98e5e29f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 13:02:20 -0700 Subject: [PATCH 0681/1722] Update FloorFilterViewModel.swift Organization --- .../FloorFilter/FloorFilterViewModel.swift | 193 +++++++++--------- 1 file changed, 102 insertions(+), 91 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index fad2a3f1a..74adf34ad 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -45,20 +45,33 @@ final class FloorFilterViewModel: ObservableObject { } } + // MARK: Published members + + /// The currently selected `FloorFacility`. + @Published + var selectedFacility: FloorFacility? + + /// The currently selected `FloorLevel`. + @Published + var selectedLevel: FloorLevel? + + /// The currently selected `FloorSite`. + @Published + var selectedSite: FloorSite? + + /// `true` if the model is loading it's properties, `false` if not loading. + @Published + private(set) var isLoading = true + + // MARK: Constants + /// The selection behavior of the floor filter. private let automaticSelectionMode: AutomaticSelectionMode - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. - /// If `nil`, there will be no automatic pan/zoom operations. - var viewpoint: Binding - /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager - /// The floor manager sites. - var sites: [FloorSite] { - floorManager.sites - } + // MARK: Public members /// The floor manager facilities. var facilities: [FloorFacility] { @@ -70,54 +83,16 @@ final class FloorFilterViewModel: ObservableObject { floorManager.levels } - /// `true` if the model is loading it's properties, `false` if not loading. - @Published - private(set) var isLoading = true - - /// Gets the default level for a facility. - /// - Parameter facility: The facility to get the default level for. - /// - Returns: The default level for the facility, which is the level with vertical order 0; - /// if there's no level with vertical order of 0, it returns the lowest level. - func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { - return levels.first(where: { level in - level.facility == facility && level.verticalOrder == .zero - }) ?? lowestLevel() - } - - /// Returns the level with the lowest vertical order. - private func lowestLevel() -> FloorLevel? { - let sortedLevels = levels.sorted { - $0.verticalOrder < $1.verticalOrder - } - return sortedLevels.first { - $0.verticalOrder != .min && $0.verticalOrder != .max - } + /// The floor manager sites. + var sites: [FloorSite] { + floorManager.sites } - @Published - var selectedSite: FloorSite? - - @Published - var selectedFacility: FloorFacility? - - @Published - var selectedLevel: FloorLevel? + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// If `nil`, there will be no automatic pan/zoom operations. + var viewpoint: Binding - // MARK: Set selection methods - - /// Updates the selected site, facility, and level based on a newly selected site. - /// - Parameters: - /// - floorSite: The selected site. - /// - zoomTo: The viewpoint should be updated to show to the extent of this site. - func setSite( - _ floorSite: FloorSite?, - zoomTo: Bool = false - ) { - selectedSite = floorSite - if zoomTo { - zoomToExtent(extent: floorSite?.geometry?.extent) - } - } + // MARK: Public methods /// Updates the selected site, facility, and level based on a newly selected facility. /// - Parameters: @@ -148,6 +123,20 @@ final class FloorFilterViewModel: ObservableObject { filterMapToSelectedLevel() } + /// Updates the selected site, facility, and level based on a newly selected site. + /// - Parameters: + /// - floorSite: The selected site. + /// - zoomTo: The viewpoint should be updated to show to the extent of this site. + func setSite( + _ floorSite: FloorSite?, + zoomTo: Bool = false + ) { + selectedSite = floorSite + if zoomTo { + zoomToExtent(extent: floorSite?.geometry?.extent) + } + } + /// Updates `selectedSite` and `selectedFacility` based on the latest viewpoint position. func updateSelection() { guard let viewpoint = viewpoint.wrappedValue, @@ -159,43 +148,33 @@ final class FloorFilterViewModel: ObservableObject { updateSelectedFacility() } - /// Updates `selectedSite` if a good selection exists. - private func updateSelectedSite() { - // Only select a facility if it is within minimum scale. Default at 4300. - let siteMinScale: Double - if let minScale = floorManager.siteLayer?.minScale, - minScale != .zero { - siteMinScale = minScale - } else { - siteMinScale = 4300 - } + // MARK: Private methods - // If viewpoint is out of range, reset selection and return early. - if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { - if automaticSelectionMode == .always { - setSite(nil) - setFacility(nil) - setLevel(nil) - } - return - } + /// Gets the default level for a facility. + /// - Parameter facility: The facility to get the default level for. + /// - Returns: The default level for the facility, which is the level with vertical order 0; + /// if there's no level with vertical order of 0, it returns the lowest level. + private func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { + return levels.first(where: { level in + level.facility == facility && level.verticalOrder == .zero + }) ?? lowestLevel() + } - // If the centerpoint is within a site's geometry, select that site. - let siteResult = floorManager.sites.first { site in - guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, - let extent2 = site.geometry?.extent else { - return false - } - return GeometryEngine.intersects( - geometry1: extent1, - geometry2: extent2 - ) + /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. + private func filterMapToSelectedLevel() { + guard let selectedLevel = selectedLevel else { return } + levels.forEach { + $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder } + } - if let siteResult = siteResult { - setSite(siteResult) - } else if automaticSelectionMode == .always { - setSite(nil) + /// Returns the level with the lowest vertical order. + private func lowestLevel() -> FloorLevel? { + let sortedLevels = levels.sorted { + $0.verticalOrder < $1.verticalOrder + } + return sortedLevels.first { + $0.verticalOrder != .min && $0.verticalOrder != .max } } @@ -233,11 +212,43 @@ 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() { - guard let selectedLevel = selectedLevel else { return } - levels.forEach { - $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder + /// Updates `selectedSite` if a good selection exists. + private func updateSelectedSite() { + // Only select a facility if it is within minimum scale. Default at 4300. + let siteMinScale: Double + if let minScale = floorManager.siteLayer?.minScale, + minScale != .zero { + siteMinScale = minScale + } else { + siteMinScale = 4300 + } + + // If viewpoint is out of range, reset selection and return early. + if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { + if automaticSelectionMode == .always { + setSite(nil) + setFacility(nil) + setLevel(nil) + } + return + } + + // If the centerpoint is within a site's geometry, select that site. + let siteResult = floorManager.sites.first { site in + guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, + let extent2 = site.geometry?.extent else { + return false + } + return GeometryEngine.intersects( + geometry1: extent1, + geometry2: extent2 + ) + } + + if let siteResult = siteResult { + setSite(siteResult) + } else if automaticSelectionMode == .always { + setSite(nil) } } From 57c6755a30a89cd1a30888b48759b0047cc42d65 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 13:06:46 -0700 Subject: [PATCH 0682/1722] Update LevelSelector.swift Organization --- .../FloorFilter/LevelSelector.swift | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 428c4324f..a561bd81c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -16,11 +16,8 @@ import ArcGIS /// A view which allows selection of levels represented in `FloorFacility`. struct LevelSelector: View { - /// The alignment configuration. - var topAligned: Bool - - /// The levels to display. - let levels: [FloorLevel] + /// The view model used by the `LevelsView`. + @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 @@ -28,13 +25,13 @@ struct LevelSelector: View { @Binding var isCollapsed: Bool - /// The view model used by the `LevelsView`. - @EnvironmentObject var viewModel: FloorFilterViewModel - /// The height of the scroll view's content. @State private var scrollViewContentHeight: CGFloat = .zero + /// The levels to display. + let levels: [FloorLevel] + /// Returns the short name of the currently selected level, the first level or "None" if none of the listed /// are available. private var selectedLevelName: String { @@ -47,6 +44,9 @@ struct LevelSelector: View { } } + /// The alignment configuration. + var topAligned: Bool + public var body: some View { VStack { if !isCollapsed, @@ -90,11 +90,12 @@ struct LevelSelector: View { /// A vertical list of floor levels. struct LevelsStack: View { - let levels: [FloorLevel] - /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel + /// The levels to display. + let levels: [FloorLevel] + var body: some View { VStack { ForEach(levels) { level in From 8e9fe312b93e25598b42ac0c9a76716c4132862f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 13:06:49 -0700 Subject: [PATCH 0683/1722] Update FloorFilter.swift --- .../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 180e92dd7..461fc9495 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -80,9 +80,9 @@ public struct FloorFilter: View { .frame(width: 30) } LevelSelector( - topAligned: topAligned, + isCollapsed: $isLevelsViewCollapsed, levels: sortedLevels, - isCollapsed: $isLevelsViewCollapsed + topAligned: topAligned ) if !topAligned { Divider() From 81b77ac345bed131c7a1733e114c7cee4dd58d8f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 13:15:14 -0700 Subject: [PATCH 0684/1722] Update LevelSelector.swift --- .../Components/FloorFilter/LevelSelector.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index a561bd81c..3b8e6a036 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -57,15 +57,12 @@ struct LevelSelector: View { .frame(width: 30) } ScrollView { - LevelsStack(levels: levels) - .background( - GeometryReader { geometry -> Color in - DispatchQueue.main.async { - scrollViewContentHeight = geometry.size.height - } - return .clear - } - ) + VStack { + LevelsStack(levels: levels) + } + .onSizeChange { + scrollViewContentHeight = $0.height + } } .frame(maxHeight: scrollViewContentHeight) if topAligned { From e4f15a5c4b3b0e2c6d8a72336380c2ca9f9da881 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 13:31:49 -0700 Subject: [PATCH 0685/1722] Update FloorFilterViewModel.swift Unmark private for testing --- .../FloorFilter/FloorFilterViewModel.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 74adf34ad..1e8f7eab0 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -94,6 +94,16 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Public methods + /// Gets the default level for a facility. + /// - Parameter facility: The facility to get the default level for. + /// - Returns: The default level for the facility, which is the level with vertical order 0; + /// if there's no level with vertical order of 0, it returns the lowest level. + func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { + return levels.first(where: { level in + level.facility == facility && level.verticalOrder == .zero + }) ?? lowestLevel() + } + /// Updates the selected site, facility, and level based on a newly selected facility. /// - Parameters: /// - floorFacility: The selected facility. @@ -150,16 +160,6 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Private methods - /// Gets the default level for a facility. - /// - Parameter facility: The facility to get the default level for. - /// - Returns: The default level for the facility, which is the level with vertical order 0; - /// if there's no level with vertical order of 0, it returns the lowest level. - private func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { - return levels.first(where: { level in - level.facility == facility && level.verticalOrder == .zero - }) ?? lowestLevel() - } - /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. private func filterMapToSelectedLevel() { guard let selectedLevel = selectedLevel else { return } From e13fd4b1008f58b796b7ec71ddfcdb2a35b145d2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 13:32:00 -0700 Subject: [PATCH 0686/1722] Update FloorFilterViewModelTests.swift Fix tests --- .../FloorFilterViewModelTests.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 2843a6c7a..a104e17ee 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -29,7 +29,12 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - let viewModel = await FloorFilterViewModel(floorManager: floorManager) + var _viewpoint: Viewpoint? = getViewpoint(.zero) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) await verifyInitialization(viewModel) let sites = await viewModel.sites let facilities = await viewModel.facilities @@ -46,7 +51,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - var _viewpoint: Viewpoint = getViewpoint(.zero) + var _viewpoint: Viewpoint? = getViewpoint(.zero) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( floorManager: floorManager, @@ -70,7 +75,7 @@ class FloorFilterViewModelTests: XCTestCase { return } let initialViewpoint = getViewpoint(.zero) - var _viewpoint = initialViewpoint + var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( floorManager: floorManager, @@ -86,12 +91,12 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedFacility) XCTAssertNil(selectedLevel) XCTAssertEqual( - _viewpoint.targetGeometry.extent.center.x, + _viewpoint?.targetGeometry.extent.center.x, initialViewpoint.targetGeometry.extent.center.x ) await viewModel.setSite(site, zoomTo: true) XCTAssertEqual( - _viewpoint.targetGeometry.extent.center.x, + _viewpoint?.targetGeometry.extent.center.x, selectedSite?.geometry?.extent.center.x ) } @@ -103,29 +108,28 @@ class FloorFilterViewModelTests: XCTestCase { return } let initialViewpoint = getViewpoint(.zero) - var _viewpoint = initialViewpoint + var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( + automaticSelectionMode: .never, floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) let facility = await viewModel.facilities.first await viewModel.setFacility(facility) - let selectedSite = await viewModel.selectedSite let selectedFacility = await viewModel.selectedFacility let selectedLevel = await viewModel.selectedLevel let defaultLevel = await viewModel.defaultLevel(for: selectedFacility) - XCTAssertEqual(selectedSite, selectedFacility?.site) XCTAssertEqual(selectedFacility, facility) XCTAssertEqual(selectedLevel, defaultLevel) XCTAssertEqual( - _viewpoint.targetGeometry.extent.center.x, + _viewpoint?.targetGeometry.extent.center.x, initialViewpoint.targetGeometry.extent.center.x ) await viewModel.setFacility(facility, zoomTo: true) XCTAssertEqual( - _viewpoint.targetGeometry.extent.center.x, + _viewpoint?.targetGeometry.extent.center.x, selectedFacility?.geometry?.extent.center.x ) } @@ -137,7 +141,7 @@ class FloorFilterViewModelTests: XCTestCase { return } let initialViewpoint = getViewpoint(.zero) - var _viewpoint = initialViewpoint + var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( floorManager: floorManager, @@ -147,14 +151,10 @@ class FloorFilterViewModelTests: XCTestCase { let levels = await viewModel.levels let level = levels.first await viewModel.setLevel(level) - let selectedSite = await viewModel.selectedSite - let selectedFacility = await viewModel.selectedFacility let selectedLevel = await viewModel.selectedLevel - XCTAssertEqual(selectedSite, selectedFacility?.site) - XCTAssertEqual(selectedFacility, level?.facility) XCTAssertEqual(selectedLevel, level) XCTAssertEqual( - _viewpoint.targetGeometry.extent.center.x, + _viewpoint?.targetGeometry.extent.center.x, initialViewpoint.targetGeometry.extent.center.x ) levels.forEach { level in From 6cbdad3e07c9dcaae79b617c3187de6cf4c19749 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Sat, 19 Mar 2022 14:14:03 -0700 Subject: [PATCH 0687/1722] Update FloorFilterViewModelTests.swift Add autoselection tests --- .../FloorFilterViewModelTests.swift | 149 ++++++++++++++++-- 1 file changed, 136 insertions(+), 13 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index a104e17ee..abe9cb83d 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -29,7 +29,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - var _viewpoint: Viewpoint? = getViewpoint(.zero) + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( floorManager: floorManager, @@ -51,7 +51,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - var _viewpoint: Viewpoint? = getViewpoint(.zero) + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( floorManager: floorManager, @@ -74,7 +74,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - let initialViewpoint = getViewpoint(.zero) + let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( @@ -107,7 +107,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - let initialViewpoint = getViewpoint(.zero) + let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( @@ -140,7 +140,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - let initialViewpoint = getViewpoint(.zero) + let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = await FloorFilterViewModel( @@ -166,6 +166,130 @@ class FloorFilterViewModelTests: XCTestCase { } } + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testAutoSelectAlways() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { + return + } + let viewpointLosAngeles = Viewpoint( + center: Point( + x: -13164116.3284, + y: 4034465.8065, + spatialReference: .webMercator + ), + scale: 10_000 + ) + var _viewpoint: Viewpoint? = viewpointLosAngeles + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + automaticSelectionMode: .always, + floorManager: floorManager, + viewpoint: viewpoint + ) + await verifyInitialization(viewModel) + + // Viewpoint is Los Angeles, selection should be nil + var selectedFacility = await viewModel.selectedFacility + var selectedSite = await viewModel.selectedSite + XCTAssertNil(selectedFacility) + XCTAssertNil(selectedSite) + + // Viewpoint is Redlands Main Q + _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + await viewModel.updateSelection() + selectedFacility = await viewModel.selectedFacility + selectedSite = await viewModel.selectedSite + XCTAssertEqual(selectedSite?.name, "Redlands Main") + XCTAssertEqual(selectedFacility?.name, "Q") + + // Viewpoint is Los Angeles, selection should be nil + _viewpoint = viewpointLosAngeles + await viewModel.updateSelection() + selectedFacility = await viewModel.selectedFacility + selectedSite = await viewModel.selectedSite + XCTAssertNil(selectedSite) + XCTAssertNil(selectedFacility) + } + + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testAutoSelectAlwaysNotClearing() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { + return + } + let viewpointLosAngeles = Viewpoint( + center: Point( + x: -13164116.3284, + y: 4034465.8065, + spatialReference: .webMercator + ), + scale: 10_000 + ) + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + automaticSelectionMode: .alwaysNotClearing, + floorManager: floorManager, + viewpoint: viewpoint + ) + await verifyInitialization(viewModel) + + // Viewpoint is Redlands Main Q + _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + await viewModel.updateSelection() + var selectedFacility = await viewModel.selectedFacility + var selectedSite = await viewModel.selectedSite + XCTAssertEqual(selectedSite?.name, "Redlands Main") + XCTAssertEqual(selectedFacility?.name, "Q") + + // Viewpoint is Los Angeles, but selection should remain Redlands Main Q + _viewpoint = viewpointLosAngeles + await viewModel.updateSelection() + selectedFacility = await viewModel.selectedFacility + selectedSite = await viewModel.selectedSite + XCTAssertEqual(selectedSite?.name, "Redlands Main") + XCTAssertEqual(selectedFacility?.name, "Q") + } + + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testAutoSelectNever() async { + guard let map = await makeMap(), + let floorManager = map.floorManager else { + return + } + let viewpointLosAngeles = Viewpoint( + center: Point( + x: -13164116.3284, + y: 4034465.8065, + spatialReference: .webMercator + ), + scale: 10_000 + ) + var _viewpoint: Viewpoint? = viewpointLosAngeles + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = await FloorFilterViewModel( + automaticSelectionMode: .never, + floorManager: floorManager, + viewpoint: viewpoint + ) + await verifyInitialization(viewModel) + + // Viewpoint is Los Angeles, selection should be nil + var selectedFacility = await viewModel.selectedFacility + var selectedSite = await viewModel.selectedSite + XCTAssertNil(selectedFacility) + XCTAssertNil(selectedSite) + + // Viewpoint is Redlands Main Q but selection should still be nil + _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + await viewModel.updateSelection() + selectedFacility = await viewModel.selectedFacility + selectedSite = await viewModel.selectedSite + XCTAssertNil(selectedFacility) + XCTAssertNil(selectedSite) + } + /// Get a map constructed from an ArcGIS portal item. /// - Returns: A map constructed from an ArcGIS portal item. private func makeMap() async -> Map? { @@ -209,20 +333,19 @@ class FloorFilterViewModelTests: XCTestCase { } extension FloorFilterViewModelTests { - /// An arbitrary point to use for testing. + /// The coordinates for the Redlands Esri campus. 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 + Point( + x: -13046157.242121734, + y: 4036329.622884897, + spatialReference: .webMercator + ) } /// Builds viewpoints to use for tests. /// - Parameter rotation: The rotation to use for the resulting viewpoint. /// - Returns: A viewpoint object for tests. - func getViewpoint(_ rotation: Double) -> Viewpoint { + func getEsriRedlandsViewpoint(_ rotation: Double = .zero, scale: Double = 10_000) -> Viewpoint { return Viewpoint(center: point, scale: scale, rotation: rotation) } } From 3bee4434dc63cde79f098fcf3a8d8e16edddad9c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 21 Mar 2022 20:14:07 -0700 Subject: [PATCH 0688/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 1c9647ae8..4760cf028 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -44,9 +44,9 @@ public struct Bookmarks: View { /// If non-`nil`, this viewpoint is updated when a bookmark is selected. private var viewpoint: Binding? - /// Sets a closure to perform when the bookmark selection changes. + /// Sets an action to perform when the bookmark selection changes. /// - Parameters: - /// - action: The closure to perform when the bookmark selection has changed. + /// - action: The action to perform when the bookmark selection has changed. public func onSelectionChanged( perform action: @escaping (Bookmark) -> Void ) -> Bookmarks { From 9df91e3e38a5f4a08bfab774f456953be1793c2a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 21 Mar 2022 20:14:18 -0700 Subject: [PATCH 0689/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 4760cf028..f3feda3d8 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -59,7 +59,6 @@ public struct Bookmarks: View { /// /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint /// binding (if provided) or calling the closure provided by the `onSelectionChanged` modifier. - /// `onSelectionChanged` modifier. /// - Parameter bookmark: The bookmark that was selected. func selectBookmark(_ bookmark: Bookmark) { isPresented = false From 26b6c9b6ebc2dccdb4753317ccfe236289af6654 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 21 Mar 2022 20:14:49 -0700 Subject: [PATCH 0690/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Mark Dostal --- 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 f3feda3d8..ffa796d7f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -58,7 +58,7 @@ public struct Bookmarks: View { /// Performs the necessary actions when a bookmark is selected. /// /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint - /// binding (if provided) or calling the closure provided by the `onSelectionChanged` modifier. + /// binding (if provided) or calling the action provided by the `onSelectionChanged` modifier. /// - Parameter bookmark: The bookmark that was selected. func selectBookmark(_ bookmark: Bookmark) { isPresented = false From aada17aa6c16116a6208c857a1cc7162e75a0075 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 09:33:57 -0700 Subject: [PATCH 0691/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Mark Dostal --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 2ca51478e..717cf8067 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -23,7 +23,7 @@ struct BookmarksExampleView: View { private var verticalSizeClass: UserInterfaceSizeClass? /// A web map with predefined bookmarks. - private let map = Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + private let map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! /// Indicates if the bookmarks list is shown or not. /// - Remark: This allows a developer to control how the bookmarks menu is shown/hidden, From 19426203cb5d9fa9fdaa0d4e91f70df76b7e11b4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 09:35:29 -0700 Subject: [PATCH 0692/1722] Update BookmarksTests.swift --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 3cb481064..0848533a4 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -191,7 +191,7 @@ extension BookmarksTests { /// A web map authored with bookmarks for testing. var webMap: Map { - return Map(url: URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + return Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! } /// Builds viewpoints to use for tests. From e72a41fd869aa16d315df426cc24c971f24dd962 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 09:38:06 -0700 Subject: [PATCH 0693/1722] `onSelectionChanged` -> `selectionChangedAction` --- .../Components/Bookmarks/Bookmarks.swift | 14 +++++++------- Tests/ArcGISToolkitTests/BookmarksTests.swift | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index ffa796d7f..0ac91d039 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -31,15 +31,15 @@ public struct Bookmarks: View { @Binding private var isPresented: Bool + /// A bookmark that was selected. + @State + private var selectedBookmark: Bookmark? = nil + /// User defined action to be performed when a bookmark is selected. /// /// Use this when you prefer to self-manage the response to a bookmark selection. Use either /// `onSelectionChanged` or `viewpoint` exclusively. - var onSelectionChanged: ((Bookmark) -> Void)? = nil - - /// A bookmark that was selected. - @State - private var selectedBookmark: Bookmark? = nil + var selectionChangedAction: ((Bookmark) -> Void)? = nil /// If non-`nil`, this viewpoint is updated when a bookmark is selected. private var viewpoint: Binding? @@ -51,7 +51,7 @@ public struct Bookmarks: View { perform action: @escaping (Bookmark) -> Void ) -> Bookmarks { var copy = self - copy.onSelectionChanged = action + copy.selectionChangedAction = action return copy } @@ -64,7 +64,7 @@ public struct Bookmarks: View { isPresented = false if let viewpoint = viewpoint { viewpoint.wrappedValue = bookmark.viewpoint - } else if let onSelectionChanged = onSelectionChanged { + } else if let onSelectionChanged = selectionChangedAction { onSelectionChanged(bookmark) } } diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 0848533a4..b3a929e22 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -36,7 +36,7 @@ class BookmarksTests: XCTestCase { isPresented: isPresented, bookmarks: sampleBookmarks ) - bookmarks.onSelectionChanged = action + bookmarks.selectionChangedAction = action XCTAssertTrue(_isPresented) bookmarks.selectBookmark(sampleBookmarks.first!) XCTAssertFalse(_isPresented) @@ -67,7 +67,7 @@ class BookmarksTests: XCTestCase { isPresented: isPresented, mapOrScene: webMap ) - bookmarks.onSelectionChanged = action + bookmarks.selectionChangedAction = action XCTAssertTrue(_isPresented) bookmarks.selectBookmark(webMap.bookmarks.first!) XCTAssertFalse(_isPresented) From 70151fa76d2f3370692bba0b7a5f04f3f6e296ad Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 09:48:43 -0700 Subject: [PATCH 0694/1722] Lift padding --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 1 + Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 0ac91d039..414471d61 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -104,6 +104,7 @@ public struct Bookmarks: View { public var body: some View { Group { BookmarksHeader(isPresented: $isPresented) + .padding() if geoModel == nil || geoModelIsLoaded { BookmarksList(bookmarks: bookmarks) .onSelectionChanged { diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift index dcffecc61..d44ad020f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -53,6 +53,5 @@ struct BookmarksHeader: View { .foregroundColor(.red) } } - .padding() } } From 6f91f6356b7d03feaa53afa240d223dc14b0f2e2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 09:48:49 -0700 Subject: [PATCH 0695/1722] Update BookmarksList.swift --- .../ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 7d227e98b..aba58957a 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -36,7 +36,7 @@ struct BookmarksList: View { private var listContentHeight: CGFloat = .zero /// Action to be performed when a bookmark is selected. - var onSelectionChanged: ((Bookmark) -> Void)? = nil + var selectionChangedAction: ((Bookmark) -> Void)? = nil /// Sets a closure to perform when the bookmark selection changes. /// - Parameter action: The closure to perform when the bookmark selection has changed. @@ -44,7 +44,7 @@ struct BookmarksList: View { perform action: @escaping (Bookmark) -> Void ) -> BookmarksList { var copy = self - copy.onSelectionChanged = action + copy.selectionChangedAction = action return copy } @@ -65,7 +65,7 @@ struct BookmarksList: View { id: \.viewpoint ) { bookmark in Button { - onSelectionChanged?(bookmark) + selectionChangedAction?(bookmark) } label: { Text(bookmark.name) .foregroundColor(.primary) From f697e34297364cc74af1ed8bc170fac207a060ce Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 09:59:04 -0700 Subject: [PATCH 0696/1722] Update BookmarksList.swift --- .../Components/Bookmarks/BookmarksList.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index aba58957a..c12e5f96f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -70,18 +70,18 @@ struct BookmarksList: View { Text(bookmark.name) .foregroundColor(.primary) } - .padding(4) + .padding(4) Divider() } } - .padding() - .onSizeChange { - listContentHeight = $0.height - } + .padding() + .onSizeChange { + listContentHeight = $0.height + } } - .frame( - maxHeight: isCompact ? .infinity : listContentHeight - ) + .frame( + maxHeight: isCompact ? .infinity : listContentHeight + ) } } } From 0fcf347a593ffae914160254590a761a15e9292f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 09:59:12 -0700 Subject: [PATCH 0697/1722] Update Bookmarks.swift --- .../ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 414471d61..5fc1bfb9a 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -104,12 +104,12 @@ public struct Bookmarks: View { public var body: some View { Group { BookmarksHeader(isPresented: $isPresented) - .padding() + .padding([.horizontal, .top]) if geoModel == nil || geoModelIsLoaded { BookmarksList(bookmarks: bookmarks) - .onSelectionChanged { - selectBookmark($0) - } + .onSelectionChanged { + selectBookmark($0) + } } else { loadingView } From 29e3df6b7800868dd30cb89aa7a707d151e5014f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 10:16:23 -0700 Subject: [PATCH 0698/1722] Update BookmarksList.swift --- Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index c12e5f96f..982dad46c 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -62,7 +62,7 @@ struct BookmarksList: View { VStack(alignment: .leading) { ForEach( bookmarks.sorted { $0.name < $1.name }, - id: \.viewpoint + id: \.name ) { bookmark in Button { selectionChangedAction?(bookmark) From 47ff839863424625f1e87743360134c9c583602b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 10:53:36 -0700 Subject: [PATCH 0699/1722] Update BookmarksExampleView.swift --- Examples/Examples/BookmarksExampleView.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 717cf8067..cc5aab817 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -59,8 +59,8 @@ struct BookmarksExampleView: View { mapOrScene: map, viewpoint: $viewpoint ) - .environment(\.horizontalSizeClass, horizontalSizeClass) - .environment(\.verticalSizeClass, verticalSizeClass) + .environment(\.horizontalSizeClass, horizontalSizeClass) + .environment(\.verticalSizeClass, verticalSizeClass) // Display the Bookmarks view with the list of bookmarks in a map. // Bookmarks( // isPresented: $showingBookmarks, @@ -68,11 +68,11 @@ struct BookmarksExampleView: View { // ) // In order to handle bookmark selection changes manually, // use `.onSelectionChanged`. -// .onSelectionChanged { -// viewpoint = $0.viewpoint -// } -// .environment(\.horizontalSizeClass, horizontalSizeClass) -// .environment(\.verticalSizeClass, verticalSizeClass) +// .onSelectionChanged { +// viewpoint = $0.viewpoint +// } +// .environment(\.horizontalSizeClass, horizontalSizeClass) +// .environment(\.verticalSizeClass, verticalSizeClass) } } } From 189dc9a137cd0a28a2e57e29bd9c06114afd607e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 11:34:35 -0700 Subject: [PATCH 0700/1722] Update Result.swift Undo unintended cahnge --- Sources/ArcGISToolkit/Extensions/Result.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index 0503e5195..d4e837b1e 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -12,6 +12,11 @@ // limitations under the License. extension Result where Failure == Error { + /// Creates a result based on the outcome of the given task. If the task + /// succeeds, the result is `success`. If the task fails, the result is + /// `failure`. + /// + /// Returns `nil` in the event that the task was cancelled. init?(awaiting task: () async throws -> Success) async { do { self = .success(try await task()) From 6ea47431b9d1f368cace9b8aab8966db1b875954 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 11:38:47 -0700 Subject: [PATCH 0701/1722] Update View.swift Undo unintended changes --- Sources/ArcGISToolkit/Extensions/View.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index d32e35fc9..e488ae53c 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -17,15 +17,14 @@ import SwiftUI struct SelectedModifier: ViewModifier { /// `true` if the view should display as selected, `false` otherwise. var isSelected: Bool - + func body(content: Content) -> some View { - let roundedRect = RoundedRectangle(cornerRadius: 4) if isSelected { content .background(Color.secondary.opacity(0.8)) - .clipShape(roundedRect) + .clipShape(RoundedRectangle(cornerRadius: 4)) .shadow( - color: Color.secondary.opacity(0.8), + color: .secondary.opacity(0.8), radius: 2 ) } else { @@ -43,7 +42,9 @@ extension View { background( GeometryReader { geometry in Color.clear - .preference(key: SizePreferenceKey.self, value: geometry.size) + .preference( + key: SizePreferenceKey.self, value: geometry.size + ) } ) .onPreferenceChange(SizePreferenceKey.self, perform: perform) @@ -51,7 +52,7 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: The view being modified. + /// - Returns: The modified view. func selected( _ isSelected: Bool = false ) -> some View { From 9a40a7a958948472293a48b9015546c85a228f55 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 11:42:24 -0700 Subject: [PATCH 0702/1722] Update View.swift Undo unintended changes --- Sources/ArcGISToolkit/Extensions/View.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index e488ae53c..8b5b4e39a 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -15,7 +15,7 @@ import SwiftUI /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { - /// `true` if the view should display as selected, `false` otherwise. + /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool func body(content: Content) -> some View { From 01164376fd0ceb6fff713166dbbc215f816d21c0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 11:48:06 -0700 Subject: [PATCH 0703/1722] Update ScalebarExampleView.swift --- Examples/Examples/ScalebarExampleView.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 084a9f71b..bf57df0ed 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -42,7 +42,14 @@ struct ScalebarExampleView: View { .onScaleChanged { scale = $0 } .onSpatialReferenceChanged { spatialReference = $0 } .overlay(alignment: .bottomLeading) { - Scalebar(scale, spatialReference, 175, viewpoint, visibleArea, units: .imperial) + Scalebar( + scale, + spatialReference, + 175, + viewpoint, + visibleArea, + units: .imperial + ) } } } From 44b5c104f034f9bf7e38f0b0a63547d1a9236a6d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 17:20:49 -0700 Subject: [PATCH 0704/1722] Update Bookmarks.swift --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 5fc1bfb9a..a70568190 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -114,6 +114,10 @@ public struct Bookmarks: View { loadingView } } + .frame( + maxHeight: .infinity, + alignment: .top + ) } /// A view that is shown while a `GeoModel` is loading. From d83f3700bf3438c590e955164e3e3f6721eac0ad Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 17:25:17 -0700 Subject: [PATCH 0705/1722] Update BookmarksList.swift --- .../Components/Bookmarks/BookmarksList.swift | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 982dad46c..1f2c1cc48 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -16,21 +16,9 @@ import SwiftUI /// `BookmarksList` displays a list of selectable bookmarks. struct BookmarksList: View { - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass: UserInterfaceSizeClass? - - @Environment(\.verticalSizeClass) - private var verticalSizeClass: UserInterfaceSizeClass? - /// A list of bookmarks for display. var bookmarks: [Bookmark] - /// If `true`, the bookmarks will display as sheet. - /// If `false`, the bookmarks will display as a popover. - private var isCompact: Bool { - return horizontalSizeClass == .compact || verticalSizeClass == .compact - } - /// The height of the list content. @State private var listContentHeight: CGFloat = .zero @@ -80,7 +68,7 @@ struct BookmarksList: View { } } .frame( - maxHeight: isCompact ? .infinity : listContentHeight + height: listContentHeight ) } } From 6da8088373c0ddc25dd06a69b11a0cbff9a282f5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 17:30:16 -0700 Subject: [PATCH 0706/1722] Update BookmarksList.swift --- .../Components/Bookmarks/BookmarksList.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 1f2c1cc48..f70ef83b9 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -50,7 +50,7 @@ struct BookmarksList: View { VStack(alignment: .leading) { ForEach( bookmarks.sorted { $0.name < $1.name }, - id: \.name + id: \.self ) { bookmark in Button { selectionChangedAction?(bookmark) @@ -74,3 +74,22 @@ struct BookmarksList: View { } } } + +extension Bookmark: Equatable { + public static func == (lhs: Bookmark, rhs: Bookmark) -> Bool { + lhs.hashValue == rhs.hashValue + } +} + +extension Bookmark: Hashable { + public var hashValue: Int { + var hasher = Hasher() + self.hash(into: &hasher) + return hasher.finalize() + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(viewpoint) + } +} From 3dbc85dde17a2653c10ecf13aff0ca3cc830cc8a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 17:56:14 -0700 Subject: [PATCH 0707/1722] Update BookmarksExampleView.swift --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index cc5aab817..e8e315c47 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -31,7 +31,7 @@ struct BookmarksExampleView: View { @State var showingBookmarks = false - /// Allows for communication between the Bookmarks and MapView or SceneView. + /// Allows for communication between the bookmarks component and MapView or SceneView. @State var viewpoint: Viewpoint? = nil From de6f89abf1c2a309aa6ea2825112f11d7f369bcf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 17:59:28 -0700 Subject: [PATCH 0708/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index a70568190..a11f57f84 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -45,8 +45,7 @@ public struct Bookmarks: View { private var viewpoint: Binding? /// Sets an action to perform when the bookmark selection changes. - /// - Parameters: - /// - action: The action to perform when the bookmark selection has changed. + /// - Parameter action: The action to perform when the bookmark selection has changed. public func onSelectionChanged( perform action: @escaping (Bookmark) -> Void ) -> Bookmarks { From b45dee68f8b2a091c181cae471a5b4e4420296c1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:00:43 -0700 Subject: [PATCH 0709/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- 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 a11f57f84..1992419dd 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -71,7 +71,7 @@ public struct Bookmarks: View { /// Creates a `Bookmarks` component. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. - /// - bookmarks: An array of bookmarks. Use this when displaying bookmarks defined at run-time. + /// - bookmarks: An array of bookmarks. Use this when displaying bookmarks defined at runtime. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. /// Alternately, you can use the `.onSelectionChanged` modifier to handle bookmark selection. public init( From 300ade5cfa6754e4f6ee566dee8449bd8b978e6e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:02:41 -0700 Subject: [PATCH 0710/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- 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 1992419dd..332b71cdb 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -73,7 +73,7 @@ public struct Bookmarks: View { /// - isPresented: Determines if the bookmarks list is presented. /// - bookmarks: An array of bookmarks. Use this when displaying bookmarks defined at runtime. /// - viewpoint: A viewpoint binding that will be updated when a bookmark is selected. - /// Alternately, you can use the `.onSelectionChanged` modifier to handle bookmark selection. + /// Alternately, you can use the `onSelectionChanged(perform:)` modifier to handle bookmark selection. public init( isPresented: Binding, bookmarks: [Bookmark], From a69e64b21a159b6c8bb0f3515dc8ecdc5086067d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:02:53 -0700 Subject: [PATCH 0711/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- 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 332b71cdb..79ff84495 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -89,7 +89,7 @@ public struct Bookmarks: View { /// - isPresented: Determines if the bookmarks list is presented. /// - mapOrScene: 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` modifier to handle bookmark selection. + /// Alternately, you can use the `onSelectionChanged(perform:)` modifier to handle bookmark selection. public init( isPresented: Binding, mapOrScene: GeoModel, From 41324c6ddee9eff931a7921dad8d7d53d181f16b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:03:38 -0700 Subject: [PATCH 0712/1722] Update Tests/ArcGISToolkitTests/BookmarksTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index b3a929e22..8ba98e319 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -99,7 +99,7 @@ class BookmarksTests: XCTestCase { XCTAssertEqual(_viewpoint, sampleBookmarks.first?.viewpoint) } - /// Assert that the list properly handles a selction when provided a viewpoint and web map. + /// Asserts that the list properly handles a selection when provided a viewpoint and web map. func testSelectBookmarkWithViewpointAndWebMap() async { let webMap = webMap do { From 4fec246921e4f8cae5ac74712a2576f2cd7d3475 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:04:40 -0700 Subject: [PATCH 0713/1722] Update Tests/ArcGISToolkitTests/BookmarksTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 8ba98e319..ab442741b 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -16,7 +16,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -class BookmarksTests: XCTestCase { +final class BookmarksTests: XCTestCase { /// Assert that the list properly handles a selction when provided a modifier. func testSelectBookmarkWithModifier() { let expectation = XCTestExpectation( From f35b5eee08ec963ddba17f291a454722b70bb8da Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:04:51 -0700 Subject: [PATCH 0714/1722] Update Tests/ArcGISToolkitTests/BookmarksTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index ab442741b..80405c8b3 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -17,7 +17,7 @@ import XCTest @testable import ArcGISToolkit final class BookmarksTests: XCTestCase { - /// Assert that the list properly handles a selction when provided a modifier. + /// Asserts that the list properly handles a selection when provided a modifier. func testSelectBookmarkWithModifier() { let expectation = XCTestExpectation( description: "Modifier action was performed" From 964dc3856d16683a25ec3ee715e9f90e7253a71c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:06:00 -0700 Subject: [PATCH 0715/1722] Update Tests/ArcGISToolkitTests/BookmarksTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 80405c8b3..a5b99074c 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -130,7 +130,7 @@ final class BookmarksTests: XCTestCase { } } -extension BookmarksTests { +private extension BookmarksTests { /// An arbitrary point to use for testing. var point: Point { Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) From 772a546e0d3f5b85f7979e0bc40f9d5b7ddcc67c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:06:19 -0700 Subject: [PATCH 0716/1722] Update Tests/ArcGISToolkitTests/BookmarksTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index a5b99074c..dda34ed9d 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -136,7 +136,7 @@ private extension BookmarksTests { Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) } - /// A list of sample bookmarks for testing + /// A list of sample bookmarks for testing. var sampleBookmarks: [Bookmark] {[ Bookmark( name: "Yosemite National Park", From 305347f87c20e2428053424cbd5276ec39c6ae33 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:07:47 -0700 Subject: [PATCH 0717/1722] Update Tests/ArcGISToolkitTests/BookmarksTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index dda34ed9d..6008da9d2 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -43,7 +43,7 @@ final class BookmarksTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - /// Assert that the list properly handles a selction when provided a modifier and web map. + /// Asserts that the list properly handles a selection when provided a modifier and web map. func testSelectBookmarkWithModifierAndWebMap() async { let expectation = XCTestExpectation( description: "Modifier action was performed" From 5479142261cdc63a987ed62d3a6a9fbc23c51427 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:08:00 -0700 Subject: [PATCH 0718/1722] Update Tests/ArcGISToolkitTests/BookmarksTests.swift Co-authored-by: Zachary A Kline --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 6008da9d2..52d45a410 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -74,7 +74,7 @@ final class BookmarksTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - /// Assert that the list properly handles a selction when provided a viewpoint. + /// Asserts that the list properly handles a selection when provided a viewpoint. func testSelectBookmarkWithViewpoint() { let sampleBookmarks = sampleBookmarks var _isPresented = true From af88262cb19cfad2e34d91c0f53b1976286dbafe Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:19:02 -0700 Subject: [PATCH 0719/1722] Update BookmarksTests.swift --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 52d45a410..0340acdaa 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -48,7 +48,7 @@ final class BookmarksTests: XCTestCase { let expectation = XCTestExpectation( description: "Modifier action was performed" ) - let webMap = webMap + let webMap = Map.portlandTreeSurvey do { try await webMap.load() } catch { @@ -82,7 +82,7 @@ final class BookmarksTests: XCTestCase { get: { _isPresented }, set: {_isPresented = $0 } ) - var _viewpoint: Viewpoint? = getViewpoint(0) + var _viewpoint: Viewpoint? = Viewpoint.esriRedlandsCampus let viewpoint = Binding( get: { _viewpoint }, set: { _viewpoint = $0 } @@ -101,7 +101,7 @@ final class BookmarksTests: XCTestCase { /// Asserts that the list properly handles a selection when provided a viewpoint and web map. func testSelectBookmarkWithViewpointAndWebMap() async { - let webMap = webMap + let webMap = Map.portlandTreeSurvey do { try await webMap.load() } catch { @@ -112,7 +112,7 @@ final class BookmarksTests: XCTestCase { get: { _isPresented }, set: {_isPresented = $0 } ) - var _viewpoint: Viewpoint? = getViewpoint(0) + var _viewpoint: Viewpoint? = Viewpoint.esriRedlandsCampus let viewpoint = Binding( get: { _viewpoint }, set: { _viewpoint = $0 } @@ -131,11 +131,6 @@ final class BookmarksTests: XCTestCase { } private extension BookmarksTests { - /// An arbitrary point to use for testing. - var point: Point { - Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84) - } - /// A list of sample bookmarks for testing. var sampleBookmarks: [Bookmark] {[ Bookmark( @@ -183,21 +178,21 @@ private extension BookmarksTests { ) ), ]} +} - /// An arbitrary scale to use for testing. - var scale: Double { - 10_000.00 - } - +private extension Map { /// A web map authored with bookmarks for testing. - var webMap: Map { - return Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! - } + static let portlandTreeSurvey = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! +} - /// Builds viewpoints to use for tests. - /// - Parameter rotation: The rotation to use for the resulting viewpoint. - /// - Returns: A viewpoint object for tests. - func getViewpoint(_ rotation: Double) -> Viewpoint { - return Viewpoint(center: point, scale: scale, rotation: rotation) - } +private extension Viewpoint { + static let esriRedlandsCampus = Viewpoint( + center: Point( + x: -117.19494, + y: 34.05723, + spatialReference: .wgs84 + ), + scale: 10_000.00, + rotation: .zero + ) } From 59ce5aac9c845215237080a37462b150eeb94969 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:23:02 -0700 Subject: [PATCH 0720/1722] Update Examples/Examples/BookmarksExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/BookmarksExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index e8e315c47..4ee3ee4c8 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -52,7 +52,7 @@ struct BookmarksExampleView: View { } .popover(isPresented: $showingBookmarks) { // Display the Bookmarks view with a pre-defined list of bookmarks. - // Passing in a viewpoint binding will allow the Bookmarks + // Passing in a viewpoint binding will allow the `Bookmarks` // view to handle bookmark selection. Bookmarks( isPresented: $showingBookmarks, From c86830925688a3272a04729583fea80d660ea944 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:29:01 -0700 Subject: [PATCH 0721/1722] Update BookmarksExampleView.swift --- Examples/Examples/BookmarksExampleView.swift | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 4ee3ee4c8..6be6d77c6 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -25,13 +25,14 @@ struct BookmarksExampleView: View { /// A web map with predefined bookmarks. private let map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! - /// Indicates if the bookmarks list is shown or not. - /// - Remark: This allows a developer to control how the bookmarks menu is shown/hidden, - /// whether that be in a group of options or a standalone button. + /// 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. @State var showingBookmarks = false - /// Allows for communication between the bookmarks component and MapView or SceneView. + /// Allows for communication between the `Bookmarks` component and a `MapView` or + /// `SceneView`. @State var viewpoint: Viewpoint? = nil @@ -51,9 +52,10 @@ struct BookmarksExampleView: View { ) } .popover(isPresented: $showingBookmarks) { - // Display the Bookmarks view with a pre-defined list of bookmarks. - // Passing in a viewpoint binding will allow the `Bookmarks` - // view to handle bookmark selection. + // 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: map, @@ -61,13 +63,14 @@ struct BookmarksExampleView: View { ) .environment(\.horizontalSizeClass, horizontalSizeClass) .environment(\.verticalSizeClass, verticalSizeClass) - // Display the Bookmarks view with the list of bookmarks in a map. + // 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`. + // In order to handle bookmark selection changes + // manually, use `onSelectionChanged(perform:)`. // .onSelectionChanged { // viewpoint = $0.viewpoint // } From da157257e063a53933d5145785e8a8624111cde2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:29:05 -0700 Subject: [PATCH 0722/1722] Update Bookmarks.swift --- Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 79ff84495..68a6ce4e1 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -73,7 +73,8 @@ public struct Bookmarks: View { /// - isPresented: Determines if the bookmarks list is presented. /// - bookmarks: An array of bookmarks. Use this when displaying bookmarks defined at runtime. /// - 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. + /// Alternately, you can use the `onSelectionChanged(perform:)` modifier to handle + /// bookmark selection. public init( isPresented: Binding, bookmarks: [Bookmark], @@ -89,7 +90,8 @@ public struct Bookmarks: View { /// - isPresented: Determines if the bookmarks list is presented. /// - mapOrScene: 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. + /// Alternately, you can use the `onSelectionChanged(perform:)` modifier to handle + /// bookmark selection. public init( isPresented: Binding, mapOrScene: GeoModel, From e8f4b66d4288b7f8538bef5b44f87731ad650565 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:45:24 -0700 Subject: [PATCH 0723/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 93e2474fe..5bfe97766 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -31,19 +31,19 @@ struct SiteAndFacilitySelector: View { var body: some View { if let selectedSite = floorFilterViewModel.selectedSite { - Facilities(facilities: selectedSite.facilities, isHidden: isHidden) + FacilitiesView(facilities: selectedSite.facilities, isHidden: isHidden) } else if floorFilterViewModel.sites.count == 1 { - Facilities( + FacilitiesView( facilities: floorFilterViewModel.sites.first!.facilities, isHidden: isHidden ) } else { - Sites(sites: floorFilterViewModel.sites, isHidden: isHidden) + SitesView(sites: floorFilterViewModel.sites, isHidden: isHidden) } } /// A view displaying the sites contained in a `FloorManager`. - struct Sites: View { + struct SitesView: View { /// The floor sites. let sites: [FloorSite] @@ -86,7 +86,7 @@ struct SiteAndFacilitySelector: View { } /// A view displaying the facilities contained in a `FloorManager`. - struct Facilities: View { + struct FacilitiesView: View { let facilities: [FloorFacility] var isHidden: Binding From a0aebfa0f07bcb12e503fca30f3c2f61363202db Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 22 Mar 2022 18:47:53 -0700 Subject: [PATCH 0724/1722] Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 5bfe97766..08442365b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -87,9 +87,13 @@ struct SiteAndFacilitySelector: View { /// A view displaying the facilities contained in a `FloorManager`. struct FacilitiesView: View { + /// The floor facilities. let facilities: [FloorFacility] + + /// Allows the user to toggle the visibility of the sites. var isHidden: Binding + /// The view model used by the `SiteAndFacilitySelector`. @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel /// The height of the scroll view's content. From d7df0d328b60a5a3e537420236dea160d3fee290 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Mar 2022 10:01:49 -0700 Subject: [PATCH 0725/1722] Fix height issues --- .../Components/Bookmarks/Bookmarks.swift | 39 +++++++++++++------ .../Components/Bookmarks/BookmarksList.swift | 19 ++++----- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index 68a6ce4e1..fb22c81a2 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -27,10 +27,18 @@ public struct Bookmarks: View { @State private var geoModelIsLoaded = false + /// The height of the header content. + @State + private var headerHeight: CGFloat = .zero + /// Determines if the bookmarks list is currently shown or not. @Binding private var isPresented: Bool + /// The height of the list content. + @State + private var listHeight: CGFloat = .zero + /// A bookmark that was selected. @State private var selectedBookmark: Bookmark? = nil @@ -103,22 +111,29 @@ public struct Bookmarks: View { } public var body: some View { - Group { + VStack { BookmarksHeader(isPresented: $isPresented) .padding([.horizontal, .top]) - if geoModel == nil || geoModelIsLoaded { - BookmarksList(bookmarks: bookmarks) - .onSelectionChanged { - selectBookmark($0) + .onSizeChange { + headerHeight = $0.height + } + ScrollView { + VStack { + if geoModel == nil || geoModelIsLoaded { + BookmarksList(bookmarks: bookmarks) + .onSelectionChanged { + selectBookmark($0) + } + } else { + loadingView } - } else { - loadingView + } + .onSizeChange { + listHeight = $0.height + } } } - .frame( - maxHeight: .infinity, - alignment: .top - ) + .frame(idealHeight: headerHeight + listHeight) } /// A view that is shown while a `GeoModel` is loading. @@ -128,8 +143,8 @@ public struct Bookmarks: View { .task { do { try await geoModel?.load() - geoModelIsLoaded = true bookmarks = geoModel?.bookmarks ?? [] + geoModelIsLoaded = true } catch { print(error.localizedDescription) } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index f70ef83b9..6a038500c 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -21,7 +21,7 @@ struct BookmarksList: View { /// The height of the list content. @State - private var listContentHeight: CGFloat = .zero + private var listHeight: CGFloat = .zero /// Action to be performed when a bookmark is selected. var selectionChangedAction: ((Bookmark) -> Void)? = nil @@ -45,6 +45,7 @@ struct BookmarksList: View { Image(systemName: "bookmark.slash") } .foregroundColor(.primary) + .padding() } else { ScrollView { VStack(alignment: .leading) { @@ -58,18 +59,18 @@ struct BookmarksList: View { Text(bookmark.name) .foregroundColor(.primary) } - .padding(4) + .padding(4) Divider() } } - .padding() - .onSizeChange { - listContentHeight = $0.height - } + .padding() + .onSizeChange { + listHeight = $0.height + } } - .frame( - height: listContentHeight - ) + .frame( + height: listHeight + ) } } } From 01933109c8dedb01fcd34c33c50bc8eae2d20f18 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Mar 2022 16:25:04 -0700 Subject: [PATCH 0726/1722] trailing whitespace, single line @State @Binding --- Examples/Examples/BookmarksExampleView.swift | 31 ++++++------- .../Components/Bookmarks/Bookmarks.swift | 46 ++++++++----------- .../Bookmarks/BookmarksHeader.swift | 11 ++--- .../Components/Bookmarks/BookmarksList.swift | 17 +++---- 4 files changed, 47 insertions(+), 58 deletions(-) diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index 6be6d77c6..f089eef65 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -18,24 +18,22 @@ import SwiftUI struct BookmarksExampleView: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass: UserInterfaceSizeClass? - + @Environment(\.verticalSizeClass) private var verticalSizeClass: UserInterfaceSizeClass? - + /// A web map with predefined bookmarks. private let 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 /// shown/hidden, whether that be in a group of options or a standalone button. - @State - var showingBookmarks = false - + @State var showingBookmarks = false + /// Allows for communication between the `Bookmarks` component and a `MapView` or /// `SceneView`. - @State - var viewpoint: Viewpoint? = nil - + @State var viewpoint: Viewpoint? = nil + var body: some View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { @@ -61,8 +59,9 @@ struct BookmarksExampleView: View { mapOrScene: map, viewpoint: $viewpoint ) - .environment(\.horizontalSizeClass, horizontalSizeClass) - .environment(\.verticalSizeClass, verticalSizeClass) + .environment(\.horizontalSizeClass, horizontalSizeClass) + .environment(\.verticalSizeClass, verticalSizeClass) + // Display the `Bookmarks` component with the list of // bookmarks in a map. // Bookmarks( @@ -71,11 +70,11 @@ struct BookmarksExampleView: View { // ) // In order to handle bookmark selection changes // manually, use `onSelectionChanged(perform:)`. -// .onSelectionChanged { -// viewpoint = $0.viewpoint -// } -// .environment(\.horizontalSizeClass, horizontalSizeClass) -// .environment(\.verticalSizeClass, verticalSizeClass) +// .onSelectionChanged { +// viewpoint = $0.viewpoint +// } +// .environment(\.horizontalSizeClass, horizontalSizeClass) +// .environment(\.verticalSizeClass, verticalSizeClass) } } } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index fb22c81a2..b59bed5e9 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -17,41 +17,35 @@ import SwiftUI /// `Bookmarks` allows a user to view and select from a set of bookmarks. public struct Bookmarks: View { /// A list of selectable bookmarks. - @State - private var bookmarks: [Bookmark] = [] - + @State private var bookmarks: [Bookmark] = [] + /// A map or scene containing bookmarks. private var geoModel: GeoModel? - + /// Indicates if bookmarks have loaded and are ready for display. - @State - private var geoModelIsLoaded = false - + @State private var geoModelIsLoaded = false + /// The height of the header content. - @State - private var headerHeight: CGFloat = .zero - + @State private var headerHeight: CGFloat = .zero + /// Determines if the bookmarks list is currently shown or not. - @Binding - private var isPresented: Bool - + @Binding private var isPresented: Bool + /// The height of the list content. - @State - private var listHeight: CGFloat = .zero - + @State private var listHeight: CGFloat = .zero + /// A bookmark that was selected. - @State - private var selectedBookmark: Bookmark? = nil - + @State private var selectedBookmark: Bookmark? = nil + /// User defined action to be performed when a bookmark is selected. /// /// Use this when you prefer to self-manage the response to a bookmark selection. Use either /// `onSelectionChanged` or `viewpoint` exclusively. var selectionChangedAction: ((Bookmark) -> Void)? = nil - + /// If non-`nil`, this viewpoint is updated when a bookmark is selected. private var viewpoint: Binding? - + /// Sets an action to perform when the bookmark selection changes. /// - Parameter action: The action to perform when the bookmark selection has changed. public func onSelectionChanged( @@ -61,7 +55,7 @@ public struct Bookmarks: View { copy.selectionChangedAction = action return copy } - + /// Performs the necessary actions when a bookmark is selected. /// /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint @@ -75,7 +69,7 @@ public struct Bookmarks: View { onSelectionChanged(bookmark) } } - + /// Creates a `Bookmarks` component. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. @@ -92,7 +86,7 @@ public struct Bookmarks: View { self.viewpoint = viewpoint _isPresented = isPresented } - + /// Creates a `Bookmarks` component. /// - Parameters: /// - isPresented: Determines if the bookmarks list is presented. @@ -109,7 +103,7 @@ public struct Bookmarks: View { self.viewpoint = viewpoint _isPresented = isPresented } - + public var body: some View { VStack { BookmarksHeader(isPresented: $isPresented) @@ -135,7 +129,7 @@ public struct Bookmarks: View { } .frame(idealHeight: headerHeight + listHeight) } - + /// A view that is shown while a `GeoModel` is loading. private var loadingView: some View { ProgressView() diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift index d44ad020f..b404ee872 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -17,20 +17,19 @@ import SwiftUI struct BookmarksHeader: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass: UserInterfaceSizeClass? - + @Environment(\.verticalSizeClass) private var verticalSizeClass: UserInterfaceSizeClass? - + /// If `true`, the bookmarks will display as sheet. /// If `false`, the bookmarks will display as a popover. private var isCompact: Bool { return horizontalSizeClass == .compact || verticalSizeClass == .compact } - + /// Determines if the bookmarks list is currently shown or not. - @Binding - var isPresented: Bool - + @Binding var isPresented: Bool + public var body: some View { HStack(alignment: .top) { Image(systemName: "bookmark") diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index 6a038500c..a79192191 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -18,14 +18,13 @@ import SwiftUI struct BookmarksList: View { /// A list of bookmarks for display. var bookmarks: [Bookmark] - + /// The height of the list content. - @State - private var listHeight: CGFloat = .zero - + @State private var listHeight: CGFloat = .zero + /// Action to be performed when a bookmark is selected. var selectionChangedAction: ((Bookmark) -> Void)? = nil - + /// Sets a closure to perform when the bookmark selection changes. /// - Parameter action: The closure to perform when the bookmark selection has changed. public func onSelectionChanged( @@ -35,7 +34,7 @@ struct BookmarksList: View { copy.selectionChangedAction = action return copy } - + var body: some View { Group { if bookmarks.isEmpty { @@ -68,9 +67,7 @@ struct BookmarksList: View { listHeight = $0.height } } - .frame( - height: listHeight - ) + .frame(height: listHeight) } } } @@ -88,7 +85,7 @@ extension Bookmark: Hashable { self.hash(into: &hasher) return hasher.finalize() } - + public func hash(into hasher: inout Hasher) { hasher.combine(name) hasher.combine(viewpoint) From bac046d51697e900460444d4c53e834ec58bbea9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Mar 2022 16:46:21 -0700 Subject: [PATCH 0727/1722] Apply PR feedback from Mark --- .../Components/Bookmarks/Bookmarks.swift | 6 ++-- .../Bookmarks/BookmarksHeader.swift | 8 ++++-- .../Components/Bookmarks/BookmarksList.swift | 19 ------------- .../ArcGISToolkit/Extensions/Bookmark.swift | 28 +++++++++++++++++++ Tests/ArcGISToolkitTests/BookmarksTests.swift | 26 ++++++++--------- 5 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Bookmark.swift diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift index b59bed5e9..b620886be 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -23,7 +23,7 @@ public struct Bookmarks: View { private var geoModel: GeoModel? /// Indicates if bookmarks have loaded and are ready for display. - @State private var geoModelIsLoaded = false + @State private var isGeoModelLoaded = false /// The height of the header content. @State private var headerHeight: CGFloat = .zero @@ -113,7 +113,7 @@ public struct Bookmarks: View { } ScrollView { VStack { - if geoModel == nil || geoModelIsLoaded { + if geoModel == nil || isGeoModelLoaded { BookmarksList(bookmarks: bookmarks) .onSelectionChanged { selectBookmark($0) @@ -138,7 +138,7 @@ public struct Bookmarks: View { do { try await geoModel?.load() bookmarks = geoModel?.bookmarks ?? [] - geoModelIsLoaded = true + isGeoModelLoaded = true } catch { print(error.localizedDescription) } diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift index b404ee872..06d15af2f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -33,15 +33,17 @@ struct BookmarksHeader: View { public var body: some View { HStack(alignment: .top) { Image(systemName: "bookmark") - VStack { + VStack(alignment: .leading) { Text("Bookmarks") .font(.headline) - .frame(maxWidth: .infinity, alignment: .leading) Text("Select a bookmark") .font(.subheadline) .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) } + .frame( + maxWidth: .infinity, + alignment: .leading + ) if isCompact { Spacer() Button { diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift index a79192191..74a6b5540 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksList.swift @@ -72,22 +72,3 @@ struct BookmarksList: View { } } } - -extension Bookmark: Equatable { - public static func == (lhs: Bookmark, rhs: Bookmark) -> Bool { - lhs.hashValue == rhs.hashValue - } -} - -extension Bookmark: Hashable { - public var hashValue: Int { - var hasher = Hasher() - self.hash(into: &hasher) - return hasher.finalize() - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(name) - hasher.combine(viewpoint) - } -} diff --git a/Sources/ArcGISToolkit/Extensions/Bookmark.swift b/Sources/ArcGISToolkit/Extensions/Bookmark.swift new file mode 100644 index 000000000..c88a38c32 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Bookmark.swift @@ -0,0 +1,28 @@ +// 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 +import Foundation + +extension Bookmark: Equatable { + public static func == (lhs: Bookmark, rhs: Bookmark) -> Bool { + lhs.hashValue == rhs.hashValue + } +} + +extension Bookmark: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(viewpoint) + } +} diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index 0340acdaa..b1a8e18cc 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -44,13 +44,13 @@ final class BookmarksTests: XCTestCase { } /// Asserts that the list properly handles a selection when provided a modifier and web map. - func testSelectBookmarkWithModifierAndWebMap() async { + func testSelectBookmarkWithModifierAndMap() async { let expectation = XCTestExpectation( description: "Modifier action was performed" ) - let webMap = Map.portlandTreeSurvey + let map = Map.portlandTreeSurvey do { - try await webMap.load() + try await map.load() } catch { XCTFail("Web map failed to load \(error.localizedDescription)") } @@ -61,15 +61,15 @@ final class BookmarksTests: XCTestCase { ) let action: ((Bookmark) -> Void) = { expectation.fulfill() - XCTAssertEqual($0.viewpoint, webMap.bookmarks.first?.viewpoint) + XCTAssertEqual($0.viewpoint, map.bookmarks.first?.viewpoint) } var bookmarks = Bookmarks( isPresented: isPresented, - mapOrScene: webMap + mapOrScene: map ) bookmarks.selectionChangedAction = action XCTAssertTrue(_isPresented) - bookmarks.selectBookmark(webMap.bookmarks.first!) + bookmarks.selectBookmark(map.bookmarks.first!) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } @@ -100,10 +100,10 @@ final class BookmarksTests: XCTestCase { } /// Asserts that the list properly handles a selection when provided a viewpoint and web map. - func testSelectBookmarkWithViewpointAndWebMap() async { - let webMap = Map.portlandTreeSurvey + func testSelectBookmarkWithViewpointAndMap() async { + let map = Map.portlandTreeSurvey do { - try await webMap.load() + try await map.load() } catch { XCTFail("Web map failed to load \(error.localizedDescription)") } @@ -119,14 +119,14 @@ final class BookmarksTests: XCTestCase { ) let bookmarks = Bookmarks( isPresented: isPresented, - mapOrScene: webMap, + mapOrScene: map, viewpoint: viewpoint ) XCTAssertTrue(_isPresented) - XCTAssertNotEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) - bookmarks.selectBookmark(webMap.bookmarks.first!) + XCTAssertNotEqual(_viewpoint, map.bookmarks.first?.viewpoint) + bookmarks.selectBookmark(map.bookmarks.first!) XCTAssertFalse(_isPresented) - XCTAssertEqual(_viewpoint, webMap.bookmarks.first?.viewpoint) + XCTAssertEqual(_viewpoint, map.bookmarks.first?.viewpoint) } } From 7bf846642cdc53d808d032e9011e745255bed7f7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 23 Mar 2022 17:10:33 -0700 Subject: [PATCH 0728/1722] Apply PR suggestions from Nimesh Add trailing whitespace, put property wrappers on same line as item --- .../Examples/FloorFilterExampleView.swift | 95 ++++++++++--------- .../Components/FloorFilter/FloorFilter.swift | 42 ++++---- .../FloorFilter/FloorFilterViewModel.swift | 52 +++++----- .../FloorFilter/SiteAndFacilitySelector.swift | 42 ++++---- 4 files changed, 110 insertions(+), 121 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 35931be70..249a9a356 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -21,69 +21,70 @@ struct FloorFilterExampleView: View { // Multiple sites/facilities: Esri IST map with all buildings. // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - + // Redlands Campus map. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities - + // Single site (ESRI Redlands Main) and facility (Building L). // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - + return Map(item: portalItem) } - - @State - private var isMapLoaded: Bool = false - - private var map = makeMap() - - @State - private var mapLoadError: Bool = false - - @State - private var viewpoint = Viewpoint( - center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), + + @State private var isMapLoaded: Bool = false + + @State private var mapLoadError: Bool = false + + @State private var viewpoint = Viewpoint( + center: Point( + x: -117.19496, + y: 34.05713, + spatialReference: .wgs84 + ), scale: 100_000 ) - + + private var map = makeMap() + var body: some View { MapView( map: map, viewpoint: viewpoint ) - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 - } - .overlay(alignment: .bottomLeading) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - floorManager: floorManager, - viewpoint: $viewpoint - ) - .frame(height: 300) - .padding(36) - } else if mapLoadError { - Label( - "Map load error!", - systemImage: "exclamationmark.triangle" - ) - .foregroundColor(.red) - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .center - ) - } + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } + .overlay(alignment: .bottomLeading) { + if isMapLoaded, + let floorManager = map.floorManager { + FloorFilter( + floorManager: floorManager, + viewpoint: $viewpoint + ) + .frame(height: 300) + .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 } + } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 400a612bc..471bf1385 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -31,26 +31,23 @@ public struct FloorFilter: View { viewpoint: viewpoint )) } - + /// The view model used by the `FloorFilter`. - @StateObject - private var viewModel: FloorFilterViewModel - + @StateObject private var viewModel: FloorFilterViewModel + /// A Boolean value that indicates whether the site or facility selector is hidden. - @State - private var isSelectorHidden: Bool = true - + @State private var isSelectorHidden: Bool = true + /// A Boolean value that indicates whether the levels view is currently collapsed. - @State - private var isLevelsViewCollapsed: Bool = false - + @State private var isLevelsViewCollapsed: Bool = false + /// The selected facility's levels, sorted by `level.verticalOrder`. private var sortedLevels: [FloorLevel] { viewModel.selectedFacility?.levels.sorted() { $0.verticalOrder > $1.verticalOrder } ?? [] } - + public var body: some View { Group { if viewModel.isLoading { @@ -97,21 +94,18 @@ public struct FloorFilter: View { struct LevelsView: View { /// The levels to display. let levels: [FloorLevel] - + /// A Boolean value that indicates whether the view shows only the selected level or all levels. /// If the value is `false`, the view will display all levels. Otherwise, display only the /// selected level. - @Binding - var isCollapsed: Bool - + @Binding var isCollapsed: Bool + /// The view model used by the `LevelsView`. - @EnvironmentObject - var viewModel: FloorFilterViewModel - + @EnvironmentObject var viewModel: FloorFilterViewModel + /// The height of the scroll view's content. - @State - private var scrollViewContentHeight: CGFloat = .zero - + @State private var scrollViewContentHeight: CGFloat = .zero + public var body: some View { VStack { if !isCollapsed, @@ -150,11 +144,11 @@ struct LevelsView: View { /// A vertical list of floor levels. struct LevelsStack: View { let levels: [FloorLevel] - + /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel - + var body: some View { VStack { ForEach(levels) { level in @@ -175,7 +169,7 @@ struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. @Binding var isCollapsed: Bool - + var body: some View { Button { isCollapsed.toggle() diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index b0c954446..f8fb53af8 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -26,7 +26,7 @@ final class FloorFilterViewModel: ObservableObject { /// A selected level. case level(FloorLevel) } - + /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilterViewModel`. @@ -37,7 +37,7 @@ final class FloorFilterViewModel: ObservableObject { ) { self.floorManager = floorManager self.viewpoint = viewpoint - + Task { do { try await floorManager.load() @@ -52,36 +52,34 @@ final class FloorFilterViewModel: ObservableObject { isLoading = false } } - + /// The `Viewpoint` used to pan/zoom to the selected site or facility. /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding? - + /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager - + /// A Boolean value that indicates whether the model is loading. - @Published - private(set) var isLoading = true - + @Published private(set) var isLoading = true + /// The floor manager sites. var sites: [FloorSite] { floorManager.sites } - + /// The floor manager facilities. var facilities: [FloorFacility] { floorManager.facilities } - + /// The floor manager levels. var levels: [FloorLevel] { floorManager.levels } - + /// The selected site, floor, or level. - @Published - var selection: Selection? { + @Published var selection: Selection? { didSet { if case let .facility(facility) = selection, let level = defaultLevel(for: facility) { @@ -92,13 +90,13 @@ final class FloorFilterViewModel: ObservableObject { zoomToSelection() } } - + /// The selected site. var selectedSite: FloorSite? { guard let selection = selection else { return nil } - + switch selection { case .site(let site): return site @@ -108,13 +106,13 @@ final class FloorFilterViewModel: ObservableObject { return level.facility?.site } } - + /// The selected facility. var selectedFacility: FloorFacility? { guard let selection = selection else { return nil } - + switch selection { case .site: return nil @@ -124,7 +122,7 @@ final class FloorFilterViewModel: ObservableObject { return level.facility } } - + /// The selected level. var selectedLevel: FloorLevel? { if case let .level(level) = selection { @@ -133,7 +131,7 @@ final class FloorFilterViewModel: ObservableObject { return nil } } - + /// 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 { @@ -142,15 +140,15 @@ final class FloorFilterViewModel: ObservableObject { } return !selectedFacility.levels.isEmpty } - + // Mark: Private Functions - + /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. private func zoomToSelection() { guard let selection = selection else { return } - + switch selection { case .site(let site): zoomToExtent(site.geometry?.extent) @@ -160,14 +158,14 @@ final class FloorFilterViewModel: ObservableObject { zoomToExtent(level.facility?.geometry?.extent) } } - + /// Zoom to given extent. private func zoomToExtent(_ extent: Envelope?) { // Make sure we have an extent and viewpoint to zoom to. guard let extent = extent, let viewpoint = viewpoint else { return } - + let builder = EnvelopeBuilder(envelope: extent) builder.expand(factor: 1.5) let targetExtent = builder.toGeometry() @@ -177,7 +175,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() { @@ -186,7 +184,7 @@ final class FloorFilterViewModel: ObservableObject { $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder } } - + /// Gets the default level for a facility. /// - Parameter facility: The facility to get the default level for. /// - Returns: The default level for the facility, which is the level with vertical order 0; @@ -196,7 +194,7 @@ final class FloorFilterViewModel: ObservableObject { level.facility == facility && level.verticalOrder == .zero }) ?? lowestLevel() } - + /// Returns the level with the lowest vertical order. private func lowestLevel() -> FloorLevel? { let sortedLevels = levels.sorted { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 08442365b..e33ec5d78 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -21,14 +21,13 @@ struct SiteAndFacilitySelector: View { init(isHidden: Binding) { self.isHidden = isHidden } - + /// The view model used by the `SiteAndFacilitySelector`. - @EnvironmentObject - var floorFilterViewModel: FloorFilterViewModel - + @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding - + var body: some View { if let selectedSite = floorFilterViewModel.selectedSite { FacilitiesView(facilities: selectedSite.facilities, isHidden: isHidden) @@ -41,23 +40,21 @@ struct SiteAndFacilitySelector: View { SitesView(sites: floorFilterViewModel.sites, isHidden: isHidden) } } - + /// A view displaying the sites contained in a `FloorManager`. struct SitesView: View { /// The floor sites. let sites: [FloorSite] - + /// Allows the user to toggle the visibility of the sites. var isHidden: Binding - + /// The view model used by the `Sites`. - @EnvironmentObject - var floorFilterViewModel: FloorFilterViewModel - + @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel + /// The height of the scroll view's content. - @State - private var scrollViewContentHeight: CGFloat = .zero - + @State private var scrollViewContentHeight: CGFloat = .zero + var body: some View { VStack(alignment: .center) { Header(title: "Select a site", isHidden: isHidden) @@ -84,22 +81,21 @@ struct SiteAndFacilitySelector: View { } } } - + /// A view displaying the facilities contained in a `FloorManager`. struct FacilitiesView: View { /// The floor facilities. let facilities: [FloorFacility] - + /// Allows the user to toggle the visibility of the sites. var isHidden: Binding - + /// The view model used by the `SiteAndFacilitySelector`. @EnvironmentObject var floorFilterViewModel: FloorFilterViewModel - + /// The height of the scroll view's content. - @State - private var scrollViewContentHeight: CGFloat = .zero - + @State private var scrollViewContentHeight: CGFloat = .zero + var body: some View { VStack(alignment: .leading) { Header(title: "Select a facility", isHidden: isHidden) @@ -127,12 +123,12 @@ struct SiteAndFacilitySelector: View { } } } - + /// The header for a site or facility selector. struct Header: View { let title: String var isHidden: Binding - + var body: some View { HStack { Text(title) From 03b35ccaebe06563fd81013087c441bb955d34bb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 24 Mar 2022 11:52:40 -0700 Subject: [PATCH 0729/1722] Update Scalebar.swift --- Examples/Examples/ScalebarExampleView.swift | 54 ++--- .../Components/Scalebar/Scalebar.swift | 197 ++++++++++++------ .../Components/Scalebar/ScalebarUnits.swift | 52 +++-- 3 files changed, 188 insertions(+), 115 deletions(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index bf57df0ed..3dda3fcf4 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -16,40 +16,40 @@ import ArcGISToolkit import SwiftUI struct ScalebarExampleView: View { - /// The map displayed in the map view. + /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. + @State private var scale: Double? + + /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. + @State private var spatialReference: SpatialReference? + + /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. + @State private var viewpoint: Viewpoint? + + /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. + @State private var visibleArea: Polygon? + + /// The `Map` displayed in the `MapView`. private let map = Map(basemapStyle: .arcGISTopographic) - - @State - /// Allows for communication between the Scalebar and MapView or SceneView. - private var scale: Double? - - @State - /// Allows for communication between the Scalebar and MapView or SceneView. - private var spatialReference: SpatialReference? - - /// Allows for communication between the Scalebar and MapView or SceneView. - @State - private var viewpoint: Viewpoint? - - /// Allows for communication between the Scalebar and MapView or SceneView. - @State - private var visibleArea: Polygon? - + var body: some View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .onVisibleAreaChanged { visibleArea = $0 } .onScaleChanged { scale = $0 } .onSpatialReferenceChanged { spatialReference = $0 } - .overlay(alignment: .bottomLeading) { - Scalebar( - scale, - spatialReference, - 175, - viewpoint, - visibleArea, - units: .imperial - ) + .overlay(alignment: .bottomTrailing) { + if map.loadStatus == .loaded { + Scalebar( + scale, + spatialReference, + 175, + viewpoint, + visibleArea, + units: .imperial + ) + .padding(.leading, 10) + .padding(.bottom, 30) + } } } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index e29b3cfd2..5b4723c94 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -16,65 +16,66 @@ import SwiftUI /// Displays the current scale on-screen public struct Scalebar: View { + @State private var displayLength: CGFloat = .zero + + /// The vertical amount of space used by the scalebar. + @State private var height: Double = 50 + + @available(*, deprecated) + @State private var extentWidth: CGFloat = .zero + + @State private var mapLengthString: String = "none" + private var alignment: ScalebarAlignment - + private var alternateFillColor = Color.black - - @State - private var displayLength: CGFloat = .zero - - @State - private var extentWidth: CGFloat = .zero - + private var fillColor = Color(uiColor: .lightGray).opacity(0.5) - + private var font: Font - + private var geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic - + private var lineColor = Color.white - - @State - private var mapLengthString: String = "none" - + /// Acts as a data provider of the current scale. private var scale: Double? - + private var shadowColor = Color(uiColor: .black).opacity(0.65) - + private var spatialReference: SpatialReference? - + private var style: ScalebarStyle - + private var targetWidth: Double - + private var textColor = Color.black - + private var textShadowColor = Color.white - - /// Unit of measure for the scalebar. + + /// Unit of measure in use. private var units: ScalebarUnits - + private var unitsPerPoint: Double { (visibleArea?.extent.width ?? .zero) / extentWidth } - + /// Allows a user to toggle geodetic calculations. private var useGeodeticCalculations: Bool - + /// Acts as a data provider of the current scale. private var viewpoint: Viewpoint? - + /// Acts as a data provider of the current scale. private var visibleArea: Polygon? - + public init( _ scale: Double?, _ spatialReference: SpatialReference? = .wgs84, _ targetWidth: Double, _ viewpoint: Viewpoint?, _ visibleArea: Polygon?, - + alignment: ScalebarAlignment = .left, font: Font = .system(size: 9.0, weight: .semibold), style: ScalebarStyle = .line, @@ -85,7 +86,7 @@ public struct Scalebar: View { self.targetWidth = targetWidth self.viewpoint = viewpoint self.visibleArea = visibleArea - + self.alignment = alignment self.font = font self.spatialReference = spatialReference @@ -93,37 +94,45 @@ public struct Scalebar: View { self.units = units self.useGeodeticCalculations = useGeodeticCalculations } - + public var body: some View { GeometryReader { geometryProxy in VStack { Rectangle() - .fill(.white) - .border(.red) - .frame(width: displayLength, height: 5, alignment: .leading) - Button { - updateScaleDisplay(forceRedraw: false) - } label: { - Text("Force Update") - } + .fill(.gray) + .border( + .white, + width: 1.5 + ) + .frame( + width: displayLength, + height: 7, + alignment: .leading + ) + .shadow( + color: Color(uiColor: .lightGray), + radius: 1 + ) Text(mapLengthString) - } - .onChange(of: scale, perform: { newValue in - updateScaleDisplay(forceRedraw: false) - }) + .onChange(of: scale) { _ in + updateScaleDisplay() + } .onChange(of: geometryProxy.size) { extentWidth = $0.width - updateScaleDisplay(forceRedraw: false) + updateScaleDisplay() } .onAppear { extentWidth = geometryProxy.size.width - updateScaleDisplay(forceRedraw: false) + updateScaleDisplay() + } + .onSizeChange { + height = $0.height } } - .frame(height: 100, alignment: .bottomLeading) + .frame(height: height) } - + internal static let labelYPad: CGFloat = 2.0 internal static let labelXPad: CGFloat = 4.0 internal static let tickHeight: CGFloat = 6.0 @@ -137,20 +146,20 @@ public struct Scalebar: View { numberFormatter.minimumFractionDigits = 0 return numberFormatter }() - + internal static let lineCap = CGLineCap.round - + internal var fontHeight: CGFloat = 0 internal var zeroStringWidth: CGFloat = 0 internal var maxRightUnitsPad: CGFloat = 0 - + /// Set a minScale if you only want the scalebar to appear when you reach a large enough scale maybe /// something like 10_000_000. This could be useful because the scalebar is really only accurate for /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will /// always be visible private let minScale: Double = 0 - - private func updateScaleDisplay(forceRedraw: Bool) { + + private func updateScaleDisplay() { guard let scale = scale else { return } @@ -164,23 +173,40 @@ public struct Scalebar: View { return } let totalWidthAvailable = targetWidth - + // TODO: - Removal of hardcoded sample renderer property (~16 - 2) derived from sample renderer let maxLength = totalWidthAvailable - 16.30243742465973 - 2 - + let lineMapLength: Double let displayUnit: LinearUnit let mapCenter = visibleArea.extent.center let lineDisplayLength: CGFloat - + if useGeodeticCalculations || spatialReference?.unit is AngularUnit { let maxLengthPlanar = unitsPerPoint * Double(maxLength) - let p1 = Point(x: mapCenter.x - (maxLengthPlanar * 0.5), y: mapCenter.y, spatialReference: sr) - let p2 = Point(x: mapCenter.x + (maxLengthPlanar * 0.5), y: mapCenter.y, spatialReference: sr) - let polyline = Polyline(points: [p1, p2], spatialReference: spatialReference) + let p1 = Point( + x: mapCenter.x - (maxLengthPlanar * 0.5), + y: mapCenter.y, + spatialReference: sr + ) + let p2 = Point( + x: mapCenter.x + (maxLengthPlanar * 0.5), + y: mapCenter.y, + spatialReference: sr + ) + let polyline = Polyline( + points: [p1, p2], spatialReference: spatialReference + ) let baseUnits = units.baseUnits() - let maxLengthGeodetic = GeometryEngine.lengthGeodetic(geometry: polyline, lengthUnit: baseUnits, curveType: geodeticCurveType) - let roundNumberDistance = units.closestDistanceWithoutGoingOver(to: maxLengthGeodetic, units: baseUnits) + let maxLengthGeodetic = GeometryEngine.geodeticLength( + of: polyline, + lengthUnit: baseUnits, + curveType: geodeticCurveType + ) + let roundNumberDistance = units.closestDistanceWithoutGoingOver( + to: maxLengthGeodetic, + units: baseUnits + ) let planarToGeodeticFactor = maxLengthPlanar / maxLengthGeodetic lineDisplayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) @@ -191,19 +217,56 @@ public struct Scalebar: View { } let unitsPerPoint = unitsPerPoint let baseUnits = units.baseUnits() - let lenAvail = srUnit.convert(to: baseUnits, value: unitsPerPoint * Double(maxLength)) - let closestLen = units.closestDistanceWithoutGoingOver(to: lenAvail, units: baseUnits) - lineDisplayLength = CGFloat(baseUnits.convert(to: srUnit, value: closestLen) / unitsPerPoint) + let lenAvail = srUnit.convert( + to: baseUnits, + value: unitsPerPoint * Double(maxLength) + ) + let closestLen = units.closestDistanceWithoutGoingOver( + to: lenAvail, + units: baseUnits + ) + lineDisplayLength = CGFloat( + baseUnits.convert(to: srUnit, value: closestLen) / unitsPerPoint) displayUnit = units.linearUnitsForDistance(distance: closestLen) - lineMapLength = baseUnits.convert(to: displayUnit, value: closestLen) + lineMapLength = baseUnits.convert( + to: displayUnit, + value: closestLen + ) } - + guard lineDisplayLength.isFinite, !lineDisplayLength.isNaN else { return } - + displayLength = lineDisplayLength - + mapLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" } } + +// - TODO: Temporary as another PR in-flight contains this extension. +extension View { + /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. + /// - Parameter perform: The closure to be executed when the content size of the receiver + /// changes. + /// - Returns: A new `View`. + func onSizeChange(perform: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometry in + Color.clear + .preference( + key: SizePreferenceKey.self, value: geometry.size + ) + } + ) + .onPreferenceChange(SizePreferenceKey.self, perform: perform) + } + +} + +// - TODO: Temporary as another PR in-flight contains this extension. +/// A `PreferenceKey` that specifies a size. +struct SizePreferenceKey: PreferenceKey { + static let defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} +} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index 37ab7d9e0..4595f853d 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -17,53 +17,63 @@ import Foundation public enum ScalebarUnits { case imperial case metric - + internal func baseUnits() -> LinearUnit { return self == .imperial ? LinearUnit.feet : LinearUnit.meters } - + + /// Get multiplier private static func multiplierAndMagnitudeForDistance(distance: Double) -> (multiplier: Double, magnitude: Double) { - // get multiplier - let magnitude = pow(10, floor(log10(distance))) let residual = distance / Double(magnitude) let multiplier: Double = ScalebarUnits.roundNumberMultipliers.filter { $0 <= residual }.last ?? 0 return (multiplier, magnitude) } - - internal func closestDistanceWithoutGoingOver(to distance: Double, units: LinearUnit) -> Double { + + internal func closestDistanceWithoutGoingOver( + to distance: Double, + units: LinearUnit + ) -> Double { let mm = ScalebarUnits.multiplierAndMagnitudeForDistance(distance: distance) let roundNumber = mm.multiplier * mm.magnitude - - // because feet and miles are not relationally multiples of 10 with each other, - // we have to convert to miles if we are dealing in miles + + // Because feet and miles are not relationally multiples of 10 with + // each other, we have to convert to miles if we are dealing in miles. if units == .feet { let displayUnits = linearUnitsForDistance(distance: roundNumber) if units != displayUnits { -// let displayDistance = closestDistanceWithoutGoingOver(to: units.convert(value: distance, to: displayUnits), units: displayUnits) - let displayDistance = closestDistanceWithoutGoingOver(to: units.convert(to: displayUnits, value: distance), units: displayUnits) -// return displayUnits.convert(displayDistance, to: units) - return displayUnits.convert(to: units, value: displayDistance) + let displayDistance = closestDistanceWithoutGoingOver( + to: units.convert( + to: displayUnits, + value: distance + ), + units: displayUnits + ) + return displayUnits.convert( + to: units, + value: displayDistance + ) } } - + return roundNumber } -// -// // this table must begin with 1 and end with 10 - private static let roundNumberMultipliers: [Double] = [1, 1.2, 1.25, 1.5, 1.75, 2, 2.4, 2.5, 3, 3.75, 4, 5, 6, 7.5, 8, 9, 10] - + + /// This table must begin with 1 and end with 10. + private static let roundNumberMultipliers: [Double] = + [1, 1.2, 1.25, 1.5, 1.75, 2, 2.4, 2.5, 3, 3.75, 4, 5, 6, 7.5, 8, 9, 10] + internal func linearUnitsForDistance(distance: Double) -> LinearUnit { switch self { case .imperial: - + if distance >= 2640 { return LinearUnit.miles } return LinearUnit.feet - + case .metric: - + if distance >= 1000 { return LinearUnit.kilometers } From 035eafc0e5280819825381a0decbdda3e356b79c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Mar 2022 10:54:15 -0700 Subject: [PATCH 0730/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- 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 b620886be..483094e38 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -40,7 +40,7 @@ public struct Bookmarks: View { /// User defined action to be performed when a bookmark is selected. /// /// Use this when you prefer to self-manage the response to a bookmark selection. Use either - /// `onSelectionChanged` or `viewpoint` exclusively. + /// `onSelectionChanged(perform:)` or `viewpoint` exclusively. var selectionChangedAction: ((Bookmark) -> Void)? = nil /// If non-`nil`, this viewpoint is updated when a bookmark is selected. From 0e7e7587574356d19d3238a07f311e0edf7f5521 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Mar 2022 10:54:23 -0700 Subject: [PATCH 0731/1722] Update Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift Co-authored-by: Zachary A Kline --- 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 483094e38..ba8cac697 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/Bookmarks.swift @@ -59,7 +59,7 @@ public struct Bookmarks: View { /// Performs the necessary actions when a bookmark is selected. /// /// This includes indicating that bookmarks should be set to a hidden state, and changing the viewpoint - /// binding (if provided) or calling the action provided by the `onSelectionChanged` modifier. + /// binding (if provided) or calling the action provided by the `onSelectionChanged(perform:)` modifier. /// - Parameter bookmark: The bookmark that was selected. func selectBookmark(_ bookmark: Bookmark) { isPresented = false From 0dd1eeb82ca1601b6704b7575fa5309981b8dda3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 25 Mar 2022 15:35:04 -0700 Subject: [PATCH 0732/1722] Scalebar progress --- Examples/Examples/ScalebarExampleView.swift | 13 +- .../Components/Scalebar/Scalebar.swift | 162 ++++++++++++------ 2 files changed, 118 insertions(+), 57 deletions(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 3dda3fcf4..1d2349ea5 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -22,6 +22,8 @@ struct ScalebarExampleView: View { /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. @State private var spatialReference: SpatialReference? + @State private var unitsPerPoint: Double? + /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. @State private var viewpoint: Viewpoint? @@ -37,18 +39,21 @@ struct ScalebarExampleView: View { .onVisibleAreaChanged { visibleArea = $0 } .onScaleChanged { scale = $0 } .onSpatialReferenceChanged { spatialReference = $0 } - .overlay(alignment: .bottomTrailing) { + .onUnitsPerPointChanged { unitsPerPoint = $0 } + .overlay(alignment: .bottomLeading) { if map.loadStatus == .loaded { Scalebar( - scale, + .left, spatialReference, + .alternatingBar, 175, + $unitsPerPoint, viewpoint, visibleArea, units: .imperial ) - .padding(.leading, 10) - .padding(.bottom, 30) + .padding(.horizontal, 10) + .padding(.vertical, 30) } } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 5b4723c94..ff525b892 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -18,14 +18,15 @@ import SwiftUI public struct Scalebar: View { @State private var displayLength: CGFloat = .zero + @State private var displayUnit: LinearUnit? = nil + /// The vertical amount of space used by the scalebar. @State private var height: Double = 50 - @available(*, deprecated) - @State private var extentWidth: CGFloat = .zero - @State private var mapLengthString: String = "none" + @Binding private var unitsPerPoint: Double? + private var alignment: ScalebarAlignment private var alternateFillColor = Color.black @@ -38,9 +39,6 @@ public struct Scalebar: View { private var lineColor = Color.white - /// Acts as a data provider of the current scale. - private var scale: Double? - private var shadowColor = Color(uiColor: .black).opacity(0.65) private var spatialReference: SpatialReference? @@ -56,10 +54,6 @@ public struct Scalebar: View { /// Unit of measure in use. private var units: ScalebarUnits - private var unitsPerPoint: Double { - (visibleArea?.extent.width ?? .zero) / extentWidth - } - /// Allows a user to toggle geodetic calculations. private var useGeodeticCalculations: Bool @@ -69,20 +63,22 @@ public struct Scalebar: View { /// Acts as a data provider of the current scale. private var visibleArea: Polygon? + @State private var finalLengthWidth: Double = .zero + public init( - _ scale: Double?, + _ alignment: ScalebarAlignment = .left, _ spatialReference: SpatialReference? = .wgs84, + _ style: ScalebarStyle = .alternatingBar, _ targetWidth: Double, + _ unitsPerPoint: Binding, _ viewpoint: Viewpoint?, _ visibleArea: Polygon?, - alignment: ScalebarAlignment = .left, font: Font = .system(size: 9.0, weight: .semibold), - style: ScalebarStyle = .line, + units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, useGeodeticCalculations: Bool = true ) { - self.scale = scale self.targetWidth = targetWidth self.viewpoint = viewpoint self.visibleArea = visibleArea @@ -92,45 +88,34 @@ public struct Scalebar: View { self.spatialReference = spatialReference self.style = style self.units = units + self.useGeodeticCalculations = useGeodeticCalculations + + self._unitsPerPoint = unitsPerPoint } public var body: some View { - GeometryReader { geometryProxy in - VStack { - Rectangle() - .fill(.gray) - .border( - .white, - width: 1.5 - ) - .frame( - width: displayLength, - height: 7, - alignment: .leading - ) - .shadow( - color: Color(uiColor: .lightGray), - radius: 1 - ) - Text(mapLengthString) - } - .onChange(of: scale) { _ in - updateScaleDisplay() - } - .onChange(of: geometryProxy.size) { - extentWidth = $0.width - updateScaleDisplay() - } - .onAppear { - extentWidth = geometryProxy.size.width - updateScaleDisplay() - } - .onSizeChange { - height = $0.height + Group { + if style == .alternatingBar { + alternatingBarView + } else { + barView } } - .frame(height: height) + .onChange(of: visibleArea) { _ in + updateScaleDisplay() + } + .onAppear { + updateScaleDisplay() + } + .onSizeChange { + height = $0.height + } + .frame( + width: displayLength, + height: height + ) +// .border(.red) } internal static let labelYPad: CGFloat = 2.0 @@ -160,12 +145,15 @@ public struct Scalebar: View { private let minScale: Double = 0 private func updateScaleDisplay() { - guard let scale = scale else { + guard let scale = viewpoint?.targetScale else { return } guard minScale <= 0 || scale < minScale else { return } + guard let unitsPerPoint = unitsPerPoint else { + return + } guard let visibleArea = visibleArea else { return } @@ -180,7 +168,7 @@ public struct Scalebar: View { let lineMapLength: Double let displayUnit: LinearUnit let mapCenter = visibleArea.extent.center - let lineDisplayLength: CGFloat + let displayLength: CGFloat if useGeodeticCalculations || spatialReference?.unit is AngularUnit { let maxLengthPlanar = unitsPerPoint * Double(maxLength) @@ -208,7 +196,7 @@ public struct Scalebar: View { units: baseUnits ) let planarToGeodeticFactor = maxLengthPlanar / maxLengthGeodetic - lineDisplayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) + displayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) lineMapLength = baseUnits.convert(to: displayUnit, value: roundNumberDistance) } else { @@ -225,7 +213,7 @@ public struct Scalebar: View { to: lenAvail, units: baseUnits ) - lineDisplayLength = CGFloat( + displayLength = CGFloat( baseUnits.convert(to: srUnit, value: closestLen) / unitsPerPoint) displayUnit = units.linearUnitsForDistance(distance: closestLen) lineMapLength = baseUnits.convert( @@ -234,16 +222,84 @@ public struct Scalebar: View { ) } - guard lineDisplayLength.isFinite, !lineDisplayLength.isNaN else { + guard displayLength.isFinite, !displayLength.isNaN else { return } - displayLength = lineDisplayLength + self.displayLength = displayLength + + self.displayUnit = displayUnit mapLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" } } +extension Scalebar { + var alternatingBarView: some View { + VStack(spacing: 2) { + Rectangle() + .fill(.gray) + .border( + .white, + width: 1.5 + ) + .frame( + width: displayLength, + height: 7, + alignment: .leading + ) + .shadow( + color: Color(uiColor: .lightGray), + radius: 1 + ) + HStack { + Text("0") + .font(.system(size: 10)) + .fontWeight(.semibold) + .frame(maxWidth: .infinity, alignment: .leading) + Text("\(mapLengthString) \(displayUnit?.abbreviation ?? "")") + .font(.system(size: 10)) + .fontWeight(.semibold) + .onSizeChange { + finalLengthWidth = $0.width + } + .frame(maxWidth: .infinity, alignment: .trailing) + .offset(x: finalLengthWidth / 2) + } + } + } + + var barView: some View { + VStack(spacing: 2) { + Rectangle() + .fill(.gray) + .border( + .white, + width: 1.5 + ) + .frame( + width: displayLength, + height: 7, + alignment: .leading + ) + .shadow( + color: Color(uiColor: .lightGray), + radius: 1 + ) + HStack { + Text("\(mapLengthString) \(displayUnit?.abbreviation ?? "")") + .font(.system(size: 10)) + .fontWeight(.semibold) + .onSizeChange { + finalLengthWidth = $0.width + } + .frame(maxWidth: .infinity, alignment: .center) + } + } + } +} + + // - TODO: Temporary as another PR in-flight contains this extension. extension View { /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. From aa3917cc33681ba2dbc0c4ee5081b2c1b398861a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 28 Mar 2022 10:55:06 -0700 Subject: [PATCH 0733/1722] Label alignment param --- Examples/Examples/ScalebarExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 1d2349ea5..6983f7ddc 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -43,7 +43,7 @@ struct ScalebarExampleView: View { .overlay(alignment: .bottomLeading) { if map.loadStatus == .loaded { Scalebar( - .left, + alignment: .left, spatialReference, .alternatingBar, 175, From 5bd629da006d700c3e9797d68b98bb2e29d56a0a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 28 Mar 2022 10:55:14 -0700 Subject: [PATCH 0734/1722] Unify font --- .../ArcGISToolkit/Components/Scalebar/Scalebar.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index ff525b892..e34163e3a 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -66,7 +66,7 @@ public struct Scalebar: View { @State private var finalLengthWidth: Double = .zero public init( - _ alignment: ScalebarAlignment = .left, + alignment: ScalebarAlignment = .left, _ spatialReference: SpatialReference? = .wgs84, _ style: ScalebarStyle = .alternatingBar, _ targetWidth: Double, @@ -74,7 +74,7 @@ public struct Scalebar: View { _ viewpoint: Viewpoint?, _ visibleArea: Polygon?, - font: Font = .system(size: 9.0, weight: .semibold), + font: Font = .system(size: 10.0, weight: .semibold), units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, useGeodeticCalculations: Bool = true @@ -254,11 +254,11 @@ extension Scalebar { ) HStack { Text("0") - .font(.system(size: 10)) + .font(font) .fontWeight(.semibold) .frame(maxWidth: .infinity, alignment: .leading) Text("\(mapLengthString) \(displayUnit?.abbreviation ?? "")") - .font(.system(size: 10)) + .font(font) .fontWeight(.semibold) .onSizeChange { finalLengthWidth = $0.width @@ -288,7 +288,7 @@ extension Scalebar { ) HStack { Text("\(mapLengthString) \(displayUnit?.abbreviation ?? "")") - .font(.system(size: 10)) + .font(font) .fontWeight(.semibold) .onSizeChange { finalLengthWidth = $0.width From 5f409fea4a96f21a9c077813e1e9d6d1ff7e7273 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Mon, 28 Mar 2022 11:38:50 -0700 Subject: [PATCH 0735/1722] `FloorFacility` and `FloorSite` now conform to `Identifiable` --- Sources/ArcGISToolkit/Extensions/FloorFacility.swift | 6 +----- Sources/ArcGISToolkit/Extensions/FloorSite.swift | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift index b7efe890e..4924a27b5 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift @@ -16,10 +16,6 @@ import ArcGIS extension FloorFacility: Equatable { public static func == (lhs: FloorFacility, rhs: FloorFacility) -> Bool { - lhs.facilityId == rhs.facilityId + lhs.id == rhs.id } } - -extension FloorFacility: Identifiable { - public var id: String { facilityId } -} diff --git a/Sources/ArcGISToolkit/Extensions/FloorSite.swift b/Sources/ArcGISToolkit/Extensions/FloorSite.swift index 2cf858aad..ff8b5faa2 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorSite.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorSite.swift @@ -16,10 +16,6 @@ import ArcGIS extension FloorSite: Equatable { public static func == (lhs: FloorSite, rhs: FloorSite) -> Bool { - lhs.siteId == rhs.siteId + lhs.id == rhs.id } } - -extension FloorSite: Identifiable { - public var id: String { siteId } -} From 9e11c269acf758a77de22a77b4e7f0c2ef9a9784 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 28 Mar 2022 17:26:43 -0700 Subject: [PATCH 0736/1722] throttle updates --- Examples/Examples/ScalebarExampleView.swift | 4 +- .../Components/Scalebar/Scalebar.swift | 155 +++------------ .../Scalebar/ScalebarViewModel.swift | 177 ++++++++++++++++++ 3 files changed, 205 insertions(+), 131 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 6983f7ddc..bc5560a94 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -48,8 +48,8 @@ struct ScalebarExampleView: View { .alternatingBar, 175, $unitsPerPoint, - viewpoint, - visibleArea, + $viewpoint, + $visibleArea, units: .imperial ) .padding(.horizontal, 10) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index e34163e3a..cb6b026bf 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -16,16 +16,14 @@ import SwiftUI /// Displays the current scale on-screen public struct Scalebar: View { - @State private var displayLength: CGFloat = .zero - - @State private var displayUnit: LinearUnit? = nil + /// The view model used by the `Scalebar`. + @StateObject + private var viewModel: ScalebarViewModel /// The vertical amount of space used by the scalebar. @State private var height: Double = 50 - @State private var mapLengthString: String = "none" - - @Binding private var unitsPerPoint: Double? + @State private var finalLengthWidth: Double = .zero private var alignment: ScalebarAlignment @@ -35,35 +33,21 @@ public struct Scalebar: View { private var font: Font - private var geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic - private var lineColor = Color.white private var shadowColor = Color(uiColor: .black).opacity(0.65) - private var spatialReference: SpatialReference? - private var style: ScalebarStyle - private var targetWidth: Double - private var textColor = Color.black private var textShadowColor = Color.white - /// Unit of measure in use. - private var units: ScalebarUnits - - /// Allows a user to toggle geodetic calculations. - private var useGeodeticCalculations: Bool - /// Acts as a data provider of the current scale. - private var viewpoint: Viewpoint? + private var viewpoint: Binding /// Acts as a data provider of the current scale. - private var visibleArea: Polygon? - - @State private var finalLengthWidth: Double = .zero + private var visibleArea: Binding public init( alignment: ScalebarAlignment = .left, @@ -71,27 +55,32 @@ public struct Scalebar: View { _ style: ScalebarStyle = .alternatingBar, _ targetWidth: Double, _ unitsPerPoint: Binding, - _ viewpoint: Viewpoint?, - _ visibleArea: Polygon?, + _ viewpoint: Binding, + _ visibleArea: Binding, font: Font = .system(size: 10.0, weight: .semibold), units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, useGeodeticCalculations: Bool = true ) { - self.targetWidth = targetWidth self.viewpoint = viewpoint self.visibleArea = visibleArea self.alignment = alignment self.font = font - self.spatialReference = spatialReference self.style = style - self.units = units - self.useGeodeticCalculations = useGeodeticCalculations - - self._unitsPerPoint = unitsPerPoint + _viewModel = StateObject( + wrappedValue: ScalebarViewModel( + spatialReference, + targetWidth, + units, + unitsPerPoint, + useGeodeticCalculations, + viewpoint, + visibleArea + ) + ) } public var body: some View { @@ -102,17 +91,14 @@ public struct Scalebar: View { barView } } - .onChange(of: visibleArea) { _ in - updateScaleDisplay() - } - .onAppear { - updateScaleDisplay() + .onChange(of: visibleArea.wrappedValue) { + viewModel.subject.send($0) } .onSizeChange { height = $0.height } .frame( - width: displayLength, + width: $viewModel.displayLength.wrappedValue, height: height ) // .border(.red) @@ -143,95 +129,6 @@ public struct Scalebar: View { /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will /// always be visible private let minScale: Double = 0 - - private func updateScaleDisplay() { - guard let scale = viewpoint?.targetScale else { - return - } - guard minScale <= 0 || scale < minScale else { - return - } - guard let unitsPerPoint = unitsPerPoint else { - return - } - guard let visibleArea = visibleArea else { - return - } - guard let sr = spatialReference else { - return - } - let totalWidthAvailable = targetWidth - - // TODO: - Removal of hardcoded sample renderer property (~16 - 2) derived from sample renderer - let maxLength = totalWidthAvailable - 16.30243742465973 - 2 - - let lineMapLength: Double - let displayUnit: LinearUnit - let mapCenter = visibleArea.extent.center - let displayLength: CGFloat - - if useGeodeticCalculations || spatialReference?.unit is AngularUnit { - let maxLengthPlanar = unitsPerPoint * Double(maxLength) - let p1 = Point( - x: mapCenter.x - (maxLengthPlanar * 0.5), - y: mapCenter.y, - spatialReference: sr - ) - let p2 = Point( - x: mapCenter.x + (maxLengthPlanar * 0.5), - y: mapCenter.y, - spatialReference: sr - ) - let polyline = Polyline( - points: [p1, p2], spatialReference: spatialReference - ) - let baseUnits = units.baseUnits() - let maxLengthGeodetic = GeometryEngine.geodeticLength( - of: polyline, - lengthUnit: baseUnits, - curveType: geodeticCurveType - ) - let roundNumberDistance = units.closestDistanceWithoutGoingOver( - to: maxLengthGeodetic, - units: baseUnits - ) - let planarToGeodeticFactor = maxLengthPlanar / maxLengthGeodetic - displayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) - displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) - lineMapLength = baseUnits.convert(to: displayUnit, value: roundNumberDistance) - } else { - guard let srUnit = sr.unit as? LinearUnit else { - return - } - let unitsPerPoint = unitsPerPoint - let baseUnits = units.baseUnits() - let lenAvail = srUnit.convert( - to: baseUnits, - value: unitsPerPoint * Double(maxLength) - ) - let closestLen = units.closestDistanceWithoutGoingOver( - to: lenAvail, - units: baseUnits - ) - displayLength = CGFloat( - baseUnits.convert(to: srUnit, value: closestLen) / unitsPerPoint) - displayUnit = units.linearUnitsForDistance(distance: closestLen) - lineMapLength = baseUnits.convert( - to: displayUnit, - value: closestLen - ) - } - - guard displayLength.isFinite, !displayLength.isNaN else { - return - } - - self.displayLength = displayLength - - self.displayUnit = displayUnit - - mapLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" - } } extension Scalebar { @@ -244,7 +141,7 @@ extension Scalebar { width: 1.5 ) .frame( - width: displayLength, + width: $viewModel.displayLength.wrappedValue, height: 7, alignment: .leading ) @@ -257,7 +154,7 @@ extension Scalebar { .font(font) .fontWeight(.semibold) .frame(maxWidth: .infinity, alignment: .leading) - Text("\(mapLengthString) \(displayUnit?.abbreviation ?? "")") + Text("\($viewModel.mapLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") .font(font) .fontWeight(.semibold) .onSizeChange { @@ -278,7 +175,7 @@ extension Scalebar { width: 1.5 ) .frame( - width: displayLength, + width: $viewModel.displayLength.wrappedValue, height: 7, alignment: .leading ) @@ -287,7 +184,7 @@ extension Scalebar { radius: 1 ) HStack { - Text("\(mapLengthString) \(displayUnit?.abbreviation ?? "")") + Text("\($viewModel.mapLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") .font(font) .fontWeight(.semibold) .onSizeChange { diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift new file mode 100644 index 000000000..9e9f3c1fe --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -0,0 +1,177 @@ +// 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 +import Combine +import Foundation +import SwiftUI + +@MainActor +final class ScalebarViewModel: ObservableObject { + @Published var displayLength: CGFloat = .zero + + @Published var displayUnit: LinearUnit? = nil + + @Published var mapLengthString = "" + + var subject = PassthroughSubject() + + private var cancellable: AnyCancellable? + + private var cancellable2: AnyCancellable? + + /// The amount of time to wait between value calculations. + private var delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.05) + + private var spatialReference: SpatialReference? = .wgs84 + + private var targetWidth: Double + + private var unitsPerPoint: Binding + + /// Allows a user to toggle geodetic calculations. + private var useGeodeticCalculations: Bool + + /// Acts as a data provider of the current scale. + private var viewpoint: Binding + + /// Acts as a data provider of the current scale. + private var visibleArea: Binding + + /// Unit of measure in use. + private var units: ScalebarUnits + + private var geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic + + /// Set a minScale if you only want the scalebar to appear when you reach a large enough scale maybe + /// something like 10_000_000. This could be useful because the scalebar is really only accurate for + /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will + /// always be visible + private let minScale: Double = 0 + + init( + _ spatialReference: SpatialReference? = .wgs84, + _ targetWidth: Double, + _ units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, + _ unitsPerPoint: Binding, + _ useGeodeticCalculations: Bool = true, + _ viewpoint: Binding, + _ visibleArea: Binding + + ) { + self.spatialReference = spatialReference + self.targetWidth = targetWidth + self.units = units + self.unitsPerPoint = unitsPerPoint + self.useGeodeticCalculations = useGeodeticCalculations + self.viewpoint = viewpoint + self.visibleArea = visibleArea + + cancellable = subject + .debounce(for: delay, scheduler: DispatchQueue.main) + .sink(receiveValue: { [weak self] _ in + self?.updateScaleDisplay() + }) + + updateScaleDisplay() + } + + private func updateScaleDisplay() { + guard let scale = viewpoint.wrappedValue?.targetScale else { + return + } + guard minScale <= 0 || scale < minScale else { + return + } + guard let unitsPerPoint = unitsPerPoint.wrappedValue else { + return + } + guard let visibleArea = visibleArea.wrappedValue else { + return + } + guard let sr = spatialReference else { + return + } + let totalWidthAvailable = targetWidth + + // TODO: - Removal of hardcoded sample renderer property (~16 - 2) derived from sample renderer + let maxLength = totalWidthAvailable - 16.30243742465973 - 2 + + let lineMapLength: Double + let displayUnit: LinearUnit + let mapCenter = visibleArea.extent.center + let displayLength: CGFloat + + if useGeodeticCalculations || spatialReference?.unit is AngularUnit { + let maxLengthPlanar = unitsPerPoint * Double(maxLength) + let p1 = Point( + x: mapCenter.x - (maxLengthPlanar * 0.5), + y: mapCenter.y, + spatialReference: sr + ) + let p2 = Point( + x: mapCenter.x + (maxLengthPlanar * 0.5), + y: mapCenter.y, + spatialReference: sr + ) + let polyline = Polyline( + points: [p1, p2], spatialReference: spatialReference + ) + let baseUnits = units.baseUnits() + let maxLengthGeodetic = GeometryEngine.geodeticLength( + of: polyline, + lengthUnit: baseUnits, + curveType: geodeticCurveType + ) + let roundNumberDistance = units.closestDistanceWithoutGoingOver( + to: maxLengthGeodetic, + units: baseUnits + ) + let planarToGeodeticFactor = maxLengthPlanar / maxLengthGeodetic + displayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) + displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) + lineMapLength = baseUnits.convert(to: displayUnit, value: roundNumberDistance) + } else { + guard let srUnit = sr.unit as? LinearUnit else { + return + } + let unitsPerPoint = unitsPerPoint + let baseUnits = units.baseUnits() + let lenAvail = srUnit.convert( + to: baseUnits, + value: unitsPerPoint * Double(maxLength) + ) + let closestLen = units.closestDistanceWithoutGoingOver( + to: lenAvail, + units: baseUnits + ) + displayLength = CGFloat( + baseUnits.convert(to: srUnit, value: closestLen) / unitsPerPoint) + displayUnit = units.linearUnitsForDistance(distance: closestLen) + lineMapLength = baseUnits.convert( + to: displayUnit, + value: closestLen + ) + } + + guard displayLength.isFinite, !displayLength.isNaN else { + return + } + + self.displayLength = displayLength + + self.displayUnit = displayUnit + + mapLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" + } +} From 62a4764276478fd5d691bef9fc421222adb96410 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 29 Mar 2022 10:04:39 -0700 Subject: [PATCH 0737/1722] Fix build to compile with latest SwiftSDK updates --- .../Examples/FloorFilterExampleView.swift | 117 +++++++++--------- .../Components/FloorFilter/FloorFilter.swift | 47 ++++--- .../FloorFilter/FloorFilterViewModel.swift | 76 ++++++------ .../FloorFilter/SiteAndFacilitySelector.swift | 74 +++++------ .../Extensions/FloorFacility.swift | 6 +- .../ArcGISToolkit/Extensions/FloorLevel.swift | 4 +- .../ArcGISToolkit/Extensions/FloorSite.swift | 6 +- .../SearchViewModelTests.swift | 32 ++--- 8 files changed, 172 insertions(+), 190 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 559857736..7e5016ad7 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -19,80 +19,81 @@ struct FloorFilterExampleView: View { /// Make a map from a portal item. static func makeMap() -> Map { // Multiple sites/facilities: Esri IST map with all buildings. -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - - // Redlands Campus map with multiple sites and facilities + // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) + + // Redlands Campus map. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) - + let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities + // Single site (ESRI Redlands Main) and facility (Building L). -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - + // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) + return Map(item: portalItem) } - + /// Determines the arrangement of the inner `FloorFilter` UI componenets. private let filterAlignment = Alignment.bottomLeading - + /// Determines the appropriate time to initialize the `FloorFilter`. - @State - private var isMapLoaded: Bool = false - - /// The `Map` that will be provided to the `MapView`. - private var map = makeMap() - - @State - private var mapLoadError: Bool = false - + @State private var isMapLoaded: Bool = false + + @State private var mapLoadError: Bool = false + /// The initial viewpoint of the map. - @State - private var viewpoint: Viewpoint? = Viewpoint( - center: Point(x: -117.19496, y: 34.05713, spatialReference: .wgs84), + @State private var viewpoint: Viewpoint? = Viewpoint( + center: Point( + x: -117.19496, + y: 34.05713, + spatialReference: .wgs84 + ), scale: 100_000 ) - + + private var map = makeMap() + var body: some View { MapView( map: map, viewpoint: viewpoint ) - .onViewpointChanged(kind: .centerAndScale) { - viewpoint = $0 - } - /// Preserve the current viewpoint when a keyboard is presented in landscape. - .ignoresSafeArea(.keyboard, edges: .bottom) - .overlay(alignment: filterAlignment) { - if isMapLoaded, - let floorManager = map.floorManager { - FloorFilter( - alignment: filterAlignment, - floorManager: floorManager, - viewpoint: $viewpoint - ) - .frame(maxWidth: 300, maxHeight: 300) - .padding(36) - } else if mapLoadError { - Label( - "Map load error!", - systemImage: "exclamationmark.triangle" - ) - .foregroundColor(.red) - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .center - ) - } + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } + /// Preserve the current viewpoint when a keyboard is presented in landscape. + .ignoresSafeArea(.keyboard, edges: .bottom) + .overlay(alignment: filterAlignment) { + if isMapLoaded, + let floorManager = map.floorManager { + FloorFilter( + alignment: filterAlignment, + automaticSelectionMode: .alwaysNotClearing, + floorManager: floorManager, + viewpoint: $viewpoint + ) + .frame(maxWidth: 300, maxHeight: 300) + .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 } + } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 461fc9495..dc289fce0 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -38,22 +38,19 @@ public struct FloorFilter: View { self.alignment = alignment self.viewpoint = viewpoint } - + /// The view model used by the `FloorFilter`. - @StateObject - private var viewModel: FloorFilterViewModel - + @StateObject private var viewModel: FloorFilterViewModel + /// A Boolean value that indicates whether the levels view is currently collapsed. - @State - private var isLevelsViewCollapsed: Bool = false - + @State private var isLevelsViewCollapsed: Bool = false + /// A Boolean value that indicates whether the site/facility selector is hidden. - @State - private var isSelectorHidden: Bool = true - + @State private var isSelectorHidden: Bool = true + /// The alignment configuration. private let alignment: Alignment - + /// Button to open and close the site and facility selector. private var facilityButtonView: some View { Button { @@ -61,16 +58,16 @@ public struct FloorFilter: View { } label: { Image(systemName: "building.2") } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity) } - + /// 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. private var hasLevelsToDisplay: Bool { !(viewModel.selectedFacility == nil || viewModel.selectedFacility!.levels.isEmpty) } - + /// Displays the available levels. private var levelsSelectorView: some View { Group { @@ -91,7 +88,7 @@ public struct FloorFilter: View { } } } - + /// A view that allows selecting between levels. private var levelSelectorView: some View { VStack { @@ -112,9 +109,9 @@ public struct FloorFilter: View { Spacer() } } - .frame(width: 75) + .frame(width: 75) } - + /// Indicates that the selector should be presented with a right oriented aligment configuration. private var rightAligned: Bool { switch alignment { @@ -124,7 +121,7 @@ public struct FloorFilter: View { return false } } - + /// A configured `SiteAndFacilitySelector` view. private var siteAndFacilitySelectorView: some View { SiteAndFacilitySelector(isHidden: $isSelectorHidden) @@ -134,7 +131,7 @@ public struct FloorFilter: View { viewModel.updateSelection() } } - + /// The selected facility's levels, sorted by `level.verticalOrder`. private var sortedLevels: [FloorLevel] { let levels = viewModel.selectedFacility?.levels ?? [] @@ -142,7 +139,7 @@ public struct FloorFilter: View { $0.verticalOrder > $1.verticalOrder } } - + /// Indicates that the selector should be presented with a top oriented aligment configuration. private var topAligned: Bool { switch alignment { @@ -152,11 +149,11 @@ public struct FloorFilter: View { return false } } - + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. private var viewpoint: Binding - + public var body: some View { Group { if viewModel.isLoading { @@ -178,8 +175,8 @@ public struct FloorFilter: View { } } } - // Ensure space for filter text field on small screens in landscape - .frame(minHeight: 100) - .environmentObject(viewModel) + // Ensure space for filter text field on small screens in landscape + .frame(minHeight: 100) + .environmentObject(viewModel) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 1e8f7eab0..3827aa7f6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -30,6 +30,7 @@ final class FloorFilterViewModel: ObservableObject { self.automaticSelectionMode = automaticSelectionMode self.floorManager = floorManager self.viewpoint = viewpoint + Task { do { try await floorManager.load() @@ -44,56 +45,56 @@ final class FloorFilterViewModel: ObservableObject { isLoading = false } } - + // MARK: Published members - + /// The currently selected `FloorFacility`. @Published var selectedFacility: FloorFacility? - + /// The currently selected `FloorLevel`. @Published var selectedLevel: FloorLevel? - + /// The currently selected `FloorSite`. @Published var selectedSite: FloorSite? - + /// `true` if the model is loading it's properties, `false` if not loading. @Published private(set) var isLoading = true - + // MARK: Constants - + /// The selection behavior of the floor filter. private let automaticSelectionMode: AutomaticSelectionMode - + /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager - + // MARK: Public members - + /// The floor manager facilities. var facilities: [FloorFacility] { floorManager.facilities } - + /// The floor manager levels. var levels: [FloorLevel] { floorManager.levels } - + /// The floor manager sites. var sites: [FloorSite] { floorManager.sites } - + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding - + // MARK: Public methods - + /// Gets the default level for a facility. /// - Parameter facility: The facility to get the default level for. /// - Returns: The default level for the facility, which is the level with vertical order 0; @@ -103,7 +104,7 @@ final class FloorFilterViewModel: ObservableObject { level.facility == facility && level.verticalOrder == .zero }) ?? lowestLevel() } - + /// Updates the selected site, facility, and level based on a newly selected facility. /// - Parameters: /// - floorFacility: The selected facility. @@ -125,14 +126,14 @@ final class FloorFilterViewModel: ObservableObject { zoomToExtent(extent: floorFacility?.geometry?.extent) } } - + /// Updates the selected site, facility, and level based on a newly selected level. /// - Parameter floorLevel: The selected level. func setLevel(_ floorLevel: FloorLevel?) { selectedLevel = floorLevel filterMapToSelectedLevel() } - + /// Updates the selected site, facility, and level based on a newly selected site. /// - Parameters: /// - floorSite: The selected site. @@ -146,7 +147,7 @@ final class FloorFilterViewModel: ObservableObject { zoomToExtent(extent: floorSite?.geometry?.extent) } } - + /// Updates `selectedSite` and `selectedFacility` based on the latest viewpoint position. func updateSelection() { guard let viewpoint = viewpoint.wrappedValue, @@ -157,9 +158,9 @@ final class FloorFilterViewModel: ObservableObject { updateSelectedSite() updateSelectedFacility() } - + // MARK: Private methods - + /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. private func filterMapToSelectedLevel() { guard let selectedLevel = selectedLevel else { return } @@ -167,7 +168,7 @@ final class FloorFilterViewModel: ObservableObject { $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder } } - + /// Returns the level with the lowest vertical order. private func lowestLevel() -> FloorLevel? { let sortedLevels = levels.sorted { @@ -177,7 +178,7 @@ final class FloorFilterViewModel: ObservableObject { $0.verticalOrder != .min && $0.verticalOrder != .max } } - + /// Updates `selectedFacility` if a good selection exists. private func updateSelectedFacility() { // Only select a facility if it is within minimum scale. Default at 1500. @@ -188,30 +189,27 @@ final class FloorFilterViewModel: ObservableObject { } else { facilityMinScale = 1500 } - + if viewpoint.wrappedValue?.targetScale ?? .zero > facilityMinScale { return } - + // If the centerpoint is within a facilities' geometry, select that site. let facilityResult = floorManager.facilities.first { facility in guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, let extent2 = facility.geometry?.extent else { return false } - return GeometryEngine.intersects( - geometry1: extent1, - geometry2: extent2 - ) + return GeometryEngine.isGeometry(extent1, intersecting: extent2) } - + if let facilityResult = facilityResult { setFacility(facilityResult) } else if automaticSelectionMode == .always { setFacility(nil) } } - + /// Updates `selectedSite` if a good selection exists. private func updateSelectedSite() { // Only select a facility if it is within minimum scale. Default at 4300. @@ -222,7 +220,7 @@ final class FloorFilterViewModel: ObservableObject { } else { siteMinScale = 4300 } - + // If viewpoint is out of range, reset selection and return early. if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { if automaticSelectionMode == .always { @@ -232,26 +230,23 @@ final class FloorFilterViewModel: ObservableObject { } return } - + // If the centerpoint is within a site's geometry, select that site. let siteResult = floorManager.sites.first { site in guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, let extent2 = site.geometry?.extent else { return false } - return GeometryEngine.intersects( - geometry1: extent1, - geometry2: extent2 - ) + return GeometryEngine.isGeometry(extent1, intersecting: extent2) } - + if let siteResult = siteResult { setSite(siteResult) } else if automaticSelectionMode == .always { setSite(nil) } } - + /// Updates the viewpoint to display a given extent. /// - Parameter extent: The new extent to be shown. private func zoomToExtent(extent: Envelope?) { @@ -259,7 +254,6 @@ final class FloorFilterViewModel: ObservableObject { guard let extent = extent else { return } - let builder = EnvelopeBuilder(envelope: extent) builder.expand(factor: 1.5) let targetExtent = builder.toGeometry() @@ -273,7 +267,7 @@ final class FloorFilterViewModel: ObservableObject { extension FloorSite: Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(self.siteId) + hasher.combine(self.id) hasher.combine(self.name) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 97cb748e5..4d8fed46e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -21,13 +21,13 @@ struct SiteAndFacilitySelector: View { init(isHidden: Binding) { self.isHidden = isHidden } - + /// The view model used by the `SiteAndFacilitySelector`. @EnvironmentObject var viewModel: FloorFilterViewModel - + /// Allows the user to toggle the visibility of the site and facility selector. private var isHidden: Binding - + var body: some View { if viewModel.sites.count == 1 { FacilitiesList( @@ -42,26 +42,26 @@ struct SiteAndFacilitySelector: View { ) } } - + /// A view displaying the sites contained in a `FloorManager`. struct SitesList: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel - + /// Indicates that the keyboard is animating and some views may require reload. @State private var keyboardAnimating = false - + /// A site name filter phrase entered by the user. @State private var searchPhrase: String = "" - + /// Sites contained in a `FloorManager`. let sites: [FloorSite] - + /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding - + /// A subset of `sites` with names containing `searchPhrase` or all `sites` if /// `searchPhrase` is empty. var matchingSites: [FloorSite] { @@ -72,7 +72,7 @@ struct SiteAndFacilitySelector: View { $0.name.lowercased().contains(searchPhrase.lowercased()) } } - + var body: some View { siteListAndFilterView // Trigger a reload on keyboard frame changes for proper layout @@ -98,7 +98,7 @@ struct SiteAndFacilitySelector: View { } } } - + /// A view containing a filter-via-name field, a list of the site names and an "All sites" button. var siteListAndFilterView: some View { NavigationView { @@ -129,7 +129,7 @@ struct SiteAndFacilitySelector: View { ) } } - + /// A view containing a list of the site names. /// /// If `AutomaticSelectionMode` mode is in use, items will automatically be @@ -157,12 +157,12 @@ struct SiteAndFacilitySelector: View { .listStyle(.plain) } } - + /// A view displaying the facilities contained in a `FloorManager`. struct FacilitiesList: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel - + /// Presentation styles for the facility list. enum PresentationStyle { /// A specific site was selected and the body is presented within a navigation view. @@ -172,20 +172,20 @@ struct SiteAndFacilitySelector: View { /// Only one site exists and the body is not presented within a navigation view. case singleSite } - + /// A facility name filter phrase entered by the user. @State var searchPhrase: String = "" - + /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] - + /// The selected presentation style for the facility list. let presentationStyle: PresentationStyle - + /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding - + /// A subset of `facilities` with names containing `searchPhrase` or all /// `facilities` if `searchPhrase` is empty. var matchingFacilities: [FloorFacility] { @@ -196,19 +196,19 @@ struct SiteAndFacilitySelector: View { $0.name.lowercased().contains(searchPhrase.lowercased()) } } - + /// Determines the SF Symbols image name to represent selection/non-selection of a facility. /// - Parameter facility: The facility of interest /// - Returns: "circle.fill" if the facility is marked selected or "cirlce" if the facility is not selected /// in the view model. func imageFor(_ facility: FloorFacility) -> String { - if facility.facilityId == viewModel.selectedFacility?.facilityId { + if facility.id == viewModel.selectedFacility?.id { return "circle.fill" } else { return "circle" } } - + var body: some View { if presentationStyle == .singleSite { facilityListAndFilterView @@ -222,7 +222,7 @@ struct SiteAndFacilitySelector: View { ) } } - + /// A view containing a label for the site name, a filter-via-name bar and a list of the facility names. var facilityListAndFilterView: some View { VStack { @@ -247,7 +247,7 @@ struct SiteAndFacilitySelector: View { } } } - + /// Displays a list of facilities matching the filter criteria as determined by /// `matchingFacilities`. /// @@ -258,7 +258,7 @@ struct SiteAndFacilitySelector: View { /// selected item. var facilityListView: some View { ScrollViewReader { proxy in - List(matchingFacilities, id: \.facilityId) { facility in + List(matchingFacilities, id: \.id) { facility in Button { viewModel.setFacility( facility, @@ -288,18 +288,18 @@ struct SiteAndFacilitySelector: View { } } } - .listStyle(.plain) - .onChange(of: viewModel.selectedFacility) { - guard let facility = $0 else { - return - } - withAnimation { - proxy.scrollTo( - facility.facilityId, - anchor: .center - ) - } + .listStyle(.plain) + .onChange(of: viewModel.selectedFacility) { + guard let facility = $0 else { + return } + withAnimation { + proxy.scrollTo( + facility.id, + anchor: .center + ) + } + } } } } @@ -320,7 +320,7 @@ struct NoMatchesView: View { struct CloseButton: View { /// The button's action to be performed when tapped. var action: (() -> Void) - + var body: some View { Button { action() diff --git a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift index b7efe890e..4924a27b5 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift @@ -16,10 +16,6 @@ import ArcGIS extension FloorFacility: Equatable { public static func == (lhs: FloorFacility, rhs: FloorFacility) -> Bool { - lhs.facilityId == rhs.facilityId + lhs.id == rhs.id } } - -extension FloorFacility: Identifiable { - public var id: String { facilityId } -} diff --git a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift index c98d5e85b..97e7d320d 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift @@ -16,8 +16,6 @@ import ArcGIS extension FloorLevel: Equatable { public static func == (lhs: FloorLevel, rhs: FloorLevel) -> Bool { - lhs.levelId == rhs.levelId + lhs.id == rhs.id } } - -extension FloorLevel: Identifiable { } diff --git a/Sources/ArcGISToolkit/Extensions/FloorSite.swift b/Sources/ArcGISToolkit/Extensions/FloorSite.swift index 2cf858aad..ff8b5faa2 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorSite.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorSite.swift @@ -16,10 +16,6 @@ import ArcGIS extension FloorSite: Equatable { public static func == (lhs: FloorSite, rhs: FloorSite) -> Bool { - lhs.siteId == rhs.siteId + lhs.id == rhs.id } } - -extension FloorSite: Identifiable { - public var id: String { siteId } -} diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index d015eb12c..887883528 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -203,14 +203,14 @@ class SearchViewModelTests: XCTestCase { let resultGeometryUnion: Geometry = try XCTUnwrap( GeometryEngine.union( - geometries: result.compactMap { $0.geoElement?.geometry } + of: result.compactMap { $0.geoElement?.geometry } ) ) XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.queryArea!, - geometry2: resultGeometryUnion + GeometryEngine.doesGeometry( + model.queryArea!, + contain: resultGeometryUnion ) ) @@ -256,9 +256,9 @@ class SearchViewModelTests: XCTestCase { ) var geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .portland, - point2: resultPoint, + GeometryEngine.geodeticDistance( + from: .portland, + to: resultPoint, distanceUnit: .meters, azimuthUnit: nil, curveType: .geodesic @@ -266,7 +266,7 @@ class SearchViewModelTests: XCTestCase { ) // First result within 1500m of Portland. - XCTAssertLessThan(geodeticDistance.distance, 1500) + XCTAssertLessThan(geodeticDistance.distance.value, 1500) // Set queryCenter to Edinburgh model.queryCenter = .edinburgh @@ -283,9 +283,9 @@ class SearchViewModelTests: XCTestCase { // Web Mercator distance between .edinburgh and first result. geodeticDistance = try XCTUnwrap ( - GeometryEngine.distanceGeodetic( - point1: .edinburgh, - point2: resultPoint, + GeometryEngine.geodeticDistance( + from: .edinburgh, + to: resultPoint, distanceUnit: .meters, azimuthUnit: nil, curveType: .geodesic @@ -293,7 +293,7 @@ class SearchViewModelTests: XCTestCase { ) // First result within 100m of Edinburgh. - XCTAssertLessThan(geodeticDistance.distance, 100) + XCTAssertLessThan(geodeticDistance.distance.value, 100) } func testRepeatSearch() async throws { @@ -311,14 +311,14 @@ class SearchViewModelTests: XCTestCase { let resultGeometryUnion: Geometry = try XCTUnwrap( GeometryEngine.union( - geometries: result.compactMap { $0.geoElement?.geometry } + of: result.compactMap { $0.geoElement?.geometry } ) ) XCTAssertTrue( - GeometryEngine.contains( - geometry1: model.geoViewExtent!, - geometry2: resultGeometryUnion + GeometryEngine.doesGeometry( + model.geoViewExtent!, + contain: resultGeometryUnion ) ) From 1182f2ed4e66696b9f8aa1b7fbcf7b8cc9244e2e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 29 Mar 2022 11:01:32 -0700 Subject: [PATCH 0738/1722] Revert unintended change --- Examples/Examples/FloorFilterExampleView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 7e5016ad7..f52de6916 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -68,7 +68,6 @@ struct FloorFilterExampleView: View { let floorManager = map.floorManager { FloorFilter( alignment: filterAlignment, - automaticSelectionMode: .alwaysNotClearing, floorManager: floorManager, viewpoint: $viewpoint ) From a47146a675358f447a435d69e51be9f090522a47 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 29 Mar 2022 13:24:42 -0700 Subject: [PATCH 0739/1722] Update FloorLevel.swift --- Sources/ArcGISToolkit/Extensions/FloorLevel.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift index c98d5e85b..97e7d320d 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift @@ -16,8 +16,6 @@ import ArcGIS extension FloorLevel: Equatable { public static func == (lhs: FloorLevel, rhs: FloorLevel) -> Bool { - lhs.levelId == rhs.levelId + lhs.id == rhs.id } } - -extension FloorLevel: Identifiable { } From 0212e3383a518193bfef7792095e349b60ab0875 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 29 Mar 2022 13:28:57 -0700 Subject: [PATCH 0740/1722] Merge v.next into scalebar --- .../Components/Scalebar/Scalebar.swift | 28 ------------------- .../ArcGISToolkit/Extensions/FloorLevel.swift | 4 +-- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index cb6b026bf..1653a0960 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -195,31 +195,3 @@ extension Scalebar { } } } - - -// - TODO: Temporary as another PR in-flight contains this extension. -extension View { - /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. - /// - Parameter perform: The closure to be executed when the content size of the receiver - /// changes. - /// - Returns: A new `View`. - func onSizeChange(perform: @escaping (CGSize) -> Void) -> some View { - background( - GeometryReader { geometry in - Color.clear - .preference( - key: SizePreferenceKey.self, value: geometry.size - ) - } - ) - .onPreferenceChange(SizePreferenceKey.self, perform: perform) - } - -} - -// - TODO: Temporary as another PR in-flight contains this extension. -/// A `PreferenceKey` that specifies a size. -struct SizePreferenceKey: PreferenceKey { - static let defaultValue: CGSize = .zero - static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} -} diff --git a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift index c98d5e85b..97e7d320d 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift @@ -16,8 +16,6 @@ import ArcGIS extension FloorLevel: Equatable { public static func == (lhs: FloorLevel, rhs: FloorLevel) -> Bool { - lhs.levelId == rhs.levelId + lhs.id == rhs.id } } - -extension FloorLevel: Identifiable { } From d3234db24fddf191f297ba88297d1fa8034ef699 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 29 Mar 2022 15:24:23 -0700 Subject: [PATCH 0741/1722] Update BookmarksHeader.swift --- .../Components/Bookmarks/BookmarksHeader.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift index 06d15af2f..dc55d8d9f 100644 --- a/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift +++ b/Sources/ArcGISToolkit/Components/Bookmarks/BookmarksHeader.swift @@ -47,11 +47,11 @@ struct BookmarksHeader: View { if isCompact { Spacer() Button { - isPresented = false + isPresented.toggle() } label: { - Text("Cancel") + Text("Done") + .fontWeight(.semibold) } - .foregroundColor(.red) } } } From d280b8a1fa802cbd092f828ad22af8907b336bb7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 30 Mar 2022 17:59:29 -0700 Subject: [PATCH 0742/1722] Scalebar progress --- Examples/Examples/ScalebarExampleView.swift | 2 +- .../Components/Scalebar/Scalebar.swift | 98 ++++--------------- .../Components/Scalebar/ScalebarLine.swift | 68 +++++++++++++ .../Scalebar/ScalebarStyleRender.swift | 75 ++++++++++++++ .../Scalebar/ScalebarViewModel.swift | 14 ++- 5 files changed, 170 insertions(+), 87 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index bc5560a94..58a462e11 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -45,7 +45,7 @@ struct ScalebarExampleView: View { Scalebar( alignment: .left, spatialReference, - .alternatingBar, + .line, 175, $unitsPerPoint, $viewpoint, diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 1653a0960..0f7e984ca 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -18,24 +18,22 @@ import SwiftUI public struct Scalebar: View { /// The view model used by the `Scalebar`. @StateObject - private var viewModel: ScalebarViewModel + internal var viewModel: ScalebarViewModel /// The vertical amount of space used by the scalebar. @State private var height: Double = 50 - @State private var finalLengthWidth: Double = .zero + @State internal var finalLengthWidth: Double = .zero private var alignment: ScalebarAlignment private var alternateFillColor = Color.black - private var fillColor = Color(uiColor: .lightGray).opacity(0.5) + internal var fillColor = Color(uiColor: .lightGray).opacity(0.5) - private var font: Font + internal var shadowColor = Color(uiColor: .black).opacity(0.65) - private var lineColor = Color.white - - private var shadowColor = Color(uiColor: .black).opacity(0.65) + internal var font: Font private var style: ScalebarStyle @@ -85,14 +83,24 @@ public struct Scalebar: View { public var body: some View { Group { - if style == .alternatingBar { - alternatingBarView - } else { - barView + switch style { + case .line: + lineStyleRender + case .bar: + barStyleRender + case .graduatedLine: + #warning(".graduatedLine not yet implemented") + EmptyView() + case .alternatingBar: + #warning(".alternatingBar not yet implemented") + EmptyView() + case .dualUnitLine: + #warning(".dualUnitLine not yet implemented") + EmptyView() } } .onChange(of: visibleArea.wrappedValue) { - viewModel.subject.send($0) + viewModel.visibleAreaSubject.send($0) } .onSizeChange { height = $0.height @@ -101,7 +109,6 @@ public struct Scalebar: View { width: $viewModel.displayLength.wrappedValue, height: height ) -// .border(.red) } internal static let labelYPad: CGFloat = 2.0 @@ -130,68 +137,3 @@ public struct Scalebar: View { /// always be visible private let minScale: Double = 0 } - -extension Scalebar { - var alternatingBarView: some View { - VStack(spacing: 2) { - Rectangle() - .fill(.gray) - .border( - .white, - width: 1.5 - ) - .frame( - width: $viewModel.displayLength.wrappedValue, - height: 7, - alignment: .leading - ) - .shadow( - color: Color(uiColor: .lightGray), - radius: 1 - ) - HStack { - Text("0") - .font(font) - .fontWeight(.semibold) - .frame(maxWidth: .infinity, alignment: .leading) - Text("\($viewModel.mapLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") - .font(font) - .fontWeight(.semibold) - .onSizeChange { - finalLengthWidth = $0.width - } - .frame(maxWidth: .infinity, alignment: .trailing) - .offset(x: finalLengthWidth / 2) - } - } - } - - var barView: some View { - VStack(spacing: 2) { - Rectangle() - .fill(.gray) - .border( - .white, - width: 1.5 - ) - .frame( - width: $viewModel.displayLength.wrappedValue, - height: 7, - alignment: .leading - ) - .shadow( - color: Color(uiColor: .lightGray), - radius: 1 - ) - HStack { - Text("\($viewModel.mapLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") - .font(font) - .fontWeight(.semibold) - .onSizeChange { - finalLengthWidth = $0.width - } - .frame(maxWidth: .infinity, alignment: .center) - } - } - } -} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift new file mode 100644 index 000000000..21576a7dd --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift @@ -0,0 +1,68 @@ +// 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 + +extension Scalebar { + /// Provides simple and predictable line creation. + struct Line { + /// The stroke width used to produce a line. + private static let strokeWidth = 4.0 + + /// A basic line. + private struct basic: View { + /// The width of the line. + var width: Double + + /// The color used to fill the line. + let color: Color = .white + + var body: some View { + Path { path in + path.move(to: CGPoint(x: Double.zero, y: .zero)) + path.addLine(to: CGPoint(x: width, y: .zero)) + path.addLine(to: CGPoint(x: width, y: strokeWidth)) + path.addLine(to: CGPoint(x: .zero, y: strokeWidth)) + path.addLine(to: CGPoint(x: Double.zero, y: .zero)) + } + .fill(color) + } + } + + /// A horizontally oriented line + struct basicHorizontal: View { + /// The width of the line. + var width: CGFloat + + var body: some View { + basic(width: width) + .frame(width: width, height: strokeWidth) + .cornerRadius(strokeWidth/2) + } + } + + /// A vertically oriented line + struct basicVertical: View { + /// The height of the line. + var height: CGFloat + + var body: some View { + basic(width: height) + .rotationEffect(Angle(degrees: 90), anchor: .topLeading) + .offset(x: strokeWidth) + .frame(width: strokeWidth, height: height) + .cornerRadius(strokeWidth/2) + } + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift new file mode 100644 index 000000000..c23ece5a3 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -0,0 +1,75 @@ +// 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 + +extension Scalebar { + var barStyleRender: some View { + VStack(spacing: 2) { + Rectangle() + .fill(fillColor) + .border( + .white, + width: 1.5 + ) + .frame( + height: 7, + alignment: .leading + ) + .shadow( + color: shadowColor, + radius: 1 + ) + HStack { + Text("\($viewModel.displayLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") + .font(font) + .fontWeight(.semibold) + .onSizeChange { + finalLengthWidth = $0.width + } + .frame(maxWidth: .infinity, alignment: .center) + } + } + } + + var lineStyleRender: some View { + VStack(spacing: 2) { + GeometryReader { geoProxy in + ZStack(alignment: .bottom) { + Line.basicHorizontal(width: geoProxy.size.width) + HStack { + Line.basicVertical(height: geoProxy.size.height) + Spacer() + Line.basicVertical(height: geoProxy.size.height) + } + } + .compositingGroup() + .shadow( + color: shadowColor, + radius: 1 + ) + } + .frame(height: 10) + HStack { + Text("\($viewModel.displayLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") + .font(font) + .fontWeight(.semibold) + .onSizeChange { + finalLengthWidth = $0.width + } + .frame(maxWidth: .infinity, alignment: .center) + } + } + } + +} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 9e9f3c1fe..f97471364 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -20,15 +20,13 @@ import SwiftUI final class ScalebarViewModel: ObservableObject { @Published var displayLength: CGFloat = .zero - @Published var displayUnit: LinearUnit? = nil - - @Published var mapLengthString = "" + @Published var displayLengthString = "" - var subject = PassthroughSubject() + @Published var displayUnit: LinearUnit? = nil - private var cancellable: AnyCancellable? + var visibleAreaSubject = PassthroughSubject() - private var cancellable2: AnyCancellable? + private var visibleAreaCancellable: AnyCancellable? /// The amount of time to wait between value calculations. private var delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.05) @@ -77,7 +75,7 @@ final class ScalebarViewModel: ObservableObject { self.viewpoint = viewpoint self.visibleArea = visibleArea - cancellable = subject + visibleAreaCancellable = visibleAreaSubject .debounce(for: delay, scheduler: DispatchQueue.main) .sink(receiveValue: { [weak self] _ in self?.updateScaleDisplay() @@ -172,6 +170,6 @@ final class ScalebarViewModel: ObservableObject { self.displayUnit = displayUnit - mapLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" + displayLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" } } From 6a13e251204aedd000e9f0fe2db66349eec22573 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 1 Apr 2022 15:42:15 -0500 Subject: [PATCH 0743/1722] Refactor model and some UI improvements. --- .../Examples/BasemapGalleryExampleView.swift | 16 ++---- .../BasemapGallery/BasemapGallery.swift | 41 +++++++++----- .../BasemapGallery/BasemapGalleryCell.swift | 51 ++++++++++++------ .../BasemapGallery/BasemapGalleryItem.swift | 27 ++++------ .../BasemapGalleryViewModel.swift | 32 +++++------ .../ArcGISToolkit/Extensions/FloorLevel.swift | 4 +- .../defaultthumbnail.imageset/basemap@1x.png | Bin 3072 -> 1954 bytes .../defaultthumbnail.imageset/basemap@2x.png | Bin 5488 -> 3375 bytes .../defaultthumbnail.imageset/basemap@3x.png | Bin 7247 -> 5490 bytes 9 files changed, 91 insertions(+), 80 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index cb88b6740..0f71b175a 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -19,10 +19,6 @@ struct BasemapGalleryExampleView: View { /// The map displayed in the map view. let map: Map - /// The view model for the basemap gallery. - @ObservedObject - var viewModel: BasemapGalleryViewModel - /// A Boolean value indicating whether to show the basemap gallery. @State private var showBasemapGallery: Bool = false @@ -33,20 +29,18 @@ struct BasemapGalleryExampleView: View { scale: 1_000_000 ) + /// The initial list of basemaps. + private let basemaps = Self.initialBasemaps() + init() { self.map = Map(basemapStyle: .arcGISImagery) - self.viewModel = BasemapGalleryViewModel( - geoModel: self.map, - // You can add your own basemaps by passing them in here: - items: Self.initialBasemaps() - ) } var body: some View { MapView(map: map, viewpoint: initialViewpoint) .overlay(alignment: .topTrailing) { if showBasemapGallery { - BasemapGallery(viewModel: viewModel) + BasemapGallery(geoModel: self.map, items: basemaps) .style(.automatic()) .esriBorder() .padding() @@ -67,7 +61,7 @@ struct BasemapGalleryExampleView: View { ] return identifiers.map { identifier in - let url = URL(string: "https://runtime.maps.arcgis.com/home/item.html?id=\(identifier)")! + let url = URL(string: "https://maps.arcgis.com/home/item.html?id=\(identifier)")! return BasemapGalleryItem(basemap: Basemap(item: PortalItem(url: url)!)) } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index 7398e763e..c1866fda5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -32,18 +32,35 @@ public struct BasemapGallery: View { case list(width: CGFloat = 125) } - /// Creates a `BasemapGallery`. - /// - Parameter viewModel: The view model used by the `BasemapGallery`. - public init(viewModel: BasemapGalleryViewModel? = nil) { - self.viewModel = viewModel ?? BasemapGalleryViewModel() + /// Creates a `BasemapGallery`. Uses the given array of basemap gallery items. + /// - Remark: If `items` is empty, ArcGIS Online's developer basemaps will + /// be loaded and added to `items`. + /// - Parameters: + /// - geoModel: A geo model. + /// - items: A list of pre-defined base maps to display. + public init( + geoModel: GeoModel? = nil, + items: [BasemapGalleryItem] = [] + ) { + viewModel = BasemapGalleryViewModel(geoModel: geoModel, items: items) } + /// Creates a `BasemapGallery`. The portal will be used to to retrieve basemaps. + /// - Parameters: + /// - geoModel: A geo model. + /// - portal: The portal to use to load basemaps. + init( + _ geoModel: GeoModel? = nil, + portal: Portal + ) { + viewModel = 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 - public var viewModel: BasemapGalleryViewModel + @ObservedObject 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``. @@ -72,12 +89,10 @@ public struct BasemapGallery: View { } /// A Boolean value indicating whether to show an error alert. - @State - private var showErrorAlert = false + @State private var showErrorAlert = false /// The current alert item to display. - @State - private var alertItem: AlertItem? + @State private var alertItem: AlertItem? public var body: some View { makeGalleryView() @@ -209,10 +224,10 @@ extension AlertItem { let message: String switch (spatialReferenceMismatchError.basemapSpatialReference, spatialReferenceMismatchError.geoModelSpatialReference) { - case (.some(let basemapSpatialReference), .some(let geoModelSpatialReference)): - message = "The spatial reference of the basemap: \(basemapSpatialReference.description) does not match that of the geomodel: \(geoModelSpatialReference.description)." + case (.some(_), .some(_)): + message = "The basemap has a spatial reference that is incompatible with the map." case (_, .none): - message = "The geo model does not have a spatial reference." + message = "The map does not have a spatial reference." case (.none, _): message = "The basemap does not have a spatial reference." } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index d8a19ce80..3ef19fddc 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -41,35 +41,54 @@ struct BasemapGalleryCell: View { ) } - // Display an image representing either a load basemap error - // or a spatial reference mismatch error. - if item.loadBasemapError != nil { - makeErrorImage(systemName: "minus") - } else if item.spatialReferenceStatus == .noMatch { - makeErrorImage(systemName: "x") + // Display an overlay if the item has an error. + if item.hasError { + makeErrorOverlay() } // Display a progress view if the item is loading. if item.isBasemapLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .esriBorder() + makeProgressView() } } // Display the name of the item. Text(item.name ?? "") - .font(.footnote) + .font(Font.custom("AvenirNext-Regular", fixedSize: 12)) .multilineTextAlignment(.center) - .foregroundColor(.primary) + .foregroundColor(item.hasError ? .secondary : .primary) } }).disabled(item.isBasemapLoading) } + + /// Creates a partially transparent rectangle, used to denote a basemap with an error. + /// - Returns: A new transparent rectagle view. + private func makeErrorOverlay() -> some View { + Rectangle() + .foregroundColor(.secondary) + .opacity(0.75) + } + + /// Creates a circular progress view with a rounded rectangle background. + /// - Returns: A new progress view. + private func makeProgressView() -> some View { + ProgressView() + .progressViewStyle(.circular) + .padding(EdgeInsets( + top: 8, + leading: 12, + bottom: 8, + trailing: 12) + ) + .background(Color(uiColor: .systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} - private func makeErrorImage(systemName: String) -> some View { - Image(systemName: systemName) - .symbolVariant(.circle.fill) - .font(.title) - .foregroundColor(.red) +extension BasemapGalleryItem { + /// A Boolean denoting if the item has an error or not. + var hasError: Bool { + loadBasemapError != nil || + spatialReferenceStatus == .noMatch } } diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index 2caf5be92..c61f01ae8 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -57,32 +57,26 @@ public class BasemapGalleryItem: ObservableObject { } /// The error generated loading the basemap, if any. - @Published - private(set) var loadBasemapError: Error? = nil + @Published private(set) var loadBasemapError: Error? = nil /// The basemap represented by `BasemapGalleryItem`. public let basemap: Basemap /// The name of the `basemap`. - @Published - public private(set) var name: String? + @Published public private(set) var name: String? /// The description of the `basemap`. - @Published - public private(set) var description: String? + @Published public private(set) var description: String? /// The thumbnail used to represent the `basemap`. - @Published - public private(set) var thumbnail: UIImage? + @Published public private(set) var thumbnail: UIImage? /// A Boolean value indicating whether the `basemap` or it's base layers are being loaded. - @Published - private(set) var isBasemapLoading = true + @Published private(set) var isBasemapLoading = true /// The spatial reference status of the item. This is set via a call to /// ``updateSpatialReferenceStatus(_:)``. - @Published - public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown + @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown /// The spatial reference of ``basemap``. This will be `nil` until the /// basemap's base layers have been loaded by @@ -107,8 +101,7 @@ private extension BasemapGalleryItem { /// Updates the item in response to basemap loading completion. /// - Parameter error: The basemap load error, if any. - @MainActor - func finalizeLoading(error: Error? = nil) { + @MainActor func finalizeLoading(error: Error? = nil) { if name == nil { name = basemap.name } @@ -139,8 +132,7 @@ public extension BasemapGalleryItem { /// Updates the ``spatialReferenceStatus-swift.property`` by loading the first base layer of /// ``basemap`` and determining if it matches with the given spatial reference. /// - Parameter referenceSpatialReference: The spatial reference to match to. - @MainActor - func updateSpatialReferenceStatus( + @MainActor func updateSpatialReferenceStatus( _ referenceSpatialReference: SpatialReference? ) async throws { guard basemap.loadStatus == .loaded else { return } @@ -159,8 +151,7 @@ public extension BasemapGalleryItem { /// - Parameter referenceSpatialReference: The spatial reference used to /// compare to `basemap`'s spatial reference, represented by the first base layer's /// spatial reference. - @MainActor - private func finalizeUpdateSpatialReferenceStatus( + @MainActor private func finalizeUpdateSpatialReferenceStatus( with referenceSpatialReference: SpatialReference? ) { spatialReference = basemap.baseLayers.first?.spatialReference diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index 004622de8..edf0e9cb8 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -15,15 +15,14 @@ import SwiftUI import ArcGIS /// Manages the state for a `BasemapGallery`. -@MainActor -public class BasemapGalleryViewModel: ObservableObject { +@MainActor class BasemapGalleryViewModel: ObservableObject { /// Creates a `BasemapGalleryViewModel`. Uses the given array of basemap gallery items. /// - Remark: If `items` is empty, ArcGIS Online's developer basemaps will /// be loaded and added to `items`. /// - Parameters: /// - geoModel: A geo model. /// - items: A list of pre-defined base maps to display. - public init( + init( geoModel: GeoModel? = nil, items: [BasemapGalleryItem] = [] ) { @@ -45,7 +44,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// - Parameters: /// - geoModel: A geo model. /// - portal: The portal to use to load basemaps. - public init( + init( _ geoModel: GeoModel? = nil, portal: Portal ) { @@ -58,19 +57,17 @@ public class BasemapGalleryViewModel: ObservableObject { } /// The error generated by fetching the `Basemaps` from the `Portal`. - @Published - public var fetchBasemapsError: Error? = nil + @Published var fetchBasemapsError: Error? = nil /// The error signifying the spatial reference of ``geoModel`` and the spatial reference of /// ``currentItem`` do not match. - @Published - public private(set) var spatialReferenceMismatchError: SpatialReferenceMismatchError? = nil + @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, /// the geoModel will have its basemap replaced with the selected basemap. - public var geoModel: GeoModel? { + var geoModel: GeoModel? { didSet { geoModelDidChange(oldValue) } @@ -78,20 +75,18 @@ public class BasemapGalleryViewModel: ObservableObject { /// The `Portal` object, if any. Setting the portal will automatically fetch it's basemaps /// and replace the ``items`` array with the fetched basemaps. - public var portal: Portal? { + var portal: Portal? { didSet { portalDidChange(oldValue) } } /// The list of basemaps shown in the gallery. - @Published - public var items: [BasemapGalleryItem] + @Published var items: [BasemapGalleryItem] /// The `BasemapGalleryItem` representing the `GeoModel`'s current basemap. This may be a /// basemap which does not exist in the gallery. - @Published - public private(set) var currentItem: BasemapGalleryItem? = nil { + @Published private(set) var currentItem: BasemapGalleryItem? = nil { didSet { guard let item = currentItem else { return } geoModel?.basemap = item.basemap @@ -123,8 +118,7 @@ public class BasemapGalleryViewModel: ObservableObject { /// matches with ``geoModel``'s spatial reference. Otherwise ``currentItem`` /// will be unchanged. /// - Parameter basemapGalleryItem: The new, potential, basemap gallery item. - @MainActor - func setCurrentItem( + @MainActor func setCurrentItem( _ basemapGalleryItem: BasemapGalleryItem ) { // Reset the mismatch error. @@ -204,12 +198,12 @@ private extension BasemapGalleryViewModel { } /// An error describing a spatial reference mismatch between a geo model and a basemap. -public struct SpatialReferenceMismatchError: Error { +struct SpatialReferenceMismatchError: Error { /// The basemap's spatial reference. - public let basemapSpatialReference: SpatialReference? + let basemapSpatialReference: SpatialReference? /// The geo model's spatial reference. - public let geoModelSpatialReference: SpatialReference? + let geoModelSpatialReference: SpatialReference? } extension SpatialReferenceMismatchError: Equatable {} diff --git a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift index c98d5e85b..97e7d320d 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift @@ -16,8 +16,6 @@ import ArcGIS extension FloorLevel: Equatable { public static func == (lhs: FloorLevel, rhs: FloorLevel) -> Bool { - lhs.levelId == rhs.levelId + lhs.id == rhs.id } } - -extension FloorLevel: Identifiable { } diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@1x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@1x.png index 95bf6fe6618bceb99241ae79bf317c92c58f38bb..5129aa56efb66fc7083efc9e362b319dd425d3d3 100644 GIT binary patch delta 1121 zcmV-n1fKhV7@`l57YZZ@1^@s6!duBrks&_|#=y)V&%nzd&Y+!6%a8nI6N<9cxh%jP-%E&Y*~{S1Rs0}tFX@>9gS5-YkdcEFC_Hab^kV%n3u>Z~3(6P|5 ze`E#rHWlO63ml0=Hr{fz`rIzJUFS<~&c++;7&HZ2E|-7u@$n&u=isv&@pxQgp66*i z7K zkuU)>Z61H55)R-D;`{x+y0Th*req?Xke8Pi84L!JPN${aZp*{NgU)49EQcu=Da48c zcY1m%0T3{x8Ez5>pj0Y-fdYbC)Mc?))P};s1RbVe2&cx_ zY+qkrRVWRDIJE@;*8l~YL5#t|un5jY4F-5I>xyV_!n@tB3cR|ylDoS*-OJoFq)Str z1B6W+7%L#R7G{ni5;xehNXJU0qV1n%!8L#92Ijz$%1dmOmCaK4h1p~)803qgYHafxR#SF}?HZ`^Q^VejZU@F{gz|7Gn|8@7|SrTxX z1|8GYoDr9pfl*)VUkG*W9FeZ>NI{wiHN{@%Y7J0oa-D~NeJ?1W*#?aeS4W#fam`r` zf1_~Uac4It=-JFMgy4c7z%R}Zvnjsqx_|C2?@inNHmF?$SQbZKvHRc>i?lM@4Nlj{Q&5Hc__ zJTGN%#8-~$JfAp|aemrHoW000M5NklLnGRj|FHRVS>gsB#`uX#x zeEs@WQdOz{yy0hm`p7vym(59gd%IM@oJyr6l}w!;F~^5TXmN>5uxv;slW~_Tzb_+9 zfZFEb11T`4wxqZ)T>clKRaI5;<;xdYUS1ZA^9|)ao6VLla&IWg=kwCi(jr}5U2=4E zB=6q6)85TGjzVtZSSXjvY0o;w{P*78Ub%SjqK<_*SHTy5Wh$^Jdk5q9_xI)a_*lBT zyM=v8E`R&>P20VD_Y?${q1c?k{QCO3^z`)TJnw+4si{#Tk%6;V)7jan>kbYMWNB$h zkjeO;v$Lu<43K)m{uJv#0hkR&=<)uoTenmZQEX~zlFiLcjf!~`F#yAO9CCPgSdv^@ zTPrti+)(d-6HVT~fB&wakxP_zc6Q|R=g)HU=1m0#a3u#aD#Zpy$`LgpM!Rs~f?U3Q zSr!%+1 zbUH16ym_PVZ2Xd!O;ezUhl23rNx$wT_S*aR?`78QC)OfQ&m))aaR8Z`*ue0zpIRs- zJ@4GPBlQgpGBGishG7)s^O{l{XYSZVhI40RWJHTngOhPPeOp>vTXpYlY;2S&ZkQ}z zu06+pPDRoPqXB~=fLY()-!E@m)J8`~<>AAJ($Uf3j)?+5Q)oSFsDAr<`;y6KM#$1H%x~hibL5vLbEmZSEv0q$1!*ACy0R`c(H10P#9=tYf`T z_4W12_vzE8lHu@B2_HOopeb!m8y_D(Gqrtx**r%EW(z6di;Ig|)u@7Oc=nlrqo&qE`BF7h@0JwVfsyu)G zTmij&`BD#Uj4%VZ#Zb`zLvi3>HsHQ3q%&t2jfke43_$Ws^!?_qi~1ZdV$N~wEkO2v z_4VoX==JN@^5Vq{H402z8PP0(=W8JnJ-js*%mk%6HA zMQnDw3@{9|!<9&ySIr>i_xAQQnr1YApGg=>Dq1v?sib~gU{DzqBS*yIQe{(>^>&m&x-y#7+7>08vymIAA zxdiuS0RJ{o9PlH|4W7uxA>&4R zh9lbA+O&e660s4$0;U)zJh6dkpaR7}qBD*WBEkM&9;V6oB0R zhlhtX9jz({^#$J}1H%AvoNpS8%nv&ac(_+{J6Adf(H5Trt{US zS7my9p0MZ0z)S$(NGTeGa#DqValjY=p8auK?>oZhw|ROSH(q)h^Z8DDjt$HgMwMf~ zaDnC4K(kaaqkMjQrhLN}ta4_&MT(v=<2y~J$+rI7z=+1$+M4b;_EJ8db03;%NxQ?= z&NRm1^NC2$%*^Qit7NEU1ZixT2nyIDq~xMFxf;6a_vYImgW7zR_kZ>Bl%A z{rO79GEYi#Goh*ilp7AR__pKUc_=b46HX)mW!EADWAlC=28wZiYshMDZq@=sgvkcd z-R8q&_(EOCcVHzQ~mT~4G##s}W;sJAM!Jhf} z@uQ~YFJ*-fCruv*$av;|%){q^Xn;J1qm4G2(i+^`Kih<|W k;V@X*CtPX%U;ZETU!I!4F%gy#0ssI207*qoM6N<$g5pr=X#fBK diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@2x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@2x.png index 89e04b686eada81472e07630f3aa722048930f8d..080af232a56fdad7dabf561c9a6bcc0b41e7bd01 100644 GIT binary patch delta 2553 zcma)8XHb*d7X4BX2vP*1gc1nSBvM7`p@ynJxNwoGB279d5=97A5{MWG9f518f<(Fk zQoK?E1W<}d6-&4TDT06nk&AckyqP!ie!R11&szKJbd=w-h76WcmxWEX6 zkAs7Z;&Co7uUs51y^=tdqNHCk!(wUb@rGu%5%B-1`fm~21=r$cqg6|pPSnV1JX`HM z+27)#=^ngSzI!%DK1f0qmewmR_SV*udn1jy}51GaqC+Kr7fp>e17H_ z?!n8ZrY6fbB#fhot*+Nun{!sy)~Uj&SPp|6{um{S_0;BO%2UF=M^sl?`sBof?B3p< zm9+ovk8cf3CX+!Dl5Z5J`HjuZ)kF^u4Ou;C%oI&3E_J1angmn_*bnD8>H~8#W7W3$ zww;7l^m$nJ$o2-!*Avl z+>DBXch}Xa72b8wjw^N3+B9;%@8T&i@%nY8;?18wTA0Jbhaz>*si~=PPr?d2d;9t# zFtdTpc~&~%8KKn*47KCDpOSjxhg}mO38PblsS`n~LG`Wmib<;S_iIu*9y+qlwo&D6 z2kwYzI=GJ%=~FdB=9k>Aef@Mv``QxakmM69zDJAUUJ4ws+Dr$>XYl1)qZcxh`6UPJ zb8SOOVqz$NbTUKr$J>yam;BET88K>HA7^STwl?_gXAI`3@T5x_bq_4pxAso&AnERk zgcMZ?PvSUPm7eu*0Zf_(ejY4u`1Z0Rujqi;iri!F4rb(dU$lCNnq*ka_pS90$V!qo z=$ZLdnI|x1ad1j_>yr1}RraWCq4{A`uGoiT6}E#fOUxsJ zwsuxs9eGi@tn!YcA|x-H`{en8I)RRGR!XZbaS>(cF3?lg_~DdZq_#Ooyp1PvZ5Brf z3yTe;zbL}bB2r>4;yDz!;>HsM zhQ)oxn_5z0^zzX(fd7RZ$OznhQIwvHPb*w4E6XrToQdlf9Rw=$>4Bc%o`Y7oKbuCD zomIfTrm3@?4Gkv!bM*ao=HQLJNB04b-El}}dP^DkN2nnF zx9G`4e`#X8lFaml&KP~>rQL`P9psq{1qJyZ1=9k&Ig7OE=TFM%O3qZ*&J$y7ThO$x zq}YUA&rx|}f{Da-CRU<3sYvezEjSfp=e6O0)ATyAdt_;An#Wg-V?Dy)zR^4y3&;pN0xm$wpE}x?ME+B&pbrbex z8bDzptlynZRoxKqDV>@c&>rJ!xH&WN62|v)!_uLPpu&q}^GjmvE0FHmA@%ToSiLP23{F!AlC(UWKlu|Nmt#OHY&N0>V(EV!Ho0PNC>}+uDtd7& zk-f&jdR0C;I@h^I1gnli)qIEV;hI#FOYi8w!r^X26NEsE#KMPTn;gyxN=q)OI^s{P zX%++R1-dkV+$5-YNm5Fl(^GrVa(XnLY;Jvh5fMo~*nmKchl2?};39FAbZ%b)qu6GYq| zu*8f$85d2|ZAAvx{WYab0>eAfEjrcrkJ!gD^{_xZqV8ckUY-NjWF|o~?Vtt0K3_87 zIwL1vk=WH!(h?=fu@p!Kt1b|(whKY3e&x+FakIDeQiWB_eRJd-tzoHEYj^o{+dGGx0E5>%GUF!)uq=;oH?SrFW=_u9M06 zwMwR|fJXJB3vzp=u@_e(L#y3u-eoI5n8W*UxbX8g20hDt9fSdup|9&>wgu^NORT>a29HPl?<6k-ZpjZEj T9A;|#GEbIR8&kUR#rVGgtU84G delta 4646 zcmY*d2Qb`S_ujRFh!7?0>Lg^5C@X|0yNMd2ixw?fwAGglSwB)pkStb|=s}3pqDPP3 zdw8|gR-y;dW%<0{eDk0A-#d5CGiUC-bLKhcJTvzeb8d-7bI@zh(SSgp->kzxi~#ad zcT)?0H-9I87k^5OTM88ejBpVw25=7gfpIipINw<9$nK1GNd!z<^2!1PB&`mLEaQHesCH;(t#vb3@FQ@}6t1nKCzi%#o4$mOge z-N;St8O4bLZEXCs+2Nz|rC=={PU$>$l`eDIutZRU`mTe(QM2ldL{70@-Y(nCe)sjZfleX6H9Z3zNt7gW&#X|c?3JIc3 ztH*ZhU-&WKKAW3<{1q`z zscR{(8(}2Mmn{>-tyZnE^RGQTJ&S7t_grB{GgF?8GZf&@08-nLu5qgi8>6PwG{Sp4 zxMZ!6Y7@AUv)Oec{iY+v{_{|%Ah%lY&&IRUTGgA8uilp?NZ5$)YT$Wq(fml~s`JO$ z@gGFSo}OCO-Zhs$92F6x*Y9}GBc?;{tX8q0R1oYnITC38UekEb_OS*T9mQHxgD&u4 z&47zqWR3zWBe}IFzZ)BS{q|Vwbs`jII{1L;RE2<|pyQR?+NWvHKEoAUY?Q7I&7Gac zW zDDwp$J~@~Pm=%fN)_y|f(+T zQ&yj|LNb_;H2pcsVxrHRx#V)DIsOuj`XrXNihr*uhu^wmZZJi^*UGBrX9G|$2Q5rU zzedXbR0`3ujo9%lEAEHgkJIzWE^q@pM(HS!^Qt9#tgVn=PSJGXVN8RVCgY@^QD}rg zL0$)MP{GMLkF~`O$N9Ct8%~^x{$xa4K1o_JDVS^d)4$YXdi+7gI|=0TC1EPO8ingz zwyrPtZ|jw4$b`~1G=4!DeEj|NO?`cRe-sCJGFLZ6PHJMjIwN##!ek11haX6UzM!%7 zdijFEfXAE+Q1-?h)>lx+<||+#FOc=YBtXvg=rfg?le$9*E#LE!SHJjhy9O60V1}A~ zSZ7AND-XyXd%N5r%E z(){3~rG$k~iYpx`nVEat00U)jk>_#HZLmLdQmkn<32Tbht@47#_Ns{EQ)k|Ow zsCAb%SBmTO@RL_{Xsq(pyVB1-)H#PZIw@57dM=*rj_Cce)Wb>w`sk~>$x&IeDc-pZ z0r-c_{`}R=kq=EQa8}h`U%6w14!FaDnP)qL{;6AuU?J%;)FdUwEyZ#RNkuIRCG31L z$`#JNf;;(Cz9DyKD^WKe`q<-a=bvblN;=5wAfPA-WzQw=%DdLx7ZTAlwJwE8f0~Y~ zgtq1T$m1dL8TOrkG-XUA*`M%Tw8SDTtb^!}X0%g!TBwC3Tc`yjRyv{7;KV0fLW78G zZnJ$6l>9DRYU=YHhk%74)+Nh9lHltz;Sl+RKeo5@C3RDGJG5CIYT}Wn*nk~!7gfPY zxtR8`*8tW7ahg58k4y%u3A{*G3aaeY98iyH&`Qc=&I7Kou;l~i+|E?Z22Z6@4&4Jb ziN21G^IaQp!S!pL`vYc{C2>Ims)9Y!Ze06)`9Av37h3Hp3dLPPg&*a1OWsz_V_b%`{5ufsu5 zP|!?j0O{LTyCyq*kIpYt9CSD3(RoGqja|r3{Xj8l1GRRty1ey<}2` zZ$4@qy!?#q+I|=>4by6l1_wUIt0WmDJEQ0GX9Ou!lze*QrMl{iMXfByT8~W`iIry?05INi$)VF3u^qhr2tPV z<5&0;(;{8#UT-U1K2p%{daC%`vvC^$S9#Y}CHwa$ML*%tukk`?;^ati{J1Me?4lKf zd6JAt*QW5Y<2xx8nmQp7FVyC&J!Z%EG?E>K

}@A=gjF&SibJe!HEk%#+hDM4ne$ z23cp|I#}ovUJ6q{$2?GeU@{U6Y}vML=;A-z|7uKz$leCK>c(5|z%{)+BE_i3lFm|B z`p^6tQj`yEuM2!VJ#Iz${{4(5)b#bSz{2WOeHhRKk=mF0O1DSSrrnrhvPQd4WN7ny zD@j+D{uH|yw}P5UBNAzwq~nvqozmz`YFNS?W9svs02g>x0W9URNsb{+t~ZImrxZ_ zZM=4pYuXLg4I?>DXwXX>ji$m5Bt@Q^ba_Kh&q+bO3P2cWIJrdLkAjo;E#oNn9T*1xtPj7-FfYP`L8w_6#6hva)PdSnSkczX2^YB1^vamX>PgtvYWuccCo=L^6@6nqXq@ zSq`cy7%&mTCQ(f%~lG*UJrGynM^-x4b461eM@BpuIjs8bF>+Th)&> zw)LsMHBIE`KSA2f)zlY>m9kCH-TB}ii0^mA`>4hTpG-5^aPq(-5GgIDL-8lX#$zyhWAqe>I#1_S-ggliJxyhdQ1Z5!j-w#Mcg~T-B{N{pCAsBpUnUuWw^vRPx{*+WdkPEtUwd z%4Px)mo@jTC3q1Nu77-p>$s#-e=h;~&Pa*Ocx0l|dv$v&zfEAH(!d5Kw1&7cdNO_Y zZq7i;m21ZacPF@w`P!&#ltA|rOL0-DZ+ zoj&&^t)wCuvKjT2jqKOa1ZA?$VC-9Ivi%FS#wSx|2YsOtR0bBC6CDS*W`7{I{SMn| z=uaaAnpbcQNj4CR%8djY6-^nnk35{6ni3O>$Q<&F9Uhm8`%S{VTpoWEt~iK0qZ}-m zGuj;7GW5+T{ZuWg-t()|GRB?{Xk^xtr-}2u_CEdj)Ayt*Kpq(x^G$NVv1p+`a7se@ ziX^V_J?*up78wpXK!@w5vti%lXXZoo;Yw ze1)1%#ISmW%qh@^ZEp+P!dlb(=k3l+jgJ#zox6#LGb`KrvyHlkHCvZazEH)Y`}%==QWngkW4?%$k+vXYOv8>J zM3tGH=9y}T+O1%a>1*~Gps9~jbUc`Uyc($bYm*j{0lOvx&S#H0v32N{gmMUgRc7?J z2Pdl54H_P@_#M8ZpPL*9nvLcJe0FOeRD50C{$N^h+2t9CXhW|_Jf0ntZOjW_XL>%% z4Hpwkk2HtLS`(YrLLR-10c}tF4t%gl*rru{&y)PztzWLm=SXofm_4g#;amPK8m9B4 z8t;WK)w@pXOnn4jZF@b>;#kHUxvOfhsZymmSFI){;&<5Ug)t8ToY}W6Oam$ahUJ`; znE6y3+TUg0V-FzWz zF7&#q)NtT>tM721u1*NOCPN?Jf(k2t z5OV6v&qdu(NxbX_0hUy`pO03i8vn_KVopEngk~t#_F4Y^c~c|T{k&HlzyuuzEDb&11?3TgjgRx=hLAX`8zL!Ae z7soDKQru&WG^2at4t*hZAbPpm73O%Jjkf51M#%kXceGzn)GQ4fMwIrAjK2tKZY}xV z4p#@z1z<-}E|5GzuFg?07w`{>8m@I{mhR16P|1>pNCHjl+N02`(4k@vE!r3efRfkS z^>9tx+T|7=poR@cdCyYA?W)w9urI^_>$|icv(ES? z;LH57(gm`kW;=^K4c$&kFP$DfbI4mUB&oj-EKs8$gkhHU!h{yu>^>7?3TM`#2U1;k z47GoszRk8t-@hOJWA9p|WXF4kLPORJIye048I5Mrp48~Wi^@*sFP=`W`7GyR0C3ja z?8vxS8+Mb@t@B|1&YXb<$GgnT{PcGfdC1|F);veL6|-mH{5FtNph3K#h^ni)*@xBr zniS=cnxyGS=fKH{>d48pnwk_T@*|3SLAfdV^(bkK-*Gz2DP8Dsa}N!K(y`;k{zm%@ a`PX{9ksSY%<=>B%2Wq;iMfYvNFa85%r}sJl diff --git a/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@3x.png b/Sources/ArcGISToolkit/Resources/Images.xcassets/defaultthumbnail.imageset/basemap@3x.png index 6342f9c17e7df1d1d3caef1b66c636281f5101b5..7d1694585e7d71ca3c36e3963f7a2ffbdbb6bd39 100644 GIT binary patch delta 4781 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 Yz`ifeL)bOo?|F`aj)``GmTky?0LfI=ZU6uP delta 6599 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 Date: Fri, 1 Apr 2022 12:34:22 -0700 Subject: [PATCH 0744/1722] Add all style renderers --- .../Components/Scalebar/Scalebar.swift | 63 ++++-- .../Components/Scalebar/ScalebarLine.swift | 68 ------ .../Scalebar/ScalebarStyleRender.swift | 194 ++++++++++++++++-- .../Components/Scalebar/ScalebarUnits.swift | 51 +++++ .../Scalebar/ScalebarViewModel.swift | 79 ++++++- 5 files changed, 347 insertions(+), 108 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 0f7e984ca..840e33f6d 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -27,19 +27,28 @@ public struct Scalebar: View { private var alignment: ScalebarAlignment - private var alternateFillColor = Color.black + internal var lineColor = Color.white - internal var fillColor = Color(uiColor: .lightGray).opacity(0.5) + internal var fillColor1 = Color.black + + internal var fillColor2 = Color(uiColor: .lightGray).opacity(0.5) internal var shadowColor = Color(uiColor: .black).opacity(0.65) + internal var shadowRadius = 1.0 + + internal var lineWidth = 3.0 + + internal var lineFrameHeight = 6.0 - internal var font: Font + internal var barFrameHeight = 10.0 + + internal var lineCornerRadius = 2.5 private var style: ScalebarStyle private var textColor = Color.black - private var textShadowColor = Color.white + var textShadowColor = Color.white /// Acts as a data provider of the current scale. private var viewpoint: Binding @@ -48,24 +57,28 @@ public struct Scalebar: View { private var visibleArea: Binding public init( - alignment: ScalebarAlignment = .left, + alignment: Alignment, _ spatialReference: SpatialReference? = .wgs84, _ style: ScalebarStyle = .alternatingBar, _ targetWidth: Double, _ unitsPerPoint: Binding, _ viewpoint: Binding, _ visibleArea: Binding, - - font: Font = .system(size: 10.0, weight: .semibold), - units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, useGeodeticCalculations: Bool = true ) { self.viewpoint = viewpoint self.visibleArea = visibleArea - self.alignment = alignment - self.font = font + switch alignment { + case .topTrailing, .trailing, .bottomTrailing: + self.alignment = .right + case .top, .center, .bottom: + self.alignment = .center + default: + self.alignment = .left + } + self.style = style _viewModel = StateObject( @@ -84,19 +97,16 @@ public struct Scalebar: View { public var body: some View { Group { switch style { - case .line: - lineStyleRender + case .alternatingBar: + alternatingBarStyleRender case .bar: barStyleRender - case .graduatedLine: - #warning(".graduatedLine not yet implemented") - EmptyView() - case .alternatingBar: - #warning(".alternatingBar not yet implemented") - EmptyView() case .dualUnitLine: - #warning(".dualUnitLine not yet implemented") - EmptyView() + dualUnitLineStyleRender + case .graduatedLine: + graduatedLineStyleRender + case .line: + lineStyleRender } } .onChange(of: visibleArea.wrappedValue) { @@ -111,7 +121,19 @@ public struct Scalebar: View { ) } + internal static var font: (Font: Font, UIFont: UIFont) { + let size = 10.0 + let uiFont = UIFont.systemFont( + ofSize: size, + weight: .semibold + ) + let font = Font(uiFont as CTFont) + return (font, uiFont) + } + + /// The spacing between labels and the scalebar. internal static let labelYPad: CGFloat = 2.0 + internal static let labelXPad: CGFloat = 4.0 internal static let tickHeight: CGFloat = 6.0 internal static let tick2Height: CGFloat = 4.5 @@ -137,3 +159,4 @@ public struct Scalebar: View { /// always be visible private let minScale: Double = 0 } + diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift deleted file mode 100644 index 21576a7dd..000000000 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLine.swift +++ /dev/null @@ -1,68 +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 - -extension Scalebar { - /// Provides simple and predictable line creation. - struct Line { - /// The stroke width used to produce a line. - private static let strokeWidth = 4.0 - - /// A basic line. - private struct basic: View { - /// The width of the line. - var width: Double - - /// The color used to fill the line. - let color: Color = .white - - var body: some View { - Path { path in - path.move(to: CGPoint(x: Double.zero, y: .zero)) - path.addLine(to: CGPoint(x: width, y: .zero)) - path.addLine(to: CGPoint(x: width, y: strokeWidth)) - path.addLine(to: CGPoint(x: .zero, y: strokeWidth)) - path.addLine(to: CGPoint(x: Double.zero, y: .zero)) - } - .fill(color) - } - } - - /// A horizontally oriented line - struct basicHorizontal: View { - /// The width of the line. - var width: CGFloat - - var body: some View { - basic(width: width) - .frame(width: width, height: strokeWidth) - .cornerRadius(strokeWidth/2) - } - } - - /// A vertically oriented line - struct basicVertical: View { - /// The height of the line. - var height: CGFloat - - var body: some View { - basic(width: height) - .rotationEffect(Angle(degrees: 90), anchor: .topLeading) - .offset(x: strokeWidth) - .frame(width: strokeWidth, height: height) - .cornerRadius(strokeWidth/2) - } - } - } -} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index c23ece5a3..d10a1ff2f 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -14,26 +14,60 @@ import SwiftUI extension Scalebar { + var alternatingBarStyleRender: some View { + VStack(spacing: Scalebar.labelYPad) { + HStack(spacing: -lineWidth) { + ForEach(viewModel.segments, id: \Scalebar.Segment.index) { + Rectangle() + .fill($0.index % 2 == 0 ? fillColor1 : fillColor2) + .border( + lineColor, + width: lineWidth + ) + } + } + .frame( + height: barFrameHeight, + alignment: .leading + ) + .compositingGroup() + .cornerRadius(lineCornerRadius) + .shadow( + color: shadowColor, + radius: shadowRadius + ) + ZStack { + ForEach(viewModel.segments, id: \Scalebar.Segment.index) { + Text($0.text) + .font(Scalebar.font.Font) + .shadow(color: textShadowColor, radius: shadowRadius) + .position(x: $0.xOffset, y: $0.yOffset) + } + } + } + } + var barStyleRender: some View { - VStack(spacing: 2) { + VStack(spacing: Scalebar.labelYPad) { Rectangle() - .fill(fillColor) + .fill(fillColor2) .border( - .white, - width: 1.5 + lineColor, + width: lineWidth ) .frame( - height: 7, + height: barFrameHeight, alignment: .leading ) .shadow( color: shadowColor, - radius: 1 + radius: shadowRadius ) + .cornerRadius(lineCornerRadius) HStack { Text("\($viewModel.displayLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") - .font(font) - .fontWeight(.semibold) + .font(Scalebar.font.Font) + .shadow(color: textShadowColor, radius: shadowRadius) .onSizeChange { finalLengthWidth = $0.width } @@ -43,27 +77,38 @@ extension Scalebar { } var lineStyleRender: some View { - VStack(spacing: 2) { + VStack(spacing: Scalebar.labelYPad) { GeometryReader { geoProxy in ZStack(alignment: .bottom) { - Line.basicHorizontal(width: geoProxy.size.width) - HStack { - Line.basicVertical(height: geoProxy.size.height) - Spacer() - Line.basicVertical(height: geoProxy.size.height) + Path { path in + let zero = Double.zero + let maxX = geoProxy.size.width + let maxY = geoProxy.size.height + path.move(to: CGPoint(x: zero, y: zero)) + path.addLine(to: CGPoint(x: zero, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: zero)) } + .stroke( + style: .init( + lineWidth: lineWidth, + lineCap: .round, + lineJoin: .round + ) + ) + .fill(lineColor) } .compositingGroup() .shadow( color: shadowColor, - radius: 1 + radius: shadowRadius ) } - .frame(height: 10) + .frame(height: lineFrameHeight) HStack { Text("\($viewModel.displayLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") - .font(font) - .fontWeight(.semibold) + .font(Scalebar.font.Font) + .shadow(color: textShadowColor, radius: shadowRadius) .onSizeChange { finalLengthWidth = $0.width } @@ -72,4 +117,117 @@ extension Scalebar { } } + var dualUnitLineStyleRender: some View { + VStack(spacing: Scalebar.labelYPad) { + GeometryReader { geoProxy in + ZStack(alignment: .bottom) { + Path { path in + let zero = Double.zero + let maxX = geoProxy.size.width + let maxY = geoProxy.size.height + let midY = maxY / 2 + let alternateUnitX = viewModel.alternateUnitLength + + // Leading vertical bar + path.move(to: CGPoint(x: zero, y: zero)) + path.addLine(to: CGPoint(x: zero, y: maxY)) + + // Horiontal cross bar + path.move(to: CGPoint(x: zero, y: midY)) + path.addLine(to: CGPoint(x: maxX, y: midY)) + + // Unit 1 vertical bar + path.move(to: CGPoint(x: maxX, y: zero)) + path.addLine(to: CGPoint(x: maxX, y: midY)) + + // Unit 2 vertical bar + path.move(to: CGPoint(x: alternateUnitX, y: midY)) + path.addLine(to: CGPoint(x: alternateUnitX, y: maxY)) + } + .stroke( + style: .init( + lineWidth: lineWidth, + lineCap: .round, + lineJoin: .round + ) + ) + .fill(lineColor) + } + .compositingGroup() + .shadow( + color: shadowColor, + radius: shadowRadius + ) + } + .frame(height: barFrameHeight) + ZStack { + Text("0") + .font(Scalebar.font.Font) + .shadow(color: textShadowColor, radius: shadowRadius) + .offset(x: -10) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + var graduatedLineStyleRender: some View { + VStack(spacing: Scalebar.labelYPad) { + GeometryReader { geoProxy in + ZStack(alignment: .bottom) { + Path { path in + let segments = viewModel.segments + let zero = Double.zero + let maxX = geoProxy.size.width + let maxY = geoProxy.size.height + path.move(to: CGPoint(x: zero, y: zero)) + path.addLine(to: CGPoint(x: zero, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: zero)) + for segment in segments { + if segment.index == segments.last?.index { + continue + } + let segmentX = segment.xOffset + path.move(to: CGPoint(x: segmentX, y: zero)) + path.addLine(to: CGPoint(x: segmentX, y: maxY)) + } + } + .stroke( + style: .init( + lineWidth: lineWidth, + lineCap: .round, + lineJoin: .round + ) + ) + .fill(lineColor) + } + .compositingGroup() + .shadow( + color: shadowColor, + radius: shadowRadius + ) + } + .frame(height: lineFrameHeight) + ZStack { + ForEach(viewModel.segments, id: \Scalebar.Segment.index) { + Text($0.text) + .font(Scalebar.font.Font) + .shadow(color: textShadowColor, radius: shadowRadius) + .position(x: $0.xOffset, y: $0.yOffset) + } + } + } + } +} + +extension Scalebar { + struct Segment { + var index: Int + var segmentScreenLength: CGFloat + var xOffset: CGFloat + var yOffset: CGFloat + var segmentMapLength: Double + var text: String + var textWidth: CGFloat + } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index 4595f853d..bed0d8260 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -59,6 +59,16 @@ public enum ScalebarUnits { return roundNumber } + internal static func numSegmentsForDistance(distance: Double, maxNumSegments: Int) -> Int { + // this function returns the best number of segments so that we get relatively round + // numbers when the distance is divided up. + + let mm = multiplierAndMagnitudeForDistance(distance: distance) + let options = segmentOptionsForMultiplier(multiplier: mm.multiplier) + let num = options.filter { $0 <= maxNumSegments }.last ?? 1 + return num + } + /// This table must begin with 1 and end with 10. private static let roundNumberMultipliers: [Double] = [1, 1.2, 1.25, 1.5, 1.75, 2, 2.4, 2.5, 3, 3.75, 4, 5, 6, 7.5, 8, 9, 10] @@ -80,4 +90,45 @@ public enum ScalebarUnits { return LinearUnit.meters } } + + private static func segmentOptionsForMultiplier(multiplier: Double) -> [Int] { + switch multiplier { + case 1: + return [1, 2, 4, 5] + case 1.2: + return [1, 2, 3, 4] + case 1.25: + return [1, 2] + case 1.5: + return [1, 2, 3, 5] + case 1.75: + return [1, 2] + case 2: + return [1, 2, 4, 5] + case 2.4: + return [1, 2, 3] + case 2.5: + return [1, 2, 5] + case 3: + return [1, 2, 3] + case 3.75: + return [1, 3] + case 4: + return [1, 2, 4] + case 5: + return [1, 2, 5] + case 6: + return [1, 2, 3] + case 7.5: + return [1, 2] + case 8: + return [1, 2, 4] + case 9: + return [1, 2, 3] + case 10: + return [1, 2, 5] + default: + return [1] + } + } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index f97471364..05f207b89 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -24,6 +24,10 @@ final class ScalebarViewModel: ObservableObject { @Published var displayUnit: LinearUnit? = nil + @Published var lineMapLength: Double = .zero + + @Published var segments = [Scalebar.Segment]() + var visibleAreaSubject = PassthroughSubject() private var visibleAreaCancellable: AnyCancellable? @@ -154,7 +158,11 @@ final class ScalebarViewModel: ObservableObject { units: baseUnits ) displayLength = CGFloat( - baseUnits.convert(to: srUnit, value: closestLen) / unitsPerPoint) + baseUnits.convert( + to: srUnit, + value: closestLen + ) / unitsPerPoint + ) displayUnit = units.linearUnitsForDistance(distance: closestLen) lineMapLength = baseUnits.convert( to: displayUnit, @@ -170,6 +178,73 @@ final class ScalebarViewModel: ObservableObject { self.displayUnit = displayUnit - displayLengthString = Scalebar.numberFormatter.string(from: NSNumber(value: lineMapLength)) ?? "" + self.lineMapLength = lineMapLength + + displayLengthString = Scalebar.numberFormatter.string( + from: NSNumber(value: lineMapLength) + ) ?? "" + + updateSegments() + } + + func updateSegments() { + let lineDisplayLength = displayLength + + // Use a string with at least a few characters in case the number string + // only has 1. + // The dividers will be decimal values and we want to make sure they all + // fit very basic hueristics. + let minSegmentTestString = (displayLengthString.count > 3) ? displayLengthString : "9.9" + // Use 1.5 because the last segment, the text is right justified insted + // of center, which makes it harder to squeeze text in. + let minSegmentWidth = (minSegmentTestString.size(withAttributes: [.font: Scalebar.font.UIFont]).width * 1.5) + (Scalebar.labelXPad * 2) + var maxNumSegments = Int(lineDisplayLength / minSegmentWidth) + maxNumSegments = min(maxNumSegments, 4) // cap it at 4 + let numSegments: Int = ScalebarUnits.numSegmentsForDistance(distance: lineMapLength, maxNumSegments: maxNumSegments) + + let segmentScreenLength: CGFloat = (lineDisplayLength / CGFloat(numSegments)) + var currSegmentX: CGFloat = 0 + var segments = [Scalebar.Segment]() + + for index in 0.. Date: Mon, 4 Apr 2022 14:35:33 -0500 Subject: [PATCH 0745/1722] Update Examples/Examples/BasemapGalleryExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/BasemapGalleryExampleView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 0f71b175a..8517f8002 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -20,8 +20,7 @@ struct BasemapGalleryExampleView: View { let map: Map /// A Boolean value indicating whether to show the basemap gallery. - @State - private var showBasemapGallery: Bool = false + @State private var showBasemapGallery: Bool = false /// The initial viewpoint of the map. let initialViewpoint = Viewpoint( From 884e329108c3eaa20919bce78089f595c96ae37d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 4 Apr 2022 14:43:36 -0500 Subject: [PATCH 0746/1722] Update Examples/Examples/BasemapGalleryExampleView.swift Co-authored-by: David Feinzimer --- 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 8517f8002..3cbff5870 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -29,7 +29,7 @@ struct BasemapGalleryExampleView: View { ) /// The initial list of basemaps. - private let basemaps = Self.initialBasemaps() + private let basemaps = initialBasemaps() init() { self.map = Map(basemapStyle: .arcGISImagery) From b94af5624469dd894652cf3ed4d0bfa715226167 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 4 Apr 2022 14:43:44 -0500 Subject: [PATCH 0747/1722] Update Examples/Examples/BasemapGalleryExampleView.swift Co-authored-by: David Feinzimer --- 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 3cbff5870..e7a6dc351 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -30,7 +30,7 @@ struct BasemapGalleryExampleView: View { /// The initial list of basemaps. private let basemaps = initialBasemaps() - + init() { self.map = Map(basemapStyle: .arcGISImagery) } From 2c8f327b47c6afb4340a9a020c3d4ee80fd58e26 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 4 Apr 2022 14:44:07 -0500 Subject: [PATCH 0748/1722] Update Examples/Examples/BasemapGalleryExampleView.swift Co-authored-by: David Feinzimer --- 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 e7a6dc351..2bb5ddb0e 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -39,7 +39,7 @@ struct BasemapGalleryExampleView: View { MapView(map: map, viewpoint: initialViewpoint) .overlay(alignment: .topTrailing) { if showBasemapGallery { - BasemapGallery(geoModel: self.map, items: basemaps) + BasemapGallery(geoModel: map, items: basemaps) .style(.automatic()) .esriBorder() .padding() From 624b2948afaaf34b136d9a9ee6e1acb30dbd73f7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 4 Apr 2022 14:44:13 -0500 Subject: [PATCH 0749/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift Co-authored-by: David Feinzimer --- .../Components/BasemapGallery/BasemapGallery.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index c1866fda5..ac31b1c11 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -55,7 +55,7 @@ public struct BasemapGallery: View { ) { viewModel = 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 From 890b1ca56bc90ca14ca8951e495a2d9775c4dc72 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 4 Apr 2022 15:06:05 -0500 Subject: [PATCH 0750/1722] Reorder properties. --- .../BasemapGallery/BasemapGalleryItem.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift index c61f01ae8..476f2b9b3 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryItem.swift @@ -56,9 +56,6 @@ public class BasemapGalleryItem: ObservableObject { } } - /// The error generated loading the basemap, if any. - @Published private(set) var loadBasemapError: Error? = nil - /// The basemap represented by `BasemapGalleryItem`. public let basemap: Basemap @@ -70,14 +67,17 @@ public class BasemapGalleryItem: ObservableObject { /// The thumbnail used to represent the `basemap`. @Published public private(set) var thumbnail: UIImage? - - /// A Boolean value indicating whether the `basemap` or it's base layers are being loaded. - @Published private(set) var isBasemapLoading = true - + /// The spatial reference status of the item. This is set via a call to /// ``updateSpatialReferenceStatus(_:)``. @Published public private(set) var spatialReferenceStatus: SpatialReferenceStatus = .unknown + /// A Boolean value indicating whether the `basemap` or it's base layers are being loaded. + @Published private(set) var isBasemapLoading = true + + /// The error generated loading the basemap, if any. + @Published private(set) var loadBasemapError: Error? = nil + /// The spatial reference of ``basemap``. This will be `nil` until the /// basemap's base layers have been loaded by /// ``updateSpatialReferenceStatus(_:)``. From 0b91b59786f273a49ee11cb5bd58397c067bfb35 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 4 Apr 2022 12:33:27 -0700 Subject: [PATCH 0751/1722] Update Scalebar --- .../Components/Scalebar/Scalebar.swift | 10 +- .../Components/Scalebar/ScalebarLabel.swift | 24 ++++ .../Scalebar/ScalebarStyleRender.swift | 87 +++++------- .../Components/Scalebar/ScalebarText.swift | 36 +++++ .../Components/Scalebar/ScalebarUnits.swift | 21 +-- .../Scalebar/ScalebarViewModel.swift | 132 +++++++++--------- 6 files changed, 180 insertions(+), 130 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarText.swift diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 840e33f6d..9c3434512 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -23,8 +23,6 @@ public struct Scalebar: View { /// The vertical amount of space used by the scalebar. @State private var height: Double = 50 - @State internal var finalLengthWidth: Double = .zero - private var alignment: ScalebarAlignment internal var lineColor = Color.white @@ -121,7 +119,7 @@ public struct Scalebar: View { ) } - internal static var font: (Font: Font, UIFont: UIFont) { + internal static var font: (font: Font, uiFont: UIFont) { let size = 10.0 let uiFont = UIFont.systemFont( ofSize: size, @@ -131,6 +129,10 @@ public struct Scalebar: View { return (font, uiFont) } + internal static var fontHeight: Double { + return "".size(withAttributes: [.font: Scalebar.font.uiFont]).height + } + /// The spacing between labels and the scalebar. internal static let labelYPad: CGFloat = 2.0 @@ -149,7 +151,6 @@ public struct Scalebar: View { internal static let lineCap = CGLineCap.round - internal var fontHeight: CGFloat = 0 internal var zeroStringWidth: CGFloat = 0 internal var maxRightUnitsPad: CGFloat = 0 @@ -159,4 +160,3 @@ public struct Scalebar: View { /// always be visible private let minScale: Double = 0 } - diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift new file mode 100644 index 000000000..88f132830 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift @@ -0,0 +1,24 @@ +// 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 + +struct ScalebarLabel { + let index: Int + let xOffset: CGFloat + let text: String + + static var yOffset: CGFloat { + return Scalebar.fontHeight / 2.0 + } +} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index d10a1ff2f..3f5e5d11b 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -17,7 +17,7 @@ extension Scalebar { var alternatingBarStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { HStack(spacing: -lineWidth) { - ForEach(viewModel.segments, id: \Scalebar.Segment.index) { + ForEach(viewModel.labels.dropFirst(), id: \.index) { Rectangle() .fill($0.index % 2 == 0 ? fillColor1 : fillColor2) .border( @@ -37,11 +37,13 @@ extension Scalebar { radius: shadowRadius ) ZStack { - ForEach(viewModel.segments, id: \Scalebar.Segment.index) { + ForEach(viewModel.labels, id: \.index) { Text($0.text) - .font(Scalebar.font.Font) - .shadow(color: textShadowColor, radius: shadowRadius) - .position(x: $0.xOffset, y: $0.yOffset) + .scalebarText(self) + .position( + x: $0.xOffset, + y: ScalebarLabel.yOffset + ) } } } @@ -59,20 +61,13 @@ extension Scalebar { height: barFrameHeight, alignment: .leading ) + .cornerRadius(lineCornerRadius) .shadow( color: shadowColor, radius: shadowRadius ) - .cornerRadius(lineCornerRadius) - HStack { - Text("\($viewModel.displayLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") - .font(Scalebar.font.Font) - .shadow(color: textShadowColor, radius: shadowRadius) - .onSizeChange { - finalLengthWidth = $0.width - } - .frame(maxWidth: .infinity, alignment: .center) - } + Text(viewModel.labels.last?.text ?? "") + .scalebarText(self) } } @@ -105,20 +100,22 @@ extension Scalebar { ) } .frame(height: lineFrameHeight) - HStack { - Text("\($viewModel.displayLengthString.wrappedValue) \($viewModel.displayUnit.wrappedValue?.abbreviation ?? "")") - .font(Scalebar.font.Font) - .shadow(color: textShadowColor, radius: shadowRadius) - .onSizeChange { - finalLengthWidth = $0.width - } - .frame(maxWidth: .infinity, alignment: .center) - } + Text(viewModel.labels.last?.text ?? "") + .scalebarText(self) } } var dualUnitLineStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { + ZStack { + Text(viewModel.labels.last?.text ?? "") + .scalebarText(self) + .position( + x: viewModel.labels.last?.xOffset ?? .zero, + y: ScalebarLabel.yOffset + ) + .frame(height: Scalebar.fontHeight) + } GeometryReader { geoProxy in ZStack(alignment: .bottom) { Path { path in @@ -126,7 +123,7 @@ extension Scalebar { let maxX = geoProxy.size.width let maxY = geoProxy.size.height let midY = maxY / 2 - let alternateUnitX = viewModel.alternateUnitLength + let alternateUnitX = viewModel.alternateUnit.screenLength // Leading vertical bar path.move(to: CGPoint(x: zero, y: zero)) @@ -161,12 +158,14 @@ extension Scalebar { } .frame(height: barFrameHeight) ZStack { - Text("0") - .font(Scalebar.font.Font) - .shadow(color: textShadowColor, radius: shadowRadius) - .offset(x: -10) + Text(viewModel.alternateUnit.label) + .scalebarText(self) + .position( + x: viewModel.alternateUnit.screenLength, + y: ScalebarLabel.yOffset + ) + .frame(height: Scalebar.fontHeight) } - .frame(maxWidth: .infinity, alignment: .leading) } } @@ -175,18 +174,13 @@ extension Scalebar { GeometryReader { geoProxy in ZStack(alignment: .bottom) { Path { path in - let segments = viewModel.segments + let segments = viewModel.labels let zero = Double.zero let maxX = geoProxy.size.width let maxY = geoProxy.size.height - path.move(to: CGPoint(x: zero, y: zero)) - path.addLine(to: CGPoint(x: zero, y: maxY)) + path.move(to: CGPoint(x: zero, y: maxY)) path.addLine(to: CGPoint(x: maxX, y: maxY)) - path.addLine(to: CGPoint(x: maxX, y: zero)) for segment in segments { - if segment.index == segments.last?.index { - continue - } let segmentX = segment.xOffset path.move(to: CGPoint(x: segmentX, y: zero)) path.addLine(to: CGPoint(x: segmentX, y: maxY)) @@ -209,25 +203,12 @@ extension Scalebar { } .frame(height: lineFrameHeight) ZStack { - ForEach(viewModel.segments, id: \Scalebar.Segment.index) { + ForEach(viewModel.labels, id: \.index) { Text($0.text) - .font(Scalebar.font.Font) - .shadow(color: textShadowColor, radius: shadowRadius) - .position(x: $0.xOffset, y: $0.yOffset) + .scalebarText(self) + .position(x: $0.xOffset, y: ScalebarLabel.yOffset) } } } } } - -extension Scalebar { - struct Segment { - var index: Int - var segmentScreenLength: CGFloat - var xOffset: CGFloat - var yOffset: CGFloat - var segmentMapLength: Double - var text: String - var textWidth: CGFloat - } -} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarText.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarText.swift new file mode 100644 index 000000000..811d1e569 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarText.swift @@ -0,0 +1,36 @@ +// 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 modifier which "styles" a Text element's font, shadow color and radius. +struct ScalebarTextModifier: ViewModifier { + let scalebar: Scalebar + + func body(content: Content) -> some View { + content + .font(Scalebar.font.font) + .shadow( + color: scalebar.textShadowColor, + radius: scalebar.shadowRadius + ) + } +} + +extension Text { + func scalebarText(_ scalebar: Scalebar) -> some View { + modifier( + ScalebarTextModifier(scalebar: scalebar) + ) + } +} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index bed0d8260..fc53173af 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -23,7 +23,9 @@ public enum ScalebarUnits { } /// Get multiplier - private static func multiplierAndMagnitudeForDistance(distance: Double) -> (multiplier: Double, magnitude: Double) { + private static func multiplierAndMagnitudeForDistance( + distance: Double) -> (multiplier: Double, magnitude: Double + ) { let magnitude = pow(10, floor(log10(distance))) let residual = distance / Double(magnitude) let multiplier: Double = ScalebarUnits.roundNumberMultipliers.filter { $0 <= residual }.last ?? 0 @@ -59,10 +61,12 @@ public enum ScalebarUnits { return roundNumber } - internal static func numSegmentsForDistance(distance: Double, maxNumSegments: Int) -> Int { - // this function returns the best number of segments so that we get relatively round - // numbers when the distance is divided up. - + /// This function returns the best number of segments so that we get relatively round numbers when the + /// distance is divided up. + internal static func numSegmentsForDistance( + distance: Double, + maxNumSegments: Int + ) -> Int { let mm = multiplierAndMagnitudeForDistance(distance: distance) let options = segmentOptionsForMultiplier(multiplier: mm.multiplier) let num = options.filter { $0 <= maxNumSegments }.last ?? 1 @@ -76,14 +80,11 @@ public enum ScalebarUnits { internal func linearUnitsForDistance(distance: Double) -> LinearUnit { switch self { case .imperial: - if distance >= 2640 { return LinearUnit.miles } return LinearUnit.feet - case .metric: - if distance >= 1000 { return LinearUnit.kilometers } @@ -91,7 +92,9 @@ public enum ScalebarUnits { } } - private static func segmentOptionsForMultiplier(multiplier: Double) -> [Int] { + private static func segmentOptionsForMultiplier( + multiplier: Double + ) -> [Int] { switch multiplier { case 1: return [1, 2, 4, 5] diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 05f207b89..71be34b7a 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -24,21 +24,30 @@ final class ScalebarViewModel: ObservableObject { @Published var displayUnit: LinearUnit? = nil - @Published var lineMapLength: Double = .zero + @Published var labels = [ScalebarLabel]() - @Published var segments = [Scalebar.Segment]() + @Published var lineMapLength: Double = .zero var visibleAreaSubject = PassthroughSubject() - private var visibleAreaCancellable: AnyCancellable? + /// Set a minScale if you only want the scalebar to appear when you reach a large enough scale maybe + /// something like 10_000_000. This could be useful because the scalebar is really only accurate for + /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will + /// always be visible + private let minScale: Double = 0 /// The amount of time to wait between value calculations. private var delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.05) + private var geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic + private var spatialReference: SpatialReference? = .wgs84 private var targetWidth: Double + /// Unit of measure in use. + private var units: ScalebarUnits + private var unitsPerPoint: Binding /// Allows a user to toggle geodetic calculations. @@ -50,16 +59,7 @@ final class ScalebarViewModel: ObservableObject { /// Acts as a data provider of the current scale. private var visibleArea: Binding - /// Unit of measure in use. - private var units: ScalebarUnits - - private var geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic - - /// Set a minScale if you only want the scalebar to appear when you reach a large enough scale maybe - /// something like 10_000_000. This could be useful because the scalebar is really only accurate for - /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will - /// always be visible - private let minScale: Double = 0 + private var visibleAreaCancellable: AnyCancellable? init( _ spatialReference: SpatialReference? = .wgs84, @@ -69,7 +69,6 @@ final class ScalebarViewModel: ObservableObject { _ useGeodeticCalculations: Bool = true, _ viewpoint: Binding, _ visibleArea: Binding - ) { self.spatialReference = spatialReference self.targetWidth = targetWidth @@ -88,6 +87,52 @@ final class ScalebarViewModel: ObservableObject { updateScaleDisplay() } + private func updateLabels() { + let lineDisplayLength = displayLength + + // Use a string with at least a few characters in case the number string + // only has 1. + // The dividers will be decimal values and we want to make sure they all + // fit very basic hueristics. + let minSegmentTestString = (displayLengthString.count > 3) ? displayLengthString : "9.9" + // Use 1.5 because the last segment, the text is right justified insted + // of center, which makes it harder to squeeze text in. + let minSegmentWidth = (minSegmentTestString.size(withAttributes: [.font: Scalebar.font.uiFont]).width * 1.5) + (Scalebar.labelXPad * 2) + var maxNumSegments = Int(lineDisplayLength / minSegmentWidth) + maxNumSegments = min(maxNumSegments, 4) // cap it at 4 + let numSegments: Int = ScalebarUnits.numSegmentsForDistance(distance: lineMapLength, maxNumSegments: maxNumSegments) + + let segmentScreenLength: CGFloat = (lineDisplayLength / CGFloat(numSegments)) + var currSegmentX: CGFloat = 0 + var labels = [ScalebarLabel]() + + labels.append( + ScalebarLabel( + index: -1, + xOffset: .zero, + text: "0" + ) + ) + + for index in 0.. 3) ? displayLengthString : "9.9" - // Use 1.5 because the last segment, the text is right justified insted - // of center, which makes it harder to squeeze text in. - let minSegmentWidth = (minSegmentTestString.size(withAttributes: [.font: Scalebar.font.UIFont]).width * 1.5) + (Scalebar.labelXPad * 2) - var maxNumSegments = Int(lineDisplayLength / minSegmentWidth) - maxNumSegments = min(maxNumSegments, 4) // cap it at 4 - let numSegments: Int = ScalebarUnits.numSegmentsForDistance(distance: lineMapLength, maxNumSegments: maxNumSegments) - - let segmentScreenLength: CGFloat = (lineDisplayLength / CGFloat(numSegments)) - var currSegmentX: CGFloat = 0 - var segments = [Scalebar.Segment]() - - for index in 0.. Date: Tue, 5 Apr 2022 14:49:38 -0500 Subject: [PATCH 0752/1722] Update Examples/Examples/BasemapGalleryExampleView.swift Co-authored-by: Nimesh Jarecha --- 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 2bb5ddb0e..2e90979df 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -60,7 +60,7 @@ struct BasemapGalleryExampleView: View { ] return identifiers.map { identifier in - let url = URL(string: "https://maps.arcgis.com/home/item.html?id=\(identifier)")! + let url = URL(string: "https://www.arcgis.com/home/item.html?id=\(identifier)")! return BasemapGalleryItem(basemap: Basemap(item: PortalItem(url: url)!)) } } From 2b5b1be98cd7be6629f5ee397592b7b195a96812 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 5 Apr 2022 14:50:03 -0500 Subject: [PATCH 0753/1722] Update Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift Co-authored-by: Nimesh Jarecha --- .../Components/BasemapGallery/BasemapGalleryCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index 3ef19fddc..1ffccf29c 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -86,7 +86,7 @@ struct BasemapGalleryCell: View { } extension BasemapGalleryItem { - /// A Boolean denoting if the item has an error or not. + /// A Boolean value indicating whether the item has an error. var hasError: Bool { loadBasemapError != nil || spatialReferenceStatus == .noMatch From bd7f0fb98dab4a41cd85318a9181b861eb4e0f2b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 5 Apr 2022 14:51:38 -0500 Subject: [PATCH 0754/1722] Property modifiers need to be on the same line as the property. Use a .menu picker style and put it in a toolbar. --- .../Examples/OverviewMapExampleView.swift | 39 ++++++------- .../Components/OverviewMap.swift | 55 +++++++++---------- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 18b7cc605..72871cc0b 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -23,33 +23,35 @@ struct OverviewMapExampleView: View { case scene } - @State - private var mapOrScene: MapOrScene = .map + @State private var mapOrScene: MapOrScene = .map var body: some View { - Picker("Map or Scene", selection: $mapOrScene, content: { - Text("Map").tag(MapOrScene.map) - Text("Scene").tag(MapOrScene.scene) - }) - .pickerStyle(SegmentedPickerStyle()) - .padding() - switch mapOrScene { - case .map: - OverviewMapForMapView() - case .scene: - OverviewMapForSceneView() + Group { + switch mapOrScene { + case .map: + OverviewMapForMapView() + case .scene: + OverviewMapForSceneView() + } } + .toolbar(content: { + ToolbarItem(placement: .navigationBarTrailing) { + Picker("Map or Scene", selection: $mapOrScene) { + Text("Map").tag(MapOrScene.map) + Text("Scene").tag(MapOrScene.scene) + } + .pickerStyle(.menu) + } + }) } } struct OverviewMapForMapView: View { let map = Map(basemapStyle: .arcGISImagery) - @State - private var viewpoint: Viewpoint? + @State private var viewpoint: Viewpoint? - @State - private var visibleArea: ArcGIS.Polygon? + @State private var visibleArea: ArcGIS.Polygon? var body: some View { MapView(map: map) @@ -75,8 +77,7 @@ struct OverviewMapForMapView: View { struct OverviewMapForSceneView: View { let scene = Scene(basemapStyle: .arcGISImagery) - @State - private var viewpoint: Viewpoint? + @State private var viewpoint: Viewpoint? var body: some View { SceneView(scene: scene) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index df0b10f3b..0c789693d 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -28,16 +28,13 @@ public struct OverviewMap: View { private var scaleFactor = 25.0 - @StateObject - private var map = Map(basemapStyle: .arcGISTopographic) + @StateObject private var map = Map(basemapStyle: .arcGISTopographic) /// The `Graphic` displaying the visible area of the main `GeoView`. - @StateObject - private var graphic: Graphic + @StateObject private var graphic: Graphic /// The `GraphicsOverlay` used to display the visible area graphic. - @StateObject - private var graphicsOverlay: GraphicsOverlay + @StateObject private var graphicsOverlay: GraphicsOverlay /// Creates an `OverviewMap` for use on a `MapView`. /// - Parameters: @@ -60,7 +57,7 @@ public struct OverviewMap: View { ) -> OverviewMap { OverviewMap(viewpoint: viewpoint, symbol: .defaultMarker) } - + /// 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. @@ -75,7 +72,7 @@ public struct OverviewMap: View { self.symbol = symbol let graphic = Graphic(symbol: self.symbol) - + // It is necessary to set the graphic and graphicsOverlay this way // in order to prevent the main geoview from recreating the // graphicsOverlay every draw cycle. That was causing refresh issues @@ -90,27 +87,27 @@ public struct OverviewMap: View { viewpoint: makeOverviewViewpoint(), graphicsOverlays: [graphicsOverlay] ) - .attributionText(hidden: true) - .interactionModes([]) - .border(.black, width: 1) - .onAppear(perform: { - graphic.symbol = symbol - }) - .onChange(of: visibleArea, perform: { visibleArea in - if let visibleArea = visibleArea { - graphic.geometry = visibleArea - } - }) - .onChange(of: viewpoint, perform: { viewpoint in - if visibleArea == nil, - let viewpoint = viewpoint, - let point = viewpoint.targetGeometry as? Point { - graphic.geometry = point - } - }) - .onChange(of: symbol, perform: { - graphic.symbol = $0 - }) + .attributionText(hidden: true) + .interactionModes([]) + .border(.black, width: 1) + .onAppear { + graphic.symbol = symbol + } + .onChange(of: visibleArea) { visibleArea in + if let visibleArea = visibleArea { + graphic.geometry = visibleArea + } + } + .onChange(of: viewpoint) { viewpoint in + if visibleArea == nil, + let viewpoint = viewpoint, + let point = viewpoint.targetGeometry as? Point { + graphic.geometry = point + } + } + .onChange(of: symbol) { + graphic.symbol = $0 + } } /// Creates an overview viewpoint based on the observed `viewpoint` center, scale, and `scaleFactor`. From ec0b9ca83b6fcc0960203691057937200207d4cc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 5 Apr 2022 14:56:12 -0500 Subject: [PATCH 0755/1722] Doc changes --- .../Components/BasemapGallery/BasemapGallery.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index ac31b1c11..ac8ad20f6 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -32,7 +32,7 @@ public struct BasemapGallery: View { case list(width: CGFloat = 125) } - /// Creates a `BasemapGallery`. Uses the given array of basemap gallery items. + /// Creates a `BasemapGallery` with the given geo model and array of basemap gallery items. /// - Remark: If `items` is empty, ArcGIS Online's developer basemaps will /// be loaded and added to `items`. /// - Parameters: @@ -45,7 +45,8 @@ public struct BasemapGallery: View { viewModel = BasemapGalleryViewModel(geoModel: geoModel, items: items) } - /// Creates a `BasemapGallery`. The portal will be used to to retrieve basemaps. + /// Creates a `BasemapGallery` with the given geo model and portal. + /// The portal will be used to retrieve basemaps. /// - Parameters: /// - geoModel: A geo model. /// - portal: The portal to use to load basemaps. From a8d32d367b527d45790ddc97cbab39605b65c699 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 6 Apr 2022 11:49:55 -0500 Subject: [PATCH 0756/1722] line length change --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 0c789693d..4e5870b69 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -113,7 +113,10 @@ public struct OverviewMap: View { /// Creates an overview viewpoint based on the observed `viewpoint` center, scale, and `scaleFactor`. /// - Returns: The new `Viewpoint`. func makeOverviewViewpoint() -> Viewpoint? { - guard let viewpoint = viewpoint, let center = viewpoint.targetGeometry as? Point else { return nil } + guard let viewpoint = viewpoint, + let center = viewpoint.targetGeometry as? Point + else { return nil } + return Viewpoint( center: center, scale: viewpoint.targetScale * scaleFactor From de91f528ed6f9b7553966d3ae9975c16e7c944b5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Apr 2022 11:36:15 -0700 Subject: [PATCH 0757/1722] Update Scalebar --- Examples/Examples/ScalebarExampleView.swift | 35 +- .../Components/Scalebar/Scalebar.swift | 198 +++++------ .../Scalebar/ScalebarAlignment.swift | 20 -- .../Components/Scalebar/ScalebarLabel.swift | 7 + ...ebarText.swift => ScalebarModifiers.swift} | 30 +- .../Components/Scalebar/ScalebarStyle.swift | 16 +- .../Scalebar/ScalebarStyleRender.swift | 162 ++++----- .../Components/Scalebar/ScalebarUnits.swift | 170 ++++++---- .../Scalebar/ScalebarViewModel.swift | 315 ++++++++++++------ Tests/ArcGISToolkitTests/ScalebarTests.swift | 203 +++++++++++ 10 files changed, 750 insertions(+), 406 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift rename Sources/ArcGISToolkit/Components/Scalebar/{ScalebarText.swift => ScalebarModifiers.swift} (58%) create mode 100644 Tests/ArcGISToolkitTests/ScalebarTests.swift diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 58a462e11..57d074588 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -16,44 +16,39 @@ import ArcGISToolkit import SwiftUI struct ScalebarExampleView: View { - /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. - @State private var scale: Double? - - /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. + /// Allows for communication between the `Scalebar` and `MapView`. @State private var spatialReference: SpatialReference? + /// Allows for communication between the `Scalebar` and `MapView`. @State private var unitsPerPoint: Double? - /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. + /// Allows for communication between the `Scalebar` and `MapView`. @State private var viewpoint: Viewpoint? - /// Allows for communication between the `Scalebar` and `MapView` or `SceneView`. - @State private var visibleArea: Polygon? + /// The location of the scalebar on screen. + private let alignment: Alignment = .bottomLeading /// The `Map` displayed in the `MapView`. private let map = Map(basemapStyle: .arcGISTopographic) + /// The width of the scalebar. + private let width: Double = 175.0 + var body: some View { MapView(map: map, viewpoint: viewpoint) - .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .onVisibleAreaChanged { visibleArea = $0 } - .onScaleChanged { scale = $0 } .onSpatialReferenceChanged { spatialReference = $0 } .onUnitsPerPointChanged { unitsPerPoint = $0 } - .overlay(alignment: .bottomLeading) { + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: alignment) { if map.loadStatus == .loaded { Scalebar( - alignment: .left, - spatialReference, - .line, - 175, - $unitsPerPoint, - $viewpoint, - $visibleArea, - units: .imperial + spatialReference: spatialReference, + unitsPerPoint: $unitsPerPoint, + viewpoint: $viewpoint, + width: width ) .padding(.horizontal, 10) - .padding(.vertical, 30) + .padding(.vertical, 50) } } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 9c3434512..4219e7db7 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -16,147 +16,147 @@ import SwiftUI /// Displays the current scale on-screen public struct Scalebar: View { - /// The view model used by the `Scalebar`. - @StateObject - internal var viewModel: ScalebarViewModel + // - MARK: Internal/Private vars /// The vertical amount of space used by the scalebar. - @State private var height: Double = 50 + @State private var height: Double? + + /// The view model used by the `Scalebar`. + @StateObject internal var viewModel: ScalebarViewModel + + /// The font used by the scalebar, available in both `Font` and `UIFont` types. + internal static var font: (font: Font, uiFont: UIFont) { + let size = 10.0 + let uiFont = UIFont.systemFont( + ofSize: size, + weight: .semibold + ) + let font = Font(uiFont as CTFont) + return (font, uiFont) + } - private var alignment: ScalebarAlignment + /// The rendering height of the scalebar font. + internal static var fontHeight: Double { + return "".size(withAttributes: [.font: Scalebar.font.uiFont]).height + } - internal var lineColor = Color.white + /// Acts as a data provider of the current scale. + private var viewpoint: Binding - internal var fillColor1 = Color.black + // - MARK: Internal/Private constants - internal var fillColor2 = Color(uiColor: .lightGray).opacity(0.5) + /// The corner radius used by bar style scalebar renders. + internal static let barCornerRadius = 2.5 - internal var shadowColor = Color(uiColor: .black).opacity(0.65) - internal var shadowRadius = 1.0 + /// The frame height alloted to bar style scalebar renders. + internal static let barFrameHeight = 10.0 - internal var lineWidth = 3.0 + /// The darker fill color used by the alternating bar style render. + internal static let fillColor1 = Color.black - internal var lineFrameHeight = 6.0 + /// The lighter fill color used by the bar style renders. + internal static let fillColor2 = Color(uiColor: .lightGray).opacity(0.5) - internal var barFrameHeight = 10.0 + /// The spacing between labels and the scalebar. + internal static let labelYPad: CGFloat = 2.0 - internal var lineCornerRadius = 2.5 + /// The required padding between scalebar labels. + internal static let labelXPad: CGFloat = 4.0 - private var style: ScalebarStyle + /// The color of the prominent scalebar line. + internal static let lineColor = Color.white - private var textColor = Color.black + /// The line height alloted to line style scalebar renders. + internal static let lineFrameHeight = 6.0 - var textShadowColor = Color.white + /// The width of the prominent scalebar line. + internal static let lineWidth = 3.0 - /// Acts as a data provider of the current scale. - private var viewpoint: Binding + /// The shadow color used by all scalebar style renders. + internal static let shadowColor = Color(uiColor: .black).opacity(0.65) - /// Acts as a data provider of the current scale. - private var visibleArea: Binding + /// The shadow radius used by all scalebar style renders. + internal static let shadowRadius = 1.0 + + /// The text shadow color used by all scalebar style renders. + internal static let textShadowColor = Color.white + + /// The render style for this `Scalebar`. + private let style: ScalebarStyle + // - MARK: Public methods/vars + + /// A scalebar displays the current map scale. + /// - Parameters: + /// - autoHide: Set this to `true` to have the scalebar automatically show & hide itself. + /// - minScale: Set a minScale if you only want the scalebar to appear when you reach a large + /// enough scale maybe something like 10_000_000. This could be useful because the scalebar is + /// really only accurate for the center of the map on smaller scales (when zoomed way out). A + /// minScale of 0 means it will always be visible. + /// - spatialReference: The map's spatial reference. + /// - style: The visual appearance of the scalebar. + /// - units: The units to be displayed in the scalebar. + /// - unitsPerPoint: The current number of device independent pixels to map display units. + /// - useGeodeticCalculations: Set `false` to compute scale without a geodesic curve. + /// - viewpoint: The map's current viewpoint. + /// - width: The screen width alloted to the scalebar. public init( - alignment: Alignment, - _ spatialReference: SpatialReference? = .wgs84, - _ style: ScalebarStyle = .alternatingBar, - _ targetWidth: Double, - _ unitsPerPoint: Binding, - _ viewpoint: Binding, - _ visibleArea: Binding, + autoHide: Bool = false, + minScale: Double = .zero, + spatialReference: SpatialReference? = nil, + style: ScalebarStyle = .alternatingBar, units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, - useGeodeticCalculations: Bool = true + unitsPerPoint: Binding, + useGeodeticCalculations: Bool = true, + viewpoint: Binding, + width: Double ) { - self.viewpoint = viewpoint - self.visibleArea = visibleArea - - switch alignment { - case .topTrailing, .trailing, .bottomTrailing: - self.alignment = .right - case .top, .center, .bottom: - self.alignment = .center - default: - self.alignment = .left - } - self.style = style + self.viewpoint = viewpoint _viewModel = StateObject( wrappedValue: ScalebarViewModel( + autoHide, + minScale, spatialReference, - targetWidth, + style, + width, units, unitsPerPoint, useGeodeticCalculations, - viewpoint, - visibleArea + viewpoint.wrappedValue ) ) } public var body: some View { Group { - switch style { - case .alternatingBar: - alternatingBarStyleRender - case .bar: - barStyleRender - case .dualUnitLine: - dualUnitLineStyleRender - case .graduatedLine: - graduatedLineStyleRender - case .line: - lineStyleRender + if $viewModel.isVisible.wrappedValue { + switch style { + case .alternatingBar: + alternatingBarStyleRender + case .bar: + barStyleRender + case .dualUnitLine: + dualUnitLineStyleRender + case .graduatedLine: + graduatedLineStyleRender + case .line: + lineStyleRender + } + } else { + EmptyView() } } - .onChange(of: visibleArea.wrappedValue) { - viewModel.visibleAreaSubject.send($0) + .onChange(of: viewpoint.wrappedValue) { + viewModel.viewpointSubject.send($0) } .onSizeChange { height = $0.height } .frame( width: $viewModel.displayLength.wrappedValue, - height: height + height: height ?? .zero ) } - - internal static var font: (font: Font, uiFont: UIFont) { - let size = 10.0 - let uiFont = UIFont.systemFont( - ofSize: size, - weight: .semibold - ) - let font = Font(uiFont as CTFont) - return (font, uiFont) - } - - internal static var fontHeight: Double { - return "".size(withAttributes: [.font: Scalebar.font.uiFont]).height - } - - /// The spacing between labels and the scalebar. - internal static let labelYPad: CGFloat = 2.0 - - internal static let labelXPad: CGFloat = 4.0 - internal static let tickHeight: CGFloat = 6.0 - internal static let tick2Height: CGFloat = 4.5 - internal static let notchHeight: CGFloat = 6.0 - internal static var numberFormatter: NumberFormatter = { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .decimal - numberFormatter.formatterBehavior = .behavior10_4 - numberFormatter.maximumFractionDigits = 2 - numberFormatter.minimumFractionDigits = 0 - return numberFormatter - }() - - internal static let lineCap = CGLineCap.round - - internal var zeroStringWidth: CGFloat = 0 - internal var maxRightUnitsPad: CGFloat = 0 - - /// Set a minScale if you only want the scalebar to appear when you reach a large enough scale maybe - /// something like 10_000_000. This could be useful because the scalebar is really only accurate for - /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will - /// always be visible - private let minScale: Double = 0 } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift deleted file mode 100644 index 436bb338e..000000000 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarAlignment.swift +++ /dev/null @@ -1,20 +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 Foundation - -public enum ScalebarAlignment { - case left - case right - case center -} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift index 88f132830..361db15d1 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarLabel.swift @@ -13,11 +13,18 @@ import SwiftUI +/// Models a label displayed along the edge of a scalebar struct ScalebarLabel { + /// The number of the label with the leftmost label ("0") starting at -1. let index: Int + + /// The horizontal offset of this label. let xOffset: CGFloat + + /// The text to be displayed by this label. let text: String + /// The vertical offset of this label. static var yOffset: CGFloat { return Scalebar.fontHeight / 2.0 } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarText.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift similarity index 58% rename from Sources/ArcGISToolkit/Components/Scalebar/ScalebarText.swift rename to Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift index 811d1e569..ca95ff9ad 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarText.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift @@ -15,22 +15,40 @@ import SwiftUI /// A modifier which "styles" a Text element's font, shadow color and radius. struct ScalebarTextModifier: ViewModifier { - let scalebar: Scalebar - func body(content: Content) -> some View { content .font(Scalebar.font.font) .shadow( - color: scalebar.textShadowColor, - radius: scalebar.shadowRadius + color: Scalebar.textShadowColor, + radius: Scalebar.shadowRadius ) } } extension Text { - func scalebarText(_ scalebar: Scalebar) -> some View { + func scalebarText() -> some View { + modifier( + ScalebarTextModifier() + ) + } +} + +/// A modifier which "styles" a scalebar's shadow color and radius. +struct ScalebarGroupShadowModifier: ViewModifier { + func body(content: Content) -> some View { + content + .compositingGroup() + .shadow( + color: Scalebar.shadowColor, + radius: Scalebar.shadowRadius + ) + } +} + +extension View { + func scalebarShadow() -> some View { modifier( - ScalebarTextModifier(scalebar: scalebar) + ScalebarGroupShadowModifier() ) } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift index 302a67586..c41078c1a 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyle.swift @@ -13,10 +13,20 @@ import Foundation +/// Visual scalebar styles. public enum ScalebarStyle { - case line - case bar - case graduatedLine + /// Displays a single unit with segemeted bars of alternating fill color. case alternatingBar + + /// Displays a single unit. + case bar + + /// Displays both metric and imperial units. The primary unit is displayed on top. case dualUnitLine + + /// Displays a single unit with tick marks. + case graduatedLine + + /// Displays a single unit with endpoint tick marks. + case line } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index 3f5e5d11b..f5c0f8728 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -14,102 +14,70 @@ import SwiftUI extension Scalebar { + /// Renders all of the scalebar labels. + var allLabelsView: some View { + ZStack { + ForEach(viewModel.labels, id: \.index) { + Text($0.text) + .scalebarText() + .position( + x: $0.xOffset, + y: ScalebarLabel.yOffset + ) + } + } + .frame(height: Scalebar.fontHeight) + } + + /// Renders a scalebar with `ScalebarStyle.alternatingBar`. var alternatingBarStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { - HStack(spacing: -lineWidth) { + HStack(spacing: -Scalebar.lineWidth) { ForEach(viewModel.labels.dropFirst(), id: \.index) { Rectangle() - .fill($0.index % 2 == 0 ? fillColor1 : fillColor2) + .fill($0.index % 2 == 1 ? Scalebar.fillColor1 : Scalebar.fillColor2) .border( - lineColor, - width: lineWidth + Scalebar.lineColor, + width: Scalebar.lineWidth ) } } .frame( - height: barFrameHeight, + height: Scalebar.barFrameHeight, alignment: .leading ) - .compositingGroup() - .cornerRadius(lineCornerRadius) - .shadow( - color: shadowColor, - radius: shadowRadius - ) - ZStack { - ForEach(viewModel.labels, id: \.index) { - Text($0.text) - .scalebarText(self) - .position( - x: $0.xOffset, - y: ScalebarLabel.yOffset - ) - } - } + .cornerRadius(Scalebar.barCornerRadius) + .scalebarShadow() + allLabelsView } } + /// Renders a scalebar with `ScalebarStyle.bar`. var barStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { Rectangle() - .fill(fillColor2) + .fill(Scalebar.fillColor2) .border( - lineColor, - width: lineWidth + Scalebar.lineColor, + width: Scalebar.lineWidth ) .frame( - height: barFrameHeight, + height: Scalebar.barFrameHeight, alignment: .leading ) - .cornerRadius(lineCornerRadius) - .shadow( - color: shadowColor, - radius: shadowRadius - ) + .cornerRadius(Scalebar.barCornerRadius) + .scalebarShadow() Text(viewModel.labels.last?.text ?? "") - .scalebarText(self) - } - } - - var lineStyleRender: some View { - VStack(spacing: Scalebar.labelYPad) { - GeometryReader { geoProxy in - ZStack(alignment: .bottom) { - Path { path in - let zero = Double.zero - let maxX = geoProxy.size.width - let maxY = geoProxy.size.height - path.move(to: CGPoint(x: zero, y: zero)) - path.addLine(to: CGPoint(x: zero, y: maxY)) - path.addLine(to: CGPoint(x: maxX, y: maxY)) - path.addLine(to: CGPoint(x: maxX, y: zero)) - } - .stroke( - style: .init( - lineWidth: lineWidth, - lineCap: .round, - lineJoin: .round - ) - ) - .fill(lineColor) - } - .compositingGroup() - .shadow( - color: shadowColor, - radius: shadowRadius - ) - } - .frame(height: lineFrameHeight) - Text(viewModel.labels.last?.text ?? "") - .scalebarText(self) + .scalebarText() } } + /// Renders a scalebar with `ScalebarStyle.dualUnitLine`. var dualUnitLineStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { ZStack { Text(viewModel.labels.last?.text ?? "") - .scalebarText(self) + .scalebarText() .position( x: viewModel.labels.last?.xOffset ?? .zero, y: ScalebarLabel.yOffset @@ -143,23 +111,19 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: lineWidth, + lineWidth: Scalebar.lineWidth, lineCap: .round, lineJoin: .round ) ) - .fill(lineColor) + .fill(Scalebar.lineColor) } - .compositingGroup() - .shadow( - color: shadowColor, - radius: shadowRadius - ) + .scalebarShadow() } - .frame(height: barFrameHeight) + .frame(height: Scalebar.barFrameHeight) ZStack { Text(viewModel.alternateUnit.label) - .scalebarText(self) + .scalebarText() .position( x: viewModel.alternateUnit.screenLength, y: ScalebarLabel.yOffset @@ -169,6 +133,7 @@ extension Scalebar { } } + /// Renders a scalebar with `ScalebarStyle.graduatedLine`. var graduatedLineStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { GeometryReader { geoProxy in @@ -188,27 +153,48 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: lineWidth, + lineWidth: Scalebar.lineWidth, lineCap: .round, lineJoin: .round ) ) - .fill(lineColor) + .fill(Scalebar.lineColor) } - .compositingGroup() - .shadow( - color: shadowColor, - radius: shadowRadius - ) + .scalebarShadow() } - .frame(height: lineFrameHeight) - ZStack { - ForEach(viewModel.labels, id: \.index) { - Text($0.text) - .scalebarText(self) - .position(x: $0.xOffset, y: ScalebarLabel.yOffset) + .frame(height: Scalebar.lineFrameHeight) + allLabelsView + } + } + + /// Renders a scalebar with `ScalebarStyle.line`. + var lineStyleRender: some View { + VStack(spacing: Scalebar.labelYPad) { + GeometryReader { geoProxy in + ZStack(alignment: .bottom) { + Path { path in + let zero = Double.zero + let maxX = geoProxy.size.width + let maxY = geoProxy.size.height + path.move(to: CGPoint(x: zero, y: zero)) + path.addLine(to: CGPoint(x: zero, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: zero)) + } + .stroke( + style: .init( + lineWidth: Scalebar.lineWidth, + lineCap: .round, + lineJoin: .round + ) + ) + .fill(Scalebar.lineColor) } + .scalebarShadow() } + .frame(height: Scalebar.lineFrameHeight) + Text(viewModel.labels.last?.text ?? "") + .scalebarText() } } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index fc53173af..08a089e83 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -15,83 +15,52 @@ import ArcGIS import Foundation public enum ScalebarUnits { + /// Imperial units (feet, miles, etc) case imperial + + /// Metric units (meters, etc) case metric - internal func baseUnits() -> LinearUnit { - return self == .imperial ? LinearUnit.feet : LinearUnit.meters - } + /// Mulitplier options. + /// This table must begin with 1 and end with 10. + private static let roundNumberMultipliers: [Double] = + [1, 1.2, 1.25, 1.5, 1.75, 2, 2.4, 2.5, 3, 3.75, 4, 5, 6, 7.5, 8, 9, 10] - /// Get multiplier - private static func multiplierAndMagnitudeForDistance( - distance: Double) -> (multiplier: Double, magnitude: Double - ) { - let magnitude = pow(10, floor(log10(distance))) - let residual = distance / Double(magnitude) - let multiplier: Double = ScalebarUnits.roundNumberMultipliers.filter { $0 <= residual }.last ?? 0 - return (multiplier, magnitude) + /// Determines an appropriate base linear unit for this scalebar unit. + /// - Returns: `LinearUnit.feet` or `LinearUnit.meters` depending on this unit. + /// + /// `ScalebarUnits.imperial` will return `LinearUnit.feet` as feet is the smallest linear + /// unit that will be displayed. + /// `ScalebarUnits.metric` will return `LinearUnit.meters` as meter is the smallest linear + /// unit that will be displayed. + internal var baseLinearUnit: LinearUnit { + return self == .imperial ? LinearUnit.feet : LinearUnit.meters } - internal func closestDistanceWithoutGoingOver( - to distance: Double, - units: LinearUnit - ) -> Double { - let mm = ScalebarUnits.multiplierAndMagnitudeForDistance(distance: distance) - let roundNumber = mm.multiplier * mm.magnitude - - // Because feet and miles are not relationally multiples of 10 with - // each other, we have to convert to miles if we are dealing in miles. - if units == .feet { - let displayUnits = linearUnitsForDistance(distance: roundNumber) - if units != displayUnits { - let displayDistance = closestDistanceWithoutGoingOver( - to: units.convert( - to: displayUnits, - value: distance - ), - units: displayUnits - ) - return displayUnits.convert( - to: units, - value: displayDistance - ) - } - } - - return roundNumber + /// Calculates a magnitude for a given distance. + /// - Parameter distance: A distance to compute the magnitude for. + /// - Returns: A magnitude for a given distance. + /// + /// For example: + /// A distance of 25 will return 10 as 10 is the highest power of 10 that will fit into 25. + /// A distance of 550 will return 100 as 100 is the highest power of 10 that will fit into 550. + /// A distance of 2,222 will return 1000 as 1000 is the highest power of 10 that will fit into 2,222. + private static func magnitudeForDistance(distance: Double) -> Double { + return pow(10, floor(log10(distance))) } - /// This function returns the best number of segments so that we get relatively round numbers when the - /// distance is divided up. - internal static func numSegmentsForDistance( - distance: Double, - maxNumSegments: Int - ) -> Int { - let mm = multiplierAndMagnitudeForDistance(distance: distance) - let options = segmentOptionsForMultiplier(multiplier: mm.multiplier) - let num = options.filter { $0 <= maxNumSegments }.last ?? 1 - return num - } - - /// This table must begin with 1 and end with 10. - private static let roundNumberMultipliers: [Double] = - [1, 1.2, 1.25, 1.5, 1.75, 2, 2.4, 2.5, 3, 3.75, 4, 5, 6, 7.5, 8, 9, 10] - - internal func linearUnitsForDistance(distance: Double) -> LinearUnit { - switch self { - case .imperial: - if distance >= 2640 { - return LinearUnit.miles - } - return LinearUnit.feet - case .metric: - if distance >= 1000 { - return LinearUnit.kilometers - } - return LinearUnit.meters - } + /// Returns a multiplier for a given distance. + /// - Parameter distance: A distance to compute the multiplier for. + /// - Returns: A multiplier for a given distance. + private static func multiplierForDistance(distance: Double) -> Double { + let residual = distance / ScalebarUnits.magnitudeForDistance(distance: distance) + let multiplier = ScalebarUnits.roundNumberMultipliers.filter { $0 <= residual }.last ?? 0 + return multiplier } + /// Returns a list of segment options for a given multiplier. + /// - Parameter multiplier: A distance to compute the multiplier for. + /// - Returns: A list of segment options for a given multiplier. private static func segmentOptionsForMultiplier( multiplier: Double ) -> [Int] { @@ -134,4 +103,71 @@ public enum ScalebarUnits { return [1] } } + + /// - Returns: The best number of segments so that we get relatively round numbers when the + /// distance is divided up. + internal static func numSegmentsForDistance( + distance: Double, + maxNumSegments: Int + ) -> Int { + let multiplier = multiplierForDistance(distance: distance) + let options = segmentOptionsForMultiplier(multiplier: multiplier) + let num = options.filter { $0 <= maxNumSegments }.last ?? 1 + return num + } + + /// Calculates a round number suitable for display. + /// - Returns: A displayable round number. + internal func closestDistanceWithoutGoingOver( + to distance: Double, + units: LinearUnit + ) -> Double { + let magnitude = ScalebarUnits.magnitudeForDistance(distance: distance) + let multiplier = ScalebarUnits.multiplierForDistance(distance: distance) + let roundNumber = multiplier * magnitude + + // Because feet and miles are not relationally multiples of 10 with + // each other, we have to convert to miles if we are dealing in feet. + if units == .feet { + let displayUnits = linearUnitsForDistance(distance: roundNumber) + if units != displayUnits { + let displayDistance = closestDistanceWithoutGoingOver( + to: units.convert( + to: displayUnits, + value: distance + ), + units: displayUnits + ) + return displayUnits.convert( + to: units, + value: displayDistance + ) + } + } + + return roundNumber + } + + /// Determines a suitable display unit for the given distance. + /// - Parameter distance: The distance to be displayed. + /// - Returns: A suitable linear unit. + /// + /// `ScalebarUnits.imperial` will return `LinearUnit.miles` if the given distance is greater + /// than or equal to 1/2 mile, and `LinearUnit.feet` otherwise. + /// `ScalebarUnits.metric` will return `LinearUnit.kilometers` if the given distance is + /// greater than or equal to 1 kilometer, and `LinearUnit.meters` otherwise. + internal func linearUnitsForDistance(distance: Double) -> LinearUnit { + switch self { + case .imperial: + if distance >= 2640 { + return LinearUnit.miles + } + return LinearUnit.feet + case .metric: + if distance >= 1000 { + return LinearUnit.kilometers + } + return LinearUnit.meters + } + } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 71be34b7a..135f933db 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -18,91 +18,233 @@ import SwiftUI @MainActor final class ScalebarViewModel: ObservableObject { - @Published var displayLength: CGFloat = .zero + // - MARK: Published vars - @Published var displayLengthString = "" + /// The computed display length of the scalebar. + @Published var displayLength: CGFloat = .zero - @Published var displayUnit: LinearUnit? = nil + /// Indicates if the scalebar should be hidden or not. + @Published var isVisible: Bool + /// The current set of labels to be displayed by the scalebar. @Published var labels = [ScalebarLabel]() - @Published var lineMapLength: Double = .zero + // - MARK: Public vars - var visibleAreaSubject = PassthroughSubject() + /// A sreen length and displayable string for the equivalent length in the alternate unit. + var alternateUnit: (screenLength: CGFloat, label: String) { + guard let displayUnit = displayUnit else { + return (.zero, "") + } + let altUnit: ScalebarUnits = units == .imperial ? .metric : .imperial + let altMapBaseLength = displayUnit.convert( + to: altUnit.baseLinearUnit, + value: lineMapLength + ) + let altClosestBaseLength = altUnit.closestDistanceWithoutGoingOver( + to: altMapBaseLength, + units: altUnit.baseLinearUnit + ) + let altDisplayUnits = altUnit.linearUnitsForDistance( + distance: altClosestBaseLength + ) + let altMapLength = altUnit.baseLinearUnit.convert( + to: altDisplayUnits, value: altClosestBaseLength + ) + let displayFactor = lineMapLength / displayLength + let convertedDisplayFactor = displayUnit.convert( + to: altDisplayUnits, + value: displayFactor + ) + let altScreenLength = altMapLength / convertedDisplayFactor + let numberString = numberFormatter.string( + from: NSNumber(value: altMapLength) + ) ?? "" + let bottomUnitsText = " \(altDisplayUnits.abbreviation)" + let label = "\(numberString)\(bottomUnitsText)" + return (altScreenLength, label) + } + + /// A subject to which viewpoint updates can be submitted. + var viewpointSubject = PassthroughSubject() + + // - MARK: Public methods + + /// A scalebar view model controls the underlying data used to render a scalebar. + /// - Parameters: + /// - autoHide: Determines if the scalebar should automatically show & hide itself. + /// - minScale: A value of 0 indicates the scalebar segments should always recalculate. + /// - spatialReference: The map's spatial reference. + /// - style: The visual appearance of the scalebar. + /// - targetWidth: The screen width alloted to the scalebar. + /// - units: The units to be displayed in the scalebar. + /// - unitsPerPoint: The current number of device independent pixels to map display units. + /// - useGeodeticCalculations: Determines if a geodesic curve should be used to compute + /// the scale. + /// - viewpoint: The map's current viewpoint. + init( + _ autoHide: Bool, + _ minScale: Double, + _ spatialReference: SpatialReference?, + _ style: ScalebarStyle, + _ targetWidth: Double, + _ units: ScalebarUnits, + _ unitsPerPoint: Binding, + _ useGeodeticCalculations: Bool, + _ viewpoint: Viewpoint? + ) { + self.isVisible = autoHide ? false : true + self.minScale = minScale + self.spatialReference = spatialReference + self.style = style + self.targetWidth = targetWidth + self.units = units + self.unitsPerPoint = unitsPerPoint + self.useGeodeticCalculations = useGeodeticCalculations + self.viewpoint = viewpoint + + viewpointSubscription = viewpointSubject + .debounce(for: delay, scheduler: DispatchQueue.main) + .sink(receiveValue: { [weak self] in + guard let self = self else { + return + } + self.viewpoint = $0 + self.updateScaleDisplay() + if autoHide { + self.performVisibilityAnimation() + } + }) + + updateScaleDisplay() + } - /// Set a minScale if you only want the scalebar to appear when you reach a large enough scale maybe - /// something like 10_000_000. This could be useful because the scalebar is really only accurate for - /// the center of the map on smaller scales (when zoomed way out). A minScale of 0 means it will - /// always be visible - private let minScale: Double = 0 + // - MARK: Private constants /// The amount of time to wait between value calculations. - private var delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.05) + private let delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.05) + + /// The speed at which to animate `isVisible` to `true`. + private let displaySpeed = 3.0 + + /// The curve type to use when performing scale calculations. + private let geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic + + /// The time to wait in seconds before animating `isVisible` to `false`. + private let hideTimeInterval = 1.75 - private var geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic + /// A `minScale` of 0 means the scalebar segments will always recalculate. + private let minScale: Double - private var spatialReference: SpatialReference? = .wgs84 + /// Converts numbers into a readable format. + private let numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + numberFormatter.formatterBehavior = .behavior10_4 + numberFormatter.maximumFractionDigits = 2 + numberFormatter.minimumFractionDigits = 0 + return numberFormatter + }() + /// The map's spatial reference. + private let spatialReference: SpatialReference? + + /// The visual appearance of the scalebar. + private let style: ScalebarStyle + + // - MARK: Private vars + + /// The timer to determine when to autohide the scalebar. + private var autoHideTimer: Timer? + + /// Determines the amount of display space to use based on the scalebar style. + private var availableLineDisplayLength: CGFloat { + switch style { + case .alternatingBar, .dualUnitLine, .graduatedLine: + let unitDisplayWidth = max( + " mi".size(withAttributes: [.font: Scalebar.font.uiFont]).width, + " km".size(withAttributes: [.font: Scalebar.font.uiFont]).width + ) + return targetWidth - (Scalebar.lineWidth / 2.0) - unitDisplayWidth + case .bar, .line: + return targetWidth - Scalebar.lineWidth + } + } + + /// The units to be displayed in the scalebar. + private var displayUnit: LinearUnit? = nil + + /// The length of the line to display in map units. + private var lineMapLength: Double = .zero + + /// The maximum width allowed for the scalebar. private var targetWidth: Double /// Unit of measure in use. private var units: ScalebarUnits + /// The current number of device independent pixels to map display units. private var unitsPerPoint: Binding /// Allows a user to toggle geodetic calculations. private var useGeodeticCalculations: Bool /// Acts as a data provider of the current scale. - private var viewpoint: Binding + private var viewpoint: Viewpoint? - /// Acts as a data provider of the current scale. - private var visibleArea: Binding + /// A subscription to handle listening for viewpoint changes. + private var viewpointSubscription: AnyCancellable? - private var visibleAreaCancellable: AnyCancellable? + // - MARK: Private methods - init( - _ spatialReference: SpatialReference? = .wgs84, - _ targetWidth: Double, - _ units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, - _ unitsPerPoint: Binding, - _ useGeodeticCalculations: Bool = true, - _ viewpoint: Binding, - _ visibleArea: Binding - ) { - self.spatialReference = spatialReference - self.targetWidth = targetWidth - self.units = units - self.unitsPerPoint = unitsPerPoint - self.useGeodeticCalculations = useGeodeticCalculations - self.viewpoint = viewpoint - self.visibleArea = visibleArea - - visibleAreaCancellable = visibleAreaSubject - .debounce(for: delay, scheduler: DispatchQueue.main) - .sink(receiveValue: { [weak self] _ in - self?.updateScaleDisplay() - }) - - updateScaleDisplay() + /// Animates `isVisible` between `true` and `false` as necessary. + private func performVisibilityAnimation() { + self.autoHideTimer?.invalidate() + withAnimation(.easeInOut.speed(displaySpeed)) { + self.isVisible = true + } + self.autoHideTimer = Timer.scheduledTimer( + withTimeInterval: hideTimeInterval, + repeats: false, + block: { _ in + withAnimation { + self.isVisible = false + } + } + ) } + /// Updates the labels to be displayed by the scalebar. private func updateLabels() { let lineDisplayLength = displayLength // Use a string with at least a few characters in case the number string - // only has 1. - // The dividers will be decimal values and we want to make sure they all - // fit very basic hueristics. - let minSegmentTestString = (displayLengthString.count > 3) ? displayLengthString : "9.9" - // Use 1.5 because the last segment, the text is right justified insted - // of center, which makes it harder to squeeze text in. - let minSegmentWidth = (minSegmentTestString.size(withAttributes: [.font: Scalebar.font.uiFont]).width * 1.5) + (Scalebar.labelXPad * 2) - var maxNumSegments = Int(lineDisplayLength / minSegmentWidth) - maxNumSegments = min(maxNumSegments, 4) // cap it at 4 - let numSegments: Int = ScalebarUnits.numSegmentsForDistance(distance: lineMapLength, maxNumSegments: maxNumSegments) + // only has 1. The dividers will be decimal values and we want to make + // sure they all fit very basic hueristics. + let minSegmentTestString: String + if let longestString = labels.last?.text, longestString.count > 3 { + minSegmentTestString = longestString + } else { + minSegmentTestString = "9.9" + } + + // Use 1.5 because in the text is longer in the last label + let minSegmentWidth = + minSegmentTestString.size(withAttributes: [.font: Scalebar.font.uiFont]).width * 1.5 + + + Scalebar.labelXPad * 2 + + let suggestedNumSegments = Int(lineDisplayLength / minSegmentWidth) + + // Cap segments at 4 + let maxNumSegments = min(suggestedNumSegments, 4) - let segmentScreenLength: CGFloat = (lineDisplayLength / CGFloat(numSegments)) + let numSegments: Int = ScalebarUnits.numSegmentsForDistance( + distance: lineMapLength, + maxNumSegments: maxNumSegments + ) + + let segmentScreenLength = CGFloat(lineDisplayLength / CGFloat(numSegments)) var currSegmentX: CGFloat = 0 var labels = [ScalebarLabel]() @@ -118,7 +260,7 @@ final class ScalebarViewModel: ObservableObject { currSegmentX += segmentScreenLength let segmentMapLength = Double((segmentScreenLength * CGFloat(index + 1)) / lineDisplayLength) * lineMapLength - var segmentText = Scalebar.numberFormatter.string(from: NSNumber(value: segmentMapLength)) ?? "" + var segmentText = numberFormatter.string(from: NSNumber(value: segmentMapLength)) ?? "" if index == numSegments - 1, let displayUnit = displayUnit?.abbreviation { segmentText += " \(displayUnit)" } @@ -133,48 +275,41 @@ final class ScalebarViewModel: ObservableObject { self.labels = labels } + /// Updates the information necessary to render a scalebar based off the latest viewpoint and units per + /// point information. private func updateScaleDisplay() { - guard let scale = viewpoint.wrappedValue?.targetScale else { - return - } - guard minScale <= 0 || scale < minScale else { - return - } - guard let unitsPerPoint = unitsPerPoint.wrappedValue else { + guard let spatialReference = spatialReference, + let unitsPerPoint = unitsPerPoint.wrappedValue, + let viewpoint = viewpoint, + minScale <= 0 || viewpoint.targetScale < minScale else { return } - guard let visibleArea = visibleArea.wrappedValue else { - return - } - guard let sr = spatialReference else { - return - } - let totalWidthAvailable = targetWidth - // TODO: - Removal of hardcoded sample renderer property (~16 - 2) derived from sample renderer - let maxLength = totalWidthAvailable - 16.30243742465973 - 2 + let mapCenter = viewpoint.targetGeometry.extent.center + + let maxLength = availableLineDisplayLength let lineMapLength: Double let displayUnit: LinearUnit - let mapCenter = visibleArea.extent.center let displayLength: CGFloat - if useGeodeticCalculations || spatialReference?.unit is AngularUnit { + if useGeodeticCalculations || spatialReference.unit is AngularUnit { let maxLengthPlanar = unitsPerPoint * Double(maxLength) let p1 = Point( x: mapCenter.x - (maxLengthPlanar * 0.5), y: mapCenter.y, - spatialReference: sr + spatialReference: spatialReference ) let p2 = Point( x: mapCenter.x + (maxLengthPlanar * 0.5), y: mapCenter.y, - spatialReference: sr + spatialReference: spatialReference ) let polyline = Polyline( - points: [p1, p2], spatialReference: spatialReference + points: [p1, p2], + spatialReference: spatialReference ) - let baseUnits = units.baseUnits() + let baseUnits = units.baseLinearUnit let maxLengthGeodetic = GeometryEngine.geodeticLength( of: polyline, lengthUnit: baseUnits, @@ -189,11 +324,11 @@ final class ScalebarViewModel: ObservableObject { displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) lineMapLength = baseUnits.convert(to: displayUnit, value: roundNumberDistance) } else { - guard let srUnit = sr.unit as? LinearUnit else { + guard let srUnit = spatialReference.unit as? LinearUnit else { return } let unitsPerPoint = unitsPerPoint - let baseUnits = units.baseUnits() + let baseUnits = units.baseLinearUnit let lenAvail = srUnit.convert( to: baseUnits, value: unitsPerPoint * Double(maxLength) @@ -225,32 +360,6 @@ final class ScalebarViewModel: ObservableObject { self.lineMapLength = lineMapLength - displayLengthString = Scalebar.numberFormatter.string( - from: NSNumber(value: lineMapLength) - ) ?? "" - updateLabels() } - - var alternateUnit: (screenLength: CGFloat, label: String) { - guard let displayUnit = displayUnit else { - return (.zero, "") - } - let otherUnit = (units == ScalebarUnits.imperial) ? ScalebarUnits.metric : ScalebarUnits.imperial - let otherMapBaseLength = displayUnit.convert(to: otherUnit.baseUnits(), value: lineMapLength) - let otherClosestBaseLength = otherUnit.closestDistanceWithoutGoingOver(to: otherMapBaseLength, units: otherUnit.baseUnits()) - let otherDisplayUnits = otherUnit.linearUnitsForDistance(distance: otherClosestBaseLength) - let otherLineMapLength = otherUnit.baseUnits().convert(to: otherDisplayUnits, value: otherClosestBaseLength) - - let displayFactor = lineMapLength / Double(displayLength) - let convertedDisplayFactor = displayUnit.convert(to: otherDisplayUnits, value: displayFactor) - let otherLineScreenLength = CGFloat(otherLineMapLength / convertedDisplayFactor) - - let numberString = Scalebar.numberFormatter.string(from: NSNumber(value: otherLineMapLength)) ?? "" - let bottomUnitsText = " \(otherDisplayUnits.abbreviation)" - - let bottomText = "\(numberString)\(bottomUnitsText)" - - return (otherLineScreenLength, bottomText) - } } diff --git a/Tests/ArcGISToolkitTests/ScalebarTests.swift b/Tests/ArcGISToolkitTests/ScalebarTests.swift new file mode 100644 index 000000000..20172245c --- /dev/null +++ b/Tests/ArcGISToolkitTests/ScalebarTests.swift @@ -0,0 +1,203 @@ +// 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 +import Combine +import SwiftUI +import XCTest +@testable import ArcGISToolkit + +@MainActor +class ScalebarTests: XCTestCase { + /// Asserts that the scalebar view model correctly changes `isVisible`. + func testAutohide() { + var subscriptions = Set() + let expectation1 = expectation(description: "Went visible") + let expectation2 = expectation(description: "Went hidden") + var changedToVisible = false + let viewpoint1 = Viewpoint( + center: esriRedlands, + scale: 1 + ) + let viewpoint2 = Viewpoint( + center: esriRedlands, + scale: 2 + ) + let unitsPerPoint = unitsPerPointBinding(1) + let viewModel = scalebarViewModel( + autoHide: true, + unitsPerPoint: unitsPerPoint, + viewpoint: viewpoint1 + ) + viewModel.$isVisible.sink { isVisible in + if isVisible && !changedToVisible { + changedToVisible = true + expectation1.fulfill() + } else if !isVisible && changedToVisible { + expectation2.fulfill() + } + } + .store(in: &subscriptions) + viewModel.viewpointSubject.send(viewpoint2) + waitForExpectations(timeout: 5.0) + } + + /// Asserts that the scalebar view model provides the correct label at a scale of 10,000,000. + func testScale_10000000() { + let correctImperialLabel = "200 mi" + let correctMetricLabel = "300 km" + let viewpoint = Viewpoint( + center: esriRedlands, + scale: 10_000_000.00 + ) + let unitsPerPoint = unitsPerPointBinding(2645.833333330476) + let viewModel = scalebarViewModel( + unitsPerPoint: unitsPerPoint, + viewpoint: viewpoint + ) + let calculatedMetricLabel = viewModel.alternateUnit.label + if let calculatedImperialLabel = viewModel.labels.last?.text { + XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) + XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) + } else { + XCTFail() + } + } + + /// Asserts that the scalebar view model provides the correct label at a scale of 1,000,000. + func testScale_1000000() { + let correctImperialLabel = "20 mi" + let correctMetricLabel = "30 km" + let viewpoint = Viewpoint( + center: esriRedlands, + scale: 1_000_000.00 + ) + let unitsPerPoint = unitsPerPointBinding(264.58333333304756) + let viewModel = scalebarViewModel( + unitsPerPoint: unitsPerPoint, + viewpoint: viewpoint + ) + let calculatedMetricLabel = viewModel.alternateUnit.label + if let calculatedImperialLabel = viewModel.labels.last?.text { + XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) + XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) + } else { + XCTFail() + } + } + + /// Asserts that the scalebar view model provides the correct label at a scale of 100,000. + func testScale_100000() { + let correctImperialLabel = "2 mi" + let correctMetricLabel = "3 km" + let viewpoint = Viewpoint( + center: esriRedlands, + scale: 100_000.00 + ) + let unitsPerPoint = unitsPerPointBinding(26.458333333304758) + let viewModel = scalebarViewModel( + unitsPerPoint: unitsPerPoint, + viewpoint: viewpoint + ) + let calculatedMetricLabel = viewModel.alternateUnit.label + if let calculatedImperialLabel = viewModel.labels.last?.text { + XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) + XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) + } else { + XCTFail() + } + } + + /// Asserts that the scalebar view model provides the correct label at a scale of 10,000. + func testScale_10000() { + let correctImperialLabel = "1,000 ft" + let correctMetricLabel = "300 m" + let viewpoint = Viewpoint( + center: esriRedlands, + scale: 10_000.00 + ) + let unitsPerPoint = unitsPerPointBinding(2.6458333333304758) + let viewModel = scalebarViewModel( + unitsPerPoint: unitsPerPoint, + viewpoint: viewpoint + ) + let calculatedMetricLabel = viewModel.alternateUnit.label + if let calculatedImperialLabel = viewModel.labels.last?.text { + XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) + XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) + } else { + XCTFail() + } + } + + /// Asserts that the scalebar view model provides the correct label at a scale of 100. + func testScale_100() { + let correctImperialLabel = "10 ft" + let correctMetricLabel = "3 m" + let viewpoint = Viewpoint( + center: esriRedlands, + scale: 100.00 + ) + let unitsPerPoint = unitsPerPointBinding(0.02645833333330476) + let viewModel = scalebarViewModel( + unitsPerPoint: unitsPerPoint, + viewpoint: viewpoint + ) + let calculatedMetricLabel = viewModel.alternateUnit.label + if let calculatedImperialLabel = viewModel.labels.last?.text { + XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) + XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) + } else { + XCTFail() + } + } +} + +extension ScalebarTests { + /// Web mercator coordinates for Esri's Redlands campus + var esriRedlands: Point { + return Point( + x: -13046081.04434825, + y: 4036489.208008117, + spatialReference: .webMercator + ) + } + + /// Generates a binding to a provided units per point value. + func unitsPerPointBinding(_ value: Double) -> Binding { + var _value = value + return Binding( + get: { _value }, + set: { _value = $0 ?? .zero } + ) + } + + /// Generates a new scalebar view model. + func scalebarViewModel( + autoHide: Bool = false, + unitsPerPoint: Binding, + viewpoint: Viewpoint? + ) -> ScalebarViewModel { + return ScalebarViewModel( + autoHide, + 0, + .webMercator, + .alternatingBar, + 175, + .imperial, + unitsPerPoint, + true, + viewpoint + ) + } +} From 1efc18576864871bba8ae00b1133ea7f91362107 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 7 Apr 2022 11:04:00 -0500 Subject: [PATCH 0758/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: David Feinzimer --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 4e5870b69..0a5aecc63 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -89,7 +89,10 @@ public struct OverviewMap: View { ) .attributionText(hidden: true) .interactionModes([]) - .border(.black, width: 1) + .border( + .black, + width: 1 + ) .onAppear { graphic.symbol = symbol } From 68632e5bf22803587632fb36b38ff9a16dd29fc1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 7 Apr 2022 16:07:57 -0500 Subject: [PATCH 0759/1722] Update Sources/ArcGISToolkit/Components/OverviewMap.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 0a5aecc63..cb1f67b81 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -117,8 +117,7 @@ public struct OverviewMap: View { /// - Returns: The new `Viewpoint`. func makeOverviewViewpoint() -> Viewpoint? { guard let viewpoint = viewpoint, - let center = viewpoint.targetGeometry as? Point - else { return nil } + let center = viewpoint.targetGeometry as? Point else { return nil } return Viewpoint( center: center, From 02fe59de97aed5c03d4f7aad2bfadd9c7409831a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 8 Apr 2022 10:26:50 -0500 Subject: [PATCH 0760/1722] Make model internal; add view constructor. --- .../Components/Search/SearchView.swift | 41 ++++++++++-------- .../Components/Search/SearchViewModel.swift | 42 +++++++++---------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 71ea823b0..dc63ef9d8 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -16,21 +16,34 @@ import ArcGIS /// `SearchView` presents a search experience, powered by an underlying `SearchViewModel`. public struct SearchView: View { - /// Creates a new `SearchView`. + /// Creates a `SearchView`. /// - Parameters: - /// - searchViewModel: The view model used by `SearchView`. - public init(searchViewModel: SearchViewModel? = nil) { - self.searchViewModel = searchViewModel ?? SearchViewModel( - sources: [LocatorSearchSource()] + /// - queryArea: The search area to be used for the current query. + /// - queryCenter: Defines the center for the search. + /// - resultMode: Defines how many results to return. + /// - sources: Collection of search sources to be used. + init( + queryArea: Geometry? = nil, + queryCenter: Point? = nil, + resultMode: SearchResultMode = .automatic, + sources: [SearchSource] = [] + ) { + searchViewModel = SearchViewModel( + queryArea: queryArea, + queryCenter: queryCenter, + resultMode: resultMode, + sources: [sources.isEmpty ? [LocatorSearchSource()] : sources] ) } - + /// The view model used by the view. The `SearchViewModel` manages state and handles the /// activity of searching. The view observes `SearchViewModel` for changes in state. The view /// calls methods on `SearchViewModel` in response to user action. - @ObservedObject - var searchViewModel: SearchViewModel + @ObservedObject var searchViewModel: SearchViewModel + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass + /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". Note: this is set using the /// `prompt` modifier. @@ -46,13 +59,7 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. Defaults to "No results found". /// Note: this is set using the `noResultsMessage` modifier. private var noResultsMessage = "No results found" - - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass: UserInterfaceSizeClass? - - @Environment(\.verticalSizeClass) - private var verticalSizeClass: UserInterfaceSizeClass? - + /// The width of the search bar, taking into account the horizontal and vertical size classes /// of the device. This will cause the search field to display full-width on an iPhone in portrait /// orientation (and certain iPad multitasking configurations) and limit the width to `360` in other cases. @@ -68,8 +75,7 @@ public struct SearchView: View { } /// Determines whether the results lists are displayed. - @State - private var isResultListHidden: Bool = false + @State private var isResultListHidden: Bool = false public var body: some View { VStack { @@ -254,6 +260,7 @@ struct ResultRow: View { if !subtitle.isEmpty { Text(subtitle) .font(.caption) + .foregroundColor(.secondary) } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 17aabf8da..a31aa1fe0 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -16,9 +16,9 @@ import ArcGIS /// Performs searches and manages search state for a search, or optionally without a UI connection. @MainActor -public class SearchViewModel: ObservableObject { +class SearchViewModel: ObservableObject { /// Defines how many results to return; one, many, or automatic based on circumstance. - public enum SearchResultMode { + enum SearchResultMode { /// Search should always result in at most one result. case single /// Search should always try to return multiple results. @@ -34,7 +34,7 @@ public class SearchViewModel: ObservableObject { /// /// The `failure` case contains the error (if any) generated by the last search or suggestion /// operation. - public enum SearchOutcome { + enum SearchOutcome { case results([SearchResult]) case suggestions([SearchSuggestion]) case failure(String) @@ -46,7 +46,7 @@ public class SearchViewModel: ObservableObject { /// - queryCenter: Defines the center for the search. /// - resultMode: Defines how many results to return. /// - sources: Collection of search sources to be used. - public init( + init( queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, @@ -63,7 +63,7 @@ public class SearchViewModel: ObservableObject { /// Tracks the current user-entered query. This property drives both suggestions and searches. @Published - public var currentQuery: String = "" { + var currentQuery: String = "" { willSet { isEligibleForRequery = false @@ -91,7 +91,7 @@ public class SearchViewModel: ObservableObject { /// This should be updated as the user navigates the map/scene. It will be /// used to determine the value of `isEligibleForRequery` for the 'Repeat /// search here' behavior. If that behavior is not wanted, it should be left `nil`. - public var geoViewExtent: Envelope? = nil { + var geoViewExtent: Envelope? = nil { willSet { guard isGeoViewNavigating, !isEligibleForRequery, @@ -124,13 +124,13 @@ public class SearchViewModel: ObservableObject { } /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. - public var isGeoViewNavigating: Bool = false + var isGeoViewNavigating: Bool = false /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. - public var viewpoint: Binding? = nil + var viewpoint: Binding? = nil /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. - public var resultsOverlay: GraphicsOverlay? = nil + var resultsOverlay: GraphicsOverlay? = nil /// If `true`, will set the viewpoint to the extent of the results, plus a little buffer, which will /// cause the geoView to zoom to the extent of the results. If `false`, @@ -145,24 +145,24 @@ public class SearchViewModel: ObservableObject { /// `true` if the extent center changes by more than 25% of the average of the extent's height and width /// at the time of the last search or if the extent width/height changes by the same amount. @Published - public private(set) var isEligibleForRequery: Bool = false + private(set) var isEligibleForRequery: Bool = false /// The search area to be used for the current query. Results will be limited to those /// within `QueryArea`. Defaults to `nil`. - public var queryArea: Geometry? = nil + var queryArea: Geometry? = nil /// Defines the center for the search. For most use cases, this should be updated by the view /// every time the user navigates the map. - public var queryCenter: Point? + var queryCenter: Point? /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate /// number of results is returned based on the type of suggestion chosen /// (driven by the `isCollection` property). - public var resultMode: SearchResultMode = .automatic + var resultMode: SearchResultMode = .automatic /// The collection of search and suggestion results. A `nil` value means no query has been made. @Published - public private(set) var searchOutcome: SearchOutcome? { + private(set) var searchOutcome: SearchOutcome? { didSet { if case let .results(results) = searchOutcome { display(searchResults: results) @@ -179,7 +179,7 @@ public class SearchViewModel: ObservableObject { /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. @Published - public var selectedResult: SearchResult? { + var selectedResult: SearchResult? { willSet { (selectedResult?.geoElement as? Graphic)?.isSelected = false } @@ -193,7 +193,7 @@ public class SearchViewModel: ObservableObject { /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. /// NOTE: Only the first source is currently used; multiple sources are not yet supported. - public var sources: [SearchSource] = [] + var sources: [SearchSource] = [] /// The currently executing async task. `currentTask` will be cancelled /// prior to starting another async task. @@ -205,17 +205,17 @@ public class SearchViewModel: ObservableObject { /// Starts a search. `selectedResult` and `results`, among other properties, are set /// asynchronously. Other query properties are read to define the parameters of the search. - public func commitSearch() { + func commitSearch() { currentTask = Task { await self.doSearch() } } /// Repeats the last search, limiting results to the extent specified in `geoViewExtent`. - public func repeatSearch() { + func repeatSearch() { currentTask = Task { await self.doRepeatSearch() } } /// Updates suggestions list asynchronously. - public func updateSuggestions() { + func updateSuggestions() { guard currentSuggestion == nil else { // Don't update suggestions if currently searching for one. @@ -227,7 +227,7 @@ public class SearchViewModel: ObservableObject { /// The suggestion currently selected by the user. @Published - public var currentSuggestion: SearchSuggestion? { + var currentSuggestion: SearchSuggestion? { didSet { if let currentSuggestion = currentSuggestion { acceptSuggestion(currentSuggestion) @@ -239,7 +239,7 @@ public class SearchViewModel: ObservableObject { /// generally the same as `commitSearch`, except `searchSuggestion` is used instead of the /// `currentQuery` property. /// - Parameter searchSuggestion: The suggestion to use to commit the search. - public func acceptSuggestion(_ searchSuggestion: SearchSuggestion) { + func acceptSuggestion(_ searchSuggestion: SearchSuggestion) { currentQuery = searchSuggestion.displayTitle currentTask = Task { await self.doAcceptSuggestion(searchSuggestion) } } From 705cec409474e6d54820c146c9acd6d6350a8e19 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 7 Apr 2022 16:01:16 -0700 Subject: [PATCH 0761/1722] Revert to a single point of selection --- .../Components/FloorFilter/FloorFilter.swift | 21 +- .../FloorFilter/FloorFilterViewModel.swift | 290 ++++++++++++------ .../FloorFilter/LevelSelector.swift | 40 +-- .../FloorFilter/SiteAndFacilitySelector.swift | 79 +++-- .../FloorFilterViewModelTests.swift | 164 +++++----- 5 files changed, 361 insertions(+), 233 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index dc289fce0..839ec5424 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -61,24 +61,17 @@ public struct FloorFilter: View { .frame(maxWidth: .infinity) } - /// 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. - private var hasLevelsToDisplay: Bool { - !(viewModel.selectedFacility == nil || - viewModel.selectedFacility!.levels.isEmpty) - } - /// Displays the available levels. private var levelsSelectorView: some View { Group { - if hasLevelsToDisplay { + if viewModel.hasLevelsToDisplay { if topAligned { Divider() .frame(width: 30) } LevelSelector( isCollapsed: $isLevelsViewCollapsed, - levels: sortedLevels, + levels: viewModel.sortedLevels, topAligned: topAligned ) if !topAligned { @@ -128,18 +121,10 @@ public struct FloorFilter: View { .esriBorder() .opacity(isSelectorHidden ? .zero : 1) .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in - viewModel.updateSelection() + viewModel.viewpointSubject.send(viewpoint.wrappedValue) } } - /// The selected facility's levels, sorted by `level.verticalOrder`. - private var sortedLevels: [FloorLevel] { - let levels = viewModel.selectedFacility?.levels ?? [] - return levels.sorted { - $0.verticalOrder > $1.verticalOrder - } - } - /// Indicates that the selector should be presented with a top oriented aligment configuration. private var topAligned: Bool { switch alignment { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 3827aa7f6..db42e4dff 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -11,12 +11,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI import ArcGIS +import Combine +import SwiftUI /// Manages the state for a `FloorFilter`. @MainActor final class FloorFilterViewModel: ObservableObject { + /// A selected site, floor, or level. + enum Selection { + /// 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. @@ -31,13 +41,24 @@ final class FloorFilterViewModel: ObservableObject { self.floorManager = floorManager self.viewpoint = viewpoint + viewpointSubscription = viewpointSubject + .debounce(for: delay, scheduler: DispatchQueue.main) + .sink(receiveValue: { [weak self] _ in + guard let self = self, + let viewpoint = self.viewpoint.wrappedValue, + !viewpoint.targetScale.isZero else { + return + } + self.makeAutoSelection() + }) + Task { do { try await floorManager.load() if sites.count == 1, let firstSite = sites.first { // If we have only one site, select it. - setSite(firstSite, zoomTo: true) + setSite(firstSite) } } catch { print("error: \(error)") @@ -48,27 +69,35 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Published members - /// The currently selected `FloorFacility`. - @Published - var selectedFacility: FloorFacility? - - /// The currently selected `FloorLevel`. - @Published - var selectedLevel: FloorLevel? - - /// The currently selected `FloorSite`. - @Published - var selectedSite: FloorSite? - /// `true` if the model is loading it's properties, `false` if not loading. - @Published - private(set) var isLoading = true + @Published private(set) var isLoading = true + + /// The selected site, floor, or level. + @Published var selection: Selection? { + didSet { + if case let .level(oldLevel) = oldValue, + case let .facility(facility) = selection, + let newLevel = facility.levels.first(where: { level in + level.verticalOrder == oldLevel.verticalOrder + }) { + selection = .level(newLevel) + } else if case let .facility(facility) = selection, + let level = defaultLevel(for: facility) { + selection = .level(level) + } else { + filterMapToSelectedLevel() + } + } + } // MARK: Constants /// The selection behavior of the floor filter. private let automaticSelectionMode: AutomaticSelectionMode + /// The amount of time to wait between selection updates. + private let delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.20) + /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager @@ -79,20 +108,77 @@ final class FloorFilterViewModel: ObservableObject { floorManager.facilities } + /// 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 { + guard let selectedFacility = selectedFacility else { + return false + } + return !selectedFacility.levels.isEmpty + } + /// The floor manager levels. var levels: [FloorLevel] { 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 .site: + return nil + 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 } + /// The selected facility's levels, sorted by `level.verticalOrder`. + var sortedLevels: [FloorLevel] { + let levels = selectedFacility?.levels ?? [] + return levels.sorted { + $0.verticalOrder > $1.verticalOrder + } + } + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. var viewpoint: Binding + /// A subject to which viewpoint updates can be submitted. + var viewpointSubject = PassthroughSubject() + // MARK: Public methods /// Gets the default level for a facility. @@ -102,85 +188,62 @@ final class FloorFilterViewModel: ObservableObject { func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { return levels.first(where: { level in level.facility == facility && level.verticalOrder == .zero - }) ?? lowestLevel() + }) ?? nil } /// Updates the selected site, facility, and level based on a newly selected facility. - /// - Parameters: - /// - floorFacility: The selected facility. - /// - zoomTo: The viewpoint should be updated to show to the extent of this facility. - func setFacility( - _ floorFacility: FloorFacility?, - zoomTo: Bool = false - ) { - selectedFacility = floorFacility - if let currentVerticalOrder = selectedLevel?.verticalOrder, - let newLevel = floorFacility?.levels.first(where: { level in - level.verticalOrder == currentVerticalOrder - }) { - setLevel(newLevel) - } else { - setLevel(defaultLevel(for: floorFacility)) - } - if zoomTo { - zoomToExtent(extent: floorFacility?.geometry?.extent) + /// - Parameter floorFacility: The selected facility. + func setFacility(_ floorFacility: FloorFacility?) { + if let floorFacility = floorFacility { + selection = .facility(floorFacility) } + zoomToSelection() } /// Updates the selected site, facility, and level based on a newly selected level. /// - Parameter floorLevel: The selected level. func setLevel(_ floorLevel: FloorLevel?) { - selectedLevel = floorLevel - filterMapToSelectedLevel() + if let floorLevel = floorLevel { + selection = .level(floorLevel) + } } /// Updates the selected site, facility, and level based on a newly selected site. - /// - Parameters: - /// - floorSite: The selected site. - /// - zoomTo: The viewpoint should be updated to show to the extent of this site. - func setSite( - _ floorSite: FloorSite?, - zoomTo: Bool = false - ) { - selectedSite = floorSite - if zoomTo { - zoomToExtent(extent: floorSite?.geometry?.extent) + /// - Parameter floorSite: The selected site. + func setSite(_ floorSite: FloorSite?) { + if let floorSite = floorSite { + selection = .site(floorSite) } + zoomToSelection() } - /// Updates `selectedSite` and `selectedFacility` based on the latest viewpoint position. - func updateSelection() { - guard let viewpoint = viewpoint.wrappedValue, - !viewpoint.targetScale.isZero, - automaticSelectionMode != .never else { - return + // MARK: Private items + + /// 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. + internal func makeAutoSelection() { + guard automaticSelectionMode != .never else { + return + } + if !autoSelectFacility() { + autoSelectSite() } - updateSelectedSite() - updateSelectedFacility() } - // MARK: Private methods - /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level. private func filterMapToSelectedLevel() { - guard let selectedLevel = selectedLevel else { return } - levels.forEach { - $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder - } - } - - /// Returns the level with the lowest vertical order. - private func lowestLevel() -> FloorLevel? { - let sortedLevels = levels.sorted { - $0.verticalOrder < $1.verticalOrder - } - return sortedLevels.first { - $0.verticalOrder != .min && $0.verticalOrder != .max + if let selectedLevel = selectedLevel { + levels.forEach { + $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder + } } } /// Updates `selectedFacility` if a good selection exists. - private func updateSelectedFacility() { + /// - Returns: `false` if a selection was not made. + @discardableResult + private func autoSelectFacility() -> Bool { // Only select a facility if it is within minimum scale. Default at 1500. let facilityMinScale: Double if let minScale = floorManager.facilityLayer?.minScale, @@ -191,7 +254,7 @@ final class FloorFilterViewModel: ObservableObject { } if viewpoint.wrappedValue?.targetScale ?? .zero > facilityMinScale { - return + return false } // If the centerpoint is within a facilities' geometry, select that site. @@ -204,14 +267,17 @@ final class FloorFilterViewModel: ObservableObject { } if let facilityResult = facilityResult { - setFacility(facilityResult) + selection = .facility(facilityResult) } else if automaticSelectionMode == .always { - setFacility(nil) + return false } + return true } /// Updates `selectedSite` if a good selection exists. - private func updateSelectedSite() { + /// - Returns: `false` if a selection was not made. + @discardableResult + private func autoSelectSite() -> Bool { // Only select a facility if it is within minimum scale. Default at 4300. let siteMinScale: Double if let minScale = floorManager.siteLayer?.minScale, @@ -224,11 +290,9 @@ final class FloorFilterViewModel: ObservableObject { // If viewpoint is out of range, reset selection and return early. if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { if automaticSelectionMode == .always { - setSite(nil) - setFacility(nil) - setLevel(nil) + selection = nil } - return + return false } // If the centerpoint is within a site's geometry, select that site. @@ -241,19 +305,20 @@ final class FloorFilterViewModel: ObservableObject { } if let siteResult = siteResult { - setSite(siteResult) + selection = .site(siteResult) } else if automaticSelectionMode == .always { - setSite(nil) + selection = nil } + return true } - /// Updates the viewpoint to display a given extent. - /// - Parameter extent: The new extent to be shown. - private func zoomToExtent(extent: Envelope?) { + /// Zoom to given extent. + private func zoomToExtent(_ extent: Envelope?) { // Make sure we have an extent and viewpoint to zoom to. guard let extent = extent else { return } + let builder = EnvelopeBuilder(envelope: extent) builder.expand(factor: 1.5) let targetExtent = builder.toGeometry() @@ -263,13 +328,23 @@ final class FloorFilterViewModel: ObservableObject { ) } } -} - -extension FloorSite: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - hasher.combine(self.name) + + /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. + private func zoomToSelection() { + switch selection { + case .site(let site): + zoomToExtent(site.geometry?.extent) + case .facility(let facility): + zoomToExtent(facility.geometry?.extent) + case .level(let level): + zoomToExtent(level.facility?.geometry?.extent) + default: + break + } } + + /// A subscription to handle listening for viewpoint changes. + private var viewpointSubscription: AnyCancellable? } /// Defines automatic selection behavior. @@ -283,3 +358,38 @@ public enum AutomaticSelectionMode { /// Never update selection based on the GeoView's current viewpoint. case never } + +extension FloorSite: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + hasher.combine(self.name) + } +} + +extension FloorFilterViewModel.Selection: Hashable { + static func == ( + lhs: FloorFilterViewModel.Selection, + rhs: FloorFilterViewModel.Selection + ) -> Bool { + switch (lhs, rhs) { + case (.site(let a), .site(let b)): + return a.id == b.id + case (.facility(let a), .facility(let b)): + return a.id == b.id + case (.level(let a), .level(let b)): + return a.id == b.id + default: return false + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .site(let site): + hasher.combine(site.id) + case.facility(let facility): + hasher.combine(facility.id) + case .level(let level): + hasher.combine(level.id) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 3b8e6a036..543ad0df0 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -18,20 +18,20 @@ import ArcGIS struct LevelSelector: View { /// The view model used by the `LevelsView`. @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. @Binding var isCollapsed: Bool - + /// The height of the scroll view's content. @State private var scrollViewContentHeight: CGFloat = .zero - + /// The levels to display. let levels: [FloorLevel] - + /// Returns the short name of the currently selected level, the first level or "None" if none of the listed /// are available. private var selectedLevelName: String { @@ -43,10 +43,10 @@ struct LevelSelector: View { return "None" } } - + /// The alignment configuration. var topAligned: Bool - + public var body: some View { VStack { if !isCollapsed, @@ -60,9 +60,9 @@ struct LevelSelector: View { VStack { LevelsStack(levels: levels) } - .onSizeChange { - scrollViewContentHeight = $0.height - } + .onSizeChange { + scrollViewContentHeight = $0.height + } } .frame(maxHeight: scrollViewContentHeight) if topAligned { @@ -79,7 +79,7 @@ struct LevelSelector: View { Text(selectedLevelName) .lineLimit(1) } - .selected(true) + .selected(true) } } } @@ -89,10 +89,10 @@ struct LevelSelector: View { struct LevelsStack: View { /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel - + /// The levels to display. let levels: [FloorLevel] - + var body: some View { VStack { ForEach(levels) { level in @@ -102,7 +102,7 @@ struct LevelsStack: View { Text(level.shortName) .lineLimit(1) } - .selected(level == viewModel.selectedLevel) + .selected(level == viewModel.selectedLevel) } } } @@ -113,7 +113,7 @@ struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. @Binding var isCollapsed: Bool - + var body: some View { Button { withAnimation { @@ -122,11 +122,11 @@ struct CollapseButton: View { } label: { Image(systemName: "xmark") } - .padding(EdgeInsets( - top: 2, - leading: 4, - bottom: 2, - trailing: 4 - )) + .padding(EdgeInsets( + top: 2, + leading: 4, + bottom: 2, + trailing: 4 + )) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 4d8fed46e..1fd8c3aa3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -48,13 +48,28 @@ struct SiteAndFacilitySelector: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel + /// Indicates whether the view model should be notified of the selection update. + @State private var updateViewModel = true + /// Indicates that the keyboard is animating and some views may require reload. - @State - private var keyboardAnimating = false + @State private var keyboardAnimating = false /// A site name filter phrase entered by the user. - @State - private var searchPhrase: String = "" + @State private var searchPhrase: String = "" + + /// A local record of the site selected in the view model. + /// + /// As the view model's selection will change to `.facility(FloorFacility)` and + /// `.level(FloorLevel)` over time, this is needed to keep track of the site at the top of the + /// hierarchy to keep the site selection persistent in the navigation view. + @State private var selectedSite: FloorSite? { + didSet { + if updateViewModel { + viewModel.setSite(selectedSite) + } + updateViewModel = true + } + } /// Sites contained in a `FloorManager`. let sites: [FloorSite] @@ -120,13 +135,13 @@ struct SiteAndFacilitySelector: View { } .padding([.vertical], 4) } - .navigationBarTitle( - Text("Select a site"), - displayMode: .inline - ) - .navigationBarItems(trailing: - CloseButton { isHidden.wrappedValue.toggle() } - ) + .navigationBarTitle( + Text("Select a site"), + displayMode: .inline + ) + .navigationBarItems(trailing: + CloseButton { isHidden.wrappedValue.toggle() } + ) } } @@ -139,7 +154,7 @@ struct SiteAndFacilitySelector: View { NavigationLink( site.name, tag: site, - selection: $viewModel.selectedSite + selection: $selectedSite ) { FacilitiesList( facilities: site.facilities, @@ -147,14 +162,18 @@ struct SiteAndFacilitySelector: View { isHidden: isHidden ) } - .onTapGesture { - viewModel.setSite( - site, - zoomTo: true - ) - } + .onTapGesture { + viewModel.setSite(site) + } + } + .listStyle(.plain) + .onChange(of: $viewModel.selection.wrappedValue) { _ in + // Setting the `updateViewModel` flag false allows + // `selectedSite` to receive upstream updates from the view + // model without republishing them back up to the view model. + updateViewModel = false + selectedSite = viewModel.selectedSite } - .listStyle(.plain) } } @@ -260,10 +279,7 @@ struct SiteAndFacilitySelector: View { ScrollViewReader { proxy in List(matchingFacilities, id: \.id) { facility in Button { - viewModel.setFacility( - facility, - zoomTo: true - ) + viewModel.setFacility(facility) isHidden.wrappedValue.toggle() } label: { HStack { @@ -289,15 +305,14 @@ struct SiteAndFacilitySelector: View { } } .listStyle(.plain) - .onChange(of: viewModel.selectedFacility) { - guard let facility = $0 else { - return - } - withAnimation { - proxy.scrollTo( - facility.id, - anchor: .center - ) + .onChange(of: viewModel.selection) { _ in + if let floorFacility = viewModel.selectedFacility { + withAnimation { + proxy.scrollTo( + floorFacility.id, + anchor: .center + ) + } } } } diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index abe9cb83d..144c44188 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -17,33 +17,35 @@ import SwiftUI import XCTest @testable import ArcGISToolkit +@MainActor class FloorFilterViewModelTests: XCTestCase { /// Applies credentials necessary to run tests. override func setUp() async throws { await addCredentials() } - + /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` func testInitFloorFilterViewModelWithFloorManager() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } + try? await floorManager.load() var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - let sites = await viewModel.sites - let facilities = await viewModel.facilities - let levels = await viewModel.levels + let sites = viewModel.sites + let facilities = viewModel.facilities + let levels = viewModel.levels XCTAssertFalse(sites.isEmpty) XCTAssertFalse(facilities.isEmpty) XCTAssertFalse(levels.isEmpty) } - + /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager` and /// `Binding?`.` func testInitFloorFilterViewModelWithFloorManagerAndViewpoint() async { @@ -51,42 +53,47 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } + try? await floorManager.load() var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - let sites = await viewModel.sites - let facilities = await viewModel.facilities - let levels = await viewModel.levels - let vmViewpoint = await viewModel.viewpoint + let sites = viewModel.sites + let facilities = viewModel.facilities + let levels = viewModel.levels + let vmViewpoint = viewModel.viewpoint XCTAssertFalse(sites.isEmpty) XCTAssertFalse(facilities.isEmpty) XCTAssertFalse(levels.isEmpty) XCTAssertNotNil(vmViewpoint) } - + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testSetSite() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } + try? await floorManager.load() let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - let site = await viewModel.sites.first - await viewModel.setSite(site) - let selectedSite = await viewModel.selectedSite - let selectedFacility = await viewModel.selectedFacility - let selectedLevel = await viewModel.selectedLevel + guard let site = viewModel.sites.first else { + XCTFail() + return + } + viewModel.selection = .site(site) + let selectedSite = viewModel.selectedSite + let selectedFacility = viewModel.selectedFacility + let selectedLevel = viewModel.selectedLevel XCTAssertEqual(selectedSite, site) XCTAssertNil(selectedFacility) XCTAssertNil(selectedLevel) @@ -94,64 +101,69 @@ class FloorFilterViewModelTests: XCTestCase { _viewpoint?.targetGeometry.extent.center.x, initialViewpoint.targetGeometry.extent.center.x ) - await viewModel.setSite(site, zoomTo: true) + viewModel.setSite(site) XCTAssertEqual( _viewpoint?.targetGeometry.extent.center.x, selectedSite?.geometry?.extent.center.x ) } - + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testSetFacility() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } + try? await floorManager.load() let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - let facility = await viewModel.facilities.first - await viewModel.setFacility(facility) - let selectedFacility = await viewModel.selectedFacility - let selectedLevel = await viewModel.selectedLevel - let defaultLevel = await viewModel.defaultLevel(for: selectedFacility) + guard let facility = viewModel.facilities.first else { + XCTFail() + return + } + viewModel.selection = .facility(facility) + let selectedFacility = viewModel.selectedFacility + let selectedLevel = viewModel.selectedLevel + let defaultLevel = viewModel.defaultLevel(for: selectedFacility) XCTAssertEqual(selectedFacility, facility) XCTAssertEqual(selectedLevel, defaultLevel) XCTAssertEqual( _viewpoint?.targetGeometry.extent.center.x, initialViewpoint.targetGeometry.extent.center.x ) - await viewModel.setFacility(facility, zoomTo: true) + viewModel.setFacility(facility) XCTAssertEqual( _viewpoint?.targetGeometry.extent.center.x, selectedFacility?.geometry?.extent.center.x ) } - + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testSetLevel() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } + try? await floorManager.load() let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - let levels = await viewModel.levels + let levels = viewModel.levels let level = levels.first - await viewModel.setLevel(level) - let selectedLevel = await viewModel.selectedLevel + viewModel.setLevel(level) + let selectedLevel = viewModel.selectedLevel XCTAssertEqual(selectedLevel, level) XCTAssertEqual( _viewpoint?.targetGeometry.extent.center.x, @@ -165,13 +177,14 @@ class FloorFilterViewModelTests: XCTestCase { } } } - + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testAutoSelectAlways() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } + try? await floorManager.load() let viewpointLosAngeles = Viewpoint( center: Point( x: -13164116.3284, @@ -182,42 +195,43 @@ class FloorFilterViewModelTests: XCTestCase { ) var _viewpoint: Viewpoint? = viewpointLosAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( automaticSelectionMode: .always, floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - + // Viewpoint is Los Angeles, selection should be nil - var selectedFacility = await viewModel.selectedFacility - var selectedSite = await viewModel.selectedSite + var selectedFacility = viewModel.selectedFacility + var selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - + // Viewpoint is Redlands Main Q _viewpoint = getEsriRedlandsViewpoint(scale: 1000) - await viewModel.updateSelection() - selectedFacility = await viewModel.selectedFacility - selectedSite = await viewModel.selectedSite + viewModel.makeAutoSelection() + selectedFacility = viewModel.selectedFacility + selectedSite = viewModel.selectedSite XCTAssertEqual(selectedSite?.name, "Redlands Main") XCTAssertEqual(selectedFacility?.name, "Q") - + // Viewpoint is Los Angeles, selection should be nil _viewpoint = viewpointLosAngeles - await viewModel.updateSelection() - selectedFacility = await viewModel.selectedFacility - selectedSite = await viewModel.selectedSite + viewModel.makeAutoSelection() + selectedFacility = viewModel.selectedFacility + selectedSite = viewModel.selectedSite XCTAssertNil(selectedSite) XCTAssertNil(selectedFacility) } - + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testAutoSelectAlwaysNotClearing() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } + try? await floorManager.load() let viewpointLosAngeles = Viewpoint( center: Point( x: -13164116.3284, @@ -228,36 +242,37 @@ class FloorFilterViewModelTests: XCTestCase { ) var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( automaticSelectionMode: .alwaysNotClearing, floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - + // Viewpoint is Redlands Main Q _viewpoint = getEsriRedlandsViewpoint(scale: 1000) - await viewModel.updateSelection() - var selectedFacility = await viewModel.selectedFacility - var selectedSite = await viewModel.selectedSite + viewModel.makeAutoSelection() + var selectedFacility = viewModel.selectedFacility + var selectedSite = viewModel.selectedSite XCTAssertEqual(selectedSite?.name, "Redlands Main") XCTAssertEqual(selectedFacility?.name, "Q") - + // Viewpoint is Los Angeles, but selection should remain Redlands Main Q _viewpoint = viewpointLosAngeles - await viewModel.updateSelection() - selectedFacility = await viewModel.selectedFacility - selectedSite = await viewModel.selectedSite + viewModel.makeAutoSelection() + selectedFacility = viewModel.selectedFacility + selectedSite = viewModel.selectedSite XCTAssertEqual(selectedSite?.name, "Redlands Main") XCTAssertEqual(selectedFacility?.name, "Q") } - + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testAutoSelectNever() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } + try? await floorManager.load() let viewpointLosAngeles = Viewpoint( center: Point( x: -13164116.3284, @@ -268,43 +283,43 @@ class FloorFilterViewModelTests: XCTestCase { ) var _viewpoint: Viewpoint? = viewpointLosAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = await FloorFilterViewModel( + let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, floorManager: floorManager, viewpoint: viewpoint ) await verifyInitialization(viewModel) - + // Viewpoint is Los Angeles, selection should be nil - var selectedFacility = await viewModel.selectedFacility - var selectedSite = await viewModel.selectedSite + var selectedFacility = viewModel.selectedFacility + var selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - + // Viewpoint is Redlands Main Q but selection should still be nil _viewpoint = getEsriRedlandsViewpoint(scale: 1000) - await viewModel.updateSelection() - selectedFacility = await viewModel.selectedFacility - selectedSite = await viewModel.selectedSite + viewModel.makeAutoSelection() + selectedFacility = viewModel.selectedFacility + selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) } - + /// Get a map constructed from an ArcGIS portal item. /// - Returns: A map constructed from an ArcGIS portal item. private func makeMap() async -> Map? { // Multiple sites/facilities: Esri IST map with all buildings. // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - + // Redlands Campus map with multiple sites and facilities. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) - + // Single site (ESRI Redlands Main) and facility (Building L). // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - + let map = Map(item: portalItem) do { try await map.load() @@ -314,21 +329,24 @@ class FloorFilterViewModelTests: XCTestCase { } return map } - + /// Verifies that the `FloorFilterViewModel` has succesfully initialized. /// - Parameter viewModel: The view model to analyze. private func verifyInitialization(_ viewModel: FloorFilterViewModel) async { let expectation = XCTestExpectation( description: "View model successfully initialized" ) - let subscription = await viewModel.$isLoading + let subscription = viewModel.$isLoading .sink { loading in + print(#function, loading) if !loading { expectation.fulfill() } } - wait(for: [expectation], timeout: 10.0) - subscription.cancel() + Task { + wait(for: [expectation], timeout: 30.0) + subscription.cancel() + } } } @@ -341,7 +359,7 @@ extension FloorFilterViewModelTests { spatialReference: .webMercator ) } - + /// Builds viewpoints to use for tests. /// - Parameter rotation: The rotation to use for the resulting viewpoint. /// - Returns: A viewpoint object for tests. From 9a75309f34a590d849df66a84358289566632555 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Apr 2022 15:00:42 -0700 Subject: [PATCH 0762/1722] Apply Mark's initial feedback --- .../Components/Scalebar/Scalebar.swift | 2 +- .../Scalebar/ScalebarStyleRender.swift | 174 +++++++++--------- .../Components/Scalebar/ScalebarUnits.swift | 10 +- 3 files changed, 85 insertions(+), 101 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 4219e7db7..4ac617fa2 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -22,7 +22,7 @@ public struct Scalebar: View { @State private var height: Double? /// The view model used by the `Scalebar`. - @StateObject internal var viewModel: ScalebarViewModel + @StateObject var viewModel: ScalebarViewModel /// The font used by the scalebar, available in both `Font` and `UIFont` types. internal static var font: (font: Font, uiFont: UIFont) { diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index f5c0f8728..154a82a08 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -75,91 +75,83 @@ extension Scalebar { /// Renders a scalebar with `ScalebarStyle.dualUnitLine`. var dualUnitLineStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { - ZStack { - Text(viewModel.labels.last?.text ?? "") - .scalebarText() - .position( - x: viewModel.labels.last?.xOffset ?? .zero, - y: ScalebarLabel.yOffset - ) - .frame(height: Scalebar.fontHeight) - } - GeometryReader { geoProxy in - ZStack(alignment: .bottom) { - Path { path in - let zero = Double.zero - let maxX = geoProxy.size.width - let maxY = geoProxy.size.height - let midY = maxY / 2 - let alternateUnitX = viewModel.alternateUnit.screenLength - - // Leading vertical bar - path.move(to: CGPoint(x: zero, y: zero)) - path.addLine(to: CGPoint(x: zero, y: maxY)) - - // Horiontal cross bar - path.move(to: CGPoint(x: zero, y: midY)) - path.addLine(to: CGPoint(x: maxX, y: midY)) - - // Unit 1 vertical bar - path.move(to: CGPoint(x: maxX, y: zero)) - path.addLine(to: CGPoint(x: maxX, y: midY)) - - // Unit 2 vertical bar - path.move(to: CGPoint(x: alternateUnitX, y: midY)) - path.addLine(to: CGPoint(x: alternateUnitX, y: maxY)) - } - .stroke( - style: .init( - lineWidth: Scalebar.lineWidth, - lineCap: .round, - lineJoin: .round - ) - ) - .fill(Scalebar.lineColor) + Text(viewModel.labels.last?.text ?? "") + .scalebarText() + .position( + x: viewModel.labels.last?.xOffset ?? .zero, + y: ScalebarLabel.yOffset + ) + .frame(height: Scalebar.fontHeight) + GeometryReader { geometry in + Path { path in + let zero = Double.zero + let maxX = geometry.size.width + let maxY = geometry.size.height + let midY = maxY / 2 + let alternateUnitX = viewModel.alternateUnit.screenLength + + // Leading vertical bar + path.move(to: CGPoint(x: zero, y: zero)) + path.addLine(to: CGPoint(x: zero, y: maxY)) + + // Horiontal cross bar + path.move(to: CGPoint(x: zero, y: midY)) + path.addLine(to: CGPoint(x: maxX, y: midY)) + + // Unit 1 vertical bar + path.move(to: CGPoint(x: maxX, y: zero)) + path.addLine(to: CGPoint(x: maxX, y: midY)) + + // Unit 2 vertical bar + path.move(to: CGPoint(x: alternateUnitX, y: midY)) + path.addLine(to: CGPoint(x: alternateUnitX, y: maxY)) } + .stroke( + style: .init( + lineWidth: Scalebar.lineWidth, + lineCap: .round, + lineJoin: .round + ) + ) + .fill(Scalebar.lineColor) .scalebarShadow() } .frame(height: Scalebar.barFrameHeight) - ZStack { - Text(viewModel.alternateUnit.label) - .scalebarText() - .position( - x: viewModel.alternateUnit.screenLength, - y: ScalebarLabel.yOffset - ) - .frame(height: Scalebar.fontHeight) - } + Text(viewModel.alternateUnit.label) + .scalebarText() + .position( + x: viewModel.alternateUnit.screenLength, + y: ScalebarLabel.yOffset + ) + .frame(height: Scalebar.fontHeight) } } /// Renders a scalebar with `ScalebarStyle.graduatedLine`. var graduatedLineStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { - GeometryReader { geoProxy in - ZStack(alignment: .bottom) { - Path { path in - let segments = viewModel.labels - let zero = Double.zero - let maxX = geoProxy.size.width - let maxY = geoProxy.size.height - path.move(to: CGPoint(x: zero, y: maxY)) - path.addLine(to: CGPoint(x: maxX, y: maxY)) - for segment in segments { - let segmentX = segment.xOffset - path.move(to: CGPoint(x: segmentX, y: zero)) - path.addLine(to: CGPoint(x: segmentX, y: maxY)) - } + GeometryReader { geometry in + Path { path in + let segments = viewModel.labels + let zero = Double.zero + let maxX = geometry.size.width + let maxY = geometry.size.height + path.move(to: CGPoint(x: zero, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: maxY)) + for segment in segments { + let segmentX = segment.xOffset + path.move(to: CGPoint(x: segmentX, y: zero)) + path.addLine(to: CGPoint(x: segmentX, y: maxY)) } - .stroke( - style: .init( - lineWidth: Scalebar.lineWidth, - lineCap: .round, - lineJoin: .round - ) - ) - .fill(Scalebar.lineColor) } + .stroke( + style: .init( + lineWidth: Scalebar.lineWidth, + lineCap: .round, + lineJoin: .round + ) + ) + .fill(Scalebar.lineColor) .scalebarShadow() } .frame(height: Scalebar.lineFrameHeight) @@ -170,26 +162,24 @@ extension Scalebar { /// Renders a scalebar with `ScalebarStyle.line`. var lineStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { - GeometryReader { geoProxy in - ZStack(alignment: .bottom) { - Path { path in - let zero = Double.zero - let maxX = geoProxy.size.width - let maxY = geoProxy.size.height - path.move(to: CGPoint(x: zero, y: zero)) - path.addLine(to: CGPoint(x: zero, y: maxY)) - path.addLine(to: CGPoint(x: maxX, y: maxY)) - path.addLine(to: CGPoint(x: maxX, y: zero)) - } - .stroke( - style: .init( - lineWidth: Scalebar.lineWidth, - lineCap: .round, - lineJoin: .round - ) - ) - .fill(Scalebar.lineColor) + GeometryReader { geometry in + Path { path in + let zero = Double.zero + let maxX = geometry.size.width + let maxY = geometry.size.height + path.move(to: CGPoint(x: zero, y: zero)) + path.addLine(to: CGPoint(x: zero, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: maxY)) + path.addLine(to: CGPoint(x: maxX, y: zero)) } + .stroke( + style: .init( + lineWidth: Scalebar.lineWidth, + lineCap: .round, + lineJoin: .round + ) + ) + .fill(Scalebar.lineColor) .scalebarShadow() } .frame(height: Scalebar.lineFrameHeight) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index 08a089e83..ce2a53b2a 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -159,15 +159,9 @@ public enum ScalebarUnits { internal func linearUnitsForDistance(distance: Double) -> LinearUnit { switch self { case .imperial: - if distance >= 2640 { - return LinearUnit.miles - } - return LinearUnit.feet + return distance >= 2640 ? .miles : .feet case .metric: - if distance >= 1000 { - return LinearUnit.kilometers - } - return LinearUnit.meters + return distance >= 1000 ? .kilometers : .meters } } } From 651ac8979d2c8d751e29975753ee6369eb1e3c57 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Apr 2022 09:25:01 -0700 Subject: [PATCH 0763/1722] #43 Link to compass doc --- Documentation/README.md | 2 ++ README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Documentation/README.md b/Documentation/README.md index dfa9209f6..e9766f86c 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,3 +1,5 @@ ### Table of Contents +* [Compass](Compass) - Shows a compass direction when the map is rotated. Auto-hides when the map points north up. + * [Overview Map](OverviewMap) - Displays an "overview" (or "inset") map on top of an existing map or scene view. diff --git a/README.md b/README.md index c171ed48d..209afba6b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ To use Toolkit in your project: ## Toolkit Components +* [Compass](./Documentation/Compass/README.md) + * [OverviewMap](./Documentation/OverviewMap/README.md) ## Requirements From f88655258be2b05b161473051ae615d5e21000bc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Apr 2022 10:04:07 -0700 Subject: [PATCH 0764/1722] PR feedback --- .../Components/FloorFilter/FloorFilter.swift | 29 +++-- .../FloorFilter/FloorFilterViewModel.swift | 107 ++++-------------- .../FloorFilter/LevelSelector.swift | 46 ++++---- .../FloorFilter/SiteAndFacilitySelector.swift | 2 +- .../Extensions/FloorFacility.swift | 14 +++ .../ArcGISToolkit/Extensions/FloorLevel.swift | 6 + .../ArcGISToolkit/Extensions/FloorSite.swift | 7 ++ .../FloorFilterViewModelTests.swift | 54 ++------- 8 files changed, 92 insertions(+), 173 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 839ec5424..d94e18e2a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -62,22 +62,21 @@ public struct FloorFilter: View { } /// Displays the available levels. + @ViewBuilder private var levelsSelectorView: some View { - Group { - if viewModel.hasLevelsToDisplay { - if topAligned { - Divider() - .frame(width: 30) - } - LevelSelector( - isCollapsed: $isLevelsViewCollapsed, - levels: viewModel.sortedLevels, - topAligned: topAligned - ) - if !topAligned { - Divider() - .frame(width: 30) - } + if viewModel.hasLevelsToDisplay { + if topAligned { + Divider() + .frame(width: 30) + } + LevelSelector( + isCollapsed: $isLevelsViewCollapsed, + levels: viewModel.sortedLevels, + isTopAligned: topAligned + ) + if !topAligned { + Divider() + .frame(width: 30) } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index db42e4dff..9562adbea 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -27,6 +27,7 @@ final class FloorFilterViewModel: ObservableObject { /// A selected level. case level(FloorLevel) } + /// Creates a `FloorFilterViewModel`. /// - Parameters: /// - automaticSelectionMode: The selection behavior of the floor filter. @@ -39,17 +40,17 @@ final class FloorFilterViewModel: ObservableObject { ) { self.automaticSelectionMode = automaticSelectionMode self.floorManager = floorManager - self.viewpoint = viewpoint + self._viewpoint = viewpoint viewpointSubscription = viewpointSubject .debounce(for: delay, scheduler: DispatchQueue.main) .sink(receiveValue: { [weak self] _ in guard let self = self, - let viewpoint = self.viewpoint.wrappedValue, + let viewpoint = self.viewpoint, !viewpoint.targetScale.isZero else { return } - self.makeAutoSelection() + self.automaticallySelectFacilityOrSite() }) Task { @@ -73,16 +74,16 @@ final class FloorFilterViewModel: ObservableObject { @Published private(set) var isLoading = true /// The selected site, floor, or level. - @Published var selection: Selection? { + @Published private(set) var selection: Selection? { didSet { if case let .level(oldLevel) = oldValue, case let .facility(facility) = selection, let newLevel = facility.levels.first(where: { level in - level.verticalOrder == oldLevel.verticalOrder - }) { - selection = .level(newLevel) + level.verticalOrder == oldLevel.verticalOrder + }) { + selection = .level(newLevel) } else if case let .facility(facility) = selection, - let level = defaultLevel(for: facility) { + let level = facility.defaultLevel { selection = .level(level) } else { filterMapToSelectedLevel() @@ -111,10 +112,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 { - guard let selectedFacility = selectedFacility else { - return false - } - return !selectedFacility.levels.isEmpty + !(selectedFacility?.levels.isEmpty ?? true) } /// The floor manager levels. @@ -139,8 +137,6 @@ final class FloorFilterViewModel: ObservableObject { /// The selected facility. var selectedFacility: FloorFacility? { switch selection { - case .site: - return nil case .facility(let facility): return facility case .level(let level): @@ -166,38 +162,26 @@ final class FloorFilterViewModel: ObservableObject { /// The selected facility's levels, sorted by `level.verticalOrder`. var sortedLevels: [FloorLevel] { - let levels = selectedFacility?.levels ?? [] - return levels.sorted { - $0.verticalOrder > $1.verticalOrder - } + selectedFacility?.levels + .sorted(by: { $0.verticalOrder > $1.verticalOrder }) ?? [] } /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. - var viewpoint: Binding + @Binding var viewpoint: Viewpoint? /// A subject to which viewpoint updates can be submitted. var viewpointSubject = PassthroughSubject() // MARK: Public methods - /// Gets the default level for a facility. - /// - Parameter facility: The facility to get the default level for. - /// - Returns: The default level for the facility, which is the level with vertical order 0; - /// if there's no level with vertical order of 0, it returns the lowest level. - func defaultLevel(for facility: FloorFacility?) -> FloorLevel? { - return levels.first(where: { level in - level.facility == facility && level.verticalOrder == .zero - }) ?? nil - } - /// Updates the selected site, facility, and level based on a newly selected facility. /// - Parameter floorFacility: The selected facility. func setFacility(_ floorFacility: FloorFacility?) { if let floorFacility = floorFacility { selection = .facility(floorFacility) } - zoomToSelection() + zoomToExtent(floorFacility?.geometry?.extent) } /// Updates the selected site, facility, and level based on a newly selected level. @@ -214,7 +198,7 @@ final class FloorFilterViewModel: ObservableObject { if let floorSite = floorSite { selection = .site(floorSite) } - zoomToSelection() + zoomToExtent(floorSite?.geometry?.extent) } // MARK: Private items @@ -222,7 +206,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. - internal func makeAutoSelection() { + func automaticallySelectFacilityOrSite() { guard automaticSelectionMode != .never else { return } @@ -253,13 +237,13 @@ final class FloorFilterViewModel: ObservableObject { facilityMinScale = 1500 } - if viewpoint.wrappedValue?.targetScale ?? .zero > facilityMinScale { + if viewpoint?.targetScale ?? .zero > facilityMinScale { return false } // If the centerpoint is within a facilities' geometry, select that site. let facilityResult = floorManager.facilities.first { facility in - guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, + guard let extent1 = viewpoint?.targetGeometry.extent, let extent2 = facility.geometry?.extent else { return false } @@ -288,7 +272,7 @@ final class FloorFilterViewModel: ObservableObject { } // If viewpoint is out of range, reset selection and return early. - if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { + if viewpoint?.targetScale ?? .zero > siteMinScale { if automaticSelectionMode == .always { selection = nil } @@ -297,7 +281,7 @@ final class FloorFilterViewModel: ObservableObject { // If the centerpoint is within a site's geometry, select that site. let siteResult = floorManager.sites.first { site in - guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, + guard let extent1 = viewpoint?.targetGeometry.extent, let extent2 = site.geometry?.extent else { return false } @@ -323,26 +307,12 @@ final class FloorFilterViewModel: ObservableObject { builder.expand(factor: 1.5) let targetExtent = builder.toGeometry() if !targetExtent.isEmpty { - viewpoint.wrappedValue = Viewpoint( + viewpoint = Viewpoint( targetExtent: targetExtent ) } } - /// Zooms to the selected facility; if there is no selected facility, zooms to the selected site. - private func zoomToSelection() { - switch selection { - case .site(let site): - zoomToExtent(site.geometry?.extent) - case .facility(let facility): - zoomToExtent(facility.geometry?.extent) - case .level(let level): - zoomToExtent(level.facility?.geometry?.extent) - default: - break - } - } - /// A subscription to handle listening for viewpoint changes. private var viewpointSubscription: AnyCancellable? } @@ -359,37 +329,4 @@ public enum AutomaticSelectionMode { case never } -extension FloorSite: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - hasher.combine(self.name) - } -} - -extension FloorFilterViewModel.Selection: Hashable { - static func == ( - lhs: FloorFilterViewModel.Selection, - rhs: FloorFilterViewModel.Selection - ) -> Bool { - switch (lhs, rhs) { - case (.site(let a), .site(let b)): - return a.id == b.id - case (.facility(let a), .facility(let b)): - return a.id == b.id - case (.level(let a), .level(let b)): - return a.id == b.id - default: return false - } - } - - func hash(into hasher: inout Hasher) { - switch self { - case .site(let site): - hasher.combine(site.id) - case.facility(let facility): - hasher.combine(facility.id) - case .level(let level): - hasher.combine(level.id) - } - } -} +extension FloorFilterViewModel.Selection: Hashable { } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 543ad0df0..1f74b0f47 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -22,12 +22,10 @@ struct LevelSelector: View { /// 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. - @Binding - var isCollapsed: Bool + @Binding var isCollapsed: Bool /// The height of the scroll view's content. - @State - private var scrollViewContentHeight: CGFloat = .zero + @State private var scrollViewContentHeight: CGFloat = .zero /// The levels to display. let levels: [FloorLevel] @@ -35,23 +33,17 @@ struct LevelSelector: View { /// Returns the short name of the currently selected level, the first level or "None" if none of the listed /// are available. private var selectedLevelName: String { - if let shortName = viewModel.selectedLevel?.shortName { - return shortName - } else if let firstLevelShortName = levels.first?.shortName { - return firstLevelShortName - } else { - return "None" - } + viewModel.selectedLevel?.shortName ?? "" } /// The alignment configuration. - var topAligned: Bool + var isTopAligned: Bool public var body: some View { - VStack { - if !isCollapsed, - levels.count > 1 { - if !topAligned { + if !isCollapsed, + levels.count > 1 { + VStack { + if !isTopAligned { CollapseButton(isCollapsed: $isCollapsed) Divider() .frame(width: 30) @@ -64,23 +56,23 @@ struct LevelSelector: View { scrollViewContentHeight = $0.height } } - .frame(maxHeight: scrollViewContentHeight) - if topAligned { + .frame(maxHeight: scrollViewContentHeight) + if isTopAligned { Divider() .frame(width: 30) CollapseButton(isCollapsed: $isCollapsed) } - } else { - Button { - if levels.count > 1 { - isCollapsed.toggle() - } - } label: { - Text(selectedLevelName) - .lineLimit(1) + } + } else { + Button { + if levels.count > 1 { + isCollapsed.toggle() } - .selected(true) + } label: { + Text(selectedLevelName) + .lineLimit(1) } + .selected(true) } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 1fd8c3aa3..d6373f3f4 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -167,7 +167,7 @@ struct SiteAndFacilitySelector: View { } } .listStyle(.plain) - .onChange(of: $viewModel.selection.wrappedValue) { _ in + .onChange(of: viewModel.selection) { _ in // Setting the `updateViewModel` flag false allows // `selectedSite` to receive upstream updates from the view // model without republishing them back up to the view model. diff --git a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift index 4924a27b5..15a6dd5a6 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift @@ -14,8 +14,22 @@ import SwiftUI import ArcGIS +extension FloorFacility { + /// - Returns: The default level for the facility, which is the level with vertical order 0. + var defaultLevel: FloorLevel? { + levels.first(where: { $0.verticalOrder == .zero }) + } +} + extension FloorFacility: Equatable { public static func == (lhs: FloorFacility, rhs: FloorFacility) -> Bool { lhs.id == rhs.id } } + +extension FloorFacility: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + hasher.combine(self.name) + } +} diff --git a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift index 97e7d320d..05cdbc628 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift @@ -19,3 +19,9 @@ extension FloorLevel: Equatable { lhs.id == rhs.id } } + +extension FloorLevel: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} diff --git a/Sources/ArcGISToolkit/Extensions/FloorSite.swift b/Sources/ArcGISToolkit/Extensions/FloorSite.swift index ff8b5faa2..fb6c49e95 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorSite.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorSite.swift @@ -19,3 +19,10 @@ extension FloorSite: Equatable { lhs.id == rhs.id } } + +extension FloorSite: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + hasher.combine(self.name) + } +} diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 144c44188..edfe90bd1 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -37,7 +37,6 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) let sites = viewModel.sites let facilities = viewModel.facilities let levels = viewModel.levels @@ -60,7 +59,6 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) let sites = viewModel.sites let facilities = viewModel.facilities let levels = viewModel.levels @@ -85,23 +83,17 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) guard let site = viewModel.sites.first else { XCTFail() return } - viewModel.selection = .site(site) + viewModel.setSite(site) let selectedSite = viewModel.selectedSite let selectedFacility = viewModel.selectedFacility let selectedLevel = viewModel.selectedLevel XCTAssertEqual(selectedSite, site) XCTAssertNil(selectedFacility) XCTAssertNil(selectedLevel) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - initialViewpoint.targetGeometry.extent.center.x - ) - viewModel.setSite(site) XCTAssertEqual( _viewpoint?.targetGeometry.extent.center.x, selectedSite?.geometry?.extent.center.x @@ -123,22 +115,16 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) guard let facility = viewModel.facilities.first else { XCTFail() return } - viewModel.selection = .facility(facility) + viewModel.setFacility(facility) let selectedFacility = viewModel.selectedFacility let selectedLevel = viewModel.selectedLevel - let defaultLevel = viewModel.defaultLevel(for: selectedFacility) + let defaultLevel = selectedFacility?.defaultLevel XCTAssertEqual(selectedFacility, facility) XCTAssertEqual(selectedLevel, defaultLevel) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - initialViewpoint.targetGeometry.extent.center.x - ) - viewModel.setFacility(facility) XCTAssertEqual( _viewpoint?.targetGeometry.extent.center.x, selectedFacility?.geometry?.extent.center.x @@ -159,7 +145,6 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) let levels = viewModel.levels let level = levels.first viewModel.setLevel(level) @@ -200,7 +185,6 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) // Viewpoint is Los Angeles, selection should be nil var selectedFacility = viewModel.selectedFacility @@ -210,7 +194,7 @@ class FloorFilterViewModelTests: XCTestCase { // Viewpoint is Redlands Main Q _viewpoint = getEsriRedlandsViewpoint(scale: 1000) - viewModel.makeAutoSelection() + viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite XCTAssertEqual(selectedSite?.name, "Redlands Main") @@ -218,7 +202,7 @@ class FloorFilterViewModelTests: XCTestCase { // Viewpoint is Los Angeles, selection should be nil _viewpoint = viewpointLosAngeles - viewModel.makeAutoSelection() + viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite XCTAssertNil(selectedSite) @@ -247,11 +231,10 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) // Viewpoint is Redlands Main Q _viewpoint = getEsriRedlandsViewpoint(scale: 1000) - viewModel.makeAutoSelection() + viewModel.automaticallySelectFacilityOrSite() var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite XCTAssertEqual(selectedSite?.name, "Redlands Main") @@ -259,7 +242,7 @@ class FloorFilterViewModelTests: XCTestCase { // Viewpoint is Los Angeles, but selection should remain Redlands Main Q _viewpoint = viewpointLosAngeles - viewModel.makeAutoSelection() + viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite XCTAssertEqual(selectedSite?.name, "Redlands Main") @@ -288,7 +271,6 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) // Viewpoint is Los Angeles, selection should be nil var selectedFacility = viewModel.selectedFacility @@ -298,7 +280,7 @@ class FloorFilterViewModelTests: XCTestCase { // Viewpoint is Redlands Main Q but selection should still be nil _viewpoint = getEsriRedlandsViewpoint(scale: 1000) - viewModel.makeAutoSelection() + viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) @@ -323,31 +305,13 @@ class FloorFilterViewModelTests: XCTestCase { let map = Map(item: portalItem) do { try await map.load() + try await map.floorManager?.load() } catch { XCTFail("\(#fileID), \(#function), \(#line), \(error.localizedDescription)") return nil } return map } - - /// Verifies that the `FloorFilterViewModel` has succesfully initialized. - /// - Parameter viewModel: The view model to analyze. - private func verifyInitialization(_ viewModel: FloorFilterViewModel) async { - let expectation = XCTestExpectation( - description: "View model successfully initialized" - ) - let subscription = viewModel.$isLoading - .sink { loading in - print(#function, loading) - if !loading { - expectation.fulfill() - } - } - Task { - wait(for: [expectation], timeout: 30.0) - subscription.cancel() - } - } } extension FloorFilterViewModelTests { From b973dba1755d1882e7f241c37e2b9885626f4fd5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Apr 2022 12:29:19 -0700 Subject: [PATCH 0765/1722] PR feedback --- .../FloorFilter/FloorFilterViewModel.swift | 28 ++++++++----------- .../FloorFilter/SiteAndFacilitySelector.swift | 4 +-- .../FloorFilterViewModelTests.swift | 5 +++- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 9562adbea..a5079295c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -176,29 +176,23 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Public methods /// Updates the selected site, facility, and level based on a newly selected facility. - /// - Parameter floorFacility: The selected facility. - func setFacility(_ floorFacility: FloorFacility?) { - if let floorFacility = floorFacility { - selection = .facility(floorFacility) - } - zoomToExtent(floorFacility?.geometry?.extent) + /// - Parameter newFacility: The selected facility. + func setFacility(_ newFacility: FloorFacility) { + selection = .facility(newFacility) + zoomToExtent(newFacility.geometry?.extent) } /// Updates the selected site, facility, and level based on a newly selected level. - /// - Parameter floorLevel: The selected level. - func setLevel(_ floorLevel: FloorLevel?) { - if let floorLevel = floorLevel { - selection = .level(floorLevel) - } + /// - Parameter newLevel: The selected level. + func setLevel(_ newLevel: FloorLevel) { + selection = .level(newLevel) } /// Updates the selected site, facility, and level based on a newly selected site. - /// - Parameter floorSite: The selected site. - func setSite(_ floorSite: FloorSite?) { - if let floorSite = floorSite { - selection = .site(floorSite) - } - zoomToExtent(floorSite?.geometry?.extent) + /// - Parameter newSite: The selected site. + func setSite(_ newSite: FloorSite) { + selection = .site(newSite) + zoomToExtent(newSite.geometry?.extent) } // MARK: Private items diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index d6373f3f4..5bb96369f 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -64,8 +64,8 @@ struct SiteAndFacilitySelector: View { /// hierarchy to keep the site selection persistent in the navigation view. @State private var selectedSite: FloorSite? { didSet { - if updateViewModel { - viewModel.setSite(selectedSite) + if updateViewModel, let site = selectedSite { + viewModel.setSite(site) } updateViewModel = true } diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index edfe90bd1..c31399ecb 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -146,7 +146,10 @@ class FloorFilterViewModelTests: XCTestCase { viewpoint: viewpoint ) let levels = viewModel.levels - let level = levels.first + guard let level = levels.first else { + XCTFail("A first level does not exist") + return + } viewModel.setLevel(level) let selectedLevel = viewModel.selectedLevel XCTAssertEqual(selectedLevel, level) From b5ac5889766b014363ddadf6d9172302b2b5cd9d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Apr 2022 13:20:56 -0700 Subject: [PATCH 0766/1722] PR feedback --- .../FloorFilter/FloorFilterViewModel.swift | 59 ++++++++----------- .../FloorFilter/SiteAndFacilitySelector.swift | 6 +- .../FloorFilterViewModelTests.swift | 4 +- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index a5079295c..3172f741b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -59,7 +59,7 @@ final class FloorFilterViewModel: ObservableObject { if sites.count == 1, let firstSite = sites.first { // If we have only one site, select it. - setSite(firstSite) + setSite(firstSite, zoomTo: true) } } catch { print("error: \(error)") @@ -74,22 +74,7 @@ final class FloorFilterViewModel: ObservableObject { @Published private(set) var isLoading = true /// The selected site, floor, or level. - @Published private(set) var selection: Selection? { - didSet { - if case let .level(oldLevel) = oldValue, - case let .facility(facility) = selection, - let newLevel = facility.levels.first(where: { level in - level.verticalOrder == oldLevel.verticalOrder - }) { - selection = .level(newLevel) - } else if case let .facility(facility) = selection, - let level = facility.defaultLevel { - selection = .level(level) - } else { - filterMapToSelectedLevel() - } - } - } + @Published private(set) var selection: Selection? // MARK: Constants @@ -177,22 +162,39 @@ final class FloorFilterViewModel: ObservableObject { /// Updates the selected site, facility, and level based on a newly selected facility. /// - Parameter newFacility: The selected facility. - func setFacility(_ newFacility: FloorFacility) { - selection = .facility(newFacility) - zoomToExtent(newFacility.geometry?.extent) + func setFacility(_ newFacility: FloorFacility, zoomTo: Bool = false) { + if let oldLevel = selectedLevel, + let newLevel = newFacility.levels.first( + where: { $0.verticalOrder == oldLevel.verticalOrder } + ) { + setLevel(newLevel) + } else if let defaultLevel = newFacility.defaultLevel { + setLevel(defaultLevel) + } else { + selection = .facility(newFacility) + } + + if zoomTo { + zoomToExtent(newFacility.geometry?.extent) + } } /// Updates the selected site, facility, and level based on a newly selected level. /// - Parameter newLevel: The selected level. func setLevel(_ newLevel: FloorLevel) { selection = .level(newLevel) + levels.forEach { + $0.isVisible = $0.verticalOrder == newLevel.verticalOrder + } } /// Updates the selected site, facility, and level based on a newly selected site. /// - Parameter newSite: The selected site. - func setSite(_ newSite: FloorSite) { + func setSite(_ newSite: FloorSite, zoomTo: Bool = false) { selection = .site(newSite) - zoomToExtent(newSite.geometry?.extent) + if zoomTo { + zoomToExtent(newSite.geometry?.extent) + } } // MARK: Private items @@ -209,15 +211,6 @@ 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 { - levels.forEach { - $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder - } - } - } - /// Updates `selectedFacility` if a good selection exists. /// - Returns: `false` if a selection was not made. @discardableResult @@ -245,7 +238,7 @@ final class FloorFilterViewModel: ObservableObject { } if let facilityResult = facilityResult { - selection = .facility(facilityResult) + setFacility(facilityResult) } else if automaticSelectionMode == .always { return false } @@ -283,7 +276,7 @@ final class FloorFilterViewModel: ObservableObject { } if let siteResult = siteResult { - selection = .site(siteResult) + setSite(siteResult) } else if automaticSelectionMode == .always { selection = nil } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 5bb96369f..8fcc8d9fe 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -65,7 +65,7 @@ struct SiteAndFacilitySelector: View { @State private var selectedSite: FloorSite? { didSet { if updateViewModel, let site = selectedSite { - viewModel.setSite(site) + viewModel.setSite(site, zoomTo: true) } updateViewModel = true } @@ -163,7 +163,7 @@ struct SiteAndFacilitySelector: View { ) } .onTapGesture { - viewModel.setSite(site) + selectedSite = site } } .listStyle(.plain) @@ -279,7 +279,7 @@ struct SiteAndFacilitySelector: View { ScrollViewReader { proxy in List(matchingFacilities, id: \.id) { facility in Button { - viewModel.setFacility(facility) + viewModel.setFacility(facility, zoomTo: true) isHidden.wrappedValue.toggle() } label: { HStack { diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index c31399ecb..5084ba112 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -87,7 +87,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTFail() return } - viewModel.setSite(site) + viewModel.setSite(site, zoomTo: true) let selectedSite = viewModel.selectedSite let selectedFacility = viewModel.selectedFacility let selectedLevel = viewModel.selectedLevel @@ -119,7 +119,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTFail() return } - viewModel.setFacility(facility) + viewModel.setFacility(facility, zoomTo: true) let selectedFacility = viewModel.selectedFacility let selectedLevel = viewModel.selectedLevel let defaultLevel = selectedFacility?.defaultLevel From e370f676373525ce3d8b32c51be89c7416b1073f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 12 Apr 2022 16:38:57 -0500 Subject: [PATCH 0767/1722] Fix BasemapGallery documentation warning. --- .../Components/BasemapGallery/BasemapGallery.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift index ac8ad20f6..9e3657f22 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGallery.swift @@ -189,7 +189,7 @@ private extension BasemapGallery { // MARK: Modifiers public extension BasemapGallery { - /// The style of the basemap gallery. Defaults to ``Style/automatic``. + /// The style of the basemap gallery. Defaults to ``Style/automatic(listWidth:gridWidth:)``. /// - Parameter style: The `Style` to use. /// - Returns: The `BasemapGallery`. func style( From 7df6a0a994cf9b17636c8e329d411dd534f091a4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Apr 2022 15:16:37 -0700 Subject: [PATCH 0768/1722] PR feedback --- .../FloorFilter/LevelSelector.swift | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 1f74b0f47..274c69641 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -24,9 +24,6 @@ struct LevelSelector: View { /// the selected level. @Binding var isCollapsed: Bool - /// The height of the scroll view's content. - @State private var scrollViewContentHeight: CGFloat = .zero - /// The levels to display. let levels: [FloorLevel] @@ -48,15 +45,7 @@ struct LevelSelector: View { Divider() .frame(width: 30) } - ScrollView { - VStack { - LevelsStack(levels: levels) - } - .onSizeChange { - scrollViewContentHeight = $0.height - } - } - .frame(maxHeight: scrollViewContentHeight) + LevelsStack(levels: levels) if isTopAligned { Divider() .frame(width: 30) @@ -64,15 +53,11 @@ struct LevelSelector: View { } } } else { - Button { - if levels.count > 1 { - isCollapsed.toggle() - } - } label: { + Toggle(isOn: $isCollapsed) { Text(selectedLevelName) .lineLimit(1) } - .selected(true) + .toggleStyle(.button) } } } @@ -82,29 +67,37 @@ 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] var body: some View { - VStack { - ForEach(levels) { level in - Button { - viewModel.setLevel(level) - } label: { - Text(level.shortName) - .lineLimit(1) + ScrollView { + VStack { + ForEach(levels) { level in + Button { + viewModel.setLevel(level) + } label: { + Text(level.shortName) + .lineLimit(1) + } + .selected(level == viewModel.selectedLevel) } - .selected(level == viewModel.selectedLevel) + } + .onSizeChange { + contentHeight = $0.height } } + .frame(maxHeight: contentHeight) } } /// A button used to collapse the floor level list. struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. - @Binding - var isCollapsed: Bool + @Binding var isCollapsed: Bool var body: some View { Button { From b27e0889824847347225ae9b628b1cb36645354d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Apr 2022 16:11:10 -0700 Subject: [PATCH 0769/1722] Small doc update --- .../Components/FloorFilter/FloorFilterViewModel.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 3172f741b..32911c5e9 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -161,7 +161,9 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Public methods /// Updates the selected site, facility, and level based on a newly selected facility. - /// - Parameter newFacility: The selected facility. + /// - Parameters: + /// - 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, let newLevel = newFacility.levels.first( @@ -189,7 +191,9 @@ final class FloorFilterViewModel: ObservableObject { } /// Updates the selected site, facility, and level based on a newly selected site. - /// - Parameter newSite: The selected site. + /// - Parameters: + /// - newSite: The new site to be selected. + /// - zoomTo: If `true`, changes the viewpoint to the extent of the new site. func setSite(_ newSite: FloorSite, zoomTo: Bool = false) { selection = .site(newSite) if zoomTo { From 0654ba93645ce4070df8175957f05f9e09fe3df2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Apr 2022 16:01:00 -0700 Subject: [PATCH 0770/1722] Update FloorFilter --- .../Examples/FloorFilterExampleView.swift | 19 +- .../FloorFilter/FloorFilterViewModel.swift | 53 +++--- .../FloorFilter/LevelSelector.swift | 2 +- .../FloorFilter/SiteAndFacilitySelector.swift | 8 +- .../FloorFilterViewModelTests.swift | 162 ++++++------------ 5 files changed, 96 insertions(+), 148 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index f52de6916..f28f8bb56 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -18,18 +18,8 @@ import ArcGIS struct FloorFilterExampleView: View { /// Make a map from a portal item. static func makeMap() -> Map { - // Multiple sites/facilities: Esri IST map with all buildings. - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - - // Redlands Campus map. - let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) //<= another multiple sites/facilities - - // Single site (ESRI Redlands Main) and facility (Building L). - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - + let portal = Portal(url: URL(string: "https://www.arcgis.com/")!, isLoginRequired: false) + let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "b4b599a43a474d33946cf0df526426f5")!) return Map(item: portalItem) } @@ -71,7 +61,10 @@ struct FloorFilterExampleView: View { floorManager: floorManager, viewpoint: $viewpoint ) - .frame(maxWidth: 300, maxHeight: 300) + .frame( + maxWidth: 300, + maxHeight: 300 + ) .padding(36) } else if mapLoadError { Label( diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index db42e4dff..a36a85b4a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -52,19 +52,7 @@ final class FloorFilterViewModel: ObservableObject { self.makeAutoSelection() }) - Task { - do { - try await floorManager.load() - if sites.count == 1, - let firstSite = sites.first { - // If we have only one site, select it. - setSite(firstSite) - } - } catch { - print("error: \(error)") - } - isLoading = false - } + loadFloorManager() } // MARK: Published members @@ -231,15 +219,6 @@ 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 { - levels.forEach { - $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder - } - } - } - /// Updates `selectedFacility` if a good selection exists. /// - Returns: `false` if a selection was not made. @discardableResult @@ -312,6 +291,36 @@ 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. + private func filterMapToSelectedLevel() { + if let selectedLevel = selectedLevel { + levels.forEach { + $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder + } + } + } + + /// Loads the given `FloorManager` if needed, then sets `isLoading` to `false`. + private func loadFloorManager() { + guard floorManager.loadStatus == .notLoaded else { + isLoading = false + return + } + Task { + do { + try await floorManager.load() + if sites.count == 1, + let firstSite = sites.first { + // If we have only one site, select it. + setSite(firstSite) + } + } catch { + print("error: \(error)") + } + isLoading = false + } + } + /// Zoom to given extent. private func zoomToExtent(_ extent: Envelope?) { // Make sure we have an extent and viewpoint to zoom to. diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 543ad0df0..b2b14e7ee 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -120,7 +120,7 @@ struct CollapseButton: View { isCollapsed.toggle() } } label: { - Image(systemName: "xmark") + Image(systemName: "chevron.down.circle") } .padding(EdgeInsets( top: 2, diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 1fd8c3aa3..4d86ba5ce 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -139,9 +139,9 @@ struct SiteAndFacilitySelector: View { Text("Select a site"), displayMode: .inline ) - .navigationBarItems(trailing: + .toolbar { CloseButton { isHidden.wrappedValue.toggle() } - ) + } } } @@ -236,9 +236,9 @@ struct SiteAndFacilitySelector: View { // Only apply navigation modifiers if this is displayed // within a navigation view .navigationBarTitle("Select a facility") - .navigationBarItems(trailing: + .toolbar { CloseButton { isHidden.wrappedValue.toggle() } - ) + } } } diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 144c44188..b09af25fc 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -19,25 +19,18 @@ import XCTest @MainActor class FloorFilterViewModelTests: XCTestCase { - /// Applies credentials necessary to run tests. - override func setUp() async throws { - await addCredentials() - } - /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` func testInitFloorFilterViewModelWithFloorManager() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } - try? await floorManager.load() - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) + var _viewpoint: Viewpoint? = nil let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) let sites = viewModel.sites let facilities = viewModel.facilities let levels = viewModel.levels @@ -47,20 +40,18 @@ class FloorFilterViewModelTests: XCTestCase { } /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager` and - /// `Binding?`.` + /// `Binding`.` func testInitFloorFilterViewModelWithFloorManagerAndViewpoint() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } - try? await floorManager.load() - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) + var _viewpoint: Viewpoint? = nil let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) let sites = viewModel.sites let facilities = viewModel.facilities let levels = viewModel.levels @@ -77,15 +68,13 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let initialViewpoint = getEsriRedlandsViewpoint(.zero) + let initialViewpoint: Viewpoint = .site_ResearchAnnex_facility_Lattice var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) guard let site = viewModel.sites.first else { XCTFail() return @@ -114,8 +103,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let initialViewpoint = getEsriRedlandsViewpoint(.zero) + let initialViewpoint: Viewpoint = .site_ResearchAnnex_facility_Lattice var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( @@ -123,7 +111,6 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) guard let facility = viewModel.facilities.first else { XCTFail() return @@ -151,15 +138,13 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let initialViewpoint = getEsriRedlandsViewpoint(.zero) + let initialViewpoint: Viewpoint = .site_ResearchAnnex_facility_Lattice var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) let levels = viewModel.levels let level = levels.first viewModel.setLevel(level) @@ -184,15 +169,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let viewpointLosAngeles = Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) + let viewpointLosAngeles: Viewpoint = .losAngeles var _viewpoint: Viewpoint? = viewpointLosAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( @@ -200,25 +177,28 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) // Viewpoint is Los Angeles, selection should be nil + viewModel.makeAutoSelection() + var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - // Viewpoint is Redlands Main Q - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = .site_ResearchAnnex_facility_Lattice viewModel.makeAutoSelection() + + // Viewpoint is the Lattice facility at the Research Annex site selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Redlands Main") - XCTAssertEqual(selectedFacility?.name, "Q") + XCTAssertEqual(selectedSite?.name, "Research Annex") + XCTAssertEqual(selectedFacility?.name, "Lattice") - // Viewpoint is Los Angeles, selection should be nil _viewpoint = viewpointLosAngeles viewModel.makeAutoSelection() + + // Viewpoint is Los Angeles, selection should be nil selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite XCTAssertNil(selectedSite) @@ -231,39 +211,31 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let viewpointLosAngeles = Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) + let viewpointLosAngeles: Viewpoint = .losAngeles + var _viewpoint: Viewpoint? = .site_ResearchAnnex_facility_Lattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .alwaysNotClearing, floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) - // Viewpoint is Redlands Main Q - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.makeAutoSelection() + + // Viewpoint is the Lattice facility at the Research Annex site var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Redlands Main") - XCTAssertEqual(selectedFacility?.name, "Q") + XCTAssertEqual(selectedSite?.name, "Research Annex") + XCTAssertEqual(selectedFacility?.name, "Lattice") - // Viewpoint is Los Angeles, but selection should remain Redlands Main Q _viewpoint = viewpointLosAngeles viewModel.makeAutoSelection() + + // Viewpoint is Los Angeles, but selection should remain Redlands Main Q selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Redlands Main") - XCTAssertEqual(selectedFacility?.name, "Q") + XCTAssertEqual(selectedSite?.name, "Research Annex") + XCTAssertEqual(selectedFacility?.name, "Lattice") } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. @@ -272,15 +244,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let viewpointLosAngeles = Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) + let viewpointLosAngeles: Viewpoint = .losAngeles var _viewpoint: Viewpoint? = viewpointLosAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( @@ -288,7 +252,8 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - await verifyInitialization(viewModel) + + viewModel.makeAutoSelection() // Viewpoint is Los Angeles, selection should be nil var selectedFacility = viewModel.selectedFacility @@ -296,9 +261,10 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - // Viewpoint is Redlands Main Q but selection should still be nil - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = .site_ResearchAnnex_facility_Lattice viewModel.makeAutoSelection() + + // Viewpoint is the Lattice facility at the Research Annex site selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) @@ -308,62 +274,42 @@ class FloorFilterViewModelTests: XCTestCase { /// Get a map constructed from an ArcGIS portal item. /// - Returns: A map constructed from an ArcGIS portal item. private func makeMap() async -> Map? { - // Multiple sites/facilities: Esri IST map with all buildings. -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - - // Redlands Campus map with multiple sites and facilities. - let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) - - // Single site (ESRI Redlands Main) and facility (Building L). -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) + let portal = Portal(url: URL(string: "https://www.arcgis.com/")!, isLoginRequired: false) + let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "b4b599a43a474d33946cf0df526426f5")!) let map = Map(item: portalItem) do { try await map.load() + try await map.floorManager?.load() } catch { XCTFail("\(#fileID), \(#function), \(#line), \(error.localizedDescription)") return nil } return map } - - /// Verifies that the `FloorFilterViewModel` has succesfully initialized. - /// - Parameter viewModel: The view model to analyze. - private func verifyInitialization(_ viewModel: FloorFilterViewModel) async { - let expectation = XCTestExpectation( - description: "View model successfully initialized" - ) - let subscription = viewModel.$isLoading - .sink { loading in - print(#function, loading) - if !loading { - expectation.fulfill() - } - } - Task { - wait(for: [expectation], timeout: 30.0) - subscription.cancel() - } - } } -extension FloorFilterViewModelTests { - /// The coordinates for the Redlands Esri campus. - var point: Point { - Point( - x: -13046157.242121734, - y: 4036329.622884897, - spatialReference: .webMercator +extension Viewpoint { + static var losAngeles: Viewpoint { + Viewpoint( + center: Point( + x: -13164116.3284, + y: 4034465.8065, + spatialReference: .webMercator + ), + scale: 10_000 ) } - /// Builds viewpoints to use for tests. - /// - Parameter rotation: The rotation to use for the resulting viewpoint. - /// - Returns: A viewpoint object for tests. - func getEsriRedlandsViewpoint(_ rotation: Double = .zero, scale: Double = 10_000) -> Viewpoint { - return Viewpoint(center: point, scale: scale, rotation: rotation) + static var site_ResearchAnnex_facility_Lattice: Viewpoint { + Viewpoint( + center: + Point( + x: -13045075.712950204, + y: 4036858.6146756615, + spatialReference: .webMercator + ), + scale: 550.0 + ) } } From 5d0013c86286c52dda9a8dc0a5af4e74b7243305 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Apr 2022 17:52:11 -0700 Subject: [PATCH 0771/1722] `Button` -> `Toggle` Delete Button.swift --- .../FloorFilter/LevelSelector.swift | 16 +++++-- Sources/ArcGISToolkit/Extensions/Button.swift | 43 ------------------- 2 files changed, 12 insertions(+), 47 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/Button.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 274c69641..e08110cb2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -77,13 +77,21 @@ struct LevelsStack: View { ScrollView { VStack { ForEach(levels) { level in - Button { - viewModel.setLevel(level) - } label: { + Toggle( + isOn: Binding( + get: { + viewModel.selectedLevel == level + }, + set: { newIsOn in + guard newIsOn else { return } + viewModel.setLevel(level) + } + ) + ) { Text(level.shortName) .lineLimit(1) } - .selected(level == viewModel.selectedLevel) + .toggleStyle(.button) } } .onSizeChange { diff --git a/Sources/ArcGISToolkit/Extensions/Button.swift b/Sources/ArcGISToolkit/Extensions/Button.swift deleted file mode 100644 index ed7cd9f21..000000000 --- a/Sources/ArcGISToolkit/Extensions/Button.swift +++ /dev/null @@ -1,43 +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 modifier which "selects" a button. If selected, the button will be displayed with the -/// `BorderedProminentButtonStyle`. Otherwise, the button will be displayed with the -/// `BorderedButtonStyle`. -struct ButtonSelectedModifier: ViewModifier { - /// A Boolean value that indicates whether view should display as selected. - var isSelected: Bool - - func body(content: Content) -> some View { - if isSelected { - content - .buttonStyle(BorderedProminentButtonStyle()) - } else { - content - .buttonStyle(BorderedButtonStyle()) - } - } -} - -extension Button { - /// Button modifier used to denote the button is selected. - /// - Parameter isSelected: `true` if the button is selected, `false` otherwise. - /// - Returns: The modified button. - func selected( - _ isSelected: Bool = false - ) -> some View { - modifier(ButtonSelectedModifier(isSelected: isSelected)) - } -} From eddb575730c61c7d69723b9f6515a9837028c11c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 09:54:37 -0700 Subject: [PATCH 0772/1722] `AutomaticSelectionMode` -> `FloorFilterAutomaticSelectionMode` --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 2 +- .../Components/FloorFilter/FloorFilterViewModel.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index d94e18e2a..1dc9c4b41 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -26,7 +26,7 @@ public struct FloorFilter: View { /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( alignment: Alignment, - automaticSelectionMode: AutomaticSelectionMode = .always, + automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, floorManager: FloorManager, viewpoint: Binding ) { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 32911c5e9..e6660f85e 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: AutomaticSelectionMode = .always, + automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, floorManager: FloorManager, viewpoint: Binding ) { @@ -79,7 +79,7 @@ final class FloorFilterViewModel: ObservableObject { // MARK: Constants /// The selection behavior of the floor filter. - private let automaticSelectionMode: AutomaticSelectionMode + private let automaticSelectionMode: FloorFilterAutomaticSelectionMode /// The amount of time to wait between selection updates. private let delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.20) @@ -309,7 +309,7 @@ final class FloorFilterViewModel: ObservableObject { } /// Defines automatic selection behavior. -public enum AutomaticSelectionMode { +public enum FloorFilterAutomaticSelectionMode { /// Always update selection based on the current viewpoint; clear the selection when the user /// navigates away. case always From d87c8ef3d952d3639599ddfd6372ce96b9de79f1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 10:10:36 -0700 Subject: [PATCH 0773/1722] Fix first set of tests --- .../BasemapGalleryViewModelTests.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 3d000848b..28907ea09 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -26,6 +26,11 @@ import Combine // accomodate those differences. @MainActor class BasemapGalleryViewModelTests: XCTestCase { + override func setUp() async throws { + #warning("Set your developer API key") + ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") + } + let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( basemap: Basemap( @@ -51,7 +56,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // // GeoModel. // - let geoModel = Map(basemap: .lightGrayCanvas()) + let geoModel = Map(basemapStyle: .arcGISLightGray) let geoModelViewModel = BasemapGalleryViewModel(geoModel: geoModel) XCTAssertIdentical(geoModelViewModel.geoModel, geoModel) @@ -71,7 +76,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // // Portal. // - let geoModel2 = Map(basemap: .lightGrayCanvas()) + let geoModel2 = Map(basemapStyle: .arcGISLightGray) let portal = Portal.arcGISOnline(isLoginRequired: false) let portalViewModel = BasemapGalleryViewModel(geoModel2, portal: portal) @@ -102,7 +107,7 @@ class BasemapGalleryViewModelTests: XCTestCase { // // BasemapGalleryItems. No basemaps are fetched from a portal. // - let geoModel3 = Map(basemap: .lightGrayCanvas()) + let geoModel3 = Map(basemapStyle: .arcGISLightGray) let itemsViewModel = BasemapGalleryViewModel( geoModel: geoModel3, items: defaultBasemapGalleryItems @@ -118,7 +123,7 @@ class BasemapGalleryViewModelTests: XCTestCase { /// Test the `GeoModel.actualSpatialReference` extension property. func testGeoModelActualSpatialReference() async throws { // Map with Web Mercator basemap. - let geoModel = Map(basemap: .lightGrayCanvas()) + let geoModel = Map(basemapStyle: .arcGISLightGray) try await geoModel.load() XCTAssertEqual(geoModel.actualSpatialReference, .webMercator) From e5e1a93e77fda40c3f1b772c30d073940a4203b9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 10:11:46 -0700 Subject: [PATCH 0774/1722] Update Basemap usage --- Documentation/OverviewMap/README.md | 6 ++---- Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 7c91f7215..bd1b620a0 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -72,11 +72,9 @@ var body: some View { Note that for a `SceneView`, you need to provide the `OverviewMap` only a viewpoint. ```swift -@StateObject -var scene = Scene(basemap: .imageryWithLabels()) +@StateObject var scene = Scene(basemapStyle: .arcGISImageryLabels) -@State -private var viewpoint: Viewpoint? +@State private var viewpoint: Viewpoint? var body: some View { SceneView(scene: scene) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 28907ea09..64be3f0bc 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -27,7 +27,6 @@ import Combine @MainActor class BasemapGalleryViewModelTests: XCTestCase { override func setUp() async throws { - #warning("Set your developer API key") ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") } @@ -165,7 +164,7 @@ class BasemapGalleryViewModelTests: XCTestCase { /// Test the `currentItem` property including valid and invalid basemaps. func testCurrentItem() async throws { - let basemap = Basemap.streets() + let basemap = Basemap(style: .arcGISStreets) let geoModel = Map(basemap: basemap) let viewModel = BasemapGalleryViewModel(geoModel: geoModel) From 83ed7b7367064fcb7aa26229123f6becbe1faab1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 10:50:31 -0700 Subject: [PATCH 0775/1722] Update picker styling By using Button consistently across the level and facility picker we get consistent style by using a common modifier Update SiteAndFacilitySelector.swift Update SiteAndFacilitySelector.swift Create Button.swift --- .../FloorFilter/LevelSelector.swift | 22 ++++------ .../FloorFilter/SiteAndFacilitySelector.swift | 30 ++++--------- Sources/ArcGISToolkit/Extensions/Button.swift | 43 +++++++++++++++++++ 3 files changed, 59 insertions(+), 36 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Button.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index d7104cc57..a304b0cd2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -53,11 +53,13 @@ struct LevelSelector: View { } } } else { - Toggle(isOn: $isCollapsed) { + Button { + isCollapsed.toggle() + } label: { Text(selectedLevelName) .lineLimit(1) } - .toggleStyle(.button) + .selected(true) } } } @@ -77,21 +79,13 @@ struct LevelsStack: View { ScrollView { VStack { ForEach(levels) { level in - Toggle( - isOn: Binding( - get: { - viewModel.selectedLevel == level - }, - set: { newIsOn in - guard newIsOn else { return } - viewModel.setLevel(level) - } - ) - ) { + Button { + viewModel.setLevel(level) + } label: { Text(level.shortName) .lineLimit(1) } - .toggleStyle(.button) + .selected(viewModel.selectedLevel == level) } } .onSizeChange { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 70749141e..878a377bb 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -193,8 +193,7 @@ struct SiteAndFacilitySelector: View { } /// A facility name filter phrase entered by the user. - @State - var searchPhrase: String = "" + @State var searchPhrase: String = "" /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] @@ -216,18 +215,6 @@ struct SiteAndFacilitySelector: View { } } - /// Determines the SF Symbols image name to represent selection/non-selection of a facility. - /// - Parameter facility: The facility of interest - /// - Returns: "circle.fill" if the facility is marked selected or "cirlce" if the facility is not selected - /// in the view model. - func imageFor(_ facility: FloorFacility) -> String { - if facility.id == viewModel.selectedFacility?.id { - return "circle.fill" - } else { - return "circle" - } - } - var body: some View { if presentationStyle == .singleSite { facilityListAndFilterView @@ -282,27 +269,26 @@ struct SiteAndFacilitySelector: View { viewModel.setFacility(facility, zoomTo: true) isHidden.wrappedValue.toggle() } label: { - HStack { - Image(systemName: imageFor(facility)) - VStack { - Text(facility.name) + VStack { + Text(facility.name) .fontWeight(.regular) .frame( maxWidth: .infinity, alignment: .leading ) - if presentationStyle == .allSites, - let siteName = facility.site?.name { - Text(siteName) + if presentationStyle == .allSites, + let siteName = facility.site?.name { + Text(siteName) .fontWeight(.ultraLight) .frame( maxWidth: .infinity, alignment: .leading ) - } } } } + .selected(facility.id == viewModel.selectedFacility?.id) + .listRowSeparator(.hidden) } .listStyle(.plain) .onChange(of: viewModel.selection) { _ in diff --git a/Sources/ArcGISToolkit/Extensions/Button.swift b/Sources/ArcGISToolkit/Extensions/Button.swift new file mode 100644 index 000000000..ed7cd9f21 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Button.swift @@ -0,0 +1,43 @@ +// 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 modifier which "selects" a button. If selected, the button will be displayed with the +/// `BorderedProminentButtonStyle`. Otherwise, the button will be displayed with the +/// `BorderedButtonStyle`. +struct ButtonSelectedModifier: ViewModifier { + /// A Boolean value that indicates whether view should display as selected. + var isSelected: Bool + + func body(content: Content) -> some View { + if isSelected { + content + .buttonStyle(BorderedProminentButtonStyle()) + } else { + content + .buttonStyle(BorderedButtonStyle()) + } + } +} + +extension Button { + /// Button modifier used to denote the button is selected. + /// - Parameter isSelected: `true` if the button is selected, `false` otherwise. + /// - Returns: The modified button. + func selected( + _ isSelected: Bool = false + ) -> some View { + modifier(ButtonSelectedModifier(isSelected: isSelected)) + } +} From de6f749c252f724bd370f6f5dc2bcea3c8e65de3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 11:58:10 -0700 Subject: [PATCH 0776/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 878a377bb..6c1ae55c4 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -126,21 +126,24 @@ struct SiteAndFacilitySelector: View { } else { siteListView } - NavigationLink("All sites") { - FacilitiesList( - facilities: sites.flatMap({ $0.facilities }), - presentationStyle: .allSites, - isHidden: isHidden - ) - } - .padding([.vertical], 4) } .navigationBarTitle( Text("Select a site"), displayMode: .inline ) .toolbar { - CloseButton { isHidden.wrappedValue.toggle() } + ToolbarItem { + CloseButton { isHidden.wrappedValue.toggle() } + } + ToolbarItem(placement: .bottomBar) { + NavigationLink("All sites") { + FacilitiesList( + facilities: sites.flatMap({ $0.facilities }), + presentationStyle: .allSites, + isHidden: isHidden + ) + } + } } } } From 49d9026c58ab166549a75dbbc212bf8097817383 Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Wed, 13 Apr 2022 12:55:20 -0700 Subject: [PATCH 0777/1722] fix build --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 6 ++++++ .../ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index cb1f67b81..1511bd7e2 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -183,3 +183,9 @@ private extension Symbol { ) ) } + +extension Symbol: Equatable { + public static func == (lhs: Symbol, rhs: Symbol) -> Bool { + lhs === rhs + } +} diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 17aabf8da..02be4e232 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -113,7 +113,7 @@ public class SearchViewModel: ObservableObject { guard !isEligibleForRequery else { return } // Check center difference. - let centerDiff = GeometryEngine.distance( + let centerDiff = try? GeometryEngine.distance( from: lastExtent.center, to: newExtent.center ) From 6cd10bf761992ddf48ffef76d5f86c26e34f79c3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 12:37:47 -0700 Subject: [PATCH 0778/1722] Add searchable Update SiteAndFacilitySelector.swift Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 6c1ae55c4..e134f99ad 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -117,16 +117,20 @@ struct SiteAndFacilitySelector: View { /// A view containing a filter-via-name field, a list of the site names and an "All sites" button. var siteListAndFilterView: some View { NavigationView { - VStack { - TextField("Filter sites", text: $searchPhrase) - .keyboardType(.alphabet) - .disableAutocorrection(true) + Group { if matchingSites.isEmpty { NoMatchesView() } else { siteListView } } + .searchable( + text: $searchPhrase, + placement: .navigationBarDrawer, + prompt: "Filter sites" + ) + .keyboardType(.alphabet) + .disableAutocorrection(true) .navigationBarTitle( Text("Select a site"), displayMode: .inline @@ -246,15 +250,19 @@ struct SiteAndFacilitySelector: View { CloseButton { isHidden.wrappedValue.toggle() } } } - TextField("Filter facilities", text: $searchPhrase) - .keyboardType(.alphabet) - .disableAutocorrection(true) if matchingFacilities.isEmpty { NoMatchesView() } else { facilityListView } } + .searchable( + text: $searchPhrase, + placement: .navigationBarDrawer, + prompt: "Filter facilities" + ) + .keyboardType(.alphabet) + .disableAutocorrection(true) } /// Displays a list of facilities matching the filter criteria as determined by @@ -312,11 +320,11 @@ struct SiteAndFacilitySelector: View { /// Displays text "No matches found". struct NoMatchesView: View { var body: some View { - VStack { - Spacer() - Text("No matches found") - Spacer() - } + Text("No matches found") + .frame( + maxHeight: .infinity, + alignment: .center + ) } } From 4b7ad5607e381cfb3fb5e4a5827d98cf8af13ffc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 17:44:33 -0700 Subject: [PATCH 0779/1722] Fix site selection --- .../FloorFilter/SiteAndFacilitySelector.swift | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 8fcc8d9fe..3e22df708 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -62,14 +62,7 @@ struct SiteAndFacilitySelector: View { /// As the view model's selection will change to `.facility(FloorFacility)` and /// `.level(FloorLevel)` over time, this is needed to keep track of the site at the top of the /// hierarchy to keep the site selection persistent in the navigation view. - @State private var selectedSite: FloorSite? { - didSet { - if updateViewModel, let site = selectedSite { - viewModel.setSite(site, zoomTo: true) - } - updateViewModel = true - } - } + @State private var selectedSite: FloorSite? /// Sites contained in a `FloorManager`. let sites: [FloorSite] @@ -162,9 +155,6 @@ struct SiteAndFacilitySelector: View { isHidden: isHidden ) } - .onTapGesture { - selectedSite = site - } } .listStyle(.plain) .onChange(of: viewModel.selection) { _ in @@ -174,6 +164,12 @@ struct SiteAndFacilitySelector: View { updateViewModel = false selectedSite = viewModel.selectedSite } + .onChange(of: selectedSite) { _ in + if updateViewModel, let site = selectedSite { + viewModel.setSite(site, zoomTo: true) + } + updateViewModel = true + } } } From 72b415deffce14013b5dd4061c88288a6f93bb2c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 17:52:51 -0700 Subject: [PATCH 0780/1722] Fix empty bottom toolbar --- .../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 453c0fc15..a9ea586df 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -161,6 +161,7 @@ struct SiteAndFacilitySelector: View { presentationStyle: .standard, isHidden: isHidden ) + .navigationViewStyle(StackNavigationViewStyle()) } } .listStyle(.plain) From bcb2d005495b7334c519a316e8a9f438e3b91bd4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 18:47:35 -0700 Subject: [PATCH 0781/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index a9ea586df..69d6ce429 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -111,10 +111,19 @@ struct SiteAndFacilitySelector: View { var siteListAndFilterView: some View { NavigationView { Group { - if matchingSites.isEmpty { - NoMatchesView() - } else { - siteListView + VStack { + if matchingSites.isEmpty { + NoMatchesView() + } else { + siteListView + } + NavigationLink("All sites") { + FacilitiesList( + facilities: sites.flatMap({ $0.facilities }), + presentationStyle: .allSites, + isHidden: isHidden + ) + } } } .searchable( @@ -129,18 +138,7 @@ struct SiteAndFacilitySelector: View { displayMode: .inline ) .toolbar { - ToolbarItem { - CloseButton { isHidden.wrappedValue.toggle() } - } - ToolbarItem(placement: .bottomBar) { - NavigationLink("All sites") { - FacilitiesList( - facilities: sites.flatMap({ $0.facilities }), - presentationStyle: .allSites, - isHidden: isHidden - ) - } - } + CloseButton { isHidden.wrappedValue.toggle() } } } } @@ -161,7 +159,6 @@ struct SiteAndFacilitySelector: View { presentationStyle: .standard, isHidden: isHidden ) - .navigationViewStyle(StackNavigationViewStyle()) } } .listStyle(.plain) From eeaac1962cbcd0338c94eda2a49612d8c3b02af1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Apr 2022 19:01:20 -0700 Subject: [PATCH 0782/1722] Update BasemapGalleryItemTests.swift --- .../ArcGISToolkitTests/BasemapGalleryItemTests.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index b43f5da36..651589e2c 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -26,22 +26,26 @@ import Combine // test design have been added to accomodate those differences. @MainActor final class BasemapGalleryItemTests: XCTestCase { + override func setUp() async throws { + ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") + } + func testInit() async throws { - let basemap = Basemap.lightGrayCanvas() + let basemap = Basemap(style: .arcGISLightGray) let item = BasemapGalleryItem(basemap: basemap) let isBasemapLoading = try await item.$isBasemapLoading.dropFirst().first let loading = try XCTUnwrap(isBasemapLoading) XCTAssertFalse(loading, "Item is not loading.") XCTAssertIdentical(item.basemap, basemap) - XCTAssertEqual(item.name, "Light Gray Canvas") + XCTAssertEqual(item.name, "ArcGIS:LightGray") XCTAssertNil(item.description) XCTAssertNotNil(item.thumbnail) XCTAssertNil(item.loadBasemapError) // Test with overrides. let thumbnail = UIImage(systemName: "magnifyingglass")! - let basemap2 = Basemap.lightGrayCanvas() + let basemap2 = Basemap(style: .arcGISLightGray) let item2 = BasemapGalleryItem( basemap: basemap2, name: "My Basemap", @@ -92,7 +96,7 @@ final class BasemapGalleryItemTests: XCTestCase { } func testSpatialReferenceAndStatus() async throws { - let basemap = Basemap.lightGrayCanvas() + let basemap = Basemap(style: .arcGISLightGray) let item = BasemapGalleryItem(basemap: basemap) let isBasemapLoading = try await item.$isBasemapLoading.dropFirst().first From ca87284c5e3201bb4a5038069fdc7811cc52a69a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 14 Apr 2022 10:04:38 -0700 Subject: [PATCH 0783/1722] Updates --- .../Examples/FloorFilterExampleView.swift | 2 +- .../Components/FloorFilter/FloorFilter.swift | 59 +++++++--- .../FloorFilter/FloorFilterViewModel.swift | 2 +- .../FloorFilter/LevelSelector.swift | 34 ++++-- .../FloorFilter/SiteAndFacilitySelector.swift | 104 ++++++------------ Sources/ArcGISToolkit/Extensions/View.swift | 37 +++++++ 6 files changed, 143 insertions(+), 95 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index f28f8bb56..f68abd0c5 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. - private let filterAlignment = Alignment.bottomLeading + private let filterAlignment: Alignment = .bottomLeading /// Determines the appropriate time to initialize the `FloorFilter`. @State private var isMapLoaded: Bool = false diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 1dc9c4b41..fb6ba981c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -18,6 +18,18 @@ import ArcGIS /// 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? + + @Environment(\.verticalSizeClass) + private var verticalSizeClass: UserInterfaceSizeClass? + + /// If `true`, the site and facility selector will appear as a sheet. + /// If `false`, the site and facility selector will appear as a popup modal alongside the level selector. + private var isCompact: Bool { + return horizontalSizeClass == .compact || verticalSizeClass == .compact + } + /// Creates a `FloorFilter` /// - Parameters: /// - alignment: Determines the display configuration of Floor Filter elements. @@ -45,25 +57,37 @@ public struct FloorFilter: View { /// A Boolean value that indicates whether the levels view is currently collapsed. @State private var isLevelsViewCollapsed: Bool = false - /// A Boolean value that indicates whether the site/facility selector is hidden. - @State private var isSelectorHidden: Bool = true + /// A Boolean value that indicates whether the site and facility selector is presented. + @State private var siteAndFacilitySelectorIsPresented: Bool = false /// The alignment configuration. private let alignment: Alignment + /// The width for buttons in the level selector. + private let buttonWidth = 30.0 + /// Button to open and close the site and facility selector. private var facilityButtonView: some View { Button { - isSelectorHidden.toggle() + siteAndFacilitySelectorIsPresented.toggle() } label: { Image(systemName: "building.2") + .frame(width: buttonWidth) + .padding(.horizontal) + } + .sheet( + isAllowed: isCompact, + isPresented: $siteAndFacilitySelectorIsPresented + ) { + SiteAndFacilitySelector(isHidden: $siteAndFacilitySelectorIsPresented) + .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in + viewModel.viewpointSubject.send(viewpoint.wrappedValue) + } } - .frame(maxWidth: .infinity) } /// Displays the available levels. - @ViewBuilder - private var levelsSelectorView: some View { + @ViewBuilder private var levelsSelectorView: some View { if viewModel.hasLevelsToDisplay { if topAligned { Divider() @@ -71,8 +95,9 @@ public struct FloorFilter: View { } LevelSelector( isCollapsed: $isLevelsViewCollapsed, - levels: viewModel.sortedLevels, - isTopAligned: topAligned + buttonWidth: buttonWidth, + isTopAligned: topAligned, + levels: viewModel.sortedLevels ) if !topAligned { Divider() @@ -96,12 +121,12 @@ public struct FloorFilter: View { facilityButtonView } } - .esriBorder() + .esriBorder() if topAligned { Spacer() } } - .frame(width: 75) + .frame(maxWidth: isCompact ? .infinity : nil, alignment: alignment) } /// Indicates that the selector should be presented with a right oriented aligment configuration. @@ -116,12 +141,16 @@ public struct FloorFilter: View { /// A configured `SiteAndFacilitySelector` view. private var siteAndFacilitySelectorView: some View { - SiteAndFacilitySelector(isHidden: $isSelectorHidden) - .esriBorder() - .opacity(isSelectorHidden ? .zero : 1) - .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in - viewModel.viewpointSubject.send(viewpoint.wrappedValue) + Group { + if !isCompact { + SiteAndFacilitySelector(isHidden: $siteAndFacilitySelectorIsPresented) + .esriBorder() + .opacity(siteAndFacilitySelectorIsPresented ? 1 : .zero) + .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in + viewModel.viewpointSubject.send(viewpoint.wrappedValue) + } } + } } /// Indicates that the selector should be presented with a top oriented aligment configuration. diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 440f0b8dd..98de8ceb6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -296,7 +296,7 @@ final class FloorFilterViewModel: ObservableObject { if sites.count == 1, let firstSite = sites.first { // If we have only one site, select it. - setSite(firstSite) + setSite(firstSite, zoomTo: true) } } catch { print("error: \(error)") diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index a304b0cd2..c003c9e20 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -24,6 +24,12 @@ struct LevelSelector: View { /// the selected level. @Binding var isCollapsed: Bool + /// The width for buttons in the level selector. + let buttonWidth: Double + + /// The alignment configuration. + let isTopAligned: Bool + /// The levels to display. let levels: [FloorLevel] @@ -33,23 +39,29 @@ struct LevelSelector: View { viewModel.selectedLevel?.shortName ?? "" } - /// The alignment configuration. - var isTopAligned: Bool - public var body: some View { if !isCollapsed, levels.count > 1 { VStack { if !isTopAligned { - CollapseButton(isCollapsed: $isCollapsed) + CollapseButton( + isCollapsed: $isCollapsed, + isTopAligned: isTopAligned + ) Divider() .frame(width: 30) } - LevelsStack(levels: levels) + LevelsStack( + buttonWidth: buttonWidth, + levels: levels + ) if isTopAligned { Divider() .frame(width: 30) - CollapseButton(isCollapsed: $isCollapsed) + CollapseButton( + isCollapsed: $isCollapsed, + isTopAligned: isTopAligned + ) } } } else { @@ -58,6 +70,7 @@ struct LevelSelector: View { } label: { Text(selectedLevelName) .lineLimit(1) + .frame(width: buttonWidth) } .selected(true) } @@ -72,6 +85,9 @@ struct LevelsStack: View { /// The height of the scroll view's content. @State private var contentHeight: CGFloat = .zero + /// The width for buttons in the level selector. + let buttonWidth: Double + /// The levels to display. let levels: [FloorLevel] @@ -84,6 +100,7 @@ struct LevelsStack: View { } label: { Text(level.shortName) .lineLimit(1) + .frame(width: buttonWidth) } .selected(viewModel.selectedLevel == level) } @@ -101,13 +118,16 @@ struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. @Binding var isCollapsed: Bool + /// The alignment configuration. + let isTopAligned: Bool + var body: some View { Button { withAnimation { isCollapsed.toggle() } } label: { - Image(systemName: "chevron.down.circle") + Image(systemName: isTopAligned ? "chevron.up.circle" : "chevron.down.circle") } .padding(EdgeInsets( top: 2, diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 69d6ce429..d19f8e45c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -29,18 +29,7 @@ struct SiteAndFacilitySelector: View { private var isHidden: Binding var body: some View { - if viewModel.sites.count == 1 { - FacilitiesList( - facilities: viewModel.sites.first!.facilities, - presentationStyle: .singleSite, - isHidden: isHidden - ) - } else { - SitesList( - sites: viewModel.sites, - isHidden: isHidden - ) - } + SitesList(isHidden: isHidden) } /// A view displaying the sites contained in a `FloorManager`. @@ -64,9 +53,6 @@ struct SiteAndFacilitySelector: View { /// hierarchy to keep the site selection persistent in the navigation view. @State private var selectedSite: FloorSite? - /// Sites contained in a `FloorManager`. - let sites: [FloorSite] - /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding @@ -74,9 +60,9 @@ struct SiteAndFacilitySelector: View { /// `searchPhrase` is empty. var matchingSites: [FloorSite] { if searchPhrase.isEmpty { - return sites + return viewModel.sites } - return sites.filter { + return viewModel.sites.filter { $0.name.lowercased().contains(searchPhrase.lowercased()) } } @@ -110,17 +96,24 @@ struct SiteAndFacilitySelector: View { /// A view containing a filter-via-name field, a list of the site names and an "All sites" button. var siteListAndFilterView: some View { NavigationView { - Group { - VStack { - if matchingSites.isEmpty { - NoMatchesView() - } else { - siteListView - } + 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( - facilities: sites.flatMap({ $0.facilities }), - presentationStyle: .allSites, + allSiteStyle: true, + facilities: viewModel.sites.flatMap({ $0.facilities }), isHidden: isHidden ) } @@ -128,7 +121,7 @@ struct SiteAndFacilitySelector: View { } .searchable( text: $searchPhrase, - placement: .navigationBarDrawer, + placement: .navigationBarDrawer(displayMode: .always), prompt: "Filter sites" ) .keyboardType(.alphabet) @@ -155,8 +148,8 @@ struct SiteAndFacilitySelector: View { selection: $selectedSite ) { FacilitiesList( + allSiteStyle: false, facilities: site.facilities, - presentationStyle: .standard, isHidden: isHidden ) } @@ -183,25 +176,15 @@ struct SiteAndFacilitySelector: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel - /// Presentation styles for the facility list. - enum PresentationStyle { - /// A specific site was selected and the body is presented within a navigation view. - case standard - /// The all sites button was selcted and the body is presented within a navigation view. - case allSites - /// Only one site exists and the body is not presented within a navigation view. - case singleSite - } - /// A facility name filter phrase entered by the user. @State var searchPhrase: String = "" + /// When `true`, the facilites list will be display with all sites styling. + let allSiteStyle: Bool + /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] - /// The selected presentation style for the facility list. - let presentationStyle: PresentationStyle - /// Allows the user to toggle the visibility of the site and facility selector. var isHidden: Binding @@ -217,33 +200,7 @@ struct SiteAndFacilitySelector: View { } var body: some View { - if presentationStyle == .singleSite { - facilityListAndFilterView - } else { - facilityListAndFilterView - // Only apply navigation modifiers if this is displayed - // within a navigation view - .navigationBarTitle("Select a facility") - .toolbar { - CloseButton { isHidden.wrappedValue.toggle() } - } - } - } - - /// A view containing a label for the site name, a filter-via-name bar and a list of the facility names. - var facilityListAndFilterView: some View { - VStack { - HStack { - if presentationStyle == .standard { - Text(facilities.first?.site?.name ?? "N/A") - } else if presentationStyle == .allSites { - Text("All sites") - } else if presentationStyle == .singleSite { - Text(facilities.first?.site?.name ?? "N/A") - Spacer() - CloseButton { isHidden.wrappedValue.toggle() } - } - } + Group { if matchingFacilities.isEmpty { NoMatchesView() } else { @@ -252,11 +209,17 @@ struct SiteAndFacilitySelector: View { } .searchable( text: $searchPhrase, - placement: .navigationBarDrawer, + placement: .navigationBarDrawer(displayMode: .always), prompt: "Filter facilities" ) .keyboardType(.alphabet) .disableAutocorrection(true) + .navigationBarTitle( + allSiteStyle ? "All sites" : viewModel.selectedSite?.name ?? "Select a facility" + ) + .toolbar { + CloseButton { isHidden.wrappedValue.toggle() } + } } /// Displays a list of facilities matching the filter criteria as determined by @@ -281,8 +244,7 @@ struct SiteAndFacilitySelector: View { maxWidth: .infinity, alignment: .leading ) - if presentationStyle == .allSites, - let siteName = facility.site?.name { + if allSiteStyle, let siteName = facility.site?.name { Text(siteName) .fontWeight(.ultraLight) .frame( diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index b85bbf53d..ce829075c 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -13,6 +13,27 @@ import SwiftUI +/// A modifier which proivdes conditional control over when a sheet is used. +struct ConditionalSheetModifier: ViewModifier { + /// A Boolean value that indicates whether `sheetContent` will be presented or not. + var isAllowed: Bool + + /// Determines when the sheet is presented or not. + var isPresented: Binding + + /// Content to be shown in the sheet. + var sheetContent: () -> SheetContent + + func body(content: Content) -> some View { + if isAllowed { + content + .sheet(isPresented: isPresented, content: sheetContent) + } else { + content + } + } +} + /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { /// A Boolean value that indicates whether view should display as selected. @@ -58,4 +79,20 @@ extension View { ) -> some View { modifier(SelectedModifier(isSelected: isSelected)) } + + /// - Parameter isAllowed: Condition that determines if this sheet can be shown. + /// - Returns: Produces a sheet that is only shown if `isAllowed` is set `true`. + func sheet( + isAllowed: Bool, + isPresented: Binding, + content: @escaping () -> SheetContent + ) -> some View { + modifier( + ConditionalSheetModifier( + isAllowed: isAllowed, + isPresented: isPresented, + sheetContent: content + ) + ) + } } From 76c2f1db8537ac0788f4d46eb43d6da912790fb3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Apr 2022 09:51:04 -0700 Subject: [PATCH 0784/1722] Updates --- .../Examples/FloorFilterExampleView.swift | 4 +- .../Components/FloorFilter/FloorFilter.swift | 12 +++--- .../FloorFilter/LevelSelector.swift | 37 ++++++++++++------- .../FloorFilter/SiteAndFacilitySelector.swift | 6 +-- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index f68abd0c5..4e37070f4 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -62,8 +62,8 @@ struct FloorFilterExampleView: View { viewpoint: $viewpoint ) .frame( - maxWidth: 300, - maxHeight: 300 + maxWidth: 400, + maxHeight: 400 ) .padding(36) } else if mapLoadError { diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index fb6ba981c..66d9093b9 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -40,6 +40,7 @@ public struct FloorFilter: View { alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, floorManager: FloorManager, + levelSelectorWidth: Double = 30.0, viewpoint: Binding ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( @@ -48,6 +49,7 @@ public struct FloorFilter: View { viewpoint: viewpoint )) self.alignment = alignment + self.levelSelectorWidth = levelSelectorWidth self.viewpoint = viewpoint } @@ -64,7 +66,7 @@ public struct FloorFilter: View { private let alignment: Alignment /// The width for buttons in the level selector. - private let buttonWidth = 30.0 + private let levelSelectorWidth: Double /// Button to open and close the site and facility selector. private var facilityButtonView: some View { @@ -72,7 +74,7 @@ public struct FloorFilter: View { siteAndFacilitySelectorIsPresented.toggle() } label: { Image(systemName: "building.2") - .frame(width: buttonWidth) + .frame(width: levelSelectorWidth) .padding(.horizontal) } .sheet( @@ -91,17 +93,17 @@ public struct FloorFilter: View { if viewModel.hasLevelsToDisplay { if topAligned { Divider() - .frame(width: 30) + .frame(width: levelSelectorWidth) } LevelSelector( isCollapsed: $isLevelsViewCollapsed, - buttonWidth: buttonWidth, + buttonWidth: levelSelectorWidth, isTopAligned: topAligned, levels: viewModel.sortedLevels ) if !topAligned { Divider() - .frame(width: 30) + .frame(width: levelSelectorWidth) } } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index c003c9e20..5e11d4fcb 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -92,24 +92,35 @@ struct LevelsStack: View { let levels: [FloorLevel] var body: some View { - ScrollView { - VStack { - ForEach(levels) { level in - Button { - viewModel.setLevel(level) - } label: { - Text(level.shortName) - .lineLimit(1) - .frame(width: buttonWidth) + ScrollViewReader { proxy in + ScrollView { + VStack { + ForEach(levels, id: \.id) { level in + Button { + viewModel.setLevel(level) + } label: { + Text(level.shortName) + .lineLimit(1) + .frame(width: buttonWidth) + } + .selected(viewModel.selectedLevel == level) } - .selected(viewModel.selectedLevel == level) + } + .onSizeChange { + contentHeight = $0.height } } - .onSizeChange { - contentHeight = $0.height + .frame(maxHeight: contentHeight) + .onAppear { + if let floorLevel = viewModel.selectedLevel { + withAnimation { + proxy.scrollTo( + floorLevel.id + ) + } + } } } - .frame(maxHeight: contentHeight) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index d19f8e45c..b0f4a860b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -117,6 +117,7 @@ struct SiteAndFacilitySelector: View { isHidden: isHidden ) } + .buttonStyle(.bordered) } } .searchable( @@ -246,7 +247,7 @@ struct SiteAndFacilitySelector: View { ) if allSiteStyle, let siteName = facility.site?.name { Text(siteName) - .fontWeight(.ultraLight) + .fontWeight(.light) .frame( maxWidth: .infinity, alignment: .leading @@ -262,8 +263,7 @@ struct SiteAndFacilitySelector: View { if let floorFacility = viewModel.selectedFacility { withAnimation { proxy.scrollTo( - floorFacility.id, - anchor: .center + floorFacility.id ) } } From 8638d04a6c56d920f5cb96bc4407427306d6b3d8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Apr 2022 15:39:35 -0700 Subject: [PATCH 0785/1722] Updates Update FloorFilter.swift Remove `isLevelsViewCollapsed` `facilityButtonView` -> `sitesAndFacilitiesButton` `rightAligned` -> `trailingAligned` Remove `trailingAligned`, simplify `topAligned` Update FloorFilter.swift Update View.swift Update FloorFilter.swift Relocate `FloorFilterAutomaticSelectionMode` `navigationBarTitle` -> `navigationTitle` Update SiteAndFacilitySelector.swift Update SiteAndFacilitySelector.swift Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/FloorFilter.swift | 109 ++++++------------ .../FloorFilterAutomaticSelectionMode.swift | 24 ++++ .../FloorFilter/FloorFilterViewModel.swift | 12 -- .../FloorFilter/LevelSelector.swift | 2 +- .../FloorFilter/SiteAndFacilitySelector.swift | 33 +++--- Sources/ArcGISToolkit/Extensions/View.swift | 27 ++++- 6 files changed, 99 insertions(+), 108 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 1dc9c4b41..16478bf34 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -28,7 +28,7 @@ public struct FloorFilter: View { alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, floorManager: FloorManager, - viewpoint: Binding + viewpoint: Binding = .constant(nil) ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( automaticSelectionMode: automaticSelectionMode, @@ -42,19 +42,16 @@ 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: Bool = false - - /// A Boolean value that indicates whether the site/facility selector is hidden. - @State private var isSelectorHidden: Bool = true + /// A Boolean value that indicates whether the site and facility selector is hidden. + @State private var isSitesAndFacilitiesHidden: Bool = true /// The alignment configuration. private let alignment: Alignment /// Button to open and close the site and facility selector. - private var facilityButtonView: some View { + private var sitesAndFacilitiesButton: some View { Button { - isSelectorHidden.toggle() + isSitesAndFacilitiesHidden.toggle() } label: { Image(systemName: "building.2") } @@ -63,62 +60,39 @@ public struct FloorFilter: View { /// Displays the available levels. @ViewBuilder - private var levelsSelectorView: some View { - if viewModel.hasLevelsToDisplay { - if topAligned { - Divider() - .frame(width: 30) - } - LevelSelector( - isCollapsed: $isLevelsViewCollapsed, - levels: viewModel.sortedLevels, - isTopAligned: topAligned - ) - if !topAligned { - Divider() - .frame(width: 30) - } - } + private var levelSelector: some View { + LevelSelector( + levels: viewModel.sortedLevels, + isTopAligned: topAligned + ) + .hidden(!viewModel.hasLevelsToDisplay) } /// A view that allows selecting between levels. - private var levelSelectorView: some View { + private var floorFilter: some View { VStack { - if !topAligned { - Spacer() - } - VStack { - if topAligned { - facilityButtonView - levelsSelectorView - } else { - levelsSelectorView - facilityButtonView - } - } - .esriBorder() if topAligned { - Spacer() + sitesAndFacilitiesButton + Divider() + .hidden(!viewModel.hasLevelsToDisplay) + levelSelector + } else { + levelSelector + Divider() + .hidden(!viewModel.hasLevelsToDisplay) + sitesAndFacilitiesButton } } + .esriBorder() .frame(width: 75) - } - - /// Indicates that the selector should be presented with a right oriented aligment configuration. - private var rightAligned: Bool { - switch alignment { - case .topTrailing, .trailing, .bottomTrailing: - return true - default: - return false - } + .frame(maxHeight: .infinity, alignment: alignment) } /// A configured `SiteAndFacilitySelector` view. - private var siteAndFacilitySelectorView: some View { - SiteAndFacilitySelector(isHidden: $isSelectorHidden) + private var siteAndFacilitySelector: some View { + SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) .esriBorder() - .opacity(isSelectorHidden ? .zero : 1) + .opacity(isSitesAndFacilitiesHidden ? .zero : 1) .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in viewModel.viewpointSubject.send(viewpoint.wrappedValue) } @@ -126,12 +100,7 @@ public struct FloorFilter: View { /// Indicates that the selector should be presented with a top oriented aligment configuration. private var topAligned: Bool { - switch alignment { - case .topLeading, .top, .topTrailing: - return true - default: - return false - } + alignment.vertical == .top } /// The `Viewpoint` used to pan/zoom to the selected site/facilty. @@ -139,28 +108,18 @@ public struct FloorFilter: View { private var viewpoint: Binding public var body: some View { - Group { - if viewModel.isLoading { - ProgressView() - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: alignment - ) + HStack(alignment: .bottom) { + if alignment.horizontal == .trailing { + siteAndFacilitySelector + floorFilter } else { - HStack(alignment: .bottom) { - if rightAligned { - siteAndFacilitySelectorView - levelSelectorView - } else { - levelSelectorView - siteAndFacilitySelectorView - } - } + floorFilter + siteAndFacilitySelector } } // Ensure space for filter text field on small screens in landscape .frame(minHeight: 100) .environmentObject(viewModel) + .disabled(viewModel.isLoading) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift new file mode 100644 index 000000000..177b985f4 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift @@ -0,0 +1,24 @@ +// 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. + +/// Defines automatic selection behavior. +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. + case alwaysNotClearing + /// Never update selection based on the GeoView's current viewpoint. + case never +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index e6660f85e..0e5e72ec9 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -308,16 +308,4 @@ final class FloorFilterViewModel: ObservableObject { private var viewpointSubscription: AnyCancellable? } -/// Defines automatic selection behavior. -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. - case alwaysNotClearing - /// Never update selection based on the GeoView's current viewpoint. - case never -} - extension FloorFilterViewModel.Selection: Hashable { } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index e08110cb2..5967fec9c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -22,7 +22,7 @@ struct LevelSelector: View { /// 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. - @Binding var isCollapsed: Bool + @State var isCollapsed: Bool = false /// The levels to display. let levels: [FloorLevel] diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 3e22df708..efb7940a4 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -128,13 +128,13 @@ struct SiteAndFacilitySelector: View { } .padding([.vertical], 4) } - .navigationBarTitle( - Text("Select a site"), - displayMode: .inline - ) - .navigationBarItems(trailing: - CloseButton { isHidden.wrappedValue.toggle() } - ) + .navigationTitle("Select a site") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + CloseButton { isHidden.wrappedValue.toggle() } + } + } } } @@ -189,8 +189,7 @@ struct SiteAndFacilitySelector: View { } /// A facility name filter phrase entered by the user. - @State - var searchPhrase: String = "" + @State var searchPhrase: String = "" /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] @@ -199,7 +198,7 @@ struct SiteAndFacilitySelector: View { let presentationStyle: PresentationStyle /// Allows the user to toggle the visibility of the site and facility selector. - var isHidden: Binding + @Binding var isHidden: Bool /// A subset of `facilities` with names containing `searchPhrase` or all /// `facilities` if `searchPhrase` is empty. @@ -231,10 +230,12 @@ struct SiteAndFacilitySelector: View { facilityListAndFilterView // Only apply navigation modifiers if this is displayed // within a navigation view - .navigationBarTitle("Select a facility") - .navigationBarItems(trailing: - CloseButton { isHidden.wrappedValue.toggle() } - ) + .navigationTitle("Select a facility") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + CloseButton { isHidden.toggle() } + } + } } } @@ -249,7 +250,7 @@ struct SiteAndFacilitySelector: View { } else if presentationStyle == .singleSite { Text(facilities.first?.site?.name ?? "N/A") Spacer() - CloseButton { isHidden.wrappedValue.toggle() } + CloseButton { isHidden.toggle() } } } TextField("Filter facilities", text: $searchPhrase) @@ -276,7 +277,7 @@ struct SiteAndFacilitySelector: View { List(matchingFacilities, id: \.id) { facility in Button { viewModel.setFacility(facility, zoomTo: true) - isHidden.wrappedValue.toggle() + isHidden.toggle() } label: { HStack { Image(systemName: imageFor(facility)) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index b85bbf53d..837056e86 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -13,6 +13,20 @@ import SwiftUI +/// A modifier which conditionally hides its underlying view. +struct ConditionallyHiddenModifier: ViewModifier { + /// A Boolean value that indicates whether the underlying view should be hidden. + var isHidden: Bool + + func body(content: Content) -> some View { + if isHidden { + EmptyView() + } else { + content + } + } +} + /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { /// A Boolean value that indicates whether view should display as selected. @@ -34,6 +48,13 @@ struct SelectedModifier: ViewModifier { } extension View { + /// View modifier used to conditionally hide a view. + /// - Parameter condition: A value of `true` will hide the underlying view. + /// - Returns: A new `View`. + func hidden(_ condition: Bool) -> some View { + modifier(ConditionallyHiddenModifier(isHidden: condition)) + } + /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. /// - Parameter perform: The closure to be executed when the content size of the receiver /// changes. @@ -52,10 +73,8 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: The modified view. - func selected( - _ isSelected: Bool = false - ) -> some View { + /// - Returns: A new `View`. + func selected(_ isSelected: Bool = false) -> some View { modifier(SelectedModifier(isSelected: isSelected)) } } From d4f6744626e5307ebe58d6cb4db94556060474c2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Apr 2022 08:37:44 -0700 Subject: [PATCH 0786/1722] Updates --- Examples/Examples/FloorFilterExampleView.swift | 2 +- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index f52de6916..132af9dc4 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -67,8 +67,8 @@ struct FloorFilterExampleView: View { if isMapLoaded, let floorManager = map.floorManager { FloorFilter( - alignment: filterAlignment, floorManager: floorManager, + alignment: filterAlignment, viewpoint: $viewpoint ) .frame(maxWidth: 300, maxHeight: 300) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 16478bf34..644f82b03 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -25,9 +25,9 @@ public struct FloorFilter: View { /// - floorManager: The floor manager used by the `FloorFilter`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( + floorManager: FloorManager, alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, - floorManager: FloorManager, viewpoint: Binding = .constant(nil) ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( From fc84a5598c00b15f5a0e5eb32654336232a6ba3e Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Mon, 18 Apr 2022 09:22:02 -0700 Subject: [PATCH 0787/1722] remove `Equatable` conformance --- Sources/ArcGISToolkit/Components/OverviewMap.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 1511bd7e2..cb1f67b81 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -183,9 +183,3 @@ private extension Symbol { ) ) } - -extension Symbol: Equatable { - public static func == (lhs: Symbol, rhs: Symbol) -> Bool { - lhs === rhs - } -} From 42e9dde362fdbb5a22bc1ab43e637ca665d50e25 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Apr 2022 10:36:59 -0700 Subject: [PATCH 0788/1722] Updates Updates Updates Create FloorFilterAutomaticSelectionMode.swift Update FloorFilterViewModel.swift Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/FloorFilter.swift | 124 +++++++----------- .../FloorFilterAutomaticSelectionMode.swift | 24 ++++ .../FloorFilter/FloorFilterViewModel.swift | 12 -- .../FloorFilter/LevelSelector.swift | 2 +- .../FloorFilter/SiteAndFacilitySelector.swift | 21 +-- Sources/ArcGISToolkit/Extensions/View.swift | 29 +++- 6 files changed, 105 insertions(+), 107 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 66d9093b9..ecb5a3d21 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -37,11 +37,11 @@ public struct FloorFilter: View { /// - floorManager: The floor manager used by the `FloorFilter`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( + floorManager: FloorManager, alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, - floorManager: FloorManager, levelSelectorWidth: Double = 30.0, - viewpoint: Binding + viewpoint: Binding = .constant(nil) ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( automaticSelectionMode: automaticSelectionMode, @@ -69,7 +69,7 @@ public struct FloorFilter: View { private let levelSelectorWidth: Double /// Button to open and close the site and facility selector. - private var facilityButtonView: some View { + private var sitesAndFacilitiesButton: some View { Button { siteAndFacilitySelectorIsPresented.toggle() } label: { @@ -89,80 +89,54 @@ public struct FloorFilter: View { } /// Displays the available levels. - @ViewBuilder private var levelsSelectorView: some View { - if viewModel.hasLevelsToDisplay { - if topAligned { - Divider() - .frame(width: levelSelectorWidth) - } - LevelSelector( - isCollapsed: $isLevelsViewCollapsed, - buttonWidth: levelSelectorWidth, - isTopAligned: topAligned, - levels: viewModel.sortedLevels - ) - if !topAligned { - Divider() - .frame(width: levelSelectorWidth) - } - } + @ViewBuilder private var levelSelector: some View { + LevelSelector( + buttonWidth: levelSelectorWidth, + isTopAligned: topAligned, + levels: viewModel.sortedLevels + ) + .hidden(!viewModel.hasLevelsToDisplay) } /// A view that allows selecting between levels. - private var levelSelectorView: some View { + private var floorFilter: some View { VStack { - if !topAligned { - Spacer() - } - VStack { - if topAligned { - facilityButtonView - levelsSelectorView - } else { - levelsSelectorView - facilityButtonView - } - } - .esriBorder() if topAligned { - Spacer() + sitesAndFacilitiesButton + Divider() + .hidden(!viewModel.hasLevelsToDisplay) + levelSelector + } else { + levelSelector + Divider() + .hidden(!viewModel.hasLevelsToDisplay) + sitesAndFacilitiesButton } } - .frame(maxWidth: isCompact ? .infinity : nil, alignment: alignment) - } - - /// Indicates that the selector should be presented with a right oriented aligment configuration. - private var rightAligned: Bool { - switch alignment { - case .topTrailing, .trailing, .bottomTrailing: - return true - default: - return false - } + .frame(width: 75) + .esriBorder() + .frame( + maxWidth: isCompact ? .infinity : nil, + maxHeight: .infinity, + alignment: alignment + ) } - /// A configured `SiteAndFacilitySelector` view. - private var siteAndFacilitySelectorView: some View { - Group { - if !isCompact { - SiteAndFacilitySelector(isHidden: $siteAndFacilitySelectorIsPresented) - .esriBorder() - .opacity(siteAndFacilitySelectorIsPresented ? 1 : .zero) - .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in - viewModel.viewpointSubject.send(viewpoint.wrappedValue) - } - } + /// A configured `SiteAndFacilitySelector`. + @ViewBuilder private var siteAndFacilitySelector: some View { + if !isCompact { + SiteAndFacilitySelector(isHidden: $siteAndFacilitySelectorIsPresented) + .esriBorder() + .opacity(siteAndFacilitySelectorIsPresented ? 1 : .zero) + .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in + viewModel.viewpointSubject.send(viewpoint.wrappedValue) + } } } /// Indicates that the selector should be presented with a top oriented aligment configuration. private var topAligned: Bool { - switch alignment { - case .topLeading, .top, .topTrailing: - return true - default: - return false - } + alignment.vertical == .top } /// The `Viewpoint` used to pan/zoom to the selected site/facilty. @@ -170,28 +144,18 @@ public struct FloorFilter: View { private var viewpoint: Binding public var body: some View { - Group { - if viewModel.isLoading { - ProgressView() - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: alignment - ) + HStack(alignment: .bottom) { + if alignment.horizontal == .trailing { + siteAndFacilitySelector + floorFilter } else { - HStack(alignment: .bottom) { - if rightAligned { - siteAndFacilitySelectorView - levelSelectorView - } else { - levelSelectorView - siteAndFacilitySelectorView - } - } + floorFilter + siteAndFacilitySelector } } // Ensure space for filter text field on small screens in landscape .frame(minHeight: 100) .environmentObject(viewModel) + .disabled(viewModel.isLoading) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift new file mode 100644 index 000000000..177b985f4 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift @@ -0,0 +1,24 @@ +// 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. + +/// Defines automatic selection behavior. +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. + case alwaysNotClearing + /// Never update selection based on the GeoView's current viewpoint. + case never +} diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 98de8ceb6..2dab01c16 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -326,16 +326,4 @@ final class FloorFilterViewModel: ObservableObject { private var viewpointSubscription: AnyCancellable? } -/// Defines automatic selection behavior. -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. - case alwaysNotClearing - /// Never update selection based on the GeoView's current viewpoint. - case never -} - extension FloorFilterViewModel.Selection: Hashable { } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 5e11d4fcb..87b3b73f6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -22,7 +22,7 @@ struct LevelSelector: View { /// 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. - @Binding var isCollapsed: Bool + @State var isCollapsed: Bool = false /// The width for buttons in the level selector. let buttonWidth: Double diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index b0f4a860b..d71691265 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -127,12 +127,12 @@ struct SiteAndFacilitySelector: View { ) .keyboardType(.alphabet) .disableAutocorrection(true) - .navigationBarTitle( - Text("Select a site"), - displayMode: .inline - ) + .navigationTitle("Select a site") + .navigationBarTitleDisplayMode(.inline) .toolbar { - CloseButton { isHidden.wrappedValue.toggle() } + ToolbarItem(placement: .navigationBarTrailing) { + CloseButton { isHidden.wrappedValue.toggle() } + } } } } @@ -187,7 +187,7 @@ struct SiteAndFacilitySelector: View { let facilities: [FloorFacility] /// Allows the user to toggle the visibility of the site and facility selector. - var isHidden: Binding + @Binding var isHidden: Bool /// A subset of `facilities` with names containing `searchPhrase` or all /// `facilities` if `searchPhrase` is empty. @@ -215,11 +215,14 @@ struct SiteAndFacilitySelector: View { ) .keyboardType(.alphabet) .disableAutocorrection(true) - .navigationBarTitle( + .navigationTitle( allSiteStyle ? "All sites" : viewModel.selectedSite?.name ?? "Select a facility" ) + .navigationBarTitleDisplayMode(.inline) .toolbar { - CloseButton { isHidden.wrappedValue.toggle() } + ToolbarItem(placement: .navigationBarTrailing) { + CloseButton { isHidden.toggle() } + } } } @@ -236,7 +239,7 @@ struct SiteAndFacilitySelector: View { List(matchingFacilities, id: \.id) { facility in Button { viewModel.setFacility(facility, zoomTo: true) - isHidden.wrappedValue.toggle() + isHidden.toggle() } label: { VStack { Text(facility.name) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index ce829075c..9f65ec429 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -33,12 +33,26 @@ struct ConditionalSheetModifier: ViewModifier { } } } + +/// A modifier which conditionally hides its underlying view. +struct ConditionallyHiddenModifier: ViewModifier { + /// A Boolean value that indicates whether the underlying view should be hidden. + var isHidden: Bool + + func body(content: Content) -> some View { + if isHidden { + EmptyView() + } else { + content + } + } +} /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool - + func body(content: Content) -> some View { if isSelected { content @@ -55,6 +69,13 @@ struct SelectedModifier: ViewModifier { } extension View { + /// View modifier used to conditionally hide a view. + /// - Parameter condition: A value of `true` will hide the underlying view. + /// - Returns: A new `View`. + func hidden(_ condition: Bool) -> some View { + modifier(ConditionallyHiddenModifier(isHidden: condition)) + } + /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. /// - Parameter perform: The closure to be executed when the content size of the receiver /// changes. @@ -73,10 +94,8 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: The modified view. - func selected( - _ isSelected: Bool = false - ) -> some View { + /// - Returns: A new `View`. + func selected(_ isSelected: Bool = false) -> some View { modifier(SelectedModifier(isSelected: isSelected)) } From ae57438067f61988021619b1b657aed99e8de66f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Apr 2022 10:49:41 -0700 Subject: [PATCH 0789/1722] Update View.swift --- Sources/ArcGISToolkit/Extensions/View.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 9f65ec429..be4a3aa82 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -33,9 +33,9 @@ struct ConditionalSheetModifier: ViewModifier { } } } - + /// A modifier which conditionally hides its underlying view. -struct ConditionallyHiddenModifier: ViewModifier { +struct ConditionalHiddenModifier: ViewModifier { /// A Boolean value that indicates whether the underlying view should be hidden. var isHidden: Bool @@ -73,7 +73,7 @@ extension View { /// - Parameter condition: A value of `true` will hide the underlying view. /// - Returns: A new `View`. func hidden(_ condition: Bool) -> some View { - modifier(ConditionallyHiddenModifier(isHidden: condition)) + modifier(ConditionalHiddenModifier(isHidden: condition)) } /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. From 0d06be190704f85355ef45820424195caa9e0f06 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Apr 2022 16:45:49 -0700 Subject: [PATCH 0790/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index efb7940a4..7e3970a5b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -230,7 +230,10 @@ struct SiteAndFacilitySelector: View { facilityListAndFilterView // Only apply navigation modifiers if this is displayed // within a navigation view - .navigationTitle("Select a facility") + .navigationTitle( + presentationStyle == .allSites ? "All sites" : viewModel.selectedSite?.name ?? "Select a facility" + ) + .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { CloseButton { isHidden.toggle() } @@ -242,12 +245,8 @@ struct SiteAndFacilitySelector: View { /// A view containing a label for the site name, a filter-via-name bar and a list of the facility names. var facilityListAndFilterView: some View { VStack { - HStack { - if presentationStyle == .standard { - Text(facilities.first?.site?.name ?? "N/A") - } else if presentationStyle == .allSites { - Text("All sites") - } else if presentationStyle == .singleSite { + if presentationStyle == .singleSite { + HStack { Text(facilities.first?.site?.name ?? "N/A") Spacer() CloseButton { isHidden.toggle() } From 4e969602a07bf19edecd8753bc3aa629968d402a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Apr 2022 16:49:29 -0700 Subject: [PATCH 0791/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 7e3970a5b..ade2db76e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -230,9 +230,7 @@ struct SiteAndFacilitySelector: View { facilityListAndFilterView // Only apply navigation modifiers if this is displayed // within a navigation view - .navigationTitle( - presentationStyle == .allSites ? "All sites" : viewModel.selectedSite?.name ?? "Select a facility" - ) + .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -247,7 +245,7 @@ struct SiteAndFacilitySelector: View { VStack { if presentationStyle == .singleSite { HStack { - Text(facilities.first?.site?.name ?? "N/A") + Text(navigationTitle) Spacer() CloseButton { isHidden.toggle() } } @@ -313,6 +311,16 @@ struct SiteAndFacilitySelector: View { } } } + + /// A title to be displayed at the top of facility list. + private var navigationTitle: String { + switch presentationStyle { + case .allSites: + return "All Sites" + default: + return viewModel.selectedSite?.name ?? "Select a facility" + } + } } } From 85ffa8afdfe1633a170f2a6d8aa9320c2a6f62d3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Apr 2022 16:50:37 -0700 Subject: [PATCH 0792/1722] Update FloorFilter.swift --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 644f82b03..1d742929b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -59,8 +59,7 @@ public struct FloorFilter: View { } /// Displays the available levels. - @ViewBuilder - private var levelSelector: some View { + @ViewBuilder private var levelSelector: some View { LevelSelector( levels: viewModel.sortedLevels, isTopAligned: topAligned From 878681e16e83694f240773a9caebf2b3df39d1c1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 09:54:52 -0700 Subject: [PATCH 0793/1722] "Select a site" -> "Sites" --- .../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 ade2db76e..a0ca2dd24 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -128,7 +128,7 @@ struct SiteAndFacilitySelector: View { } .padding([.vertical], 4) } - .navigationTitle("Select a site") + .navigationTitle("Sites") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { From a5465b8697fd3d038afc2cac39b0607cbbee0bc7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 10:47:48 -0700 Subject: [PATCH 0794/1722] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 209afba6b..92cdefb19 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ The *ArcGIS Runtime Toolkit for Swift* has a *Target SDK* version of *15.0*, mea New to Swift Package Manager? Visit [swift.org/package-manager/](https://swift.org/package-manager/). +## Configure API Key + +Some of the Toolkit components and examples utilize ArcGIS Platform location services and therefore require an API Key to be set in `ExamplesApp.swift`. Please see the [setup guide](https://developers.arcgis.com/documentation/) for more information. + ## Additional Resources * [Developers guide documentation](https://developers.arcgis.com/ios) @@ -49,7 +53,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 - 2021 Esri +Copyright 2017 - 2022 Esri Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From b0a68e602d4a3025e6b2669a26b4f4eeeb3426c1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 11:48:27 -0700 Subject: [PATCH 0795/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift Co-authored-by: Philip Ridgeway --- 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 1d742929b..5f882f068 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -18,7 +18,7 @@ import ArcGIS /// 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 { - /// Creates a `FloorFilter` + /// Creates a `FloorFilter`. /// - Parameters: /// - alignment: Determines the display configuration of Floor Filter elements. /// - automaticSelectionMode: The selection behavior of the floor filter. From 42313f98c405aac7dbd66f0dbbe11408b13be5ff Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 12:07:13 -0700 Subject: [PATCH 0796/1722] Add `EdgeInsets.esriInsets` --- .../Components/FloorFilter/FloorFilter.swift | 1 + .../FloorFilter/LevelSelector.swift | 7 +---- .../ArcGISToolkit/Extensions/EdgeInsets.swift | 26 +++++++++++++++++++ .../Utility/EsriBorderViewModifier.swift | 7 +---- 4 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/EdgeInsets.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 1d742929b..79dbe1ec1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -54,6 +54,7 @@ public struct FloorFilter: View { isSitesAndFacilitiesHidden.toggle() } label: { Image(systemName: "building.2") + .padding(EdgeInsets.esriInsets) } .frame(maxWidth: .infinity) } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 5967fec9c..87af930d6 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -114,12 +114,7 @@ struct CollapseButton: View { } } label: { Image(systemName: "xmark") + .padding(EdgeInsets.esriInsets) } - .padding(EdgeInsets( - top: 2, - leading: 4, - bottom: 2, - trailing: 4 - )) } } diff --git a/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift b/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift new file mode 100644 index 000000000..11db5a8de --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift @@ -0,0 +1,26 @@ +// 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 + +extension EdgeInsets { + /// A reusable set of edge insets for use across toolkit components. + public static var esriInsets: EdgeInsets { + EdgeInsets( + top: 8, + leading: 12, + bottom: 8, + trailing: 12 + ) + } +} diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index dacf71f82..0e940853d 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -39,12 +39,7 @@ struct EsriBorderViewModifier: ViewModifier { public extension View { func esriBorder( - padding: EdgeInsets = EdgeInsets( - top: 8, - leading: 12, - bottom: 8, - trailing: 12 - ) + padding: EdgeInsets = EdgeInsets.esriInsets ) -> some View { modifier(EsriBorderViewModifier(padding: padding)) } From d724b3deb79a1f96a96e65578e67807c7fcc608a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 12:27:50 -0700 Subject: [PATCH 0797/1722] Merge in latest --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 4 ++-- .../Components/FloorFilter/LevelSelector.swift | 7 +------ .../Components/FloorFilter/SiteAndFacilitySelector.swift | 4 ++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index ecb5a3d21..1291f3759 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -30,7 +30,7 @@ public struct FloorFilter: View { return horizontalSizeClass == .compact || verticalSizeClass == .compact } - /// Creates a `FloorFilter` + /// Creates a `FloorFilter`. /// - Parameters: /// - alignment: Determines the display configuration of Floor Filter elements. /// - automaticSelectionMode: The selection behavior of the floor filter. @@ -75,7 +75,7 @@ public struct FloorFilter: View { } label: { Image(systemName: "building.2") .frame(width: levelSelectorWidth) - .padding(.horizontal) + .padding(EdgeInsets.esriInsets) } .sheet( isAllowed: isCompact, diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 87b3b73f6..559f4cde0 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -139,12 +139,7 @@ struct CollapseButton: View { } } label: { Image(systemName: isTopAligned ? "chevron.up.circle" : "chevron.down.circle") + .padding(EdgeInsets.esriInsets) } - .padding(EdgeInsets( - top: 2, - leading: 4, - bottom: 2, - trailing: 4 - )) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index d71691265..4c23e0b11 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -127,7 +127,7 @@ struct SiteAndFacilitySelector: View { ) .keyboardType(.alphabet) .disableAutocorrection(true) - .navigationTitle("Select a site") + .navigationTitle("Sites") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -216,7 +216,7 @@ struct SiteAndFacilitySelector: View { .keyboardType(.alphabet) .disableAutocorrection(true) .navigationTitle( - allSiteStyle ? "All sites" : viewModel.selectedSite?.name ?? "Select a facility" + allSiteStyle ? "All Sites" : viewModel.selectedSite?.name ?? "Select a facility" ) .navigationBarTitleDisplayMode(.inline) .toolbar { From a5a0e004f8e3e9d769f06a09907aac1b8cab8cef Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 13:07:38 -0700 Subject: [PATCH 0798/1722] Add toggle styles --- .../FloorFilter/LevelSelector.swift | 4 +- .../Extensions/ToggleStyle.swift | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/ToggleStyle.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 87af930d6..7db1a8a52 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -57,7 +57,7 @@ struct LevelSelector: View { Text(selectedLevelName) .lineLimit(1) } - .toggleStyle(.button) + .toggleStyle(.selectedButton) } } } @@ -91,7 +91,7 @@ struct LevelsStack: View { Text(level.shortName) .lineLimit(1) } - .toggleStyle(.button) + .toggleStyle(.selectableButton) } } .onSizeChange { diff --git a/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift new file mode 100644 index 000000000..cea65c590 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift @@ -0,0 +1,57 @@ +// 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 despending 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 fc348150533a08e8ca812058a6c29b6dea58cb96 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 13:33:02 -0700 Subject: [PATCH 0799/1722] Refine level selector width use --- .../Examples/FloorFilterExampleView.swift | 1 + .../Components/FloorFilter/FloorFilter.swift | 53 ++++++++++--------- .../FloorFilter/LevelSelector.swift | 39 ++++++-------- 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 702dbc1b4..b7807e8c8 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -61,6 +61,7 @@ struct FloorFilterExampleView: View { alignment: filterAlignment, viewpoint: $viewpoint ) + .filterWidth(75) .frame( maxWidth: 400, maxHeight: 400 diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 1291f3759..917a302fb 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -40,7 +40,6 @@ public struct FloorFilter: View { floorManager: FloorManager, alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, - levelSelectorWidth: Double = 30.0, viewpoint: Binding = .constant(nil) ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( @@ -49,7 +48,6 @@ public struct FloorFilter: View { viewpoint: viewpoint )) self.alignment = alignment - self.levelSelectorWidth = levelSelectorWidth self.viewpoint = viewpoint } @@ -65,8 +63,12 @@ public struct FloorFilter: View { /// The alignment configuration. private let alignment: Alignment - /// The width for buttons in the level selector. - private let levelSelectorWidth: Double + /// The width of the level selector. + private var filterWidth: Double = 50 + + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. + private var viewpoint: Binding /// Button to open and close the site and facility selector. private var sitesAndFacilitiesButton: some View { @@ -74,7 +76,6 @@ public struct FloorFilter: View { siteAndFacilitySelectorIsPresented.toggle() } label: { Image(systemName: "building.2") - .frame(width: levelSelectorWidth) .padding(EdgeInsets.esriInsets) } .sheet( @@ -88,16 +89,6 @@ public struct FloorFilter: View { } } - /// Displays the available levels. - @ViewBuilder private var levelSelector: some View { - LevelSelector( - buttonWidth: levelSelectorWidth, - isTopAligned: topAligned, - levels: viewModel.sortedLevels - ) - .hidden(!viewModel.hasLevelsToDisplay) - } - /// A view that allows selecting between levels. private var floorFilter: some View { VStack { @@ -113,7 +104,7 @@ public struct FloorFilter: View { sitesAndFacilitiesButton } } - .frame(width: 75) + .frame(width: filterWidth) .esriBorder() .frame( maxWidth: isCompact ? .infinity : nil, @@ -122,6 +113,20 @@ public struct FloorFilter: View { ) } + /// Indicates that the selector should be presented with a top oriented aligment configuration. + private var topAligned: Bool { + alignment.vertical == .top + } + + /// Displays the available levels. + @ViewBuilder private var levelSelector: some View { + LevelSelector( + isTopAligned: topAligned, + levels: viewModel.sortedLevels + ) + .hidden(!viewModel.hasLevelsToDisplay) + } + /// A configured `SiteAndFacilitySelector`. @ViewBuilder private var siteAndFacilitySelector: some View { if !isCompact { @@ -134,15 +139,6 @@ public struct FloorFilter: View { } } - /// Indicates that the selector should be presented with a top oriented aligment configuration. - private var topAligned: Bool { - alignment.vertical == .top - } - - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. - /// If `nil`, there will be no automatic pan/zoom operations or automatic selection support. - private var viewpoint: Binding - public var body: some View { HStack(alignment: .bottom) { if alignment.horizontal == .trailing { @@ -158,4 +154,11 @@ public struct FloorFilter: View { .environmentObject(viewModel) .disabled(viewModel.isLoading) } + + /// Modifies the width used by the level selector. Increase this value to lessen level name truncation. + public func filterWidth(_ newWidth: Double) -> FloorFilter { + var copy = self + copy.filterWidth = newWidth + return copy + } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 559f4cde0..a6a71f6d8 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -24,9 +24,6 @@ struct LevelSelector: View { /// the selected level. @State var isCollapsed: Bool = false - /// The width for buttons in the level selector. - let buttonWidth: Double - /// The alignment configuration. let isTopAligned: Bool @@ -49,15 +46,10 @@ struct LevelSelector: View { isTopAligned: isTopAligned ) Divider() - .frame(width: 30) } - LevelsStack( - buttonWidth: buttonWidth, - levels: levels - ) + LevelsStack(levels: levels) if isTopAligned { Divider() - .frame(width: 30) CollapseButton( isCollapsed: $isCollapsed, isTopAligned: isTopAligned @@ -65,14 +57,12 @@ struct LevelSelector: View { } } } else { - Button { - isCollapsed.toggle() - } label: { + Toggle(isOn: $isCollapsed) { Text(selectedLevelName) .lineLimit(1) - .frame(width: buttonWidth) + .frame(maxWidth: .infinity) } - .selected(true) + .toggleStyle(.selectedButton) } } } @@ -85,9 +75,6 @@ struct LevelsStack: View { /// The height of the scroll view's content. @State private var contentHeight: CGFloat = .zero - /// The width for buttons in the level selector. - let buttonWidth: Double - /// The levels to display. let levels: [FloorLevel] @@ -96,14 +83,22 @@ struct LevelsStack: View { ScrollView { VStack { ForEach(levels, id: \.id) { level in - Button { - viewModel.setLevel(level) - } label: { + Toggle( + isOn: Binding( + get: { + viewModel.selectedLevel == level + }, + set: { newIsOn in + guard newIsOn else { return } + viewModel.setLevel(level) + } + ) + ) { Text(level.shortName) .lineLimit(1) - .frame(width: buttonWidth) + .frame(maxWidth: .infinity) } - .selected(viewModel.selectedLevel == level) + .toggleStyle(.selectableButton) } } .onSizeChange { From 5e3f250a27df5f7a27123cff915c9f95e186cddb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 13:36:47 -0700 Subject: [PATCH 0800/1722] Delete Button.swift --- Sources/ArcGISToolkit/Extensions/Button.swift | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/Button.swift diff --git a/Sources/ArcGISToolkit/Extensions/Button.swift b/Sources/ArcGISToolkit/Extensions/Button.swift deleted file mode 100644 index ed7cd9f21..000000000 --- a/Sources/ArcGISToolkit/Extensions/Button.swift +++ /dev/null @@ -1,43 +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 modifier which "selects" a button. If selected, the button will be displayed with the -/// `BorderedProminentButtonStyle`. Otherwise, the button will be displayed with the -/// `BorderedButtonStyle`. -struct ButtonSelectedModifier: ViewModifier { - /// A Boolean value that indicates whether view should display as selected. - var isSelected: Bool - - func body(content: Content) -> some View { - if isSelected { - content - .buttonStyle(BorderedProminentButtonStyle()) - } else { - content - .buttonStyle(BorderedButtonStyle()) - } - } -} - -extension Button { - /// Button modifier used to denote the button is selected. - /// - Parameter isSelected: `true` if the button is selected, `false` otherwise. - /// - Returns: The modified button. - func selected( - _ isSelected: Bool = false - ) -> some View { - modifier(ButtonSelectedModifier(isSelected: isSelected)) - } -} From 6a34a16cd2282cab286a0175d353835e9b5d4522 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 13:46:06 -0700 Subject: [PATCH 0801/1722] Revert "Delete Button.swift" This reverts commit 5e3f250a27df5f7a27123cff915c9f95e186cddb. --- Sources/ArcGISToolkit/Extensions/Button.swift | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Sources/ArcGISToolkit/Extensions/Button.swift diff --git a/Sources/ArcGISToolkit/Extensions/Button.swift b/Sources/ArcGISToolkit/Extensions/Button.swift new file mode 100644 index 000000000..d03907955 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Button.swift @@ -0,0 +1,43 @@ +// 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 modifier which "selects" a button. If selected, the button will be displayed with the +/// `BorderedProminentButtonStyle`. Otherwise, the button will be displayed with the +/// `BorderedButtonStyle`. +struct ButtonSelectedModifier: ViewModifier { + /// A Boolean value that indicates whether view should display as selected. + var isSelected: Bool + + func body(content: Content) -> some View { + if isSelected { + content + .buttonStyle(BorderedProminentButtonStyle()) + } else { + content + .buttonStyle(BorderedButtonStyle()) + } + } +} + +extension Button { + /// Button modifier used to denote the button is selected. + /// - Parameter isSelected: `true` if the button is selected, `false` otherwise. + /// - Returns: The modified button. + func selected( + _ isSelected: Bool = false + ) -> some View { + modifier(ButtonSelectedModifier(isSelected: isSelected)) + } +} From 682bd6c874a5614086c15af2a6c42b0e1b756983 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Apr 2022 10:03:14 -0700 Subject: [PATCH 0802/1722] Update README.md Co-authored-by: Mark Dostal --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92cdefb19..9a5734d5f 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The *ArcGIS Runtime Toolkit for Swift* has a *Target SDK* version of *15.0*, mea ## Configure API Key -Some of the Toolkit components and examples utilize ArcGIS Platform location services and therefore require an API Key to be set in `ExamplesApp.swift`. Please see the [setup guide](https://developers.arcgis.com/documentation/) for more information. +Some of the toolkit components and examples utilize a set of ready-to-use ArcGIS Platform services, including basemaps, and therefore require an API Key to be set in `ExamplesApp.swift`. Please see the [setup guide](https://developers.arcgis.com/documentation/) for more information. ## Additional Resources From 31597347c6b809211c9feaeb1155082a89d55ebe Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Apr 2022 14:23:51 -0500 Subject: [PATCH 0803/1722] Initial component documentation. --- Documentation/BasemapGallery/README.md | 12 +++++ Documentation/Bookmarks/README.md | 29 ++++++++++ Documentation/FloatingPanel/README.md | 15 ++++++ Documentation/FloorFilter/README.md | 21 ++++++++ Documentation/README.md | 11 ++-- .../Scalebar/Images/alternating-bar.png | Bin 0 -> 12451 bytes Documentation/Scalebar/Images/bar.png | Bin 0 -> 4804 bytes .../Scalebar/Images/dual-unit-line.png | Bin 0 -> 10714 bytes .../Scalebar/Images/graduated-line.png | Bin 0 -> 9557 bytes Documentation/Scalebar/Images/line.png | Bin 0 -> 12669 bytes Documentation/Scalebar/README.md | 51 ++++++++++++++++++ Documentation/Search/README.md | 17 ++++++ README.md | 11 ++++ 13 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 Documentation/BasemapGallery/README.md create mode 100644 Documentation/Bookmarks/README.md create mode 100644 Documentation/FloatingPanel/README.md create mode 100644 Documentation/FloorFilter/README.md create mode 100644 Documentation/Scalebar/Images/alternating-bar.png create mode 100644 Documentation/Scalebar/Images/bar.png create mode 100644 Documentation/Scalebar/Images/dual-unit-line.png create mode 100644 Documentation/Scalebar/Images/graduated-line.png create mode 100644 Documentation/Scalebar/Images/line.png create mode 100644 Documentation/Scalebar/README.md create mode 100644 Documentation/Search/README.md diff --git a/Documentation/BasemapGallery/README.md b/Documentation/BasemapGallery/README.md new file mode 100644 index 000000000..1a3d599b9 --- /dev/null +++ b/Documentation/BasemapGallery/README.md @@ -0,0 +1,12 @@ +# BasemapGallery + +The `BasemapGallery` tool displays a collection of basemaps from either ArcGIS Online, a user-defined portal, or an array of `BasemapGalleryItem`s. When a new basemap is selected from the `BasemapGallery` and the optional `BasemapGalleryViewModel.geoModel` property is set, then the basemap of the `geoModel` is replaced with the basemap in the gallery. + +## BasemapGallery Behavior: + +## Usage + +```swift +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [BasemapGalleryExample.swift](../../Examples/Examples/BasemapGalleryExampleView.swift) in the project. diff --git a/Documentation/Bookmarks/README.md b/Documentation/Bookmarks/README.md new file mode 100644 index 000000000..8c657cfd7 --- /dev/null +++ b/Documentation/Bookmarks/README.md @@ -0,0 +1,29 @@ +# Bookmarks + +The Bookmarks component will display a list of bookmarks in a table view and allows the user to select a bookmark and perform some action. + +## Bookmarks Behavior: + +The `BookmarksTableViewController` can be created using either an `AGSGeoView` or an array of `AGSBookmark`s. + +Clients must set the `delegate` property and implement the `bookmarksViewController:didSelect` delegate method in order to be notified when a bookmark is selected. + +The `BookmarksTableViewController` observes changes to the `map` or `scene` property on the `AGSGeoView` and also the map or scene's `bookmarks` property and will udpate the list of bookmarks accordingly. + +## Usage + +```swift +bookmarksVC = BookmarksViewController(geoView: mapView) +bookmarksVC?.delegate = self + +... + +func bookmarksViewController(_ controller: BookmarksViewController, didSelect bookmark: AGSBookmark) { + if let viewpoint = bookmark.viewpoint { + mapView.setViewpoint(viewpoint, duration: 2.0) + dismiss(animated: true) + } +} +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [BookmarksExample.swift](../../Examples/ArcGISToolkitExamples/BookmarksExample.swift) in the project. diff --git a/Documentation/FloatingPanel/README.md b/Documentation/FloatingPanel/README.md new file mode 100644 index 000000000..5f7251613 --- /dev/null +++ b/Documentation/FloatingPanel/README.md @@ -0,0 +1,15 @@ +# FloatingPanel + +A floating panel is a view that overlays a view and supplies view-related content. For a map view, for instance, it could display a legend, bookmarks, search results, etc.. Apple Maps, Google Maps, Windows 10, and Collector have floating panel implementations, sometimes referred to as a "bottom sheet". + +Floating panels are non-modal and can be transient, only displaying information for a short period of time like identify results, or persistent, where the information is always displayed, for example a dedicated search panel. They will also be primarily simple containers that clients will fill with their own content. + +### Behavior: + + +### Usage + +```swift +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [FloatingPanelExampleView.swift](../../Examples/ArcGISToolkitExamples/FloatingPanelExampleView.swift) in the project. diff --git a/Documentation/FloorFilter/README.md b/Documentation/FloorFilter/README.md new file mode 100644 index 000000000..117698f4b --- /dev/null +++ b/Documentation/FloorFilter/README.md @@ -0,0 +1,21 @@ +# FloorFilter + +The `FloorFilter` component simplifies visualization of GIS data for a specific floor of a building in your application. It allows you to filter down the floor plan data displayed in your geo view to a site, a building in the site, or a floor in the building. + +The ArcGIS Runtime SDK currently supports filtering a 2D floor aware map based on the sites, buildings, or levels in the map. + +###Behavior: + +When the Site button is tapped, a prompt opens so the user can select a site and then a facility. After selecting a site and facility, a list of levels is displayed either above or below the site button. + +### Usage + +```swift + +addChild(floorFilterViewController) +view.addSubview(floorFilterViewController.view) +floorFilterViewController.didMove(toParent: self) + +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [FloorFilterExample.swift](../../Examples/ArcGISToolkitExamples/FloorFilterExample.swift) in the project. diff --git a/Documentation/README.md b/Documentation/README.md index e9766f86c..aa040934e 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,5 +1,10 @@ ### Table of Contents -* [Compass](Compass) - Shows a compass direction when the map is rotated. Auto-hides when the map points north up. - -* [Overview Map](OverviewMap) - Displays an "overview" (or "inset") map on top of an existing map or scene view. +* **[BasemapGallery](BasemapGallery)** - Displays a collection of basemaps. +* **[Bookmarks](Bookmarks)** - Shows bookmarks, from a map, scene, or a list. +* **[Compass](Compass)** - Shows a compass direction when the map is rotated. Auto-hides when the map points north up. +* **[FloatingPanel](FloatingPanel)** - Allows display of view-related content in a "bottom sheet". +* **[FloorFilter](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](OverviewMap)** - Displays an "overview" (or "inset") map on top of an existing map or scene view. +* **[Scalebar](Scalebar)** - Displays current scale reference. +* **[Search](Search)** - Displays a search experience for geo views. diff --git a/Documentation/Scalebar/Images/alternating-bar.png b/Documentation/Scalebar/Images/alternating-bar.png new file mode 100644 index 0000000000000000000000000000000000000000..48286c321487b48d1b56e3246da877f8ec284558 GIT binary patch literal 12451 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 F{{X$RcpLx# literal 0 HcmV?d00001 diff --git a/Documentation/Scalebar/Images/bar.png b/Documentation/Scalebar/Images/bar.png new file mode 100644 index 0000000000000000000000000000000000000000..8768274c9fafe429e44f8f143ea521d7885045d7 GIT binary patch literal 4804 zcmV;#5004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x z00(qQO+^Rc1Q8V|3agCLk^le_I!Q!9RCwC$oq3d0Rer}m_tnx}y>B!P4LzU;%BB(G zl2PLp7bFADDC+2_BaRXx$;lZ{k~x}~j3+tq7$+VlP9~1aOwiF0N5Kd>#uX88&=9ae z1=*p2?xy$Z+Uw?zSIeuVSGws}-M`N{)z$UhefPb(-S78(_x|qhchyHHq8dREgu$Re z0R~_w^Q9Cxl({`t1T<*t1~rItGIwz_jOP1*!XbJ653~suMG(v{GoLh!US&m8L2>t{XNVhZ#Z=v!zI# zVU{eQ(hH+wQ>A2VWh_)2%`cWb4aJ6(yUOAJqXpu{@#w$#NrQneneK*!a~j?!XXHwhNO5XOIM+< z>7NNFw#s7jbX~S13i485s_2e5@imZ-%A$J)P z;TY$JNgjpAAzf8L$UWq)9bW9!zQJ&W6aFwAQ3D)KXk|5X!Gj)g_@}VBHr=fZrk`jc0Ai^OCHetB@0@5+CAGz)WlA$U= zurpcmJN4J-#2wZd>jhbcPU1U6SA>Be!XXH`K$B5^= z!oIEud%xO`&+A406$N47^mNy#ETvRzI#nVERXqPxKPyj!h{zzpuwkb2iYJ0hPLEb2 z7(1i2X6o#-Ir|UKXJXwq1~rLEeRLu!7G4l+g2rOVV|S*q{BC~M3865Q>8Mo7Tv)9) zW2AIiJYlR(Eo~NhhDv>nq_l4f!R({HDwb_EDH?qlp}024Q{u>D zjo8msL^uRtIO)w&_hU65l~#d@iv_Wyh5dYT)qQ*xP7$&0N213UcT8EI5R|gLrigF| z!Z47r7*aNmcPzhl!gg57spOVqT`F}*$~xSkvYjK13}1%bt!vpYrZL~OG3r{6o?K8rw$Pg!RBb9ktkYg42RLf zQpB3hwpI@A-bM4V21?5-IBnJ(O3EwytTP)*DRZVtAM;xee*G1%KK&&1TQ+gkjkj{u zjki%&S(R4Vz`t>(cvyNCY43tC5{PgJwj|*5QNLv)DE zrwRN0oOj;&Oqfu^zJ2@HwQCnI|NL=6!2pZDzl`eI$%*>AyE+N_{dhbcii?YJxm+pd zHw=T?@*+xH21-Gsk8x%NRJ#?`Zu3K+kF_&YuX~eaGG1vyBsw9C6e1jgt!Ox~m**b& zN7k=hjnm~~+VmMTH#O4P(Lt#T3SHm}nCWh|ZTg5$-hGQ`Fu*e0# zdGNspS-pBS+dg=kKf3e^#!s3Y^Cf6gI0AkaE?mg6Wy_c{WlF|%l$4dzs6T7r6D54%5_dn7L=4!$W`nckJK0 zhsPd!jLwdZlyX{Y+L{_cYi7@x!=GGvC3UAyqb=ehR8zwF*L;hu+drq_&;dN%EtEN- zHDn&^^L7yo1gNX4vi(D9w&XH26ZA!Gy>9yj^_0`D4D`v5mZfLe9SMBEB*G zELxiy`Rv1YX*_&@b1%M(;<9qKZupS>yS||A^fS2N%4?{tn@UGpEA?A8v328md|of} zFTRxd7hgu@xN4Nc$Xr`P#n&nqgj0hEhhQrTOG>%^wmZ3O-dxU|J(FkmAI$#5uQlgh zayiqdolae`n^|Yh;AqH6Q&1DpaNuYY?VX)?JRVB(T@*N=Bi4K9?&=~K@Kau1P5=j+ zzSzToqsQ?S7BRkdGKIw@DL#>b5RF9Gux<@6{`8Urr%k28tq5sN zN6-|8#v_N>zIg-9M;o~Ay6dQ`tHTkCe~4)Cc{>TkjP>?A9K+) z*KzZbo2dVEGwar_W$VWuvb%m8GtZgN-#qj%VQ&X(UVVifJ9e;r(|Rtv_>Y+|X)^Bo z{FLdp8tHKerxpUUwr}PXiy69s5`Y%>6ckW#FTz)D3 zK#(=7U!$_3oLj$p3pX#h0Z(2Yg@uJY@%T^hcC-^}Z^j4*(BzwyNQzrwgpjpe1=|tP z5RCTilujq$>*lSOSMs}6&*S%YvE-(kxpu(Z$)=*N)G*Zs9`izUwX~ z)=igR^FwLCu5-91Un%@+&A9KZ$%#0ksn*Fl*WrqIm@z3OUJ}FooiZ zD#GC~jz|zEk&*uM2!ixDgwa93*Ud{${)9EZ{78JMOrhl5sU0@@cvv zmQfs~C@w3f%3VfYftwRiC&vRCUj%es5&2c)ad|xWyk4}o4I>)rQ#b$$3JS=}%cHZi zleO#Kr24FL_~ybTf{Llg3TeFH|2ljLAEw?js{soDB zpn^Upueh7B8d2P*G9I!gd9cF+SEvQS0@0WP>6lcl~w%oGur0=FDN~U3c;A?|cV5#RU9%KsT>448D2ox7hdPPTpC)iud1G zP2IFJxqQ)$y&W~ujVX)(HrIw;ku}E!ThrCihNGjIN)=*K?L<2AOKA?60?^ssit1>l zN`>(B6027=t{PobK|{!(I|86_gaWu)8#wY+J-c@8!r^c*efo4}&YVeYZ7o4Z9tVAz zw%Bl2whZgRfFEB=BNbsUQ^r>iEUct4;vf)?67FcB$k$4t5yf3viLaoXc7F(eOC!Z$ zFLgEJ2p3h-8a4=pqqMg)wDYwof5LR={ea%ppxp1D$>qOa8cBlbN+I0U;>>4us-Gx^`KfEgDNcX*e(%zd6d zg1lQp=CM9Y7=%kfO&(Ob6)wZPaU&Yq!f@2D>5e3)y+Z0@N^$ZXFex939g3!)rZK1q zYcNoZbHlh;-6w*Y6CrRJFy3um?}?x(OyQ_mXJsB#xE1~gGzE1`7-z!8)&6;uR{ywn&74*+_0R(>gUXyL1RfD zVoy6E3=vs{LxD2Vg#%l!)+Ig;VN9bjg(B2bKz1x>LdQ#IS)fU&OpkZESXiq|EGL~l z$Ssd>0?GdyJ3mB-a0o&##BXASPOQ)upPQSq5}%P2h4clOl7(#h04dmtM2M$t5#bPo z9*Ez*eHr_NfW)1gy5}-+(uctGz$R}AqX2DrSN7J43`95tp%+vyzo)DTPI8PoTWm&F zXSVM3qc8@^l?MnB4ngRP*wB{5JY~%%l{{#qzldNftZUQf*ifbjhamJs z#?zm&_&idl=Vq)4u60U`aKCyJxnebhF^>p`AoPQb9)-#rl1jWM1Si!)l9IPh#-+wA exg0Sb=l=mWycA@R!{y@u0000004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x z00(qQO+^Rc1Q8V{EdCiwpa1|VP)S5VRCwC$oq3oX)ph89x2mV7XV+*n+R(lW0b(X%V_qVp6;&u{-~<%TT8FA_uSw2^-OnlRo$xUd+#~-oO90=8`~|3Ml(hc0ze`J z7=S<^0D<$_{QyYk<^B;uxL;{11XI|*x%;yR>H_K;kjPBEu%uH*q(n;LR1^C>&OVi1 zVIm`a0UFIf5kg=f^9lnMyM5bVRCxOySGd2s;oBxWpPi20f2_2Rlg_tNWCa8Qvamt0V@fQ0K0nGIXndi^y}X zu#L6?Aw1s+ArMm9Un)W&*Od(i1JPx2U#Zjrp%gM&rYPd)&(@#JF=driF2$B7^M15X zHITYqCu2LaDwv)O!23 zv>TdT7lhYwTzpZU5AF!TEnhh1M(LQF+z-+V z^)(K!qsz-c-BxZu(Ux zVAHcL%)%Kux^T1+5q?n7*r9VpYX8IC$%_1&17;vc`^%UD6myYglvAi8=lV*9>>`x- zAn6S+R_n_^$U2F`u2neYy7nVSo05WZf|pfOvQ##B`myL=hgvU`x>0UTkZMCyB8M{NOWTiShocIF zaONkJS)j*HBC|K_`J1Ehd*{=XhA7|w)dHbR0uCtLPE30CvCT4WCz!Q|s6UKfHh9V@ z!+s~>s9eKVxKVZj1ZJYAL1a^(W9dmzwa$Cd46N}zv@6X z7`PzFfALu@+#S8!CL^^`+V4v(U;2yi`QQE9{l{7iO}*Cwp$xp0Fl%qj2!M>KC_4^x zg@q~<+{z^alEI-&XF|pyMvjq!G9xqB@kKui<@$1&J_8L}86LYn0_pVlp`(J(jzWup z(Tv}ZrFs(rBjG$gHQ_r()!Nk{fk71rBDYD`E=y$n4cRTFKc9?!7|(nNyQfh67`P5? zds880uUsTrAe4-&9Aqn|@KkUi)Dc23tj#@!koUpD26(MXN9wOXS&=0}win;s$}nxt zC*QpH9@dz>8-(w8??q`rFlGpt4pj}u)wHGady8;o0biXG-tYb8$THiE2uLa4Wh?us zLe5p^OD)!oX37-dQ~uluavo=cdhfElmJo=P^_@fugaW7$1(|LB?}Q!t=G_>s>15A4 z#CGoxtt+5lmv9NC&Wp(BBsMbNDaImIAOs38!R(+J6$F1L?`YYzjkpSw-w)tdBGmt$ z_cAyd9;$l@)kg+0VaQ{W5+UabxA(j4r(A*Hx-8V`A?{#B3xtx$c1wY%mt370!R$TNz8uOyqg2MwLg_sR@%&*Fs@3I2+PM*kY5<6IZ%t23%S~86;}-yiqPeK= z3yoX-=N`=ZrCzJ>Z^-#s8M@*dj>@c7pNm${;Qaz)6E1V)+1Nr5txG`D@Rga^5+cv_L2lPla~3h1fiE3hrSr69p;0 z5cPW8;l01JSBD9iC?#dMp!!vUw;zl*Q771Bo}qsdQ@-er0)89K$fBACGiD-zGExvu zhm@VziL;+NLCf<+4$_uI$Jw)-J$;&FvY+ObNwl=K5{YEHxI|wcr%#@wtFtr5{2OR! zYGUG~R${T3=c`mIMf;gE?A^VK-P^Ze8YYXDEo0uoMKm=}#Eh5)pGQ|`ClCGoSNY+0 z9_EIdZ{fcG`ByAhyu`DQXVj1@{MGqemJdOKlvc<&#b7#>JzdA(oU)W6%e_(&@=d0H z6JG5`eW}q7!*|ESj>xl7(J}D2s729d(u@g$Z98An3$__&;i51n@*vEE0Z1m3tbglG ze)umB^TwJN=;`j}GheueyZ`)uFlE~G%znFfe89KA_J4Tk*4pUDuHK9~sT+Jq1p=~));NAx5*7L?i7k`$) zxtJlkP7(HAgnzFY0i-gH69#JA&TYPPCtQMwbR5UZDAKd9QdDloU0ymnI{4`i9_8^z z|CJB-@29`NAHy)HHK8#gs1kyt)8Y0f62yCZ5kfFy#tfR9n?2_;O_N1)W;40ImZ)K5 zj$8Z2YkcRMUuXSWZ!&Y{OjfO0MJk!(wbx$b^_PBwL=vm1;nFKt6#Zzc3_*hS}p@7-t!R}h3bzchnFvCmUK_pyODtlH;1 zc6J!--?F2xo9AMO}-^AeoHDShEeOu&z@mGt5;a#TdIgjb@N2 zuI76cEVUhFzZi-()SpO@=;MqUZOwI$CC3e;CDZM2eanP<&8Bj^78Y)=EDQ~sj02ys^7krn{NAkX3d>j(zOh# zQ@PYsGvx-)6t-k$_sUhKM;PGMVpNx^*V&~-gJ%4ZYhn5TYFd|JXeu?ewcPT_JDKwN z&#`#UOtxAwf%1BSxf(tsm@Q!{#ld(-uq5zku2E=Lek0M-Cn2_|YRw znKFfW^X3t)tEVds2}=^Gt!L)^g-n_}g?)SXaO}`QF1~yP{UzLkWm!D`)RR2?^{;X4 z@P~Z%kH5f`mtRIhZ4KS=J`Nu`z?(0xp{}lu_cyL*_l_N$I(Y)HO<4za?_$R6IlTGu z8usnpiDlUi1@ZuDq4W{CldVn7eD8F-~J{ij~?dppZg>3zxN)dPM=C=+Pm+b zojaH?;R?R;jelfH%S3+q_`mb)v(K{Txo4R={T%N6%xC%7>i@!)b?@-#qmQy{*Dkhi z+Q`uZ`Geg;J#axesumPNeRHW_W)xRGNgPH^ERm-0I||1Jmi?_s^kF^=A`n4i{>gf zr&D`=1I>kc|J;WoA zJi@!{*7Dx^walD72g5KhO%ovmmSvH$tUy`PvNGDYX&6KdgOn(&7;oOZnfKm%j{^q| zaNF&-^W{JLGK1+>GtM~&qA@zs5NVu9!-QsHu^6*w&ZM<<3SDV| ztZ$}a(qyWtqFCuPBAvoSk}mSc8Tbf6hQt_!#OeFv_w664ydwR%g!~ue58YC)NnNWq za35u)vDIiQ7D3VZqy=Oaxer7bjRLA;G5+h__j2F8_t6lsBar$m=uCsrI)h7p`+8n| z<1Kc5u#JwB$B@Yc)eTM5)in@_RMFqxPdpwcolb*U1=#lgcsx!bksun4(%9HYb#*l< ztMK*g+qVy}`U4h=yo@xt<--$|lY>@|d35BLO<^!y7PtNjNWO_7Yk(Y9qtI@-^2qPvrp zrbZf?nn;OAX0Ac3aRN2<_4N1mlTP#zkroj%LPJv%HFb4#bac?xc7|lKA2Vt@p3~_x zZKqGs)zLvqOA8YwOrY31pEPL_3l=Qk)TvXfTepty|KNvIw@hW;(q%a3HfWVN! z48`SAZqmE}!AKqZge2mY3AyZ2^<0l?`K$2oiE40UyNG&MC5i&kL>Sg>R%v**s^#EBE^ z-Mxpdj?T<6o$Y7Yzh@V1r%!SI`RB7}@nU;v%OX0AtFF3=2OfBUPk!=~)YaAT%8S3@ zg`vcsZXo-z#|}c0 z^JSZciuuu;G014G;Tp|oK!z)2g(z2i}Uo)e!|l~`w2%59i*eZ zogG`>=QqFlC3|=6Wa`wZ%$hZe>R1d3oOj{HTzchI#A<4I>K9M)^pij5%*hj+Jbs)X zfBz9)_|?-)m@t8Buf3L~%g%E&^}=@$i^Z5c{Ty!lgFocAZ(PmUwl(RTgXmF-`!>)0;t9U@;6L;7^S`FKWfI2@f5^dod#R2_x#^~xxaz8_i9}<>(~?NUq=@fU|aR{v`nhoLdlN{6gH5$6CxCX zrxSti^gux~ZYj2-V|2C?Az=QZCH(pS{%dAVo61lB{V_JLTkCu$Shj2#_uY3Nx8Hs{ zwG$@NsW^rgpL-tne)%t`kHvWE=RafPJ8uKfG+_dt{`9B0_uhM%zi<&<7Np9ElsW&B zOS$z^cX90K5jL$~%M*|Ph{@BYF=NiW;eSpW+pU2wG=!bNDidWOM8-s<3IWd|82&{) zOD;6eaRq$^*%iJr+m+J=d^XBLE`#QD(P+jBGWW-f^5?V-QHF9Ubk=m@$I|3l`AO*hoqWI@5MOBh~aHgrL@d#Mx8q z+qDzJFj%~JF)b}Eq$G5upeIdcjzTUBA!szAJ_7xgeSHZ_VhBNl2@MhZd32?q+prFvZ#7?KtnSx%Y%Z>~wtvs*5c2Rd5z_-bGpzfiT)MAB|>6$ut0E_HD?#9yfr*bY3MF<7;RGX=gD* z5H;*s%Y9b(JcTMlP-8%q$l58A68h4%0Lj%KM4jUzPV(2ZQ-+-rsc?rTgrHhLm0=&7 zu*jspjtD`_37S!_J!%N54JSdLl@)M?u(wwUTeu{Y2&5|K7@YGIJ8~JYKyb^+6-|_N z;Ci)kk=4@&Qv8rJ0xLaeG@}Ez-8wUXGZ|f!SZw#Li6anj1i~0;Eb3xVrMR<2#&cR( zF^EIDVp?9IG*CalFfm5zwMH}O1V#j5lX5)fp6n31Oz+?b++%rNdZh^hmk~3GkD^6_ zG#ZUY!w?AsJ()|R8T*8-hUZUlU#Xh%cx7G%|i&Ag}4Xhs%Q zD5y)%qIdu^nE^yLwfuMz6T+D!3$_z@EZ_U!t%snjZQy?()dnP?6l&=3(E z-?fdl6URxX$LeF#XvQC7D0pbthzh7PZKXG63VKhU;KYXqiO1t3eQ9Me=mH@-?;z_* zF!2T|q~mcmzx5iYPaGTR``2hRCE}WZ4U{C3uDNKOjr$GZofc(a@$R~}*|uf#n0yk0 zC=jGGDWa&%ahy4MlD>HFNZ-FkqbW#msi@?SNF zk;iNVjCVmWmKjljpwVbXIc_JfOg;82M#W&Ive%3Tbn;G(Ml&EhNgs6r$T7`;M+j>4 zkeb2N0w)Vaaj~MhDr6XpSS<&19MJGw2pR))=_S$h9FV6-r85 zAV?*Xph|Uhl;);JYO1TK(sf3o8F9ih%4b8z?dTzy6$;LO!{aybMHmW?tGOaEDr`hF zC?kZhg(u0gZQ0T#%$qxxSgeLfw2~gDMl;4Jl#w`WbIz^fuPwU^162xxo6E@kU!Y#Z zs;h}rSC90)Yc!hi*M|=&QjYED$pH&DJ4MCVeE=GbMl%?sQYnrcJI3iVXGZ!`%4XoB zogBhIVviKGsL*IMm5yau?AW!N=U;e{lc&$n(Ab1&nj?K#B{O*0PMwz;UEp0$Im^k@ zr`fmv06o1rb%jPVoTw?RJ=UT;rlav@Rhrn`%-lJ%X=-c)Ae~Ng?~RDn=z&DpbeKRs{=u4t~NoeY$nNV@H(-*$Q zZVX{BfRk$-bm?le>ozL>iuAsi5A@n$57#OB$Kd&*$;EWJN;7x{{ zJ_I3%R8>(GjgIs)&}b@_;BvMyFe(rTM<~dwa1i!3!~NcWQuVB1jeP9(iI|26MBEAl zrfE_YjaGD@H3$MBe9hG{Tpu)=ibQRbYMSb7wo-R=Ani$LnXL|C8BBH5NJTMZ$?e9| zGI|=#SVH*T8%{P;b!wS#7Dg7XU8&TLUv~gwk|E7@>M9gkiO^_9hHNKq_t2{O$2AWb z%6s8N2@0tQf;=W+O=U48GnK)c6HBAfj3WH2oRJjL8lc_Z?PPSp2>Er zss#cJB}-ybO43qb3P|+#vun>Dy1Kg%V>moZGq^~f!!lppkYUcvB91F`e5qxMkasfA zb*Uksg|0F*aHp7MDMQvY7Eb7BM*A1a!ZvtE1>WdV->4w$`S{Wq3$nS)#%k+MN=RCg zsIYIYv$K=qCr&_Z!+_5{+8Gibg4a0(9Vs~5c8XI6_Of%w4%Thj#Oy`qQdeL34_H%~ zgk|}arRv1Fdd!?DgUi!Ug#c6JUfs-p8PfAb5#4%$^NlsqQqta^=D_hIe7JWH@4ojg zM^B#SifcYv(IKZWSnDD97v3hTOb7v%gk-Fd;|Yr+9o;PZ$dz1r)k-F|wvP1uYsMh? z7F`;yGX1gr#avECnRbWN#ZK?fHKHj{mipei=CP&_X~U#H65~jF7jbED*~)7;|DsEX zR(O_Xv7z0{Sp3SZEb34@6TYSh1QGV1%CRwO27x@2n?#Yj7hROo!3uJiKuRpL7;{%f zph&;?5EQa<`L8$ca+M+53}=KeK36Qzl!7xtkWOVOy_pHe&L)(Wq}nR&IAw# zgb`6~{e~!AZ&zcQ)orRk2+oMqqXSJ4H7=pp`k~gpQp`wh8U7H-Qp~p0 zTm`OJ_;$|M@t4t@HRGI!2~K2!o5!R?bwUIybU;&)PGy$GMS*C>DNfapzRwZ*0mZE`4sfB{3FgZhCeA^shP>fr*V7)7*%&_le*mfnAsILgm!tA-;hu=WRN1?=i zJKF=-t%nL@g>))SPfs_d8KJhWZrIIz5s$N1lsrBtilD+$z-X!&cjSwJk-3BcA@=w@ zsLF_{;fJ0&PnntHJ#T^s3-{vv3X}g{3w`!Is4#^q|AkjR{r&wMJ93ykJ3gR4kzm1+ zrOcW;kLm(ZK~lomwlnP7zKvuu$%4g8m^^jrh^%K|r7a$R^t(L%$alEpiWPk3?t59d zWa+S8L&Oj{D(N7SZ64#ZvT6z={T->V6ZMY(GPP*{l{Qy&P`0M#DAPS&*(Gz$fxro& zv@IFZ)u%t4_ukJXSF9jf z?VUehS(4*N5A*255Aw4gexLTXHiQt=)z@?L$M4{?_k58Bio<01_KmI*WJpLF$z=aoH$h>*;=<4cX^ZIoh zK6rqcbLKMboSDPcUDejr(bC$=;sp!1_2!Rp%~dOi)z*<5#-BKn4L2R;PFknO)p$^H z<+v?S(Tu|4?Nvvvx05WdOeuql$K)*2w~r^9PJm~z8M1;)gj`@r%|MnsAG%^WxNi^7 zJ@q8JKG?=Z7hS~t_utQT*IkEcn!Nt{>wM!I-(b_awY>Jib1Xi08B=G>0BrNp`nTTT z?blyLT4_H2-@m};KmU2^>gss%$tQW}p@(?;)t6biY#DRs&1d4|$-{O}%P;u|S6q52 zjS)Z?bfkv&nP$RHyFComd7I(dF{r5`9#!pY;uIDGH`(z3Ym#v8feh8t;|)Ji-JD?fHCTX*ec z*RI{XyY6ic?A*?@88eYcI?kSD%lfq(+P{yF-EU8?erXo{y}!n#QIHnbW`Z>dUNq<8|gPSjZKtev2v7rVs0Lji@S> zETf)!Cfm5@DRizvA$FQ^NmirIs@7_Zf0$`gt!u9TRvl7^`Oyo9b4^roSH@-(K;olmT$mJ`R0a^lE` zr1}#?s$)5?<=qYI`PXm#6V1)d%wN2OUE8;@XXg$8qSe)`zV&u4`p6Z$`0Ouv`_)%S zr_%@_c=4HE@<(6%Q-1%h&(PS^1i;&`zQV)*@Kru~<7yTzTEw*J=L~C>)#3A)Xf$~+ zgupP|mz`?CGz5kam6yEX;A#9%k z*(7qIegR)r;G8eQ4lV)bdMZ`z5<(EGsU;e%#jC65!n|*tB zvuoS?tiJgczW%LmbNvlBU>XL`KlKZ~_0_+lsi~QN_~!??_r5POYt}4|e0Yd;uddSq?0Os*-&Th9PL3 zGKI-gr?PF!Cf2;PhJ_bg#HCkX1BSu2Et^^M+%udwehh$)jt+XeyJ)Cur2Wiky4u^R zudk=Jrk0chOF4IKT^%(wwVXY5l6X%yRytjj=G;(U&s}$Ynm@k#PiSeG$dZK%=;-KV z-P(7!@~Rcw|D`XoYUN52@m`|QC=Y(?pE-5xC_QIR5*31epUD*dlo4E@YvFh6E*}3(7*1r7~$4{JK=>^L% z41-%Lgv_;yosH%!!81~97mQryI!?3qY36{*P?NW%T zQ>QX-&TL{eHFPAQdDeWU&YDeR;|9(<_goe)m`9%l-H}=*&t1UeDO2g|>qGV@s4}2G zJ|{`Z}s2CK7>FKroo5NmW&p1O^F9LL`D=MuJPbew`C%56oFurDH#gQ9nh&#Dx13cdT##2 zo!qo~H3xQVr@gI>g$oxlZ~i>CZQssgXV2Pxfr%4|)zs3Tf{87YXqwQ>p?!O4Z$C>q zmBQ@`)9Ex_ogKt`duVE?r@p=(Gg3ve$m_B2jy6pI#uO`-zgTHm75U;%62s}r)*^K6_{t>e`&@v?QLgx z>Zd>EZ@&B`zVVp#{aRAVB+HjCXW^p72y^`C&Ew}zH6>DnyVaBJ zGaF~GcK+Z^+2E#r_6rM(G6|bd=DQ2s_g5yANv7dC%0R1<(1rX&pq65VOTw8>AOtL1 zej#7@;-69-F?sog-|)nbAG7B+#A2*kwTdr&=}TOD-E|}}NR1yzNJ$-Xv!p=?5F$0QTA`&%FdlTXlQ8Qyz|au{`~p0v`ivt zR?(SqXE0}xQqtFchLijE@WIyiNhXslUAmMdOP0{u+Dg9>ew?3d3s zuxcCWwIG>H(st?;^ma0{bs{y*E%e9;DJkjf=%D+|DHom1dWm=#hT9e9mWmX-yY0bmM2 zoe8xjxH(PJ5?nmGrhfu~l~Xf%U`76{{sV0!(sP}4{IBZ_pg zIE`lPq6I>wB7+^XZMjibj|pa;@M!wZ8-r~0kbS7k2m=FkMPpym-a?W=OAd`@RG|ez zg(Gu*mX2Syv|c%roUY^}H5$!mM+=1UfV(z;&xD`(SVYh6nR)E{s|ABbQyFN1P^pL_ z7puvY$J(F%+BF|}vXu%?$f;-6vPPpBb7+B3p$x2B_N{4o5(bfD2x5?UZsFoB>V zfiopoWpF literal 0 HcmV?d00001 diff --git a/Documentation/Scalebar/Images/graduated-line.png b/Documentation/Scalebar/Images/graduated-line.png new file mode 100644 index 0000000000000000000000000000000000000000..7725445603df12603ac5893c463fe5b19ed1ec7d GIT binary patch literal 9557 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 z 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 literal 0 HcmV?d00001 diff --git a/Documentation/Scalebar/Images/line.png b/Documentation/Scalebar/Images/line.png new file mode 100644 index 0000000000000000000000000000000000000000..6a74d4bba90c7d1d8b1e56492bd0ae02a09f636f GIT binary patch literal 12669 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 literal 0 HcmV?d00001 diff --git a/Documentation/Scalebar/README.md b/Documentation/Scalebar/README.md new file mode 100644 index 000000000..40d695152 --- /dev/null +++ b/Documentation/Scalebar/README.md @@ -0,0 +1,51 @@ +# Scalebar + +A scalebar displays the representation of an accurate linear measurement on the map. It provides a visual indication through which users can determine the size of features or the distance between features on a map. A scale bar is a line or bar divided into parts. It is labeled with its ground length, usually in multiples of map units, such as tens of kilometers or hundreds of miles. + +The scalebar uses geodetic calculations to provide accurate measurements for maps of any spatial reference. The measurement is accurate for the center of the map extent being displayed. This means at smaller scales (zoomed way out) you might find it somewhat inaccurate at the extremes of the visible extent. As the map is panned and zoomed, the scalebar automatically grows and shrinks and updates its measurement based on the new map extent. + +### Usage + +```swift + let scalebar = Scalebar(mapView: mapView) + scalebar.style = .alternatingBar + scalebar.units = .metric + scalebar.alignment = .left + view.addSubview(scalebar) +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [ScalebarExample.swift](../../Examples/ArcGISToolkitExamples/ScalebarExample.swift) in the project. + + +### Styles + +| Style | Example | +|------------- |-------- | +|`line` |![line](Images/line.png) | +|`graduated line` |![graduated line](Images/graduated-line.png) | +|`bar` |![bar](Images/bar.png) | +|`alternatingBar` |![alternating bar](Images/alternating-bar.png) | +|`dualUnitLine` |![dual unit line](Images/dual-unit-line.png) | + + + +### Units + +Two options are available - `metric` and `imperial`. Defaults to the option most appropriate for the device locale. `metric` displays distances in meters and kilometers depending on the map scale, and `imperial` displays distances in feet and miles. + +### Alignment + +As you pan & zoom the map, the scalebar automatically grows and shrinks to update its measurements for the new map extent. The `alignment` property indicates how the scalebar has been placed on the UI and which section of the scalebar should remain fixed. For example, if you place it in the lower-left corner of the UI, set the alignment to `left` which means that it will grow/shrink on the right side. + +### Customization + +You can customize many visual elements of the scalebar such as - + +* `fillColor` +* `alternateFillColor` +* `lineColor` +* `shadowColor` +* `textColor` +* `textShadowColor` +* `font` + diff --git a/Documentation/Search/README.md b/Documentation/Search/README.md new file mode 100644 index 000000000..99457eaeb --- /dev/null +++ b/Documentation/Search/README.md @@ -0,0 +1,17 @@ +# Search + +/// `SearchView` presents a search experience. + +## Features + +## Key properties + +## Behavior: + +## Usage + +```swift +} +``` + +To see the `SearchView` in action, and for examples of `Search` customization, check out the [Examples](../../Examples) and refer to [SearchExampleView.swift](../../Examples/Examples/SearchExampleView.swift) in the project. diff --git a/README.md b/README.md index 9a5734d5f..7da160d21 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,17 @@ To use Toolkit in your project: * [OverviewMap](./Documentation/OverviewMap/README.md) +* **[BasemapGallery](Documentation/BasemapGallery)** - Displays a collection of basemaps. +* **[Bookmarks](Documentation/Bookmarks)** - Shows bookmarks, from a map, scene, or a list. +* **[Compass](Documentation/Compass)** - Shows a compass direction when the map is rotated. Auto-hides when the map + points north up. +* **[FloatingPanel](Documentation/FloatingPanel)** - Allows display of view-related content in a "bottom sheet". +* **[FloorFilter](Documentation/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. +* **[OverviewMap](Documentation/OverviewMap)** - Displays the visible extent of a geo view in a small "inset" map. +* **[Scalebar](Documentation/Scalebar)** - Displays current scale reference. +* **[Search](Documentation/Search)** - Displays a search experience for geo views. + + ## Requirements * ArcGIS Runtime SDK for Swift * Xcode 13.0 (or higher) From 2be533be76c68e0534c3ea9c07512de781b90f58 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Apr 2022 14:56:58 -0500 Subject: [PATCH 0804/1722] Updates to doc; remove out-of-date scalebar screenshots. --- Documentation/BasemapGallery/README.md | 2 +- Documentation/Bookmarks/README.md | 21 +-------- Documentation/FloatingPanel/README.md | 2 +- Documentation/FloorFilter/README.md | 2 +- .../Scalebar/Images/alternating-bar.png | Bin 12451 -> 0 bytes Documentation/Scalebar/Images/bar.png | Bin 4804 -> 0 bytes .../Scalebar/Images/dual-unit-line.png | Bin 10714 -> 0 bytes .../Scalebar/Images/graduated-line.png | Bin 9557 -> 0 bytes Documentation/Scalebar/Images/line.png | Bin 12669 -> 0 bytes Documentation/Scalebar/README.md | 41 +----------------- Documentation/Search/README.md | 2 +- README.md | 4 -- 12 files changed, 7 insertions(+), 67 deletions(-) delete mode 100644 Documentation/Scalebar/Images/alternating-bar.png delete mode 100644 Documentation/Scalebar/Images/bar.png delete mode 100644 Documentation/Scalebar/Images/dual-unit-line.png delete mode 100644 Documentation/Scalebar/Images/graduated-line.png delete mode 100644 Documentation/Scalebar/Images/line.png diff --git a/Documentation/BasemapGallery/README.md b/Documentation/BasemapGallery/README.md index 1a3d599b9..3f11d6899 100644 --- a/Documentation/BasemapGallery/README.md +++ b/Documentation/BasemapGallery/README.md @@ -9,4 +9,4 @@ The `BasemapGallery` tool displays a collection of basemaps from either ArcGIS O ```swift ``` -To see it in action, try out the [Examples](../../Examples) and refer to [BasemapGalleryExample.swift](../../Examples/Examples/BasemapGalleryExampleView.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [BasemapGalleryExampleView.swift](../../Examples/Examples/BasemapGalleryExampleView.swift) in the project. diff --git a/Documentation/Bookmarks/README.md b/Documentation/Bookmarks/README.md index 8c657cfd7..28f8611bf 100644 --- a/Documentation/Bookmarks/README.md +++ b/Documentation/Bookmarks/README.md @@ -1,29 +1,12 @@ # Bookmarks -The Bookmarks component will display a list of bookmarks in a table view and allows the user to select a bookmark and perform some action. +The Bookmarks component will display a list of bookmarks and allows the user to select a bookmark and perform some action. ## Bookmarks Behavior: -The `BookmarksTableViewController` can be created using either an `AGSGeoView` or an array of `AGSBookmark`s. - -Clients must set the `delegate` property and implement the `bookmarksViewController:didSelect` delegate method in order to be notified when a bookmark is selected. - -The `BookmarksTableViewController` observes changes to the `map` or `scene` property on the `AGSGeoView` and also the map or scene's `bookmarks` property and will udpate the list of bookmarks accordingly. - ## Usage ```swift -bookmarksVC = BookmarksViewController(geoView: mapView) -bookmarksVC?.delegate = self - -... - -func bookmarksViewController(_ controller: BookmarksViewController, didSelect bookmark: AGSBookmark) { - if let viewpoint = bookmark.viewpoint { - mapView.setViewpoint(viewpoint, duration: 2.0) - dismiss(animated: true) - } -} ``` -To see it in action, try out the [Examples](../../Examples) and refer to [BookmarksExample.swift](../../Examples/ArcGISToolkitExamples/BookmarksExample.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [BookmarksExampleView.swift](../../Examples/Examples/BookmarksExampleView.swift) in the project. diff --git a/Documentation/FloatingPanel/README.md b/Documentation/FloatingPanel/README.md index 5f7251613..69a4b637d 100644 --- a/Documentation/FloatingPanel/README.md +++ b/Documentation/FloatingPanel/README.md @@ -12,4 +12,4 @@ Floating panels are non-modal and can be transient, only displaying information ```swift ``` -To see it in action, try out the [Examples](../../Examples) and refer to [FloatingPanelExampleView.swift](../../Examples/ArcGISToolkitExamples/FloatingPanelExampleView.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [FloatingPanelExampleView.swift](../../Examples/Examples/FloatingPanelExampleView.swift) in the project. diff --git a/Documentation/FloorFilter/README.md b/Documentation/FloorFilter/README.md index 117698f4b..caa78683d 100644 --- a/Documentation/FloorFilter/README.md +++ b/Documentation/FloorFilter/README.md @@ -4,7 +4,7 @@ The `FloorFilter` component simplifies visualization of GIS data for a specific The ArcGIS Runtime SDK currently supports filtering a 2D floor aware map based on the sites, buildings, or levels in the map. -###Behavior: +### Behavior: When the Site button is tapped, a prompt opens so the user can select a site and then a facility. After selecting a site and facility, a list of levels is displayed either above or below the site button. diff --git a/Documentation/Scalebar/Images/alternating-bar.png b/Documentation/Scalebar/Images/alternating-bar.png deleted file mode 100644 index 48286c321487b48d1b56e3246da877f8ec284558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12451 zcmX9_1yEaE)25W-4#nLm?ryx;XmxVyV+ad%oAic4_`4#nNwUH|m^Co?yhlg!Oo zdv^EPb0d`%rBM(G5TT%;P-K7-s!&j$N+F*$;9()xkQDZ6$Qzj{P*nj6%8LdHDj)<3 z>KU>Wa0mtE$_fQ_Yy<_xpAH3u>yX{1A_(~c){qM5GbQ2HE(%=-D(`}diDRPr#z3F6vPfPX|OnYobZ z6EKnK6T^yDj}}JxHN2LfILRw*$4wcqp{fI!0|1E#1sVS1_l!%!CEQ?&>3N#w-ytKl z1m-8=1%W9P({yO`A);oO^fLsnc8T=uQJS4+?>&19go?J!7Z@y|h%@N0`A`~q#f zSi~I4%|)9t$zYD=Q7CGq#yg)*XlKM-AI%)yW8nczjACV|;}|&LG)rR$P)it!c(v$^ zRgq=+d8{HmYO3#npecpP|8#;!H|=2Cv8mcGEB>2qg*Dr+ZJy(m8h2_*MEC#v2bfu* z>OA)YJ2KOqKqAuuRvI2Wh6x>F{6(I(suo$Yp0XeA=C?93qii~MFlvM0hHiJfseBZt zyU{UCUs7&(Z1C(wYHi9XydVfcJ`5=Mf=!SQ+W(3P&B_RuS1WwNeq4N_7z)^uKu9*Z zOy1T>x;0CxP|H^p#`|ejOXe*UNn3t?v(_$8c@?8TFi5Z>*)a_Ll&f zvB)M0kRx>wWqa*=3LQOrK1649by;;|=ky?{$R1(IW6|CUIEK!KII`pt6P=Xa+s(x9 z#sq~oc98`P4EKt-m}|Kb3=+L1v`p3utYbML&SRqlbPbZR)%ALGsGcdT^sro!bic(h zmO8~bFgh3d`bzf3fDwuM-6O5TBeiWQAt+4@O2A@cEv!t=kc3Dpp>aNy1=W%is1)W- z&$x-O?F4CvXz)|*c%T(of{7p+;tFQ3+5BkcxMa*_+px2eh@^&Zt^&tmqy5>mff88I z^hr@ud9>*O()NaozyFLL#=%5#EDp zZ*lFv80#49r+XKbM6eFy|V`!rT!bFD^r{{{Pf&;UJz7 zB^Hn()6kayYY}D_m=ITc`c_NNX{^IyC!pKTB#*hB`qz|KHJG-N#*$rAmC*NdgfU{u zAGW3Ws~MS6;rcUXaT>7EyT#TpyYbsFa-=6mk}*k&bB8=zBV_-|$QEt2sa{{PDTwg# ziSS8OdxvN1g(4c?undiKM6Oyqkrg|v+4wNp|H&LRyjd-qgV$(Z%++OjaXc)Q zf==s2p$alw9GW|A6o(E;{l*DBg~H0|JT}6t#C{6cRL7Ox?`zzsWGoU#p_WG+AS-bG z69@#SrNvC0%&XQnCzqB`AcN0-uq#qmQcp{=Uy{ep-2x_{oUAje8V4hhXQ|WB>B6Z~ zGL2V7ff2d~jYB!b*q~+Fqi+P$6=41OKtv(ZYlt_|!Q#`Our2v}FE5tb(~irdQY8PS zlB7_baD-~R-lBGQwV}+R*hA;g&E}k{45aqflhcuFCIkiRA7O;z)9!tXK1vWpNCvP) z9LCGz-e<~C1e?!kr?k=G-V18A`;71@mbOnd`XPh)XY0}i(1A>z=;WgPWNQ50(u109z*mC|3-kxWXkzS8blb z%@9DO0`}GKCl%~W^va2qd8)2q%gd~=iv8ON@X(CuPF#FNvngE-9YyYOTa^OY+N5e9 zZyYn^;O{hujtR*xxd*k1irHHa|Z+0fy zh|gAJ#S*|k3wyFt2d7V&oU z-}1!5Yoik-`F#+%zG0!OmLp0|&PP5F(+$YiOB5_C->YCz=uqv&kVBu^HdCO;hbi!Z zv$sy@_0Jq(c)oQcvt2w2t_63u*>3rv#_1#Op;9Jc_D)w)i9r6W^be}4S~r2p$)yH_P{6dQ7wGeF?rBqvsm18w@uOluMi*Bf2`W;h+>mh; zRAkJCyKZH*UP|AbUKBxX#XvqEzaTB~MKkLWwA7Q4Y%aH6O(&q>@as=%UM(Q;pch3* zZxXuQ>?x7d=|c^lprKk}u`(U@=Kv_FT@Sh+w*A3Xcv3$3(Q{>|&8$DX#$64t5kmov zm(!dmH8J&<8Pom1c;{Q(gn`Z1aR_zD+#rOk9_`eACg@1=-lB_j1BGwy(LkxRAwe z*`NLNl#jB5Xu>@eH-3m?N|JB)aAK{ghR74juDMKlg&sTj|D#(nGHP2{a-}BAP>22<6D(dPXrD*=%g-W5I=|aiRZ{FB1 zk?29$+1dTOIsx=*G#rlGFfM6`>qJpjfc%q+C&r7;2ZO(!M7-bktj@19 zgGBg(sPNXev7;py+pXcYjNnvdeU@5N;+Vu|ISiu*mkHeL*7~3KW0>+} z4g3o5Ig>v1MW`8eX63AKH-PrScXz*}v)|wi%4Y-OL|7|nUQw_M;<(-Zvoi)~{!qUt zrvjgt>OFCe#`9+OViZm3S2L$+*;oEDK1fxt(dvSUUyy!El;&`|=6rn$)t7P%iCB2VUo=Xyb7gkt@v$Gq z-(;^dH1GP$%G$2bWsDbMs5q4!+N3@3gh%(v$53xhwzfw=Wl+w=4t?=RK@r4UT2h`?lp(`=R0fli-gBTpfbr1XYTVe2+Q2iOxer z;fJ#i@&%DwRRS6Mr7jRLi^#j>y5|*s=JEFt2FC64gLMm^r&|(Y)4$uzp6@IV4cBkY zhgP;SE@4QrQcnA6EW6Gd@608pgGC@OlRO8y;q8yEVGgv_ktP)D$e}`b^2_~QhwY5K z18dEV{+4S{PF@u-ec|KEJ=gzH*iGGriiD$V>(R-H@2vyramKyc{5HhNMSsJx6{YV_ z#1KLYGNKUKm;UfS4q@+Aq;I#Do$KIf4l7Xk4w^_;oMeq~{L0xr*zg@OPvk-G`@^|8 zxD=&Qt0ka*L=9g}_zcQx3e{64PyXb!+m;W(`2O^sT)ywWpM}$`53id}5I>VM$jx)$ z9ZQKc_xaE@EKrynOc*ijvN-Zy^?rL;taqa>l8)c1mt!&CAImshtqYEF{Z+AAn_1Bi zN7-#AAU-)V1=F-9v?a1I> z6eC))^<`Pk^JSnc5=Nk9_Zr-KG)TyMGsdyh;mM)PV{0@%^T4;CT&AYX&tCGm_WQIh zw$zz=+>?HY ziYHZ-y`Nigj`gPHnqHiLlZqu65JwXl&ali5oYTv8xTb%-T((|p^5gm{w^3kojO=3X zq#g4ie1~`Ux1IhQ^Y)UzU|(fXoYZ%I+H4=h=85jasuK~5JHkoF>$I-ebCUO^gqO^p zz;j2zyrrrHLEUwKtj*0D6=ZR928=9uBH;Uvk#qd=q;4e0>EA8W7 z?re6i9+02IrE^%KW`g^Q;)MpQZtTrwf1)jQnIP-Z<=6tZJ2V07YnBfD@wA^ldX+rq+vt918{53jm zXwcqw`7^&;^X@+?<&$gw-ll}v{97PU*5%vv<-;dP=!b3wt1&d1;3ZU9jvGOyKy)Bl2FZLSNZBfZCA0bJ|L zgur6G1$9;$W4_BSUA@y-Q{C^1K+Rl_kY)YL~x@20~& z9|p?!9vExyb zzalI+GU8CqO-peS4JQ9EZ^QlVtLq@N7jRL1@a`m%sGJ~^Omo#o`Wn#GGSOxcJSd8v z_qv+q(7L8>XD}J}Lk=D9#T9kS!}VQ}ce{sJ*Sw{*U0}k&c);8KdQjwD(a$g7SJ%gz z9Y>d@=7073!J+*WD#EVE71=&BGIN7^m3FD}Io>2YW%9Hn(RHumADfX)CJ%@ZN*#ox;v51Dbf+JDly4moJ^+)ez;jqedwRAcxM2so`9nn8c4E!B4F zFY6vXj8uyl+F|rQ`?9*FaoTz``SxT0@hc9Ket5giJ)GB2Y8w#OBKp{Nzvo7NeoIPN zn+|u^2ke!ycbA2nJto@r?ta#^bguFjz zZn{Ht`+}v!U>Qb|zP)?ay~~Vj@ACsbpF!l~9XkykeD(e}d66x3C#Sd5WtD-J%5rMA zeU5s&Cldhina_p)dpM=#L?QRFg>Sp!(rE)8k2&m>M>bdVI?Y@QZsqPLB*S;y-|at# zM@F`0iw7Jm}DIJ8$p#-TP58ek@KZe%&+s!e5U6_>pmB6%+2<;HbxiK z#CTyntF6umCk9Q8vH(X>v#ZDk;mNRg&04!N9Vw9Uz0Ug)3PUQUjXm|s!45~>6SDDm z-#m^CFYI}*N(`e2vhRrZoW=#sdJC@7JG>d`eJSdfK_#J>Q$3OKyWf>MzoSeio+%Va zSlB*83Oe)2xNqBac$V3YuqB>+jnd?>oPRG$jP1D~%3f-)#Cq7REz!q}`qT@iop=`1 z<*Bwj%lsSTCT%rCMid^8+b!2pl(S@+D@G=gwrZM55yn)Yex8yfiDZfDtiKK)##B4!)L z7m6&b!}{_iTL6`66a2l zx!j{y%h%=rAd6o-M*Pj(#l_}dS`x8A(^ap9iwrJXc9rJn(s|Y&CkwSUJ--@xPWOT} z4UNFhTrdL5pR`&pnRJe=LH4%BM?soa)irW1c-&PwVc@g<5Dww!g*4|s&3`^=%U3C& zxHL9FDH|9lN&t#XjmJbC>8mRlDH7q&6z+E0)hAm;{^d>$MmMWkT;4dMrRkN?|ZvWXrt4Lj34j3?s=f1u9dQAe=$KU z(Pu0xD?2oHxs&`BgRQo%x zx_y{|5#ZilUQs>x6=~~85{U#M{BbDw;c|nPR1!By;G!cKJo&+x9R1cZ6W3sJzsuzN7Zf&a$?vs4^D z_?}E6rkWUp!sVTyQLIVU9gsh#O0xV~h#4})iX5!p?xvzHh!7|>K5>TU7~Ihz$f;07 zo#};YSWT+GfB+jDvZ6bZ6jpEb5S-DVM15NY_PBNLHa?3o_j%pI{A0s-ko|_w-1cZv zU;m~a784TD$T?@M#W1)3cmprPGys6dPRl=GM(#6ec2&?N_>T1g1O17@%EHv@>|kWH z_iO|JFQ%(Yc;ztVBt*O#AxGXXtnX)ZFeu+;9|>@V>8&H-|54?WIdQ`kLXEPuOGBu^ zxEHzc7B$#L04a?7_ztpz8HSW<$HyepG&`fUI00nnqO-32+w6P%yD-p)%sKP(8rub^ z#hma>%~@9t7xq*uvU^Tsa_&M|Jo8_c);~s^uN!wMD6cCfVbM`O8q2v7KJe zydFbAXdl1S8XfVSmkSL0iXkPKG@3|!xcfh(39PLRNz;7OR-6T=+-@EW>75}q%@)su z>TqpJiD5mI-CY^_E8HCR>w*(}FN{Gm1MB&xO+WSH4u1lvqG*})XbECz08;h!^_Oih z6tKCBEbtKnW9@vIG=5TCbmd%QQ1y(o{Ng%)ZQ= zm8TW`<~rj6)xlVnoxv%G$}c!J~cfR-Ll z#7_`X1(+#GnA*b^24b}Mwm$kW5pkIkn3*@U4&>9fV+%Dy6`b^&sMJdm^;O?|-V6w- zaJlsesSY+)R#P**UnUZ%?JQY}S_=;fg7&c^4(b$;j<*|ryOKtu6!tmjuJo|~1c`Fb zn=UATsjmXkI7Wn|f-?Xgo~9$HxsL=26L+$WL_ z_npmPB>IZzlKMRwZ}X_ztO9MF2{AO;s#Fr|bXM(n#KevI<J3ju-8Pji$79KRGdC?OwG(0gulaQw*fk-Xe zNrruFG+zG@83K^c;r}t2$A!Cfr852f(lJ>qwk$8})>et>DHq(QQ0f*v=Hp{Xez%it z(fR}_$XvbMFk>a~1l$t$ zKl-pife!DmVk=@gaa1-NlQ!xm!}?okC}u~gG~WznS}R4qLIl9Szx5Z{=+(to@vb&}CNe|SN*8_^2Vdc0Ppz(wp)C02%`dMta`V(b z2fKTo!Nl?1Yw>S)KFHBhIJ9}AxGHujwCCUc0sam zFnG`8`SwWgc1nEp?pL1kTsrh>X1>cUuSJdzfn80Sxtz%6p1-id&N>0X$@j;FJU{8P zb7Hd>A4r4Ny1V;~pncBkZ}6#X@`Zq;CjRBTJ-)=g1K{k-6zj^>aeuqi(Dl1r6q$8>qidlc(S-{w>N4!Mkj>0Ld3}dJ#P2vwtHs>xipLOQ5zQr!+oWR zXI#GaMYQr3gO0S@BiGmK0`@oSzdi~IEYKiv@w$WNt^%>0pMSNdg0s^&JjdoNuHNV& zLNa`4pPu{QuQs|3Nlm3*|7h&!cp)HUQ?q4C2aU5z(l^h~C3(|pH#Yuh?;nbb&e@nX zyB)N^tjato-*G!P4V!$612>8-<=&Tnl}UR|XA0W-BN{-~@4X?9&2Z^aoaoQ_2-3Tbz1}Jrb+OUWuAhY zpK+o3P>vs}xf)tug2Ttfx$(`Yc_a6{O1n24+Q)Zcfo404v2?a<|M#ceuQ*s21$)~M zF9)m60i2umVOsS;l6wDaIG+~nstA9zu*#Jy`%|ByYN${ORtaWjHKUe8w0D9~%|1&JFy0KP~r$rlh7` zhqCy5()6RZWn)c%SJ-LvG%VRJqGt3xP#}4|Q%Zl$!;O&Ywb6fI>$&ItN5nZUgUY6D zT0rhF)niXHUn)NwFsw!N#%x7el4PWbB&DSVbQh{6pwjgH_{t_T#EZ6$9cvU5&CZ`T zjCR5=AjNe1s`G(8uT77444x*0YcYXPnM>`zqFzMnAC)M2(0>pvVA@#k1tBqS_G2j(|6t^2>>JC<={xB1q~>j#kNJr7B` z+}7gh@pZYU(#S#~BP>HjW?VgIq+~gy$F-bplQU^jsQe@T1D|J(Rp-gIe_3wSZHXn> zy4@u2NDkFN1r_nm*lJX{2L)8HN|=d45e5vwjnCoyadP1ub8y}k>MwkQ4c_o1hN_ty z`n#Y9Mp^iXNJ=cynx<0=A{re<+oA3Dj(D-}3JbmH8ozemfFff--;@?<%Vnr}B++jD zM4)5#q0mn>7{i=3HjefhE2;DmIYj!z!GPP9k<@>O)nMtogTmu*uWgCp_H<~aIDhka z-Pf|!HS~BOB(gtXnP&{4Rx$5=SQ$oI=i_&I`s5sXGnwMEr-?9q>+%6kqqWF zwh4f3mSVM-TAoV)7DIXmNRvA0k*(sDKEr7B6`jXubj-B%?p)+De5Lx}tr_=n+S?v7 z^vJJ;v=Sc}xP~qmd=S3NNe}_aKdpq$Zz)dEwQJ3=*K_!^ z6bRkKT@q(~mts1}n>IR5=l%%u|ra_jY>s-~fvYMt23-?F+|4($Jo$n>Vqw1~Y9!+EAr-1Eet~r}>hl zgZxwcO#sNhN`D6sYm8j`@%6NlUG1-YCTCP{_{yqncV+!bTFKj978v9YfmA zoduiqJR|fx*V>GQGVy6PXt}75Q)EuP((+;A8;w$~O)hw^lp66wcZ`n(&UI|+s>AKh z#{FrmOz1>aw)6{(X+2jE+v1eT6jA6|*1YovlV+Xg0K47GlG0OeXj+{#QhLFSs%k1j zdIDuz&eV5cxlNjS$rnHj7}nhyC2PKtLIm9iqLjy*>j=CcLJrY4zQM-HXUMnN%^R0tInTLp_^ ze8V#~j2m-{m|0X?H;_Lbm3P&?72$)DXSeR`Ek43+{5{twMrjH&_-v^JEx(p{5{6kL`uW?6S=v(QjgWEz_3-rgGr}OX zf=Wh0?L4PkPWT}CuA-ZR?J0Hfs)9M&z<9= zbiE8uW(CabO&SC)fJqMtVlDx>x(Ovn6k)|e-zreWcFA0s)ed{~N^^xTRAr+&p}WDz z!PQ7v^da+1hGV35ongneUj^SEH!_@NGUoalBD8*B*B+}}2;T<{MtE+NiLl&rB-sj4 zlAqv%BHi9>K+sVQWScmTE@FDJ{M*Wesf|Nk_cpDZH+aIt%Y{>$E$5#1_RyJyNfsyV zm@K<(WB2XCNgewn#8MvP{N*Sa=5=7@lrMhGWSD7lVM{D>%zH_9C9Hf5>PDdi_jf(s zVoKgBQR-T_Ycp+-_XWIjf)kGE>K>a zZHtL?7UulcA1vuf>8YtHrEelB%XTJNitF-dLN4rmQA(ZLGPL(fK+uzF>la^;lL zG1{}TQbk4%MFkoI;nm8rDt{3+wi9X=ziY0!AP#I&7uDW2@D89HDmxRxehC)#)KBBF zdbZ54TNF1Q(Frw4pRURWF4erprBq^k*2f5Eh0OH5B+$H60KBP&8ju43c+~iPIpmw8 z&Q!s&5=&>|28^F(B(6W2=w$w|!eC}3dE%vb+tW>(K2)0w8?jtk?t_!@yPB`pWC8;@ z)AVXFa)_xA+zm)K2=U{O4sZE2R%!t+C4ZZS8;PF}a>s9Nc$Ko+>hyseyBu|95&sEW z5~1-7W3Iu@Y<51l?*B#AC@t*g``_y>hbBK|-{H~w5Nj8J7n zqtwU~&sHy1>c$KXrFT-H(Be|EOu66bk~0>->ET) zO>~XU;x0sID{@uk`DrT(p6bD$Z$*N2(lOuSt!#YsFy|*1ZMIIy`sBW$I;7X?#(oCkN{6UaTinBrBBv85v_E-$>%KkwJ5cuS*7%S#M@T%TkzbSmov0yHdNrlJySf}DuhYN4jRY^lqJB4qW|S+3+mC6K zpJ?1mDWMFND|785BBjHP1F$9V=8BMGr1Boc`BexBR~lOa5+Y0H%+rPhJ;p&Mp6)Nc zqvhfk3xxvc60+o~)-aY;yg~{Z_{n|7+hhwyNn$MWG@bHGWMVp9Dt-0o*_~gGTBaW| z&=j@h5C_Ei@c-nSwhg+BpZ^ut18x(*e`Y1KNY_X?bxC<=HeXO(G!J&F)ia5#Yeqhh zSIgYR%K&<$&Ujx=EdOUJkE5s!bm8Y$GLd~>AhEAsb)`toxPYHNsVdlQtW>RJq&5;K zFlR%fiYw=`bjd-tA~?(h#$h2Dw;r5L(cOqCRcb`rGTkO;F*;wVRTZUNNX$e_XMRea zF+IQd)|Bl$0d>$wn%tQE$LQk*_5KSa2%w~wUy6Ha85s5VIVfj0O0rsh>hqha6moh1 zVQ7}B`K^ii{RX&H>wghOvh4*Q(x050y%4zqt=$`#BwvX{s5UudNyO}24 zTdgOVEEZP3ZT@lT$)KRBY?XBs&lE$wC+hDn);%yo&!Es~awI#uN(yZ0h}-osxay!AaE@^4tKFjHIGOHNYt7 F{{X$RcpLx# diff --git a/Documentation/Scalebar/Images/bar.png b/Documentation/Scalebar/Images/bar.png deleted file mode 100644 index 8768274c9fafe429e44f8f143ea521d7885045d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4804 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 exg0Sb=l=mWycA@R!{y@u0000004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x 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 diff --git a/Documentation/Scalebar/Images/graduated-line.png b/Documentation/Scalebar/Images/graduated-line.png deleted file mode 100644 index 7725445603df12603ac5893c463fe5b19ed1ec7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9557 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 z 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 diff --git a/Documentation/Scalebar/Images/line.png b/Documentation/Scalebar/Images/line.png deleted file mode 100644 index 6a74d4bba90c7d1d8b1e56492bd0ae02a09f636f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12669 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 diff --git a/Documentation/Scalebar/README.md b/Documentation/Scalebar/README.md index 40d695152..57d7c8c6a 100644 --- a/Documentation/Scalebar/README.md +++ b/Documentation/Scalebar/README.md @@ -7,45 +7,6 @@ The scalebar uses geodetic calculations to provide accurate measurements for map ### Usage ```swift - let scalebar = Scalebar(mapView: mapView) - scalebar.style = .alternatingBar - scalebar.units = .metric - scalebar.alignment = .left - view.addSubview(scalebar) ``` -To see it in action, try out the [Examples](../../Examples) and refer to [ScalebarExample.swift](../../Examples/ArcGISToolkitExamples/ScalebarExample.swift) in the project. - - -### Styles - -| Style | Example | -|------------- |-------- | -|`line` |![line](Images/line.png) | -|`graduated line` |![graduated line](Images/graduated-line.png) | -|`bar` |![bar](Images/bar.png) | -|`alternatingBar` |![alternating bar](Images/alternating-bar.png) | -|`dualUnitLine` |![dual unit line](Images/dual-unit-line.png) | - - - -### Units - -Two options are available - `metric` and `imperial`. Defaults to the option most appropriate for the device locale. `metric` displays distances in meters and kilometers depending on the map scale, and `imperial` displays distances in feet and miles. - -### Alignment - -As you pan & zoom the map, the scalebar automatically grows and shrinks to update its measurements for the new map extent. The `alignment` property indicates how the scalebar has been placed on the UI and which section of the scalebar should remain fixed. For example, if you place it in the lower-left corner of the UI, set the alignment to `left` which means that it will grow/shrink on the right side. - -### Customization - -You can customize many visual elements of the scalebar such as - - -* `fillColor` -* `alternateFillColor` -* `lineColor` -* `shadowColor` -* `textColor` -* `textShadowColor` -* `font` - +To see it in action, try out the [Examples](../../Examples) and refer to [ScalebarExample.swift](../../Examples/Examples/ScalebarExampleView.swift) in the project. diff --git a/Documentation/Search/README.md b/Documentation/Search/README.md index 99457eaeb..c814f95f4 100644 --- a/Documentation/Search/README.md +++ b/Documentation/Search/README.md @@ -1,6 +1,6 @@ # Search -/// `SearchView` presents a search experience. +`SearchView` presents a search experience. ## Features diff --git a/README.md b/README.md index 7da160d21..a92a30db8 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,6 @@ To use Toolkit in your project: ## Toolkit Components -* [Compass](./Documentation/Compass/README.md) - -* [OverviewMap](./Documentation/OverviewMap/README.md) - * **[BasemapGallery](Documentation/BasemapGallery)** - Displays a collection of basemaps. * **[Bookmarks](Documentation/Bookmarks)** - Shows bookmarks, from a map, scene, or a list. * **[Compass](Documentation/Compass)** - Shows a compass direction when the map is rotated. Auto-hides when the map From af280fad5a24212e14cfe4bf904412a1cce0928b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Apr 2022 14:59:48 -0500 Subject: [PATCH 0805/1722] Update FF doc based on Swift API. --- Documentation/FloorFilter/README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Documentation/FloorFilter/README.md b/Documentation/FloorFilter/README.md index caa78683d..d8e50eb6c 100644 --- a/Documentation/FloorFilter/README.md +++ b/Documentation/FloorFilter/README.md @@ -11,11 +11,6 @@ When the Site button is tapped, a prompt opens so the user can select a site and ### Usage ```swift - -addChild(floorFilterViewController) -view.addSubview(floorFilterViewController.view) -floorFilterViewController.didMove(toParent: self) - ``` -To see it in action, try out the [Examples](../../Examples) and refer to [FloorFilterExample.swift](../../Examples/ArcGISToolkitExamples/FloorFilterExample.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [FloorFilterExampleView.swift](../../Examples/Examples/FloorFilterExampleView.swift) in the project. From cc8619129cde70613474e49f576722c2c975cff3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Apr 2022 15:03:04 -0500 Subject: [PATCH 0806/1722] update link name for scale bar example. --- Documentation/Scalebar/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Scalebar/README.md b/Documentation/Scalebar/README.md index 57d7c8c6a..55ffb1bba 100644 --- a/Documentation/Scalebar/README.md +++ b/Documentation/Scalebar/README.md @@ -9,4 +9,4 @@ The scalebar uses geodetic calculations to provide accurate measurements for map ```swift ``` -To see it in action, try out the [Examples](../../Examples) and refer to [ScalebarExample.swift](../../Examples/Examples/ScalebarExampleView.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [ScalebarExampleView.swift](../../Examples/Examples/ScalebarExampleView.swift) in the project. From b0f348063f90b642169f54364f95fd9894152597 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Apr 2022 15:32:02 -0500 Subject: [PATCH 0807/1722] Update Documentation/FloatingPanel/README.md Co-authored-by: R Olson --- Documentation/FloatingPanel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/FloatingPanel/README.md b/Documentation/FloatingPanel/README.md index 69a4b637d..c01bcdc68 100644 --- a/Documentation/FloatingPanel/README.md +++ b/Documentation/FloatingPanel/README.md @@ -2,7 +2,7 @@ A floating panel is a view that overlays a view and supplies view-related content. For a map view, for instance, it could display a legend, bookmarks, search results, etc.. Apple Maps, Google Maps, Windows 10, and Collector have floating panel implementations, sometimes referred to as a "bottom sheet". -Floating panels are non-modal and can be transient, only displaying information for a short period of time like identify results, or persistent, where the information is always displayed, for example a dedicated search panel. They will also be primarily simple containers that clients will fill with their own content. +Floating panels are non-modal and primarily simple containers that clients will fill with their own content. They can be transient where they only display information for a short period of time, like identify results. Or they can be persistent, where the information is always displayed. A dedicated search panel, for example. ### Behavior: From 037d846de7e3bf5cfa123cb91ce5a74d8733f0cc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Apr 2022 16:53:54 -0700 Subject: [PATCH 0808/1722] Update Scalebar --- .../Components/Scalebar/Scalebar.swift | 9 +- .../Scalebar/ScalebarModifiers.swift | 1 + .../Scalebar/ScalebarStyleRender.swift | 2 +- .../Scalebar/ScalebarViewModel.swift | 21 ++- Tests/ArcGISToolkitTests/ScalebarTests.swift | 170 +++++++----------- 5 files changed, 85 insertions(+), 118 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 4ac617fa2..65c77893f 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -26,7 +26,7 @@ public struct Scalebar: View { /// The font used by the scalebar, available in both `Font` and `UIFont` types. internal static var font: (font: Font, uiFont: UIFont) { - let size = 10.0 + let size = 9.0 let uiFont = UIFont.systemFont( ofSize: size, weight: .semibold @@ -70,7 +70,7 @@ public struct Scalebar: View { internal static let lineFrameHeight = 6.0 /// The width of the prominent scalebar line. - internal static let lineWidth = 3.0 + internal static let lineWidth = 2.0 /// The shadow color used by all scalebar style renders. internal static let shadowColor = Color(uiColor: .black).opacity(0.65) @@ -78,6 +78,9 @@ public struct Scalebar: View { /// The shadow radius used by all scalebar style renders. internal static let shadowRadius = 1.0 + /// The text color used by all scalebar style renders. + internal static let textColor = Color.primary + /// The text shadow color used by all scalebar style renders. internal static let textShadowColor = Color.white @@ -144,8 +147,6 @@ public struct Scalebar: View { case .line: lineStyleRender } - } else { - EmptyView() } } .onChange(of: viewpoint.wrappedValue) { diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift index ca95ff9ad..47e06533d 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift @@ -18,6 +18,7 @@ struct ScalebarTextModifier: ViewModifier { func body(content: Content) -> some View { content .font(Scalebar.font.font) + .foregroundColor(Scalebar.textColor) .shadow( color: Scalebar.textShadowColor, radius: Scalebar.shadowRadius diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index 154a82a08..8c10c777d 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -35,7 +35,7 @@ extension Scalebar { HStack(spacing: -Scalebar.lineWidth) { ForEach(viewModel.labels.dropFirst(), id: \.index) { Rectangle() - .fill($0.index % 2 == 1 ? Scalebar.fillColor1 : Scalebar.fillColor2) + .fill($0.index % 2 == 0 ? Scalebar.fillColor1 : Scalebar.fillColor2) .border( Scalebar.lineColor, width: Scalebar.lineWidth diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 135f933db..03b30d61f 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -161,11 +161,9 @@ final class ScalebarViewModel: ObservableObject { private var availableLineDisplayLength: CGFloat { switch style { case .alternatingBar, .dualUnitLine, .graduatedLine: - let unitDisplayWidth = max( - " mi".size(withAttributes: [.font: Scalebar.font.uiFont]).width, - " km".size(withAttributes: [.font: Scalebar.font.uiFont]).width - ) - return targetWidth - (Scalebar.lineWidth / 2.0) - unitDisplayWidth + // " km" will render wider than " mi" + let maxUnitDisplayWidth = " km".size(withAttributes: [.font: Scalebar.font.uiFont]).width + return targetWidth - (Scalebar.lineWidth / 2.0) - maxUnitDisplayWidth case .bar, .line: return targetWidth - Scalebar.lineWidth } @@ -222,8 +220,8 @@ final class ScalebarViewModel: ObservableObject { // only has 1. The dividers will be decimal values and we want to make // sure they all fit very basic hueristics. let minSegmentTestString: String - if let longestString = labels.last?.text, longestString.count > 3 { - minSegmentTestString = longestString + if lineMapLength >= 100 { + minSegmentTestString = String(Int(lineMapLength)) } else { minSegmentTestString = "9.9" } @@ -272,7 +270,12 @@ final class ScalebarViewModel: ObservableObject { ) labels.append(label) } - self.labels = labels + + if style == .bar || style == .line, let last = labels.last { + self.labels = [last] + } else { + self.labels = labels + } } /// Updates the information necessary to render a scalebar based off the latest viewpoint and units per @@ -287,7 +290,7 @@ final class ScalebarViewModel: ObservableObject { let mapCenter = viewpoint.targetGeometry.extent.center - let maxLength = availableLineDisplayLength + let maxLength = availableLineDisplayLength let lineMapLength: Double let displayUnit: LinearUnit diff --git a/Tests/ArcGISToolkitTests/ScalebarTests.swift b/Tests/ArcGISToolkitTests/ScalebarTests.swift index 20172245c..ba358456b 100644 --- a/Tests/ArcGISToolkitTests/ScalebarTests.swift +++ b/Tests/ArcGISToolkitTests/ScalebarTests.swift @@ -52,113 +52,75 @@ class ScalebarTests: XCTestCase { waitForExpectations(timeout: 5.0) } - /// Asserts that the scalebar view model provides the correct label at a scale of 10,000,000. - func testScale_10000000() { - let correctImperialLabel = "200 mi" - let correctMetricLabel = "300 km" - let viewpoint = Viewpoint( - center: esriRedlands, - scale: 10_000_000.00 - ) - let unitsPerPoint = unitsPerPointBinding(2645.833333330476) - let viewModel = scalebarViewModel( - unitsPerPoint: unitsPerPoint, - viewpoint: viewpoint - ) - let calculatedMetricLabel = viewModel.alternateUnit.label - if let calculatedImperialLabel = viewModel.labels.last?.text { - XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) - XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) - } else { - XCTFail() - } + struct ScalebarTestCase { + let x: Double + let y: Double + let sR: SpatialReference = .webMercator + let style: ScalebarStyle + let targetWidth: Double + let units: ScalebarUnits + let scale: Double + let upp: Double + let uGC: Bool + let dL: Double + let labels: [String] } - /// Asserts that the scalebar view model provides the correct label at a scale of 1,000,000. - func testScale_1000000() { - let correctImperialLabel = "20 mi" - let correctMetricLabel = "30 km" - let viewpoint = Viewpoint( - center: esriRedlands, - scale: 1_000_000.00 - ) - let unitsPerPoint = unitsPerPointBinding(264.58333333304756) - let viewModel = scalebarViewModel( - unitsPerPoint: unitsPerPoint, - viewpoint: viewpoint - ) - let calculatedMetricLabel = viewModel.alternateUnit.label - if let calculatedImperialLabel = viewModel.labels.last?.text { - XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) - XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) - } else { - XCTFail() - } - } + var testCases: [ScalebarTestCase] {[ + // Test metric vs imperial units + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 137, labels: ["0", "100", "200", "300 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .imperial, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 147, labels: ["0", "50", "100", "150", "200 mi"]), + + // Disable geodetic calculations + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: false, dL: 151, labels: ["0", "100", "200", "300", "400 km"]), + + // Test all styles + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .bar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 171, labels: ["375 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .dualUnitLine, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 137, labels: ["0", "100", "200", "300 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .graduatedLine, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 137, labels: ["0", "100", "200", "300 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .line, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 171, labels: ["375 km"]), + + // Test alternate widths + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 100, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 80, labels: ["0", "87.5", "175 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 300, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 273, labels: ["0", "200", "400", "600 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 500, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 456, labels: ["0", "250", "500", "750", "1,000 km"]), + + // Test alternate points + ScalebarTestCase(x: -24752697, y: 15406913, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 128, labels: ["0", "20", "40", "60 km"]), // Artic ocean + ScalebarTestCase(x: -35729271, y: -13943757, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 153, labels: ["0", "30", "60", "90 km"]), // Near Antartica + + // Test different scales + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 100, upp: 0.02645833333330476, uGC: true, dL: 137, labels: ["0", "1", "2", "3 m"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 1_000, upp: 0.26458333333304757, uGC: true, dL: 137, labels: ["0", "10", "20", "30 m"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000, upp: 2.6458333333304758, uGC: true, dL: 137, labels: ["0", "100", "200", "300 m"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 100_000, upp: 26.458333333304758, uGC: true, dL: 137, labels: ["0", "1", "2", "3 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 1_000_000, upp: 264.58333333304756, uGC: true, dL: 137, labels: ["0", "10", "20", "30 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 80_000_000, upp: 21166.666666643807, uGC: true, dL: 143, labels: ["0", "1,250", "2,500 km"]) + ]} - /// Asserts that the scalebar view model provides the correct label at a scale of 100,000. - func testScale_100000() { - let correctImperialLabel = "2 mi" - let correctMetricLabel = "3 km" - let viewpoint = Viewpoint( - center: esriRedlands, - scale: 100_000.00 - ) - let unitsPerPoint = unitsPerPointBinding(26.458333333304758) - let viewModel = scalebarViewModel( - unitsPerPoint: unitsPerPoint, - viewpoint: viewpoint - ) - let calculatedMetricLabel = viewModel.alternateUnit.label - if let calculatedImperialLabel = viewModel.labels.last?.text { - XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) - XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) - } else { - XCTFail() - } - } - - /// Asserts that the scalebar view model provides the correct label at a scale of 10,000. - func testScale_10000() { - let correctImperialLabel = "1,000 ft" - let correctMetricLabel = "300 m" - let viewpoint = Viewpoint( - center: esriRedlands, - scale: 10_000.00 - ) - let unitsPerPoint = unitsPerPointBinding(2.6458333333304758) - let viewModel = scalebarViewModel( - unitsPerPoint: unitsPerPoint, - viewpoint: viewpoint - ) - let calculatedMetricLabel = viewModel.alternateUnit.label - if let calculatedImperialLabel = viewModel.labels.last?.text { - XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) - XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) - } else { - XCTFail() - } - } - - /// Asserts that the scalebar view model provides the correct label at a scale of 100. - func testScale_100() { - let correctImperialLabel = "10 ft" - let correctMetricLabel = "3 m" - let viewpoint = Viewpoint( - center: esriRedlands, - scale: 100.00 - ) - let unitsPerPoint = unitsPerPointBinding(0.02645833333330476) - let viewModel = scalebarViewModel( - unitsPerPoint: unitsPerPoint, - viewpoint: viewpoint - ) - let calculatedMetricLabel = viewModel.alternateUnit.label - if let calculatedImperialLabel = viewModel.labels.last?.text { - XCTAssertEqual(calculatedImperialLabel, correctImperialLabel) - XCTAssertEqual(calculatedMetricLabel, correctMetricLabel) - } else { - XCTFail() + func testAllCases() { + for test in testCases { + let viewpoint = Viewpoint( + center: Point(x: test.x, y: test.y, spatialReference: test.sR), + scale: test.scale + ) + let unitsPerPoint = unitsPerPointBinding(test.upp) + let viewModel = ScalebarViewModel( + false, + 0, + test.sR, + test.style, + test.targetWidth, + test.units, + unitsPerPoint, + test.uGC, + viewpoint + ) + XCTAssertEqual(viewModel.displayLength.rounded(), test.dL) + XCTAssertEqual(viewModel.labels.count, test.labels.count) + for i in 0.. Date: Mon, 25 Apr 2022 20:21:30 -0700 Subject: [PATCH 0809/1722] Update Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift Co-authored-by: Phil Gruenler --- 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 4ac617fa2..2a7365242 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -48,7 +48,7 @@ public struct Scalebar: View { /// The corner radius used by bar style scalebar renders. internal static let barCornerRadius = 2.5 - /// The frame height alloted to bar style scalebar renders. + /// The frame height allotted to bar style scalebar renders. internal static let barFrameHeight = 10.0 /// The darker fill color used by the alternating bar style render. From 777d0ba5b3409c4d7db2abe3d3f88a0d442a022a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 25 Apr 2022 20:21:46 -0700 Subject: [PATCH 0810/1722] Update Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift Co-authored-by: Phil Gruenler --- 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 2a7365242..c33bff94f 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -66,7 +66,7 @@ public struct Scalebar: View { /// The color of the prominent scalebar line. internal static let lineColor = Color.white - /// The line height alloted to line style scalebar renders. + /// The line height allotted to line style scalebar renders. internal static let lineFrameHeight = 6.0 /// The width of the prominent scalebar line. From 7fff09b09f213d23e1c1cbb5048a6f6ebdc956b4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 25 Apr 2022 20:21:52 -0700 Subject: [PATCH 0811/1722] Update Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift Co-authored-by: Phil Gruenler --- 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 c33bff94f..fd0acf446 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -99,7 +99,7 @@ public struct Scalebar: View { /// - unitsPerPoint: The current number of device independent pixels to map display units. /// - useGeodeticCalculations: Set `false` to compute scale without a geodesic curve. /// - viewpoint: The map's current viewpoint. - /// - width: The screen width alloted to the scalebar. + /// - width: The screen width allotted to the scalebar. public init( autoHide: Bool = false, minScale: Double = .zero, From 43fdbc18bb998c355c024f4063ef3ec1600eddde Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 25 Apr 2022 20:21:58 -0700 Subject: [PATCH 0812/1722] Update Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift Co-authored-by: Phil Gruenler --- .../ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 135f933db..5234e873a 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -76,7 +76,7 @@ final class ScalebarViewModel: ObservableObject { /// - minScale: A value of 0 indicates the scalebar segments should always recalculate. /// - spatialReference: The map's spatial reference. /// - style: The visual appearance of the scalebar. - /// - targetWidth: The screen width alloted to the scalebar. + /// - targetWidth: The screen width allotted to the scalebar. /// - units: The units to be displayed in the scalebar. /// - unitsPerPoint: The current number of device independent pixels to map display units. /// - useGeodeticCalculations: Determines if a geodesic curve should be used to compute From d71da17893fc56a9d4b3c6fedfb739e6fc0c5747 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 25 Apr 2022 20:25:36 -0700 Subject: [PATCH 0813/1722] Update ScalebarStyleRender.swift --- .../ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index 8c10c777d..dd4018e11 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -35,7 +35,7 @@ extension Scalebar { HStack(spacing: -Scalebar.lineWidth) { ForEach(viewModel.labels.dropFirst(), id: \.index) { Rectangle() - .fill($0.index % 2 == 0 ? Scalebar.fillColor1 : Scalebar.fillColor2) + .fill($0.index.isMultiple(of: 2) ? Scalebar.fillColor1 : Scalebar.fillColor2) .border( Scalebar.lineColor, width: Scalebar.lineWidth From 0cd561652f7c9b56f407167c269e56b74e745460 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 25 Apr 2022 20:38:12 -0700 Subject: [PATCH 0814/1722] Apply feedback --- .../Components/Scalebar/ScalebarUnits.swift | 22 +++++++++---------- .../Scalebar/ScalebarViewModel.swift | 8 +++---- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index ce2a53b2a..df3948ef8 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -45,15 +45,15 @@ public enum ScalebarUnits { /// A distance of 25 will return 10 as 10 is the highest power of 10 that will fit into 25. /// A distance of 550 will return 100 as 100 is the highest power of 10 that will fit into 550. /// A distance of 2,222 will return 1000 as 1000 is the highest power of 10 that will fit into 2,222. - private static func magnitudeForDistance(distance: Double) -> Double { + private static func magnitude(forDistance distance: Double) -> Double { return pow(10, floor(log10(distance))) } /// Returns a multiplier for a given distance. /// - Parameter distance: A distance to compute the multiplier for. /// - Returns: A multiplier for a given distance. - private static func multiplierForDistance(distance: Double) -> Double { - let residual = distance / ScalebarUnits.magnitudeForDistance(distance: distance) + private static func multiplier(forDistance distance: Double) -> Double { + let residual = distance / ScalebarUnits.magnitude(forDistance: distance) let multiplier = ScalebarUnits.roundNumberMultipliers.filter { $0 <= residual }.last ?? 0 return multiplier } @@ -61,9 +61,7 @@ public enum ScalebarUnits { /// Returns a list of segment options for a given multiplier. /// - Parameter multiplier: A distance to compute the multiplier for. /// - Returns: A list of segment options for a given multiplier. - private static func segmentOptionsForMultiplier( - multiplier: Double - ) -> [Int] { + private static func segmentOptions(forMultiplier multiplier: Double) -> [Int] { switch multiplier { case 1: return [1, 2, 4, 5] @@ -106,12 +104,12 @@ public enum ScalebarUnits { /// - Returns: The best number of segments so that we get relatively round numbers when the /// distance is divided up. - internal static func numSegmentsForDistance( - distance: Double, + internal static func numSegments( + forDistance distance: Double, maxNumSegments: Int ) -> Int { - let multiplier = multiplierForDistance(distance: distance) - let options = segmentOptionsForMultiplier(multiplier: multiplier) + let multiplier = multiplier(forDistance: distance) + let options = segmentOptions(forMultiplier: multiplier) let num = options.filter { $0 <= maxNumSegments }.last ?? 1 return num } @@ -122,8 +120,8 @@ public enum ScalebarUnits { to distance: Double, units: LinearUnit ) -> Double { - let magnitude = ScalebarUnits.magnitudeForDistance(distance: distance) - let multiplier = ScalebarUnits.multiplierForDistance(distance: distance) + let magnitude = ScalebarUnits.magnitude(forDistance: distance) + let multiplier = ScalebarUnits.multiplier(forDistance: distance) let roundNumber = multiplier * magnitude // Because feet and miles are not relationally multiples of 10 with diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 2d330db66..b26b53c40 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -31,7 +31,7 @@ final class ScalebarViewModel: ObservableObject { // - MARK: Public vars - /// A sreen length and displayable string for the equivalent length in the alternate unit. + /// A screen length and displayable string for the equivalent length in the alternate unit. var alternateUnit: (screenLength: CGFloat, label: String) { guard let displayUnit = displayUnit else { return (.zero, "") @@ -226,7 +226,7 @@ final class ScalebarViewModel: ObservableObject { minSegmentTestString = "9.9" } - // Use 1.5 because in the text is longer in the last label + // Multiply by 1.5 to accommodate the units in the last label let minSegmentWidth = minSegmentTestString.size(withAttributes: [.font: Scalebar.font.uiFont]).width * 1.5 + @@ -237,8 +237,8 @@ final class ScalebarViewModel: ObservableObject { // Cap segments at 4 let maxNumSegments = min(suggestedNumSegments, 4) - let numSegments: Int = ScalebarUnits.numSegmentsForDistance( - distance: lineMapLength, + let numSegments: Int = ScalebarUnits.numSegments( + forDistance: lineMapLength, maxNumSegments: maxNumSegments ) From b90b5aa243addac72760e9273fcc862b3224af15 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 25 Apr 2022 21:55:31 -0700 Subject: [PATCH 0815/1722] Apply feedback --- Examples/Examples/ScalebarExampleView.swift | 4 + .../Components/Scalebar/Scalebar.swift | 42 +++------- .../Scalebar/ScalebarModifiers.swift | 16 ++-- .../Scalebar/ScalebarSettings.swift | 77 +++++++++++++++++++ .../Scalebar/ScalebarStyleRender.swift | 30 ++++---- .../Scalebar/ScalebarViewModel.swift | 7 +- 6 files changed, 121 insertions(+), 55 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 57d074588..1c0121efc 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -31,6 +31,9 @@ struct ScalebarExampleView: View { /// The `Map` displayed in the `MapView`. private let map = Map(basemapStyle: .arcGISTopographic) + /// Customizes scalebar appearance. If not used, default styling will be applied. + private let scalebarSettings = ScalebarSettings() + /// The width of the scalebar. private let width: Double = 175.0 @@ -51,5 +54,6 @@ struct ScalebarExampleView: View { .padding(.vertical, 50) } } + .environment(\.scalebarSettings, scalebarSettings) } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index cbfe55f42..bf9e4b0f9 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -21,11 +21,14 @@ public struct Scalebar: View { /// The vertical amount of space used by the scalebar. @State private var height: Double? + /// Appearance settings. + @Environment(\.scalebarSettings) var settings + /// The view model used by the `Scalebar`. @StateObject var viewModel: ScalebarViewModel /// The font used by the scalebar, available in both `Font` and `UIFont` types. - internal static var font: (font: Font, uiFont: UIFont) { + static var font: (font: Font, uiFont: UIFont) { let size = 9.0 let uiFont = UIFont.systemFont( ofSize: size, @@ -36,7 +39,7 @@ public struct Scalebar: View { } /// The rendering height of the scalebar font. - internal static var fontHeight: Double { + static var fontHeight: Double { return "".size(withAttributes: [.font: Scalebar.font.uiFont]).height } @@ -45,44 +48,17 @@ public struct Scalebar: View { // - MARK: Internal/Private constants - /// The corner radius used by bar style scalebar renders. - internal static let barCornerRadius = 2.5 - /// The frame height allotted to bar style scalebar renders. - internal static let barFrameHeight = 10.0 - - /// The darker fill color used by the alternating bar style render. - internal static let fillColor1 = Color.black - - /// The lighter fill color used by the bar style renders. - internal static let fillColor2 = Color(uiColor: .lightGray).opacity(0.5) + static let barFrameHeight = 10.0 /// The spacing between labels and the scalebar. - internal static let labelYPad: CGFloat = 2.0 + static let labelYPad: CGFloat = 2.0 /// The required padding between scalebar labels. - internal static let labelXPad: CGFloat = 4.0 - - /// The color of the prominent scalebar line. - internal static let lineColor = Color.white + static let labelXPad: CGFloat = 4.0 /// The line height allotted to line style scalebar renders. - internal static let lineFrameHeight = 6.0 - - /// The width of the prominent scalebar line. - internal static let lineWidth = 2.0 - - /// The shadow color used by all scalebar style renders. - internal static let shadowColor = Color(uiColor: .black).opacity(0.65) - - /// The shadow radius used by all scalebar style renders. - internal static let shadowRadius = 1.0 - - /// The text color used by all scalebar style renders. - internal static let textColor = Color.primary - - /// The text shadow color used by all scalebar style renders. - internal static let textShadowColor = Color.white + static let lineFrameHeight = 6.0 /// The render style for this `Scalebar`. private let style: ScalebarStyle diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift index 47e06533d..c39fe09ad 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarModifiers.swift @@ -15,13 +15,16 @@ import SwiftUI /// A modifier which "styles" a Text element's font, shadow color and radius. struct ScalebarTextModifier: ViewModifier { + /// Appearance settings. + @Environment(\.scalebarSettings) var settings + func body(content: Content) -> some View { content .font(Scalebar.font.font) - .foregroundColor(Scalebar.textColor) + .foregroundColor(settings.textColor) .shadow( - color: Scalebar.textShadowColor, - radius: Scalebar.shadowRadius + color: settings.textShadowColor, + radius: settings.shadowRadius ) } } @@ -36,12 +39,15 @@ extension Text { /// A modifier which "styles" a scalebar's shadow color and radius. struct ScalebarGroupShadowModifier: ViewModifier { + /// Appearance settings. + @Environment(\.scalebarSettings) var settings + func body(content: Content) -> some View { content .compositingGroup() .shadow( - color: Scalebar.shadowColor, - radius: Scalebar.shadowRadius + color: settings.shadowColor, + radius: settings.shadowRadius ) } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift new file mode 100644 index 000000000..cf7ef0e5c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift @@ -0,0 +1,77 @@ +// 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 + +/// Customizes scalebar appearance. +public struct ScalebarSettings { + /// The corner radius used by bar style scalebar renders. + var barCornerRadius: Double + + /// The darker fill color used by the alternating bar style render. + var fillColor1: Color + + /// The lighter fill color used by the bar style renders. + var fillColor2: Color + + /// The color of the prominent scalebar line. + var lineColor: Color + + /// The width of the prominent scalebar line. + var lineWidth: Double + + /// The shadow color used by all scalebar style renders. + var shadowColor: Color + + /// The shadow radius used by all scalebar style renders. + var shadowRadius: Double + + /// The text color used by all scalebar style renders. + var textColor: Color + + /// The text shadow color used by all scalebar style renders. + var textShadowColor: Color + + public init( + barCornerRadius: Double = 2.5, + fillColor1: Color = .black, + fillColor2: Color = Color(uiColor: .lightGray).opacity(0.5), + lineColor: Color = .white, + lineWidth: Double = 2.0, + shadowColor: Color = Color(uiColor: .black).opacity(0.65), + shadowRadius: Double = 1.0, + textColor: Color = .primary, + textShadowColor: Color = .white + ) { + self.barCornerRadius = barCornerRadius + self.fillColor1 = fillColor1 + self.fillColor2 = fillColor2 + self.lineColor = lineColor + self.lineWidth = lineWidth + self.shadowColor = shadowColor + self.shadowRadius = shadowRadius + self.textColor = textColor + self.textShadowColor = textShadowColor + } +} + +struct ScalebarSettingsKey: EnvironmentKey { + static let defaultValue = ScalebarSettings() +} + +extension EnvironmentValues { + public var scalebarSettings: ScalebarSettings { + get { self[ScalebarSettingsKey.self] } + set { self[ScalebarSettingsKey.self] = newValue } + } +} diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index dd4018e11..19d86f55a 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -32,13 +32,13 @@ extension Scalebar { /// Renders a scalebar with `ScalebarStyle.alternatingBar`. var alternatingBarStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { - HStack(spacing: -Scalebar.lineWidth) { + HStack(spacing: -settings.lineWidth) { ForEach(viewModel.labels.dropFirst(), id: \.index) { Rectangle() - .fill($0.index.isMultiple(of: 2) ? Scalebar.fillColor1 : Scalebar.fillColor2) + .fill($0.index.isMultiple(of: 2) ? settings.fillColor1 : settings.fillColor2) .border( - Scalebar.lineColor, - width: Scalebar.lineWidth + settings.lineColor, + width: settings.lineWidth ) } } @@ -46,7 +46,7 @@ extension Scalebar { height: Scalebar.barFrameHeight, alignment: .leading ) - .cornerRadius(Scalebar.barCornerRadius) + .cornerRadius(settings.barCornerRadius) .scalebarShadow() allLabelsView } @@ -56,16 +56,16 @@ extension Scalebar { var barStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { Rectangle() - .fill(Scalebar.fillColor2) + .fill(settings.fillColor2) .border( - Scalebar.lineColor, - width: Scalebar.lineWidth + settings.lineColor, + width: settings.lineWidth ) .frame( height: Scalebar.barFrameHeight, alignment: .leading ) - .cornerRadius(Scalebar.barCornerRadius) + .cornerRadius(settings.barCornerRadius) .scalebarShadow() Text(viewModel.labels.last?.text ?? "") .scalebarText() @@ -108,12 +108,12 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: Scalebar.lineWidth, + lineWidth: settings.lineWidth, lineCap: .round, lineJoin: .round ) ) - .fill(Scalebar.lineColor) + .fill(settings.lineColor) .scalebarShadow() } .frame(height: Scalebar.barFrameHeight) @@ -146,12 +146,12 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: Scalebar.lineWidth, + lineWidth: settings.lineWidth, lineCap: .round, lineJoin: .round ) ) - .fill(Scalebar.lineColor) + .fill(settings.lineColor) .scalebarShadow() } .frame(height: Scalebar.lineFrameHeight) @@ -174,12 +174,12 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: Scalebar.lineWidth, + lineWidth: settings.lineWidth, lineCap: .round, lineJoin: .round ) ) - .fill(Scalebar.lineColor) + .fill(settings.lineColor) .scalebarShadow() } .frame(height: Scalebar.lineFrameHeight) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index b26b53c40..b7a197312 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -154,6 +154,9 @@ final class ScalebarViewModel: ObservableObject { // - MARK: Private vars + /// Appearance settings. + @Environment(\.scalebarSettings) var settings + /// The timer to determine when to autohide the scalebar. private var autoHideTimer: Timer? @@ -163,9 +166,9 @@ final class ScalebarViewModel: ObservableObject { case .alternatingBar, .dualUnitLine, .graduatedLine: // " km" will render wider than " mi" let maxUnitDisplayWidth = " km".size(withAttributes: [.font: Scalebar.font.uiFont]).width - return targetWidth - (Scalebar.lineWidth / 2.0) - maxUnitDisplayWidth + return targetWidth - (settings.lineWidth / 2.0) - maxUnitDisplayWidth case .bar, .line: - return targetWidth - Scalebar.lineWidth + return targetWidth - settings.lineWidth } } From 12dcfe19a607f74181fe636a6802e87b8721740d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Apr 2022 11:36:41 -0700 Subject: [PATCH 0816/1722] Update Scalebar Use maxWidth consistently Improve tests --- Examples/Examples/ScalebarExampleView.swift | 8 +- .../Components/Scalebar/Scalebar.swift | 11 ++- .../Scalebar/ScalebarSettings.swift | 14 +-- .../Scalebar/ScalebarStyleRender.swift | 12 +-- .../Scalebar/ScalebarViewModel.swift | 17 ++-- Tests/ArcGISToolkitTests/ScalebarTests.swift | 87 +++++++++---------- 6 files changed, 72 insertions(+), 77 deletions(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 1c0121efc..c402da5ad 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -34,8 +34,8 @@ struct ScalebarExampleView: View { /// Customizes scalebar appearance. If not used, default styling will be applied. private let scalebarSettings = ScalebarSettings() - /// The width of the scalebar. - private let width: Double = 175.0 + /// The maximum screen width allotted to the scalebar. + private let maxWidth: Double = 175.0 var body: some View { MapView(map: map, viewpoint: viewpoint) @@ -45,10 +45,10 @@ struct ScalebarExampleView: View { .overlay(alignment: alignment) { if map.loadStatus == .loaded { Scalebar( + maxWidth: maxWidth, spatialReference: spatialReference, unitsPerPoint: $unitsPerPoint, - viewpoint: $viewpoint, - width: width + viewpoint: $viewpoint ) .padding(.horizontal, 10) .padding(.vertical, 50) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index bf9e4b0f9..e67d5f032 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -60,6 +60,9 @@ public struct Scalebar: View { /// The line height allotted to line style scalebar renders. static let lineFrameHeight = 6.0 + /// The width of the prominent scalebar line. + static let lineWidth: Double = 2.0 + /// The render style for this `Scalebar`. private let style: ScalebarStyle @@ -68,6 +71,7 @@ public struct Scalebar: View { /// A scalebar displays the current map scale. /// - Parameters: /// - autoHide: Set this to `true` to have the scalebar automatically show & hide itself. + /// - maxWidth: The maximum screen width allotted to the scalebar. /// - minScale: Set a minScale if you only want the scalebar to appear when you reach a large /// enough scale maybe something like 10_000_000. This could be useful because the scalebar is /// really only accurate for the center of the map on smaller scales (when zoomed way out). A @@ -78,17 +82,16 @@ public struct Scalebar: View { /// - unitsPerPoint: The current number of device independent pixels to map display units. /// - useGeodeticCalculations: Set `false` to compute scale without a geodesic curve. /// - viewpoint: The map's current viewpoint. - /// - width: The screen width allotted to the scalebar. public init( autoHide: Bool = false, + maxWidth: Double, minScale: Double = .zero, spatialReference: SpatialReference? = nil, style: ScalebarStyle = .alternatingBar, units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, unitsPerPoint: Binding, useGeodeticCalculations: Bool = true, - viewpoint: Binding, - width: Double + viewpoint: Binding ) { self.style = style self.viewpoint = viewpoint @@ -96,10 +99,10 @@ public struct Scalebar: View { _viewModel = StateObject( wrappedValue: ScalebarViewModel( autoHide, + maxWidth, minScale, spatialReference, style, - width, units, unitsPerPoint, useGeodeticCalculations, diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift index cf7ef0e5c..2ee3b37e5 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift @@ -27,9 +27,6 @@ public struct ScalebarSettings { /// The color of the prominent scalebar line. var lineColor: Color - /// The width of the prominent scalebar line. - var lineWidth: Double - /// The shadow color used by all scalebar style renders. var shadowColor: Color @@ -42,12 +39,20 @@ public struct ScalebarSettings { /// The text shadow color used by all scalebar style renders. var textShadowColor: Color + /// - Parameters: + /// - barCornerRadius: The corner radius used by bar style scalebar renders. + /// - fillColor1: The darker fill color used by the alternating bar style render. + /// - fillColor2: The lighter fill color used by the bar style renders. + /// - lineColor: The color of the prominent scalebar line. + /// - shadowColor: The shadow color used by all scalebar style renders. + /// - shadowRadius: The shadow radius used by all scalebar style renders. + /// - textColor: The text color used by all scalebar style renders. + /// - textShadowColor: The text shadow color used by all scalebar style renders. public init( barCornerRadius: Double = 2.5, fillColor1: Color = .black, fillColor2: Color = Color(uiColor: .lightGray).opacity(0.5), lineColor: Color = .white, - lineWidth: Double = 2.0, shadowColor: Color = Color(uiColor: .black).opacity(0.65), shadowRadius: Double = 1.0, textColor: Color = .primary, @@ -57,7 +62,6 @@ public struct ScalebarSettings { self.fillColor1 = fillColor1 self.fillColor2 = fillColor2 self.lineColor = lineColor - self.lineWidth = lineWidth self.shadowColor = shadowColor self.shadowRadius = shadowRadius self.textColor = textColor diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift index 19d86f55a..c19d4c831 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift @@ -32,13 +32,13 @@ extension Scalebar { /// Renders a scalebar with `ScalebarStyle.alternatingBar`. var alternatingBarStyleRender: some View { VStack(spacing: Scalebar.labelYPad) { - HStack(spacing: -settings.lineWidth) { + HStack(spacing: -Scalebar.lineWidth) { ForEach(viewModel.labels.dropFirst(), id: \.index) { Rectangle() .fill($0.index.isMultiple(of: 2) ? settings.fillColor1 : settings.fillColor2) .border( settings.lineColor, - width: settings.lineWidth + width: Scalebar.lineWidth ) } } @@ -59,7 +59,7 @@ extension Scalebar { .fill(settings.fillColor2) .border( settings.lineColor, - width: settings.lineWidth + width: Scalebar.lineWidth ) .frame( height: Scalebar.barFrameHeight, @@ -108,7 +108,7 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: settings.lineWidth, + lineWidth: Scalebar.lineWidth, lineCap: .round, lineJoin: .round ) @@ -146,7 +146,7 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: settings.lineWidth, + lineWidth: Scalebar.lineWidth, lineCap: .round, lineJoin: .round ) @@ -174,7 +174,7 @@ extension Scalebar { } .stroke( style: .init( - lineWidth: settings.lineWidth, + lineWidth: Scalebar.lineWidth, lineCap: .round, lineJoin: .round ) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index b7a197312..1ffe545ba 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -73,10 +73,10 @@ final class ScalebarViewModel: ObservableObject { /// A scalebar view model controls the underlying data used to render a scalebar. /// - Parameters: /// - autoHide: Determines if the scalebar should automatically show & hide itself. + /// - maxWidth: The maximum screen width allotted to the scalebar. /// - minScale: A value of 0 indicates the scalebar segments should always recalculate. /// - spatialReference: The map's spatial reference. /// - style: The visual appearance of the scalebar. - /// - targetWidth: The screen width allotted to the scalebar. /// - units: The units to be displayed in the scalebar. /// - unitsPerPoint: The current number of device independent pixels to map display units. /// - useGeodeticCalculations: Determines if a geodesic curve should be used to compute @@ -84,20 +84,20 @@ final class ScalebarViewModel: ObservableObject { /// - viewpoint: The map's current viewpoint. init( _ autoHide: Bool, + _ maxWidth: Double, _ minScale: Double, _ spatialReference: SpatialReference?, _ style: ScalebarStyle, - _ targetWidth: Double, _ units: ScalebarUnits, _ unitsPerPoint: Binding, _ useGeodeticCalculations: Bool, _ viewpoint: Viewpoint? ) { self.isVisible = autoHide ? false : true + self.maxWidth = maxWidth self.minScale = minScale self.spatialReference = spatialReference self.style = style - self.targetWidth = targetWidth self.units = units self.unitsPerPoint = unitsPerPoint self.useGeodeticCalculations = useGeodeticCalculations @@ -154,9 +154,6 @@ final class ScalebarViewModel: ObservableObject { // - MARK: Private vars - /// Appearance settings. - @Environment(\.scalebarSettings) var settings - /// The timer to determine when to autohide the scalebar. private var autoHideTimer: Timer? @@ -166,9 +163,9 @@ final class ScalebarViewModel: ObservableObject { case .alternatingBar, .dualUnitLine, .graduatedLine: // " km" will render wider than " mi" let maxUnitDisplayWidth = " km".size(withAttributes: [.font: Scalebar.font.uiFont]).width - return targetWidth - (settings.lineWidth / 2.0) - maxUnitDisplayWidth + return maxWidth - (Scalebar.lineWidth / 2.0) - maxUnitDisplayWidth case .bar, .line: - return targetWidth - settings.lineWidth + return maxWidth - Scalebar.lineWidth } } @@ -178,8 +175,8 @@ final class ScalebarViewModel: ObservableObject { /// The length of the line to display in map units. private var lineMapLength: Double = .zero - /// The maximum width allowed for the scalebar. - private var targetWidth: Double + /// The maximum screen width allotted to the scalebar. + private var maxWidth: Double /// Unit of measure in use. private var units: ScalebarUnits diff --git a/Tests/ArcGISToolkitTests/ScalebarTests.swift b/Tests/ArcGISToolkitTests/ScalebarTests.swift index ba358456b..d043a1181 100644 --- a/Tests/ArcGISToolkitTests/ScalebarTests.swift +++ b/Tests/ArcGISToolkitTests/ScalebarTests.swift @@ -34,10 +34,16 @@ class ScalebarTests: XCTestCase { scale: 2 ) let unitsPerPoint = unitsPerPointBinding(1) - let viewModel = scalebarViewModel( - autoHide: true, - unitsPerPoint: unitsPerPoint, - viewpoint: viewpoint1 + let viewModel = ScalebarViewModel( + true, + 0, + 175, + .webMercator, + .alternatingBar, + .imperial, + unitsPerPoint, + true, + viewpoint1 ) viewModel.$isVisible.sink { isVisible in if isVisible && !changedToVisible { @@ -55,65 +61,69 @@ class ScalebarTests: XCTestCase { struct ScalebarTestCase { let x: Double let y: Double - let sR: SpatialReference = .webMercator + let spatialReference: SpatialReference = .webMercator let style: ScalebarStyle - let targetWidth: Double + let maxWidth: Double let units: ScalebarUnits let scale: Double let upp: Double - let uGC: Bool + var useGedeticCalculations: Bool = true let dL: Double let labels: [String] } var testCases: [ScalebarTestCase] {[ // Test metric vs imperial units - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 137, labels: ["0", "100", "200", "300 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .imperial, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 147, labels: ["0", "50", "100", "150", "200 mi"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 137, labels: ["0", "100", "200", "300 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .imperial, scale: 10_000_000, upp: 2645.833333330476, dL: 147, labels: ["0", "50", "100", "150", "200 mi"]), // Disable geodetic calculations - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: false, dL: 151, labels: ["0", "100", "200", "300", "400 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, useGedeticCalculations: false, dL: 151, labels: ["0", "100", "200", "300", "400 km"]), // Test all styles - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .bar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 171, labels: ["375 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .dualUnitLine, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 137, labels: ["0", "100", "200", "300 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .graduatedLine, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 137, labels: ["0", "100", "200", "300 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .line, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 171, labels: ["375 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .bar, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 171, labels: ["375 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .dualUnitLine, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 137, labels: ["0", "100", "200", "300 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .graduatedLine, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 137, labels: ["0", "100", "200", "300 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .line, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 171, labels: ["375 km"]), // Test alternate widths - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 100, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 80, labels: ["0", "87.5", "175 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 300, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 273, labels: ["0", "200", "400", "600 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 500, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 456, labels: ["0", "250", "500", "750", "1,000 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 100, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 80, labels: ["0", "87.5", "175 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 300, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 273, labels: ["0", "200", "400", "600 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 500, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 456, labels: ["0", "250", "500", "750", "1,000 km"]), // Test alternate points - ScalebarTestCase(x: -24752697, y: 15406913, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 128, labels: ["0", "20", "40", "60 km"]), // Artic ocean - ScalebarTestCase(x: -35729271, y: -13943757, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, uGC: true, dL: 153, labels: ["0", "30", "60", "90 km"]), // Near Antartica + ScalebarTestCase(x: -24752697, y: 15406913, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 128, labels: ["0", "20", "40", "60 km"]), // Artic ocean + ScalebarTestCase(x: -35729271, y: -13943757, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 10_000_000, upp: 2645.833333330476, dL: 153, labels: ["0", "30", "60", "90 km"]), // Near Antartica // Test different scales - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 100, upp: 0.02645833333330476, uGC: true, dL: 137, labels: ["0", "1", "2", "3 m"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 1_000, upp: 0.26458333333304757, uGC: true, dL: 137, labels: ["0", "10", "20", "30 m"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 10_000, upp: 2.6458333333304758, uGC: true, dL: 137, labels: ["0", "100", "200", "300 m"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 100_000, upp: 26.458333333304758, uGC: true, dL: 137, labels: ["0", "1", "2", "3 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 1_000_000, upp: 264.58333333304756, uGC: true, dL: 137, labels: ["0", "10", "20", "30 km"]), - ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, targetWidth: 175, units: .metric, scale: 80_000_000, upp: 21166.666666643807, uGC: true, dL: 143, labels: ["0", "1,250", "2,500 km"]) + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 100, upp: 0.02645833333330476, dL: 137, labels: ["0", "1", "2", "3 m"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 1_000, upp: 0.26458333333304757, dL: 137, labels: ["0", "10", "20", "30 m"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 10_000, upp: 2.6458333333304758, dL: 137, labels: ["0", "100", "200", "300 m"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 100_000, upp: 26.458333333304758, dL: 137, labels: ["0", "1", "2", "3 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 1_000_000, upp: 264.58333333304756, dL: 137, labels: ["0", "10", "20", "30 km"]), + ScalebarTestCase(x: esriRedlands.x, y: esriRedlands.y, style: .alternatingBar, maxWidth: 175, units: .metric, scale: 80_000_000, upp: 21166.666666643807, dL: 143, labels: ["0", "1,250", "2,500 km"]) ]} func testAllCases() { for test in testCases { let viewpoint = Viewpoint( - center: Point(x: test.x, y: test.y, spatialReference: test.sR), + center: Point( + x: test.x, + y: test.y, + spatialReference: test.spatialReference + ), scale: test.scale ) let unitsPerPoint = unitsPerPointBinding(test.upp) let viewModel = ScalebarViewModel( false, + test.maxWidth, 0, - test.sR, + test.spatialReference, test.style, - test.targetWidth, test.units, unitsPerPoint, - test.uGC, + test.useGedeticCalculations, viewpoint ) XCTAssertEqual(viewModel.displayLength.rounded(), test.dL) @@ -143,23 +153,4 @@ extension ScalebarTests { set: { _value = $0 ?? .zero } ) } - - /// Generates a new scalebar view model. - func scalebarViewModel( - autoHide: Bool = false, - unitsPerPoint: Binding, - viewpoint: Viewpoint? - ) -> ScalebarViewModel { - return ScalebarViewModel( - autoHide, - 0, - .webMercator, - .alternatingBar, - 175, - .imperial, - unitsPerPoint, - true, - viewpoint - ) - } } From fe4fbbced5ff3e9e405c77ff733692c86e4a45de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Apr 2022 10:33:50 -0700 Subject: [PATCH 0817/1722] Fix new build errors --- .../Components/FloorFilter/FloorFilterViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 0e5e72ec9..6afbd9df4 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -221,7 +221,7 @@ final class FloorFilterViewModel: ObservableObject { private func autoSelectFacility() -> Bool { // Only select a facility if it is within minimum scale. Default at 1500. let facilityMinScale: Double - if let minScale = floorManager.facilityLayer?.minScale, + if let minScale = floorManager.facilityLayer?.scaleRange?.lowerBound.inverseRatio, minScale != .zero { facilityMinScale = minScale } else { @@ -255,7 +255,7 @@ final class FloorFilterViewModel: ObservableObject { private func autoSelectSite() -> Bool { // Only select a facility if it is within minimum scale. Default at 4300. let siteMinScale: Double - if let minScale = floorManager.siteLayer?.minScale, + if let minScale = floorManager.siteLayer?.scaleRange?.lowerBound.inverseRatio, minScale != .zero { siteMinScale = minScale } else { From 5b56028c1c7949503e18558890a952a9ee965ba2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Apr 2022 10:50:40 -0700 Subject: [PATCH 0818/1722] Update renderer naming --- .../ArcGISToolkit/Components/Scalebar/Scalebar.swift | 10 +++++----- ...arStyleRender.swift => ScalebarStyleRenderer.swift} | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) rename Sources/ArcGISToolkit/Components/Scalebar/{ScalebarStyleRender.swift => ScalebarStyleRenderer.swift} (96%) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index e67d5f032..ab37bc4f1 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -116,15 +116,15 @@ public struct Scalebar: View { if $viewModel.isVisible.wrappedValue { switch style { case .alternatingBar: - alternatingBarStyleRender + alternatingBarStyleRenderer case .bar: - barStyleRender + barStyleRenderer case .dualUnitLine: - dualUnitLineStyleRender + dualUnitLineStyleRenderer case .graduatedLine: - graduatedLineStyleRender + graduatedLineStyleRenderer case .line: - lineStyleRender + lineStyleRenderer } } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRenderer.swift similarity index 96% rename from Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift rename to Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRenderer.swift index c19d4c831..6a778debb 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRender.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarStyleRenderer.swift @@ -30,7 +30,7 @@ extension Scalebar { } /// Renders a scalebar with `ScalebarStyle.alternatingBar`. - var alternatingBarStyleRender: some View { + var alternatingBarStyleRenderer: some View { VStack(spacing: Scalebar.labelYPad) { HStack(spacing: -Scalebar.lineWidth) { ForEach(viewModel.labels.dropFirst(), id: \.index) { @@ -53,7 +53,7 @@ extension Scalebar { } /// Renders a scalebar with `ScalebarStyle.bar`. - var barStyleRender: some View { + var barStyleRenderer: some View { VStack(spacing: Scalebar.labelYPad) { Rectangle() .fill(settings.fillColor2) @@ -73,7 +73,7 @@ extension Scalebar { } /// Renders a scalebar with `ScalebarStyle.dualUnitLine`. - var dualUnitLineStyleRender: some View { + var dualUnitLineStyleRenderer: some View { VStack(spacing: Scalebar.labelYPad) { Text(viewModel.labels.last?.text ?? "") .scalebarText() @@ -128,7 +128,7 @@ extension Scalebar { } /// Renders a scalebar with `ScalebarStyle.graduatedLine`. - var graduatedLineStyleRender: some View { + var graduatedLineStyleRenderer: some View { VStack(spacing: Scalebar.labelYPad) { GeometryReader { geometry in Path { path in @@ -160,7 +160,7 @@ extension Scalebar { } /// Renders a scalebar with `ScalebarStyle.line`. - var lineStyleRender: some View { + var lineStyleRenderer: some View { VStack(spacing: Scalebar.labelYPad) { GeometryReader { geometry in Path { path in From 99604b0332fc91ada9506067c96d31c14a182c4b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 10:57:40 -0700 Subject: [PATCH 0819/1722] `linearUnitsForDistance(distance:)` -> `linearUnits(forDistance:)` --- .../ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift | 4 ++-- .../Components/Scalebar/ScalebarViewModel.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index df3948ef8..442b0e2e7 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -127,7 +127,7 @@ public enum ScalebarUnits { // Because feet and miles are not relationally multiples of 10 with // each other, we have to convert to miles if we are dealing in feet. if units == .feet { - let displayUnits = linearUnitsForDistance(distance: roundNumber) + let displayUnits = linearUnits(forDistance: roundNumber) if units != displayUnits { let displayDistance = closestDistanceWithoutGoingOver( to: units.convert( @@ -154,7 +154,7 @@ public enum ScalebarUnits { /// than or equal to 1/2 mile, and `LinearUnit.feet` otherwise. /// `ScalebarUnits.metric` will return `LinearUnit.kilometers` if the given distance is /// greater than or equal to 1 kilometer, and `LinearUnit.meters` otherwise. - internal func linearUnitsForDistance(distance: Double) -> LinearUnit { + internal func linearUnits(forDistance distance: Double) -> LinearUnit { switch self { case .imperial: return distance >= 2640 ? .miles : .feet diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 1ffe545ba..e933fe4d5 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -45,8 +45,8 @@ final class ScalebarViewModel: ObservableObject { to: altMapBaseLength, units: altUnit.baseLinearUnit ) - let altDisplayUnits = altUnit.linearUnitsForDistance( - distance: altClosestBaseLength + let altDisplayUnits = altUnit.linearUnits( + forDistance: altClosestBaseLength ) let altMapLength = altUnit.baseLinearUnit.convert( to: altDisplayUnits, value: altClosestBaseLength @@ -324,7 +324,7 @@ final class ScalebarViewModel: ObservableObject { ) let planarToGeodeticFactor = maxLengthPlanar / maxLengthGeodetic displayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) - displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) + displayUnit = units.linearUnits(forDistance: roundNumberDistance) lineMapLength = baseUnits.convert(to: displayUnit, value: roundNumberDistance) } else { guard let srUnit = spatialReference.unit as? LinearUnit else { @@ -346,7 +346,7 @@ final class ScalebarViewModel: ObservableObject { value: closestLen ) / unitsPerPoint ) - displayUnit = units.linearUnitsForDistance(distance: closestLen) + displayUnit = units.linearUnits(forDistance: closestLen) lineMapLength = baseUnits.convert( to: displayUnit, value: closestLen From 632e2fb669f2fdfb9ae64534026e9a76674edb66 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 11:45:24 -0700 Subject: [PATCH 0820/1722] Remove viewpoint in sample's MapView constructor --- Examples/Examples/ScalebarExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index c402da5ad..14b08ea90 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -38,12 +38,12 @@ struct ScalebarExampleView: View { private let maxWidth: Double = 175.0 var body: some View { - MapView(map: map, viewpoint: viewpoint) + MapView(map: map) .onSpatialReferenceChanged { spatialReference = $0 } .onUnitsPerPointChanged { unitsPerPoint = $0 } .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: alignment) { - if map.loadStatus == .loaded { + if spatialReference != nil { Scalebar( maxWidth: maxWidth, spatialReference: spatialReference, From 8ebf4bbafd5af9064303d1ebdb1ccb20eb1c9b53 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 12:15:16 -0700 Subject: [PATCH 0821/1722] Remove precondition for Scalebar in sample --- Examples/Examples/ScalebarExampleView.swift | 18 ++++++++---------- .../Components/Scalebar/Scalebar.swift | 2 +- .../Scalebar/ScalebarViewModel.swift | 10 +++++----- Tests/ArcGISToolkitTests/ScalebarTests.swift | 17 +++++++++++++++-- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 14b08ea90..1754e95c6 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -43,16 +43,14 @@ struct ScalebarExampleView: View { .onUnitsPerPointChanged { unitsPerPoint = $0 } .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: alignment) { - if spatialReference != nil { - Scalebar( - maxWidth: maxWidth, - spatialReference: spatialReference, - unitsPerPoint: $unitsPerPoint, - viewpoint: $viewpoint - ) - .padding(.horizontal, 10) - .padding(.vertical, 50) - } + Scalebar( + maxWidth: maxWidth, + spatialReference: $spatialReference, + unitsPerPoint: $unitsPerPoint, + viewpoint: $viewpoint + ) + .padding(.horizontal, 10) + .padding(.vertical, 50) } .environment(\.scalebarSettings, scalebarSettings) } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index ab37bc4f1..4bc4dd775 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -86,7 +86,7 @@ public struct Scalebar: View { autoHide: Bool = false, maxWidth: Double, minScale: Double = .zero, - spatialReference: SpatialReference? = nil, + spatialReference: Binding, style: ScalebarStyle = .alternatingBar, units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, unitsPerPoint: Binding, diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index e933fe4d5..27033e250 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -86,7 +86,7 @@ final class ScalebarViewModel: ObservableObject { _ autoHide: Bool, _ maxWidth: Double, _ minScale: Double, - _ spatialReference: SpatialReference?, + _ spatialReference: Binding, _ style: ScalebarStyle, _ units: ScalebarUnits, _ unitsPerPoint: Binding, @@ -146,9 +146,6 @@ final class ScalebarViewModel: ObservableObject { return numberFormatter }() - /// The map's spatial reference. - private let spatialReference: SpatialReference? - /// The visual appearance of the scalebar. private let style: ScalebarStyle @@ -178,6 +175,9 @@ final class ScalebarViewModel: ObservableObject { /// The maximum screen width allotted to the scalebar. private var maxWidth: Double + /// The map's spatial reference. + private var spatialReference: Binding + /// Unit of measure in use. private var units: ScalebarUnits @@ -281,7 +281,7 @@ final class ScalebarViewModel: ObservableObject { /// Updates the information necessary to render a scalebar based off the latest viewpoint and units per /// point information. private func updateScaleDisplay() { - guard let spatialReference = spatialReference, + guard let spatialReference = spatialReference.wrappedValue, let unitsPerPoint = unitsPerPoint.wrappedValue, let viewpoint = viewpoint, minScale <= 0 || viewpoint.targetScale < minScale else { diff --git a/Tests/ArcGISToolkitTests/ScalebarTests.swift b/Tests/ArcGISToolkitTests/ScalebarTests.swift index d043a1181..b54548830 100644 --- a/Tests/ArcGISToolkitTests/ScalebarTests.swift +++ b/Tests/ArcGISToolkitTests/ScalebarTests.swift @@ -38,7 +38,7 @@ class ScalebarTests: XCTestCase { true, 0, 175, - .webMercator, + spatialReferenceBinding(.webMercator), .alternatingBar, .imperial, unitsPerPoint, @@ -119,7 +119,7 @@ class ScalebarTests: XCTestCase { false, test.maxWidth, 0, - test.spatialReference, + spatialReferenceBinding(test.spatialReference), test.style, test.units, unitsPerPoint, @@ -153,4 +153,17 @@ extension ScalebarTests { set: { _value = $0 ?? .zero } ) } + + /// Generates a binding to a provided units per point value. + func spatialReferenceBinding(_ value: SpatialReference) -> Binding { + var _value = value + return Binding( + get: { _value }, + set: { + if let newValue = $0 { + _value = newValue + } + } + ) + } } From 64a49a8c5513fb79c2896e0c7dd50973c63124ac Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 12:28:39 -0700 Subject: [PATCH 0822/1722] Internalize ScalebarSettings --- Examples/Examples/ScalebarExampleView.swift | 4 ---- .../ArcGISToolkit/Components/Scalebar/Scalebar.swift | 10 +++++++--- .../Components/Scalebar/ScalebarSettings.swift | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 1754e95c6..4f074ae9d 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -31,9 +31,6 @@ struct ScalebarExampleView: View { /// The `Map` displayed in the `MapView`. private let map = Map(basemapStyle: .arcGISTopographic) - /// Customizes scalebar appearance. If not used, default styling will be applied. - private let scalebarSettings = ScalebarSettings() - /// The maximum screen width allotted to the scalebar. private let maxWidth: Double = 175.0 @@ -52,6 +49,5 @@ struct ScalebarExampleView: View { .padding(.horizontal, 10) .padding(.vertical, 50) } - .environment(\.scalebarSettings, scalebarSettings) } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 4bc4dd775..0a258db88 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -21,9 +21,6 @@ public struct Scalebar: View { /// The vertical amount of space used by the scalebar. @State private var height: Double? - /// Appearance settings. - @Environment(\.scalebarSettings) var settings - /// The view model used by the `Scalebar`. @StateObject var viewModel: ScalebarViewModel @@ -63,6 +60,9 @@ public struct Scalebar: View { /// The width of the prominent scalebar line. static let lineWidth: Double = 2.0 + /// Appearance settings. + let settings: ScalebarSettings + /// The render style for this `Scalebar`. private let style: ScalebarStyle @@ -76,6 +76,7 @@ public struct Scalebar: View { /// enough scale maybe something like 10_000_000. This could be useful because the scalebar is /// really only accurate for the center of the map on smaller scales (when zoomed way out). A /// minScale of 0 means it will always be visible. + /// - settings: Appearance settings. /// - spatialReference: The map's spatial reference. /// - style: The visual appearance of the scalebar. /// - units: The units to be displayed in the scalebar. @@ -86,6 +87,7 @@ public struct Scalebar: View { autoHide: Bool = false, maxWidth: Double, minScale: Double = .zero, + settings: ScalebarSettings = ScalebarSettings(), spatialReference: Binding, style: ScalebarStyle = .alternatingBar, units: ScalebarUnits = NSLocale.current.usesMetricSystem ? .metric : .imperial, @@ -93,6 +95,7 @@ public struct Scalebar: View { useGeodeticCalculations: Bool = true, viewpoint: Binding ) { + self.settings = settings self.style = style self.viewpoint = viewpoint @@ -138,5 +141,6 @@ public struct Scalebar: View { width: $viewModel.displayLength.wrappedValue, height: height ?? .zero ) + .environment(\.scalebarSettings, settings) } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift index 2ee3b37e5..2924512e7 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift @@ -74,7 +74,7 @@ struct ScalebarSettingsKey: EnvironmentKey { } extension EnvironmentValues { - public var scalebarSettings: ScalebarSettings { + var scalebarSettings: ScalebarSettings { get { self[ScalebarSettingsKey.self] } set { self[ScalebarSettingsKey.self] = newValue } } From 0e523b190a0742f097578ef4567e2afe7e07e5c4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 13:24:56 -0700 Subject: [PATCH 0823/1722] Move autoHide logic out of view model --- .../Components/Scalebar/Scalebar.swift | 40 +++++++++++-------- .../Scalebar/ScalebarSettings.swift | 14 ++++++- .../Scalebar/ScalebarViewModel.swift | 37 ----------------- Tests/ArcGISToolkitTests/ScalebarTests.swift | 40 ------------------- 4 files changed, 37 insertions(+), 94 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 0a258db88..6997ad535 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -21,6 +21,9 @@ public struct Scalebar: View { /// The vertical amount of space used by the scalebar. @State private var height: Double? + /// Controls the current opacity of the scalebar. + @State var opacity: Double + /// The view model used by the `Scalebar`. @StateObject var viewModel: ScalebarViewModel @@ -70,7 +73,6 @@ public struct Scalebar: View { /// A scalebar displays the current map scale. /// - Parameters: - /// - autoHide: Set this to `true` to have the scalebar automatically show & hide itself. /// - maxWidth: The maximum screen width allotted to the scalebar. /// - minScale: Set a minScale if you only want the scalebar to appear when you reach a large /// enough scale maybe something like 10_000_000. This could be useful because the scalebar is @@ -84,7 +86,6 @@ public struct Scalebar: View { /// - useGeodeticCalculations: Set `false` to compute scale without a geodesic curve. /// - viewpoint: The map's current viewpoint. public init( - autoHide: Bool = false, maxWidth: Double, minScale: Double = .zero, settings: ScalebarSettings = ScalebarSettings(), @@ -95,13 +96,13 @@ public struct Scalebar: View { useGeodeticCalculations: Bool = true, viewpoint: Binding ) { + self.opacity = settings.autoHide ? .zero : 1 self.settings = settings self.style = style self.viewpoint = viewpoint _viewModel = StateObject( wrappedValue: ScalebarViewModel( - autoHide, maxWidth, minScale, spatialReference, @@ -116,23 +117,30 @@ public struct Scalebar: View { public var body: some View { Group { - if $viewModel.isVisible.wrappedValue { - switch style { - case .alternatingBar: - alternatingBarStyleRenderer - case .bar: - barStyleRenderer - case .dualUnitLine: - dualUnitLineStyleRenderer - case .graduatedLine: - graduatedLineStyleRenderer - case .line: - lineStyleRenderer - } + switch style { + case .alternatingBar: + alternatingBarStyleRenderer + case .bar: + barStyleRenderer + case .dualUnitLine: + dualUnitLineStyleRenderer + case .graduatedLine: + graduatedLineStyleRenderer + case .line: + lineStyleRenderer } } + .opacity(opacity) .onChange(of: viewpoint.wrappedValue) { viewModel.viewpointSubject.send($0) + if settings.autoHide { + withAnimation { + opacity = 1 + } + withAnimation(.default.delay(settings.hideTimeInterval)) { + opacity = .zero + } + } } .onSizeChange { height = $0.height diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift index 2924512e7..8bfa48b0e 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift @@ -13,8 +13,11 @@ import SwiftUI -/// Customizes scalebar appearance. +/// Customizes scalebar appearance and behavior. public struct ScalebarSettings { + /// Determines if the scalebar should automatically hide/show itself. + var autoHide: Bool + /// The corner radius used by bar style scalebar renders. var barCornerRadius: Double @@ -24,6 +27,9 @@ public struct ScalebarSettings { /// The lighter fill color used by the bar style renders. var fillColor2: Color + /// The time to wait in seconds before the scalebar hides itself. + var hideTimeInterval: TimeInterval = 1.75 + /// The color of the prominent scalebar line. var lineColor: Color @@ -40,27 +46,33 @@ public struct ScalebarSettings { var textShadowColor: Color /// - Parameters: + /// - autoHide: Determines if the scalebar should automatically hide/show itself. /// - barCornerRadius: The corner radius used by bar style scalebar renders. /// - fillColor1: The darker fill color used by the alternating bar style render. /// - fillColor2: The lighter fill color used by the bar style renders. + /// - hideTimeInterval: The time to wait in seconds before the scalebar hides itself. /// - lineColor: The color of the prominent scalebar line. /// - shadowColor: The shadow color used by all scalebar style renders. /// - shadowRadius: The shadow radius used by all scalebar style renders. /// - textColor: The text color used by all scalebar style renders. /// - textShadowColor: The text shadow color used by all scalebar style renders. public init( + autoHide: Bool = false, barCornerRadius: Double = 2.5, fillColor1: Color = .black, fillColor2: Color = Color(uiColor: .lightGray).opacity(0.5), + hideTimeInterval: TimeInterval = 1.75, lineColor: Color = .white, shadowColor: Color = Color(uiColor: .black).opacity(0.65), shadowRadius: Double = 1.0, textColor: Color = .primary, textShadowColor: Color = .white ) { + self.autoHide = autoHide self.barCornerRadius = barCornerRadius self.fillColor1 = fillColor1 self.fillColor2 = fillColor2 + self.hideTimeInterval = hideTimeInterval self.lineColor = lineColor self.shadowColor = shadowColor self.shadowRadius = shadowRadius diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift index 27033e250..309b97222 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarViewModel.swift @@ -23,9 +23,6 @@ final class ScalebarViewModel: ObservableObject { /// The computed display length of the scalebar. @Published var displayLength: CGFloat = .zero - /// Indicates if the scalebar should be hidden or not. - @Published var isVisible: Bool - /// The current set of labels to be displayed by the scalebar. @Published var labels = [ScalebarLabel]() @@ -72,7 +69,6 @@ final class ScalebarViewModel: ObservableObject { /// A scalebar view model controls the underlying data used to render a scalebar. /// - Parameters: - /// - autoHide: Determines if the scalebar should automatically show & hide itself. /// - maxWidth: The maximum screen width allotted to the scalebar. /// - minScale: A value of 0 indicates the scalebar segments should always recalculate. /// - spatialReference: The map's spatial reference. @@ -83,7 +79,6 @@ final class ScalebarViewModel: ObservableObject { /// the scale. /// - viewpoint: The map's current viewpoint. init( - _ autoHide: Bool, _ maxWidth: Double, _ minScale: Double, _ spatialReference: Binding, @@ -93,7 +88,6 @@ final class ScalebarViewModel: ObservableObject { _ useGeodeticCalculations: Bool, _ viewpoint: Viewpoint? ) { - self.isVisible = autoHide ? false : true self.maxWidth = maxWidth self.minScale = minScale self.spatialReference = spatialReference @@ -111,9 +105,6 @@ final class ScalebarViewModel: ObservableObject { } self.viewpoint = $0 self.updateScaleDisplay() - if autoHide { - self.performVisibilityAnimation() - } }) updateScaleDisplay() @@ -124,15 +115,9 @@ final class ScalebarViewModel: ObservableObject { /// The amount of time to wait between value calculations. private let delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.05) - /// The speed at which to animate `isVisible` to `true`. - private let displaySpeed = 3.0 - /// The curve type to use when performing scale calculations. private let geodeticCurveType: GeometryEngine.GeodeticCurveType = .geodesic - /// The time to wait in seconds before animating `isVisible` to `false`. - private let hideTimeInterval = 1.75 - /// A `minScale` of 0 means the scalebar segments will always recalculate. private let minScale: Double @@ -151,9 +136,6 @@ final class ScalebarViewModel: ObservableObject { // - MARK: Private vars - /// The timer to determine when to autohide the scalebar. - private var autoHideTimer: Timer? - /// Determines the amount of display space to use based on the scalebar style. private var availableLineDisplayLength: CGFloat { switch style { @@ -195,23 +177,6 @@ final class ScalebarViewModel: ObservableObject { // - MARK: Private methods - /// Animates `isVisible` between `true` and `false` as necessary. - private func performVisibilityAnimation() { - self.autoHideTimer?.invalidate() - withAnimation(.easeInOut.speed(displaySpeed)) { - self.isVisible = true - } - self.autoHideTimer = Timer.scheduledTimer( - withTimeInterval: hideTimeInterval, - repeats: false, - block: { _ in - withAnimation { - self.isVisible = false - } - } - ) - } - /// Updates the labels to be displayed by the scalebar. private func updateLabels() { let lineDisplayLength = displayLength @@ -358,9 +323,7 @@ final class ScalebarViewModel: ObservableObject { } self.displayLength = displayLength - self.displayUnit = displayUnit - self.lineMapLength = lineMapLength updateLabels() diff --git a/Tests/ArcGISToolkitTests/ScalebarTests.swift b/Tests/ArcGISToolkitTests/ScalebarTests.swift index b54548830..4b2cdc0c7 100644 --- a/Tests/ArcGISToolkitTests/ScalebarTests.swift +++ b/Tests/ArcGISToolkitTests/ScalebarTests.swift @@ -19,45 +19,6 @@ import XCTest @MainActor class ScalebarTests: XCTestCase { - /// Asserts that the scalebar view model correctly changes `isVisible`. - func testAutohide() { - var subscriptions = Set() - let expectation1 = expectation(description: "Went visible") - let expectation2 = expectation(description: "Went hidden") - var changedToVisible = false - let viewpoint1 = Viewpoint( - center: esriRedlands, - scale: 1 - ) - let viewpoint2 = Viewpoint( - center: esriRedlands, - scale: 2 - ) - let unitsPerPoint = unitsPerPointBinding(1) - let viewModel = ScalebarViewModel( - true, - 0, - 175, - spatialReferenceBinding(.webMercator), - .alternatingBar, - .imperial, - unitsPerPoint, - true, - viewpoint1 - ) - viewModel.$isVisible.sink { isVisible in - if isVisible && !changedToVisible { - changedToVisible = true - expectation1.fulfill() - } else if !isVisible && changedToVisible { - expectation2.fulfill() - } - } - .store(in: &subscriptions) - viewModel.viewpointSubject.send(viewpoint2) - waitForExpectations(timeout: 5.0) - } - struct ScalebarTestCase { let x: Double let y: Double @@ -116,7 +77,6 @@ class ScalebarTests: XCTestCase { ) let unitsPerPoint = unitsPerPointBinding(test.upp) let viewModel = ScalebarViewModel( - false, test.maxWidth, 0, spatialReferenceBinding(test.spatialReference), From 100ac897be51bc6281ee89244a64cd7b607e4d65 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 15:55:38 -0700 Subject: [PATCH 0824/1722] `ScalebarSettings.hideTimeInterval` -> `autoHideDelay` --- .../ArcGISToolkit/Components/Scalebar/Scalebar.swift | 2 +- .../Components/Scalebar/ScalebarSettings.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift index 6997ad535..ca19bf4ea 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/Scalebar.swift @@ -137,7 +137,7 @@ public struct Scalebar: View { withAnimation { opacity = 1 } - withAnimation(.default.delay(settings.hideTimeInterval)) { + withAnimation(.default.delay(settings.autoHideDelay)) { opacity = .zero } } diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift index 8bfa48b0e..47cf62df9 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarSettings.swift @@ -18,6 +18,9 @@ public struct ScalebarSettings { /// Determines if the scalebar should automatically hide/show itself. var autoHide: Bool + /// The time to wait in seconds before the scalebar hides itself. + var autoHideDelay: TimeInterval + /// The corner radius used by bar style scalebar renders. var barCornerRadius: Double @@ -27,9 +30,6 @@ public struct ScalebarSettings { /// The lighter fill color used by the bar style renders. var fillColor2: Color - /// The time to wait in seconds before the scalebar hides itself. - var hideTimeInterval: TimeInterval = 1.75 - /// The color of the prominent scalebar line. var lineColor: Color @@ -47,10 +47,10 @@ public struct ScalebarSettings { /// - Parameters: /// - autoHide: Determines if the scalebar should automatically hide/show itself. + /// - autoHideDelay: The time to wait in seconds before the scalebar hides itself. /// - barCornerRadius: The corner radius used by bar style scalebar renders. /// - fillColor1: The darker fill color used by the alternating bar style render. /// - fillColor2: The lighter fill color used by the bar style renders. - /// - hideTimeInterval: The time to wait in seconds before the scalebar hides itself. /// - lineColor: The color of the prominent scalebar line. /// - shadowColor: The shadow color used by all scalebar style renders. /// - shadowRadius: The shadow radius used by all scalebar style renders. @@ -58,10 +58,10 @@ public struct ScalebarSettings { /// - textShadowColor: The text shadow color used by all scalebar style renders. public init( autoHide: Bool = false, + autoHideDelay: TimeInterval = 1.75, barCornerRadius: Double = 2.5, fillColor1: Color = .black, fillColor2: Color = Color(uiColor: .lightGray).opacity(0.5), - hideTimeInterval: TimeInterval = 1.75, lineColor: Color = .white, shadowColor: Color = Color(uiColor: .black).opacity(0.65), shadowRadius: Double = 1.0, @@ -69,10 +69,10 @@ public struct ScalebarSettings { textShadowColor: Color = .white ) { self.autoHide = autoHide + self.autoHideDelay = autoHideDelay self.barCornerRadius = barCornerRadius self.fillColor1 = fillColor1 self.fillColor2 = fillColor2 - self.hideTimeInterval = hideTimeInterval self.lineColor = lineColor self.shadowColor = shadowColor self.shadowRadius = shadowRadius From 767403f2b0cd9141a11ea362a40292327ffc3795 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 18:52:12 -0700 Subject: [PATCH 0825/1722] Introduce/implement `filterWidth` modifier --- Examples/Examples/FloorFilterExampleView.swift | 6 +++++- .../Components/FloorFilter/FloorFilter.swift | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 132af9dc4..22a6d984d 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -71,7 +71,11 @@ struct FloorFilterExampleView: View { alignment: filterAlignment, viewpoint: $viewpoint ) - .frame(maxWidth: 300, maxHeight: 300) + .filterWidth(50) + .frame( + maxWidth: 300, + maxHeight: 300 + ) .padding(36) } else if mapLoadError { Label( diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 85eb3ec04..8801dfc84 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -48,6 +48,9 @@ public struct FloorFilter: View { /// The alignment configuration. private let alignment: Alignment + /// The width of the level selector. + private var filterWidth: Double = 50 + /// Button to open and close the site and facility selector. private var sitesAndFacilitiesButton: some View { Button { @@ -83,8 +86,8 @@ public struct FloorFilter: View { sitesAndFacilitiesButton } } + .frame(width: filterWidth) .esriBorder() - .frame(width: 75) .frame(maxHeight: .infinity, alignment: alignment) } @@ -122,4 +125,11 @@ public struct FloorFilter: View { .environmentObject(viewModel) .disabled(viewModel.isLoading) } + + /// Modifies the width used by the level selector. Increase this value to lessen level name truncation. + public func filterWidth(_ newWidth: Double) -> FloorFilter { + var copy = self + copy.filterWidth = newWidth + return copy + } } From 812fe327fd38cdf02c97a0ed3cda2ff08812b137 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 18:54:50 -0700 Subject: [PATCH 0826/1722] 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 b7807e8c8..74a3504ef 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -61,7 +61,7 @@ struct FloorFilterExampleView: View { alignment: filterAlignment, viewpoint: $viewpoint ) - .filterWidth(75) + .filterWidth(50) .frame( maxWidth: 400, maxHeight: 400 From a92b14e67bbc70a71fd8a74a5ee2bf63fd9cae55 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Apr 2022 19:32:00 -0700 Subject: [PATCH 0827/1722] Update ScalebarUnits.swift --- .../ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift index 442b0e2e7..8231ea75b 100644 --- a/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift +++ b/Sources/ArcGISToolkit/Components/Scalebar/ScalebarUnits.swift @@ -33,7 +33,7 @@ public enum ScalebarUnits { /// unit that will be displayed. /// `ScalebarUnits.metric` will return `LinearUnit.meters` as meter is the smallest linear /// unit that will be displayed. - internal var baseLinearUnit: LinearUnit { + var baseLinearUnit: LinearUnit { return self == .imperial ? LinearUnit.feet : LinearUnit.meters } @@ -104,7 +104,7 @@ public enum ScalebarUnits { /// - Returns: The best number of segments so that we get relatively round numbers when the /// distance is divided up. - internal static func numSegments( + static func numSegments( forDistance distance: Double, maxNumSegments: Int ) -> Int { @@ -116,7 +116,7 @@ public enum ScalebarUnits { /// Calculates a round number suitable for display. /// - Returns: A displayable round number. - internal func closestDistanceWithoutGoingOver( + func closestDistanceWithoutGoingOver( to distance: Double, units: LinearUnit ) -> Double { @@ -154,7 +154,7 @@ public enum ScalebarUnits { /// than or equal to 1/2 mile, and `LinearUnit.feet` otherwise. /// `ScalebarUnits.metric` will return `LinearUnit.kilometers` if the given distance is /// greater than or equal to 1 kilometer, and `LinearUnit.meters` otherwise. - internal func linearUnits(forDistance distance: Double) -> LinearUnit { + func linearUnits(forDistance distance: Double) -> LinearUnit { switch self { case .imperial: return distance >= 2640 ? .miles : .feet From eb4d4aa174a9e6f611067505bb412a5eeb2b125c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 4 May 2022 10:36:01 -0500 Subject: [PATCH 0828/1722] Update initializers and add view modifiers. --- Examples/Examples/SearchExampleView.swift | 35 +++-- .../Components/Search/SearchView.swift | 143 ++++++++++++++++-- .../Components/Search/SearchViewModel.swift | 54 +++---- 3 files changed, 174 insertions(+), 58 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 538978cb2..32f44943c 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -16,14 +16,10 @@ import ArcGIS import ArcGISToolkit struct SearchExampleView: View { - /// The `SearchViewModel` used to define behavior of the `SearchView`. - @ObservedObject - var searchViewModel = SearchViewModel( - sources: [SmartLocatorSearchSource( - name: "My locator", - maximumResults: 16, - maximumSuggestions: 16 - )] + let locatorDataSource = SmartLocatorSearchSource( + name: "My locator", + maximumResults: 16, + maximumSuggestions: 16 ) let map = Map(basemapStyle: .arcGISImagery) @@ -39,15 +35,19 @@ struct SearchExampleView: View { /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. let searchResultsOverlay = GraphicsOverlay() + @State var isGeoViewNavigating: Bool = false + @State var geoViewExtent: Envelope? = nil + @State var queryCenter: Point? = nil + var body: some View { MapView( map: map, viewpoint: searchResultViewpoint, graphicsOverlays: [searchResultsOverlay] ) - .onNavigatingChanged { searchViewModel.isGeoViewNavigating = $0 } + .onNavigatingChanged { isGeoViewNavigating = $0 } .onViewpointChanged(kind: .centerAndScale) { - searchViewModel.queryCenter = $0.targetGeometry as? Point + queryCenter = $0.targetGeometry as? Point // Reset `searchResultViewpoint` here when the user pans/zooms // the map, so if the user commits the same search with the @@ -63,15 +63,18 @@ struct SearchExampleView: View { // For "Repeat Search Here" behavior, set the // `searchViewModel.geoViewExtent` property when navigating. - searchViewModel.geoViewExtent = newValue.extent + geoViewExtent = newValue.extent } .overlay(alignment: .topTrailing) { - SearchView(searchViewModel: searchViewModel) + SearchView( + queryCenter: queryCenter, + sources: [locatorDataSource] + ) + .isGeoViewNavigating(isGeoViewNavigating) + .geoViewExtent(geoViewExtent) + .viewpoint($searchResultViewpoint) + .resultsOverlay(searchResultsOverlay) .padding() } - .onAppear { - searchViewModel.viewpoint = $searchResultViewpoint - searchViewModel.resultsOverlay = searchResultsOverlay - } } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index dc63ef9d8..086db5e6b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -22,24 +22,93 @@ public struct SearchView: View { /// - queryCenter: Defines the center for the search. /// - resultMode: Defines how many results to return. /// - sources: Collection of search sources to be used. - init( + public init( queryArea: Geometry? = nil, queryCenter: Point? = nil, resultMode: SearchResultMode = .automatic, sources: [SearchSource] = [] ) { - searchViewModel = SearchViewModel( + _viewModel = StateObject(wrappedValue: SearchViewModel( queryArea: queryArea, queryCenter: queryCenter, resultMode: resultMode, - sources: [sources.isEmpty ? [LocatorSearchSource()] : sources] - ) + sources: sources.isEmpty ? [LocatorSearchSource()] : sources + )) } - + /// The view model used by the view. The `SearchViewModel` manages state and handles the /// activity of searching. The view observes `SearchViewModel` for changes in state. The view /// calls methods on `SearchViewModel` in response to user action. - @ObservedObject var searchViewModel: SearchViewModel + @StateObject private var viewModel: SearchViewModel + + /// Tracks the current user-entered query. This property drives both suggestions and searches. + public var currentQuery: String { + viewModel.currentQuery + } + + /// The current map/scene view extent. Defaults to `nil`. + /// + /// This should be updated via `geoViewExtent(:)`as the user navigates the map/scene. It will be + /// used to determine the value of `isEligibleForRequery` for the 'Repeat + /// search here' behavior. If that behavior is not wanted, it should be left `nil`. + public var geoViewExtent: Envelope? { + viewModel.geoViewExtent + } + + /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. + public var viewpoint: Binding? { + viewModel.viewpoint + } + + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. + public var resultsOverlay: GraphicsOverlay? { + viewModel.resultsOverlay + } + + /// The search area to be used for the current query. Results will be limited to those. + /// within `QueryArea`. Defaults to `nil`. + public var queryArea: Geometry? { + viewModel.queryArea + } + + /// Defines the center for the search. For most use cases, this should be updated by the view + /// every time the user navigates the map. + public var queryCenter: Point? { + viewModel.queryCenter + } + + /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate + /// number of results is returned based on the type of suggestion chosen + /// (driven by the suggestion's `isCollection` property). + public var resultMode: SearchResultMode { + viewModel.resultMode + } + + /// The collection of search and suggestion results. A `nil` value means no query has been made. + var searchOutcome: SearchOutcome? { + viewModel.searchOutcome + } + + /// Tracks selection of results from the `results` collection. When there is only one result, + /// that result is automatically assigned to this property. If there are multiple results, the view sets + /// this property upon user selection. This property is observable. The view should observe this + /// property and update the associated GeoView's viewpoint, if configured. + public var selectedResult: SearchResult? { + viewModel.selectedResult + } + + /// Collection of search sources to be used. This list is maintained over time and is not nullable. + /// The view should observe this list for changes. Consumers should add and remove sources from + /// this list as needed. + /// NOTE: Only the first source is currently used; multiple sources are not yet supported. + public var sources: [SearchSource] { + viewModel.sources + } + + /// The suggestion currently selected by the user. + public var currentSuggestion: SearchSuggestion? { + viewModel.currentSuggestion + } @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -84,29 +153,29 @@ public struct SearchView: View { Spacer() VStack { SearchField( - query: $searchViewModel.currentQuery, + query: $viewModel.currentQuery, prompt: prompt, isResultsButtonHidden: !enableResultListView, isResultListHidden: $isResultListHidden ) - .onSubmit { searchViewModel.commitSearch() } + .onSubmit { viewModel.commitSearch() } .submitLabel(.search) if enableResultListView, !isResultListHidden, - let searchOutcome = searchViewModel.searchOutcome { + let searchOutcome = viewModel.searchOutcome { Group { switch searchOutcome { case .results(let results): SearchResultList( searchResults: results, - selectedResult: $searchViewModel.selectedResult, + selectedResult: $viewModel.selectedResult, noResultsMessage: noResultsMessage ) .frame(height: useHalfHeightResults ? geometry.size.height / 2 : nil) case .suggestions(let suggestions): SearchSuggestionList( suggestionResults: suggestions, - currentSuggestion: $searchViewModel.currentSuggestion, + currentSuggestion: $viewModel.currentSuggestion, noResultsMessage: noResultsMessage ) case .failure(let errorString): @@ -122,16 +191,16 @@ public struct SearchView: View { } } Spacer() - if searchViewModel.isEligibleForRequery { + if viewModel.isEligibleForRequery { Button("Repeat Search Here") { - searchViewModel.repeatSearch() + viewModel.repeatSearch() } .esriBorder() } } .listStyle(.plain) - .onReceive(searchViewModel.$currentQuery) { _ in - searchViewModel.updateSuggestions() + .onReceive(viewModel.$currentQuery) { _ in + viewModel.updateSuggestions() } } } @@ -170,6 +239,50 @@ extension SearchView { copy.noResultsMessage = newNoResultsMessage return copy } + + /// Sets the current query. + /// - Parameter newQueryString: The new value. + /// - Returns: The `SearchView`. + public func currentQuery(_ newQuery: String) -> Self { + viewModel.currentQuery = newQuery + return self + } + + /// The current map/scene view extent. Defaults to `nil`. + /// + /// This should be updated as the user navigates the map/scene. It will be + /// used to determine the value of `isEligibleForRequery` for the 'Repeat + /// search here' behavior. If that behavior is not wanted, it should be left `nil`. + /// - Parameter newExtent: The new value. + /// - Returns: The `SearchView`. + public func geoViewExtent(_ newExtent: Envelope?) -> Self { + viewModel.geoViewExtent = newExtent + return self + } + + /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. + /// - Parameter newIsGeoViewNavigating: The new value. + /// - Returns: The `SearchView`. + public func isGeoViewNavigating(_ newIsGeoViewNavigating: Bool) -> Self { + viewModel.isGeoViewNavigating = newIsGeoViewNavigating + return self + } + + /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. + /// - Parameter newViewpoint: The new value. + /// - Returns: The `SearchView`. + public func viewpoint(_ newViewpoint: Binding?) -> Self { + viewModel.viewpoint = newViewpoint + return self + } + + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. + /// - Parameter newResultsOverlay: The new value. + /// - Returns: The `SearchView`. + public func resultsOverlay(_ newResultsOverlay: GraphicsOverlay?) -> Self { + viewModel.resultsOverlay = newResultsOverlay + return self + } } /// A View displaying the list of search results. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 389942611..9248d7a90 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -14,32 +14,32 @@ import SwiftUI import ArcGIS +/// Defines how many results to return; one, many, or automatic based on circumstance. +public enum SearchResultMode { + /// Search should always result in at most one result. + case single + /// Search should always try to return multiple results. + case multiple + /// Search should make a choice based on context. E.g. 'coffee shop' should be multiple results, + /// while '380 New York St. Redlands' should be one result. + case automatic +} + +/// The outcome of a geocode operation (search or suggestion). +/// +/// An empty results or suggestions array means there were no results. +/// +/// The `failure` case contains the error (if any) generated by the last search or suggestion +/// operation. +public enum SearchOutcome { + case results([SearchResult]) + case suggestions([SearchSuggestion]) + case failure(String) +} + /// Performs searches and manages search state for a search, or optionally without a UI connection. @MainActor -class SearchViewModel: ObservableObject { - /// Defines how many results to return; one, many, or automatic based on circumstance. - enum SearchResultMode { - /// Search should always result in at most one result. - case single - /// Search should always try to return multiple results. - case multiple - /// Search should make a choice based on context. E.g. 'coffee shop' should be multiple results, - /// while '380 New York St. Redlands' should be one result. - case automatic - } - - /// The outcome of a geocode operation (search or suggestion). - /// - /// An empty results or suggestions array means there were no results. - /// - /// The `failure` case contains the error (if any) generated by the last search or suggestion - /// operation. - enum SearchOutcome { - case results([SearchResult]) - case suggestions([SearchSuggestion]) - case failure(String) - } - +final class SearchViewModel: ObservableObject { /// Creates a `SearchViewModel`. /// - Parameters: /// - queryArea: The search area to be used for the current query. @@ -147,7 +147,7 @@ class SearchViewModel: ObservableObject { @Published private(set) var isEligibleForRequery: Bool = false - /// The search area to be used for the current query. Results will be limited to those + /// The search area to be used for the current query. Results will be limited to those. /// within `QueryArea`. Defaults to `nil`. var queryArea: Geometry? = nil @@ -157,7 +157,7 @@ class SearchViewModel: ObservableObject { /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate /// number of results is returned based on the type of suggestion chosen - /// (driven by the `isCollection` property). + /// (driven by the suggestion's `isCollection` property). var resultMode: SearchResultMode = .automatic /// The collection of search and suggestion results. A `nil` value means no query has been made. @@ -408,7 +408,7 @@ private extension SearchViewModel { } } -extension SearchViewModel.SearchOutcome: Equatable {} +extension SearchOutcome: Equatable {} private extension Graphic { func update(with result: SearchResult) { From 4ca3b5d8626ba992978383d7ae4a407b5c9d8f72 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 4 May 2022 17:35:30 -0700 Subject: [PATCH 0829/1722] `filterAlignment` -> `floorFilterAlignment` --- Examples/Examples/FloorFilterExampleView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 22a6d984d..95142f66c 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -34,7 +34,7 @@ struct FloorFilterExampleView: View { } /// Determines the arrangement of the inner `FloorFilter` UI componenets. - private let filterAlignment = Alignment.bottomLeading + private let floorFilterAlignment = Alignment.bottomLeading /// Determines the appropriate time to initialize the `FloorFilter`. @State private var isMapLoaded: Bool = false @@ -63,12 +63,12 @@ struct FloorFilterExampleView: View { } /// Preserve the current viewpoint when a keyboard is presented in landscape. .ignoresSafeArea(.keyboard, edges: .bottom) - .overlay(alignment: filterAlignment) { + .overlay(alignment: floorFilterAlignment) { if isMapLoaded, let floorManager = map.floorManager { FloorFilter( floorManager: floorManager, - alignment: filterAlignment, + alignment: floorFilterAlignment, viewpoint: $viewpoint ) .filterWidth(50) From c12af2d0005037695734706dbb681d80fa30eeb2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 4 May 2022 17:44:19 -0700 Subject: [PATCH 0830/1722] Simplify usage of viewModel.hasLevelsToDisplay --- .../Components/FloorFilter/FloorFilter.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 8801dfc84..5c5453811 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -68,7 +68,6 @@ public struct FloorFilter: View { levels: viewModel.sortedLevels, isTopAligned: topAligned ) - .hidden(!viewModel.hasLevelsToDisplay) } /// A view that allows selecting between levels. @@ -76,13 +75,15 @@ public struct FloorFilter: View { VStack { if topAligned { sitesAndFacilitiesButton - Divider() - .hidden(!viewModel.hasLevelsToDisplay) - levelSelector + if viewModel.hasLevelsToDisplay { + Divider() + levelSelector + } } else { - levelSelector - Divider() - .hidden(!viewModel.hasLevelsToDisplay) + if viewModel.hasLevelsToDisplay { + levelSelector + Divider() + } sitesAndFacilitiesButton } } From 1ba8625c0df78815cce8d0558dbaba71e8f6e657 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 May 2022 12:05:30 -0500 Subject: [PATCH 0831/1722] Add error icon in place of graying out the thumbnail. --- .../BasemapGallery/BasemapGalleryCell.swift | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index 1ffccf29c..21bd66402 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -25,6 +25,7 @@ struct BasemapGalleryCell: View { let onSelection: () -> Void var body: some View { + let roundedRect = RoundedRectangle(cornerRadius: 8) Button(action: { onSelection() }, label: { @@ -32,13 +33,18 @@ struct BasemapGalleryCell: View { ZStack(alignment: .center) { // Display the thumbnail, if available. if let thumbnailImage = item.thumbnail { - Image(uiImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .border( - isSelected ? Color.accentColor : Color.clear, - width: 3 - ) + Group { + Image(uiImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(roundedRect) + if isSelected { + roundedRect + .stroke(lineWidth: 2) + .foregroundColor(Color.accentColor) + } + } + .padding(.all, 4) } // Display an overlay if the item has an error. @@ -64,9 +70,23 @@ struct BasemapGalleryCell: View { /// Creates a partially transparent rectangle, used to denote a basemap with an error. /// - Returns: A new transparent rectagle view. private func makeErrorOverlay() -> some View { - Rectangle() - .foregroundColor(.secondary) - .opacity(0.75) + HStack { + Spacer() + VStack{ + ZStack { + // For a white background behind the exclamation mark. + Circle() + .foregroundColor(.white) + .frame(width: 16, height: 16) + Image(systemName: "exclamationmark.circle.fill") + .foregroundColor(.red) + .background(Color.clear) + .frame(width: 32, height: 32) + } + Spacer() + } + } + .padding(.all, -6) } /// Creates a circular progress view with a rounded rectangle background. From 9360dee9a3bf21c209460cc90ae080f34b0788fe Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 May 2022 12:29:25 -0500 Subject: [PATCH 0832/1722] Update doc for updated error overlay. --- .../Components/BasemapGallery/BasemapGalleryCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift index 21bd66402..3490f81d5 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryCell.swift @@ -67,7 +67,7 @@ struct BasemapGalleryCell: View { }).disabled(item.isBasemapLoading) } - /// Creates a partially transparent rectangle, used to denote a basemap with an error. + /// Creates a red exclamation mark, used to denote a basemap with an error. /// - Returns: A new transparent rectagle view. private func makeErrorOverlay() -> some View { HStack { From 222228dcedafaadc00d80598a8a5d885aa3ae0b9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 5 May 2022 16:31:02 -0700 Subject: [PATCH 0833/1722] Apply updates --- .../Components/Compass/Compass.swift | 20 +++++++++---------- .../Components/Compass/CompassBody.swift | 2 +- .../Components/Compass/Needle.swift | 10 +++++----- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 18fafb0e6..26970129f 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -19,22 +19,20 @@ public struct Compass: View { /// Determines if the compass should automatically hide/show itself when the parent view is oriented /// north. private let autoHide: Bool - + /// Controls the current opacity of the compass. - @State - private var opacity: Double - + @State private var opacity: Double + /// Indicates if the compass should hide based on the current viewpoint rotation and autoHide /// preference. var shouldHide: Bool { guard let viewpoint = viewpoint else { return autoHide } return viewpoint.rotation.isZero && autoHide } - + /// Acts as link between the compass and the parent map or scene view. - @Binding - private var viewpoint: Viewpoint? - + @Binding private var viewpoint: Viewpoint? + /// Creates a `Compass` /// - Parameters: /// - viewpoint: Acts a communication link between the `MapView` or `SceneView` and the @@ -49,7 +47,7 @@ public struct Compass: View { _opacity = State(initialValue: .zero) self.autoHide = autoHide } - + public var body: some View { GeometryReader { geometry in ZStack { @@ -76,7 +74,7 @@ public struct Compass: View { .onAppear { opacity = shouldHide ? 0 : 1 } .accessibilityLabel(viewpoint?.compassHeadingDescription ?? "Compass") } - + /// Resets the viewpoints `rotation` to zero. func resetHeading() { guard let viewpoint = viewpoint else { return } @@ -94,7 +92,7 @@ internal extension Viewpoint { var compassHeading: Double { rotation.isZero ? .zero : 360 - rotation } - + /// A text description of the current heading, sutiable for accessibility voiceover. var compassHeadingDescription: String { "Compass, heading " diff --git a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift index adb0e15bc..c3622eef1 100644 --- a/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift +++ b/Sources/ArcGISToolkit/Components/Compass/CompassBody.swift @@ -30,7 +30,7 @@ struct CompassBody: View { private extension Color { /// The background color of the compass housing. static let fill = Color(red: 228, green: 240, blue: 244) - + /// The outline color of the compass housing. static let outline = Color(red: 127, green: 127, blue: 127) } diff --git a/Sources/ArcGISToolkit/Components/Compass/Needle.swift b/Sources/ArcGISToolkit/Components/Compass/Needle.swift index e19f71186..349e18120 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Needle.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Needle.swift @@ -65,7 +65,7 @@ struct NeedleCenter: View { struct NeedleQuadrant: View { /// The color of this needle quadrant. let color: Color - + var body: some View { GeometryReader { geometry in Path { path in @@ -83,16 +83,16 @@ struct NeedleQuadrant: View { private extension Color { /// The bronze color of the center of the compass needle. static let bronze = Color(red: 241, green: 169, blue: 59) - + /// The dark gray color of the compass needle. static let darkGray = Color(red: 128, green: 128, blue: 128) - + /// The dark red color of the compass needle. static let darkRed = Color(red: 124, green: 22, blue: 13) - + /// The light gray color of the compass needle. static let lightGray = Color(red: 169, green: 168, blue: 168) - + /// The light red color of the compass needle. static let lightRed = Color(red: 233, green: 51, blue: 35) } From 533c866def457a0fc41875dba5681a7b560bea0a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 5 May 2022 16:50:31 -0700 Subject: [PATCH 0834/1722] Revert use of `MapScale` --- .../Components/FloorFilter/FloorFilterViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 6afbd9df4..0e5e72ec9 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -221,7 +221,7 @@ final class FloorFilterViewModel: ObservableObject { private func autoSelectFacility() -> Bool { // Only select a facility if it is within minimum scale. Default at 1500. let facilityMinScale: Double - if let minScale = floorManager.facilityLayer?.scaleRange?.lowerBound.inverseRatio, + if let minScale = floorManager.facilityLayer?.minScale, minScale != .zero { facilityMinScale = minScale } else { @@ -255,7 +255,7 @@ final class FloorFilterViewModel: ObservableObject { private func autoSelectSite() -> Bool { // Only select a facility if it is within minimum scale. Default at 4300. let siteMinScale: Double - if let minScale = floorManager.siteLayer?.scaleRange?.lowerBound.inverseRatio, + if let minScale = floorManager.siteLayer?.minScale, minScale != .zero { siteMinScale = minScale } else { From 6f408a59e5e90be7fb7f0cdfe0faee3dd4d78f79 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 6 May 2022 11:50:13 -0500 Subject: [PATCH 0835/1722] Proper modifiers for properties. --- Examples/Examples/SearchExampleView.swift | 2 +- .../Components/Search/SearchView.swift | 93 +++++++++---------- .../Components/Search/SearchViewModel.swift | 20 ++-- 3 files changed, 53 insertions(+), 62 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 32f44943c..0035ae207 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -67,7 +67,7 @@ struct SearchExampleView: View { } .overlay(alignment: .topTrailing) { SearchView( - queryCenter: queryCenter, + queryCenter: $queryCenter, sources: [locatorDataSource] ) .isGeoViewNavigating(isGeoViewNavigating) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 086db5e6b..e7becf159 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -23,8 +23,8 @@ public struct SearchView: View { /// - resultMode: Defines how many results to return. /// - sources: Collection of search sources to be used. public init( - queryArea: Geometry? = nil, - queryCenter: Point? = nil, + queryArea: Binding? = nil, + queryCenter: Binding? = nil, resultMode: SearchResultMode = .automatic, sources: [SearchSource] = [] ) { @@ -42,47 +42,20 @@ public struct SearchView: View { @StateObject private var viewModel: SearchViewModel /// Tracks the current user-entered query. This property drives both suggestions and searches. - public var currentQuery: String { - viewModel.currentQuery - } + var currentQuery: String = "" /// The current map/scene view extent. Defaults to `nil`. /// /// This should be updated via `geoViewExtent(:)`as the user navigates the map/scene. It will be /// used to determine the value of `isEligibleForRequery` for the 'Repeat /// search here' behavior. If that behavior is not wanted, it should be left `nil`. - public var geoViewExtent: Envelope? { - viewModel.geoViewExtent - } + var geoViewExtent: Envelope? = nil /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. - public var viewpoint: Binding? { - viewModel.viewpoint - } + var viewpoint: Binding? = nil /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. - public var resultsOverlay: GraphicsOverlay? { - viewModel.resultsOverlay - } - - /// The search area to be used for the current query. Results will be limited to those. - /// within `QueryArea`. Defaults to `nil`. - public var queryArea: Geometry? { - viewModel.queryArea - } - - /// Defines the center for the search. For most use cases, this should be updated by the view - /// every time the user navigates the map. - public var queryCenter: Point? { - viewModel.queryCenter - } - - /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate - /// number of results is returned based on the type of suggestion chosen - /// (driven by the suggestion's `isCollection` property). - public var resultMode: SearchResultMode { - viewModel.resultMode - } + var resultsOverlay: GraphicsOverlay? = nil /// The collection of search and suggestion results. A `nil` value means no query has been made. var searchOutcome: SearchOutcome? { @@ -93,21 +66,25 @@ public struct SearchView: View { /// that result is automatically assigned to this property. If there are multiple results, the view sets /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. - public var selectedResult: SearchResult? { - viewModel.selectedResult + @State public var selectedResult: SearchResult? { + didSet { + viewModel.selectedResult = selectedResult + } } /// Collection of search sources to be used. This list is maintained over time and is not nullable. /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. /// NOTE: Only the first source is currently used; multiple sources are not yet supported. - public var sources: [SearchSource] { + var sources: [SearchSource] { viewModel.sources } /// The suggestion currently selected by the user. - public var currentSuggestion: SearchSuggestion? { - viewModel.currentSuggestion + @State public var currentSuggestion: SearchSuggestion? { + didSet { + viewModel.currentSuggestion = currentSuggestion + } } @Environment(\.horizontalSizeClass) var horizontalSizeClass @@ -146,6 +123,9 @@ public struct SearchView: View { /// Determines whether the results lists are displayed. @State private var isResultListHidden: Bool = false + private var isGeoViewNavigating: Bool = false + + public var body: some View { VStack { GeometryReader { geometry in @@ -162,20 +142,20 @@ public struct SearchView: View { .submitLabel(.search) if enableResultListView, !isResultListHidden, - let searchOutcome = viewModel.searchOutcome { + let searchOutcome = searchOutcome { Group { switch searchOutcome { case .results(let results): SearchResultList( searchResults: results, - selectedResult: $viewModel.selectedResult, + selectedResult: $selectedResult, noResultsMessage: noResultsMessage ) .frame(height: useHalfHeightResults ? geometry.size.height / 2 : nil) case .suggestions(let suggestions): SearchSuggestionList( suggestionResults: suggestions, - currentSuggestion: $viewModel.currentSuggestion, + currentSuggestion: $currentSuggestion, noResultsMessage: noResultsMessage ) case .failure(let errorString): @@ -202,6 +182,12 @@ public struct SearchView: View { .onReceive(viewModel.$currentQuery) { _ in viewModel.updateSuggestions() } + .onAppear() { + viewModel.currentQuery = currentQuery + viewModel.geoViewExtent = geoViewExtent + viewModel.viewpoint = viewpoint + viewModel.resultsOverlay = resultsOverlay + } } } @@ -244,8 +230,9 @@ extension SearchView { /// - Parameter newQueryString: The new value. /// - Returns: The `SearchView`. public func currentQuery(_ newQuery: String) -> Self { - viewModel.currentQuery = newQuery - return self + var copy = self + copy.currentQuery = newQuery + return copy } /// The current map/scene view extent. Defaults to `nil`. @@ -256,32 +243,36 @@ extension SearchView { /// - Parameter newExtent: The new value. /// - Returns: The `SearchView`. public func geoViewExtent(_ newExtent: Envelope?) -> Self { - viewModel.geoViewExtent = newExtent - return self + var copy = self + copy.geoViewExtent = newExtent + return copy } /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. /// - Parameter newIsGeoViewNavigating: The new value. /// - Returns: The `SearchView`. public func isGeoViewNavigating(_ newIsGeoViewNavigating: Bool) -> Self { - viewModel.isGeoViewNavigating = newIsGeoViewNavigating - return self + var copy = self + copy.isGeoViewNavigating = newIsGeoViewNavigating + return copy } /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. /// - Parameter newViewpoint: The new value. /// - Returns: The `SearchView`. public func viewpoint(_ newViewpoint: Binding?) -> Self { - viewModel.viewpoint = newViewpoint - return self + var copy = self + copy.viewpoint = newViewpoint + return copy } /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. /// - Parameter newResultsOverlay: The new value. /// - Returns: The `SearchView`. public func resultsOverlay(_ newResultsOverlay: GraphicsOverlay?) -> Self { - viewModel.resultsOverlay = newResultsOverlay - return self + var copy = self + copy.resultsOverlay = newResultsOverlay + return copy } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 9248d7a90..fd4f05e97 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -47,8 +47,8 @@ final class SearchViewModel: ObservableObject { /// - resultMode: Defines how many results to return. /// - sources: Collection of search sources to be used. init( - queryArea: Geometry? = nil, - queryCenter: Point? = nil, + queryArea: Binding? = nil, + queryCenter: Binding? = nil, resultMode: SearchResultMode = .automatic, sources: [SearchSource] = [] ) { @@ -149,11 +149,11 @@ final class SearchViewModel: ObservableObject { /// The search area to be used for the current query. Results will be limited to those. /// within `QueryArea`. Defaults to `nil`. - var queryArea: Geometry? = nil + var queryArea: Binding? = nil /// Defines the center for the search. For most use cases, this should be updated by the view /// every time the user navigates the map. - var queryCenter: Point? + var queryCenter: Binding? /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate /// number of results is returned based on the type of suggestion chosen @@ -272,8 +272,8 @@ private extension SearchViewModel { await search(with: { try await source.search( currentQuery, - searchArea: queryArea, - preferredSearchLocation: queryCenter + searchArea: queryArea?.wrappedValue, + preferredSearchLocation: queryCenter?.wrappedValue ) } ) } @@ -287,8 +287,8 @@ private extension SearchViewModel { do { let suggestions = try await source.suggest( currentQuery, - searchArea: queryArea, - preferredSearchLocation: queryCenter + searchArea: queryArea?.wrappedValue, + preferredSearchLocation: queryCenter?.wrappedValue ) searchOutcome = .suggestions(suggestions) } catch is CancellationError { @@ -305,8 +305,8 @@ private extension SearchViewModel { with: { try await searchSuggestion.owningSource.search( searchSuggestion, - searchArea: queryArea, - preferredSearchLocation: queryCenter + searchArea: queryArea?.wrappedValue, + preferredSearchLocation: queryCenter?.wrappedValue ) }, isCollection: searchSuggestion.isCollection From 4fff4a6ba102c610b3150fb76d55eddef1b282ab Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 6 May 2022 17:18:07 -0700 Subject: [PATCH 0836/1722] Refactor width handling --- .../Examples/FloorFilterExampleView.swift | 1 - .../Components/FloorFilter/FloorFilter.swift | 9 +------- .../FloorFilter/LevelSelector.swift | 21 +++++++++++++------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 95142f66c..86f209b5f 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -71,7 +71,6 @@ struct FloorFilterExampleView: View { alignment: floorFilterAlignment, viewpoint: $viewpoint ) - .filterWidth(50) .frame( maxWidth: 300, maxHeight: 300 diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 5c5453811..37456f7bf 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -49,7 +49,7 @@ public struct FloorFilter: View { private let alignment: Alignment /// The width of the level selector. - private var filterWidth: Double = 50 + private var filterWidth: Double = 60 /// Button to open and close the site and facility selector. private var sitesAndFacilitiesButton: some View { @@ -126,11 +126,4 @@ public struct FloorFilter: View { .environmentObject(viewModel) .disabled(viewModel.isLoading) } - - /// Modifies the width used by the level selector. Increase this value to lessen level name truncation. - public func filterWidth(_ newWidth: Double) -> FloorFilter { - var copy = self - copy.filterWidth = newWidth - return copy - } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 7db1a8a52..ab78bef8f 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -43,19 +43,16 @@ struct LevelSelector: View { if !isTopAligned { CollapseButton(isCollapsed: $isCollapsed) Divider() - .frame(width: 30) } LevelsStack(levels: levels) if isTopAligned { Divider() - .frame(width: 30) CollapseButton(isCollapsed: $isCollapsed) } } } else { Toggle(isOn: $isCollapsed) { - Text(selectedLevelName) - .lineLimit(1) + LevelText(levelName: selectedLevelName) } .toggleStyle(.selectedButton) } @@ -88,8 +85,7 @@ struct LevelsStack: View { } ) ) { - Text(level.shortName) - .lineLimit(1) + LevelText(levelName: level.shortName) } .toggleStyle(.selectableButton) } @@ -102,6 +98,19 @@ struct LevelsStack: View { } } +/// Intended to display the name of a level. +struct LevelText: View { + /// The name of the level to be displayed. + var levelName: String + + var body: some View { + Text(levelName) + .lineLimit(1) + .fixedSize() + .frame(minWidth: 40) + } +} + /// A button used to collapse the floor level list. struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. From 2015eec06bb24b3f3097eb5e6dc03ef26a6031e6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 9 May 2022 11:41:38 -0700 Subject: [PATCH 0837/1722] Update README.md --- Documentation/Scalebar/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Documentation/Scalebar/README.md b/Documentation/Scalebar/README.md index 55ffb1bba..412219c10 100644 --- a/Documentation/Scalebar/README.md +++ b/Documentation/Scalebar/README.md @@ -7,6 +7,18 @@ The scalebar uses geodetic calculations to provide accurate measurements for map ### Usage ```swift +MapView(map: map) + .onSpatialReferenceChanged { spatialReference = $0 } + .onUnitsPerPointChanged { unitsPerPoint = $0 } + .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } + .overlay(alignment: alignment) { + Scalebar( + maxWidth: maxWidth, + spatialReference: $spatialReference, + unitsPerPoint: $unitsPerPoint, + viewpoint: $viewpoint + ) + } ``` To see it in action, try out the [Examples](../../Examples) and refer to [ScalebarExampleView.swift](../../Examples/Examples/ScalebarExampleView.swift) in the project. From 7b79a42d6a78e15700b96f940690c9e8b3751f73 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 9 May 2022 15:24:08 -0500 Subject: [PATCH 0838/1722] More updates for @State and @Published --- Examples/Examples/SearchExampleView.swift | 21 +++--- .../Components/Search/SearchView.swift | 70 +++++-------------- .../Components/Search/SearchViewModel.swift | 5 +- 3 files changed, 31 insertions(+), 65 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 0035ae207..c454f1869 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -37,6 +37,7 @@ struct SearchExampleView: View { @State var isGeoViewNavigating: Bool = false @State var geoViewExtent: Envelope? = nil + @State var queryArea: Geometry? = nil @State var queryCenter: Point? = nil var body: some View { @@ -57,21 +58,23 @@ struct SearchExampleView: View { searchResultViewpoint = nil } .onVisibleAreaChanged { newValue in - // Setting `searchViewModel.queryArea` will limit the - // results to `queryArea`. - //searchViewModel.queryArea = newValue - - // For "Repeat Search Here" behavior, set the - // `searchViewModel.geoViewExtent` property when navigating. + // For "Repeat Search Here" behavior, pass the `geoViewExtent` + // to the `searchView.geoViewExtent` modifier. geoViewExtent = newValue.extent + + // You can also use the visible area in the `SearchView` + // initializer to limit the results to `queryArea` + // to limit the search results. +// queryArea = newValue } .overlay(alignment: .topTrailing) { SearchView( +// queryArea: $queryArea, queryCenter: $queryCenter, - sources: [locatorDataSource] + sources: [locatorDataSource], + geoViewExtent: $geoViewExtent, + isGeoViewNavigating: $isGeoViewNavigating ) - .isGeoViewNavigating(isGeoViewNavigating) - .geoViewExtent(geoViewExtent) .viewpoint($searchResultViewpoint) .resultsOverlay(searchResultsOverlay) .padding() diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e7becf159..d2de36717 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -26,7 +26,9 @@ public struct SearchView: View { queryArea: Binding? = nil, queryCenter: Binding? = nil, resultMode: SearchResultMode = .automatic, - sources: [SearchSource] = [] + sources: [SearchSource] = [], + geoViewExtent: Binding? = nil, + isGeoViewNavigating: Binding? = nil ) { _viewModel = StateObject(wrappedValue: SearchViewModel( queryArea: queryArea, @@ -34,6 +36,9 @@ public struct SearchView: View { resultMode: resultMode, sources: sources.isEmpty ? [LocatorSearchSource()] : sources )) + + _geoViewExtent = geoViewExtent ?? Binding.constant(nil) + _isGeoViewNavigating = isGeoViewNavigating ?? Binding.constant(false) } /// The view model used by the view. The `SearchViewModel` manages state and handles the @@ -49,7 +54,7 @@ public struct SearchView: View { /// This should be updated via `geoViewExtent(:)`as the user navigates the map/scene. It will be /// used to determine the value of `isEligibleForRequery` for the 'Repeat /// search here' behavior. If that behavior is not wanted, it should be left `nil`. - var geoViewExtent: Envelope? = nil + @Binding var geoViewExtent: Envelope? /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. var viewpoint: Binding? = nil @@ -57,21 +62,6 @@ public struct SearchView: View { /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. var resultsOverlay: GraphicsOverlay? = nil - /// The collection of search and suggestion results. A `nil` value means no query has been made. - var searchOutcome: SearchOutcome? { - viewModel.searchOutcome - } - - /// Tracks selection of results from the `results` collection. When there is only one result, - /// that result is automatically assigned to this property. If there are multiple results, the view sets - /// this property upon user selection. This property is observable. The view should observe this - /// property and update the associated GeoView's viewpoint, if configured. - @State public var selectedResult: SearchResult? { - didSet { - viewModel.selectedResult = selectedResult - } - } - /// Collection of search sources to be used. This list is maintained over time and is not nullable. /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. @@ -80,13 +70,6 @@ public struct SearchView: View { viewModel.sources } - /// The suggestion currently selected by the user. - @State public var currentSuggestion: SearchSuggestion? { - didSet { - viewModel.currentSuggestion = currentSuggestion - } - } - @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -123,9 +106,9 @@ public struct SearchView: View { /// Determines whether the results lists are displayed. @State private var isResultListHidden: Bool = false - private var isGeoViewNavigating: Bool = false + /// Determines whether the geoView is navigating in response to user interaction. + @Binding private var isGeoViewNavigating: Bool - public var body: some View { VStack { GeometryReader { geometry in @@ -142,20 +125,20 @@ public struct SearchView: View { .submitLabel(.search) if enableResultListView, !isResultListHidden, - let searchOutcome = searchOutcome { + let searchOutcome = viewModel.searchOutcome { Group { switch searchOutcome { case .results(let results): SearchResultList( searchResults: results, - selectedResult: $selectedResult, + selectedResult: $viewModel.selectedResult, noResultsMessage: noResultsMessage ) .frame(height: useHalfHeightResults ? geometry.size.height / 2 : nil) case .suggestions(let suggestions): SearchSuggestionList( suggestionResults: suggestions, - currentSuggestion: $currentSuggestion, + currentSuggestion: $viewModel.currentSuggestion, noResultsMessage: noResultsMessage ) case .failure(let errorString): @@ -182,9 +165,14 @@ public struct SearchView: View { .onReceive(viewModel.$currentQuery) { _ in viewModel.updateSuggestions() } + .onChange(of: geoViewExtent) { _ in + viewModel.geoViewExtent = geoViewExtent + } + .onChange(of: isGeoViewNavigating) { _ in + viewModel.isGeoViewNavigating = isGeoViewNavigating + } .onAppear() { viewModel.currentQuery = currentQuery - viewModel.geoViewExtent = geoViewExtent viewModel.viewpoint = viewpoint viewModel.resultsOverlay = resultsOverlay } @@ -235,28 +223,6 @@ extension SearchView { return copy } - /// The current map/scene view extent. Defaults to `nil`. - /// - /// This should be updated as the user navigates the map/scene. It will be - /// used to determine the value of `isEligibleForRequery` for the 'Repeat - /// search here' behavior. If that behavior is not wanted, it should be left `nil`. - /// - Parameter newExtent: The new value. - /// - Returns: The `SearchView`. - public func geoViewExtent(_ newExtent: Envelope?) -> Self { - var copy = self - copy.geoViewExtent = newExtent - return copy - } - - /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. - /// - Parameter newIsGeoViewNavigating: The new value. - /// - Returns: The `SearchView`. - public func isGeoViewNavigating(_ newIsGeoViewNavigating: Bool) -> Self { - var copy = self - copy.isGeoViewNavigating = newIsGeoViewNavigating - return copy - } - /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. /// - Parameter newViewpoint: The new value. /// - Returns: The `SearchView`. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index fd4f05e97..bea25bf00 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -125,7 +125,7 @@ final class SearchViewModel: ObservableObject { /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. var isGeoViewNavigating: Bool = false - + /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. var viewpoint: Binding? = nil @@ -135,7 +135,6 @@ final class SearchViewModel: ObservableObject { /// If `true`, will set the viewpoint to the extent of the results, plus a little buffer, which will /// cause the geoView to zoom to the extent of the results. If `false`, /// no setting of the viewpoint will occur. - @Published private var shouldZoomToResults = true /// `true` if the extent has changed by a set amount after a `Search` or `AcceptSuggestion` @@ -178,7 +177,6 @@ final class SearchViewModel: ObservableObject { /// that result is automatically assigned to this property. If there are multiple results, the view sets /// this property upon user selection. This property is observable. The view should observe this /// property and update the associated GeoView's viewpoint, if configured. - @Published var selectedResult: SearchResult? { willSet { (selectedResult?.geoElement as? Graphic)?.isSelected = false @@ -226,7 +224,6 @@ final class SearchViewModel: ObservableObject { } /// The suggestion currently selected by the user. - @Published var currentSuggestion: SearchSuggestion? { didSet { if let currentSuggestion = currentSuggestion { From f0d6eb33bedb07ccb6aa538a9de32155b02de83c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 11 May 2022 10:14:23 -0700 Subject: [PATCH 0839/1722] Update README.md --- Documentation/Bookmarks/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/Bookmarks/README.md b/Documentation/Bookmarks/README.md index 28f8611bf..135b58283 100644 --- a/Documentation/Bookmarks/README.md +++ b/Documentation/Bookmarks/README.md @@ -7,6 +7,11 @@ The Bookmarks component will display a list of bookmarks and allows the user to ## Usage ```swift +Bookmarks( + isPresented: $showingBookmarks, + mapOrScene: map, + viewpoint: $viewpoint +) ``` To see it in action, try out the [Examples](../../Examples) and refer to [BookmarksExampleView.swift](../../Examples/Examples/BookmarksExampleView.swift) in the project. From 1fe68a03b6aa39dcf04f0d8a068e4acace9eaf7c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 11 May 2022 14:04:47 -0500 Subject: [PATCH 0840/1722] Clean up initializers and doc. --- Examples/Examples/SearchExampleView.swift | 11 +--- .../Components/Search/SearchView.swift | 55 ++++++++++++------- .../Components/Search/SearchViewModel.swift | 12 ++-- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index c454f1869..eb9349d76 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -49,13 +49,6 @@ struct SearchExampleView: View { .onNavigatingChanged { isGeoViewNavigating = $0 } .onViewpointChanged(kind: .centerAndScale) { queryCenter = $0.targetGeometry as? Point - - // Reset `searchResultViewpoint` here when the user pans/zooms - // the map, so if the user commits the same search with the - // same result, the Map will pan/zoom to the result. Otherwise, - // `searchResultViewpoint` doesn't change which doesn't - // redraw the map with the new viewpoint. - searchResultViewpoint = nil } .onVisibleAreaChanged { newValue in // For "Repeat Search Here" behavior, pass the `geoViewExtent` @@ -69,14 +62,14 @@ struct SearchExampleView: View { } .overlay(alignment: .topTrailing) { SearchView( -// queryArea: $queryArea, queryCenter: $queryCenter, sources: [locatorDataSource], + viewpoint: $searchResultViewpoint, geoViewExtent: $geoViewExtent, isGeoViewNavigating: $isGeoViewNavigating ) - .viewpoint($searchResultViewpoint) .resultsOverlay(searchResultsOverlay) +// .queryArea($queryArea) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index d2de36717..c969703e6 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -18,23 +18,25 @@ import ArcGIS public struct SearchView: View { /// Creates a `SearchView`. /// - Parameters: - /// - queryArea: The search area to be used for the current query. /// - queryCenter: Defines the center for the search. - /// - resultMode: Defines how many results to return. /// - 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. + /// - geoViewExtent: The current map/scene view extent. Defaults to `nil`. Used to allow + /// repeat searches after panning/zooming the map. Set to nil if repeat search behavior is not wanted. + /// - isGeoViewNavigating: Denotes whether the geoview is navigating. Used for the + /// repeat search behavior. public init( - queryArea: Binding? = nil, queryCenter: Binding? = nil, - resultMode: SearchResultMode = .automatic, sources: [SearchSource] = [], + viewpoint: Binding? = nil, geoViewExtent: Binding? = nil, isGeoViewNavigating: Binding? = nil ) { _viewModel = StateObject(wrappedValue: SearchViewModel( - queryArea: queryArea, queryCenter: queryCenter, - resultMode: resultMode, - sources: sources.isEmpty ? [LocatorSearchSource()] : sources + sources: sources.isEmpty ? [LocatorSearchSource()] : sources, + viewpoint: viewpoint )) _geoViewExtent = geoViewExtent ?? Binding.constant(nil) @@ -48,16 +50,19 @@ public struct SearchView: View { /// Tracks the current user-entered query. This property drives both suggestions and searches. var currentQuery: String = "" - + + /// Tracks the current user-entered query. This property drives both suggestions and searches. + var resultMode: SearchResultMode = .automatic + + /// The search area to be used for the current query. + var queryArea: Binding? = nil + /// The current map/scene view extent. Defaults to `nil`. /// /// This should be updated via `geoViewExtent(:)`as the user navigates the map/scene. It will be /// used to determine the value of `isEligibleForRequery` for the 'Repeat /// search here' behavior. If that behavior is not wanted, it should be left `nil`. @Binding var geoViewExtent: Envelope? - - /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. - var viewpoint: Binding? = nil /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. var resultsOverlay: GraphicsOverlay? = nil @@ -173,8 +178,9 @@ public struct SearchView: View { } .onAppear() { viewModel.currentQuery = currentQuery - viewModel.viewpoint = viewpoint viewModel.resultsOverlay = resultsOverlay + viewModel.resultMode = resultMode + viewModel.queryArea = queryArea } } } @@ -223,21 +229,30 @@ extension SearchView { return copy } - /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. - /// - Parameter newViewpoint: The new value. + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. + /// - Parameter newResultsOverlay: The new value. /// - Returns: The `SearchView`. - public func viewpoint(_ newViewpoint: Binding?) -> Self { + public func resultsOverlay(_ newResultsOverlay: GraphicsOverlay?) -> Self { var copy = self - copy.viewpoint = newViewpoint + copy.resultsOverlay = newResultsOverlay return copy } - /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. - /// - Parameter newResultsOverlay: The new value. + /// Defines how many results to return. + /// - Parameter newResultMode: The new value. /// - Returns: The `SearchView`. - public func resultsOverlay(_ newResultsOverlay: GraphicsOverlay?) -> Self { + public func resultMode(_ newResultMode: SearchResultMode) -> Self { var copy = self - copy.resultsOverlay = newResultsOverlay + copy.resultMode = newResultMode + return copy + } + + /// The search area to be used for the current query. + /// - Parameter newQueryArea: The new value. + /// - Returns: The `SearchView`. + public func queryArea(_ newQueryArea: Binding?) -> Self { + var copy = self + copy.queryArea = newQueryArea return copy } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index bea25bf00..98a4a5ee8 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -42,20 +42,18 @@ public enum SearchOutcome { final class SearchViewModel: ObservableObject { /// Creates a `SearchViewModel`. /// - Parameters: - /// - queryArea: The search area to be used for the current query. /// - queryCenter: Defines the center for the search. - /// - resultMode: Defines how many results to return. /// - 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. init( - queryArea: Binding? = nil, queryCenter: Binding? = nil, - resultMode: SearchResultMode = .automatic, - sources: [SearchSource] = [] + sources: [SearchSource] = [], + viewpoint: Binding? = nil ) { - self.queryArea = queryArea self.queryCenter = queryCenter - self.resultMode = resultMode self.sources = sources + self.viewpoint = viewpoint } /// The active search source. If `nil`, the first item in `sources` is used. From 8fdd1eae2662ad28953e05d69b4c0f0063c18772 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 12 May 2022 10:49:58 -0600 Subject: [PATCH 0841/1722] start work on authentication handlers --- .../Authentication/AuthenticationView.swift | 32 ++++ .../Authentication/Authenticator.swift | 60 +++++++ .../Authentication/UsernamePasswordView.swift | 168 ++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift create mode 100644 Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift create mode 100644 Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift new file mode 100644 index 000000000..6a7a85618 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -0,0 +1,32 @@ +// +// 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 SwiftUI + +struct AuthenticationView: View { + init(continuation: ChallengeContinuation) { + self.continuation = continuation + } + + var continuation: ChallengeContinuation + + var body: some View { + EmptyView() +// UsernamePasswordView(viewModel: UsernamePasswordViewModel(continuation)) + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift new file mode 100644 index 000000000..5470ebc77 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -0,0 +1,60 @@ +// +// 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 ArcGIS +import SwiftUI + +final class ChallengeContinuation { + let challenge: ArcGISAuthenticationChallenge + let continuation: UnsafeContinuation + + init(challenge: ArcGISAuthenticationChallenge, continuation: UnsafeContinuation) { + self.challenge = challenge + self.continuation = continuation + } + + func resume(with result: Result) { + continuation.resume(with: result) + } + + func cancel() { + continuation.resume(throwing: CancellationError()) + } +} + +@MainActor +final class Authenticator: ObservableObject { + init() {} + + @Published + var continuation: ChallengeContinuation? +} + +extension Authenticator: AuthenticationChallengeHandler { + func handleArcGISChallenge( + _ challenge: ArcGISAuthenticationChallenge + ) async throws -> ArcGISAuthenticationChallenge.Disposition { + guard challenge.proposedCredential == nil else { + return .performDefaultHandling + } + + return try await withUnsafeThrowingContinuation { continuation in + self.continuation = ChallengeContinuation(challenge: challenge, continuation: continuation) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift new file mode 100644 index 000000000..6e5626c0a --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -0,0 +1,168 @@ +// +// 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 SwiftUI +import ArcGIS + +@MainActor +class UsernamePasswordViewModel: ObservableObject { + init(challengingHost: String) { + self.challengingHost = challengingHost + continuation = nil + } + + private let continuation: ChallengeContinuation? + + init(continuation: ChallengeContinuation) { + self.continuation = continuation + self.challengingHost = continuation.challenge.request.url!.host! + } + + @Published var username = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var password = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var signinButtonEnabled = false + + private func updateSigninButtonEnabled() { + signinButtonEnabled = !username.isEmpty && !password.isEmpty + } + + let challengingHost: String + + func signIn() { + if let continuation = continuation { + Task { + let disposition: Task = Task { + .useCredential( + try await .token( + challenge: continuation.challenge, + username: username, + password: password + ) + ) + } + continuation.resume(with: await disposition.result) + } + } + } + + func cancel() { + if let continuation = continuation { + continuation.cancel() + } + } +} + +struct UsernamePasswordView: View { + init(viewModel: UsernamePasswordViewModel) { + self.viewModel = viewModel + } + + var viewModel: UsernamePasswordViewModel + + /// The focused field. + @FocusState private var focusedField: Field? + + /// The username to be entered by the user. + @State private var username = "" + + /// The password to be entered by the user. + @State private var password = "" + + var body: some View { + NavigationView { + Form { + Section { + VStack { + Image(systemName: "person.circle") + .resizable() + .frame(width: 150, height: 150) + .shadow( + color: .gray.opacity(0.4), + radius: 3, + x: 1, + y: 2 + ) + Text("You need to sign in to access '\(viewModel.challengingHost)'") + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + } + + Section { + TextField("Username", text: $username) + .focused($focusedField, equals: .username) + .textContentType(.username) + .submitLabel(.next) + .onSubmit { focusedField = .password } + SecureField("Password", text: $password) + .focused($focusedField, equals: .password) + .textContentType(.password) + .submitLabel(.go) + .onSubmit { viewModel.signIn() } + } + .autocapitalization(.none) + .disableAutocorrection(true) + + Section { + Button(action: { + viewModel.signIn() + }, label: { + Text("Sign In") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundColor(.white) + }) + .disabled(!viewModel.signinButtonEnabled) + .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) + } + } + .navigationTitle("Sign In") + .navigationBarTitleDisplayMode(.inline) + .interactiveDismissDisabled() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { viewModel.cancel() } + } + } + .onAppear { + // Workaround for Apple bug - FB9676178. + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + focusedField = .username + } + } + } + } +} + +struct UsernamePasswordView_Previews: PreviewProvider { + static var previews: some View { + UsernamePasswordView(viewModel: UsernamePasswordViewModel(challengingHost: "arcgis.com")) + } +} + +private extension UsernamePasswordView { + /// The field to set the focus. + enum Field: Hashable { + case username + case password + } +} From 4a775b0f7e60bb90cd313bf1fc58bca5303c0fd1 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 12 May 2022 13:03:04 -0600 Subject: [PATCH 0842/1722] update result --- .../Authentication/UsernamePasswordView.swift | 60 ++++++++++--------- Sources/ArcGISToolkit/Extensions/Result.swift | 6 +- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 6e5626c0a..2d19bd8d1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -19,8 +19,7 @@ import SwiftUI import ArcGIS -@MainActor -class UsernamePasswordViewModel: ObservableObject { +@MainActor class UsernamePasswordViewModel: ObservableObject { init(challengingHost: String) { self.challengingHost = challengingHost continuation = nil @@ -50,7 +49,7 @@ class UsernamePasswordViewModel: ObservableObject { func signIn() { if let continuation = continuation { Task { - let disposition: Task = Task { + continuation.resume(with: await Result { .useCredential( try await .token( challenge: continuation.challenge, @@ -58,8 +57,7 @@ class UsernamePasswordViewModel: ObservableObject { password: password ) ) - } - continuation.resume(with: await disposition.result) + }) } } } @@ -71,12 +69,12 @@ class UsernamePasswordViewModel: ObservableObject { } } -struct UsernamePasswordView: View { +@MainActor struct UsernamePasswordView: View { init(viewModel: UsernamePasswordViewModel) { self.viewModel = viewModel } - var viewModel: UsernamePasswordViewModel + private var viewModel: UsernamePasswordViewModel /// The focused field. @FocusState private var focusedField: Field? @@ -86,21 +84,13 @@ struct UsernamePasswordView: View { /// The password to be entered by the user. @State private var password = "" - + var body: some View { NavigationView { Form { Section { VStack { - Image(systemName: "person.circle") - .resizable() - .frame(width: 150, height: 150) - .shadow( - color: .gray.opacity(0.4), - radius: 3, - x: 1, - y: 2 - ) + person Text("You need to sign in to access '\(viewModel.challengingHost)'") .fixedSize(horizontal: false, vertical: true) } @@ -124,15 +114,7 @@ struct UsernamePasswordView: View { .disableAutocorrection(true) Section { - Button(action: { - viewModel.signIn() - }, label: { - Text("Sign In") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundColor(.white) - }) - .disabled(!viewModel.signinButtonEnabled) - .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) + signinButton } } .navigationTitle("Sign In") @@ -151,6 +133,30 @@ struct UsernamePasswordView: View { } } } + + private var person: some View { + Image(systemName: "person.circle") + .resizable() + .frame(width: 150, height: 150) + .shadow( + color: .gray.opacity(0.4), + radius: 3, + x: 1, + y: 2 + ) + } + + private var signinButton: some View { + Button(action: { + viewModel.signIn() + }, label: { + Text("Sign In") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundColor(.white) + }) + .disabled(!viewModel.signinButtonEnabled) + .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) + } } struct UsernamePasswordView_Previews: PreviewProvider { @@ -160,7 +166,7 @@ struct UsernamePasswordView_Previews: PreviewProvider { } private extension UsernamePasswordView { - /// The field to set the focus. + /// A type that represents the fields in the user name and password sign-in form. enum Field: Hashable { case username case password diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index d4e837b1e..118ea4631 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -15,13 +15,9 @@ extension Result where Failure == Error { /// Creates a result based on the outcome of the given task. If the task /// succeeds, the result is `success`. If the task fails, the result is /// `failure`. - /// - /// Returns `nil` in the event that the task was cancelled. - init?(awaiting task: () async throws -> Success) async { + init(awaiting task: () async throws -> Success) async { do { self = .success(try await task()) - } catch is CancellationError { - return nil } catch { self = .failure(error) } From 65af0c5ff5cd1e00b74bd61f2c43dd3d9274520d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 12 May 2022 13:36:09 -0600 Subject: [PATCH 0843/1722] add auth example --- Examples/Examples.xcodeproj/project.pbxproj | 8 +- .../Examples/AuthenticationExampleView.swift | 83 +++++++++++++++++++ Examples/ExamplesApp/Examples.swift | 10 ++- .../Authentication/AuthenticationView.swift | 38 ++++----- .../Authentication/Authenticator.swift | 42 +++++----- .../Authentication/UsernamePasswordView.swift | 29 +++---- 6 files changed, 146 insertions(+), 64 deletions(-) create mode 100644 Examples/Examples/AuthenticationExampleView.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 002f63bd9..85cb445c2 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,9 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.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 */; }; + 883CA916282D951D00D9842C /* AuthenticationExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 883CA915282D951D00D9842C /* AuthenticationExampleView.swift */; }; E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; @@ -40,9 +41,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksExampleView.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 = ""; }; + 883CA915282D951D00D9842C /* AuthenticationExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationExampleView.swift; sourceTree = ""; }; E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloorFilterExampleView.swift; sourceTree = ""; }; E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -85,6 +87,7 @@ E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, 75657E4727ABAC8400EE865B /* CompassExampleView.swift */, E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */, + 883CA915282D951D00D9842C /* AuthenticationExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -224,6 +227,7 @@ 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */, E48A73452658227100F5C118 /* Examples.swift in Sources */, 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */, + 883CA916282D951D00D9842C /* AuthenticationExampleView.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift new file mode 100644 index 000000000..2ed09afee --- /dev/null +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -0,0 +1,83 @@ +// 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 + +private extension URL { + static let worldImageryMapServer = URL(string: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer")! +} + +struct AuthenticationExampleView: View { + @State private var mapLoadResult: Result? + //@StateObject private var authenticator = Authenticator() + + static func makeMap() -> Map { + let basemap = Basemap(baseLayer: ArcGISTiledLayer(url: .worldImageryMapServer)) + return Map(basemap: basemap) + } + + var body: some View { + Group { + if let mapLoadResult = mapLoadResult { + switch mapLoadResult { + case .success(let value): + MapView(map: value) + case .failure(let error): + Text("Error loading map: \(errorString(for: error))") + .padding() + } + } else { + ProgressView() + } + } + .task { + do { + let map = Self.makeMap() + try await map.load() + mapLoadResult = .success(map) + } catch { + mapLoadResult = .failure(error) + } + } +// .sheet(isPresented: authenticator) { +// SignInSheet(model: signInModel) +// } +// .onAppear { +// ArcGISURLSession.challengeHandler = signInModel +// } +// .onDisappear { +// ArcGISURLSession.challengeHandler = nil +// } + } + + private func errorString(for error: Error) -> String { + switch error { + case let authenticationError as ArcGISAuthenticationChallenge.Error: + switch authenticationError { + case .userCancelled(_): + return "User cancelled error" + case .credentialCannotBeShared: + return "Provided credential cannot be shared" + } + default: + return error.localizedDescription + } + } +} + +struct AuthenticationExampleView_Previews: PreviewProvider { + static var previews: some View { + AuthenticationExampleView() + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index e8d654441..905ba2086 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -17,7 +17,8 @@ struct Examples: View { /// The list of example lists. Allows for a hierarchical navigation model for examples. let lists: [ExampleList] = [ .geoview, - .views + .views, + .authentication ] var body: some View { @@ -51,4 +52,11 @@ extension ExampleList { AnyExample("Floating Panel", content: FloatingPanelExampleView()) ] ) + + static let authentication = Self( + name: "Authentication", + examples: [ + AnyExample("Token", content: AuthenticationExampleView()) + ] + ) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 6a7a85618..0ea39da78 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -1,32 +1,26 @@ -// -// 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 -// +// 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 -struct AuthenticationView: View { - init(continuation: ChallengeContinuation) { +public struct AuthenticationView: View { + public init(continuation: ChallengeContinuation) { self.continuation = continuation } var continuation: ChallengeContinuation - var body: some View { - EmptyView() -// UsernamePasswordView(viewModel: UsernamePasswordViewModel(continuation)) + public var body: some View { + UsernamePasswordView(viewModel: UsernamePasswordViewModel(continuation: continuation)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 5470ebc77..2f41ffe71 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -1,25 +1,20 @@ -// -// 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 -// +// 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 import SwiftUI -final class ChallengeContinuation { +public final class ChallengeContinuation { let challenge: ArcGISAuthenticationChallenge let continuation: UnsafeContinuation @@ -37,16 +32,18 @@ final class ChallengeContinuation { } } +extension ChallengeContinuation: Identifiable {} + @MainActor -final class Authenticator: ObservableObject { - init() {} +public final class Authenticator: ObservableObject { + public init() {} @Published - var continuation: ChallengeContinuation? + public var continuation: ChallengeContinuation? } extension Authenticator: AuthenticationChallengeHandler { - func handleArcGISChallenge( + public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { guard challenge.proposedCredential == nil else { @@ -54,6 +51,7 @@ extension Authenticator: AuthenticationChallengeHandler { } return try await withUnsafeThrowingContinuation { continuation in + print("-- auth challenge: \(challenge.request.url!)") self.continuation = ChallengeContinuation(challenge: challenge, continuation: continuation) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 2d19bd8d1..493a26b92 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -1,20 +1,15 @@ -// -// 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 -// +// 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 From e4d5422c342fac3a8754a846d6de04b7bc13b29c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 12 May 2022 13:33:12 -0700 Subject: [PATCH 0844/1722] Update Color.swift --- Sources/ArcGISToolkit/Extensions/Color.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/Color.swift b/Sources/ArcGISToolkit/Extensions/Color.swift index f85f61306..ca4ad698b 100644 --- a/Sources/ArcGISToolkit/Extensions/Color.swift +++ b/Sources/ArcGISToolkit/Extensions/Color.swift @@ -15,7 +15,7 @@ import SwiftUI extension Color { /// Initializes a new color with RGB integer values. - /// - Precondition: `red`, `blue` and `green` are values between 0 and 255 inclusive. + /// - Precondition: `red`, `green` and `blue` are values between 0 and 255 inclusive. init(red: Int, green: Int, blue: Int) { let validRange = 0...255 precondition(validRange.contains(red)) From 2b33328924b32b59699ebd59cc1f68db79b27118 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 12 May 2022 15:12:03 -0600 Subject: [PATCH 0845/1722] focus authentication example --- .../Examples/AuthenticationExampleView.swift | 132 +++++++++++------- Examples/ExamplesApp/Examples.swift | 2 +- 2 files changed, 83 insertions(+), 51 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 2ed09afee..328b01568 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -13,71 +13,103 @@ import SwiftUI import ArcGIS - -private extension URL { - static let worldImageryMapServer = URL(string: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer")! -} +import ArcGISToolkit struct AuthenticationExampleView: View { - @State private var mapLoadResult: Result? - //@StateObject private var authenticator = Authenticator() - - static func makeMap() -> Map { - let basemap = Basemap(baseLayer: ArcGISTiledLayer(url: .worldImageryMapServer)) - return Map(basemap: basemap) - } + @StateObject var authenticator = Authenticator() + @State var previousApiKey: APIKey? var body: some View { - Group { - if let mapLoadResult = mapLoadResult { - switch mapLoadResult { - case .success(let value): - MapView(map: value) - case .failure(let error): - Text("Error loading map: \(errorString(for: error))") - .padding() - } - } else { - ProgressView() - } + List(AuthenticationItem.all, id: \.title) { item in + AuthenticationItemView(item: item) } - .task { - do { - let map = Self.makeMap() - try await map.load() - mapLoadResult = .success(map) - } catch { - mapLoadResult = .failure(error) - } + .navigationBarTitle(Text("Authentication"), displayMode: .inline) + .sheet(item: $authenticator.continuation) { + AuthenticationView(continuation: $0) + }.onAppear { + ArcGISURLSession.challengeHandler = authenticator + } + .onAppear { + // Save off the api key + previousApiKey = ArcGISRuntimeEnvironment.apiKey + // Set the api key to nil so that the authenticated services will prompt. + ArcGISRuntimeEnvironment.apiKey = nil + } + .onDisappear { + // Restore api key when exiting this example. + ArcGISRuntimeEnvironment.apiKey = previousApiKey } -// .sheet(isPresented: authenticator) { -// SignInSheet(model: signInModel) -// } -// .onAppear { -// ArcGISURLSession.challengeHandler = signInModel -// } -// .onDisappear { -// ArcGISURLSession.challengeHandler = nil -// } } private func errorString(for error: Error) -> String { switch error { - case let authenticationError as ArcGISAuthenticationChallenge.Error: - switch authenticationError { - case .userCancelled(_): - return "User cancelled error" - case .credentialCannotBeShared: - return "Provided credential cannot be shared" - } + case is ArcGISAuthenticationChallenge.Error: + return "Authentication error" default: return error.localizedDescription } } } -struct AuthenticationExampleView_Previews: PreviewProvider { - static var previews: some View { - AuthenticationExampleView() +private struct AuthenticationItemView: View { + let loadable: Loadable + let title: String + @State var status = LoadStatus.notLoaded + + init(item: AuthenticationItem) { + self.loadable = item.loadable + self.title = item.title + } + + var body: some View { + Button { + Task { + status = .loading + try? await loadable.load() + status = loadable.loadStatus + } + } label: { + buttonContent + } + } + + var buttonContent: some View { + HStack { + Text(title) + Spacer() + switch status { + case .loading: + ProgressView() + case .loaded: + Text("Loaded") + .foregroundColor(.green) + case .notLoaded: + Text("Tap to load") + .foregroundColor(.secondary) + case .failed: + Text("Failed to laod") + .foregroundColor(.red) + } + } } } + +private extension URL { + static let worldImageryMapServer = URL(string: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer")! +} + +private struct AuthenticationItem { + let title: String + let loadable: Loadable +} + +extension AuthenticationItem { + static let token = AuthenticationItem( + title: "Token secured resource", + loadable: ArcGISTiledLayer(url: .worldImageryMapServer) + ) + + static let all: [AuthenticationItem] = [ + .token + ] +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 905ba2086..e3ed35a52 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -56,7 +56,7 @@ extension ExampleList { static let authentication = Self( name: "Authentication", examples: [ - AnyExample("Token", content: AuthenticationExampleView()) + AnyExample("All Authentication Types", content: AuthenticationExampleView()) ] ) } From 81bcf289487a58c779c209292ac4270a24800a2f Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 12 May 2022 15:17:34 -0600 Subject: [PATCH 0846/1722] add comment --- Examples/Examples/AuthenticationExampleView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 328b01568..5fd624b3a 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -29,6 +29,12 @@ struct AuthenticationExampleView: View { }.onAppear { ArcGISURLSession.challengeHandler = authenticator } + // Save and restore the API Key. + // Note: This is only necessary in this example. Other examples make use of the global + // api key that is set when the app starts up. Using an api key will prevent an + // authentication challenge prompt for certain services. Since this example highlights + // the usage of authentication challenge prompts, we want to set the api key to `nil` + // when this example appears and restore it when this example disappears. .onAppear { // Save off the api key previousApiKey = ArcGISRuntimeEnvironment.apiKey From b66817d65c5d63197efc0ca04d916e5a6806039c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 12 May 2022 15:30:37 -0600 Subject: [PATCH 0847/1722] updates --- .../Authentication/Authenticator.swift | 13 +++++++------ .../Authentication/UsernamePasswordView.swift | 17 +++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 2f41ffe71..e3d9b93d4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -16,19 +16,21 @@ import SwiftUI public final class ChallengeContinuation { let challenge: ArcGISAuthenticationChallenge - let continuation: UnsafeContinuation + var continuation: CheckedContinuation? - init(challenge: ArcGISAuthenticationChallenge, continuation: UnsafeContinuation) { + init(challenge: ArcGISAuthenticationChallenge, continuation: CheckedContinuation) { self.challenge = challenge self.continuation = continuation } func resume(with result: Result) { - continuation.resume(with: result) + continuation?.resume(with: result) + continuation = nil } func cancel() { - continuation.resume(throwing: CancellationError()) + continuation?.resume(throwing: CancellationError()) + continuation = nil } } @@ -50,8 +52,7 @@ extension Authenticator: AuthenticationChallengeHandler { return .performDefaultHandling } - return try await withUnsafeThrowingContinuation { continuation in - print("-- auth challenge: \(challenge.request.url!)") + return try await withCheckedThrowingContinuation { continuation in self.continuation = ChallengeContinuation(challenge: challenge, continuation: continuation) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 493a26b92..13e4dddc7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -34,6 +34,7 @@ import ArcGIS didSet { updateSigninButtonEnabled() } } @Published var signinButtonEnabled = false + @Published var isDismissed = false private func updateSigninButtonEnabled() { signinButtonEnabled = !username.isEmpty && !password.isEmpty @@ -42,6 +43,7 @@ import ArcGIS let challengingHost: String func signIn() { + isDismissed = true if let continuation = continuation { Task { continuation.resume(with: await Result { @@ -58,6 +60,7 @@ import ArcGIS } func cancel() { + isDismissed = true if let continuation = continuation { continuation.cancel() } @@ -66,19 +69,13 @@ import ArcGIS @MainActor struct UsernamePasswordView: View { init(viewModel: UsernamePasswordViewModel) { - self.viewModel = viewModel + _viewModel = StateObject(wrappedValue: viewModel) } - private var viewModel: UsernamePasswordViewModel + @StateObject private var viewModel: UsernamePasswordViewModel /// The focused field. @FocusState private var focusedField: Field? - - /// The username to be entered by the user. - @State private var username = "" - - /// The password to be entered by the user. - @State private var password = "" var body: some View { NavigationView { @@ -94,12 +91,12 @@ import ArcGIS } Section { - TextField("Username", text: $username) + TextField("Username", text: $viewModel.username) .focused($focusedField, equals: .username) .textContentType(.username) .submitLabel(.next) .onSubmit { focusedField = .password } - SecureField("Password", text: $password) + SecureField("Password", text: $viewModel.password) .focused($focusedField, equals: .password) .textContentType(.password) .submitLabel(.go) From 63c4522cbad66ffae32ea973fbe526e65007556a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 10:03:45 -0600 Subject: [PATCH 0848/1722] challenge queue --- .../Examples/AuthenticationExampleView.swift | 4 +- .../Authentication/AuthenticationView.swift | 8 +-- .../Authentication/Authenticator.swift | 51 ++++++++++++++++--- .../Authentication/UsernamePasswordView.swift | 20 ++++---- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 5fd624b3a..4eb0ae794 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -24,8 +24,8 @@ struct AuthenticationExampleView: View { AuthenticationItemView(item: item) } .navigationBarTitle(Text("Authentication"), displayMode: .inline) - .sheet(item: $authenticator.continuation) { - AuthenticationView(continuation: $0) + .sheet(item: $authenticator.currentFoo) { + AuthenticationView(foo: $0) }.onAppear { ArcGISURLSession.challengeHandler = authenticator } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 0ea39da78..0bc0393f0 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -14,13 +14,13 @@ import SwiftUI public struct AuthenticationView: View { - public init(continuation: ChallengeContinuation) { - self.continuation = continuation + public init(foo: Foo) { + self.foo = foo } - var continuation: ChallengeContinuation + var foo: Foo public var body: some View { - UsernamePasswordView(viewModel: UsernamePasswordViewModel(continuation: continuation)) + UsernamePasswordView(viewModel: UsernamePasswordViewModel(foo: foo)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index e3d9b93d4..4d1840e25 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -13,14 +13,14 @@ import ArcGIS import SwiftUI +import Combine -public final class ChallengeContinuation { +public final class Foo { let challenge: ArcGISAuthenticationChallenge var continuation: CheckedContinuation? - init(challenge: ArcGISAuthenticationChallenge, continuation: CheckedContinuation) { + init(challenge: ArcGISAuthenticationChallenge) { self.challenge = challenge - self.continuation = continuation } func resume(with result: Result) { @@ -32,16 +32,53 @@ public final class ChallengeContinuation { continuation?.resume(throwing: CancellationError()) continuation = nil } + + func waitForChallengeToBeHandled() async throws -> ArcGISAuthenticationChallenge.Disposition { + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + } + } } -extension ChallengeContinuation: Identifiable {} +extension Foo: Identifiable {} + +private struct QueuedChallenge { + let challenge: ArcGISAuthenticationChallenge + var continuation: CheckedContinuation + + init(challenge: ArcGISAuthenticationChallenge, continuation: CheckedContinuation) { + self.challenge = challenge + self.continuation = continuation + } +} @MainActor public final class Authenticator: ObservableObject { - public init() {} + public init() { + Task { await observeChallengeQueue() } + } + + private func observeChallengeQueue() async { + for await challengeContinuation in challengeQueue { + let foo = Foo(challenge: challengeContinuation.challenge) + currentFoo = foo + do { + let disposition = try await foo.waitForChallengeToBeHandled() + challengeContinuation.continuation.resume(returning: disposition) + } catch { + challengeContinuation.continuation.resume(throwing: error) + } + currentFoo = nil + } + } @Published - public var continuation: ChallengeContinuation? + public var currentFoo: Foo? + + private var subject = PassthroughSubject() + private var challengeQueue: AsyncPublisher> { + AsyncPublisher(subject) + } } extension Authenticator: AuthenticationChallengeHandler { @@ -53,7 +90,7 @@ extension Authenticator: AuthenticationChallengeHandler { } return try await withCheckedThrowingContinuation { continuation in - self.continuation = ChallengeContinuation(challenge: challenge, continuation: continuation) + subject.send(QueuedChallenge(challenge: challenge, continuation: continuation)) } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 13e4dddc7..228b62583 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -17,14 +17,14 @@ import ArcGIS @MainActor class UsernamePasswordViewModel: ObservableObject { init(challengingHost: String) { self.challengingHost = challengingHost - continuation = nil + foo = nil } - private let continuation: ChallengeContinuation? + private let foo: Foo? - init(continuation: ChallengeContinuation) { - self.continuation = continuation - self.challengingHost = continuation.challenge.request.url!.host! + init(foo: Foo) { + self.foo = foo + self.challengingHost = foo.challenge.request.url!.host! } @Published var username = "" { @@ -44,12 +44,12 @@ import ArcGIS func signIn() { isDismissed = true - if let continuation = continuation { + if let foo = foo { Task { - continuation.resume(with: await Result { + foo.resume(with: await Result { .useCredential( try await .token( - challenge: continuation.challenge, + challenge: foo.challenge, username: username, password: password ) @@ -61,8 +61,8 @@ import ArcGIS func cancel() { isDismissed = true - if let continuation = continuation { - continuation.cancel() + if let foo = foo { + foo.cancel() } } } From 1c93edc731b224268d5a62c9691b66234dbf34d3 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 10:05:43 -0600 Subject: [PATCH 0849/1722] use result --- .../Components/Authentication/Authenticator.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 4d1840e25..5ba996890 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -59,15 +59,12 @@ public final class Authenticator: ObservableObject { } private func observeChallengeQueue() async { - for await challengeContinuation in challengeQueue { - let foo = Foo(challenge: challengeContinuation.challenge) + for await queuedChallenge in challengeQueue { + let foo = Foo(challenge: queuedChallenge.challenge) currentFoo = foo - do { - let disposition = try await foo.waitForChallengeToBeHandled() - challengeContinuation.continuation.resume(returning: disposition) - } catch { - challengeContinuation.continuation.resume(throwing: error) - } + queuedChallenge.continuation.resume( + with: await Result { try await foo.waitForChallengeToBeHandled() } + ) currentFoo = nil } } From 990b60ce46d3ffc20eea9cedc7e0d12f481587e6 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 11:30:51 -0600 Subject: [PATCH 0850/1722] buffer queue --- .../Examples/AuthenticationExampleView.swift | 42 +++++++++++++++---- Examples/ExamplesApp/Info.plist | 5 +++ .../Authentication/Authenticator.swift | 39 ++++++++++++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 4eb0ae794..507950493 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -58,12 +58,12 @@ struct AuthenticationExampleView: View { } private struct AuthenticationItemView: View { - let loadable: Loadable + let loadables: [Loadable] let title: String @State var status = LoadStatus.notLoaded init(item: AuthenticationItem) { - self.loadable = item.loadable + self.loadables = item.loadables self.title = item.title } @@ -71,8 +71,19 @@ private struct AuthenticationItemView: View { Button { Task { status = .loading - try? await loadable.load() - status = loadable.loadStatus + do { + try await withThrowingTaskGroup(of: Void.self) { group in + for loadable in loadables { + group.addTask { + try await loadable.load() + } + } + try await group.waitForAll() + } + status = .loaded + } catch { + status = .failed + } } } label: { buttonContent @@ -102,20 +113,37 @@ private struct AuthenticationItemView: View { private extension URL { static let worldImageryMapServer = URL(string: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer")! + static let hostedPointsLayer = URL(string: "https://rt-server107a.esri.com/server/rest/services/Hosted/PointsLayer/FeatureServer/0")! } private struct AuthenticationItem { let title: String - let loadable: Loadable + let loadables: [Loadable] } extension AuthenticationItem { static let token = AuthenticationItem( title: "Token secured resource", - loadable: ArcGISTiledLayer(url: .worldImageryMapServer) + loadables: [ArcGISTiledLayer(url: .worldImageryMapServer)] + ) + static let multipleToken = AuthenticationItem( + title: "Multiple token secured resources", + loadables: [ + ArcGISTiledLayer(url: .worldImageryMapServer), + ServiceFeatureTable(url: .hostedPointsLayer) + ] + ) + static let multipleTokenSame = AuthenticationItem( + title: "Two of same token secured resources", + loadables: [ + ArcGISTiledLayer(url: .worldImageryMapServer), + ArcGISTiledLayer(url: .worldImageryMapServer) + ] ) static let all: [AuthenticationItem] = [ - .token + .token, + .multipleToken, + .multipleTokenSame ] } diff --git a/Examples/ExamplesApp/Info.plist b/Examples/ExamplesApp/Info.plist index efc211a0c..61d79be64 100644 --- a/Examples/ExamplesApp/Info.plist +++ b/Examples/ExamplesApp/Info.plist @@ -46,5 +46,10 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 5ba996890..eb8d900c9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -40,9 +40,13 @@ public final class Foo { } } -extension Foo: Identifiable {} +extension Foo: Identifiable { + public var id: ObjectIdentifier { + ObjectIdentifier(self) + } +} -private struct QueuedChallenge { +private class QueuedChallenge { let challenge: ArcGISAuthenticationChallenge var continuation: CheckedContinuation @@ -60,12 +64,14 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { + print(" -- handing challenge") let foo = Foo(challenge: queuedChallenge.challenge) currentFoo = foo + let result = await Result { try await foo.waitForChallengeToBeHandled() } + currentFoo = nil queuedChallenge.continuation.resume( - with: await Result { try await foo.waitForChallengeToBeHandled() } + with: result ) - currentFoo = nil } } @@ -73,8 +79,12 @@ public final class Authenticator: ObservableObject { public var currentFoo: Foo? private var subject = PassthroughSubject() - private var challengeQueue: AsyncPublisher> { - AsyncPublisher(subject) + private var challengeQueue: AsyncPublisher> { + AsyncPublisher( + subject + .buffer(size: .max, prefetch: .byRequest, whenFull: .dropOldest) + .eraseToAnyPublisher() + ) } } @@ -82,7 +92,11 @@ extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { + print("-- high level challenge receieved") + await Task.yield() + guard challenge.proposedCredential == nil else { + print(" -- performing default handling") return .performDefaultHandling } @@ -90,4 +104,17 @@ extension Authenticator: AuthenticationChallengeHandler { subject.send(QueuedChallenge(challenge: challenge, continuation: continuation)) } } + + public func handleURLSessionChallenge( + _ challenge: URLAuthenticationChallenge, + scope: URLAuthenticationChallengeScope + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust { + // This will cause a self-signed certificate to be trusted. + return (.useCredential, URLCredential(trust: trust)) + } else { + return (.performDefaultHandling, nil) + } + } } From 425468667dc23edf5a19fe6536f78d431a2bab94 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 12:27:32 -0600 Subject: [PATCH 0851/1722] add ability to clear credential store --- .../Examples/AuthenticationExampleView.swift | 83 +++++++++++++------ .../Authentication/AuthenticationView.swift | 1 + .../Authentication/Authenticator.swift | 6 +- .../Authentication/UsernamePasswordView.swift | 7 +- 4 files changed, 65 insertions(+), 32 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 507950493..1c80d89fa 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -16,12 +16,26 @@ import ArcGIS import ArcGISToolkit struct AuthenticationExampleView: View { - @StateObject var authenticator = Authenticator() + @ObservedObject var authenticator = Authenticator() @State var previousApiKey: APIKey? + @State private var items = AuthenticationItem.makeAll() var body: some View { - List(AuthenticationItem.all, id: \.title) { item in - AuthenticationItemView(item: item) + VStack { + if items.isEmpty { + ProgressView() + } else { + List(items) { item in + AuthenticationItemView(item: item) + } + } + Button("Clear Credentials") { + items = [] + Task { + await ArcGISURLSession.credentialStore.removeAll() + items = AuthenticationItem.makeAll() + } + } } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .sheet(item: $authenticator.currentFoo) { @@ -116,34 +130,49 @@ private extension URL { static let hostedPointsLayer = URL(string: "https://rt-server107a.esri.com/server/rest/services/Hosted/PointsLayer/FeatureServer/0")! } -private struct AuthenticationItem { +private class AuthenticationItem { let title: String let loadables: [Loadable] + + init(title: String, loadables: [Loadable]) { + self.title = title + self.loadables = loadables + } } +extension AuthenticationItem: Identifiable {} + extension AuthenticationItem { - static let token = AuthenticationItem( - title: "Token secured resource", - loadables: [ArcGISTiledLayer(url: .worldImageryMapServer)] - ) - static let multipleToken = AuthenticationItem( - title: "Multiple token secured resources", - loadables: [ - ArcGISTiledLayer(url: .worldImageryMapServer), - ServiceFeatureTable(url: .hostedPointsLayer) - ] - ) - static let multipleTokenSame = AuthenticationItem( - title: "Two of same token secured resources", - loadables: [ - ArcGISTiledLayer(url: .worldImageryMapServer), - ArcGISTiledLayer(url: .worldImageryMapServer) - ] - ) + static func makeToken() -> AuthenticationItem { + AuthenticationItem( + title: "Token secured resource", + loadables: [ArcGISTiledLayer(url: .worldImageryMapServer)] + ) + } + static func makeMultipleToken() -> AuthenticationItem { + AuthenticationItem( + title: "Multiple token secured resources", + loadables: [ + ArcGISTiledLayer(url: .worldImageryMapServer), + ServiceFeatureTable(url: .hostedPointsLayer) + ] + ) + } + static func makeMultipleTokenSame() -> AuthenticationItem { + AuthenticationItem( + title: "Two of same token secured resources", + loadables: [ + ArcGISTiledLayer(url: .worldImageryMapServer), + ArcGISTiledLayer(url: .worldImageryMapServer) + ] + ) + } - static let all: [AuthenticationItem] = [ - .token, - .multipleToken, - .multipleTokenSame - ] + static func makeAll() -> [AuthenticationItem] { + [ + .makeToken(), + .makeMultipleToken(), + .makeMultipleTokenSame() + ] + } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 0bc0393f0..8463297b0 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -16,6 +16,7 @@ import SwiftUI public struct AuthenticationView: View { public init(foo: Foo) { self.foo = foo + print(" -- initing auth view: \(foo.challenge.request.url!)") } var foo: Foo diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index eb8d900c9..3770688e6 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -20,6 +20,7 @@ public final class Foo { var continuation: CheckedContinuation? init(challenge: ArcGISAuthenticationChallenge) { + print(" -- initing foo: \(challenge.request.url!)") self.challenge = challenge } @@ -67,11 +68,10 @@ public final class Authenticator: ObservableObject { print(" -- handing challenge") let foo = Foo(challenge: queuedChallenge.challenge) currentFoo = foo - let result = await Result { try await foo.waitForChallengeToBeHandled() } - currentFoo = nil queuedChallenge.continuation.resume( - with: result + with: await Result { try await foo.waitForChallengeToBeHandled() } ) + currentFoo = nil } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 228b62583..b0657b3c2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -25,6 +25,7 @@ import ArcGIS init(foo: Foo) { self.foo = foo self.challengingHost = foo.challenge.request.url!.host! + print(" -- initing view model: \(challengingHost)") } @Published var username = "" { @@ -69,10 +70,12 @@ import ArcGIS @MainActor struct UsernamePasswordView: View { init(viewModel: UsernamePasswordViewModel) { - _viewModel = StateObject(wrappedValue: viewModel) + //_viewModel = StateObject(wrappedValue: viewModel) + self.viewModel = viewModel + print(" -- initing username/pw view: \(viewModel.challengingHost)") } - @StateObject private var viewModel: UsernamePasswordViewModel + @ObservedObject private var viewModel: UsernamePasswordViewModel /// The focused field. @FocusState private var focusedField: Field? From 4d439cc6f68910190b3abff1ff42192fd7c7b7c5 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 12:27:54 -0600 Subject: [PATCH 0852/1722] label --- Examples/Examples/AuthenticationExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 1c80d89fa..b69e7dcbf 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -29,7 +29,7 @@ struct AuthenticationExampleView: View { AuthenticationItemView(item: item) } } - Button("Clear Credentials") { + Button("Clear Credential Store") { items = [] Task { await ArcGISURLSession.credentialStore.removeAll() From 99c0f429763982355a459dc0832685b316105e2a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 12:29:35 -0600 Subject: [PATCH 0853/1722] remove prints --- .../Components/Authentication/AuthenticationView.swift | 1 - .../Components/Authentication/Authenticator.swift | 1 - .../Components/Authentication/UsernamePasswordView.swift | 3 --- 3 files changed, 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 8463297b0..0bc0393f0 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -16,7 +16,6 @@ import SwiftUI public struct AuthenticationView: View { public init(foo: Foo) { self.foo = foo - print(" -- initing auth view: \(foo.challenge.request.url!)") } var foo: Foo diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 3770688e6..934522aff 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -20,7 +20,6 @@ public final class Foo { var continuation: CheckedContinuation? init(challenge: ArcGISAuthenticationChallenge) { - print(" -- initing foo: \(challenge.request.url!)") self.challenge = challenge } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index b0657b3c2..9f23e44a5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -25,7 +25,6 @@ import ArcGIS init(foo: Foo) { self.foo = foo self.challengingHost = foo.challenge.request.url!.host! - print(" -- initing view model: \(challengingHost)") } @Published var username = "" { @@ -70,9 +69,7 @@ import ArcGIS @MainActor struct UsernamePasswordView: View { init(viewModel: UsernamePasswordViewModel) { - //_viewModel = StateObject(wrappedValue: viewModel) self.viewModel = viewModel - print(" -- initing username/pw view: \(viewModel.challengingHost)") } @ObservedObject private var viewModel: UsernamePasswordViewModel From 31628cdfd3bd765982150143c7aed2b157fe8780 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 13:32:32 -0600 Subject: [PATCH 0854/1722] use stream instead of continuation --- .../Authentication/Authenticator.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 934522aff..22675cd70 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -17,25 +17,27 @@ import Combine public final class Foo { let challenge: ArcGISAuthenticationChallenge - var continuation: CheckedContinuation? init(challenge: ArcGISAuthenticationChallenge) { self.challenge = challenge } func resume(with result: Result) { - continuation?.resume(with: result) - continuation = nil + guard _result == nil else { return } + _result = result } func cancel() { - continuation?.resume(throwing: CancellationError()) - continuation = nil + guard _result == nil else { return } + _result = .failure(CancellationError()) } - func waitForChallengeToBeHandled() async throws -> ArcGISAuthenticationChallenge.Disposition { - return try await withCheckedThrowingContinuation { continuation in - self.continuation = continuation + @Streamed + private var _result: Result? + + var result: Result { + get async { + await $_result.compactMap({ $0 }).first(where: { _ in true })! } } } @@ -68,7 +70,7 @@ public final class Authenticator: ObservableObject { let foo = Foo(challenge: queuedChallenge.challenge) currentFoo = foo queuedChallenge.continuation.resume( - with: await Result { try await foo.waitForChallengeToBeHandled() } + with: await foo.result ) currentFoo = nil } From 79742ed5a817f1465b73f7d410bec8be7f78cccc Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 13:51:27 -0600 Subject: [PATCH 0855/1722] merge queued challenge --- .../Examples/AuthenticationExampleView.swift | 4 +-- .../Authentication/AuthenticationView.swift | 8 ++--- .../Authentication/Authenticator.swift | 35 +++++-------------- .../Authentication/UsernamePasswordView.swift | 20 +++++------ 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index b69e7dcbf..4cecbbf59 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -38,8 +38,8 @@ struct AuthenticationExampleView: View { } } .navigationBarTitle(Text("Authentication"), displayMode: .inline) - .sheet(item: $authenticator.currentFoo) { - AuthenticationView(foo: $0) + .sheet(item: $authenticator.currentChallenge) { + AuthenticationView(challenge: $0) }.onAppear { ArcGISURLSession.challengeHandler = authenticator } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 0bc0393f0..78742fa34 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -14,13 +14,13 @@ import SwiftUI public struct AuthenticationView: View { - public init(foo: Foo) { - self.foo = foo + public init(challenge: QueuedChallenge) { + self.challenge = challenge } - var foo: Foo + var challenge: QueuedChallenge public var body: some View { - UsernamePasswordView(viewModel: UsernamePasswordViewModel(foo: foo)) + UsernamePasswordView(viewModel: UsernamePasswordViewModel(challenge: challenge)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 22675cd70..1eed60cba 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -15,7 +15,7 @@ import ArcGIS import SwiftUI import Combine -public final class Foo { +public final class QueuedChallenge { let challenge: ArcGISAuthenticationChallenge init(challenge: ArcGISAuthenticationChallenge) { @@ -42,21 +42,7 @@ public final class Foo { } } -extension Foo: Identifiable { - public var id: ObjectIdentifier { - ObjectIdentifier(self) - } -} - -private class QueuedChallenge { - let challenge: ArcGISAuthenticationChallenge - var continuation: CheckedContinuation - - init(challenge: ArcGISAuthenticationChallenge, continuation: CheckedContinuation) { - self.challenge = challenge - self.continuation = continuation - } -} +extension QueuedChallenge: Identifiable {} @MainActor public final class Authenticator: ObservableObject { @@ -67,17 +53,14 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { print(" -- handing challenge") - let foo = Foo(challenge: queuedChallenge.challenge) - currentFoo = foo - queuedChallenge.continuation.resume( - with: await foo.result - ) - currentFoo = nil + currentChallenge = queuedChallenge + _ = await queuedChallenge.result + currentChallenge = nil } } @Published - public var currentFoo: Foo? + public var currentChallenge: QueuedChallenge? private var subject = PassthroughSubject() private var challengeQueue: AsyncPublisher> { @@ -101,9 +84,9 @@ extension Authenticator: AuthenticationChallengeHandler { return .performDefaultHandling } - return try await withCheckedThrowingContinuation { continuation in - subject.send(QueuedChallenge(challenge: challenge, continuation: continuation)) - } + let queuedChallenge = QueuedChallenge(challenge: challenge) + subject.send(queuedChallenge) + return try await queuedChallenge.result.get() } public func handleURLSessionChallenge( diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 9f23e44a5..8ee567aac 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -17,14 +17,14 @@ import ArcGIS @MainActor class UsernamePasswordViewModel: ObservableObject { init(challengingHost: String) { self.challengingHost = challengingHost - foo = nil + challenge = nil } - private let foo: Foo? + private let challenge: QueuedChallenge? - init(foo: Foo) { - self.foo = foo - self.challengingHost = foo.challenge.request.url!.host! + init(challenge: QueuedChallenge) { + self.challenge = challenge + self.challengingHost = challenge.challenge.request.url!.host! } @Published var username = "" { @@ -44,12 +44,12 @@ import ArcGIS func signIn() { isDismissed = true - if let foo = foo { + if let challenge = challenge { Task { - foo.resume(with: await Result { + challenge.resume(with: await Result { .useCredential( try await .token( - challenge: foo.challenge, + challenge: challenge.challenge, username: username, password: password ) @@ -61,8 +61,8 @@ import ArcGIS func cancel() { isDismissed = true - if let foo = foo { - foo.cancel() + if let challenge = challenge { + challenge.cancel() } } } From 136785e06f27d5e5075d897ff7cbe10f1af0fd2c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 13:54:32 -0600 Subject: [PATCH 0856/1722] don't use result --- .../Authentication/Authenticator.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 1eed60cba..03f2bf210 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -35,9 +35,12 @@ public final class QueuedChallenge { @Streamed private var _result: Result? - var result: Result { - get async { - await $_result.compactMap({ $0 }).first(where: { _ in true })! + var disposition: ArcGISAuthenticationChallenge.Disposition { + get async throws { + try await $_result + .compactMap({ $0 }) + .first(where: { _ in true })! + .get() } } } @@ -53,8 +56,11 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { print(" -- handing challenge") + // Set the current challenge, this should show the challenge view. currentChallenge = queuedChallenge - _ = await queuedChallenge.result + // Wait for the queued challenge to finish. + _ = try? await queuedChallenge.disposition + // Set the current challenge to `nil`, this should dismiss the challenge view. currentChallenge = nil } } @@ -86,7 +92,7 @@ extension Authenticator: AuthenticationChallengeHandler { let queuedChallenge = QueuedChallenge(challenge: challenge) subject.send(queuedChallenge) - return try await queuedChallenge.result.get() + return try await queuedChallenge.disposition } public func handleURLSessionChallenge( From 6069aa8866bae4ffe28a5f089e1b09f850426c8c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 14:11:39 -0600 Subject: [PATCH 0857/1722] doc --- .../Components/Authentication/Authenticator.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 03f2bf210..c0d9f3535 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -32,6 +32,8 @@ public final class QueuedChallenge { _result = .failure(CancellationError()) } + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. @Streamed private var _result: Result? @@ -90,8 +92,11 @@ extension Authenticator: AuthenticationChallengeHandler { return .performDefaultHandling } + // Queue up the challenge. let queuedChallenge = QueuedChallenge(challenge: challenge) subject.send(queuedChallenge) + + // Wait for it to complete and return the resulting disposition. return try await queuedChallenge.disposition } From 8b8a507ee988067a86b3d7bb4dbcea9b9379ab59 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 May 2022 15:13:18 -0500 Subject: [PATCH 0858/1722] Move non-essential initializer arguments to modifiers. --- Examples/Examples/SearchExampleView.swift | 28 ++++--- .../Components/Search/SearchView.swift | 80 +++++++++++++------ .../Components/Search/SearchViewModel.swift | 19 ++--- .../SearchViewModelTests.swift | 2 +- 4 files changed, 83 insertions(+), 46 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index eb9349d76..561385643 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -34,10 +34,17 @@ struct SearchExampleView: View { /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. let searchResultsOverlay = GraphicsOverlay() - + + /// Denotes whether the geoview is navigating. Used for the repeat search behavior. @State var isGeoViewNavigating: Bool = false + + /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. @State var geoViewExtent: Envelope? = nil + + /// The search area to be used for the current query @State var queryArea: Geometry? = nil + + /// Defines the center for the search. @State var queryCenter: Point? = nil var body: some View { @@ -51,25 +58,24 @@ struct SearchExampleView: View { queryCenter = $0.targetGeometry as? Point } .onVisibleAreaChanged { newValue in - // For "Repeat Search Here" behavior, pass the `geoViewExtent` - // to the `searchView.geoViewExtent` modifier. + // For "Repeat Search Here" behavior, use the `geoViewExtent` and + // `isGeoViewNavigating` modifiers on the `SearchView`. geoViewExtent = newValue.extent - // You can also use the visible area in the `SearchView` - // initializer to limit the results to `queryArea` - // to limit the search results. + // The visible area can be used to limit the results by + // using the `queryArea` modifier on the `SearchView`. // queryArea = newValue } .overlay(alignment: .topTrailing) { SearchView( - queryCenter: $queryCenter, sources: [locatorDataSource], - viewpoint: $searchResultViewpoint, - geoViewExtent: $geoViewExtent, - isGeoViewNavigating: $isGeoViewNavigating + viewpoint: $searchResultViewpoint ) .resultsOverlay(searchResultsOverlay) -// .queryArea($queryArea) +// .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 c969703e6..f7e0b76c9 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -18,29 +18,22 @@ import ArcGIS public struct SearchView: View { /// Creates a `SearchView`. /// - Parameters: - /// - queryCenter: Defines the center for the search. /// - 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. - /// - geoViewExtent: The current map/scene view extent. Defaults to `nil`. Used to allow - /// repeat searches after panning/zooming the map. Set to nil if repeat search behavior is not wanted. - /// - isGeoViewNavigating: Denotes whether the geoview is navigating. Used for the - /// repeat search behavior. public init( - queryCenter: Binding? = nil, sources: [SearchSource] = [], - viewpoint: Binding? = nil, - geoViewExtent: Binding? = nil, - isGeoViewNavigating: Binding? = nil + viewpoint: Binding? = nil ) { _viewModel = StateObject(wrappedValue: SearchViewModel( - queryCenter: queryCenter, sources: sources.isEmpty ? [LocatorSearchSource()] : sources, viewpoint: viewpoint )) - - _geoViewExtent = geoViewExtent ?? Binding.constant(nil) - _isGeoViewNavigating = isGeoViewNavigating ?? Binding.constant(false) + + _queryArea = Binding.constant(nil) + _queryCenter = Binding.constant(nil) + _geoViewExtent = Binding.constant(nil) + _isGeoViewNavigating = Binding.constant(false) } /// The view model used by the view. The `SearchViewModel` manages state and handles the @@ -54,16 +47,25 @@ public struct SearchView: View { /// Tracks the current user-entered query. This property drives both suggestions and searches. var resultMode: SearchResultMode = .automatic - /// The search area to be used for the current query. - var queryArea: Binding? = nil + /// The search area to be used for the current query. Defaults to `nil`. + /// + /// If `nil`, then there is no limiting of the search results to a given area. + @Binding var queryArea: Geometry? + + /// Defines the center for the search. Defaults to `nil`. + /// + /// If `nil`, does not prioritize the search results around any point. + @Binding var queryCenter: Point? /// The current map/scene view extent. Defaults to `nil`. /// - /// This should be updated via `geoViewExtent(:)`as the user navigates the map/scene. It will be - /// used to determine the value of `isEligibleForRequery` for the 'Repeat + /// This will be used to determine the value of `isEligibleForRequery` for the 'Repeat /// search here' behavior. If that behavior is not wanted, it should be left `nil`. @Binding var geoViewExtent: Envelope? + /// Determines whether the geoView is navigating in response to user interaction. + @Binding private var isGeoViewNavigating: Bool + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. var resultsOverlay: GraphicsOverlay? = nil @@ -110,9 +112,6 @@ public struct SearchView: View { /// Determines whether the results lists are displayed. @State private var isResultListHidden: Bool = false - - /// Determines whether the geoView is navigating in response to user interaction. - @Binding private var isGeoViewNavigating: Bool public var body: some View { VStack { @@ -176,11 +175,16 @@ public struct SearchView: View { .onChange(of: isGeoViewNavigating) { _ in viewModel.isGeoViewNavigating = isGeoViewNavigating } + .onChange(of: queryCenter) { _ in + viewModel.queryCenter = queryCenter + } + .onChange(of: queryArea) { _ in + viewModel.queryArea = queryArea + } .onAppear() { viewModel.currentQuery = currentQuery viewModel.resultsOverlay = resultsOverlay viewModel.resultMode = resultMode - viewModel.queryArea = queryArea } } } @@ -250,11 +254,41 @@ extension SearchView { /// The search area to be used for the current query. /// - Parameter newQueryArea: The new value. /// - Returns: The `SearchView`. - public func queryArea(_ newQueryArea: Binding?) -> Self { + public func queryArea(_ newQueryArea: Binding) -> Self { var copy = self - copy.queryArea = newQueryArea + copy._queryArea = newQueryArea return copy } + + /// Defines the center for the search. + /// - Parameter newQueryCenter: The new value. + /// - Returns: The `SearchView`. + public func queryCenter(_ newQueryCenter: Binding) -> Self { + var copy = self + copy._queryCenter = newQueryCenter + return copy + } + + /// The current map/scene view extent. Defaults to `nil`. Used to allow repeat searches after + /// panning/zooming the map. Set to `nil` if repeat search behavior is not wanted. + /// - Parameter newGeoViewExtent: The new value. + /// - Returns: The `SearchView`. + public func geoViewExtent(_ newGeoViewExtent: Binding) -> Self { + var copy = self + copy._geoViewExtent = newGeoViewExtent + return copy + } + + /// Denotes whether the geoview is navigating. Used for the repeat search behavior. + /// - Parameter newIsGeoViewNavigating: The new value. + /// - Returns: The `SearchView`. + public func isGeoViewNavigating(_ newIsGeoViewNavigating: Binding) -> Self { + var copy = self + copy._isGeoViewNavigating = newIsGeoViewNavigating + return copy + } + + } /// A View displaying the list of search results. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 98a4a5ee8..cc3d40929 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -42,16 +42,13 @@ public enum SearchOutcome { final class SearchViewModel: ObservableObject { /// Creates a `SearchViewModel`. /// - Parameters: - /// - queryCenter: Defines the center for the search. /// - 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. init( - queryCenter: Binding? = nil, sources: [SearchSource] = [], viewpoint: Binding? = nil ) { - self.queryCenter = queryCenter self.sources = sources self.viewpoint = viewpoint } @@ -146,11 +143,11 @@ final class SearchViewModel: ObservableObject { /// The search area to be used for the current query. Results will be limited to those. /// within `QueryArea`. Defaults to `nil`. - var queryArea: Binding? = nil + var queryArea: Geometry? = nil /// Defines the center for the search. For most use cases, this should be updated by the view /// every time the user navigates the map. - var queryCenter: Binding? + var queryCenter: Point? = nil /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate /// number of results is returned based on the type of suggestion chosen @@ -267,8 +264,8 @@ private extension SearchViewModel { await search(with: { try await source.search( currentQuery, - searchArea: queryArea?.wrappedValue, - preferredSearchLocation: queryCenter?.wrappedValue + searchArea: queryArea, + preferredSearchLocation: queryCenter ) } ) } @@ -282,8 +279,8 @@ private extension SearchViewModel { do { let suggestions = try await source.suggest( currentQuery, - searchArea: queryArea?.wrappedValue, - preferredSearchLocation: queryCenter?.wrappedValue + searchArea: queryArea, + preferredSearchLocation: queryCenter ) searchOutcome = .suggestions(suggestions) } catch is CancellationError { @@ -300,8 +297,8 @@ private extension SearchViewModel { with: { try await searchSuggestion.owningSource.search( searchSuggestion, - searchArea: queryArea?.wrappedValue, - preferredSearchLocation: queryCenter?.wrappedValue + searchArea: queryArea, + preferredSearchLocation: queryCenter ) }, isCollection: searchSuggestion.isCollection diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 887883528..5067c09e1 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -13,7 +13,7 @@ import XCTest import ArcGIS -import ArcGISToolkit +@testable import ArcGISToolkit @MainActor class SearchViewModelTests: XCTestCase { From 3a7c537b17cda3952f71ae9ba9248580a32964e0 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 15:54:09 -0600 Subject: [PATCH 0859/1722] add oauth support --- .../Examples/AuthenticationExampleView.swift | 18 +++++++++- .../Authentication/Authenticator.swift | 33 ++++++++++++------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 4cecbbf59..325e01dc3 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -16,7 +16,11 @@ import ArcGIS import ArcGISToolkit struct AuthenticationExampleView: View { - @ObservedObject var authenticator = Authenticator() + @ObservedObject var authenticator = Authenticator( + oAuthConfigurations: [ + .arcgisDotCom + ] + ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() @@ -176,3 +180,15 @@ extension AuthenticationItem { ] } } + +private extension OAuthConfiguration { + static let arcgisDotCom = OAuthConfiguration( + portalURL: .arcgisDotCom, + clientID: "W0DOrjQjPCL1C6LA", + redirectURL: URL(string: "runtimeswiftexamples://auth")! + ) +} + +private extension URL { + static let arcgisDotCom = URL(string: "https://www.arcgis.com")! +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index c0d9f3535..c3d060409 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -51,19 +51,32 @@ extension QueuedChallenge: Identifiable {} @MainActor public final class Authenticator: ObservableObject { - public init() { + let oAuthConfigurations: [OAuthConfiguration] + + public init(oAuthConfigurations: [OAuthConfiguration] = []) { + self.oAuthConfigurations = oAuthConfigurations Task { await observeChallengeQueue() } } private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { - print(" -- handing challenge") - // Set the current challenge, this should show the challenge view. - currentChallenge = queuedChallenge - // Wait for the queued challenge to finish. - _ = try? await queuedChallenge.disposition - // Set the current challenge to `nil`, this should dismiss the challenge view. - currentChallenge = nil + if let url = queuedChallenge.challenge.request.url, + let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { + // For an OAuth challenge, we create the credential and resume. + // Creating the OAuth credential will present the OAuth login view. + queuedChallenge.resume( + with: await Result { + .useCredential(try await .oauth(configuration: config)) + } + ) + } else { + // Set the current challenge, this should show the challenge view. + currentChallenge = queuedChallenge + // Wait for the queued challenge to finish. + _ = try? await queuedChallenge.disposition + // Set the current challenge to `nil`, this should dismiss the challenge view. + currentChallenge = nil + } } } @@ -84,11 +97,7 @@ extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { - print("-- high level challenge receieved") - await Task.yield() - guard challenge.proposedCredential == nil else { - print(" -- performing default handling") return .performDefaultHandling } From efb076a20f9645f8e3d2a261e297eed45c4c5c85 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 May 2022 17:01:56 -0500 Subject: [PATCH 0860/1722] PR review comments. --- Examples/Examples/SearchExampleView.swift | 19 +++++++++---------- .../Components/Search/SearchView.swift | 16 +++++++--------- .../Components/Search/SearchViewModel.swift | 9 +++++---- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 561385643..56df4ce86 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -22,30 +22,29 @@ struct SearchExampleView: View { maximumSuggestions: 16 ) - let map = Map(basemapStyle: .arcGISImagery) + @StateObject private var map = Map(basemapStyle: .arcGISImagery) + /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. + private let searchResultsOverlay = GraphicsOverlay() + /// The map viewpoint used by the `SearchView` to pan/zoom the map /// to the extent of the search results. - @State - private var searchResultViewpoint: Viewpoint? = Viewpoint( + @State private var searchResultViewpoint: Viewpoint? = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), scale: 1000000 ) - /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. - let searchResultsOverlay = GraphicsOverlay() - /// Denotes whether the geoview is navigating. Used for the repeat search behavior. - @State var isGeoViewNavigating: Bool = false + @State private var isGeoViewNavigating: Bool = false /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. - @State var geoViewExtent: Envelope? = nil + @State private var geoViewExtent: Envelope? = nil /// The search area to be used for the current query - @State var queryArea: Geometry? = nil + @State private var queryArea: Geometry? = nil /// Defines the center for the search. - @State var queryCenter: Point? = nil + @State private var queryCenter: Point? = nil var body: some View { MapView( diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index f7e0b76c9..0f7310ee3 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -18,7 +18,7 @@ import ArcGIS public struct SearchView: View { /// Creates a `SearchView`. /// - Parameters: - /// - sources: Collection of search sources to be used. + /// - 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. public init( @@ -42,17 +42,17 @@ public struct SearchView: View { @StateObject private var viewModel: SearchViewModel /// Tracks the current user-entered query. This property drives both suggestions and searches. - var currentQuery: String = "" + var currentQuery = "" /// Tracks the current user-entered query. This property drives both suggestions and searches. var resultMode: SearchResultMode = .automatic - /// The search area to be used for the current query. Defaults to `nil`. + /// The search area to be used for the current query. Defaults to `nil`. /// /// If `nil`, then there is no limiting of the search results to a given area. @Binding var queryArea: Geometry? - /// Defines the center for the search. Defaults to `nil`. + /// Defines the center for the search. Defaults to `nil`. /// /// If `nil`, does not prioritize the search results around any point. @Binding var queryCenter: Point? @@ -60,16 +60,16 @@ public struct SearchView: View { /// The current map/scene view extent. Defaults to `nil`. /// /// This will be used to determine the value of `isEligibleForRequery` for the 'Repeat - /// search here' behavior. If that behavior is not wanted, it should be left `nil`. + /// search here' behavior. If that behavior is not wanted, it should be left as `nil`. @Binding var geoViewExtent: Envelope? - /// Determines whether the geoView is navigating in response to user interaction. + /// Determines whether the `geoView` is navigating in response to user interaction. @Binding private var isGeoViewNavigating: Bool /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. var resultsOverlay: GraphicsOverlay? = nil - /// Collection of search sources to be used. This list is maintained over time and is not nullable. + /// A collection of search sources to be used. This list is maintained over time. /// The view should observe this list for changes. Consumers should add and remove sources from /// this list as needed. /// NOTE: Only the first source is currently used; multiple sources are not yet supported. @@ -287,8 +287,6 @@ extension SearchView { copy._isGeoViewNavigating = newIsGeoViewNavigating return copy } - - } /// A View displaying the list of search results. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index cc3d40929..12680dd9b 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -149,12 +149,13 @@ final class SearchViewModel: ObservableObject { /// every time the user navigates the map. var queryCenter: Point? = nil - /// Defines how many results to return. Defaults to Automatic. In automatic mode, an appropriate - /// number of results is returned based on the type of suggestion chosen - /// (driven by the suggestion's `isCollection` property). + /// Defines how many results to return. Defaults to ``SearchResultMode/automatic``. + /// In automatic mode, an appropriate number of results is returned based on the type of suggestion + /// chosen (driven by the suggestion's `isCollection` property). var resultMode: SearchResultMode = .automatic - /// The collection of search and suggestion results. A `nil` value means no query has been made. + /// A search outcome that contains the search and suggestion results. A `nil` value means no + /// query has been made. @Published private(set) var searchOutcome: SearchOutcome? { didSet { From 5aa3a5ea81883f61c8a28387e35c1b27ddbe87dc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 May 2022 17:05:45 -0500 Subject: [PATCH 0861/1722] Add whitespace for blank line. --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 12680dd9b..41988f44c 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -120,7 +120,7 @@ final class SearchViewModel: ObservableObject { /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. var isGeoViewNavigating: Bool = false - + /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. var viewpoint: Binding? = nil From 7e5854dfc161c40b3b41933fd24fe5f20039494e Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 13 May 2022 16:08:05 -0600 Subject: [PATCH 0862/1722] support oauth --- Examples/Examples/AuthenticationExampleView.swift | 11 ++++++++++- .../Components/Authentication/Authenticator.swift | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 325e01dc3..d20584451 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -172,11 +172,20 @@ extension AuthenticationItem { ) } + static func makePortal() -> AuthenticationItem { + AuthenticationItem( + title: "Portal", + loadables: [ + Portal.arcGISOnline(isLoginRequired: true) + ] + ) + } static func makeAll() -> [AuthenticationItem] { [ .makeToken(), .makeMultipleToken(), - .makeMultipleTokenSame() + .makeMultipleTokenSame(), + .makePortal() ] } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index c3d060409..0e1b7725f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -53,9 +53,13 @@ extension QueuedChallenge: Identifiable {} public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] + let logger = ConsoleNetworkLogger() + public init(oAuthConfigurations: [OAuthConfiguration] = []) { self.oAuthConfigurations = oAuthConfigurations Task { await observeChallengeQueue() } + logger.shouldLogResponseData = true + logger.startLogging() } private func observeChallengeQueue() async { From b4427b31565fd7e153f5380c9b32ef67a882835e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 May 2022 13:33:05 -0500 Subject: [PATCH 0863/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 56df4ce86..973dda5b3 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -26,7 +26,7 @@ struct SearchExampleView: View { /// The `GraphicsOverlay` used by the `SearchView` to display search results on the map. private let searchResultsOverlay = GraphicsOverlay() - + /// The map viewpoint used by the `SearchView` to pan/zoom the map /// to the extent of the search results. @State private var searchResultViewpoint: Viewpoint? = Viewpoint( From 0733cc1284b1a41c66b62c8007297fc76ba11473 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 May 2022 13:33:15 -0500 Subject: [PATCH 0864/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 973dda5b3..42598998f 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -39,7 +39,7 @@ struct SearchExampleView: View { /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. @State private var geoViewExtent: Envelope? = nil - + /// The search area to be used for the current query @State private var queryArea: Geometry? = nil From 3e3b3597f6dd03138089550d56e9dbb9d1784a2f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 May 2022 13:33:22 -0500 Subject: [PATCH 0865/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 42598998f..a7cd6b613 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -42,7 +42,7 @@ struct SearchExampleView: View { /// The search area to be used for the current query @State private var queryArea: Geometry? = nil - + /// Defines the center for the search. @State private var queryCenter: Point? = nil From e4043e9b76a08e08a7e73276ec3f81f0632cb83f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 May 2022 13:33:30 -0500 Subject: [PATCH 0866/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index a7cd6b613..8a9952af6 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -45,7 +45,7 @@ struct SearchExampleView: View { /// Defines the center for the search. @State private var queryCenter: Point? = nil - +  var body: some View { MapView( map: map, From cd37ceb67b5d7e98466933a44a3533c47be3dabd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 May 2022 13:33:42 -0500 Subject: [PATCH 0867/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/SearchExampleView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 8a9952af6..bb1a1c649 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -16,6 +16,7 @@ import ArcGIS import ArcGISToolkit struct SearchExampleView: View { + /// Provides search behavior customization. let locatorDataSource = SmartLocatorSearchSource( name: "My locator", maximumResults: 16, From 65c172b4d3b4debee53f3caea2bec5318a752b85 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 May 2022 13:34:08 -0500 Subject: [PATCH 0868/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: David Feinzimer --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 0f7310ee3..a6d274669 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -380,6 +380,8 @@ struct ResultRow: View { .foregroundColor(.secondary) } } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) } From 070536d388ec3887c8d22fdbaaa49671b997a3e8 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 16 May 2022 13:22:14 -0600 Subject: [PATCH 0869/1722] start working on URL challenges --- .../Examples/AuthenticationExampleView.swift | 21 +++-- .../Authentication/AuthenticationView.swift | 2 +- .../Authentication/Authenticator.swift | 80 +++++++++++++++---- .../Authentication/UsernamePasswordView.swift | 65 ++++++++------- 4 files changed, 114 insertions(+), 54 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index d20584451..c23847c08 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,9 +17,8 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( - oAuthConfigurations: [ - .arcgisDotCom - ] + oAuthConfigurations: [.arcgisDotCom], + trustedHosts: ["rt-server107a.esri.com"] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() @@ -180,12 +179,20 @@ extension AuthenticationItem { ] ) } + static func makeIWAPortal() -> AuthenticationItem { + AuthenticationItem( + title: "IWA Portal", + loadables: [Portal.init(url: URL(string: "https://dev0004327.esri.com/portal")!)] + ) + } + static func makeAll() -> [AuthenticationItem] { [ - .makeToken(), - .makeMultipleToken(), - .makeMultipleTokenSame(), - .makePortal() + makeToken(), + makeMultipleToken(), + makeMultipleTokenSame(), + makePortal(), + makeIWAPortal() ] } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 78742fa34..01fdfbd2e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -21,6 +21,6 @@ public struct AuthenticationView: View { var challenge: QueuedChallenge public var body: some View { - UsernamePasswordView(viewModel: UsernamePasswordViewModel(challenge: challenge)) + UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge as! QueuedArcGISChallenge)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 0e1b7725f..e666e7519 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -15,11 +15,11 @@ import ArcGIS import SwiftUI import Combine -public final class QueuedChallenge { - let challenge: ArcGISAuthenticationChallenge +public final class QueuedArcGISChallenge: QueuedChallenge { + let arcGISChallenge: ArcGISAuthenticationChallenge - init(challenge: ArcGISAuthenticationChallenge) { - self.challenge = challenge + init(arcGISChallenge: ArcGISAuthenticationChallenge) { + self.arcGISChallenge = arcGISChallenge } func resume(with result: Result) { @@ -45,30 +45,74 @@ public final class QueuedChallenge { .get() } } + + public func complete() async { + _ = try? await disposition + } } -extension QueuedChallenge: Identifiable {} +public final class QueuedURLChallenge: QueuedChallenge { + let urlChallenge: URLAuthenticationChallenge + + init(urlChallenge: URLAuthenticationChallenge) { + self.urlChallenge = urlChallenge + } + + func resume(with result: Result) { + guard _result == nil else { return } + _result = result + } + + func cancel() { + guard _result == nil else { return } + _result = .failure(CancellationError()) + } + + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. + @Streamed + private var _result: Result? + + var disposition: URLSession.AuthChallengeDisposition { + get async throws { + try await $_result + .compactMap({ $0 }) + .first(where: { _ in true })! + .get() + } + } + + public func complete() async { + _ = try? await disposition + } +} + +public protocol QueuedChallenge: Identifiable { + func complete() async +} @MainActor public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] + let trustedHosts: [String] - let logger = ConsoleNetworkLogger() - - public init(oAuthConfigurations: [OAuthConfiguration] = []) { + public init( + oAuthConfigurations: [OAuthConfiguration] = [], + trustedHosts: [String] = [] + ) { self.oAuthConfigurations = oAuthConfigurations + self.trustedHosts = trustedHosts Task { await observeChallengeQueue() } - logger.shouldLogResponseData = true - logger.startLogging() } private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { - if let url = queuedChallenge.challenge.request.url, + if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, + let url = queuedArcGISChallenge.arcGISChallenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { // For an OAuth challenge, we create the credential and resume. // Creating the OAuth credential will present the OAuth login view. - queuedChallenge.resume( + queuedArcGISChallenge.resume( with: await Result { .useCredential(try await .oauth(configuration: config)) } @@ -77,7 +121,7 @@ public final class Authenticator: ObservableObject { // Set the current challenge, this should show the challenge view. currentChallenge = queuedChallenge // Wait for the queued challenge to finish. - _ = try? await queuedChallenge.disposition + await queuedChallenge.complete() // Set the current challenge to `nil`, this should dismiss the challenge view. currentChallenge = nil } @@ -106,7 +150,7 @@ extension Authenticator: AuthenticationChallengeHandler { } // Queue up the challenge. - let queuedChallenge = QueuedChallenge(challenge: challenge) + let queuedChallenge = QueuedArcGISChallenge(arcGISChallenge: challenge) subject.send(queuedChallenge) // Wait for it to complete and return the resulting disposition. @@ -118,10 +162,16 @@ extension Authenticator: AuthenticationChallengeHandler { scope: URLAuthenticationChallengeScope ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let trust = challenge.protectionSpace.serverTrust { + let trust = challenge.protectionSpace.serverTrust, + trustedHosts.contains(challenge.protectionSpace.host) { // This will cause a self-signed certificate to be trusted. return (.useCredential, URLCredential(trust: trust)) } else { + + // Queue up the challenge. + let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) + subject.send(queuedChallenge) + return (.performDefaultHandling, nil) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 8ee567aac..01d1a955d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -14,17 +14,22 @@ import SwiftUI import ArcGIS -@MainActor class UsernamePasswordViewModel: ObservableObject { - init(challengingHost: String) { - self.challengingHost = challengingHost - challenge = nil - } +@MainActor protocol UsernamePasswordViewModel: ObservableObject { + var username: String { get set } + var password: String { get set } + var signinButtonEnabled: Bool { get } + var isDismissed: Bool { get } + var challengingHost: String { get } - private let challenge: QueuedChallenge? + func signIn() + func cancel() +} + +@MainActor class TokenCredentialViewModel: UsernamePasswordViewModel { + private let challenge: QueuedArcGISChallenge - init(challenge: QueuedChallenge) { + init(challenge: QueuedArcGISChallenge) { self.challenge = challenge - self.challengingHost = challenge.challenge.request.url!.host! } @Published var username = "" { @@ -40,39 +45,37 @@ import ArcGIS signinButtonEnabled = !username.isEmpty && !password.isEmpty } - let challengingHost: String + var challengingHost: String { + challenge.arcGISChallenge.request.url!.host! + } func signIn() { isDismissed = true - if let challenge = challenge { - Task { - challenge.resume(with: await Result { - .useCredential( - try await .token( - challenge: challenge.challenge, - username: username, - password: password - ) + Task { + challenge.resume(with: await Result { + .useCredential( + try await .token( + challenge: challenge.arcGISChallenge, + username: username, + password: password ) - }) - } + ) + }) } } func cancel() { isDismissed = true - if let challenge = challenge { - challenge.cancel() - } + challenge.cancel() } } -@MainActor struct UsernamePasswordView: View { - init(viewModel: UsernamePasswordViewModel) { +@MainActor struct UsernamePasswordView: View { + init(viewModel: ViewModel) { self.viewModel = viewModel } - @ObservedObject private var viewModel: UsernamePasswordViewModel + @ObservedObject private var viewModel: ViewModel /// The focused field. @FocusState private var focusedField: Field? @@ -151,11 +154,11 @@ import ArcGIS } } -struct UsernamePasswordView_Previews: PreviewProvider { - static var previews: some View { - UsernamePasswordView(viewModel: UsernamePasswordViewModel(challengingHost: "arcgis.com")) - } -} +//struct UsernamePasswordView_Previews: PreviewProvider { +// static var previews: some View { +// UsernamePasswordView(viewModel: UsernamePasswordViewModel(challengingHost: "arcgis.com")) +// } +//} private extension UsernamePasswordView { /// A type that represents the fields in the user name and password sign-in form. From bceb5d05cebecf0f6fa901843ea9f35370e5e8be Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 16 May 2022 13:57:02 -0600 Subject: [PATCH 0870/1722] identifiable queued challenge --- Examples/Examples/AuthenticationExampleView.swift | 2 +- .../Components/Authentication/Authenticator.swift | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index c23847c08..80f1dede1 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -42,7 +42,7 @@ struct AuthenticationExampleView: View { } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .sheet(item: $authenticator.currentChallenge) { - AuthenticationView(challenge: $0) + AuthenticationView(challenge: $0.queuedChallenge) }.onAppear { ArcGISURLSession.challengeHandler = authenticator } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index e666e7519..8eb193cf2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -87,7 +87,7 @@ public final class QueuedURLChallenge: QueuedChallenge { } } -public protocol QueuedChallenge: Identifiable { +public protocol QueuedChallenge: AnyObject { func complete() async } @@ -119,7 +119,7 @@ public final class Authenticator: ObservableObject { ) } else { // Set the current challenge, this should show the challenge view. - currentChallenge = queuedChallenge + currentChallenge = IdentifiableQueuedChallenge(queuedChallenge: queuedChallenge) // Wait for the queued challenge to finish. await queuedChallenge.complete() // Set the current challenge to `nil`, this should dismiss the challenge view. @@ -129,7 +129,7 @@ public final class Authenticator: ObservableObject { } @Published - public var currentChallenge: QueuedChallenge? + public var currentChallenge: IdentifiableQueuedChallenge? private var subject = PassthroughSubject() private var challengeQueue: AsyncPublisher> { @@ -141,6 +141,14 @@ public final class Authenticator: ObservableObject { } } +public struct IdentifiableQueuedChallenge { + public let queuedChallenge: QueuedChallenge +} + +extension IdentifiableQueuedChallenge: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(queuedChallenge) } +} + extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge From b4e7c915b486fae1d62d7e217ae9df51cf86df58 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 16 May 2022 15:02:16 -0600 Subject: [PATCH 0871/1722] url credential --- .../Authentication/Authenticator.swift | 8 ++-- .../Authentication/UsernamePasswordView.swift | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 8eb193cf2..263766030 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -58,7 +58,7 @@ public final class QueuedURLChallenge: QueuedChallenge { self.urlChallenge = urlChallenge } - func resume(with result: Result) { + func resume(with result: Result<(URLSession.AuthChallengeDisposition, URLCredential?), Error>) { guard _result == nil else { return } _result = result } @@ -71,9 +71,9 @@ public final class QueuedURLChallenge: QueuedChallenge { /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. @Streamed - private var _result: Result? + private var _result: Result<(URLSession.AuthChallengeDisposition, URLCredential?), Error>? - var disposition: URLSession.AuthChallengeDisposition { + var dispositionAndCredential: (URLSession.AuthChallengeDisposition, URLCredential?) { get async throws { try await $_result .compactMap({ $0 }) @@ -83,7 +83,7 @@ public final class QueuedURLChallenge: QueuedChallenge { } public func complete() async { - _ = try? await disposition + _ = try? await dispositionAndCredential } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 01d1a955d..300858adf 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -70,6 +70,45 @@ import ArcGIS } } +@MainActor class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { + private let challenge: QueuedURLChallenge + + init(challenge: QueuedURLChallenge) { + self.challenge = challenge + } + + @Published var username = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var password = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var signinButtonEnabled = false + @Published var isDismissed = false + + private func updateSigninButtonEnabled() { + signinButtonEnabled = !username.isEmpty && !password.isEmpty + } + + var challengingHost: String { + challenge.urlChallenge.protectionSpace.host + } + + func signIn() { + isDismissed = true + Task { + challenge.resume(with: Result { + (.useCredential, URLCredential(user: username, password: password, persistence: .forSession)) + }) + } + } + + func cancel() { + isDismissed = true + challenge.cancel() + } +} + @MainActor struct UsernamePasswordView: View { init(viewModel: ViewModel) { self.viewModel = viewModel From 06c6eb47d73513784b0f74bb2acd0ae4a3f32da9 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 16 May 2022 15:06:48 -0600 Subject: [PATCH 0872/1722] url credential --- .../Authentication/AuthenticationView.swift | 9 +- .../Authentication/UsernamePasswordView.swift | 208 ++++++++++-------- 2 files changed, 127 insertions(+), 90 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 01fdfbd2e..ac7d83422 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -21,6 +21,13 @@ public struct AuthenticationView: View { var challenge: QueuedChallenge public var body: some View { - UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge as! QueuedArcGISChallenge)) + switch challenge { + case let challenge as QueuedArcGISChallenge: + UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) + case let challenge as QueuedURLChallenge: + UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + default: + fatalError() + } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 300858adf..025f7aac9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -25,90 +25,6 @@ import ArcGIS func cancel() } -@MainActor class TokenCredentialViewModel: UsernamePasswordViewModel { - private let challenge: QueuedArcGISChallenge - - init(challenge: QueuedArcGISChallenge) { - self.challenge = challenge - } - - @Published var username = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var password = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var signinButtonEnabled = false - @Published var isDismissed = false - - private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty - } - - var challengingHost: String { - challenge.arcGISChallenge.request.url!.host! - } - - func signIn() { - isDismissed = true - Task { - challenge.resume(with: await Result { - .useCredential( - try await .token( - challenge: challenge.arcGISChallenge, - username: username, - password: password - ) - ) - }) - } - } - - func cancel() { - isDismissed = true - challenge.cancel() - } -} - -@MainActor class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { - private let challenge: QueuedURLChallenge - - init(challenge: QueuedURLChallenge) { - self.challenge = challenge - } - - @Published var username = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var password = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var signinButtonEnabled = false - @Published var isDismissed = false - - private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty - } - - var challengingHost: String { - challenge.urlChallenge.protectionSpace.host - } - - func signIn() { - isDismissed = true - Task { - challenge.resume(with: Result { - (.useCredential, URLCredential(user: username, password: password, persistence: .forSession)) - }) - } - } - - func cancel() { - isDismissed = true - challenge.cancel() - } -} - @MainActor struct UsernamePasswordView: View { init(viewModel: ViewModel) { self.viewModel = viewModel @@ -193,11 +109,11 @@ import ArcGIS } } -//struct UsernamePasswordView_Previews: PreviewProvider { -// static var previews: some View { -// UsernamePasswordView(viewModel: UsernamePasswordViewModel(challengingHost: "arcgis.com")) -// } -//} +struct UsernamePasswordView_Previews: PreviewProvider { + static var previews: some View { + UsernamePasswordView(viewModel: MockUsernamePasswordViewModel(challengingHost: "arcgis.com")) + } +} private extension UsernamePasswordView { /// A type that represents the fields in the user name and password sign-in form. @@ -206,3 +122,117 @@ private extension UsernamePasswordView { case password } } + + +@MainActor class MockUsernamePasswordViewModel: UsernamePasswordViewModel { + init(challengingHost: String) { + self.challengingHost = challengingHost + } + + @Published var username = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var password = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var signinButtonEnabled = false + @Published var isDismissed = false + + private func updateSigninButtonEnabled() { + signinButtonEnabled = !username.isEmpty && !password.isEmpty + } + + let challengingHost: String + + func signIn() { + isDismissed = true + } + + func cancel() { + isDismissed = true + } +} + +@MainActor class TokenCredentialViewModel: UsernamePasswordViewModel { + private let challenge: QueuedArcGISChallenge + + init(challenge: QueuedArcGISChallenge) { + self.challenge = challenge + } + + @Published var username = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var password = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var signinButtonEnabled = false + @Published var isDismissed = false + + private func updateSigninButtonEnabled() { + signinButtonEnabled = !username.isEmpty && !password.isEmpty + } + + var challengingHost: String { + challenge.arcGISChallenge.request.url!.host! + } + + func signIn() { + isDismissed = true + Task { + challenge.resume(with: await Result { + .useCredential( + try await .token( + challenge: challenge.arcGISChallenge, + username: username, + password: password + ) + ) + }) + } + } + + func cancel() { + isDismissed = true + challenge.cancel() + } +} + +@MainActor class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { + private let challenge: QueuedURLChallenge + + init(challenge: QueuedURLChallenge) { + self.challenge = challenge + } + + @Published var username = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var password = "" { + didSet { updateSigninButtonEnabled() } + } + @Published var signinButtonEnabled = false + @Published var isDismissed = false + + private func updateSigninButtonEnabled() { + signinButtonEnabled = !username.isEmpty && !password.isEmpty + } + + var challengingHost: String { + challenge.urlChallenge.protectionSpace.host + } + + func signIn() { + isDismissed = true + Task { + challenge.resume(with: Result { + (.useCredential, URLCredential(user: username, password: password, persistence: .forSession)) + }) + } + } + + func cancel() { + isDismissed = true + challenge.cancel() + } +} From dbbcd9b32f33ba3be63b8973a839303359944de7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 16 May 2022 15:28:52 -0600 Subject: [PATCH 0873/1722] get url cred working --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/Authenticator.swift | 32 ++++++++++--------- .../Authentication/UsernamePasswordView.swift | 6 ++-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 80f1dede1..2483d3983 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -18,7 +18,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( oAuthConfigurations: [.arcgisDotCom], - trustedHosts: ["rt-server107a.esri.com"] + trustedHosts: ["rt-server107a.esri.com", "dev0004327.esri.com"] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 263766030..d9572306e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -58,32 +58,31 @@ public final class QueuedURLChallenge: QueuedChallenge { self.urlChallenge = urlChallenge } - func resume(with result: Result<(URLSession.AuthChallengeDisposition, URLCredential?), Error>) { - guard _result == nil else { return } - _result = result + func resume(with dispositionAndCredential: (URLSession.AuthChallengeDisposition, URLCredential?)) { + guard _dispositionAndCredential == nil else { return } + _dispositionAndCredential = dispositionAndCredential } func cancel() { - guard _result == nil else { return } - _result = .failure(CancellationError()) + guard _dispositionAndCredential == nil else { return } + _dispositionAndCredential = (.cancelAuthenticationChallenge, nil) } /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. @Streamed - private var _result: Result<(URLSession.AuthChallengeDisposition, URLCredential?), Error>? + private var _dispositionAndCredential: (URLSession.AuthChallengeDisposition, URLCredential?)? var dispositionAndCredential: (URLSession.AuthChallengeDisposition, URLCredential?) { - get async throws { - try await $_result + get async { + await $_dispositionAndCredential .compactMap({ $0 }) .first(where: { _ in true })! - .get() } } public func complete() async { - _ = try? await dispositionAndCredential + _ = await dispositionAndCredential } } @@ -170,17 +169,20 @@ extension Authenticator: AuthenticationChallengeHandler { scope: URLAuthenticationChallengeScope ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let trust = challenge.protectionSpace.serverTrust, - trustedHosts.contains(challenge.protectionSpace.host) { - // This will cause a self-signed certificate to be trusted. - return (.useCredential, URLCredential(trust: trust)) + let trust = challenge.protectionSpace.serverTrust { + if trustedHosts.contains(challenge.protectionSpace.host) { + // This will cause a self-signed certificate to be trusted. + return (.useCredential, URLCredential(trust: trust)) + } else { + return (.performDefaultHandling, nil) + } } else { // Queue up the challenge. let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) subject.send(queuedChallenge) - return (.performDefaultHandling, nil) + return await queuedChallenge.dispositionAndCredential } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 025f7aac9..38eb733bd 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -225,9 +225,9 @@ private extension UsernamePasswordView { func signIn() { isDismissed = true Task { - challenge.resume(with: Result { - (.useCredential, URLCredential(user: username, password: password, persistence: .forSession)) - }) + challenge.resume( + with: (.useCredential, URLCredential(user: username, password: password, persistence: .forSession)) + ) } } From 500c6cc4bc310959f1b51aa880ed1e21e39f81f6 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 16 May 2022 17:25:30 -0600 Subject: [PATCH 0874/1722] trust host view --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/AuthenticationView.swift | 20 ++++ .../Authentication/Authenticator.swift | 48 +++++--- .../Authentication/TrustHostView.swift | 105 ++++++++++++++++++ .../Authentication/UsernamePasswordView.swift | 9 +- 5 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 2483d3983..f3d1e52a5 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( - oAuthConfigurations: [.arcgisDotCom], + //oAuthConfigurations: [.arcgisDotCom], trustedHosts: ["rt-server107a.esri.com", "dev0004327.esri.com"] ) @State var previousApiKey: APIKey? diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index ac7d83422..295beff5f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -25,6 +25,26 @@ public struct AuthenticationView: View { case let challenge as QueuedArcGISChallenge: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) case let challenge as QueuedURLChallenge: + view(forURLChallenge: challenge) + default: + fatalError() + } + } + + @MainActor + @ViewBuilder + func view(forURLChallenge challenge: QueuedURLChallenge) -> some View { + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) + case NSURLAuthenticationMethodClientCertificate: + // TODO: Show certificate picker + fatalError() + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) default: fatalError() diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index d9572306e..32639a0a4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -168,21 +168,39 @@ extension Authenticator: AuthenticationChallengeHandler { _ challenge: URLAuthenticationChallenge, scope: URLAuthenticationChallengeScope ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let trust = challenge.protectionSpace.serverTrust { - if trustedHosts.contains(challenge.protectionSpace.host) { - // This will cause a self-signed certificate to be trusted. - return (.useCredential, URLCredential(trust: trust)) - } else { - return (.performDefaultHandling, nil) - } - } else { - - // Queue up the challenge. - let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) - subject.send(queuedChallenge) - - return await queuedChallenge.dispositionAndCredential + guard challenge.protectionSpace.authenticationMethod != NSURLAuthenticationMethodDefault else { + return (.performDefaultHandling, nil) } + + // Queue up the url challenge. + let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) + subject.send(queuedChallenge) + return await queuedChallenge.dispositionAndCredential + +// switch challenge.protectionSpace.authenticationMethod { +// case NSURLAuthenticationMethodServerTrust: +// if let trust = challenge.protectionSpace.serverTrust { +// if trustedHosts.contains(challenge.protectionSpace.host) { +// // Ryan - TODO: Show alert +// // This will cause a self-signed certificate to be trusted. +// return (.useCredential, URLCredential(trust: trust)) +// } else { +// return (.performDefaultHandling, nil) +// } +// } +// case NSURLAuthenticationMethodClientCertificate: +// // Show certificate picker? +// UIDocumentPickerViewController() +// fatalError() +// case NSURLAuthenticationMethodDefault: +// case NSURLAuthenticationMethodNTLM: +// case NSURLAuthenticationMethodHTMLForm: +// case NSURLAuthenticationMethodHTTPBasic: +// case NSURLAuthenticationMethodHTTPDigest: +// // Queue up the username and password challenge. +// let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) +// subject.send(queuedChallenge) +// return await queuedChallenge.dispositionAndCredential +// } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift new file mode 100644 index 000000000..37b8af7df --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift @@ -0,0 +1,105 @@ +// 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 + +@MainActor protocol TrustHostViewModel: ObservableObject { + var challengingHost: String { get } + + func allowConnection() + func cancel() +} + +final class TrustHostChallengeViewModel: TrustHostViewModel { + init(challenge: QueuedURLChallenge) { + self.challenge = challenge + } + + let challenge: QueuedURLChallenge + + var challengingHost: String { + challenge.urlChallenge.protectionSpace.host + } + + func allowConnection() { + challenge.resume( + with: ( + .useCredential, + URLCredential(trust: challenge.urlChallenge.protectionSpace.serverTrust!) + ) + ) + } + + func cancel() { + challenge.resume(with: (.performDefaultHandling, nil)) + } +} + +final class MockTrustHostViewModel: TrustHostViewModel { + let challengingHost: String + init(challengingHost: String) { + self.challengingHost = challengingHost + } + func allowConnection() {} + func cancel() {} +} + +struct TrustHostView: View { + init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + @ObservedObject private var viewModel: ViewModel + + var body: some View { + VStack { + Text("Certificate Trust Warning") + .font(.title) + .padding([.bottom]) + .multilineTextAlignment(.center) + + Text("The certificate provided by '\(viewModel.challengingHost)' is not signed by a trusted authority.") + .font(.body) + .padding([.bottom]) + + Button(role: .cancel) { + viewModel.cancel() + } label: { + Text("Cancel") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.large) + + Button(role: .destructive) { + viewModel.allowConnection() + } label: { + Text("Dangerous: Allow Connection") + .frame(maxWidth: .infinity) + + } + .buttonStyle(.bordered) + .controlSize(.large) + + Spacer() + } + .padding() + .interactiveDismissDisabled() + } +} + +struct TrustHostView_Previews: PreviewProvider { + static var previews: some View { + TrustHostView(viewModel: MockTrustHostViewModel(challengingHost: "arcgis.com")) + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 38eb733bd..71f447966 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -25,7 +25,7 @@ import ArcGIS func cancel() } -@MainActor struct UsernamePasswordView: View { +struct UsernamePasswordView: View { init(viewModel: ViewModel) { self.viewModel = viewModel } @@ -123,8 +123,7 @@ private extension UsernamePasswordView { } } - -@MainActor class MockUsernamePasswordViewModel: UsernamePasswordViewModel { +class MockUsernamePasswordViewModel: UsernamePasswordViewModel { init(challengingHost: String) { self.challengingHost = challengingHost } @@ -153,7 +152,7 @@ private extension UsernamePasswordView { } } -@MainActor class TokenCredentialViewModel: UsernamePasswordViewModel { +class TokenCredentialViewModel: UsernamePasswordViewModel { private let challenge: QueuedArcGISChallenge init(challenge: QueuedArcGISChallenge) { @@ -198,7 +197,7 @@ private extension UsernamePasswordView { } } -@MainActor class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { +class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { private let challenge: QueuedURLChallenge init(challenge: QueuedURLChallenge) { From 33455232d13632421fc3c425bfe1d64df4b5b08a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 10:02:32 -0600 Subject: [PATCH 0875/1722] add to trusted hosts --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/AuthenticationView.swift | 4 +- .../Authentication/Authenticator.swift | 71 +++++++++++++++---- .../Authentication/TrustHostView.swift | 9 +-- .../Authentication/UsernamePasswordView.swift | 4 +- 5 files changed, 63 insertions(+), 27 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index f3d1e52a5..518d7c3b1 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -18,7 +18,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( //oAuthConfigurations: [.arcgisDotCom], - trustedHosts: ["rt-server107a.esri.com", "dev0004327.esri.com"] + //trustedHosts: ["rt-server107a.esri.com", "dev0004327.esri.com"] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 295beff5f..0bb67c4b8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -17,8 +17,8 @@ public struct AuthenticationView: View { public init(challenge: QueuedChallenge) { self.challenge = challenge } - - var challenge: QueuedChallenge + + let challenge: QueuedChallenge public var body: some View { switch challenge { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 32639a0a4..0c50b6329 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -58,31 +58,37 @@ public final class QueuedURLChallenge: QueuedChallenge { self.urlChallenge = urlChallenge } - func resume(with dispositionAndCredential: (URLSession.AuthChallengeDisposition, URLCredential?)) { - guard _dispositionAndCredential == nil else { return } - _dispositionAndCredential = dispositionAndCredential + func resume(with response: Response) { + guard _response == nil else { return } + _response = response } func cancel() { - guard _dispositionAndCredential == nil else { return } - _dispositionAndCredential = (.cancelAuthenticationChallenge, nil) + guard _response == nil else { return } + _response = .cancel } /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. @Streamed - private var _dispositionAndCredential: (URLSession.AuthChallengeDisposition, URLCredential?)? + private var _response: (Response)? - var dispositionAndCredential: (URLSession.AuthChallengeDisposition, URLCredential?) { + var response: Response { get async { - await $_dispositionAndCredential + await $_response .compactMap({ $0 }) .first(where: { _ in true })! } } public func complete() async { - _ = await dispositionAndCredential + _ = await response + } + + enum Response { + case userCredential(username: String, password: String) + case trustHost + case cancel } } @@ -93,14 +99,12 @@ public protocol QueuedChallenge: AnyObject { @MainActor public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] - let trustedHosts: [String] + var trustedHosts: [String] = [] public init( - oAuthConfigurations: [OAuthConfiguration] = [], - trustedHosts: [String] = [] + oAuthConfigurations: [OAuthConfiguration] = [] ) { self.oAuthConfigurations = oAuthConfigurations - self.trustedHosts = trustedHosts Task { await observeChallengeQueue() } } @@ -172,10 +176,49 @@ extension Authenticator: AuthenticationChallengeHandler { return (.performDefaultHandling, nil) } + guard challenge.proposedCredential == nil else { + return (.performDefaultHandling, nil) + } + + // If the host is already trusted, then continue trusting it. + if let trust = challenge.protectionSpace.serverTrust, + trustedHosts.contains(challenge.protectionSpace.host) { + return (.useCredential, URLCredential(trust: trust)) + } + // Queue up the url challenge. let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) subject.send(queuedChallenge) - return await queuedChallenge.dispositionAndCredential + + switch await queuedChallenge.response { + case .cancel: + return (.cancelAuthenticationChallenge, nil) + case .trustHost: + if let trust = challenge.protectionSpace.serverTrust { + trustedHosts.append(challenge.protectionSpace.host) + return (.useCredential, URLCredential(trust: trust)) + } else { + return (.performDefaultHandling, nil) + } + case .userCredential(let user, let password): + return (.useCredential, URLCredential(user: user, password: password, persistence: .forSession)) + } + + //return await queuedChallenge.dispositionAndCredential + +// // If we trusted a host, then add it to the list of trusted hosts. +// if let trust = challenge.protectionSpace.serverTrust, +// disposition == .useCredential, +// let credential = credential { +// if credential. +// if trustedHosts.contains(challenge.protectionSpace.host) { +// // Ryan - TODO: Show alert +// // This will cause a self-signed certificate to be trusted. +// return (.useCredential, URLCredential(trust: trust)) +// } else { +// return (.performDefaultHandling, nil) +// } +// } // switch challenge.protectionSpace.authenticationMethod { // case NSURLAuthenticationMethodServerTrust: diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift index 37b8af7df..da24cee02 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift @@ -32,16 +32,11 @@ final class TrustHostChallengeViewModel: TrustHostViewModel { } func allowConnection() { - challenge.resume( - with: ( - .useCredential, - URLCredential(trust: challenge.urlChallenge.protectionSpace.serverTrust!) - ) - ) + challenge.resume(with: .trustHost) } func cancel() { - challenge.resume(with: (.performDefaultHandling, nil)) + challenge.cancel() } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 71f447966..1c6fb88a7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -224,9 +224,7 @@ class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { func signIn() { isDismissed = true Task { - challenge.resume( - with: (.useCredential, URLCredential(user: username, password: password, persistence: .forSession)) - ) + challenge.resume(with: .userCredential(username: username, password: password)) } } From c5339855508e3b927f03bc00bb4bd859df5fefe8 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 11:13:48 -0600 Subject: [PATCH 0876/1722] trust host --- .../Examples/AuthenticationExampleView.swift | 5 ++- .../Authentication/UsernamePasswordView.swift | 42 +++++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 518d7c3b1..01857e095 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,11 +17,11 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( - //oAuthConfigurations: [.arcgisDotCom], - //trustedHosts: ["rt-server107a.esri.com", "dev0004327.esri.com"] + oAuthConfigurations: [.arcgisDotCom] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() + var logger = ConsoleNetworkLogger(shouldLogResponseData: true) var body: some View { VStack { @@ -45,6 +45,7 @@ struct AuthenticationExampleView: View { AuthenticationView(challenge: $0.queuedChallenge) }.onAppear { ArcGISURLSession.challengeHandler = authenticator + logger.startLogging() } // Save and restore the API Key. // Note: This is only necessary in this example. Other examples make use of the global diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 1c6fb88a7..fdbd01e04 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -18,7 +18,6 @@ import ArcGIS var username: String { get set } var password: String { get set } var signinButtonEnabled: Bool { get } - var isDismissed: Bool { get } var challengingHost: String { get } func signIn() @@ -135,23 +134,26 @@ class MockUsernamePasswordViewModel: UsernamePasswordViewModel { didSet { updateSigninButtonEnabled() } } @Published var signinButtonEnabled = false - @Published var isDismissed = false + var respondedToChallenge = false private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty + signinButtonEnabled = !respondedToChallenge && !username.isEmpty && !password.isEmpty } let challengingHost: String func signIn() { - isDismissed = true + respondedToChallenge = true + updateSigninButtonEnabled() } func cancel() { - isDismissed = true + respondedToChallenge = true + updateSigninButtonEnabled() } } +@MainActor class TokenCredentialViewModel: UsernamePasswordViewModel { private let challenge: QueuedArcGISChallenge @@ -166,10 +168,10 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { didSet { updateSigninButtonEnabled() } } @Published var signinButtonEnabled = false - @Published var isDismissed = false + var respondedToChallenge = false private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty + signinButtonEnabled = !respondedToChallenge && !username.isEmpty && !password.isEmpty } var challengingHost: String { @@ -177,8 +179,19 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { } func signIn() { - isDismissed = true + Task { + do { + let tokenCredential = try await ArcGISCredential.token( + challenge: challenge.arcGISChallenge, + username: username, + password: password + ) + print("-- tc: \(tokenCredential)") + } catch { + print("-- error: \(error)") + } + challenge.resume(with: await Result { .useCredential( try await .token( @@ -192,7 +205,8 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { } func cancel() { - isDismissed = true + respondedToChallenge = true + updateSigninButtonEnabled() challenge.cancel() } } @@ -211,10 +225,10 @@ class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { didSet { updateSigninButtonEnabled() } } @Published var signinButtonEnabled = false - @Published var isDismissed = false + var respondedToChallenge = false private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty + signinButtonEnabled = !respondedToChallenge && !username.isEmpty && !password.isEmpty } var challengingHost: String { @@ -222,14 +236,16 @@ class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { } func signIn() { - isDismissed = true + respondedToChallenge = true + updateSigninButtonEnabled() Task { challenge.resume(with: .userCredential(username: username, password: password)) } } func cancel() { - isDismissed = true + respondedToChallenge = true + updateSigninButtonEnabled() challenge.cancel() } } From 2680fbce6cccd7b90106a5eb326125b11b64cc0b Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 11:30:24 -0600 Subject: [PATCH 0877/1722] activity --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/Authenticator.swift | 43 +-------------- .../Authentication/UsernamePasswordView.swift | 54 ++++++++----------- 3 files changed, 25 insertions(+), 74 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 01857e095..f64341515 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( - oAuthConfigurations: [.arcgisDotCom] + //oAuthConfigurations: [.arcgisDotCom] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 0c50b6329..7040a36ee 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -172,6 +172,7 @@ extension Authenticator: AuthenticationChallengeHandler { _ challenge: URLAuthenticationChallenge, scope: URLAuthenticationChallengeScope ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + //return (.performDefaultHandling, nil) guard challenge.protectionSpace.authenticationMethod != NSURLAuthenticationMethodDefault else { return (.performDefaultHandling, nil) } @@ -203,47 +204,5 @@ extension Authenticator: AuthenticationChallengeHandler { case .userCredential(let user, let password): return (.useCredential, URLCredential(user: user, password: password, persistence: .forSession)) } - - //return await queuedChallenge.dispositionAndCredential - -// // If we trusted a host, then add it to the list of trusted hosts. -// if let trust = challenge.protectionSpace.serverTrust, -// disposition == .useCredential, -// let credential = credential { -// if credential. -// if trustedHosts.contains(challenge.protectionSpace.host) { -// // Ryan - TODO: Show alert -// // This will cause a self-signed certificate to be trusted. -// return (.useCredential, URLCredential(trust: trust)) -// } else { -// return (.performDefaultHandling, nil) -// } -// } - -// switch challenge.protectionSpace.authenticationMethod { -// case NSURLAuthenticationMethodServerTrust: -// if let trust = challenge.protectionSpace.serverTrust { -// if trustedHosts.contains(challenge.protectionSpace.host) { -// // Ryan - TODO: Show alert -// // This will cause a self-signed certificate to be trusted. -// return (.useCredential, URLCredential(trust: trust)) -// } else { -// return (.performDefaultHandling, nil) -// } -// } -// case NSURLAuthenticationMethodClientCertificate: -// // Show certificate picker? -// UIDocumentPickerViewController() -// fatalError() -// case NSURLAuthenticationMethodDefault: -// case NSURLAuthenticationMethodNTLM: -// case NSURLAuthenticationMethodHTMLForm: -// case NSURLAuthenticationMethodHTTPBasic: -// case NSURLAuthenticationMethodHTTPDigest: -// // Queue up the username and password challenge. -// let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) -// subject.send(queuedChallenge) -// return await queuedChallenge.dispositionAndCredential -// } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index fdbd01e04..8578970f8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -18,6 +18,7 @@ import ArcGIS var username: String { get set } var password: String { get set } var signinButtonEnabled: Bool { get } + var formEnabled: Bool { get } var challengingHost: String { get } func signIn() @@ -66,6 +67,7 @@ struct UsernamePasswordView: View { signinButton } } + .disabled(!viewModel.formEnabled) .navigationTitle("Sign In") .navigationBarTitleDisplayMode(.inline) .interactiveDismissDisabled() @@ -99,9 +101,15 @@ struct UsernamePasswordView: View { Button(action: { viewModel.signIn() }, label: { - Text("Sign In") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundColor(.white) + if viewModel.formEnabled { + Text("Sign In") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundColor(.white) + } else { + ProgressView() + .frame(maxWidth: .infinity, alignment: .center) + .tint(.white) + } }) .disabled(!viewModel.signinButtonEnabled) .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) @@ -134,22 +142,20 @@ class MockUsernamePasswordViewModel: UsernamePasswordViewModel { didSet { updateSigninButtonEnabled() } } @Published var signinButtonEnabled = false - var respondedToChallenge = false + @Published var formEnabled: Bool = true private func updateSigninButtonEnabled() { - signinButtonEnabled = !respondedToChallenge && !username.isEmpty && !password.isEmpty + signinButtonEnabled = !username.isEmpty && !password.isEmpty } let challengingHost: String func signIn() { - respondedToChallenge = true - updateSigninButtonEnabled() + formEnabled = false } func cancel() { - respondedToChallenge = true - updateSigninButtonEnabled() + formEnabled = false } } @@ -168,10 +174,10 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { didSet { updateSigninButtonEnabled() } } @Published var signinButtonEnabled = false - var respondedToChallenge = false + @Published var formEnabled: Bool = true private func updateSigninButtonEnabled() { - signinButtonEnabled = !respondedToChallenge && !username.isEmpty && !password.isEmpty + signinButtonEnabled = !username.isEmpty && !password.isEmpty } var challengingHost: String { @@ -179,19 +185,8 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { } func signIn() { - + formEnabled = false Task { - do { - let tokenCredential = try await ArcGISCredential.token( - challenge: challenge.arcGISChallenge, - username: username, - password: password - ) - print("-- tc: \(tokenCredential)") - } catch { - print("-- error: \(error)") - } - challenge.resume(with: await Result { .useCredential( try await .token( @@ -205,8 +200,7 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { } func cancel() { - respondedToChallenge = true - updateSigninButtonEnabled() + formEnabled = false challenge.cancel() } } @@ -225,10 +219,10 @@ class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { didSet { updateSigninButtonEnabled() } } @Published var signinButtonEnabled = false - var respondedToChallenge = false + @Published var formEnabled: Bool = true private func updateSigninButtonEnabled() { - signinButtonEnabled = !respondedToChallenge && !username.isEmpty && !password.isEmpty + signinButtonEnabled = !username.isEmpty && !password.isEmpty } var challengingHost: String { @@ -236,16 +230,14 @@ class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { } func signIn() { - respondedToChallenge = true - updateSigninButtonEnabled() + formEnabled = false Task { challenge.resume(with: .userCredential(username: username, password: password)) } } func cancel() { - respondedToChallenge = true - updateSigninButtonEnabled() + formEnabled = false challenge.cancel() } } From 2f07f8fffcc226669e9893cc984f37cc2fe19828 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 13:16:53 -0600 Subject: [PATCH 0878/1722] fix deadlock --- .../Authentication/Authenticator.swift | 45 +++++++++++-------- .../Authentication/UsernamePasswordView.swift | 10 +---- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 7040a36ee..2457eaa02 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -22,32 +22,36 @@ public final class QueuedArcGISChallenge: QueuedChallenge { self.arcGISChallenge = arcGISChallenge } - func resume(with result: Result) { - guard _result == nil else { return } - _result = result + func resume(with response: Response) { + guard _response == nil else { return } + _response = response } func cancel() { - guard _result == nil else { return } - _result = .failure(CancellationError()) + guard _response == nil else { return } + _response = .cancel } /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. @Streamed - private var _result: Result? + private var _response: Response? - var disposition: ArcGISAuthenticationChallenge.Disposition { - get async throws { - try await $_result + var response: Response { + get async { + await $_response .compactMap({ $0 }) .first(where: { _ in true })! - .get() } } public func complete() async { - _ = try? await disposition + _ = await response + } + + enum Response { + case tokenCredential(username: String, password: String) + case cancel } } @@ -115,11 +119,12 @@ public final class Authenticator: ObservableObject { let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { // For an OAuth challenge, we create the credential and resume. // Creating the OAuth credential will present the OAuth login view. - queuedArcGISChallenge.resume( - with: await Result { - .useCredential(try await .oauth(configuration: config)) - } - ) + fatalError() +// queuedArcGISChallenge.resume( +// with: await Result { +// .useCredential(try await .oauth(configuration: config)) +// } +// ) } else { // Set the current challenge, this should show the challenge view. currentChallenge = IdentifiableQueuedChallenge(queuedChallenge: queuedChallenge) @@ -165,14 +170,18 @@ extension Authenticator: AuthenticationChallengeHandler { subject.send(queuedChallenge) // Wait for it to complete and return the resulting disposition. - return try await queuedChallenge.disposition + switch await queuedChallenge.response { + case .tokenCredential(let username, let password): + return try await .useCredential(.token(challenge: challenge, username: username, password: password)) + case .cancel: + return .continueWithoutCredential + } } public func handleURLSessionChallenge( _ challenge: URLAuthenticationChallenge, scope: URLAuthenticationChallengeScope ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - //return (.performDefaultHandling, nil) guard challenge.protectionSpace.authenticationMethod != NSURLAuthenticationMethodDefault else { return (.performDefaultHandling, nil) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 8578970f8..a3d990078 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -187,15 +187,7 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { func signIn() { formEnabled = false Task { - challenge.resume(with: await Result { - .useCredential( - try await .token( - challenge: challenge.arcGISChallenge, - username: username, - password: password - ) - ) - }) + challenge.resume(with: .tokenCredential(username: username, password: password)) } } From b07d9d85b3a70cc92d3c410579fabdc0fb8a12f0 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 13:51:50 -0600 Subject: [PATCH 0879/1722] get oauth working again --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/Authenticator.swift | 30 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index f64341515..01857e095 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( - //oAuthConfigurations: [.arcgisDotCom] + oAuthConfigurations: [.arcgisDotCom] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 2457eaa02..ccd9ecba5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -51,6 +51,7 @@ public final class QueuedArcGISChallenge: QueuedChallenge { enum Response { case tokenCredential(username: String, password: String) + case oAuth(configuration: OAuthConfiguration) case cancel } } @@ -119,12 +120,7 @@ public final class Authenticator: ObservableObject { let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { // For an OAuth challenge, we create the credential and resume. // Creating the OAuth credential will present the OAuth login view. - fatalError() -// queuedArcGISChallenge.resume( -// with: await Result { -// .useCredential(try await .oauth(configuration: config)) -// } -// ) + queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) } else { // Set the current challenge, this should show the challenge view. currentChallenge = IdentifiableQueuedChallenge(queuedChallenge: queuedChallenge) @@ -173,6 +169,8 @@ extension Authenticator: AuthenticationChallengeHandler { switch await queuedChallenge.response { case .tokenCredential(let username, let password): return try await .useCredential(.token(challenge: challenge, username: username, password: password)) + case .oAuth(let configuration): + return try await .useCredential(.oauth(configuration: configuration)) case .cancel: return .continueWithoutCredential } @@ -190,16 +188,28 @@ extension Authenticator: AuthenticationChallengeHandler { return (.performDefaultHandling, nil) } - // If the host is already trusted, then continue trusting it. - if let trust = challenge.protectionSpace.serverTrust, - trustedHosts.contains(challenge.protectionSpace.host) { - return (.useCredential, URLCredential(trust: trust)) + // Check for server trust challenge. + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust { + if trustedHosts.contains(challenge.protectionSpace.host) { + // If the host is already trusted, then continue trusting it. + return (.useCredential, URLCredential(trust: trust)) + } else { + // See if the challenge is a recoverable trust failure, if so then we can + // challenge the user. If not, then we perform default handling. + var secResult = SecTrustResultType.invalid + SecTrustGetTrustResult(trust, &secResult) + if secResult != .recoverableTrustFailure { + return (.performDefaultHandling, nil) + } + } } // Queue up the url challenge. let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) subject.send(queuedChallenge) + // Respond accordingly. switch await queuedChallenge.response { case .cancel: return (.cancelAuthenticationChallenge, nil) From 8601be234751bdf08ff2e3b6df0517af59025c62 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 14:42:23 -0600 Subject: [PATCH 0880/1722] remove old code --- Examples/Examples/AuthenticationExampleView.swift | 9 --------- .../Components/Authentication/Authenticator.swift | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 01857e095..047535574 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -64,15 +64,6 @@ struct AuthenticationExampleView: View { ArcGISRuntimeEnvironment.apiKey = previousApiKey } } - - private func errorString(for error: Error) -> String { - switch error { - case is ArcGISAuthenticationChallenge.Error: - return "Authentication error" - default: - return error.localizedDescription - } - } } private struct AuthenticationItemView: View { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index ccd9ecba5..92938a8ce 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -172,7 +172,7 @@ extension Authenticator: AuthenticationChallengeHandler { case .oAuth(let configuration): return try await .useCredential(.oauth(configuration: configuration)) case .cancel: - return .continueWithoutCredential + return .cancelAuthenticationChallenge } } From 993d971f4e773b4bc778e13c47a049f6f82e3e3c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 16:29:15 -0600 Subject: [PATCH 0881/1722] split files --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/AuthenticationView.swift | 4 +- .../Authentication/Authenticator.swift | 94 --------------- .../Authentication/QueuedChallenge.swift | 109 ++++++++++++++++++ 4 files changed, 112 insertions(+), 97 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 047535574..b6e8f64c5 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -42,7 +42,7 @@ struct AuthenticationExampleView: View { } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .sheet(item: $authenticator.currentChallenge) { - AuthenticationView(challenge: $0.queuedChallenge) + AuthenticationView(challenge: $0) }.onAppear { ArcGISURLSession.challengeHandler = authenticator logger.startLogging() diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 0bb67c4b8..444d302e8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -14,8 +14,8 @@ import SwiftUI public struct AuthenticationView: View { - public init(challenge: QueuedChallenge) { - self.challenge = challenge + public init(challenge: IdentifiableQueuedChallenge) { + self.challenge = challenge.queuedChallenge } let challenge: QueuedChallenge diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 92938a8ce..88ea9844f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -15,92 +15,6 @@ import ArcGIS import SwiftUI import Combine -public final class QueuedArcGISChallenge: QueuedChallenge { - let arcGISChallenge: ArcGISAuthenticationChallenge - - init(arcGISChallenge: ArcGISAuthenticationChallenge) { - self.arcGISChallenge = arcGISChallenge - } - - func resume(with response: Response) { - guard _response == nil else { return } - _response = response - } - - func cancel() { - guard _response == nil else { return } - _response = .cancel - } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed - private var _response: Response? - - var response: Response { - get async { - await $_response - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await response - } - - enum Response { - case tokenCredential(username: String, password: String) - case oAuth(configuration: OAuthConfiguration) - case cancel - } -} - -public final class QueuedURLChallenge: QueuedChallenge { - let urlChallenge: URLAuthenticationChallenge - - init(urlChallenge: URLAuthenticationChallenge) { - self.urlChallenge = urlChallenge - } - - func resume(with response: Response) { - guard _response == nil else { return } - _response = response - } - - func cancel() { - guard _response == nil else { return } - _response = .cancel - } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed - private var _response: (Response)? - - var response: Response { - get async { - await $_response - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await response - } - - enum Response { - case userCredential(username: String, password: String) - case trustHost - case cancel - } -} - -public protocol QueuedChallenge: AnyObject { - func complete() async -} - @MainActor public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] @@ -145,14 +59,6 @@ public final class Authenticator: ObservableObject { } } -public struct IdentifiableQueuedChallenge { - public let queuedChallenge: QueuedChallenge -} - -extension IdentifiableQueuedChallenge: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(queuedChallenge) } -} - extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift new file mode 100644 index 000000000..d9bd4d253 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -0,0 +1,109 @@ +// 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 Foundation +import ArcGIS + +protocol QueuedChallenge: AnyObject { + func complete() async +} + +public final class QueuedArcGISChallenge: QueuedChallenge { + let arcGISChallenge: ArcGISAuthenticationChallenge + + init(arcGISChallenge: ArcGISAuthenticationChallenge) { + self.arcGISChallenge = arcGISChallenge + } + + func resume(with response: Response) { + guard _response == nil else { return } + _response = response + } + + func cancel() { + guard _response == nil else { return } + _response = .cancel + } + + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. + @Streamed + private var _response: Response? + + var response: Response { + get async { + await $_response + .compactMap({ $0 }) + .first(where: { _ in true })! + } + } + + public func complete() async { + _ = await response + } + + enum Response { + case tokenCredential(username: String, password: String) + case oAuth(configuration: OAuthConfiguration) + case cancel + } +} + +public final class QueuedURLChallenge: QueuedChallenge { + let urlChallenge: URLAuthenticationChallenge + + init(urlChallenge: URLAuthenticationChallenge) { + self.urlChallenge = urlChallenge + } + + func resume(with response: Response) { + guard _response == nil else { return } + _response = response + } + + func cancel() { + guard _response == nil else { return } + _response = .cancel + } + + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. + @Streamed + private var _response: (Response)? + + var response: Response { + get async { + await $_response + .compactMap({ $0 }) + .first(where: { _ in true })! + } + } + + public func complete() async { + _ = await response + } + + enum Response { + case userCredential(username: String, password: String) + case trustHost + case cancel + } +} + +public struct IdentifiableQueuedChallenge { + let queuedChallenge: QueuedChallenge +} + +extension IdentifiableQueuedChallenge: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(queuedChallenge) } +} From 038fb49e62574223073d0e5453df155a18fa2b57 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 16:39:48 -0600 Subject: [PATCH 0882/1722] recoverableTrustFailure --- .../Authentication/Authenticator.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 88ea9844f..020984af5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -100,14 +100,10 @@ extension Authenticator: AuthenticationChallengeHandler { if trustedHosts.contains(challenge.protectionSpace.host) { // If the host is already trusted, then continue trusting it. return (.useCredential, URLCredential(trust: trust)) - } else { + } else if !trust.isRecoverableTrustFailure { // See if the challenge is a recoverable trust failure, if so then we can // challenge the user. If not, then we perform default handling. - var secResult = SecTrustResultType.invalid - SecTrustGetTrustResult(trust, &secResult) - if secResult != .recoverableTrustFailure { - return (.performDefaultHandling, nil) - } + return (.performDefaultHandling, nil) } } @@ -131,3 +127,11 @@ extension Authenticator: AuthenticationChallengeHandler { } } } + +extension SecTrust { + var isRecoverableTrustFailure: Bool { + var result = SecTrustResultType.invalid + SecTrustGetTrustResult(self, &result) + return result == .recoverableTrustFailure + } +} From 26d597f4948b4982b883a73c2c7ec5f664702f38 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 17:23:00 -0600 Subject: [PATCH 0883/1722] signout --- .../Authentication/Authenticator.swift | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 020984af5..c9a23676a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -19,14 +19,49 @@ import Combine public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] var trustedHosts: [String] = [] + let hasPersistentStore: Bool public init( oAuthConfigurations: [OAuthConfiguration] = [] ) { self.oAuthConfigurations = oAuthConfigurations + hasPersistentStore = false Task { await observeChallengeQueue() } } + /// Foo... + /// - Parameters: + /// - oAuthConfigurations: Foo... + /// - access: When the item can be accessed. + /// - accessGroup: The access group that the item will be in. + /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. + public init( + oAuthConfigurations: [OAuthConfiguration] = [], + access: KeychainAccess, + accessGroup: String, + isSynchronizable: Bool + ) async throws { + ArcGISURLSession.credentialStore = try await .makePersistent( + access: .whenUnlockedThisDeviceOnly, + accessGroup: "", + isSynchronizable: false + ) + self.oAuthConfigurations = oAuthConfigurations + hasPersistentStore = true + } + + public func clearCredentialStores() async { + // Clear ArcGIS Credentials. + await ArcGISURLSession.credentialStore.removeAll() + + // Clear URLCredentials. + URLCredentialStorage.shared.removeAllCredentials() + + // We have to reset the sessions for URLCredential storage to respect the removed credentials + ArcGISURLSession.shared = ArcGISURLSession.makeDefaultSharedSession() + ArcGISURLSession.sharedBackground = ArcGISURLSession.makeDefaultSharedBackgroundSession() + } + private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, @@ -135,3 +170,23 @@ extension SecTrust { return result == .recoverableTrustFailure } } + +extension URLCredentialStorage { +// func removeCredentials(for host: String) { +// allCredentials.forEach { (protectionSpace: URLProtectionSpace, usernamesToCredentials: [String : URLCredential]) in +// guard protectionSpace.host.lowercased() == host.lowercased() else { +// return +// } +// for credential in usernamesToCredentials.values { +// remove(credential, for: protectionSpace) +// } +// } +// } + func removeAllCredentials() { + allCredentials.forEach { (protectionSpace: URLProtectionSpace, usernamesToCredentials: [String : URLCredential]) in + for credential in usernamesToCredentials.values { + remove(credential, for: protectionSpace) + } + } + } +} From 2661556048a907bc329ba0df99d98ddfe8acac34 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 17 May 2022 17:28:34 -0600 Subject: [PATCH 0884/1722] persistence --- .../Components/Authentication/Authenticator.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index c9a23676a..f8c670aa3 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -51,6 +51,9 @@ public final class Authenticator: ObservableObject { } public func clearCredentialStores() async { + // Clear trusted hosts + trustedHosts.removeAll() + // Clear ArcGIS Credentials. await ArcGISURLSession.credentialStore.removeAll() @@ -146,6 +149,8 @@ extension Authenticator: AuthenticationChallengeHandler { let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) subject.send(queuedChallenge) + let persistence: URLCredential.Persistence = hasPersistentStore ? .permanent : .forSession + // Respond accordingly. switch await queuedChallenge.response { case .cancel: @@ -158,7 +163,7 @@ extension Authenticator: AuthenticationChallengeHandler { return (.performDefaultHandling, nil) } case .userCredential(let user, let password): - return (.useCredential, URLCredential(user: user, password: password, persistence: .forSession)) + return (.useCredential, URLCredential(user: user, password: password, persistence: persistence)) } } } From 6a8b13f5c0c022000b5e6a42385d2f82d670bb39 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 17 May 2022 17:37:54 -0700 Subject: [PATCH 0885/1722] Modify viewpoint handling --- .../Components/FloorFilter/FloorFilter.swift | 13 +- .../FloorFilter/FloorFilterViewModel.swift | 22 ++-- .../FloorFilterViewModelTests.swift | 111 ++++-------------- 3 files changed, 42 insertions(+), 104 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 37456f7bf..20d4c4f47 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -32,8 +32,7 @@ public struct FloorFilter: View { ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( automaticSelectionMode: automaticSelectionMode, - floorManager: floorManager, - viewpoint: viewpoint + floorManager: floorManager )) self.alignment = alignment self.viewpoint = viewpoint @@ -97,8 +96,14 @@ public struct FloorFilter: View { SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) .esriBorder() .opacity(isSitesAndFacilitiesHidden ? .zero : 1) - .onChange(of: viewpoint.wrappedValue?.targetGeometry) { _ in - viewModel.viewpointSubject.send(viewpoint.wrappedValue) + .onChange(of: viewpoint.wrappedValue) { + viewModel.viewpoint = $0 + } + .onChange(of: viewModel.viewpoint) { newValue in + guard newValue != viewpoint.wrappedValue else { + return + } + viewpoint.wrappedValue = newValue } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 0e5e72ec9..50b004d11 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -35,12 +35,10 @@ final class FloorFilterViewModel: ObservableObject { /// - viewpoint: Viewpoint updated when the selected site or facility changes. init( automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, - floorManager: FloorManager, - viewpoint: Binding + floorManager: FloorManager ) { self.automaticSelectionMode = automaticSelectionMode self.floorManager = floorManager - self._viewpoint = viewpoint viewpointSubscription = viewpointSubject .debounce(for: delay, scheduler: DispatchQueue.main) @@ -76,6 +74,14 @@ final class FloorFilterViewModel: ObservableObject { /// The selected site, floor, or level. @Published private(set) var selection: Selection? + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// If `nil`, there will be no automatic pan/zoom operations. + @Published var viewpoint: Viewpoint? { + willSet { + viewpointSubject.send(newValue) + } + } + // MARK: Constants /// The selection behavior of the floor filter. @@ -151,13 +157,6 @@ final class FloorFilterViewModel: ObservableObject { .sorted(by: { $0.verticalOrder > $1.verticalOrder }) ?? [] } - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. - /// If `nil`, there will be no automatic pan/zoom operations. - @Binding var viewpoint: Viewpoint? - - /// A subject to which viewpoint updates can be submitted. - var viewpointSubject = PassthroughSubject() - // MARK: Public methods /// Updates the selected site, facility, and level based on a newly selected facility. @@ -304,6 +303,9 @@ final class FloorFilterViewModel: ObservableObject { } } + /// A subject to which viewpoint updates can be submitted. + private var viewpointSubject = PassthroughSubject() + /// A subscription to handle listening for viewpoint changes. private var viewpointSubscription: AnyCancellable? } diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 5084ba112..c8e578266 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -31,42 +31,10 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = FloorFilterViewModel( - floorManager: floorManager, - viewpoint: viewpoint - ) - let sites = viewModel.sites - let facilities = viewModel.facilities - let levels = viewModel.levels - XCTAssertFalse(sites.isEmpty) - XCTAssertFalse(facilities.isEmpty) - XCTAssertFalse(levels.isEmpty) - } - - /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager` and - /// `Binding?`.` - func testInitFloorFilterViewModelWithFloorManagerAndViewpoint() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } - try? await floorManager.load() - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = FloorFilterViewModel( - floorManager: floorManager, - viewpoint: viewpoint - ) - let sites = viewModel.sites - let facilities = viewModel.facilities - let levels = viewModel.levels - let vmViewpoint = viewModel.viewpoint - XCTAssertFalse(sites.isEmpty) - XCTAssertFalse(facilities.isEmpty) - XCTAssertFalse(levels.isEmpty) - XCTAssertNotNil(vmViewpoint) + let viewModel = FloorFilterViewModel(floorManager: floorManager) + XCTAssertFalse(viewModel.sites.isEmpty) + XCTAssertFalse(viewModel.facilities.isEmpty) + XCTAssertFalse(viewModel.levels.isEmpty) } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. @@ -76,13 +44,7 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - let initialViewpoint = getEsriRedlandsViewpoint(.zero) - var _viewpoint: Viewpoint? = initialViewpoint - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = FloorFilterViewModel( - floorManager: floorManager, - viewpoint: viewpoint - ) + let viewModel = FloorFilterViewModel(floorManager: floorManager) guard let site = viewModel.sites.first else { XCTFail() return @@ -95,7 +57,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedFacility) XCTAssertNil(selectedLevel) XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, + viewModel.viewpoint?.targetGeometry.extent.center.x, selectedSite?.geometry?.extent.center.x ) } @@ -107,13 +69,9 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - let initialViewpoint = getEsriRedlandsViewpoint(.zero) - var _viewpoint: Viewpoint? = initialViewpoint - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, - floorManager: floorManager, - viewpoint: viewpoint + floorManager: floorManager ) guard let facility = viewModel.facilities.first else { XCTFail() @@ -126,7 +84,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(selectedFacility, facility) XCTAssertEqual(selectedLevel, defaultLevel) XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, + viewModel.viewpoint?.targetGeometry.extent.center.x, selectedFacility?.geometry?.extent.center.x ) } @@ -138,13 +96,7 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - let initialViewpoint = getEsriRedlandsViewpoint(.zero) - var _viewpoint: Viewpoint? = initialViewpoint - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = FloorFilterViewModel( - floorManager: floorManager, - viewpoint: viewpoint - ) + let viewModel = FloorFilterViewModel(floorManager: floorManager) let levels = viewModel.levels guard let level = levels.first else { XCTFail("A first level does not exist") @@ -153,10 +105,6 @@ class FloorFilterViewModelTests: XCTestCase { viewModel.setLevel(level) let selectedLevel = viewModel.selectedLevel XCTAssertEqual(selectedLevel, level) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - initialViewpoint.targetGeometry.extent.center.x - ) levels.forEach { level in if level.verticalOrder == selectedLevel?.verticalOrder { XCTAssertTrue(level.isVisible) @@ -181,12 +129,9 @@ class FloorFilterViewModelTests: XCTestCase { ), scale: 10_000 ) - var _viewpoint: Viewpoint? = viewpointLosAngeles - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .always, - floorManager: floorManager, - viewpoint: viewpoint + floorManager: floorManager ) // Viewpoint is Los Angeles, selection should be nil @@ -196,7 +141,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedSite) // Viewpoint is Redlands Main Q - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + viewModel.viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -204,7 +149,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(selectedFacility?.name, "Q") // Viewpoint is Los Angeles, selection should be nil - _viewpoint = viewpointLosAngeles + viewModel.viewpoint = viewpointLosAngeles viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -227,16 +172,13 @@ class FloorFilterViewModelTests: XCTestCase { ), scale: 10_000 ) - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .alwaysNotClearing, - floorManager: floorManager, - viewpoint: viewpoint + floorManager: floorManager ) // Viewpoint is Redlands Main Q - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + viewModel.viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.automaticallySelectFacilityOrSite() var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite @@ -244,7 +186,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(selectedFacility?.name, "Q") // Viewpoint is Los Angeles, but selection should remain Redlands Main Q - _viewpoint = viewpointLosAngeles + viewModel.viewpoint = viewpointLosAngeles viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -259,20 +201,9 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - let viewpointLosAngeles = Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) - var _viewpoint: Viewpoint? = viewpointLosAngeles - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, - floorManager: floorManager, - viewpoint: viewpoint + floorManager: floorManager ) // Viewpoint is Los Angeles, selection should be nil @@ -282,7 +213,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedSite) // Viewpoint is Redlands Main Q but selection should still be nil - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + viewModel.viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -294,16 +225,16 @@ class FloorFilterViewModelTests: XCTestCase { /// - Returns: A map constructed from an ArcGIS portal item. private func makeMap() async -> Map? { // Multiple sites/facilities: Esri IST map with all buildings. -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) + // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) // Redlands Campus map with multiple sites and facilities. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) // Single site (ESRI Redlands Main) and facility (Building L). -// let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) -// let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) + // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) let map = Map(item: portalItem) do { From 8974e031be8ad44faadd2a589e6c9141a7de02ff Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:17:23 -0500 Subject: [PATCH 0886/1722] An alternative viewpoint implementation. --- .../Components/FloorFilter/FloorFilter.swift | 11 ++--- .../FloorFilter/FloorFilterViewModel.swift | 43 +++++++------------ 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 20d4c4f47..483ca7056 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -32,7 +32,8 @@ public struct FloorFilter: View { ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( automaticSelectionMode: automaticSelectionMode, - floorManager: floorManager + floorManager: floorManager, + viewpoint: viewpoint )) self.alignment = alignment self.viewpoint = viewpoint @@ -97,13 +98,7 @@ public struct FloorFilter: View { .esriBorder() .opacity(isSitesAndFacilitiesHidden ? .zero : 1) .onChange(of: viewpoint.wrappedValue) { - viewModel.viewpoint = $0 - } - .onChange(of: viewModel.viewpoint) { newValue in - guard newValue != viewpoint.wrappedValue else { - return - } - viewpoint.wrappedValue = newValue + viewModel.onViewpointChanged($0) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 50b004d11..bedabfb5b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -35,21 +35,12 @@ final class FloorFilterViewModel: ObservableObject { /// - viewpoint: Viewpoint updated when the selected site or facility changes. init( automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, - floorManager: FloorManager + floorManager: FloorManager, + viewpoint: Binding ) { self.automaticSelectionMode = automaticSelectionMode self.floorManager = floorManager - - viewpointSubscription = viewpointSubject - .debounce(for: delay, scheduler: DispatchQueue.main) - .sink(receiveValue: { [weak self] _ in - guard let self = self, - let viewpoint = self.viewpoint, - !viewpoint.targetScale.isZero else { - return - } - self.automaticallySelectFacilityOrSite() - }) + self.viewpoint = viewpoint Task { do { @@ -76,10 +67,14 @@ final class FloorFilterViewModel: ObservableObject { /// The `Viewpoint` used to pan/zoom to the selected site/facilty. /// If `nil`, there will be no automatic pan/zoom operations. - @Published var viewpoint: Viewpoint? { - willSet { - viewpointSubject.send(newValue) - } + private var viewpoint: Binding + + func onViewpointChanged(_ viewpoint: Viewpoint?) { + guard let viewpoint = viewpoint, + !viewpoint.targetScale.isZero else { + return + } + automaticallySelectFacilityOrSite() } // MARK: Constants @@ -227,13 +222,13 @@ final class FloorFilterViewModel: ObservableObject { facilityMinScale = 1500 } - if viewpoint?.targetScale ?? .zero > facilityMinScale { + if viewpoint.wrappedValue?.targetScale ?? .zero > facilityMinScale { return false } // If the centerpoint is within a facilities' geometry, select that site. let facilityResult = floorManager.facilities.first { facility in - guard let extent1 = viewpoint?.targetGeometry.extent, + guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, let extent2 = facility.geometry?.extent else { return false } @@ -262,7 +257,7 @@ final class FloorFilterViewModel: ObservableObject { } // If viewpoint is out of range, reset selection and return early. - if viewpoint?.targetScale ?? .zero > siteMinScale { + if viewpoint.wrappedValue?.targetScale ?? .zero > siteMinScale { if automaticSelectionMode == .always { selection = nil } @@ -271,7 +266,7 @@ final class FloorFilterViewModel: ObservableObject { // If the centerpoint is within a site's geometry, select that site. let siteResult = floorManager.sites.first { site in - guard let extent1 = viewpoint?.targetGeometry.extent, + guard let extent1 = viewpoint.wrappedValue?.targetGeometry.extent, let extent2 = site.geometry?.extent else { return false } @@ -297,17 +292,11 @@ final class FloorFilterViewModel: ObservableObject { builder.expand(factor: 1.5) let targetExtent = builder.toGeometry() if !targetExtent.isEmpty { - viewpoint = Viewpoint( + viewpoint.wrappedValue = Viewpoint( targetExtent: targetExtent ) } } - - /// A subject to which viewpoint updates can be submitted. - private var viewpointSubject = PassthroughSubject() - - /// A subscription to handle listening for viewpoint changes. - private var viewpointSubscription: AnyCancellable? } extension FloorFilterViewModel.Selection: Hashable { } From e1e71d747923deff0d17feb0e2ce82e888cb47b4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:28:48 -0500 Subject: [PATCH 0887/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index bb1a1c649..0fe0f86e7 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -36,7 +36,7 @@ struct SearchExampleView: View { ) /// Denotes whether the geoview is navigating. Used for the repeat search behavior. - @State private var isGeoViewNavigating: Bool = false + @State private var isGeoViewNavigating = false /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. @State private var geoViewExtent: Envelope? = nil From 72595a959cfece2171976b5cfc42172e13ce8d03 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:29:22 -0500 Subject: [PATCH 0888/1722] Update Examples/Examples/SearchExampleView.swift You know, back in the typewriter days, two spaces was the norm.... ;-) Co-authored-by: Zachary A Kline --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 0fe0f86e7..038327f57 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -35,7 +35,7 @@ struct SearchExampleView: View { scale: 1000000 ) - /// Denotes whether the geoview is navigating. Used for the repeat search behavior. + /// Denotes whether the geoview is navigating. Used for the repeat search behavior. @State private var isGeoViewNavigating = false /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. From 82cd916a4329c0bfa7b314aa026ec047e1c99165 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:29:30 -0500 Subject: [PATCH 0889/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 038327f57..ed981e363 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -38,7 +38,7 @@ struct SearchExampleView: View { /// Denotes whether the geoview is navigating. Used for the repeat search behavior. @State private var isGeoViewNavigating = false - /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. + /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. @State private var geoViewExtent: Envelope? = nil /// The search area to be used for the current query From 0a54f5de39b8efc4e2c0bb366eba7a430025e450 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:29:40 -0500 Subject: [PATCH 0890/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a6d274669..a03e04e66 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -30,10 +30,10 @@ public struct SearchView: View { viewpoint: viewpoint )) - _queryArea = Binding.constant(nil) - _queryCenter = Binding.constant(nil) - _geoViewExtent = Binding.constant(nil) - _isGeoViewNavigating = Binding.constant(false) + _queryArea = .constant(nil) + _queryCenter = .constant(nil) + _geoViewExtent = .constant(nil) + _isGeoViewNavigating = .constant(false) } /// The view model used by the view. The `SearchViewModel` manages state and handles the From 5dbcc688f693a7d1634860e572bf2d0e0827179d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:30:14 -0500 Subject: [PATCH 0891/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index a03e04e66..3dbe78ee8 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -181,7 +181,7 @@ public struct SearchView: View { .onChange(of: queryArea) { _ in viewModel.queryArea = queryArea } - .onAppear() { + .onAppear { viewModel.currentQuery = currentQuery viewModel.resultsOverlay = resultsOverlay viewModel.resultMode = resultMode From 64027b1a191e2da54c15abf146f8c2648b3cbb03 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:30:21 -0500 Subject: [PATCH 0892/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 3dbe78ee8..444099938 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -269,7 +269,7 @@ extension SearchView { return copy } - /// The current map/scene view extent. Defaults to `nil`. Used to allow repeat searches after + /// The current map/scene view extent. Defaults to `nil`. Used to allow repeat searches after /// panning/zooming the map. Set to `nil` if repeat search behavior is not wanted. /// - Parameter newGeoViewExtent: The new value. /// - Returns: The `SearchView`. From 90d52df66622b8aea7837dbfbd60a18b64dbc3f4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:30:27 -0500 Subject: [PATCH 0893/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 444099938..e9e9ca781 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -270,7 +270,7 @@ extension SearchView { } /// The current map/scene view extent. Defaults to `nil`. Used to allow repeat searches after - /// panning/zooming the map. Set to `nil` if repeat search behavior is not wanted. + /// panning/zooming the map. Set to `nil` if repeat search behavior is not wanted. /// - Parameter newGeoViewExtent: The new value. /// - Returns: The `SearchView`. public func geoViewExtent(_ newGeoViewExtent: Binding) -> Self { From af4c7c13b5123d5cdfe08da70ee6eb8cd9fe553c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:30:37 -0500 Subject: [PATCH 0894/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index e9e9ca781..fb0f08308 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -279,7 +279,7 @@ extension SearchView { return copy } - /// Denotes whether the geoview is navigating. Used for the repeat search behavior. + /// Denotes whether the `GeoView` is navigating. Used for the repeat search behavior. /// - Parameter newIsGeoViewNavigating: The new value. /// - Returns: The `SearchView`. public func isGeoViewNavigating(_ newIsGeoViewNavigating: Binding) -> Self { From 446e26854591bb5defa4878aabac2a0d440caf65 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:30:54 -0500 Subject: [PATCH 0895/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 41988f44c..779d928a4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -147,7 +147,7 @@ final class SearchViewModel: ObservableObject { /// Defines the center for the search. For most use cases, this should be updated by the view /// every time the user navigates the map. - var queryCenter: Point? = nil + var queryCenter: Point? /// Defines how many results to return. Defaults to ``SearchResultMode/automatic``. /// In automatic mode, an appropriate number of results is returned based on the type of suggestion From 68b12f868f2ea5df6fa097457c3b223c7739ab84 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:31:24 -0500 Subject: [PATCH 0896/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index ed981e363..e442568a7 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -72,7 +72,7 @@ struct SearchExampleView: View { viewpoint: $searchResultViewpoint ) .resultsOverlay(searchResultsOverlay) -// .queryArea(queryArea) +// .queryArea($queryArea) .queryCenter($queryCenter) .geoViewExtent($geoViewExtent) .isGeoViewNavigating($isGeoViewNavigating) From 8c4b198241652edc850e7b1134877556f1b89ae3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:31:37 -0500 Subject: [PATCH 0897/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: David Feinzimer --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index fb0f08308..532440ebd 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -125,8 +125,8 @@ public struct SearchView: View { isResultsButtonHidden: !enableResultListView, isResultListHidden: $isResultListHidden ) - .onSubmit { viewModel.commitSearch() } - .submitLabel(.search) + .onSubmit { viewModel.commitSearch() } + .submitLabel(.search) if enableResultListView, !isResultListHidden, let searchOutcome = viewModel.searchOutcome { From b2af96ac87899a1226c087c4e3de9283155a1f23 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:31:45 -0500 Subject: [PATCH 0898/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: David Feinzimer --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 532440ebd..1822067be 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -138,7 +138,7 @@ public struct SearchView: View { selectedResult: $viewModel.selectedResult, noResultsMessage: noResultsMessage ) - .frame(height: useHalfHeightResults ? geometry.size.height / 2 : nil) + .frame(height: useHalfHeightResults ? geometry.size.height / 2 : nil) case .suggestions(let suggestions): SearchSuggestionList( suggestionResults: suggestions, From 0f365218e1937f1bbe18e6253ead0631c1f5de88 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:32:14 -0500 Subject: [PATCH 0899/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: David Feinzimer --- Examples/Examples/SearchExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index e442568a7..f4d1c1b3e 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -66,7 +66,7 @@ struct SearchExampleView: View { // using the `queryArea` modifier on the `SearchView`. // queryArea = newValue } - .overlay(alignment: .topTrailing) { + .overlay { SearchView( sources: [locatorDataSource], viewpoint: $searchResultViewpoint From d68bf23daef2ac87150afd48e184008bc9c2e3b7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 18 May 2022 14:44:45 -0500 Subject: [PATCH 0900/1722] PR review changes. --- Examples/Examples/SearchExampleView.swift | 52 +++++++++---------- .../Components/Search/SearchView.swift | 21 ++++---- .../Components/Search/SearchViewModel.swift | 12 ++--- 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index f4d1c1b3e..5237a8d14 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -46,37 +46,37 @@ struct SearchExampleView: View { /// Defines the center for the search. @State private var queryCenter: Point? = nil -  + var body: some View { MapView( map: 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 - ) - .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 + ) + .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 1822067be..cd438baf1 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -47,9 +47,8 @@ public struct SearchView: View { /// Tracks the current user-entered query. This property drives both suggestions and searches. var resultMode: SearchResultMode = .automatic - /// The search area to be used for the current query. Defaults to `nil`. - /// - /// If `nil`, then there is no limiting of the search results to a given area. + /// The search area to be used for the current query. If `nil`, then there is no limiting of the + /// search results to a given area. @Binding var queryArea: Geometry? /// Defines the center for the search. Defaults to `nil`. @@ -95,7 +94,7 @@ public struct SearchView: View { /// Message to show when there are no results or suggestions. Defaults to "No results found". /// Note: this is set using the `noResultsMessage` modifier. private var noResultsMessage = "No results found" - + /// The width of the search bar, taking into account the horizontal and vertical size classes /// of the device. This will cause the search field to display full-width on an iPhone in portrait /// orientation (and certain iPad multitasking configurations) and limit the width to `360` in other cases. @@ -112,7 +111,7 @@ public struct SearchView: View { /// Determines whether the results lists are displayed. @State private var isResultListHidden: Bool = false - + public var body: some View { VStack { GeometryReader { geometry in @@ -223,7 +222,7 @@ extension SearchView { copy.noResultsMessage = newNoResultsMessage return copy } - + /// Sets the current query. /// - Parameter newQueryString: The new value. /// - Returns: The `SearchView`. @@ -241,7 +240,7 @@ extension SearchView { copy.resultsOverlay = newResultsOverlay return copy } - + /// Defines how many results to return. /// - Parameter newResultMode: The new value. /// - Returns: The `SearchView`. @@ -250,7 +249,7 @@ extension SearchView { copy.resultMode = newResultMode return copy } - + /// The search area to be used for the current query. /// - Parameter newQueryArea: The new value. /// - Returns: The `SearchView`. @@ -259,7 +258,7 @@ extension SearchView { copy._queryArea = newQueryArea return copy } - + /// Defines the center for the search. /// - Parameter newQueryCenter: The new value. /// - Returns: The `SearchView`. @@ -268,7 +267,7 @@ extension SearchView { copy._queryCenter = newQueryCenter return copy } - + /// The current map/scene view extent. Defaults to `nil`. Used to allow repeat searches after /// panning/zooming the map. Set to `nil` if repeat search behavior is not wanted. /// - Parameter newGeoViewExtent: The new value. @@ -278,7 +277,7 @@ extension SearchView { copy._geoViewExtent = newGeoViewExtent return copy } - + /// Denotes whether the `GeoView` is navigating. Used for the repeat search behavior. /// - Parameter newIsGeoViewNavigating: The new value. /// - Returns: The `SearchView`. diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 779d928a4..34e03f840 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -38,8 +38,7 @@ public enum SearchOutcome { } /// Performs searches and manages search state for a search, or optionally without a UI connection. -@MainActor -final class SearchViewModel: ObservableObject { +@MainActor final class SearchViewModel: ObservableObject { /// Creates a `SearchViewModel`. /// - Parameters: /// - sources: Collection of search sources to be used. @@ -57,8 +56,7 @@ final class SearchViewModel: ObservableObject { private var activeSource: SearchSource? = nil /// Tracks the current user-entered query. This property drives both suggestions and searches. - @Published - var currentQuery: String = "" { + @Published var currentQuery: String = "" { willSet { isEligibleForRequery = false @@ -138,8 +136,7 @@ final class SearchViewModel: ObservableObject { /// Changes to this property are driven by changes to the `geoViewExtent` property. This value will be /// `true` if the extent center changes by more than 25% of the average of the extent's height and width /// at the time of the last search or if the extent width/height changes by the same amount. - @Published - private(set) var isEligibleForRequery: Bool = false + @Published private(set) var isEligibleForRequery: Bool = false /// The search area to be used for the current query. Results will be limited to those. /// within `QueryArea`. Defaults to `nil`. @@ -156,8 +153,7 @@ final class SearchViewModel: ObservableObject { /// A search outcome that contains the search and suggestion results. A `nil` value means no /// query has been made. - @Published - private(set) var searchOutcome: SearchOutcome? { + @Published private(set) var searchOutcome: SearchOutcome? { didSet { if case let .results(results) = searchOutcome { display(searchResults: results) From 15e2c35988e2941ba5c7193fa16705a502224230 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 19 May 2022 10:58:32 -0500 Subject: [PATCH 0901/1722] Update Examples/Examples/SearchExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/SearchExampleView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/SearchExampleView.swift b/Examples/Examples/SearchExampleView.swift index 5237a8d14..afe2dfe6b 100644 --- a/Examples/Examples/SearchExampleView.swift +++ b/Examples/Examples/SearchExampleView.swift @@ -39,13 +39,13 @@ struct SearchExampleView: View { @State private var isGeoViewNavigating = false /// The current map/scene view extent. Used to allow repeat searches after panning/zooming the map. - @State private var geoViewExtent: Envelope? = nil + @State private var geoViewExtent: Envelope? - /// The search area to be used for the current query - @State private var queryArea: Geometry? = nil + /// The search area to be used for the current query. + @State private var queryArea: Geometry? /// Defines the center for the search. - @State private var queryCenter: Point? = nil + @State private var queryCenter: Point? var body: some View { MapView( From f31d0e08ac5fdd7cd056a2c9c8b515ee0cfa442f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 19 May 2022 10:58:51 -0500 Subject: [PATCH 0902/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 34e03f840..326d3fa52 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -56,7 +56,7 @@ public enum SearchOutcome { private var activeSource: SearchSource? = nil /// Tracks the current user-entered query. This property drives both suggestions and searches. - @Published var currentQuery: String = "" { + @Published var currentQuery = "" { willSet { isEligibleForRequery = false From 7c0b984a5b18d15ebe055bb3b0920ef58ba57d33 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 19 May 2022 10:59:00 -0500 Subject: [PATCH 0903/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 326d3fa52..af0994437 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -136,7 +136,7 @@ public enum SearchOutcome { /// Changes to this property are driven by changes to the `geoViewExtent` property. This value will be /// `true` if the extent center changes by more than 25% of the average of the extent's height and width /// at the time of the last search or if the extent width/height changes by the same amount. - @Published private(set) var isEligibleForRequery: Bool = false + @Published private(set) var isEligibleForRequery = false /// The search area to be used for the current query. Results will be limited to those. /// within `QueryArea`. Defaults to `nil`. From cd35cc647f41204575961a168123ff0fa34786a5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 19 May 2022 11:08:25 -0500 Subject: [PATCH 0904/1722] Fixed a couple other unnecessary type specifications. --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 4 ++-- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index cd438baf1..5140ee834 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -82,7 +82,7 @@ public struct SearchView: View { /// The string shown in the search view when no user query is entered. /// Defaults to "Find a place or address". Note: this is set using the /// `prompt` modifier. - private var prompt: String = "Find a place or address" + private var prompt = "Find a place or address" /// Determines whether a built-in result view will be shown. Defaults to `true`. /// If `false`, the result display/selection list is not shown. Set to false if you want to hide the results @@ -110,7 +110,7 @@ public struct SearchView: View { } /// Determines whether the results lists are displayed. - @State private var isResultListHidden: Bool = false + @State private var isResultListHidden = false public var body: some View { VStack { diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index af0994437..7fe4bd425 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -117,7 +117,7 @@ public enum SearchOutcome { } /// `true` when the geoView is navigating, `false` otherwise. Set by the external client. - var isGeoViewNavigating: Bool = false + var isGeoViewNavigating = false /// The `Viewpoint` used to pan/zoom to results. If `nil`, there will be no zooming to results. var viewpoint: Binding? = nil From defd8bbf43d64cc290548ffa4a4947bea96e1e4c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 19 May 2022 16:26:41 -0700 Subject: [PATCH 0905/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 87 ++++++++++++++----- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index c8e578266..e9970d7af 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -31,7 +31,15 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - let viewModel = FloorFilterViewModel(floorManager: floorManager) + + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) + XCTAssertFalse(viewModel.sites.isEmpty) XCTAssertFalse(viewModel.facilities.isEmpty) XCTAssertFalse(viewModel.levels.isEmpty) @@ -44,7 +52,14 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - let viewModel = FloorFilterViewModel(floorManager: floorManager) + + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) guard let site = viewModel.sites.first else { XCTFail() return @@ -57,21 +72,24 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedFacility) XCTAssertNil(selectedLevel) XCTAssertEqual( - viewModel.viewpoint?.targetGeometry.extent.center.x, + _viewpoint?.targetGeometry.extent.center.x, selectedSite?.geometry?.extent.center.x ) } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testSetFacility() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return } try? await floorManager.load() + let initialViewpoint = getEsriRedlandsViewpoint(.zero) + var _viewpoint: Viewpoint? = initialViewpoint + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, - floorManager: floorManager + floorManager: floorManager, + viewpoint: viewpoint ) guard let facility = viewModel.facilities.first else { XCTFail() @@ -84,7 +102,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(selectedFacility, facility) XCTAssertEqual(selectedLevel, defaultLevel) XCTAssertEqual( - viewModel.viewpoint?.targetGeometry.extent.center.x, + _viewpoint?.targetGeometry.extent.center.x, selectedFacility?.geometry?.extent.center.x ) } @@ -96,7 +114,13 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() - let viewModel = FloorFilterViewModel(floorManager: floorManager) + let initialViewpoint = getEsriRedlandsViewpoint(.zero) + var _viewpoint: Viewpoint? = initialViewpoint + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: viewpoint + ) let levels = viewModel.levels guard let level = levels.first else { XCTFail("A first level does not exist") @@ -105,6 +129,10 @@ class FloorFilterViewModelTests: XCTestCase { viewModel.setLevel(level) let selectedLevel = viewModel.selectedLevel XCTAssertEqual(selectedLevel, level) + XCTAssertEqual( + _viewpoint?.targetGeometry.extent.center.x, + initialViewpoint.targetGeometry.extent.center.x + ) levels.forEach { level in if level.verticalOrder == selectedLevel?.verticalOrder { XCTAssertTrue(level.isVisible) @@ -129,9 +157,12 @@ class FloorFilterViewModelTests: XCTestCase { ), scale: 10_000 ) + var _viewpoint: Viewpoint? = viewpointLosAngeles + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .always, - floorManager: floorManager + floorManager: floorManager, + viewpoint: viewpoint ) // Viewpoint is Los Angeles, selection should be nil @@ -141,7 +172,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedSite) // Viewpoint is Redlands Main Q - viewModel.viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -149,7 +180,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(selectedFacility?.name, "Q") // Viewpoint is Los Angeles, selection should be nil - viewModel.viewpoint = viewpointLosAngeles + _viewpoint = viewpointLosAngeles viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -172,13 +203,16 @@ class FloorFilterViewModelTests: XCTestCase { ), scale: 10_000 ) + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .alwaysNotClearing, - floorManager: floorManager + floorManager: floorManager, + viewpoint: viewpoint ) // Viewpoint is Redlands Main Q - viewModel.viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.automaticallySelectFacilityOrSite() var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite @@ -186,7 +220,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(selectedFacility?.name, "Q") // Viewpoint is Los Angeles, but selection should remain Redlands Main Q - viewModel.viewpoint = viewpointLosAngeles + _viewpoint = viewpointLosAngeles viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -201,19 +235,30 @@ class FloorFilterViewModelTests: XCTestCase { return } try? await floorManager.load() + let viewpointLosAngeles = Viewpoint( + center: Point( + x: -13164116.3284, + y: 4034465.8065, + spatialReference: .webMercator + ), + scale: 10_000 + ) + var _viewpoint: Viewpoint? = viewpointLosAngeles + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, - floorManager: floorManager + floorManager: floorManager, + viewpoint: viewpoint ) - + // Viewpoint is Los Angeles, selection should be nil var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - + // Viewpoint is Redlands Main Q but selection should still be nil - viewModel.viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -225,16 +270,16 @@ class FloorFilterViewModelTests: XCTestCase { /// - Returns: A map constructed from an ArcGIS portal item. private func makeMap() async -> Map? { // Multiple sites/facilities: Esri IST map with all buildings. - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) + // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) // Redlands Campus map with multiple sites and facilities. let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) // Single site (ESRI Redlands Main) and facility (Building L). - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) + // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) + // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) let map = Map(item: portalItem) do { From ad8e688e17772b45d6a68e8ace93c937105b359c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 19 May 2022 16:33:28 -0700 Subject: [PATCH 0906/1722] Update FloorFilterViewModelTests.swift --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index e9970d7af..459d3f717 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -250,13 +250,13 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) - + // Viewpoint is Los Angeles, selection should be nil var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - + // Viewpoint is Redlands Main Q but selection should still be nil _viewpoint = getEsriRedlandsViewpoint(scale: 1000) viewModel.automaticallySelectFacilityOrSite() From d5829fb115e3004d3e2754a52294c5ed6b2b0012 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 19 May 2022 16:53:35 -0700 Subject: [PATCH 0907/1722] Improve tests --- .../FloorFilterViewModelTests.swift | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 459d3f717..eb6b0d9d2 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -30,11 +30,9 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint @@ -51,15 +49,14 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let viewModel = FloorFilterViewModel( floorManager: floorManager, viewpoint: viewpoint ) + guard let site = viewModel.sites.first else { XCTFail() return @@ -82,15 +79,15 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let initialViewpoint = getEsriRedlandsViewpoint(.zero) - var _viewpoint: Viewpoint? = initialViewpoint + + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, floorManager: floorManager, viewpoint: viewpoint ) + guard let facility = viewModel.facilities.first else { XCTFail() return @@ -113,7 +110,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() + let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -121,6 +118,7 @@ class FloorFilterViewModelTests: XCTestCase { floorManager: floorManager, viewpoint: viewpoint ) + let levels = viewModel.levels guard let level = levels.first else { XCTFail("A first level does not exist") @@ -148,7 +146,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() + let viewpointLosAngeles = Viewpoint( center: Point( x: -13164116.3284, @@ -194,15 +192,7 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let viewpointLosAngeles = Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) + var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( @@ -220,7 +210,7 @@ class FloorFilterViewModelTests: XCTestCase { XCTAssertEqual(selectedFacility?.name, "Q") // Viewpoint is Los Angeles, but selection should remain Redlands Main Q - _viewpoint = viewpointLosAngeles + _viewpoint = .losAngeles viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -234,16 +224,8 @@ class FloorFilterViewModelTests: XCTestCase { let floorManager = map.floorManager else { return } - try? await floorManager.load() - let viewpointLosAngeles = Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) - var _viewpoint: Viewpoint? = viewpointLosAngeles + + var _viewpoint: Viewpoint? = .losAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, @@ -306,7 +288,23 @@ extension FloorFilterViewModelTests { /// Builds viewpoints to use for tests. /// - Parameter rotation: The rotation to use for the resulting viewpoint. /// - Returns: A viewpoint object for tests. - func getEsriRedlandsViewpoint(_ rotation: Double = .zero, scale: Double = 10_000) -> Viewpoint { + func getEsriRedlandsViewpoint( + _ rotation: Double = .zero, + scale: Double = 10_000 + ) -> Viewpoint { return Viewpoint(center: point, scale: scale, rotation: rotation) } } + +private extension Viewpoint { + static var losAngeles: Viewpoint { + Viewpoint( + center: Point( + x: -13164116.3284, + y: 4034465.8065, + spatialReference: .webMercator + ), + scale: 10_000 + ) + } +} From fd8818bc3147ff77df419390edf554a11022f2c4 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 19 May 2022 21:25:12 -0600 Subject: [PATCH 0908/1722] sync to keychain with func, not init --- .../Examples/AuthenticationExampleView.swift | 2 -- .../Authentication/Authenticator.swift | 17 +++++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index b6e8f64c5..c93db8d08 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -21,7 +21,6 @@ struct AuthenticationExampleView: View { ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() - var logger = ConsoleNetworkLogger(shouldLogResponseData: true) var body: some View { VStack { @@ -45,7 +44,6 @@ struct AuthenticationExampleView: View { AuthenticationView(challenge: $0) }.onAppear { ArcGISURLSession.challengeHandler = authenticator - logger.startLogging() } // Save and restore the API Key. // Note: This is only necessary in this example. Other examples make use of the global diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index f8c670aa3..86599440f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -19,7 +19,7 @@ import Combine public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] var trustedHosts: [String] = [] - let hasPersistentStore: Bool + var hasPersistentStore: Bool public init( oAuthConfigurations: [OAuthConfiguration] = [] @@ -31,22 +31,19 @@ public final class Authenticator: ObservableObject { /// Foo... /// - Parameters: - /// - oAuthConfigurations: Foo... /// - access: When the item can be accessed. /// - accessGroup: The access group that the item will be in. /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. - public init( - oAuthConfigurations: [OAuthConfiguration] = [], + public func synchronizeWithKeychain( access: KeychainAccess, - accessGroup: String, - isSynchronizable: Bool + accessGroup: String? = nil, + isSynchronizable: Bool = false ) async throws { ArcGISURLSession.credentialStore = try await .makePersistent( - access: .whenUnlockedThisDeviceOnly, - accessGroup: "", - isSynchronizable: false + access: access, + accessGroup: accessGroup, + isSynchronizable: isSynchronizable ) - self.oAuthConfigurations = oAuthConfigurations hasPersistentStore = true } From 3f2f885abcb2988a59c8c4fbb368db5509e81ec1 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 08:44:28 -0600 Subject: [PATCH 0909/1722] doc --- .../Authentication/Authenticator.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 86599440f..46aadb4ee 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -29,20 +29,24 @@ public final class Authenticator: ObservableObject { Task { await observeChallengeQueue() } } - /// Foo... + /// Sets up a credential store that is synchronized with the keychain. + /// - Remark: If no group is specified then the item will be stored in the default group. + /// To know more about what the default group would be you can find information about that 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 item can be accessed. - /// - accessGroup: The access group that the item will be in. + /// - access: When the credentials stored in the keychain can be accessed. + /// - groupIdentifier: The identifier of the group that credentials should be persisted to + /// within the keychain. /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. public func synchronizeWithKeychain( access: KeychainAccess, - accessGroup: String? = nil, - isSynchronizable: Bool = false + groupIdentifier: String? = nil, + isCloudSynchronizable: Bool = false ) async throws { ArcGISURLSession.credentialStore = try await .makePersistent( access: access, - accessGroup: accessGroup, - isSynchronizable: isSynchronizable + groupIdentifier: groupIdentifier, + isSynchronizable: isCloudSynchronizable ) hasPersistentStore = true } From 78b43ded3ff517b66fefbb4f7b6936127622e590 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 08:46:57 -0600 Subject: [PATCH 0910/1722] add pki map --- Examples/Examples/AuthenticationExampleView.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index c93db8d08..0dc3acd77 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -142,6 +142,7 @@ extension AuthenticationItem { loadables: [ArcGISTiledLayer(url: .worldImageryMapServer)] ) } + static func makeMultipleToken() -> AuthenticationItem { AuthenticationItem( title: "Multiple token secured resources", @@ -151,6 +152,7 @@ extension AuthenticationItem { ] ) } + static func makeMultipleTokenSame() -> AuthenticationItem { AuthenticationItem( title: "Two of same token secured resources", @@ -169,6 +171,7 @@ extension AuthenticationItem { ] ) } + static func makeIWAPortal() -> AuthenticationItem { AuthenticationItem( title: "IWA Portal", @@ -176,13 +179,21 @@ extension AuthenticationItem { ) } + static func makePKIMap() -> AuthenticationItem { + AuthenticationItem( + title: "PKI Map", + loadables: [Map(url: URL(string: "https://dev0002028.esri.com/portal/home/item.html?id=7fd418d5de2e4752b616a6463318cc4e")!)!] + ) + } + static func makeAll() -> [AuthenticationItem] { [ makeToken(), makeMultipleToken(), makeMultipleTokenSame(), makePortal(), - makeIWAPortal() + makeIWAPortal(), + makePKIMap() ] } } From 770b84425ecd181b4462700a07fdbdd3b35bde56 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 10:56:56 -0600 Subject: [PATCH 0911/1722] pki --- .../Authentication/AuthenticationView.swift | 3 +- .../Authentication/Authenticator.swift | 72 ++++++++++++++++--- .../CertificatePickerView.swift | 59 +++++++++++++++ .../Authentication/DocumentPickerView.swift | 26 +++++++ .../Authentication/QueuedChallenge.swift | 1 + 5 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift create mode 100644 Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 444d302e8..f6c42490c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -38,8 +38,7 @@ public struct AuthenticationView: View { case NSURLAuthenticationMethodServerTrust: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) case NSURLAuthenticationMethodClientCertificate: - // TODO: Show certificate picker - fatalError() + CertificatePickerView() case NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodHTMLForm, diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 46aadb4ee..1502a4aa5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -165,6 +165,15 @@ extension Authenticator: AuthenticationChallengeHandler { } case .userCredential(let user, let password): return (.useCredential, URLCredential(user: user, password: password, persistence: persistence)) + case .certificate(let url, let password): + do { + return ( + .useCredential, + try URLCredential.urlCredentialForCertificate(at: url, password: password, persistence: persistence) + ) + } catch { + return (.performDefaultHandling, nil) + } } } } @@ -178,16 +187,6 @@ extension SecTrust { } extension URLCredentialStorage { -// func removeCredentials(for host: String) { -// allCredentials.forEach { (protectionSpace: URLProtectionSpace, usernamesToCredentials: [String : URLCredential]) in -// guard protectionSpace.host.lowercased() == host.lowercased() else { -// return -// } -// for credential in usernamesToCredentials.values { -// remove(credential, for: protectionSpace) -// } -// } -// } func removeAllCredentials() { allCredentials.forEach { (protectionSpace: URLProtectionSpace, usernamesToCredentials: [String : URLCredential]) in for credential in usernamesToCredentials.values { @@ -196,3 +195,56 @@ extension URLCredentialStorage { } } } + +private extension URLCredential { + /// An error that can occur when importing a certificate. + struct CertificateImportError: Error, Hashable { + /// The backing status code for this error. + let status: OSStatus + + /// Initializes a certificate import error. This init will fail if the specified status is a success + /// status value. + /// - Parameter status: An `OSStatus`, usually the return value of a keychain operation. + init?(status: OSStatus) { + guard status != errSecSuccess else { return nil } + self.status = status + } + } + + static func urlCredentialForCertificate( + at fileURL: URL, + password: String, + persistence: URLCredential.Persistence + ) throws -> URLCredential { + let data = try Data(contentsOf: fileURL) + let options = [kSecImportExportPassphrase: password] + var rawItems: CFArray? + + let status = SecPKCS12Import( + data as CFData, + options as CFDictionary, + &rawItems + ) + + guard status == errSecSuccess else { + throw CertificateImportError(status: status)! + } + + let items = rawItems! as! [[CFString: Any]] + let identity = items[0][kSecImportItemIdentity] as! SecIdentity + let certificates = items[0][kSecImportItemCertChain] as! [SecTrust] + + return URLCredential( + identity: identity, + certificates: certificates, + persistence: persistence + ) + } +} + +extension URLCredential.CertificateImportError { + // The message for this error. + var message: String { + (SecCopyErrorMessageString(status, nil) as String?) ?? "" + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift new file mode 100644 index 000000000..4d812dcb6 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -0,0 +1,59 @@ +// 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 UniformTypeIdentifiers + +@MainActor protocol CertificatePickerViewModelProtocol: ObservableObject { + var challengingHost: String { get } + + var certificateURL: URL? { get set } + + func signIn() + func cancel() +} + +final class CertificatePickerViewModel: CertificatePickerViewModelProtocol { + let challengingHost: String + let challenge: QueuedURLChallenge + + @Published var certificateURL: URL? + @Published var password: String = "" + + init(challenge: QueuedURLChallenge) { + self.challenge = challenge + challengingHost = challenge.urlChallenge.protectionSpace.host + } + + func signIn() { + guard let certificateURL = certificateURL else { + preconditionFailure() + } + + challenge.resume(with: .certificate(url: certificateURL, passsword: password)) + } + + func cancel() { + challenge.cancel() + } +} + +struct CertificatePickerView: View { + var body: some View { + VStack { + Text("Choose a certificate for host") + DocumentPickerView(contentTypes: [.image]) + } + .edgesIgnoringSafeArea(.bottom) + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift new file mode 100644 index 000000000..cadfc91b0 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift @@ -0,0 +1,26 @@ +// 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 UniformTypeIdentifiers + +struct DocumentPickerView: UIViewControllerRepresentable { + var contentTypes: [UTType] + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes) + return documentPicker + } + + func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index d9bd4d253..a110acf99 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -95,6 +95,7 @@ public final class QueuedURLChallenge: QueuedChallenge { enum Response { case userCredential(username: String, password: String) + case certificate(url: URL, passsword: String) case trustHost case cancel } From 331eec23179596d554cd4227edbe030a0700fa4c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 20 May 2022 12:34:30 -0700 Subject: [PATCH 0912/1722] Apply `@StateObject` to sample maps and scenes --- Examples/Examples/BasemapGalleryExampleView.swift | 4 ++-- Examples/Examples/BookmarksExampleView.swift | 2 +- Examples/Examples/CompassExampleView.swift | 9 ++++----- Examples/Examples/FloatingPanelExampleView.swift | 2 +- Examples/Examples/FloorFilterExampleView.swift | 2 +- Examples/Examples/OverviewMapExampleView.swift | 4 ++-- Examples/Examples/ScalebarExampleView.swift | 2 +- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 2e90979df..9460cfed5 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct BasemapGalleryExampleView: View { /// The map displayed in the map view. - let map: Map + @StateObject private var map: Map /// A Boolean value indicating whether to show the basemap gallery. @State private var showBasemapGallery: Bool = false @@ -32,7 +32,7 @@ struct BasemapGalleryExampleView: View { private let basemaps = initialBasemaps() init() { - self.map = Map(basemapStyle: .arcGISImagery) + _map = StateObject(wrappedValue: Map(basemapStyle: .arcGISImagery)) } var body: some View { diff --git a/Examples/Examples/BookmarksExampleView.swift b/Examples/Examples/BookmarksExampleView.swift index f089eef65..79650bc6f 100644 --- a/Examples/Examples/BookmarksExampleView.swift +++ b/Examples/Examples/BookmarksExampleView.swift @@ -23,7 +23,7 @@ struct BookmarksExampleView: View { private var verticalSizeClass: UserInterfaceSizeClass? /// A web map with predefined bookmarks. - private let map = Map(url: URL(string: "https://www.arcgis.com/home/item.html?id=16f1b8ba37b44dc3884afc8d5f454dd2")!)! + @StateObject 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 diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index f22a1b3ce..34d2a722c 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -17,16 +17,15 @@ import SwiftUI struct CompassExampleView: View { /// The map displayed in the map view. - private let map = Map(basemapStyle: .arcGISImagery) - + @StateObject private var map = Map(basemapStyle: .arcGISImagery) + /// Allows for communication between the Compass and MapView or SceneView. - @State - private var viewpoint: Viewpoint? = Viewpoint( + @State private var viewpoint: Viewpoint? = Viewpoint( center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), scale: 10_000, rotation: -45 ) - + var body: some View { MapView(map: map, viewpoint: viewpoint) .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 3de93e6f4..59f1ec186 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -16,7 +16,7 @@ import ArcGISToolkit import ArcGIS struct FloatingPanelExampleView: View { - let map = Map(basemapStyle: .arcGISImagery) + @StateObject private var map = Map(basemapStyle: .arcGISImagery) private let initialViewpoint = Viewpoint( center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 249a9a356..e5829381f 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -46,7 +46,7 @@ struct FloorFilterExampleView: View { scale: 100_000 ) - private var map = makeMap() + @StateObject private var map = makeMap() var body: some View { MapView( diff --git a/Examples/Examples/OverviewMapExampleView.swift b/Examples/Examples/OverviewMapExampleView.swift index 72871cc0b..6c0844c91 100644 --- a/Examples/Examples/OverviewMapExampleView.swift +++ b/Examples/Examples/OverviewMapExampleView.swift @@ -47,7 +47,7 @@ struct OverviewMapExampleView: View { } struct OverviewMapForMapView: View { - let map = Map(basemapStyle: .arcGISImagery) + @StateObject private var map = Map(basemapStyle: .arcGISImagery) @State private var viewpoint: Viewpoint? @@ -75,7 +75,7 @@ struct OverviewMapForMapView: View { } struct OverviewMapForSceneView: View { - let scene = Scene(basemapStyle: .arcGISImagery) + @StateObject private var scene = Scene(basemapStyle: .arcGISImagery) @State private var viewpoint: Viewpoint? diff --git a/Examples/Examples/ScalebarExampleView.swift b/Examples/Examples/ScalebarExampleView.swift index 4f074ae9d..67dd87a86 100644 --- a/Examples/Examples/ScalebarExampleView.swift +++ b/Examples/Examples/ScalebarExampleView.swift @@ -29,7 +29,7 @@ struct ScalebarExampleView: View { private let alignment: Alignment = .bottomLeading /// The `Map` displayed in the `MapView`. - private let map = Map(basemapStyle: .arcGISTopographic) + @StateObject private var map = Map(basemapStyle: .arcGISTopographic) /// The maximum screen width allotted to the scalebar. private let maxWidth: Double = 175.0 From 5d0370496036e4eccdee3c0a8452e573f476837c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 20 May 2022 14:31:18 -0700 Subject: [PATCH 0913/1722] Update BasemapGalleryExampleView.swift --- Examples/Examples/BasemapGalleryExampleView.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Examples/Examples/BasemapGalleryExampleView.swift b/Examples/Examples/BasemapGalleryExampleView.swift index 9460cfed5..ec1599907 100644 --- a/Examples/Examples/BasemapGalleryExampleView.swift +++ b/Examples/Examples/BasemapGalleryExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct BasemapGalleryExampleView: View { /// The map displayed in the map view. - @StateObject private var map: Map + @StateObject private var map = Map(basemapStyle: .arcGISImagery) /// A Boolean value indicating whether to show the basemap gallery. @State private var showBasemapGallery: Bool = false @@ -31,10 +31,6 @@ struct BasemapGalleryExampleView: View { /// The initial list of basemaps. private let basemaps = initialBasemaps() - init() { - _map = StateObject(wrappedValue: Map(basemapStyle: .arcGISImagery)) - } - var body: some View { MapView(map: map, viewpoint: initialViewpoint) .overlay(alignment: .topTrailing) { From 456c82194b121afd7e60791b9745b06f189bc9a7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 16:19:35 -0600 Subject: [PATCH 0914/1722] pki --- .../Authentication/AuthenticationView.swift | 2 +- .../CertificatePickerView.swift | 150 ++++++++++++++++-- .../Authentication/DocumentPickerView.swift | 27 ++++ 3 files changed, 165 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index f6c42490c..ab4da08a1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -38,7 +38,7 @@ public struct AuthenticationView: View { case NSURLAuthenticationMethodServerTrust: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) case NSURLAuthenticationMethodClientCertificate: - CertificatePickerView() + CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) case NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodHTMLForm, diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index 4d812dcb6..a185b409e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -14,16 +14,7 @@ import SwiftUI import UniformTypeIdentifiers -@MainActor protocol CertificatePickerViewModelProtocol: ObservableObject { - var challengingHost: String { get } - - var certificateURL: URL? { get set } - - func signIn() - func cancel() -} - -final class CertificatePickerViewModel: CertificatePickerViewModelProtocol { +@MainActor final class CertificatePickerViewModel: ObservableObject { let challengingHost: String let challenge: QueuedURLChallenge @@ -49,11 +40,144 @@ final class CertificatePickerViewModel: CertificatePickerViewModelProtocol { } struct CertificatePickerView: View { + enum Step { + case browsePrompt + case documentPicker + case enterPassword + } + + @ObservedObject var viewModel: CertificatePickerViewModel + @State var step: Step = .browsePrompt + + @State var showPicker: Bool = false + var body: some View { - VStack { - Text("Choose a certificate for host") - DocumentPickerView(contentTypes: [.image]) + Group { + switch step { + case .browsePrompt: + PromptBrowseCertificateView(host: viewModel.challengingHost) { + viewModel.cancel() + } onContinue: { + step = .documentPicker + showPicker = true + } + case .documentPicker: + Text("picker") +// DocumentPickerView(contentTypes: [.pfx]) { +// print("-- here!!! \($0)") +// viewModel.certificateURL = $0 +// step = .enterYPassword +// } onCancel: { +// viewModel.cancel() +// } + case .enterPassword: + EnterPasswordView(password: $viewModel.password) { + viewModel.cancel() + } onContinue: { + viewModel.signIn() + } + } + } + .sheet(isPresented: $showPicker) { + DocumentPickerView(contentTypes: [.pfx]) { + viewModel.certificateURL = $0 + step = .enterPassword + } onCancel: { + viewModel.cancel() + } } .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() + } +} + +private extension UTType { + static let pfx = UTType(filenameExtension: "pfx")! +} + +struct PromptBrowseCertificateView: View { + var host: String + var onCancel: () -> Void + var onContinue: () -> Void + + var body: some View { + VStack { + Text("A certificate is required to access content on \(host)") + .font(.body) + .padding([.bottom]) + .multilineTextAlignment(.center) + + Button(role: .cancel) { + onCancel() + } label: { + Text("Cancel") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.large) + + Button { + onContinue() + } label: { + Text("Browse for a certificate") + .frame(maxWidth: .infinity) + + } + .buttonStyle(.bordered) + .controlSize(.large) + + Spacer() + } + .padding() + .navigationTitle("Certificate Required") + } +} + +struct EnterPasswordView: View { + @Binding var password: String + var onCancel: () -> Void + var onContinue: () -> Void + + + var body: some View { + VStack { + Text("Please enter a password for the chosen certificate.") + .font(.body) + .padding([.bottom]) + .multilineTextAlignment(.center) + + SecureField(text: $password, prompt: Text("Password")) { + Text("label") + } + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + + HStack { + Button { + onContinue() + } label: { + Text("OK") + .frame(maxWidth: .infinity) + + } + .buttonStyle(.bordered) + .controlSize(.large) + + Button(role: .cancel) { + onCancel() + } label: { + Text("Cancel") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.large) + } + + + Spacer() + } + .padding() + .navigationTitle("Certificate Required") } } + diff --git a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift index cadfc91b0..fb4e4ef2a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift @@ -16,11 +16,38 @@ import UniformTypeIdentifiers struct DocumentPickerView: UIViewControllerRepresentable { var contentTypes: [UTType] + var onPickDocument: (URL) -> Void + var onCancel: () -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(onPickDocument: onPickDocument, onCancel: onCancel) + } func makeUIViewController(context: Context) -> UIDocumentPickerViewController { let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes) + documentPicker.delegate = context.coordinator return documentPicker } func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} } + +extension DocumentPickerView { + final class Coordinator: NSObject, UIDocumentPickerDelegate { + var onPickDocument: (URL) -> Void + var onCancel: () -> Void + + init(onPickDocument: @escaping (URL) -> Void, onCancel: @escaping () -> Void) { + self.onPickDocument = onPickDocument + self.onCancel = onCancel + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + onCancel() + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { + onPickDocument(url) + } + } +} From 4fe7a58db674d0bb2b1698d31c6afc135478b0e1 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 18:12:20 -0600 Subject: [PATCH 0915/1722] work on alerts --- .../Examples/AuthenticationExampleView.swift | 10 ++- .../Authentication/AuthenticationView.swift | 74 +++++++++--------- .../Authentication/Authenticator.swift | 75 ++++++++++++++++++- .../Authentication/QueuedChallenge.swift | 11 +-- .../Authentication/TrustHostView.swift | 4 +- .../Authentication/UsernamePasswordView.swift | 4 +- 6 files changed, 123 insertions(+), 55 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 0dc3acd77..52d7d7f48 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -40,9 +40,13 @@ struct AuthenticationExampleView: View { } } .navigationBarTitle(Text("Authentication"), displayMode: .inline) - .sheet(item: $authenticator.currentChallenge) { - AuthenticationView(challenge: $0) - }.onAppear { + .sheet(isPresented: $authenticator.showSheet) { + authenticator.currentView + } + .alert("foo", isPresented: $authenticator.showAlert) { + authenticator.currentView + } + .onAppear { ArcGISURLSession.challengeHandler = authenticator } // Save and restore the API Key. diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index ab4da08a1..267da8114 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -13,40 +13,40 @@ import SwiftUI -public struct AuthenticationView: View { - public init(challenge: IdentifiableQueuedChallenge) { - self.challenge = challenge.queuedChallenge - } - - let challenge: QueuedChallenge - - public var body: some View { - switch challenge { - case let challenge as QueuedArcGISChallenge: - UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) - case let challenge as QueuedURLChallenge: - view(forURLChallenge: challenge) - default: - fatalError() - } - } - - @MainActor - @ViewBuilder - func view(forURLChallenge challenge: QueuedURLChallenge) -> some View { - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: - TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) - case NSURLAuthenticationMethodClientCertificate: - CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: - UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) - default: - fatalError() - } - } -} +//public struct AuthenticationView: View { +// public init(challenge: QueuedChallenge) { +// self.challenge = challenge.queuedChallenge +// } +// +// let challenge: QueuedChallenge +// +// public var body: some View { +// switch challenge { +// case let challenge as QueuedArcGISChallenge: +// UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) +// case let challenge as QueuedURLChallenge: +// view(forURLChallenge: challenge) +// default: +// fatalError() +// } +// } +// +// @MainActor +// @ViewBuilder +// func view(forURLChallenge challenge: QueuedURLChallenge) -> some View { +// switch challenge.urlChallenge.protectionSpace.authenticationMethod { +// case NSURLAuthenticationMethodServerTrust: +// TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) +// case NSURLAuthenticationMethodClientCertificate: +// CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) +// case NSURLAuthenticationMethodDefault, +// NSURLAuthenticationMethodNTLM, +// NSURLAuthenticationMethodHTMLForm, +// NSURLAuthenticationMethodHTTPBasic, +// NSURLAuthenticationMethodHTTPDigest: +// UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) +// default: +// fatalError() +// } +// } +//} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 1502a4aa5..334d9909b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -75,8 +75,12 @@ public final class Authenticator: ObservableObject { // Creating the OAuth credential will present the OAuth login view. queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) } else { - // Set the current challenge, this should show the challenge view. - currentChallenge = IdentifiableQueuedChallenge(queuedChallenge: queuedChallenge) + // Set the current challenge + currentChallenge = queuedChallenge + + // Show the challenge view + showChallengeView(makeView(for: queuedChallenge)) + // Wait for the queued challenge to finish. await queuedChallenge.complete() // Set the current challenge to `nil`, this should dismiss the challenge view. @@ -85,8 +89,7 @@ public final class Authenticator: ObservableObject { } } - @Published - public var currentChallenge: IdentifiableQueuedChallenge? + var currentChallenge: QueuedChallenge? private var subject = PassthroughSubject() private var challengeQueue: AsyncPublisher> { @@ -96,6 +99,70 @@ public final class Authenticator: ObservableObject { .eraseToAnyPublisher() ) } + + @Published + public var showSheet: Bool = false + + @Published + public var showAlert: Bool = false + + public var currentView: AnyView = AnyView(EmptyView()) + + func showChallengeView(_ content: Content) { + showSheet = false + showAlert = false + + currentView = AnyView(content) + guard let content = content as? ChallengeView else { + preconditionFailure() + } + + switch content.style { + case .alert: + showAlert = true + case .sheet: + showSheet = true + } + } + + @ViewBuilder + func makeView(for challenge: QueuedChallenge) -> some View { + switch challenge { + case let challenge as QueuedArcGISChallenge: + UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) + case let challenge as QueuedURLChallenge: + makeView(forURLChallenge: challenge) + default: + fatalError() + } + } + + @ViewBuilder + func makeView(forURLChallenge challenge: QueuedURLChallenge) -> some View { + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) + case NSURLAuthenticationMethodClientCertificate: + return CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: + UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + default: + fatalError() + } + } +} + +protocol ChallengeView { + var style: ChallengeViewStyle { get } +} + +enum ChallengeViewStyle { + case sheet + case alert } extension Authenticator: AuthenticationChallengeHandler { diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index a110acf99..94f75119a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -14,10 +14,11 @@ import Foundation import ArcGIS -protocol QueuedChallenge: AnyObject { +protocol QueuedChallenge { func complete() async } +// TODO: Ryan - Can these be structs? public final class QueuedArcGISChallenge: QueuedChallenge { let arcGISChallenge: ArcGISAuthenticationChallenge @@ -100,11 +101,3 @@ public final class QueuedURLChallenge: QueuedChallenge { case cancel } } - -public struct IdentifiableQueuedChallenge { - let queuedChallenge: QueuedChallenge -} - -extension IdentifiableQueuedChallenge: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(queuedChallenge) } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift index da24cee02..e01f33e34 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift @@ -49,11 +49,13 @@ final class MockTrustHostViewModel: TrustHostViewModel { func cancel() {} } -struct TrustHostView: View { +struct TrustHostView: ChallengeView, View { init(viewModel: ViewModel) { self.viewModel = viewModel } + var style: ChallengeViewStyle { .alert } + @ObservedObject private var viewModel: ViewModel var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index a3d990078..c050575bf 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -25,11 +25,13 @@ import ArcGIS func cancel() } -struct UsernamePasswordView: View { +struct UsernamePasswordView: ChallengeView, View { init(viewModel: ViewModel) { self.viewModel = viewModel } + var style: ChallengeViewStyle { .sheet } + @ObservedObject private var viewModel: ViewModel /// The focused field. From f3b701ff0940d7227539c1fc36beb4b3223c0c28 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 18:21:23 -0600 Subject: [PATCH 0916/1722] kinda working --- .../Authentication/Authenticator.swift | 52 ++++++++++++++----- .../Authentication/QueuedChallenge.swift | 4 +- .../Authentication/TrustHostView.swift | 4 +- .../Authentication/UsernamePasswordView.swift | 4 +- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 334d9909b..aad83a4cb 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -83,8 +83,12 @@ public final class Authenticator: ObservableObject { // Wait for the queued challenge to finish. await queuedChallenge.complete() - // Set the current challenge to `nil`, this should dismiss the challenge view. + + // Set the current challenge to `nil`. currentChallenge = nil + + // Hide the view. + hideChallengeView() } } } @@ -108,10 +112,12 @@ public final class Authenticator: ObservableObject { public var currentView: AnyView = AnyView(EmptyView()) - func showChallengeView(_ content: Content) { + func hideChallengeView() { showSheet = false showAlert = false - + } + + func showChallengeView(_ content: Content) { currentView = AnyView(content) guard let content = content as? ChallengeView else { preconditionFailure() @@ -125,39 +131,57 @@ public final class Authenticator: ObservableObject { } } - @ViewBuilder - func makeView(for challenge: QueuedChallenge) -> some View { + func makeView(for challenge: QueuedChallenge) -> ChallengeView { switch challenge { case let challenge as QueuedArcGISChallenge: - UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) + return ChallengeView( + style: .sheet, + content: AnyView(UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge))) + ) case let challenge as QueuedURLChallenge: - makeView(forURLChallenge: challenge) + return makeView(forURLChallenge: challenge) default: fatalError() } } - @ViewBuilder - func makeView(forURLChallenge challenge: QueuedURLChallenge) -> some View { + func makeView(forURLChallenge challenge: QueuedURLChallenge) -> ChallengeView { switch challenge.urlChallenge.protectionSpace.authenticationMethod { case NSURLAuthenticationMethodServerTrust: - TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) + return ChallengeView( + style: .alert, + content: AnyView( + TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) + ) + ) case NSURLAuthenticationMethodClientCertificate: - return CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) + return ChallengeView( + style: .alert, + content: AnyView( + CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) + ) + ) case NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodHTMLForm, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest: - UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + return ChallengeView( + style: .sheet, + content: AnyView( + UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + ) + ) default: fatalError() } } } -protocol ChallengeView { - var style: ChallengeViewStyle { get } +struct ChallengeView: View { + var style: ChallengeViewStyle + var content: AnyView + var body: some View { content } } enum ChallengeViewStyle { diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 94f75119a..ada34a91a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -19,7 +19,7 @@ protocol QueuedChallenge { } // TODO: Ryan - Can these be structs? -public final class QueuedArcGISChallenge: QueuedChallenge { +final class QueuedArcGISChallenge: QueuedChallenge { let arcGISChallenge: ArcGISAuthenticationChallenge init(arcGISChallenge: ArcGISAuthenticationChallenge) { @@ -60,7 +60,7 @@ public final class QueuedArcGISChallenge: QueuedChallenge { } } -public final class QueuedURLChallenge: QueuedChallenge { +final class QueuedURLChallenge: QueuedChallenge { let urlChallenge: URLAuthenticationChallenge init(urlChallenge: URLAuthenticationChallenge) { diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift index e01f33e34..da24cee02 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift @@ -49,13 +49,11 @@ final class MockTrustHostViewModel: TrustHostViewModel { func cancel() {} } -struct TrustHostView: ChallengeView, View { +struct TrustHostView: View { init(viewModel: ViewModel) { self.viewModel = viewModel } - var style: ChallengeViewStyle { .alert } - @ObservedObject private var viewModel: ViewModel var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index c050575bf..a3d990078 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -25,13 +25,11 @@ import ArcGIS func cancel() } -struct UsernamePasswordView: ChallengeView, View { +struct UsernamePasswordView: View { init(viewModel: ViewModel) { self.viewModel = viewModel } - var style: ChallengeViewStyle { .sheet } - @ObservedObject private var viewModel: ViewModel /// The focused field. From 36eae18ff1c36461982065c0f8f477611cfc2b9c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 18:37:07 -0600 Subject: [PATCH 0917/1722] updates --- .../Examples/AuthenticationExampleView.swift | 4 +- .../Authentication/Authenticator.swift | 95 ++++++++----------- 2 files changed, 39 insertions(+), 60 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 52d7d7f48..54583e97a 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -40,10 +40,10 @@ struct AuthenticationExampleView: View { } } .navigationBarTitle(Text("Authentication"), displayMode: .inline) - .sheet(isPresented: $authenticator.showSheet) { + .sheet(isPresented: $authenticator.isSheetPresented) { authenticator.currentView } - .alert("foo", isPresented: $authenticator.showAlert) { + .alert("foo", isPresented: $authenticator.isAlertPresented) { authenticator.currentView } .onAppear { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index aad83a4cb..f17241dfd 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -75,25 +75,17 @@ public final class Authenticator: ObservableObject { // Creating the OAuth credential will present the OAuth login view. queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) } else { - // Set the current challenge - currentChallenge = queuedChallenge - - // Show the challenge view - showChallengeView(makeView(for: queuedChallenge)) + // Present the challenge view + presentView(for: queuedChallenge) // Wait for the queued challenge to finish. await queuedChallenge.complete() - // Set the current challenge to `nil`. - currentChallenge = nil - - // Hide the view. - hideChallengeView() + // Dismiss the view. + dismissView() } } } - - var currentChallenge: QueuedChallenge? private var subject = PassthroughSubject() private var challengeQueue: AsyncPublisher> { @@ -105,73 +97,60 @@ public final class Authenticator: ObservableObject { } @Published - public var showSheet: Bool = false + public var isSheetPresented: Bool = false @Published - public var showAlert: Bool = false + public var isAlertPresented: Bool = false public var currentView: AnyView = AnyView(EmptyView()) - func hideChallengeView() { - showSheet = false - showAlert = false + func dismissView() { + isSheetPresented = false + isAlertPresented = false } - func showChallengeView(_ content: Content) { + func presentView(with style: ChallengeViewStyle, content: Content) { currentView = AnyView(content) - guard let content = content as? ChallengeView else { - preconditionFailure() - } - switch content.style { + switch style { case .alert: - showAlert = true + isAlertPresented = true case .sheet: - showSheet = true + isSheetPresented = true } } - func makeView(for challenge: QueuedChallenge) -> ChallengeView { + func presentView(for challenge: QueuedChallenge) { switch challenge { case let challenge as QueuedArcGISChallenge: - return ChallengeView( - style: .sheet, - content: AnyView(UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge))) + presentView( + with: .sheet, + content: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) ) case let challenge as QueuedURLChallenge: - return makeView(forURLChallenge: challenge) - default: - fatalError() - } - } - - func makeView(forURLChallenge challenge: QueuedURLChallenge) -> ChallengeView { - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: - return ChallengeView( - style: .alert, - content: AnyView( - TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + presentView( + with: .alert, + content: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) ) - ) - case NSURLAuthenticationMethodClientCertificate: - return ChallengeView( - style: .alert, - content: AnyView( - CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) + case NSURLAuthenticationMethodClientCertificate: + presentView( + with: .alert, + content: CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) ) - ) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: - return ChallengeView( - style: .sheet, - content: AnyView( - UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: + presentView( + with: .sheet, + content: UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) ) - ) + default: + fatalError() + } default: fatalError() } From b4275dac5164ec9e5b77e5e4d445f8dc44b0882a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 20:32:23 -0600 Subject: [PATCH 0918/1722] present sheet --- .../Examples/AuthenticationExampleView.swift | 9 ++-- .../Authentication/Authenticator.swift | 47 ++++++++----------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 54583e97a..96e6f42a2 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -41,10 +41,13 @@ struct AuthenticationExampleView: View { } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .sheet(isPresented: $authenticator.isSheetPresented) { - authenticator.currentView + authenticator.currentSheet } - .alert("foo", isPresented: $authenticator.isAlertPresented) { - authenticator.currentView + .alert("hello", isPresented: $authenticator.isSheetPresented, presenting: "data", actions: { d in + + }) + .alert(isPresented: $authenticator.isAlertPresented) { + Alert(title: Text("title.."), message: Text("massage"), dismissButton: .cancel()) } .onAppear { ArcGISURLSession.challengeHandler = authenticator diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index f17241dfd..8620110b9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -82,7 +82,7 @@ public final class Authenticator: ObservableObject { await queuedChallenge.complete() // Dismiss the view. - dismissView() + dismiss() } } } @@ -99,55 +99,46 @@ public final class Authenticator: ObservableObject { @Published public var isSheetPresented: Bool = false + public var currentSheet: AnyView = AnyView(EmptyView()) + @Published public var isAlertPresented: Bool = false - public var currentView: AnyView = AnyView(EmptyView()) + public var currentAlert: AnyView = AnyView(EmptyView()) - func dismissView() { + func dismiss() { isSheetPresented = false isAlertPresented = false } - func presentView(with style: ChallengeViewStyle, content: Content) { - currentView = AnyView(content) - - switch style { - case .alert: - isAlertPresented = true - case .sheet: - isSheetPresented = true - } + func present(sheet: Content) { + dismiss() + currentSheet = AnyView(sheet) + isSheetPresented = true + } + + func present(alert: Content) { + dismiss() + currentAlert = AnyView(alert) + isAlertPresented = true } func presentView(for challenge: QueuedChallenge) { switch challenge { case let challenge as QueuedArcGISChallenge: - presentView( - with: .sheet, - content: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) - ) + present(sheet: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge))) case let challenge as QueuedURLChallenge: switch challenge.urlChallenge.protectionSpace.authenticationMethod { case NSURLAuthenticationMethodServerTrust: - presentView( - with: .alert, - content: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) - ) + present(alert: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge))) case NSURLAuthenticationMethodClientCertificate: - presentView( - with: .alert, - content: CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) - ) + present(sheet: CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge))) case NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodHTMLForm, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest: - presentView( - with: .sheet, - content: UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) - ) + present(sheet: UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge))) default: fatalError() } From f69df138684b49e77a06e5d7f7a741d9ba90b797 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 20:40:30 -0600 Subject: [PATCH 0919/1722] revert --- .../Examples/AuthenticationExampleView.swift | 17 ++- .../Authentication/AuthenticationView.swift | 74 ++++++------ .../Authentication/Authenticator.swift | 109 +++++++++--------- .../Authentication/QueuedChallenge.swift | 15 ++- 4 files changed, 112 insertions(+), 103 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 96e6f42a2..5a05dea19 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -31,6 +31,7 @@ struct AuthenticationExampleView: View { AuthenticationItemView(item: item) } } + Button("Clear Credential Store") { items = [] Task { @@ -38,17 +39,15 @@ struct AuthenticationExampleView: View { items = AuthenticationItem.makeAll() } } - } - .navigationBarTitle(Text("Authentication"), displayMode: .inline) - .sheet(isPresented: $authenticator.isSheetPresented) { - authenticator.currentSheet - } - .alert("hello", isPresented: $authenticator.isSheetPresented, presenting: "data", actions: { d in - }) - .alert(isPresented: $authenticator.isAlertPresented) { - Alert(title: Text("title.."), message: Text("massage"), dismissButton: .cancel()) + if let challenge = authenticator.currentChallenge { + AuthenticationView(challenge: challenge) + } } + .navigationBarTitle(Text("Authentication"), displayMode: .inline) +// .sheet(isPresented: $authenticator.isSheetPresented) { +// authenticator.currentSheet +// } .onAppear { ArcGISURLSession.challengeHandler = authenticator } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 267da8114..ab4da08a1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -13,40 +13,40 @@ import SwiftUI -//public struct AuthenticationView: View { -// public init(challenge: QueuedChallenge) { -// self.challenge = challenge.queuedChallenge -// } -// -// let challenge: QueuedChallenge -// -// public var body: some View { -// switch challenge { -// case let challenge as QueuedArcGISChallenge: -// UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) -// case let challenge as QueuedURLChallenge: -// view(forURLChallenge: challenge) -// default: -// fatalError() -// } -// } -// -// @MainActor -// @ViewBuilder -// func view(forURLChallenge challenge: QueuedURLChallenge) -> some View { -// switch challenge.urlChallenge.protectionSpace.authenticationMethod { -// case NSURLAuthenticationMethodServerTrust: -// TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) -// case NSURLAuthenticationMethodClientCertificate: -// CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) -// case NSURLAuthenticationMethodDefault, -// NSURLAuthenticationMethodNTLM, -// NSURLAuthenticationMethodHTMLForm, -// NSURLAuthenticationMethodHTTPBasic, -// NSURLAuthenticationMethodHTTPDigest: -// UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) -// default: -// fatalError() -// } -// } -//} +public struct AuthenticationView: View { + public init(challenge: IdentifiableQueuedChallenge) { + self.challenge = challenge.queuedChallenge + } + + let challenge: QueuedChallenge + + public var body: some View { + switch challenge { + case let challenge as QueuedArcGISChallenge: + UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) + case let challenge as QueuedURLChallenge: + view(forURLChallenge: challenge) + default: + fatalError() + } + } + + @MainActor + @ViewBuilder + func view(forURLChallenge challenge: QueuedURLChallenge) -> some View { + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) + case NSURLAuthenticationMethodClientCertificate: + CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: + UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + default: + fatalError() + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 8620110b9..5de4dd4c8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -75,14 +75,14 @@ public final class Authenticator: ObservableObject { // Creating the OAuth credential will present the OAuth login view. queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) } else { - // Present the challenge view - presentView(for: queuedChallenge) - + // Set the current challenge, this should present the appropriate view. + currentChallenge = IdentifiableQueuedChallenge(queuedChallenge: queuedChallenge) + // Wait for the queued challenge to finish. await queuedChallenge.complete() - // Dismiss the view. - dismiss() + // Reset the crrent challenge to `nil`, that will dismiss the view. + currentChallenge = nil } } } @@ -97,55 +97,58 @@ public final class Authenticator: ObservableObject { } @Published - public var isSheetPresented: Bool = false - - public var currentSheet: AnyView = AnyView(EmptyView()) - - @Published - public var isAlertPresented: Bool = false - - public var currentAlert: AnyView = AnyView(EmptyView()) - - func dismiss() { - isSheetPresented = false - isAlertPresented = false - } + public var currentChallenge: IdentifiableQueuedChallenge? - func present(sheet: Content) { - dismiss() - currentSheet = AnyView(sheet) - isSheetPresented = true - } - - func present(alert: Content) { - dismiss() - currentAlert = AnyView(alert) - isAlertPresented = true - } - - func presentView(for challenge: QueuedChallenge) { - switch challenge { - case let challenge as QueuedArcGISChallenge: - present(sheet: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge))) - case let challenge as QueuedURLChallenge: - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: - present(alert: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge))) - case NSURLAuthenticationMethodClientCertificate: - present(sheet: CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge))) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: - present(sheet: UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge))) - default: - fatalError() - } - default: - fatalError() - } - } +// @Published +// public var isSheetPresented: Bool = false +// +// public var currentSheet: AnyView = AnyView(EmptyView()) +// +// @Published +// public var isAlertPresented: Bool = false +// +// public var currentAlert: AnyView = AnyView(EmptyView()) +// +// func dismiss() { +// isSheetPresented = false +// isAlertPresented = false +// } +// +// func present(sheet: Content) { +// dismiss() +// currentSheet = AnyView(sheet) +// isSheetPresented = true +// } +// +// func present(alert: Content) { +// dismiss() +// currentAlert = AnyView(alert) +// isAlertPresented = true +// } +// +// func presentView(for challenge: QueuedChallenge) { +// switch challenge { +// case let challenge as QueuedArcGISChallenge: +// present(sheet: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge))) +// case let challenge as QueuedURLChallenge: +// switch challenge.urlChallenge.protectionSpace.authenticationMethod { +// case NSURLAuthenticationMethodServerTrust: +// present(alert: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge))) +// case NSURLAuthenticationMethodClientCertificate: +// present(sheet: CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge))) +// case NSURLAuthenticationMethodDefault, +// NSURLAuthenticationMethodNTLM, +// NSURLAuthenticationMethodHTMLForm, +// NSURLAuthenticationMethodHTTPBasic, +// NSURLAuthenticationMethodHTTPDigest: +// present(sheet: UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge))) +// default: +// fatalError() +// } +// default: +// fatalError() +// } +// } } struct ChallengeView: View { diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index ada34a91a..a110acf99 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -14,12 +14,11 @@ import Foundation import ArcGIS -protocol QueuedChallenge { +protocol QueuedChallenge: AnyObject { func complete() async } -// TODO: Ryan - Can these be structs? -final class QueuedArcGISChallenge: QueuedChallenge { +public final class QueuedArcGISChallenge: QueuedChallenge { let arcGISChallenge: ArcGISAuthenticationChallenge init(arcGISChallenge: ArcGISAuthenticationChallenge) { @@ -60,7 +59,7 @@ final class QueuedArcGISChallenge: QueuedChallenge { } } -final class QueuedURLChallenge: QueuedChallenge { +public final class QueuedURLChallenge: QueuedChallenge { let urlChallenge: URLAuthenticationChallenge init(urlChallenge: URLAuthenticationChallenge) { @@ -101,3 +100,11 @@ final class QueuedURLChallenge: QueuedChallenge { case cancel } } + +public struct IdentifiableQueuedChallenge { + let queuedChallenge: QueuedChallenge +} + +extension IdentifiableQueuedChallenge: Identifiable { + public var id: ObjectIdentifier { ObjectIdentifier(queuedChallenge) } +} From 5c369fab8feb670be86125ba1b5461043af5f650 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 20:59:18 -0600 Subject: [PATCH 0920/1722] use alert --- .../Authentication/AuthenticationView.swift | 2 +- .../Authentication/TrustHostView.swift | 85 +++---------------- 2 files changed, 14 insertions(+), 73 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index ab4da08a1..d6df7fc6e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -36,7 +36,7 @@ public struct AuthenticationView: View { func view(forURLChallenge challenge: QueuedURLChallenge) -> some View { switch challenge.urlChallenge.protectionSpace.authenticationMethod { case NSURLAuthenticationMethodServerTrust: - TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge)) + TrustHostView(challenge: challenge) case NSURLAuthenticationMethodClientCertificate: CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) case NSURLAuthenticationMethodDefault, diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift index da24cee02..1f7ee366d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift @@ -13,88 +13,29 @@ import SwiftUI -@MainActor protocol TrustHostViewModel: ObservableObject { - var challengingHost: String { get } - - func allowConnection() - func cancel() -} - -final class TrustHostChallengeViewModel: TrustHostViewModel { +struct TrustHostView: View { init(challenge: QueuedURLChallenge) { self.challenge = challenge } let challenge: QueuedURLChallenge - var challengingHost: String { + var host: String { challenge.urlChallenge.protectionSpace.host } - func allowConnection() { - challenge.resume(with: .trustHost) - } - - func cancel() { - challenge.cancel() - } -} - -final class MockTrustHostViewModel: TrustHostViewModel { - let challengingHost: String - init(challengingHost: String) { - self.challengingHost = challengingHost - } - func allowConnection() {} - func cancel() {} -} - -struct TrustHostView: View { - init(viewModel: ViewModel) { - self.viewModel = viewModel - } - - @ObservedObject private var viewModel: ViewModel - var body: some View { - VStack { - Text("Certificate Trust Warning") - .font(.title) - .padding([.bottom]) - .multilineTextAlignment(.center) - - Text("The certificate provided by '\(viewModel.challengingHost)' is not signed by a trusted authority.") - .font(.body) - .padding([.bottom]) - - Button(role: .cancel) { - viewModel.cancel() - } label: { - Text("Cancel") - .frame(maxWidth: .infinity) + Color.clear + .frame(width: 0, height: 0) + .alert("Certificate Trust Warning", isPresented: .constant(true), presenting: challenge) { _ in + Button("Dangerous: Allow Connection", role: .destructive) { + challenge.resume(with: .trustHost) + } + Button("Cancel", role: .cancel) { + challenge.cancel() + } + } message: { _ in + Text("The certificate provided by '\(host)' is not signed by a trusted authority.") } - .buttonStyle(.bordered) - .controlSize(.large) - - Button(role: .destructive) { - viewModel.allowConnection() - } label: { - Text("Dangerous: Allow Connection") - .frame(maxWidth: .infinity) - - } - .buttonStyle(.bordered) - .controlSize(.large) - - Spacer() - } - .padding() - .interactiveDismissDisabled() - } -} - -struct TrustHostView_Previews: PreviewProvider { - static var previews: some View { - TrustHostView(viewModel: MockTrustHostViewModel(challengingHost: "arcgis.com")) } } From fd81c5921c6e6e70512bdd027086f478875f93e2 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 21:28:14 -0600 Subject: [PATCH 0921/1722] remove old code --- .../Authentication/Authenticator.swift | 62 ----------- .../CertificatePickerView.swift | 104 ++++++------------ .../Authentication/InvisibleView.swift | 21 ++++ .../Authentication/TrustHostView.swift | 3 +- 4 files changed, 57 insertions(+), 133 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 5de4dd4c8..6eebc6889 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -98,68 +98,6 @@ public final class Authenticator: ObservableObject { @Published public var currentChallenge: IdentifiableQueuedChallenge? - -// @Published -// public var isSheetPresented: Bool = false -// -// public var currentSheet: AnyView = AnyView(EmptyView()) -// -// @Published -// public var isAlertPresented: Bool = false -// -// public var currentAlert: AnyView = AnyView(EmptyView()) -// -// func dismiss() { -// isSheetPresented = false -// isAlertPresented = false -// } -// -// func present(sheet: Content) { -// dismiss() -// currentSheet = AnyView(sheet) -// isSheetPresented = true -// } -// -// func present(alert: Content) { -// dismiss() -// currentAlert = AnyView(alert) -// isAlertPresented = true -// } -// -// func presentView(for challenge: QueuedChallenge) { -// switch challenge { -// case let challenge as QueuedArcGISChallenge: -// present(sheet: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge))) -// case let challenge as QueuedURLChallenge: -// switch challenge.urlChallenge.protectionSpace.authenticationMethod { -// case NSURLAuthenticationMethodServerTrust: -// present(alert: TrustHostView(viewModel: TrustHostChallengeViewModel(challenge: challenge))) -// case NSURLAuthenticationMethodClientCertificate: -// present(sheet: CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge))) -// case NSURLAuthenticationMethodDefault, -// NSURLAuthenticationMethodNTLM, -// NSURLAuthenticationMethodHTMLForm, -// NSURLAuthenticationMethodHTTPBasic, -// NSURLAuthenticationMethodHTTPDigest: -// present(sheet: UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge))) -// default: -// fatalError() -// } -// default: -// fatalError() -// } -// } -} - -struct ChallengeView: View { - var style: ChallengeViewStyle - var content: AnyView - var body: some View { content } -} - -enum ChallengeViewStyle { - case sheet - case alert } extension Authenticator: AuthenticationChallengeHandler { diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index a185b409e..83bb8944d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -40,54 +40,40 @@ import UniformTypeIdentifiers } struct CertificatePickerView: View { - enum Step { - case browsePrompt - case documentPicker - case enterPassword - } - @ObservedObject var viewModel: CertificatePickerViewModel - @State var step: Step = .browsePrompt - @State var showPicker: Bool = false + @State var showPrompt: Bool = false + @State var showPicker: Bool = true + @State var showPassword: Bool = false var body: some View { - Group { - switch step { - case .browsePrompt: - PromptBrowseCertificateView(host: viewModel.challengingHost) { - viewModel.cancel() - } onContinue: { - step = .documentPicker - showPicker = true - } - case .documentPicker: - Text("picker") -// DocumentPickerView(contentTypes: [.pfx]) { -// print("-- here!!! \($0)") -// viewModel.certificateURL = $0 -// step = .enterYPassword -// } onCancel: { -// viewModel.cancel() -// } - case .enterPassword: - EnterPasswordView(password: $viewModel.password) { - viewModel.cancel() - } onContinue: { - viewModel.signIn() - } - } - } + InvisibleView() +// PromptBrowseCertificateView(host: viewModel.challengingHost) { +// step = .documentPicker +// showPicker = true +// } onCancel: { +// viewModel.cancel() +// } +// } .sheet(isPresented: $showPicker) { DocumentPickerView(contentTypes: [.pfx]) { viewModel.certificateURL = $0 - step = .enterPassword + showPassword = true } onCancel: { viewModel.cancel() } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() + } + .sheet(isPresented: $showPassword) { + EnterPasswordView(password: $viewModel.password) { + viewModel.signIn() + } onCancel: { + viewModel.cancel() + } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() } } @@ -97,47 +83,28 @@ private extension UTType { struct PromptBrowseCertificateView: View { var host: String - var onCancel: () -> Void var onContinue: () -> Void + var onCancel: () -> Void var body: some View { - VStack { - Text("A certificate is required to access content on \(host)") - .font(.body) - .padding([.bottom]) - .multilineTextAlignment(.center) - - Button(role: .cancel) { - onCancel() - } label: { - Text("Cancel") - .frame(maxWidth: .infinity) - } - .buttonStyle(.bordered) - .controlSize(.large) - - Button { - onContinue() - } label: { - Text("Browse for a certificate") - .frame(maxWidth: .infinity) - + InvisibleView() + .alert("Certificate Required", isPresented: .constant(true), presenting: host) { _ in + Button("Browse for a certificate") { + onContinue() + } + Button("Cancel", role: .cancel) { + onCancel() + } + } message: { _ in + Text("A certificate is required to access content on \(host)") } - .buttonStyle(.bordered) - .controlSize(.large) - - Spacer() - } - .padding() - .navigationTitle("Certificate Required") } } struct EnterPasswordView: View { @Binding var password: String - var onCancel: () -> Void var onContinue: () -> Void - + var onCancel: () -> Void var body: some View { VStack { @@ -180,4 +147,3 @@ struct EnterPasswordView: View { .navigationTitle("Certificate Required") } } - diff --git a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift new file mode 100644 index 000000000..9857e5246 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift @@ -0,0 +1,21 @@ +// 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 + +struct InvisibleView: View { + var body: some View { + Color.clear + .frame(width: 0, height: 0) + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift index 1f7ee366d..1d647bb29 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift @@ -25,8 +25,7 @@ struct TrustHostView: View { } var body: some View { - Color.clear - .frame(width: 0, height: 0) + InvisibleView() .alert("Certificate Trust Warning", isPresented: .constant(true), presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { challenge.resume(with: .trustHost) From 54b2f4fb868abd2e27057a1a555b7c8fb07e5d39 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 22:08:50 -0600 Subject: [PATCH 0922/1722] view modifier --- .../Examples/AuthenticationExampleView.swift | 8 +-- .../Authentication/AuthenticationView.swift | 49 +++++++++++++++++-- .../Authentication/Authenticator.swift | 3 +- .../Authentication/InvisibleView.swift | 3 +- .../Authentication/QueuedChallenge.swift | 6 +-- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 5a05dea19..e59352878 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -39,15 +39,9 @@ struct AuthenticationExampleView: View { items = AuthenticationItem.makeAll() } } - - if let challenge = authenticator.currentChallenge { - AuthenticationView(challenge: challenge) - } } + .authentication(authenticator: authenticator) .navigationBarTitle(Text("Authentication"), displayMode: .inline) -// .sheet(isPresented: $authenticator.isSheetPresented) { -// authenticator.currentSheet -// } .onAppear { ArcGISURLSession.challengeHandler = authenticator } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index d6df7fc6e..82d7bd228 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -13,14 +13,14 @@ import SwiftUI -public struct AuthenticationView: View { - public init(challenge: IdentifiableQueuedChallenge) { +struct AuthenticationView: View { + init(challenge: IdentifiableQueuedChallenge) { self.challenge = challenge.queuedChallenge } let challenge: QueuedChallenge - public var body: some View { + var body: some View { switch challenge { case let challenge as QueuedArcGISChallenge: UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) @@ -50,3 +50,46 @@ public struct AuthenticationView: View { } } } + +public extension View { + @MainActor + @ViewBuilder + func authentication(authenticator: Authenticator) -> some View { + modifier(AuthenticationModifier(authenticator: authenticator)) + } + +// func authentication(authenticator: Authenticator) -> some View { +// ZStack { +// if let challenge = authenticator.currentChallenge { +// AuthenticationView(challenge: challenge) +// } +// } +// } +// func authentication(authenticator: Authenticator) -> some View { +// if let challenge = authenticator.currentChallenge { +// overlay(AuthenticationView(challenge: challenge)) +// } else { +// overlay(EmptyView()) +// } +// } +} + +struct AuthenticationModifier: ViewModifier { + @ObservedObject var authenticator: Authenticator + +// func body(content: Content) -> some View { +// if let challenge = authenticator.currentChallenge { +// content.overlay(AuthenticationView(challenge: challenge)) +// } else { +// content +// } +// } + func body(content: Content) -> some View { + ZStack { + content + if let challenge = authenticator.currentChallenge { + AuthenticationView(challenge: challenge) + } + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 6eebc6889..7fe4b51e2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -68,6 +68,7 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { + print("-- challenged...") if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, let url = queuedArcGISChallenge.arcGISChallenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { @@ -97,7 +98,7 @@ public final class Authenticator: ObservableObject { } @Published - public var currentChallenge: IdentifiableQueuedChallenge? + var currentChallenge: IdentifiableQueuedChallenge? } extension Authenticator: AuthenticationChallengeHandler { diff --git a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift index 9857e5246..648fe9524 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift @@ -15,7 +15,6 @@ import SwiftUI struct InvisibleView: View { var body: some View { - Color.clear - .frame(width: 0, height: 0) + Color.clear.frame(width: 0, height: 0, alignment: .bottom) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index a110acf99..ebeba6e81 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -18,7 +18,7 @@ protocol QueuedChallenge: AnyObject { func complete() async } -public final class QueuedArcGISChallenge: QueuedChallenge { +final class QueuedArcGISChallenge: QueuedChallenge { let arcGISChallenge: ArcGISAuthenticationChallenge init(arcGISChallenge: ArcGISAuthenticationChallenge) { @@ -59,7 +59,7 @@ public final class QueuedArcGISChallenge: QueuedChallenge { } } -public final class QueuedURLChallenge: QueuedChallenge { +final class QueuedURLChallenge: QueuedChallenge { let urlChallenge: URLAuthenticationChallenge init(urlChallenge: URLAuthenticationChallenge) { @@ -101,7 +101,7 @@ public final class QueuedURLChallenge: QueuedChallenge { } } -public struct IdentifiableQueuedChallenge { +struct IdentifiableQueuedChallenge { let queuedChallenge: QueuedChallenge } From 245a70e7e32082a56df1e30c45c0c603840a2413 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 22:09:31 -0600 Subject: [PATCH 0923/1722] delete old code --- .../Authentication/AuthenticationView.swift | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 82d7bd228..c910ce2f2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -57,33 +57,11 @@ public extension View { func authentication(authenticator: Authenticator) -> some View { modifier(AuthenticationModifier(authenticator: authenticator)) } - -// func authentication(authenticator: Authenticator) -> some View { -// ZStack { -// if let challenge = authenticator.currentChallenge { -// AuthenticationView(challenge: challenge) -// } -// } -// } -// func authentication(authenticator: Authenticator) -> some View { -// if let challenge = authenticator.currentChallenge { -// overlay(AuthenticationView(challenge: challenge)) -// } else { -// overlay(EmptyView()) -// } -// } } struct AuthenticationModifier: ViewModifier { @ObservedObject var authenticator: Authenticator -// func body(content: Content) -> some View { -// if let challenge = authenticator.currentChallenge { -// content.overlay(AuthenticationView(challenge: challenge)) -// } else { -// content -// } -// } func body(content: Content) -> some View { ZStack { content From bbd94013ca038a75ed0b3f7d1233a2d77300bcf5 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 20 May 2022 22:33:51 -0600 Subject: [PATCH 0924/1722] fix alert --- .../Authentication/AuthenticationView.swift | 4 +- .../CertificatePickerView.swift | 89 ++++++++++--------- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index c910ce2f2..33b7e8ce4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -44,7 +44,9 @@ struct AuthenticationView: View { NSURLAuthenticationMethodHTMLForm, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest: - UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + InvisibleView().sheet(isPresented: .constant(true)) { + UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + } default: fatalError() } diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index 83bb8944d..06e3576b9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -30,7 +30,7 @@ import UniformTypeIdentifiers guard let certificateURL = certificateURL else { preconditionFailure() } - + challenge.resume(with: .certificate(url: certificateURL, passsword: password)) } @@ -42,38 +42,39 @@ import UniformTypeIdentifiers struct CertificatePickerView: View { @ObservedObject var viewModel: CertificatePickerViewModel - @State var showPrompt: Bool = false - @State var showPicker: Bool = true + @State var showPrompt: Bool = true + @State var showPicker: Bool = false @State var showPassword: Bool = false var body: some View { InvisibleView() -// PromptBrowseCertificateView(host: viewModel.challengingHost) { -// step = .documentPicker -// showPicker = true -// } onCancel: { -// viewModel.cancel() -// } -// } - .sheet(isPresented: $showPicker) { - DocumentPickerView(contentTypes: [.pfx]) { - viewModel.certificateURL = $0 - showPassword = true - } onCancel: { - viewModel.cancel() + .promptBrowseCertificateView( + isPresented: $showPrompt, + host: viewModel.challengingHost, + onContinue: { + showPicker = true + }, onCancel: { + viewModel.cancel() + }) + .sheet(isPresented: $showPicker) { + DocumentPickerView(contentTypes: [.pfx]) { + viewModel.certificateURL = $0 + showPassword = true + } onCancel: { + viewModel.cancel() + } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() - } - .sheet(isPresented: $showPassword) { - EnterPasswordView(password: $viewModel.password) { - viewModel.signIn() - } onCancel: { - viewModel.cancel() + .sheet(isPresented: $showPassword) { + EnterPasswordView(password: $viewModel.password) { + viewModel.signIn() + } onCancel: { + viewModel.cancel() + } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() - } } } @@ -81,23 +82,25 @@ private extension UTType { static let pfx = UTType(filenameExtension: "pfx")! } -struct PromptBrowseCertificateView: View { - var host: String - var onContinue: () -> Void - var onCancel: () -> Void - - var body: some View { - InvisibleView() - .alert("Certificate Required", isPresented: .constant(true), presenting: host) { _ in - Button("Browse for a certificate") { - onContinue() - } - Button("Cancel", role: .cancel) { - onCancel() - } - } message: { _ in - Text("A certificate is required to access content on \(host)") +private extension View { + @MainActor + @ViewBuilder + func promptBrowseCertificateView( + isPresented: Binding, + host: String, + onContinue: @escaping () -> Void, + onCancel: @escaping () -> Void + ) -> some View { + alert("Certificate Required", isPresented: isPresented, presenting: host) { _ in + Button("Browse For Certificate") { + onContinue() } + Button("Cancel", role: .cancel) { + onCancel() + } + } message: { _ in + Text("A certificate is required to access content on \(host).") + } } } From 9322b826efce7e1a90fae6c0e530348ecb264834 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 08:26:55 -0600 Subject: [PATCH 0925/1722] better cert password view --- .../CertificatePickerView.swift | 65 ++++++++++++++++++- .../Authentication/UsernamePasswordView.swift | 2 +- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index 06e3576b9..549f2218a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -104,7 +104,7 @@ private extension View { } } -struct EnterPasswordView: View { +struct EnterPasswordView1: View { @Binding var password: String var onContinue: () -> Void var onCancel: () -> Void @@ -150,3 +150,66 @@ struct EnterPasswordView: View { .navigationTitle("Certificate Required") } } + +struct EnterPasswordView: View { + @Binding var password: String + var onContinue: () -> Void + var onCancel: () -> Void + @FocusState var isPasswordFocused: Bool + + var body: some View { + NavigationView { + Form { + Section { + VStack { + //person + Text("Please enter a password for the chosen certificate.") + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + } + + Section { + SecureField("Password", text: $password) + .focused($isPasswordFocused) + .textContentType(.password) + .submitLabel(.go) + .onSubmit { onContinue() } + } + .autocapitalization(.none) + .disableAutocorrection(true) + + Section { + okButton + } + } + .navigationTitle("Certificate") + .navigationBarTitleDisplayMode(.inline) + .interactiveDismissDisabled() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { onCancel() } + } + } + .onAppear { + // Workaround for Apple bug - FB9676178. + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + isPasswordFocused = true + } + } + } + } + + private var okButton: some View { + Button(action: { + onContinue() + }, label: { + Text("OK") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundColor(.white) + }) + .disabled(!password.isEmpty) + .listRowBackground(!password.isEmpty ? Color.accentColor : Color.gray) + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index a3d990078..79e01173c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -41,7 +41,7 @@ struct UsernamePasswordView: View { Section { VStack { person - Text("You need to sign in to access '\(viewModel.challengingHost)'") + Text("You must sign in to access '\(viewModel.challengingHost)'") .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity) From f7e22bcc36627b280cd64e19cb85c77f4aa3b9e3 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 08:27:28 -0600 Subject: [PATCH 0926/1722] delete old code --- .../CertificatePickerView.swift | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index 549f2218a..ac2db567e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -104,53 +104,6 @@ private extension View { } } -struct EnterPasswordView1: View { - @Binding var password: String - var onContinue: () -> Void - var onCancel: () -> Void - - var body: some View { - VStack { - Text("Please enter a password for the chosen certificate.") - .font(.body) - .padding([.bottom]) - .multilineTextAlignment(.center) - - SecureField(text: $password, prompt: Text("Password")) { - Text("label") - } - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - - HStack { - Button { - onContinue() - } label: { - Text("OK") - .frame(maxWidth: .infinity) - - } - .buttonStyle(.bordered) - .controlSize(.large) - - Button(role: .cancel) { - onCancel() - } label: { - Text("Cancel") - .frame(maxWidth: .infinity) - } - .buttonStyle(.bordered) - .controlSize(.large) - } - - - Spacer() - } - .padding() - .navigationTitle("Certificate Required") - } -} - struct EnterPasswordView: View { @Binding var password: String var onContinue: () -> Void From 820d663ff0dc2c98d697c90da27d4cdadeaea832 Mon Sep 17 00:00:00 2001 From: Zachary Kline Date: Mon, 23 May 2022 10:30:04 -0700 Subject: [PATCH 0927/1722] fix warning --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 02be4e232..17aabf8da 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -113,7 +113,7 @@ public class SearchViewModel: ObservableObject { guard !isEligibleForRequery else { return } // Check center difference. - let centerDiff = try? GeometryEngine.distance( + let centerDiff = GeometryEngine.distance( from: lastExtent.center, to: newExtent.center ) From cf8b272afd420d4e2060f6cae1c99ba921d84a0d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 16:25:30 -0600 Subject: [PATCH 0928/1722] remove identifiable queued challenge --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/AuthenticationView.swift | 42 ++++++++----------- .../Authentication/Authenticator.swift | 4 +- .../CertificatePickerView.swift | 4 ++ .../Authentication/InvisibleView.swift | 30 +++++++++++++ .../Authentication/QueuedChallenge.swift | 8 ---- .../Authentication/UsernamePasswordView.swift | 14 +++++-- 7 files changed, 66 insertions(+), 38 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index e59352878..648fb8be1 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,7 +17,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( - oAuthConfigurations: [.arcgisDotCom] + //oAuthConfigurations: [.arcgisDotCom] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 33b7e8ce4..3207327ae 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -14,8 +14,8 @@ import SwiftUI struct AuthenticationView: View { - init(challenge: IdentifiableQueuedChallenge) { - self.challenge = challenge.queuedChallenge + init(challenge: QueuedChallenge) { + self.challenge = challenge } let challenge: QueuedChallenge @@ -23,29 +23,23 @@ struct AuthenticationView: View { var body: some View { switch challenge { case let challenge as QueuedArcGISChallenge: - UsernamePasswordView(viewModel: TokenCredentialViewModel(challenge: challenge)) + Sheet { + UsernamePasswordView(challenge: challenge) + } case let challenge as QueuedURLChallenge: - view(forURLChallenge: challenge) - default: - fatalError() - } - } - - @MainActor - @ViewBuilder - func view(forURLChallenge challenge: QueuedURLChallenge) -> some View { - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: - TrustHostView(challenge: challenge) - case NSURLAuthenticationMethodClientCertificate: - CertificatePickerView(viewModel: CertificatePickerViewModel(challenge: challenge)) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: - InvisibleView().sheet(isPresented: .constant(true)) { - UsernamePasswordView(viewModel: URLCredentialUsernamePasswordViewModel(challenge: challenge)) + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + TrustHostView(challenge: challenge) + case NSURLAuthenticationMethodClientCertificate: + CertificatePickerView(challenge: challenge) + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: + UsernamePasswordView(challenge: challenge) + default: + fatalError() } default: fatalError() diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 7fe4b51e2..b42d604d6 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -77,7 +77,7 @@ public final class Authenticator: ObservableObject { queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) } else { // Set the current challenge, this should present the appropriate view. - currentChallenge = IdentifiableQueuedChallenge(queuedChallenge: queuedChallenge) + currentChallenge = queuedChallenge // Wait for the queued challenge to finish. await queuedChallenge.complete() @@ -98,7 +98,7 @@ public final class Authenticator: ObservableObject { } @Published - var currentChallenge: IdentifiableQueuedChallenge? + var currentChallenge: QueuedChallenge? } extension Authenticator: AuthenticationChallengeHandler { diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index ac2db567e..d598f2647 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -40,6 +40,10 @@ import UniformTypeIdentifiers } struct CertificatePickerView: View { + init(challenge: QueuedURLChallenge) { + viewModel = CertificatePickerViewModel(challenge: challenge) + } + @ObservedObject var viewModel: CertificatePickerViewModel @State var showPrompt: Bool = true diff --git a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift index 648fe9524..c1b978aee 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift @@ -18,3 +18,33 @@ struct InvisibleView: View { Color.clear.frame(width: 0, height: 0, alignment: .bottom) } } + +struct Sheet: View { + @State private var isPresented: Bool = true + var onDismiss: (() -> Void)? + var content: () -> Content + + var body: some View { + InvisibleView() + .onAppear { isPresented = true } + .sheet( + isPresented: $isPresented, + onDismiss: onDismiss, + content: content + ) + } +} + +//public extension View { +// @MainActor +// @ViewBuilder +// func asSheet() -> some View { +// modifier(SheetModifier()) +// } +//} +// +//struct SheetModifier: ViewModifier { +// func body(content: Content) -> some View { +// Sheet(isPresented: true, content: { content } ) +// } +//} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index ebeba6e81..3686b96a3 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -100,11 +100,3 @@ final class QueuedURLChallenge: QueuedChallenge { case cancel } } - -struct IdentifiableQueuedChallenge { - let queuedChallenge: QueuedChallenge -} - -extension IdentifiableQueuedChallenge: Identifiable { - public var id: ObjectIdentifier { ObjectIdentifier(queuedChallenge) } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 79e01173c..80b9db46e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -26,8 +26,16 @@ import ArcGIS } struct UsernamePasswordView: View { - init(viewModel: ViewModel) { - self.viewModel = viewModel + init(challengingHost: String) where ViewModel == MockUsernamePasswordViewModel { + viewModel = MockUsernamePasswordViewModel(challengingHost: challengingHost) + } + + init(challenge: QueuedURLChallenge) where ViewModel == URLCredentialUsernamePasswordViewModel { + viewModel = URLCredentialUsernamePasswordViewModel(challenge: challenge) + } + + init(challenge: QueuedArcGISChallenge) where ViewModel == TokenCredentialViewModel { + viewModel = TokenCredentialViewModel(challenge: challenge) } @ObservedObject private var viewModel: ViewModel @@ -118,7 +126,7 @@ struct UsernamePasswordView: View { struct UsernamePasswordView_Previews: PreviewProvider { static var previews: some View { - UsernamePasswordView(viewModel: MockUsernamePasswordViewModel(challengingHost: "arcgis.com")) + UsernamePasswordView(challengingHost: "arcgis.com") } } From e1898d0ffe3ef35c7a3767f7709b740a8a07e27e Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 17:13:03 -0600 Subject: [PATCH 0929/1722] view modifiers --- .../Authentication/AuthenticationView.swift | 76 ++++++++++++------- .../CertificatePickerView.swift | 15 +++- .../Authentication/TrustHostView.swift | 6 +- .../Authentication/UsernamePasswordView.swift | 21 +++-- 4 files changed, 82 insertions(+), 36 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 3207327ae..258ae64f1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -21,23 +21,66 @@ struct AuthenticationView: View { let challenge: QueuedChallenge var body: some View { + fatalError() +// switch challenge { +// case let challenge as QueuedArcGISChallenge: +//// Sheet { +//// UsernamePasswordView(challenge: challenge) +//// } +// case let challenge as QueuedURLChallenge: +// switch challenge.urlChallenge.protectionSpace.authenticationMethod { +// case NSURLAuthenticationMethodServerTrust: +// //TrustHostView(challenge: challenge) +// fatalError() +// case NSURLAuthenticationMethodClientCertificate: +// CertificatePickerView(challenge: challenge) +// case NSURLAuthenticationMethodDefault, +// NSURLAuthenticationMethodNTLM, +// NSURLAuthenticationMethodHTMLForm, +// NSURLAuthenticationMethodHTTPBasic, +// NSURLAuthenticationMethodHTTPDigest: +// UsernamePasswordView(challenge: challenge) +// default: +// fatalError() +// } +// default: +// fatalError() +// } + } +} + +public extension View { + @MainActor + @ViewBuilder + func authentication(authenticator: Authenticator) -> some View { + modifier(AuthenticationModifier(authenticator: authenticator)) + } +} + +struct AuthenticationModifier: ViewModifier { + @ObservedObject var authenticator: Authenticator + @State private var isPresented = false + + func body(content: Content) -> some View { + guard let challenge = authenticator.currentChallenge else { + return AnyView(content) + } + switch challenge { case let challenge as QueuedArcGISChallenge: - Sheet { - UsernamePasswordView(challenge: challenge) - } + return AnyView(content.modifier(UsernamePasswordViewModifier(challenge: challenge))) case let challenge as QueuedURLChallenge: switch challenge.urlChallenge.protectionSpace.authenticationMethod { case NSURLAuthenticationMethodServerTrust: - TrustHostView(challenge: challenge) + return AnyView(content.modifier(TrustHostViewModifier(challenge: challenge))) case NSURLAuthenticationMethodClientCertificate: - CertificatePickerView(challenge: challenge) + return AnyView(content.modifier(CertificatePickerViewModifier(challenge: challenge))) case NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodHTMLForm, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest: - UsernamePasswordView(challenge: challenge) + return AnyView(content.modifier(UsernamePasswordViewModifier(challenge: challenge))) default: fatalError() } @@ -47,23 +90,4 @@ struct AuthenticationView: View { } } -public extension View { - @MainActor - @ViewBuilder - func authentication(authenticator: Authenticator) -> some View { - modifier(AuthenticationModifier(authenticator: authenticator)) - } -} - -struct AuthenticationModifier: ViewModifier { - @ObservedObject var authenticator: Authenticator - - func body(content: Content) -> some View { - ZStack { - content - if let challenge = authenticator.currentChallenge { - AuthenticationView(challenge: challenge) - } - } - } -} +extension Authenticator: Identifiable {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index d598f2647..c768ad4cd 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -14,7 +14,7 @@ import SwiftUI import UniformTypeIdentifiers -@MainActor final class CertificatePickerViewModel: ObservableObject { +@MainActor final private class CertificatePickerViewModel: ObservableObject { let challengingHost: String let challenge: QueuedURLChallenge @@ -39,7 +39,18 @@ import UniformTypeIdentifiers } } -struct CertificatePickerView: View { + +@MainActor +struct CertificatePickerViewModifier: ViewModifier { + let challenge: QueuedURLChallenge + + func body(content: Content) -> some View { + content.sheet(isPresented: .constant(true)) { + CertificatePickerView(challenge: challenge) + } +} + +private struct CertificatePickerView: View { init(challenge: QueuedURLChallenge) { viewModel = CertificatePickerViewModel(challenge: challenge) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift index 1d647bb29..b97bbec5a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift @@ -13,7 +13,7 @@ import SwiftUI -struct TrustHostView: View { +struct TrustHostViewModifier: ViewModifier { init(challenge: QueuedURLChallenge) { self.challenge = challenge } @@ -24,8 +24,8 @@ struct TrustHostView: View { challenge.urlChallenge.protectionSpace.host } - var body: some View { - InvisibleView() + func body(content: Content) -> some View { + content .alert("Certificate Trust Warning", isPresented: .constant(true), presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { challenge.resume(with: .trustHost) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 80b9db46e..4c9972628 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -25,10 +25,9 @@ import ArcGIS func cancel() } -struct UsernamePasswordView: View { - init(challengingHost: String) where ViewModel == MockUsernamePasswordViewModel { - viewModel = MockUsernamePasswordViewModel(challengingHost: challengingHost) - } +@MainActor +struct UsernamePasswordViewModifier: ViewModifier { + let viewModel: ViewModel init(challenge: QueuedURLChallenge) where ViewModel == URLCredentialUsernamePasswordViewModel { viewModel = URLCredentialUsernamePasswordViewModel(challenge: challenge) @@ -38,6 +37,18 @@ struct UsernamePasswordView: View { viewModel = TokenCredentialViewModel(challenge: challenge) } + func body(content: Content) -> some View { + content.sheet(isPresented: .constant(true)) { + UsernamePasswordView(viewModel: viewModel) + } + } +} + +private struct UsernamePasswordView: View { + init(viewModel: ViewModel) { + _viewModel = ObservedObject(initialValue: viewModel) + } + @ObservedObject private var viewModel: ViewModel /// The focused field. @@ -126,7 +137,7 @@ struct UsernamePasswordView: View { struct UsernamePasswordView_Previews: PreviewProvider { static var previews: some View { - UsernamePasswordView(challengingHost: "arcgis.com") + UsernamePasswordView(viewModel: MockUsernamePasswordViewModel(challengingHost: "arcgis.com")) } } From 0ef521f5d1f219076bd2f0f41b189e924940b8c8 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 17:27:36 -0600 Subject: [PATCH 0930/1722] view modifiers --- .../Authentication/AuthenticationView.swift | 46 +++++++++---------- .../CertificatePickerView.swift | 17 ++----- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 258ae64f1..db80df084 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -61,33 +61,33 @@ struct AuthenticationModifier: ViewModifier { @ObservedObject var authenticator: Authenticator @State private var isPresented = false + @ViewBuilder func body(content: Content) -> some View { - guard let challenge = authenticator.currentChallenge else { - return AnyView(content) - } - - switch challenge { - case let challenge as QueuedArcGISChallenge: - return AnyView(content.modifier(UsernamePasswordViewModifier(challenge: challenge))) - case let challenge as QueuedURLChallenge: - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: - return AnyView(content.modifier(TrustHostViewModifier(challenge: challenge))) - case NSURLAuthenticationMethodClientCertificate: - return AnyView(content.modifier(CertificatePickerViewModifier(challenge: challenge))) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: - return AnyView(content.modifier(UsernamePasswordViewModifier(challenge: challenge))) + if let challenge = authenticator.currentChallenge { + switch challenge { + case let challenge as QueuedArcGISChallenge: + content.modifier(UsernamePasswordViewModifier(challenge: challenge)) + case let challenge as QueuedURLChallenge: + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + content.modifier(TrustHostViewModifier(challenge: challenge)) + case NSURLAuthenticationMethodClientCertificate: + content.modifier(CertificatePickerViewModifier(challenge: challenge)) + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: + content.modifier(UsernamePasswordViewModifier(challenge: challenge)) + default: + fatalError() + } default: fatalError() } - default: - fatalError() + } + else { + content } } } - -extension Authenticator: Identifiable {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index c768ad4cd..0f3068054 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -42,27 +42,18 @@ import UniformTypeIdentifiers @MainActor struct CertificatePickerViewModifier: ViewModifier { - let challenge: QueuedURLChallenge - - func body(content: Content) -> some View { - content.sheet(isPresented: .constant(true)) { - CertificatePickerView(challenge: challenge) - } -} - -private struct CertificatePickerView: View { init(challenge: QueuedURLChallenge) { viewModel = CertificatePickerViewModel(challenge: challenge) } - @ObservedObject var viewModel: CertificatePickerViewModel + @ObservedObject private var viewModel: CertificatePickerViewModel @State var showPrompt: Bool = true @State var showPicker: Bool = false @State var showPassword: Bool = false - - var body: some View { - InvisibleView() + + func body(content: Content) -> some View { + content .promptBrowseCertificateView( isPresented: $showPrompt, host: viewModel.challengingHost, From 33e37705b0f8cf221cb4a9721aa4f1a7efa89751 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 17:48:58 -0600 Subject: [PATCH 0931/1722] idk --- .../Authentication/AuthenticationView.swift | 24 +++++++++++++++++-- .../Authentication/Authenticator.swift | 1 - 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index db80df084..5fc659157 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -21,7 +21,6 @@ struct AuthenticationView: View { let challenge: QueuedChallenge var body: some View { - fatalError() // switch challenge { // case let challenge as QueuedArcGISChallenge: //// Sheet { @@ -46,6 +45,28 @@ struct AuthenticationView: View { // default: // fatalError() // } + + switch challenge { + case let challenge as QueuedArcGISChallenge: + modifier(UsernamePasswordViewModifier(challenge: challenge)) + case let challenge as QueuedURLChallenge: + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + modifier(TrustHostViewModifier(challenge: challenge)) + case NSURLAuthenticationMethodClientCertificate: + modifier(CertificatePickerViewModifier(challenge: challenge)) + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: + modifier(UsernamePasswordViewModifier(challenge: challenge)) + default: + fatalError() + } + default: + fatalError() + } } } @@ -59,7 +80,6 @@ public extension View { struct AuthenticationModifier: ViewModifier { @ObservedObject var authenticator: Authenticator - @State private var isPresented = false @ViewBuilder func body(content: Content) -> some View { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index b42d604d6..1b5685f95 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -68,7 +68,6 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { - print("-- challenged...") if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, let url = queuedArcGISChallenge.arcGISChallenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { From 677dfe94015c107104cbeac5d95a928cfcd16240 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 19:27:29 -0600 Subject: [PATCH 0932/1722] remove invisible view --- .../Authentication/AuthenticationView.swift | 46 +++++++++++-------- .../Authentication/InvisibleView.swift | 46 +++++++------------ .../Authentication/UsernamePasswordView.swift | 9 ++-- 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 5fc659157..5083c8e54 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -84,30 +84,36 @@ struct AuthenticationModifier: ViewModifier { @ViewBuilder func body(content: Content) -> some View { if let challenge = authenticator.currentChallenge { - switch challenge { - case let challenge as QueuedArcGISChallenge: + authenticationView(for: challenge, content: content) + } + else { + content + } + } + + @ViewBuilder + func authenticationView(for challenge: QueuedChallenge, content: Content) -> some View { + switch challenge { + case let challenge as QueuedArcGISChallenge: + content.modifier(UsernamePasswordViewModifier(challenge: challenge)) + case let challenge as QueuedURLChallenge: + switch challenge.urlChallenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodServerTrust: + content.modifier(TrustHostViewModifier(challenge: challenge)) + case NSURLAuthenticationMethodClientCertificate: + content.modifier(CertificatePickerViewModifier(challenge: challenge)) + case NSURLAuthenticationMethodDefault, + NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodHTMLForm, + NSURLAuthenticationMethodHTTPBasic, + NSURLAuthenticationMethodHTTPDigest: content.modifier(UsernamePasswordViewModifier(challenge: challenge)) - case let challenge as QueuedURLChallenge: - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: - content.modifier(TrustHostViewModifier(challenge: challenge)) - case NSURLAuthenticationMethodClientCertificate: - content.modifier(CertificatePickerViewModifier(challenge: challenge)) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: - content.modifier(UsernamePasswordViewModifier(challenge: challenge)) - default: - fatalError() - } default: fatalError() } - } - else { - content + default: + fatalError() } } + } diff --git a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift index c1b978aee..b68bb609c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift @@ -13,38 +13,24 @@ import SwiftUI -struct InvisibleView: View { - var body: some View { - Color.clear.frame(width: 0, height: 0, alignment: .bottom) +@MainActor +struct SheetViewModifier: ViewModifier { + @State private var isPresented = false + var sheetContent: () -> SheetContent + + func body(content: Content) -> some View { + withAnimation { + content + .task { isPresented = true } + .sheet(isPresented: $isPresented, content: sheetContent) + } } } -struct Sheet: View { - @State private var isPresented: Bool = true - var onDismiss: (() -> Void)? - var content: () -> Content - - var body: some View { - InvisibleView() - .onAppear { isPresented = true } - .sheet( - isPresented: $isPresented, - onDismiss: onDismiss, - content: content - ) +extension View { + @MainActor + @ViewBuilder + func sheet(content: @escaping () -> Content) -> some View { + modifier(SheetViewModifier(sheetContent: content)) } } - -//public extension View { -// @MainActor -// @ViewBuilder -// func asSheet() -> some View { -// modifier(SheetModifier()) -// } -//} -// -//struct SheetModifier: ViewModifier { -// func body(content: Content) -> some View { -// Sheet(isPresented: true, content: { content } ) -// } -//} diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 4c9972628..1d945a252 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -37,10 +37,13 @@ struct UsernamePasswordViewModifier: ViewM viewModel = TokenCredentialViewModel(challenge: challenge) } + @State var isPresented = false + func body(content: Content) -> some View { - content.sheet(isPresented: .constant(true)) { - UsernamePasswordView(viewModel: viewModel) - } + content + .sheet { + UsernamePasswordView(viewModel: viewModel) + } } } From d39935e056fcab257ebf4d1ea59874ba92c06067 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 19:29:32 -0600 Subject: [PATCH 0933/1722] cleanup --- .../Components/Authentication/AuthenticationView.swift | 1 - .../{InvisibleView.swift => SheetViewModifier.swift} | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{InvisibleView.swift => SheetViewModifier.swift} (89%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 5083c8e54..e79a1dfdd 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -115,5 +115,4 @@ struct AuthenticationModifier: ViewModifier { fatalError() } } - } diff --git a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift similarity index 89% rename from Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift rename to Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift index b68bb609c..d1b97fc6e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/InvisibleView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift @@ -13,7 +13,6 @@ import SwiftUI -@MainActor struct SheetViewModifier: ViewModifier { @State private var isPresented = false var sheetContent: () -> SheetContent @@ -28,9 +27,7 @@ struct SheetViewModifier: ViewModifier { } extension View { - @MainActor - @ViewBuilder - func sheet(content: @escaping () -> Content) -> some View { + @ViewBuilder func sheet(content: @escaping () -> Content) -> some View { modifier(SheetViewModifier(sheetContent: content)) } } From c4261ae8dd469677ee14ffe0a280e91e4985582a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 19:47:44 -0600 Subject: [PATCH 0934/1722] remove main actors --- .../Components/Authentication/AuthenticationView.swift | 2 +- .../Authentication/CertificatePickerView.swift | 9 +++------ .../Components/Authentication/SheetViewModifier.swift | 6 ++++-- .../Components/Authentication/UsernamePasswordView.swift | 4 +--- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index e79a1dfdd..7bd8a7902 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -71,9 +71,9 @@ struct AuthenticationView: View { } public extension View { - @MainActor @ViewBuilder func authentication(authenticator: Authenticator) -> some View { +// overlay(Color.clear.frame(width: 0, height: 0, alignment: .bottom) .modifier(AuthenticationModifier(authenticator: authenticator))) modifier(AuthenticationModifier(authenticator: authenticator)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift index 0f3068054..49e7aa2a8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift @@ -14,7 +14,7 @@ import SwiftUI import UniformTypeIdentifiers -@MainActor final private class CertificatePickerViewModel: ObservableObject { +final private class CertificatePickerViewModel: ObservableObject { let challengingHost: String let challenge: QueuedURLChallenge @@ -39,8 +39,6 @@ import UniformTypeIdentifiers } } - -@MainActor struct CertificatePickerViewModifier: ViewModifier { init(challenge: QueuedURLChallenge) { viewModel = CertificatePickerViewModel(challenge: challenge) @@ -54,7 +52,7 @@ struct CertificatePickerViewModifier: ViewModifier { func body(content: Content) -> some View { content - .promptBrowseCertificateView( + .promptBrowseCertificate( isPresented: $showPrompt, host: viewModel.challengingHost, onContinue: { @@ -89,9 +87,8 @@ private extension UTType { } private extension View { - @MainActor @ViewBuilder - func promptBrowseCertificateView( + func promptBrowseCertificate( isPresented: Binding, host: String, onContinue: @escaping () -> Void, diff --git a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift index d1b97fc6e..fbf07710f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift @@ -14,7 +14,8 @@ import SwiftUI struct SheetViewModifier: ViewModifier { - @State private var isPresented = false + @State + private var isPresented = false var sheetContent: () -> SheetContent func body(content: Content) -> some View { @@ -27,7 +28,8 @@ struct SheetViewModifier: ViewModifier { } extension View { - @ViewBuilder func sheet(content: @escaping () -> Content) -> some View { + @ViewBuilder + func sheet(content: @escaping () -> Content) -> some View { modifier(SheetViewModifier(sheetContent: content)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index 1d945a252..cbc9284a9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -14,7 +14,7 @@ import SwiftUI import ArcGIS -@MainActor protocol UsernamePasswordViewModel: ObservableObject { +protocol UsernamePasswordViewModel: ObservableObject { var username: String { get set } var password: String { get set } var signinButtonEnabled: Bool { get } @@ -25,7 +25,6 @@ import ArcGIS func cancel() } -@MainActor struct UsernamePasswordViewModifier: ViewModifier { let viewModel: ViewModel @@ -181,7 +180,6 @@ class MockUsernamePasswordViewModel: UsernamePasswordViewModel { } } -@MainActor class TokenCredentialViewModel: UsernamePasswordViewModel { private let challenge: QueuedArcGISChallenge From 7e8750c3282f01265fa1694e1c3d2d96bb71bc6e Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 19:50:10 -0600 Subject: [PATCH 0935/1722] cleanup --- .../Components/Authentication/QueuedChallenge.swift | 6 ++---- .../Components/Authentication/SheetViewModifier.swift | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 3686b96a3..bf81e233a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -37,8 +37,7 @@ final class QueuedArcGISChallenge: QueuedChallenge { /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. - @Streamed - private var _response: Response? + @Streamed private var _response: Response? var response: Response { get async { @@ -78,8 +77,7 @@ final class QueuedURLChallenge: QueuedChallenge { /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. - @Streamed - private var _response: (Response)? + @Streamed private var _response: (Response)? var response: Response { get async { diff --git a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift index fbf07710f..d446a3d6a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift @@ -14,8 +14,7 @@ import SwiftUI struct SheetViewModifier: ViewModifier { - @State - private var isPresented = false + @State private var isPresented = false var sheetContent: () -> SheetContent func body(content: Content) -> some View { From 3815762d09e406798ed2b89aa579f23ad61bc6d2 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 20:07:04 -0600 Subject: [PATCH 0936/1722] fix ui bugs --- .../Authentication/AuthenticationView.swift | 78 +++++-------------- 1 file changed, 19 insertions(+), 59 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift index 7bd8a7902..3b4ab3691 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift @@ -13,72 +13,32 @@ import SwiftUI -struct AuthenticationView: View { - init(challenge: QueuedChallenge) { - self.challenge = challenge - } - - let challenge: QueuedChallenge - - var body: some View { -// switch challenge { -// case let challenge as QueuedArcGISChallenge: -//// Sheet { -//// UsernamePasswordView(challenge: challenge) -//// } -// case let challenge as QueuedURLChallenge: -// switch challenge.urlChallenge.protectionSpace.authenticationMethod { -// case NSURLAuthenticationMethodServerTrust: -// //TrustHostView(challenge: challenge) -// fatalError() -// case NSURLAuthenticationMethodClientCertificate: -// CertificatePickerView(challenge: challenge) -// case NSURLAuthenticationMethodDefault, -// NSURLAuthenticationMethodNTLM, -// NSURLAuthenticationMethodHTMLForm, -// NSURLAuthenticationMethodHTTPBasic, -// NSURLAuthenticationMethodHTTPDigest: -// UsernamePasswordView(challenge: challenge) -// default: -// fatalError() -// } -// default: -// fatalError() -// } - - switch challenge { - case let challenge as QueuedArcGISChallenge: - modifier(UsernamePasswordViewModifier(challenge: challenge)) - case let challenge as QueuedURLChallenge: - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: - modifier(TrustHostViewModifier(challenge: challenge)) - case NSURLAuthenticationMethodClientCertificate: - modifier(CertificatePickerViewModifier(challenge: challenge)) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: - modifier(UsernamePasswordViewModifier(challenge: challenge)) - default: - fatalError() - } - default: - fatalError() - } +public extension View { + @ViewBuilder + func authentication(authenticator: Authenticator) -> some View { + modifier(InvisibleOverlayModifier(authenticator: authenticator)) } } -public extension View { +/// This is an intermediate modifier that overlays an always present invisible view. +/// This view is necessary because of SwiftUI behavior that prevent the UI from functioning +/// properly when showing and hiding sheets. The sheets need to be off of a view that is always +/// present. +private struct InvisibleOverlayModifier: ViewModifier { + @ObservedObject var authenticator: Authenticator + @ViewBuilder - func authentication(authenticator: Authenticator) -> some View { -// overlay(Color.clear.frame(width: 0, height: 0, alignment: .bottom) .modifier(AuthenticationModifier(authenticator: authenticator))) - modifier(AuthenticationModifier(authenticator: authenticator)) + func body(content: Content) -> some View { + ZStack { + content + Color.clear + .frame(width: 0, height: 0) + .modifier(AuthenticationModifier(authenticator: authenticator)) + } } } -struct AuthenticationModifier: ViewModifier { +private struct AuthenticationModifier: ViewModifier { @ObservedObject var authenticator: Authenticator @ViewBuilder From 39354b3219053fa0bc953312cb47601ec3f60c78 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 20:10:00 -0600 Subject: [PATCH 0937/1722] cleanup --- .../Components/Authentication/UsernamePasswordView.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift index cbc9284a9..67fcf4407 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift @@ -40,9 +40,7 @@ struct UsernamePasswordViewModifier: ViewM func body(content: Content) -> some View { content - .sheet { - UsernamePasswordView(viewModel: viewModel) - } + .sheet { UsernamePasswordView(viewModel: viewModel) } } } From a1cbf40c800a0678fb5f2edfdf561fa2ace64c1a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 20:15:28 -0600 Subject: [PATCH 0938/1722] cleanup --- .../{AuthenticationView.swift => AuthenticationModifier.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{AuthenticationView.swift => AuthenticationModifier.swift} (100%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Authentication/AuthenticationView.swift rename to Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift From 693a32914ba65533befa80339a8bed9e4c9606db Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 20:54:57 -0600 Subject: [PATCH 0939/1722] hack fix for multiple challenges --- .../Components/Authentication/Authenticator.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 1b5685f95..617e0c8df 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -68,6 +68,9 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { + // Give chance to stop presenting last view for the case of queued up challenges. + try? await Task.sleep(nanoseconds: 250 * 1_000_000) + if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, let url = queuedArcGISChallenge.arcGISChallenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { From 5bb71ec4cd4bc51010a366b5c176ffd09c565780 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 23 May 2022 21:01:09 -0600 Subject: [PATCH 0940/1722] fix already presenting bug --- .../Components/Authentication/Authenticator.swift | 3 --- .../Components/Authentication/SheetViewModifier.swift | 4 ++++ .../{TrustHostView.swift => TrustHostViewModifier.swift} | 9 ++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{TrustHostView.swift => TrustHostViewModifier.swift} (71%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 617e0c8df..1b5685f95 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -68,9 +68,6 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { - // Give chance to stop presenting last view for the case of queued up challenges. - try? await Task.sleep(nanoseconds: 250 * 1_000_000) - if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, let url = queuedArcGISChallenge.arcGISChallenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { diff --git a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift index d446a3d6a..bb26fb6e7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift @@ -14,6 +14,10 @@ import SwiftUI struct SheetViewModifier: ViewModifier { + // Even though we will present it right away we need to use a state variable for this. + // Using a constant has 2 issues. One, it won't animate. Two, when challenging for multiple + // endpoints at a time, and the challenges stack up, you can end up with a "already presenting" + // error. @State private var isPresented = false var sheetContent: () -> SheetContent diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift similarity index 71% rename from Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift rename to Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index b97bbec5a..507895958 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -24,9 +24,16 @@ struct TrustHostViewModifier: ViewModifier { challenge.urlChallenge.protectionSpace.host } + // Even though we will present it right away we need to use a state variable for this. + // Using a constant has 2 issues. One, it won't animate. Two, when challenging for multiple + // endpoints at a time, and the challenges stack up, you can end up with a "already presenting" + // error. + @State private var isPresented: Bool = false + func body(content: Content) -> some View { content - .alert("Certificate Trust Warning", isPresented: .constant(true), presenting: challenge) { _ in + .task { isPresented = true } // Present the alert right away + .alert("Certificate Trust Warning", isPresented: $isPresented, presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { challenge.resume(with: .trustHost) } From 7161b1f9f29faee5cbcadeb4fb30d726e14452b4 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 24 May 2022 12:41:49 -0600 Subject: [PATCH 0941/1722] add property to specify whether to prompt for untrusted hosts --- Examples/Examples/AuthenticationExampleView.swift | 1 + .../Components/Authentication/Authenticator.swift | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 648fb8be1..f899ee38d 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,6 +17,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( + promptForUntrustedHosts: true //oAuthConfigurations: [.arcgisDotCom] ) @State var previousApiKey: APIKey? diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 1b5685f95..cf80e51a4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -20,10 +20,13 @@ public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] var trustedHosts: [String] = [] var hasPersistentStore: Bool + var promptForUntrustedHosts: Bool public init( + promptForUntrustedHosts: Bool = false, oAuthConfigurations: [OAuthConfiguration] = [] ) { + self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations hasPersistentStore = false Task { await observeChallengeQueue() } @@ -142,8 +145,10 @@ extension Authenticator: AuthenticationChallengeHandler { // If the host is already trusted, then continue trusting it. return (.useCredential, URLCredential(trust: trust)) } else if !trust.isRecoverableTrustFailure { - // See if the challenge is a recoverable trust failure, if so then we can - // challenge the user. If not, then we perform default handling. + // If not a recoverable trust failure then we perform default handling. + return (.performDefaultHandling, nil) + } else if !promptForUntrustedHosts { + // If we aren't allowed to prompt for untrusted hosts then perform default handling. return (.performDefaultHandling, nil) } } From 16454e506c029c36a3d7a4af8a4d3637ddb9f355 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 24 May 2022 13:12:23 -0600 Subject: [PATCH 0942/1722] file name changes --- .../Components/Authentication/Authenticator.swift | 3 +++ ...ckerView.swift => CertificatePickerViewModifier.swift} | 0 ...swordView.swift => UsernamePasswordViewModifier.swift} | 8 ++------ 3 files changed, 5 insertions(+), 6 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{CertificatePickerView.swift => CertificatePickerViewModifier.swift} (100%) rename Sources/ArcGISToolkit/Components/Authentication/{UsernamePasswordView.swift => UsernamePasswordViewModifier.swift} (96%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index cf80e51a4..862d9682c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -71,6 +71,9 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { + // A yield here alleviate the already presenting bug. + await Task.yield() + if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, let url = queuedArcGISChallenge.arcGISChallenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Authentication/CertificatePickerView.swift rename to Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift similarity index 96% rename from Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift rename to Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 67fcf4407..0cc30361c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -204,9 +204,7 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { func signIn() { formEnabled = false - Task { - challenge.resume(with: .tokenCredential(username: username, password: password)) - } + challenge.resume(with: .tokenCredential(username: username, password: password)) } func cancel() { @@ -241,9 +239,7 @@ class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { func signIn() { formEnabled = false - Task { - challenge.resume(with: .userCredential(username: username, password: password)) - } + challenge.resume(with: .userCredential(username: username, password: password)) } func cancel() { From 73b8f32651670f79151ad59bf0fab7d04b98f977 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 24 May 2022 13:54:44 -0600 Subject: [PATCH 0943/1722] update isPresented --- .../Components/Authentication/TrustHostViewModifier.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index 507895958..99a3f969a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -35,9 +35,11 @@ struct TrustHostViewModifier: ViewModifier { .task { isPresented = true } // Present the alert right away .alert("Certificate Trust Warning", isPresented: $isPresented, presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { + isPresented = false challenge.resume(with: .trustHost) } Button("Cancel", role: .cancel) { + isPresented = false challenge.cancel() } } message: { _ in From c77f7515f72377a20b82697a8b03d0b1eb087725 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 24 May 2022 14:25:35 -0600 Subject: [PATCH 0944/1722] make sure to hide the view --- .../CertificatePickerViewModifier.swift | 6 ++++++ .../UsernamePasswordViewModifier.swift | 15 +++++++++++---- .../Components/Search/SearchViewModel.swift | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 49e7aa2a8..10bffce4e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -56,15 +56,19 @@ struct CertificatePickerViewModifier: ViewModifier { isPresented: $showPrompt, host: viewModel.challengingHost, onContinue: { + showPrompt = false showPicker = true }, onCancel: { + showPrompt = false viewModel.cancel() }) .sheet(isPresented: $showPicker) { DocumentPickerView(contentTypes: [.pfx]) { viewModel.certificateURL = $0 + showPicker = false showPassword = true } onCancel: { + showPicker = false viewModel.cancel() } .edgesIgnoringSafeArea(.bottom) @@ -72,8 +76,10 @@ struct CertificatePickerViewModifier: ViewModifier { } .sheet(isPresented: $showPassword) { EnterPasswordView(password: $viewModel.password) { + showPassword = false viewModel.signIn() } onCancel: { + showPassword = false viewModel.cancel() } .edgesIgnoringSafeArea(.bottom) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 0cc30361c..e1abf8e66 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -40,16 +40,19 @@ struct UsernamePasswordViewModifier: ViewM func body(content: Content) -> some View { content - .sheet { UsernamePasswordView(viewModel: viewModel) } + .task { isPresented = true } + .sheet(isPresented: $isPresented) { UsernamePasswordView(viewModel: viewModel, isPresented: $isPresented) } } } private struct UsernamePasswordView: View { - init(viewModel: ViewModel) { + init(viewModel: ViewModel, isPresented: Binding) { _viewModel = ObservedObject(initialValue: viewModel) + self.isPresented = isPresented } @ObservedObject private var viewModel: ViewModel + private var isPresented: Binding /// The focused field. @FocusState private var focusedField: Field? @@ -92,7 +95,10 @@ private struct UsernamePasswordView: View .interactiveDismissDisabled() .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { viewModel.cancel() } + Button("Cancel") { + isPresented.wrappedValue = false + viewModel.cancel() + } } } .onAppear { @@ -118,6 +124,7 @@ private struct UsernamePasswordView: View private var signinButton: some View { Button(action: { + isPresented.wrappedValue = false viewModel.signIn() }, label: { if viewModel.formEnabled { @@ -137,7 +144,7 @@ private struct UsernamePasswordView: View struct UsernamePasswordView_Previews: PreviewProvider { static var previews: some View { - UsernamePasswordView(viewModel: MockUsernamePasswordViewModel(challengingHost: "arcgis.com")) + UsernamePasswordView(viewModel: MockUsernamePasswordViewModel(challengingHost: "arcgis.com"), isPresented: .constant(true)) } } diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 1f4441b46..a8b0e81c6 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -414,7 +414,7 @@ private extension Symbol { static func searchResult() -> MarkerSymbol { let image = UIImage.mapPin let symbol = PictureMarkerSymbol(image: image) - symbol.offsetY = Float(image.size.height / 2.0) + symbol.offsetY = CGFloat(image.size.height / 2.0) return symbol } } From 75e95784ea3e2d1d016d9485e78b2ca1c6723c3b Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 24 May 2022 16:01:09 -0600 Subject: [PATCH 0945/1722] doc --- Examples/Examples/AuthenticationExampleView.swift | 3 +-- .../Components/Authentication/Authenticator.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index f899ee38d..f732314f0 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -17,8 +17,7 @@ import ArcGISToolkit struct AuthenticationExampleView: View { @ObservedObject var authenticator = Authenticator( - promptForUntrustedHosts: true - //oAuthConfigurations: [.arcgisDotCom] + promptForUntrustedHosts: true//, oAuthConfigurations: [.arcgisDotCom] ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 862d9682c..27bb65c89 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -71,7 +71,7 @@ public final class Authenticator: ObservableObject { private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { - // A yield here alleviate the already presenting bug. + // A yield here helps alleviate the already presenting bug. await Task.yield() if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, From 21309df6aa1c9427f1b5a639d3e8eb0ae683557d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 24 May 2022 16:39:10 -0600 Subject: [PATCH 0946/1722] use correct method to clear --- Examples/Examples/AuthenticationExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index f732314f0..0846e88d6 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -35,7 +35,7 @@ struct AuthenticationExampleView: View { Button("Clear Credential Store") { items = [] Task { - await ArcGISURLSession.credentialStore.removeAll() + await authenticator.clearCredentialStores() items = AuthenticationItem.makeAll() } } From b1823ee91636fa2f05b8fa65dc13273a9d369149 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 24 May 2022 16:18:14 -0700 Subject: [PATCH 0947/1722] Update SearchViewModel.swift This PR fixes a build error introduced with the latest SDK --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index 1f4441b46..2b6f164b8 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -414,7 +414,7 @@ private extension Symbol { static func searchResult() -> MarkerSymbol { let image = UIImage.mapPin let symbol = PictureMarkerSymbol(image: image) - symbol.offsetY = Float(image.size.height / 2.0) + symbol.offsetY = image.size.height / 2.0 return symbol } } From e3e78feeaafc6d60ba3a1599578ad1231805001c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 25 May 2022 09:23:05 -0600 Subject: [PATCH 0948/1722] add keychain --- .../Authentication/Authenticator.swift | 76 +++- .../Components/Authentication/Keychain.swift | 377 ++++++++++++++++++ 2 files changed, 439 insertions(+), 14 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/Keychain.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 27bb65c89..76c20871a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -19,8 +19,9 @@ import Combine public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] var trustedHosts: [String] = [] - var hasPersistentStore: Bool + var urlCredentialPersistence: URLCredential.Persistence = .forSession var promptForUntrustedHosts: Bool +// var foo: AnyObject public init( promptForUntrustedHosts: Bool = false, @@ -28,7 +29,9 @@ public final class Authenticator: ObservableObject { ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations - hasPersistentStore = false +// foo = NotificationCenter.default.addObserver(forName: .NSURLCredentialStorageChanged, object: URLCredentialStorage.shared, queue: .main) { notification in +// print("-- creds changed: \(notification.userInfo)") +// } Task { await observeChallengeQueue() } } @@ -42,7 +45,7 @@ public final class Authenticator: ObservableObject { /// within the keychain. /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. public func synchronizeWithKeychain( - access: KeychainAccess, + access: ArcGIS.KeychainAccess, groupIdentifier: String? = nil, isCloudSynchronizable: Bool = false ) async throws { @@ -51,7 +54,7 @@ public final class Authenticator: ObservableObject { groupIdentifier: groupIdentifier, isSynchronizable: isCloudSynchronizable ) - hasPersistentStore = true + urlCredentialPersistence = isCloudSynchronizable ? .synchronizable : .forSession } public func clearCredentialStores() async { @@ -81,6 +84,12 @@ public final class Authenticator: ObservableObject { // Creating the OAuth credential will present the OAuth login view. queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) } else { + + if let challenge = queuedChallenge as? QueuedArcGISChallenge, + let previous = await ArcGISURLSession.credentialStore.credential(for: challenge.arcGISChallenge.request.url!) { + print("-- previous: \(previous)") + } + // Set the current challenge, this should present the appropriate view. currentChallenge = queuedChallenge @@ -156,12 +165,14 @@ extension Authenticator: AuthenticationChallengeHandler { } } + defer { + URLCredentialStorage.shared.printAll() + } + // Queue up the url challenge. let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) subject.send(queuedChallenge) - let persistence: URLCredential.Persistence = hasPersistentStore ? .permanent : .forSession - // Respond accordingly. switch await queuedChallenge.response { case .cancel: @@ -174,12 +185,12 @@ extension Authenticator: AuthenticationChallengeHandler { return (.performDefaultHandling, nil) } case .userCredential(let user, let password): - return (.useCredential, URLCredential(user: user, password: password, persistence: persistence)) + return (.useCredential, URLCredential(user: user, password: password, persistence: urlCredentialPersistence)) case .certificate(let url, let password): do { return ( .useCredential, - try URLCredential.urlCredentialForCertificate(at: url, password: password, persistence: persistence) + try await URLCredential.urlCredential(forCertificateAt: url, password: password) ) } catch { return (.performDefaultHandling, nil) @@ -204,6 +215,17 @@ extension URLCredentialStorage { } } } + + func printAll() { + allCredentials.forEach { (protectionSpace: URLProtectionSpace, usernamesToCredentials: [String : URLCredential]) in + for credential in usernamesToCredentials.values { + print(" -- credential: \(credential)") + print(" -- certificates: \(credential.certificates)") + print(" -- identity: \(credential.identity)") + print(" -- password: \(credential.password)") + } + } + } } private extension URLCredential { @@ -221,12 +243,19 @@ private extension URLCredential { } } - static func urlCredentialForCertificate( - at fileURL: URL, - password: String, - persistence: URLCredential.Persistence + static func urlCredential( + forCertificateAt fileURL: URL, + password: String + ) async throws -> URLCredential { + return try urlCredential( + forCertificateWithData: try await Data(asyncWithContentsOf: fileURL), + password: password + ) + } + static func urlCredential( + forCertificateWithData data: Data, + password: String ) throws -> URLCredential { - let data = try Data(contentsOf: fileURL) let options = [kSecImportExportPassphrase: password] var rawItems: CFArray? @@ -247,14 +276,33 @@ private extension URLCredential { return URLCredential( identity: identity, certificates: certificates, - persistence: persistence + // Persistence is not supported at all at this point by apple for certificate credentials. + // So we can just pass `.none`. + persistence: .none ) } } +extension Data { + init(asyncWithContentsOf fileURL: URL) async throws { + let task = Task { + try Data(contentsOf: fileURL) + } + self = try await task.value + } +} + extension URLCredential.CertificateImportError { // The message for this error. var message: String { (SecCopyErrorMessageString(status, nil) as String?) ?? "" } } + +struct CertificateCredential { + let host: String + let data: Data + let password: String +} + +extension CertificateCredential: Codable {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Keychain.swift b/Sources/ArcGISToolkit/Components/Authentication/Keychain.swift new file mode 100644 index 000000000..e9e27e022 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/Keychain.swift @@ -0,0 +1,377 @@ +// +// 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 + +/// An actor that provides access to the device keychain. +actor Keychain { + /// The shared instance. + static let shared = Keychain() + + private init() {} + + /// Creates parameters for storing a generic password item in the keychain. + /// - Parameters: + /// - identifier: The unique identifier for the item. + /// - value: The data value of the item. + /// - access: When the item can be accessed. + /// - service: The associated service of the item. + /// - label: A label for the item. + /// - groupIdentifier: The identifier of the group that the item should be stored in. + /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. + static func makeStoreItemParameters( + identifier: String, + value: Data, + access: KeychainAccess, + service: String?, + label: String?, + groupIdentifier: String?, + isSynchronizable: Bool + ) -> CFDictionary { + var parameters = [CFString : Any]() + parameters[kSecClass] = kSecClassGenericPassword + parameters[kSecAttrAccount] = identifier + if let service = service { + parameters[kSecAttrService] = service + } + if let label = label { + parameters[kSecAttrLabel] = label + } + parameters[kSecValueData] = value + parameters[kSecAttrAccessible] = access.value + parameters[kSecAttrAccessGroup] = groupIdentifier + parameters[kSecAttrSynchronizable] = isSynchronizable + // This is highly recommended to be set to true for all keychain operations. + // This key helps to improve the portability of your code across platforms. + // More information here: https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain + parameters[kSecUseDataProtectionKeychain] = true + return parameters as CFDictionary + } + + /// Stores a generic password item in the keychain. + /// - Remark: If no group is specified then the item will be stored in the default group. + /// To know more about what the default group would be you can find information about that here: + /// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps. + /// - Parameters: + /// - identifier: The unique identifier for the item to be stored under. + /// - value: The data value of the item. + /// - access: When the item can be accessed. + /// - service: The service that the item is associated with. + /// - label: A label for the item. + /// - groupIdentifier: The identifier of the group that the item should be stored in. + /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. + func storeItem( + identifier: String, + value: Data, + access: KeychainAccess, + service: URL? = nil, + label: String? = nil, + groupIdentifier: String? = nil, + isSynchronizable: Bool = false + ) throws { + try removeItem(withIdentifier: identifier) + let parameters = Self.makeStoreItemParameters( + identifier: identifier, + value: value, + access: access, + service: service?.absoluteString, + label: label, + groupIdentifier: groupIdentifier, + isSynchronizable: isSynchronizable + ) + let status = SecItemAdd(parameters, nil) + guard status == errSecSuccess else { + throw KeychainError(status: status)! + } + } + + /// Creates parameters for retrieving items from the keychain. + /// - Parameters: + /// - identifier: The identifier of the item to retrieve. + /// - label: The label of the item to retrieve. + /// - groupIdentifier: The identifier of the group to limit the search to. If `nil` then the + /// search will not be limited to a particular group. + /// - limitOne: A value indicating if only a single result should be returned. + static func makeGetItemsParameters( + identifier: String?, + label: String?, + groupIdentifier: String?, + limitOne: Bool + ) -> CFDictionary { + var parameters = [CFString : Any]() + parameters[kSecClass] = kSecClassGenericPassword + parameters[kSecReturnAttributes] = true + parameters[kSecReturnData] = true + parameters[kSecAttrSynchronizable] = kSecAttrSynchronizableAny + if let identifier = identifier { + parameters[kSecAttrAccount] = identifier + } + if limitOne { + parameters[kSecMatchLimit] = kSecMatchLimitOne + } else { + parameters[kSecMatchLimit] = kSecMatchLimitAll + } + if let label = label { + parameters[kSecAttrLabel] = label + } + if let groupIdentifier = groupIdentifier { + parameters[kSecAttrAccessGroup] = groupIdentifier + } + parameters[kSecUseDataProtectionKeychain] = true + return parameters as CFDictionary + } + + /// Retrieves a generic password item from the keychain with a given identifier. + /// - Parameters: + /// - identifier: The identifier of the item to retrieve. + /// - groupIdentifier: The identifier of the group to limit the search to. If `nil` then the + /// search will not be limited to a particular group. + /// - Returns: The found item or `nil` if an item with the specified identifier could + /// not be found. + func item( + withIdentifier identifier: String, + inGroup groupIdentifier: String? = nil + ) throws -> KeychainItem? { + let parameters = Self.makeGetItemsParameters( + identifier: identifier, + label: nil, + groupIdentifier: groupIdentifier, + limitOne: true + ) + var result: CFTypeRef? = nil + let status = SecItemCopyMatching(parameters, &result) + switch status { + case errSecSuccess: + return KeychainItem(attributes: (result as! [CFString: Any])) + case errSecItemNotFound: + return nil + default: + throw KeychainError(status: status)! + } + } + + /// Retrieves all generic password items in the keychain with a specific label. + /// - Parameters: + /// - label: The label of the items to be retrieved. Or `nil` if all + /// generic password items should be retrieved. + /// - groupIdentifier: The identifier of the group to limit the search to. If `nil` then the + /// search will not be limited to a particular group. + /// - Returns: The found items. + func items( + labeled label: String? = nil, + inGroup groupIdentifier: String? = nil + ) throws -> [KeychainItem] { + let parameters = Self.makeGetItemsParameters( + identifier: nil, + label: label, + groupIdentifier: groupIdentifier, + limitOne: false + ) + var result: CFTypeRef? = nil + let status = SecItemCopyMatching(parameters, &result) + switch status { + case errSecSuccess: + return (result as! [[CFString: Any]]) + .map(KeychainItem.init(attributes:)) + case errSecItemNotFound: + return [] + default: + throw KeychainError(status: status)! + } + } + + /// All generic password items in the keychain. + var items: [KeychainItem] { + get throws { try items() } + } + + /// Make the parameters for a remove item keychain operation. + /// - Parameters: + /// - identifier: The identifier of the item to remove. + /// - label: A label for the item. + /// - groupIdentifier: The identifier of the group of the item to remove. + static func makeRemoveItemParameters( + identifier: String?, + label: String?, + groupIdentifier: String? + ) -> CFDictionary { + var parameters = [CFString : Any]() + if let identifier = identifier { + parameters[kSecAttrAccount] = identifier + } + parameters[kSecClass] = kSecClassGenericPassword + if let label = label { + parameters[kSecAttrLabel] = label + } + if let groupIdentifier = groupIdentifier { + parameters[kSecAttrAccessGroup] = groupIdentifier + } + parameters[kSecAttrSynchronizable] = kSecAttrSynchronizableAny + parameters[kSecUseDataProtectionKeychain] = true + return parameters as CFDictionary + } + + /// Removes a generic password item with a given identifier from the keychain. + /// - Parameters + /// - identifier: The unique identifier that the item is stored under. + /// - groupIdentifier: The identifier of the group of the item to remove. + func removeItem( + withIdentifier identifier: String, + inGroup groupIdentifier: String? = nil + ) throws { + let parameters = Self.makeRemoveItemParameters( + identifier: identifier, + label: nil, + groupIdentifier: groupIdentifier + ) + let status = SecItemDelete(parameters) + switch status { + case errSecSuccess, errSecItemNotFound: + return + default: + throw KeychainError(status: status)! + } + } + + /// Removes a specified item from the keychain. + /// - Parameter item: The item to be removed. + func remove(item: KeychainItem) throws { + return try removeItem(withIdentifier: item.identifier, inGroup: item.groupIdentifier) + } + + /// Remove all generic password items with a specific label from the keychain. + /// - Parameters: + /// - label: The label of the items to be removed, or `nil` if all + /// items should be removed. + /// - groupIdentifier: The identifier of the group of the item to remove. + func removeItems( + labeled label: String? = nil, + inGroup groupIdentifier: String? = nil + ) throws { + let parameters = Self.makeRemoveItemParameters( + identifier: nil, + label: label, + groupIdentifier: groupIdentifier + ) + let status = SecItemDelete(parameters) + switch status { + case errSecSuccess, errSecItemNotFound: + return + default: + throw KeychainError(status: status)! + } + } +} + +private extension KeychainAccess { + /// The `CFString` value of the `Access`. + var value: CFString { + switch self { + case .afterFirstUnlock: + return kSecAttrAccessibleAfterFirstUnlock + case .afterFirstUnlockThisDeviceOnly: + return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + case .whenUnlocked: + return kSecAttrAccessibleWhenUnlocked + case .whenUnlockedThisDeviceOnly: + return kSecAttrAccessibleWhenUnlockedThisDeviceOnly + case .whenPasscodeSetThisDeviceOnly: + return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly + } + } +} + +private extension KeychainItem { + /// Initializes a keychain item with an attribute dictionary. + /// - Parameter attributes: The attributes returned from a keychain query by which an item + /// should be initialized with. + init(attributes: [CFString: Any]) { + self.init( + identifier: attributes[kSecAttrAccount]! as! String, + service: (attributes[kSecAttrService] as? String).flatMap({ URL(string:$0) }), + created: attributes[kSecAttrCreationDate] as? Date, + modified: attributes[kSecAttrModificationDate] as? Date, + label: attributes[kSecAttrLabel] as? String, + groupIdentifier: attributes[kSecAttrAccessGroup] as? String, + isSynchronizable: attributes[kSecAttrSynchronizable] as? Bool ?? false, + value: attributes["v_Data" as CFString] as? Data ?? Data() + ) + } +} + +/// An enum that describes when an item in the keychain is accessible. +public enum KeychainAccess { + case afterFirstUnlock + case afterFirstUnlockThisDeviceOnly + case whenUnlocked + case whenUnlockedThisDeviceOnly + case whenPasscodeSetThisDeviceOnly +} + +/// A value representing a generic password item in the keychain. +struct KeychainItem { + /// The unique identifier of the item. + var identifier: String + /// The service this item is associated with. + var service: URL? + /// The date this item was created. + var created: Date? + /// The date this item was last modified. + var modified: Date? + /// A label for this item. + var label: String? + /// The identifier of the group that the item is stored in. + var groupIdentifier: String? + /// A value indicating whether the item is synchronized with iCloud. + var isSynchronizable: Bool + /// The data value of this item. + var value: Data +} + +/// An error that can occur in a keychain operation. +struct KeychainError: Error, Hashable { + /// The backing status code for this error. + /// - Note: Status code numeric values can be found here: + /// https://opensource.apple.com/source/Security/Security-55471/libsecurity_keychain/lib/SecBase.h.auto.html + let status: OSStatus + + + /// Initializes a keychain error. This init will fail if the specified status is a success + /// status value. + /// - Parameter status: An `OSStatus`, usually the return value of a keychain operation. + init?(status: OSStatus) { + guard status != errSecSuccess else { return nil } + self.status = status + } +} + +extension KeychainError { + // The message for this error. + var message: String { + (SecCopyErrorMessageString(status, nil) as String?) ?? "" + } +} + +extension KeychainError { + /// The item was not found in the keychain. + static let itemNotFound = Self(status: errSecItemNotFound)! + /// One or more arguments specified were invalid. + static let invalidArgument = Self(status: errSecParam)! + /// An entitlement is missing. + static let missingEntitlement = Self(status: errSecMissingEntitlement)! +} From c96a37b5532a73fca0127f1db2656ff5f41c0586 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 25 May 2022 16:49:12 -0600 Subject: [PATCH 0949/1722] serialize pki credentials --- .../Examples/AuthenticationExampleView.swift | 3 + .../Authentication/Authenticator.swift | 47 +++++------ .../CertificateCredential.swift | 81 +++++++++++++++++++ 3 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 0846e88d6..547a09c6f 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -41,6 +41,9 @@ struct AuthenticationExampleView: View { } } .authentication(authenticator: authenticator) + .task { + try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) + } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .onAppear { ArcGISURLSession.challengeHandler = authenticator diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 76c20871a..553b79fcd 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -21,7 +21,7 @@ public final class Authenticator: ObservableObject { var trustedHosts: [String] = [] var urlCredentialPersistence: URLCredential.Persistence = .forSession var promptForUntrustedHosts: Bool -// var foo: AnyObject + var certificateStore: CertificateCredentialStore? public init( promptForUntrustedHosts: Bool = false, @@ -29,9 +29,7 @@ public final class Authenticator: ObservableObject { ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations -// foo = NotificationCenter.default.addObserver(forName: .NSURLCredentialStorageChanged, object: URLCredentialStorage.shared, queue: .main) { notification in -// print("-- creds changed: \(notification.userInfo)") -// } + self.certificateStore = nil Task { await observeChallengeQueue() } } @@ -54,7 +52,12 @@ public final class Authenticator: ObservableObject { groupIdentifier: groupIdentifier, isSynchronizable: isCloudSynchronizable ) - urlCredentialPersistence = isCloudSynchronizable ? .synchronizable : .forSession + certificateStore = try await CertificateCredentialStore( + access: .whenUnlockedThisDeviceOnly, + groupIdentifier: nil, + isSynchronizable: isCloudSynchronizable + ) + urlCredentialPersistence = isCloudSynchronizable ? .synchronizable : .permanent } public func clearCredentialStores() async { @@ -64,6 +67,9 @@ public final class Authenticator: ObservableObject { // Clear ArcGIS Credentials. await ArcGISURLSession.credentialStore.removeAll() + // Clear certificate store. + await certificateStore?.clear() + // Clear URLCredentials. URLCredentialStorage.shared.removeAllCredentials() @@ -165,8 +171,10 @@ extension Authenticator: AuthenticationChallengeHandler { } } - defer { - URLCredentialStorage.shared.printAll() + // Look in certificate store, use that credential if available. + if let certificateCredential = await certificateStore?.credential(for: challenge.protectionSpace), + let urlCredential = try? URLCredential.withCertificateCredential(certificateCredential) { + return (.useCredential, urlCredential) } // Queue up the url challenge. @@ -188,9 +196,11 @@ extension Authenticator: AuthenticationChallengeHandler { return (.useCredential, URLCredential(user: user, password: password, persistence: urlCredentialPersistence)) case .certificate(let url, let password): do { + let data = try await Data(asyncWithContentsOf: url) + await certificateStore?.add(credential: .init(host: challenge.protectionSpace.host, data: data, password: password)) return ( .useCredential, - try await URLCredential.urlCredential(forCertificateAt: url, password: password) + try URLCredential.urlCredential(forCertificateWithData: data, password: password) ) } catch { return (.performDefaultHandling, nil) @@ -215,17 +225,6 @@ extension URLCredentialStorage { } } } - - func printAll() { - allCredentials.forEach { (protectionSpace: URLProtectionSpace, usernamesToCredentials: [String : URLCredential]) in - for credential in usernamesToCredentials.values { - print(" -- credential: \(credential)") - print(" -- certificates: \(credential.certificates)") - print(" -- identity: \(credential.identity)") - print(" -- password: \(credential.password)") - } - } - } } private extension URLCredential { @@ -299,10 +298,8 @@ extension URLCredential.CertificateImportError { } } -struct CertificateCredential { - let host: String - let data: Data - let password: String +extension URLCredential { + static func withCertificateCredential(_ certificateCredential: CertificateCredential) throws -> URLCredential { + try URLCredential.urlCredential(forCertificateWithData: certificateCredential.data, password: certificateCredential.password) + } } - -extension CertificateCredential: Codable {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift new file mode 100644 index 000000000..f34d6f9c4 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift @@ -0,0 +1,81 @@ +// 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 Foundation + +struct CertificateCredential: Hashable { + let host: String + let data: Data + let password: String +} + +extension CertificateCredential: Codable {} + +final actor CertificateCredentialStore { + private var storage: [String: CertificateCredential] = [:] + + let keychain: Keychain? + let access: KeychainAccess? + let groupIdentifier: String? + let isSynchronizable: Bool + + static let label: String = "ArcGIS Certificate Credential" + + init() { + keychain = nil + groupIdentifier = nil + access = nil + isSynchronizable = false + } + + init(access: KeychainAccess, groupIdentifier: String? = nil, isSynchronizable: Bool = false) async throws { + keychain = .shared + self.access = access + self.groupIdentifier = groupIdentifier + self.isSynchronizable = isSynchronizable + + for item in try await Keychain.shared.items(labeled: Self.label, inGroup: groupIdentifier) { + let credential = try JSONDecoder().decode(CertificateCredential.self, from: item.value) + storage[credential.host] = credential + } + } + + func credential(for protectionSpace: URLProtectionSpace) -> CertificateCredential? { + guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate else { + return nil + } + return storage[protectionSpace.host] + } + + func add(credential: CertificateCredential) async { + storage[credential.host] = credential + if let keychain = keychain, let access = access { + try? await keychain.storeItem( + identifier: credential.host, + value: try JSONEncoder().encode(credential), + access: access, + label: Self.label, + groupIdentifier: groupIdentifier, + isSynchronizable: isSynchronizable + ) + } + } + + func clear() async { + storage.removeAll() + if let keychain = keychain { + try? await keychain.removeItems(labeled: Self.label, inGroup: groupIdentifier) + } + } +} + From ccfa7b5cd5ed931d9d906a91b971c7b4fbbc56b6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 07:27:07 -0700 Subject: [PATCH 0950/1722] Reorder FloorFilter initializer parameter documentation --- 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 483ca7056..731336db1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -20,9 +20,9 @@ import ArcGIS public struct FloorFilter: View { /// Creates a `FloorFilter`. /// - Parameters: + /// - floorManager: The floor manager used by the `FloorFilter`. /// - alignment: Determines the display configuration of Floor Filter elements. /// - automaticSelectionMode: The selection behavior of the floor filter. - /// - floorManager: The floor manager used by the `FloorFilter`. /// - viewpoint: Viewpoint updated when the selected site or facility changes. public init( floorManager: FloorManager, From f15cb8305bd18ab63f1a841f6ede7913f989c5ca Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 07:31:52 -0700 Subject: [PATCH 0951/1722] Change `filterWidth` to constant and CGFloat --- 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 731336db1..41cbadfd5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -49,7 +49,7 @@ public struct FloorFilter: View { private let alignment: Alignment /// The width of the level selector. - private var filterWidth: Double = 60 + private let filterWidth: CGFloat = 60 /// Button to open and close the site and facility selector. private var sitesAndFacilitiesButton: some View { From 23a19bc1c01ed5759c89a7523ce7dc9ece1f1704 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 07:44:13 -0700 Subject: [PATCH 0952/1722] Update FloorFilter.swift --- 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 41cbadfd5..a712f7bda 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -57,7 +57,7 @@ public struct FloorFilter: View { isSitesAndFacilitiesHidden.toggle() } label: { Image(systemName: "building.2") - .padding(EdgeInsets.esriInsets) + .padding(.esriInsets) } .frame(maxWidth: .infinity) } From 7ab03f7516d3593e087d43406bb522d045cfaf5d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:02:58 -0700 Subject: [PATCH 0953/1722] `topAligned` -> `isTopAligned` --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index a712f7bda..7e4becd17 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -66,14 +66,14 @@ public struct FloorFilter: View { @ViewBuilder private var levelSelector: some View { LevelSelector( levels: viewModel.sortedLevels, - isTopAligned: topAligned + isTopAligned: isTopAligned ) } /// A view that allows selecting between levels. private var floorFilter: some View { VStack { - if topAligned { + if isTopAligned { sitesAndFacilitiesButton if viewModel.hasLevelsToDisplay { Divider() @@ -103,7 +103,7 @@ public struct FloorFilter: View { } /// Indicates that the selector should be presented with a top oriented aligment configuration. - private var topAligned: Bool { + private var isTopAligned: Bool { alignment.vertical == .top } From 8b6a106d0dce08157d6742170091c7baf8735570 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:03:55 -0700 Subject: [PATCH 0954/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift Co-authored-by: Philip Ridgeway --- .../FloorFilter/FloorFilterAutomaticSelectionMode.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift index 177b985f4..12778410b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterAutomaticSelectionMode.swift @@ -19,6 +19,6 @@ public enum FloorFilterAutomaticSelectionMode { /// 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 GeoView's current viewpoint. + /// Never update selection based on the map or scene view's current viewpoint. case never } From ff29c5db8b1ac793eaf41ff5cb81e40e316feaa6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:16:01 -0700 Subject: [PATCH 0955/1722] Update FloorFilterViewModel.swift --- .../Components/FloorFilter/FloorFilterViewModel.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index bedabfb5b..8a63c52f1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -19,7 +19,7 @@ import SwiftUI @MainActor final class FloorFilterViewModel: ObservableObject { /// A selected site, floor, or level. - enum Selection { + enum Selection: Hashable { /// A selected site. case site(FloorSite) /// A selected facility. @@ -298,5 +298,3 @@ final class FloorFilterViewModel: ObservableObject { } } } - -extension FloorFilterViewModel.Selection: Hashable { } From 37fd0d89fe800b636db62da7c7423ee250e41014 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:16:54 -0700 Subject: [PATCH 0956/1722] Update LevelSelector.swift --- .../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 ab78bef8f..4daf4d8d3 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -22,7 +22,7 @@ struct LevelSelector: View { /// 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. - @State var isCollapsed: Bool = false + @State private var isCollapsed: Bool = false /// The levels to display. let levels: [FloorLevel] From 26e47cac28867b7b1d621211327dde1b327922fb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:17:51 -0700 Subject: [PATCH 0957/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift Co-authored-by: Philip Ridgeway --- .../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 4daf4d8d3..9d9132e10 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -27,7 +27,7 @@ struct LevelSelector: View { /// The levels to display. let levels: [FloorLevel] - /// Returns the short name of the currently selected level, the first level or "None" if none of the listed + /// 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 dbaaabe651c589895624dc0f9ec76ea2469f5284 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:23:57 -0700 Subject: [PATCH 0958/1722] Introduce `LevelNameFormat` --- .../FloorFilter/LevelSelector.swift | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 9d9132e10..4dc40549c 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -52,7 +52,8 @@ struct LevelSelector: View { } } else { Toggle(isOn: $isCollapsed) { - LevelText(levelName: selectedLevelName) + Text(selectedLevelName) + .modifier(LevelNameFormat()) } .toggleStyle(.selectedButton) } @@ -85,7 +86,8 @@ struct LevelsStack: View { } ) ) { - LevelText(levelName: level.shortName) + Text(level.shortName) + .modifier(LevelNameFormat()) } .toggleStyle(.selectableButton) } @@ -98,19 +100,6 @@ struct LevelsStack: View { } } -/// Intended to display the name of a level. -struct LevelText: View { - /// The name of the level to be displayed. - var levelName: String - - var body: some View { - Text(levelName) - .lineLimit(1) - .fixedSize() - .frame(minWidth: 40) - } -} - /// A button used to collapse the floor level list. struct CollapseButton: View { /// Allows the user to toggle the visibility of the site and facility selector. @@ -127,3 +116,12 @@ struct CollapseButton: View { } } } + +struct LevelNameFormat: ViewModifier { + func body(content: Content) -> some View { + content + .lineLimit(1) + .fixedSize() + .frame(minWidth: 40) + } +} From 1b514051657fa6f2dec3a4ce208eae730b59ff25 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:47:39 -0700 Subject: [PATCH 0959/1722] Update LevelSelector.swift --- .../FloorFilter/LevelSelector.swift | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 4dc40549c..a01d1d4d2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -41,13 +41,13 @@ struct LevelSelector: View { levels.count > 1 { VStack { if !isTopAligned { - CollapseButton(isCollapsed: $isCollapsed) + makeCollapseButton() Divider() } LevelsStack(levels: levels) if isTopAligned { Divider() - CollapseButton(isCollapsed: $isCollapsed) + makeCollapseButton() } } } else { @@ -58,6 +58,18 @@ struct LevelSelector: View { .toggleStyle(.selectedButton) } } + + /// A button used to collapse the floor level list. + @ViewBuilder func makeCollapseButton() -> some View { + Button { + withAnimation { + isCollapsed.toggle() + } + } label: { + Image(systemName: "xmark") + .padding(EdgeInsets.esriInsets) + } + } } /// A vertical list of floor levels. @@ -100,23 +112,6 @@ struct LevelsStack: View { } } -/// A button used to collapse the floor level list. -struct CollapseButton: View { - /// Allows the user to toggle the visibility of the site and facility selector. - @Binding var isCollapsed: Bool - - var body: some View { - Button { - withAnimation { - isCollapsed.toggle() - } - } label: { - Image(systemName: "xmark") - .padding(EdgeInsets.esriInsets) - } - } -} - struct LevelNameFormat: ViewModifier { func body(content: Content) -> some View { content From 9ba81e64fa16e41e753ca2cfd22b1623f5dfc069 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:48:25 -0700 Subject: [PATCH 0960/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift Co-authored-by: Philip Ridgeway --- .../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 a01d1d4d2..2c49e20a8 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -73,7 +73,7 @@ struct LevelSelector: View { } /// A vertical list of floor levels. -struct LevelsStack: View { +private struct LevelsStack: View { /// The view model used by the `LevelsView`. @EnvironmentObject var viewModel: FloorFilterViewModel From 14440afdcb2c45088fd7498f1bb4d79c1543db14 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 08:49:38 -0700 Subject: [PATCH 0961/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift Co-authored-by: Philip Ridgeway --- .../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 a0ca2dd24..ab3e2cb4e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -16,7 +16,7 @@ import ArcGIS /// A view which allows selection of sites and facilities represented in a `FloorManager`. struct SiteAndFacilitySelector: View { - /// Creates a `SiteAndFacilitySelector` + /// Creates a `SiteAndFacilitySelector`. /// - Parameter isHidden: A binding used to dismiss the site selector. init(isHidden: Binding) { self.isHidden = isHidden From cdbd745b46732c8b7e451bffe113361c72e85051 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:00:54 -0700 Subject: [PATCH 0962/1722] `updateViewModel` -> `shouldUpdateViewModel` --- .../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 a0ca2dd24..82bc1e4eb 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -49,7 +49,7 @@ struct SiteAndFacilitySelector: View { @EnvironmentObject var viewModel: FloorFilterViewModel /// Indicates whether the view model should be notified of the selection update. - @State private var updateViewModel = true + @State private var shouldUpdateViewModel = true /// Indicates that the keyboard is animating and some views may require reload. @State private var keyboardAnimating = false @@ -161,14 +161,14 @@ struct SiteAndFacilitySelector: View { // Setting the `updateViewModel` flag false allows // `selectedSite` to receive upstream updates from the view // model without republishing them back up to the view model. - updateViewModel = false + shouldUpdateViewModel = false selectedSite = viewModel.selectedSite } .onChange(of: selectedSite) { _ in - if updateViewModel, let site = selectedSite { + if shouldUpdateViewModel, let site = selectedSite { viewModel.setSite(site, zoomTo: true) } - updateViewModel = true + shouldUpdateViewModel = true } } } From 7c6e7a3d89fc1bd2ef562355ca58fb4bb8db019c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:02:29 -0700 Subject: [PATCH 0963/1722] `keyboardAnimating` -> `isKeyboardAnimating` --- .../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 412a20ba4..664327343 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -52,7 +52,7 @@ struct SiteAndFacilitySelector: View { @State private var shouldUpdateViewModel = true /// Indicates that the keyboard is animating and some views may require reload. - @State private var keyboardAnimating = false + @State private var isKeyboardAnimating = false /// A site name filter phrase entered by the user. @State private var searchPhrase: String = "" @@ -85,7 +85,7 @@ struct SiteAndFacilitySelector: View { siteListAndFilterView // Trigger a reload on keyboard frame changes for proper layout // across all devices. - .opacity(keyboardAnimating ? 0.99 : 1.0) + .opacity(isKeyboardAnimating ? 0.99 : 1.0) .navigationViewStyle(.stack) .onReceive( NotificationCenter.default.publisher( @@ -93,7 +93,7 @@ struct SiteAndFacilitySelector: View { ) ) { _ in withAnimation { - keyboardAnimating = true + isKeyboardAnimating = true } } .onReceive( @@ -102,7 +102,7 @@ struct SiteAndFacilitySelector: View { ) ) { _ in withAnimation { - keyboardAnimating = false + isKeyboardAnimating = false } } } From f67ae19fac821c692684c6055fdec601e6a044cb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:03:32 -0700 Subject: [PATCH 0964/1722] `searchPhrase` -> `query` --- .../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 664327343..1d27d1a8d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -55,7 +55,7 @@ struct SiteAndFacilitySelector: View { @State private var isKeyboardAnimating = false /// A site name filter phrase entered by the user. - @State private var searchPhrase: String = "" + @State private var query: String = "" /// A local record of the site selected in the view model. /// @@ -73,11 +73,11 @@ struct SiteAndFacilitySelector: View { /// A subset of `sites` with names containing `searchPhrase` or all `sites` if /// `searchPhrase` is empty. var matchingSites: [FloorSite] { - if searchPhrase.isEmpty { + if query.isEmpty { return sites } return sites.filter { - $0.name.lowercased().contains(searchPhrase.lowercased()) + $0.name.lowercased().contains(query.lowercased()) } } @@ -111,7 +111,7 @@ struct SiteAndFacilitySelector: View { var siteListAndFilterView: some View { NavigationView { VStack { - TextField("Filter sites", text: $searchPhrase) + TextField("Filter sites", text: $query) .keyboardType(.alphabet) .disableAutocorrection(true) if matchingSites.isEmpty { From 5c0abdda358ef72abdd9479c0d9a4b82b7502d77 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:11:57 -0700 Subject: [PATCH 0965/1722] 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 1d27d1a8d..d8a0c3614 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -121,7 +121,7 @@ struct SiteAndFacilitySelector: View { } NavigationLink("All sites") { FacilitiesList( - facilities: sites.flatMap({ $0.facilities }), + facilities: sites.flatMap(\.facilities), presentationStyle: .allSites, isHidden: isHidden ) From 30b6b917c12a2880491941ad3c3472cdc3e07214 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:15:18 -0700 Subject: [PATCH 0966/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift Co-authored-by: Philip Ridgeway --- .../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 d8a0c3614..f8e5c1916 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -119,7 +119,7 @@ struct SiteAndFacilitySelector: View { } else { siteListView } - NavigationLink("All sites") { + NavigationLink("All Sites") { FacilitiesList( facilities: sites.flatMap(\.facilities), presentationStyle: .allSites, From 1e19e5e25f9136010b6cf466594c3014867a01e2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:17:39 -0700 Subject: [PATCH 0967/1722] `searchPhrase` -> `query` --- .../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 f8e5c1916..4bcdcb8e2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -189,7 +189,7 @@ struct SiteAndFacilitySelector: View { } /// A facility name filter phrase entered by the user. - @State var searchPhrase: String = "" + @State var query: String = "" /// `FloorFacility`s to be displayed by this view. let facilities: [FloorFacility] @@ -203,11 +203,11 @@ struct SiteAndFacilitySelector: View { /// A subset of `facilities` with names containing `searchPhrase` or all /// `facilities` if `searchPhrase` is empty. var matchingFacilities: [FloorFacility] { - if searchPhrase.isEmpty { + if query.isEmpty { return facilities } return facilities.filter { - $0.name.lowercased().contains(searchPhrase.lowercased()) + $0.name.lowercased().contains(query.lowercased()) } } @@ -250,7 +250,7 @@ struct SiteAndFacilitySelector: View { CloseButton { isHidden.toggle() } } } - TextField("Filter facilities", text: $searchPhrase) + TextField("Filter facilities", text: $query) .keyboardType(.alphabet) .disableAutocorrection(true) if matchingFacilities.isEmpty { From 5c85681c57c402c369242101abf141cddb8756df Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:23:17 -0700 Subject: [PATCH 0968/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 4bcdcb8e2..63efa63fd 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -211,18 +211,6 @@ struct SiteAndFacilitySelector: View { } } - /// Determines the SF Symbols image name to represent selection/non-selection of a facility. - /// - Parameter facility: The facility of interest - /// - Returns: "circle.fill" if the facility is marked selected or "cirlce" if the facility is not selected - /// in the view model. - func imageFor(_ facility: FloorFacility) -> String { - if facility.id == viewModel.selectedFacility?.id { - return "circle.fill" - } else { - return "circle" - } - } - var body: some View { if presentationStyle == .singleSite { facilityListAndFilterView @@ -277,7 +265,8 @@ struct SiteAndFacilitySelector: View { isHidden.toggle() } label: { HStack { - Image(systemName: imageFor(facility)) + Image(systemName: "circle") + .symbolVariant(facility.id == viewModel.selectedFacility?.id ? .fill : .none) VStack { Text(facility.name) .fontWeight(.regular) From 33b80037cac19d62baadf4e7a4f2a90e4549a50d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:24:49 -0700 Subject: [PATCH 0969/1722] Fix indentation --- .../FloorFilter/SiteAndFacilitySelector.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 63efa63fd..3ffda4527 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -269,19 +269,19 @@ struct SiteAndFacilitySelector: View { .symbolVariant(facility.id == viewModel.selectedFacility?.id ? .fill : .none) VStack { Text(facility.name) - .fontWeight(.regular) - .frame( - maxWidth: .infinity, - alignment: .leading - ) - if presentationStyle == .allSites, - let siteName = facility.site?.name { - Text(siteName) - .fontWeight(.ultraLight) + .fontWeight(.regular) .frame( maxWidth: .infinity, alignment: .leading ) + if presentationStyle == .allSites, + let siteName = facility.site?.name { + Text(siteName) + .fontWeight(.ultraLight) + .frame( + maxWidth: .infinity, + alignment: .leading + ) } } } From 0e3471354b4af0a75c9e2e8c25c2b5a469e45db5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 09:59:39 -0700 Subject: [PATCH 0970/1722] Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 3ffda4527..8e105519b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -314,13 +314,10 @@ struct SiteAndFacilitySelector: View { } /// Displays text "No matches found". -struct NoMatchesView: View { +private struct NoMatchesView: View { var body: some View { - VStack { - Spacer() - Text("No matches found") - Spacer() - } + Text("No matches found") + .frame(maxHeight: .infinity) } } From 66ba036702505cd6623fe0528dec4839743a16eb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 10:03:44 -0700 Subject: [PATCH 0971/1722] 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 8e105519b..75920abd5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -322,7 +322,7 @@ private struct NoMatchesView: View { } /// A custom button with an "X" enclosed within a circle to be used as a "close" button. -struct CloseButton: View { +private struct CloseButton: View { /// The button's action to be performed when tapped. var action: (() -> Void) From a540ac58f7a69a88f901ad288831e839cd2795f6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 10:05:53 -0700 Subject: [PATCH 0972/1722] Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 75920abd5..e1d8078ae 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -327,9 +327,7 @@ private struct CloseButton: View { var action: (() -> Void) var body: some View { - Button { - action() - } label: { + Button(action: action) { Image(systemName: "xmark.circle") } } From b38c845bc459acdde196a28fd621ac67ba4031cf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 10:32:04 -0700 Subject: [PATCH 0973/1722] `esriInsets` -> `toolkitDefault` --- .../Components/FloorFilter/FloorFilter.swift | 2 +- .../Components/FloorFilter/LevelSelector.swift | 2 +- Sources/ArcGISToolkit/Extensions/EdgeInsets.swift | 14 ++++++-------- .../Utility/EsriBorderViewModifier.swift | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 7e4becd17..192be32fe 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -57,7 +57,7 @@ public struct FloorFilter: View { isSitesAndFacilitiesHidden.toggle() } label: { Image(systemName: "building.2") - .padding(.esriInsets) + .padding(.toolkitDefault) } .frame(maxWidth: .infinity) } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 2c49e20a8..3afbb20e0 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -67,7 +67,7 @@ struct LevelSelector: View { } } label: { Image(systemName: "xmark") - .padding(EdgeInsets.esriInsets) + .padding(.toolkitDefault) } } } diff --git a/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift b/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift index 11db5a8de..bcc3eacad 100644 --- a/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift +++ b/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift @@ -15,12 +15,10 @@ import SwiftUI extension EdgeInsets { /// A reusable set of edge insets for use across toolkit components. - public static var esriInsets: EdgeInsets { - EdgeInsets( - top: 8, - leading: 12, - bottom: 8, - trailing: 12 - ) - } + public static let toolkitDefault = Self( + top: 8, + leading: 12, + bottom: 8, + trailing: 12 + ) } diff --git a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift index 0e940853d..62d765e7f 100644 --- a/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift +++ b/Sources/ArcGISToolkit/Utility/EsriBorderViewModifier.swift @@ -39,7 +39,7 @@ struct EsriBorderViewModifier: ViewModifier { public extension View { func esriBorder( - padding: EdgeInsets = EdgeInsets.esriInsets + padding: EdgeInsets = .toolkitDefault ) -> some View { modifier(EsriBorderViewModifier(padding: padding)) } From 078b1aa69d10ba3d745269190fcef9eec2cd4908 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 10:43:56 -0700 Subject: [PATCH 0974/1722] Update LevelSelector.swift --- .../ArcGISToolkit/Components/FloorFilter/LevelSelector.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 3afbb20e0..05fa7031a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -38,7 +38,7 @@ struct LevelSelector: View { public var body: some View { if !isCollapsed, - levels.count > 1 { + levels.count > 1 { VStack { if !isTopAligned { makeCollapseButton() @@ -112,7 +112,7 @@ private struct LevelsStack: View { } } -struct LevelNameFormat: ViewModifier { +private struct LevelNameFormat: ViewModifier { func body(content: Content) -> some View { content .lineLimit(1) From b8321064303c87d928c6b956d09ca9900a2ad550 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 10:46:09 -0700 Subject: [PATCH 0975/1722] Update Sources/ArcGISToolkit/Extensions/FloorFacility.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Extensions/FloorFacility.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift index 15a6dd5a6..12d2706a9 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorFacility.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorFacility.swift @@ -29,7 +29,6 @@ extension FloorFacility: Equatable { extension FloorFacility: Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - hasher.combine(self.name) + hasher.combine(id) } } From 03ad060cde309dac469abf1ef490b580defbc3c3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 10:48:09 -0700 Subject: [PATCH 0976/1722] Update Sources/ArcGISToolkit/Extensions/FloorLevel.swift Co-authored-by: Philip Ridgeway --- Sources/ArcGISToolkit/Extensions/FloorLevel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift index 05cdbc628..b3847d667 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorLevel.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorLevel.swift @@ -22,6 +22,6 @@ extension FloorLevel: Equatable { extension FloorLevel: Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) + hasher.combine(id) } } From 943a0669bf336aaa126100a5d78a9dff785b90bf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 10:50:55 -0700 Subject: [PATCH 0977/1722] Update FloorSite.swift --- Sources/ArcGISToolkit/Extensions/FloorSite.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/FloorSite.swift b/Sources/ArcGISToolkit/Extensions/FloorSite.swift index fb6c49e95..1fe9a23c7 100644 --- a/Sources/ArcGISToolkit/Extensions/FloorSite.swift +++ b/Sources/ArcGISToolkit/Extensions/FloorSite.swift @@ -22,7 +22,6 @@ extension FloorSite: Equatable { extension FloorSite: Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - hasher.combine(self.name) + hasher.combine(id) } } From a5170c2f7581bb712d90495ac0f80a29b748862f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 11:32:14 -0700 Subject: [PATCH 0978/1722] Revert to simple button style --- .../FloorFilter/LevelSelector.swift | 4 +- .../Extensions/ToggleStyle.swift | 57 ------------------- 2 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/ToggleStyle.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index 05fa7031a..e1d0b5b5a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -55,7 +55,7 @@ struct LevelSelector: View { Text(selectedLevelName) .modifier(LevelNameFormat()) } - .toggleStyle(.selectedButton) + .toggleStyle(.button) } } @@ -101,7 +101,7 @@ private struct LevelsStack: View { Text(level.shortName) .modifier(LevelNameFormat()) } - .toggleStyle(.selectableButton) + .toggleStyle(.button) } } .onSizeChange { diff --git a/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift deleted file mode 100644 index cea65c590..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 despending 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 8d4f4491341014c70f282038cafe3d7f552878ed Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 11:59:30 -0700 Subject: [PATCH 0979/1722] Update View.swift --- Sources/ArcGISToolkit/Extensions/View.swift | 23 +-------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 837056e86..1d7fb1bc0 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -13,25 +13,11 @@ import SwiftUI -/// A modifier which conditionally hides its underlying view. -struct ConditionallyHiddenModifier: ViewModifier { - /// A Boolean value that indicates whether the underlying view should be hidden. - var isHidden: Bool - - func body(content: Content) -> some View { - if isHidden { - EmptyView() - } else { - content - } - } -} - /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool - + func body(content: Content) -> some View { if isSelected { content @@ -48,13 +34,6 @@ struct SelectedModifier: ViewModifier { } extension View { - /// View modifier used to conditionally hide a view. - /// - Parameter condition: A value of `true` will hide the underlying view. - /// - Returns: A new `View`. - func hidden(_ condition: Bool) -> some View { - modifier(ConditionallyHiddenModifier(isHidden: condition)) - } - /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. /// - Parameter perform: The closure to be executed when the content size of the receiver /// changes. From d219fafd53a213ea18480f73b30792c0ba57911b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 12:04:45 -0700 Subject: [PATCH 0980/1722] Update View.swift --- Sources/ArcGISToolkit/Extensions/View.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 1d7fb1bc0..eceff4d64 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -53,7 +53,9 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. /// - Returns: A new `View`. - func selected(_ isSelected: Bool = false) -> some View { + func selected( + _ isSelected: Bool = false + ) -> some View { modifier(SelectedModifier(isSelected: isSelected)) } } From 975b315e0ed69407b9b2ab504e94bc1b25734bad Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 12:07:13 -0700 Subject: [PATCH 0981/1722] Update View.swift --- Sources/ArcGISToolkit/Extensions/View.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index eceff4d64..74e0f9948 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -17,7 +17,7 @@ import SwiftUI struct SelectedModifier: ViewModifier { /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool - + func body(content: Content) -> some View { if isSelected { content @@ -37,7 +37,7 @@ extension View { /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. /// - Parameter perform: The closure to be executed when the content size of the receiver /// changes. - /// - Returns: A new `View`. + /// - Returns: The modified view. func onSizeChange(perform: @escaping (CGSize) -> Void) -> some View { background( GeometryReader { geometry in From 6d8b7124a02f648f205b724137cd4675e27c6a72 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 12:08:32 -0700 Subject: [PATCH 0982/1722] Update View.swift --- Sources/ArcGISToolkit/Extensions/View.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 74e0f9948..b85bbf53d 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -37,7 +37,7 @@ extension View { /// Returns a new `View` that allows a parent `View` to be informed of a child view's size. /// - Parameter perform: The closure to be executed when the content size of the receiver /// changes. - /// - Returns: The modified view. + /// - Returns: A new `View`. func onSizeChange(perform: @escaping (CGSize) -> Void) -> some View { background( GeometryReader { geometry in @@ -52,7 +52,7 @@ extension View { /// View modifier used to denote the view is selected. /// - Parameter isSelected: `true` if the view is selected, `false` otherwise. - /// - Returns: A new `View`. + /// - Returns: The modified view. func selected( _ isSelected: Bool = false ) -> some View { From 1698c172805fb9f4109a971e91c7dbfe8fc013f7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 12:49:12 -0700 Subject: [PATCH 0983/1722] Restore button styling --- .../FloorFilter/LevelSelector.swift | 4 +- .../Extensions/ToggleStyle.swift | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/ToggleStyle.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift index e1d0b5b5a..05fa7031a 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/LevelSelector.swift @@ -55,7 +55,7 @@ struct LevelSelector: View { Text(selectedLevelName) .modifier(LevelNameFormat()) } - .toggleStyle(.button) + .toggleStyle(.selectedButton) } } @@ -101,7 +101,7 @@ private struct LevelsStack: View { Text(level.shortName) .modifier(LevelNameFormat()) } - .toggleStyle(.button) + .toggleStyle(.selectableButton) } } .onSizeChange { diff --git a/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift new file mode 100644 index 000000000..cea65c590 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/ToggleStyle.swift @@ -0,0 +1,57 @@ +// 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 despending 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 cbd972b780a20b1e02f5cd687f51926f46b09642 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 13:22:21 -0700 Subject: [PATCH 0984/1722] Update FloorFilterViewModelTests.swift --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index eb6b0d9d2..1eef90c29 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -19,11 +19,6 @@ import XCTest @MainActor class FloorFilterViewModelTests: XCTestCase { - /// Applies credentials necessary to run tests. - override func setUp() async throws { - await addCredentials() - } - /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` func testInitFloorFilterViewModelWithFloorManager() async { guard let map = await makeMap(), From 21041016c898360f6ac90b7dd2c1295590a54cb6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 13:27:53 -0700 Subject: [PATCH 0985/1722] Update FloorFilterViewModelTests.swift --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 1eef90c29..81a9c64e8 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import ArcGISToolkit @MainActor -class FloorFilterViewModelTests: XCTestCase { +final class FloorFilterViewModelTests: XCTestCase { /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` func testInitFloorFilterViewModelWithFloorManager() async { guard let map = await makeMap(), From 7643f7ec1e964271cf1416471ac31cb1c14d5dc9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 13:31:09 -0700 Subject: [PATCH 0986/1722] Update FloorFilterViewModelTests.swift --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 81a9c64e8..0bf68a377 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -20,7 +20,7 @@ import XCTest @MainActor final class FloorFilterViewModelTests: XCTestCase { /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` - func testInitFloorFilterViewModelWithFloorManager() async { + func testInitWithFloorManager() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return From e6ce514b881c38e0e34355da3c47df2390a3a295 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 13:33:49 -0700 Subject: [PATCH 0987/1722] Update FloorFilterViewModelTests.swift --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 0bf68a377..92fbf2f4a 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -20,7 +20,7 @@ import XCTest @MainActor final class FloorFilterViewModelTests: XCTestCase { /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` - func testInitWithFloorManager() async { + func testInitWithFloorManagerAndViewpoint() async { guard let map = await makeMap(), let floorManager = map.floorManager else { return From 1a0bf0fad28f4caad726a243e8e2fc90cbbe6a62 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 14:16:55 -0700 Subject: [PATCH 0988/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 101 ++++++++---------- 1 file changed, 43 insertions(+), 58 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 92fbf2f4a..a4496f401 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -20,11 +20,10 @@ import XCTest @MainActor final class FloorFilterViewModelTests: XCTestCase { /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` - func testInitWithFloorManagerAndViewpoint() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testInitWithFloorManagerAndViewpoint() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -39,11 +38,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetSite() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testSetSite() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -69,11 +67,10 @@ final class FloorFilterViewModelTests: XCTestCase { ) } - func testSetFacility() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testSetFacility() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -100,11 +97,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetLevel() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testSetLevel() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) let initialViewpoint = getEsriRedlandsViewpoint(.zero) var _viewpoint: Viewpoint? = initialViewpoint @@ -136,11 +132,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlways() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testAutoSelectAlways() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) let viewpointLosAngeles = Viewpoint( center: Point( @@ -182,11 +177,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlwaysNotClearing() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testAutoSelectAlwaysNotClearing() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -214,11 +208,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectNever() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testAutoSelectNever() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) var _viewpoint: Viewpoint? = .losAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -243,33 +236,25 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedSite) } - /// Get a map constructed from an ArcGIS portal item. - /// - Returns: A map constructed from an ArcGIS portal item. - private func makeMap() async -> Map? { - // Multiple sites/facilities: Esri IST map with all buildings. - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "49520a67773842f1858602735ef538b5")!) - - // Redlands Campus map with multiple sites and facilities. + private func floorManager( + forWebMapWithIdentifier id: PortalItem.ID, + file: StaticString = #filePath, + line: UInt = #line + ) async throws -> FloorManager { let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "7687805bd42549f5ba41237443d0c60a")!) - - // Single site (ESRI Redlands Main) and facility (Building L). - // let portal = Portal(url: URL(string: "https://indoors.maps.arcgis.com/")!, isLoginRequired: false) - // let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "f133a698536f44c8884ad81f80b6cfc7")!) - - let map = Map(item: portalItem) - do { - try await map.load() - try await map.floorManager?.load() - } catch { - XCTFail("\(#fileID), \(#function), \(#line), \(error.localizedDescription)") - return nil - } - return map + let item = PortalItem(portal: portal, id: id) + let map = Map(item: item) + try await map.load() + let floorManager = try XCTUnwrap(map.floorManager, file: file, line: line) + try await floorManager.load() + return floorManager } } +private extension PortalItem.ID { + static let redlandsCampusMap = Self("7687805bd42549f5ba41237443d0c60a")! +} + extension FloorFilterViewModelTests { /// The coordinates for the Redlands Esri campus. var point: Point { From 7be1c375b96419c96dd7215b2ccaedddae973afd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 14:22:46 -0700 Subject: [PATCH 0989/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 92 +++++++++---------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 34991e7f5..c48f3eee2 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -22,11 +22,10 @@ final class FloorFilterViewModelTests: XCTestCase { /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager` and /// `Binding`.` /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` - func testInitWithFloorManagerAndViewpoint() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testInitWithFloorManagerAndViewpoint() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) var _viewpoint: Viewpoint? = nil let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -41,11 +40,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetSite() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testSetSite() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) var _viewpoint: Viewpoint? = .site_ResearchAnnex_facility_Lattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -71,11 +69,10 @@ final class FloorFilterViewModelTests: XCTestCase { ) } - func testSetFacility() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testSetFacility() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) var _viewpoint: Viewpoint? = .site_ResearchAnnex_facility_Lattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -102,11 +99,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetLevel() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testSetLevel() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) let initialViewpoint: Viewpoint = .site_ResearchAnnex_facility_Lattice var _viewpoint: Viewpoint? = initialViewpoint @@ -138,11 +134,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlways() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testAutoSelectAlways() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) var _viewpoint: Viewpoint? = .losAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -180,11 +175,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlwaysNotClearing() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testAutoSelectAlwaysNotClearing() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) var _viewpoint: Viewpoint? = .site_ResearchAnnex_facility_Lattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -214,11 +208,10 @@ final class FloorFilterViewModelTests: XCTestCase { } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectNever() async { - guard let map = await makeMap(), - let floorManager = map.floorManager else { - return - } + func testAutoSelectNever() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) var _viewpoint: Viewpoint? = .losAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -246,24 +239,25 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedSite) } - /// Get a map constructed from an ArcGIS portal item. - /// - Returns: A map constructed from an ArcGIS portal item. - private func makeMap() async -> Map? { + private func floorManager( + forWebMapWithIdentifier id: PortalItem.ID, + file: StaticString = #filePath, + line: UInt = #line + ) async throws -> FloorManager { let portal = Portal(url: URL(string: "https://www.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "b4b599a43a474d33946cf0df526426f5")!) - - let map = Map(item: portalItem) - do { - try await map.load() - try await map.floorManager?.load() - } catch { - XCTFail("\(#fileID), \(#function), \(#line), \(error.localizedDescription)") - return nil - } - return map + let item = PortalItem(portal: portal, id: id) + let map = Map(item: item) + try await map.load() + let floorManager = try XCTUnwrap(map.floorManager, file: file, line: line) + try await floorManager.load() + return floorManager } } +private extension PortalItem.ID { + static let testMap = Self("b4b599a43a474d33946cf0df526426f5")! +} + private extension Viewpoint { static var losAngeles: Viewpoint { Viewpoint( From a1078154c6c3a22dd5c482d0ddddcf0911f9ea3e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 14:23:55 -0700 Subject: [PATCH 0990/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index a4496f401..e9f198508 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -21,7 +21,7 @@ import XCTest final class FloorFilterViewModelTests: XCTestCase { /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` func testInitWithFloorManagerAndViewpoint() async throws { - let floorManager = try await floorManager( + let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) @@ -39,7 +39,7 @@ final class FloorFilterViewModelTests: XCTestCase { /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testSetSite() async throws { - let floorManager = try await floorManager( + let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) @@ -68,7 +68,7 @@ final class FloorFilterViewModelTests: XCTestCase { } func testSetFacility() async throws { - let floorManager = try await floorManager( + let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) @@ -98,7 +98,7 @@ final class FloorFilterViewModelTests: XCTestCase { /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testSetLevel() async throws { - let floorManager = try await floorManager( + let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) @@ -133,7 +133,7 @@ final class FloorFilterViewModelTests: XCTestCase { /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testAutoSelectAlways() async throws { - let floorManager = try await floorManager( + let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) @@ -178,7 +178,7 @@ final class FloorFilterViewModelTests: XCTestCase { /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testAutoSelectAlwaysNotClearing() async throws { - let floorManager = try await floorManager( + let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) @@ -209,7 +209,7 @@ final class FloorFilterViewModelTests: XCTestCase { /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. func testAutoSelectNever() async throws { - let floorManager = try await floorManager( + let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) From 6d9cd524712d8972925e1cfdcc097aaa272987ac Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 16:22:01 -0700 Subject: [PATCH 0991/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index e9f198508..805fdd837 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -25,7 +25,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .redlandsCampusMap ) - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() + var _viewpoint: Viewpoint? = .esriRedlands() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, @@ -43,7 +43,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .redlandsCampusMap ) - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint() + var _viewpoint: Viewpoint? = .esriRedlands() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, @@ -72,7 +72,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .redlandsCampusMap ) - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(.zero) + var _viewpoint: Viewpoint? = .esriRedlands() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, @@ -102,7 +102,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .redlandsCampusMap ) - let initialViewpoint = getEsriRedlandsViewpoint(.zero) + let initialViewpoint: Viewpoint = .esriRedlands() var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( @@ -160,7 +160,7 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedSite) // Viewpoint is Redlands Main Q - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = .esriRedlands(scale: 1000) viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -182,7 +182,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .redlandsCampusMap ) - var _viewpoint: Viewpoint? = getEsriRedlandsViewpoint(scale: 1000) + var _viewpoint: Viewpoint? = .esriRedlands(scale: 1000) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .alwaysNotClearing, @@ -191,7 +191,7 @@ final class FloorFilterViewModelTests: XCTestCase { ) // Viewpoint is Redlands Main Q - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = .esriRedlands(scale: 1000) viewModel.automaticallySelectFacilityOrSite() var selectedFacility = viewModel.selectedFacility var selectedSite = viewModel.selectedSite @@ -228,7 +228,7 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedSite) // Viewpoint is Redlands Main Q but selection should still be nil - _viewpoint = getEsriRedlandsViewpoint(scale: 1000) + _viewpoint = .esriRedlands(scale: 1000) viewModel.automaticallySelectFacilityOrSite() selectedFacility = viewModel.selectedFacility selectedSite = viewModel.selectedSite @@ -255,28 +255,23 @@ private extension PortalItem.ID { static let redlandsCampusMap = Self("7687805bd42549f5ba41237443d0c60a")! } -extension FloorFilterViewModelTests { +private extension Point { /// The coordinates for the Redlands Esri campus. - var point: Point { - Point( - x: -13046157.242121734, - y: 4036329.622884897, - spatialReference: .webMercator - ) - } - - /// Builds viewpoints to use for tests. - /// - Parameter rotation: The rotation to use for the resulting viewpoint. - /// - Returns: A viewpoint object for tests. - func getEsriRedlandsViewpoint( - _ rotation: Double = .zero, - scale: Double = 10_000 - ) -> Viewpoint { - return Viewpoint(center: point, scale: scale, rotation: rotation) - } + static let esriRedlands = Point( + x: -13046157.242121734, + y: 4036329.622884897, + spatialReference: .webMercator + ) } private extension Viewpoint { + static func esriRedlands( + scale: Double = 10_000, + rotation: Double = .zero + ) -> Viewpoint { + .init(center: .esriRedlands, scale: scale, rotation: rotation) + } + static var losAngeles: Viewpoint { Viewpoint( center: Point( From 3d7e840518bce21b17444291e2fdb51fecced3d0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 16:29:43 -0700 Subject: [PATCH 0992/1722] Update FloorFilterViewModelTests.swift --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 805fdd837..7d0b54058 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -25,11 +25,9 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .redlandsCampusMap ) - var _viewpoint: Viewpoint? = .esriRedlands() - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, - viewpoint: viewpoint + viewpoint: .constant(.esriRedlands()) ) XCTAssertFalse(viewModel.sites.isEmpty) From 72a665c9ebb102d12295214b42b16bd3fb2c97fd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 16:30:00 -0700 Subject: [PATCH 0993/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index c48f3eee2..09939c2d5 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -45,7 +45,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .testMap ) - var _viewpoint: Viewpoint? = .site_ResearchAnnex_facility_Lattice + var _viewpoint: Viewpoint? = .researchAnnexLattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, @@ -74,7 +74,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .testMap ) - var _viewpoint: Viewpoint? = .site_ResearchAnnex_facility_Lattice + var _viewpoint: Viewpoint? = .researchAnnexLattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, @@ -104,7 +104,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .testMap ) - let initialViewpoint: Viewpoint = .site_ResearchAnnex_facility_Lattice + let initialViewpoint: Viewpoint = .researchAnnexLattice var _viewpoint: Viewpoint? = initialViewpoint let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( @@ -155,7 +155,7 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - _viewpoint = .site_ResearchAnnex_facility_Lattice + _viewpoint = .researchAnnexLattice viewModel.automaticallySelectFacilityOrSite() // Viewpoint is the Lattice facility at the Research Annex site @@ -180,7 +180,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .testMap ) - var _viewpoint: Viewpoint? = .site_ResearchAnnex_facility_Lattice + var _viewpoint: Viewpoint? = .researchAnnexLattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .alwaysNotClearing, @@ -229,7 +229,7 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(selectedFacility) XCTAssertNil(selectedSite) - _viewpoint = .site_ResearchAnnex_facility_Lattice + _viewpoint = .researchAnnexLattice viewModel.automaticallySelectFacilityOrSite() // Viewpoint is the Lattice facility at the Research Annex site @@ -270,7 +270,7 @@ private extension Viewpoint { ) } - static var site_ResearchAnnex_facility_Lattice: Viewpoint { + static var researchAnnexLattice: Viewpoint { Viewpoint( center: Point( From 2e15a5fda27d8b211eaa07cf8a39c65117656e15 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 16:50:06 -0700 Subject: [PATCH 0994/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 7d0b54058..ea2d37d77 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -48,10 +48,8 @@ final class FloorFilterViewModelTests: XCTestCase { viewpoint: viewpoint ) - guard let site = viewModel.sites.first else { - XCTFail() - return - } + let site = try XCTUnwrap(viewModel.sites.first) + viewModel.setSite(site, zoomTo: true) let selectedSite = viewModel.selectedSite let selectedFacility = viewModel.selectedFacility @@ -78,10 +76,8 @@ final class FloorFilterViewModelTests: XCTestCase { viewpoint: viewpoint ) - guard let facility = viewModel.facilities.first else { - XCTFail() - return - } + let facility = try XCTUnwrap(viewModel.facilities.first) + viewModel.setFacility(facility, zoomTo: true) let selectedFacility = viewModel.selectedFacility let selectedLevel = viewModel.selectedLevel @@ -109,10 +105,8 @@ final class FloorFilterViewModelTests: XCTestCase { ) let levels = viewModel.levels - guard let level = levels.first else { - XCTFail("A first level does not exist") - return - } + let level = try XCTUnwrap(levels.first) + viewModel.setLevel(level) let selectedLevel = viewModel.selectedLevel XCTAssertEqual(selectedLevel, level) @@ -135,14 +129,7 @@ final class FloorFilterViewModelTests: XCTestCase { forWebMapWithIdentifier: .redlandsCampusMap ) - let viewpointLosAngeles = Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) + let viewpointLosAngeles: Viewpoint = .losAngeles var _viewpoint: Viewpoint? = viewpointLosAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( From e16bcee3af800593d919c9cdb172f70576caaccf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 16:55:24 -0700 Subject: [PATCH 0995/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index ea2d37d77..4e0b60327 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -257,14 +257,12 @@ private extension Viewpoint { .init(center: .esriRedlands, scale: scale, rotation: rotation) } - static var losAngeles: Viewpoint { - Viewpoint( - center: Point( - x: -13164116.3284, - y: 4034465.8065, - spatialReference: .webMercator - ), - scale: 10_000 - ) - } + static let losAngeles = Viewpoint( + center: Point( + x: -13164116.3284, + y: 4034465.8065, + spatialReference: .webMercator + ), + scale: 10_000 + ) } From 74762af8e91be4a861656bee18bd14b5f6b8d7de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 31 May 2022 17:09:46 -0700 Subject: [PATCH 0996/1722] 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 e1d8078ae..2794410f7 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -158,7 +158,7 @@ struct SiteAndFacilitySelector: View { } .listStyle(.plain) .onChange(of: viewModel.selection) { _ in - // Setting the `updateViewModel` flag false allows + // Setting the `shouldUpdateViewModel` flag false allows // `selectedSite` to receive upstream updates from the view // model without republishing them back up to the view model. shouldUpdateViewModel = false From 632da93c5c4abb73f0ae7449cf1376a7b1c34ab6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 1 Jun 2022 10:07:00 -0700 Subject: [PATCH 0997/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 256 ++++++++++-------- 1 file changed, 139 insertions(+), 117 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 4e0b60327..e492ee289 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -19,56 +19,71 @@ import XCTest @MainActor final class FloorFilterViewModelTests: XCTestCase { - /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` - func testInitWithFloorManagerAndViewpoint() async throws { + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testAutoSelectAlways() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) + var _viewpoint: Viewpoint? = .losAngeles + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( + automaticSelectionMode: .always, floorManager: floorManager, - viewpoint: .constant(.esriRedlands()) + viewpoint: viewpoint ) - XCTAssertFalse(viewModel.sites.isEmpty) - XCTAssertFalse(viewModel.facilities.isEmpty) - XCTAssertFalse(viewModel.levels.isEmpty) + // Viewpoint is Los Angeles, selection should be nil + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedSite) + + // Viewpoint is Redlands Main Q + _viewpoint = .esriRedlands(scale: 1000) + viewModel.automaticallySelectFacilityOrSite() + XCTAssertEqual(viewModel.selectedSite?.name, "Redlands Main") + XCTAssertEqual(viewModel.selectedFacility?.name, "Q") + + // Viewpoint is Los Angeles, selection should be nil + _viewpoint = .losAngeles + viewModel.automaticallySelectFacilityOrSite() + XCTAssertNil(viewModel.selectedSite) + XCTAssertNil(viewModel.selectedFacility) } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetSite() async throws { + func testAutoSelectAlwaysNotClearing() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) - var _viewpoint: Viewpoint? = .esriRedlands() + var _viewpoint: Viewpoint? = .esriRedlands(scale: 1000) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( + automaticSelectionMode: .alwaysNotClearing, floorManager: floorManager, viewpoint: viewpoint ) - let site = try XCTUnwrap(viewModel.sites.first) + // Viewpoint is Redlands Main Q + _viewpoint = .esriRedlands(scale: 1000) + viewModel.automaticallySelectFacilityOrSite() + XCTAssertEqual(viewModel.selectedSite?.name, "Redlands Main") + XCTAssertEqual(viewModel.selectedFacility?.name, "Q") - viewModel.setSite(site, zoomTo: true) - let selectedSite = viewModel.selectedSite - let selectedFacility = viewModel.selectedFacility - let selectedLevel = viewModel.selectedLevel - XCTAssertEqual(selectedSite, site) - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedLevel) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - selectedSite?.geometry?.extent.center.x - ) + // Viewpoint is Los Angeles, but selection should remain Redlands Main Q + _viewpoint = .losAngeles + viewModel.automaticallySelectFacilityOrSite() + XCTAssertEqual(viewModel.selectedSite?.name, "Redlands Main") + XCTAssertEqual(viewModel.selectedFacility?.name, "Q") } - func testSetFacility() async throws { + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testAutoSelectNever() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) - var _viewpoint: Viewpoint? = .esriRedlands() + var _viewpoint: Viewpoint? = .losAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, @@ -76,46 +91,45 @@ final class FloorFilterViewModelTests: XCTestCase { viewpoint: viewpoint ) - let facility = try XCTUnwrap(viewModel.facilities.first) + // Viewpoint is Los Angeles, selection should be nil + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedSite) - viewModel.setFacility(facility, zoomTo: true) - let selectedFacility = viewModel.selectedFacility - let selectedLevel = viewModel.selectedLevel - let defaultLevel = selectedFacility?.defaultLevel - XCTAssertEqual(selectedFacility, facility) - XCTAssertEqual(selectedLevel, defaultLevel) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - selectedFacility?.geometry?.extent.center.x + // Viewpoint is Redlands Main Q but selection should still be nil + _viewpoint = .esriRedlands(scale: 1000) + viewModel.automaticallySelectFacilityOrSite() + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedSite) + } + + /// Tests that a `FloorFilterViewModel` succesfully initializes. + func testInitWithFloorManagerAndViewpoint() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap ) + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: .constant(.esriRedlands()) + ) + XCTAssertFalse(viewModel.sites.isEmpty) + XCTAssertFalse(viewModel.facilities.isEmpty) + XCTAssertFalse(viewModel.levels.isEmpty) } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetLevel() async throws { + /// Confirms that the proper level is visible and all others are hidden. + func testLevelVisibility() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) - - let initialViewpoint: Viewpoint = .esriRedlands() - var _viewpoint: Viewpoint? = initialViewpoint - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, - viewpoint: viewpoint + viewpoint: .constant(.esriRedlands()) ) - let levels = viewModel.levels - let level = try XCTUnwrap(levels.first) - - viewModel.setLevel(level) - let selectedLevel = viewModel.selectedLevel - XCTAssertEqual(selectedLevel, level) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - initialViewpoint.targetGeometry.extent.center.x - ) + let firstLevel = try XCTUnwrap(levels.first) + viewModel.setLevel(firstLevel) levels.forEach { level in - if level.verticalOrder == selectedLevel?.verticalOrder { + if level.verticalOrder == firstLevel.verticalOrder { XCTAssertTrue(level.isVisible) } else { XCTAssertFalse(level.isVisible) @@ -123,102 +137,110 @@ final class FloorFilterViewModelTests: XCTestCase { } } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlways() async throws { + /// Confirms that the selected site/facility/level properties are correctly updated. + func testSelectedProperties() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) - - let viewpointLosAngeles: Viewpoint = .losAngeles - var _viewpoint: Viewpoint? = viewpointLosAngeles - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( - automaticSelectionMode: .always, floorManager: floorManager, - viewpoint: viewpoint + viewpoint: .constant(.esriRedlands()) ) + let site = try XCTUnwrap(viewModel.sites.first) + let facility = try XCTUnwrap(viewModel.facilities.first) + let level = try XCTUnwrap(viewModel.levels.first) - // Viewpoint is Los Angeles, selection should be nil - var selectedFacility = viewModel.selectedFacility - var selectedSite = viewModel.selectedSite - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedSite) + viewModel.setSite(site, zoomTo: true) + XCTAssertEqual(viewModel.selectedSite, site) + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedLevel) - // Viewpoint is Redlands Main Q - _viewpoint = .esriRedlands(scale: 1000) - viewModel.automaticallySelectFacilityOrSite() - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Redlands Main") - XCTAssertEqual(selectedFacility?.name, "Q") + viewModel.setFacility(facility, zoomTo: true) + XCTAssertEqual(viewModel.selectedSite, facility.site) + XCTAssertEqual(viewModel.selectedFacility, facility) + XCTAssertEqual(viewModel.selectedLevel, facility.defaultLevel) - // Viewpoint is Los Angeles, selection should be nil - _viewpoint = viewpointLosAngeles - viewModel.automaticallySelectFacilityOrSite() - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertNil(selectedSite) - XCTAssertNil(selectedFacility) + viewModel.setLevel(level) + XCTAssertEqual(viewModel.selectedSite, level.facility?.site) + XCTAssertEqual(viewModel.selectedFacility, level.facility) + XCTAssertEqual(viewModel.selectedLevel, level) } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlwaysNotClearing() async throws { + /// Confirms that the selection property indicates the correct facility (and therefore level) value. + func testSelectionOfFacility() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) - - var _viewpoint: Viewpoint? = .esriRedlands(scale: 1000) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( - automaticSelectionMode: .alwaysNotClearing, floorManager: floorManager, - viewpoint: viewpoint + viewpoint: .constant(.esriRedlands()) + ) + let facility = try XCTUnwrap(viewModel.facilities.first) + let level = try XCTUnwrap(facility.defaultLevel) + viewModel.setFacility(facility, zoomTo: true) + XCTAssertEqual( + viewModel.selection, .level(level) ) - - // Viewpoint is Redlands Main Q - _viewpoint = .esriRedlands(scale: 1000) - viewModel.automaticallySelectFacilityOrSite() - var selectedFacility = viewModel.selectedFacility - var selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Redlands Main") - XCTAssertEqual(selectedFacility?.name, "Q") - - // Viewpoint is Los Angeles, but selection should remain Redlands Main Q - _viewpoint = .losAngeles - viewModel.automaticallySelectFacilityOrSite() - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Redlands Main") - XCTAssertEqual(selectedFacility?.name, "Q") } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectNever() async throws { + /// Confirms that the selection property indicates the correct level value. + func testSelectionOfLevel() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .redlandsCampusMap ) - - var _viewpoint: Viewpoint? = .losAngeles + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: .constant(.esriRedlands()) + ) + let level = try XCTUnwrap(viewModel.levels.first) + viewModel.setLevel(level) + XCTAssertEqual( + viewModel.selection, .level(level) + ) + } + + /// Confirms that the selection property indicates the correct site value. + func testSelectionOfSite() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: .constant(.esriRedlands()) + ) + let site = try XCTUnwrap(viewModel.sites.first) + viewModel.setSite(site, zoomTo: true) + XCTAssertEqual( + viewModel.selection, .site(site) + ) + } + + /// Confirms that the viewpoint is correctly updated. + func testViewpointUpdates() async throws { + var _viewpoint: Viewpoint? = .esriRedlands() let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let floorManager = try await floorManager( + forWebMapWithIdentifier: .redlandsCampusMap + ) let viewModel = FloorFilterViewModel( - automaticSelectionMode: .never, floorManager: floorManager, viewpoint: viewpoint ) - // Viewpoint is Los Angeles, selection should be nil - var selectedFacility = viewModel.selectedFacility - var selectedSite = viewModel.selectedSite - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedSite) + let site = try XCTUnwrap(viewModel.sites.first) + let facility = try XCTUnwrap(viewModel.facilities.first) - // Viewpoint is Redlands Main Q but selection should still be nil - _viewpoint = .esriRedlands(scale: 1000) - viewModel.automaticallySelectFacilityOrSite() - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedSite) + viewModel.setSite(site, zoomTo: true) + XCTAssertEqual( + _viewpoint?.targetGeometry.extent.center.x, + site.geometry?.extent.center.x + ) + + viewModel.setFacility(facility, zoomTo: true) + XCTAssertEqual( + _viewpoint?.targetGeometry.extent.center.x, + facility.geometry?.extent.center.x + ) } private func floorManager( From 9d67a6f06a20ba044f78f1ab0a578783ec8a5188 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 1 Jun 2022 10:26:37 -0700 Subject: [PATCH 0998/1722] Update FloorFilterViewModelTests.swift --- .../FloorFilterViewModelTests.swift | 283 +++++++++--------- 1 file changed, 146 insertions(+), 137 deletions(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 6ef22aaed..98c955b47 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -19,26 +19,39 @@ import XCTest @MainActor final class FloorFilterViewModelTests: XCTestCase { - /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager` and - /// `Binding`.` - /// Tests that a `FloorFilterViewModel` succesfully initializes with a `FloorManager`.` - func testInitWithFloorManagerAndViewpoint() async throws { + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testAutoSelectAlways() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap ) + var _viewpoint: Viewpoint? = .losAngeles + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( + automaticSelectionMode: .always, floorManager: floorManager, - viewpoint: .constant(.researchAnnexLattice) + viewpoint: viewpoint ) - XCTAssertFalse(viewModel.sites.isEmpty) - XCTAssertFalse(viewModel.facilities.isEmpty) - XCTAssertFalse(viewModel.levels.isEmpty) + // Viewpoint is Los Angeles, selection should be nil + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedSite) + + // Viewpoint is Research Annex Lattice + _viewpoint = .researchAnnexLattice + viewModel.automaticallySelectFacilityOrSite() + XCTAssertEqual(viewModel.selectedSite?.name, "Research Annex") + XCTAssertEqual(viewModel.selectedFacility?.name, "Lattice") + + // Viewpoint is Los Angeles, selection should be nil + _viewpoint = .losAngeles + viewModel.automaticallySelectFacilityOrSite() + XCTAssertNil(viewModel.selectedSite) + XCTAssertNil(viewModel.selectedFacility) } /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetSite() async throws { + func testAutoSelectAlwaysNotClearing() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap ) @@ -46,31 +59,31 @@ final class FloorFilterViewModelTests: XCTestCase { var _viewpoint: Viewpoint? = .researchAnnexLattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( + automaticSelectionMode: .alwaysNotClearing, floorManager: floorManager, viewpoint: viewpoint ) - let site = try XCTUnwrap(viewModel.sites.first) + // Viewpoint is Research Annex Lattice + _viewpoint = .researchAnnexLattice + viewModel.automaticallySelectFacilityOrSite() + XCTAssertEqual(viewModel.selectedSite?.name, "Research Annex") + XCTAssertEqual(viewModel.selectedFacility?.name, "Lattice") - viewModel.setSite(site, zoomTo: true) - let selectedSite = viewModel.selectedSite - let selectedFacility = viewModel.selectedFacility - let selectedLevel = viewModel.selectedLevel - XCTAssertEqual(selectedSite, site) - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedLevel) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - selectedSite?.geometry?.extent.center.x - ) + // 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") } - func testSetFacility() async throws { + /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. + func testAutoSelectNever() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap ) - var _viewpoint: Viewpoint? = .researchAnnexLattice + var _viewpoint: Viewpoint? = .losAngeles let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( automaticSelectionMode: .never, @@ -78,46 +91,45 @@ final class FloorFilterViewModelTests: XCTestCase { viewpoint: viewpoint ) - let facility = try XCTUnwrap(viewModel.facilities.first) + // Viewpoint is Los Angeles, selection should be nil + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedSite) - viewModel.setFacility(facility, zoomTo: true) - let selectedFacility = viewModel.selectedFacility - let selectedLevel = viewModel.selectedLevel - let defaultLevel = selectedFacility?.defaultLevel - XCTAssertEqual(selectedFacility, facility) - XCTAssertEqual(selectedLevel, defaultLevel) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - selectedFacility?.geometry?.extent.center.x + // Viewpoint is Research Annex Lattice but selection should remain nil + _viewpoint = .researchAnnexLattice + viewModel.automaticallySelectFacilityOrSite() + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedSite) + } + + /// Tests that a `FloorFilterViewModel` succesfully initializes. + func testInitWithFloorManagerAndViewpoint() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap ) + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: .constant(.researchAnnexLattice) + ) + XCTAssertFalse(viewModel.sites.isEmpty) + XCTAssertFalse(viewModel.facilities.isEmpty) + XCTAssertFalse(viewModel.levels.isEmpty) } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testSetLevel() async throws { + /// Confirms that the proper level is visible and all others are hidden. + func testLevelVisibility() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap ) - - let initialViewpoint: Viewpoint = .researchAnnexLattice - var _viewpoint: Viewpoint? = initialViewpoint - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( floorManager: floorManager, - viewpoint: viewpoint + viewpoint: .constant(.researchAnnexLattice) ) - let levels = viewModel.levels - let level = try XCTUnwrap(levels.first) - - viewModel.setLevel(level) - let selectedLevel = viewModel.selectedLevel - XCTAssertEqual(selectedLevel, level) - XCTAssertEqual( - _viewpoint?.targetGeometry.extent.center.x, - initialViewpoint.targetGeometry.extent.center.x - ) + let firstLevel = try XCTUnwrap(levels.first) + viewModel.setLevel(firstLevel) levels.forEach { level in - if level.verticalOrder == selectedLevel?.verticalOrder { + if level.verticalOrder == firstLevel.verticalOrder { XCTAssertTrue(level.isVisible) } else { XCTAssertFalse(level.isVisible) @@ -125,111 +137,110 @@ final class FloorFilterViewModelTests: XCTestCase { } } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlways() async throws { + /// Confirms that the selected site/facility/level properties are correctly updated. + func testSelectedProperties() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap ) - - let viewpointLosAngeles: Viewpoint = .losAngeles - var _viewpoint: Viewpoint? = viewpointLosAngeles - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( - automaticSelectionMode: .always, floorManager: floorManager, - viewpoint: viewpoint + viewpoint: .constant(.researchAnnexLattice) ) + let site = try XCTUnwrap(viewModel.sites.first) + let facility = try XCTUnwrap(viewModel.facilities.first) + let level = try XCTUnwrap(viewModel.levels.first) - // Viewpoint is Los Angeles, selection should be nil - viewModel.automaticallySelectFacilityOrSite() - - var selectedFacility = viewModel.selectedFacility - var selectedSite = viewModel.selectedSite - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedSite) - - _viewpoint = .researchAnnexLattice - viewModel.automaticallySelectFacilityOrSite() - - // Viewpoint is the Lattice facility at the Research Annex site - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Research Annex") - XCTAssertEqual(selectedFacility?.name, "Lattice") + viewModel.setSite(site, zoomTo: true) + XCTAssertEqual(viewModel.selectedSite, site) + XCTAssertNil(viewModel.selectedFacility) + XCTAssertNil(viewModel.selectedLevel) - _viewpoint = .losAngeles - viewModel.automaticallySelectFacilityOrSite() + viewModel.setFacility(facility, zoomTo: true) + XCTAssertEqual(viewModel.selectedSite, facility.site) + XCTAssertEqual(viewModel.selectedFacility, facility) + XCTAssertEqual(viewModel.selectedLevel, facility.defaultLevel) - // Viewpoint is Los Angeles, selection should be nil - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertNil(selectedSite) - XCTAssertNil(selectedFacility) + viewModel.setLevel(level) + XCTAssertEqual(viewModel.selectedSite, level.facility?.site) + XCTAssertEqual(viewModel.selectedFacility, level.facility) + XCTAssertEqual(viewModel.selectedLevel, level) } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectAlwaysNotClearing() async throws { + /// Confirms that the selection property indicates the correct facility (and therefore level) value. + func testSelectionOfFacility() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap ) - - var _viewpoint: Viewpoint? = .researchAnnexLattice - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let viewModel = FloorFilterViewModel( - automaticSelectionMode: .alwaysNotClearing, floorManager: floorManager, - viewpoint: viewpoint + viewpoint: .constant(.researchAnnexLattice) + ) + let facility = try XCTUnwrap(viewModel.facilities[4]) + let level = try XCTUnwrap(facility.defaultLevel) + viewModel.setFacility(facility, zoomTo: true) + XCTAssertEqual( + viewModel.selection, .level(level) ) - - viewModel.automaticallySelectFacilityOrSite() - - // Viewpoint is the Lattice facility at the Research Annex site - var selectedFacility = viewModel.selectedFacility - var selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Research Annex") - XCTAssertEqual(selectedFacility?.name, "Lattice") - - // Viewpoint is Los Angeles, but selection should remain Redlands Main Q - _viewpoint = .losAngeles - viewModel.automaticallySelectFacilityOrSite() - - // Viewpoint is Los Angeles, but selection should remain Redlands Main Q - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertEqual(selectedSite?.name, "Research Annex") - XCTAssertEqual(selectedFacility?.name, "Lattice") } - /// Confirms that the selected site/facility/level properties and the viewpoint are correctly updated. - func testAutoSelectNever() async throws { + /// Confirms that the selection property indicates the correct level value. + func testSelectionOfLevel() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap ) - - var _viewpoint: Viewpoint? = .losAngeles + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: .constant(.researchAnnexLattice) + ) + let level = try XCTUnwrap(viewModel.levels.first) + viewModel.setLevel(level) + XCTAssertEqual( + viewModel.selection, .level(level) + ) + } + + /// Confirms that the selection property indicates the correct site value. + func testSelectionOfSite() async throws { + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) + let viewModel = FloorFilterViewModel( + floorManager: floorManager, + viewpoint: .constant(.researchAnnexLattice) + ) + let site = try XCTUnwrap(viewModel.sites.first) + viewModel.setSite(site, zoomTo: true) + XCTAssertEqual( + viewModel.selection, .site(site) + ) + } + + /// Confirms that the viewpoint is correctly updated. + func testViewpointUpdates() async throws { + var _viewpoint: Viewpoint? = .researchAnnexLattice let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let floorManager = try await floorManager( + forWebMapWithIdentifier: .testMap + ) let viewModel = FloorFilterViewModel( - automaticSelectionMode: .never, floorManager: floorManager, viewpoint: viewpoint ) - viewModel.automaticallySelectFacilityOrSite() - - // Viewpoint is Los Angeles, selection should be nil - var selectedFacility = viewModel.selectedFacility - var selectedSite = viewModel.selectedSite - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedSite) + let site = try XCTUnwrap(viewModel.sites.first) + let facility = try XCTUnwrap(viewModel.facilities.first) - _viewpoint = .researchAnnexLattice - viewModel.automaticallySelectFacilityOrSite() + viewModel.setSite(site, zoomTo: true) + XCTAssertEqual( + _viewpoint?.targetGeometry.extent.center.x, + site.geometry?.extent.center.x + ) - // Viewpoint is the Lattice facility at the Research Annex site - selectedFacility = viewModel.selectedFacility - selectedSite = viewModel.selectedSite - XCTAssertNil(selectedFacility) - XCTAssertNil(selectedSite) + viewModel.setFacility(facility, zoomTo: true) + XCTAssertEqual( + _viewpoint?.targetGeometry.extent.center.x, + facility.geometry?.extent.center.x + ) } private func floorManager( @@ -252,17 +263,15 @@ private extension PortalItem.ID { } private extension Viewpoint { - static var researchAnnexLattice: Viewpoint { - Viewpoint( - center: - Point( - x: -13045075.712950204, - y: 4036858.6146756615, - spatialReference: .webMercator - ), - scale: 550.0 - ) - } + static let researchAnnexLattice = Viewpoint( + center: + Point( + x: -13045075.712950204, + y: 4036858.6146756615, + spatialReference: .webMercator + ), + scale: 550.0 + ) static let losAngeles = Viewpoint( center: Point( From 5ff89ecd7b4d01560168de7c9ac8dc63fe0695cc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 1 Jun 2022 10:51:11 -0700 Subject: [PATCH 0999/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 2794410f7..13fc0184e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -164,11 +164,11 @@ struct SiteAndFacilitySelector: View { shouldUpdateViewModel = false selectedSite = viewModel.selectedSite } - .onChange(of: selectedSite) { _ in - if shouldUpdateViewModel, let site = selectedSite { - viewModel.setSite(site, zoomTo: true) - } - shouldUpdateViewModel = true + .onChange(of: selectedSite) { _ in + if shouldUpdateViewModel, let site = selectedSite { + viewModel.setSite(site, zoomTo: true) + } + shouldUpdateViewModel = true } } } From 0d81624b2268fa6149f0b58085809e019b0d9c8f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 2 Jun 2022 15:39:09 -0700 Subject: [PATCH 1000/1722] Bind site selection to view model selection --- .../FloorFilter/SiteAndFacilitySelector.swift | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 13fc0184e..eb37bca56 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -48,21 +48,14 @@ struct SiteAndFacilitySelector: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel - /// Indicates whether the view model should be notified of the selection update. - @State private var shouldUpdateViewModel = true - /// Indicates that the keyboard is animating and some views may require reload. @State private var isKeyboardAnimating = false /// A site name filter phrase entered by the user. @State private var query: String = "" - /// A local record of the site selected in the view model. - /// - /// As the view model's selection will change to `.facility(FloorFacility)` and - /// `.level(FloorLevel)` over time, this is needed to keep track of the site at the top of the - /// hierarchy to keep the site selection persistent in the navigation view. - @State private var selectedSite: FloorSite? + /// Indicates whether a site entry should be considered "selected" in the list. + @State var shouldAutoSelect = false /// Sites contained in a `FloorManager`. let sites: [FloorSite] @@ -147,28 +140,29 @@ struct SiteAndFacilitySelector: View { NavigationLink( site.name, tag: site, - selection: $selectedSite + selection: Binding( + get: { + return shouldAutoSelect ? viewModel.selectedSite : nil + }, + set: { newSite in + guard let newSite = newSite else { return } + viewModel.setSite(newSite, zoomTo: true) + } + ) ) { FacilitiesList( facilities: site.facilities, presentationStyle: .standard, isHidden: isHidden ) + .onDisappear { + shouldAutoSelect = false + } } } .listStyle(.plain) .onChange(of: viewModel.selection) { _ in - // Setting the `shouldUpdateViewModel` flag false allows - // `selectedSite` to receive upstream updates from the view - // model without republishing them back up to the view model. - shouldUpdateViewModel = false - selectedSite = viewModel.selectedSite - } - .onChange(of: selectedSite) { _ in - if shouldUpdateViewModel, let site = selectedSite { - viewModel.setSite(site, zoomTo: true) - } - shouldUpdateViewModel = true + shouldAutoSelect = true } } } From e941e9f1b1c9754541b2548a3a11ceb4e0053bf7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 2 Jun 2022 15:40:06 -0700 Subject: [PATCH 1001/1722] 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 eb37bca56..76062e121 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -119,7 +119,7 @@ struct SiteAndFacilitySelector: View { isHidden: isHidden ) } - .padding([.vertical], 4) + .padding([.vertical], 4) } .navigationTitle("Sites") .navigationBarTitleDisplayMode(.inline) From 7fc55c5a4284c0cc8ca30736e802c48294b2b496 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Fri, 3 Jun 2022 14:26:33 -0700 Subject: [PATCH 1002/1722] Refactor `Compass` to use heading --- .../Components/Compass/Compass.swift | 138 +++++++++--------- 1 file changed, 73 insertions(+), 65 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 26970129f..dd1deee5e 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -14,90 +14,98 @@ 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 { - /// Determines if the compass should automatically hide/show itself when the parent view is oriented - /// north. + /// A Boolean value indicating whether the compass should automatically + /// hide/show itself when the heading is `0`. private let autoHide: Bool - /// Controls the current opacity of the compass. - @State private var opacity: Double + /// The opacity of the compass. + @State private var opacity: Double = .zero - /// Indicates if the compass should hide based on the current viewpoint rotation and autoHide - /// preference. - var shouldHide: Bool { - guard let viewpoint = viewpoint else { return autoHide } - return viewpoint.rotation.isZero && autoHide - } + /// A Boolean value indicating whether the compass should hide based on the + /// current heading and whether the compass automatically hides. + private var shouldHide: Bool { heading.isZero && autoHide } - /// Acts as link between the compass and the parent map or scene view. - @Binding private var viewpoint: Viewpoint? + /// The heading of the compass in degrees. + @Binding private var heading: Double - /// Creates a `Compass` + /// Creates a compass with a binding to a heading. /// - Parameters: - /// - viewpoint: Acts a communication link between the `MapView` or `SceneView` and the - /// compass. - /// - autoHide: Determines if the compass automatically hides itself when the `MapView` or - /// `SceneView` is oriented north. + /// - heading: The heading of the compass. + /// - autoHide: A Boolean value that determines whether the compass + /// automatically hides itself when the heading is `0`. public init( - viewpoint: Binding, + heading: Binding, autoHide: Bool = true ) { - _viewpoint = viewpoint - _opacity = State(initialValue: .zero) + _heading = heading self.autoHide = autoHide } public var body: some View { - GeometryReader { geometry in - ZStack { - CompassBody() - Needle() - .rotationEffect( - Angle(degrees: viewpoint?.compassHeading ?? .zero) - ) - } - .frame( - width: min(geometry.size.width, geometry.size.height), - height: min(geometry.size.width, geometry.size.height) - ) - } - .opacity(opacity) - .onTapGesture { resetHeading() } - .onChange(of: viewpoint) { _ in - let newOpacity: Double = shouldHide ? .zero : 1 - guard opacity != newOpacity else { return } - withAnimation(.default.delay(shouldHide ? 0.25 : 0)) { - opacity = newOpacity - } + if !heading.isNaN { + CompassBody() + .overlay { + Needle() + .rotationEffect(.degrees(heading)) + } + .aspectRatio(1, contentMode: .fit) + .opacity(opacity) + .onTapGesture { heading = .zero } + .onChange(of: heading) { _ in + let newOpacity: Double = shouldHide ? .zero : 1 + guard opacity != newOpacity else { return } + withAnimation(.default.delay(shouldHide ? 0.25 : 0)) { + opacity = newOpacity + } + } + .onAppear { opacity = shouldHide ? 0 : 1 } + .accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)") } - .onAppear { opacity = shouldHide ? 0 : 1 } - .accessibilityLabel(viewpoint?.compassHeadingDescription ?? "Compass") - } - - /// Resets the viewpoints `rotation` to zero. - func resetHeading() { - guard let viewpoint = viewpoint else { return } - self.viewpoint = Viewpoint( - center: viewpoint.targetGeometry.extent.center, - scale: viewpoint.targetScale, - rotation: .zero - ) } } -internal extension Viewpoint { - /// The heading appropriate for displaying a compass. - /// - Remark: The viewpoint rotation is opposite of the direction of a compass needle. - var compassHeading: Double { - rotation.isZero ? .zero : 360 - rotation +public extension Compass { + /// Creates a compass with a binding to a viewpoint rotation. + /// - Parameters: + /// - viewpointRotation: The viewpoint rotation whose value determines the + /// heading of the compass. + /// - autoHide: A Boolean value that determines whether the compass + /// automatically hides itself when the viewpoint rotation is 0 degrees. + init( + viewpointRotation: Binding, + autoHide: Bool = true + ) { + 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, autoHide: autoHide) } - /// A text description of the current heading, sutiable for accessibility voiceover. - var compassHeadingDescription: String { - "Compass, heading " - + Int(self.compassHeading.rounded()).description - + " degrees " - + CompassDirection(self.compassHeading).rawValue + /// Creates a compass with a binding to an optional viewpoint. + /// - Parameters: + /// - viewpoint: The viewopint whose rotation determines the heading of + /// the compass. + /// - autoHide: A Boolean value that determines whether the compass + /// automatically hides itself when the viewpoint's rotation is 0 degrees. + init( + viewpoint: Binding, + autoHide: Bool = true + ) { + let viewpointRotation = Binding { + viewpoint.wrappedValue?.rotation ?? .nan + } set: { newViewpointRotation in + guard let oldViewopint = viewpoint.wrappedValue else { return } + viewpoint.wrappedValue = Viewpoint( + center: oldViewopint.targetGeometry.extent.center, + scale: oldViewopint.targetScale, + rotation: newViewpointRotation + ) + } + self.init(viewpointRotation: viewpointRotation, autoHide: autoHide) } } From 194e7e4131684776b4d557a0a91bd7f5f35eaf92 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 7 Jun 2022 13:33:38 -0700 Subject: [PATCH 1003/1722] Apply latest PR feedback --- .../FloorFilter/FloorFilterViewModel.swift | 36 +++++++++---------- .../ArcGISToolkit/Extensions/EdgeInsets.swift | 4 +-- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 8a63c52f1..3f12a067b 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -65,30 +65,15 @@ final class FloorFilterViewModel: ObservableObject { /// The selected site, floor, or level. @Published private(set) var selection: Selection? - /// The `Viewpoint` used to pan/zoom to the selected site/facilty. - /// If `nil`, there will be no automatic pan/zoom operations. - private var viewpoint: Binding - - func onViewpointChanged(_ viewpoint: Viewpoint?) { - guard let viewpoint = viewpoint, - !viewpoint.targetScale.isZero else { - return - } - automaticallySelectFacilityOrSite() - } - // MARK: Constants /// The selection behavior of the floor filter. private let automaticSelectionMode: FloorFilterAutomaticSelectionMode - /// The amount of time to wait between selection updates. - private let delay = DispatchQueue.SchedulerTimeType.Stride.seconds(0.20) - /// The `FloorManager` containing the site, floor, and level information. let floorManager: FloorManager - // MARK: Public members + // MARK: Properties /// The floor manager facilities. var facilities: [FloorFacility] { @@ -152,7 +137,16 @@ final class FloorFilterViewModel: ObservableObject { .sorted(by: { $0.verticalOrder > $1.verticalOrder }) ?? [] } - // MARK: Public methods + // MARK: Methods + + /// Allows model users to alert the model that the viewpoint has changed. + func onViewpointChanged(_ viewpoint: Viewpoint?) { + guard let viewpoint = viewpoint, + !viewpoint.targetScale.isZero else { + return + } + automaticallySelectFacilityOrSite() + } /// Updates the selected site, facility, and level based on a newly selected facility. /// - Parameters: @@ -195,8 +189,6 @@ final class FloorFilterViewModel: ObservableObject { } } - // MARK: Private items - /// 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. @@ -209,6 +201,12 @@ final class FloorFilterViewModel: ObservableObject { } } + // MARK: Private items + + /// The `Viewpoint` used to pan/zoom to the selected site/facilty. + /// If `nil`, there will be no automatic pan/zoom operations. + private var viewpoint: Binding + /// Updates `selectedFacility` if a good selection exists. /// - Returns: `false` if a selection was not made. @discardableResult diff --git a/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift b/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift index bcc3eacad..fec49f8c2 100644 --- a/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift +++ b/Sources/ArcGISToolkit/Extensions/EdgeInsets.swift @@ -13,9 +13,9 @@ import SwiftUI -extension EdgeInsets { +public extension EdgeInsets { /// A reusable set of edge insets for use across toolkit components. - public static let toolkitDefault = Self( + static let toolkitDefault = Self( top: 8, leading: 12, bottom: 8, From c49e6fe7c984a770ff90b3af2ded256be09c6f25 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Tue, 7 Jun 2022 15:44:52 -0700 Subject: [PATCH 1004/1722] Expand documentation for heading and viewpoint rotation --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index dd1deee5e..d17f6a9a5 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -31,7 +31,9 @@ public struct Compass: View { /// The heading of the compass in degrees. @Binding private var heading: Double - /// Creates a compass with a binding to a heading. + /// 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. /// - autoHide: A Boolean value that determines whether the compass @@ -68,7 +70,9 @@ public struct Compass: View { } public extension Compass { - /// Creates a compass with a binding to a viewpoint rotation. + /// 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. From 48f1d34ce1317f250b04d2fc63e3e8f85808f6c2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 7 Jun 2022 16:47:36 -0700 Subject: [PATCH 1005/1722] Remove `UIResponder` workaround --- .../Components/FloorFilter/FloorFilter.swift | 22 +++++++++---- .../FloorFilter/SiteAndFacilitySelector.swift | 32 ++----------------- 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 192be32fe..0023dfc11 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -93,13 +93,23 @@ public struct FloorFilter: View { } /// A configured `SiteAndFacilitySelector` view. + /// + /// The layering of the `SiteAndFacilitySelector` over a `RoundedRectangle` is needed to + /// produce a rounded corners effect. We can not simply use `.esriBorder()` here because + /// applying the `cornerRadius()` modifier on `SiteAndFacilitySelector`'s underlying + /// `NavigationView` causes a rendering bug. This bug remains in iOS 16 with + /// `NavigationStack` and has been reported to Apple as FB10034457. private var siteAndFacilitySelector: some View { - SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) - .esriBorder() - .opacity(isSitesAndFacilitiesHidden ? .zero : 1) - .onChange(of: viewpoint.wrappedValue) { - viewModel.onViewpointChanged($0) - } + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(Color(uiColor: .systemBackground)) + SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) + .onChange(of: viewpoint.wrappedValue) { + viewModel.onViewpointChanged($0) + } + .padding() + } + .opacity(isSitesAndFacilitiesHidden ? .zero : 1) } /// Indicates that the selector should be presented with a top oriented aligment configuration. diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 76062e121..13846d592 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -48,9 +48,6 @@ struct SiteAndFacilitySelector: View { /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel - /// Indicates that the keyboard is animating and some views may require reload. - @State private var isKeyboardAnimating = false - /// A site name filter phrase entered by the user. @State private var query: String = "" @@ -74,34 +71,8 @@ struct SiteAndFacilitySelector: View { } } - var body: some View { - siteListAndFilterView - // Trigger a reload on keyboard frame changes for proper layout - // across all devices. - .opacity(isKeyboardAnimating ? 0.99 : 1.0) - .navigationViewStyle(.stack) - .onReceive( - NotificationCenter.default.publisher( - for: UIResponder.keyboardWillChangeFrameNotification - ) - ) { _ in - withAnimation { - isKeyboardAnimating = true - } - } - .onReceive( - NotificationCenter.default.publisher( - for: UIResponder.keyboardDidChangeFrameNotification - ) - ) { _ in - withAnimation { - isKeyboardAnimating = false - } - } - } - /// A view containing a filter-via-name field, a list of the site names and an "All sites" button. - var siteListAndFilterView: some View { + var body: some View { NavigationView { VStack { TextField("Filter sites", text: $query) @@ -129,6 +100,7 @@ struct SiteAndFacilitySelector: View { } } } + .navigationViewStyle(.stack) } /// A view containing a list of the site names. From e4ad6db680f0cf69dc34456071175b1038324e2f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 8 Jun 2022 08:04:23 -0700 Subject: [PATCH 1006/1722] Add esriBorder back to the new background --- Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 0023dfc11..60a169772 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -103,6 +103,7 @@ public struct FloorFilter: View { ZStack { RoundedRectangle(cornerRadius: 8) .fill(Color(uiColor: .systemBackground)) + .esriBorder() SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) .onChange(of: viewpoint.wrappedValue) { viewModel.onViewpointChanged($0) From ca77f58afccf498380b14e72c1a8ff9fc7d38be4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 9 Jun 2022 14:55:41 -0700 Subject: [PATCH 1007/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 13846d592..0d40b1b08 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -63,11 +63,11 @@ struct SiteAndFacilitySelector: View { /// A subset of `sites` with names containing `searchPhrase` or all `sites` if /// `searchPhrase` is empty. var matchingSites: [FloorSite] { - if query.isEmpty { + guard !query.isEmpty else { return sites } return sites.filter { - $0.name.lowercased().contains(query.lowercased()) + $0.name.localizedStandardContains(query) } } @@ -169,11 +169,11 @@ struct SiteAndFacilitySelector: View { /// A subset of `facilities` with names containing `searchPhrase` or all /// `facilities` if `searchPhrase` is empty. var matchingFacilities: [FloorFacility] { - if query.isEmpty { + guard !query.isEmpty else { return facilities } return facilities.filter { - $0.name.lowercased().contains(query.lowercased()) + $0.name.localizedStandardContains(query) } } @@ -218,7 +218,7 @@ 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 slighlty different + /// 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 From 97ba4c2c5adf24dcee33a297d1c978ac60c65b60 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 10 Jun 2022 14:08:59 -0700 Subject: [PATCH 1008/1722] Update SiteAndFacilitySelector.swift --- .../Components/FloorFilter/SiteAndFacilitySelector.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index aeff8084c..4a80abea2 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -50,9 +50,9 @@ struct SiteAndFacilitySelector: View { /// `searchPhrase` is empty. var matchingSites: [FloorSite] { guard !query.isEmpty else { - return sites + return viewModel.sites } - return sites.filter { + return viewModel.sites.filter { $0.name.localizedStandardContains(query) } } From 8a3b52d83f8957a8f49d98ccf92366ce5db4eafb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 10 Jun 2022 14:39:54 -0700 Subject: [PATCH 1009/1722] Undo unneeded name change --- .../Components/FloorFilter/FloorFilter.swift | 12 ++++++------ .../FloorFilter/SiteAndFacilitySelector.swift | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 5cdb54afd..773cbdf95 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -58,7 +58,7 @@ public struct FloorFilter: View { @State private var isLevelsViewCollapsed: Bool = false /// A Boolean value that indicates whether the site and facility selector is presented. - @State private var siteAndFacilitySelectorIsPresented: Bool = false + @State private var isSitesAndFacilitiesHidden: Bool = false /// The alignment configuration. private let alignment: Alignment @@ -73,16 +73,16 @@ public struct FloorFilter: View { /// Button to open and close the site and facility selector. private var sitesAndFacilitiesButton: some View { Button { - siteAndFacilitySelectorIsPresented.toggle() + isSitesAndFacilitiesHidden.toggle() } label: { Image(systemName: "building.2") .padding(.toolkitDefault) } .sheet( isAllowed: isCompact, - isPresented: $siteAndFacilitySelectorIsPresented + isPresented: $isSitesAndFacilitiesHidden ) { - SiteAndFacilitySelector(isHidden: $siteAndFacilitySelectorIsPresented) + SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) .onChange(of: viewpoint.wrappedValue) { viewModel.onViewpointChanged($0) } @@ -141,13 +141,13 @@ public struct FloorFilter: View { RoundedRectangle(cornerRadius: 8) .fill(Color(uiColor: .systemBackground)) .esriBorder() - SiteAndFacilitySelector(isHidden: $siteAndFacilitySelectorIsPresented) + SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) .onChange(of: viewpoint.wrappedValue) { viewModel.onViewpointChanged($0) } .padding() } - .opacity(siteAndFacilitySelectorIsPresented ? .zero : 1) + .opacity(isSitesAndFacilitiesHidden ? .zero : 1) } } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 4a80abea2..576cf4cab 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -82,6 +82,7 @@ struct SiteAndFacilitySelector: View { ) } .buttonStyle(.bordered) + .padding([.bottom]) } } .searchable( From f8355e4af880a86d3eb9946bcbd4ea31b84043fe Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 10 Jun 2022 17:30:29 -0700 Subject: [PATCH 1010/1722] Create IsCompactKey.swift --- .../ArcGISToolkit/Utility/IsCompactKey.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Sources/ArcGISToolkit/Utility/IsCompactKey.swift diff --git a/Sources/ArcGISToolkit/Utility/IsCompactKey.swift b/Sources/ArcGISToolkit/Utility/IsCompactKey.swift new file mode 100644 index 000000000..14f69bb1a --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/IsCompactKey.swift @@ -0,0 +1,19 @@ +// 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 `EnvironmentKey` that specifies compact environment state. +struct IsCompactKey: EnvironmentKey { + static let defaultValue = false +} From d6d6a576dfc284103f8a6da7515b5ede698f61d9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 10 Jun 2022 17:30:45 -0700 Subject: [PATCH 1011/1722] Create EnvironmentValues.swift --- .../Extensions/EnvironmentValues.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift diff --git a/Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift b/Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift new file mode 100644 index 000000000..f6bdd9616 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift @@ -0,0 +1,22 @@ +// 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 + +extension EnvironmentValues { + /// Indicates whether the environment is compact. + var isCompact: Bool { + get { self[IsCompactKey.self] } + set { self[IsCompactKey.self] = newValue } + } +} From 5c1de3ae4f49c0076a70c47664705bdaf4af97f2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 10 Jun 2022 17:31:26 -0700 Subject: [PATCH 1012/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 576cf4cab..5bcf77729 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -34,6 +34,8 @@ struct SiteAndFacilitySelector: View { /// A view displaying the sites contained in a `FloorManager`. struct SitesList: View { + @Environment(\.isCompact) var isCompact: Bool + /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel @@ -82,7 +84,7 @@ struct SiteAndFacilitySelector: View { ) } .buttonStyle(.bordered) - .padding([.bottom]) + .padding([.bottom], isCompact ? 5 : 0) } } .searchable( @@ -141,6 +143,8 @@ struct SiteAndFacilitySelector: View { /// A view displaying the facilities contained in a `FloorManager`. struct FacilitiesList: View { + @Environment(\.isCompact) var isCompact: Bool + /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel @@ -206,7 +210,9 @@ struct SiteAndFacilitySelector: View { List(matchingFacilities, id: \.id) { facility in Button { viewModel.setFacility(facility, zoomTo: true) - isHidden.toggle() + if isCompact { + isHidden.toggle() + } } label: { VStack { Text(facility.name) From cb14064a746c037ff4f2f58055c76708a80fa4e1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 10 Jun 2022 17:31:48 -0700 Subject: [PATCH 1013/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 773cbdf95..71ef8f47d 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -36,11 +36,13 @@ public struct FloorFilter: View { /// - alignment: Determines the display configuration of Floor Filter elements. /// - automaticSelectionMode: The selection behavior of the floor filter. /// - viewpoint: Viewpoint updated when the selected site or facility changes. + /// - isNavigating: The map is currently being navigated. public init( floorManager: FloorManager, alignment: Alignment, automaticSelectionMode: FloorFilterAutomaticSelectionMode = .always, - viewpoint: Binding = .constant(nil) + viewpoint: Binding = .constant(nil), + isNavigating: Binding ) { _viewModel = StateObject(wrappedValue: FloorFilterViewModel( automaticSelectionMode: automaticSelectionMode, @@ -48,6 +50,7 @@ public struct FloorFilter: View { viewpoint: viewpoint )) self.alignment = alignment + self.isNavigating = isNavigating self.viewpoint = viewpoint } @@ -83,8 +86,8 @@ public struct FloorFilter: View { isPresented: $isSitesAndFacilitiesHidden ) { SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) - .onChange(of: viewpoint.wrappedValue) { - viewModel.onViewpointChanged($0) + .onChange(of: viewpoint.wrappedValue) { viewpoint in + reportChange(of: viewpoint) } } } @@ -115,11 +118,20 @@ public struct FloorFilter: View { ) } + /// The map is currently being navigated. + private var isNavigating: Binding + /// Indicates that the selector should be presented with a top oriented aligment configuration. private var isTopAligned: Bool { 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) + } + /// Displays the available levels. @ViewBuilder private var levelSelector: some View { LevelSelector( @@ -142,8 +154,8 @@ public struct FloorFilter: View { .fill(Color(uiColor: .systemBackground)) .esriBorder() SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) - .onChange(of: viewpoint.wrappedValue) { - viewModel.onViewpointChanged($0) + .onChange(of: viewpoint.wrappedValue) { viewpoint in + reportChange(of: viewpoint) } .padding() } @@ -163,6 +175,7 @@ public struct FloorFilter: View { } // Ensure space for filter text field on small screens in landscape .frame(minHeight: 100) + .environment(\.isCompact, isCompact) .environmentObject(viewModel) .disabled(viewModel.isLoading) } From d200d0ed7a8255cf737dddc1f557e548723a679d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 10 Jun 2022 17:33:05 -0700 Subject: [PATCH 1014/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 3185cb037..24556453a 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -29,6 +29,9 @@ struct FloorFilterExampleView: View { /// Determines the appropriate time to initialize the `FloorFilter`. @State private var isMapLoaded: Bool = false + /// The map is currently being navigated. + @State private var isNavigating: Bool = false + @State private var mapLoadError: Bool = false /// The initial viewpoint of the map. @@ -48,6 +51,9 @@ struct FloorFilterExampleView: View { map: map, viewpoint: viewpoint ) + .onNavigatingChanged { + isNavigating = $0 + } .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } @@ -59,7 +65,8 @@ struct FloorFilterExampleView: View { FloorFilter( floorManager: floorManager, alignment: floorFilterAlignment, - viewpoint: $viewpoint + viewpoint: $viewpoint, + isNavigating: $isNavigating ) .frame( maxWidth: 400, From cf3f10fe089ff1337f0896f573dd4795fa3af964 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 13 Jun 2022 14:22:14 -0600 Subject: [PATCH 1015/1722] update --- Examples/Examples/AuthenticationExampleView.swift | 4 ++-- .../Components/Authentication/Authenticator.swift | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 547a09c6f..e47fc4ace 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -16,7 +16,7 @@ import ArcGIS import ArcGISToolkit struct AuthenticationExampleView: View { - @ObservedObject var authenticator = Authenticator( + @MainActor var authenticator = Authenticator( promptForUntrustedHosts: true//, oAuthConfigurations: [.arcgisDotCom] ) @State var previousApiKey: APIKey? @@ -42,7 +42,7 @@ struct AuthenticationExampleView: View { } .authentication(authenticator: authenticator) .task { - try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) + //try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .onAppear { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 553b79fcd..98c4613f2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -34,22 +34,18 @@ public final class Authenticator: ObservableObject { } /// Sets up a credential store that is synchronized with the keychain. - /// - Remark: If no group is specified then the item will be stored in the default group. + /// - Remark: The item will be stored in the default access group. /// To know more about what the default group would be you can find information about that 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. - /// - groupIdentifier: The identifier of the group that credentials should be persisted to - /// within the keychain. /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. public func synchronizeWithKeychain( access: ArcGIS.KeychainAccess, - groupIdentifier: String? = nil, isCloudSynchronizable: Bool = false ) async throws { ArcGISURLSession.credentialStore = try await .makePersistent( access: access, - groupIdentifier: groupIdentifier, isSynchronizable: isCloudSynchronizable ) certificateStore = try await CertificateCredentialStore( @@ -125,10 +121,6 @@ extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { - guard challenge.proposedCredential == nil else { - return .performDefaultHandling - } - // Queue up the challenge. let queuedChallenge = QueuedArcGISChallenge(arcGISChallenge: challenge) subject.send(queuedChallenge) From c054121f5e9b4ac43f74941c0da1857fb71937ce Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:05:41 -0700 Subject: [PATCH 1016/1722] Update Examples/Examples/FloorFilterExampleView.swift Co-authored-by: Zachary A Kline --- Examples/Examples/FloorFilterExampleView.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 24556453a..ab2727f2a 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -18,9 +18,10 @@ import ArcGIS struct FloorFilterExampleView: View { /// Make a map from a portal item. static func makeMap() -> Map { - let portal = Portal(url: URL(string: "https://www.arcgis.com/")!, isLoginRequired: false) - let portalItem = PortalItem(portal: portal, id: Item.ID(rawValue: "b4b599a43a474d33946cf0df526426f5")!) - return Map(item: portalItem) + Map(item: PortalItem( + portal: .arcGISOnline(isLoginRequired: false), + id: Item.ID("b4b599a43a474d33946cf0df526426f5")! + )) } /// Determines the arrangement of the inner `FloorFilter` UI componenets. From 1bcc08dd6a761be13d5edc42a4d9c79ec418183f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:17:31 -0700 Subject: [PATCH 1017/1722] Update Examples/Examples/FloorFilterExampleView.swift Co-authored-by: Zachary A Kline --- 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 ab2727f2a..50873c9a7 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -31,7 +31,7 @@ struct FloorFilterExampleView: View { @State private var isMapLoaded: Bool = false /// The map is currently being navigated. - @State private var isNavigating: Bool = false + @State private var isNavigating = false @State private var mapLoadError: Bool = false From cd80d18bb05e31874e4d2a0f9ce54b2e36a383b9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:18:12 -0700 Subject: [PATCH 1018/1722] Update FloorFilterExampleView.swift --- Examples/Examples/FloorFilterExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloorFilterExampleView.swift b/Examples/Examples/FloorFilterExampleView.swift index 50873c9a7..5b1a2142a 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -28,12 +28,12 @@ struct FloorFilterExampleView: View { private let floorFilterAlignment = Alignment.bottomLeading /// Determines the appropriate time to initialize the `FloorFilter`. - @State private var isMapLoaded: Bool = false + @State private var isMapLoaded = false /// The map is currently being navigated. @State private var isNavigating = false - @State private var mapLoadError: Bool = false + @State private var mapLoadError = false /// The initial viewpoint of the map. @State private var viewpoint: Viewpoint? = Viewpoint( From 2b523881e71ce9805de98f0f0ea652b1228843a5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:19:15 -0700 Subject: [PATCH 1019/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift Co-authored-by: Zachary A Kline --- 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 71ef8f47d..aab621f18 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -27,7 +27,7 @@ public struct FloorFilter: View { /// If `true`, the site and facility selector will appear as a sheet. /// If `false`, the site and facility selector will appear as a popup modal alongside the level selector. private var isCompact: Bool { - return horizontalSizeClass == .compact || verticalSizeClass == .compact + horizontalSizeClass == .compact || verticalSizeClass == .compact } /// Creates a `FloorFilter`. From e9baf2c6506f1ad6e48c68325d3f78b12d6f6c3d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:19:27 -0700 Subject: [PATCH 1020/1722] Update Examples/Examples/FloorFilterExampleView.swift Co-authored-by: Zachary A Kline --- 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 5b1a2142a..6a1d89951 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -30,7 +30,7 @@ struct FloorFilterExampleView: View { /// Determines the appropriate time to initialize the `FloorFilter`. @State private var isMapLoaded = false - /// The map is currently being navigated. + /// A Boolean value indicating if the map is currently being navigated. @State private var isNavigating = false @State private var mapLoadError = false From 28c6cef67200273bbeeea86c5e01c43f4846a93c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:19:38 -0700 Subject: [PATCH 1021/1722] Update Sources/ArcGISToolkit/Extensions/View.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Extensions/View.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index ce829075c..d58186541 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -13,7 +13,7 @@ import SwiftUI -/// A modifier which proivdes conditional control over when a sheet is used. +/// A modifier that provides conditional control over when a sheet is used. struct ConditionalSheetModifier: ViewModifier { /// A Boolean value that indicates whether `sheetContent` will be presented or not. var isAllowed: Bool From e8b5b1093da301170d83b5493fd1ef6635e144c9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:19:48 -0700 Subject: [PATCH 1022/1722] Update Sources/ArcGISToolkit/Extensions/View.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Extensions/View.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index d58186541..6f64044c9 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -80,7 +80,7 @@ extension View { modifier(SelectedModifier(isSelected: isSelected)) } - /// - Parameter isAllowed: Condition that determines if this sheet can be shown. + /// - Parameter isAllowed: A Boolean that indicates if this sheet can be shown. /// - Returns: Produces a sheet that is only shown if `isAllowed` is set `true`. func sheet( isAllowed: Bool, From 73d2e5b89f590ecc77fb827aeed2ae18b245a00d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:21:39 -0700 Subject: [PATCH 1023/1722] Update Sources/ArcGISToolkit/Extensions/Button.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Extensions/Button.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/Button.swift b/Sources/ArcGISToolkit/Extensions/Button.swift index d03907955..c5c1fdd6d 100644 --- a/Sources/ArcGISToolkit/Extensions/Button.swift +++ b/Sources/ArcGISToolkit/Extensions/Button.swift @@ -18,7 +18,7 @@ import SwiftUI /// `BorderedButtonStyle`. struct ButtonSelectedModifier: ViewModifier { /// A Boolean value that indicates whether view should display as selected. - var isSelected: Bool + let isSelected: Bool func body(content: Content) -> some View { if isSelected { From 2124b6cdd071f2b48bbd1be8cdcf44f7ace2b960 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:22:38 -0700 Subject: [PATCH 1024/1722] Update Sources/ArcGISToolkit/Extensions/View.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Extensions/View.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 6f64044c9..ffa75c113 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -16,7 +16,7 @@ import SwiftUI /// A modifier that provides conditional control over when a sheet is used. struct ConditionalSheetModifier: ViewModifier { /// A Boolean value that indicates whether `sheetContent` will be presented or not. - var isAllowed: Bool + let isAllowed: Bool /// Determines when the sheet is presented or not. var isPresented: Binding From 8e490654b4606eab96cac83403d4cf79ba75bbd9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 14 Jun 2022 14:23:21 -0700 Subject: [PATCH 1025/1722] Update Sources/ArcGISToolkit/Extensions/View.swift Co-authored-by: Zachary A Kline --- Sources/ArcGISToolkit/Extensions/View.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index ffa75c113..41495a82e 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -22,7 +22,7 @@ struct ConditionalSheetModifier: ViewModifier { var isPresented: Binding /// Content to be shown in the sheet. - var sheetContent: () -> SheetContent + let sheetContent: () -> SheetContent func body(content: Content) -> some View { if isAllowed { From b071a1b641ae4153b868be83e6ae097396383bcf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 9 Jun 2022 11:10:23 -0700 Subject: [PATCH 1026/1722] Add partial sheet --- Examples/Examples.xcodeproj/project.pbxproj | 8 +- .../Examples/FloatingPanelExampleView.swift | 2 +- .../Examples/PartialSheetExampleView.swift | 46 ++++++ Examples/ExamplesApp/Examples.swift | 3 +- .../FloatingPanel/FloatingPanel.swift | 17 --- .../Components/Handle/Handle.swift | 32 ++++ .../PartialSheet/PartialSheet.swift | 137 ++++++++++++++++++ 7 files changed, 224 insertions(+), 21 deletions(-) create mode 100644 Examples/Examples/PartialSheetExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/Handle/Handle.swift create mode 100644 Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 002f63bd9..673737728 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,8 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */; }; + 75230DAC2861392B009AF501 /* PartialSheetExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAB2861392B009AF501 /* PartialSheetExampleView.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 */; }; E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */; }; @@ -40,8 +41,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksExampleView.swift; sourceTree = ""; }; + 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSheetExampleView.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 = ""; }; E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloorFilterExampleView.swift; sourceTree = ""; }; @@ -85,6 +87,7 @@ E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, 75657E4727ABAC8400EE865B /* CompassExampleView.swift */, E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */, + 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -225,6 +228,7 @@ E48A73452658227100F5C118 /* Examples.swift in Sources */, 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, + 75230DAC2861392B009AF501 /* PartialSheetExampleView.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, E4AA9316276BF5ED000E6289 /* FloatingPanelExampleView.swift in Sources */, diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 59f1ec186..236a29448 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -38,7 +38,7 @@ struct FloatingPanelExampleView: View { } } -struct SampleContent: View { +private struct SampleContent: View { var body: some View { List(1..<21) { Text("\($0)") } .listStyle(.plain) diff --git a/Examples/Examples/PartialSheetExampleView.swift b/Examples/Examples/PartialSheetExampleView.swift new file mode 100644 index 000000000..268f35a61 --- /dev/null +++ b/Examples/Examples/PartialSheetExampleView.swift @@ -0,0 +1,46 @@ +// 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 +import ArcGISToolkit +import SwiftUI + +struct PartialSheetExampleView: View { + @StateObject private var map = Map(basemapStyle: .arcGISImagery) + + @State var preset: PartialSheetPreset? = .mid + + private let initialViewpoint = Viewpoint( + center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), + scale: 1_000_000 + ) + + var body: some View { + MapView( + map: map, + viewpoint: initialViewpoint + ) + .overlay { + PartialSheet(preset: $preset) { + SampleContent() + } + } + } +} + +private struct SampleContent: View { + var body: some View { + List(1..<21) { Text("\($0)") } + .listStyle(.plain) + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index e8d654441..e09b8843e 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -48,7 +48,8 @@ extension ExampleList { static let views = Self( name: "Views", examples: [ - AnyExample("Floating Panel", content: FloatingPanelExampleView()) + AnyExample("Floating Panel", content: FloatingPanelExampleView()), + AnyExample("Partial Sheet", content: PartialSheetExampleView()) ] ) } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 71cc2636c..066621083 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -72,23 +72,6 @@ public struct FloatingPanel: View where Content: View { } } -/// The "Handle" view of the floating panel. -private struct Handle: View { - /// The color of the handle. - var color: Color - - var body: some View { - RoundedRectangle(cornerRadius: 4.0) - .foregroundColor(color) - .frame(width: 100, height: 8.0) - } -} - private extension CGFloat { static let minHeight: CGFloat = 66 } - -private extension Color { - static var defaultHandleColor: Color { .secondary } - static var activeHandleColor: Color { .primary } -} diff --git a/Sources/ArcGISToolkit/Components/Handle/Handle.swift b/Sources/ArcGISToolkit/Components/Handle/Handle.swift new file mode 100644 index 000000000..f9c490f64 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Handle/Handle.swift @@ -0,0 +1,32 @@ +// 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 "Handle" view for resizeable containers. +struct Handle: View { + /// The color of the handle. + var color: Color + + var body: some View { + RoundedRectangle(cornerRadius: 4.0) + .foregroundColor(color) + .frame(width: 100, height: 8.0) + } +} + +extension Color { + static var defaultHandleColor: Color { .secondary } + static var activeHandleColor: Color { .primary } +} + diff --git a/Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift b/Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift new file mode 100644 index 000000000..edadc52e4 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift @@ -0,0 +1,137 @@ +// 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 + +public enum PartialSheetPreset { + case min, mid, max +} + +/// A partial sheet is a view that overlays a view and supplies view-related +/// content. +/// +/// A partial sheet offers an advantage over a native sheet in that it allows user interaction with any view +/// behind it. +public struct PartialSheet: View { + /// Records the drag gesture for new height calculations. + @State private var dragTranslation: CGFloat = .zero + + /// The color of the handle. + @State private var handleColor: Color = .defaultHandleColor + + /// Allows the user to drag the sheet to a specific height. + @State private var yOffset: CGFloat = 0 + + /// Allows the parent to control the height of the sheet. + @Binding var preset: PartialSheetPreset? + + /// The content shown in the floating panel. + private let content: Content + + /// Creates a `FloatingPanel` + /// - Parameter content: The view shown in the floating panel. + public init(preset: Binding, @ViewBuilder content: () -> Content) { + self.content = content() + _preset = preset + } + + /// Determines the current of the sheet, either from the preset value or `yOffset`. + private func heightFromPreset(with geometryProxy: GeometryProxy) -> CGFloat { + switch preset { + case .min: + return 200.0 + case .mid: + return geometryProxy.size.height / 2.0 + case .max: + return geometryProxy.size.height - 50.0 + default: + return yOffset + } + } + + public var body: some View { + GeometryReader { geometryProxy in + return VStack { + Handle(color: handleColor) + .padding() + .gesture(drag) + content + } + .frame(maxWidth: .infinity) + .frame(height: heightFromPreset(with: geometryProxy)) + .background(Color(uiColor: .systemGroupedBackground)) + .cornerRadius( + 15, + corners: [.topLeft, .topRight] + ) + .shadow(radius: 10) + .frame( + maxHeight: .infinity, + alignment: .bottom + ) + .edgesIgnoringSafeArea(.bottom) + .animation(.default, value: preset) + .onChange(of: dragTranslation) { newValue in + preset = nil + yOffset = max( + 200, + min( + geometryProxy.size.height - 50, + yOffset - newValue + ) + ) + } + } + } + + private var drag: some Gesture { + DragGesture(minimumDistance: 0) + .onChanged { value in + handleColor = .activeHandleColor + dragTranslation = value.translation.height + } + .onEnded { _ in + handleColor = .defaultHandleColor + } + } +} + +private struct RoundedCorner: Shape { + var corners: UIRectCorner + + var radius: CGFloat + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize( + width: radius, + height: radius + ) + ) + return Path(path.cgPath) + } +} + +private extension View { + func cornerRadius( + _ radius: CGFloat, + corners: UIRectCorner + ) -> some View { + clipShape(RoundedCorner( + corners: corners, + radius: radius + )) + } +} From 29d5513a086012cd49009f2dbc10c9ed4e2c3821 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 20 Jun 2022 17:11:31 -0700 Subject: [PATCH 1027/1722] Add Utility Network Trace Tool --- Examples/Examples.xcodeproj/project.pbxproj | 4 + .../UtilityNetworkTraceExampleView.swift | 78 +++ Examples/ExamplesApp/Examples.swift | 3 +- .../UtilityNetworkTrace.swift | 507 ++++++++++++++++++ .../UtilityNetworkTraceFunctionOutput.swift | 21 + .../UtilityNetworkTraceStartingPoint.swift | 43 ++ .../UtilityNetworkTraceViewModel.swift | 286 ++++++++++ .../UtilityNetworkTraceViewModelTrace.swift | 58 ++ .../UtilityNamedTraceConfiguration.swift | 27 + 9 files changed, 1026 insertions(+), 1 deletion(-) create mode 100644 Examples/Examples/UtilityNetworkTraceExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.swift create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift create mode 100644 Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 673737728..a9b07e4c5 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 75230DAC2861392B009AF501 /* PartialSheetExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */; }; + 75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */; }; 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75657E4727ABAC8400EE865B /* CompassExampleView.swift */; }; 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */; }; 75D41B2B27C6F21400624D7C /* ScalebarExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D41B2A27C6F21400624D7C /* ScalebarExampleView.swift */; }; @@ -42,6 +43,7 @@ /* Begin PBXFileReference section */ 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSheetExampleView.swift; sourceTree = ""; }; + 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityNetworkTraceExampleView.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 = ""; }; @@ -88,6 +90,7 @@ 75657E4727ABAC8400EE865B /* CompassExampleView.swift */, E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */, 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */, + 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -227,6 +230,7 @@ 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */, E48A73452658227100F5C118 /* Examples.swift in Sources */, 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */, + 75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, 75230DAC2861392B009AF501 /* PartialSheetExampleView.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift new file mode 100644 index 000000000..d1a4dcebd --- /dev/null +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -0,0 +1,78 @@ +// 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 +import ArcGISToolkit +import SwiftUI + +struct UtilityNetworkTraceExampleView: View { + @StateObject private var map = makeMap() + + /// 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 pointInMap: Point? + + /// Provides the ability to detect tap locations in the context of the screen. + @State var pointInScreen: CGPoint? + + /// A container for graphical trace results. + @State var resultGraphicsOverlay = GraphicsOverlay() + + @State var viewpoint: Viewpoint? + + var body: some View { + MapViewReader { mapViewProxy in + MapView( + map: map, + viewpoint: viewpoint, + graphicsOverlays: [resultGraphicsOverlay] + ) + .onSingleTapGesture { pointInScreen, pointInMap in + self.pointInScreen = pointInScreen + self.pointInMap = pointInMap + self.mapViewProxy = mapViewProxy + } + .edgesIgnoringSafeArea(.bottom) + .overlay { + UtilityNetworkTrace( + $resultGraphicsOverlay, + map, + $pointInMap, + $pointInScreen, + $mapViewProxy, + $viewpoint + ) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .bottomLeading + ) + } + } + } + + /// Make a map from a portal item. + static func makeMap() -> Map { + let portal = Portal( + url: URL(string: "https://www.arcgis.com/")!, + isLoginRequired: false + ) + let portalItem = PortalItem( + portal: portal, + id: Item.ID(rawValue: "471eb0bf37074b1fbb972b1da70fb310")! + ) + return Map(item: portalItem) + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index e09b8843e..2436823d3 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -41,7 +41,8 @@ extension ExampleList { AnyExample("Floor Filter", content: FloorFilterExampleView()), AnyExample("Overview Map", content: OverviewMapExampleView()), AnyExample("Scalebar", content: ScalebarExampleView()), - AnyExample("Search", content: SearchExampleView()) + AnyExample("Search", content: SearchExampleView()), + AnyExample("Utility Network Trace", content: UtilityNetworkTraceExampleView()) ] ) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift new file mode 100644 index 000000000..670332fa7 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -0,0 +1,507 @@ +// 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 +import SwiftUI + +public struct UtilityNetworkTrace: View { + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? + + @Environment(\.verticalSizeClass) + private var verticalSizeClass: UserInterfaceSizeClass? + + /// If `true`, the site and facility selector will appear as a sheet. + /// If `false`, the site and facility selector will appear as a popup modal alongside the level selector. + private var isCompact: Bool { + return horizontalSizeClass == .compact || verticalSizeClass == .compact + } + + // MARK: Enums + + /// Activities users will perform while creating a new trace. + private enum TraceCreationActivity: Hashable { + /// The user is adding starting points. + case addingStartingPoints + /// The user is inspecting details of a chosen starting point. + case inspectingStartingPoint(UtilityNetworkTraceStartingPoint) + /// The user is viewing the list of chosen starting points. + case viewingStartingPoints + /// The user is viewing the list of available trace configurations. + case viewingTraceConfigurations + } + + /// Activities users will perform while using the Utility Network Trace tool. + private enum UserActivity: Hashable { + /// The user is creating a new trace. + case creatingTrace(TraceCreationActivity?) + /// The user is viewing traces that have been created. + case viewingTraces + } + + // MARK: States + + /// The current user activity. + @State private var currentActivity: UserActivity = .creatingTrace(nil) + + /// The current height of the partial sheet displaying the Utility Network Trace Tool. + @State private var sheetHeight: PartialSheetPreset? = .mid + + /// Indicates if the warning that all traces will be deleted is presented. + @State private var warningIsPresented = false + + /// The view model used by the view. The `UtilityNetworkTraceViewModel` manages state. + /// The view observes `UtilityNetworkTraceViewModel` for changes in state. + @StateObject private var viewModel: UtilityNetworkTraceViewModel + + // MARK: Bindings + + /// 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. + @Binding private var mapViewProxy: MapViewProxy? + + /// Acts as the point of identification for items tapped in the utility network. + @Binding private var pointInScreen: CGPoint? + + /// Acts as the point at which newly selected starting point graphics will be created. + @Binding private var pointInMap: Point? + + /// Allows the Utility Network Trace Tool to update the parent map view's viewpoint. + @Binding private var viewpoint: Viewpoint? + + // MARK: ViewBuilders + + /// Allows the user to switch between the trace creation and viewing tabs. + @ViewBuilder private var activityPicker: some View { + Picker( + "Mode", + selection: Binding( + get: { + switch currentActivity { + case .creatingTrace(_): + return UserActivity.creatingTrace(nil) + case .viewingTraces: + return UserActivity.viewingTraces + } + }, set: { newActivity, _ in + currentActivity = newActivity + } + ) + ) { + Text("New trace").tag(UserActivity.creatingTrace(nil)) + Text("Results").tag(UserActivity.viewingTraces) + } + .pickerStyle(.segmented) + .padding() + } + + /// Displays the list of available named trace configurations. + @ViewBuilder private var configurationsList: some View { + if viewModel.configurations.isEmpty { + Text("No configurations available") + } else { + ForEach(viewModel.configurations, id: \.name) { configuration in + Button { + withAnimation { + viewModel.pendingTrace.configuration = configuration + currentActivity = .creatingTrace(nil) + } + } label: { + Label { + Text(configuration.name) + .lineLimit(1) + } icon: { + Image(systemName: "circle") + .symbolVariant(configuration == viewModel.pendingTrace.configuration ? .fill: .none) + } + } + } + } + } + + /// The main contents of Utility Network Trace Tool. + @ViewBuilder private var innerBody: some View { + VStack { + if !viewModel.completedTraces.isEmpty && !isAddingStartingPoints { + activityPicker + } + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .inspectingStartingPoint: + startingPointDetail + default: + newTraceTab + } + case .viewingTraces: + resultsTab + } + } + .animation(.default, value: currentActivity) + .onChange(of: pointInScreen) { newValue in + guard isAddingStartingPoints, + let mapViewProxy = mapViewProxy, + let pointInMap = pointInMap, + let pointInScreen = pointInScreen else { + return + } + currentActivity = .creatingTrace(.viewingStartingPoints) + Task { + await viewModel.setStartingPoint( + at: pointInScreen, + mapPoint: pointInMap, + with: mapViewProxy + ) + } + } + .onChange(of: isAddingStartingPoints) { newValue in + if newValue { + sheetHeight = .min + } else { + sheetHeight = .mid + } + } + } + + /// The tab that allows for a new trace to be configured. + @ViewBuilder private var newTraceTab: some View { + List { + if !isAddingStartingPoints { + Section("Trace Configuration") { + DisclosureGroup( + viewModel.pendingTrace.configuration?.name ?? "None selected", + isExpanded: configurationOptionsIsExpanded + ) { + configurationsList + } + } + } + Section("Starting Points") { + if isAddingStartingPoints { + Button { + currentActivity = .creatingTrace(nil) + } label: { + Text("Cancel starting point selection") + } + .tint(.red) + } else { + Button { + currentActivity = .creatingTrace(.addingStartingPoints) + } label: { + Text("Add new starting point") + } + DisclosureGroup( + "\(viewModel.pendingTrace.startingPoints.count) selected", + isExpanded: startingPointsListIsExpanded + ) { + startingPointsList + } + } + } + if !isAddingStartingPoints { + Section("Advanced") { + ColorPicker( + selection: $viewModel.pendingTrace.color + ) { + Text("Trace Color") + } + if !isCompact { + TextField( + "Trace Name", + text: $viewModel.pendingTrace.name + ) + } + } + } + } + if !isAddingStartingPoints { + Button { + viewModel.trace() + currentActivity = .viewingTraces + } label: { + Text("Trace") + } + .buttonStyle(.bordered) + .padding() + .disabled(!viewModel.canRunTrace) + } + } + + /// The tab that allows for viewing completed traces. + @ViewBuilder private var resultsTab: some View { + HStack { + Button { + viewModel.selectPreviousTrace() + } label: { + Image(systemName: "chevron.backward") + } + Text(currentTraceLabel) + .padding(.horizontal) + Button { + viewModel.selectNextTrace() + } label: { + Image(systemName: "chevron.forward") + } + } + .font(.title3) + .padding() + if let traceName = viewModel.selectedTrace?.name, traceName != "" { + Text(traceName) + } + List { + Section("Element Result") { + DisclosureGroup(viewModel + .selectedTrace? + .utilityElementTraceResult? + .elements.count.description ?? "0") { + ForEach(viewModel.selectedTrace?.assetLabels ?? [], id: \.self) { label in + Text(label) + } + } + } + Section("Function Result") { + DisclosureGroup(viewModel + .selectedTrace? + .utilityFunctionTraceResult? + .functionOutputs.count.description ?? "0") { + ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.id) { item in + HStack { + Text(item.output.function.networkAttribute?.name ?? "Unnamed") + Spacer() + Text((item.output.result as? Double)?.description ?? "N/A") + } + } + } + } + } + Button { + warningIsPresented.toggle() + } label: { + Text("Clear All Results") + .tint(.red) + } + .padding() + .alert("Clear All Results", isPresented: $warningIsPresented) { + Button(role: .destructive) { + viewModel.deleteAllTraces() + currentActivity = .creatingTrace(nil) + } label: { + Text("OK") + } + } message: { + Text("Are you sure? All the trace Inputs and Results will be lost.") + } + } + + /// Displays information about a chosen starting point. + @ViewBuilder private var startingPointDetail: some View { + Button { + currentActivity = .creatingTrace(.viewingStartingPoints) + } label: { + Label { + Text("Back") + } icon: { + Image(systemName: "chevron.backward") + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + Text(selectedStartingPoint?.utilityElement.assetType.name ?? "Unnamed") + .font(.title3) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .center) + List { + Text(selectedStartingPoint?.utilityElement.globalID.uuidString ?? "N/A") + ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in + HStack{ + Text(item.key) + Spacer() + Text(item.value as? String ?? "") + } + } + } + Button { + sheetHeight = .min + if let selectedStartingPoint { + viewpoint = Viewpoint(targetExtent: selectedStartingPoint.extent) + } + } label: { + Label { + Text("Zoom To") + } icon: { + Image(systemName: "scope") + } + } + .padding() + } + + /// Displays the chosen starting points for the new trace. + @ViewBuilder private var startingPointsList: some View { + ForEach(viewModel.pendingTrace.startingPoints, id: \.utilityElement.globalID) { startingPoint in + Button { + currentActivity = .creatingTrace( + .inspectingStartingPoint(startingPoint) + ) + } label: { + Label { + Text(startingPoint.utilityElement.assetType.name) + .lineLimit(1) + } icon: { + Image(uiImage: startingPoint.image) + } + } + .swipeActions { + Button(role: .destructive) { + viewModel.delete(startingPoint) + } label: { + Image(systemName: "trash") + } + } + } + } + + /// A graphical interface to run pre-configured traces on a map's utility networks. + /// - Parameters: + /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace + /// graphics. + /// - map: The parent map view. + /// - pointInMap: Acts as the point at which newly selected starting point graphics will be + /// created. + /// - pointInScreen: 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. + public init( + _ graphicsOverlay: Binding, + _ map: Map, + _ pointInMap: Binding, + _ pointInScreen: Binding, + _ mapViewProxy: Binding, + _ viewpoint: Binding + ) { + _pointInScreen = pointInScreen + _pointInMap = pointInMap + _mapViewProxy = mapViewProxy + _graphicsOverlay = graphicsOverlay + _viewpoint = viewpoint + _viewModel = StateObject( + wrappedValue: UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: graphicsOverlay.wrappedValue + ) + ) + UITableView.appearance().backgroundColor = UIColor.systemGroupedBackground + } + + public var body: some View { + if isCompact { + PartialSheet(preset: $sheetHeight) { + innerBody + } + } else { + innerBody + .esriBorder() + .frame( + width: 400, + height: 450 + ) + .padding(30) + } + } + + // MARK: Computed Properties + + /// Indicates if the list of trace configuration options is expanded. + private var configurationOptionsIsExpanded: Binding { + Binding(get: { + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .viewingTraceConfigurations: + return true + default: + return false + } + default: + return false + } + }, set: { val in + if val { + currentActivity = .creatingTrace(.viewingTraceConfigurations) + } else { + currentActivity = .creatingTrace(nil) + } + }) + } + + /// Indicates if the list of chosen starting points is expanded. + private var startingPointsListIsExpanded: Binding { + Binding(get: { + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .viewingStartingPoints: + return true + default: + return false + } + default: + return false + } + }, set: { val in + if val { + currentActivity = .creatingTrace(.viewingStartingPoints) + } else { + currentActivity = .creatingTrace(nil) + } + }) + } + + /// Indicates the number of the trace currently being viewed out the total number of traces. + private var currentTraceLabel: String { + guard let index = viewModel.selectedTraceIndex else { return "Error" } + return "Trace \(index+1) of \(viewModel.completedTraces.count.description)" + } + + /// Indicates if the user is currently adding starting points. + private var isAddingStartingPoints: Bool { + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .addingStartingPoints: + return true + default: + return false + } + default: + return false + } + } + + /// The starting point being inspected (if one exists). + private var selectedStartingPoint: UtilityNetworkTraceStartingPoint? { + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .inspectingStartingPoint(let startingPoint): + return startingPoint + default: + return nil + } + default: + return nil + } + } +} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.swift new file mode 100644 index 000000000..fdec7ad7d --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.swift @@ -0,0 +1,21 @@ +// 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 +import Foundation + +/// A `Hashable` wrapper over `UtilityTraceFunctionOutput`. +struct UtilityNetworkTraceFunctionOutput { + let id = UUID() + let output: UtilityTraceFunctionOutput +} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift new file mode 100644 index 000000000..df8af957a --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift @@ -0,0 +1,43 @@ +// 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 +import UIKit + +/// A starting point of a utility network trace. +struct UtilityNetworkTraceStartingPoint { + /// The starting point's geometry. + var extent: Envelope + + /// The underlying geo element. + var geoElement: GeoElement + + /// The generated marker graphic. + var graphic: Graphic + + /// The symbol of the starting point as shown on the map. + var image: UIImage + + /// The underlying utility element as generated by the utility network. + var utilityElement: UtilityElement +} + +extension UtilityNetworkTraceStartingPoint: Hashable { + static func == (lhs: UtilityNetworkTraceStartingPoint, rhs: UtilityNetworkTraceStartingPoint) -> Bool { + lhs.utilityElement.globalID == rhs.utilityElement.globalID + } + + func hash(into hasher: inout Hasher) { + hasher.combine(utilityElement.globalID) + } +} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift new file mode 100644 index 000000000..46ca48a7f --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -0,0 +1,286 @@ +// 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 +import Foundation +import SwiftUI + +@MainActor final class UtilityNetworkTraceViewModel: ObservableObject { + // MARK: Published Properties + + /// A list of completed traces. + @Published private(set) var completedTraces = [Trace]() + + /// The available named trace configurations. + @Published private(set) var configurations = [UtilityNamedTraceConfiguration]() + + /// The trace currently under configuration. + @Published var pendingTrace = Trace() + + /// The index of the selected trace. + @Published private(set) var selectedTraceIndex: Int? { + didSet { + if let lastIndex = oldValue { + changeSelectedStateForTrace( + at: lastIndex, + to: false + ) + } + if let currentIndex = selectedTraceIndex { + changeSelectedStateForTrace( + at: currentIndex, + to: true + ) + } + } + } + + // MARK: Other Properties + + /// Indicates that the pending trace is configured to the point that it can be ran. + var canRunTrace: Bool { + pendingTrace.configuration != nil && !pendingTrace.startingPoints.isEmpty + } + + /// The overlay on which trace graphics will be drawn. + private var graphicsOverlay: GraphicsOverlay + + /// The utility network on which traces will be ran. + private var network: UtilityNetwork? + + /// The selected trace. + var selectedTrace: Trace? { + if let index = selectedTraceIndex { + return completedTraces[index] + } else { + return nil + } + } + + // MARK: Public Methods + + /// Performs required setup. + /// + /// The first utility network in the provided map will be used. + /// - Parameter map: The map to be loaded that contains at least one utility network. + /// - Parameter graphicsOverlay: The overlay on which trace graphics will be drawn. + init(map: Map, graphicsOverlay: GraphicsOverlay) { + self.graphicsOverlay = graphicsOverlay + Task { + await loadMap(map) + network = map.utilityNetworks.first + await loadNamedTraceConfigurations(map) + } + } + + /// Deletes the provided starting point from the pending trace. + /// - Parameter startingPoint: The starting point to be deleted. + func delete(_ startingPoint: UtilityNetworkTraceStartingPoint) { + pendingTrace.startingPoints.removeAll { + $0.utilityElement.globalID == startingPoint.utilityElement.globalID + } + graphicsOverlay.removeGraphic(startingPoint.graphic) + } + + /// Deletes all of the completed traces. + func deleteAllTraces() { + selectedTraceIndex = nil + completedTraces.forEach { traceResult in + traceResult.startingPoints.forEach { startingPoint in + graphicsOverlay.removeGraphic(startingPoint.graphic) + } + traceResult.graphics.forEach { graphic in + graphicsOverlay.removeGraphic(graphic) + } + } + completedTraces.removeAll() + } + + /// Selects the next trace from the list of completed traces. + func selectNextTrace() { + if let current = selectedTraceIndex { + if current + 1 <= completedTraces.count - 1 { + selectedTraceIndex = current + 1 + } else { + selectedTraceIndex = 0 + } + } + } + + /// Selects the previous trace from the list of completed traces. + func selectPreviousTrace() { + if let current = selectedTraceIndex { + if current - 1 >= 0 { + selectedTraceIndex = current - 1 + } else { + selectedTraceIndex = completedTraces.count - 1 + } + } + } + + /// Adds a new starting point 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 setStartingPoint( + at point: CGPoint, + mapPoint: Point, + with proxy: MapViewProxy + ) async { + let identifyLayerResults = try? await proxy.identifyLayers( + screenPoint: point, + tolerance: 10 + ) + identifyLayerResults?.forEach { identifyLayerResult in + identifyLayerResult.geoElements.forEach { geoElement in + Task { + guard let network = network, + let feature = geoElement as? ArcGISFeature, + let extent = feature.geometry?.extent, + let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? + .renderer? + .symbol(for: feature)? + .makeSwatch(scale: 1.0), + let utilityElement = network.createElement(arcGISFeature: feature) else { return } + let graphic = Graphic( + geometry: mapPoint, + symbol: SimpleMarkerSymbol( + color: UIColor(pendingTrace.color), + size: 20 + ) + ) + let startingPoint = UtilityNetworkTraceStartingPoint( + extent: extent, + geoElement: geoElement, + graphic: graphic, + image: symbol, + utilityElement: utilityElement + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.startingPoints.append(startingPoint) + } + } + } + } + + /// Runs the pending trace and stores it into the list of completed traces. + func trace() { + guard let config = pendingTrace.configuration, + let network = network else { return } + let params = UtilityTraceParameters( + namedTraceConfiguration: config, + startingLocations: pendingTrace.startingPoints.compactMap{ $0.utilityElement } + ) + Task { + let traceResults = try await network.trace(traceParameters: params) + var assetGroups = [String: Int]() + for result in traceResults { + switch result { + case let result as UtilityElementTraceResult: + result.elements.forEach({ element in + let count = assetGroups[element.assetGroup.name] ?? 0 + 1 + assetGroups.updateValue(count, forKey: element.assetGroup.name) + }) + assetGroups.forEach { (key, value) in + pendingTrace.assetLabels.append("\(key): \(value)") + } + pendingTrace.utilityElementTraceResult = result + case let result as UtilityGeometryTraceResult: + if let polygon = result.polygon { + let graphic = Graphic( + geometry: polygon, + symbol: SimpleLineSymbol( + style: .solid, + color: UIColor(pendingTrace.color), + width: 5.0 + ) + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.graphics.append(graphic) + } + if let polyline = result.polyline { + let graphic = Graphic( + geometry: polyline, + symbol: SimpleLineSymbol( + style: .dash, + color: UIColor(pendingTrace.color), + width: 5.0 + ) + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.graphics.append(graphic) + } + if let multipoint = result.multipoint { + let graphic = Graphic( + geometry: multipoint, + symbol: SimpleLineSymbol( + style: .dot, + color: UIColor(pendingTrace.color), + width: 5.0 + ) + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.graphics.append(graphic) + } + pendingTrace.utilityGeometryTraceResult = result + case let result as UtilityFunctionTraceResult: + let functionOutputs = result.functionOutputs + functionOutputs.forEach { functionOutput in + pendingTrace.functionOutputs.append( + UtilityNetworkTraceFunctionOutput(output: functionOutput) + ) + } + pendingTrace.utilityFunctionTraceResult = result + default: + break + } + } + completedTraces.append(pendingTrace) + selectedTraceIndex = completedTraces.count - 1 + pendingTrace = Trace() + } + } + + // MARK: Private Items + + /// Changes the selected state of the graphics for the completed trace at the provided index. + /// - Parameters: + /// - index: The index of the completed trace. + /// - isSelected: The new selection state. + private func changeSelectedStateForTrace( + at index: Int, + to isSelected: Bool + ) { + guard index >= 0, index <= completedTraces.count - 1 else { return } + completedTraces[index].graphics.forEach({ graphic in + graphic.isSelected = isSelected + }) + completedTraces[index].startingPoints.forEach({ startingPoint in + startingPoint.graphic.isSelected = isSelected + }) + } + + /// Loads the provided map. + /// - Parameter map: The map to be loaded. + private func loadMap(_ map: Map) async { + guard map.loadStatus != .loaded else { return } + try? await map.load() + } + + /// Loads the named trace configurations in the network on the provided map. + private func loadNamedTraceConfigurations(_ map: Map) async { + guard let network = network else { return } + configurations = (try? await map.getNamedTraceConfigurations(from: network)) ?? [] + } +} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift new file mode 100644 index 000000000..502e7e023 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -0,0 +1,58 @@ +// 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 +import SwiftUI + +extension UtilityNetworkTraceViewModel { + /// A trace performed on a utility network. + struct Trace { + /// A collection of asset labels for all elements returned in the trace. + var assetLabels = [String]() + + /// A user given color for the trace with a default value of green. + var color: Color = .green { + didSet { + // Update the color of any pre-existing starting points + startingPoints.forEach { startingPoint in + guard let symbol = startingPoint.graphic.symbol as? SimpleMarkerSymbol else { return } + symbol.color = UIColor(color) + } + } + } + + /// The chosen named trace configuration. + var configuration: UtilityNamedTraceConfiguration? + + var functionOutputs = [UtilityNetworkTraceFunctionOutput]() + + /// A collection of graphics generated from the trace. + var graphics = [Graphic]() + + /// A user given name for the trace. + var name: String = "" + + /// A collection of starting points for the trace. + var startingPoints = [UtilityNetworkTraceStartingPoint]() + + /// A trace result set comprised of an collection of UtilityElement objects. + var utilityElementTraceResult: UtilityElementTraceResult? + + /// A trace result set comprised of a collection of UtilityTraceFunctionOutput objects. + var utilityFunctionTraceResult: UtilityFunctionTraceResult? + + /// The trace result comprised of a set of Geometry objects representing the network elements + /// identified by the trace. + var utilityGeometryTraceResult: UtilityGeometryTraceResult? + } +} diff --git a/Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.swift b/Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.swift new file mode 100644 index 000000000..db1bee708 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.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 ArcGIS + +extension UtilityNamedTraceConfiguration: Hashable { + public static func == ( + lhs: UtilityNamedTraceConfiguration, + rhs: UtilityNamedTraceConfiguration + ) -> Bool { + lhs.globalID == rhs.globalID + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(globalID) + } +} From 7f31f7a1e4351660334f6adec8cdb06680a874be Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 20 Jun 2022 17:47:21 -0700 Subject: [PATCH 1028/1722] Update FloorFilter.swift --- .../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 aab621f18..8c1e13421 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -58,10 +58,10 @@ public struct FloorFilter: View { @StateObject private var viewModel: FloorFilterViewModel /// A Boolean value that indicates whether the levels view is currently collapsed. - @State private var isLevelsViewCollapsed: Bool = false + @State private var isLevelsViewCollapsed = false /// A Boolean value that indicates whether the site and facility selector is presented. - @State private var isSitesAndFacilitiesHidden: Bool = false + @State private var isSitesAndFacilitiesHidden = false /// The alignment configuration. private let alignment: Alignment From c55a5f3adfaa19a2a5a4a26026af0446d1a3c638 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:11:34 -0700 Subject: [PATCH 1029/1722] PR feedback --- .../Components/FloorFilter/FloorFilter.swift | 16 +++----------- .../FloorFilter/SiteAndFacilitySelector.swift | 10 +++++---- .../Extensions/EnvironmentValues.swift | 22 ------------------- .../ArcGISToolkit/Utility/IsCompactKey.swift | 19 ---------------- 4 files changed, 9 insertions(+), 58 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift delete mode 100644 Sources/ArcGISToolkit/Utility/IsCompactKey.swift diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 8c1e13421..b6f82efe1 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -21,15 +21,6 @@ public struct FloorFilter: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass: UserInterfaceSizeClass? - @Environment(\.verticalSizeClass) - private var verticalSizeClass: UserInterfaceSizeClass? - - /// If `true`, the site and facility selector will appear as a sheet. - /// If `false`, the site and facility selector will appear as a popup modal alongside the level selector. - private var isCompact: Bool { - horizontalSizeClass == .compact || verticalSizeClass == .compact - } - /// Creates a `FloorFilter`. /// - Parameters: /// - floorManager: The floor manager used by the `FloorFilter`. @@ -82,7 +73,7 @@ public struct FloorFilter: View { .padding(.toolkitDefault) } .sheet( - isAllowed: isCompact, + isAllowed: horizontalSizeClass == .compact, isPresented: $isSitesAndFacilitiesHidden ) { SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) @@ -112,7 +103,7 @@ public struct FloorFilter: View { .frame(width: filterWidth) .esriBorder() .frame( - maxWidth: isCompact ? .infinity : nil, + maxWidth: horizontalSizeClass == .compact ? .infinity : nil, maxHeight: .infinity, alignment: alignment ) @@ -148,7 +139,7 @@ public struct FloorFilter: View { /// `NavigationView` causes a rendering bug. This bug remains in iOS 16 with /// `NavigationStack` and has been reported to Apple as FB10034457. @ViewBuilder private var siteAndFacilitySelector: some View { - if !isCompact { + if !(horizontalSizeClass == .compact) { ZStack { RoundedRectangle(cornerRadius: 8) .fill(Color(uiColor: .systemBackground)) @@ -175,7 +166,6 @@ public struct FloorFilter: View { } // Ensure space for filter text field on small screens in landscape .frame(minHeight: 100) - .environment(\.isCompact, isCompact) .environmentObject(viewModel) .disabled(viewModel.isLoading) } diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index 5bcf77729..79afc3b08 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -34,7 +34,8 @@ struct SiteAndFacilitySelector: View { /// A view displaying the sites contained in a `FloorManager`. struct SitesList: View { - @Environment(\.isCompact) var isCompact: Bool + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel @@ -84,7 +85,7 @@ struct SiteAndFacilitySelector: View { ) } .buttonStyle(.bordered) - .padding([.bottom], isCompact ? 5 : 0) + .padding([.bottom], horizontalSizeClass == .compact ? 5 : 0) } } .searchable( @@ -143,7 +144,8 @@ struct SiteAndFacilitySelector: View { /// A view displaying the facilities contained in a `FloorManager`. struct FacilitiesList: View { - @Environment(\.isCompact) var isCompact: Bool + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? /// The view model used by this selector. @EnvironmentObject var viewModel: FloorFilterViewModel @@ -210,7 +212,7 @@ struct SiteAndFacilitySelector: View { List(matchingFacilities, id: \.id) { facility in Button { viewModel.setFacility(facility, zoomTo: true) - if isCompact { + if horizontalSizeClass == .compact { isHidden.toggle() } } label: { diff --git a/Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift b/Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift deleted file mode 100644 index f6bdd9616..000000000 --- a/Sources/ArcGISToolkit/Extensions/EnvironmentValues.swift +++ /dev/null @@ -1,22 +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 - -extension EnvironmentValues { - /// Indicates whether the environment is compact. - var isCompact: Bool { - get { self[IsCompactKey.self] } - set { self[IsCompactKey.self] = newValue } - } -} diff --git a/Sources/ArcGISToolkit/Utility/IsCompactKey.swift b/Sources/ArcGISToolkit/Utility/IsCompactKey.swift deleted file mode 100644 index 14f69bb1a..000000000 --- a/Sources/ArcGISToolkit/Utility/IsCompactKey.swift +++ /dev/null @@ -1,19 +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 `EnvironmentKey` that specifies compact environment state. -struct IsCompactKey: EnvironmentKey { - static let defaultValue = false -} From c05022b0dc66b3eb4d9127349c8841da6b655118 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:12:19 -0700 Subject: [PATCH 1030/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift Co-authored-by: Mark Dostal --- 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 b6f82efe1..48f1fc981 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -109,7 +109,7 @@ public struct FloorFilter: View { ) } - /// The map is currently being navigated. + /// A Boolean value indicating whether the map is currently being navigated. private var isNavigating: Binding /// Indicates that the selector should be presented with a top oriented aligment configuration. From 6da7c4b30d31fbce6317b37d740fdab77b58961a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:13:03 -0700 Subject: [PATCH 1031/1722] Update Examples/Examples/FloorFilterExampleView.swift Co-authored-by: Mark Dostal --- 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 6a1d89951..9a97b5ecf 100644 --- a/Examples/Examples/FloorFilterExampleView.swift +++ b/Examples/Examples/FloorFilterExampleView.swift @@ -30,7 +30,7 @@ struct FloorFilterExampleView: View { /// Determines the appropriate time to initialize the `FloorFilter`. @State private var isMapLoaded = false - /// A Boolean value indicating if the map is currently being navigated. + /// A Boolean value indicating whether the map is currently being navigated. @State private var isNavigating = false @State private var mapLoadError = false From 2aaf7e9a280f8e2d685cdb828ee2e57b59f51313 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:13:31 -0700 Subject: [PATCH 1032/1722] Update Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift Co-authored-by: Mark Dostal --- 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 48f1fc981..74fa8d274 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -27,7 +27,7 @@ public struct FloorFilter: View { /// - alignment: Determines the display configuration of Floor Filter elements. /// - automaticSelectionMode: The selection behavior of the floor filter. /// - viewpoint: Viewpoint updated when the selected site or facility changes. - /// - isNavigating: The map is currently being navigated. + /// - isNavigating: A Boolean value indicating whether the map is currently being navigated. public init( floorManager: FloorManager, alignment: Alignment, From e861a77e0d600329cc482060b537765300d1d8ac Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:33:48 -0700 Subject: [PATCH 1033/1722] Reorganize --- .../Components/FloorFilter/FloorFilter.swift | 19 +++++---- Sources/ArcGISToolkit/Extensions/View.swift | 39 +------------------ 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index b6f82efe1..825c1f2c5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -72,15 +72,6 @@ public struct FloorFilter: View { Image(systemName: "building.2") .padding(.toolkitDefault) } - .sheet( - isAllowed: horizontalSizeClass == .compact, - isPresented: $isSitesAndFacilitiesHidden - ) { - SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) - .onChange(of: viewpoint.wrappedValue) { viewpoint in - reportChange(of: viewpoint) - } - } } /// A view that allows selecting between levels. @@ -139,7 +130,15 @@ public struct FloorFilter: View { /// `NavigationView` causes a rendering bug. This bug remains in iOS 16 with /// `NavigationStack` and has been reported to Apple as FB10034457. @ViewBuilder private var siteAndFacilitySelector: some View { - if !(horizontalSizeClass == .compact) { + if horizontalSizeClass == .compact { + Color.clear + .sheet(isPresented: $isSitesAndFacilitiesHidden) { + SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) + .onChange(of: viewpoint.wrappedValue) { viewpoint in + reportChange(of: viewpoint) + } + } + } else { ZStack { RoundedRectangle(cornerRadius: 8) .fill(Color(uiColor: .systemBackground)) diff --git a/Sources/ArcGISToolkit/Extensions/View.swift b/Sources/ArcGISToolkit/Extensions/View.swift index 41495a82e..8444a867a 100644 --- a/Sources/ArcGISToolkit/Extensions/View.swift +++ b/Sources/ArcGISToolkit/Extensions/View.swift @@ -13,32 +13,11 @@ import SwiftUI -/// A modifier that provides conditional control over when a sheet is used. -struct ConditionalSheetModifier: ViewModifier { - /// A Boolean value that indicates whether `sheetContent` will be presented or not. - let isAllowed: Bool - - /// Determines when the sheet is presented or not. - var isPresented: Binding - - /// Content to be shown in the sheet. - let sheetContent: () -> SheetContent - - func body(content: Content) -> some View { - if isAllowed { - content - .sheet(isPresented: isPresented, content: sheetContent) - } else { - content - } - } -} - /// A modifier which displays a background and shadow for a view. Used to represent a selected view. struct SelectedModifier: ViewModifier { /// A Boolean value that indicates whether view should display as selected. var isSelected: Bool - + func body(content: Content) -> some View { if isSelected { content @@ -79,20 +58,4 @@ extension View { ) -> some View { modifier(SelectedModifier(isSelected: isSelected)) } - - /// - Parameter isAllowed: A Boolean that indicates if this sheet can be shown. - /// - Returns: Produces a sheet that is only shown if `isAllowed` is set `true`. - func sheet( - isAllowed: Bool, - isPresented: Binding, - content: @escaping () -> SheetContent - ) -> some View { - modifier( - ConditionalSheetModifier( - isAllowed: isAllowed, - isPresented: isPresented, - sheetContent: content - ) - ) - } } From 87f76894bb992ce6bb8e11caee5216a3d91bb62f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:38:43 -0700 Subject: [PATCH 1034/1722] PR feedback --- .../Components/FloorFilter/FloorFilterViewModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index baa5303d9..12bac0e90 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -278,7 +278,8 @@ final class FloorFilterViewModel: ObservableObject { /// Loads the given `FloorManager` if needed, then sets `isLoading` to `false`. private func loadFloorManager() { - guard floorManager.loadStatus == .notLoaded else { + guard floorManager.loadStatus == .notLoaded, + floorManager.loadStatus != .loading else { isLoading = false return } From b557fa8d6e7bac70f541fbe965e550bcac26fb31 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:39:02 -0700 Subject: [PATCH 1035/1722] Indentation --- .../FloorFilter/FloorFilterViewModel.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift index 12bac0e90..8b446748e 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilterViewModel.swift @@ -131,8 +131,8 @@ final class FloorFilterViewModel: ObservableObject { func onViewpointChanged(_ viewpoint: Viewpoint?) { guard let viewpoint = viewpoint, !viewpoint.targetScale.isZero else { - return - } + return + } automaticallySelectFacilityOrSite() } @@ -142,9 +142,9 @@ final class FloorFilterViewModel: ObservableObject { /// - zoomTo: If `true`, changes the viewpoint to the extent of the new facility. func setFacility(_ newFacility: FloorFacility, zoomTo: Bool = false) { if let oldLevel = selectedLevel, - let newLevel = newFacility.levels.first( + let newLevel = newFacility.levels.first( where: { $0.verticalOrder == oldLevel.verticalOrder } - ) { + ) { setLevel(newLevel) } else if let defaultLevel = newFacility.defaultLevel { setLevel(defaultLevel) @@ -202,7 +202,7 @@ final class FloorFilterViewModel: ObservableObject { // Only select a facility if it is within minimum scale. Default at 1500. let facilityMinScale: Double if let minScale = floorManager.facilityLayer?.minScale, - minScale != .zero { + minScale != .zero { facilityMinScale = minScale } else { facilityMinScale = 1500 @@ -236,7 +236,7 @@ final class FloorFilterViewModel: ObservableObject { // Only select a facility if it is within minimum scale. Default at 4300. let siteMinScale: Double if let minScale = floorManager.siteLayer?.minScale, - minScale != .zero { + minScale != .zero { siteMinScale = minScale } else { siteMinScale = 4300 @@ -287,7 +287,7 @@ final class FloorFilterViewModel: ObservableObject { do { try await floorManager.load() if sites.count == 1, - let firstSite = sites.first { + let firstSite = sites.first { // If we have only one site, select it. setSite(firstSite, zoomTo: true) } From 25c3ee92feb5a1c1af1201b59335378c12581ff7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 15:45:03 -0700 Subject: [PATCH 1036/1722] Update SiteAndFacilitySelector.swift --- .../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 79afc3b08..e9018b675 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -218,14 +218,13 @@ struct SiteAndFacilitySelector: View { } label: { VStack { Text(facility.name) - .fontWeight(.regular) .frame( maxWidth: .infinity, alignment: .leading ) if allSiteStyle, let siteName = facility.site?.name { Text(siteName) - .fontWeight(.regular) + .font(.caption) .frame( maxWidth: .infinity, alignment: .leading From 961d7e9f78b59f9e3dde1782b1ca428202fb1c11 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 22 Jun 2022 21:00:28 -0700 Subject: [PATCH 1037/1722] Update SiteAndFacilitySelector.swift --- .../FloorFilter/SiteAndFacilitySelector.swift | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift index e9018b675..936d6f8fa 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -210,30 +210,29 @@ struct SiteAndFacilitySelector: View { var facilityListView: some View { ScrollViewReader { proxy in List(matchingFacilities, id: \.id) { facility in - Button { - viewModel.setFacility(facility, zoomTo: true) - if horizontalSizeClass == .compact { - isHidden.toggle() - } - } label: { - VStack { - Text(facility.name) + VStack { + Text(facility.name) + .frame( + maxWidth: .infinity, + alignment: .leading + ) + if allSiteStyle, let siteName = facility.site?.name { + Text(siteName) + .font(.caption) .frame( maxWidth: .infinity, alignment: .leading ) - if allSiteStyle, let siteName = facility.site?.name { - Text(siteName) - .font(.caption) - .frame( - maxWidth: .infinity, - alignment: .leading - ) - } } } - .selected(facility.id == viewModel.selectedFacility?.id) - .listRowSeparator(.hidden) + .contentShape(Rectangle()) + .listRowBackground(facility.id == viewModel.selectedFacility?.id ? Color.secondary.opacity(0.5) : Color.clear) + .onTapGesture { + viewModel.setFacility(facility, zoomTo: true) + if horizontalSizeClass == .compact { + isHidden.toggle() + } + } } .listStyle(.plain) .onChange(of: viewModel.selection) { _ in From 7f2031a67a6756eb5ae70f3f46789e563211e784 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Jun 2022 11:50:03 -0700 Subject: [PATCH 1038/1722] Match use of `isHidden` in `FacilitiesList` to `SitesList` --- .../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 936d6f8fa..f64172945 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/SiteAndFacilitySelector.swift @@ -160,7 +160,7 @@ struct SiteAndFacilitySelector: View { let facilities: [FloorFacility] /// Allows the user to toggle the visibility of the site and facility selector. - @Binding var isHidden: Bool + var isHidden: Binding /// A subset of `facilities` with names containing `searchPhrase` or all /// `facilities` if `searchPhrase` is empty. @@ -194,7 +194,7 @@ struct SiteAndFacilitySelector: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - CloseButton { isHidden.toggle() } + CloseButton { isHidden.wrappedValue.toggle() } } } } @@ -230,7 +230,7 @@ struct SiteAndFacilitySelector: View { .onTapGesture { viewModel.setFacility(facility, zoomTo: true) if horizontalSizeClass == .compact { - isHidden.toggle() + isHidden.wrappedValue.toggle() } } } From 1cb521f96e16fdf465019f956085e3bbeecd8150 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Jun 2022 13:09:17 -0700 Subject: [PATCH 1039/1722] Adjust padding --- .../ArcGISToolkit/Components/FloorFilter/FloorFilter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index d3a3358b1..8796bf2e4 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -140,14 +140,14 @@ public struct FloorFilter: View { } } else { ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(Color(uiColor: .systemBackground)) + Color.clear .esriBorder() SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) .onChange(of: viewpoint.wrappedValue) { viewpoint in reportChange(of: viewpoint) } - .padding() + .padding([.top, .leading, .trailing], 2.5) + .padding(.bottom) } .opacity(isSitesAndFacilitiesHidden ? .zero : 1) } From ebfaaa1b89e087000fd6c2ce26a79479b831e180 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Jun 2022 14:59:35 -0700 Subject: [PATCH 1040/1722] Update FloorFilter.swift --- 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 8796bf2e4..f2c7b1331 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -132,7 +132,7 @@ public struct FloorFilter: View { @ViewBuilder private var siteAndFacilitySelector: some View { if horizontalSizeClass == .compact { Color.clear - .sheet(isPresented: $isSitesAndFacilitiesHidden) { + .sheet(isPresented: .constant(!$isSitesAndFacilitiesHidden.wrappedValue)) { SiteAndFacilitySelector(isHidden: $isSitesAndFacilitiesHidden) .onChange(of: viewpoint.wrappedValue) { viewpoint in reportChange(of: viewpoint) From 11e82e58b7c6d21a72cf21410aba8b808fe790db Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Jun 2022 16:04:04 -0700 Subject: [PATCH 1041/1722] Switch to `FloatingPanel` --- .../UtilityNetworkTraceExampleView.swift | 27 +++--- .../UtilityNetworkTrace.swift | 91 ++++++++----------- 2 files changed, 50 insertions(+), 68 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index d1a4dcebd..b5388f4f2 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -45,20 +45,19 @@ struct UtilityNetworkTraceExampleView: View { self.mapViewProxy = mapViewProxy } .edgesIgnoringSafeArea(.bottom) - .overlay { - UtilityNetworkTrace( - $resultGraphicsOverlay, - map, - $pointInMap, - $pointInScreen, - $mapViewProxy, - $viewpoint - ) - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .bottomLeading - ) + .overlay(alignment: .topTrailing) { + FloatingPanel { + UtilityNetworkTrace( + $resultGraphicsOverlay, + map, + $pointInMap, + $pointInScreen, + $mapViewProxy, + $viewpoint + ) + } + .padding() + .frame(width: 360) } } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 670332fa7..0641d11b2 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -131,50 +131,6 @@ public struct UtilityNetworkTrace: View { } } - /// The main contents of Utility Network Trace Tool. - @ViewBuilder private var innerBody: some View { - VStack { - if !viewModel.completedTraces.isEmpty && !isAddingStartingPoints { - activityPicker - } - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .inspectingStartingPoint: - startingPointDetail - default: - newTraceTab - } - case .viewingTraces: - resultsTab - } - } - .animation(.default, value: currentActivity) - .onChange(of: pointInScreen) { newValue in - guard isAddingStartingPoints, - let mapViewProxy = mapViewProxy, - let pointInMap = pointInMap, - let pointInScreen = pointInScreen else { - return - } - currentActivity = .creatingTrace(.viewingStartingPoints) - Task { - await viewModel.setStartingPoint( - at: pointInScreen, - mapPoint: pointInMap, - with: mapViewProxy - ) - } - } - .onChange(of: isAddingStartingPoints) { newValue in - if newValue { - sheetHeight = .min - } else { - sheetHeight = .mid - } - } - } - /// The tab that allows for a new trace to be configured. @ViewBuilder private var newTraceTab: some View { List { @@ -406,18 +362,45 @@ public struct UtilityNetworkTrace: View { } public var body: some View { - if isCompact { - PartialSheet(preset: $sheetHeight) { - innerBody + VStack { + if !viewModel.completedTraces.isEmpty && !isAddingStartingPoints { + activityPicker } - } else { - innerBody - .esriBorder() - .frame( - width: 400, - height: 450 + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .inspectingStartingPoint: + startingPointDetail + default: + newTraceTab + } + case .viewingTraces: + resultsTab + } + } + .animation(.default, value: currentActivity) + .onChange(of: pointInScreen) { newValue in + guard isAddingStartingPoints, + let mapViewProxy = mapViewProxy, + let pointInMap = pointInMap, + let pointInScreen = pointInScreen else { + return + } + currentActivity = .creatingTrace(.viewingStartingPoints) + Task { + await viewModel.setStartingPoint( + at: pointInScreen, + mapPoint: pointInMap, + with: mapViewProxy ) - .padding(30) + } + } + .onChange(of: isAddingStartingPoints) { newValue in + if newValue { + sheetHeight = .min + } else { + sheetHeight = .mid + } } } From 333bfe16a58dbbf654b161efbd5e4b8b828bda65 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Jun 2022 16:16:19 -0700 Subject: [PATCH 1042/1722] Remove `PartialSheet` --- Examples/Examples.xcodeproj/project.pbxproj | 4 - .../Examples/PartialSheetExampleView.swift | 46 ------ Examples/ExamplesApp/Examples.swift | 3 +- .../FloatingPanel/FloatingPanel.swift | 17 +++ .../Components/Handle/Handle.swift | 32 ---- .../PartialSheet/PartialSheet.swift | 137 ------------------ .../UtilityNetworkTrace.swift | 11 -- 7 files changed, 18 insertions(+), 232 deletions(-) delete mode 100644 Examples/Examples/PartialSheetExampleView.swift delete mode 100644 Sources/ArcGISToolkit/Components/Handle/Handle.swift delete mode 100644 Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index a9b07e4c5..795b19b25 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 75230DAC2861392B009AF501 /* PartialSheetExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */; }; 75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */; }; 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75657E4727ABAC8400EE865B /* CompassExampleView.swift */; }; 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */; }; @@ -42,7 +41,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSheetExampleView.swift; sourceTree = ""; }; 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityNetworkTraceExampleView.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 = ""; }; @@ -89,7 +87,6 @@ E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, 75657E4727ABAC8400EE865B /* CompassExampleView.swift */, E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */, - 75230DAB2861392B009AF501 /* PartialSheetExampleView.swift */, 75230DAD28614369009AF501 /* UtilityNetworkTraceExampleView.swift */, ); name = Examples; @@ -232,7 +229,6 @@ 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */, 75230DAE28614369009AF501 /* UtilityNetworkTraceExampleView.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, - 75230DAC2861392B009AF501 /* PartialSheetExampleView.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, E4AA9316276BF5ED000E6289 /* FloatingPanelExampleView.swift in Sources */, diff --git a/Examples/Examples/PartialSheetExampleView.swift b/Examples/Examples/PartialSheetExampleView.swift deleted file mode 100644 index 268f35a61..000000000 --- a/Examples/Examples/PartialSheetExampleView.swift +++ /dev/null @@ -1,46 +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 -import ArcGISToolkit -import SwiftUI - -struct PartialSheetExampleView: View { - @StateObject private var map = Map(basemapStyle: .arcGISImagery) - - @State var preset: PartialSheetPreset? = .mid - - private let initialViewpoint = Viewpoint( - center: Point(x: -93.258133, y: 44.986656, spatialReference: .wgs84), - scale: 1_000_000 - ) - - var body: some View { - MapView( - map: map, - viewpoint: initialViewpoint - ) - .overlay { - PartialSheet(preset: $preset) { - SampleContent() - } - } - } -} - -private struct SampleContent: View { - var body: some View { - List(1..<21) { Text("\($0)") } - .listStyle(.plain) - } -} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index 2436823d3..006789356 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -49,8 +49,7 @@ extension ExampleList { static let views = Self( name: "Views", examples: [ - AnyExample("Floating Panel", content: FloatingPanelExampleView()), - AnyExample("Partial Sheet", content: PartialSheetExampleView()) + AnyExample("Floating Panel", content: FloatingPanelExampleView()) ] ) } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 066621083..71cc2636c 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -72,6 +72,23 @@ public struct FloatingPanel: View where Content: View { } } +/// The "Handle" view of the floating panel. +private struct Handle: View { + /// The color of the handle. + var color: Color + + var body: some View { + RoundedRectangle(cornerRadius: 4.0) + .foregroundColor(color) + .frame(width: 100, height: 8.0) + } +} + private extension CGFloat { static let minHeight: CGFloat = 66 } + +private extension Color { + static var defaultHandleColor: Color { .secondary } + static var activeHandleColor: Color { .primary } +} diff --git a/Sources/ArcGISToolkit/Components/Handle/Handle.swift b/Sources/ArcGISToolkit/Components/Handle/Handle.swift deleted file mode 100644 index f9c490f64..000000000 --- a/Sources/ArcGISToolkit/Components/Handle/Handle.swift +++ /dev/null @@ -1,32 +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 "Handle" view for resizeable containers. -struct Handle: View { - /// The color of the handle. - var color: Color - - var body: some View { - RoundedRectangle(cornerRadius: 4.0) - .foregroundColor(color) - .frame(width: 100, height: 8.0) - } -} - -extension Color { - static var defaultHandleColor: Color { .secondary } - static var activeHandleColor: Color { .primary } -} - diff --git a/Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift b/Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift deleted file mode 100644 index edadc52e4..000000000 --- a/Sources/ArcGISToolkit/Components/PartialSheet/PartialSheet.swift +++ /dev/null @@ -1,137 +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 - -public enum PartialSheetPreset { - case min, mid, max -} - -/// A partial sheet is a view that overlays a view and supplies view-related -/// content. -/// -/// A partial sheet offers an advantage over a native sheet in that it allows user interaction with any view -/// behind it. -public struct PartialSheet: View { - /// Records the drag gesture for new height calculations. - @State private var dragTranslation: CGFloat = .zero - - /// The color of the handle. - @State private var handleColor: Color = .defaultHandleColor - - /// Allows the user to drag the sheet to a specific height. - @State private var yOffset: CGFloat = 0 - - /// Allows the parent to control the height of the sheet. - @Binding var preset: PartialSheetPreset? - - /// The content shown in the floating panel. - private let content: Content - - /// Creates a `FloatingPanel` - /// - Parameter content: The view shown in the floating panel. - public init(preset: Binding, @ViewBuilder content: () -> Content) { - self.content = content() - _preset = preset - } - - /// Determines the current of the sheet, either from the preset value or `yOffset`. - private func heightFromPreset(with geometryProxy: GeometryProxy) -> CGFloat { - switch preset { - case .min: - return 200.0 - case .mid: - return geometryProxy.size.height / 2.0 - case .max: - return geometryProxy.size.height - 50.0 - default: - return yOffset - } - } - - public var body: some View { - GeometryReader { geometryProxy in - return VStack { - Handle(color: handleColor) - .padding() - .gesture(drag) - content - } - .frame(maxWidth: .infinity) - .frame(height: heightFromPreset(with: geometryProxy)) - .background(Color(uiColor: .systemGroupedBackground)) - .cornerRadius( - 15, - corners: [.topLeft, .topRight] - ) - .shadow(radius: 10) - .frame( - maxHeight: .infinity, - alignment: .bottom - ) - .edgesIgnoringSafeArea(.bottom) - .animation(.default, value: preset) - .onChange(of: dragTranslation) { newValue in - preset = nil - yOffset = max( - 200, - min( - geometryProxy.size.height - 50, - yOffset - newValue - ) - ) - } - } - } - - private var drag: some Gesture { - DragGesture(minimumDistance: 0) - .onChanged { value in - handleColor = .activeHandleColor - dragTranslation = value.translation.height - } - .onEnded { _ in - handleColor = .defaultHandleColor - } - } -} - -private struct RoundedCorner: Shape { - var corners: UIRectCorner - - var radius: CGFloat - - func path(in rect: CGRect) -> Path { - let path = UIBezierPath( - roundedRect: rect, - byRoundingCorners: corners, - cornerRadii: CGSize( - width: radius, - height: radius - ) - ) - return Path(path.cgPath) - } -} - -private extension View { - func cornerRadius( - _ radius: CGFloat, - corners: UIRectCorner - ) -> some View { - clipShape(RoundedCorner( - corners: corners, - radius: radius - )) - } -} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 0641d11b2..b72d196e5 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -54,9 +54,6 @@ public struct UtilityNetworkTrace: View { /// The current user activity. @State private var currentActivity: UserActivity = .creatingTrace(nil) - /// The current height of the partial sheet displaying the Utility Network Trace Tool. - @State private var sheetHeight: PartialSheetPreset? = .mid - /// Indicates if the warning that all traces will be deleted is presented. @State private var warningIsPresented = false @@ -289,7 +286,6 @@ public struct UtilityNetworkTrace: View { } } Button { - sheetHeight = .min if let selectedStartingPoint { viewpoint = Viewpoint(targetExtent: selectedStartingPoint.extent) } @@ -395,13 +391,6 @@ public struct UtilityNetworkTrace: View { ) } } - .onChange(of: isAddingStartingPoints) { newValue in - if newValue { - sheetHeight = .min - } else { - sheetHeight = .mid - } - } } // MARK: Computed Properties From d087ddabec9154cf53304a82d532e2eb0b6e552f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 23 Jun 2022 16:17:42 -0700 Subject: [PATCH 1043/1722] Update FloatingPanelExampleView.swift --- Examples/Examples/FloatingPanelExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 236a29448..59f1ec186 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -38,7 +38,7 @@ struct FloatingPanelExampleView: View { } } -private struct SampleContent: View { +struct SampleContent: View { var body: some View { List(1..<21) { Text("\($0)") } .listStyle(.plain) From f9e90359878ecdcce0aba89d546b021d4cceeb88 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Jun 2022 12:18:27 -0700 Subject: [PATCH 1044/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 46ca48a7f..af0106878 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -274,7 +274,6 @@ import SwiftUI /// Loads the provided map. /// - Parameter map: The map to be loaded. private func loadMap(_ map: Map) async { - guard map.loadStatus != .loaded else { return } try? await map.load() } From 6803fa7f0e41bfdaa15acff202e8d3dd98e30877 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Jun 2022 14:16:27 -0700 Subject: [PATCH 1045/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTraceViewModel.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index af0106878..a9317afa3 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -77,7 +77,7 @@ import SwiftUI init(map: Map, graphicsOverlay: GraphicsOverlay) { self.graphicsOverlay = graphicsOverlay Task { - await loadMap(map) + try? await map.load() network = map.utilityNetworks.first await loadNamedTraceConfigurations(map) } @@ -271,12 +271,6 @@ import SwiftUI }) } - /// Loads the provided map. - /// - Parameter map: The map to be loaded. - private func loadMap(_ map: Map) async { - try? await map.load() - } - /// Loads the named trace configurations in the network on the provided map. private func loadNamedTraceConfigurations(_ map: Map) async { guard let network = network else { return } From ba2036c13cd74786aa660e6c9422bc37b818d62d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 24 Jun 2022 15:56:32 -0700 Subject: [PATCH 1046/1722] Update FloatingPanel.swift --- .../Examples/FloatingPanelExampleView.swift | 12 +-- .../FloatingPanel/FloatingPanel.swift | 94 +++++++++++++++---- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 59f1ec186..1a96bdf9e 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -28,13 +28,13 @@ struct FloatingPanelExampleView: View { map: map, viewpoint: initialViewpoint ) - .overlay(alignment: .topTrailing) { - FloatingPanel { - SampleContent() - } - .padding() - .frame(width: 360) + .overlay { + FloatingPanel(alignment: .topLeading) { + SampleContent() } + .padding([.leading, .top, .trailing]) + .padding(.bottom, 50) + } } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 71cc2636c..bcf79e93c 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -32,39 +32,95 @@ public struct FloatingPanel: View where Content: View { let content: Content /// Creates a `FloatingPanel` + /// - Parameter alignment: Alignment of the floating panel within the parent view. + /// - Parameter initialHeight: The initial height given to the content of the floating panel. + /// Default is 200. + /// - Parameter width: The width given to the content of the floating panel. Default is 360. /// - Parameter content: The view shown in the floating panel. - public init(@ViewBuilder content: () -> Content) { + public init( + alignment: Alignment, + initialHeight: CGFloat = 200, + width: CGFloat = 360, + @ViewBuilder content: () -> Content + ) { + self.alignment = alignment + self.width = width self.content = content() + _height = State(initialValue: initialHeight) } - @State - private var handleColor: Color = .defaultHandleColor + /// Alignment of the floating panel within the parent view. + private let alignment: Alignment - @State - private var height: CGFloat? + /// The width given to the content of the floating panel. + private let width: CGFloat + + /// The color of the handle. + @State private var handleColor: Color = .defaultHandleColor + + /// The height of the content. + @State private var height: CGFloat + + /// The maximum allowed height of the content. + @State private var maximumHeight: CGFloat = .infinity + + /// The vertical alignment of the floating panel with the parent view is `VerticalAlignment.top` + private var isTopAligned: Bool { + return alignment.vertical == .top + } public var body: some View { - VStack { - content - .frame(minHeight: .minHeight, maxHeight: height) - Divider() - Handle(color: handleColor) - .gesture(drag) + GeometryReader { geometryProxy in + VStack { + if isTopAligned { + content + .frame(minHeight: .minHeight, maxHeight: height) + Divider() + Handle(color: handleColor) + .gesture(drag) + } else { + Handle(color: handleColor) + .gesture(drag) + Divider() + content + .frame(minHeight: .minHeight, maxHeight: height) + } + } + .frame(width: width) + .esriBorder() + .frame( + width: geometryProxy.size.width, + height: geometryProxy.size.height, + alignment: alignment + ) + .onSizeChange { + maximumHeight = $0.height + if height > maximumHeight { + height = maximumHeight + } + } } - .esriBorder() } var drag: some Gesture { DragGesture(minimumDistance: 0) .onChanged { value in handleColor = .activeHandleColor - // Note: There is a bug here where `height` can be set - // larger than the displayed height. This occurs by continuing - // to drag down on the handle after the panel reaches it's max - // height. When that happens subsequent "drag up" operations - // don't cause the panel to shrink immediately, but will - // ultimately snap to the correct height. - height = max(.minHeight, (height ?? 0) + value.translation.height) + let proposedHeight: CGFloat + if isTopAligned { + proposedHeight = max( + .minHeight, + height + value.translation.height + ) + } else { + proposedHeight = max( + .minHeight, + height - value.translation.height + ) + } + if proposedHeight <= maximumHeight { + height = proposedHeight + } } .onEnded { _ in handleColor = .defaultHandleColor From 2faa4b0a2f027e14af21ab7f3aaa8cdd759dfdbf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Jun 2022 16:09:51 -0700 Subject: [PATCH 1047/1722] Update Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift Co-authored-by: Mark Dostal --- .../Components/FloatingPanel/FloatingPanel.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index bcf79e93c..eb0096e24 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -118,9 +118,7 @@ public struct FloatingPanel: View where Content: View { height - value.translation.height ) } - if proposedHeight <= maximumHeight { - height = proposedHeight - } + height = min(proposedHeight, maximumHeight) } .onEnded { _ in handleColor = .defaultHandleColor From 29fbdefe17749c375b0cf2ecf3a994287481eabc Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 27 Jun 2022 14:11:26 -0700 Subject: [PATCH 1048/1722] Small updates Add documentation to `UtilityNetworkTraceExampleView` Remove extra padding --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 5 +++++ .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index b5388f4f2..451b89dfe 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -15,6 +15,11 @@ 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. +/// +/// - Note: An ``ArcGISCredential`` is needed to interact with this sample. This can be done within +/// `init()` in `ExamplesApp.swift`. The public credentials can be found [here](https://developers.arcgis.com/javascript/latest/sample-code/widgets-untrace). struct UtilityNetworkTraceExampleView: View { @StateObject private var map = makeMap() diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index b72d196e5..3f8d97a89 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -187,7 +187,6 @@ public struct UtilityNetworkTrace: View { Text("Trace") } .buttonStyle(.bordered) - .padding() .disabled(!viewModel.canRunTrace) } } From 1e039a0cf49932d90e2e08517f12f60052d530e7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 28 Jun 2022 11:58:37 -0600 Subject: [PATCH 1049/1722] URL Challenge -> Network Challenge --- .../AuthenticationModifier.swift | 16 +- .../Authentication/Authenticator.swift | 178 ++---------------- .../CertificateCredential.swift | 81 -------- .../CertificatePickerViewModifier.swift | 8 +- .../Authentication/QueuedChallenge.swift | 29 ++- .../TrustHostViewModifier.swift | 6 +- .../UsernamePasswordViewModifier.swift | 14 +- 7 files changed, 59 insertions(+), 273 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift index 3b4ab3691..1f7f113b9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift @@ -56,20 +56,14 @@ private struct AuthenticationModifier: ViewModifier { switch challenge { case let challenge as QueuedArcGISChallenge: content.modifier(UsernamePasswordViewModifier(challenge: challenge)) - case let challenge as QueuedURLChallenge: - switch challenge.urlChallenge.protectionSpace.authenticationMethod { - case NSURLAuthenticationMethodServerTrust: + case let challenge as QueuedNetworkChallenge: + switch challenge.kind { + case .serverTrust: content.modifier(TrustHostViewModifier(challenge: challenge)) - case NSURLAuthenticationMethodClientCertificate: + case .certificate: content.modifier(CertificatePickerViewModifier(challenge: challenge)) - case NSURLAuthenticationMethodDefault, - NSURLAuthenticationMethodNTLM, - NSURLAuthenticationMethodHTMLForm, - NSURLAuthenticationMethodHTTPBasic, - NSURLAuthenticationMethodHTTPDigest: + case .login: content.modifier(UsernamePasswordViewModifier(challenge: challenge)) - default: - fatalError() } default: fatalError() diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 98c4613f2..9b5087519 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -18,10 +18,7 @@ import Combine @MainActor public final class Authenticator: ObservableObject { let oAuthConfigurations: [OAuthConfiguration] - var trustedHosts: [String] = [] - var urlCredentialPersistence: URLCredential.Persistence = .forSession var promptForUntrustedHosts: Bool - var certificateStore: CertificateCredentialStore? public init( promptForUntrustedHosts: Bool = false, @@ -29,7 +26,6 @@ public final class Authenticator: ObservableObject { ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations - self.certificateStore = nil Task { await observeChallengeQueue() } } @@ -48,26 +44,18 @@ public final class Authenticator: ObservableObject { access: access, isSynchronizable: isCloudSynchronizable ) - certificateStore = try await CertificateCredentialStore( - access: .whenUnlockedThisDeviceOnly, - groupIdentifier: nil, - isSynchronizable: isCloudSynchronizable + + NetworkCredentialStore.setShared( + try await .makePersistent(access: access, isSynchronizable: isCloudSynchronizable) ) - urlCredentialPersistence = isCloudSynchronizable ? .synchronizable : .permanent } public func clearCredentialStores() async { - // Clear trusted hosts - trustedHosts.removeAll() - // Clear ArcGIS Credentials. await ArcGISURLSession.credentialStore.removeAll() - // Clear certificate store. - await certificateStore?.clear() - - // Clear URLCredentials. - URLCredentialStorage.shared.removeAllCredentials() + // Clear network credentials. + await NetworkCredentialStore.shared.removeAll() // We have to reset the sessions for URLCredential storage to respect the removed credentials ArcGISURLSession.shared = ArcGISURLSession.makeDefaultSharedSession() @@ -136,162 +124,28 @@ extension Authenticator: AuthenticationChallengeHandler { } } - public func handleURLSessionChallenge( - _ challenge: URLAuthenticationChallenge, - scope: URLAuthenticationChallengeScope - ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - guard challenge.protectionSpace.authenticationMethod != NSURLAuthenticationMethodDefault else { - return (.performDefaultHandling, nil) - } - - guard challenge.proposedCredential == nil else { - return (.performDefaultHandling, nil) - } - - // Check for server trust challenge. - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let trust = challenge.protectionSpace.serverTrust { - if trustedHosts.contains(challenge.protectionSpace.host) { - // If the host is already trusted, then continue trusting it. - return (.useCredential, URLCredential(trust: trust)) - } else if !trust.isRecoverableTrustFailure { - // If not a recoverable trust failure then we perform default handling. - return (.performDefaultHandling, nil) - } else if !promptForUntrustedHosts { - // If we aren't allowed to prompt for untrusted hosts then perform default handling. - return (.performDefaultHandling, nil) - } - } - - // Look in certificate store, use that credential if available. - if let certificateCredential = await certificateStore?.credential(for: challenge.protectionSpace), - let urlCredential = try? URLCredential.withCertificateCredential(certificateCredential) { - return (.useCredential, urlCredential) - } - + public func handleNetworkChallenge( + _ challenge: NetworkAuthenticationChallenge + ) async -> NetworkAuthenticationChallengeDisposition { // Queue up the url challenge. - let queuedChallenge = QueuedURLChallenge(urlChallenge: challenge) + let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) subject.send(queuedChallenge) // Respond accordingly. switch await queuedChallenge.response { case .cancel: - return (.cancelAuthenticationChallenge, nil) + return .cancelAuthenticationChallenge case .trustHost: - if let trust = challenge.protectionSpace.serverTrust { - trustedHosts.append(challenge.protectionSpace.host) - return (.useCredential, URLCredential(trust: trust)) - } else { - return (.performDefaultHandling, nil) - } - case .userCredential(let user, let password): - return (.useCredential, URLCredential(user: user, password: password, persistence: urlCredentialPersistence)) + return .useCredential(.serverTrust) + case .login(let user, let password): + return .useCredential(.login(username: user, password: password)) case .certificate(let url, let password): do { - let data = try await Data(asyncWithContentsOf: url) - await certificateStore?.add(credential: .init(host: challenge.protectionSpace.host, data: data, password: password)) - return ( - .useCredential, - try URLCredential.urlCredential(forCertificateWithData: data, password: password) - ) + return .useCredential(try .certificate(at: url, password: password)) } catch { - return (.performDefaultHandling, nil) - } - } - } -} - -extension SecTrust { - var isRecoverableTrustFailure: Bool { - var result = SecTrustResultType.invalid - SecTrustGetTrustResult(self, &result) - return result == .recoverableTrustFailure - } -} - -extension URLCredentialStorage { - func removeAllCredentials() { - allCredentials.forEach { (protectionSpace: URLProtectionSpace, usernamesToCredentials: [String : URLCredential]) in - for credential in usernamesToCredentials.values { - remove(credential, for: protectionSpace) + // TODO: handle error + fatalError() } } } } - -private extension URLCredential { - /// An error that can occur when importing a certificate. - struct CertificateImportError: Error, Hashable { - /// The backing status code for this error. - let status: OSStatus - - /// Initializes a certificate import error. This init will fail if the specified status is a success - /// status value. - /// - Parameter status: An `OSStatus`, usually the return value of a keychain operation. - init?(status: OSStatus) { - guard status != errSecSuccess else { return nil } - self.status = status - } - } - - static func urlCredential( - forCertificateAt fileURL: URL, - password: String - ) async throws -> URLCredential { - return try urlCredential( - forCertificateWithData: try await Data(asyncWithContentsOf: fileURL), - password: password - ) - } - static func urlCredential( - forCertificateWithData data: Data, - password: String - ) throws -> URLCredential { - let options = [kSecImportExportPassphrase: password] - var rawItems: CFArray? - - let status = SecPKCS12Import( - data as CFData, - options as CFDictionary, - &rawItems - ) - - guard status == errSecSuccess else { - throw CertificateImportError(status: status)! - } - - let items = rawItems! as! [[CFString: Any]] - let identity = items[0][kSecImportItemIdentity] as! SecIdentity - let certificates = items[0][kSecImportItemCertChain] as! [SecTrust] - - return URLCredential( - identity: identity, - certificates: certificates, - // Persistence is not supported at all at this point by apple for certificate credentials. - // So we can just pass `.none`. - persistence: .none - ) - } -} - -extension Data { - init(asyncWithContentsOf fileURL: URL) async throws { - let task = Task { - try Data(contentsOf: fileURL) - } - self = try await task.value - } -} - -extension URLCredential.CertificateImportError { - // The message for this error. - var message: String { - (SecCopyErrorMessageString(status, nil) as String?) ?? "" - } -} - -extension URLCredential { - static func withCertificateCredential(_ certificateCredential: CertificateCredential) throws -> URLCredential { - try URLCredential.urlCredential(forCertificateWithData: certificateCredential.data, password: certificateCredential.password) - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift deleted file mode 100644 index f34d6f9c4..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificateCredential.swift +++ /dev/null @@ -1,81 +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 Foundation - -struct CertificateCredential: Hashable { - let host: String - let data: Data - let password: String -} - -extension CertificateCredential: Codable {} - -final actor CertificateCredentialStore { - private var storage: [String: CertificateCredential] = [:] - - let keychain: Keychain? - let access: KeychainAccess? - let groupIdentifier: String? - let isSynchronizable: Bool - - static let label: String = "ArcGIS Certificate Credential" - - init() { - keychain = nil - groupIdentifier = nil - access = nil - isSynchronizable = false - } - - init(access: KeychainAccess, groupIdentifier: String? = nil, isSynchronizable: Bool = false) async throws { - keychain = .shared - self.access = access - self.groupIdentifier = groupIdentifier - self.isSynchronizable = isSynchronizable - - for item in try await Keychain.shared.items(labeled: Self.label, inGroup: groupIdentifier) { - let credential = try JSONDecoder().decode(CertificateCredential.self, from: item.value) - storage[credential.host] = credential - } - } - - func credential(for protectionSpace: URLProtectionSpace) -> CertificateCredential? { - guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate else { - return nil - } - return storage[protectionSpace.host] - } - - func add(credential: CertificateCredential) async { - storage[credential.host] = credential - if let keychain = keychain, let access = access { - try? await keychain.storeItem( - identifier: credential.host, - value: try JSONEncoder().encode(credential), - access: access, - label: Self.label, - groupIdentifier: groupIdentifier, - isSynchronizable: isSynchronizable - ) - } - } - - func clear() async { - storage.removeAll() - if let keychain = keychain { - try? await keychain.removeItems(labeled: Self.label, inGroup: groupIdentifier) - } - } -} - diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 10bffce4e..8981080a5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -16,14 +16,14 @@ import UniformTypeIdentifiers final private class CertificatePickerViewModel: ObservableObject { let challengingHost: String - let challenge: QueuedURLChallenge + let challenge: QueuedNetworkChallenge @Published var certificateURL: URL? @Published var password: String = "" - init(challenge: QueuedURLChallenge) { + init(challenge: QueuedNetworkChallenge) { self.challenge = challenge - challengingHost = challenge.urlChallenge.protectionSpace.host + challengingHost = challenge.networkChallenge.host } func signIn() { @@ -40,7 +40,7 @@ final private class CertificatePickerViewModel: ObservableObject { } struct CertificatePickerViewModifier: ViewModifier { - init(challenge: QueuedURLChallenge) { + init(challenge: QueuedNetworkChallenge) { viewModel = CertificatePickerViewModel(challenge: challenge) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index bf81e233a..4a32843d4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -58,11 +58,11 @@ final class QueuedArcGISChallenge: QueuedChallenge { } } -final class QueuedURLChallenge: QueuedChallenge { - let urlChallenge: URLAuthenticationChallenge +final class QueuedNetworkChallenge: QueuedChallenge { + let networkChallenge: NetworkAuthenticationChallenge - init(urlChallenge: URLAuthenticationChallenge) { - self.urlChallenge = urlChallenge + init(networkChallenge: NetworkAuthenticationChallenge) { + self.networkChallenge = networkChallenge } func resume(with response: Response) { @@ -92,9 +92,28 @@ final class QueuedURLChallenge: QueuedChallenge { } enum Response { - case userCredential(username: String, password: String) + case login(username: String, password: String) case certificate(url: URL, passsword: String) case trustHost case cancel } + + enum Kind { + case serverTrust + case login + case certificate + } + + var kind: Kind { + switch networkChallenge.kind { + case .serverTrust: + return .serverTrust + case .ntlm, .basic, .digest: + return .login + case .pki: + return .certificate + case .htmlForm, .negotiate: + fatalError("TODO: ") + } + } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index 99a3f969a..999ee6623 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -14,14 +14,14 @@ import SwiftUI struct TrustHostViewModifier: ViewModifier { - init(challenge: QueuedURLChallenge) { + init(challenge: QueuedNetworkChallenge) { self.challenge = challenge } - let challenge: QueuedURLChallenge + let challenge: QueuedNetworkChallenge var host: String { - challenge.urlChallenge.protectionSpace.host + challenge.networkChallenge.host } // Even though we will present it right away we need to use a state variable for this. diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index e1abf8e66..1ce79bbf8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -28,8 +28,8 @@ protocol UsernamePasswordViewModel: ObservableObject { struct UsernamePasswordViewModifier: ViewModifier { let viewModel: ViewModel - init(challenge: QueuedURLChallenge) where ViewModel == URLCredentialUsernamePasswordViewModel { - viewModel = URLCredentialUsernamePasswordViewModel(challenge: challenge) + init(challenge: QueuedNetworkChallenge) where ViewModel == NetworkCredentialUsernamePasswordViewModel { + viewModel = NetworkCredentialUsernamePasswordViewModel(challenge: challenge) } init(challenge: QueuedArcGISChallenge) where ViewModel == TokenCredentialViewModel { @@ -220,10 +220,10 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { } } -class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { - private let challenge: QueuedURLChallenge +class NetworkCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { + private let challenge: QueuedNetworkChallenge - init(challenge: QueuedURLChallenge) { + init(challenge: QueuedNetworkChallenge) { self.challenge = challenge } @@ -241,12 +241,12 @@ class URLCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { } var challengingHost: String { - challenge.urlChallenge.protectionSpace.host + challenge.networkChallenge.host } func signIn() { formEnabled = false - challenge.resume(with: .userCredential(username: username, password: password)) + challenge.resume(with: .login(username: username, password: password)) } func cancel() { From bc7d8fd7d0bd51c2db357050129d0e648302ee58 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 28 Jun 2022 11:40:41 -0700 Subject: [PATCH 1050/1722] Automatically set trace name --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 5 ++++- .../UtilityNetworkTraceViewModel.swift | 11 +++++++++++ .../UtilityNetworkTraceViewModelTrace.swift | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 3f8d97a89..51a325b5f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -112,7 +112,7 @@ public struct UtilityNetworkTrace: View { ForEach(viewModel.configurations, id: \.name) { configuration in Button { withAnimation { - viewModel.pendingTrace.configuration = configuration + viewModel.setPendingTrace(configuration: configuration) currentActivity = .creatingTrace(nil) } } label: { @@ -175,6 +175,9 @@ public struct UtilityNetworkTrace: View { "Trace Name", text: $viewModel.pendingTrace.name ) + .onSubmit { + viewModel.pendingTrace.userDidSpecifyName = true + } } } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index a9317afa3..112a23390 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -117,6 +117,17 @@ import SwiftUI } } + /// Updates the pending trace's configuration and name, if applicable. + /// + /// The pending trace's name will only be updated if the user hasn't specified one already. + /// - Parameter configuration: The selected configuration for the pending trace. + func setPendingTrace(configuration: UtilityNamedTraceConfiguration) { + pendingTrace.configuration = configuration + if !pendingTrace.userDidSpecifyName { + pendingTrace.name = "\(configuration.name) \((completedTraces.filter({ $0.configuration == configuration }).count + 1).description)" + } + } + /// Selects the previous trace from the list of completed traces. func selectPreviousTrace() { if let current = selectedTraceIndex { diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 502e7e023..1feba78b9 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -45,6 +45,9 @@ extension UtilityNetworkTraceViewModel { /// A collection of starting points for the trace. var startingPoints = [UtilityNetworkTraceStartingPoint]() + /// Indicates that the user has specified a name for the trace. + var userDidSpecifyName: Bool = false + /// A trace result set comprised of an collection of UtilityElement objects. var utilityElementTraceResult: UtilityElementTraceResult? From 97ce09f9dd6a2828bc9c824179fa1199f1996a01 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 28 Jun 2022 16:24:26 -0600 Subject: [PATCH 1051/1722] authentication -> authenticator --- Examples/Examples/AuthenticationExampleView.swift | 4 ++-- .../Components/Authentication/Authenticator.swift | 12 ++++++------ ...ionModifier.swift => AuthenticatorModifier.swift} | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{AuthenticationModifier.swift => AuthenticatorModifier.swift} (94%) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index e47fc4ace..56b835444 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -40,9 +40,9 @@ struct AuthenticationExampleView: View { } } } - .authentication(authenticator: authenticator) + .authenticator(authenticator: authenticator) .task { - //try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) + try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .onAppear { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 9b5087519..32154dfc0 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -74,12 +74,6 @@ public final class Authenticator: ObservableObject { // Creating the OAuth credential will present the OAuth login view. queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) } else { - - if let challenge = queuedChallenge as? QueuedArcGISChallenge, - let previous = await ArcGISURLSession.credentialStore.credential(for: challenge.arcGISChallenge.request.url!) { - print("-- previous: \(previous)") - } - // Set the current challenge, this should present the appropriate view. currentChallenge = queuedChallenge @@ -127,6 +121,12 @@ extension Authenticator: AuthenticationChallengeHandler { public func handleNetworkChallenge( _ challenge: NetworkAuthenticationChallenge ) async -> NetworkAuthenticationChallengeDisposition { + // If `promptForUntrustedHosts` is `false` then perform default handling + // for server trust challenges. + guard promptForUntrustedHosts || challenge.kind != .serverTrust else { + return .performDefaultHandling + } + // Queue up the url challenge. let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) subject.send(queuedChallenge) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift similarity index 94% rename from Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift rename to Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 1f7f113b9..a9bbcf29c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticationModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -33,12 +33,12 @@ private struct InvisibleOverlayModifier: ViewModifier { content Color.clear .frame(width: 0, height: 0) - .modifier(AuthenticationModifier(authenticator: authenticator)) + .modifier(AuthenticatorModifier(authenticator: authenticator)) } } } -private struct AuthenticationModifier: ViewModifier { +private struct AuthenticatorModifier: ViewModifier { @ObservedObject var authenticator: Authenticator @ViewBuilder From 3db439c6fb00abf652b2174ada4b90d27b87efef Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 28 Jun 2022 16:27:22 -0600 Subject: [PATCH 1052/1722] authentication -> authenticator --- .../Examples/AuthenticationExampleView.swift | 2 +- .../AuthenticatorModifier.swift | 2 +- .../Components/Authentication/Keychain.swift | 377 ------------------ 3 files changed, 2 insertions(+), 379 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/Keychain.swift diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 56b835444..88bbd87f5 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -40,7 +40,7 @@ struct AuthenticationExampleView: View { } } } - .authenticator(authenticator: authenticator) + .authenticator(authenticator) .task { try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index a9bbcf29c..2b81d396e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -15,7 +15,7 @@ import SwiftUI public extension View { @ViewBuilder - func authentication(authenticator: Authenticator) -> some View { + func authenticator(_ authenticator: Authenticator) -> some View { modifier(InvisibleOverlayModifier(authenticator: authenticator)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Keychain.swift b/Sources/ArcGISToolkit/Components/Authentication/Keychain.swift deleted file mode 100644 index e9e27e022..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/Keychain.swift +++ /dev/null @@ -1,377 +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 - -/// An actor that provides access to the device keychain. -actor Keychain { - /// The shared instance. - static let shared = Keychain() - - private init() {} - - /// Creates parameters for storing a generic password item in the keychain. - /// - Parameters: - /// - identifier: The unique identifier for the item. - /// - value: The data value of the item. - /// - access: When the item can be accessed. - /// - service: The associated service of the item. - /// - label: A label for the item. - /// - groupIdentifier: The identifier of the group that the item should be stored in. - /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. - static func makeStoreItemParameters( - identifier: String, - value: Data, - access: KeychainAccess, - service: String?, - label: String?, - groupIdentifier: String?, - isSynchronizable: Bool - ) -> CFDictionary { - var parameters = [CFString : Any]() - parameters[kSecClass] = kSecClassGenericPassword - parameters[kSecAttrAccount] = identifier - if let service = service { - parameters[kSecAttrService] = service - } - if let label = label { - parameters[kSecAttrLabel] = label - } - parameters[kSecValueData] = value - parameters[kSecAttrAccessible] = access.value - parameters[kSecAttrAccessGroup] = groupIdentifier - parameters[kSecAttrSynchronizable] = isSynchronizable - // This is highly recommended to be set to true for all keychain operations. - // This key helps to improve the portability of your code across platforms. - // More information here: https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain - parameters[kSecUseDataProtectionKeychain] = true - return parameters as CFDictionary - } - - /// Stores a generic password item in the keychain. - /// - Remark: If no group is specified then the item will be stored in the default group. - /// To know more about what the default group would be you can find information about that here: - /// https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps. - /// - Parameters: - /// - identifier: The unique identifier for the item to be stored under. - /// - value: The data value of the item. - /// - access: When the item can be accessed. - /// - service: The service that the item is associated with. - /// - label: A label for the item. - /// - groupIdentifier: The identifier of the group that the item should be stored in. - /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. - func storeItem( - identifier: String, - value: Data, - access: KeychainAccess, - service: URL? = nil, - label: String? = nil, - groupIdentifier: String? = nil, - isSynchronizable: Bool = false - ) throws { - try removeItem(withIdentifier: identifier) - let parameters = Self.makeStoreItemParameters( - identifier: identifier, - value: value, - access: access, - service: service?.absoluteString, - label: label, - groupIdentifier: groupIdentifier, - isSynchronizable: isSynchronizable - ) - let status = SecItemAdd(parameters, nil) - guard status == errSecSuccess else { - throw KeychainError(status: status)! - } - } - - /// Creates parameters for retrieving items from the keychain. - /// - Parameters: - /// - identifier: The identifier of the item to retrieve. - /// - label: The label of the item to retrieve. - /// - groupIdentifier: The identifier of the group to limit the search to. If `nil` then the - /// search will not be limited to a particular group. - /// - limitOne: A value indicating if only a single result should be returned. - static func makeGetItemsParameters( - identifier: String?, - label: String?, - groupIdentifier: String?, - limitOne: Bool - ) -> CFDictionary { - var parameters = [CFString : Any]() - parameters[kSecClass] = kSecClassGenericPassword - parameters[kSecReturnAttributes] = true - parameters[kSecReturnData] = true - parameters[kSecAttrSynchronizable] = kSecAttrSynchronizableAny - if let identifier = identifier { - parameters[kSecAttrAccount] = identifier - } - if limitOne { - parameters[kSecMatchLimit] = kSecMatchLimitOne - } else { - parameters[kSecMatchLimit] = kSecMatchLimitAll - } - if let label = label { - parameters[kSecAttrLabel] = label - } - if let groupIdentifier = groupIdentifier { - parameters[kSecAttrAccessGroup] = groupIdentifier - } - parameters[kSecUseDataProtectionKeychain] = true - return parameters as CFDictionary - } - - /// Retrieves a generic password item from the keychain with a given identifier. - /// - Parameters: - /// - identifier: The identifier of the item to retrieve. - /// - groupIdentifier: The identifier of the group to limit the search to. If `nil` then the - /// search will not be limited to a particular group. - /// - Returns: The found item or `nil` if an item with the specified identifier could - /// not be found. - func item( - withIdentifier identifier: String, - inGroup groupIdentifier: String? = nil - ) throws -> KeychainItem? { - let parameters = Self.makeGetItemsParameters( - identifier: identifier, - label: nil, - groupIdentifier: groupIdentifier, - limitOne: true - ) - var result: CFTypeRef? = nil - let status = SecItemCopyMatching(parameters, &result) - switch status { - case errSecSuccess: - return KeychainItem(attributes: (result as! [CFString: Any])) - case errSecItemNotFound: - return nil - default: - throw KeychainError(status: status)! - } - } - - /// Retrieves all generic password items in the keychain with a specific label. - /// - Parameters: - /// - label: The label of the items to be retrieved. Or `nil` if all - /// generic password items should be retrieved. - /// - groupIdentifier: The identifier of the group to limit the search to. If `nil` then the - /// search will not be limited to a particular group. - /// - Returns: The found items. - func items( - labeled label: String? = nil, - inGroup groupIdentifier: String? = nil - ) throws -> [KeychainItem] { - let parameters = Self.makeGetItemsParameters( - identifier: nil, - label: label, - groupIdentifier: groupIdentifier, - limitOne: false - ) - var result: CFTypeRef? = nil - let status = SecItemCopyMatching(parameters, &result) - switch status { - case errSecSuccess: - return (result as! [[CFString: Any]]) - .map(KeychainItem.init(attributes:)) - case errSecItemNotFound: - return [] - default: - throw KeychainError(status: status)! - } - } - - /// All generic password items in the keychain. - var items: [KeychainItem] { - get throws { try items() } - } - - /// Make the parameters for a remove item keychain operation. - /// - Parameters: - /// - identifier: The identifier of the item to remove. - /// - label: A label for the item. - /// - groupIdentifier: The identifier of the group of the item to remove. - static func makeRemoveItemParameters( - identifier: String?, - label: String?, - groupIdentifier: String? - ) -> CFDictionary { - var parameters = [CFString : Any]() - if let identifier = identifier { - parameters[kSecAttrAccount] = identifier - } - parameters[kSecClass] = kSecClassGenericPassword - if let label = label { - parameters[kSecAttrLabel] = label - } - if let groupIdentifier = groupIdentifier { - parameters[kSecAttrAccessGroup] = groupIdentifier - } - parameters[kSecAttrSynchronizable] = kSecAttrSynchronizableAny - parameters[kSecUseDataProtectionKeychain] = true - return parameters as CFDictionary - } - - /// Removes a generic password item with a given identifier from the keychain. - /// - Parameters - /// - identifier: The unique identifier that the item is stored under. - /// - groupIdentifier: The identifier of the group of the item to remove. - func removeItem( - withIdentifier identifier: String, - inGroup groupIdentifier: String? = nil - ) throws { - let parameters = Self.makeRemoveItemParameters( - identifier: identifier, - label: nil, - groupIdentifier: groupIdentifier - ) - let status = SecItemDelete(parameters) - switch status { - case errSecSuccess, errSecItemNotFound: - return - default: - throw KeychainError(status: status)! - } - } - - /// Removes a specified item from the keychain. - /// - Parameter item: The item to be removed. - func remove(item: KeychainItem) throws { - return try removeItem(withIdentifier: item.identifier, inGroup: item.groupIdentifier) - } - - /// Remove all generic password items with a specific label from the keychain. - /// - Parameters: - /// - label: The label of the items to be removed, or `nil` if all - /// items should be removed. - /// - groupIdentifier: The identifier of the group of the item to remove. - func removeItems( - labeled label: String? = nil, - inGroup groupIdentifier: String? = nil - ) throws { - let parameters = Self.makeRemoveItemParameters( - identifier: nil, - label: label, - groupIdentifier: groupIdentifier - ) - let status = SecItemDelete(parameters) - switch status { - case errSecSuccess, errSecItemNotFound: - return - default: - throw KeychainError(status: status)! - } - } -} - -private extension KeychainAccess { - /// The `CFString` value of the `Access`. - var value: CFString { - switch self { - case .afterFirstUnlock: - return kSecAttrAccessibleAfterFirstUnlock - case .afterFirstUnlockThisDeviceOnly: - return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly - case .whenUnlocked: - return kSecAttrAccessibleWhenUnlocked - case .whenUnlockedThisDeviceOnly: - return kSecAttrAccessibleWhenUnlockedThisDeviceOnly - case .whenPasscodeSetThisDeviceOnly: - return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly - } - } -} - -private extension KeychainItem { - /// Initializes a keychain item with an attribute dictionary. - /// - Parameter attributes: The attributes returned from a keychain query by which an item - /// should be initialized with. - init(attributes: [CFString: Any]) { - self.init( - identifier: attributes[kSecAttrAccount]! as! String, - service: (attributes[kSecAttrService] as? String).flatMap({ URL(string:$0) }), - created: attributes[kSecAttrCreationDate] as? Date, - modified: attributes[kSecAttrModificationDate] as? Date, - label: attributes[kSecAttrLabel] as? String, - groupIdentifier: attributes[kSecAttrAccessGroup] as? String, - isSynchronizable: attributes[kSecAttrSynchronizable] as? Bool ?? false, - value: attributes["v_Data" as CFString] as? Data ?? Data() - ) - } -} - -/// An enum that describes when an item in the keychain is accessible. -public enum KeychainAccess { - case afterFirstUnlock - case afterFirstUnlockThisDeviceOnly - case whenUnlocked - case whenUnlockedThisDeviceOnly - case whenPasscodeSetThisDeviceOnly -} - -/// A value representing a generic password item in the keychain. -struct KeychainItem { - /// The unique identifier of the item. - var identifier: String - /// The service this item is associated with. - var service: URL? - /// The date this item was created. - var created: Date? - /// The date this item was last modified. - var modified: Date? - /// A label for this item. - var label: String? - /// The identifier of the group that the item is stored in. - var groupIdentifier: String? - /// A value indicating whether the item is synchronized with iCloud. - var isSynchronizable: Bool - /// The data value of this item. - var value: Data -} - -/// An error that can occur in a keychain operation. -struct KeychainError: Error, Hashable { - /// The backing status code for this error. - /// - Note: Status code numeric values can be found here: - /// https://opensource.apple.com/source/Security/Security-55471/libsecurity_keychain/lib/SecBase.h.auto.html - let status: OSStatus - - - /// Initializes a keychain error. This init will fail if the specified status is a success - /// status value. - /// - Parameter status: An `OSStatus`, usually the return value of a keychain operation. - init?(status: OSStatus) { - guard status != errSecSuccess else { return nil } - self.status = status - } -} - -extension KeychainError { - // The message for this error. - var message: String { - (SecCopyErrorMessageString(status, nil) as String?) ?? "" - } -} - -extension KeychainError { - /// The item was not found in the keychain. - static let itemNotFound = Self(status: errSecItemNotFound)! - /// One or more arguments specified were invalid. - static let invalidArgument = Self(status: errSecParam)! - /// An entitlement is missing. - static let missingEntitlement = Self(status: errSecMissingEntitlement)! -} From 5e64eba84fc4f7c4047b116e0c9555a7db5ec68d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 28 Jun 2022 17:14:17 -0600 Subject: [PATCH 1053/1722] add doc --- .../Components/Authentication/AuthenticatorModifier.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 2b81d396e..63c0419a5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -24,6 +24,8 @@ public extension View { /// This view is necessary because of SwiftUI behavior that prevent the UI from functioning /// properly when showing and hiding sheets. The sheets need to be off of a view that is always /// present. +/// The problem that I was seeing without this is content in lists not updating properly +/// after a sheet was dismissed. private struct InvisibleOverlayModifier: ViewModifier { @ObservedObject var authenticator: Authenticator From 3718cad072e7a39dcc8ba79cf88e301d109be8c9 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 29 Jun 2022 13:42:36 -0600 Subject: [PATCH 1054/1722] cleanup and doc --- .../Authentication/Authenticator.swift | 22 +++++++++++++--- .../AuthenticatorModifier.swift | 25 ++++++++----------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 32154dfc0..6333627a7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -15,27 +15,37 @@ import ArcGIS import SwiftUI import Combine +/// A configurable object that handles authentication challenges. @MainActor public final class Authenticator: ObservableObject { + /// The OAuth configurations that this authenticator can work with. let oAuthConfigurations: [OAuthConfiguration] + + /// A value indicating whether we should prompt the user when encountering an untrusted host. var promptForUntrustedHosts: Bool + /// Creates an authenticator. + /// - Parameters: + /// - promptForUntrustedHosts: A value indicating whether we should prompt the user when + /// encountering an untrusted host. + /// - oAuthConfigurations: The OAuth configurations that this authenticator can work with. public init( promptForUntrustedHosts: Bool = false, oAuthConfigurations: [OAuthConfiguration] = [] ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations + // TODO: how to cancel this task? Task { await observeChallengeQueue() } } - /// Sets up a credential store that is synchronized with the keychain. - /// - Remark: The item will be stored in the default access group. + /// Sets up credential stores that are synchronized with the keychain. + /// - Remark: The credentials will be stored in the default access group. /// To know more about what the default group would be you can find information about that 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. - /// - isSynchronizable: A value indicating whether the item is synchronized with iCloud. + /// - isCloudSynchronizable: A value indicating whether the credentials are synchronized with iCloud. public func synchronizeWithKeychain( access: ArcGIS.KeychainAccess, isCloudSynchronizable: Bool = false @@ -50,6 +60,8 @@ public final class Authenticator: ObservableObject { ) } + /// Clears all ArcGIS and network credentials from the stores and resets the backing `URLSessions` + /// so that removed network credentials are respected right away. public func clearCredentialStores() async { // Clear ArcGIS Credentials. await ArcGISURLSession.credentialStore.removeAll() @@ -62,6 +74,7 @@ public final class Authenticator: ObservableObject { ArcGISURLSession.sharedBackground = ArcGISURLSession.makeDefaultSharedBackgroundSession() } + /// Observes the challenge queue and sets the current challenge. private func observeChallengeQueue() async { for await queuedChallenge in challengeQueue { // A yield here helps alleviate the already presenting bug. @@ -87,6 +100,8 @@ public final class Authenticator: ObservableObject { } private var subject = PassthroughSubject() + + /// A serial queue for authentication challenges. private var challengeQueue: AsyncPublisher> { AsyncPublisher( subject @@ -95,6 +110,7 @@ public final class Authenticator: ObservableObject { ) } + /// The current queued challenge. @Published var currentChallenge: QueuedChallenge? } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 63c0419a5..496acc40d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -14,19 +14,23 @@ import SwiftUI public extension View { + /// Presents user experiences for collecting network authentication credentials from the + /// user. + /// - Parameter authenticator: The authenticator for which credentials will be prompted. @ViewBuilder func authenticator(_ authenticator: Authenticator) -> some View { - modifier(InvisibleOverlayModifier(authenticator: authenticator)) + modifier(AuthenticatorOverlayModifier(authenticator: authenticator)) } } -/// This is an intermediate modifier that overlays an always present invisible view. +/// A view modifier that overlays an always present invisible view for which the +/// authenticator view modifiers can modify. /// This view is necessary because of SwiftUI behavior that prevent the UI from functioning /// properly when showing and hiding sheets. The sheets need to be off of a view that is always /// present. /// The problem that I was seeing without this is content in lists not updating properly /// after a sheet was dismissed. -private struct InvisibleOverlayModifier: ViewModifier { +private struct AuthenticatorOverlayModifier: ViewModifier { @ObservedObject var authenticator: Authenticator @ViewBuilder @@ -40,22 +44,13 @@ private struct InvisibleOverlayModifier: ViewModifier { } } +/// A view modifier that prompts for credentials. private struct AuthenticatorModifier: ViewModifier { @ObservedObject var authenticator: Authenticator @ViewBuilder func body(content: Content) -> some View { - if let challenge = authenticator.currentChallenge { - authenticationView(for: challenge, content: content) - } - else { - content - } - } - - @ViewBuilder - func authenticationView(for challenge: QueuedChallenge, content: Content) -> some View { - switch challenge { + switch authenticator.currentChallenge { case let challenge as QueuedArcGISChallenge: content.modifier(UsernamePasswordViewModifier(challenge: challenge)) case let challenge as QueuedNetworkChallenge: @@ -67,6 +62,8 @@ private struct AuthenticatorModifier: ViewModifier { case .login: content.modifier(UsernamePasswordViewModifier(challenge: challenge)) } + case .none: + content default: fatalError() } From e4b764d406ae6d2ae262d6c8fc5d88fd0ee09ed7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 29 Jun 2022 14:00:23 -0600 Subject: [PATCH 1055/1722] doc --- .../Components/Authentication/DocumentPickerView.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift index fb4e4ef2a..7a15a56fc 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift @@ -14,9 +14,14 @@ import SwiftUI import UniformTypeIdentifiers +/// A view that allows the user to browse to and select a document. +/// This view wraps a `UIDocumentPickerViewController`. struct DocumentPickerView: UIViewControllerRepresentable { + /// The content types that are allowed to be selected. var contentTypes: [UTType] + /// The closure called when a document is selected. var onPickDocument: (URL) -> Void + /// The closure called when the user cancels. var onCancel: () -> Void func makeCoordinator() -> Coordinator { @@ -33,6 +38,8 @@ struct DocumentPickerView: UIViewControllerRepresentable { } extension DocumentPickerView { + /// The coordinator for the document picker view that acts as a delegate to the underlying + /// `UIDocumentPickerViewController`. final class Coordinator: NSObject, UIDocumentPickerDelegate { var onPickDocument: (URL) -> Void var onCancel: () -> Void From c101e6a87d6e8c25ad2d6f6b3ed5cd1cde66147e Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 29 Jun 2022 14:02:53 -0600 Subject: [PATCH 1056/1722] doc --- .../Components/Authentication/SheetViewModifier.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift index bb26fb6e7..6a21a104b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift @@ -13,6 +13,7 @@ import SwiftUI +/// A view modifier that presents a sheet without waiting for a condition. struct SheetViewModifier: ViewModifier { // Even though we will present it right away we need to use a state variable for this. // Using a constant has 2 issues. One, it won't animate. Two, when challenging for multiple @@ -31,6 +32,7 @@ struct SheetViewModifier: ViewModifier { } extension View { + /// Presents a sheet without waiting for a condition. @ViewBuilder func sheet(content: @escaping () -> Content) -> some View { modifier(SheetViewModifier(sheetContent: content)) From 8677fb0e8cea84054571db708d36422692784892 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 29 Jun 2022 14:06:49 -0600 Subject: [PATCH 1057/1722] doc --- .../Components/Authentication/TrustHostViewModifier.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index 999ee6623..7d39f84fe 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -13,13 +13,18 @@ import SwiftUI +/// Prompts the user when encountering an untrusted host. struct TrustHostViewModifier: ViewModifier { + /// Creates a `TrustHostViewModifier`. + /// - Parameter challenge: The network authentication challenge for the untrusted host. init(challenge: QueuedNetworkChallenge) { self.challenge = challenge } + /// The network authentication challenge for the untrusted host. let challenge: QueuedNetworkChallenge + /// The untrusted host. var host: String { challenge.networkChallenge.host } @@ -32,7 +37,7 @@ struct TrustHostViewModifier: ViewModifier { func body(content: Content) -> some View { content - .task { isPresented = true } // Present the alert right away + .task { isPresented = true } // Present the alert right away. .alert("Certificate Trust Warning", isPresented: $isPresented, presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { isPresented = false From 9d34ecf8a6c91b0f46713bc994022dbe20116f03 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 29 Jun 2022 15:03:48 -0600 Subject: [PATCH 1058/1722] add sample --- Examples/Examples/AuthenticationExampleView.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 88bbd87f5..96c679b45 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -21,6 +21,7 @@ struct AuthenticationExampleView: View { ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() + let logger = ConsoleNetworkLogger(shouldLogResponseData: false) var body: some View { VStack { @@ -43,6 +44,7 @@ struct AuthenticationExampleView: View { .authenticator(authenticator) .task { try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) + logger.startLogging() } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .onAppear { @@ -189,6 +191,13 @@ extension AuthenticationItem { ) } + static func makeEricPKIMap() -> AuthenticationItem { + AuthenticationItem( + title: "Eric PKI Map", + loadables: [Map(url: URL(string: "https://enterprise.esrigxdev.com/portal1091/sharing/rest/content/items/3e3b4cec95d143478cf187ba6ea4f7c4")!)!] + ) + } + static func makeAll() -> [AuthenticationItem] { [ makeToken(), @@ -196,7 +205,8 @@ extension AuthenticationItem { makeMultipleTokenSame(), makePortal(), makeIWAPortal(), - makePKIMap() + makePKIMap(), + makeEricPKIMap() ] } } From b804f64dfe1deec8ca70f597b8ac76b8f992c492 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 29 Jun 2022 16:23:47 -0700 Subject: [PATCH 1059/1722] Add fractional edge and terminal selection --- .../UtilityNetworkTrace.swift | 34 ++++++++ .../UtilityNetworkTraceViewModel.swift | 78 ++++++++++++++++++- .../Extensions/UtilityTerminal.swift | 27 +++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 51a325b5f..5c6bf5000 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -278,6 +278,40 @@ public struct UtilityNetworkTrace: View { .lineLimit(1) .frame(maxWidth: .infinity, alignment: .center) List { + if selectedStartingPoint?.utilityElement.networkSource.kind == .edge { + Section("Fraction Along Edge") { + Slider(value: Binding(get: { + viewModel.pendingTrace.startingPoints.first { sp in + sp.utilityElement.globalID == selectedStartingPoint?.utilityElement.globalID + }?.utilityElement.fractionAlongEdge ?? .zero + }, set: { newValue in + if let selectedStartingPoint { + viewModel.setFractionAlongEdgeFor( + startingPoint: selectedStartingPoint, + to: newValue + ) + } + })) + } + } else if selectedStartingPoint?.utilityElement.networkSource.kind == .junction && + !(selectedStartingPoint?.utilityElement.assetType.terminalConfiguration?.terminals.isEmpty ?? true) { + Section { + Picker( + "Terminal Configuration", + selection: Binding(get: { + selectedStartingPoint!.utilityElement.terminal! + }, set: { newValue in + viewModel.setTerminalConfigurationFor(startingPoint: selectedStartingPoint!, to: newValue) + }) + ) { + ForEach(viewModel.pendingTrace.startingPoints.first { sp in + sp.utilityElement.globalID == selectedStartingPoint?.utilityElement.globalID + }?.utilityElement.assetType.terminalConfiguration?.terminals ?? [], id: \.self) { + Text($0.name) + } + } + } + } Text(selectedStartingPoint?.utilityElement.globalID.uuidString ?? "N/A") ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in HStack{ diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 112a23390..d9dba5647 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -158,12 +158,23 @@ import SwiftUI Task { guard let network = network, let feature = geoElement as? ArcGISFeature, - let extent = feature.geometry?.extent, + let geometry = feature.geometry, let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? .renderer? .symbol(for: feature)? .makeSwatch(scale: 1.0), let utilityElement = network.createElement(arcGISFeature: feature) else { return } + + if utilityElement.networkSource.kind == .edge && geometry is Polyline { + utilityElement.fractionAlongEdge = fractionAlongEdge( + of: geometry, + at: mapPoint + ) + } else if utilityElement.networkSource.kind == .junction && + utilityElement.assetType.terminalConfiguration?.terminals.count ?? 0 > 1 { + utilityElement.terminal = utilityElement.assetType.terminalConfiguration?.terminals.first + } + let graphic = Graphic( geometry: mapPoint, symbol: SimpleMarkerSymbol( @@ -172,7 +183,7 @@ import SwiftUI ) ) let startingPoint = UtilityNetworkTraceStartingPoint( - extent: extent, + extent: geometry.extent, geoElement: geoElement, graphic: graphic, image: symbol, @@ -185,6 +196,32 @@ import SwiftUI } } + func setFractionAlongEdgeFor( + startingPoint: UtilityNetworkTraceStartingPoint, + to newValue: Double + ) { + pendingTrace.startingPoints.first { + $0.utilityElement.globalID == startingPoint.utilityElement.globalID + }?.utilityElement.fractionAlongEdge = newValue + if let geometry = startingPoint.geoElement.geometry, + let polyline = geometry as? Polyline { + startingPoint.graphic.geometry = GeometryEngine.point( + along: polyline, + atDistance: GeometryEngine.length(of: geometry) * newValue + ) + } + } + + func setTerminalConfigurationFor( + startingPoint: UtilityNetworkTraceStartingPoint, + to newValue: UtilityTerminal + ) { + pendingTrace.startingPoints.first { + $0.utilityElement.globalID == startingPoint.utilityElement.globalID + }?.utilityElement.terminal = newValue + objectWillChange.send() + } + /// Runs the pending trace and stores it into the list of completed traces. func trace() { guard let config = pendingTrace.configuration, @@ -288,3 +325,40 @@ import SwiftUI configurations = (try? await map.getNamedTraceConfigurations(from: network)) ?? [] } } + +extension UtilityNetworkTraceViewModel { + /// Finds the location on the line nearest the input point, expressed as the fraction along the line’s total + /// geodesic length. + /// - Parameters: + /// - inputGeometry: The line to be measured. + /// - point: A location along the line. + private func fractionAlongEdge( + of inputGeometry: Geometry, + at point: Point + ) -> Double { + var geometry = inputGeometry + // Remove Z + if geometry.hasZ { + geometry = GeometryEngine.makeGeometry( + from: geometry, + z: nil + ) + } + + // Confirm spatial references match + if let spatialReference = point.spatialReference, + spatialReference != geometry.spatialReference, + let projectedGeometry = GeometryEngine.project( + geometry, + into: spatialReference + ) { + geometry = projectedGeometry + } + + return GeometryEngine.polyline( + geometry as! Polyline, + fractionalLengthClosestTo: point, + tolerance: 10 + ) + } +} diff --git a/Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift b/Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift new file mode 100644 index 000000000..7a8aafa82 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/UtilityTerminal.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 ArcGIS + +extension UtilityTerminal: Hashable { + public static func == ( + lhs: ArcGIS.UtilityTerminal, + rhs: ArcGIS.UtilityTerminal + ) -> Bool { + lhs.hashValue == rhs.hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} From a21b675fc65a66ed7628226149c22c7a65b4f062 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Jun 2022 11:38:41 -0700 Subject: [PATCH 1060/1722] 2 small removals --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 1 - .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 451b89dfe..6a9bf0ac6 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -49,7 +49,6 @@ struct UtilityNetworkTraceExampleView: View { self.pointInMap = pointInMap self.mapViewProxy = mapViewProxy } - .edgesIgnoringSafeArea(.bottom) .overlay(alignment: .topTrailing) { FloatingPanel { UtilityNetworkTrace( diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 3f8d97a89..f30daf828 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -295,7 +295,6 @@ public struct UtilityNetworkTrace: View { Image(systemName: "scope") } } - .padding() } /// Displays the chosen starting points for the new trace. From d4e1794ff1a3b15a6f09c544f350667d6438484d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Jun 2022 14:43:04 -0700 Subject: [PATCH 1061/1722] Update UtilityNetworkTraceExampleView.swift Add public credentials to sample --- .../UtilityNetworkTraceExampleView.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 6a9bf0ac6..a49edbed9 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -17,9 +17,6 @@ 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. -/// -/// - Note: An ``ArcGISCredential`` is needed to interact with this sample. This can be done within -/// `init()` in `ExamplesApp.swift`. The public credentials can be found [here](https://developers.arcgis.com/javascript/latest/sample-code/widgets-untrace). struct UtilityNetworkTraceExampleView: View { @StateObject private var map = makeMap() @@ -59,6 +56,9 @@ struct UtilityNetworkTraceExampleView: View { $mapViewProxy, $viewpoint ) + .task { + await ArcGISURLSession.credentialStore.add(try! await .publicSample) + } } .padding() .frame(width: 360) @@ -79,3 +79,15 @@ struct UtilityNetworkTraceExampleView: View { return Map(item: portalItem) } } + +private extension ArcGISCredential { + static var publicSample: ArcGISCredential { + get async throws { + try await .token( + url: URL(string: "https://sampleserver7.arcgisonline.com/portal/sharing/rest")!, + username: "viewer01", + password: "I68VGU^nMurF" + ) + } + } +} From 00e1bb5259518a4bca9e676e72e2b07dc78b321e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Jun 2022 18:46:38 -0700 Subject: [PATCH 1062/1722] Update UtilityNetworkTraceExampleView.swift --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index a49edbed9..94137a613 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -15,7 +15,7 @@ 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 +/// 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 { @StateObject private var map = makeMap() @@ -32,6 +32,7 @@ struct UtilityNetworkTraceExampleView: View { /// A container for graphical trace results. @State var resultGraphicsOverlay = GraphicsOverlay() + /// The map viewpoint used by the `UtilityNetworkTrace` to pan/zoom the map to selected features. @State var viewpoint: Viewpoint? var body: some View { From 88eef66b747e5aa102db81dce11d2aecf4791b77 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 30 Jun 2022 19:04:22 -0700 Subject: [PATCH 1063/1722] Apply feedback --- .../UtilityNetworkTraceExampleView.swift | 8 ++----- .../UtilityNetworkTrace.swift | 17 +++++++-------- .../UtilityNetworkTraceFunctionOutput.swift | 21 ------------------- .../UtilityNetworkTraceViewModel.swift | 13 +++++------- .../UtilityNetworkTraceViewModelTrace.swift | 3 ++- 5 files changed, 17 insertions(+), 45 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.swift diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 94137a613..b06e5276f 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -67,14 +67,10 @@ struct UtilityNetworkTraceExampleView: View { } } - /// Make a map from a portal item. + /// Makes a map from a portal item. static func makeMap() -> Map { - let portal = Portal( - url: URL(string: "https://www.arcgis.com/")!, - isLoginRequired: false - ) let portalItem = PortalItem( - portal: portal, + portal: .arcGISOnline(isLoginRequired: false), id: Item.ID(rawValue: "471eb0bf37074b1fbb972b1da70fb310")! ) return Map(item: portalItem) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index f30daf828..b499f12c2 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -143,12 +143,11 @@ public struct UtilityNetworkTrace: View { } Section("Starting Points") { if isAddingStartingPoints { - Button { + Button(role: .destructive) { currentActivity = .creatingTrace(nil) } label: { Text("Cancel starting point selection") } - .tint(.red) } else { Button { currentActivity = .creatingTrace(.addingStartingPoints) @@ -209,7 +208,7 @@ public struct UtilityNetworkTrace: View { } .font(.title3) .padding() - if let traceName = viewModel.selectedTrace?.name, traceName != "" { + if let traceName = viewModel.selectedTrace?.name, !traceName.isEmpty { Text(traceName) } List { @@ -230,9 +229,9 @@ public struct UtilityNetworkTrace: View { .functionOutputs.count.description ?? "0") { ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.id) { item in HStack { - Text(item.output.function.networkAttribute?.name ?? "Unnamed") + Text(item.function.networkAttribute?.name ?? "Unnamed") Spacer() - Text((item.output.result as? Double)?.description ?? "N/A") + Text((item.result as? Double)?.description ?? "N/A") } } } @@ -253,7 +252,7 @@ public struct UtilityNetworkTrace: View { Text("OK") } } message: { - Text("Are you sure? All the trace Inputs and Results will be lost.") + Text("Are you sure? All the trace inputs and results will be lost.") } } @@ -326,13 +325,13 @@ public struct UtilityNetworkTrace: View { /// - Parameters: /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace /// graphics. - /// - map: The parent map view. + /// - map: The parent map. /// - pointInMap: Acts as the point at which newly selected starting point graphics will be /// created. /// - pointInScreen: 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. + /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. public init( _ graphicsOverlay: Binding, _ map: Map, @@ -352,7 +351,7 @@ public struct UtilityNetworkTrace: View { graphicsOverlay: graphicsOverlay.wrappedValue ) ) - UITableView.appearance().backgroundColor = UIColor.systemGroupedBackground + UITableView.appearance().backgroundColor = .systemGroupedBackground } public var body: some View { diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.swift deleted file mode 100644 index fdec7ad7d..000000000 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceFunctionOutput.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 -import Foundation - -/// A `Hashable` wrapper over `UtilityTraceFunctionOutput`. -struct UtilityNetworkTraceFunctionOutput { - let id = UUID() - let output: UtilityTraceFunctionOutput -} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index a9317afa3..4a4ac0535 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -45,9 +45,7 @@ import SwiftUI } } - // MARK: Other Properties - - /// Indicates that the pending trace is configured to the point that it can be ran. + /// A Boolean value indicating if the pending trace is configured to the point that it can be run. var canRunTrace: Bool { pendingTrace.configuration != nil && !pendingTrace.startingPoints.isEmpty } @@ -67,8 +65,6 @@ import SwiftUI } } - // MARK: Public Methods - /// Performs required setup. /// /// The first utility network in the provided map will be used. @@ -237,9 +233,7 @@ import SwiftUI case let result as UtilityFunctionTraceResult: let functionOutputs = result.functionOutputs functionOutputs.forEach { functionOutput in - pendingTrace.functionOutputs.append( - UtilityNetworkTraceFunctionOutput(output: functionOutput) - ) + pendingTrace.functionOutputs.append(functionOutput) } pendingTrace.utilityFunctionTraceResult = result default: @@ -272,8 +266,11 @@ import SwiftUI } /// Loads the named trace configurations in the network on the provided map. + /// - Parameter map: A web map containing one or more utility networks. private func loadNamedTraceConfigurations(_ map: Map) async { guard let network = network else { return } configurations = (try? await map.getNamedTraceConfigurations(from: network)) ?? [] } } + +extension UtilityTraceFunctionOutput: Identifiable { } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 502e7e023..5600da470 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -34,7 +34,8 @@ extension UtilityNetworkTraceViewModel { /// The chosen named trace configuration. var configuration: UtilityNamedTraceConfiguration? - var functionOutputs = [UtilityNetworkTraceFunctionOutput]() + /// A collection of utility trace function outputs. + var functionOutputs = [UtilityTraceFunctionOutput]() /// A collection of graphics generated from the trace. var graphics = [Graphic]() From f3bc49a42d4b42f9c6c4b723a0d7913e9061130b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 1 Jul 2022 10:23:03 -0700 Subject: [PATCH 1064/1722] Small UI tweaks --- .../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 b499f12c2..d40afe92d 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -243,7 +243,6 @@ public struct UtilityNetworkTrace: View { Text("Clear All Results") .tint(.red) } - .padding() .alert("Clear All Results", isPresented: $warningIsPresented) { Button(role: .destructive) { viewModel.deleteAllTraces() @@ -371,6 +370,7 @@ public struct UtilityNetworkTrace: View { resultsTab } } + .background(Color(uiColor: .systemGroupedBackground)) .animation(.default, value: currentActivity) .onChange(of: pointInScreen) { newValue in guard isAddingStartingPoints, From 26490a5f263b290336d7bfd086c836af0cf48607 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 1 Jul 2022 10:50:48 -0700 Subject: [PATCH 1065/1722] Simplify newTraceTab --- .../UtilityNetworkTrace.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index d40afe92d..7466063a5 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -130,8 +130,14 @@ public struct UtilityNetworkTrace: View { /// The tab that allows for a new trace to be configured. @ViewBuilder private var newTraceTab: some View { - List { - if !isAddingStartingPoints { + if isAddingStartingPoints { + Button(role: .destructive) { + currentActivity = .creatingTrace(nil) + } label: { + Text("Cancel starting point selection") + } + } else { + List { Section("Trace Configuration") { DisclosureGroup( viewModel.pendingTrace.configuration?.name ?? "None selected", @@ -140,15 +146,7 @@ public struct UtilityNetworkTrace: View { configurationsList } } - } - Section("Starting Points") { - if isAddingStartingPoints { - Button(role: .destructive) { - currentActivity = .creatingTrace(nil) - } label: { - Text("Cancel starting point selection") - } - } else { + Section("Starting Points") { Button { currentActivity = .creatingTrace(.addingStartingPoints) } label: { @@ -161,8 +159,6 @@ public struct UtilityNetworkTrace: View { startingPointsList } } - } - if !isAddingStartingPoints { Section("Advanced") { ColorPicker( selection: $viewModel.pendingTrace.color From dc4da6d553e3ef55d5e970789de6b7f79f6f21e5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 1 Jul 2022 11:21:22 -0700 Subject: [PATCH 1066/1722] Further simplifications --- .../UtilityNetworkTrace.swift | 101 +++++++++--------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 7466063a5..c6d735755 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -78,10 +78,10 @@ public struct UtilityNetworkTrace: View { /// Allows the Utility Network Trace Tool to update the parent map view's viewpoint. @Binding private var viewpoint: Viewpoint? - // MARK: ViewBuilders + // MARK: Subviews /// Allows the user to switch between the trace creation and viewing tabs. - @ViewBuilder private var activityPicker: some View { + private var activityPicker: some View { Picker( "Mode", selection: Binding( @@ -104,6 +104,15 @@ public struct UtilityNetworkTrace: View { .padding() } + /// Allows the user to cancel out of selecting a new starting point. + private var cancelAddStartingPoints: some View { + Button(role: .destructive) { + currentActivity = .creatingTrace(nil) + } label: { + Text("Cancel starting point selection") + } + } + /// Displays the list of available named trace configurations. @ViewBuilder private var configurationsList: some View { if viewModel.configurations.isEmpty { @@ -130,60 +139,50 @@ public struct UtilityNetworkTrace: View { /// The tab that allows for a new trace to be configured. @ViewBuilder private var newTraceTab: some View { - if isAddingStartingPoints { - Button(role: .destructive) { - currentActivity = .creatingTrace(nil) - } label: { - Text("Cancel starting point selection") + List { + Section("Trace Configuration") { + DisclosureGroup( + viewModel.pendingTrace.configuration?.name ?? "None selected", + isExpanded: configurationOptionsIsExpanded + ) { + configurationsList + } } - } else { - List { - Section("Trace Configuration") { - DisclosureGroup( - viewModel.pendingTrace.configuration?.name ?? "None selected", - isExpanded: configurationOptionsIsExpanded - ) { - configurationsList - } + Section("Starting Points") { + Button { + currentActivity = .creatingTrace(.addingStartingPoints) + } label: { + Text("Add new starting point") } - Section("Starting Points") { - Button { - currentActivity = .creatingTrace(.addingStartingPoints) - } label: { - Text("Add new starting point") - } - DisclosureGroup( - "\(viewModel.pendingTrace.startingPoints.count) selected", - isExpanded: startingPointsListIsExpanded - ) { - startingPointsList - } + DisclosureGroup( + "\(viewModel.pendingTrace.startingPoints.count) selected", + isExpanded: startingPointsListIsExpanded + ) { + startingPointsList } - Section("Advanced") { - ColorPicker( - selection: $viewModel.pendingTrace.color - ) { - Text("Trace Color") - } - if !isCompact { - TextField( - "Trace Name", - text: $viewModel.pendingTrace.name - ) - } + } + Section("Advanced") { + ColorPicker( + selection: $viewModel.pendingTrace.color + ) { + Text("Trace Color") + } + if !isCompact { + TextField( + "Trace Name", + text: $viewModel.pendingTrace.name + ) } } } - if !isAddingStartingPoints { - Button { - viewModel.trace() - currentActivity = .viewingTraces - } label: { - Text("Trace") - } - .buttonStyle(.bordered) - .disabled(!viewModel.canRunTrace) + Button { + viewModel.trace() + currentActivity = .viewingTraces + } label: { + Text("Trace") } + .buttonStyle(.bordered) + .disabled(!viewModel.canRunTrace) } /// The tab that allows for viewing completed traces. @@ -292,7 +291,7 @@ public struct UtilityNetworkTrace: View { } /// Displays the chosen starting points for the new trace. - @ViewBuilder private var startingPointsList: some View { + private var startingPointsList: some View { ForEach(viewModel.pendingTrace.startingPoints, id: \.utilityElement.globalID) { startingPoint in Button { currentActivity = .creatingTrace( @@ -357,6 +356,8 @@ public struct UtilityNetworkTrace: View { switch currentActivity { case .creatingTrace(let activity): switch activity { + case .addingStartingPoints: + cancelAddStartingPoints case .inspectingStartingPoint: startingPointDetail default: From 5189dd4aa9e35d3dffc0553d6ab9c28578bd388a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 1 Jul 2022 15:27:02 -0600 Subject: [PATCH 1067/1722] build fixes --- Examples/Examples/AuthenticationExampleView.swift | 4 +--- .../Components/Authentication/Authenticator.swift | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 96c679b45..f89e2c871 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -21,7 +21,6 @@ struct AuthenticationExampleView: View { ) @State var previousApiKey: APIKey? @State private var items = AuthenticationItem.makeAll() - let logger = ConsoleNetworkLogger(shouldLogResponseData: false) var body: some View { VStack { @@ -43,8 +42,7 @@ struct AuthenticationExampleView: View { } .authenticator(authenticator) .task { - try? await authenticator.synchronizeWithKeychain(access: .whenUnlockedThisDeviceOnly) - logger.startLogging() + try? await authenticator.makePersistent(access: .whenUnlockedThisDeviceOnly) } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .onAppear { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 6333627a7..318ed9933 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -39,14 +39,14 @@ public final class Authenticator: ObservableObject { Task { await observeChallengeQueue() } } - /// Sets up credential stores that are synchronized with the keychain. + /// Sets up credential stores so that they are persisted to the keychain. /// - Remark: The credentials will be stored in the default access group. /// To know more about what the default group would be you can find information about that 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. /// - isCloudSynchronizable: A value indicating whether the credentials are synchronized with iCloud. - public func synchronizeWithKeychain( + public func makePersistent( access: ArcGIS.KeychainAccess, isCloudSynchronizable: Bool = false ) async throws { @@ -55,7 +55,7 @@ public final class Authenticator: ObservableObject { isSynchronizable: isCloudSynchronizable ) - NetworkCredentialStore.setShared( + await NetworkCredentialStore.setShared( try await .makePersistent(access: access, isSynchronizable: isCloudSynchronizable) ) } From 63b275f49c89ef82a945302c0f18d880efefc58f Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 1 Jul 2022 15:45:37 -0600 Subject: [PATCH 1068/1722] fix button disabled bug --- .../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 8981080a5..64917f5dd 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -171,7 +171,7 @@ struct EnterPasswordView: View { .frame(maxWidth: .infinity, alignment: .center) .foregroundColor(.white) }) - .disabled(!password.isEmpty) - .listRowBackground(!password.isEmpty ? Color.accentColor : Color.gray) + .disabled(password.isEmpty) + .listRowBackground(!password.isEmpty ? Color.accentColor : Color.gray) } } From 868125abb3a320e40eaf3bc973c7e8e8274c3cb6 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 1 Jul 2022 15:45:41 -0600 Subject: [PATCH 1069/1722] cleanup --- .../Authentication/Authenticator.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 318ed9933..79e80931d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -39,29 +39,30 @@ public final class Authenticator: ObservableObject { Task { await observeChallengeQueue() } } - /// Sets up credential stores so that they are persisted to the keychain. - /// - Remark: The credentials will be stored in the default access group. + /// 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. /// To know more about what the default group would be you can find information about that 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. - /// - isCloudSynchronizable: A value indicating whether the credentials are synchronized with iCloud. + /// - isSynchronizable: A value indicating whether the credentials are synchronized with iCloud. public func makePersistent( access: ArcGIS.KeychainAccess, - isCloudSynchronizable: Bool = false + isSynchronizable: Bool = false ) async throws { ArcGISURLSession.credentialStore = try await .makePersistent( access: access, - isSynchronizable: isCloudSynchronizable + isSynchronizable: isSynchronizable ) await NetworkCredentialStore.setShared( - try await .makePersistent(access: access, isSynchronizable: isCloudSynchronizable) + try await .makePersistent(access: access, isSynchronizable: isSynchronizable) ) } - /// Clears all ArcGIS and network credentials from the stores and resets the backing `URLSessions` - /// so that removed network credentials are respected right away. + /// 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. await ArcGISURLSession.credentialStore.removeAll() From 27e550143e72efe30a6405ca4c3bbaf6caac8d1c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 1 Jul 2022 15:56:04 -0600 Subject: [PATCH 1070/1722] use disposition --- .../Authentication/Authenticator.swift | 16 +--------------- .../CertificatePickerViewModifier.swift | 5 +++-- .../Authentication/QueuedChallenge.swift | 18 +++--------------- .../Authentication/TrustHostViewModifier.swift | 4 ++-- .../UsernamePasswordViewModifier.swift | 4 ++-- 5 files changed, 11 insertions(+), 36 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 79e80931d..086c31427 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -149,20 +149,6 @@ extension Authenticator: AuthenticationChallengeHandler { subject.send(queuedChallenge) // Respond accordingly. - switch await queuedChallenge.response { - case .cancel: - return .cancelAuthenticationChallenge - case .trustHost: - return .useCredential(.serverTrust) - case .login(let user, let password): - return .useCredential(.login(username: user, password: password)) - case .certificate(let url, let password): - do { - return .useCredential(try .certificate(at: url, password: password)) - } catch { - // TODO: handle error - fatalError() - } - } + return await queuedChallenge.response } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 64917f5dd..63b05d44f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -31,11 +31,12 @@ final private class CertificatePickerViewModel: ObservableObject { preconditionFailure() } - challenge.resume(with: .certificate(url: certificateURL, passsword: password)) + // TODO: handle error + challenge.resume(with: .useCredential(try! .certificate(at: certificateURL, password: password))) } func cancel() { - challenge.cancel() + challenge.resume(with: .cancelAuthenticationChallenge) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 4a32843d4..75b84bbe1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -65,21 +65,16 @@ final class QueuedNetworkChallenge: QueuedChallenge { self.networkChallenge = networkChallenge } - func resume(with response: Response) { + func resume(with response: NetworkAuthenticationChallengeDisposition) { guard _response == nil else { return } _response = response } - func cancel() { - guard _response == nil else { return } - _response = .cancel - } - /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. - @Streamed private var _response: (Response)? + @Streamed private var _response: (NetworkAuthenticationChallengeDisposition)? - var response: Response { + var response: NetworkAuthenticationChallengeDisposition { get async { await $_response .compactMap({ $0 }) @@ -91,13 +86,6 @@ final class QueuedNetworkChallenge: QueuedChallenge { _ = await response } - enum Response { - case login(username: String, password: String) - case certificate(url: URL, passsword: String) - case trustHost - case cancel - } - enum Kind { case serverTrust case login diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index 7d39f84fe..d567293c7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -41,11 +41,11 @@ struct TrustHostViewModifier: ViewModifier { .alert("Certificate Trust Warning", isPresented: $isPresented, presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { isPresented = false - challenge.resume(with: .trustHost) + challenge.resume(with: .useCredential(.serverTrust)) } Button("Cancel", role: .cancel) { isPresented = false - challenge.cancel() + challenge.resume(with: .cancelAuthenticationChallenge) } } message: { _ in Text("The certificate provided by '\(host)' is not signed by a trusted authority.") diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 1ce79bbf8..375ee266d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -246,11 +246,11 @@ class NetworkCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { func signIn() { formEnabled = false - challenge.resume(with: .login(username: username, password: password)) + challenge.resume(with: .useCredential(.login(username: username, password: password))) } func cancel() { formEnabled = false - challenge.cancel() + challenge.resume(with: .cancelAuthenticationChallenge) } } From 4b5c4df772a59125a65ba69bcfa70acf10c5481d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 1 Jul 2022 15:54:42 -0700 Subject: [PATCH 1071/1722] Put advanced option into disclosure group --- .../UtilityNetworkTrace.swift | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index bdae69190..41c2217f3 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -35,6 +35,8 @@ public struct UtilityNetworkTrace: View { case addingStartingPoints /// The user is inspecting details of a chosen starting point. case inspectingStartingPoint(UtilityNetworkTraceStartingPoint) + /// The user is viewing the list of advanced options. + case viewingAdvancedOptions /// The user is viewing the list of chosen starting points. case viewingStartingPoints /// The user is viewing the list of available trace configurations. @@ -164,7 +166,10 @@ public struct UtilityNetworkTrace: View { } } Section { - DisclosureGroup("Advanced Options") { + DisclosureGroup( + "Advanced Options", + isExpanded: advancedOptionsIsExpanded + ) { ColorPicker( selection: $viewModel.pendingTrace.color ) { @@ -428,6 +433,29 @@ public struct UtilityNetworkTrace: View { // MARK: Computed Properties + /// Indicates if the list of advanced options is expanded. + private var advancedOptionsIsExpanded: Binding { + Binding(get: { + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .viewingAdvancedOptions: + return true + default: + return false + } + default: + return false + } + }, set: { val in + if val { + currentActivity = .creatingTrace(.viewingAdvancedOptions) + } else { + currentActivity = .creatingTrace(nil) + } + }) + } + /// Indicates if the list of trace configuration options is expanded. private var configurationOptionsIsExpanded: Binding { Binding(get: { From a930d4384300e703920e79334d3c708e7ced2fba Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 09:05:08 -0600 Subject: [PATCH 1072/1722] workaround --- .../CertificatePickerViewModifier.swift | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 63b05d44f..54ec3c5b1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -20,19 +20,30 @@ final private class CertificatePickerViewModel: ObservableObject { @Published var certificateURL: URL? @Published var password: String = "" + @Published var certificateImportFailed = false init(challenge: QueuedNetworkChallenge) { self.challenge = challenge challengingHost = challenge.networkChallenge.host } + @MainActor func signIn() { guard let certificateURL = certificateURL else { preconditionFailure() } - // TODO: handle error - challenge.resume(with: .useCredential(try! .certificate(at: certificateURL, password: password))) + Task { + do { + challenge.resume(with: .useCredential(try .certificate(at: certificateURL, password: password))) + } catch { + // TODO: Why is this required? + try await Task.sleep(nanoseconds: 1_000_000_000) + certificateImportFailed = true + certificateURL = nil + password = "" + } + } } func cancel() { @@ -47,12 +58,23 @@ struct CertificatePickerViewModifier: ViewModifier { @ObservedObject private var viewModel: CertificatePickerViewModel + // TODO: These should be in the view model? @State var showPrompt: Bool = true @State var showPicker: Bool = false @State var showPassword: Bool = false func body(content: Content) -> some View { content + .alert("Error importing certificate", isPresented: $viewModel.certificateImportFailed) { + Button("Try Again") { + showPicker = true + } + Button("Cancel", role: .cancel) { + viewModel.cancel() + } + } message: { + Text("The certificate file or password was invalid.") + } .promptBrowseCertificate( isPresented: $showPrompt, host: viewModel.challengingHost, @@ -62,7 +84,8 @@ struct CertificatePickerViewModifier: ViewModifier { }, onCancel: { showPrompt = false viewModel.cancel() - }) + } + ) .sheet(isPresented: $showPicker) { DocumentPickerView(contentTypes: [.pfx]) { viewModel.certificateURL = $0 @@ -86,6 +109,10 @@ struct CertificatePickerViewModifier: ViewModifier { .edgesIgnoringSafeArea(.bottom) .interactiveDismissDisabled() } +// .sheet(isPresented: $viewModel.showError) { +// Text("Error importing certificate. The certificate file or password was invalid.") +// } + } } From d79b43ef25d33589741cdd415e4a16a0bc0bb0bd Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 10:30:32 -0600 Subject: [PATCH 1073/1722] fix issue showing error --- .../CertificatePickerViewModifier.swift | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 54ec3c5b1..2344ed6f0 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -14,21 +14,32 @@ import SwiftUI import UniformTypeIdentifiers +@MainActor final private class CertificatePickerViewModel: ObservableObject { let challengingHost: String let challenge: QueuedNetworkChallenge - @Published var certificateURL: URL? - @Published var password: String = "" - @Published var certificateImportFailed = false + @Published private(set) var certificateURL: URL? + @Published var showPrompt = true + @Published var showPicker = false + @Published var showPassword = false + @Published var showCertificateImportError = false init(challenge: QueuedNetworkChallenge) { self.challenge = challenge challengingHost = challenge.networkChallenge.host } - @MainActor - func signIn() { + func proceedFromPrompt() { + showPicker = true + } + + func proceed(withCertificateURL url: URL) { + certificateURL = url + showPassword = true + } + + func proceed(withPassword password: String) { guard let certificateURL = certificateURL else { preconditionFailure() } @@ -37,11 +48,9 @@ final private class CertificatePickerViewModel: ObservableObject { do { challenge.resume(with: .useCredential(try .certificate(at: certificateURL, password: password))) } catch { - // TODO: Why is this required? - try await Task.sleep(nanoseconds: 1_000_000_000) - certificateImportFailed = true - certificateURL = nil - password = "" + // This is required to prevent an "already presenting" error. + await Task.yield() + showCertificateImportError = true } } } @@ -57,61 +66,46 @@ struct CertificatePickerViewModifier: ViewModifier { } @ObservedObject private var viewModel: CertificatePickerViewModel - - // TODO: These should be in the view model? - @State var showPrompt: Bool = true - @State var showPicker: Bool = false - @State var showPassword: Bool = false func body(content: Content) -> some View { content - .alert("Error importing certificate", isPresented: $viewModel.certificateImportFailed) { - Button("Try Again") { - showPicker = true - } - Button("Cancel", role: .cancel) { - viewModel.cancel() - } - } message: { - Text("The certificate file or password was invalid.") - } .promptBrowseCertificate( - isPresented: $showPrompt, + isPresented: $viewModel.showPrompt, host: viewModel.challengingHost, onContinue: { - showPrompt = false - showPicker = true + viewModel.proceedFromPrompt() }, onCancel: { - showPrompt = false viewModel.cancel() } ) - .sheet(isPresented: $showPicker) { + .sheet(isPresented: $viewModel.showPicker) { DocumentPickerView(contentTypes: [.pfx]) { - viewModel.certificateURL = $0 - showPicker = false - showPassword = true + viewModel.proceed(withCertificateURL: $0) } onCancel: { - showPicker = false viewModel.cancel() } .edgesIgnoringSafeArea(.bottom) .interactiveDismissDisabled() } - .sheet(isPresented: $showPassword) { - EnterPasswordView(password: $viewModel.password) { - showPassword = false - viewModel.signIn() + .sheet(isPresented: $viewModel.showPassword) { + EnterPasswordView() { password in + viewModel.proceed(withPassword: password) } onCancel: { - showPassword = false viewModel.cancel() } .edgesIgnoringSafeArea(.bottom) .interactiveDismissDisabled() } -// .sheet(isPresented: $viewModel.showError) { -// Text("Error importing certificate. The certificate file or password was invalid.") -// } + .alert("Error importing certificate", isPresented: $viewModel.showCertificateImportError) { + Button("Try Again") { + viewModel.proceedFromPrompt() + } + Button("Cancel", role: .cancel) { + viewModel.cancel() + } + } message: { + Text("The certificate file or password was invalid.") + } } } @@ -142,8 +136,9 @@ private extension View { } struct EnterPasswordView: View { - @Binding var password: String - var onContinue: () -> Void + @Environment(\.dismiss) var dismiss + @State var password: String = "" + var onContinue: (String) -> Void var onCancel: () -> Void @FocusState var isPasswordFocused: Bool @@ -152,7 +147,6 @@ struct EnterPasswordView: View { Form { Section { VStack { - //person Text("Please enter a password for the chosen certificate.") .fixedSize(horizontal: false, vertical: true) } @@ -165,7 +159,10 @@ struct EnterPasswordView: View { .focused($isPasswordFocused) .textContentType(.password) .submitLabel(.go) - .onSubmit { onContinue() } + .onSubmit { + dismiss() + onContinue(password) + } } .autocapitalization(.none) .disableAutocorrection(true) @@ -176,10 +173,12 @@ struct EnterPasswordView: View { } .navigationTitle("Certificate") .navigationBarTitleDisplayMode(.inline) - .interactiveDismissDisabled() .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { onCancel() } + Button("Cancel") { + dismiss() + onCancel() + } } } .onAppear { @@ -193,7 +192,8 @@ struct EnterPasswordView: View { private var okButton: some View { Button(action: { - onContinue() + dismiss() + onContinue(password) }, label: { Text("OK") .frame(maxWidth: .infinity, alignment: .center) From 9bd8b92e0e9f713787b0a92595ba79c28b7ddb74 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 10:32:59 -0600 Subject: [PATCH 1074/1722] cleanup --- .../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 2344ed6f0..7942f2d07 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -19,7 +19,7 @@ final private class CertificatePickerViewModel: ObservableObject { let challengingHost: String let challenge: QueuedNetworkChallenge - @Published private(set) var certificateURL: URL? + private var certificateURL: URL? @Published var showPrompt = true @Published var showPicker = false @Published var showPassword = false @@ -49,7 +49,7 @@ final private class CertificatePickerViewModel: ObservableObject { challenge.resume(with: .useCredential(try .certificate(at: certificateURL, password: password))) } catch { // This is required to prevent an "already presenting" error. - await Task.yield() + try? await Task.sleep(nanoseconds: 100_000) showCertificateImportError = true } } From c96f933e14c26f8e65ba2abc880e4a4deeb665ff Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 10:45:24 -0700 Subject: [PATCH 1075/1722] Block duplicate starting point selection --- .../UtilityNetworkTraceViewModel.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 9aec29293..b31527490 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -151,9 +151,16 @@ import SwiftUI ) identifyLayerResults?.forEach { identifyLayerResult in identifyLayerResult.geoElements.forEach { geoElement in + + // Block duplicate starting point selection + guard let feature = geoElement as? ArcGISFeature, + let globalid = feature.attributes["globalid"] as? UUID, + !pendingTrace.startingPoints.contains(where: { startingPoint in + return startingPoint.utilityElement.globalID == globalid + }) else { return } + Task { guard let network = network, - let feature = geoElement as? ArcGISFeature, let geometry = feature.geometry, let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? .renderer? From 4479d3067d04bd6720b1299acf41ba392101c1e5 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 12:28:19 -0600 Subject: [PATCH 1076/1722] doc --- .../CertificatePickerViewModifier.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 7942f2d07..34be54d0e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -14,31 +14,50 @@ import SwiftUI import UniformTypeIdentifiers +/// An object that provides the business logic for the workflow of prompting the user for a +/// certificate and a password. @MainActor final private class CertificatePickerViewModel: ObservableObject { + /// The host that prompted the challenge. let challengingHost: String + /// The challenge that requires a certificate to proceed. let challenge: QueuedNetworkChallenge + /// The URL of the certificate that the user chose. private var certificateURL: URL? + + /// A Boolean value indicating whether to show the prompt. @Published var showPrompt = true + /// A Boolean value indicating whether to show the certificate file picker. @Published var showPicker = false + /// A Boolean value indicating whether to show the password field view. @Published var showPassword = false + /// A Boolean value indicating whether to display the error. @Published var showCertificateImportError = false + /// Creates a certificate picker view model. + /// - Parameter challenge: The challenge that requires a certificate. init(challenge: QueuedNetworkChallenge) { self.challenge = challenge challengingHost = challenge.networkChallenge.host } + /// Proceeds to show the file picker. This should be called after the prompt that notifies the + /// user that a certificate must be selected. func proceedFromPrompt() { showPicker = true } + /// Proceeds to show the user the password form. This should be called after the user selects + /// a certificate. + /// - Parameter url: The URL of the certificate that the user chose. func proceed(withCertificateURL url: URL) { certificateURL = url showPassword = true } + /// Attempts to use the certificate and password to respond to the challenge. + /// - Parameter password: The password for the certificate. func proceed(withPassword password: String) { guard let certificateURL = certificateURL else { preconditionFailure() @@ -55,16 +74,21 @@ final private class CertificatePickerViewModel: ObservableObject { } } + /// Cancels the challenge. func cancel() { challenge.resume(with: .cancelAuthenticationChallenge) } } +/// A view modifier that presents a certificate picker workflow. struct CertificatePickerViewModifier: ViewModifier { + /// Creates a certificate picker view modifier. + /// - Parameter challenge: The challenge that requires a certificate. init(challenge: QueuedNetworkChallenge) { viewModel = CertificatePickerViewModel(challenge: challenge) } + /// The view model. @ObservedObject private var viewModel: CertificatePickerViewModel func body(content: Content) -> some View { @@ -111,11 +135,13 @@ struct CertificatePickerViewModifier: ViewModifier { } private extension UTType { + /// A `UTType` that represents a pfx file. static let pfx = UTType(filenameExtension: "pfx")! } private extension View { @ViewBuilder + /// Displays a prompt to the user to let them know that picking a certificate is required. func promptBrowseCertificate( isPresented: Binding, host: String, From 7d25d2a3952d640900f53503abf75b3d37c4eb4b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 11:53:09 -0700 Subject: [PATCH 1077/1722] Add warning --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 9 +++++++++ .../UtilityNetworkTraceViewModel.swift | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 41c2217f3..3b2b53f50 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -429,6 +429,15 @@ public struct UtilityNetworkTrace: View { ) } } + .alert( + "Warning", + isPresented: Binding( + get: { !viewModel.userWarning.isEmpty }, + set: { _ in viewModel.userWarning = "" } + ) + ) { } message: { + Text(viewModel.userWarning) + } } // MARK: Computed Properties diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index b31527490..6bad0f162 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -45,6 +45,9 @@ import SwiftUI } } + /// Warning message presented to the user + @Published var userWarning = "" + /// A Boolean value indicating if the pending trace is configured to the point that it can be run. var canRunTrace: Bool { pendingTrace.configuration != nil && !pendingTrace.startingPoints.isEmpty @@ -157,7 +160,10 @@ import SwiftUI let globalid = feature.attributes["globalid"] as? UUID, !pendingTrace.startingPoints.contains(where: { startingPoint in return startingPoint.utilityElement.globalID == globalid - }) else { return } + }) else { + userWarning = "Duplicate starting points cannot be added " + return + } Task { guard let network = network, From 2190d17cbb9e0708790b3f8fe00f4b783d4a8990 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 13:16:49 -0600 Subject: [PATCH 1078/1722] cleanup --- .../CertificatePickerViewModifier.swift | 124 ++++++++++++------ 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 34be54d0e..3185d8dc5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -95,42 +95,20 @@ struct CertificatePickerViewModifier: ViewModifier { content .promptBrowseCertificate( isPresented: $viewModel.showPrompt, - host: viewModel.challengingHost, - onContinue: { - viewModel.proceedFromPrompt() - }, onCancel: { - viewModel.cancel() - } + viewModel: viewModel + ) + .certificateFilePicker( + isPresented: $viewModel.showPicker, + viewModel: viewModel + ) + .passwordSheet( + isPresented: $viewModel.showPassword, + viewModel: viewModel + ) + .alertCertificateImportError( + isPresented: $viewModel.showCertificateImportError, + viewModel: viewModel ) - .sheet(isPresented: $viewModel.showPicker) { - DocumentPickerView(contentTypes: [.pfx]) { - viewModel.proceed(withCertificateURL: $0) - } onCancel: { - viewModel.cancel() - } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() - } - .sheet(isPresented: $viewModel.showPassword) { - EnterPasswordView() { password in - viewModel.proceed(withPassword: password) - } onCancel: { - viewModel.cancel() - } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() - } - .alert("Error importing certificate", isPresented: $viewModel.showCertificateImportError) { - Button("Try Again") { - viewModel.proceedFromPrompt() - } - Button("Cancel", role: .cancel) { - viewModel.cancel() - } - } message: { - Text("The certificate file or password was invalid.") - } - } } @@ -140,23 +118,83 @@ private extension UTType { } private extension View { - @ViewBuilder /// Displays a prompt to the user to let them know that picking a certificate is required. + @MainActor + @ViewBuilder func promptBrowseCertificate( isPresented: Binding, - host: String, - onContinue: @escaping () -> Void, - onCancel: @escaping () -> Void + viewModel: CertificatePickerViewModel ) -> some View { - alert("Certificate Required", isPresented: isPresented, presenting: host) { _ in + alert("Certificate Required", isPresented: isPresented, presenting: viewModel.challengingHost) { _ in Button("Browse For Certificate") { - onContinue() + viewModel.proceedFromPrompt() } Button("Cancel", role: .cancel) { - onCancel() + viewModel.cancel() } } message: { _ in - Text("A certificate is required to access content on \(host).") + Text("A certificate is required to access content on \(viewModel.challengingHost).") + } + } +} + +private extension View { + /// Displays a sheet that allows the user to select a certificate file. + @MainActor + @ViewBuilder + func certificateFilePicker( + isPresented: Binding, + viewModel: CertificatePickerViewModel + ) -> some View { + sheet(isPresented: isPresented) { + DocumentPickerView(contentTypes: [.pfx]) { + viewModel.proceed(withCertificateURL: $0) + } onCancel: { + viewModel.cancel() + } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() + } + } +} + +private extension View { + /// Displays a sheet that allows the user to enter a password. + @MainActor + @ViewBuilder + func passwordSheet( + isPresented: Binding, + viewModel: CertificatePickerViewModel + ) -> some View { + sheet(isPresented: isPresented) { + EnterPasswordView() { password in + viewModel.proceed(withPassword: password) + } onCancel: { + viewModel.cancel() + } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() + } + } +} + +private extension View { + /// Displays an alert to notify that there was an error importing the certificate. + @MainActor + @ViewBuilder + func alertCertificateImportError( + isPresented: Binding, + viewModel: CertificatePickerViewModel + ) -> some View { + alert("Error importing certificate", isPresented: isPresented) { + Button("Try Again") { + viewModel.proceedFromPrompt() + } + Button("Cancel", role: .cancel) { + viewModel.cancel() + } + } message: { + Text("The certificate file or password was invalid.") } } } From 5d94efe57790bcc58d05114b463a0ed5e6ff4850 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 12:39:42 -0700 Subject: [PATCH 1079/1722] Crash fix --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 3b2b53f50..41d592d3c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -294,6 +294,7 @@ public struct UtilityNetworkTrace: View { })) } } else if selectedStartingPoint?.utilityElement.networkSource.kind == .junction && + selectedStartingPoint?.utilityElement.terminal != nil && !(selectedStartingPoint?.utilityElement.assetType.terminalConfiguration?.terminals.isEmpty ?? true) { Section { Picker( From 788db7873fce8500bb5f197f528ce6c1085bd03d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 13:52:32 -0600 Subject: [PATCH 1080/1722] doc --- .../CertificatePickerViewModifier.swift | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 3185d8dc5..d6205fa76 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -119,6 +119,9 @@ private extension UTType { private extension View { /// Displays a prompt to the user to let them know that picking a certificate is required. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. @MainActor @ViewBuilder func promptBrowseCertificate( @@ -140,6 +143,9 @@ private extension View { private extension View { /// Displays a sheet that allows the user to select a certificate file. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. @MainActor @ViewBuilder func certificateFilePicker( @@ -160,6 +166,9 @@ private extension View { private extension View { /// Displays a sheet that allows the user to enter a password. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. @MainActor @ViewBuilder func passwordSheet( @@ -180,6 +189,9 @@ private extension View { private extension View { /// Displays an alert to notify that there was an error importing the certificate. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. @MainActor @ViewBuilder func alertCertificateImportError( @@ -199,11 +211,20 @@ private extension View { } } +/// A view that allows the user to enter a password. struct EnterPasswordView: View { - @Environment(\.dismiss) var dismiss + @Environment(\.dismiss) var dismissAction + + /// The password that the user entered. @State var password: String = "" + + /// The action to call once the user has completed entering the password. var onContinue: (String) -> Void + + /// The action to call if the user cancels. var onCancel: () -> Void + + /// A Boolean value indicating whether the password field has focus. @FocusState var isPasswordFocused: Bool var body: some View { @@ -224,7 +245,7 @@ struct EnterPasswordView: View { .textContentType(.password) .submitLabel(.go) .onSubmit { - dismiss() + dismissAction() onContinue(password) } } @@ -240,7 +261,7 @@ struct EnterPasswordView: View { .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - dismiss() + dismissAction() onCancel() } } @@ -254,9 +275,10 @@ struct EnterPasswordView: View { } } + /// The "OK" button. private var okButton: some View { Button(action: { - dismiss() + dismissAction() onContinue(password) }, label: { Text("OK") From 6facfbeaddc9c30e464564630d6bfdef6457c2f0 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 13:56:16 -0600 Subject: [PATCH 1081/1722] response -> disposition --- .../Components/Authentication/Authenticator.swift | 2 +- .../Components/Authentication/QueuedChallenge.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 086c31427..c3fc43906 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -149,6 +149,6 @@ extension Authenticator: AuthenticationChallengeHandler { subject.send(queuedChallenge) // Respond accordingly. - return await queuedChallenge.response + return await queuedChallenge.disposition } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 75b84bbe1..78e5b8ec3 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -66,24 +66,24 @@ final class QueuedNetworkChallenge: QueuedChallenge { } func resume(with response: NetworkAuthenticationChallengeDisposition) { - guard _response == nil else { return } - _response = response + guard _disposition == nil else { return } + _disposition = response } /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. - @Streamed private var _response: (NetworkAuthenticationChallengeDisposition)? + @Streamed private var _disposition: (NetworkAuthenticationChallengeDisposition)? - var response: NetworkAuthenticationChallengeDisposition { + var disposition: NetworkAuthenticationChallengeDisposition { get async { - await $_response + await $_disposition .compactMap({ $0 }) .first(where: { _ in true })! } } public func complete() async { - _ = await response + _ = await disposition } enum Kind { From 6a21e69a8401265d794a30bf86c415d6d2308d67 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 13:20:25 -0700 Subject: [PATCH 1082/1722] On trace error, display it and don't go to results --- .../UtilityNetworkTrace.swift | 8 +- .../UtilityNetworkTraceViewModel.swift | 135 ++++++++++-------- 2 files changed, 79 insertions(+), 64 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 41d592d3c..96a4dc286 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -186,8 +186,12 @@ public struct UtilityNetworkTrace: View { } } Button { - viewModel.trace() - currentActivity = .viewingTraces + Task { + let traceSuccess = await viewModel.trace() + if traceSuccess { + currentActivity = .viewingTraces + } + } } label: { Text("Trace") } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 6bad0f162..1e1649f97 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -232,79 +232,90 @@ import SwiftUI } /// Runs the pending trace and stores it into the list of completed traces. - func trace() { + /// - Returns: A Boolean value indicating whether the trace was successful or not. + func trace() async -> Bool { guard let config = pendingTrace.configuration, - let network = network else { return } + let network = network else { return false } let params = UtilityTraceParameters( namedTraceConfiguration: config, startingLocations: pendingTrace.startingPoints.compactMap{ $0.utilityElement } ) - Task { - let traceResults = try await network.trace(traceParameters: params) - var assetGroups = [String: Int]() - for result in traceResults { - switch result { - case let result as UtilityElementTraceResult: - result.elements.forEach({ element in - let count = assetGroups[element.assetGroup.name] ?? 0 + 1 - assetGroups.updateValue(count, forKey: element.assetGroup.name) - }) - assetGroups.forEach { (key, value) in - pendingTrace.assetLabels.append("\(key): \(value)") - } - pendingTrace.utilityElementTraceResult = result - case let result as UtilityGeometryTraceResult: - if let polygon = result.polygon { - let graphic = Graphic( - geometry: polygon, - symbol: SimpleLineSymbol( - style: .solid, - color: UIColor(pendingTrace.color), - width: 5.0 - ) + let traceResults: [UtilityTraceResult] + do { + traceResults = try await network.trace(traceParameters: params) + } catch(let serviceError as ServiceError) { + if let reason = serviceError.failureReason { + userWarning = reason + } + return false + } catch { + userWarning = "An unknown error occurred" + return false + } + var assetGroups = [String: Int]() + for result in traceResults { + switch result { + case let result as UtilityElementTraceResult: + result.elements.forEach({ element in + let count = assetGroups[element.assetGroup.name] ?? 0 + 1 + assetGroups.updateValue(count, forKey: element.assetGroup.name) + }) + assetGroups.forEach { (key, value) in + pendingTrace.assetLabels.append("\(key): \(value)") + } + pendingTrace.utilityElementTraceResult = result + case let result as UtilityGeometryTraceResult: + if let polygon = result.polygon { + let graphic = Graphic( + geometry: polygon, + symbol: SimpleLineSymbol( + style: .solid, + color: UIColor(pendingTrace.color), + width: 5.0 ) - graphicsOverlay.addGraphic(graphic) - pendingTrace.graphics.append(graphic) - } - if let polyline = result.polyline { - let graphic = Graphic( - geometry: polyline, - symbol: SimpleLineSymbol( - style: .dash, - color: UIColor(pendingTrace.color), - width: 5.0 - ) + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.graphics.append(graphic) + } + if let polyline = result.polyline { + let graphic = Graphic( + geometry: polyline, + symbol: SimpleLineSymbol( + style: .dash, + color: UIColor(pendingTrace.color), + width: 5.0 ) - graphicsOverlay.addGraphic(graphic) - pendingTrace.graphics.append(graphic) - } - if let multipoint = result.multipoint { - let graphic = Graphic( - geometry: multipoint, - symbol: SimpleLineSymbol( - style: .dot, - color: UIColor(pendingTrace.color), - width: 5.0 - ) + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.graphics.append(graphic) + } + if let multipoint = result.multipoint { + let graphic = Graphic( + geometry: multipoint, + symbol: SimpleLineSymbol( + style: .dot, + color: UIColor(pendingTrace.color), + width: 5.0 ) - graphicsOverlay.addGraphic(graphic) - pendingTrace.graphics.append(graphic) - } - pendingTrace.utilityGeometryTraceResult = result - case let result as UtilityFunctionTraceResult: - let functionOutputs = result.functionOutputs - functionOutputs.forEach { functionOutput in - pendingTrace.functionOutputs.append(functionOutput) - } - pendingTrace.utilityFunctionTraceResult = result - default: - break + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.graphics.append(graphic) + } + pendingTrace.utilityGeometryTraceResult = result + case let result as UtilityFunctionTraceResult: + let functionOutputs = result.functionOutputs + functionOutputs.forEach { functionOutput in + pendingTrace.functionOutputs.append(functionOutput) } + pendingTrace.utilityFunctionTraceResult = result + default: + break } - completedTraces.append(pendingTrace) - selectedTraceIndex = completedTraces.count - 1 - pendingTrace = Trace() } + completedTraces.append(pendingTrace) + selectedTraceIndex = completedTraces.count - 1 + pendingTrace = Trace() + return true } // MARK: Private Items From df269bd1aa81967874263c5e296b2d14965379d3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 13:37:03 -0700 Subject: [PATCH 1083/1722] Update FloorFilterViewModelTests.swift --- Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift index 98c955b47..bfe747e97 100644 --- a/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift @@ -102,7 +102,7 @@ final class FloorFilterViewModelTests: XCTestCase { XCTAssertNil(viewModel.selectedSite) } - /// Tests that a `FloorFilterViewModel` succesfully initializes. + /// Tests that a `FloorFilterViewModel` successfully initializes. func testInitWithFloorManagerAndViewpoint() async throws { let floorManager = try await floorManager( forWebMapWithIdentifier: .testMap From 2814ef2f413c5a2f4d09be810ec2d08124bf124a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 13:54:13 -0700 Subject: [PATCH 1084/1722] Fix sample code --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index b06e5276f..fb7ea2178 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -47,6 +47,9 @@ struct UtilityNetworkTraceExampleView: View { self.pointInMap = pointInMap self.mapViewProxy = mapViewProxy } + .onViewpointChanged(kind: .centerAndScale) { + viewpoint = $0 + } .overlay(alignment: .topTrailing) { FloatingPanel { UtilityNetworkTrace( From 585e7cff254216f5250271b53def5d170f08b6e7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 15:09:38 -0600 Subject: [PATCH 1085/1722] doc --- .../Authentication/QueuedChallenge.swift | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 78e5b8ec3..66b6d5214 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -14,13 +14,19 @@ import Foundation import ArcGIS +/// A type that represents a challenge in the queue of authentication challenges. protocol QueuedChallenge: AnyObject { + /// Waits for the challenge to complete. func complete() async } +/// An object that represents an ArcGIS authentication challenge in the queue of challenges. final class QueuedArcGISChallenge: QueuedChallenge { + /// The ArcGIS authentication challenge. let arcGISChallenge: ArcGISAuthenticationChallenge + /// Creates a `QueuedArcGISChallenge`. + /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. init(arcGISChallenge: ArcGISAuthenticationChallenge) { self.arcGISChallenge = arcGISChallenge } @@ -58,22 +64,29 @@ final class QueuedArcGISChallenge: QueuedChallenge { } } +/// An object that represents a network authentication challenge in the queue of challenges. final class QueuedNetworkChallenge: QueuedChallenge { + /// The associated network authentication challenge. let networkChallenge: NetworkAuthenticationChallenge + /// Creates a `QueuedNetworkChallenge`. + /// - Parameter networkChallenge: The associated network authentication challenge. init(networkChallenge: NetworkAuthenticationChallenge) { self.networkChallenge = networkChallenge } - - func resume(with response: NetworkAuthenticationChallengeDisposition) { + + /// Resumes the queued challenge. + /// - Parameter disposition: The disposition to resume with. + func resume(with disposition: NetworkAuthenticationChallengeDisposition) { guard _disposition == nil else { return } - _disposition = response + _disposition = disposition } /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. @Streamed private var _disposition: (NetworkAuthenticationChallengeDisposition)? + /// The resulting disposition of the challenge. var disposition: NetworkAuthenticationChallengeDisposition { get async { await $_disposition @@ -86,12 +99,17 @@ final class QueuedNetworkChallenge: QueuedChallenge { _ = await disposition } + /// An enumeration that describes the kind of challenge. enum Kind { + /// A challenge for an untrusted host. case serverTrust + /// A challenge that requires a login via username and password. case login + /// A challenge that requires a client certificate. case certificate } + /// The kind of challenge. var kind: Kind { switch networkChallenge.kind { case .serverTrust: From eb68333511af05bde8d80aa31d15fe415f67a794 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 15:27:19 -0600 Subject: [PATCH 1086/1722] use result instead of wrapping disposition into a Response --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/Authenticator.swift | 15 +++++------- .../Authentication/QueuedChallenge.swift | 24 +++++++------------ .../UsernamePasswordViewModifier.swift | 10 +++++++- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index f89e2c871..4a4ff9d1f 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -114,7 +114,7 @@ private struct AuthenticationItemView: View { Text("Tap to load") .foregroundColor(.secondary) case .failed: - Text("Failed to laod") + Text("Failed to load") .foregroundColor(.red) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index c3fc43906..19bd0b48c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -86,7 +86,11 @@ public final class Authenticator: ObservableObject { let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { // For an OAuth challenge, we create the credential and resume. // Creating the OAuth credential will present the OAuth login view. - queuedArcGISChallenge.resume(with: .oAuth(configuration: config)) + queuedArcGISChallenge.resume( + with: await Result { + .useCredential(try await .oauth(configuration: config)) + } + ) } else { // Set the current challenge, this should present the appropriate view. currentChallenge = queuedChallenge @@ -125,14 +129,7 @@ extension Authenticator: AuthenticationChallengeHandler { subject.send(queuedChallenge) // Wait for it to complete and return the resulting disposition. - switch await queuedChallenge.response { - case .tokenCredential(let username, let password): - return try await .useCredential(.token(challenge: challenge, username: username, password: password)) - case .oAuth(let configuration): - return try await .useCredential(.oauth(configuration: configuration)) - case .cancel: - return .cancelAuthenticationChallenge - } + return try await queuedChallenge.result.get() } public func handleNetworkChallenge( diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 66b6d5214..0b404d685 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -31,36 +31,30 @@ final class QueuedArcGISChallenge: QueuedChallenge { self.arcGISChallenge = arcGISChallenge } - func resume(with response: Response) { - guard _response == nil else { return } - _response = response + func resume(with result: Result) { + guard _result == nil else { return } + _result = result } func cancel() { - guard _response == nil else { return } - _response = .cancel + guard _result == nil else { return } + _result = .success(.cancelAuthenticationChallenge) } /// Use a streamed property because we need to support multiple listeners /// to know when the challenge completed. - @Streamed private var _response: Response? + @Streamed private var _result: Result? - var response: Response { + var result: Result { get async { - await $_response + await $_result .compactMap({ $0 }) .first(where: { _ in true })! } } public func complete() async { - _ = await response - } - - enum Response { - case tokenCredential(username: String, password: String) - case oAuth(configuration: OAuthConfiguration) - case cancel + _ = await result } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 375ee266d..5253f01b5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -211,7 +211,15 @@ class TokenCredentialViewModel: UsernamePasswordViewModel { func signIn() { formEnabled = false - challenge.resume(with: .tokenCredential(username: username, password: password)) + Task { + challenge.resume( + with: await Result { + .useCredential( + try await .token(challenge: challenge.arcGISChallenge, username: username, password: password) + ) + } + ) + } } func cancel() { From 067a3807134f5ae0048214bf20bc5a946609f79a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 15:30:16 -0600 Subject: [PATCH 1087/1722] doc --- .../Components/Authentication/QueuedChallenge.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 0b404d685..ad6c0574f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -30,12 +30,15 @@ final class QueuedArcGISChallenge: QueuedChallenge { init(arcGISChallenge: ArcGISAuthenticationChallenge) { self.arcGISChallenge = arcGISChallenge } - + + /// Resumes the challenge with a result. + /// - Parameter result: The result of the challenge. func resume(with result: Result) { guard _result == nil else { return } _result = result } + /// Cancels the challenge. func cancel() { guard _result == nil else { return } _result = .success(.cancelAuthenticationChallenge) @@ -45,6 +48,7 @@ final class QueuedArcGISChallenge: QueuedChallenge { /// to know when the challenge completed. @Streamed private var _result: Result? + /// The result of the challenge. var result: Result { get async { await $_result From 2198d7388ed4b05972f23f4ed2d9f0dde2664f24 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 15:45:47 -0600 Subject: [PATCH 1088/1722] doc, use dismiss environment variable. --- .../UsernamePasswordViewModifier.swift | 78 ++++++++----------- 1 file changed, 32 insertions(+), 46 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 5253f01b5..4b2792256 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -14,45 +14,64 @@ import SwiftUI import ArcGIS +/// A type that provides the business logic for a view that prompts the user to login with a +/// username and password. protocol UsernamePasswordViewModel: ObservableObject { + /// The username. var username: String { get set } + /// The password. var password: String { get set } + /// A Boolean value indicating if the sign-in button is enabled. var signinButtonEnabled: Bool { get } + /// A Boolean value indicating if the form is enabled. var formEnabled: Bool { get } + /// The host that initiated the challenge. var challengingHost: String { get } + /// Attempts to log in with a username and password. func signIn() + /// Cancels the challenge. func cancel() } +/// A view modifier that prompts a user to login with a username and password. struct UsernamePasswordViewModifier: ViewModifier { + /// The view model. let viewModel: ViewModel + /// Creates a `UsernamePasswordViewModifier` with a queued network challenge. init(challenge: QueuedNetworkChallenge) where ViewModel == NetworkCredentialUsernamePasswordViewModel { viewModel = NetworkCredentialUsernamePasswordViewModel(challenge: challenge) } - init(challenge: QueuedArcGISChallenge) where ViewModel == TokenCredentialViewModel { - viewModel = TokenCredentialViewModel(challenge: challenge) + /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. + init(challenge: QueuedArcGISChallenge) where ViewModel == TokenCredentialUsernamePasswordViewModel { + viewModel = TokenCredentialUsernamePasswordViewModel(challenge: challenge) } + /// A Boolean value indicating whether or not the prompt to login is displayed. @State var isPresented = false func body(content: Content) -> some View { content .task { isPresented = true } - .sheet(isPresented: $isPresented) { UsernamePasswordView(viewModel: viewModel, isPresented: $isPresented) } + .sheet(isPresented: $isPresented) { + UsernamePasswordView(viewModel: viewModel) + } } } +/// A view that prompts a user to login with a username and password. private struct UsernamePasswordView: View { - init(viewModel: ViewModel, isPresented: Binding) { + /// Creates the view. + /// - Parameters: + /// - viewModel: The view model. + init(viewModel: ViewModel) { _viewModel = ObservedObject(initialValue: viewModel) - self.isPresented = isPresented } + @Environment(\.dismiss) var dismissAction @ObservedObject private var viewModel: ViewModel - private var isPresented: Binding /// The focused field. @FocusState private var focusedField: Field? @@ -96,7 +115,7 @@ private struct UsernamePasswordView: View .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { - isPresented.wrappedValue = false + dismissAction() viewModel.cancel() } } @@ -124,7 +143,7 @@ private struct UsernamePasswordView: View private var signinButton: some View { Button(action: { - isPresented.wrappedValue = false + dismissAction() viewModel.signIn() }, label: { if viewModel.formEnabled { @@ -137,55 +156,22 @@ private struct UsernamePasswordView: View .tint(.white) } }) - .disabled(!viewModel.signinButtonEnabled) - .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) - } -} - -struct UsernamePasswordView_Previews: PreviewProvider { - static var previews: some View { - UsernamePasswordView(viewModel: MockUsernamePasswordViewModel(challengingHost: "arcgis.com"), isPresented: .constant(true)) + .disabled(!viewModel.signinButtonEnabled) + .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) } } private extension UsernamePasswordView { /// A type that represents the fields in the user name and password sign-in form. enum Field: Hashable { + /// The username field. case username + /// The password field. case password } } -class MockUsernamePasswordViewModel: UsernamePasswordViewModel { - init(challengingHost: String) { - self.challengingHost = challengingHost - } - - @Published var username = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var password = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var signinButtonEnabled = false - @Published var formEnabled: Bool = true - - private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty - } - - let challengingHost: String - - func signIn() { - formEnabled = false - } - - func cancel() { - formEnabled = false - } -} - -class TokenCredentialViewModel: UsernamePasswordViewModel { +class TokenCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { private let challenge: QueuedArcGISChallenge init(challenge: QueuedArcGISChallenge) { From 0b77fce79b0847edfa11542d2bee61583811658c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 5 Jul 2022 15:50:30 -0600 Subject: [PATCH 1089/1722] doc --- .../UsernamePasswordViewModifier.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 4b2792256..a288e0d72 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -71,6 +71,8 @@ private struct UsernamePasswordView: View } @Environment(\.dismiss) var dismissAction + + /// The view model. @ObservedObject private var viewModel: ViewModel /// The focused field. @@ -129,6 +131,7 @@ private struct UsernamePasswordView: View } } + /// An image used in the form. private var person: some View { Image(systemName: "person.circle") .resizable() @@ -141,6 +144,7 @@ private struct UsernamePasswordView: View ) } + /// The sign-in button. private var signinButton: some View { Button(action: { dismissAction() @@ -171,9 +175,13 @@ private extension UsernamePasswordView { } } +/// A view model that has the business logic for handling a token challenge. class TokenCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { + /// The associated challenge. private let challenge: QueuedArcGISChallenge + /// Creates a `TokenCredentialUsernamePasswordViewModel`. + /// - Parameter challenge: The associated challenge. init(challenge: QueuedArcGISChallenge) { self.challenge = challenge } @@ -214,9 +222,13 @@ class TokenCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { } } +/// A view model that has the business logic for handling a network challenge. class NetworkCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { + /// The associated challenge. private let challenge: QueuedNetworkChallenge + /// Creates a `NetworkCredentialUsernamePasswordViewModel`. + /// - Parameter challenge: The associated challenge. init(challenge: QueuedNetworkChallenge) { self.challenge = challenge } From c41f6d8c0c29aac3ddaa62fb2472dda2dce306af Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 14:58:34 -0700 Subject: [PATCH 1090/1722] Update completed trace color Update UtilityNetworkTraceViewModel.swift Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace.swift | 16 ++++++++++++++++ .../UtilityNetworkTraceViewModel.swift | 9 +++++++++ .../UtilityNetworkTraceViewModelTrace.swift | 11 +++++++++++ 3 files changed, 36 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 96a4dc286..69b579744 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -245,6 +245,22 @@ public struct UtilityNetworkTrace: View { } } } + Section { + DisclosureGroup("Advanced Options") { + ColorPicker( + selection: Binding(get: { + viewModel.selectedTrace?.color ?? Color.clear + }, set: { newValue in + if var trace = viewModel.selectedTrace { + trace.color = newValue + viewModel.update(completedTrace: trace) + } + }) + ) { + Text("Trace Color") + } + } + } } Button { warningIsPresented.toggle() diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 1e1649f97..3ae6aae1d 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -318,6 +318,15 @@ import SwiftUI return true } + /// Updates the matching completed trace. + /// - Parameter newValue: The new completed trace. + func update(completedTrace newValue: Trace) { + guard let traceIndex = completedTraces.firstIndex( where: { trace in + trace == newValue + }) else { return } + completedTraces[traceIndex] = newValue + } + // MARK: Private Items /// Changes the selected state of the graphics for the completed trace at the provided index. diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index b6e00ed7a..f5527dae5 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -28,6 +28,10 @@ extension UtilityNetworkTraceViewModel { guard let symbol = startingPoint.graphic.symbol as? SimpleMarkerSymbol else { return } symbol.color = UIColor(color) } + graphics.forEach { graphic in + guard let symbol = graphic.symbol as? SimpleLineSymbol else { return } + symbol.color = UIColor(color) + } } } @@ -40,6 +44,9 @@ extension UtilityNetworkTraceViewModel { /// A collection of graphics generated from the trace. var graphics = [Graphic]() + /// A unique identifier for the trace. + let id = UUID() + /// A user given name for the trace. var name: String = "" @@ -58,5 +65,9 @@ extension UtilityNetworkTraceViewModel { /// The trace result comprised of a set of Geometry objects representing the network elements /// identified by the trace. var utilityGeometryTraceResult: UtilityGeometryTraceResult? + + static func == (lhs: UtilityNetworkTraceViewModel.Trace, rhs: UtilityNetworkTraceViewModel.Trace) -> Bool { + return lhs.id == rhs.id + } } } From d401aa1cc55349a82a74f4b0c56cc26e44a826c9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 16:26:45 -0700 Subject: [PATCH 1091/1722] Enforce minimum starting locations --- .../UtilityNetworkTraceViewModel.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 3ae6aae1d..cccdf9e3a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -236,6 +236,14 @@ import SwiftUI func trace() async -> Bool { guard let config = pendingTrace.configuration, let network = network else { return false } + + let minStartingPoints = config.minimumStartingLocations.rawValue + + guard pendingTrace.startingPoints.count >= minStartingPoints else { + userWarning = "Please set at least \(minStartingPoints) starting location\(minStartingPoints > 1 ? "s" : "")." + return false + } + let params = UtilityTraceParameters( namedTraceConfiguration: config, startingLocations: pendingTrace.startingPoints.compactMap{ $0.utilityElement } From a1455422f6f5869b274a67aaab7790e9b731f83d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 17:00:06 -0700 Subject: [PATCH 1092/1722] Manage focus in the results tab --- .../UtilityNetworkTrace.swift | 80 +++++++++++++------ 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 69b579744..106bfcf9d 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -43,12 +43,22 @@ public struct UtilityNetworkTrace: View { case viewingTraceConfigurations } + /// Activities users will perform while viewing completed traces. + private enum TraceViewingActivity: Hashable { + /// The user is viewing the list of available trace options. + case viewingAdvancedOptions + /// The user is viewing the list of element results. + case viewingElementResults + /// The user is viewing the list of function results. + case viewingFunctionResults + } + /// Activities users will perform while using the Utility Network Trace tool. private enum UserActivity: Hashable { /// The user is creating a new trace. case creatingTrace(TraceCreationActivity?) /// The user is viewing traces that have been created. - case viewingTraces + case viewingTraces(TraceViewingActivity?) } // MARK: States @@ -92,7 +102,7 @@ public struct UtilityNetworkTrace: View { case .creatingTrace(_): return UserActivity.creatingTrace(nil) case .viewingTraces: - return UserActivity.viewingTraces + return UserActivity.viewingTraces(nil) } }, set: { newActivity, _ in currentActivity = newActivity @@ -100,7 +110,7 @@ public struct UtilityNetworkTrace: View { ) ) { Text("New trace").tag(UserActivity.creatingTrace(nil)) - Text("Results").tag(UserActivity.viewingTraces) + Text("Results").tag(UserActivity.viewingTraces(nil)) } .pickerStyle(.segmented) .padding() @@ -189,7 +199,7 @@ public struct UtilityNetworkTrace: View { Task { let traceSuccess = await viewModel.trace() if traceSuccess { - currentActivity = .viewingTraces + currentActivity = .viewingTraces(nil) } } } label: { @@ -222,31 +232,43 @@ public struct UtilityNetworkTrace: View { } List { Section("Element Result") { - DisclosureGroup(viewModel - .selectedTrace? - .utilityElementTraceResult? - .elements.count.description ?? "0") { - ForEach(viewModel.selectedTrace?.assetLabels ?? [], id: \.self) { label in - Text(label) - } + DisclosureGroup( + viewModel.selectedTrace?.utilityElementTraceResult?.elements.count.description ?? "0", + isExpanded: Binding( + get: { isFocused(traceViewingActivity: .viewingElementResults) }, + set: { _ in currentActivity = .viewingTraces(.viewingElementResults) } + ) + ) { + ForEach(viewModel.selectedTrace?.assetLabels ?? [], id: \.self) { label in + Text(label) } + } } Section("Function Result") { - DisclosureGroup(viewModel - .selectedTrace? - .utilityFunctionTraceResult? - .functionOutputs.count.description ?? "0") { - ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.id) { item in - HStack { - Text(item.function.networkAttribute?.name ?? "Unnamed") - Spacer() - Text((item.result as? Double)?.description ?? "N/A") - } + DisclosureGroup( + viewModel.selectedTrace?.utilityFunctionTraceResult?.functionOutputs.count.description ?? "0", + isExpanded: Binding( + get: { isFocused(traceViewingActivity: .viewingFunctionResults) }, + set: { _ in currentActivity = .viewingTraces(.viewingFunctionResults) } + ) + ) { + ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.id) { item in + HStack { + Text(item.function.networkAttribute?.name ?? "Unnamed") + Spacer() + Text((item.result as? Double)?.description ?? "N/A") } } + } } Section { - DisclosureGroup("Advanced Options") { + DisclosureGroup( + "Advanced Options", + isExpanded: Binding( + get: { isFocused(traceViewingActivity: .viewingAdvancedOptions) }, + set: { _ in currentActivity = .viewingTraces(.viewingAdvancedOptions) } + ) + ) { ColorPicker( selection: Binding(get: { viewModel.selectedTrace?.color ?? Color.clear @@ -461,8 +483,6 @@ public struct UtilityNetworkTrace: View { } } - // MARK: Computed Properties - /// Indicates if the list of advanced options is expanded. private var advancedOptionsIsExpanded: Binding { Binding(get: { @@ -567,4 +587,16 @@ public struct UtilityNetworkTrace: View { return nil } } + + /// Determines if the provided viewing activity is the currently focused viewing activity. + /// - Parameter traceViewingActivity: A possible focus activity when viewing traces. + /// - Returns: A Boolean value indicating whether the provided activity is the currently focused + /// viewing activity. + private func isFocused(traceViewingActivity: TraceViewingActivity) -> Bool { + switch currentActivity { + case .viewingTraces(let currentActivity): + return traceViewingActivity == currentActivity + default: return false + } + } } From bc36f16ff36c49c478b79a628e16dfdd4fc7f64f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 17:10:58 -0700 Subject: [PATCH 1093/1722] Lessen padding, simplify computed bindings --- .../UtilityNetworkTrace.swift | 70 ++++++------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index c6d735755..9523794cd 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -143,7 +143,10 @@ public struct UtilityNetworkTrace: View { Section("Trace Configuration") { DisclosureGroup( viewModel.pendingTrace.configuration?.name ?? "None selected", - isExpanded: configurationOptionsIsExpanded + isExpanded: Binding( + get: { isFocused(traceCreationActivity: .viewingTraceConfigurations) }, + set: { _ in currentActivity = .creatingTrace(.viewingTraceConfigurations) } + ) ) { configurationsList } @@ -156,7 +159,10 @@ public struct UtilityNetworkTrace: View { } DisclosureGroup( "\(viewModel.pendingTrace.startingPoints.count) selected", - isExpanded: startingPointsListIsExpanded + isExpanded: Binding( + get: { isFocused(traceCreationActivity: .viewingStartingPoints) }, + set: { _ in currentActivity = .creatingTrace(.viewingStartingPoints) } + ) ) { startingPointsList } @@ -202,7 +208,7 @@ public struct UtilityNetworkTrace: View { } } .font(.title3) - .padding() + .padding(2.5) if let traceName = viewModel.selectedTrace?.name, !traceName.isEmpty { Text(traceName) } @@ -389,52 +395,6 @@ public struct UtilityNetworkTrace: View { // MARK: Computed Properties - /// Indicates if the list of trace configuration options is expanded. - private var configurationOptionsIsExpanded: Binding { - Binding(get: { - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .viewingTraceConfigurations: - return true - default: - return false - } - default: - return false - } - }, set: { val in - if val { - currentActivity = .creatingTrace(.viewingTraceConfigurations) - } else { - currentActivity = .creatingTrace(nil) - } - }) - } - - /// Indicates if the list of chosen starting points is expanded. - private var startingPointsListIsExpanded: Binding { - Binding(get: { - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .viewingStartingPoints: - return true - default: - return false - } - default: - return false - } - }, set: { val in - if val { - currentActivity = .creatingTrace(.viewingStartingPoints) - } else { - currentActivity = .creatingTrace(nil) - } - }) - } - /// Indicates the number of the trace currently being viewed out the total number of traces. private var currentTraceLabel: String { guard let index = viewModel.selectedTraceIndex else { return "Error" } @@ -470,4 +430,16 @@ public struct UtilityNetworkTrace: View { return nil } } + + /// Determines if the provided creation activity is the currently focused creation activity. + /// - Parameter traceCreationActivity: A possible focus activity when creating traces. + /// - Returns: A Boolean value indicating whether the provided activity is the currently focused + /// creation activity. + private func isFocused(traceCreationActivity: TraceCreationActivity) -> Bool { + switch currentActivity { + case .creatingTrace(let currentActivity): + return traceCreationActivity == currentActivity + default: return false + } + } } From fda6499bf92d2f962961f10a79b4af71347589d4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 5 Jul 2022 17:19:07 -0700 Subject: [PATCH 1094/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace.swift | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 743bdd70d..102c871e0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -184,7 +184,10 @@ public struct UtilityNetworkTrace: View { Section { DisclosureGroup( "Advanced Options", - isExpanded: advancedOptionsIsExpanded + isExpanded: Binding( + get: { isFocused(traceCreationActivity: .viewingAdvancedOptions) }, + set: { _ in currentActivity = .creatingTrace(.viewingAdvancedOptions) } + ) ) { ColorPicker( selection: $viewModel.pendingTrace.color @@ -489,29 +492,6 @@ public struct UtilityNetworkTrace: View { } } - /// Indicates if the list of advanced options is expanded. - private var advancedOptionsIsExpanded: Binding { - Binding(get: { - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .viewingAdvancedOptions: - return true - default: - return false - } - default: - return false - } - }, set: { val in - if val { - currentActivity = .creatingTrace(.viewingAdvancedOptions) - } else { - currentActivity = .creatingTrace(nil) - } - }) - } - /// Indicates the number of the trace currently being viewed out the total number of traces. private var currentTraceLabel: String { guard let index = viewModel.selectedTraceIndex else { return "Error" } From 58e7049e2781a2320feb40dcd5757d570001a6f6 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 6 Jul 2022 09:25:53 -0600 Subject: [PATCH 1095/1722] cleanup --- .../QueuedArcGISChallenge.swift | 57 ++++++++++ .../Authentication/QueuedChallenge.swift | 103 ------------------ .../QueuedNetworkChallenge.swift | 75 +++++++++++++ .../TrustHostViewModifier.swift | 5 +- 4 files changed, 136 insertions(+), 104 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift create mode 100644 Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift new file mode 100644 index 000000000..a915bba82 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift @@ -0,0 +1,57 @@ +// 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 Foundation +import ArcGIS + +/// An object that represents an ArcGIS authentication challenge in the queue of challenges. +final class QueuedArcGISChallenge: QueuedChallenge { + /// The ArcGIS authentication challenge. + let arcGISChallenge: ArcGISAuthenticationChallenge + + /// Creates a `QueuedArcGISChallenge`. + /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. + init(arcGISChallenge: ArcGISAuthenticationChallenge) { + self.arcGISChallenge = arcGISChallenge + } + + /// Resumes the challenge with a result. + /// - Parameter result: The result of the challenge. + func resume(with result: Result) { + guard _result == nil else { return } + _result = result + } + + /// Cancels the challenge. + func cancel() { + guard _result == nil else { return } + _result = .success(.cancelAuthenticationChallenge) + } + + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. + @Streamed private var _result: Result? + + /// The result of the challenge. + var result: Result { + get async { + await $_result + .compactMap({ $0 }) + .first(where: { _ in true })! + } + } + + public func complete() async { + _ = await result + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index ad6c0574f..ddcf3a261 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -12,112 +12,9 @@ // limitations under the License. import Foundation -import ArcGIS /// A type that represents a challenge in the queue of authentication challenges. protocol QueuedChallenge: AnyObject { /// Waits for the challenge to complete. func complete() async } - -/// An object that represents an ArcGIS authentication challenge in the queue of challenges. -final class QueuedArcGISChallenge: QueuedChallenge { - /// The ArcGIS authentication challenge. - let arcGISChallenge: ArcGISAuthenticationChallenge - - /// Creates a `QueuedArcGISChallenge`. - /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. - init(arcGISChallenge: ArcGISAuthenticationChallenge) { - self.arcGISChallenge = arcGISChallenge - } - - /// Resumes the challenge with a result. - /// - Parameter result: The result of the challenge. - func resume(with result: Result) { - guard _result == nil else { return } - _result = result - } - - /// Cancels the challenge. - func cancel() { - guard _result == nil else { return } - _result = .success(.cancelAuthenticationChallenge) - } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed private var _result: Result? - - /// The result of the challenge. - var result: Result { - get async { - await $_result - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await result - } -} - -/// An object that represents a network authentication challenge in the queue of challenges. -final class QueuedNetworkChallenge: QueuedChallenge { - /// The associated network authentication challenge. - let networkChallenge: NetworkAuthenticationChallenge - - /// Creates a `QueuedNetworkChallenge`. - /// - Parameter networkChallenge: The associated network authentication challenge. - init(networkChallenge: NetworkAuthenticationChallenge) { - self.networkChallenge = networkChallenge - } - - /// Resumes the queued challenge. - /// - Parameter disposition: The disposition to resume with. - func resume(with disposition: NetworkAuthenticationChallengeDisposition) { - guard _disposition == nil else { return } - _disposition = disposition - } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed private var _disposition: (NetworkAuthenticationChallengeDisposition)? - - /// The resulting disposition of the challenge. - var disposition: NetworkAuthenticationChallengeDisposition { - get async { - await $_disposition - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await disposition - } - - /// An enumeration that describes the kind of challenge. - enum Kind { - /// A challenge for an untrusted host. - case serverTrust - /// A challenge that requires a login via username and password. - case login - /// A challenge that requires a client certificate. - case certificate - } - - /// The kind of challenge. - var kind: Kind { - switch networkChallenge.kind { - case .serverTrust: - return .serverTrust - case .ntlm, .basic, .digest: - return .login - case .pki: - return .certificate - case .htmlForm, .negotiate: - fatalError("TODO: ") - } - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift new file mode 100644 index 000000000..8b4eeeb22 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -0,0 +1,75 @@ +// 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 Foundation +import ArcGIS + +/// An object that represents a network authentication challenge in the queue of challenges. +final class QueuedNetworkChallenge: QueuedChallenge { + /// The associated network authentication challenge. + let networkChallenge: NetworkAuthenticationChallenge + + /// Creates a `QueuedNetworkChallenge`. + /// - Parameter networkChallenge: The associated network authentication challenge. + init(networkChallenge: NetworkAuthenticationChallenge) { + self.networkChallenge = networkChallenge + } + + /// Resumes the queued challenge. + /// - Parameter disposition: The disposition to resume with. + func resume(with disposition: NetworkAuthenticationChallengeDisposition) { + guard _disposition == nil else { return } + _disposition = disposition + } + + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. + @Streamed private var _disposition: (NetworkAuthenticationChallengeDisposition)? + + /// The resulting disposition of the challenge. + var disposition: NetworkAuthenticationChallengeDisposition { + get async { + await $_disposition + .compactMap({ $0 }) + .first(where: { _ in true })! + } + } + + public func complete() async { + _ = await disposition + } + + /// An enumeration that describes the kind of challenge. + enum Kind { + /// A challenge for an untrusted host. + case serverTrust + /// A challenge that requires a login via username and password. + case login + /// A challenge that requires a client certificate. + case certificate + } + + /// The kind of challenge. + var kind: Kind { + switch networkChallenge.kind { + case .serverTrust: + return .serverTrust + case .ntlm, .basic, .digest: + return .login + case .pki: + return .certificate + case .htmlForm, .negotiate: + fatalError("TODO: ") + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index d567293c7..2ad8521c4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -37,7 +37,10 @@ struct TrustHostViewModifier: ViewModifier { func body(content: Content) -> some View { content - .task { isPresented = true } // Present the alert right away. + .task { + // Present the alert right away. This makes it animated. + isPresented = true + } .alert("Certificate Trust Warning", isPresented: $isPresented, presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { isPresented = false From 5406fc5a3b499014af70b57c5b66eeffad4ff131 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 6 Jul 2022 09:28:02 -0600 Subject: [PATCH 1096/1722] cleanup --- .../Components/Authentication/Authenticator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 19bd0b48c..795b32b11 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -141,11 +141,11 @@ extension Authenticator: AuthenticationChallengeHandler { return .performDefaultHandling } - // Queue up the url challenge. + // Queue up the challenge. let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) subject.send(queuedChallenge) - // Respond accordingly. + // Wait for it to complete and return the resulting disposition. return await queuedChallenge.disposition } } From e91b67449f762d2432a33b4ee3adfcac1cf298be Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 6 Jul 2022 10:58:06 -0700 Subject: [PATCH 1097/1722] Allow all disclosures to be collapsed --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 102c871e0..f230ad7b6 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -157,7 +157,7 @@ public struct UtilityNetworkTrace: View { viewModel.pendingTrace.configuration?.name ?? "None selected", isExpanded: Binding( get: { isFocused(traceCreationActivity: .viewingTraceConfigurations) }, - set: { _ in currentActivity = .creatingTrace(.viewingTraceConfigurations) } + set: { currentActivity = .creatingTrace($0 ? .viewingTraceConfigurations : nil) } ) ) { configurationsList @@ -174,7 +174,7 @@ public struct UtilityNetworkTrace: View { "\(viewModel.pendingTrace.startingPoints.count) selected", isExpanded: Binding( get: { isFocused(traceCreationActivity: .viewingStartingPoints) }, - set: { _ in currentActivity = .creatingTrace(.viewingStartingPoints) } + set: { currentActivity = .creatingTrace($0 ? .viewingStartingPoints : nil) } ) ) { startingPointsList @@ -186,7 +186,7 @@ public struct UtilityNetworkTrace: View { "Advanced Options", isExpanded: Binding( get: { isFocused(traceCreationActivity: .viewingAdvancedOptions) }, - set: { _ in currentActivity = .creatingTrace(.viewingAdvancedOptions) } + set: { currentActivity = .creatingTrace($0 ? .viewingAdvancedOptions : nil) } ) ) { ColorPicker( @@ -245,7 +245,7 @@ public struct UtilityNetworkTrace: View { viewModel.selectedTrace?.utilityElementTraceResult?.elements.count.description ?? "0", isExpanded: Binding( get: { isFocused(traceViewingActivity: .viewingElementResults) }, - set: { _ in currentActivity = .viewingTraces(.viewingElementResults) } + set: { currentActivity = .viewingTraces($0 ? .viewingElementResults : nil) } ) ) { ForEach(viewModel.selectedTrace?.assetLabels ?? [], id: \.self) { label in @@ -258,7 +258,7 @@ public struct UtilityNetworkTrace: View { viewModel.selectedTrace?.utilityFunctionTraceResult?.functionOutputs.count.description ?? "0", isExpanded: Binding( get: { isFocused(traceViewingActivity: .viewingFunctionResults) }, - set: { _ in currentActivity = .viewingTraces(.viewingFunctionResults) } + set: { currentActivity = .viewingTraces($0 ? .viewingFunctionResults : nil) } ) ) { ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.id) { item in @@ -275,7 +275,7 @@ public struct UtilityNetworkTrace: View { "Advanced Options", isExpanded: Binding( get: { isFocused(traceViewingActivity: .viewingAdvancedOptions) }, - set: { _ in currentActivity = .viewingTraces(.viewingAdvancedOptions) } + set: { currentActivity = .viewingTraces($0 ? .viewingAdvancedOptions : nil) } ) ) { ColorPicker( From 4833fb268119e52525733067352f07575f2328f7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 6 Jul 2022 11:43:00 -0700 Subject: [PATCH 1098/1722] Simplify trace() function --- .../UtilityNetworkTraceViewModel.swift | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index cccdf9e3a..0c56273f4 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -234,23 +234,25 @@ import SwiftUI /// Runs the pending trace and stores it into the list of completed traces. /// - Returns: A Boolean value indicating whether the trace was successful or not. func trace() async -> Bool { - guard let config = pendingTrace.configuration, + guard let configuration = pendingTrace.configuration, let network = network else { return false } - let minStartingPoints = config.minimumStartingLocations.rawValue + let minStartingPoints = configuration.minimumStartingLocations.rawValue guard pendingTrace.startingPoints.count >= minStartingPoints else { userWarning = "Please set at least \(minStartingPoints) starting location\(minStartingPoints > 1 ? "s" : "")." return false } - let params = UtilityTraceParameters( - namedTraceConfiguration: config, + let parameters = UtilityTraceParameters( + namedTraceConfiguration: configuration, startingLocations: pendingTrace.startingPoints.compactMap{ $0.utilityElement } ) + let traceResults: [UtilityTraceResult] + do { - traceResults = try await network.trace(traceParameters: params) + traceResults = try await network.trace(traceParameters: parameters) } catch(let serviceError as ServiceError) { if let reason = serviceError.failureReason { userWarning = reason @@ -260,6 +262,7 @@ import SwiftUI userWarning = "An unknown error occurred" return false } + var assetGroups = [String: Int]() for result in traceResults { switch result { @@ -273,46 +276,34 @@ import SwiftUI } pendingTrace.utilityElementTraceResult = result case let result as UtilityGeometryTraceResult: - if let polygon = result.polygon { - let graphic = Graphic( - geometry: polygon, + let createGraphic: ((Geometry, SimpleLineSymbol.Style, Color) -> (Graphic)) = { geometry, style, color in + return Graphic( + geometry: geometry, symbol: SimpleLineSymbol( - style: .solid, - color: UIColor(pendingTrace.color), + style: style, + color: UIColor(color), width: 5.0 ) ) + } + if let polygon = result.polygon { + let graphic = createGraphic(polygon, .solid, pendingTrace.color) graphicsOverlay.addGraphic(graphic) pendingTrace.graphics.append(graphic) } if let polyline = result.polyline { - let graphic = Graphic( - geometry: polyline, - symbol: SimpleLineSymbol( - style: .dash, - color: UIColor(pendingTrace.color), - width: 5.0 - ) - ) + let graphic = createGraphic(polyline, .dash, pendingTrace.color) graphicsOverlay.addGraphic(graphic) pendingTrace.graphics.append(graphic) } if let multipoint = result.multipoint { - let graphic = Graphic( - geometry: multipoint, - symbol: SimpleLineSymbol( - style: .dot, - color: UIColor(pendingTrace.color), - width: 5.0 - ) - ) + let graphic = createGraphic(multipoint, .dot, pendingTrace.color) graphicsOverlay.addGraphic(graphic) pendingTrace.graphics.append(graphic) } pendingTrace.utilityGeometryTraceResult = result case let result as UtilityFunctionTraceResult: - let functionOutputs = result.functionOutputs - functionOutputs.forEach { functionOutput in + result.functionOutputs.forEach { functionOutput in pendingTrace.functionOutputs.append(functionOutput) } pendingTrace.utilityFunctionTraceResult = result From 9f1ae12dcee1b04eb92850f06fc6171252dd9de1 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 6 Jul 2022 13:15:37 -0600 Subject: [PATCH 1099/1722] tests --- .../Authentication/Authenticator.swift | 15 ++++-- .../AuthenticatorTests.swift | 47 +++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/AuthenticatorTests.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 795b32b11..93b137b64 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -50,14 +50,19 @@ public final class Authenticator: ObservableObject { access: ArcGIS.KeychainAccess, isSynchronizable: Bool = false ) async throws { - ArcGISURLSession.credentialStore = try await .makePersistent( + ArcGISCredentialStore.shared = try await .makePersistent( access: access, isSynchronizable: isSynchronizable ) - await NetworkCredentialStore.setShared( - try await .makePersistent(access: access, isSynchronizable: isSynchronizable) - ) + do { + await NetworkCredentialStore.setShared( + try await .makePersistent(access: access, isSynchronizable: isSynchronizable) + ) + } catch { + // If cannot make + throw error + } } /// Clears all ArcGIS and network credentials from the respective stores. @@ -65,7 +70,7 @@ public final class Authenticator: ObservableObject { /// right away. public func clearCredentialStores() async { // Clear ArcGIS Credentials. - await ArcGISURLSession.credentialStore.removeAll() + await ArcGISCredentialStore.shared.removeAll() // Clear network credentials. await NetworkCredentialStore.shared.removeAll() diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift new file mode 100644 index 000000000..b9a14bb2d --- /dev/null +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -0,0 +1,47 @@ +// 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 XCTest +@testable import ArcGISToolkit +import ArcGIS + +class AuthenticatorTests: XCTestCase { + @MainActor + func testInit() { + let config = OAuthConfiguration( + portalURL: URL(string:"www.arcgis.com")!, + clientID: "client id", + redirectURL: URL(string:"myapp://oauth")! + ) + let authenticator = Authenticator(promptForUntrustedHosts: true, oAuthConfigurations: [config]) + XCTAssertTrue(authenticator.promptForUntrustedHosts) + XCTAssertEqual(authenticator.oAuthConfigurations, [config]) + } + + @MainActor + func testMakePersistent() async throws { + // Make sure credential stores are restored. + addTeardownBlock { + ArcGISCredentialStore.shared = ArcGISCredentialStore() + await NetworkCredentialStore.setShared(await NetworkCredentialStore()) + } + + // This tests that calling makePersistent tries to sync with the keychain. + let authenticator = Authenticator() + do { + try await authenticator.makePersistent(access: .whenUnlocked) + XCTFail("Expected an error to be thrown as unit tests should not have access to the keychain") + } catch {} + } +} From 0b4d8f2d5915c70c67020077a27f3a7851b1afca Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 6 Jul 2022 13:27:52 -0600 Subject: [PATCH 1100/1722] test --- .../AuthenticatorTests.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index b9a14bb2d..ad76ee198 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -44,4 +44,28 @@ class AuthenticatorTests: XCTestCase { XCTFail("Expected an error to be thrown as unit tests should not have access to the keychain") } catch {} } + + @MainActor + func testClearCredentialStores() async { + await ArcGISCredentialStore.shared.add( + .staticToken( + url: URL(string: "www.arcgis.com")!, + tokenInfo: .init( + accessToken: "token", + isSSLRequired: false, + expirationDate: .distantFuture + )! + ) + ) + + let authenticator = Authenticator() + + var arcGISCreds = await ArcGISCredentialStore.shared.credentials + XCTAssertEqual(arcGISCreds.count, 1) + + await authenticator.clearCredentialStores() + + arcGISCreds = await ArcGISCredentialStore.shared.credentials + XCTAssertTrue(arcGISCreds.isEmpty) + } } From 2be9fc7da7e58bce2af877a29842c0e686824f13 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 6 Jul 2022 14:35:02 -0700 Subject: [PATCH 1101/1722] Updates --- .../Examples/FloatingPanelExampleView.swift | 14 +++--- .../FloatingPanel/FloatingPanel.swift | 44 +++++++------------ 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 1a96bdf9e..fb9c41af3 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -16,6 +16,9 @@ import ArcGISToolkit import ArcGIS struct FloatingPanelExampleView: View { + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? + @StateObject private var map = Map(basemapStyle: .arcGISImagery) private let initialViewpoint = Viewpoint( @@ -28,12 +31,13 @@ struct FloatingPanelExampleView: View { map: map, viewpoint: initialViewpoint ) - .overlay { - FloatingPanel(alignment: .topLeading) { + .overlay(alignment: .topTrailing) { + FloatingPanel { SampleContent() } - .padding([.leading, .top, .trailing]) - .padding(.bottom, 50) + .padding(horizontalSizeClass == .compact ? [] : [.leading, .top, .trailing]) + .padding(.bottom, horizontalSizeClass == .compact ? 0 : 50) + .frame(width: horizontalSizeClass == .compact ? nil : 360) } } } @@ -41,6 +45,6 @@ struct FloatingPanelExampleView: View { struct SampleContent: View { var body: some View { List(1..<21) { Text("\($0)") } - .listStyle(.plain) + .listStyle(.plain) } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index eb0096e24..fa569da70 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -28,33 +28,24 @@ public struct FloatingPanel: View where Content: View { // to have it be a view modifier, similar to how SwiftUI doesn't have a // SheetView, but a modifier that presents a sheet. + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass: UserInterfaceSizeClass? + /// The content shown in the floating panel. let content: Content /// Creates a `FloatingPanel` - /// - Parameter alignment: Alignment of the floating panel within the parent view. /// - Parameter initialHeight: The initial height given to the content of the floating panel. /// Default is 200. - /// - Parameter width: The width given to the content of the floating panel. Default is 360. /// - Parameter content: The view shown in the floating panel. public init( - alignment: Alignment, initialHeight: CGFloat = 200, - width: CGFloat = 360, @ViewBuilder content: () -> Content ) { - self.alignment = alignment - self.width = width self.content = content() _height = State(initialValue: initialHeight) } - /// Alignment of the floating panel within the parent view. - private let alignment: Alignment - - /// The width given to the content of the floating panel. - private let width: CGFloat - /// The color of the handle. @State private var handleColor: Color = .defaultHandleColor @@ -64,34 +55,33 @@ public struct FloatingPanel: View where Content: View { /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity - /// The vertical alignment of the floating panel with the parent view is `VerticalAlignment.top` - private var isTopAligned: Bool { - return alignment.vertical == .top + /// A Boolean value indicating whether the panel should be configured for a compact environment. + private var isCompact: Bool { + horizontalSizeClass == .compact ? true : false } public var body: some View { GeometryReader { geometryProxy in VStack { - if isTopAligned { - content - .frame(minHeight: .minHeight, maxHeight: height) - Divider() - Handle(color: handleColor) - .gesture(drag) - } else { + if isCompact { Handle(color: handleColor) .gesture(drag) Divider() content .frame(minHeight: .minHeight, maxHeight: height) + } else { + content + .frame(minHeight: .minHeight, maxHeight: height) + Divider() + Handle(color: handleColor) + .gesture(drag) } } - .frame(width: width) .esriBorder() .frame( width: geometryProxy.size.width, height: geometryProxy.size.height, - alignment: alignment + alignment: isCompact ? .bottom : .top ) .onSizeChange { maximumHeight = $0.height @@ -107,15 +97,15 @@ public struct FloatingPanel: View where Content: View { .onChanged { value in handleColor = .activeHandleColor let proposedHeight: CGFloat - if isTopAligned { + if isCompact { proposedHeight = max( .minHeight, - height + value.translation.height + height - value.translation.height ) } else { proposedHeight = max( .minHeight, - height - value.translation.height + height + value.translation.height ) } height = min(proposedHeight, maximumHeight) From 90fe4a2ba42c612bb5e1975cce3dc444a5cd2b2a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 6 Jul 2022 14:40:51 -0700 Subject: [PATCH 1102/1722] Update FloatingPanel.swift --- .../Components/FloatingPanel/FloatingPanel.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index fa569da70..1c7c84c2f 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -35,15 +35,10 @@ public struct FloatingPanel: View where Content: View { let content: Content /// Creates a `FloatingPanel` - /// - Parameter initialHeight: The initial height given to the content of the floating panel. - /// Default is 200. /// - Parameter content: The view shown in the floating panel. - public init( - initialHeight: CGFloat = 200, - @ViewBuilder content: () -> Content - ) { + public init(@ViewBuilder content: () -> Content) { self.content = content() - _height = State(initialValue: initialHeight) + _height = State(initialValue: .infinity) } /// The color of the handle. From 177c4528d354cb2d4441babee3a74e8d65fd1258 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 6 Jul 2022 16:40:45 -0600 Subject: [PATCH 1103/1722] roll back arcgis credential store during failure --- .../Components/Authentication/Authenticator.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 93b137b64..4718c85ac 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -50,6 +50,8 @@ public final class Authenticator: ObservableObject { access: ArcGIS.KeychainAccess, isSynchronizable: Bool = false ) async throws { + let previousArcGISCredentialStore = ArcGISCredentialStore.shared + ArcGISCredentialStore.shared = try await .makePersistent( access: access, isSynchronizable: isSynchronizable @@ -60,7 +62,9 @@ public final class Authenticator: ObservableObject { try await .makePersistent(access: access, isSynchronizable: isSynchronizable) ) } catch { - // If cannot make + // If making the shared network credential store persistent fails, + // then restore the ArcGIS credential store. + ArcGISCredentialStore.shared = previousArcGISCredentialStore throw error } } From 2893151fa2c011d3e97af866a8237c9680fc9e42 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 7 Jul 2022 14:09:59 -0600 Subject: [PATCH 1104/1722] Move to environment settings --- .../Examples/AuthenticationExampleView.swift | 2 +- .../Authentication/Authenticator.swift | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift index 4a4ff9d1f..3835efecd 100644 --- a/Examples/Examples/AuthenticationExampleView.swift +++ b/Examples/Examples/AuthenticationExampleView.swift @@ -46,7 +46,7 @@ struct AuthenticationExampleView: View { } .navigationBarTitle(Text("Authentication"), displayMode: .inline) .onAppear { - ArcGISURLSession.challengeHandler = authenticator + ArcGISRuntimeEnvironment.authenticationChallengeHandler = authenticator } // Save and restore the API Key. // Note: This is only necessary in this example. Other examples make use of the global diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 4718c85ac..546ff3a9d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -50,21 +50,23 @@ public final class Authenticator: ObservableObject { access: ArcGIS.KeychainAccess, isSynchronizable: Bool = false ) async throws { - let previousArcGISCredentialStore = ArcGISCredentialStore.shared + let previousArcGISCredentialStore = ArcGISRuntimeEnvironment.credentialStore - ArcGISCredentialStore.shared = try await .makePersistent( + // Set a persistent ArcGIS credential store on the ArcGIS environment. + ArcGISRuntimeEnvironment.credentialStore = try await .makePersistent( access: access, isSynchronizable: isSynchronizable ) do { - await NetworkCredentialStore.setShared( + // Set a persistent network credential store on the ArcGIS environment. + await ArcGISRuntimeEnvironment.setNetworkCredentialStore( try await .makePersistent(access: access, isSynchronizable: isSynchronizable) ) } catch { // If making the shared network credential store persistent fails, // then restore the ArcGIS credential store. - ArcGISCredentialStore.shared = previousArcGISCredentialStore + ArcGISRuntimeEnvironment.credentialStore = previousArcGISCredentialStore throw error } } @@ -74,14 +76,15 @@ public final class Authenticator: ObservableObject { /// right away. public func clearCredentialStores() async { // Clear ArcGIS Credentials. - await ArcGISCredentialStore.shared.removeAll() + await ArcGISRuntimeEnvironment.credentialStore.removeAll() // Clear network credentials. - await NetworkCredentialStore.shared.removeAll() + await ArcGISRuntimeEnvironment.networkCredentialStore.removeAll() - // We have to reset the sessions for URLCredential storage to respect the removed credentials - ArcGISURLSession.shared = ArcGISURLSession.makeDefaultSharedSession() - ArcGISURLSession.sharedBackground = ArcGISURLSession.makeDefaultSharedBackgroundSession() + // We have to set new sessions for URLCredential storage to respect the removed credentials + // right away. + ArcGISRuntimeEnvironment.urlSession = .makeDefault() + ArcGISRuntimeEnvironment.backgroundURLSession = .makeDefaultBackground() } /// Observes the challenge queue and sets the current challenge. From c5ffae4078948a82a0005eaa83f00123d43e0129 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 7 Jul 2022 16:13:42 -0700 Subject: [PATCH 1105/1722] Move padding internal --- Examples/Examples/FloatingPanelExampleView.swift | 2 -- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index fb9c41af3..f79698e73 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -35,8 +35,6 @@ struct FloatingPanelExampleView: View { FloatingPanel { SampleContent() } - .padding(horizontalSizeClass == .compact ? [] : [.leading, .top, .trailing]) - .padding(.bottom, horizontalSizeClass == .compact ? 0 : 50) .frame(width: horizontalSizeClass == .compact ? nil : 360) } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 1c7c84c2f..f5017a44f 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -73,6 +73,8 @@ public struct FloatingPanel: View where Content: View { } } .esriBorder() + .padding(isCompact ? [] : [.leading, .top, .trailing]) + .padding(.bottom, isCompact ? 0 : 50) .frame( width: geometryProxy.size.width, height: geometryProxy.size.height, From f5daa27c2cd3b857142427e256686f78bc068bb5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 7 Jul 2022 16:33:48 -0700 Subject: [PATCH 1106/1722] Update FloatingPanelExampleView.swift --- Examples/Examples/FloatingPanelExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index f79698e73..126f2f5cd 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -35,7 +35,7 @@ struct FloatingPanelExampleView: View { FloatingPanel { SampleContent() } - .frame(width: horizontalSizeClass == .compact ? nil : 360) + .frame(maxWidth: horizontalSizeClass == .regular ? 360 : .infinity) } } } From 4d3a509f7b11579746e5bc7ef20cd494e411edba Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 14:41:55 -0700 Subject: [PATCH 1107/1722] Remove `isCompact` --- .../UtilityNetworkTrace.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 9523794cd..3964d6244 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -21,12 +21,6 @@ public struct UtilityNetworkTrace: View { @Environment(\.verticalSizeClass) private var verticalSizeClass: UserInterfaceSizeClass? - /// If `true`, the site and facility selector will appear as a sheet. - /// If `false`, the site and facility selector will appear as a popup modal alongside the level selector. - private var isCompact: Bool { - return horizontalSizeClass == .compact || verticalSizeClass == .compact - } - // MARK: Enums /// Activities users will perform while creating a new trace. @@ -173,12 +167,10 @@ public struct UtilityNetworkTrace: View { ) { Text("Trace Color") } - if !isCompact { - TextField( - "Trace Name", - text: $viewModel.pendingTrace.name - ) - } + TextField( + "Trace Name", + text: $viewModel.pendingTrace.name + ) } } Button { From 8f787091231b1079d74092e473550615c4167f33 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 14:47:53 -0700 Subject: [PATCH 1108/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 3964d6244..c9d75c254 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -48,8 +48,8 @@ public struct UtilityNetworkTrace: View { /// The current user activity. @State private var currentActivity: UserActivity = .creatingTrace(nil) - /// Indicates if the warning that all traces will be deleted is presented. - @State private var warningIsPresented = false + /// A Boolean value indicating if the warning that all traces will be deleted is presented. + @State private var showWarningAlert = false /// The view model used by the view. The `UtilityNetworkTraceViewModel` manages state. /// The view observes `UtilityNetworkTraceViewModel` for changes in state. @@ -231,12 +231,12 @@ public struct UtilityNetworkTrace: View { } } Button { - warningIsPresented.toggle() + showWarningAlert.toggle() } label: { Text("Clear All Results") .tint(.red) } - .alert("Clear All Results", isPresented: $warningIsPresented) { + .alert("Clear All Results", isPresented: $showWarningAlert) { Button(role: .destructive) { viewModel.deleteAllTraces() currentActivity = .creatingTrace(nil) From a1f3edc03642a6b5f6e1b243b7aa548204911688 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 14:51:57 -0700 Subject: [PATCH 1109/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index c9d75c254..d6b09eb42 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -105,6 +105,7 @@ public struct UtilityNetworkTrace: View { } label: { Text("Cancel starting point selection") } + .buttonStyle(.bordered) } /// Displays the list of available named trace configurations. From baedf507fc4e308e2d34ab637b80f7f5c4d72639 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 15:05:39 -0700 Subject: [PATCH 1110/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index d6b09eb42..64623e0bd 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -114,20 +114,13 @@ public struct UtilityNetworkTrace: View { Text("No configurations available") } else { ForEach(viewModel.configurations, id: \.name) { configuration in - Button { - withAnimation { + Text(configuration.name) + .lineLimit(1) + .listRowBackground(configuration == viewModel.pendingTrace.configuration ? Color.secondary.opacity(0.5) : nil) + .onTapGesture { viewModel.pendingTrace.configuration = configuration currentActivity = .creatingTrace(nil) } - } label: { - Label { - Text(configuration.name) - .lineLimit(1) - } icon: { - Image(systemName: "circle") - .symbolVariant(configuration == viewModel.pendingTrace.configuration ? .fill: .none) - } - } } } } From 1824bf6a5e41d617a06a893ce8629dada9e8bd97 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 15:31:29 -0700 Subject: [PATCH 1111/1722] `UtilityNamedTraceConfiguration` `Identifiable` conformance --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 2 +- .../Extensions/UtilityNamedTraceConfiguration.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 64623e0bd..58d283910 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -113,7 +113,7 @@ public struct UtilityNetworkTrace: View { if viewModel.configurations.isEmpty { Text("No configurations available") } else { - ForEach(viewModel.configurations, id: \.name) { configuration in + ForEach(viewModel.configurations) { configuration in Text(configuration.name) .lineLimit(1) .listRowBackground(configuration == viewModel.pendingTrace.configuration ? Color.secondary.opacity(0.5) : nil) diff --git a/Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.swift b/Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.swift index db1bee708..d5ec91265 100644 --- a/Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.swift +++ b/Sources/ArcGISToolkit/Extensions/UtilityNamedTraceConfiguration.swift @@ -25,3 +25,5 @@ extension UtilityNamedTraceConfiguration: Hashable { hasher.combine(globalID) } } + +extension UtilityNamedTraceConfiguration: Identifiable {} From 1ecc3450dbbb8dfb365adf868f9ed5dd78460514 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 15:41:14 -0700 Subject: [PATCH 1112/1722] Update UtilityNetworkTrace.swift --- .../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 58d283910..c6fcc0837 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -337,7 +337,6 @@ public struct UtilityNetworkTrace: View { graphicsOverlay: graphicsOverlay.wrappedValue ) ) - UITableView.appearance().backgroundColor = .systemGroupedBackground } public var body: some View { From dee5726e0016708d3166854f264610e7954fccd9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 15:53:52 -0700 Subject: [PATCH 1113/1722] Hide result navigation when there's only 1 --- .../UtilityNetworkTrace.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index c6fcc0837..9eadee2e6 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -180,17 +180,21 @@ public struct UtilityNetworkTrace: View { /// The tab that allows for viewing completed traces. @ViewBuilder private var resultsTab: some View { HStack { - Button { - viewModel.selectPreviousTrace() - } label: { - Image(systemName: "chevron.backward") + if viewModel.completedTraces.count > 1 { + Button { + viewModel.selectPreviousTrace() + } label: { + Image(systemName: "chevron.backward") + } } Text(currentTraceLabel) .padding(.horizontal) - Button { - viewModel.selectNextTrace() - } label: { - Image(systemName: "chevron.forward") + if viewModel.completedTraces.count > 1 { + Button { + viewModel.selectNextTrace() + } label: { + Image(systemName: "chevron.forward") + } } } .font(.title3) From 8609645402f140cd17d2b7cf164bce06101d8a7e Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 8 Jul 2022 16:55:49 -0600 Subject: [PATCH 1114/1722] consume changes from sdk --- .../Components/Authentication/Authenticator.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 546ff3a9d..e0f710ad7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -60,9 +60,8 @@ public final class Authenticator: ObservableObject { do { // Set a persistent network credential store on the ArcGIS environment. - await ArcGISRuntimeEnvironment.setNetworkCredentialStore( + ArcGISRuntimeEnvironment.networkCredentialStore = try await .makePersistent(access: access, isSynchronizable: isSynchronizable) - ) } catch { // If making the shared network credential store persistent fails, // then restore the ArcGIS credential store. From 6c52c9df6655dbe1d02ef6b9ccfcb005a3f3e199 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:02:03 -0700 Subject: [PATCH 1115/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 9eadee2e6..78c5e9e43 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -133,7 +133,7 @@ public struct UtilityNetworkTrace: View { viewModel.pendingTrace.configuration?.name ?? "None selected", isExpanded: Binding( get: { isFocused(traceCreationActivity: .viewingTraceConfigurations) }, - set: { _ in currentActivity = .creatingTrace(.viewingTraceConfigurations) } + set: { currentActivity = .creatingTrace($0 ? .viewingTraceConfigurations : nil) } ) ) { configurationsList @@ -149,7 +149,7 @@ public struct UtilityNetworkTrace: View { "\(viewModel.pendingTrace.startingPoints.count) selected", isExpanded: Binding( get: { isFocused(traceCreationActivity: .viewingStartingPoints) }, - set: { _ in currentActivity = .creatingTrace(.viewingStartingPoints) } + set: { currentActivity = .creatingTrace($0 ? .viewingStartingPoints : nil) } ) ) { startingPointsList From 75c2c850b59ca2be0e87a869d9f0887417278437 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:06:26 -0700 Subject: [PATCH 1116/1722] 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 78c5e9e43..3554e1e6b 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -315,7 +315,7 @@ public struct UtilityNetworkTrace: View { /// - Parameters: /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace /// graphics. - /// - map: The parent map. + /// - map: The map containing the utility network(s). /// - pointInMap: Acts as the point at which newly selected starting point graphics will be /// created. /// - pointInScreen: Acts as the point of identification for items tapped in the utility network. From 8baa254ec9ff08d84bde74cf633f2d817856a121 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:35:58 -0700 Subject: [PATCH 1117/1722] Rename `pointInScreen` and `pointInMap` --- .../UtilityNetworkTraceExampleView.swift | 14 +++++----- .../UtilityNetworkTrace.swift | 26 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index b06e5276f..e97f9d7ca 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -24,10 +24,10 @@ struct UtilityNetworkTraceExampleView: View { @State var mapViewProxy: MapViewProxy? /// Provides the ability to detect tap locations in the context of the map view. - @State var pointInMap: Point? + @State var mapPoint: Point? /// Provides the ability to detect tap locations in the context of the screen. - @State var pointInScreen: CGPoint? + @State var viewPoint: CGPoint? /// A container for graphical trace results. @State var resultGraphicsOverlay = GraphicsOverlay() @@ -42,9 +42,9 @@ struct UtilityNetworkTraceExampleView: View { viewpoint: viewpoint, graphicsOverlays: [resultGraphicsOverlay] ) - .onSingleTapGesture { pointInScreen, pointInMap in - self.pointInScreen = pointInScreen - self.pointInMap = pointInMap + .onSingleTapGesture { viewPoint, mapPoint in + self.viewPoint = viewPoint + self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } .overlay(alignment: .topTrailing) { @@ -52,8 +52,8 @@ struct UtilityNetworkTraceExampleView: View { UtilityNetworkTrace( $resultGraphicsOverlay, map, - $pointInMap, - $pointInScreen, + $mapPoint, + $viewPoint, $mapViewProxy, $viewpoint ) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 3554e1e6b..c7a20a4c8 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -64,10 +64,10 @@ 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 pointInScreen: CGPoint? + @Binding private var viewPoint: CGPoint? /// Acts as the point at which newly selected starting point graphics will be created. - @Binding private var pointInMap: Point? + @Binding private var mapPoint: Point? /// Allows the Utility Network Trace Tool to update the parent map view's viewpoint. @Binding private var viewpoint: Viewpoint? @@ -316,22 +316,22 @@ public struct UtilityNetworkTrace: View { /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace /// graphics. /// - map: The map containing the utility network(s). - /// - pointInMap: Acts as the point at which newly selected starting point graphics will be + /// - mapPoint: Acts as the point at which newly selected starting point graphics will be /// created. - /// - pointInScreen: Acts as the point of identification for items tapped in the utility network. + /// - viewPoint: 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. public init( _ graphicsOverlay: Binding, _ map: Map, - _ pointInMap: Binding, - _ pointInScreen: Binding, + _ mapPoint: Binding, + _ viewPoint: Binding, _ mapViewProxy: Binding, _ viewpoint: Binding ) { - _pointInScreen = pointInScreen - _pointInMap = pointInMap + _viewPoint = viewPoint + _mapPoint = mapPoint _mapViewProxy = mapViewProxy _graphicsOverlay = graphicsOverlay _viewpoint = viewpoint @@ -364,18 +364,18 @@ public struct UtilityNetworkTrace: View { } .background(Color(uiColor: .systemGroupedBackground)) .animation(.default, value: currentActivity) - .onChange(of: pointInScreen) { newValue in + .onChange(of: viewPoint) { newValue in guard isAddingStartingPoints, let mapViewProxy = mapViewProxy, - let pointInMap = pointInMap, - let pointInScreen = pointInScreen else { + let mapPoint = mapPoint, + let viewPoint = viewPoint else { return } currentActivity = .creatingTrace(.viewingStartingPoints) Task { await viewModel.setStartingPoint( - at: pointInScreen, - mapPoint: pointInMap, + at: viewPoint, + mapPoint: mapPoint, with: mapViewProxy ) } From 8eb58d56456e7a6654746c2667fdf822fff4182d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:42:03 -0700 Subject: [PATCH 1118/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace.swift | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index c7a20a4c8..2b0122b64 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -345,7 +345,8 @@ public struct UtilityNetworkTrace: View { public var body: some View { VStack { - if !viewModel.completedTraces.isEmpty && !isAddingStartingPoints { + if !viewModel.completedTraces.isEmpty && + !isFocused(traceCreationActivity: .addingStartingPoints) { activityPicker } switch currentActivity { @@ -365,7 +366,7 @@ public struct UtilityNetworkTrace: View { .background(Color(uiColor: .systemGroupedBackground)) .animation(.default, value: currentActivity) .onChange(of: viewPoint) { newValue in - guard isAddingStartingPoints, + guard isFocused(traceCreationActivity: .addingStartingPoints), let mapViewProxy = mapViewProxy, let mapPoint = mapPoint, let viewPoint = viewPoint else { @@ -390,21 +391,6 @@ public struct UtilityNetworkTrace: View { return "Trace \(index+1) of \(viewModel.completedTraces.count.description)" } - /// Indicates if the user is currently adding starting points. - private var isAddingStartingPoints: Bool { - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .addingStartingPoints: - return true - default: - return false - } - default: - return false - } - } - /// The starting point being inspected (if one exists). private var selectedStartingPoint: UtilityNetworkTraceStartingPoint? { switch currentActivity { From 5f8f931591cc8ec822b45757905df865d0c704c1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:44:24 -0700 Subject: [PATCH 1119/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 2b0122b64..98644ae10 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -393,17 +393,11 @@ public struct UtilityNetworkTrace: View { /// The starting point being inspected (if one exists). private var selectedStartingPoint: UtilityNetworkTraceStartingPoint? { - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .inspectingStartingPoint(let startingPoint): - return startingPoint - default: - return nil - } - default: - return nil + if case let .creatingTrace(activity) = currentActivity, + case let .inspectingStartingPoint(startingPoint) = activity { + return startingPoint } + return nil } /// Determines if the provided creation activity is the currently focused creation activity. From 35c0338b5f1b789109ccaeb7d6253fa29338a267 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:46:27 -0700 Subject: [PATCH 1120/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 98644ae10..473452c0e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -405,10 +405,9 @@ public struct UtilityNetworkTrace: View { /// - Returns: A Boolean value indicating whether the provided activity is the currently focused /// creation activity. private func isFocused(traceCreationActivity: TraceCreationActivity) -> Bool { - switch currentActivity { - case .creatingTrace(let currentActivity): - return traceCreationActivity == currentActivity - default: return false + if case let .creatingTrace(activity) = currentActivity{ + return traceCreationActivity == activity } + return false } } From 7fd5dc0a68fd2f31ee3fd976570c1504144583f4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:48:10 -0700 Subject: [PATCH 1121/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 41a9ed006..92b576c07 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -515,10 +515,9 @@ public struct UtilityNetworkTrace: View { /// - Returns: A Boolean value indicating whether the provided activity is the currently focused /// viewing activity. private func isFocused(traceViewingActivity: TraceViewingActivity) -> Bool { - switch currentActivity { - case .viewingTraces(let currentActivity): - return traceViewingActivity == currentActivity - default: return false + if case let .viewingTraces(activity) = currentActivity { + return traceViewingActivity == activity } + return false } } From cf2e9870876fcaed29cec703631eefab00567d55 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:52:47 -0700 Subject: [PATCH 1122/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTraceViewModel.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 4a4ac0535..5224c5b4a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -92,12 +92,8 @@ import SwiftUI func deleteAllTraces() { selectedTraceIndex = nil completedTraces.forEach { traceResult in - traceResult.startingPoints.forEach { startingPoint in - graphicsOverlay.removeGraphic(startingPoint.graphic) - } - traceResult.graphics.forEach { graphic in - graphicsOverlay.removeGraphic(graphic) - } + graphicsOverlay.removeGraphics(traceResult.startingPoints.map({ $0.graphic })) + graphicsOverlay.removeGraphics(traceResult.graphics) } completedTraces.removeAll() } From 53cc07890ef911ab0c44f8a4b99d9e98af6f577c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 16:54:01 -0700 Subject: [PATCH 1123/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 5224c5b4a..f424a1807 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -180,10 +180,10 @@ import SwiftUI for result in traceResults { switch result { case let result as UtilityElementTraceResult: - result.elements.forEach({ element in + result.elements.forEach { element in let count = assetGroups[element.assetGroup.name] ?? 0 + 1 assetGroups.updateValue(count, forKey: element.assetGroup.name) - }) + } assetGroups.forEach { (key, value) in pendingTrace.assetLabels.append("\(key): \(value)") } From cae98e0cd33d13b0db99b32501990cc37920a0da Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 8 Jul 2022 17:01:42 -0700 Subject: [PATCH 1124/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTraceViewModel.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index f424a1807..bd949c560 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -253,12 +253,8 @@ import SwiftUI to isSelected: Bool ) { guard index >= 0, index <= completedTraces.count - 1 else { return } - completedTraces[index].graphics.forEach({ graphic in - graphic.isSelected = isSelected - }) - completedTraces[index].startingPoints.forEach({ startingPoint in - startingPoint.graphic.isSelected = isSelected - }) + _ = completedTraces[index].graphics.map { $0.isSelected = isSelected} + _ = completedTraces[index].startingPoints.map { $0.graphic.isSelected = isSelected} } /// Loads the named trace configurations in the network on the provided map. From 90f0abb0fd5541bfcccaad8e940087ee40a6b06b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 09:46:31 -0700 Subject: [PATCH 1125/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 473452c0e..c5dc3df8c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -385,10 +385,13 @@ public struct UtilityNetworkTrace: View { // MARK: Computed Properties - /// Indicates the number of the trace currently being viewed out the total number of traces. + /// The index of the trace currently being viewed out of the total number of traces. private var currentTraceLabel: String { guard let index = viewModel.selectedTraceIndex else { return "Error" } - return "Trace \(index+1) of \(viewModel.completedTraces.count.description)" + return String( + localized: "Trace \(index+1) of \(viewModel.completedTraces.count)", + comment: "The index of the trace currently being viewed out of the total number of traces." + ) } /// The starting point being inspected (if one exists). From 66184a83248b29a79a692aecfbdbc49f8502cf70 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 10:00:05 -0700 Subject: [PATCH 1126/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index c5dc3df8c..19820dd44 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -146,7 +146,10 @@ public struct UtilityNetworkTrace: View { Text("Add new starting point") } DisclosureGroup( - "\(viewModel.pendingTrace.startingPoints.count) selected", + String( + localized: "\(viewModel.pendingTrace.startingPoints.count) selected", + comment: "The number of selected starting points." + ), isExpanded: Binding( get: { isFocused(traceCreationActivity: .viewingStartingPoints) }, set: { currentActivity = .creatingTrace($0 ? .viewingStartingPoints : nil) } From 6bf1f353fa558b901fa1c5c3a84c4903a6d1edc4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 10:29:00 -0700 Subject: [PATCH 1127/1722] Update UtilityNetworkTrace.swift --- .../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 19820dd44..d6c1c9eba 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -267,7 +267,6 @@ public struct UtilityNetworkTrace: View { .lineLimit(1) .frame(maxWidth: .infinity, alignment: .center) List { - Text(selectedStartingPoint?.utilityElement.globalID.uuidString ?? "N/A") ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in HStack{ Text(item.key) From cf9f7e084abce8dd190c0d1c0fcdf7aba9e0229b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 12:24:00 -0700 Subject: [PATCH 1128/1722] Revert "Update UtilityNetworkTrace.swift" This reverts commit 66184a83248b29a79a692aecfbdbc49f8502cf70. --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index d6c1c9eba..c6c840928 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -146,10 +146,7 @@ public struct UtilityNetworkTrace: View { Text("Add new starting point") } DisclosureGroup( - String( - localized: "\(viewModel.pendingTrace.startingPoints.count) selected", - comment: "The number of selected starting points." - ), + "\(viewModel.pendingTrace.startingPoints.count) selected", isExpanded: Binding( get: { isFocused(traceCreationActivity: .viewingStartingPoints) }, set: { currentActivity = .creatingTrace($0 ? .viewingStartingPoints : nil) } From 50273de912e35feec5933c4460f54c290ec692ac Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 12:27:27 -0700 Subject: [PATCH 1129/1722] Revert "Update UtilityNetworkTrace.swift" This reverts commit 90f0abb0fd5541bfcccaad8e940087ee40a6b06b. --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index c6c840928..3a6d0d1e9 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -384,13 +384,10 @@ public struct UtilityNetworkTrace: View { // MARK: Computed Properties - /// The index of the trace currently being viewed out of the total number of traces. + /// Indicates the number of the trace currently being viewed out the total number of traces. private var currentTraceLabel: String { guard let index = viewModel.selectedTraceIndex else { return "Error" } - return String( - localized: "Trace \(index+1) of \(viewModel.completedTraces.count)", - comment: "The index of the trace currently being viewed out of the total number of traces." - ) + return "Trace \(index+1) of \(viewModel.completedTraces.count.description)" } /// The starting point being inspected (if one exists). From d1d679016d71f829b934a30e4565d94f28c21311 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 12:28:56 -0700 Subject: [PATCH 1130/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 3a6d0d1e9..264ce8a3a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -156,9 +156,7 @@ public struct UtilityNetworkTrace: View { } } Section("Advanced") { - ColorPicker( - selection: $viewModel.pendingTrace.color - ) { + ColorPicker(selection: $viewModel.pendingTrace.color) { Text("Trace Color") } TextField( From 440013ffe9421413c891452d8ef1457b4d592659 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 14:32:48 -0700 Subject: [PATCH 1131/1722] Update FloatingPanel.swift --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index f5017a44f..b13fb3a19 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -52,7 +52,7 @@ public struct FloatingPanel: View where Content: View { /// A Boolean value indicating whether the panel should be configured for a compact environment. private var isCompact: Bool { - horizontalSizeClass == .compact ? true : false + horizontalSizeClass == .compact } public var body: some View { From 0dc3c8e91f588d44022067f3ee2b41726f6552f5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 14:48:38 -0700 Subject: [PATCH 1132/1722] Add terminal config picker color --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index f9767ff56..5ef99f656 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -352,6 +352,7 @@ public struct UtilityNetworkTrace: View { Text($0.name) } } + .foregroundColor(.blue) } } ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in From 3022deaa254f197f2d1635a16892602724f2cc56 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 11 Jul 2022 15:47:28 -0700 Subject: [PATCH 1133/1722] Zoom to result extent --- .../UtilityNetworkTrace.swift | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 5ef99f656..9b08bfb35 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -283,6 +283,16 @@ public struct UtilityNetworkTrace: View { } } } + makeZoomToButtom { + if let resultEnvelope = GeometryEngine.combineExtents(of: [ + viewModel.selectedTrace?.utilityGeometryTraceResult?.multipoint, + viewModel.selectedTrace?.utilityGeometryTraceResult?.polygon, + viewModel.selectedTrace?.utilityGeometryTraceResult?.polyline + ].compactMap { $0 }) { + viewpoint = Viewpoint(targetExtent: resultEnvelope.extent) + } + } + .padding([.vertical], 2) Button { showWarningAlert.toggle() } label: { @@ -363,16 +373,10 @@ public struct UtilityNetworkTrace: View { } } } - Button { + makeZoomToButtom { if let selectedStartingPoint { viewpoint = Viewpoint(targetExtent: selectedStartingPoint.extent) } - } label: { - Label { - Text("Zoom To") - } icon: { - Image(systemName: "scope") - } } } @@ -520,4 +524,19 @@ public struct UtilityNetworkTrace: View { } return false } + + /// Returns a "Zoom To" button that performs a specified action when pressed. + /// - Parameter action: The action to be performed. + /// - Returns: The configured button. + private func makeZoomToButtom(_ action: @escaping () -> Void) -> some View { + Button { + action() + } label: { + Label { + Text("Zoom To") + } icon: { + Image(systemName: "scope") + } + } + } } From d50b6991cf76c76e83418a2d0daee4f68ef40eec Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 11 Jul 2022 17:25:29 -0600 Subject: [PATCH 1134/1722] absorb changes from sdk --- .../Authentication/Authenticator.swift | 4 +-- .../AuthenticatorTests.swift | 26 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index e0f710ad7..359ccfb12 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -82,8 +82,8 @@ public final class Authenticator: ObservableObject { // We have to set new sessions for URLCredential storage to respect the removed credentials // right away. - ArcGISRuntimeEnvironment.urlSession = .makeDefault() - ArcGISRuntimeEnvironment.backgroundURLSession = .makeDefaultBackground() + ArcGISRuntimeEnvironment.urlSession = ArcGISURLSession(configuration: .default) + ArcGISRuntimeEnvironment.backgroundURLSession = ArcGISURLSession(configuration: .background(withIdentifier: "com.esri.com.arcgis.toolkit." + UUID().uuidString)) } /// Observes the challenge queue and sets the current challenge. diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index ad76ee198..e25436790 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -16,8 +16,8 @@ import XCTest @testable import ArcGISToolkit import ArcGIS +@MainActor class AuthenticatorTests: XCTestCase { - @MainActor func testInit() { let config = OAuthConfiguration( portalURL: URL(string:"www.arcgis.com")!, @@ -29,12 +29,11 @@ class AuthenticatorTests: XCTestCase { XCTAssertEqual(authenticator.oAuthConfigurations, [config]) } - @MainActor func testMakePersistent() async throws { // Make sure credential stores are restored. addTeardownBlock { - ArcGISCredentialStore.shared = ArcGISCredentialStore() - await NetworkCredentialStore.setShared(await NetworkCredentialStore()) + ArcGISRuntimeEnvironment.credentialStore = ArcGISCredentialStore() + ArcGISRuntimeEnvironment.networkCredentialStore = NetworkCredentialStore() } // This tests that calling makePersistent tries to sync with the keychain. @@ -45,9 +44,8 @@ class AuthenticatorTests: XCTestCase { } catch {} } - @MainActor func testClearCredentialStores() async { - await ArcGISCredentialStore.shared.add( + await ArcGISRuntimeEnvironment.credentialStore.add( .staticToken( url: URL(string: "www.arcgis.com")!, tokenInfo: .init( @@ -60,12 +58,24 @@ class AuthenticatorTests: XCTestCase { let authenticator = Authenticator() - var arcGISCreds = await ArcGISCredentialStore.shared.credentials + var arcGISCreds = await ArcGISRuntimeEnvironment.credentialStore.credentials XCTAssertEqual(arcGISCreds.count, 1) await authenticator.clearCredentialStores() - arcGISCreds = await ArcGISCredentialStore.shared.credentials + arcGISCreds = await ArcGISRuntimeEnvironment.credentialStore.credentials XCTAssertTrue(arcGISCreds.isEmpty) } + + func testChallengeQueue() async throws { + class MockQueuedChallenge: QueuedChallenge { + func complete() async { + + } + } + + + let authenticator = Authenticator() + + } } From 21363fa49f4c9f61e5a3d95a14d735e2e0d70342 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 11 Jul 2022 22:13:13 -0600 Subject: [PATCH 1135/1722] implement test for the challenge queue --- .../Authentication/Authenticator.swift | 2 +- .../AuthenticatorTests.swift | 68 +++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 359ccfb12..5744b7821 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -115,7 +115,7 @@ public final class Authenticator: ObservableObject { } } - private var subject = PassthroughSubject() + var subject = PassthroughSubject() /// A serial queue for authentication challenges. private var challengeQueue: AsyncPublisher> { diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index e25436790..f1935f34e 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -15,9 +15,10 @@ import SwiftUI import XCTest @testable import ArcGISToolkit import ArcGIS +import Combine -@MainActor class AuthenticatorTests: XCTestCase { + @MainActor func testInit() { let config = OAuthConfiguration( portalURL: URL(string:"www.arcgis.com")!, @@ -29,6 +30,7 @@ class AuthenticatorTests: XCTestCase { XCTAssertEqual(authenticator.oAuthConfigurations, [config]) } + @MainActor func testMakePersistent() async throws { // Make sure credential stores are restored. addTeardownBlock { @@ -44,6 +46,7 @@ class AuthenticatorTests: XCTestCase { } catch {} } + @MainActor func testClearCredentialStores() async { await ArcGISRuntimeEnvironment.credentialStore.add( .staticToken( @@ -67,15 +70,72 @@ class AuthenticatorTests: XCTestCase { XCTAssertTrue(arcGISCreds.isEmpty) } + @MainActor func testChallengeQueue() async throws { - class MockQueuedChallenge: QueuedChallenge { + actor MockQueuedChallenge: QueuedChallenge { + nonisolated let id: Int + init(id: Int) { + self.id = id + } + private var isComplete: Bool = false + func setCompleted() { + isComplete = true + } + func complete() async { - + while !isComplete { + await Task.yield() + } } } - let authenticator = Authenticator() + // Give chance for authenticator to start observation + await Task.yield() + + XCTAssertNil(authenticator.currentChallenge) + + // Create and enqueue first challenge. + let challenge = MockQueuedChallenge(id: 1) + authenticator.subject.send(challenge) + + // Make sure first challenge is published as the current challenge. + let currentChallenge = await AsyncPublisher(authenticator.$currentChallenge) + .compactMap( { $0 as? MockQueuedChallenge }) + .first(where: { _ in true }) + + XCTAssertEqual(currentChallenge?.id, 1) + XCTAssertNotNil(authenticator.currentChallenge) + + // Create and enqueue second challenge. + let challenge2 = MockQueuedChallenge(id: 2) + authenticator.subject.send(challenge2) + + // Make sure first challenge is still the current challenge + let mockedCurrentChallenge = try XCTUnwrap(authenticator.currentChallenge as? MockQueuedChallenge) + XCTAssertEqual(mockedCurrentChallenge.id, 1) + + // Complete first challenge. + await challenge.setCompleted() + + // Check next queued challenge + let currentChallenge2 = await AsyncPublisher(authenticator.$currentChallenge) + .compactMap( { $0 as? MockQueuedChallenge }) + .dropFirst() + .first(where: { _ in true }) + + XCTAssertEqual(currentChallenge2?.id, 2) + XCTAssertNotNil(authenticator.currentChallenge) + + // Complete second challenge. + await challenge2.setCompleted() + + // Check next queued challenge, should be nil + let currentChallenge3 = await AsyncPublisher(authenticator.$currentChallenge) + .dropFirst() + .first(where: { _ in true }) + // nil coalescing seems required because currentChallenge3 is Optional> + XCTAssertNil(currentChallenge3 ?? nil) } } From e9afe62dc20199983386c950cbb613ed93129b8d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 12 Jul 2022 15:42:14 -0700 Subject: [PATCH 1136/1722] Select utility network --- .../UtilityNetworkTrace.swift | 28 ++++++++++ .../UtilityNetworkTraceViewModel.swift | 51 +++++++++++++++---- .../Extensions/UtilityNetwork.swift | 28 ++++++++++ 3 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 9b08bfb35..0b76c2698 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -31,6 +31,8 @@ public struct UtilityNetworkTrace: View { case inspectingStartingPoint(UtilityNetworkTraceStartingPoint) /// The user is viewing the list of advanced options. case viewingAdvancedOptions + /// The user is viewing the list of available networks. + case viewingNetworkOptions /// The user is viewing the list of chosen starting points. case viewingStartingPoints /// The user is viewing the list of available trace configurations. @@ -137,9 +139,35 @@ public struct UtilityNetworkTrace: View { } } + /// Displays the list of available networks. + @ViewBuilder private var networksList: some View { + ForEach(viewModel.networks, id: \.self) { network in + Text(network.name) + .lineLimit(1) + .listRowBackground(network == viewModel.network ? Color.secondary.opacity(0.5) : nil) + .onTapGesture { + viewModel.setNetwork(network) + currentActivity = .creatingTrace(nil) + } + } + } + /// The tab that allows for a new trace to be configured. @ViewBuilder private var newTraceTab: some View { List { + if viewModel.networks.count > 1 { + Section("Network") { + DisclosureGroup( + viewModel.network?.name ?? "None selected", + isExpanded: Binding( + get: { isFocused(traceCreationActivity: .viewingNetworkOptions) }, + set: { currentActivity = .creatingTrace($0 ? .viewingNetworkOptions : nil) } + ) + ) { + networksList + } + } + } Section("Trace Configuration") { DisclosureGroup( viewModel.pendingTrace.configuration?.name ?? "None selected", diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index cef153a38..5d1880e55 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -24,6 +24,9 @@ import SwiftUI /// The available named trace configurations. @Published private(set) var configurations = [UtilityNamedTraceConfiguration]() + /// The utility network on which traces will be ran. + @Published private(set) var network: UtilityNetwork? + /// The trace currently under configuration. @Published var pendingTrace = Trace() @@ -50,14 +53,21 @@ import SwiftUI /// A Boolean value indicating if the pending trace is configured to the point that it can be run. var canRunTrace: Bool { - pendingTrace.configuration != nil && !pendingTrace.startingPoints.isEmpty + network != nil && + pendingTrace.configuration != nil && + !pendingTrace.startingPoints.isEmpty + } + + /// The map's utility networks. + var networks: [UtilityNetwork] { + return map.utilityNetworks } /// The overlay on which trace graphics will be drawn. private var graphicsOverlay: GraphicsOverlay - /// The utility network on which traces will be ran. - private var network: UtilityNetwork? + /// A map containing one or more utility networks. + private var map: Map /// The selected trace. var selectedTrace: Trace? { @@ -74,11 +84,19 @@ import SwiftUI /// - Parameter map: The map to be loaded that contains at least one utility network. /// - Parameter graphicsOverlay: The overlay on which trace graphics will be drawn. init(map: Map, graphicsOverlay: GraphicsOverlay) { + self.map = map self.graphicsOverlay = graphicsOverlay Task { - try? await map.load() + do { + try await map.load() + for network in map.utilityNetworks { + try await network.load() + } + } catch { + print(error.localizedDescription) + } network = map.utilityNetworks.first - await loadNamedTraceConfigurations(map) + await loadNamedTraceConfigurations() } } @@ -112,6 +130,15 @@ import SwiftUI } } + /// Changes the selected network. + /// - Parameter network: The new utility network to be selected. + func setNetwork(_ network: UtilityNetwork) { + self.network = network + Task { + await loadNamedTraceConfigurations() + } + } + /// Updates the pending trace's configuration and name, if applicable. /// /// The pending trace's name will only be updated if the user hasn't specified one already. @@ -153,11 +180,14 @@ import SwiftUI // Block duplicate starting point selection guard let feature = geoElement as? ArcGISFeature, - let globalid = feature.attributes["globalid"] as? UUID, - !pendingTrace.startingPoints.contains(where: { startingPoint in + let globalid = feature.attributes["globalid"] as? UUID else { + userWarning = "Element could not be identified" + return + } + guard !pendingTrace.startingPoints.contains(where: { startingPoint in return startingPoint.utilityElement.globalID == globalid }) else { - userWarning = "Duplicate starting points cannot be added " + userWarning = "Duplicate starting points cannot be added" return } @@ -337,9 +367,8 @@ import SwiftUI _ = completedTraces[index].startingPoints.map { $0.graphic.isSelected = isSelected} } - /// Loads the named trace configurations in the network on the provided map. - /// - Parameter map: A web map containing one or more utility networks. - private func loadNamedTraceConfigurations(_ map: Map) async { + /// Loads the named trace configurations in the network. + private func loadNamedTraceConfigurations() async { guard let network = network else { return } configurations = (try? await map.getNamedTraceConfigurations(from: network)) ?? [] } diff --git a/Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift b/Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift new file mode 100644 index 000000000..c6b4f0e04 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift @@ -0,0 +1,28 @@ +// 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 UtilityNetwork: Hashable { + public static func == ( + lhs: UtilityNetwork, + rhs: UtilityNetwork + ) -> Bool { + lhs.name == rhs.name && lhs.url == rhs.url + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(url) + } +} From db0d6205a4cc26e74e38fb5db140f98ac29d83c8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Jul 2022 09:27:34 -0700 Subject: [PATCH 1137/1722] loadNamedTraceConfigurations no longer async --- .../UtilityNetworkTraceViewModel.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 5d1880e55..98341cd1d 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -96,7 +96,7 @@ import SwiftUI print(error.localizedDescription) } network = map.utilityNetworks.first - await loadNamedTraceConfigurations() + loadNamedTraceConfigurations() } } @@ -134,9 +134,7 @@ import SwiftUI /// - Parameter network: The new utility network to be selected. func setNetwork(_ network: UtilityNetwork) { self.network = network - Task { - await loadNamedTraceConfigurations() - } + loadNamedTraceConfigurations() } /// Updates the pending trace's configuration and name, if applicable. @@ -368,9 +366,13 @@ import SwiftUI } /// Loads the named trace configurations in the network. - private func loadNamedTraceConfigurations() async { + private func loadNamedTraceConfigurations() { guard let network = network else { return } - configurations = (try? await map.getNamedTraceConfigurations(from: network)) ?? [] + Task { + configurations = (try? await map.getNamedTraceConfigurations( + from: network + )) ?? [] + } } } From 0e1a6be196662310010f3223f8ffb855f5c7d504 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Jul 2022 09:38:32 -0700 Subject: [PATCH 1138/1722] Remove starting points when changing the network --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 98341cd1d..8320abe60 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -132,8 +132,12 @@ import SwiftUI /// Changes the selected network. /// - Parameter network: The new utility network to be selected. + /// + /// This function also clears any set starting points in the pending trace and reloads the list of available + /// trace configurations. func setNetwork(_ network: UtilityNetwork) { self.network = network + pendingTrace.startingPoints.removeAll() loadNamedTraceConfigurations() } From ce1f356532b2077fad691692707712f5082f4506 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Jul 2022 10:59:01 -0700 Subject: [PATCH 1139/1722] Show correct asset group count in results --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 12 +++++++++--- .../UtilityNetworkTraceViewModel.swift | 15 ++++++++------- .../UtilityNetworkTraceViewModelTrace.swift | 7 ++----- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 0b76c2698..e77003339 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -260,14 +260,20 @@ public struct UtilityNetworkTrace: View { List { Section("Element Result") { DisclosureGroup( - viewModel.selectedTrace?.utilityElementTraceResult?.elements.count.description ?? "0", + viewModel.selectedTrace?.assets.map({ $0.value.count }).reduce(0, +).description ?? "0", isExpanded: Binding( get: { isFocused(traceViewingActivity: .viewingElementResults) }, set: { currentActivity = .viewingTraces($0 ? .viewingElementResults : nil) } ) ) { - ForEach(viewModel.selectedTrace?.assetLabels ?? [], id: \.self) { label in - Text(label) + ForEach( + (viewModel.selectedTrace?.assets ?? [:]).sorted(by: { $0.key < $1.key }), id: \.key + ) { asset in + HStack { + Text(asset.key) + Spacer() + Text(asset.value.count.description) + } } } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 8320abe60..a5d9c7b8b 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -291,18 +291,19 @@ import SwiftUI return false } - var assetGroups = [String: Int]() + var assets = [String: [UtilityElement]]() for result in traceResults { switch result { case let result as UtilityElementTraceResult: result.elements.forEach { element in - let count = assetGroups[element.assetGroup.name] ?? 0 + 1 - assetGroups.updateValue(count, forKey: element.assetGroup.name) - } - assetGroups.forEach { (key, value) in - pendingTrace.assetLabels.append("\(key): \(value)") + var assetsInGroup = assets[element.assetGroup.name, default: []] + assetsInGroup.append(element) + assets.updateValue( + assetsInGroup, + forKey: element.assetGroup.name + ) } - pendingTrace.utilityElementTraceResult = result + pendingTrace.assets = assets case let result as UtilityGeometryTraceResult: let createGraphic: ((Geometry, SimpleLineSymbol.Style, Color) -> (Graphic)) = { geometry, style, color in return Graphic( diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index f5527dae5..702e952b3 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -17,8 +17,8 @@ import SwiftUI extension UtilityNetworkTraceViewModel { /// A trace performed on a utility network. struct Trace { - /// A collection of asset labels for all elements returned in the trace. - var assetLabels = [String]() + /// A collection of all elements returned in the trace, grouped by asset group. + var assets = [String: [UtilityElement]]() /// A user given color for the trace with a default value of green. var color: Color = .green { @@ -56,9 +56,6 @@ extension UtilityNetworkTraceViewModel { /// Indicates that the user has specified a name for the trace. var userDidSpecifyName: Bool = false - /// A trace result set comprised of an collection of UtilityElement objects. - var utilityElementTraceResult: UtilityElementTraceResult? - /// A trace result set comprised of a collection of UtilityTraceFunctionOutput objects. var utilityFunctionTraceResult: UtilityFunctionTraceResult? From 818016fc9582133173a61ada6a1d8d5de45ee8dd Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 13 Jul 2022 13:43:34 -0600 Subject: [PATCH 1140/1722] remove all code not required for trust host --- Examples/Examples.xcodeproj/project.pbxproj | 4 - .../Examples/AuthenticationExampleView.swift | 222 ------------- Examples/ExamplesApp/Examples.swift | 10 +- .../Authentication/Authenticator.swift | 47 +-- .../AuthenticatorModifier.swift | 8 +- .../CertificatePickerViewModifier.swift | 291 ------------------ .../Authentication/DocumentPickerView.swift | 60 ---- .../QueuedArcGISChallenge.swift | 57 ---- .../Authentication/SheetViewModifier.swift | 40 --- .../UsernamePasswordViewModifier.swift | 262 ---------------- 10 files changed, 19 insertions(+), 982 deletions(-) delete mode 100644 Examples/Examples/AuthenticationExampleView.swift delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 85cb445c2..6707a39ee 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 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 */; }; - 883CA916282D951D00D9842C /* AuthenticationExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 883CA915282D951D00D9842C /* AuthenticationExampleView.swift */; }; E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */; }; E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47ABE432652FE0900FD2FE3 /* ExamplesApp.swift */; }; @@ -44,7 +43,6 @@ 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 = ""; }; - 883CA915282D951D00D9842C /* AuthenticationExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationExampleView.swift; sourceTree = ""; }; E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloorFilterExampleView.swift; sourceTree = ""; }; E47ABE402652FE0900FD2FE3 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -87,7 +85,6 @@ E4AA9315276BF5ED000E6289 /* FloatingPanelExampleView.swift */, 75657E4727ABAC8400EE865B /* CompassExampleView.swift */, E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */, - 883CA915282D951D00D9842C /* AuthenticationExampleView.swift */, ); name = Examples; sourceTree = ""; @@ -227,7 +224,6 @@ 75657E4827ABAC8400EE865B /* CompassExampleView.swift in Sources */, E48A73452658227100F5C118 /* Examples.swift in Sources */, 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */, - 883CA916282D951D00D9842C /* AuthenticationExampleView.swift in Sources */, E48A73432658227100F5C118 /* Example.swift in Sources */, E47ABE442652FE0900FD2FE3 /* ExamplesApp.swift in Sources */, E4F9BC99265EFCAF001280FF /* OverviewMapExampleView.swift in Sources */, diff --git a/Examples/Examples/AuthenticationExampleView.swift b/Examples/Examples/AuthenticationExampleView.swift deleted file mode 100644 index 3835efecd..000000000 --- a/Examples/Examples/AuthenticationExampleView.swift +++ /dev/null @@ -1,222 +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 -import ArcGIS -import ArcGISToolkit - -struct AuthenticationExampleView: View { - @MainActor var authenticator = Authenticator( - promptForUntrustedHosts: true//, oAuthConfigurations: [.arcgisDotCom] - ) - @State var previousApiKey: APIKey? - @State private var items = AuthenticationItem.makeAll() - - var body: some View { - VStack { - if items.isEmpty { - ProgressView() - } else { - List(items) { item in - AuthenticationItemView(item: item) - } - } - - Button("Clear Credential Store") { - items = [] - Task { - await authenticator.clearCredentialStores() - items = AuthenticationItem.makeAll() - } - } - } - .authenticator(authenticator) - .task { - try? await authenticator.makePersistent(access: .whenUnlockedThisDeviceOnly) - } - .navigationBarTitle(Text("Authentication"), displayMode: .inline) - .onAppear { - ArcGISRuntimeEnvironment.authenticationChallengeHandler = authenticator - } - // Save and restore the API Key. - // Note: This is only necessary in this example. Other examples make use of the global - // api key that is set when the app starts up. Using an api key will prevent an - // authentication challenge prompt for certain services. Since this example highlights - // the usage of authentication challenge prompts, we want to set the api key to `nil` - // when this example appears and restore it when this example disappears. - .onAppear { - // Save off the api key - previousApiKey = ArcGISRuntimeEnvironment.apiKey - // Set the api key to nil so that the authenticated services will prompt. - ArcGISRuntimeEnvironment.apiKey = nil - } - .onDisappear { - // Restore api key when exiting this example. - ArcGISRuntimeEnvironment.apiKey = previousApiKey - } - } -} - -private struct AuthenticationItemView: View { - let loadables: [Loadable] - let title: String - @State var status = LoadStatus.notLoaded - - init(item: AuthenticationItem) { - self.loadables = item.loadables - self.title = item.title - } - - var body: some View { - Button { - Task { - status = .loading - do { - try await withThrowingTaskGroup(of: Void.self) { group in - for loadable in loadables { - group.addTask { - try await loadable.load() - } - } - try await group.waitForAll() - } - status = .loaded - } catch { - status = .failed - } - } - } label: { - buttonContent - } - } - - var buttonContent: some View { - HStack { - Text(title) - Spacer() - switch status { - case .loading: - ProgressView() - case .loaded: - Text("Loaded") - .foregroundColor(.green) - case .notLoaded: - Text("Tap to load") - .foregroundColor(.secondary) - case .failed: - Text("Failed to load") - .foregroundColor(.red) - } - } - } -} - -private extension URL { - static let worldImageryMapServer = URL(string: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer")! - static let hostedPointsLayer = URL(string: "https://rt-server107a.esri.com/server/rest/services/Hosted/PointsLayer/FeatureServer/0")! -} - -private class AuthenticationItem { - let title: String - let loadables: [Loadable] - - init(title: String, loadables: [Loadable]) { - self.title = title - self.loadables = loadables - } -} - -extension AuthenticationItem: Identifiable {} - -extension AuthenticationItem { - static func makeToken() -> AuthenticationItem { - AuthenticationItem( - title: "Token secured resource", - loadables: [ArcGISTiledLayer(url: .worldImageryMapServer)] - ) - } - - static func makeMultipleToken() -> AuthenticationItem { - AuthenticationItem( - title: "Multiple token secured resources", - loadables: [ - ArcGISTiledLayer(url: .worldImageryMapServer), - ServiceFeatureTable(url: .hostedPointsLayer) - ] - ) - } - - static func makeMultipleTokenSame() -> AuthenticationItem { - AuthenticationItem( - title: "Two of same token secured resources", - loadables: [ - ArcGISTiledLayer(url: .worldImageryMapServer), - ArcGISTiledLayer(url: .worldImageryMapServer) - ] - ) - } - - static func makePortal() -> AuthenticationItem { - AuthenticationItem( - title: "Portal", - loadables: [ - Portal.arcGISOnline(isLoginRequired: true) - ] - ) - } - - static func makeIWAPortal() -> AuthenticationItem { - AuthenticationItem( - title: "IWA Portal", - loadables: [Portal.init(url: URL(string: "https://dev0004327.esri.com/portal")!)] - ) - } - - static func makePKIMap() -> AuthenticationItem { - AuthenticationItem( - title: "PKI Map", - loadables: [Map(url: URL(string: "https://dev0002028.esri.com/portal/home/item.html?id=7fd418d5de2e4752b616a6463318cc4e")!)!] - ) - } - - static func makeEricPKIMap() -> AuthenticationItem { - AuthenticationItem( - title: "Eric PKI Map", - loadables: [Map(url: URL(string: "https://enterprise.esrigxdev.com/portal1091/sharing/rest/content/items/3e3b4cec95d143478cf187ba6ea4f7c4")!)!] - ) - } - - static func makeAll() -> [AuthenticationItem] { - [ - makeToken(), - makeMultipleToken(), - makeMultipleTokenSame(), - makePortal(), - makeIWAPortal(), - makePKIMap(), - makeEricPKIMap() - ] - } -} - -private extension OAuthConfiguration { - static let arcgisDotCom = OAuthConfiguration( - portalURL: .arcgisDotCom, - clientID: "W0DOrjQjPCL1C6LA", - redirectURL: URL(string: "runtimeswiftexamples://auth")! - ) -} - -private extension URL { - static let arcgisDotCom = URL(string: "https://www.arcgis.com")! -} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index e3ed35a52..e8d654441 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -17,8 +17,7 @@ struct Examples: View { /// The list of example lists. Allows for a hierarchical navigation model for examples. let lists: [ExampleList] = [ .geoview, - .views, - .authentication + .views ] var body: some View { @@ -52,11 +51,4 @@ extension ExampleList { AnyExample("Floating Panel", content: FloatingPanelExampleView()) ] ) - - static let authentication = Self( - name: "Authentication", - examples: [ - AnyExample("All Authentication Types", content: AuthenticationExampleView()) - ] - ) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 5744b7821..a3d7af2e5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -24,6 +24,13 @@ public final class Authenticator: ObservableObject { /// A value indicating whether we should prompt the user when encountering an untrusted host. var promptForUntrustedHosts: Bool + deinit { + observationTask?.cancel() + } + + // The task for the observation of the challenge queue. + private var observationTask: Task? + /// Creates an authenticator. /// - Parameters: /// - promptForUntrustedHosts: A value indicating whether we should prompt the user when @@ -35,8 +42,7 @@ public final class Authenticator: ObservableObject { ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations - // TODO: how to cancel this task? - Task { await observeChallengeQueue() } + observationTask = Task { [weak self] in await self?.observeChallengeQueue() } } /// Sets up new credential stores that will be persisted to the keychain. @@ -92,26 +98,14 @@ public final class Authenticator: ObservableObject { // A yield here helps alleviate the already presenting bug. await Task.yield() - if let queuedArcGISChallenge = queuedChallenge as? QueuedArcGISChallenge, - let url = queuedArcGISChallenge.arcGISChallenge.request.url, - let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { - // For an OAuth challenge, we create the credential and resume. - // Creating the OAuth credential will present the OAuth login view. - queuedArcGISChallenge.resume( - with: await Result { - .useCredential(try await .oauth(configuration: config)) - } - ) - } else { - // Set the current challenge, this should present the appropriate view. - currentChallenge = queuedChallenge + // Set the current challenge, this should present the appropriate view. + currentChallenge = queuedChallenge - // Wait for the queued challenge to finish. - await queuedChallenge.complete() - - // Reset the crrent challenge to `nil`, that will dismiss the view. - currentChallenge = nil - } + // Wait for the queued challenge to finish. + await queuedChallenge.complete() + + // Reset the crrent challenge to `nil`, that will dismiss the view. + currentChallenge = nil } } @@ -132,17 +126,6 @@ public final class Authenticator: ObservableObject { } extension Authenticator: AuthenticationChallengeHandler { - public func handleArcGISChallenge( - _ challenge: ArcGISAuthenticationChallenge - ) async throws -> ArcGISAuthenticationChallenge.Disposition { - // Queue up the challenge. - let queuedChallenge = QueuedArcGISChallenge(arcGISChallenge: challenge) - subject.send(queuedChallenge) - - // Wait for it to complete and return the resulting disposition. - return try await queuedChallenge.result.get() - } - public func handleNetworkChallenge( _ challenge: NetworkAuthenticationChallenge ) async -> NetworkAuthenticationChallengeDisposition { diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 496acc40d..7111f1d09 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -51,21 +51,19 @@ private struct AuthenticatorModifier: ViewModifier { @ViewBuilder func body(content: Content) -> some View { switch authenticator.currentChallenge { - case let challenge as QueuedArcGISChallenge: - content.modifier(UsernamePasswordViewModifier(challenge: challenge)) case let challenge as QueuedNetworkChallenge: switch challenge.kind { case .serverTrust: content.modifier(TrustHostViewModifier(challenge: challenge)) case .certificate: - content.modifier(CertificatePickerViewModifier(challenge: challenge)) + fatalError("implementation coming soon in another PR") case .login: - content.modifier(UsernamePasswordViewModifier(challenge: challenge)) + fatalError("implementation coming soon in another PR") } case .none: content default: - fatalError() + fatalError("unknown challenge type") } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift deleted file mode 100644 index d6205fa76..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ /dev/null @@ -1,291 +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 -import UniformTypeIdentifiers - -/// An object that provides the business logic for the workflow of prompting the user for a -/// certificate and a password. -@MainActor -final private class CertificatePickerViewModel: ObservableObject { - /// The host that prompted the challenge. - let challengingHost: String - /// The challenge that requires a certificate to proceed. - let challenge: QueuedNetworkChallenge - - /// The URL of the certificate that the user chose. - private var certificateURL: URL? - - /// A Boolean value indicating whether to show the prompt. - @Published var showPrompt = true - /// A Boolean value indicating whether to show the certificate file picker. - @Published var showPicker = false - /// A Boolean value indicating whether to show the password field view. - @Published var showPassword = false - /// A Boolean value indicating whether to display the error. - @Published var showCertificateImportError = false - - /// Creates a certificate picker view model. - /// - Parameter challenge: The challenge that requires a certificate. - init(challenge: QueuedNetworkChallenge) { - self.challenge = challenge - challengingHost = challenge.networkChallenge.host - } - - /// Proceeds to show the file picker. This should be called after the prompt that notifies the - /// user that a certificate must be selected. - func proceedFromPrompt() { - showPicker = true - } - - /// Proceeds to show the user the password form. This should be called after the user selects - /// a certificate. - /// - Parameter url: The URL of the certificate that the user chose. - func proceed(withCertificateURL url: URL) { - certificateURL = url - showPassword = true - } - - /// Attempts to use the certificate and password to respond to the challenge. - /// - Parameter password: The password for the certificate. - func proceed(withPassword password: String) { - guard let certificateURL = certificateURL else { - preconditionFailure() - } - - Task { - do { - challenge.resume(with: .useCredential(try .certificate(at: certificateURL, password: password))) - } catch { - // This is required to prevent an "already presenting" error. - try? await Task.sleep(nanoseconds: 100_000) - showCertificateImportError = true - } - } - } - - /// Cancels the challenge. - func cancel() { - challenge.resume(with: .cancelAuthenticationChallenge) - } -} - -/// A view modifier that presents a certificate picker workflow. -struct CertificatePickerViewModifier: ViewModifier { - /// Creates a certificate picker view modifier. - /// - Parameter challenge: The challenge that requires a certificate. - init(challenge: QueuedNetworkChallenge) { - viewModel = CertificatePickerViewModel(challenge: challenge) - } - - /// The view model. - @ObservedObject private var viewModel: CertificatePickerViewModel - - func body(content: Content) -> some View { - content - .promptBrowseCertificate( - isPresented: $viewModel.showPrompt, - viewModel: viewModel - ) - .certificateFilePicker( - isPresented: $viewModel.showPicker, - viewModel: viewModel - ) - .passwordSheet( - isPresented: $viewModel.showPassword, - viewModel: viewModel - ) - .alertCertificateImportError( - isPresented: $viewModel.showCertificateImportError, - viewModel: viewModel - ) - } -} - -private extension UTType { - /// A `UTType` that represents a pfx file. - static let pfx = UTType(filenameExtension: "pfx")! -} - -private extension View { - /// Displays a prompt to the user to let them know that picking a certificate is required. - /// - Parameters: - /// - isPresented: A Boolean value indicating if the view is presented. - /// - viewModel: The view model associated with the view. - @MainActor - @ViewBuilder - func promptBrowseCertificate( - isPresented: Binding, - viewModel: CertificatePickerViewModel - ) -> some View { - alert("Certificate Required", isPresented: isPresented, presenting: viewModel.challengingHost) { _ in - Button("Browse For Certificate") { - viewModel.proceedFromPrompt() - } - Button("Cancel", role: .cancel) { - viewModel.cancel() - } - } message: { _ in - Text("A certificate is required to access content on \(viewModel.challengingHost).") - } - } -} - -private extension View { - /// Displays a sheet that allows the user to select a certificate file. - /// - Parameters: - /// - isPresented: A Boolean value indicating if the view is presented. - /// - viewModel: The view model associated with the view. - @MainActor - @ViewBuilder - func certificateFilePicker( - isPresented: Binding, - viewModel: CertificatePickerViewModel - ) -> some View { - sheet(isPresented: isPresented) { - DocumentPickerView(contentTypes: [.pfx]) { - viewModel.proceed(withCertificateURL: $0) - } onCancel: { - viewModel.cancel() - } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() - } - } -} - -private extension View { - /// Displays a sheet that allows the user to enter a password. - /// - Parameters: - /// - isPresented: A Boolean value indicating if the view is presented. - /// - viewModel: The view model associated with the view. - @MainActor - @ViewBuilder - func passwordSheet( - isPresented: Binding, - viewModel: CertificatePickerViewModel - ) -> some View { - sheet(isPresented: isPresented) { - EnterPasswordView() { password in - viewModel.proceed(withPassword: password) - } onCancel: { - viewModel.cancel() - } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() - } - } -} - -private extension View { - /// Displays an alert to notify that there was an error importing the certificate. - /// - Parameters: - /// - isPresented: A Boolean value indicating if the view is presented. - /// - viewModel: The view model associated with the view. - @MainActor - @ViewBuilder - func alertCertificateImportError( - isPresented: Binding, - viewModel: CertificatePickerViewModel - ) -> some View { - alert("Error importing certificate", isPresented: isPresented) { - Button("Try Again") { - viewModel.proceedFromPrompt() - } - Button("Cancel", role: .cancel) { - viewModel.cancel() - } - } message: { - Text("The certificate file or password was invalid.") - } - } -} - -/// A view that allows the user to enter a password. -struct EnterPasswordView: View { - @Environment(\.dismiss) var dismissAction - - /// The password that the user entered. - @State var password: String = "" - - /// The action to call once the user has completed entering the password. - var onContinue: (String) -> Void - - /// The action to call if the user cancels. - var onCancel: () -> Void - - /// A Boolean value indicating whether the password field has focus. - @FocusState var isPasswordFocused: Bool - - var body: some View { - NavigationView { - Form { - Section { - VStack { - Text("Please enter a password for the chosen certificate.") - .fixedSize(horizontal: false, vertical: true) - } - .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) - } - - Section { - SecureField("Password", text: $password) - .focused($isPasswordFocused) - .textContentType(.password) - .submitLabel(.go) - .onSubmit { - dismissAction() - onContinue(password) - } - } - .autocapitalization(.none) - .disableAutocorrection(true) - - Section { - okButton - } - } - .navigationTitle("Certificate") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - dismissAction() - onCancel() - } - } - } - .onAppear { - // Workaround for Apple bug - FB9676178. - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - isPasswordFocused = true - } - } - } - } - - /// The "OK" button. - private var okButton: some View { - Button(action: { - dismissAction() - onContinue(password) - }, label: { - Text("OK") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundColor(.white) - }) - .disabled(password.isEmpty) - .listRowBackground(!password.isEmpty ? Color.accentColor : Color.gray) - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift deleted file mode 100644 index 7a15a56fc..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift +++ /dev/null @@ -1,60 +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 -import UniformTypeIdentifiers - -/// A view that allows the user to browse to and select a document. -/// This view wraps a `UIDocumentPickerViewController`. -struct DocumentPickerView: UIViewControllerRepresentable { - /// The content types that are allowed to be selected. - var contentTypes: [UTType] - /// The closure called when a document is selected. - var onPickDocument: (URL) -> Void - /// The closure called when the user cancels. - var onCancel: () -> Void - - func makeCoordinator() -> Coordinator { - Coordinator(onPickDocument: onPickDocument, onCancel: onCancel) - } - - func makeUIViewController(context: Context) -> UIDocumentPickerViewController { - let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes) - documentPicker.delegate = context.coordinator - return documentPicker - } - - func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} -} - -extension DocumentPickerView { - /// The coordinator for the document picker view that acts as a delegate to the underlying - /// `UIDocumentPickerViewController`. - final class Coordinator: NSObject, UIDocumentPickerDelegate { - var onPickDocument: (URL) -> Void - var onCancel: () -> Void - - init(onPickDocument: @escaping (URL) -> Void, onCancel: @escaping () -> Void) { - self.onPickDocument = onPickDocument - self.onCancel = onCancel - } - - func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { - onCancel() - } - - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { - onPickDocument(url) - } - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift deleted file mode 100644 index a915bba82..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.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 Foundation -import ArcGIS - -/// An object that represents an ArcGIS authentication challenge in the queue of challenges. -final class QueuedArcGISChallenge: QueuedChallenge { - /// The ArcGIS authentication challenge. - let arcGISChallenge: ArcGISAuthenticationChallenge - - /// Creates a `QueuedArcGISChallenge`. - /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. - init(arcGISChallenge: ArcGISAuthenticationChallenge) { - self.arcGISChallenge = arcGISChallenge - } - - /// Resumes the challenge with a result. - /// - Parameter result: The result of the challenge. - func resume(with result: Result) { - guard _result == nil else { return } - _result = result - } - - /// Cancels the challenge. - func cancel() { - guard _result == nil else { return } - _result = .success(.cancelAuthenticationChallenge) - } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed private var _result: Result? - - /// The result of the challenge. - var result: Result { - get async { - await $_result - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await result - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift deleted file mode 100644 index 6a21a104b..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/SheetViewModifier.swift +++ /dev/null @@ -1,40 +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 view modifier that presents a sheet without waiting for a condition. -struct SheetViewModifier: ViewModifier { - // Even though we will present it right away we need to use a state variable for this. - // Using a constant has 2 issues. One, it won't animate. Two, when challenging for multiple - // endpoints at a time, and the challenges stack up, you can end up with a "already presenting" - // error. - @State private var isPresented = false - var sheetContent: () -> SheetContent - - func body(content: Content) -> some View { - withAnimation { - content - .task { isPresented = true } - .sheet(isPresented: $isPresented, content: sheetContent) - } - } -} - -extension View { - /// Presents a sheet without waiting for a condition. - @ViewBuilder - func sheet(content: @escaping () -> Content) -> some View { - modifier(SheetViewModifier(sheetContent: content)) - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift deleted file mode 100644 index a288e0d72..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ /dev/null @@ -1,262 +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 -import ArcGIS - -/// A type that provides the business logic for a view that prompts the user to login with a -/// username and password. -protocol UsernamePasswordViewModel: ObservableObject { - /// The username. - var username: String { get set } - /// The password. - var password: String { get set } - /// A Boolean value indicating if the sign-in button is enabled. - var signinButtonEnabled: Bool { get } - /// A Boolean value indicating if the form is enabled. - var formEnabled: Bool { get } - /// The host that initiated the challenge. - var challengingHost: String { get } - - /// Attempts to log in with a username and password. - func signIn() - /// Cancels the challenge. - func cancel() -} - -/// A view modifier that prompts a user to login with a username and password. -struct UsernamePasswordViewModifier: ViewModifier { - /// The view model. - let viewModel: ViewModel - - /// Creates a `UsernamePasswordViewModifier` with a queued network challenge. - init(challenge: QueuedNetworkChallenge) where ViewModel == NetworkCredentialUsernamePasswordViewModel { - viewModel = NetworkCredentialUsernamePasswordViewModel(challenge: challenge) - } - - /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. - init(challenge: QueuedArcGISChallenge) where ViewModel == TokenCredentialUsernamePasswordViewModel { - viewModel = TokenCredentialUsernamePasswordViewModel(challenge: challenge) - } - - /// A Boolean value indicating whether or not the prompt to login is displayed. - @State var isPresented = false - - func body(content: Content) -> some View { - content - .task { isPresented = true } - .sheet(isPresented: $isPresented) { - UsernamePasswordView(viewModel: viewModel) - } - } -} - -/// A view that prompts a user to login with a username and password. -private struct UsernamePasswordView: View { - /// Creates the view. - /// - Parameters: - /// - viewModel: The view model. - init(viewModel: ViewModel) { - _viewModel = ObservedObject(initialValue: viewModel) - } - - @Environment(\.dismiss) var dismissAction - - /// The view model. - @ObservedObject private var viewModel: ViewModel - - /// The focused field. - @FocusState private var focusedField: Field? - - var body: some View { - NavigationView { - Form { - Section { - VStack { - person - Text("You must sign in to access '\(viewModel.challengingHost)'") - .fixedSize(horizontal: false, vertical: true) - } - .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) - } - - Section { - TextField("Username", text: $viewModel.username) - .focused($focusedField, equals: .username) - .textContentType(.username) - .submitLabel(.next) - .onSubmit { focusedField = .password } - SecureField("Password", text: $viewModel.password) - .focused($focusedField, equals: .password) - .textContentType(.password) - .submitLabel(.go) - .onSubmit { viewModel.signIn() } - } - .autocapitalization(.none) - .disableAutocorrection(true) - - Section { - signinButton - } - } - .disabled(!viewModel.formEnabled) - .navigationTitle("Sign In") - .navigationBarTitleDisplayMode(.inline) - .interactiveDismissDisabled() - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - dismissAction() - viewModel.cancel() - } - } - } - .onAppear { - // Workaround for Apple bug - FB9676178. - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - focusedField = .username - } - } - } - } - - /// An image used in the form. - private var person: some View { - Image(systemName: "person.circle") - .resizable() - .frame(width: 150, height: 150) - .shadow( - color: .gray.opacity(0.4), - radius: 3, - x: 1, - y: 2 - ) - } - - /// The sign-in button. - private var signinButton: some View { - Button(action: { - dismissAction() - viewModel.signIn() - }, label: { - if viewModel.formEnabled { - Text("Sign In") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundColor(.white) - } else { - ProgressView() - .frame(maxWidth: .infinity, alignment: .center) - .tint(.white) - } - }) - .disabled(!viewModel.signinButtonEnabled) - .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) - } -} - -private extension UsernamePasswordView { - /// A type that represents the fields in the user name and password sign-in form. - enum Field: Hashable { - /// The username field. - case username - /// The password field. - case password - } -} - -/// A view model that has the business logic for handling a token challenge. -class TokenCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { - /// The associated challenge. - private let challenge: QueuedArcGISChallenge - - /// Creates a `TokenCredentialUsernamePasswordViewModel`. - /// - Parameter challenge: The associated challenge. - init(challenge: QueuedArcGISChallenge) { - self.challenge = challenge - } - - @Published var username = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var password = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var signinButtonEnabled = false - @Published var formEnabled: Bool = true - - private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty - } - - var challengingHost: String { - challenge.arcGISChallenge.request.url!.host! - } - - func signIn() { - formEnabled = false - Task { - challenge.resume( - with: await Result { - .useCredential( - try await .token(challenge: challenge.arcGISChallenge, username: username, password: password) - ) - } - ) - } - } - - func cancel() { - formEnabled = false - challenge.cancel() - } -} - -/// A view model that has the business logic for handling a network challenge. -class NetworkCredentialUsernamePasswordViewModel: UsernamePasswordViewModel { - /// The associated challenge. - private let challenge: QueuedNetworkChallenge - - /// Creates a `NetworkCredentialUsernamePasswordViewModel`. - /// - Parameter challenge: The associated challenge. - init(challenge: QueuedNetworkChallenge) { - self.challenge = challenge - } - - @Published var username = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var password = "" { - didSet { updateSigninButtonEnabled() } - } - @Published var signinButtonEnabled = false - @Published var formEnabled: Bool = true - - private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty - } - - var challengingHost: String { - challenge.networkChallenge.host - } - - func signIn() { - formEnabled = false - challenge.resume(with: .useCredential(.login(username: username, password: password))) - } - - func cancel() { - formEnabled = false - challenge.resume(with: .cancelAuthenticationChallenge) - } -} From 9c4a722ac6c03d374d8c63a5961966db5c16dc79 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 13 Jul 2022 14:20:50 -0600 Subject: [PATCH 1141/1722] add tests --- .../Authentication/Authenticator.swift | 5 +- .../QueuedNetworkChallenge.swift | 50 +++++++++++++------ .../TrustHostViewModifier.swift | 11 ++-- .../TrustHostViewModifierTests.swift | 27 ++++++++++ 4 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index a3d7af2e5..c7f32084f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -135,8 +135,11 @@ extension Authenticator: AuthenticationChallengeHandler { return .performDefaultHandling } + guard let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) else { + return .performDefaultHandling + } + // Queue up the challenge. - let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) subject.send(queuedChallenge) // Wait for it to complete and return the resulting disposition. diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index 8b4eeeb22..618aac638 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -16,13 +16,18 @@ import ArcGIS /// An object that represents a network authentication challenge in the queue of challenges. final class QueuedNetworkChallenge: QueuedChallenge { - /// The associated network authentication challenge. - let networkChallenge: NetworkAuthenticationChallenge + /// The host that prompted the challenge. + let host: String + /// The kind of challenge. + let kind: Kind /// Creates a `QueuedNetworkChallenge`. - /// - Parameter networkChallenge: The associated network authentication challenge. - init(networkChallenge: NetworkAuthenticationChallenge) { - self.networkChallenge = networkChallenge + /// - Parameters: + /// - host: The host that prompted the challenge. + /// - kind: The kind of challenge. + init(host: String, kind: Kind) { + self.host = host + self.kind = kind } /// Resumes the queued challenge. @@ -48,7 +53,20 @@ final class QueuedNetworkChallenge: QueuedChallenge { public func complete() async { _ = await disposition } - +} + +extension QueuedNetworkChallenge { + /// Creates a `QueuedNetworkChallenge`. + /// - Parameter networkChallenge: The associated network authentication challenge. + convenience init?(networkChallenge: NetworkAuthenticationChallenge) { + guard let kind = Kind(networkChallenge.kind) else { + return nil + } + self.init(host: networkChallenge.host, kind: kind) + } +} + +extension QueuedNetworkChallenge { /// An enumeration that describes the kind of challenge. enum Kind { /// A challenge for an untrusted host. @@ -58,18 +76,22 @@ final class QueuedNetworkChallenge: QueuedChallenge { /// A challenge that requires a client certificate. case certificate } - - /// The kind of challenge. - var kind: Kind { - switch networkChallenge.kind { +} + +extension QueuedNetworkChallenge.Kind { + /// Creates an instance. + /// - Parameter networkAuthenticationChallengeKind: The kind of network authentication + /// challenge. + init?(_ networkAuthenticationChallengeKind: NetworkAuthenticationChallenge.Kind) { + switch networkAuthenticationChallengeKind { case .serverTrust: - return .serverTrust + self = .serverTrust case .ntlm, .basic, .digest: - return .login + self = .login case .pki: - return .certificate + self = .certificate case .htmlForm, .negotiate: - fatalError("TODO: ") + return nil } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index 2ad8521c4..33ada15a0 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -17,23 +17,20 @@ import SwiftUI struct TrustHostViewModifier: ViewModifier { /// Creates a `TrustHostViewModifier`. /// - Parameter challenge: The network authentication challenge for the untrusted host. + /// - Precondition: `challenge.kind` is equal to `serverTrust`. init(challenge: QueuedNetworkChallenge) { + precondition(challenge.kind == .serverTrust) self.challenge = challenge } /// The network authentication challenge for the untrusted host. let challenge: QueuedNetworkChallenge - /// The untrusted host. - var host: String { - challenge.networkChallenge.host - } - // Even though we will present it right away we need to use a state variable for this. // Using a constant has 2 issues. One, it won't animate. Two, when challenging for multiple // endpoints at a time, and the challenges stack up, you can end up with a "already presenting" // error. - @State private var isPresented: Bool = false + @State var isPresented: Bool = false func body(content: Content) -> some View { content @@ -51,7 +48,7 @@ struct TrustHostViewModifier: ViewModifier { challenge.resume(with: .cancelAuthenticationChallenge) } } message: { _ in - Text("The certificate provided by '\(host)' is not signed by a trusted authority.") + Text("The certificate provided by '\(challenge.host)' is not signed by a trusted authority.") } } } diff --git a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift new file mode 100644 index 000000000..a0ca65fd6 --- /dev/null +++ b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.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 XCTest +@testable import ArcGISToolkit + +@MainActor +class TrustHostViewModifierTets: XCTestCase { + func testInit() async { + let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) + // Tests the initial state. + let modifier = TrustHostViewModifier(challenge: challenge) + XCTAssertIdentical(modifier.challenge, challenge) + XCTAssertFalse(modifier.isPresented) + } +} From 62ee2e22c8249e14037a2c7eb2fb0b4c5f413cd7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 13 Jul 2022 14:29:28 -0600 Subject: [PATCH 1142/1722] add test --- .../AuthenticatorTests.swift | 1 - .../QueuedNetworkChallengeTests.swift | 37 +++++++++++++++++++ .../TrustHostViewModifierTests.swift | 3 +- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index f1935f34e..d326e6076 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI import XCTest @testable import ArcGISToolkit import ArcGIS diff --git a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift new file mode 100644 index 000000000..a8c40352a --- /dev/null +++ b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift @@ -0,0 +1,37 @@ +// 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 XCTest +@testable import ArcGISToolkit + +class QueuedNetworkChallengeTests: XCTestCase { + func testInit() { + let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) + XCTAssertEqual(challenge.host, "host.com") + XCTAssertEqual(challenge.kind, .serverTrust) + } + + func testResumeAndComplete() async { + let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) + challenge.resume(with: .useCredential(.serverTrust)) + let disposition = await challenge.disposition + XCTAssertEqual(disposition, .useCredential(.serverTrust)) + + // Make sure multiple simultaneous listeners can await the completion. + let t1 = Task { await challenge.complete() } + let t2 = Task { await challenge.complete() } + await t1.value + await t2.value + } +} diff --git a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift index a0ca65fd6..5d868919a 100644 --- a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift +++ b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift @@ -11,13 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI import XCTest @testable import ArcGISToolkit @MainActor class TrustHostViewModifierTets: XCTestCase { - func testInit() async { + func testInit() { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) // Tests the initial state. let modifier = TrustHostViewModifier(challenge: challenge) From bb4f87d6e446b8ff7508d56307aef38983846316 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 13 Jul 2022 14:33:15 -0600 Subject: [PATCH 1143/1722] Update Examples/ExamplesApp/Info.plist --- Examples/ExamplesApp/Info.plist | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Examples/ExamplesApp/Info.plist b/Examples/ExamplesApp/Info.plist index 61d79be64..efc211a0c 100644 --- a/Examples/ExamplesApp/Info.plist +++ b/Examples/ExamplesApp/Info.plist @@ -46,10 +46,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - From c910bc7d9755f76323f166930b6dca6e3eaeec17 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 13 Jul 2022 14:34:34 -0600 Subject: [PATCH 1144/1722] Update Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift --- Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift index a8b0e81c6..1f4441b46 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchViewModel.swift @@ -414,7 +414,7 @@ private extension Symbol { static func searchResult() -> MarkerSymbol { let image = UIImage.mapPin let symbol = PictureMarkerSymbol(image: image) - symbol.offsetY = CGFloat(image.size.height / 2.0) + symbol.offsetY = Float(image.size.height / 2.0) return symbol } } From 763d5b3917ee80d10260e1149468b6ca2c208ae0 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 13 Jul 2022 14:36:44 -0600 Subject: [PATCH 1145/1722] remove unused file --- Sources/ArcGISToolkit/Extensions/Result.swift | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Extensions/Result.swift diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift deleted file mode 100644 index 118ea4631..000000000 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021 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. - -extension Result where Failure == Error { - /// Creates a result based on the outcome of the given task. If the task - /// succeeds, the result is `success`. If the task fails, the result is - /// `failure`. - init(awaiting task: () async throws -> Success) async { - do { - self = .success(try await task()) - } catch { - self = .failure(error) - } - } -} From 3d23bb789ec80f6a1049cae9cb4861d6bc7840a3 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 13 Jul 2022 14:57:22 -0600 Subject: [PATCH 1146/1722] skip tests if no apikey --- Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift | 1 + Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 651589e2c..c35c628a2 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -28,6 +28,7 @@ import Combine final class BasemapGalleryItemTests: XCTestCase { override func setUp() async throws { ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") + try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == nil) } func testInit() async throws { diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 64be3f0bc..dfc8b201f 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -28,6 +28,7 @@ import Combine class BasemapGalleryViewModelTests: XCTestCase { override func setUp() async throws { ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") + try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == nil) } let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ From e920bd846d3d29b02d6d7806e46a1c020b34ec6f Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 13 Jul 2022 15:05:15 -0600 Subject: [PATCH 1147/1722] skip for placeholder --- Tests/ArcGISToolkitTests/APIKey.swift | 19 +++++++++++++++++++ .../BasemapGalleryItemTests.swift | 2 +- .../BasemapGalleryViewModelTests.swift | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/APIKey.swift diff --git a/Tests/ArcGISToolkitTests/APIKey.swift b/Tests/ArcGISToolkitTests/APIKey.swift new file mode 100644 index 000000000..b820199b1 --- /dev/null +++ b/Tests/ArcGISToolkitTests/APIKey.swift @@ -0,0 +1,19 @@ +// 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 APIKey { + /// The placeholder APIKey. This is not a valid API key. + static let placeholder = APIKey("<#API Key#>") +} diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index c35c628a2..07d917bf7 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -28,7 +28,7 @@ import Combine final class BasemapGalleryItemTests: XCTestCase { override func setUp() async throws { ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") - try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == nil) + try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) } func testInit() async throws { diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index dfc8b201f..3accaa95a 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -28,7 +28,7 @@ import Combine class BasemapGalleryViewModelTests: XCTestCase { override func setUp() async throws { ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") - try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == nil) + try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) } let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ From ecc9b48b511aaf7a3d7bf6965ad1bed5bb76f533 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Jul 2022 11:30:41 -0700 Subject: [PATCH 1148/1722] Expand element results --- .../UtilityNetworkTrace.swift | 117 ++++++++++++++---- .../UtilityNetworkTraceViewModel.swift | 14 ++- .../UtilityNetworkTraceViewModelTrace.swift | 7 +- .../Extensions/UtilityAssetType.swift | 24 ++++ .../Extensions/UtilityElement.swift | 24 ++++ 5 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift create mode 100644 Sources/ArcGISToolkit/Extensions/UtilityElement.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index e77003339..186035f39 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -43,6 +43,8 @@ public struct UtilityNetworkTrace: View { private enum TraceViewingActivity: Hashable { /// The user is viewing the list of available trace options. case viewingAdvancedOptions + /// The user is viewing a list of element results, grouped by asset group and asset type. + case viewingElementGroup([String: [UtilityElement]]) /// The user is viewing the list of element results. case viewingElementResults /// The user is viewing the list of function results. @@ -122,6 +124,27 @@ public struct UtilityNetworkTrace: View { .buttonStyle(.bordered) } + /// Displays information about a chosen asset group. + @ViewBuilder private var assetGroupDetail: some View { + if let assetGroup = selectedAssetGroup { + makeBackButton(title: elementResultsTitle) { + currentActivity = .viewingTraces(.viewingElementResults) + } + makeDetailSectionHeader( + title: assetGroup.first?.value.first?.assetGroup.name ?? "Unnamed Asset Group" + ) + List { + ForEach(assetGroup.sorted(by: { $0.key < $1.key }), id: \.key) { assetTypeGroup in + Section(assetTypeGroup.key) { + ForEach(assetTypeGroup.value) { element in + Text(element.objectID.description) + } + } + } + } + } + } + /// Displays the list of available named trace configurations. @ViewBuilder private var configurationsList: some View { if viewModel.configurations.isEmpty { @@ -179,7 +202,7 @@ public struct UtilityNetworkTrace: View { configurationsList } } - Section("Starting Points") { + Section(startingPointsTitle) { Button { currentActivity = .creatingTrace(.addingStartingPoints) } label: { @@ -258,9 +281,9 @@ public struct UtilityNetworkTrace: View { Text(traceName) } List { - Section("Element Result") { + Section(elementResultsTitle) { DisclosureGroup( - viewModel.selectedTrace?.assets.map({ $0.value.count }).reduce(0, +).description ?? "0", + viewModel.selectedTrace?.assetCount.description ?? "0", isExpanded: Binding( get: { isFocused(traceViewingActivity: .viewingElementResults) }, set: { currentActivity = .viewingTraces($0 ? .viewingElementResults : nil) } @@ -268,11 +291,16 @@ public struct UtilityNetworkTrace: View { ) { ForEach( (viewModel.selectedTrace?.assets ?? [:]).sorted(by: { $0.key < $1.key }), id: \.key - ) { asset in + ) { assetGroup in HStack { - Text(asset.key) + Text(assetGroup.key) Spacer() - Text(asset.value.count.description) + Text(assetGroup.value.compactMap({ $0.value.count }).reduce(0, +).description) + } + .foregroundColor(.blue) + .contentShape(Rectangle()) + .onTapGesture { + currentActivity = .viewingTraces(.viewingElementGroup(assetGroup.value)) } } } @@ -317,7 +345,7 @@ public struct UtilityNetworkTrace: View { } } } - makeZoomToButtom { + makeZoomToButton { if let resultEnvelope = GeometryEngine.combineExtents(of: [ viewModel.selectedTrace?.utilityGeometryTraceResult?.multipoint, viewModel.selectedTrace?.utilityGeometryTraceResult?.polygon, @@ -347,21 +375,12 @@ public struct UtilityNetworkTrace: View { /// Displays information about a chosen starting point. @ViewBuilder private var startingPointDetail: some View { - Button { + makeBackButton(title: startingPointsTitle) { currentActivity = .creatingTrace(.viewingStartingPoints) - } label: { - Label { - Text("Back") - } icon: { - Image(systemName: "chevron.backward") - } } - .padding() - .frame(maxWidth: .infinity, alignment: .leading) - Text(selectedStartingPoint?.utilityElement.assetType.name ?? "Unnamed") - .font(.title3) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .center) + makeDetailSectionHeader( + title: selectedStartingPoint?.utilityElement.assetType.name ?? "Unnamed Asset Type" + ) List { if selectedStartingPoint?.utilityElement.networkSource.kind == .edge { Section("Fraction Along Edge") { @@ -407,7 +426,7 @@ public struct UtilityNetworkTrace: View { } } } - makeZoomToButtom { + makeZoomToButton { if let selectedStartingPoint { viewpoint = Viewpoint(targetExtent: selectedStartingPoint.extent) } @@ -487,8 +506,13 @@ public struct UtilityNetworkTrace: View { default: newTraceTab } - case .viewingTraces: - resultsTab + case .viewingTraces(let activity): + switch activity { + case .viewingElementGroup: + assetGroupDetail + default: + resultsTab + } } } .background(Color(uiColor: .systemGroupedBackground)) @@ -528,6 +552,15 @@ public struct UtilityNetworkTrace: View { return "Trace \(index+1) of \(viewModel.completedTraces.count.description)" } + /// The selected utility element asset group. + private var selectedAssetGroup: [String: [UtilityElement]]? { + if case let .viewingTraces(activity) = currentActivity, + case let .viewingElementGroup(elementGroup) = activity { + return elementGroup + } + return nil + } + /// The starting point being inspected (if one exists). private var selectedStartingPoint: UtilityNetworkTraceStartingPoint? { if case let .creatingTrace(activity) = currentActivity, @@ -559,13 +592,37 @@ public struct UtilityNetworkTrace: View { return false } + /// Returns a "Back" button that performs a specified action when pressed. + /// - Parameter title: The button's title. + /// - Parameter action: The action to be performed. + /// - Returns: The configured button. + private func makeBackButton(title: String, _ action: @escaping () -> Void) -> some View { + Button { action() } label: { + Label { + Text(title) + } icon: { + Image(systemName: "chevron.backward") + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + + /// Returns a section header. + /// - Parameter title: The title of the header. + /// - Returns: The configured title. + private func makeDetailSectionHeader(title: String) -> some View { + Text(title) + .font(.title3) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .center) + } + /// Returns a "Zoom To" button that performs a specified action when pressed. /// - Parameter action: The action to be performed. /// - Returns: The configured button. - private func makeZoomToButtom(_ action: @escaping () -> Void) -> some View { - Button { - action() - } label: { + private func makeZoomToButton(_ action: @escaping () -> Void) -> some View { + Button { action() } label: { Label { Text("Zoom To") } icon: { @@ -573,4 +630,10 @@ public struct UtilityNetworkTrace: View { } } } + + /// Title for the element results section + private let elementResultsTitle = "Element Results" + + /// Title for the starting points section + private let startingPointsTitle = "Starting Points" } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index a5d9c7b8b..1e894fa20 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -291,18 +291,24 @@ import SwiftUI return false } - var assets = [String: [UtilityElement]]() + var assets = [String: [String: [UtilityElement]]]() for result in traceResults { switch result { case let result as UtilityElementTraceResult: result.elements.forEach { element in - var assetsInGroup = assets[element.assetGroup.name, default: []] - assetsInGroup.append(element) + var assetGroup = assets[element.assetGroup.name, default: [:]] + var assetTypeGroup = assetGroup[element.assetType.name, default: []] + assetTypeGroup.append(element) + assetGroup.updateValue( + assetTypeGroup, + forKey: element.assetType.name + ) assets.updateValue( - assetsInGroup, + assetGroup, forKey: element.assetGroup.name ) } + pendingTrace.assetCount = result.elements.count pendingTrace.assets = assets case let result as UtilityGeometryTraceResult: let createGraphic: ((Geometry, SimpleLineSymbol.Style, Color) -> (Graphic)) = { geometry, style, color in diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 702e952b3..53d4e3212 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -17,8 +17,11 @@ import SwiftUI extension UtilityNetworkTraceViewModel { /// A trace performed on a utility network. struct Trace { - /// A collection of all elements returned in the trace, grouped by asset group. - var assets = [String: [UtilityElement]]() + /// The number of assets returned by the trace. + var assetCount: Int = 0 + + /// A collection of all elements returned in the trace, grouped by asset group and asset type. + var assets = [String: [String: [UtilityElement]]]() /// A user given color for the trace with a default value of green. var color: Color = .green { diff --git a/Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift b/Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift new file mode 100644 index 000000000..e1248a701 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift @@ -0,0 +1,24 @@ +// 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 UtilityAssetType: Hashable, Identifiable { + public static func == (lhs: ArcGIS.UtilityAssetType, rhs: ArcGIS.UtilityAssetType) -> Bool { + lhs.hashValue == rhs.hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(hashValue) + } +} diff --git a/Sources/ArcGISToolkit/Extensions/UtilityElement.swift b/Sources/ArcGISToolkit/Extensions/UtilityElement.swift new file mode 100644 index 000000000..b57c07f2c --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/UtilityElement.swift @@ -0,0 +1,24 @@ +// 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 UtilityElement: Hashable, Identifiable { + public static func == (lhs: ArcGIS.UtilityElement, rhs: ArcGIS.UtilityElement) -> Bool { + lhs.globalID == rhs.globalID + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(globalID) + } +} From 63f6f630e0e4e91dd00f86f582ba40271d8b87a5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 13 Jul 2022 14:59:35 -0700 Subject: [PATCH 1149/1722] Update asset count --- Tests/ArcGISToolkitTests/CompassTests.swift | 173 -------------------- 1 file changed, 173 deletions(-) delete mode 100644 Tests/ArcGISToolkitTests/CompassTests.swift diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift deleted file mode 100644 index fc7944dc8..000000000 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ /dev/null @@ -1,173 +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 -import SwiftUI -import XCTest -@testable import ArcGISToolkit - -class CompassTests: XCTestCase { - /// Asserts that viewpoint rotations are correctly adjusted to provide for natural compass rotation - /// behavior. - func testAdjustedRotation() { - var viewpoint = getViewpoint(.zero) - XCTAssertEqual(viewpoint.compassHeading, .zero) - viewpoint = getViewpoint(45) - XCTAssertEqual(viewpoint.compassHeading, 315) - viewpoint = getViewpoint(315) - XCTAssertEqual(viewpoint.compassHeading, 45) - viewpoint = getViewpoint(180) - XCTAssertEqual(viewpoint.compassHeading, 180) - } - - /// Asserts that accessibility labels are properly generated. - func testCompassHeadingDescription() { - var viewpoint = getViewpoint(0.0) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 0 degrees north" - ) - viewpoint = getViewpoint(23) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 337 degrees northwest" - ) - viewpoint = getViewpoint(68) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 292 degrees west" - ) - viewpoint = getViewpoint(113) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 247 degrees southwest" - ) - viewpoint = getViewpoint(158) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 202 degrees south" - ) - viewpoint = getViewpoint(203) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 157 degrees southeast" - ) - viewpoint = getViewpoint(248) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 112 degrees east" - ) - viewpoint = getViewpoint(293) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 67 degrees northeast" - ) - viewpoint = getViewpoint(293) - XCTAssertEqual( - viewpoint.compassHeadingDescription, - "Compass, heading 67 degrees northeast" - ) - } - - /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide - /// is disabled. - func testHiddenWithAutoHideOff() { - let initialValue = 0.0 - let finalValue = 90.0 - var _viewpoint: Viewpoint? = getViewpoint(initialValue) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint, autoHide: false) - XCTAssertFalse(compass.shouldHide) - _viewpoint = getViewpoint(finalValue) - XCTAssertFalse(compass.shouldHide) - } - - /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide - /// is enabled. - func testHiddenWithAutoHideOn() { - let initialValue = 0.0 - let finalValue = 90.0 - var _viewpoint: Viewpoint? = getViewpoint(initialValue) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) - XCTAssertTrue(compass.shouldHide) - _viewpoint = getViewpoint(finalValue) - XCTAssertFalse(compass.shouldHide) - } - - /// Asserts that the compass correctly initializes when given a nil viewpoint - func testInit() { - var _viewpoint: Viewpoint? = nil - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(compass.shouldHide, true) - } - - /// Asserts that the compass correctly initializes when given a nil viewpoint, and autoHide is disabled. - func testInitNoAutoHide() { - var _viewpoint: Viewpoint? = nil - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint, autoHide: false) - XCTAssertEqual(compass.shouldHide, false) - } - - /// Asserts that the compass correctly initializes when given only a viewpoint. - func testInitWithViewpoint() { - var _viewpoint: Viewpoint? = getViewpoint(0.0) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(compass.shouldHide, true) - } - - /// Asserts that the compass correctly initializes when given only a viewpoint. - func testInitWithViewpointAndAutoHide() { - let autoHide = false - var _viewpoint: Viewpoint? = getViewpoint(0) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass( - viewpoint: viewpoint, - autoHide: autoHide - ) - XCTAssertEqual(compass.shouldHide, false) - } - - func testResetHeading() { - let initialValue = 0.5 - let finalValue = 0.0 - var _viewpoint: Viewpoint? = getViewpoint(initialValue) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(_viewpoint?.rotation, initialValue) - compass.resetHeading() - XCTAssertEqual(_viewpoint?.rotation, finalValue) - } -} - -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 - } - - /// Builds viewpoints to use for tests. - /// - Parameter rotation: The rotation to use for the resulting viewpoint. - /// - Returns: A viewpoint object for tests. - func getViewpoint(_ rotation: Double) -> Viewpoint { - return Viewpoint(center: point, scale: scale, rotation: rotation) - } -} From 186bce3f78c34f9309aa066bab19ae28617d3771 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 13 Jul 2022 17:13:10 -0600 Subject: [PATCH 1150/1722] add viewmodel tests for username and password view modifier --- .../AuthenticatorModifier.swift | 2 +- .../UsernamePasswordViewModifier.swift | 221 ++++++++++++++++++ .../UsernamePasswordViewModelTests.swift | 70 ++++++ 3 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift create mode 100644 Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 7111f1d09..8a144fed6 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -58,7 +58,7 @@ private struct AuthenticatorModifier: ViewModifier { case .certificate: fatalError("implementation coming soon in another PR") case .login: - fatalError("implementation coming soon in another PR") + content.modifier(UsernamePasswordViewModifier(challenge: challenge)) } case .none: content diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift new file mode 100644 index 000000000..80d7ed006 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -0,0 +1,221 @@ +// 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 type that provides the business logic for a view that prompts the user to login with a +/// username and password. +final class UsernamePasswordViewModel: ObservableObject { + /// The username. + @Published var username = "" { + didSet { updateSigninButtonEnabled() } + } + /// The password. + @Published var password = "" { + didSet { updateSigninButtonEnabled() } + } + /// A Boolean value indicating if the sign-in button is enabled. + @Published var signinButtonEnabled = false + /// A Boolean value indicating if the form is enabled. + @Published var formEnabled: Bool = true + + /// The action to perform when the user signs in. This is a closure that takes a username + /// and password, respectively. + var signInAction: (String, String) -> Void + /// The action to perform when the user cancels. + var cancelAction: () -> Void + + /// Creates a `UsernamePasswordViewModel`. + /// - Parameters: + /// - challengingHost: The host that prompted the challenge. + /// - signInAction: The action to perform when the user signs in. This is a closure that takes + /// a username and password, respectively. + /// - cancelAction: The action to perform when the user cancels. + init( + challengingHost: String, + onSignIn signInAction: @escaping (String, String) -> Void, + onCancel cancelAction: @escaping () -> Void + ) { + self.challengingHost = challengingHost + self.signInAction = signInAction + self.cancelAction = cancelAction + } + + private func updateSigninButtonEnabled() { + signinButtonEnabled = !username.isEmpty && !password.isEmpty + } + + /// The host that initiated the challenge. + var challengingHost: String + + /// Attempts to log in with a username and password. + func signIn() { + formEnabled = false + signInAction(username, password) + } + + /// Cancels the challenge. + func cancel() { + formEnabled = false + cancelAction() + } +} + +/// A view modifier that prompts a user to login with a username and password. +struct UsernamePasswordViewModifier: ViewModifier { + /// The view model. + let viewModel: UsernamePasswordViewModel + + /// A Boolean value indicating whether or not the prompt to login is displayed. + @State var isPresented = false + + func body(content: Content) -> some View { + content + .task { isPresented = true } + .sheet(isPresented: $isPresented) { + UsernamePasswordView(viewModel: viewModel) + } + } +} + +extension UsernamePasswordViewModifier { + /// Creates a `UsernamePasswordViewModifier` with a queued network challenge. + init(challenge: QueuedNetworkChallenge) { + self.init( + viewModel: UsernamePasswordViewModel( + challengingHost: challenge.host, + onSignIn: { username, password in + challenge.resume(with: .useCredential(.login(username: username, password: password))) + }, + onCancel: { + challenge.resume(with: .cancelAuthenticationChallenge) + } + ) + ) + } +} + +/// A view that prompts a user to login with a username and password. +private struct UsernamePasswordView: View { + /// Creates the view. + /// - Parameters: + /// - viewModel: The view model. + init(viewModel: UsernamePasswordViewModel) { + _viewModel = ObservedObject(initialValue: viewModel) + } + + @Environment(\.dismiss) var dismissAction + + /// The view model. + @ObservedObject private var viewModel: UsernamePasswordViewModel + + /// The focused field. + @FocusState private var focusedField: Field? + + var body: some View { + NavigationView { + Form { + Section { + VStack { + person + Text("You must sign in to access '\(viewModel.challengingHost)'") + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + } + + Section { + TextField("Username", text: $viewModel.username) + .focused($focusedField, equals: .username) + .textContentType(.username) + .submitLabel(.next) + .onSubmit { focusedField = .password } + SecureField("Password", text: $viewModel.password) + .focused($focusedField, equals: .password) + .textContentType(.password) + .submitLabel(.go) + .onSubmit { viewModel.signIn() } + } + .autocapitalization(.none) + .disableAutocorrection(true) + + Section { + signinButton + } + } + .disabled(!viewModel.formEnabled) + .navigationTitle("Sign In") + .navigationBarTitleDisplayMode(.inline) + .interactiveDismissDisabled() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismissAction() + viewModel.cancel() + } + } + } + .onAppear { + // Workaround for Apple bug - FB9676178. + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + focusedField = .username + } + } + } + } + + /// An image used in the form. + private var person: some View { + Image(systemName: "person.circle") + .resizable() + .frame(width: 150, height: 150) + .shadow( + color: .gray.opacity(0.4), + radius: 3, + x: 1, + y: 2 + ) + } + + /// The sign-in button. + private var signinButton: some View { + Button(action: { + dismissAction() + viewModel.signIn() + }, label: { + if viewModel.formEnabled { + Text("Sign In") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundColor(.white) + } else { + ProgressView() + .frame(maxWidth: .infinity, alignment: .center) + .tint(.white) + } + }) + .disabled(!viewModel.signinButtonEnabled) + .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) + } +} + +private extension UsernamePasswordView { + /// A type that represents the fields in the user name and password sign-in form. + enum Field: Hashable { + /// The username field. + case username + /// The password field. + case password + } +} diff --git a/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift b/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift new file mode 100644 index 000000000..f8d50aea1 --- /dev/null +++ b/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift @@ -0,0 +1,70 @@ +// 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 XCTest +@testable import ArcGISToolkit + +@MainActor +class UsernamePasswordViewModelTests: XCTestCase { + func testViewModel() { + var signInCalled = false + func signIn(username: String, password: String) { + signInCalled = true + } + + var cancelCalled = false + func cancel() { + cancelCalled = true + } + + let model = UsernamePasswordViewModel( + challengingHost: "host.com", + onSignIn: { _, _ in + signInCalled = true + }, + onCancel: { + cancelCalled = true + } + ) + + XCTAssertEqual(model.challengingHost, "host.com") + XCTAssertNotNil(model.signInAction) + XCTAssertNotNil(model.cancelAction) + XCTAssertFalse(model.signinButtonEnabled) + XCTAssertTrue(model.formEnabled) + XCTAssertTrue(model.username.isEmpty) + XCTAssertTrue(model.password.isEmpty) + XCTAssertFalse(signInCalled) + XCTAssertFalse(cancelCalled) + + model.username = "abc" + XCTAssertFalse(model.signinButtonEnabled) + + model.password = "123" + XCTAssertTrue(model.signinButtonEnabled) + + model.password = "" + XCTAssertFalse(model.signinButtonEnabled) + + model.password = "123" + XCTAssertTrue(model.signinButtonEnabled) + + model.signIn() + XCTAssertFalse(model.formEnabled) + XCTAssertTrue(signInCalled) + XCTAssertFalse(cancelCalled) + + model.cancel() + XCTAssertTrue(cancelCalled) + } +} From 8ea9bcc06019927c5dbd6834f8ac2c199b862ff0 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 12:15:56 -0600 Subject: [PATCH 1151/1722] api updates --- .../AuthenticatorModifier.swift | 3 + .../QueuedArcGISChallenge.swift | 57 +++++++++++++++++++ .../QueuedNetworkChallenge.swift | 4 +- .../UsernamePasswordViewModifier.swift | 21 +++++++ 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 8a144fed6..efcc3efdb 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -51,6 +51,9 @@ private struct AuthenticatorModifier: ViewModifier { @ViewBuilder func body(content: Content) -> some View { switch authenticator.currentChallenge { + case let challenge as QueuedArcGISChallenge: + //content.modifier(UsernamePasswordViewModifier() + fatalError() case let challenge as QueuedNetworkChallenge: switch challenge.kind { case .serverTrust: diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift new file mode 100644 index 000000000..a915bba82 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift @@ -0,0 +1,57 @@ +// 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 Foundation +import ArcGIS + +/// An object that represents an ArcGIS authentication challenge in the queue of challenges. +final class QueuedArcGISChallenge: QueuedChallenge { + /// The ArcGIS authentication challenge. + let arcGISChallenge: ArcGISAuthenticationChallenge + + /// Creates a `QueuedArcGISChallenge`. + /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. + init(arcGISChallenge: ArcGISAuthenticationChallenge) { + self.arcGISChallenge = arcGISChallenge + } + + /// Resumes the challenge with a result. + /// - Parameter result: The result of the challenge. + func resume(with result: Result) { + guard _result == nil else { return } + _result = result + } + + /// Cancels the challenge. + func cancel() { + guard _result == nil else { return } + _result = .success(.cancelAuthenticationChallenge) + } + + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. + @Streamed private var _result: Result? + + /// The result of the challenge. + var result: Result { + get async { + await $_result + .compactMap({ $0 }) + .first(where: { _ in true })! + } + } + + public func complete() async { + _ = await result + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index 618aac638..62e859eac 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -88,10 +88,8 @@ extension QueuedNetworkChallenge.Kind { self = .serverTrust case .ntlm, .basic, .digest: self = .login - case .pki: + case .clientCertificate: self = .certificate - case .htmlForm, .negotiate: - return nil } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 80d7ed006..ff52de6f5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -104,6 +104,27 @@ extension UsernamePasswordViewModifier { ) ) } + + /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. + init(challenge: QueuedArcGISChallenge) { + self.init( + viewModel: UsernamePasswordViewModel( + challengingHost: challenge.host, + onSignIn: { username, password in + Task { + challenge.resume( + with: .success(.useCredential( + try await .token(challenge: challenge.arcGISChallenge, username: username, password: password) + )) + ) + } + }, + onCancel: { + challenge.resume(with: .cancelAuthenticationChallenge) + } + ) + ) + } } /// A view that prompts a user to login with a username and password. From 61f5524a71c13a161ce74c365769be64cda87993 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 12:28:28 -0600 Subject: [PATCH 1152/1722] token credential challenge --- .../Authentication/Authenticator.swift | 11 ++++++++ .../AuthenticatorModifier.swift | 3 +-- .../QueuedArcGISChallenge.swift | 5 ++++ .../Authentication/QueuedChallenge.swift | 3 +++ .../UsernamePasswordViewModifier.swift | 14 ++++++++--- Sources/ArcGISToolkit/Extensions/Result.swift | 25 +++++++++++++++++++ 6 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/Result.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index c7f32084f..7a8dad64e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -126,6 +126,17 @@ public final class Authenticator: ObservableObject { } extension Authenticator: AuthenticationChallengeHandler { + public func handleArcGISChallenge( + _ challenge: ArcGISAuthenticationChallenge + ) async throws -> ArcGISAuthenticationChallenge.Disposition { + // Queue up the challenge. + let queuedChallenge = QueuedArcGISChallenge(arcGISChallenge: challenge) + subject.send(queuedChallenge) + + // Wait for it to complete and return the resulting disposition. + return try await queuedChallenge.result.get() + } + public func handleNetworkChallenge( _ challenge: NetworkAuthenticationChallenge ) async -> NetworkAuthenticationChallengeDisposition { diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index efcc3efdb..6399b4839 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -52,8 +52,7 @@ private struct AuthenticatorModifier: ViewModifier { func body(content: Content) -> some View { switch authenticator.currentChallenge { case let challenge as QueuedArcGISChallenge: - //content.modifier(UsernamePasswordViewModifier() - fatalError() + content.modifier(UsernamePasswordViewModifier(challenge: challenge)) case let challenge as QueuedNetworkChallenge: switch challenge.kind { case .serverTrust: diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift index a915bba82..dabf71f72 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift @@ -51,6 +51,11 @@ final class QueuedArcGISChallenge: QueuedChallenge { } } + /// The host that prompted the challenge. + var host: String { + arcGISChallenge.request.url?.host ?? "" + } + public func complete() async { _ = await result } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index ddcf3a261..025600c48 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -15,6 +15,9 @@ import Foundation /// A type that represents a challenge in the queue of authentication challenges. protocol QueuedChallenge: AnyObject { + /// The host that prompted the challenge. + var host: String { get } + /// Waits for the challenge to complete. func complete() async } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index ff52de6f5..a27ce02c1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -113,14 +113,20 @@ extension UsernamePasswordViewModifier { onSignIn: { username, password in Task { challenge.resume( - with: .success(.useCredential( - try await .token(challenge: challenge.arcGISChallenge, username: username, password: password) - )) + with: await Result { + .useCredential( + try await .token( + challenge: challenge.arcGISChallenge, + username: username, + password: password + ) + ) + } ) } }, onCancel: { - challenge.resume(with: .cancelAuthenticationChallenge) + challenge.resume(with: .success(.cancelAuthenticationChallenge)) } ) ) diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift new file mode 100644 index 000000000..118ea4631 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -0,0 +1,25 @@ +// Copyright 2021 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. + +extension Result where Failure == Error { + /// Creates a result based on the outcome of the given task. If the task + /// succeeds, the result is `success`. If the task fails, the result is + /// `failure`. + init(awaiting task: () async throws -> Success) async { + do { + self = .success(try await task()) + } catch { + self = .failure(error) + } + } +} From 1355095eb674be28ce8ebed446c8b3cc53b52c13 Mon Sep 17 00:00:00 2001 From: R Olson Date: Thu, 14 Jul 2022 12:47:29 -0600 Subject: [PATCH 1153/1722] Update Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift Co-authored-by: Mark Dostal --- .../ArcGISToolkit/Components/Authentication/Authenticator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index c7f32084f..67829e453 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -89,7 +89,7 @@ public final class Authenticator: ObservableObject { // We have to set new sessions for URLCredential storage to respect the removed credentials // right away. ArcGISRuntimeEnvironment.urlSession = ArcGISURLSession(configuration: .default) - ArcGISRuntimeEnvironment.backgroundURLSession = ArcGISURLSession(configuration: .background(withIdentifier: "com.esri.com.arcgis.toolkit." + UUID().uuidString)) + ArcGISRuntimeEnvironment.backgroundURLSession = ArcGISURLSession(configuration: .background(withIdentifier: "com.esri.arcgis.toolkit." + UUID().uuidString)) } /// Observes the challenge queue and sets the current challenge. From 5412d08b04dfbb4686004a8d5179675ea9c022bf Mon Sep 17 00:00:00 2001 From: R Olson Date: Thu, 14 Jul 2022 12:48:08 -0600 Subject: [PATCH 1154/1722] Update Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift Co-authored-by: Mark Dostal --- .../Components/Authentication/TrustHostViewModifier.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index 33ada15a0..c4d304bce 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -40,7 +40,6 @@ struct TrustHostViewModifier: ViewModifier { } .alert("Certificate Trust Warning", isPresented: $isPresented, presenting: challenge) { _ in Button("Dangerous: Allow Connection", role: .destructive) { - isPresented = false challenge.resume(with: .useCredential(.serverTrust)) } Button("Cancel", role: .cancel) { From 95f1a2711d82c5c1db8929e12c87096c7d44b434 Mon Sep 17 00:00:00 2001 From: R Olson Date: Thu, 14 Jul 2022 12:48:26 -0600 Subject: [PATCH 1155/1722] Update Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift Co-authored-by: Mark Dostal --- .../Components/Authentication/TrustHostViewModifier.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift index c4d304bce..b93c24c35 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TrustHostViewModifier.swift @@ -43,7 +43,6 @@ struct TrustHostViewModifier: ViewModifier { challenge.resume(with: .useCredential(.serverTrust)) } Button("Cancel", role: .cancel) { - isPresented = false challenge.resume(with: .cancelAuthenticationChallenge) } } message: { _ in From 5a8693fdeef6b0ff7eee048eca946724de28e69e Mon Sep 17 00:00:00 2001 From: R Olson Date: Thu, 14 Jul 2022 12:48:49 -0600 Subject: [PATCH 1156/1722] Update Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift Co-authored-by: Mark Dostal --- .../ArcGISToolkit/Components/Authentication/Authenticator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 67829e453..3601a88ab 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -47,7 +47,7 @@ public final class Authenticator: ObservableObject { /// 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. - /// To know more about what the default group would be you can find information about that here: + /// 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. From 632577fb3021773eb485c0a44eecc5d5d71a2ced Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 13:04:18 -0600 Subject: [PATCH 1157/1722] api updates --- .../Components/Authentication/QueuedNetworkChallenge.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index 618aac638..62e859eac 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -88,10 +88,8 @@ extension QueuedNetworkChallenge.Kind { self = .serverTrust case .ntlm, .basic, .digest: self = .login - case .pki: + case .clientCertificate: self = .certificate - case .htmlForm, .negotiate: - return nil } } } From b15ab718e8df3cf79015e7f90af93b05a4caa160 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 13:29:42 -0600 Subject: [PATCH 1158/1722] make queued arcgis challenge testable --- .../QueuedArcGISChallenge.swift | 34 ++++++++++------- .../UsernamePasswordViewModifier.swift | 20 ++-------- .../QueuedArcGISChallengeTests.swift | 37 +++++++++++++++++++ 3 files changed, 62 insertions(+), 29 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift index dabf71f72..f5b3eb3ad 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift @@ -14,22 +14,35 @@ import Foundation import ArcGIS +// TODO: should be actor? /// An object that represents an ArcGIS authentication challenge in the queue of challenges. final class QueuedArcGISChallenge: QueuedChallenge { - /// The ArcGIS authentication challenge. - let arcGISChallenge: ArcGISAuthenticationChallenge + /// The host that prompted the challenge. + let host: String + + /// A closure that provides a token credential from a username and password. + let tokenCredentialProvider: (String, String) async throws -> ArcGISCredential /// Creates a `QueuedArcGISChallenge`. /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. init(arcGISChallenge: ArcGISAuthenticationChallenge) { - self.arcGISChallenge = arcGISChallenge + host = arcGISChallenge.request.url?.host ?? "" + tokenCredentialProvider = { username, password in + try await .token(challenge: arcGISChallenge, username: username, password: password) + } } - /// Resumes the challenge with a result. - /// - Parameter result: The result of the challenge. - func resume(with result: Result) { - guard _result == nil else { return } - _result = result + /// Resumes the challenge with a username and password. + /// - Parameters: + /// - username: The username. + /// - password: The password. + func resume(username: String, password: String) { + Task { + guard _result == nil else { return } + _result = await Result { + .useCredential(try await tokenCredentialProvider(username, password)) + } + } } /// Cancels the challenge. @@ -51,11 +64,6 @@ final class QueuedArcGISChallenge: QueuedChallenge { } } - /// The host that prompted the challenge. - var host: String { - arcGISChallenge.request.url?.host ?? "" - } - public func complete() async { _ = await result } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index a27ce02c1..9bcaf3e2f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -111,22 +111,10 @@ extension UsernamePasswordViewModifier { viewModel: UsernamePasswordViewModel( challengingHost: challenge.host, onSignIn: { username, password in - Task { - challenge.resume( - with: await Result { - .useCredential( - try await .token( - challenge: challenge.arcGISChallenge, - username: username, - password: password - ) - ) - } - ) - } + challenge.resume(username: username, password: password) }, onCancel: { - challenge.resume(with: .success(.cancelAuthenticationChallenge)) + challenge.cancel() } ) ) @@ -162,7 +150,7 @@ private struct UsernamePasswordView: View { .frame(maxWidth: .infinity) .listRowBackground(Color.clear) } - + Section { TextField("Username", text: $viewModel.username) .focused($focusedField, equals: .username) @@ -177,7 +165,7 @@ private struct UsernamePasswordView: View { } .autocapitalization(.none) .disableAutocorrection(true) - + Section { signinButton } diff --git a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift new file mode 100644 index 000000000..76948fffe --- /dev/null +++ b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift @@ -0,0 +1,37 @@ +// 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 XCTest +@testable import ArcGISToolkit + +class QueuedArcGISChallengeTests: XCTestCase { + func testInit() { + let challenge = QueuedArcGISChallenge(arcGISChallenge: ArcGISAuthenticationChallenge()) + XCTAssertEqual(challenge.host, "host.com") + XCTAssertEqual(challenge.kind, .serverTrust) + } + +// func testResumeAndComplete() async { +// let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) +// challenge.resume(with: .useCredential(.serverTrust)) +// let disposition = await challenge.disposition +// XCTAssertEqual(disposition, .useCredential(.serverTrust)) +// +// // Make sure multiple simultaneous listeners can await the completion. +// let t1 = Task { await challenge.complete() } +// let t2 = Task { await challenge.complete() } +// await t1.value +// await t2.value +// } +} From f88c96ffeecbc31364d28f159443bb00a8347239 Mon Sep 17 00:00:00 2001 From: R Olson Date: Thu, 14 Jul 2022 14:46:21 -0600 Subject: [PATCH 1159/1722] Update Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift Co-authored-by: Mark Dostal --- .../Components/Authentication/UsernamePasswordViewModifier.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 80d7ed006..34b60ec5d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -130,6 +130,7 @@ private struct UsernamePasswordView: View { VStack { person Text("You must sign in to access '\(viewModel.challengingHost)'") + .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity) From 6b0244f065d0b85b7a3167399632e4767dc458a8 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 15:08:31 -0600 Subject: [PATCH 1160/1722] LoginCredential --- .../QueuedArcGISChallenge.swift | 17 +++++++------ .../UsernamePasswordViewModifier.swift | 25 +++++++++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift index f5b3eb3ad..bd61d4576 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift @@ -21,26 +21,29 @@ final class QueuedArcGISChallenge: QueuedChallenge { let host: String /// A closure that provides a token credential from a username and password. - let tokenCredentialProvider: (String, String) async throws -> ArcGISCredential + let tokenCredentialProvider: (LoginCredential) async throws -> ArcGISCredential /// Creates a `QueuedArcGISChallenge`. /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. init(arcGISChallenge: ArcGISAuthenticationChallenge) { host = arcGISChallenge.request.url?.host ?? "" - tokenCredentialProvider = { username, password in - try await .token(challenge: arcGISChallenge, username: username, password: password) + tokenCredentialProvider = { loginCredential in + try await .token( + challenge: arcGISChallenge, + username: loginCredential.username, + password: loginCredential.password + ) } } /// Resumes the challenge with a username and password. /// - Parameters: - /// - username: The username. - /// - password: The password. - func resume(username: String, password: String) { + /// - loginCredential: The username and password. + func resume(loginCredential: LoginCredential) { Task { guard _result == nil else { return } _result = await Result { - .useCredential(try await tokenCredentialProvider(username, password)) + .useCredential(try await tokenCredentialProvider(loginCredential)) } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 9bcaf3e2f..bce70b868 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -14,6 +14,12 @@ import SwiftUI import ArcGIS +/// A value that contains a username and password pair. +struct LoginCredential { + let username: String + let password: String +} + /// A type that provides the business logic for a view that prompts the user to login with a /// username and password. final class UsernamePasswordViewModel: ObservableObject { @@ -32,7 +38,7 @@ final class UsernamePasswordViewModel: ObservableObject { /// The action to perform when the user signs in. This is a closure that takes a username /// and password, respectively. - var signInAction: (String, String) -> Void + var signInAction: (LoginCredential) -> Void /// The action to perform when the user cancels. var cancelAction: () -> Void @@ -44,7 +50,7 @@ final class UsernamePasswordViewModel: ObservableObject { /// - cancelAction: The action to perform when the user cancels. init( challengingHost: String, - onSignIn signInAction: @escaping (String, String) -> Void, + onSignIn signInAction: @escaping (LoginCredential) -> Void, onCancel cancelAction: @escaping () -> Void ) { self.challengingHost = challengingHost @@ -62,7 +68,7 @@ final class UsernamePasswordViewModel: ObservableObject { /// Attempts to log in with a username and password. func signIn() { formEnabled = false - signInAction(username, password) + signInAction(LoginCredential(username: username, password: password)) } /// Cancels the challenge. @@ -95,8 +101,12 @@ extension UsernamePasswordViewModifier { self.init( viewModel: UsernamePasswordViewModel( challengingHost: challenge.host, - onSignIn: { username, password in - challenge.resume(with: .useCredential(.login(username: username, password: password))) + onSignIn: { loginCredential in + challenge.resume( + with: .useCredential( + .login(username: loginCredential.username, password: loginCredential.password) + ) + ) }, onCancel: { challenge.resume(with: .cancelAuthenticationChallenge) @@ -110,8 +120,8 @@ extension UsernamePasswordViewModifier { self.init( viewModel: UsernamePasswordViewModel( challengingHost: challenge.host, - onSignIn: { username, password in - challenge.resume(username: username, password: password) + onSignIn: { loginCredential in + challenge.resume(loginCredential: loginCredential) }, onCancel: { challenge.cancel() @@ -145,6 +155,7 @@ private struct UsernamePasswordView: View { VStack { person Text("You must sign in to access '\(viewModel.challengingHost)'") + .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity) From bc511c861eaf857a958e213f3babb5c80649c55f Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 15:15:18 -0600 Subject: [PATCH 1161/1722] "UsernamePassword" -> "Login" --- .../AuthenticatorModifier.swift | 4 ++-- ...PasswordViewModifier.swift => Login.swift} | 24 ++++++++++--------- .../AuthenticatorTests.swift | 19 ++++++++------- ...lTests.swift => LoginViewModelTests.swift} | 6 ++--- .../QueuedArcGISChallengeTests.swift | 6 ++--- 5 files changed, 32 insertions(+), 27 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{UsernamePasswordViewModifier.swift => Login.swift} (93%) rename Tests/ArcGISToolkitTests/{UsernamePasswordViewModelTests.swift => LoginViewModelTests.swift} (94%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 6399b4839..a9ec638e8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -52,7 +52,7 @@ private struct AuthenticatorModifier: ViewModifier { func body(content: Content) -> some View { switch authenticator.currentChallenge { case let challenge as QueuedArcGISChallenge: - content.modifier(UsernamePasswordViewModifier(challenge: challenge)) + content.modifier(LoginViewModifier(challenge: challenge)) case let challenge as QueuedNetworkChallenge: switch challenge.kind { case .serverTrust: @@ -60,7 +60,7 @@ private struct AuthenticatorModifier: ViewModifier { case .certificate: fatalError("implementation coming soon in another PR") case .login: - content.modifier(UsernamePasswordViewModifier(challenge: challenge)) + content.modifier(LoginViewModifier(challenge: challenge)) } case .none: content diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift similarity index 93% rename from Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift rename to Sources/ArcGISToolkit/Components/Authentication/Login.swift index bce70b868..f687661ac 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -16,13 +16,15 @@ import ArcGIS /// A value that contains a username and password pair. struct LoginCredential { + /// The username. let username: String + /// The password. let password: String } /// A type that provides the business logic for a view that prompts the user to login with a /// username and password. -final class UsernamePasswordViewModel: ObservableObject { +final class LoginViewModel: ObservableObject { /// The username. @Published var username = "" { didSet { updateSigninButtonEnabled() } @@ -79,9 +81,9 @@ final class UsernamePasswordViewModel: ObservableObject { } /// A view modifier that prompts a user to login with a username and password. -struct UsernamePasswordViewModifier: ViewModifier { +struct LoginViewModifier: ViewModifier { /// The view model. - let viewModel: UsernamePasswordViewModel + let viewModel: LoginViewModel /// A Boolean value indicating whether or not the prompt to login is displayed. @State var isPresented = false @@ -90,16 +92,16 @@ struct UsernamePasswordViewModifier: ViewModifier { content .task { isPresented = true } .sheet(isPresented: $isPresented) { - UsernamePasswordView(viewModel: viewModel) + LoginView(viewModel: viewModel) } } } -extension UsernamePasswordViewModifier { +extension LoginViewModifier { /// Creates a `UsernamePasswordViewModifier` with a queued network challenge. init(challenge: QueuedNetworkChallenge) { self.init( - viewModel: UsernamePasswordViewModel( + viewModel: LoginViewModel( challengingHost: challenge.host, onSignIn: { loginCredential in challenge.resume( @@ -118,7 +120,7 @@ extension UsernamePasswordViewModifier { /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. init(challenge: QueuedArcGISChallenge) { self.init( - viewModel: UsernamePasswordViewModel( + viewModel: LoginViewModel( challengingHost: challenge.host, onSignIn: { loginCredential in challenge.resume(loginCredential: loginCredential) @@ -132,18 +134,18 @@ extension UsernamePasswordViewModifier { } /// A view that prompts a user to login with a username and password. -private struct UsernamePasswordView: View { +private struct LoginView: View { /// Creates the view. /// - Parameters: /// - viewModel: The view model. - init(viewModel: UsernamePasswordViewModel) { + init(viewModel: LoginViewModel) { _viewModel = ObservedObject(initialValue: viewModel) } @Environment(\.dismiss) var dismissAction /// The view model. - @ObservedObject private var viewModel: UsernamePasswordViewModel + @ObservedObject private var viewModel: LoginViewModel /// The focused field. @FocusState private var focusedField: Field? @@ -236,7 +238,7 @@ private struct UsernamePasswordView: View { } } -private extension UsernamePasswordView { +private extension LoginView { /// A type that represents the fields in the user name and password sign-in form. enum Field: Hashable { /// The username field. diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index d326e6076..402424662 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -72,11 +72,14 @@ class AuthenticatorTests: XCTestCase { @MainActor func testChallengeQueue() async throws { actor MockQueuedChallenge: QueuedChallenge { - nonisolated let id: Int - init(id: Int) { - self.id = id + nonisolated let host: String + + init(host: String) { + self.host = host } + private var isComplete: Bool = false + func setCompleted() { isComplete = true } @@ -96,7 +99,7 @@ class AuthenticatorTests: XCTestCase { XCTAssertNil(authenticator.currentChallenge) // Create and enqueue first challenge. - let challenge = MockQueuedChallenge(id: 1) + let challenge = MockQueuedChallenge(host: "host1") authenticator.subject.send(challenge) // Make sure first challenge is published as the current challenge. @@ -104,16 +107,16 @@ class AuthenticatorTests: XCTestCase { .compactMap( { $0 as? MockQueuedChallenge }) .first(where: { _ in true }) - XCTAssertEqual(currentChallenge?.id, 1) + XCTAssertEqual(currentChallenge?.host, "host1") XCTAssertNotNil(authenticator.currentChallenge) // Create and enqueue second challenge. - let challenge2 = MockQueuedChallenge(id: 2) + let challenge2 = MockQueuedChallenge(host: "host2") authenticator.subject.send(challenge2) // Make sure first challenge is still the current challenge let mockedCurrentChallenge = try XCTUnwrap(authenticator.currentChallenge as? MockQueuedChallenge) - XCTAssertEqual(mockedCurrentChallenge.id, 1) + XCTAssertEqual(mockedCurrentChallenge.host, "host1") // Complete first challenge. await challenge.setCompleted() @@ -124,7 +127,7 @@ class AuthenticatorTests: XCTestCase { .dropFirst() .first(where: { _ in true }) - XCTAssertEqual(currentChallenge2?.id, 2) + XCTAssertEqual(currentChallenge2?.host, "host2") XCTAssertNotNil(authenticator.currentChallenge) // Complete second challenge. diff --git a/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift b/Tests/ArcGISToolkitTests/LoginViewModelTests.swift similarity index 94% rename from Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift rename to Tests/ArcGISToolkitTests/LoginViewModelTests.swift index f8d50aea1..152e7776d 100644 --- a/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/LoginViewModelTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import ArcGISToolkit @MainActor -class UsernamePasswordViewModelTests: XCTestCase { +class LoginViewModelTests: XCTestCase { func testViewModel() { var signInCalled = false func signIn(username: String, password: String) { @@ -27,9 +27,9 @@ class UsernamePasswordViewModelTests: XCTestCase { cancelCalled = true } - let model = UsernamePasswordViewModel( + let model = LoginViewModel( challengingHost: "host.com", - onSignIn: { _, _ in + onSignIn: { _ in signInCalled = true }, onCancel: { diff --git a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift index 76948fffe..af04f2aa5 100644 --- a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift @@ -17,9 +17,9 @@ import XCTest class QueuedArcGISChallengeTests: XCTestCase { func testInit() { - let challenge = QueuedArcGISChallenge(arcGISChallenge: ArcGISAuthenticationChallenge()) - XCTAssertEqual(challenge.host, "host.com") - XCTAssertEqual(challenge.kind, .serverTrust) +// let challenge = QueuedArcGISChallenge(arcGISChallenge: ArcGISAuthenticationChallenge()) +// XCTAssertEqual(challenge.host, "host.com") +// XCTAssertEqual(challenge.kind, .serverTrust) } // func testResumeAndComplete() async { From 12155d72dc42e713145efc461a37343e69d31200 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 15:39:01 -0600 Subject: [PATCH 1162/1722] add tests --- .../Components/Authentication/Login.swift | 4 +- .../QueuedArcGISChallenge.swift | 19 ++++-- .../QueuedArcGISChallengeTests.swift | 63 ++++++++++++++----- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index f687661ac..c97ea8ba9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -15,7 +15,7 @@ import SwiftUI import ArcGIS /// A value that contains a username and password pair. -struct LoginCredential { +struct LoginCredential: Hashable { /// The username. let username: String /// The password. @@ -123,7 +123,7 @@ extension LoginViewModifier { viewModel: LoginViewModel( challengingHost: challenge.host, onSignIn: { loginCredential in - challenge.resume(loginCredential: loginCredential) + challenge.resume(with: loginCredential) }, onCancel: { challenge.cancel() diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift index bd61d4576..31aaa07b2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift @@ -23,11 +23,22 @@ final class QueuedArcGISChallenge: QueuedChallenge { /// A closure that provides a token credential from a username and password. let tokenCredentialProvider: (LoginCredential) async throws -> ArcGISCredential + /// Creates a `QueuedArcGISChallenge`. + /// - Parameters: + /// - host: The host that prompted the challenge. + /// - tokenCredentialProvider: A closure that provides a token credential from a username and password. + init( + host: String, + tokenCredentialProvider: @escaping (LoginCredential) async throws -> ArcGISCredential + ) { + self.host = host + self.tokenCredentialProvider = tokenCredentialProvider + } + /// Creates a `QueuedArcGISChallenge`. /// - Parameter arcGISChallenge: The associated ArcGIS authentication challenge. - init(arcGISChallenge: ArcGISAuthenticationChallenge) { - host = arcGISChallenge.request.url?.host ?? "" - tokenCredentialProvider = { loginCredential in + convenience init(arcGISChallenge: ArcGISAuthenticationChallenge) { + self.init(host: arcGISChallenge.request.url?.host ?? "") { loginCredential in try await .token( challenge: arcGISChallenge, username: loginCredential.username, @@ -39,7 +50,7 @@ final class QueuedArcGISChallenge: QueuedChallenge { /// Resumes the challenge with a username and password. /// - Parameters: /// - loginCredential: The username and password. - func resume(loginCredential: LoginCredential) { + func resume(with loginCredential: LoginCredential) { Task { guard _result == nil else { return } _result = await Result { diff --git a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift index af04f2aa5..47e4a32ef 100644 --- a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift @@ -17,21 +17,54 @@ import XCTest class QueuedArcGISChallengeTests: XCTestCase { func testInit() { -// let challenge = QueuedArcGISChallenge(arcGISChallenge: ArcGISAuthenticationChallenge()) -// XCTAssertEqual(challenge.host, "host.com") -// XCTAssertEqual(challenge.kind, .serverTrust) + let challenge = QueuedArcGISChallenge(host: "host.com") { _ in + fatalError() + } + + XCTAssertEqual(challenge.host, "host.com") + XCTAssertNotNil(challenge.tokenCredentialProvider) } -// func testResumeAndComplete() async { -// let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) -// challenge.resume(with: .useCredential(.serverTrust)) -// let disposition = await challenge.disposition -// XCTAssertEqual(disposition, .useCredential(.serverTrust)) -// -// // Make sure multiple simultaneous listeners can await the completion. -// let t1 = Task { await challenge.complete() } -// let t2 = Task { await challenge.complete() } -// await t1.value -// await t2.value -// } + func testResumeWithLogin() async { + struct MockError: Error {} + + let challenge = QueuedArcGISChallenge(host: "host.com") { _ in + throw MockError() + } + challenge.resume(with: .init(username: "user1", password: "1234")) + + let result = await challenge.result + XCTAssertTrue(result.error is MockError) + } + + func testCancel() async { + let challenge = QueuedArcGISChallenge(host: "host.com") { _ in + fatalError() + } + challenge.cancel() + + let result = await challenge.result + XCTAssertEqual(result.success, .cancelAuthenticationChallenge) + } +} + +private extension Result { + /// The error that is encapsulated in the failure case when this result is a failure. + var error: Error? { + switch self { + case .failure(let error): + return error + case .success: + return nil + } + } + + var success: Success? { + switch self { + case .failure: + return nil + case .success(let value): + return value + } + } } From af0d6f6cca715eddca8fb4c7335134edf3bb7dd3 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 15:47:22 -0600 Subject: [PATCH 1163/1722] Put on main actor for synchronization --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 3 +++ .../Components/Authentication/QueuedArcGISChallenge.swift | 2 +- .../Components/Authentication/QueuedNetworkChallenge.swift | 1 + Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift | 1 + Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift | 1 + 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index c97ea8ba9..e00a8899c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -24,6 +24,7 @@ struct LoginCredential: Hashable { /// A type that provides the business logic for a view that prompts the user to login with a /// username and password. +@MainActor final class LoginViewModel: ObservableObject { /// The username. @Published var username = "" { @@ -99,6 +100,7 @@ struct LoginViewModifier: ViewModifier { extension LoginViewModifier { /// Creates a `UsernamePasswordViewModifier` with a queued network challenge. + @MainActor init(challenge: QueuedNetworkChallenge) { self.init( viewModel: LoginViewModel( @@ -118,6 +120,7 @@ extension LoginViewModifier { } /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. + @MainActor init(challenge: QueuedArcGISChallenge) { self.init( viewModel: LoginViewModel( diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift index 31aaa07b2..9cc953a96 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift @@ -14,8 +14,8 @@ import Foundation import ArcGIS -// TODO: should be actor? /// An object that represents an ArcGIS authentication challenge in the queue of challenges. +@MainActor final class QueuedArcGISChallenge: QueuedChallenge { /// The host that prompted the challenge. let host: String diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index 62e859eac..62834f8fd 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -15,6 +15,7 @@ import Foundation import ArcGIS /// An object that represents a network authentication challenge in the queue of challenges. +@MainActor final class QueuedNetworkChallenge: QueuedChallenge { /// The host that prompted the challenge. let host: String diff --git a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift index 47e4a32ef..140e1d722 100644 --- a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift @@ -15,6 +15,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit +@MainActor class QueuedArcGISChallengeTests: XCTestCase { func testInit() { let challenge = QueuedArcGISChallenge(host: "host.com") { _ in diff --git a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift index a8c40352a..f52e3ccad 100644 --- a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift @@ -15,6 +15,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit +@MainActor class QueuedNetworkChallengeTests: XCTestCase { func testInit() { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) From 32fbb3fc0d50bd1640527717365deefb326d3347 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 15:49:10 -0600 Subject: [PATCH 1164/1722] cleanup --- .../QueuedArcGISChallengeTests.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift index 140e1d722..dee4cf75c 100644 --- a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift @@ -36,6 +36,12 @@ class QueuedArcGISChallengeTests: XCTestCase { let result = await challenge.result XCTAssertTrue(result.error is MockError) + + // Make sure multiple simultaneous listeners can await the completion. + let t1 = Task { await challenge.complete() } + let t2 = Task { await challenge.complete() } + await t1.value + await t2.value } func testCancel() async { @@ -45,7 +51,7 @@ class QueuedArcGISChallengeTests: XCTestCase { challenge.cancel() let result = await challenge.result - XCTAssertEqual(result.success, .cancelAuthenticationChallenge) + XCTAssertEqual(result.value, .cancelAuthenticationChallenge) } } @@ -60,7 +66,8 @@ private extension Result { } } - var success: Success? { + /// The success value that is encapsulated in the success case when this result is a success. + var value: Success? { switch self { case .failure: return nil From cc647c7ee6324b63f78a7a56a307063ed400e172 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 16:20:54 -0600 Subject: [PATCH 1165/1722] QueuedArcGIS -> QueuedToken --- .../Authentication/Authenticator.swift | 33 +++++++++++++------ .../AuthenticatorModifier.swift | 2 +- .../Components/Authentication/Login.swift | 2 +- ...lenge.swift => QueuedTokenChallenge.swift} | 4 +-- ....swift => QueuedTokenChallengeTests.swift} | 8 ++--- 5 files changed, 31 insertions(+), 18 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{QueuedArcGISChallenge.swift => QueuedTokenChallenge.swift} (95%) rename Tests/ArcGISToolkitTests/{QueuedArcGISChallengeTests.swift => QueuedTokenChallengeTests.swift} (89%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index d69bcfc99..fe7d8c179 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -98,14 +98,27 @@ public final class Authenticator: ObservableObject { // A yield here helps alleviate the already presenting bug. await Task.yield() - // Set the current challenge, this should present the appropriate view. - currentChallenge = queuedChallenge - - // Wait for the queued challenge to finish. - await queuedChallenge.complete() - - // Reset the crrent challenge to `nil`, that will dismiss the view. - currentChallenge = nil + if let queuedArcGISChallenge = queuedChallenge as? QueuedTokenChallenge, + let url = queuedArcGISChallenge.arcGISChallenge.request.url, + let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { + // For an OAuth challenge, we create the credential and resume. + // Creating the OAuth credential will present the OAuth login view. + // We don't set the current challenge because this one is handled internally. + queuedArcGISChallenge.resume( + with: await Result { + .useCredential(try await .oauth(configuration: config)) + } + ) + } else { + // Set the current challenge, this should present the appropriate view. + currentChallenge = queuedChallenge + + // Wait for the queued challenge to finish. + await queuedChallenge.complete() + + // Reset the crrent challenge to `nil`, that will dismiss the view. + currentChallenge = nil + } } } @@ -120,7 +133,7 @@ public final class Authenticator: ObservableObject { ) } - /// The current queued challenge. + /// The current queued challenge being handled. @Published var currentChallenge: QueuedChallenge? } @@ -130,7 +143,7 @@ extension Authenticator: AuthenticationChallengeHandler { _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { // Queue up the challenge. - let queuedChallenge = QueuedArcGISChallenge(arcGISChallenge: challenge) + let queuedChallenge = QueuedTokenChallenge(arcGISChallenge: challenge) subject.send(queuedChallenge) // Wait for it to complete and return the resulting disposition. diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index a9ec638e8..2a07a8e5c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -51,7 +51,7 @@ private struct AuthenticatorModifier: ViewModifier { @ViewBuilder func body(content: Content) -> some View { switch authenticator.currentChallenge { - case let challenge as QueuedArcGISChallenge: + case let challenge as QueuedTokenChallenge: content.modifier(LoginViewModifier(challenge: challenge)) case let challenge as QueuedNetworkChallenge: switch challenge.kind { diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index e00a8899c..7ab7258c6 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -121,7 +121,7 @@ extension LoginViewModifier { /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. @MainActor - init(challenge: QueuedArcGISChallenge) { + init(challenge: QueuedTokenChallenge) { self.init( viewModel: LoginViewModel( challengingHost: challenge.host, diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift similarity index 95% rename from Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift rename to Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift index 9cc953a96..6d80f9ece 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedArcGISChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift @@ -14,9 +14,9 @@ import Foundation import ArcGIS -/// An object that represents an ArcGIS authentication challenge in the queue of challenges. +/// An object that represents an ArcGIS token authentication challenge in the queue of challenges. @MainActor -final class QueuedArcGISChallenge: QueuedChallenge { +final class QueuedTokenChallenge: QueuedChallenge { /// The host that prompted the challenge. let host: String diff --git a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift similarity index 89% rename from Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift rename to Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift index dee4cf75c..3f366bb02 100644 --- a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift @@ -16,9 +16,9 @@ import XCTest @testable import ArcGISToolkit @MainActor -class QueuedArcGISChallengeTests: XCTestCase { +class QueuedTokenChallengeTests: XCTestCase { func testInit() { - let challenge = QueuedArcGISChallenge(host: "host.com") { _ in + let challenge = QueuedTokenChallenge(host: "host.com") { _ in fatalError() } @@ -29,7 +29,7 @@ class QueuedArcGISChallengeTests: XCTestCase { func testResumeWithLogin() async { struct MockError: Error {} - let challenge = QueuedArcGISChallenge(host: "host.com") { _ in + let challenge = QueuedTokenChallenge(host: "host.com") { _ in throw MockError() } challenge.resume(with: .init(username: "user1", password: "1234")) @@ -45,7 +45,7 @@ class QueuedArcGISChallengeTests: XCTestCase { } func testCancel() async { - let challenge = QueuedArcGISChallenge(host: "host.com") { _ in + let challenge = QueuedTokenChallenge(host: "host.com") { _ in fatalError() } challenge.cancel() From 78d907aa140bdd9db7357dab274c5f7aefb561e1 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 16:28:56 -0600 Subject: [PATCH 1166/1722] create oauth challenge --- .../Authentication/Authenticator.swift | 40 +++++++++---------- .../Authentication/QueuedChallenge.swift | 9 +++-- .../Authentication/QueuedOAuthChallenge.swift | 29 ++++++++++++++ .../Authentication/QueuedTokenChallenge.swift | 2 +- 4 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index fe7d8c179..f7d8db67a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -98,27 +98,14 @@ public final class Authenticator: ObservableObject { // A yield here helps alleviate the already presenting bug. await Task.yield() - if let queuedArcGISChallenge = queuedChallenge as? QueuedTokenChallenge, - let url = queuedArcGISChallenge.arcGISChallenge.request.url, - let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { - // For an OAuth challenge, we create the credential and resume. - // Creating the OAuth credential will present the OAuth login view. - // We don't set the current challenge because this one is handled internally. - queuedArcGISChallenge.resume( - with: await Result { - .useCredential(try await .oauth(configuration: config)) - } - ) - } else { - // Set the current challenge, this should present the appropriate view. - currentChallenge = queuedChallenge - - // Wait for the queued challenge to finish. - await queuedChallenge.complete() - - // Reset the crrent challenge to `nil`, that will dismiss the view. - currentChallenge = nil - } + // Set the current challenge, this should present the appropriate view. + currentChallenge = queuedChallenge + + // Wait for the queued challenge to finish. + await queuedChallenge.complete() + + // Reset the crrent challenge to `nil`, that will dismiss the view. + currentChallenge = nil } } @@ -142,8 +129,17 @@ extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { + let queuedChallenge: QueuedArcGISChallenge + + // Create the correct challenge type. + if let url = challenge.request.url, + let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { + queuedChallenge = QueuedOAuthChallenge() + } else { + queuedChallenge = QueuedTokenChallenge(arcGISChallenge: challenge) + } + // Queue up the challenge. - let queuedChallenge = QueuedTokenChallenge(arcGISChallenge: challenge) subject.send(queuedChallenge) // Wait for it to complete and return the resulting disposition. diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index 025600c48..fd86d6961 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -12,12 +12,15 @@ // limitations under the License. import Foundation +import ArcGIS /// A type that represents a challenge in the queue of authentication challenges. protocol QueuedChallenge: AnyObject { - /// The host that prompted the challenge. - var host: String { get } - /// Waits for the challenge to complete. func complete() async } + +protocol QueuedArcGISChallenge: QueuedChallenge { + /// The result of the challenge. + var result: Result { get async } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift new file mode 100644 index 000000000..d83d2dddf --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift @@ -0,0 +1,29 @@ +// 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 Foundation +import ArcGIS + +/// An object that represents an ArcGIS OAuth authentication challenge in the queue of challenges. +@MainActor +final class QueuedOAuthChallenge: QueuedArcGISChallenge { + init() {} + + public func complete() async { + fatalError() + } + + var result: Result { + get async { fatalError() } + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift index 6d80f9ece..246521976 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents an ArcGIS token authentication challenge in the queue of challenges. @MainActor -final class QueuedTokenChallenge: QueuedChallenge { +final class QueuedTokenChallenge: QueuedArcGISChallenge { /// The host that prompted the challenge. let host: String From 8b7a88048befd134c66e044c23bd76d84318aeda Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 17:05:02 -0600 Subject: [PATCH 1167/1722] doc --- .../Authentication/Authenticator.swift | 8 +++-- .../AuthenticatorModifier.swift | 4 +++ .../Authentication/QueuedOAuthChallenge.swift | 34 ++++++++++++++++--- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index f7d8db67a..86896eaf3 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -89,7 +89,9 @@ public final class Authenticator: ObservableObject { // We have to set new sessions for URLCredential storage to respect the removed credentials // right away. ArcGISRuntimeEnvironment.urlSession = ArcGISURLSession(configuration: .default) - ArcGISRuntimeEnvironment.backgroundURLSession = ArcGISURLSession(configuration: .background(withIdentifier: "com.esri.arcgis.toolkit." + UUID().uuidString)) + ArcGISRuntimeEnvironment.backgroundURLSession = ArcGISURLSession( + configuration: .background(withIdentifier: "com.esri.arcgis.toolkit." + UUID().uuidString) + ) } /// Observes the challenge queue and sets the current challenge. @@ -134,7 +136,9 @@ extension Authenticator: AuthenticationChallengeHandler { // Create the correct challenge type. if let url = challenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { - queuedChallenge = QueuedOAuthChallenge() + let oAuthChallenge = QueuedOAuthChallenge(configuration: config) + queuedChallenge = oAuthChallenge + oAuthChallenge.presentPrompt() } else { queuedChallenge = QueuedTokenChallenge(arcGISChallenge: challenge) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 2a07a8e5c..4fdee14d9 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -51,6 +51,10 @@ private struct AuthenticatorModifier: ViewModifier { @ViewBuilder func body(content: Content) -> some View { switch authenticator.currentChallenge { + case is QueuedOAuthChallenge: + // OAuth is handled by the authenticator itself + // so just return the unmodified content here. + content case let challenge as QueuedTokenChallenge: content.modifier(LoginViewModifier(challenge: challenge)) case let challenge as QueuedNetworkChallenge: diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift index d83d2dddf..5cf360828 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift @@ -17,13 +17,39 @@ import ArcGIS /// An object that represents an ArcGIS OAuth authentication challenge in the queue of challenges. @MainActor final class QueuedOAuthChallenge: QueuedArcGISChallenge { - init() {} + /// The OAuth configuration to be used for this challenge. + let configuration: OAuthConfiguration - public func complete() async { - fatalError() + /// Creates a `QueuedOAuthChallenge`. + /// - Parameter configuration: The OAuth configuration to be used for this challenge. + init(configuration: OAuthConfiguration) { + self.configuration = configuration + } + + /// Presents the user with a prompt to login with OAuth. This is done by creating the OAuth + /// credential. + func presentPrompt() { + Task { + _result = await Result { + .useCredential(try await .oauth(configuration: configuration)) + } + } } + /// Use a streamed property because we need to support multiple listeners + /// to know when the challenge completed. + @Streamed private var _result: Result? + + /// The result of the challenge. var result: Result { - get async { fatalError() } + get async { + await $_result + .compactMap({ $0 }) + .first(where: { _ in true })! + } + } + + public func complete() async { + _ = await result } } From dcb05675fe1c18d7404283a2963455f06bd8ad75 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 14 Jul 2022 17:20:32 -0600 Subject: [PATCH 1168/1722] add test --- .../QueuedOAuthChallengeTests.swift | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift diff --git a/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift new file mode 100644 index 000000000..134ca039c --- /dev/null +++ b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift @@ -0,0 +1,35 @@ +// 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 XCTest +@testable import ArcGISToolkit +import ArcGIS + +@MainActor +class QueuedOAuthChallengeTests: XCTestCase { + func testInit() { + let portalURL = URL(string: "www.test-portal.com")! + let clientID = "client id" + let redirectURL = URL(string: "test-app://redirect")! + + let config = OAuthConfiguration( + portalURL: portalURL, + clientID: clientID, + redirectURL: redirectURL + ) + + let challenge = QueuedOAuthChallenge(configuration: config) + XCTAssertEqual(challenge.configuration, config) + } +} From 0a7e2dbe98825ed4a32cd4ac4c3bdc9f09091183 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 15 Jul 2022 12:27:52 -0500 Subject: [PATCH 1169/1722] Initial implementation of PopupView and Example --- Examples/Examples.xcodeproj/project.pbxproj | 8 +- Examples/Examples/PopupExampleView.swift | 110 ++++++++++++++++++ Examples/ExamplesApp/Examples.swift | 1 + .../Popups/FieldsPopupElementView.swift | 88 ++++++++++++++ .../Popups/PopupElementHeader.swift | 28 +++++ .../Components/Popups/PopupView.swift | 90 ++++++++++++++ Sources/ArcGISToolkit/Extensions/Result.swift | 2 +- 7 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 Examples/Examples/PopupExampleView.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupView.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 002f63bd9..063bae6f8 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,8 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 75C37C9227BEDBD800FC9DCE /* BookmarksExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */; }; + 4D19FCB52881C8F3002601E8 /* PopupExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D19FCB42881C8F3002601E8 /* PopupExampleView.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 */; }; E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42BFBE82672BF9500159107 /* SearchExampleView.swift */; }; E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */; }; @@ -40,8 +41,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksExampleView.swift; sourceTree = ""; }; + 4D19FCB42881C8F3002601E8 /* PopupExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopupExampleView.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 = ""; }; E42BFBE82672BF9500159107 /* SearchExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchExampleView.swift; sourceTree = ""; }; E4624A24278CE815000D2A38 /* FloorFilterExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloorFilterExampleView.swift; sourceTree = ""; }; @@ -77,6 +79,7 @@ E40F58042656E509006F5CB9 /* Examples */ = { isa = PBXGroup; children = ( + 4D19FCB42881C8F3002601E8 /* PopupExampleView.swift */, E4C389D426B8A12C002BC255 /* BasemapGalleryExampleView.swift */, 75C37C9127BEDBD800FC9DCE /* BookmarksExampleView.swift */, E4F9BC98265EFCAF001280FF /* OverviewMapExampleView.swift */, @@ -231,6 +234,7 @@ E48A73442658227100F5C118 /* AnyExample.swift in Sources */, E48A73472658227100F5C118 /* ExampleList.swift in Sources */, E4624A25278CE815000D2A38 /* FloorFilterExampleView.swift in Sources */, + 4D19FCB52881C8F3002601E8 /* PopupExampleView.swift in Sources */, E48A73462658227100F5C118 /* ExampleView.swift in Sources */, E42BFBE92672BF9500159107 /* SearchExampleView.swift in Sources */, E4C389D526B8A12C002BC255 /* BasemapGalleryExampleView.swift in Sources */, diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift new file mode 100644 index 000000000..e352fca1f --- /dev/null +++ b/Examples/Examples/PopupExampleView.swift @@ -0,0 +1,110 @@ +// 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 +import ArcGISToolkit + +struct PopupExampleView: View { + static func makeMap() -> Map { + let portal = Portal(url: URL(string: "https://runtimecoretest.maps.arcgis.com/")!, isLoginRequired: false) + + // Popups example map. - 4 types: text, media image, media chart, fields list + let portalItem1 = PortalItem(portal: portal, id: Item.ID(rawValue: "10b79e7ad1944422b87f73da86dcf752")!) + + // Popups example map. - Arcade + //Test Case 3.5 Display popup with expression elements defining media [SDK] + //https://runtimecoretest.maps.arcgis.com/home/item.html?id=34752f1d149f4b2db96f7a1637767173 + let portalItem2 = PortalItem(portal: portal, id: Item.ID(rawValue: "34752f1d149f4b2db96f7a1637767173")!) + + //Test Case 3.2 Display popup with multiple fields elements [FT-SDK] + //https://runtimecoretest.maps.arcgis.com/home/item.html?id=8d75d1dbdb5c4ad5849abb26b783987e **Modified** + let portalItem3 = PortalItem(portal: portal, id: Item.ID(rawValue: "8d75d1dbdb5c4ad5849abb26b783987e")!) + + //Recreation Map with Attachments. + //https://runtimecoretest.maps.arcgis.com/home/item.html?id=2afef81236db4eabbbae357e4f990039 + let portalItem4 = PortalItem(portal: portal, id: Item.ID(rawValue: "2afef81236db4eabbbae357e4f990039")!) + + //Recreation Map with Attachments - New + //https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=79c995874bea47d08aab5a2c85120e7f + let portalItem5 = PortalItem(portal: portal, id: Item.ID(rawValue: "79c995874bea47d08aab5a2c85120e7f")!) + + //Attachments + //https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=9e3baeb5dcd4473aa13e0065d7794ca6 + let portalItem6 = PortalItem(portal: portal, id: Item.ID(rawValue: "9e3baeb5dcd4473aa13e0065d7794ca6")!) + + return Map(item: portalItem1) + } + + /// The map displayed in the map view. + @StateObject private var map = makeMap() + + @State private var identifyScreenPoint: CGPoint? + @State private var identifyResult: Result<[IdentifyLayerResult], Error>? + + var body: some View { + MapViewReader { proxy in + VStack { + MapView(map: map) + .onSingleTapGesture { screenPoint, _ in + identifyScreenPoint = screenPoint + } + .task(id: identifyScreenPoint) { + guard let identifyScreenPoint = identifyScreenPoint, + let identifyResult = await Result(awaiting: { + try await proxy.identifyLayers( + screenPoint: identifyScreenPoint, + tolerance: 10, + returnPopupsOnly: true + ) + }) + else { + return + } + + self.identifyResult = identifyResult + self.identifyScreenPoint = nil + } + .overlay(alignment: .topLeading) { + Group { + if identifyScreenPoint != nil { + ProgressView() + .esriBorder() + } else if let identifyResult = identifyResult { + IdentifyResultView(identifyResult: identifyResult) + .esriBorder() + } + } + .frame(maxWidth: 400) + .padding() + } + } + } + } +} + +struct IdentifyResultView: View { + var identifyResult: Result<[IdentifyLayerResult], Error> + + var body: some View { + switch identifyResult { + case .success(let identifyLayerResults): + // Get the first popup from the first layer result. + if let popup = identifyLayerResults.first?.popups.first { + PopupView(popup: popup) + } + case .failure(let error): + Text("Identify error: \(error.localizedDescription).") + } + } +} diff --git a/Examples/ExamplesApp/Examples.swift b/Examples/ExamplesApp/Examples.swift index e8d654441..dfcb64f93 100644 --- a/Examples/ExamplesApp/Examples.swift +++ b/Examples/ExamplesApp/Examples.swift @@ -40,6 +40,7 @@ extension ExampleList { AnyExample("Compass", content: CompassExampleView()), AnyExample("Floor Filter", content: FloorFilterExampleView()), AnyExample("Overview Map", content: OverviewMapExampleView()), + AnyExample("Popup", content: PopupExampleView()), AnyExample("Scalebar", content: ScalebarExampleView()), AnyExample("Search", content: SearchExampleView()) ] diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift new file mode 100644 index 000000000..28beef37c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -0,0 +1,88 @@ +// 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 view displaying a `FieldsPopupElement`. +struct FieldsPopupElementView: View { + /// Creates a new `FieldsPopupElementView`. + /// - Parameter popupElement: The `FieldsPopupElement`. + init(popupElement: FieldsPopupElement) { + self.popupElement = popupElement + self.fields = zip(popupElement.labels, popupElement.formattedValues).map { + DisplayField(label: $0, formattedValue: $1) + } + } + + /// The `PopupElement` to display. + private var popupElement: FieldsPopupElement + private let fields: [DisplayField] + + var body: some View { + PopupElementHeader( + title: popupElement.title, + description: popupElement.description + ) + FieldsList(fields: fields) + .font(.footnote) + } + + struct FieldsList: View { + let fields: [DisplayField] + + var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2) + var body: some View { + LazyVGrid(columns: columns) { + ForEach(Array(fields.enumerated()), id: \.element) { index, element in + GridCell(label: element.label, rowIndex: index) + GridCell(label: element.formattedValue, rowIndex: index) + } + } + } + } + + struct GridCell: View { + let label: String + let rowIndex: Int + var url: URL? { + label.lowercased().starts(with: "http") ? URL(string: label) : nil + } + + var body: some View { + Group { + HStack { + Text(url != nil ? "View" : label) + .underline(url != nil) + .lineLimit(1) + .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading) + .foregroundColor(url != nil ? Color(UIColor.link) : .primary) + .onTapGesture { + if let url = url { + UIApplication.shared.open(url) + } + } + + } + .padding([.leading, .trailing], 4) + .padding([.top, .bottom], 6) + .background(rowIndex % 2 != 1 ? Color.secondary: Color.clear) + } + } + } + + struct DisplayField: Hashable { + let label: String + let formattedValue: String + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift new file mode 100644 index 000000000..c9fbab8c7 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -0,0 +1,28 @@ +// 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 + +struct PopupElementHeader: View { + var title: String + var description: String + + var body: some View { + Text(title) + .font(.headline) + Text(description) + .font(.subheadline) + .foregroundColor(.secondary) + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift new file mode 100644 index 000000000..5d6c7dfd0 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -0,0 +1,90 @@ +// 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 view displaying the elements of a single Popup. +public struct PopupView: View { + /// Creates a `PopupView` with the given popup. + /// - Parameter popup: The popup to display. + public init(popup: Popup) { + self.popup = popup + } + + /// The `Popup` to display. + private var popup: Popup + + /// A Boolean value indicating whether the popup's elements have been evaluated via + /// the `popup.evaluateExpressions()` method. + @State private var isPopupEvaluated: Bool? = nil + + /// The results of calling the `popup.evaluateExpressions()` method. + @State private var expressionEvaluations: [PopupExpressionEvaluation]? = nil + + public var body: some View { + VStack(alignment: .leading) { + Text(popup.title) + .fontWeight(.bold) + Divider() + Group { + if isPopupEvaluated == nil { + VStack(alignment: .center) { + Text("Evaluating popoup expressions...") + ProgressView() + } + } else if isPopupEvaluated! { + popupElementScrollView(popup: popup) + } else { + Text("Popup failed evaluation.") + } + } + } + .task { + do { + expressionEvaluations = try await popup.evaluateExpressions() + isPopupEvaluated = true + } catch { + isPopupEvaluated = false + } + } + } + + struct popupElementScrollView: View { + var popup: Popup + var body: some View { + ScrollView { + VStack(alignment: .leading) { + ForEach(popup.evaluatedElements) { popupElement in + switch popupElement { + case let popupElement as AttachmentsPopupElement: + Text("AttachmentsPopupElementView implementation coming soon.") + case let popupElement as FieldsPopupElement: + FieldsPopupElementView(popupElement: popupElement) + case let popupElement as MediaPopupElement: + Text("MediaPopupElementView implementation coming soon.") + case let popupElement as TextPopupElement: + Text("TextPopupElementView implementation coming soon.") + default: + EmptyView() + } + + Divider() + } + } + } + } + } +} + +extension PopupElement: Identifiable {} diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index d4e837b1e..20aca6811 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -17,7 +17,7 @@ extension Result where Failure == Error { /// `failure`. /// /// Returns `nil` in the event that the task was cancelled. - init?(awaiting task: () async throws -> Success) async { + public init?(awaiting task: () async throws -> Success) async { do { self = .success(try await task()) } catch is CancellationError { From f8d9a1e5d04733f2952e6ba05f4a0219d4d21ec1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 11:41:11 -0700 Subject: [PATCH 1170/1722] Remove unused properties --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 264ce8a3a..b8b5a9178 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -15,12 +15,6 @@ import ArcGIS import SwiftUI public struct UtilityNetworkTrace: View { - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass: UserInterfaceSizeClass? - - @Environment(\.verticalSizeClass) - private var verticalSizeClass: UserInterfaceSizeClass? - // MARK: Enums /// Activities users will perform while creating a new trace. From f4d9870c0cb6666801fd9669a15e365ab17d56ec Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 12:03:01 -0700 Subject: [PATCH 1171/1722] Remove unneeded type specifiers --- Examples/Examples/FloatingPanelExampleView.swift | 3 +-- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 126f2f5cd..9e73fc763 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -16,8 +16,7 @@ import ArcGISToolkit import ArcGIS struct FloatingPanelExampleView: View { - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass: UserInterfaceSizeClass? + @Environment(\.horizontalSizeClass) private var horizontalSizeClass @StateObject private var map = Map(basemapStyle: .arcGISImagery) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index b13fb3a19..bd5872f20 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -28,8 +28,7 @@ public struct FloatingPanel: View where Content: View { // to have it be a view modifier, similar to how SwiftUI doesn't have a // SheetView, but a modifier that presents a sheet. - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass: UserInterfaceSizeClass? + @Environment(\.horizontalSizeClass) private var horizontalSizeClass /// The content shown in the floating panel. let content: Content From b254edd25fa2ffaf9f6d19f8957560d977a0c651 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 12:40:54 -0700 Subject: [PATCH 1172/1722] Init height state at property declaration site --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index bd5872f20..52ffce4d2 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -37,14 +37,13 @@ public struct FloatingPanel: View where Content: View { /// - Parameter content: The view shown in the floating panel. public init(@ViewBuilder content: () -> Content) { self.content = content() - _height = State(initialValue: .infinity) } /// The color of the handle. @State private var handleColor: Color = .defaultHandleColor /// The height of the content. - @State private var height: CGFloat + @State private var height: CGFloat = .infinity /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity From 766b8f479f907f6ca862973201ca51c579a5931f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 13:18:34 -0700 Subject: [PATCH 1173/1722] Mark `shouldHide` internal to provide test access --- 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 d17f6a9a5..504cc068f 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -26,7 +26,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 { heading.isZero && autoHide } + internal var shouldHide: Bool { heading.isZero && autoHide } /// The heading of the compass in degrees. @Binding private var heading: Double From 30c666cdb20de98c9213d67b407d4ed50805486e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 13:18:49 -0700 Subject: [PATCH 1174/1722] Create CompassTests.swift --- Tests/ArcGISToolkitTests/CompassTests.swift | 100 ++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 Tests/ArcGISToolkitTests/CompassTests.swift diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift new file mode 100644 index 000000000..68377d312 --- /dev/null +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -0,0 +1,100 @@ +// 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 +import SwiftUI +import XCTest +@testable import ArcGISToolkit + +class CompassTests: XCTestCase { + /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide + /// is disabled. + func testHiddenWithAutoHideOff() { + let initialValue = 0.0 + let finalValue = 90.0 + var _viewpoint: Viewpoint? = getViewpoint(initialValue) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint, autoHide: false) + XCTAssertFalse(compass.shouldHide) + _viewpoint = getViewpoint(finalValue) + XCTAssertFalse(compass.shouldHide) + } + + /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide + /// is enabled. + func testHiddenWithAutoHideOn() { + let initialValue = 0.0 + let finalValue = 90.0 + var _viewpoint: Viewpoint? = getViewpoint(initialValue) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertTrue(compass.shouldHide) + _viewpoint = getViewpoint(finalValue) + XCTAssertFalse(compass.shouldHide) + } + + /// Asserts that the compass correctly initializes when given a nil viewpoint + func testInit() { + var _viewpoint: Viewpoint? = nil + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertEqual(compass.shouldHide, true) + } + + /// Asserts that the compass correctly initializes when given a nil viewpoint, and autoHide is disabled. + func testInitNoAutoHide() { + var _viewpoint: Viewpoint? = nil + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint, autoHide: false) + XCTAssertEqual(compass.shouldHide, false) + } + + /// Asserts that the compass correctly initializes when given only a viewpoint. + func testInitWithViewpoint() { + var _viewpoint: Viewpoint? = getViewpoint(0.0) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass(viewpoint: viewpoint) + XCTAssertEqual(compass.shouldHide, true) + } + + /// Asserts that the compass correctly initializes when given only a viewpoint. + func testInitWithViewpointAndAutoHide() { + let autoHide = false + var _viewpoint: Viewpoint? = getViewpoint(0) + let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) + let compass = Compass( + viewpoint: viewpoint, + autoHide: autoHide + ) + XCTAssertEqual(compass.shouldHide, false) + } +} + +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 + } + + /// Builds viewpoints to use for tests. + /// - Parameter rotation: The rotation to use for the resulting viewpoint. + /// - Returns: A viewpoint object for tests. + func getViewpoint(_ rotation: Double) -> Viewpoint { + return Viewpoint(center: point, scale: scale, rotation: rotation) + } +} From 49f2c35aafbdc7e162a3647ce2be76b5bb78d4cb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 13:19:12 -0700 Subject: [PATCH 1175/1722] Fix test bug --- 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 504cc068f..f589d4fcb 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -101,7 +101,7 @@ public extension Compass { autoHide: Bool = true ) { let viewpointRotation = Binding { - viewpoint.wrappedValue?.rotation ?? .nan + viewpoint.wrappedValue?.rotation ?? .zero } set: { newViewpointRotation in guard let oldViewopint = viewpoint.wrappedValue else { return } viewpoint.wrappedValue = Viewpoint( From bded094c41a53c2ff13d224f1b8b553ec2e5bacc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 15 Jul 2022 16:36:47 -0500 Subject: [PATCH 1176/1722] Clean up and fill out documentation. --- Examples/Examples/PopupExampleView.swift | 3 +- .../Popups/FieldsPopupElementView.swift | 54 ++++++++++--------- .../Popups/PopupElementHeader.swift | 1 + .../Components/Popups/PopupView.swift | 1 + 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index e352fca1f..5db105580 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -82,7 +82,6 @@ struct PopupExampleView: View { .esriBorder() } else if let identifyResult = identifyResult { IdentifyResultView(identifyResult: identifyResult) - .esriBorder() } } .frame(maxWidth: 400) @@ -93,6 +92,7 @@ struct PopupExampleView: View { } } +/// A view displaying the results of an identify operation. struct IdentifyResultView: View { var identifyResult: Result<[IdentifyLayerResult], Error> @@ -102,6 +102,7 @@ struct IdentifyResultView: View { // Get the first popup from the first layer result. if let popup = identifyLayerResults.first?.popups.first { PopupView(popup: popup) + .esriBorder() } case .failure(let error): Text("Identify error: \(error.localizedDescription).") diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 28beef37c..5894707bf 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -20,30 +20,33 @@ struct FieldsPopupElementView: View { /// - Parameter popupElement: The `FieldsPopupElement`. init(popupElement: FieldsPopupElement) { self.popupElement = popupElement - self.fields = zip(popupElement.labels, popupElement.formattedValues).map { + self.displayFields = zip(popupElement.labels, popupElement.formattedValues).map { DisplayField(label: $0, formattedValue: $1) } } - + /// The `PopupElement` to display. private var popupElement: FieldsPopupElement - private let fields: [DisplayField] - + + /// The labels and values to display, as an array of `DisplayField`s. + private let displayFields: [DisplayField] + var body: some View { PopupElementHeader( title: popupElement.title, description: popupElement.description ) - FieldsList(fields: fields) + FieldsGrid(fields: displayFields) .font(.footnote) } - struct FieldsList: View { + /// A view displaying a grid of labels and values. + struct FieldsGrid: View { let fields: [DisplayField] - var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2) + var columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 0), count: 2) var body: some View { - LazyVGrid(columns: columns) { + LazyVGrid(columns: columns, spacing: 0) { ForEach(Array(fields.enumerated()), id: \.element) { index, element in GridCell(label: element.label, rowIndex: index) GridCell(label: element.formattedValue, rowIndex: index) @@ -52,35 +55,34 @@ struct FieldsPopupElementView: View { } } + /// A view for dispaying text in a grid. struct GridCell: View { + /// The String to display. let label: String + /// The index of the row the view is in. let rowIndex: Int + /// The URL off the label if the label is an "http" string. var url: URL? { label.lowercased().starts(with: "http") ? URL(string: label) : nil } var body: some View { - Group { - HStack { - Text(url != nil ? "View" : label) - .underline(url != nil) - .lineLimit(1) - .frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading) - .foregroundColor(url != nil ? Color(UIColor.link) : .primary) - .onTapGesture { - if let url = url { - UIApplication.shared.open(url) - } - } - + Text(url != nil ? "View" : label) + .underline(url != nil) + .foregroundColor(url != nil ? Color(UIColor.link) : .primary) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(6) + .onTapGesture { + if let url = url { + UIApplication.shared.open(url) + } } - .padding([.leading, .trailing], 4) - .padding([.top, .bottom], 6) - .background(rowIndex % 2 != 1 ? Color.secondary: Color.clear) - } + .background(rowIndex % 2 != 1 ? Color.secondary.opacity(0.5) : Color.clear) } } - + + /// A convenience type for displaying labels and values in a grid. struct DisplayField: Hashable { let label: String let formattedValue: String diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index c9fbab8c7..e5879101f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -14,6 +14,7 @@ import SwiftUI import ArcGIS +/// A view dispaying a title and description of a `PopupElement`. struct PopupElementHeader: View { var title: String var description: String diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 5d6c7dfd0..7832a98dc 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -43,6 +43,7 @@ public struct PopupView: View { Text("Evaluating popoup expressions...") ProgressView() } + .frame(maxWidth: .infinity) } else if isPopupEvaluated! { popupElementScrollView(popup: popup) } else { From ee8a76b3f20b16dd75e4484daad33c912f8cecb9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:03:31 -0700 Subject: [PATCH 1177/1722] Zoom to an element result --- .../UtilityNetworkTrace.swift | 17 ++++++++++++++--- .../UtilityNetworkTraceViewModel.swift | 12 ++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 9f99f1e64..2ae6febab 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -131,7 +131,14 @@ public struct UtilityNetworkTrace: View { ForEach(assetGroup.sorted(by: { $0.key < $1.key }), id: \.key) { assetTypeGroup in Section(assetTypeGroup.key) { ForEach(assetTypeGroup.value) { element in - Text(element.objectID.description) + makeZoomToButton(text: element.objectID.description) { + Task { + if let feature = await viewModel.getFeatureFor(element: element), + let geometry = feature.geometry { + viewpoint = Viewpoint(targetExtent: geometry.extent) + } + } + } } } } @@ -613,12 +620,16 @@ public struct UtilityNetworkTrace: View { } /// Returns a "Zoom To" button that performs a specified action when pressed. + /// - Parameter text: The custom text to be displayed within the button. /// - Parameter action: The action to be performed. /// - Returns: The configured button. - private func makeZoomToButton(_ action: @escaping () -> Void) -> some View { + private func makeZoomToButton( + text: String = "Zoom To", + _ action: @escaping () -> Void + ) -> some View { Button { action() } label: { Label { - Text("Zoom To") + Text(text) } icon: { Image(systemName: "scope") } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 1e894fa20..868a87808 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -119,6 +119,18 @@ import SwiftUI completedTraces.removeAll() } + /// Returns a feature for the given utility element + /// - Parameter element: The utility element to query the network for + /// - Returns: A feature for the given element + func getFeatureFor(element: UtilityElement) async -> ArcGISFeature? { + do { + return try await network?.getFeatures(for: [element]).first ?? nil + } catch { + print(error.localizedDescription) + return nil + } + } + /// Selects the next trace from the list of completed traces. func selectNextTrace() { if let current = selectedTraceIndex { From 955b6047a4233a00ea9fa7c5c4b16872f6f17239 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:15:32 -0700 Subject: [PATCH 1178/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 2ae6febab..f740bd353 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -152,13 +152,13 @@ public struct UtilityNetworkTrace: View { Text("No configurations available") } else { ForEach(viewModel.configurations) { configuration in - Text(configuration.name) - .lineLimit(1) - .listRowBackground(configuration == viewModel.pendingTrace.configuration ? Color.secondary.opacity(0.5) : nil) - .onTapGesture { - viewModel.setPendingTrace(configuration: configuration) - currentActivity = .creatingTrace(nil) - } + Button { + viewModel.setPendingTrace(configuration: configuration) + currentActivity = .creatingTrace(nil) + } label: { + Text(configuration.name) + } + .listRowBackground(configuration == viewModel.pendingTrace.configuration ? Color.secondary.opacity(0.5) : nil) } } } From 1d1e6eb1d0de2e8843f26be02c7ccf6f84b2fa20 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:35:57 -0700 Subject: [PATCH 1179/1722] Update UtilityNetworkTraceExampleView.swift --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index e97f9d7ca..68042b083 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -58,7 +58,7 @@ struct UtilityNetworkTraceExampleView: View { $viewpoint ) .task { - await ArcGISURLSession.credentialStore.add(try! await .publicSample) + await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) } } .padding() From 98ac786da5270aa477010a350bf44a968d632b4f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:39:00 -0700 Subject: [PATCH 1180/1722] Add parameter names --- .../Examples/UtilityNetworkTraceExampleView.swift | 12 ++++++------ .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 68042b083..93b338380 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -50,12 +50,12 @@ struct UtilityNetworkTraceExampleView: View { .overlay(alignment: .topTrailing) { FloatingPanel { UtilityNetworkTrace( - $resultGraphicsOverlay, - map, - $mapPoint, - $viewPoint, - $mapViewProxy, - $viewpoint + graphicsOverlay: $resultGraphicsOverlay, + map: map, + mapPoint: $mapPoint, + viewPoint: $viewPoint, + mapViewProxy: $mapViewProxy, + viewpoint: $viewpoint ) .task { await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index b8b5a9178..65fafe7c7 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -314,12 +314,12 @@ public struct UtilityNetworkTrace: View { /// chosen. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. public init( - _ graphicsOverlay: Binding, - _ map: Map, - _ mapPoint: Binding, - _ viewPoint: Binding, - _ mapViewProxy: Binding, - _ viewpoint: Binding + graphicsOverlay: Binding, + map: Map, + mapPoint: Binding, + viewPoint: Binding, + mapViewProxy: Binding, + viewpoint: Binding ) { _viewPoint = viewPoint _mapPoint = mapPoint From e3db52ce30a97e523bd680c02a5c3a036081ba0d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:40:01 -0700 Subject: [PATCH 1181/1722] 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 65fafe7c7..64b1d6878 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -396,7 +396,7 @@ public struct UtilityNetworkTrace: View { /// - Returns: A Boolean value indicating whether the provided activity is the currently focused /// creation activity. private func isFocused(traceCreationActivity: TraceCreationActivity) -> Bool { - if case let .creatingTrace(activity) = currentActivity{ + if case let .creatingTrace(activity) = currentActivity { return traceCreationActivity == activity } return false From 81454a67b92f238680299cc1690fa6054b8a5262 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:42:05 -0700 Subject: [PATCH 1182/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index bd949c560..7becbf972 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -92,7 +92,7 @@ import SwiftUI func deleteAllTraces() { selectedTraceIndex = nil completedTraces.forEach { traceResult in - graphicsOverlay.removeGraphics(traceResult.startingPoints.map({ $0.graphic })) + graphicsOverlay.removeGraphics(traceResult.startingPoints.map { $0.graphic }) graphicsOverlay.removeGraphics(traceResult.graphics) } completedTraces.removeAll() From cfbda501f80e38ff7a71f69bde9fc99ac560b89e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:42:46 -0700 Subject: [PATCH 1183/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 7becbf972..390360a9c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -172,7 +172,7 @@ import SwiftUI let network = network else { return } let params = UtilityTraceParameters( namedTraceConfiguration: config, - startingLocations: pendingTrace.startingPoints.compactMap{ $0.utilityElement } + startingLocations: pendingTrace.startingPoints.compactMap { $0.utilityElement } ) Task { let traceResults = try await network.trace(traceParameters: params) From a8a2ef62332afbafef16ff9bff9b8584a3a1f736 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:44:42 -0700 Subject: [PATCH 1184/1722] Update Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift Co-authored-by: Zachary A Kline --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 390360a9c..d6e622571 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -242,7 +242,7 @@ import SwiftUI } } - // MARK: Private Items + // MARK: Private Methods /// Changes the selected state of the graphics for the completed trace at the provided index. /// - Parameters: From 73dcc05fa303cf1da7ddbb9aced5b61cf95b3d02 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:45:51 -0700 Subject: [PATCH 1185/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index d6e622571..2112e0ab8 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -253,8 +253,8 @@ import SwiftUI to isSelected: Bool ) { guard index >= 0, index <= completedTraces.count - 1 else { return } - _ = completedTraces[index].graphics.map { $0.isSelected = isSelected} - _ = completedTraces[index].startingPoints.map { $0.graphic.isSelected = isSelected} + _ = completedTraces[index].graphics.map { $0.isSelected = isSelected } + _ = completedTraces[index].startingPoints.map { $0.graphic.isSelected = isSelected } } /// Loads the named trace configurations in the network on the provided map. From af1e42b484751f9a481de8b238b5cce01fe0bbf6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:53:44 -0700 Subject: [PATCH 1186/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTraceViewModel.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 2112e0ab8..f39d1ae27 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -75,7 +75,7 @@ import SwiftUI Task { try? await map.load() network = map.utilityNetworks.first - await loadNamedTraceConfigurations(map) + configurations = await utilityNamedTraceConfigurations(from: map) } } @@ -257,11 +257,11 @@ import SwiftUI _ = completedTraces[index].startingPoints.map { $0.graphic.isSelected = isSelected } } - /// Loads the named trace configurations in the network on the provided map. + /// Returns the named trace configurations in the network on the provided map. /// - Parameter map: A web map containing one or more utility networks. - private func loadNamedTraceConfigurations(_ map: Map) async { - guard let network = network else { return } - configurations = (try? await map.getNamedTraceConfigurations(from: network)) ?? [] + private func utilityNamedTraceConfigurations(from map: Map) async -> [UtilityNamedTraceConfiguration] { + guard let network = network else { return [] } + return (try? await map.getNamedTraceConfigurations(from: network)) ?? [] } } From 0164dda076e71ba79935a4151b2109e0c049d54b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 16:57:50 -0700 Subject: [PATCH 1187/1722] 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 5600da470..b7d3d8e25 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -41,7 +41,7 @@ extension UtilityNetworkTraceViewModel { var graphics = [Graphic]() /// A user given name for the trace. - var name: String = "" + var name = "" /// A collection of starting points for the trace. var startingPoints = [UtilityNetworkTraceStartingPoint]() From 853e24759d600e0a2b95610bb7b87900a514a1ef Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 15 Jul 2022 17:03:02 -0700 Subject: [PATCH 1188/1722] Update UtilityNetworkTraceViewModelTrace.swift --- .../UtilityNetworkTraceViewModelTrace.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index b7d3d8e25..4cd94f52f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -46,14 +46,13 @@ extension UtilityNetworkTraceViewModel { /// A collection of starting points for the trace. var startingPoints = [UtilityNetworkTraceStartingPoint]() - /// A trace result set comprised of an collection of UtilityElement objects. + /// A trace result comprised of a collection of UtilityElement objects. var utilityElementTraceResult: UtilityElementTraceResult? - /// A trace result set comprised of a collection of UtilityTraceFunctionOutput objects. + /// A trace result comprised of a collection of UtilityTraceFunctionOutput objects. var utilityFunctionTraceResult: UtilityFunctionTraceResult? - /// The trace result comprised of a set of Geometry objects representing the network elements - /// identified by the trace. + /// A trace result comprised of Geometry objects var utilityGeometryTraceResult: UtilityGeometryTraceResult? } } From b313788424f6373fc3fbf66d24bb683c7a0f9b69 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Jul 2022 10:26:05 -0700 Subject: [PATCH 1189/1722] Documentation formatting --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index f39d1ae27..ebd9ea5a9 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -67,9 +67,9 @@ import SwiftUI /// Performs required setup. /// - /// The first utility network in the provided map will be used. - /// - Parameter map: The map to be loaded that contains at least one utility network. - /// - Parameter graphicsOverlay: The overlay on which trace graphics will be drawn. + /// - Parameters: + /// - map: The map to be loaded that contains at least one utility network. + /// - graphicsOverlay: The overlay on which trace graphics will be drawn. init(map: Map, graphicsOverlay: GraphicsOverlay) { self.graphicsOverlay = graphicsOverlay Task { From a2875f56e1727510cb6c5a233aa0bac654d04dec Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Jul 2022 10:32:35 -0700 Subject: [PATCH 1190/1722] Apply PR feedback --- Tests/ArcGISToolkitTests/CompassTests.swift | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 68377d312..1d25148a6 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -22,11 +22,11 @@ class CompassTests: XCTestCase { func testHiddenWithAutoHideOff() { let initialValue = 0.0 let finalValue = 90.0 - var _viewpoint: Viewpoint? = getViewpoint(initialValue) + var _viewpoint: Viewpoint? = makeViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint, autoHide: false) XCTAssertFalse(compass.shouldHide) - _viewpoint = getViewpoint(finalValue) + _viewpoint = makeViewpoint(finalValue) XCTAssertFalse(compass.shouldHide) } @@ -35,11 +35,11 @@ class CompassTests: XCTestCase { func testHiddenWithAutoHideOn() { let initialValue = 0.0 let finalValue = 90.0 - var _viewpoint: Viewpoint? = getViewpoint(initialValue) + var _viewpoint: Viewpoint? = makeViewpoint(initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) XCTAssertTrue(compass.shouldHide) - _viewpoint = getViewpoint(finalValue) + _viewpoint = makeViewpoint(finalValue) XCTAssertFalse(compass.shouldHide) } @@ -61,7 +61,7 @@ class CompassTests: XCTestCase { /// Asserts that the compass correctly initializes when given only a viewpoint. func testInitWithViewpoint() { - var _viewpoint: Viewpoint? = getViewpoint(0.0) + var _viewpoint: Viewpoint? = makeViewpoint(0.0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) XCTAssertEqual(compass.shouldHide, true) @@ -70,12 +70,9 @@ class CompassTests: XCTestCase { /// Asserts that the compass correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { let autoHide = false - var _viewpoint: Viewpoint? = getViewpoint(0) + var _viewpoint: Viewpoint? = makeViewpoint(0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass( - viewpoint: viewpoint, - autoHide: autoHide - ) + let compass = Compass(viewpoint: viewpoint, autoHide: autoHide) XCTAssertEqual(compass.shouldHide, false) } } @@ -94,7 +91,7 @@ extension CompassTests { /// Builds viewpoints to use for tests. /// - Parameter rotation: The rotation to use for the resulting viewpoint. /// - Returns: A viewpoint object for tests. - func getViewpoint(_ rotation: Double) -> Viewpoint { + func makeViewpoint(_ rotation: Double) -> Viewpoint { return Viewpoint(center: point, scale: scale, rotation: rotation) } } From 50161e574aa2371285fb020dc96554f1a27a7524 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 18 Jul 2022 15:52:49 -0500 Subject: [PATCH 1191/1722] FieldsPopupElement formatting tweaks; update to example and size of the PopupView in compact environments. --- Examples/Examples/PopupExampleView.swift | 25 ++++++++++++++++--- .../Popups/FieldsPopupElementView.swift | 4 ++- .../Popups/PopupElementHeader.swift | 17 +++++++++---- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index 5db105580..21a7f61b7 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -43,13 +43,16 @@ struct PopupExampleView: View { //https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=9e3baeb5dcd4473aa13e0065d7794ca6 let portalItem6 = PortalItem(portal: portal, id: Item.ID(rawValue: "9e3baeb5dcd4473aa13e0065d7794ca6")!) - return Map(item: portalItem1) + return Map(item: portalItem6) } - + /// The map displayed in the map view. @StateObject private var map = makeMap() + /// The point on the screen the user tapped on and used to identify layers. @State private var identifyScreenPoint: CGPoint? + + /// The result of the layer identify operation. @State private var identifyResult: Result<[IdentifyLayerResult], Error>? var body: some View { @@ -96,16 +99,30 @@ struct PopupExampleView: View { struct IdentifyResultView: View { var identifyResult: Result<[IdentifyLayerResult], Error> + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass + + /// If `true`, will draw the popup view at half height, exposing a portion of the + /// underlying map below the view on an iPhone in portrait orientation (and certain iPad multitasking + /// configurations). If `false`, will draw the popup view full size. + private var useHalfHeightResults: Bool { + horizontalSizeClass == .compact && verticalSizeClass == .regular + } + var body: some View { switch identifyResult { case .success(let identifyLayerResults): // Get the first popup from the first layer result. if let popup = identifyLayerResults.first?.popups.first { - PopupView(popup: popup) - .esriBorder() + GeometryReader { geometry in + PopupView(popup: popup) + .frame(maxHeight: useHalfHeightResults ? geometry.size.height / 2 : nil) + .esriBorder() + } } case .failure(let error): Text("Identify error: \(error.localizedDescription).") + .esriBorder() } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 5894707bf..bbd27d3ad 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -67,7 +67,9 @@ struct FieldsPopupElementView: View { } var body: some View { - Text(url != nil ? "View" : label) + // Using a single space in place of an empty label allows the height + // of an empty cell to be calculated the same as a non-empty cell. + Text(url != nil ? "View" : (label.isEmpty ? " " : label)) .underline(url != nil) .foregroundColor(url != nil ? Color(UIColor.link) : .primary) .lineLimit(1) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index e5879101f..c2e9bbdd0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -20,10 +20,17 @@ struct PopupElementHeader: View { var description: String var body: some View { - Text(title) - .font(.headline) - Text(description) - .font(.subheadline) - .foregroundColor(.secondary) + // Text views with empty text still take up some vertical space in + // a view, so conditionally check for an empty title and description. + if !title.isEmpty { + Text(title) + .font(.headline) + } + + if !description.isEmpty { + Text(description) + .font(.subheadline) + .foregroundColor(.secondary) + } } } From 75bba66aaf774c598369987522b690a736c4884b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 18 Jul 2022 16:28:21 -0500 Subject: [PATCH 1192/1722] Tweak example's setting of PopupView.frame. --- Examples/Examples/PopupExampleView.swift | 8 +++++--- .../Components/Popups/FieldsPopupElementView.swift | 2 +- Sources/ArcGISToolkit/Components/Popups/PopupView.swift | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index 21a7f61b7..0e83792dd 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -43,7 +43,7 @@ struct PopupExampleView: View { //https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=9e3baeb5dcd4473aa13e0065d7794ca6 let portalItem6 = PortalItem(portal: portal, id: Item.ID(rawValue: "9e3baeb5dcd4473aa13e0065d7794ca6")!) - return Map(item: portalItem6) + return Map(item: portalItem1) } /// The map displayed in the map view. @@ -87,7 +87,6 @@ struct PopupExampleView: View { IdentifyResultView(identifyResult: identifyResult) } } - .frame(maxWidth: 400) .padding() } } @@ -116,7 +115,10 @@ struct IdentifyResultView: View { if let popup = identifyLayerResults.first?.popups.first { GeometryReader { geometry in PopupView(popup: popup) - .frame(maxHeight: useHalfHeightResults ? geometry.size.height / 2 : nil) + .frame( + maxWidth: useHalfHeightResults ? .infinity : 400, + maxHeight: useHalfHeightResults ? geometry.size.height / 2 : nil + ) .esriBorder() } } diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index bbd27d3ad..af9a570cb 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -59,7 +59,7 @@ struct FieldsPopupElementView: View { struct GridCell: View { /// The String to display. let label: String - /// The index of the row the view is in. + /// The index of the row the cell is in. let rowIndex: Int /// The URL off the label if the label is an "http" string. var url: URL? { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 7832a98dc..4dbb85593 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -70,12 +70,15 @@ public struct PopupView: View { switch popupElement { case let popupElement as AttachmentsPopupElement: Text("AttachmentsPopupElementView implementation coming soon.") + .font(.caption) case let popupElement as FieldsPopupElement: FieldsPopupElementView(popupElement: popupElement) case let popupElement as MediaPopupElement: Text("MediaPopupElementView implementation coming soon.") + .font(.caption) case let popupElement as TextPopupElement: Text("TextPopupElementView implementation coming soon.") + .font(.caption) default: EmptyView() } From 0a8c6bff37039ff361164fbfe7e4a4e6e5b3fbcc Mon Sep 17 00:00:00 2001 From: R Olson Date: Tue, 19 Jul 2022 09:48:04 -0600 Subject: [PATCH 1193/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Components/Authentication/Authenticator.swift | 13 ++++++------- .../Authentication/AuthenticatorModifier.swift | 6 ++---- .../Authentication/QueuedNetworkChallenge.swift | 4 +--- .../TrustHostViewModifierTests.swift | 2 +- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 3601a88ab..84b5dbc6f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -28,7 +28,7 @@ public final class Authenticator: ObservableObject { observationTask?.cancel() } - // The task for the observation of the challenge queue. + /// The task for the observation of the challenge queue. private var observationTask: Task? /// Creates an authenticator. @@ -48,7 +48,7 @@ public final class Authenticator: ObservableObject { /// 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. + /// 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. /// - isSynchronizable: A value indicating whether the credentials are synchronized with iCloud. @@ -100,11 +100,11 @@ public final class Authenticator: ObservableObject { // Set the current challenge, this should present the appropriate view. currentChallenge = queuedChallenge - + // Wait for the queued challenge to finish. await queuedChallenge.complete() - - // Reset the crrent challenge to `nil`, that will dismiss the view. + + // Reset the current challenge to `nil`, that will dismiss the view. currentChallenge = nil } } @@ -121,8 +121,7 @@ public final class Authenticator: ObservableObject { } /// The current queued challenge. - @Published - var currentChallenge: QueuedChallenge? + @Published var currentChallenge: QueuedChallenge? } extension Authenticator: AuthenticationChallengeHandler { diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 7111f1d09..a6aa62e5a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -14,11 +14,9 @@ import SwiftUI public extension View { - /// Presents user experiences for collecting network authentication credentials from the - /// user. + /// Presents user experiences for collecting network authentication credentials from the user. /// - Parameter authenticator: The authenticator for which credentials will be prompted. - @ViewBuilder - func authenticator(_ authenticator: Authenticator) -> some View { + @ViewBuilder func authenticator(_ authenticator: Authenticator) -> some View { modifier(AuthenticatorOverlayModifier(authenticator: authenticator)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index 618aac638..62e859eac 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -88,10 +88,8 @@ extension QueuedNetworkChallenge.Kind { self = .serverTrust case .ntlm, .basic, .digest: self = .login - case .pki: + case .clientCertificate: self = .certificate - case .htmlForm, .negotiate: - return nil } } } diff --git a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift index 5d868919a..53540549a 100644 --- a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift +++ b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import ArcGISToolkit @MainActor -class TrustHostViewModifierTets: XCTestCase { +class TrustHostViewModifierTests: XCTestCase { func testInit() { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) // Tests the initial state. From ee97d880605876d34ba10b133960982fc0c93543 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 19 Jul 2022 09:50:10 -0600 Subject: [PATCH 1194/1722] pr feedback --- .../Components/Authentication/AuthenticatorModifier.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index a6aa62e5a..1c5c575be 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -31,8 +31,7 @@ public extension View { private struct AuthenticatorOverlayModifier: ViewModifier { @ObservedObject var authenticator: Authenticator - @ViewBuilder - func body(content: Content) -> some View { + @ViewBuilder func body(content: Content) -> some View { ZStack { content Color.clear @@ -46,8 +45,7 @@ private struct AuthenticatorOverlayModifier: ViewModifier { private struct AuthenticatorModifier: ViewModifier { @ObservedObject var authenticator: Authenticator - @ViewBuilder - func body(content: Content) -> some View { + @ViewBuilder func body(content: Content) -> some View { switch authenticator.currentChallenge { case let challenge as QueuedNetworkChallenge: switch challenge.kind { From 997d2fb6c2481abfea90708f82d502e7fa984ec1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 18 Jul 2022 17:18:13 -0700 Subject: [PATCH 1195/1722] Predefined starting points --- .../UtilityNetworkTraceExampleView.swift | 40 +++- .../UtilityNetworkTrace.swift | 19 +- .../UtilityNetworkTraceViewModel.swift | 188 +++++++++++------- 3 files changed, 170 insertions(+), 77 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 45e7ef5de..5c3797f8d 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -18,6 +18,7 @@ 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 displayed in the map view. @StateObject private var map = makeMap() /// Provides the ability to inspect map components. @@ -32,6 +33,9 @@ struct UtilityNetworkTraceExampleView: View { /// A container for graphical trace results. @State var resultGraphicsOverlay = GraphicsOverlay() + /// Optionally used to provide programmatic starting points to the trace tool. + @State var startingPoints: [GeoElement] = [] + /// The map viewpoint used by the `UtilityNetworkTrace` to pan/zoom the map to selected features. @State var viewpoint: Viewpoint? @@ -58,7 +62,8 @@ struct UtilityNetworkTraceExampleView: View { mapPoint: $mapPoint, viewPoint: $viewPoint, mapViewProxy: $mapViewProxy, - viewpoint: $viewpoint + viewpoint: $viewpoint, + startingPoints: $startingPoints ) .task { await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) @@ -67,6 +72,17 @@ struct UtilityNetworkTraceExampleView: View { .padding() .frame(width: 360) } + .overlay(alignment: .topLeading) { + Button { + Task { + await setPredefinedStartingPoints() + } + } label: { + Text("Set predefined starting points") + } + .buttonStyle(.borderedProminent) + .padding() + } } } @@ -91,3 +107,25 @@ private extension ArcGISCredential { } } } + +extension UtilityNetworkTraceExampleView { + /// Queries the map for a feature with a certain ID and sets the list of starting points. + func setPredefinedStartingPoints() async { + let targetID = UUID(uuidString: "2A6D25D5-8B9E-400A-BC07-4A11BD8B6C82") + let groupLayer = map.operationalLayers.first as? GroupLayer + let parameters = QueryParameters() + parameters.addObjectId(1740) + for layer in groupLayer?.layers ?? [] { + guard let layer = layer as? FeatureLayer, + let table = layer.featureTable else { continue } + let query = try? await table.queryFeatures(parameters: parameters) + query?.features().forEach { element in + if let feature = element as? ArcGISFeature, + let id = feature.attributes["globalid"] as? UUID, + id == targetID { + startingPoints = [element] + } + } + } + } +} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index f656ab779..99f918a37 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -67,6 +67,9 @@ public struct UtilityNetworkTrace: View { // MARK: Bindings + /// Starting points programmatically provided to the trace tool. + @Binding private var externalStartingPoints: [GeoElement] + /// The graphics overlay to hold generated starting point and trace graphics. @Binding private var graphicsOverlay: GraphicsOverlay @@ -130,7 +133,10 @@ public struct UtilityNetworkTrace: View { List { ForEach(assetGroup.sorted(by: { $0.key < $1.key }), id: \.key) { assetTypeGroup in Section(assetTypeGroup.key) { - ForEach(assetTypeGroup.value) { element in + let elements = assetTypeGroup.value.sorted { + $0.objectID < $1.objectID + } + ForEach(elements) { element in makeZoomToButton(text: element.objectID.description) { Task { if let feature = await viewModel.getFeatureFor(element: element), @@ -470,23 +476,27 @@ public struct UtilityNetworkTrace: View { /// - 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. + /// - startingPoints: An optional list of elements to be used as starting points. public init( graphicsOverlay: Binding, map: Map, mapPoint: Binding, viewPoint: Binding, mapViewProxy: Binding, - viewpoint: Binding + viewpoint: Binding, + startingPoints: Binding<[GeoElement]> = .constant([]) ) { _viewPoint = viewPoint _mapPoint = mapPoint _mapViewProxy = mapViewProxy _graphicsOverlay = graphicsOverlay _viewpoint = viewpoint + _externalStartingPoints = startingPoints _viewModel = StateObject( wrappedValue: UtilityNetworkTraceViewModel( map: map, - graphicsOverlay: graphicsOverlay.wrappedValue + graphicsOverlay: graphicsOverlay.wrappedValue, + startingPoints: startingPoints.wrappedValue ) ) } @@ -534,6 +544,9 @@ public struct UtilityNetworkTrace: View { ) } } + .onChange(of: externalStartingPoints.count) { _ in + viewModel.externalStartingPoints = externalStartingPoints + } .alert( "Warning", isPresented: Binding( diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index effa0fe99..c2387ef83 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -69,6 +69,13 @@ import SwiftUI /// A map containing one or more utility networks. private var map: Map + /// Starting points programmatically provided to the trace tool. + var externalStartingPoints = [GeoElement]() { + didSet { + addExternalStartingPoints() + } + } + /// The selected trace. var selectedTrace: Trace? { if let index = selectedTraceIndex { @@ -83,9 +90,15 @@ import SwiftUI /// - Parameters: /// - map: The map to be loaded that contains at least one utility network. /// - graphicsOverlay: The overlay on which trace graphics will be drawn. - init(map: Map, graphicsOverlay: GraphicsOverlay) { + /// - startingPoints: Starting points programmatically provided to the trace tool. + init( + map: Map, + graphicsOverlay: GraphicsOverlay, + startingPoints: [GeoElement] + ) { self.map = map self.graphicsOverlay = graphicsOverlay + self.externalStartingPoints = startingPoints Task { do { try await map.load() @@ -142,6 +155,37 @@ import SwiftUI } } + /// Selects the previous trace from the list of completed traces. + func selectPreviousTrace() { + if let current = selectedTraceIndex { + if current - 1 >= 0 { + selectedTraceIndex = current - 1 + } else { + selectedTraceIndex = completedTraces.count - 1 + } + } + } + + /// Updates the fractional portion of an edge based starting point. + /// - Parameters: + /// - startingPoint: The starting point to be updated. + /// - newValue: A fraction along the starting point's edge. + func setFractionAlongEdgeFor( + startingPoint: UtilityNetworkTraceStartingPoint, + to newValue: Double + ) { + pendingTrace.startingPoints.first { + $0.utilityElement.globalID == startingPoint.utilityElement.globalID + }?.utilityElement.fractionAlongEdge = newValue + if let geometry = startingPoint.geoElement.geometry, + let polyline = geometry as? Polyline { + startingPoint.graphic.geometry = GeometryEngine.point( + along: polyline, + atDistance: GeometryEngine.length(of: geometry) * newValue + ) + } + } + /// Changes the selected network. /// - Parameter network: The new utility network to be selected. /// @@ -166,17 +210,6 @@ import SwiftUI } } - /// Selects the previous trace from the list of completed traces. - func selectPreviousTrace() { - if let current = selectedTraceIndex { - if current - 1 >= 0 { - selectedTraceIndex = current - 1 - } else { - selectedTraceIndex = completedTraces.count - 1 - } - } - } - /// Adds a new starting point to the pending trace. /// - Parameters: /// - point: A point on the map in screen coordinates. @@ -193,73 +226,74 @@ import SwiftUI ) identifyLayerResults?.forEach { identifyLayerResult in identifyLayerResult.geoElements.forEach { geoElement in - - // Block duplicate starting point selection - guard let feature = geoElement as? ArcGISFeature, - let globalid = feature.attributes["globalid"] as? UUID else { - userWarning = "Element could not be identified" - return - } - guard !pendingTrace.startingPoints.contains(where: { startingPoint in - return startingPoint.utilityElement.globalID == globalid - }) else { - userWarning = "Duplicate starting points cannot be added" - return - } - - Task { - guard let network = network, - let geometry = feature.geometry, - let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? - .renderer? - .symbol(for: feature)? - .makeSwatch(scale: 1.0), - let utilityElement = network.createElement(arcGISFeature: feature) else { return } - - if utilityElement.networkSource.kind == .edge && geometry is Polyline { - utilityElement.fractionAlongEdge = fractionAlongEdge( - of: geometry, - at: mapPoint - ) - } else if utilityElement.networkSource.kind == .junction && - utilityElement.assetType.terminalConfiguration?.terminals.count ?? 0 > 1 { - utilityElement.terminal = utilityElement.assetType.terminalConfiguration?.terminals.first - } - - let graphic = Graphic( - geometry: mapPoint, - symbol: SimpleMarkerSymbol( - color: UIColor(pendingTrace.color), - size: 20 - ) - ) - let startingPoint = UtilityNetworkTraceStartingPoint( - extent: geometry.extent, - geoElement: geoElement, - graphic: graphic, - image: symbol, - utilityElement: utilityElement - ) - graphicsOverlay.addGraphic(graphic) - pendingTrace.startingPoints.append(startingPoint) - } + setStartingPoint(geoElement: geoElement, mapPoint: mapPoint) } } } - func setFractionAlongEdgeFor( - startingPoint: UtilityNetworkTraceStartingPoint, - to newValue: Double + /// Adds a new starting point to the pending trace. + /// - Parameters: + /// - geoElement: An element that corresponds to another within the utility network. + /// - mapPoint: A point on the map in map coordinates. + func setStartingPoint( + geoElement: GeoElement, + mapPoint: Point? = nil ) { - pendingTrace.startingPoints.first { - $0.utilityElement.globalID == startingPoint.utilityElement.globalID - }?.utilityElement.fractionAlongEdge = newValue - if let geometry = startingPoint.geoElement.geometry, - let polyline = geometry as? Polyline { - startingPoint.graphic.geometry = GeometryEngine.point( - along: polyline, - atDistance: GeometryEngine.length(of: geometry) * newValue + Task { + guard let feature = geoElement as? ArcGISFeature, + let globalid = feature.attributes["globalid"] as? UUID else { + userWarning = "Element could not be identified" + return + } + + // Block duplicate starting point selection + guard !pendingTrace.startingPoints.contains(where: { startingPoint in + return startingPoint.utilityElement.globalID == globalid + }) else { + userWarning = "Duplicate starting points cannot be added" + return + } + + guard let network = self.network, + let geometry = feature.geometry, + let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? + .renderer? + .symbol(for: feature)? + .makeSwatch(scale: 1.0), + let utilityElement = network.createElement(arcGISFeature: feature) else { return } + + if utilityElement.networkSource.kind == .edge && geometry is Polyline { + if let mapPoint { + utilityElement.fractionAlongEdge = fractionAlongEdge( + of: geometry, + at: mapPoint + ) + } else { + utilityElement.fractionAlongEdge = 0.5 + } + + } else if utilityElement.networkSource.kind == .junction && + utilityElement.assetType.terminalConfiguration?.terminals.count ?? 0 > 1 { + utilityElement.terminal = utilityElement.assetType.terminalConfiguration?.terminals.first + } + + let graphic = Graphic( + geometry: mapPoint ?? feature.geometry?.extent.center, + symbol: SimpleMarkerSymbol( + color: UIColor(self.pendingTrace.color), + size: 20 + ) ) + + let startingPoint = UtilityNetworkTraceStartingPoint( + extent: geometry.extent, + geoElement: geoElement, + graphic: graphic, + image: symbol, + utilityElement: utilityElement + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.startingPoints.append(startingPoint) } } @@ -363,6 +397,7 @@ import SwiftUI completedTraces.append(pendingTrace) selectedTraceIndex = completedTraces.count - 1 pendingTrace = Trace() + addExternalStartingPoints() return true } @@ -377,6 +412,13 @@ import SwiftUI // MARK: Private Methods + /// Adds programatic starting points to the pending trace. + private func addExternalStartingPoints() { + externalStartingPoints.forEach { + setStartingPoint(geoElement: $0) + } + } + /// Changes the selected state of the graphics for the completed trace at the provided index. /// - Parameters: /// - index: The index of the completed trace. From 1b45c3f17804698ac9ac123a95016762d9c310d0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 10:39:19 -0700 Subject: [PATCH 1196/1722] Give symbols background --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 99f918a37..c6f21ca8a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -453,6 +453,9 @@ public struct UtilityNetworkTrace: View { .lineLimit(1) } icon: { Image(uiImage: startingPoint.image) + .frame(width: 25, height: 25) + .background(Color.secondary) + .cornerRadius(5) } } .swipeActions { From a4d33453bf52ef40054cac662c4f7084968b31ff Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 10:50:58 -0700 Subject: [PATCH 1197/1722] Move UtilityTraceFunctionOutput: Identifiable --- .../UtilityNetworkTraceViewModel.swift | 2 -- .../Extensions/UtilityTraceFunctionOutput.swift | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionOutput.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index c2387ef83..c26c3fc4d 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -477,5 +477,3 @@ extension UtilityNetworkTraceViewModel { ) } } - -extension UtilityTraceFunctionOutput: Identifiable { } diff --git a/Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionOutput.swift b/Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionOutput.swift new file mode 100644 index 000000000..48e5d8bd6 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionOutput.swift @@ -0,0 +1,16 @@ +// 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 UtilityTraceFunctionOutput: Identifiable { } From 4df52795fadc2cf1f1123658523835ebd83b067c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:16:49 -0700 Subject: [PATCH 1198/1722] 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 f589d4fcb..daad0fe9e 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -26,7 +26,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. - internal var shouldHide: Bool { heading.isZero && autoHide } + var shouldHide: Bool { heading.isZero && autoHide } /// The heading of the compass in degrees. @Binding private var heading: Double From 9a9831164726025592a38753c2bae4904a8c7cf3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:17:10 -0700 Subject: [PATCH 1199/1722] Fix spelling --- Tests/ArcGISToolkitTests/CompassDirectionTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/CompassDirectionTests.swift b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift index 34219d2ee..6bb71b874 100644 --- a/Tests/ArcGISToolkitTests/CompassDirectionTests.swift +++ b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import ArcGISToolkit class CompassDirectionTests: XCTestCase { - /// Tests the behvaior of `CompassDirection.init(_ : Double)` + /// Tests the behavior of `CompassDirection.init(_ : Double)` func testInitCompassDirection() { XCTAssertEqual(CompassDirection(-405), .northwest) XCTAssertEqual(CompassDirection(-360), .north) From cd9bf0599c9ed65a76e3405f7ebed443a7c47bcb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:19:22 -0700 Subject: [PATCH 1200/1722] Mark test classes final --- Tests/ArcGISToolkitTests/CompassDirectionTests.swift | 2 +- Tests/ArcGISToolkitTests/CompassTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassDirectionTests.swift b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift index 6bb71b874..e1b7efa3c 100644 --- a/Tests/ArcGISToolkitTests/CompassDirectionTests.swift +++ b/Tests/ArcGISToolkitTests/CompassDirectionTests.swift @@ -14,7 +14,7 @@ import XCTest @testable import ArcGISToolkit -class CompassDirectionTests: XCTestCase { +final class CompassDirectionTests: XCTestCase { /// Tests the behavior of `CompassDirection.init(_ : Double)` func testInitCompassDirection() { XCTAssertEqual(CompassDirection(-405), .northwest) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 1d25148a6..1dfbc955f 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -16,7 +16,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -class CompassTests: XCTestCase { +final class CompassTests: XCTestCase { /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide /// is disabled. func testHiddenWithAutoHideOff() { From a227e6cf3c806704f5c6b67e91a784b4ee4de4a2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:20:57 -0700 Subject: [PATCH 1201/1722] Documentation --- Tests/ArcGISToolkitTests/CompassTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 1dfbc955f..76ceb83bc 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -17,8 +17,8 @@ import XCTest @testable import ArcGISToolkit final class CompassTests: XCTestCase { - /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide - /// is disabled. + /// Verifies that the compass accurately indicates when the compass should be hidden when + /// `autoHide` is `false`. func testHiddenWithAutoHideOff() { let initialValue = 0.0 let finalValue = 90.0 From e844d45e4ba891edb4de34d1f084901ce8ceeaff Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:22:02 -0700 Subject: [PATCH 1202/1722] Documentation --- Tests/ArcGISToolkitTests/CompassTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 76ceb83bc..8e395550a 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -30,8 +30,8 @@ final class CompassTests: XCTestCase { XCTAssertFalse(compass.shouldHide) } - /// Asserts that the compass accurately indicates when the compass should be hidden when autoHide - /// is enabled. + /// Verifies that the compass accurately indicates when the compass should be hidden when + /// `autoHide` is `true`. func testHiddenWithAutoHideOn() { let initialValue = 0.0 let finalValue = 90.0 From c2937fd253c8fa3a37ad86ca24242ca8a91655a3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:23:09 -0700 Subject: [PATCH 1203/1722] Documentation --- Tests/ArcGISToolkitTests/CompassTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 8e395550a..94354b5fb 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -43,7 +43,7 @@ final class CompassTests: XCTestCase { XCTAssertFalse(compass.shouldHide) } - /// Asserts that the compass correctly initializes when given a nil viewpoint + /// Verifies that the compass correctly initializes when given a `nil` viewpoint. func testInit() { var _viewpoint: Viewpoint? = nil let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) From 97605205979b77523b23e4955f8ddd6d3e0047fa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:25:19 -0700 Subject: [PATCH 1204/1722] Documentation --- Tests/ArcGISToolkitTests/CompassTests.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 94354b5fb..e86dc2b11 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -51,7 +51,8 @@ final class CompassTests: XCTestCase { XCTAssertEqual(compass.shouldHide, true) } - /// Asserts that the compass correctly initializes when given a nil viewpoint, and autoHide is disabled. + /// Verifies that the compass correctly initializes when given a `nil` viewpoint, and `autoHide` is + /// `false`. func testInitNoAutoHide() { var _viewpoint: Viewpoint? = nil let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -59,7 +60,7 @@ final class CompassTests: XCTestCase { XCTAssertEqual(compass.shouldHide, false) } - /// Asserts that the compass correctly initializes when given only a viewpoint. + /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpoint() { var _viewpoint: Viewpoint? = makeViewpoint(0.0) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) @@ -67,7 +68,7 @@ final class CompassTests: XCTestCase { XCTAssertEqual(compass.shouldHide, true) } - /// Asserts that the compass correctly initializes when given only a viewpoint. + /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { let autoHide = false var _viewpoint: Viewpoint? = makeViewpoint(0) From 2a261b5e89e5c2515d9c546c41112d42caa7c69d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 12:28:48 -0700 Subject: [PATCH 1205/1722] Simplify tests --- Tests/ArcGISToolkitTests/CompassTests.swift | 25 +++++++-------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index e86dc2b11..6d15d91cf 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -45,36 +45,27 @@ final class CompassTests: XCTestCase { /// Verifies that the compass correctly initializes when given a `nil` viewpoint. func testInit() { - var _viewpoint: Viewpoint? = nil - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(compass.shouldHide, true) + 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 testInitNoAutoHide() { - var _viewpoint: Viewpoint? = nil - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint, autoHide: false) - XCTAssertEqual(compass.shouldHide, false) + let compass = Compass(viewpoint: .constant(nil), autoHide: false) + XCTAssertFalse(compass.shouldHide) } /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpoint() { - var _viewpoint: Viewpoint? = makeViewpoint(0.0) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint) - XCTAssertEqual(compass.shouldHide, true) + let compass = Compass(viewpoint: .constant(makeViewpoint(.zero))) + XCTAssertTrue(compass.shouldHide) } /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpointAndAutoHide() { - let autoHide = false - var _viewpoint: Viewpoint? = makeViewpoint(0) - let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) - let compass = Compass(viewpoint: viewpoint, autoHide: autoHide) - XCTAssertEqual(compass.shouldHide, false) + let compass = Compass(viewpoint: .constant(makeViewpoint(.zero)), autoHide: false) + XCTAssertFalse(compass.shouldHide) } } From f3bbd381b5a0f3e73e2e857285eb3e6a318b248a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 13:04:25 -0700 Subject: [PATCH 1206/1722] Update CompassTests.swift --- Tests/ArcGISToolkitTests/CompassTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 6d15d91cf..3591a5913 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -83,7 +83,7 @@ extension CompassTests { /// 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 { + func makeViewpoint(rotation: Double) -> Viewpoint { return Viewpoint(center: point, scale: scale, rotation: rotation) } } From 89ef0f968a7db1e536035ec30466ff5349b42645 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 13:06:24 -0700 Subject: [PATCH 1207/1722] Update CompassTests.swift --- Tests/ArcGISToolkitTests/CompassTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/ArcGISToolkitTests/CompassTests.swift b/Tests/ArcGISToolkitTests/CompassTests.swift index 3591a5913..9c8b8bb83 100644 --- a/Tests/ArcGISToolkitTests/CompassTests.swift +++ b/Tests/ArcGISToolkitTests/CompassTests.swift @@ -22,11 +22,11 @@ final class CompassTests: XCTestCase { func testHiddenWithAutoHideOff() { let initialValue = 0.0 let finalValue = 90.0 - var _viewpoint: Viewpoint? = makeViewpoint(initialValue) + var _viewpoint: Viewpoint? = makeViewpoint(rotation: initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint, autoHide: false) XCTAssertFalse(compass.shouldHide) - _viewpoint = makeViewpoint(finalValue) + _viewpoint = makeViewpoint(rotation: finalValue) XCTAssertFalse(compass.shouldHide) } @@ -35,11 +35,11 @@ final class CompassTests: XCTestCase { func testHiddenWithAutoHideOn() { let initialValue = 0.0 let finalValue = 90.0 - var _viewpoint: Viewpoint? = makeViewpoint(initialValue) + var _viewpoint: Viewpoint? = makeViewpoint(rotation: initialValue) let viewpoint = Binding(get: { _viewpoint }, set: { _viewpoint = $0 }) let compass = Compass(viewpoint: viewpoint) XCTAssertTrue(compass.shouldHide) - _viewpoint = makeViewpoint(finalValue) + _viewpoint = makeViewpoint(rotation: finalValue) XCTAssertFalse(compass.shouldHide) } @@ -58,13 +58,13 @@ final class CompassTests: XCTestCase { /// Verifies that the compass correctly initializes when given only a viewpoint. func testInitWithViewpoint() { - let compass = Compass(viewpoint: .constant(makeViewpoint(.zero))) + 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(.zero)), autoHide: false) + let compass = Compass(viewpoint: .constant(makeViewpoint(rotation: .zero)), autoHide: false) XCTAssertFalse(compass.shouldHide) } } From 59a968d05298e9f50b908ca3fec9e270b7ddd69c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 13:14:05 -0700 Subject: [PATCH 1208/1722] Spelling --- 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 daad0fe9e..92373e4fd 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -103,10 +103,10 @@ public extension Compass { let viewpointRotation = Binding { viewpoint.wrappedValue?.rotation ?? .zero } set: { newViewpointRotation in - guard let oldViewopint = viewpoint.wrappedValue else { return } + guard let oldViewpoint = viewpoint.wrappedValue else { return } viewpoint.wrappedValue = Viewpoint( - center: oldViewopint.targetGeometry.extent.center, - scale: oldViewopint.targetScale, + center: oldViewpoint.targetGeometry.extent.center, + scale: oldViewpoint.targetScale, rotation: newViewpointRotation ) } From a0a5a1ada8bc503270af513b6b74f6bceb180053 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 13:17:03 -0700 Subject: [PATCH 1209/1722] Spelling, line break points --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 92373e4fd..00404c4ef 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -92,10 +92,9 @@ public extension Compass { /// Creates a compass with a binding to an optional viewpoint. /// - Parameters: - /// - viewpoint: The viewopint whose rotation determines the heading of - /// the compass. - /// - autoHide: A Boolean value that determines whether the compass - /// automatically hides itself when the viewpoint's rotation is 0 degrees. + /// - viewpoint: The viewpoint whose rotation determines the heading of the compass. + /// - autoHide: A Boolean value that determines whether the compass automatically hides itself + /// when the viewpoint's rotation is 0 degrees. init( viewpoint: Binding, autoHide: Bool = true From 12c0f50a5083fc812b1c4e5169e55bc3e1a0a3de Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 19 Jul 2022 13:20:13 -0700 Subject: [PATCH 1210/1722] Update Compass.swift --- Sources/ArcGISToolkit/Components/Compass/Compass.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index 00404c4ef..ad0572a5a 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -26,7 +26,9 @@ public struct Compass: View { /// 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 && autoHide } + var shouldHide: Bool { + (heading.isZero || heading.isNaN) && autoHide + } /// The heading of the compass in degrees. @Binding private var heading: Double @@ -100,7 +102,7 @@ public extension Compass { autoHide: Bool = true ) { let viewpointRotation = Binding { - viewpoint.wrappedValue?.rotation ?? .zero + viewpoint.wrappedValue?.rotation ?? .nan } set: { newViewpointRotation in guard let oldViewpoint = viewpoint.wrappedValue else { return } viewpoint.wrappedValue = Viewpoint( From 65de3f764e745b6fd93275602bb380d10e5705f9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Jul 2022 16:35:30 -0500 Subject: [PATCH 1211/1722] Use single column grid instead of multi-column; increase popup.title font size; use Text view showing markup link for http formattedValues. --- .../Popups/FieldsPopupElementView.swift | 68 ++++++++++--------- .../Components/Popups/PopupView.swift | 5 +- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index af9a570cb..85cc4d45d 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -33,60 +33,64 @@ struct FieldsPopupElementView: View { var body: some View { PopupElementHeader( - title: popupElement.title, + title: popupElement.title.isEmpty ? "Fields" : popupElement.title, description: popupElement.description ) FieldsGrid(fields: displayFields) - .font(.footnote) } /// A view displaying a grid of labels and values. struct FieldsGrid: View { let fields: [DisplayField] + var columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 0), count: 1) - var columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 0), count: 2) var body: some View { LazyVGrid(columns: columns, spacing: 0) { - ForEach(Array(fields.enumerated()), id: \.element) { index, element in - GridCell(label: element.label, rowIndex: index) - GridCell(label: element.formattedValue, rowIndex: index) + ForEach(fields) { field in + VStack(alignment: .leading) { + Text(field.label) + .font(.subheadline) + .foregroundColor(.secondary) + .padding([.top], 6) + FormattedValueText(formattedValue: field.formattedValue) + .padding([.bottom], -2) + Divider() + } + .lineLimit(1) + .background(Color.clear) + .frame(maxWidth: .infinity, alignment: .leading) } } } } - /// A view for dispaying text in a grid. - struct GridCell: View { + /// A view for dispaying a formatted value. + struct FormattedValueText: View { /// The String to display. - let label: String - /// The index of the row the cell is in. - let rowIndex: Int - /// The URL off the label if the label is an "http" string. + let formattedValue: String + /// The URL of the label if the label is an "http" string. var url: URL? { - label.lowercased().starts(with: "http") ? URL(string: label) : nil + formattedValue.lowercased().starts(with: "http") ? URL(string: formattedValue) : nil } var body: some View { - // Using a single space in place of an empty label allows the height - // of an empty cell to be calculated the same as a non-empty cell. - Text(url != nil ? "View" : (label.isEmpty ? " " : label)) - .underline(url != nil) - .foregroundColor(url != nil ? Color(UIColor.link) : .primary) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(6) - .onTapGesture { - if let url = url { - UIApplication.shared.open(url) - } + Group { + if let url = url, + let link = "[View](\(url.absoluteString))"{ + Text(.init(link)) + } else { + Text(formattedValue) } - .background(rowIndex % 2 != 1 ? Color.secondary.opacity(0.5) : Color.clear) + } } } - - /// A convenience type for displaying labels and values in a grid. - struct DisplayField: Hashable { - let label: String - let formattedValue: String - } } + +/// A convenience type for displaying labels and values in a grid. +struct DisplayField: Hashable { + let label: String + let formattedValue: String + let id = UUID() +} + +extension DisplayField: Identifiable {} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 4dbb85593..ec25cae62 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -35,6 +35,7 @@ public struct PopupView: View { public var body: some View { VStack(alignment: .leading) { Text(popup.title) + .font(.title) .fontWeight(.bold) Divider() Group { @@ -45,7 +46,7 @@ public struct PopupView: View { } .frame(maxWidth: .infinity) } else if isPopupEvaluated! { - popupElementScrollView(popup: popup) + PopupElementScrollView(popup: popup) } else { Text("Popup failed evaluation.") } @@ -61,7 +62,7 @@ public struct PopupView: View { } } - struct popupElementScrollView: View { + struct PopupElementScrollView: View { var popup: Popup var body: some View { ScrollView { From dc97240430d2583d6300c9d3dfb9b5c30fef69bd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 19 Jul 2022 17:16:30 -0500 Subject: [PATCH 1212/1722] Tweaks to new VStack (as opposed to Grid) layout. --- .../Popups/FieldsPopupElementView.swift | 46 +++++++++++++------ .../Popups/PopupElementHeader.swift | 26 ++++++----- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 85cc4d45d..69c6ed299 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -33,37 +33,47 @@ struct FieldsPopupElementView: View { var body: some View { PopupElementHeader( - title: popupElement.title.isEmpty ? "Fields" : popupElement.title, + title: popupElement.displayTitle, description: popupElement.description ) - FieldsGrid(fields: displayFields) + .padding([.bottom], 4) + FieldsList(fields: displayFields) } /// A view displaying a grid of labels and values. - struct FieldsGrid: View { + struct FieldsList: View { let fields: [DisplayField] - var columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 0), count: 1) var body: some View { - LazyVGrid(columns: columns, spacing: 0) { + VStack { ForEach(fields) { field in - VStack(alignment: .leading) { - Text(field.label) - .font(.subheadline) - .foregroundColor(.secondary) - .padding([.top], 6) - FormattedValueText(formattedValue: field.formattedValue) - .padding([.bottom], -2) + FieldRow(field: field) + if field != fields.last { Divider() } - .lineLimit(1) - .background(Color.clear) - .frame(maxWidth: .infinity, alignment: .leading) } } } } + /// A view for displaying a `DisplayField`. + struct FieldRow: View { + var field: DisplayField + + var body: some View { + VStack(alignment: .leading) { + Text(field.label) + .font(.subheadline) + .foregroundColor(.secondary) + FormattedValueText(formattedValue: field.formattedValue) + .padding([.bottom], -1) + } + .lineLimit(1) + .background(Color.clear) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + /// A view for dispaying a formatted value. struct FormattedValueText: View { /// The String to display. @@ -94,3 +104,9 @@ struct DisplayField: Hashable { } extension DisplayField: Identifiable {} + +extension FieldsPopupElement { + var displayTitle: String { + title.isEmpty ? "Fields" : title + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index c2e9bbdd0..de6f90999 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -20,17 +20,21 @@ struct PopupElementHeader: View { var description: String var body: some View { - // Text views with empty text still take up some vertical space in - // a view, so conditionally check for an empty title and description. - if !title.isEmpty { - Text(title) - .font(.headline) - } - - if !description.isEmpty { - Text(description) - .font(.subheadline) - .foregroundColor(.secondary) + VStack(alignment: .leading) { + // Text views with empty text still take up some vertical space in + // a view, so conditionally check for an empty title and description. + if !title.isEmpty { + Text(title) + .font(.title3) + .fontWeight(.bold) + .foregroundColor(.primary) + } + + if !description.isEmpty { + Text(description) + .font(.subheadline) + .foregroundColor(.secondary) + } } } } From 2d6ccf0e97b345791e836c098c7f25231b94b8bb Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 19 Jul 2022 17:21:35 -0600 Subject: [PATCH 1213/1722] add client certificate modifier --- .../AuthenticatorModifier.swift | 2 +- .../CertificatePickerViewModifier.swift | 293 ++++++++++++++++++ .../Authentication/DocumentPickerView.swift | 60 ++++ 3 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift create mode 100644 Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 4fdee14d9..6a2871a4c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -62,7 +62,7 @@ private struct AuthenticatorModifier: ViewModifier { case .serverTrust: content.modifier(TrustHostViewModifier(challenge: challenge)) case .certificate: - fatalError("implementation coming soon in another PR") + content.modifier(CertificatePickerViewModifier(challenge: challenge)) case .login: content.modifier(LoginViewModifier(challenge: challenge)) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift new file mode 100644 index 000000000..81e9f2f18 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -0,0 +1,293 @@ +// 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 UniformTypeIdentifiers + +/// An object that provides the business logic for the workflow of prompting the user for a +/// certificate and a password. +@MainActor +final private class CertificatePickerViewModel: ObservableObject { + /// The challenge that requires a certificate to proceed. + let challenge: QueuedNetworkChallenge + + /// The URL of the certificate that the user chose. + private var certificateURL: URL? + + /// A Boolean value indicating whether to show the prompt. + @Published var showPrompt = true + /// A Boolean value indicating whether to show the certificate file picker. + @Published var showPicker = false + /// A Boolean value indicating whether to show the password field view. + @Published var showPassword = false + /// A Boolean value indicating whether to display the error. + @Published var showCertificateImportError = false + + /// The host that prompted the challenge. + var challengingHost: String { + challenge.host + } + + /// Creates a certificate picker view model. + /// - Parameter challenge: The challenge that requires a certificate. + init(challenge: QueuedNetworkChallenge) { + self.challenge = challenge + } + + /// Proceeds to show the file picker. This should be called after the prompt that notifies the + /// user that a certificate must be selected. + func proceedFromPrompt() { + showPicker = true + } + + /// Proceeds to show the user the password form. This should be called after the user selects + /// a certificate. + /// - Parameter url: The URL of the certificate that the user chose. + func proceed(withCertificateURL url: URL) { + certificateURL = url + showPassword = true + } + + /// Attempts to use the certificate and password to respond to the challenge. + /// - Parameter password: The password for the certificate. + func proceed(withPassword password: String) { + guard let certificateURL = certificateURL else { + preconditionFailure() + } + + Task { + do { + challenge.resume(with: .useCredential(try .certificate(at: certificateURL, password: password))) + } catch { + // This is required to prevent an "already presenting" error. + try? await Task.sleep(nanoseconds: 100_000) + showCertificateImportError = true + } + } + } + + /// Cancels the challenge. + func cancel() { + challenge.resume(with: .cancelAuthenticationChallenge) + } +} + +/// A view modifier that presents a certificate picker workflow. +struct CertificatePickerViewModifier: ViewModifier { + /// Creates a certificate picker view modifier. + /// - Parameter challenge: The challenge that requires a certificate. + init(challenge: QueuedNetworkChallenge) { + viewModel = CertificatePickerViewModel(challenge: challenge) + } + + /// The view model. + @ObservedObject private var viewModel: CertificatePickerViewModel + + func body(content: Content) -> some View { + content + .promptBrowseCertificate( + isPresented: $viewModel.showPrompt, + viewModel: viewModel + ) + .certificateFilePicker( + isPresented: $viewModel.showPicker, + viewModel: viewModel + ) + .passwordSheet( + isPresented: $viewModel.showPassword, + viewModel: viewModel + ) + .alertCertificateImportError( + isPresented: $viewModel.showCertificateImportError, + viewModel: viewModel + ) + } +} + +private extension UTType { + /// A `UTType` that represents a pfx file. + static let pfx = UTType(filenameExtension: "pfx")! +} + +private extension View { + /// Displays a prompt to the user to let them know that picking a certificate is required. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. + @MainActor + @ViewBuilder + func promptBrowseCertificate( + isPresented: Binding, + viewModel: CertificatePickerViewModel + ) -> some View { + alert("Certificate Required", isPresented: isPresented, presenting: viewModel.challengingHost) { _ in + Button("Browse For Certificate") { + viewModel.proceedFromPrompt() + } + Button("Cancel", role: .cancel) { + viewModel.cancel() + } + } message: { _ in + Text("A certificate is required to access content on \(viewModel.challengingHost).") + } + } +} + +private extension View { + /// Displays a sheet that allows the user to select a certificate file. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. + @MainActor + @ViewBuilder + func certificateFilePicker( + isPresented: Binding, + viewModel: CertificatePickerViewModel + ) -> some View { + sheet(isPresented: isPresented) { + DocumentPickerView(contentTypes: [.pfx]) { + viewModel.proceed(withCertificateURL: $0) + } onCancel: { + viewModel.cancel() + } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() + } + } +} + +private extension View { + /// Displays a sheet that allows the user to enter a password. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. + @MainActor + @ViewBuilder + func passwordSheet( + isPresented: Binding, + viewModel: CertificatePickerViewModel + ) -> some View { + sheet(isPresented: isPresented) { + EnterPasswordView() { password in + viewModel.proceed(withPassword: password) + } onCancel: { + viewModel.cancel() + } + .edgesIgnoringSafeArea(.bottom) + .interactiveDismissDisabled() + } + } +} + +private extension View { + /// Displays an alert to notify that there was an error importing the certificate. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - viewModel: The view model associated with the view. + @MainActor + @ViewBuilder + func alertCertificateImportError( + isPresented: Binding, + viewModel: CertificatePickerViewModel + ) -> some View { + alert("Error importing certificate", isPresented: isPresented) { + Button("Try Again") { + viewModel.proceedFromPrompt() + } + Button("Cancel", role: .cancel) { + viewModel.cancel() + } + } message: { + Text("The certificate file or password was invalid.") + } + } +} + +/// A view that allows the user to enter a password. +struct EnterPasswordView: View { + @Environment(\.dismiss) var dismissAction + + /// The password that the user entered. + @State var password: String = "" + + /// The action to call once the user has completed entering the password. + var onContinue: (String) -> Void + + /// The action to call if the user cancels. + var onCancel: () -> Void + + /// A Boolean value indicating whether the password field has focus. + @FocusState var isPasswordFocused: Bool + + var body: some View { + NavigationView { + Form { + Section { + VStack { + Text("Please enter a password for the chosen certificate.") + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + } + + Section { + SecureField("Password", text: $password) + .focused($isPasswordFocused) + .textContentType(.password) + .submitLabel(.go) + .onSubmit { + dismissAction() + onContinue(password) + } + } + .autocapitalization(.none) + .disableAutocorrection(true) + + Section { + okButton + } + } + .navigationTitle("Certificate") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismissAction() + onCancel() + } + } + } + .onAppear { + // Workaround for Apple bug - FB9676178. + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + isPasswordFocused = true + } + } + } + } + + /// The "OK" button. + private var okButton: some View { + Button(action: { + dismissAction() + onContinue(password) + }, label: { + Text("OK") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundColor(.white) + }) + .disabled(password.isEmpty) + .listRowBackground(!password.isEmpty ? Color.accentColor : Color.gray) + } +} diff --git a/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift new file mode 100644 index 000000000..7a15a56fc --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/DocumentPickerView.swift @@ -0,0 +1,60 @@ +// 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 UniformTypeIdentifiers + +/// A view that allows the user to browse to and select a document. +/// This view wraps a `UIDocumentPickerViewController`. +struct DocumentPickerView: UIViewControllerRepresentable { + /// The content types that are allowed to be selected. + var contentTypes: [UTType] + /// The closure called when a document is selected. + var onPickDocument: (URL) -> Void + /// The closure called when the user cancels. + var onCancel: () -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(onPickDocument: onPickDocument, onCancel: onCancel) + } + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes) + documentPicker.delegate = context.coordinator + return documentPicker + } + + func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} +} + +extension DocumentPickerView { + /// The coordinator for the document picker view that acts as a delegate to the underlying + /// `UIDocumentPickerViewController`. + final class Coordinator: NSObject, UIDocumentPickerDelegate { + var onPickDocument: (URL) -> Void + var onCancel: () -> Void + + init(onPickDocument: @escaping (URL) -> Void, onCancel: @escaping () -> Void) { + self.onPickDocument = onPickDocument + self.onCancel = onCancel + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + onCancel() + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { + onPickDocument(url) + } + } +} From 00c999b19a689ad8151af464e439b19bc4de3845 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 19 Jul 2022 17:23:51 -0600 Subject: [PATCH 1214/1722] move attribute to same line --- Tests/ArcGISToolkitTests/AuthenticatorTests.swift | 12 ++++-------- .../QueuedNetworkChallengeTests.swift | 3 +-- .../QueuedOAuthChallengeTests.swift | 3 +-- .../QueuedTokenChallengeTests.swift | 3 +-- .../TrustHostViewModifierTests.swift | 3 +-- 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index 402424662..49151abf0 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -17,8 +17,7 @@ import ArcGIS import Combine class AuthenticatorTests: XCTestCase { - @MainActor - func testInit() { + @MainActor func testInit() { let config = OAuthConfiguration( portalURL: URL(string:"www.arcgis.com")!, clientID: "client id", @@ -29,8 +28,7 @@ class AuthenticatorTests: XCTestCase { XCTAssertEqual(authenticator.oAuthConfigurations, [config]) } - @MainActor - func testMakePersistent() async throws { + @MainActor func testMakePersistent() async throws { // Make sure credential stores are restored. addTeardownBlock { ArcGISRuntimeEnvironment.credentialStore = ArcGISCredentialStore() @@ -45,8 +43,7 @@ class AuthenticatorTests: XCTestCase { } catch {} } - @MainActor - func testClearCredentialStores() async { + @MainActor func testClearCredentialStores() async { await ArcGISRuntimeEnvironment.credentialStore.add( .staticToken( url: URL(string: "www.arcgis.com")!, @@ -69,8 +66,7 @@ class AuthenticatorTests: XCTestCase { XCTAssertTrue(arcGISCreds.isEmpty) } - @MainActor - func testChallengeQueue() async throws { + @MainActor func testChallengeQueue() async throws { actor MockQueuedChallenge: QueuedChallenge { nonisolated let host: String diff --git a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift index f52e3ccad..e31b4bc50 100644 --- a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift @@ -15,8 +15,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -@MainActor -class QueuedNetworkChallengeTests: XCTestCase { +@MainActor class QueuedNetworkChallengeTests: XCTestCase { func testInit() { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) XCTAssertEqual(challenge.host, "host.com") diff --git a/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift index 134ca039c..498b78564 100644 --- a/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift @@ -16,8 +16,7 @@ import XCTest @testable import ArcGISToolkit import ArcGIS -@MainActor -class QueuedOAuthChallengeTests: XCTestCase { +@MainActor class QueuedOAuthChallengeTests: XCTestCase { func testInit() { let portalURL = URL(string: "www.test-portal.com")! let clientID = "client id" diff --git a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift index 3f366bb02..1574f080e 100644 --- a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift @@ -15,8 +15,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -@MainActor -class QueuedTokenChallengeTests: XCTestCase { +@MainActor class QueuedTokenChallengeTests: XCTestCase { func testInit() { let challenge = QueuedTokenChallenge(host: "host.com") { _ in fatalError() diff --git a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift index 5d868919a..586ada227 100644 --- a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift +++ b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift @@ -14,8 +14,7 @@ import XCTest @testable import ArcGISToolkit -@MainActor -class TrustHostViewModifierTets: XCTestCase { +@MainActor class TrustHostViewModifierTets: XCTestCase { func testInit() { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) // Tests the initial state. From 68757e14926c141ec2aec05835602da4969064b8 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 19 Jul 2022 17:25:25 -0600 Subject: [PATCH 1215/1722] move attributes to same line --- .../CertificatePickerViewModifier.swift | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 81e9f2f18..c171fdc91 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -16,8 +16,7 @@ import UniformTypeIdentifiers /// An object that provides the business logic for the workflow of prompting the user for a /// certificate and a password. -@MainActor -final private class CertificatePickerViewModel: ObservableObject { +@MainActor final private class CertificatePickerViewModel: ObservableObject { /// The challenge that requires a certificate to proceed. let challenge: QueuedNetworkChallenge @@ -124,9 +123,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 promptBrowseCertificate( + @MainActor @ViewBuilder func promptBrowseCertificate( isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { @@ -148,9 +145,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 certificateFilePicker( + @MainActor @ViewBuilder func certificateFilePicker( isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { @@ -171,9 +166,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 passwordSheet( + @MainActor @ViewBuilder func passwordSheet( isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { @@ -194,9 +187,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 alertCertificateImportError( isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { From 418b1a851dc2ed8d34e989ad7adff2e95ba89897 Mon Sep 17 00:00:00 2001 From: R Olson Date: Tue, 19 Jul 2022 18:15:39 -0600 Subject: [PATCH 1216/1722] Update Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift Co-authored-by: David Feinzimer --- .../Components/Authentication/QueuedNetworkChallenge.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index 62e859eac..dce2bbff1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -18,6 +18,7 @@ import ArcGIS final class QueuedNetworkChallenge: QueuedChallenge { /// The host that prompted the challenge. let host: String + /// The kind of challenge. let kind: Kind From 0d7fdb1acff2df41b33e574b4f5f4f98e6bcc71d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Jul 2022 08:24:03 -0700 Subject: [PATCH 1217/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index c6f21ca8a..9cf659c12 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -250,8 +250,7 @@ public struct UtilityNetworkTrace: View { } Button { Task { - let traceSuccess = await viewModel.trace() - if traceSuccess { + if await viewModel.trace() { currentActivity = .viewingTraces(nil) } } From 28bbf8786415b41a50843b60f401a9eb286e1b03 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Jul 2022 08:39:53 -0700 Subject: [PATCH 1218/1722] Support starting point edge location --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 4 ++-- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 4 ++-- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 5c3797f8d..23200aa59 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -34,7 +34,7 @@ struct UtilityNetworkTraceExampleView: View { @State var resultGraphicsOverlay = GraphicsOverlay() /// Optionally used to provide programmatic starting points to the trace tool. - @State var startingPoints: [GeoElement] = [] + @State var startingPoints: [(GeoElement, Point?)] = [] /// The map viewpoint used by the `UtilityNetworkTrace` to pan/zoom the map to selected features. @State var viewpoint: Viewpoint? @@ -123,7 +123,7 @@ extension UtilityNetworkTraceExampleView { if let feature = element as? ArcGISFeature, let id = feature.attributes["globalid"] as? UUID, id == targetID { - startingPoints = [element] + startingPoints = [(element, nil)] } } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 9cf659c12..03b605160 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -68,7 +68,7 @@ public struct UtilityNetworkTrace: View { // MARK: Bindings /// Starting points programmatically provided to the trace tool. - @Binding private var externalStartingPoints: [GeoElement] + @Binding private var externalStartingPoints: [(GeoElement, Point?)] /// The graphics overlay to hold generated starting point and trace graphics. @Binding private var graphicsOverlay: GraphicsOverlay @@ -486,7 +486,7 @@ public struct UtilityNetworkTrace: View { viewPoint: Binding, mapViewProxy: Binding, viewpoint: Binding, - startingPoints: Binding<[GeoElement]> = .constant([]) + startingPoints: Binding<[(GeoElement, Point?)]> = .constant([]) ) { _viewPoint = viewPoint _mapPoint = mapPoint diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index c26c3fc4d..0a819ddaa 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -70,7 +70,7 @@ import SwiftUI private var map: Map /// Starting points programmatically provided to the trace tool. - var externalStartingPoints = [GeoElement]() { + var externalStartingPoints = [(GeoElement, Point?)]() { didSet { addExternalStartingPoints() } @@ -94,7 +94,7 @@ import SwiftUI init( map: Map, graphicsOverlay: GraphicsOverlay, - startingPoints: [GeoElement] + startingPoints: [(GeoElement, Point?)] ) { self.map = map self.graphicsOverlay = graphicsOverlay @@ -415,7 +415,7 @@ import SwiftUI /// Adds programatic starting points to the pending trace. private func addExternalStartingPoints() { externalStartingPoints.forEach { - setStartingPoint(geoElement: $0) + setStartingPoint(geoElement: $0.0, mapPoint: $0.1) } } From 3ede695504a34817cd3b570948b33fde5d63d85e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Jul 2022 08:42:39 -0700 Subject: [PATCH 1219/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 03b605160..d5f2a0d0e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -478,7 +478,9 @@ public struct UtilityNetworkTrace: View { /// - 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. - /// - startingPoints: An optional list of elements to be used as starting points. + /// - startingPoints: An optional list of elements to be used as starting points, provided as + /// tuples. The first item is a geo element and the optional second item is a map point (useful for + /// specifying a fractional starting location along an edge element). public init( graphicsOverlay: Binding, map: Map, From 76015ffa482bb5afcad8ec6c760ddc78e255c18d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Jul 2022 10:34:37 -0700 Subject: [PATCH 1220/1722] Revert `if let` shorthand usage --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 4 ++-- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index d5f2a0d0e..db5db62e1 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -395,7 +395,7 @@ public struct UtilityNetworkTrace: View { sp.utilityElement.globalID == selectedStartingPoint?.utilityElement.globalID }?.utilityElement.fractionAlongEdge ?? .zero }, set: { newValue in - if let selectedStartingPoint { + if let selectedStartingPoint = selectedStartingPoint { viewModel.setFractionAlongEdgeFor( startingPoint: selectedStartingPoint, to: newValue @@ -433,7 +433,7 @@ public struct UtilityNetworkTrace: View { } } makeZoomToButton { - if let selectedStartingPoint { + if let selectedStartingPoint = selectedStartingPoint { viewpoint = Viewpoint(targetExtent: selectedStartingPoint.extent) } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 0a819ddaa..6051d65df 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -263,7 +263,7 @@ import SwiftUI let utilityElement = network.createElement(arcGISFeature: feature) else { return } if utilityElement.networkSource.kind == .edge && geometry is Polyline { - if let mapPoint { + if let mapPoint = mapPoint { utilityElement.fractionAlongEdge = fractionAlongEdge( of: geometry, at: mapPoint From 3e0bc2fdf80d2c444c4c5838d93f18daf6add433 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 20 Jul 2022 13:13:29 -0600 Subject: [PATCH 1221/1722] Update Sources/ArcGISToolkit/Components/Authentication/Login.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index e00a8899c..b6fff190c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -99,7 +99,7 @@ struct LoginViewModifier: ViewModifier { } extension LoginViewModifier { - /// Creates a `UsernamePasswordViewModifier` with a queued network challenge. + /// Creates a `LoginViewModifier` with a queued network challenge. @MainActor init(challenge: QueuedNetworkChallenge) { self.init( From 4ecc87b0a69d87a85cf8e5383d11e1b84da6768b Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 20 Jul 2022 13:13:40 -0600 Subject: [PATCH 1222/1722] Update Sources/ArcGISToolkit/Components/Authentication/Login.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index b6fff190c..d2cbded58 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -119,7 +119,7 @@ extension LoginViewModifier { ) } - /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. + /// Creates a `LoginViewModifier` with a queued ArcGIS challenge. @MainActor init(challenge: QueuedArcGISChallenge) { self.init( From d596b678858ad61ff1e3abf15dde5adf2222d44b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Jul 2022 13:43:56 -0700 Subject: [PATCH 1223/1722] Add trace result zoom option and buffer --- .../UtilityNetworkTrace.swift | 19 +++++++++++++------ .../UtilityNetworkTraceViewModelTrace.swift | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index db5db62e1..999cd96d2 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -58,6 +58,10 @@ public struct UtilityNetworkTrace: View { /// The current user activity. @State private var currentActivity: UserActivity = .creatingTrace(nil) + /// A value indicating whether the viewpoint should be automatically changed to show the resulting + /// extent of a trace. + @State private var shouldZoomOnTraceCompletion = false + /// A Boolean value indicating if the warning that all traces will be deleted is presented. @State private var showWarningAlert = false @@ -245,6 +249,9 @@ public struct UtilityNetworkTrace: View { .onSubmit { viewModel.pendingTrace.userDidSpecifyName = true } + Toggle(isOn: $shouldZoomOnTraceCompletion) { + Text("Zoom to result") + } } } } @@ -252,6 +259,10 @@ public struct UtilityNetworkTrace: View { Task { if await viewModel.trace() { currentActivity = .viewingTraces(nil) + if shouldZoomOnTraceCompletion, + let extent = viewModel.selectedTrace?.resultExtent { + viewpoint = Viewpoint(targetExtent: extent) + } } } } label: { @@ -352,12 +363,8 @@ public struct UtilityNetworkTrace: View { } } makeZoomToButton { - if let resultEnvelope = GeometryEngine.combineExtents(of: [ - viewModel.selectedTrace?.utilityGeometryTraceResult?.multipoint, - viewModel.selectedTrace?.utilityGeometryTraceResult?.polygon, - viewModel.selectedTrace?.utilityGeometryTraceResult?.polyline - ].compactMap { $0 }) { - viewpoint = Viewpoint(targetExtent: resultEnvelope.extent) + if let extent = viewModel.selectedTrace?.resultExtent { + viewpoint = Viewpoint(targetExtent: extent) } } .padding([.vertical], 2) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 47e732750..e7ad85f63 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -53,6 +53,23 @@ 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? { + if let resultEnvelope = GeometryEngine.combineExtents(of: [ + utilityGeometryTraceResult?.multipoint, + utilityGeometryTraceResult?.polygon, + utilityGeometryTraceResult?.polyline + ].compactMap { $0 }), + let expandedEnvelope = GeometryEngine.buffer( + around: resultEnvelope, + distance: 200 + ) { + return expandedEnvelope.extent + } else { + return nil + } + } + /// A collection of starting points for the trace. var startingPoints = [UtilityNetworkTraceStartingPoint]() From 0384474580ba1bcb3cfeb4cf48c028afd30b2f59 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Jul 2022 15:45:54 -0500 Subject: [PATCH 1224/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../ArcGISToolkit/Components/Popups/PopupElementHeader.swift | 2 +- Sources/ArcGISToolkit/Components/Popups/PopupView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index de6f90999..6eab4c4da 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -14,7 +14,7 @@ import SwiftUI import ArcGIS -/// A view dispaying a title and description of a `PopupElement`. +/// A view displaying a title and description of a `PopupElement`. struct PopupElementHeader: View { var title: String var description: String diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index ec25cae62..21b15a57d 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -41,7 +41,7 @@ public struct PopupView: View { Group { if isPopupEvaluated == nil { VStack(alignment: .center) { - Text("Evaluating popoup expressions...") + Text("Evaluating popup expressions...") ProgressView() } .frame(maxWidth: .infinity) From aa013d368f23d59072c9c284fab3ab5200dd1ef2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Jul 2022 15:53:57 -0500 Subject: [PATCH 1225/1722] PR review changes. --- .../Popups/FieldsPopupElementView.swift | 7 +++---- .../Components/Popups/PopupView.swift | 2 -- .../ArcGISToolkit/Extensions/PopupElement.swift | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/PopupElement.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 69c6ed299..20248856b 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -74,7 +74,7 @@ struct FieldsPopupElementView: View { } } - /// A view for dispaying a formatted value. + /// A view for displaying a formatted value. struct FormattedValueText: View { /// The String to display. let formattedValue: String @@ -97,15 +97,14 @@ struct FieldsPopupElementView: View { } /// A convenience type for displaying labels and values in a grid. -struct DisplayField: Hashable { +struct DisplayField: Hashable, Identifiable { let label: String let formattedValue: String let id = UUID() } -extension DisplayField: Identifiable {} - extension FieldsPopupElement { + /// Provides a default title to dispaly if `title` is empty. var displayTitle: String { title.isEmpty ? "Fields" : title } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 21b15a57d..0abb01961 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -91,5 +91,3 @@ public struct PopupView: View { } } } - -extension PopupElement: Identifiable {} diff --git a/Sources/ArcGISToolkit/Extensions/PopupElement.swift b/Sources/ArcGISToolkit/Extensions/PopupElement.swift new file mode 100644 index 000000000..974b287bf --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/PopupElement.swift @@ -0,0 +1,17 @@ +// 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 + +extension PopupElement: Identifiable {} From b9528fb6fe64a38709fd4f2eca12b57a615a63d4 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 20 Jul 2022 14:58:01 -0600 Subject: [PATCH 1226/1722] Update Sources/ArcGISToolkit/Components/Authentication/Login.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 7ab7258c6..ab33ea938 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -119,7 +119,7 @@ extension LoginViewModifier { ) } - /// Creates a `UsernamePasswordViewModifier` with a queued ArcGIS challenge. + /// Creates a `LoginViewModifier` with a queued token challenge. @MainActor init(challenge: QueuedTokenChallenge) { self.init( From 62f9edbcbe5d23260f6b95c0144a27c236efeebe Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 20 Jul 2022 15:26:28 -0600 Subject: [PATCH 1227/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Authentication/UsernamePasswordViewModifier.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 34b60ec5d..67d987e30 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -19,20 +19,24 @@ import ArcGIS final class UsernamePasswordViewModel: ObservableObject { /// The username. @Published var username = "" { - didSet { updateSigninButtonEnabled() } + didSet { updateSignInButtonEnabled() } } + /// The password. @Published var password = "" { didSet { updateSigninButtonEnabled() } } + /// A Boolean value indicating if the sign-in button is enabled. @Published var signinButtonEnabled = false + /// A Boolean value indicating if the form is enabled. @Published var formEnabled: Bool = true /// The action to perform when the user signs in. This is a closure that takes a username /// and password, respectively. var signInAction: (String, String) -> Void + /// The action to perform when the user cancels. var cancelAction: () -> Void From 425668d1a9818f7b412f2547157487c01c9d05d9 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 20 Jul 2022 15:30:06 -0600 Subject: [PATCH 1228/1722] fix casing --- .../UsernamePasswordViewModifier.swift | 12 ++++++------ .../UsernamePasswordViewModelTests.swift | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index 67d987e30..d184ee824 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -28,7 +28,7 @@ final class UsernamePasswordViewModel: ObservableObject { } /// A Boolean value indicating if the sign-in button is enabled. - @Published var signinButtonEnabled = false + @Published var signInButtonEnabled = false /// A Boolean value indicating if the form is enabled. @Published var formEnabled: Bool = true @@ -57,7 +57,7 @@ final class UsernamePasswordViewModel: ObservableObject { } private func updateSigninButtonEnabled() { - signinButtonEnabled = !username.isEmpty && !password.isEmpty + signInButtonEnabled = !username.isEmpty && !password.isEmpty } /// The host that initiated the challenge. @@ -157,7 +157,7 @@ private struct UsernamePasswordView: View { .disableAutocorrection(true) Section { - signinButton + signInButton } } .disabled(!viewModel.formEnabled) @@ -195,7 +195,7 @@ private struct UsernamePasswordView: View { } /// The sign-in button. - private var signinButton: some View { + private var signInButton: some View { Button(action: { dismissAction() viewModel.signIn() @@ -210,8 +210,8 @@ private struct UsernamePasswordView: View { .tint(.white) } }) - .disabled(!viewModel.signinButtonEnabled) - .listRowBackground(viewModel.signinButtonEnabled ? Color.accentColor : Color.gray) + .disabled(!viewModel.signInButtonEnabled) + .listRowBackground(viewModel.signInButtonEnabled ? Color.accentColor : Color.gray) } } diff --git a/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift b/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift index f8d50aea1..01fe6102e 100644 --- a/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift @@ -48,16 +48,16 @@ class UsernamePasswordViewModelTests: XCTestCase { XCTAssertFalse(cancelCalled) model.username = "abc" - XCTAssertFalse(model.signinButtonEnabled) + XCTAssertFalse(model.signInButtonEnabled) model.password = "123" - XCTAssertTrue(model.signinButtonEnabled) + XCTAssertTrue(model.signInButtonEnabled) model.password = "" - XCTAssertFalse(model.signinButtonEnabled) + XCTAssertFalse(model.signInButtonEnabled) model.password = "123" - XCTAssertTrue(model.signinButtonEnabled) + XCTAssertTrue(model.signInButtonEnabled) model.signIn() XCTAssertFalse(model.formEnabled) From 364ffee7a0ac03c2f7bcd5e7d255f7db17c2b3c6 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 20 Jul 2022 15:31:42 -0600 Subject: [PATCH 1229/1722] fix casing --- .../Authentication/UsernamePasswordViewModifier.swift | 4 ++-- Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift index d184ee824..d3a12a34a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/UsernamePasswordViewModifier.swift @@ -24,7 +24,7 @@ final class UsernamePasswordViewModel: ObservableObject { /// The password. @Published var password = "" { - didSet { updateSigninButtonEnabled() } + didSet { updateSignInButtonEnabled() } } /// A Boolean value indicating if the sign-in button is enabled. @@ -56,7 +56,7 @@ final class UsernamePasswordViewModel: ObservableObject { self.cancelAction = cancelAction } - private func updateSigninButtonEnabled() { + private func updateSignInButtonEnabled() { signInButtonEnabled = !username.isEmpty && !password.isEmpty } diff --git a/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift b/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift index 01fe6102e..bf5bd0499 100644 --- a/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UsernamePasswordViewModelTests.swift @@ -40,7 +40,7 @@ class UsernamePasswordViewModelTests: XCTestCase { XCTAssertEqual(model.challengingHost, "host.com") XCTAssertNotNil(model.signInAction) XCTAssertNotNil(model.cancelAction) - XCTAssertFalse(model.signinButtonEnabled) + XCTAssertFalse(model.signInButtonEnabled) XCTAssertTrue(model.formEnabled) XCTAssertTrue(model.username.isEmpty) XCTAssertTrue(model.password.isEmpty) From 241a049f0f19c64a81d641fbae57bcad22c9afc3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Jul 2022 14:56:38 -0700 Subject: [PATCH 1230/1722] 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 64b1d6878..b9f9711d4 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -265,7 +265,7 @@ public struct UtilityNetworkTrace: View { } } Button { - if let selectedStartingPoint { + if let selectedStartingPoint = selectedStartingPoint { viewpoint = Viewpoint(targetExtent: selectedStartingPoint.extent) } } label: { From 6e9ce7f60d3d8b4ea5e3204fff9ca0ff24b3aa9d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Jul 2022 17:00:43 -0500 Subject: [PATCH 1231/1722] Apply suggestions from code review Co-authored-by: Nimesh Jarecha --- Examples/Examples/PopupExampleView.swift | 4 ++-- .../Components/Popups/FieldsPopupElementView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index 0e83792dd..92e29e860 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -49,7 +49,7 @@ struct PopupExampleView: View { /// The map displayed in the map view. @StateObject private var map = makeMap() - /// The point on the screen the user tapped on and used to identify layers. + /// The point on the screen the user tapped on to identify a feature. @State private var identifyScreenPoint: CGPoint? /// The result of the layer identify operation. @@ -95,7 +95,7 @@ struct PopupExampleView: View { } /// A view displaying the results of an identify operation. -struct IdentifyResultView: View { +private struct IdentifyResultView: View { var identifyResult: Result<[IdentifyLayerResult], Error> @Environment(\.horizontalSizeClass) var horizontalSizeClass diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 20248856b..024274e66 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -40,7 +40,7 @@ struct FieldsPopupElementView: View { FieldsList(fields: displayFields) } - /// A view displaying a grid of labels and values. + /// A view displaying the labels and values. struct FieldsList: View { let fields: [DisplayField] From 5a7430d397dc4e7627398d20687cb5b8caed19b2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Jul 2022 17:33:06 -0500 Subject: [PATCH 1232/1722] check expression evaluations for errors. --- .../ArcGISToolkit/Components/Popups/PopupView.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 0abb01961..268e67c52 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -29,6 +29,9 @@ public struct PopupView: View { /// the `popup.evaluateExpressions()` method. @State private var isPopupEvaluated: Bool? = nil + /// A Boolean value indicating whether expression evaluations contains an error. + @State private var hasEvaluationErrors: Bool = false + /// The results of calling the `popup.evaluateExpressions()` method. @State private var expressionEvaluations: [PopupExpressionEvaluation]? = nil @@ -47,15 +50,21 @@ public struct PopupView: View { .frame(maxWidth: .infinity) } else if isPopupEvaluated! { PopupElementScrollView(popup: popup) - } else { + } else if !isPopupEvaluated! { Text("Popup failed evaluation.") } + else if hasEvaluationErrors { + Text("There were errors with the expression evaluation.") + } } } .task { do { expressionEvaluations = try await popup.evaluateExpressions() isPopupEvaluated = true + + let evaluationErrors = expressionEvaluations?.filter { $0.error != nil } + hasEvaluationErrors = evaluationErrors != nil ? evaluationErrors!.count > 0 : false } catch { isPopupEvaluated = false } From 575eec6a894d40afbb13b7a9155a528874270bf0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Jul 2022 17:43:32 -0500 Subject: [PATCH 1233/1722] Make structs private; update display of formatted value text. --- .../Popups/FieldsPopupElementView.swift | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 024274e66..ddfb2545f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -41,7 +41,7 @@ struct FieldsPopupElementView: View { } /// A view displaying the labels and values. - struct FieldsList: View { + private struct FieldsList: View { let fields: [DisplayField] var body: some View { @@ -57,7 +57,7 @@ struct FieldsPopupElementView: View { } /// A view for displaying a `DisplayField`. - struct FieldRow: View { + private struct FieldRow: View { var field: DisplayField var body: some View { @@ -75,35 +75,28 @@ struct FieldsPopupElementView: View { } /// A view for displaying a formatted value. - struct FormattedValueText: View { + private struct FormattedValueText: View { /// The String to display. let formattedValue: String - /// The URL of the label if the label is an "http" string. - var url: URL? { - formattedValue.lowercased().starts(with: "http") ? URL(string: formattedValue) : nil - } var body: some View { - Group { - if let url = url, - let link = "[View](\(url.absoluteString))"{ - Text(.init(link)) - } else { - Text(formattedValue) - } + if formattedValue.lowercased().starts(with: "http") { + Text(.init("[View](\(formattedValue))")) + } else { + Text(formattedValue) } } } } /// A convenience type for displaying labels and values in a grid. -struct DisplayField: Hashable, Identifiable { +private struct DisplayField: Hashable, Identifiable { let label: String let formattedValue: String let id = UUID() } -extension FieldsPopupElement { +private extension FieldsPopupElement { /// Provides a default title to dispaly if `title` is empty. var displayTitle: String { title.isEmpty ? "Fields" : title From 8fc0df0987070912440122d24555bdd97cdbbc38 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 20 Jul 2022 17:49:59 -0500 Subject: [PATCH 1234/1722] Add back in `Result.swift`. --- Sources/ArcGISToolkit/Extensions/Result.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Sources/ArcGISToolkit/Extensions/Result.swift diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift new file mode 100644 index 000000000..20aca6811 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -0,0 +1,29 @@ +// Copyright 2021 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. + +extension Result where Failure == Error { + /// Creates a result based on the outcome of the given task. If the task + /// succeeds, the result is `success`. If the task fails, the result is + /// `failure`. + /// + /// Returns `nil` in the event that the task was cancelled. + public init?(awaiting task: () async throws -> Success) async { + do { + self = .success(try await task()) + } catch is CancellationError { + return nil + } catch { + self = .failure(error) + } + } +} From ec84013e15adf6a41db32d37ac172697599985f0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 20 Jul 2022 17:20:41 -0700 Subject: [PATCH 1235/1722] Update BookmarksTests.swift --- Tests/ArcGISToolkitTests/BookmarksTests.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BookmarksTests.swift b/Tests/ArcGISToolkitTests/BookmarksTests.swift index b1a8e18cc..40061a726 100644 --- a/Tests/ArcGISToolkitTests/BookmarksTests.swift +++ b/Tests/ArcGISToolkitTests/BookmarksTests.swift @@ -42,9 +42,9 @@ final class BookmarksTests: XCTestCase { XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } - + /// Asserts that the list properly handles a selection when provided a modifier and web map. - func testSelectBookmarkWithModifierAndMap() async { + func testSelectBookmarkWithModifierAndMap() async throws { let expectation = XCTestExpectation( description: "Modifier action was performed" ) @@ -69,11 +69,12 @@ final class BookmarksTests: XCTestCase { ) bookmarks.selectionChangedAction = action XCTAssertTrue(_isPresented) - bookmarks.selectBookmark(map.bookmarks.first!) + let firstBookmark = try XCTUnwrap(map.bookmarks.first) + bookmarks.selectBookmark(firstBookmark) XCTAssertFalse(_isPresented) wait(for: [expectation], timeout: 1.0) } - + /// Asserts that the list properly handles a selection when provided a viewpoint. func testSelectBookmarkWithViewpoint() { let sampleBookmarks = sampleBookmarks @@ -98,9 +99,9 @@ final class BookmarksTests: XCTestCase { XCTAssertFalse(_isPresented) XCTAssertEqual(_viewpoint, sampleBookmarks.first?.viewpoint) } - + /// Asserts that the list properly handles a selection when provided a viewpoint and web map. - func testSelectBookmarkWithViewpointAndMap() async { + func testSelectBookmarkWithViewpointAndMap() async throws { let map = Map.portlandTreeSurvey do { try await map.load() @@ -124,7 +125,8 @@ final class BookmarksTests: XCTestCase { ) XCTAssertTrue(_isPresented) XCTAssertNotEqual(_viewpoint, map.bookmarks.first?.viewpoint) - bookmarks.selectBookmark(map.bookmarks.first!) + let firstBookmark = try XCTUnwrap(map.bookmarks.first) + bookmarks.selectBookmark(firstBookmark) XCTAssertFalse(_isPresented) XCTAssertEqual(_viewpoint, map.bookmarks.first?.viewpoint) } From c0aa642c837524262f0f955d5ea20be4b776de43 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 20 Jul 2022 18:21:19 -0600 Subject: [PATCH 1236/1722] pr feedback --- Tests/ArcGISToolkitTests/AuthenticatorTests.swift | 6 +----- Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift | 3 +-- Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift | 3 +-- Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift | 3 +-- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index 402424662..a086a9b9f 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -16,8 +16,7 @@ import XCTest import ArcGIS import Combine -class AuthenticatorTests: XCTestCase { - @MainActor +@MainActor final class AuthenticatorTests: XCTestCase { func testInit() { let config = OAuthConfiguration( portalURL: URL(string:"www.arcgis.com")!, @@ -29,7 +28,6 @@ class AuthenticatorTests: XCTestCase { XCTAssertEqual(authenticator.oAuthConfigurations, [config]) } - @MainActor func testMakePersistent() async throws { // Make sure credential stores are restored. addTeardownBlock { @@ -45,7 +43,6 @@ class AuthenticatorTests: XCTestCase { } catch {} } - @MainActor func testClearCredentialStores() async { await ArcGISRuntimeEnvironment.credentialStore.add( .staticToken( @@ -69,7 +66,6 @@ class AuthenticatorTests: XCTestCase { XCTAssertTrue(arcGISCreds.isEmpty) } - @MainActor func testChallengeQueue() async throws { actor MockQueuedChallenge: QueuedChallenge { nonisolated let host: String diff --git a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift index dee4cf75c..730b63838 100644 --- a/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedArcGISChallengeTests.swift @@ -15,8 +15,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -@MainActor -class QueuedArcGISChallengeTests: XCTestCase { +@MainActor final class QueuedArcGISChallengeTests: XCTestCase { func testInit() { let challenge = QueuedArcGISChallenge(host: "host.com") { _ in fatalError() diff --git a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift index f52e3ccad..b43e520be 100644 --- a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift @@ -15,8 +15,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -@MainActor -class QueuedNetworkChallengeTests: XCTestCase { +@MainActor final class QueuedNetworkChallengeTests: XCTestCase { func testInit() { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) XCTAssertEqual(challenge.host, "host.com") diff --git a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift index 53540549a..f647874bc 100644 --- a/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift +++ b/Tests/ArcGISToolkitTests/TrustHostViewModifierTests.swift @@ -14,8 +14,7 @@ import XCTest @testable import ArcGISToolkit -@MainActor -class TrustHostViewModifierTests: XCTestCase { +@MainActor final class TrustHostViewModifierTests: XCTestCase { func testInit() { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) // Tests the initial state. From f86a4ed3f0b110fe6d33a9802d5df1bce3b6b3c5 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 20 Jul 2022 18:22:54 -0600 Subject: [PATCH 1237/1722] pr feedback --- Tests/ArcGISToolkitTests/LoginViewModelTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/LoginViewModelTests.swift b/Tests/ArcGISToolkitTests/LoginViewModelTests.swift index e5449de47..8683a5cae 100644 --- a/Tests/ArcGISToolkitTests/LoginViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/LoginViewModelTests.swift @@ -14,8 +14,7 @@ import XCTest @testable import ArcGISToolkit -@MainActor -class LoginViewModelTests: XCTestCase { +@MainActor final class LoginViewModelTests: XCTestCase { func testViewModel() { var signInCalled = false func signIn(username: String, password: String) { From ee9a68a7dbb22f459f6e696188115e3e704c2b07 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 20 Jul 2022 18:24:08 -0600 Subject: [PATCH 1238/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift | 4 ++-- Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift index 134ca039c..6ef1e6e95 100644 --- a/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift @@ -17,7 +17,7 @@ import XCTest import ArcGIS @MainActor -class QueuedOAuthChallengeTests: XCTestCase { +final class QueuedOAuthChallengeTests: XCTestCase { func testInit() { let portalURL = URL(string: "www.test-portal.com")! let clientID = "client id" @@ -28,7 +28,7 @@ class QueuedOAuthChallengeTests: XCTestCase { clientID: clientID, redirectURL: redirectURL ) - + let challenge = QueuedOAuthChallenge(configuration: config) XCTAssertEqual(challenge.configuration, config) } diff --git a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift index 3f366bb02..45b26c668 100644 --- a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift @@ -16,7 +16,7 @@ import XCTest @testable import ArcGISToolkit @MainActor -class QueuedTokenChallengeTests: XCTestCase { +final class QueuedTokenChallengeTests: XCTestCase { func testInit() { let challenge = QueuedTokenChallenge(host: "host.com") { _ in fatalError() From cb318b17a4c5d83460dc6f6e9a833542f253cb02 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 20 Jul 2022 18:25:41 -0600 Subject: [PATCH 1239/1722] pr feedback --- Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift | 3 +-- Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift index 6ef1e6e95..45b6f185b 100644 --- a/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedOAuthChallengeTests.swift @@ -16,8 +16,7 @@ import XCTest @testable import ArcGISToolkit import ArcGIS -@MainActor -final class QueuedOAuthChallengeTests: XCTestCase { +@MainActor final class QueuedOAuthChallengeTests: XCTestCase { func testInit() { let portalURL = URL(string: "www.test-portal.com")! let clientID = "client id" diff --git a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift index 45b26c668..07c6ead17 100644 --- a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift @@ -15,8 +15,7 @@ import SwiftUI import XCTest @testable import ArcGISToolkit -@MainActor -final class QueuedTokenChallengeTests: XCTestCase { +@MainActor final class QueuedTokenChallengeTests: XCTestCase { func testInit() { let challenge = QueuedTokenChallenge(host: "host.com") { _ in fatalError() From 3b93f11c173e59d92eca2cf8c3b7bf11f2b35560 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 20 Jul 2022 18:41:15 -0600 Subject: [PATCH 1240/1722] add view model test --- .../CertificatePickerViewModifier.swift | 4 +- .../CertificatePIckerViewModelTests.swift | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index c171fdc91..ba47e5b43 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -16,12 +16,12 @@ import UniformTypeIdentifiers /// An object that provides the business logic for the workflow of prompting the user for a /// certificate and a password. -@MainActor final private class CertificatePickerViewModel: ObservableObject { +@MainActor final class CertificatePickerViewModel: ObservableObject { /// The challenge that requires a certificate to proceed. let challenge: QueuedNetworkChallenge /// The URL of the certificate that the user chose. - private var certificateURL: URL? + var certificateURL: URL? /// A Boolean value indicating whether to show the prompt. @Published var showPrompt = true diff --git a/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift new file mode 100644 index 000000000..27633ffea --- /dev/null +++ b/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift @@ -0,0 +1,48 @@ +// 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 XCTest +@testable import ArcGISToolkit + +@MainActor final class CertificatePIckerViewModelTests: XCTestCase { + func testViewModel() async throws { + let challenge = QueuedNetworkChallenge(host: "host.com", kind: .certificate) + let model = CertificatePickerViewModel(challenge: challenge) + + XCTAssertNil(model.certificateURL) + XCTAssertTrue(model.showPrompt) + XCTAssertFalse(model.showPicker) + XCTAssertFalse(model.showPassword) + XCTAssertFalse(model.showCertificateImportError) + XCTAssertEqual(model.challengingHost, "host.com") + + model.proceedFromPrompt() + XCTAssertTrue(model.showPicker) + + let url = URL(fileURLWithPath: "/does-not-exist.pfx") + model.proceed(withCertificateURL: url) + XCTAssertEqual(model.certificateURL, url) + XCTAssertTrue(model.showPassword) + + model.proceed(withPassword: "1234") + // Have to yield here because the proceed function kicks off a task. + await Task.yield() + // Have to wait here because the proceed function waits to avoid a bug. + try? await Task.sleep(nanoseconds: 150_000) + XCTAssertTrue(model.showCertificateImportError) + + model.cancel() + let disposition = await challenge.disposition + XCTAssertEqual(disposition, .cancelAuthenticationChallenge) + } +} From c81ae4293b6bbaa12c3df1e42f564983d87233d8 Mon Sep 17 00:00:00 2001 From: R Olson Date: Thu, 21 Jul 2022 10:05:31 -0600 Subject: [PATCH 1241/1722] Update Sources/ArcGISToolkit/Components/Authentication/Login.swift Co-authored-by: David Feinzimer --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index ed7bf87f4..eb987ac6e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -18,6 +18,7 @@ import ArcGIS struct LoginCredential: Hashable { /// The username. let username: String + /// The password. let password: String } From 7191f37da75fb93d9cec19a5429d89d4e6d91327 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 21 Jul 2022 13:36:35 -0600 Subject: [PATCH 1242/1722] casing mistake --- Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift index 27633ffea..6679ececf 100644 --- a/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift @@ -14,7 +14,7 @@ import XCTest @testable import ArcGISToolkit -@MainActor final class CertificatePIckerViewModelTests: XCTestCase { +@MainActor final class CertificatePickerViewModelTests: XCTestCase { func testViewModel() async throws { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .certificate) let model = CertificatePickerViewModel(challenge: challenge) From 9234c9244203f4e6f4954f609541e519d750adbd Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 21 Jul 2022 13:49:47 -0600 Subject: [PATCH 1243/1722] fix test hang --- Tests/ArcGISToolkitTests/AuthenticatorTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index a086a9b9f..a0c920892 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -114,8 +114,10 @@ import Combine let mockedCurrentChallenge = try XCTUnwrap(authenticator.currentChallenge as? MockQueuedChallenge) XCTAssertEqual(mockedCurrentChallenge.host, "host1") - // Complete first challenge. - await challenge.setCompleted() + Task(priority: .low) { + // Complete first challenge. + await challenge.setCompleted() + } // Check next queued challenge let currentChallenge2 = await AsyncPublisher(authenticator.$currentChallenge) From a7c75a498cc7521dfb9df9d39920fd4492600e65 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 13:06:57 -0700 Subject: [PATCH 1244/1722] Specify symbol stlye --- .../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 6051d65df..51003dbf4 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -280,6 +280,7 @@ import SwiftUI let graphic = Graphic( geometry: mapPoint ?? feature.geometry?.extent.center, symbol: SimpleMarkerSymbol( + style: .cross, color: UIColor(self.pendingTrace.color), size: 20 ) From 3db2138164fd33b0c6995f9dce8ef380c63aa7d9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 14:14:08 -0700 Subject: [PATCH 1245/1722] Add `compassSize` modifier --- Examples/Examples/CompassExampleView.swift | 3 ++- .../ArcGISToolkit/Components/Compass/Compass.swift | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Examples/Examples/CompassExampleView.swift b/Examples/Examples/CompassExampleView.swift index 34d2a722c..1efe07ee7 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -31,7 +31,8 @@ struct CompassExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { Compass(viewpoint: $viewpoint) - .frame(width: 44, height: 44) + // Optionally provide a different size for the compass. + // .compassSize(size: <#T##CGFloat#>) .padding() } } diff --git a/Sources/ArcGISToolkit/Components/Compass/Compass.swift b/Sources/ArcGISToolkit/Components/Compass/Compass.swift index ad0572a5a..00b4e4492 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -30,6 +30,9 @@ public struct Compass: View { (heading.isZero || heading.isNaN) && autoHide } + /// A value for the compass' width and height. + var size: CGFloat = 44 + /// The heading of the compass in degrees. @Binding private var heading: Double @@ -58,6 +61,7 @@ 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 guard opacity != newOpacity else { return } @@ -113,4 +117,12 @@ public extension Compass { } self.init(viewpointRotation: viewpointRotation, autoHide: autoHide) } + + /// Define a custom size for the compass. + /// - Parameter size: A value for the compass' width and height. + func compassSize(size: CGFloat) -> Self { + var copy = self + copy.size = size + return copy + } } From 0784ea8283e62e7dcba334cdb333afcb329c1289 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 21 Jul 2022 16:34:27 -0500 Subject: [PATCH 1246/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Components/Popups/FieldsPopupElementView.swift | 2 +- Sources/ArcGISToolkit/Components/Popups/PopupView.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index ddfb2545f..2a11988ab 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -97,7 +97,7 @@ private struct DisplayField: Hashable, Identifiable { } private extension FieldsPopupElement { - /// Provides a default title to dispaly if `title` is empty. + /// Provides a default title to display if `title` is empty. var displayTitle: String { title.isEmpty ? "Fields" : title } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 268e67c52..4754628ff 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -52,8 +52,7 @@ public struct PopupView: View { PopupElementScrollView(popup: popup) } else if !isPopupEvaluated! { Text("Popup failed evaluation.") - } - else if hasEvaluationErrors { + } else if hasEvaluationErrors { Text("There were errors with the expression evaluation.") } } From 410e76ee1f54f70db32b99fe0a393e3889ec9381 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 21 Jul 2022 16:40:36 -0500 Subject: [PATCH 1247/1722] PR review recommendations. --- .../Components/Popups/PopupView.swift | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 4754628ff..1a4e64edf 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -28,9 +28,6 @@ public struct PopupView: View { /// A Boolean value indicating whether the popup's elements have been evaluated via /// the `popup.evaluateExpressions()` method. @State private var isPopupEvaluated: Bool? = nil - - /// A Boolean value indicating whether expression evaluations contains an error. - @State private var hasEvaluationErrors: Bool = false /// The results of calling the `popup.evaluateExpressions()` method. @State private var expressionEvaluations: [PopupExpressionEvaluation]? = nil @@ -42,18 +39,18 @@ public struct PopupView: View { .fontWeight(.bold) Divider() Group { - if isPopupEvaluated == nil { + if let isPopupEvaluated = isPopupEvaluated { + if isPopupEvaluated { + PopupElementScrollView(popup: popup) + } else { + Text("Popup evaluation failed.") + } + } else { VStack(alignment: .center) { Text("Evaluating popup expressions...") ProgressView() } .frame(maxWidth: .infinity) - } else if isPopupEvaluated! { - PopupElementScrollView(popup: popup) - } else if !isPopupEvaluated! { - Text("Popup failed evaluation.") - } else if hasEvaluationErrors { - Text("There were errors with the expression evaluation.") } } } @@ -61,9 +58,6 @@ public struct PopupView: View { do { expressionEvaluations = try await popup.evaluateExpressions() isPopupEvaluated = true - - let evaluationErrors = expressionEvaluations?.filter { $0.error != nil } - hasEvaluationErrors = evaluationErrors != nil ? evaluationErrors!.count > 0 : false } catch { isPopupEvaluated = false } From 22922ce9b1bb74fd8fa05b4999908f9fbbd738bf Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 21 Jul 2022 15:51:04 -0600 Subject: [PATCH 1248/1722] better error messages --- .../CertificatePickerViewModifier.swift | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index ba47e5b43..091d628b4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -13,6 +13,7 @@ import SwiftUI import UniformTypeIdentifiers +import ArcGIS /// An object that provides the business logic for the workflow of prompting the user for a /// certificate and a password. @@ -25,13 +26,19 @@ import UniformTypeIdentifiers /// A Boolean value indicating whether to show the prompt. @Published var showPrompt = true + /// A Boolean value indicating whether to show the certificate file picker. @Published var showPicker = false + /// A Boolean value indicating whether to show the password field view. @Published var showPassword = false + /// A Boolean value indicating whether to display the error. @Published var showCertificateImportError = false + /// The certificate import error that occurred. + var certificateImportError: CertificateImportError? + /// The host that prompted the challenge. var challengingHost: String { challenge.host @@ -70,6 +77,7 @@ import UniformTypeIdentifiers } catch { // This is required to prevent an "already presenting" error. try? await Task.sleep(nanoseconds: 100_000) + certificateImportError = error as? CertificateImportError showCertificateImportError = true } } @@ -191,6 +199,8 @@ private extension View { isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { + + alert("Error importing certificate", isPresented: isPresented) { Button("Try Again") { viewModel.proceedFromPrompt() @@ -199,7 +209,23 @@ private extension View { viewModel.cancel() } } message: { - Text("The certificate file or password was invalid.") + 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 } } } From a51e9609b1def9b94cdb984326f6052178c826bf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 15:17:05 -0700 Subject: [PATCH 1249/1722] Create UtilityNetworkTraceTests.swift --- .../UtilityNetworkTraceTests.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift new file mode 100644 index 000000000..b139dadba --- /dev/null +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift @@ -0,0 +1,41 @@ +// Copyright 2021 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 + +class UtilityNetworkTraceTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} From a40eef9c0388bba70b3986c37ad6cad8a8b24f68 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 15:25:49 -0700 Subject: [PATCH 1250/1722] Add test cases 1.1, 1.2 --- .../UtilityNetworkTraceViewModel.swift | 5 +- .../UtilityNetworkTraceTests.swift | 41 ------- .../UtilityNetworkTraceViewModelTests.swift | 107 ++++++++++++++++++ 3 files changed, 111 insertions(+), 42 deletions(-) delete mode 100644 Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift create mode 100644 Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 51003dbf4..7761f473f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -111,6 +111,9 @@ import SwiftUI network = map.utilityNetworks.first configurations = await utilityNamedTraceConfigurations(from: map) } + if map.utilityNetworks.isEmpty { + userWarning = "No utility networks found." + } } /// Deletes the provided starting point from the pending trace. @@ -436,7 +439,7 @@ import SwiftUI /// Loads the named trace configurations in the network. /// Returns the named trace configurations in the network on the provided map. /// - Parameter map: A web map containing one or more utility networks. - private func utilityNamedTraceConfigurations(from map: Map) async -> [UtilityNamedTraceConfiguration] { + func utilityNamedTraceConfigurations(from map: Map) async -> [UtilityNamedTraceConfiguration] { guard let network = network else { return [] } return (try? await map.getNamedTraceConfigurations(from: network)) ?? [] } diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift deleted file mode 100644 index b139dadba..000000000 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2021 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 - -class UtilityNetworkTraceTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift new file mode 100644 index 000000000..78e153668 --- /dev/null +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -0,0 +1,107 @@ +// Copyright 2021 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 + +/// - See Also: [Test Design](https://devtopia.esri.com/runtime/common-toolkit/blob/main/designs/UtilityNetworkTraceTool/UtilityNetworkTraceTool_Test_Design.md) +@MainActor final class UtilityNetworkTraceViewModelTests: XCTestCase { + + override func setUpWithError() throws { + ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") + try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) + } + + func tearDownWithError() async throws { + ArcGISRuntimeEnvironment.apiKey = nil + await ArcGISRuntimeEnvironment.credentialStore.removeAll() + } + + func testCase_1_1() async { + let viewModel = UtilityNetworkTraceViewModel( + map: await makeMapWithNoUtilityNetworks(), + graphicsOverlay: GraphicsOverlay(), + startingPoints: [] + ) + XCTAssertEqual( + viewModel.userWarning, + "No utility networks found." + ) + } + + func testCase_1_2() async throws { + let sampleServer7Password: String? = nil + try XCTSkipIf(sampleServer7Password == nil) + let token = try await ArcGISCredential.token( + url: .sampleServer7, + username: "viewer01", + password: sampleServer7Password! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + let map = await makeMapWithNoUtilityNetworks() + map.addUtilityNetwork( + await makeNetworkWith(url: .sampleServer7) + ) + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [] + ) + let configurations = await viewModel.utilityNamedTraceConfigurations(from: map) + XCTAssertTrue(configurations.isEmpty) + } + + func testCase_1_3() {} + + func testCase_1_4() {} + + func testCase_2_1() {} + + func testCase_2_2() {} + + func testCase_2_3() {} + + func testCase_3_1() {} + + func testCase_3_2() {} +} + +extension UtilityNetworkTraceViewModelTests { + /// - Returns: A loaded map that contains no utility networks. + func makeMapWithNoUtilityNetworks() async -> Map { + let map = Map(basemapStyle: .arcGISTopographic) + do { + try await map.load() + } catch { + XCTFail(error.localizedDescription) + } + return map + } + + /// - Returns: A loaded utility network. + func makeNetworkWith(url: URL) async -> UtilityNetwork { + let network = UtilityNetwork(url: url) + do { + try await network.load() + } catch { + XCTFail(error.localizedDescription) + } + return network + } +} + +private extension URL { + static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! +} From b2a1f824a9eee1fe96d77aa47a31f373f5d1e465 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 20:14:28 -0700 Subject: [PATCH 1251/1722] Improve test logic --- .../UtilityNetworkTraceViewModel.swift | 4 ++++ .../UtilityNetworkTraceViewModelTests.swift | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 7761f473f..5fe2e731c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -24,6 +24,9 @@ import SwiftUI /// The available named trace configurations. @Published private(set) var configurations = [UtilityNamedTraceConfiguration]() + /// Indicates whether all of the initialization tasks completed. + @Published private(set) var initializationCompleted: Bool = false + /// The utility network on which traces will be ran. @Published private(set) var network: UtilityNetwork? @@ -110,6 +113,7 @@ import SwiftUI } network = map.utilityNetworks.first configurations = await utilityNamedTraceConfigurations(from: map) + initializationCompleted = true } if map.utilityNetworks.isEmpty { userWarning = "No utility networks found." diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 78e153668..920afba0a 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -12,13 +12,13 @@ // limitations under the License. import ArcGIS +import Combine import XCTest @testable import ArcGISToolkit /// - See Also: [Test Design](https://devtopia.esri.com/runtime/common-toolkit/blob/main/designs/UtilityNetworkTraceTool/UtilityNetworkTraceTool_Test_Design.md) @MainActor final class UtilityNetworkTraceViewModelTests: XCTestCase { - override func setUpWithError() throws { ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) @@ -60,6 +60,18 @@ import XCTest startingPoints: [] ) let configurations = await viewModel.utilityNamedTraceConfigurations(from: map) + + let expectation = expectation(description: "init completed") + + var subscription = Set() + viewModel.$initializationCompleted.sink { v in + if v == true { + expectation.fulfill() + } + }.store(in: &subscription) + + await Task.yield() + wait(for: [expectation], timeout: 2.0) XCTAssertTrue(configurations.isEmpty) } From 0332996617289ddc1ea83073b0332b1dab268ce7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 20:15:18 -0700 Subject: [PATCH 1252/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 920afba0a..b1faa03e1 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -61,7 +61,7 @@ import XCTest ) let configurations = await viewModel.utilityNamedTraceConfigurations(from: map) - let expectation = expectation(description: "init completed") + let expectation = expectation(description: "Initialization completed") var subscription = Set() viewModel.$initializationCompleted.sink { v in From a226f241271ec9c8a461ce58c2ac98ffc872a896 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 20:22:04 -0700 Subject: [PATCH 1253/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index b1faa03e1..ba67061fd 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -35,6 +35,19 @@ import XCTest graphicsOverlay: GraphicsOverlay(), startingPoints: [] ) + + let expectation = expectation(description: "Initialization completed") + + var subscription = Set() + viewModel.$initializationCompleted.sink { v in + if v == true { + expectation.fulfill() + } + }.store(in: &subscription) + + await Task.yield() + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual( viewModel.userWarning, "No utility networks found." @@ -72,6 +85,7 @@ import XCTest await Task.yield() wait(for: [expectation], timeout: 2.0) + XCTAssertTrue(configurations.isEmpty) } From 258e94164e7ccba19343ed2f2f20b14a5d314444 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 20:57:18 -0700 Subject: [PATCH 1254/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTraceViewModel.swift | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 5fe2e731c..0353d56a0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -22,10 +22,13 @@ import SwiftUI @Published private(set) var completedTraces = [Trace]() /// The available named trace configurations. - @Published private(set) var configurations = [UtilityNamedTraceConfiguration]() - - /// Indicates whether all of the initialization tasks completed. - @Published private(set) var initializationCompleted: Bool = false + @Published private(set) var configurations = [UtilityNamedTraceConfiguration]() { + didSet { + if configurations.isEmpty { + userWarning = "No trace types found." + } + } + } /// The utility network on which traces will be ran. @Published private(set) var network: UtilityNetwork? @@ -94,29 +97,18 @@ import SwiftUI /// - map: The map to be loaded that contains at least one utility network. /// - graphicsOverlay: The overlay on which trace graphics will be drawn. /// - startingPoints: Starting points programmatically provided to the trace tool. + /// - autoLoad: If set `false`, `load()` will need to be manually called. init( map: Map, graphicsOverlay: GraphicsOverlay, - startingPoints: [(GeoElement, Point?)] + startingPoints: [(GeoElement, Point?)], + autoLoad: Bool = true ) { self.map = map self.graphicsOverlay = graphicsOverlay self.externalStartingPoints = startingPoints - Task { - do { - try await map.load() - for network in map.utilityNetworks { - try await network.load() - } - } catch { - print(error.localizedDescription) - } - network = map.utilityNetworks.first - configurations = await utilityNamedTraceConfigurations(from: map) - initializationCompleted = true - } - if map.utilityNetworks.isEmpty { - userWarning = "No utility networks found." + if autoLoad { + Task { await load() } } } @@ -151,6 +143,23 @@ import SwiftUI } } + /// Manually loads the components necessary to use the trace tool. + func load() async { + do { + try await map.load() + for network in map.utilityNetworks { + try await network.load() + } + } catch { + print(error.localizedDescription) + } + network = map.utilityNetworks.first + configurations = await utilityNamedTraceConfigurations(from: map) + if map.utilityNetworks.isEmpty { + userWarning = "No utility networks found." + } + } + /// Selects the next trace from the list of completed traces. func selectNextTrace() { if let current = selectedTraceIndex { @@ -445,7 +454,15 @@ import SwiftUI /// - Parameter map: A web map containing one or more utility networks. func utilityNamedTraceConfigurations(from map: Map) async -> [UtilityNamedTraceConfiguration] { guard let network = network else { return [] } - return (try? await map.getNamedTraceConfigurations(from: network)) ?? [] + do { + return try await map.getNamedTraceConfigurations(from: network) + } catch { + print( + "Failed to retrieve configurations.", + error.localizedDescription + ) + return [] + } } } From 4b1411cbebe760bdd42ec7aa9c594ed0d97a38b6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 21 Jul 2022 20:57:33 -0700 Subject: [PATCH 1255/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index ba67061fd..65ae46a6d 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -33,21 +33,13 @@ import XCTest let viewModel = UtilityNetworkTraceViewModel( map: await makeMapWithNoUtilityNetworks(), graphicsOverlay: GraphicsOverlay(), - startingPoints: [] + startingPoints: [], + autoLoad: false ) - let expectation = expectation(description: "Initialization completed") - - var subscription = Set() - viewModel.$initializationCompleted.sink { v in - if v == true { - expectation.fulfill() - } - }.store(in: &subscription) - - await Task.yield() - wait(for: [expectation], timeout: 2.0) + await viewModel.load() + XCTAssertFalse(viewModel.canRunTrace) XCTAssertEqual( viewModel.userWarning, "No utility networks found." @@ -70,23 +62,18 @@ import XCTest let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), - startingPoints: [] + startingPoints: [], + autoLoad: false ) - let configurations = await viewModel.utilityNamedTraceConfigurations(from: map) - - let expectation = expectation(description: "Initialization completed") - - var subscription = Set() - viewModel.$initializationCompleted.sink { v in - if v == true { - expectation.fulfill() - } - }.store(in: &subscription) - await Task.yield() - wait(for: [expectation], timeout: 2.0) + await viewModel.load() - XCTAssertTrue(configurations.isEmpty) + XCTAssertFalse(viewModel.canRunTrace) + XCTAssertTrue(viewModel.configurations.isEmpty) + XCTAssertEqual( + viewModel.userWarning, + "No trace types found." + ) } func testCase_1_3() {} From 408dabf583c0997cafc25d08b2ed963b1c787997 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 22 Jul 2022 10:26:01 -0500 Subject: [PATCH 1256/1722] Use non-nil-returning version of Result extension, but add method to convert cancellation errors to `nil`. Add test for that method. --- Examples/Examples/PopupExampleView.swift | 1 + Sources/ArcGISToolkit/Extensions/Result.swift | 17 ++++++---- Tests/ArcGISToolkitTests/ResultTests.swift | 31 +++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/ResultTests.swift diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index 92e29e860..19616b70e 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -71,6 +71,7 @@ struct PopupExampleView: View { returnPopupsOnly: true ) }) + .cancellationToNil() else { return } diff --git a/Sources/ArcGISToolkit/Extensions/Result.swift b/Sources/ArcGISToolkit/Extensions/Result.swift index 20aca6811..ff0893f62 100644 --- a/Sources/ArcGISToolkit/Extensions/Result.swift +++ b/Sources/ArcGISToolkit/Extensions/Result.swift @@ -11,19 +11,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -extension Result where Failure == Error { +public extension Result where Failure == Error { /// Creates a result based on the outcome of the given task. If the task /// succeeds, the result is `success`. If the task fails, the result is /// `failure`. - /// - /// Returns `nil` in the event that the task was cancelled. - public init?(awaiting task: () async throws -> Success) async { + init(awaiting task: () async throws -> Success) async { do { self = .success(try await task()) - } catch is CancellationError { - return nil } catch { self = .failure(error) } } + + /// Converts the result to a `nil` in the case of a user cancelled error. + /// - Returns: `Self` or `nil` if there was a cancellation error. + func cancellationToNil() -> Self? { + guard case .failure(_ as CancellationError) = self else { + return self + } + return nil + } } diff --git a/Tests/ArcGISToolkitTests/ResultTests.swift b/Tests/ArcGISToolkitTests/ResultTests.swift new file mode 100644 index 000000000..85a578e65 --- /dev/null +++ b/Tests/ArcGISToolkitTests/ResultTests.swift @@ -0,0 +1,31 @@ +// 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 XCTest +@testable import ArcGISToolkit + +class ResultTests: XCTestCase { + /// Tests the conversion of a cancellation error result to `nil.` + func testCancellationToNil() { + var result: Result = .success("hello") + XCTAssertNotNil(result.cancellationToNil()) + + struct MockError: Error {} + result = .failure(MockError()) + XCTAssertNotNil(result.cancellationToNil()) + + result = .failure(CancellationError()) + XCTAssertNil(result.cancellationToNil()) + } +} From 8fb1714201834ca205065fcdc128d3d37b1f9f43 Mon Sep 17 00:00:00 2001 From: R Olson Date: Fri, 22 Jul 2022 09:42:58 -0600 Subject: [PATCH 1257/1722] Update Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift Co-authored-by: Mark Dostal --- .../Authentication/CertificatePickerViewModifier.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 091d628b4..cb0918d21 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -200,7 +200,6 @@ private extension View { viewModel: CertificatePickerViewModel ) -> some View { - alert("Error importing certificate", isPresented: isPresented) { Button("Try Again") { viewModel.proceedFromPrompt() From 7e8b9d02ab624e4cc9641f86ad38a263c39db439 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 22 Jul 2022 09:43:38 -0600 Subject: [PATCH 1258/1722] file name fix --- ...ViewModelTests.swift => CertificatePickerViewModelTests.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Tests/ArcGISToolkitTests/{CertificatePIckerViewModelTests.swift => CertificatePickerViewModelTests.swift} (100%) diff --git a/Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift similarity index 100% rename from Tests/ArcGISToolkitTests/CertificatePIckerViewModelTests.swift rename to Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift From d06609da96a700b2b9b5979799260255e7ae4686 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 09:13:47 -0700 Subject: [PATCH 1259/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 65ae46a6d..fb7c5a55d 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -47,12 +47,12 @@ import XCTest } func testCase_1_2() async throws { - let sampleServer7Password: String? = nil - try XCTSkipIf(sampleServer7Password == nil) + let serverPassword: String? = nil + try XCTSkipIf(serverPassword == nil) let token = try await ArcGISCredential.token( url: .sampleServer7, username: "viewer01", - password: sampleServer7Password! + password: serverPassword! ) await ArcGISRuntimeEnvironment.credentialStore.add(token) let map = await makeMapWithNoUtilityNetworks() @@ -76,7 +76,39 @@ import XCTest ) } - func testCase_1_3() {} + func testCase_1_3() async throws { + let serverPassword: String? = "test.publisher01" + try XCTSkipIf(serverPassword == nil) + let token = try await ArcGISCredential.token( + url: .rtc_100_8, + username: "publisher1", + password: serverPassword! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + + guard let map = Map(url: .rtc_100_8) else { + XCTFail("Failed to load map") + return + } + + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [], + autoLoad: false + ) + + await viewModel.load() + + XCTExpectFailure("Further server trust handling required.") + + XCTAssertFalse(viewModel.canRunTrace) + XCTAssertTrue(viewModel.configurations.isEmpty) + XCTAssertEqual( + viewModel.userWarning, + "No trace types found." + ) + } func testCase_1_4() {} @@ -103,6 +135,17 @@ extension UtilityNetworkTraceViewModelTests { return map } + /// - Returns: A loaded map that contains no utility networks. + func makeMapWith(url: URL) async -> Map? { + let map = Map(url: url) + do { + try await map?.load() + } catch { + XCTFail(error.localizedDescription) + } + return map + } + /// - Returns: A loaded utility network. func makeNetworkWith(url: URL) async -> UtilityNetwork { let network = UtilityNetwork(url: url) @@ -117,4 +160,6 @@ extension UtilityNetworkTraceViewModelTests { private extension URL { static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! + + static var rtc_100_8 = URL(string: "http://rtc-100-8.esri.com/portal/home/webmap/viewer.html?webmap=78f993b89bad4ba0a8a22ce2e0bcfbd0")! } From 5caeb06cba4bb0c949d8e76a9a69e7a2735c6fb4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 09:26:19 -0700 Subject: [PATCH 1260/1722] Update Sources/ArcGISToolkit/Components/Compass/Compass.swift Co-authored-by: Nimesh Jarecha --- 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 00b4e4492..c351cd0b9 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -30,7 +30,7 @@ public struct Compass: View { (heading.isZero || heading.isNaN) && autoHide } - /// A value for the compass' width and height. + /// The width and height of the compass. var size: CGFloat = 44 /// The heading of the compass in degrees. From 80d1580b8853650a4ee59449725a079cf1bd943a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 09:26:32 -0700 Subject: [PATCH 1261/1722] Update Sources/ArcGISToolkit/Components/Compass/Compass.swift Co-authored-by: Nimesh Jarecha --- 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 c351cd0b9..b21456bd8 100644 --- a/Sources/ArcGISToolkit/Components/Compass/Compass.swift +++ b/Sources/ArcGISToolkit/Components/Compass/Compass.swift @@ -119,7 +119,7 @@ public extension Compass { } /// Define a custom size for the compass. - /// - Parameter size: A value for the compass' width and height. + /// - Parameter size: The width and height of the compass. func compassSize(size: CGFloat) -> Self { var copy = self copy.size = size From 1a20419cea67c6797d52e6bfb63c1f50b1a07b4e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 09:26:53 -0700 Subject: [PATCH 1262/1722] Update Examples/Examples/CompassExampleView.swift Co-authored-by: Nimesh Jarecha --- 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 1efe07ee7..99bb778f9 100644 --- a/Examples/Examples/CompassExampleView.swift +++ b/Examples/Examples/CompassExampleView.swift @@ -31,8 +31,8 @@ struct CompassExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } .overlay(alignment: .topTrailing) { Compass(viewpoint: $viewpoint) - // Optionally provide a different size for the compass. - // .compassSize(size: <#T##CGFloat#>) + // Optionally provide a different size for the compass. + //.compassSize(size: <#T##CGFloat#>) .padding() } } From b947710c84984852b04d77d457441eab2ddab5d6 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 22 Jul 2022 12:32:13 -0600 Subject: [PATCH 1263/1722] address test timing --- .../ArcGISToolkitTests/CertificatePickerViewModelTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift index 6679ececf..ad3421d3e 100644 --- a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift @@ -38,7 +38,10 @@ import XCTest // Have to yield here because the proceed function kicks off a task. await Task.yield() // Have to wait here because the proceed function waits to avoid a bug. - try? await Task.sleep(nanoseconds: 150_000) + try? await Task.sleep(nanoseconds: 300_000) + // Another yield seems to be required to deal with timing when running the test + // repeatedly. + await Task.yield() XCTAssertTrue(model.showCertificateImportError) model.cancel() From 9447169739731c4856ee618f0ce711eeb79d5918 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 12:32:26 -0700 Subject: [PATCH 1264/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index fb7c5a55d..c30820f9d 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -159,7 +159,7 @@ extension UtilityNetworkTraceViewModelTests { } private extension URL { - static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! - static var rtc_100_8 = URL(string: "http://rtc-100-8.esri.com/portal/home/webmap/viewer.html?webmap=78f993b89bad4ba0a8a22ce2e0bcfbd0")! + + static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! } From 2e887f487a3fed7ac9010160bbd0310f1e90e51d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 12:33:18 -0700 Subject: [PATCH 1265/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index c30820f9d..d4ffe1b91 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -159,6 +159,8 @@ extension UtilityNetworkTraceViewModelTests { } private extension URL { + static var rt_server109 = URL(string: "https://rt-server109.esri.com/portal/home/item.html?id=54fa9aadf6c645d39f006cf279147204")! + static var rtc_100_8 = URL(string: "http://rtc-100-8.esri.com/portal/home/webmap/viewer.html?webmap=78f993b89bad4ba0a8a22ce2e0bcfbd0")! static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! From 68ac7e9fee90da7add9b113e144040419bb166cd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 12:33:58 -0700 Subject: [PATCH 1266/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index d4ffe1b91..fd5ee70de 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -110,7 +110,16 @@ import XCTest ) } - func testCase_1_4() {} + func testCase_1_4() async throws { + let serverPassword: String? = nil + try XCTSkipIf(serverPassword == nil) + let token = try await ArcGISCredential.token( + url: .rt_server109, + username: "publisher1", + password: serverPassword! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + } func testCase_2_1() {} From c6b0720e49c0b14b6f95a0085d1ef08e14671993 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 12:42:38 -0700 Subject: [PATCH 1267/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index fd5ee70de..579594b0e 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -77,6 +77,9 @@ import XCTest } func testCase_1_3() async throws { + + try XCTSkipIf(true, "Server trust handling required") + let serverPassword: String? = "test.publisher01" try XCTSkipIf(serverPassword == nil) let token = try await ArcGISCredential.token( @@ -100,8 +103,6 @@ import XCTest await viewModel.load() - XCTExpectFailure("Further server trust handling required.") - XCTAssertFalse(viewModel.canRunTrace) XCTAssertTrue(viewModel.configurations.isEmpty) XCTAssertEqual( @@ -111,6 +112,9 @@ import XCTest } func testCase_1_4() async throws { + + try XCTSkipIf(true, "Server trust handling required") + let serverPassword: String? = nil try XCTSkipIf(serverPassword == nil) let token = try await ArcGISCredential.token( @@ -119,6 +123,8 @@ import XCTest password: serverPassword! ) await ArcGISRuntimeEnvironment.credentialStore.add(token) + + // - TODO: Finish implementation after server trust handling is resolved } func testCase_2_1() {} From a596f196227a2bc1d1ab43938441b776ad508ae8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 12:59:12 -0700 Subject: [PATCH 1268/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 579594b0e..96889fd15 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -80,7 +80,7 @@ import XCTest try XCTSkipIf(true, "Server trust handling required") - let serverPassword: String? = "test.publisher01" + let serverPassword: String? = nil try XCTSkipIf(serverPassword == nil) let token = try await ArcGISCredential.token( url: .rtc_100_8, From 7c703325265cce4c2c60efda3db5372c5dcda63f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 14:06:57 -0700 Subject: [PATCH 1269/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 96889fd15..3ada774ca 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -26,6 +26,7 @@ import XCTest func tearDownWithError() async throws { ArcGISRuntimeEnvironment.apiKey = nil + ArcGISRuntimeEnvironment.authenticationChallengeHandler = nil await ArcGISRuntimeEnvironment.credentialStore.removeAll() } @@ -78,13 +79,25 @@ import XCTest func testCase_1_3() async throws { - try XCTSkipIf(true, "Server trust handling required") - + let serverUsername = "publisher1" let serverPassword: String? = nil try XCTSkipIf(serverPassword == nil) + + let challengeHandler = ChallengeHandler( + trustedHosts: [URL.rtc_100_8.host!], + arcgisCredentialProvider: { challenge in + try await .token( + challenge: challenge, + username: serverUsername, + password: serverPassword! + ) + } + ) + ArcGISRuntimeEnvironment.authenticationChallengeHandler = challengeHandler + let token = try await ArcGISCredential.token( url: .rtc_100_8, - username: "publisher1", + username: serverUsername, password: serverPassword! ) await ArcGISRuntimeEnvironment.credentialStore.add(token) @@ -180,3 +193,74 @@ private extension URL { static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! } + + +/// 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 handleNetworkChallenge(_ challenge: NetworkAuthenticationChallenge) async -> NetworkAuthenticationChallengeDisposition { + // 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 .useCredential(.serverTrust) + } else { + return .performDefaultHandling + } + } else if let networkCredentialProvider = networkCredentialProvider, + let networkCredential = await networkCredentialProvider(challenge) { + return .useCredential(networkCredential) + } else { + return .cancelAuthenticationChallenge + } + } + + func handleArcGISChallenge( + _ challenge: ArcGISAuthenticationChallenge + ) async throws -> ArcGISAuthenticationChallenge.Disposition { + arcGISChallenges.append(challenge) + + if let arcgisCredentialProvider = arcgisCredentialProvider, + let arcgisCredential = try? await arcgisCredentialProvider(challenge) { + return .useCredential(arcgisCredential) + } else { + return .cancelAuthenticationChallenge + } + } +} From 8bf13bfc21a9c78c5a142b4fa5c9351618255e5f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 14:12:01 -0700 Subject: [PATCH 1270/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 3ada774ca..138630456 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -197,7 +197,7 @@ private extension URL { /// A `ChallengeHandler` that that can handle trusting hosts with a self-signed certificate, the URL credential, /// and the token credential. -class ChallengeHandler: AuthenticationChallengeHandler { +private class ChallengeHandler: AuthenticationChallengeHandler { /// The hosts that can be trusted if they have certificate trust issues. let trustedHosts: Set @@ -223,13 +223,6 @@ class ChallengeHandler: AuthenticationChallengeHandler { self.arcgisCredentialProvider = arcgisCredentialProvider } - convenience init( - trustedHosts: Set, - networkCredential: NetworkCredential - ) { - self.init(trustedHosts: trustedHosts, networkCredentialProvider: { _ in networkCredential }) - } - func handleNetworkChallenge(_ challenge: NetworkAuthenticationChallenge) async -> NetworkAuthenticationChallengeDisposition { // Record challenge only if it is not a server trust. if challenge.kind != .serverTrust { @@ -250,17 +243,4 @@ class ChallengeHandler: AuthenticationChallengeHandler { return .cancelAuthenticationChallenge } } - - func handleArcGISChallenge( - _ challenge: ArcGISAuthenticationChallenge - ) async throws -> ArcGISAuthenticationChallenge.Disposition { - arcGISChallenges.append(challenge) - - if let arcgisCredentialProvider = arcgisCredentialProvider, - let arcgisCredential = try? await arcgisCredentialProvider(challenge) { - return .useCredential(arcgisCredential) - } else { - return .cancelAuthenticationChallenge - } - } } From 0373a39ded97c495970c48124294e6e8c011e2f6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 22 Jul 2022 14:42:02 -0700 Subject: [PATCH 1271/1722] Add progress --- .../XCTest/XCTestCase+ArcGISURLSession.swift | 28 ++++++ .../Utility/ChallengeHandler.swift | 90 +++++++++++++++++ .../UtilityNetworkTraceViewModelTests.swift | 98 ++++++------------- 3 files changed, 147 insertions(+), 69 deletions(-) create mode 100644 Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift create mode 100644 Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift diff --git a/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift new file mode 100644 index 000000000..7ba7d9ba0 --- /dev/null +++ b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift @@ -0,0 +1,28 @@ +// +// 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 XCTest +import ArcGIS + +extension XCTestCase { + func setChallengeHandler(_ challengeHandler: AuthenticationChallengeHandler?) { + let previous = ArcGISRuntimeEnvironment.authenticationChallengeHandler + ArcGISRuntimeEnvironment.authenticationChallengeHandler = challengeHandler + addTeardownBlock { ArcGISRuntimeEnvironment.authenticationChallengeHandler = previous } + } +} diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift new file mode 100644 index 000000000..3ae7d970c --- /dev/null +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift @@ -0,0 +1,90 @@ +// +// 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 handleNetworkChallenge(_ challenge: NetworkAuthenticationChallenge) async -> NetworkAuthenticationChallengeDisposition { + // 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 .useCredential(.serverTrust) + } else { + return .performDefaultHandling + } + } else if let networkCredentialProvider = networkCredentialProvider, + let networkCredential = await networkCredentialProvider(challenge) { + return .useCredential(networkCredential) + } else { + return .cancelAuthenticationChallenge + } + } + + func handleArcGISChallenge( + _ challenge: ArcGISAuthenticationChallenge + ) async throws -> ArcGISAuthenticationChallenge.Disposition { + arcGISChallenges.append(challenge) + + if let arcgisCredentialProvider = arcgisCredentialProvider, + let arcgisCredential = try? await arcgisCredentialProvider(challenge) { + return .useCredential(arcgisCredential) + } else { + return .cancelAuthenticationChallenge + } + } +} diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 138630456..d92490671 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -40,6 +40,7 @@ import XCTest await viewModel.load() + XCTAssertNil(viewModel.network) XCTAssertFalse(viewModel.canRunTrace) XCTAssertEqual( viewModel.userWarning, @@ -69,6 +70,7 @@ import XCTest await viewModel.load() + XCTAssertNotNil(viewModel.network) XCTAssertFalse(viewModel.canRunTrace) XCTAssertTrue(viewModel.configurations.isEmpty) XCTAssertEqual( @@ -78,26 +80,14 @@ import XCTest } func testCase_1_3() async throws { - - let serverUsername = "publisher1" let serverPassword: String? = nil try XCTSkipIf(serverPassword == nil) - let challengeHandler = ChallengeHandler( - trustedHosts: [URL.rtc_100_8.host!], - arcgisCredentialProvider: { challenge in - try await .token( - challenge: challenge, - username: serverUsername, - password: serverPassword! - ) - } - ) - ArcGISRuntimeEnvironment.authenticationChallengeHandler = challengeHandler + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtc_100_8.host!])) let token = try await ArcGISCredential.token( url: .rtc_100_8, - username: serverUsername, + username: "publisher1", password: serverPassword! ) await ArcGISRuntimeEnvironment.credentialStore.add(token) @@ -116,6 +106,7 @@ import XCTest await viewModel.load() + XCTAssertNotNil(viewModel.network) XCTAssertFalse(viewModel.canRunTrace) XCTAssertTrue(viewModel.configurations.isEmpty) XCTAssertEqual( @@ -125,11 +116,11 @@ import XCTest } func testCase_1_4() async throws { - - try XCTSkipIf(true, "Server trust handling required") - let serverPassword: String? = nil try XCTSkipIf(serverPassword == nil) + + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + let token = try await ArcGISCredential.token( url: .rt_server109, username: "publisher1", @@ -137,7 +128,27 @@ import XCTest ) await ArcGISRuntimeEnvironment.credentialStore.add(token) - // - TODO: Finish implementation after server trust handling is resolved + guard let map = Map(url: .rt_server109) else { + XCTFail("Failed to load map") + return + } + + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [], + autoLoad: false + ) + + await viewModel.load() + + XCTAssertNotNil(viewModel.network) + XCTAssertFalse(viewModel.canRunTrace) + XCTAssertFalse(viewModel.configurations.isEmpty) + XCTAssertEqual( + viewModel.network?.name, + "L23UtilityNetwork_Utility_Network Utility Network" + ) } func testCase_2_1() {} @@ -193,54 +204,3 @@ private extension URL { static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! } - - -/// A `ChallengeHandler` that that can handle trusting hosts with a self-signed certificate, the URL credential, -/// and the token credential. -private 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 - } - - func handleNetworkChallenge(_ challenge: NetworkAuthenticationChallenge) async -> NetworkAuthenticationChallengeDisposition { - // 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 .useCredential(.serverTrust) - } else { - return .performDefaultHandling - } - } else if let networkCredentialProvider = networkCredentialProvider, - let networkCredential = await networkCredentialProvider(challenge) { - return .useCredential(networkCredential) - } else { - return .cancelAuthenticationChallenge - } - } -} From 4593a04eb28712c7fd5c85cb0664c259f46a010d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 22 Jul 2022 21:26:31 -0600 Subject: [PATCH 1272/1722] fix retain cycle --- .../Authentication/Authenticator.swift | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index a56135b35..0be78d3cb 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -42,7 +42,42 @@ public final class Authenticator: ObservableObject { ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations - observationTask = Task { [weak self] in await self?.observeChallengeQueue() } + observationTask = Task { [weak self] in + // Cannot unwrap self on the `for await` line or it will introduce a retain cycle. + guard let challengeQueue = self?.challengeQueue else { return } + for await queuedChallenge in challengeQueue { + guard let self = self else { break } + + // A yield here helps alleviate the already presenting bug. + await Task.yield() + + // Set the current challenge, this should present the appropriate view. + self.currentChallenge = queuedChallenge + + // Wait for the queued challenge to finish. + await queuedChallenge.complete() + + // Reset the current challenge to `nil`, that will dismiss the view. + self.currentChallenge = nil + } + } + } + + /// Observes the challenge queue and sets the current challenge. + private func observeChallengeQueue() async { +// for await queuedChallenge in challengeQueue { +// // A yield here helps alleviate the already presenting bug. +// await Task.yield() +// +// // Set the current challenge, this should present the appropriate view. +// currentChallenge = queuedChallenge +// +// // Wait for the queued challenge to finish. +// await queuedChallenge.complete() +// +// // Reset the current challenge to `nil`, that will dismiss the view. +// currentChallenge = nil +// } } /// Sets up new credential stores that will be persisted to the keychain. @@ -94,23 +129,6 @@ public final class Authenticator: ObservableObject { ) } - /// Observes the challenge queue and sets the current challenge. - private func observeChallengeQueue() async { - for await queuedChallenge in challengeQueue { - // A yield here helps alleviate the already presenting bug. - await Task.yield() - - // Set the current challenge, this should present the appropriate view. - currentChallenge = queuedChallenge - - // Wait for the queued challenge to finish. - await queuedChallenge.complete() - - // Reset the current challenge to `nil`, that will dismiss the view. - currentChallenge = nil - } - } - var subject = PassthroughSubject() /// A serial queue for authentication challenges. From e5416e5025830d4348727b1091c36e7b1b7ac1ae Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 22 Jul 2022 21:29:24 -0600 Subject: [PATCH 1273/1722] remove old code --- .../Authentication/Authenticator.swift | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 0be78d3cb..8eaaf6b0b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -63,23 +63,6 @@ public final class Authenticator: ObservableObject { } } - /// Observes the challenge queue and sets the current challenge. - private func observeChallengeQueue() async { -// for await queuedChallenge in challengeQueue { -// // A yield here helps alleviate the already presenting bug. -// await Task.yield() -// -// // Set the current challenge, this should present the appropriate view. -// currentChallenge = queuedChallenge -// -// // Wait for the queued challenge to finish. -// await queuedChallenge.complete() -// -// // Reset the current challenge to `nil`, that will dismiss the view. -// currentChallenge = nil -// } - } - /// 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 42d088fbff0d3aec874f35125daffdb2c5b36af9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 08:15:05 -0700 Subject: [PATCH 1274/1722] Update Examples/Examples/UtilityNetworkTraceExampleView.swift Co-authored-by: Mark Dostal --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 23200aa59..4bee70125 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -18,7 +18,7 @@ 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 displayed in the map view. + /// The map containing the utility networks. @StateObject private var map = makeMap() /// Provides the ability to inspect map components. From c0f6da58423d6f6fee8cc686f326fa1df948b853 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 08:15:15 -0700 Subject: [PATCH 1275/1722] Update Examples/Examples/UtilityNetworkTraceExampleView.swift Co-authored-by: Mark Dostal --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 4bee70125..842bffa2f 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -33,7 +33,7 @@ struct UtilityNetworkTraceExampleView: View { /// A container for graphical trace results. @State var resultGraphicsOverlay = GraphicsOverlay() - /// Optionally used to provide programmatic starting points to the trace tool. + /// Optional pre-defined starting points for the utility network trace. @State var startingPoints: [(GeoElement, Point?)] = [] /// The map viewpoint used by the `UtilityNetworkTrace` to pan/zoom the map to selected features. From a7116b3d22726e51fb204047fd5841e370feea42 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 08:15:33 -0700 Subject: [PATCH 1276/1722] Update Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift Co-authored-by: Mark Dostal --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 999cd96d2..a0bb28024 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -58,8 +58,7 @@ public struct UtilityNetworkTrace: View { /// The current user activity. @State private var currentActivity: UserActivity = .creatingTrace(nil) - /// A value indicating whether the viewpoint should be automatically changed to show the resulting - /// extent of a trace. + /// A Boolean value indicating whether the map should be zoomed to the extent of the trace result. @State private var shouldZoomOnTraceCompletion = false /// A Boolean value indicating if the warning that all traces will be deleted is presented. From 35fd8ab1169b1ca95bf1fc10e9724dfd69708e42 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 08:22:01 -0700 Subject: [PATCH 1277/1722] Move `UtilityNetworkTraceViewModel.Trace` Equatable into extension --- .../UtilityNetworkTraceViewModelTrace.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index e7ad85f63..d1aea2b3e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -84,12 +84,14 @@ extension UtilityNetworkTraceViewModel { /// A trace result comprised of Geometry objects var utilityGeometryTraceResult: UtilityGeometryTraceResult? - - static func == ( - lhs: UtilityNetworkTraceViewModel.Trace, - rhs: UtilityNetworkTraceViewModel.Trace - ) -> Bool { - return lhs.id == rhs.id - } + } +} + +extension UtilityNetworkTraceViewModel.Trace: Equatable { + static func == ( + lhs: UtilityNetworkTraceViewModel.Trace, + rhs: UtilityNetworkTraceViewModel.Trace + ) -> Bool { + return lhs.id == rhs.id } } From 1c583b5765eb30aec6da124b457ba47d1f13d5a2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 09:23:22 -0700 Subject: [PATCH 1278/1722] Specify `Equatable` conformance --- Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift | 2 +- Sources/ArcGISToolkit/Extensions/UtilityElement.swift | 2 +- Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift | 2 +- Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift b/Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift index e1248a701..bc0f39ba3 100644 --- a/Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift +++ b/Sources/ArcGISToolkit/Extensions/UtilityAssetType.swift @@ -13,7 +13,7 @@ import ArcGIS -extension UtilityAssetType: Hashable, Identifiable { +extension UtilityAssetType: Equatable, Hashable, Identifiable { public static func == (lhs: ArcGIS.UtilityAssetType, rhs: ArcGIS.UtilityAssetType) -> Bool { lhs.hashValue == rhs.hashValue } diff --git a/Sources/ArcGISToolkit/Extensions/UtilityElement.swift b/Sources/ArcGISToolkit/Extensions/UtilityElement.swift index b57c07f2c..782ad7ad9 100644 --- a/Sources/ArcGISToolkit/Extensions/UtilityElement.swift +++ b/Sources/ArcGISToolkit/Extensions/UtilityElement.swift @@ -13,7 +13,7 @@ import ArcGIS -extension UtilityElement: Hashable, Identifiable { +extension UtilityElement: Equatable, Hashable, Identifiable { public static func == (lhs: ArcGIS.UtilityElement, rhs: ArcGIS.UtilityElement) -> Bool { lhs.globalID == rhs.globalID } diff --git a/Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift b/Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift index c6b4f0e04..45988eeae 100644 --- a/Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift +++ b/Sources/ArcGISToolkit/Extensions/UtilityNetwork.swift @@ -13,7 +13,7 @@ import ArcGIS -extension UtilityNetwork: Hashable { +extension UtilityNetwork: Equatable, Hashable { public static func == ( lhs: UtilityNetwork, rhs: UtilityNetwork diff --git a/Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift b/Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift index 7a8aafa82..50d8f7768 100644 --- a/Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift +++ b/Sources/ArcGISToolkit/Extensions/UtilityTerminal.swift @@ -13,7 +13,7 @@ import ArcGIS -extension UtilityTerminal: Hashable { +extension UtilityTerminal: Equatable, Hashable { public static func == ( lhs: ArcGIS.UtilityTerminal, rhs: ArcGIS.UtilityTerminal From 7d904484f9b815305d90c08e34b86c2b0864db1f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 09:58:39 -0700 Subject: [PATCH 1279/1722] Add/use `UtilityNetworkTraceSimpleStartingPoint` --- .../UtilityNetworkTraceExampleView.swift | 6 ++- .../UtilityNetworkTrace.swift | 10 ++--- ...ilityNetworkTraceSimpleStartingPoint.swift | 44 +++++++++++++++++++ .../UtilityNetworkTraceViewModel.swift | 9 ++-- 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 842bffa2f..22e266db7 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -34,7 +34,7 @@ struct UtilityNetworkTraceExampleView: View { @State var resultGraphicsOverlay = GraphicsOverlay() /// Optional pre-defined starting points for the utility network trace. - @State var startingPoints: [(GeoElement, Point?)] = [] + @State var startingPoints: [UtilityNetworkTraceSimpleStartingPoint] = [] /// The map viewpoint used by the `UtilityNetworkTrace` to pan/zoom the map to selected features. @State var viewpoint: Viewpoint? @@ -123,7 +123,9 @@ extension UtilityNetworkTraceExampleView { if let feature = element as? ArcGISFeature, let id = feature.attributes["globalid"] as? UUID, id == targetID { - startingPoints = [(element, nil)] + startingPoints = [ + UtilityNetworkTraceSimpleStartingPoint(geoElement: element) + ] } } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index a0bb28024..96f74f7bf 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -71,7 +71,7 @@ public struct UtilityNetworkTrace: View { // MARK: Bindings /// Starting points programmatically provided to the trace tool. - @Binding private var externalStartingPoints: [(GeoElement, Point?)] + @Binding private var externalStartingPoints: [UtilityNetworkTraceSimpleStartingPoint] /// The graphics overlay to hold generated starting point and trace graphics. @Binding private var graphicsOverlay: GraphicsOverlay @@ -484,9 +484,7 @@ public struct UtilityNetworkTrace: View { /// - 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. - /// - startingPoints: An optional list of elements to be used as starting points, provided as - /// tuples. The first item is a geo element and the optional second item is a map point (useful for - /// specifying a fractional starting location along an edge element). + /// - startingPoints: An optional list of programmatically provided starting points. public init( graphicsOverlay: Binding, map: Map, @@ -494,7 +492,7 @@ public struct UtilityNetworkTrace: View { viewPoint: Binding, mapViewProxy: Binding, viewpoint: Binding, - startingPoints: Binding<[(GeoElement, Point?)]> = .constant([]) + startingPoints: Binding<[UtilityNetworkTraceSimpleStartingPoint]> = .constant([]) ) { _viewPoint = viewPoint _mapPoint = mapPoint @@ -554,7 +552,7 @@ public struct UtilityNetworkTrace: View { ) } } - .onChange(of: externalStartingPoints.count) { _ in + .onChange(of: externalStartingPoints) { _ in viewModel.externalStartingPoints = externalStartingPoints } .alert( diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift new file mode 100644 index 000000000..1de26e54b --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift @@ -0,0 +1,44 @@ +// 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 +import UIKit + +/// A simplified starting point of a utility network trace. +public struct UtilityNetworkTraceSimpleStartingPoint { + /// The underlying geo element. + let geoElement: GeoElement + + /// A unique identifier for the simple starting point. + let id = UUID() + + /// A map point (useful for specifying a fractional starting location along an edge element). + let point: Point? + + /// - Parameters: + /// - geoElement: The underlying geo element. + /// - point: A map point (useful for specifying a fractional starting location along an edge element). + public init( + geoElement: GeoElement, + point: Point? = nil + ) { + self.geoElement = geoElement + self.point = point + } +} + +extension UtilityNetworkTraceSimpleStartingPoint: Equatable { + public static func == (lhs: UtilityNetworkTraceSimpleStartingPoint, rhs: UtilityNetworkTraceSimpleStartingPoint) -> Bool { + lhs.id == rhs.id + } +} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 51003dbf4..20a60c367 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -70,7 +70,7 @@ import SwiftUI private var map: Map /// Starting points programmatically provided to the trace tool. - var externalStartingPoints = [(GeoElement, Point?)]() { + var externalStartingPoints = [UtilityNetworkTraceSimpleStartingPoint]() { didSet { addExternalStartingPoints() } @@ -94,7 +94,7 @@ import SwiftUI init( map: Map, graphicsOverlay: GraphicsOverlay, - startingPoints: [(GeoElement, Point?)] + startingPoints: [UtilityNetworkTraceSimpleStartingPoint] ) { self.map = map self.graphicsOverlay = graphicsOverlay @@ -416,7 +416,10 @@ import SwiftUI /// Adds programatic starting points to the pending trace. private func addExternalStartingPoints() { externalStartingPoints.forEach { - setStartingPoint(geoElement: $0.0, mapPoint: $0.1) + setStartingPoint( + geoElement: $0.geoElement, + mapPoint: $0.point + ) } } From 1483d9db54e7662b186bfd37c2ba411dfbaaabe4 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 26 Jul 2022 13:45:29 -0600 Subject: [PATCH 1280/1722] Add auth example --- .../project.pbxproj | 394 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 +++++ .../Assets.xcassets/Contents.json | 6 + .../AuthenticationApp.swift | 71 ++++ .../FeaturedMapsView.swift | 79 ++++ .../AuthenticationExample/HomeView.swift | 46 ++ .../AuthenticationExample/Info.plist | 24 ++ .../AuthenticationExample/ItemsView.swift | 73 ++++ .../LoadableImageView.swift | 47 +++ .../AuthenticationExample/MapItemView.swift | 45 ++ .../PortalItemsView.swift | 73 ++++ .../Preview Assets.xcassets/Contents.json | 6 + .../AuthenticationExample/ProfileView.swift | 69 +++ .../AuthenticationExample/SigninView.swift | 100 +++++ .../AuthenticationExample/UserView.swift | 119 ++++++ 18 files changed, 1276 insertions(+) create mode 100644 AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj create mode 100644 AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 AuthenticationExample/AuthenticationExample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 AuthenticationExample/AuthenticationExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 AuthenticationExample/AuthenticationExample/Assets.xcassets/Contents.json create mode 100644 AuthenticationExample/AuthenticationExample/AuthenticationApp.swift create mode 100644 AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift create mode 100644 AuthenticationExample/AuthenticationExample/HomeView.swift create mode 100644 AuthenticationExample/AuthenticationExample/Info.plist create mode 100644 AuthenticationExample/AuthenticationExample/ItemsView.swift create mode 100644 AuthenticationExample/AuthenticationExample/LoadableImageView.swift create mode 100644 AuthenticationExample/AuthenticationExample/MapItemView.swift create mode 100644 AuthenticationExample/AuthenticationExample/PortalItemsView.swift create mode 100644 AuthenticationExample/AuthenticationExample/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 AuthenticationExample/AuthenticationExample/ProfileView.swift create mode 100644 AuthenticationExample/AuthenticationExample/SigninView.swift create mode 100644 AuthenticationExample/AuthenticationExample/UserView.swift diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..917f59a2e --- /dev/null +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj @@ -0,0 +1,394 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 88AD13792834355000500B2E /* AuthenticationApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AD13782834355000500B2E /* AuthenticationApp.swift */; }; + 88AD137B2834355000500B2E /* SigninView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AD137A2834355000500B2E /* SigninView.swift */; }; + 88AD137D2834355100500B2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88AD137C2834355100500B2E /* Assets.xcassets */; }; + 88AD13802834355100500B2E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88AD137F2834355100500B2E /* Preview Assets.xcassets */; }; + 88AD138A2834366100500B2E /* ArcGISToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = 88AD13892834366100500B2E /* ArcGISToolkit */; }; + 88AD138E283443F800500B2E /* MapItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AD138D283443F800500B2E /* MapItemView.swift */; }; + 88D24DF0288F062D007A539C /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DEF288F062D007A539C /* ProfileView.swift */; }; + 88D24DF2288F2FA1007A539C /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF1288F2FA1007A539C /* HomeView.swift */; }; + 88D24DF4288F4BEB007A539C /* FeaturedMapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF3288F4BEB007A539C /* FeaturedMapsView.swift */; }; + 88D24DF6288F5BAE007A539C /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF5288F5BAE007A539C /* UserView.swift */; }; + 88D24DF8288F6002007A539C /* LoadableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D24DF7288F6002007A539C /* LoadableImageView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 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 = ""; }; + 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-runtime-toolkit-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "arcgis-runtime-toolkit-swift"; path = "../../../../Source/ArcGIS/arcgis-runtime-toolkit-swift"; 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 = ""; }; + 88D24DF1288F2FA1007A539C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 88D24DF3288F4BEB007A539C /* FeaturedMapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedMapsView.swift; sourceTree = ""; }; + 88D24DF5288F5BAE007A539C /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = ""; }; + 88D24DF7288F6002007A539C /* LoadableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImageView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 88AD13722834355000500B2E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 88AD138A2834366100500B2E /* ArcGISToolkit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 88AD136C2834355000500B2E = { + isa = PBXGroup; + children = ( + 88AD13862834355C00500B2E /* arcgis-runtime-toolkit-swift */, + 88AD13772834355000500B2E /* AuthenticationExample */, + 88AD13762834355000500B2E /* Products */, + 88AD13882834366100500B2E /* Frameworks */, + ); + sourceTree = ""; + }; + 88AD13762834355000500B2E /* Products */ = { + isa = PBXGroup; + children = ( + 88AD13752834355000500B2E /* AuthenticationExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 88AD13772834355000500B2E /* AuthenticationExample */ = { + isa = PBXGroup; + children = ( + 88AD1387283435EA00500B2E /* Info.plist */, + 88AD13782834355000500B2E /* AuthenticationApp.swift */, + 88D24DF1288F2FA1007A539C /* HomeView.swift */, + 88D24DF7288F6002007A539C /* LoadableImageView.swift */, + 88D24DF3288F4BEB007A539C /* FeaturedMapsView.swift */, + 88AD138D283443F800500B2E /* MapItemView.swift */, + 88AD137A2834355000500B2E /* SigninView.swift */, + 88D24DEF288F062D007A539C /* ProfileView.swift */, + 88D24DF5288F5BAE007A539C /* UserView.swift */, + 88AD137C2834355100500B2E /* Assets.xcassets */, + 88AD137E2834355100500B2E /* Preview Content */, + ); + path = AuthenticationExample; + sourceTree = ""; + }; + 88AD137E2834355100500B2E /* Preview Content */ = { + isa = PBXGroup; + children = ( + 88AD137F2834355100500B2E /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 88AD13882834366100500B2E /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 88AD13742834355000500B2E /* AuthenticationExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 88AD13832834355100500B2E /* Build configuration list for PBXNativeTarget "AuthenticationExample" */; + buildPhases = ( + 88AD13712834355000500B2E /* Sources */, + 88AD13722834355000500B2E /* Frameworks */, + 88AD13732834355000500B2E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AuthenticationExample; + packageProductDependencies = ( + 88AD13892834366100500B2E /* ArcGISToolkit */, + ); + productName = Authentication; + productReference = 88AD13752834355000500B2E /* AuthenticationExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 88AD136D2834355000500B2E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + 88AD13742834355000500B2E = { + CreatedOnToolsVersion = 13.2.1; + }; + }; + }; + buildConfigurationList = 88AD13702834355000500B2E /* Build configuration list for PBXProject "AuthenticationExample" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 88AD136C2834355000500B2E; + productRefGroup = 88AD13762834355000500B2E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 88AD13742834355000500B2E /* AuthenticationExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 88AD13732834355000500B2E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 88AD13802834355100500B2E /* Preview Assets.xcassets in Resources */, + 88AD137D2834355100500B2E /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 88AD13712834355000500B2E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 88AD138E283443F800500B2E /* MapItemView.swift in Sources */, + 88AD137B2834355000500B2E /* SigninView.swift in Sources */, + 88D24DF4288F4BEB007A539C /* FeaturedMapsView.swift in Sources */, + 88D24DF2288F2FA1007A539C /* HomeView.swift in Sources */, + 88D24DF8288F6002007A539C /* LoadableImageView.swift in Sources */, + 88D24DF0288F062D007A539C /* ProfileView.swift in Sources */, + 88AD13792834355000500B2E /* AuthenticationApp.swift in Sources */, + 88D24DF6288F5BAE007A539C /* UserView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 88AD13812834355100500B2E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 88AD13822834355100500B2E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 88AD13842834355100500B2E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"AuthenticationExample/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AuthenticationExample/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.esri.Authentication; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 88AD13852834355100500B2E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"AuthenticationExample/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AuthenticationExample/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.esri.Authentication; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 88AD13702834355000500B2E /* Build configuration list for PBXProject "AuthenticationExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 88AD13812834355100500B2E /* Debug */, + 88AD13822834355100500B2E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 88AD13832834355100500B2E /* Build configuration list for PBXNativeTarget "AuthenticationExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 88AD13842834355100500B2E /* Debug */, + 88AD13852834355100500B2E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 88AD13892834366100500B2E /* ArcGISToolkit */ = { + isa = XCSwiftPackageProductDependency; + productName = ArcGISToolkit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 88AD136D2834355000500B2E /* Project object */; +} diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/AuthenticationExample/AuthenticationExample/Assets.xcassets/AccentColor.colorset/Contents.json b/AuthenticationExample/AuthenticationExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AuthenticationExample/AuthenticationExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/AuthenticationExample/AuthenticationExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..9221b9bb1 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AuthenticationExample/AuthenticationExample/Assets.xcassets/Contents.json b/AuthenticationExample/AuthenticationExample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift new file mode 100644 index 000000000..2cb8c5d2b --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -0,0 +1,71 @@ +// 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 ArcGISToolkit +import ArcGIS + +@main +struct AuthenticationApp: App { + @ObservedObject var authenticator: Authenticator + + init() { + // Create an authenticator. + authenticator = Authenticator( + promptForUntrustedHosts: true + // If you want to use OAuth, uncomment this code: + //, + //oAuthConfigurations: [.arcgisDotCom] + ) + // Set the challenge handler to be the authenticator we just created. + ArcGISRuntimeEnvironment.authenticationChallengeHandler = authenticator + } + + var body: some SwiftUI.Scene { + WindowGroup { + NavigationView { + HomeView() + } + // Using this view modifier will cause 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) + .authenticator(authenticator) + .environmentObject(authenticator) + .task { + // Here we make the authenticator persistent, which means that it will synchronize + // with they keychain for storing credentials. + // It also means that a user can sign in without having to be promped for + // credentials. Once credentials are cleared from the stores ("sign-out"), + // then the user will need to be prompted once again. + try? await authenticator.makePersistent(access: .whenUnlockedThisDeviceOnly) + } + } + } +} + +// If want to use OAuth, you can uncomment this code: +//private extension OAuthConfiguration { +// static let arcgisDotCom = OAuthConfiguration( +// portalURL: .arcgisDotCom, +// clientID: <#Your client ID here#>, +// redirectURL: URL(string: "toolkit-auth-example://auth")! +// ) +//} + +extension URL { + // If you want to use your own portal, provide your own URL here: + static let portal = URL(string: "https://www.arcgis.com")! +} diff --git a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift new file mode 100644 index 000000000..a2c6f2436 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift @@ -0,0 +1,79 @@ +// 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 view that displays the featured maps of a portal. +struct FeaturedMapsView: View { + /// The portal from which the featured content can be fetched. + var portal: Portal + + /// A Boolean value indicating whether the featured content is being loaded. + @State var isLoading = true + + /// The featured items. + @State var featuredItems = [PortalItem]() + + var body: some View { + VStack { + if isLoading { + ProgressView() + } else { + List(featuredItems) { item in + NavigationLink { + MapItemView(map: Map(item: item)) + } label: { + PortalItemView(item: item) + } + } + } + } + .task { + guard featuredItems.isEmpty else { return } + do { + featuredItems = try await portal.homePageFeaturedContent + .filter { $0.kind == .webMap } + } catch {} + + isLoading = false + } + .navigationTitle("Featured Maps") + } +} + +/// A view that displays information about a portal item for viewing that informationwithin a list. +struct PortalItemView: View { + /// The portal item to display information about. + var item: PortalItem + + var body: some View { + HStack { + if let thumbnail = item.thumbnail { + LoadableImageView(loadableImage: thumbnail) + .frame(width: 64, height: 44) + } + VStack(alignment: .leading, spacing: 2) { + Text(item.title) + .font(.headline) + .lineLimit(2) + Text("Owner: \(item.owner), Views: \(item.viewCount)") + .font(.caption) + Text(item.snippet) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + } + } + } +} diff --git a/AuthenticationExample/AuthenticationExample/HomeView.swift b/AuthenticationExample/AuthenticationExample/HomeView.swift new file mode 100644 index 000000000..ecefb3d6b --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/HomeView.swift @@ -0,0 +1,46 @@ +// 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 + +/// The view that is displayed after successfully signing in. +struct HomeView: View { + /// The portal that the user is signed in to. + @State var portal: Portal? + + /// A Boolean value indicating whether the profile view should be presented. + @State var showProfile = false + + var body: some View { + if let portal = portal { + FeaturedMapsView(portal: portal) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showProfile = true + } label: { + Image(systemName: "person.circle") + } + } + } + .sheet(isPresented: $showProfile) { + ProfileView(portal: portal) { + self.portal = nil + } + } + } else { + SigninView(portal: $portal) + } + } +} diff --git a/AuthenticationExample/AuthenticationExample/Info.plist b/AuthenticationExample/AuthenticationExample/Info.plist new file mode 100644 index 000000000..6680e5820 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.esri.Examples + CFBundleURLSchemes + + toolkit-auth-example + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/AuthenticationExample/AuthenticationExample/ItemsView.swift b/AuthenticationExample/AuthenticationExample/ItemsView.swift new file mode 100644 index 000000000..047faa50b --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/ItemsView.swift @@ -0,0 +1,73 @@ +// 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 +import ArcGISToolkit + +/// A view that displays portal items. +struct PortalItemsView: View { + var portal: Portal + @State var isLoading = true + @State var featuredItems = [PortalItem]() + + var body: some View { + VStack { + if isLoading { + ProgressView() + } else { + List(featuredItems) { item in + NavigationLink { + MapItemView(map: Map(item: item)) + } label: { + ItemView(item: item) + } + } + } + } + .task { + guard featuredItems.isEmpty else { return } + do { + featuredItems = try await portal.homePageFeaturedContent + .filter { $0.kind == .webMap } + } catch {} + + isLoading = false + } + .navigationTitle("Featured Maps") + } +} + +struct ItemView: View { + var item: PortalItem + + var body: some View { + HStack { + if let thumbnail = item.thumbnail { + LoadableImageView(loadableImage: thumbnail) + .frame(width: 64, height: 44) + } + VStack(alignment: .leading, spacing: 2) { + Text(item.title) + .font(.headline) + .lineLimit(2) + Text("Owner: \(item.owner), Views: \(item.viewCount)") + .font(.caption) + Text(item.snippet) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + } + } + } +} diff --git a/AuthenticationExample/AuthenticationExample/LoadableImageView.swift b/AuthenticationExample/AuthenticationExample/LoadableImageView.swift new file mode 100644 index 000000000..89c308f14 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/LoadableImageView.swift @@ -0,0 +1,47 @@ +// 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 view that loads a `LoadableImage` and displays it. +/// While the image is loading a progress view is displayed. +/// If there is an error displaying the image a red exclamation circle is displayed. +struct LoadableImageView: View { + /// The loadable image to display. + let loadableImage: LoadableImage + + /// The result of loading the image. + @State var result: Result? = nil + + var body: some View { + Group { + switch result { + case .none: + ProgressView() + case .failure: + Image(systemName: "exclamationmark.circle") + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + case .success(let image): + Image(uiImage: image) + .resizable() + } + }.task { + result = await Result { + try await loadableImage.load() + return loadableImage.image ?? UIImage() + } + } + } +} diff --git a/AuthenticationExample/AuthenticationExample/MapItemView.swift b/AuthenticationExample/AuthenticationExample/MapItemView.swift new file mode 100644 index 000000000..205fcf619 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/MapItemView.swift @@ -0,0 +1,45 @@ +// 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 view that displays a map. +struct MapItemView: View { + /// The map that is to be displayed. + let map: Map + + /// The result of loading the map. + @State var loadResult: Result? + + var body: some View { + VStack { + switch loadResult { + case .none: + ProgressView() + case .success: + MapView(map: map) + .edgesIgnoringSafeArea(.top) + case .failure(let error): + Text(error.localizedDescription) + .font(.footnote) + .foregroundColor(.secondary) + } + } + .task { + guard loadResult == nil else { return } + loadResult = await Result { try await map.load() } + } + } +} + diff --git a/AuthenticationExample/AuthenticationExample/PortalItemsView.swift b/AuthenticationExample/AuthenticationExample/PortalItemsView.swift new file mode 100644 index 000000000..f3d113458 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/PortalItemsView.swift @@ -0,0 +1,73 @@ +// 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 +import ArcGISToolkit + +/// A view that displays portal items. +struct FeaturedMapsView: View { + var portal: Portal + @State var isLoading = true + @State var featuredItems = [PortalItem]() + + var body: some View { + VStack { + if isLoading { + ProgressView() + } else { + List(featuredItems) { item in + NavigationLink { + MapItemView(map: Map(item: item)) + } label: { + ItemView(item: item) + } + } + } + } + .task { + guard featuredItems.isEmpty else { return } + do { + featuredItems = try await portal.homePageFeaturedContent + .filter { $0.kind == .webMap } + } catch {} + + isLoading = false + } + .navigationTitle("Featured Maps") + } +} + +struct ItemView: View { + var item: PortalItem + + var body: some View { + HStack { + if let thumbnail = item.thumbnail { + LoadableImageView(loadableImage: thumbnail) + .frame(width: 64, height: 44) + } + VStack(alignment: .leading, spacing: 2) { + Text(item.title) + .font(.headline) + .lineLimit(2) + Text("Owner: \(item.owner), Views: \(item.viewCount)") + .font(.caption) + Text(item.snippet) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + } + } + } +} diff --git a/AuthenticationExample/AuthenticationExample/Preview Content/Preview Assets.xcassets/Contents.json b/AuthenticationExample/AuthenticationExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift new file mode 100644 index 000000000..dd8257fca --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -0,0 +1,69 @@ +// 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 +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 + + /// A Boolean indicating whether the user is signing out. + @State var isSigningOut: Bool = false + + /// The closure to call once the user has signed out. + var signoutAction: () -> Void + + var body: some View { + VStack { + if let user = portal.user { + UserView(user: user).padding() + } + Spacer() + signOutButton + } + .padding() + } + + var signOutButton: some View { + Button { + signOut() + } label: { + if isSigningOut { + ProgressView() + .frame(maxWidth: .infinity, alignment: .center) + } else { + Text("Sign Out") + .frame(maxWidth: .infinity) + } + } + .buttonStyle(.bordered) + .controlSize(.large) + .disabled(isSigningOut) + } + + /// Signs out from the portal and clears the credential stores. + func signOut() { + isSigningOut = true + Task { + await authenticator.clearCredentialStores() + isSigningOut = false + signoutAction() + } + } +} diff --git a/AuthenticationExample/AuthenticationExample/SigninView.swift b/AuthenticationExample/AuthenticationExample/SigninView.swift new file mode 100644 index 000000000..3636dc8d0 --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/SigninView.swift @@ -0,0 +1,100 @@ +// 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 +import ArcGISToolkit + +/// 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 + + /// The error that occurred during an attempt to sign in. + @State var error: Error? + + /// A Boolean value indicating if the user is currently signing in. + @State var isSigningIn: Bool = false + + /// The portal that the user successfully signed in to. + @Binding var portal: Portal? + + var body: some View { + VStack { + Spacer() + if isSigningIn { + ProgressView() + } else { + Text(URL.portal.host!) + .font(.footnote) + .foregroundColor(.secondary) + signInButton + } + Spacer() + if let error = error, !error.isChallengeCancellationError { + Text(error.localizedDescription) + .font(.footnote) + .foregroundColor(.secondary) + } + } + .padding() + } + + var signInButton: some View { + Button { + signIn() + } label: { + Text("Sign in") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.large) + .disabled(isSigningIn || portal != nil) + } + + /// Attempts to sign into a portal. + func signIn() { + isSigningIn = true + error = nil + Task { + do { + let portal = Portal(url: .portal, isLoginRequired: true) + try await portal.load() + self.portal = portal + } catch { + self.error = error + } + isSigningIn = false + } + } +} + +private extension Error { + /// Returns a Bool 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 NSError: + return error.domain == NSURLErrorDomain && error.code == -999 + default: + return false + } + } +} diff --git a/AuthenticationExample/AuthenticationExample/UserView.swift b/AuthenticationExample/AuthenticationExample/UserView.swift new file mode 100644 index 000000000..1e3e84ccd --- /dev/null +++ b/AuthenticationExample/AuthenticationExample/UserView.swift @@ -0,0 +1,119 @@ +// 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 view that displays details of a portal user. +struct UserView: View { + /// The user to display the details for. + @State var user: PortalUser + + var body: some View { + VStack { + Group { + if let thumbnail = user.thumbnail { + LoadableImageView(loadableImage: thumbnail) + .frame(width: 100, height: 100, alignment: .center) + } else { + Image(systemName: "person.circle") + .resizable() + .foregroundColor(.secondary) + } + } + .frame(width: 100, height: 100, alignment: .center) + .padding(.bottom) + ScrollView { + VStack(spacing: 12) { + UserAttributeView(title: "Name", detail: user.fullName) + UserAttributeView(title: "Username", detail: user.username) + UserAttributeView(title: "Email", detail: user.email) + if !user.organizationID.isEmpty { + UserAttributeView(title: "Organization ID", detail: user.organizationID) + } + if let created = user.creationDate { + UserAttributeView(title: "Created", detail: created.formatted()) + } else if let modified = user.modificationDate { + UserAttributeView(title: "Modified", detail: modified.formatted()) + } + if let role = user.role { + UserAttributeView(title: "Role", detail: role.description) + } + if let units = user.units { + UserAttributeView(title: "Units", detail: units.description) + } + if let access = user.access { + UserAttributeView(title: "Access", detail: access.description) + } + UserAttributeListView(title: "Tags", details: user.tags) + UserAttributeListView(title: "Privileges", details: user.privileges) + } + } + } + } +} + +extension PortalUser.Role: CustomStringConvertible { + public var description: String { + switch self { + case .user: + return "User" + case .admin: + return "Admin" + case .publisher: + return "Publisher" + } + } +} + +/// A view that displays user attributes where the detail is a single string. +struct UserAttributeView: View { + /// The attribute title. + let title: String + + /// The attribute detail. + let detail: String + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .lineLimit(1) + .foregroundColor(.secondary) + .font(.footnote) + Text(detail) + .font(.caption) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +/// A view that displays user attributes where the details are numerous. +struct UserAttributeListView: View { + /// The attribute title. + let title: String + + /// The attribute details. + let details: [CustomStringConvertible] + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .lineLimit(1) + .foregroundColor(.secondary) + .font(.footnote) + Text(details.map(\.description).joined(separator: "\r")) + .font(.caption) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} From c0f3622c09655868dd89576735610ff9b45290b9 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 26 Jul 2022 14:15:25 -0600 Subject: [PATCH 1281/1722] fix redirect --- .../AuthenticationExample/AuthenticationApp.swift | 9 ++++++--- AuthenticationExample/AuthenticationExample/Info.plist | 7 +------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 2cb8c5d2b..430242e2a 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -59,9 +59,12 @@ struct AuthenticationApp: App { // If want to use OAuth, you can uncomment this code: //private extension OAuthConfiguration { // static let arcgisDotCom = OAuthConfiguration( -// portalURL: .arcgisDotCom, -// clientID: <#Your client ID here#>, -// redirectURL: URL(string: "toolkit-auth-example://auth")! +// portalURL: .portal, +// clientID: "<#Your client ID goes here#>", +// // Note: You must have the same redirect URL used here +// // registered with your client ID. +// // The scheme of the redirect URL is also specified in the Info.plist file. +// redirectURL: URL(string: "authexample://auth")! // ) //} diff --git a/AuthenticationExample/AuthenticationExample/Info.plist b/AuthenticationExample/AuthenticationExample/Info.plist index 6680e5820..f24f5e8c2 100644 --- a/AuthenticationExample/AuthenticationExample/Info.plist +++ b/AuthenticationExample/AuthenticationExample/Info.plist @@ -11,14 +11,9 @@ com.esri.Examples CFBundleURLSchemes - toolkit-auth-example + authexample - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - From 1aebf50d20fff459bd9fd34c0841173348e7043f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 26 Jul 2022 15:18:31 -0500 Subject: [PATCH 1282/1722] TextPopupElementView implementation. --- .../Components/Popups/HTMLTextView.swift | 83 +++++++++++++++++++ .../Components/Popups/PopupView.swift | 4 +- .../Popups/TextPopupElementView.swift | 37 +++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift new file mode 100644 index 000000000..dc405d102 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -0,0 +1,83 @@ +// 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 WebKit +import SwiftUI + +/// A SwiftUI view to display an HTML string in a `WKWebView`. +struct HTMLTextView: UIViewRepresentable { + /// The html string to dispay, including the header. + var displayHTMLString: String + + /// The height of the view, calculated in the `webView(didFinish:)` delegate method. + @Binding private var dynamicHeight: CGFloat + + /// The static header string for displaying html strings in a readable size. + static var headerString = "

" + static var footerString = "

" + + /// Creates an `HTMLTextView`. + /// - Parameters: + /// - htmlString: The html string to be displayed. + /// - dynamicHeight: A binding to the calculated height of the `WKWebView`. + init(htmlString: String, dynamicHeight: Binding) { + displayHTMLString = HTMLTextView.headerString + htmlString + HTMLTextView.footerString + _dynamicHeight = dynamicHeight + } + + func makeUIView(context: Context) -> WKWebView { + WKWebView() + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + uiView.backgroundColor = .clear + uiView.navigationDelegate = context.coordinator + uiView.loadHTMLString(displayHTMLString, baseURL: nil) + uiView.scrollView.isScrollEnabled = false + } + + class Coordinator: NSObject, WKNavigationDelegate { + var parent: HTMLTextView + + init(_ parent: HTMLTextView) { + self.parent = parent + } + + // WKNavigationDelegate method for navigation actions. + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + if navigationAction.navigationType == .linkActivated && + (navigationAction.request.url?.scheme?.lowercased() == "http" || + navigationAction.request.url?.scheme?.lowercased() == "https") { + + if let url = navigationAction.request.url { + DispatchQueue.main.async { + UIApplication.shared.open(url) + } + } + return .cancel + } + else { + return .allow + } + } + + // WKNavigationDelegate method where the size calculation happens. + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){ + self.parent.dynamicHeight = webView.scrollView.contentSize.height + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 1a4e64edf..b154b3a58 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -58,6 +58,7 @@ public struct PopupView: View { do { expressionEvaluations = try await popup.evaluateExpressions() isPopupEvaluated = true + print("EE: \(expressionEvaluations?.count); first = \(expressionEvaluations?.first?.error)") } catch { isPopupEvaluated = false } @@ -80,8 +81,7 @@ public struct PopupView: View { Text("MediaPopupElementView implementation coming soon.") .font(.caption) case let popupElement as TextPopupElement: - Text("TextPopupElementView implementation coming soon.") - .font(.caption) + TextPopupElementView(popupElement: popupElement) default: EmptyView() } diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift new file mode 100644 index 000000000..0d984d846 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -0,0 +1,37 @@ +// 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 view displaying a `TextPopupElement`. +struct TextPopupElementView: View { + /// The `PopupElement` to display. + var popupElement: TextPopupElement + + /// The calcuated height of the `HTMLTextView`. + @State private var webViewHeight: CGFloat = .zero + + var body: some View { + let roundedRect = RoundedRectangle(cornerRadius: 8) + if !popupElement.text.isEmpty { + ZStack { + HTMLTextView(htmlString: popupElement.text, dynamicHeight: $webViewHeight) + .clipShape(roundedRect) + .frame(height: webViewHeight) + roundedRect + .stroke(Color.black, lineWidth: 1) + } + } + } +} From b6ca2a958a3d75a8ac276025716ee2283f24716a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 26 Jul 2022 15:18:50 -0500 Subject: [PATCH 1283/1722] Remove line limit for Fields. --- .../ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 2a11988ab..2f03fb809 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -68,7 +68,6 @@ struct FieldsPopupElementView: View { FormattedValueText(formattedValue: field.formattedValue) .padding([.bottom], -1) } - .lineLimit(1) .background(Color.clear) .frame(maxWidth: .infinity, alignment: .leading) } From 0aac3dceb737d73f1e63f617fe782aa3615bc3b3 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 26 Jul 2022 14:24:15 -0600 Subject: [PATCH 1284/1722] start --- .../AuthenticationExample/SigninView.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/AuthenticationExample/AuthenticationExample/SigninView.swift b/AuthenticationExample/AuthenticationExample/SigninView.swift index 3636dc8d0..bd11796a2 100644 --- a/AuthenticationExample/AuthenticationExample/SigninView.swift +++ b/AuthenticationExample/AuthenticationExample/SigninView.swift @@ -29,6 +29,8 @@ struct SigninView: View { /// The portal that the user successfully signed in to. @Binding var portal: Portal? + @State var lastSignedInUser: String? + var body: some View { VStack { Spacer() @@ -47,6 +49,10 @@ struct SigninView: View { .foregroundColor(.secondary) } } + .task { + let arcGISCredential = await ArcGISRuntimeEnvironment.credentialStore.credential(for: .portal) + let networkCredential = await ArcGISRuntimeEnvironment.networkCredentialStore.credential(for: ) + } .padding() } @@ -79,6 +85,27 @@ struct SigninView: View { } } +private extension ArcGISCredential { + var username: String { + switch self { + case .oauth(let credential): + return credential.username + case .token(let credential): + return credential.username + case .staticToken(let credential): + return "" + } + } +} + +private extension NetworkCredential { + var username: String { + switch self { + case + } + } +} + private extension Error { /// Returns a Bool value indicating whether the error is the result of cancelling an /// authentication challenge. From 409789464e1b0a150afb89ab950c6c4b12636f16 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 11:54:31 -0700 Subject: [PATCH 1285/1722] Add remaining cases --- .../UtilityNetworkTraceViewModel.swift | 134 ++++---- .../Extensions/ArcGISFeature.swift | 29 ++ .../UtilityNetworkTraceViewModelTests.swift | 291 +++++++++++++++++- 3 files changed, 384 insertions(+), 70 deletions(-) create mode 100644 Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index a94aba790..a48eb0ecc 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -78,7 +78,9 @@ import SwiftUI /// Starting points programmatically provided to the trace tool. var externalStartingPoints = [UtilityNetworkTraceSimpleStartingPoint]() { didSet { - addExternalStartingPoints() + Task { + await addExternalStartingPoints() + } } } @@ -158,6 +160,7 @@ import SwiftUI if map.utilityNetworks.isEmpty { userWarning = "No utility networks found." } + await addExternalStartingPoints() } /// Selects the next trace from the list of completed traces. @@ -240,9 +243,12 @@ import SwiftUI screenPoint: point, tolerance: 10 ) - identifyLayerResults?.forEach { identifyLayerResult in - identifyLayerResult.geoElements.forEach { geoElement in - setStartingPoint(geoElement: geoElement, mapPoint: mapPoint) + for layerResult in identifyLayerResults ?? [] { + for geoElement in layerResult.geoElements { + await setStartingPoint( + geoElement: geoElement, + mapPoint: mapPoint + ) } } } @@ -254,64 +260,62 @@ import SwiftUI func setStartingPoint( geoElement: GeoElement, mapPoint: Point? = nil - ) { - Task { - guard let feature = geoElement as? ArcGISFeature, - let globalid = feature.attributes["globalid"] as? UUID else { - userWarning = "Element could not be identified" - return - } - - // Block duplicate starting point selection - guard !pendingTrace.startingPoints.contains(where: { startingPoint in - return startingPoint.utilityElement.globalID == globalid - }) else { - userWarning = "Duplicate starting points cannot be added" - return - } - - guard let network = self.network, - let geometry = feature.geometry, - let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? - .renderer? - .symbol(for: feature)? - .makeSwatch(scale: 1.0), - let utilityElement = network.createElement(arcGISFeature: feature) else { return } - - if utilityElement.networkSource.kind == .edge && geometry is Polyline { - if let mapPoint = mapPoint { - utilityElement.fractionAlongEdge = fractionAlongEdge( - of: geometry, - at: mapPoint - ) - } else { - utilityElement.fractionAlongEdge = 0.5 - } - - } else if utilityElement.networkSource.kind == .junction && - utilityElement.assetType.terminalConfiguration?.terminals.count ?? 0 > 1 { - utilityElement.terminal = utilityElement.assetType.terminalConfiguration?.terminals.first - } - - let graphic = Graphic( - geometry: mapPoint ?? feature.geometry?.extent.center, - symbol: SimpleMarkerSymbol( - style: .cross, - color: UIColor(self.pendingTrace.color), - size: 20 + ) async { + guard let feature = geoElement as? ArcGISFeature, + let globalid = feature.globalID else { + userWarning = "Element could not be identified" + return + } + + // Block duplicate starting point selection + guard !pendingTrace.startingPoints.contains(where: { startingPoint in + return startingPoint.utilityElement.globalID == globalid + }) else { + userWarning = "Duplicate starting points cannot be added" + return + } + + guard let network = self.network, + let geometry = feature.geometry, + let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? + .renderer? + .symbol(for: feature)? + .makeSwatch(scale: 1.0), + let utilityElement = network.createElement(arcGISFeature: feature) else { return } + + if utilityElement.networkSource.kind == .edge && geometry is Polyline { + if let mapPoint = mapPoint { + utilityElement.fractionAlongEdge = fractionAlongEdge( + of: geometry, + at: mapPoint ) - ) + } else { + utilityElement.fractionAlongEdge = 0.5 + } - let startingPoint = UtilityNetworkTraceStartingPoint( - extent: geometry.extent, - geoElement: geoElement, - graphic: graphic, - image: symbol, - utilityElement: utilityElement - ) - graphicsOverlay.addGraphic(graphic) - pendingTrace.startingPoints.append(startingPoint) + } else if utilityElement.networkSource.kind == .junction && + utilityElement.assetType.terminalConfiguration?.terminals.count ?? 0 > 1 { + utilityElement.terminal = utilityElement.assetType.terminalConfiguration?.terminals.first } + + let graphic = Graphic( + geometry: mapPoint ?? feature.geometry?.extent.center, + symbol: SimpleMarkerSymbol( + style: .cross, + color: UIColor(self.pendingTrace.color), + size: 20 + ) + ) + + let startingPoint = UtilityNetworkTraceStartingPoint( + extent: geometry.extent, + geoElement: geoElement, + graphic: graphic, + image: symbol, + utilityElement: utilityElement + ) + graphicsOverlay.addGraphic(graphic) + pendingTrace.startingPoints.append(startingPoint) } func setTerminalConfigurationFor( @@ -414,7 +418,7 @@ import SwiftUI completedTraces.append(pendingTrace) selectedTraceIndex = completedTraces.count - 1 pendingTrace = Trace() - addExternalStartingPoints() + await addExternalStartingPoints() return true } @@ -430,11 +434,11 @@ import SwiftUI // MARK: Private Methods /// Adds programatic starting points to the pending trace. - private func addExternalStartingPoints() { - externalStartingPoints.forEach { - setStartingPoint( - geoElement: $0.geoElement, - mapPoint: $0.point + private func addExternalStartingPoints() async { + for startingPoint in externalStartingPoints { + await setStartingPoint( + geoElement: startingPoint.geoElement, + mapPoint: startingPoint.point ) } } diff --git a/Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift b/Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift new file mode 100644 index 000000000..0e84c8b71 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift @@ -0,0 +1,29 @@ +// 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 +import Foundation + + +extension ArcGISFeature { + /// A case invariant method of accessing the global ID attribute. + var globalID: UUID? { + if let id = attributes["globalid"] as? UUID { + return id + } else if let id = attributes["GLOBALID"] as? UUID { + return id + } else { + return nil + } + } +} diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index d92490671..dec2c932d 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -151,15 +151,296 @@ import XCTest ) } - func testCase_2_1() {} + func testCase_2_1() async throws { + let serverPassword: String? = nil + try XCTSkipIf(serverPassword == nil) + + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + + let token = try await ArcGISCredential.token( + url: .rt_server109, + username: "publisher1", + password: serverPassword! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + + guard let map = await makeMapWith(url: .rt_server109) else { + XCTFail() + return + } + + let layer = try XCTUnwrap(map.operationalLayers.first { + $0.name == "ElecDist Device" + } as? FeatureLayer) + + let parameters = QueryParameters() + parameters.addObjectId(171) + + let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let features = try XCTUnwrap(result?.features().compactMap { $0 }) + + XCTAssertEqual(features.count, 1) + + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [ + UtilityNetworkTraceSimpleStartingPoint(geoElement: features.first!) + ], + autoLoad: false + ) + + await viewModel.load() + + let startingPoints = viewModel.pendingTrace.startingPoints + + XCTAssertNil(viewModel.pendingTrace.configuration) + XCTAssertFalse(viewModel.canRunTrace) + XCTAssertEqual(startingPoints.count, 1) + XCTAssertEqual( + startingPoints.first?.utilityElement.networkSource.name, + "ElecDist Device" + ) + XCTAssertEqual( + startingPoints.first?.utilityElement.assetGroup.name, + "ServicePoint" + ) + } - func testCase_2_2() {} + func testCase_2_2() async throws { + let serverPassword: String? = nil + try XCTSkipIf(serverPassword == nil) + + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + + let token = try await ArcGISCredential.token( + url: .rt_server109, + username: "publisher1", + password: serverPassword! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + + guard let map = await makeMapWith(url: .rt_server109) else { + XCTFail() + return + } + + let layer = try XCTUnwrap(map.operationalLayers.first { + $0.name == "ElecDist Device" + } as? FeatureLayer) + + let parameters = QueryParameters() + parameters.addObjectId(463) + + let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let features = try XCTUnwrap(result?.features().compactMap { $0 }) + + XCTAssertEqual(features.count, 1) + + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [ + UtilityNetworkTraceSimpleStartingPoint(geoElement: features.first!) + ], + autoLoad: false + ) + + await viewModel.load() + + XCTAssertEqual(viewModel.pendingTrace.startingPoints.count, 1) + XCTAssertFalse(viewModel.canRunTrace) + + let terminal = try XCTUnwrap(viewModel.pendingTrace.startingPoints.first?.utilityElement.assetType.terminalConfiguration?.terminals.first { $0.name == "Low" }) + + let configuration = try XCTUnwrap( viewModel.configurations.first { + $0.name == "ConnectedWithResultTypes" + }) + + viewModel.setTerminalConfigurationFor(startingPoint: viewModel.pendingTrace.startingPoints.first!, to: terminal) + + viewModel.setPendingTrace(configuration: configuration) + + XCTAssertTrue(viewModel.canRunTrace) + + let succeeded = await viewModel.trace() + + XCTAssertTrue(succeeded) + XCTAssertEqual(viewModel.completedTraces.count, 1) + XCTAssertFalse(viewModel.canRunTrace) + } - func testCase_2_3() {} + func testCase_2_3() async throws { + let serverPassword: String? = nil + try XCTSkipIf(serverPassword == nil) + + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + + let token = try await ArcGISCredential.token( + url: .rt_server109, + username: "publisher1", + password: serverPassword! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + + guard let map = await makeMapWith(url: .rt_server109) else { + XCTFail() + return + } + + let layer = try XCTUnwrap(map.operationalLayers.first { + $0.name == "ElecDist Line" + } as? FeatureLayer) + + let parameters = QueryParameters() + parameters.addObjectId(177) + + let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let features = try XCTUnwrap(result?.features().compactMap { $0 }) + + XCTAssertEqual(features.count, 1) + + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [ + UtilityNetworkTraceSimpleStartingPoint(geoElement: features.first!) + ], + autoLoad: false + ) + + await viewModel.load() + + XCTAssertEqual(viewModel.pendingTrace.startingPoints.count, 1) + + let extent1 = try XCTUnwrap(viewModel.pendingTrace.startingPoints.first?.graphic.geometry?.extent) + viewModel.setFractionAlongEdgeFor( + startingPoint: viewModel.pendingTrace.startingPoints.first!, + to: 0.789 + ) + let extent2 = try XCTUnwrap(viewModel.pendingTrace.startingPoints.first?.graphic.geometry?.extent) + + XCTAssertNotEqual( + extent1.center, + extent2.center + ) + } - func testCase_3_1() {} + func testCase_3_1() async throws { + let serverPassword: String? = nil + try XCTSkipIf(serverPassword == nil) + + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + + let token = try await ArcGISCredential.token( + url: .rt_server109, + username: "publisher1", + password: serverPassword! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + + guard let map = await makeMapWith(url: .rt_server109) else { + XCTFail() + return + } + + let layer = try XCTUnwrap(map.operationalLayers.first { + $0.name == "ElecDist Device" + } as? FeatureLayer) + + let parameters = QueryParameters() + parameters.addObjectId(171) + + let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let features = try XCTUnwrap(result?.features().compactMap { $0 }) + + XCTAssertEqual(features.count, 1) + + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [ + UtilityNetworkTraceSimpleStartingPoint(geoElement: features.first!) + ], + autoLoad: false + ) + + await viewModel.load() + + let configuration = try XCTUnwrap( viewModel.configurations.first { + $0.name == "ConnectedWithFunction" + }) + + viewModel.setPendingTrace(configuration: configuration) + + XCTAssertEqual(viewModel.pendingTrace.startingPoints.count, 1) + XCTAssertTrue(viewModel.canRunTrace) + + let success = await viewModel.trace() + let functionOutput = try XCTUnwrap( viewModel.completedTraces.first?.functionOutputs.first) + + XCTAssertTrue(success) + XCTAssertFalse(viewModel.canRunTrace) + XCTAssertEqual(viewModel.completedTraces.first?.functionOutputs.count, 1) + XCTAssertEqual(functionOutput.function.functionType, .add) + XCTAssertEqual(functionOutput.function.networkAttribute?.name, "Shape length") + } - func testCase_3_2() {} + func testCase_3_2() async throws { + let serverPassword: String? = nil + try XCTSkipIf(serverPassword == nil) + + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + + let token = try await ArcGISCredential.token( + url: .rt_server109, + username: "publisher1", + password: serverPassword! + ) + await ArcGISRuntimeEnvironment.credentialStore.add(token) + + guard let map = await makeMapWith(url: .rt_server109) else { + XCTFail() + return + } + + let layer = try XCTUnwrap(map.operationalLayers.first { + $0.name == "ElecDist Device" + } as? FeatureLayer) + + let parameters = QueryParameters() + parameters.addObjectId(171) + + let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let features = try XCTUnwrap(result?.features().compactMap { $0 }) + + XCTAssertEqual(features.count, 1) + + let viewModel = UtilityNetworkTraceViewModel( + map: map, + graphicsOverlay: GraphicsOverlay(), + startingPoints: [ + UtilityNetworkTraceSimpleStartingPoint(geoElement: features.first!) + ], + autoLoad: false + ) + + await viewModel.load() + + XCTAssertEqual( + viewModel.network?.name, + "L23UtilityNetwork_Utility_Network Utility Network" + ) + + let configuration = try XCTUnwrap( viewModel.configurations.first { + $0.name == "ConnectedWithFunction" + }) + viewModel.setPendingTrace(configuration: configuration) + + let success = await viewModel.trace() + + XCTAssertTrue(success) + } } extension UtilityNetworkTraceViewModelTests { From 207416c36a3642bf2f99d86f574ceb4e65391bdb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 16:11:56 -0700 Subject: [PATCH 1286/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index dec2c932d..568c42183 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -19,8 +19,12 @@ import XCTest /// - See Also: [Test Design](https://devtopia.esri.com/runtime/common-toolkit/blob/main/designs/UtilityNetworkTraceTool/UtilityNetworkTraceTool_Test_Design.md) @MainActor final class UtilityNetworkTraceViewModelTests: XCTestCase { + + private let apiKey = APIKey("<#API Key#>") + private let passwordFor_rt_server109 = <#Password#> + override func setUpWithError() throws { - ArcGISRuntimeEnvironment.apiKey = APIKey("<#API Key#>") + ArcGISRuntimeEnvironment.apiKey = apiKey try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) } @@ -485,3 +489,13 @@ private extension URL { static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! } + +private extension ArcGISCredential { + static func rt_server109() async throws -> ArcGISCredential { + return try await ArcGISCredential.token( + url: URL.rt_server109, + username: "publisher1", + password: passwordFor_rt_server109 + ) + } +} From c57e8e303d8a4282bd234c7a215161c1cfdbc628 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 26 Jul 2022 17:18:21 -0600 Subject: [PATCH 1287/1722] work on getting username --- .../AuthenticationApp.swift | 9 +++- .../AuthenticationExample/SigninView.swift | 48 ++++++++++++++----- .../Components/Authentication/Login.swift | 2 +- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 430242e2a..65fc0d4a9 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -18,6 +18,7 @@ import ArcGIS @main struct AuthenticationApp: App { @ObservedObject var authenticator: Authenticator + @State var isSettingUp = true init() { // Create an authenticator. @@ -34,7 +35,11 @@ struct AuthenticationApp: App { var body: some SwiftUI.Scene { WindowGroup { NavigationView { - HomeView() + if isSettingUp { + ProgressView() + } else { + HomeView() + } } // Using this view modifier will cause a prompt when the authenticator is asked // to handle an authentication challenge. @@ -45,12 +50,14 @@ struct AuthenticationApp: App { .authenticator(authenticator) .environmentObject(authenticator) .task { + isSettingUp = true // Here we make the authenticator persistent, which means that it will synchronize // with they keychain for storing credentials. // It also means that a user can sign in without having to be promped for // credentials. Once credentials are cleared from the stores ("sign-out"), // then the user will need to be prompted once again. try? await authenticator.makePersistent(access: .whenUnlockedThisDeviceOnly) + isSettingUp = false } } } diff --git a/AuthenticationExample/AuthenticationExample/SigninView.swift b/AuthenticationExample/AuthenticationExample/SigninView.swift index bd11796a2..826ae35a7 100644 --- a/AuthenticationExample/AuthenticationExample/SigninView.swift +++ b/AuthenticationExample/AuthenticationExample/SigninView.swift @@ -14,6 +14,7 @@ import SwiftUI import ArcGIS import ArcGISToolkit +import CryptoKit /// A view that allows the user to sign in to a portal. struct SigninView: View { @@ -50,17 +51,33 @@ struct SigninView: View { } } .task { - let arcGISCredential = await ArcGISRuntimeEnvironment.credentialStore.credential(for: .portal) - let networkCredential = await ArcGISRuntimeEnvironment.networkCredentialStore.credential(for: ) + if let arcGISCredential = await ArcGISRuntimeEnvironment.credentialStore.credential(for: .portal) { + lastSignedInUser = arcGISCredential.username ?? "" + } +// else if let networkCredential = await ArcGISRuntimeEnvironment.networkCredentialStore.credential(for: ) { +// +// } } .padding() } + var signInButtonText: String { + if let lastSignedInUser = lastSignedInUser { + if lastSignedInUser.isEmpty { + return "Sign in again" + } else { + return "Sign in with \(lastSignedInUser)" + } + } else { + return "Sign in" + } + } + var signInButton: some View { Button { signIn() } label: { - Text("Sign in") + Text(signInButtonText) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) @@ -86,22 +103,27 @@ struct SigninView: View { } private extension ArcGISCredential { - var username: String { - switch self { - case .oauth(let credential): - return credential.username - case .token(let credential): - return credential.username - case .staticToken(let credential): - return "" + 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 NetworkCredential { - var username: String { + var username: String? { switch self { - case + case .serverTrust, .certificate: + return nil + case .password(let credential): + return credential.username } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 8509c31c2..b86f76eab 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -112,7 +112,7 @@ extension LoginViewModifier { onSignIn: { loginCredential in challenge.resume( with: .useCredential( - .login(username: loginCredential.username, password: loginCredential.password) + .password(username: loginCredential.username, password: loginCredential.password) ) ) }, From da7c9cb8de6b370e20389cac99a6b4e4e44182c5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 26 Jul 2022 16:29:07 -0700 Subject: [PATCH 1288/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 136 +++++++----------- 1 file changed, 54 insertions(+), 82 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 568c42183..042a643c7 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -21,11 +21,13 @@ import XCTest @MainActor final class UtilityNetworkTraceViewModelTests: XCTestCase { private let apiKey = APIKey("<#API Key#>") - private let passwordFor_rt_server109 = <#Password#> + private let passwordFor_rtc_100_8: String? = nil + private let passwordFor_rt_server109: String? = nil + private let passwordFor_sampleServer7: String? = nil override func setUpWithError() throws { ArcGISRuntimeEnvironment.apiKey = apiKey - try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) + try XCTSkipIf(apiKey == .placeholder) } func tearDownWithError() async throws { @@ -53,14 +55,12 @@ import XCTest } func testCase_1_2() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - let token = try await ArcGISCredential.token( - url: .sampleServer7, - username: "viewer01", - password: serverPassword! + try XCTSkipIf(passwordFor_sampleServer7 == nil) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.sampleServer7.host!])) + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_sampleServer7() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) + let map = await makeMapWithNoUtilityNetworks() map.addUtilityNetwork( await makeNetworkWith(url: .sampleServer7) @@ -84,17 +84,11 @@ import XCTest } func testCase_1_3() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - + try XCTSkipIf(passwordFor_rtc_100_8 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtc_100_8.host!])) - - let token = try await ArcGISCredential.token( - url: .rtc_100_8, - username: "publisher1", - password: serverPassword! + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_rtc_100_8() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) guard let map = Map(url: .rtc_100_8) else { XCTFail("Failed to load map") @@ -120,17 +114,11 @@ import XCTest } func testCase_1_4() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - + try XCTSkipIf(passwordFor_rt_server109 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) - - let token = try await ArcGISCredential.token( - url: .rt_server109, - username: "publisher1", - password: serverPassword! + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_rt_server109() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) guard let map = Map(url: .rt_server109) else { XCTFail("Failed to load map") @@ -156,17 +144,11 @@ import XCTest } func testCase_2_1() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - + try XCTSkipIf(passwordFor_rt_server109 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) - - let token = try await ArcGISCredential.token( - url: .rt_server109, - username: "publisher1", - password: serverPassword! + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_rt_server109() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) guard let map = await makeMapWith(url: .rt_server109) else { XCTFail() @@ -212,17 +194,11 @@ import XCTest } func testCase_2_2() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - + try XCTSkipIf(passwordFor_rt_server109 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) - - let token = try await ArcGISCredential.token( - url: .rt_server109, - username: "publisher1", - password: serverPassword! + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_rt_server109() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) guard let map = await makeMapWith(url: .rt_server109) else { XCTFail() @@ -275,17 +251,11 @@ import XCTest } func testCase_2_3() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - + try XCTSkipIf(passwordFor_rt_server109 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) - - let token = try await ArcGISCredential.token( - url: .rt_server109, - username: "publisher1", - password: serverPassword! + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_rt_server109() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) guard let map = await makeMapWith(url: .rt_server109) else { XCTFail() @@ -331,17 +301,11 @@ import XCTest } func testCase_3_1() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - + try XCTSkipIf(passwordFor_rt_server109 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) - - let token = try await ArcGISCredential.token( - url: .rt_server109, - username: "publisher1", - password: serverPassword! + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_rt_server109() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) guard let map = await makeMapWith(url: .rt_server109) else { XCTFail() @@ -391,17 +355,11 @@ import XCTest } func testCase_3_2() async throws { - let serverPassword: String? = nil - try XCTSkipIf(serverPassword == nil) - + try XCTSkipIf(passwordFor_rt_server109 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) - - let token = try await ArcGISCredential.token( - url: .rt_server109, - username: "publisher1", - password: serverPassword! + await ArcGISRuntimeEnvironment.credentialStore.add( + try await tokenFor_rt_server109() ) - await ArcGISRuntimeEnvironment.credentialStore.add(token) guard let map = await makeMapWith(url: .rt_server109) else { XCTFail() @@ -480,6 +438,30 @@ extension UtilityNetworkTraceViewModelTests { } return network } + + func tokenFor_rtc_100_8() async throws -> ArcGISCredential { + return try await ArcGISCredential.token( + url: URL.rtc_100_8, + username: "publisher1", + password: passwordFor_rtc_100_8! + ) + } + + func tokenFor_rt_server109() async throws -> ArcGISCredential { + return try await ArcGISCredential.token( + url: URL.rt_server109, + username: "publisher1", + password: passwordFor_rt_server109! + ) + } + + func tokenFor_sampleServer7() async throws -> ArcGISCredential { + return try await ArcGISCredential.token( + url: URL.sampleServer7, + username: "viewer01", + password: passwordFor_sampleServer7! + ) + } } private extension URL { @@ -489,13 +471,3 @@ private extension URL { static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! } - -private extension ArcGISCredential { - static func rt_server109() async throws -> ArcGISCredential { - return try await ArcGISCredential.token( - url: URL.rt_server109, - username: "publisher1", - password: passwordFor_rt_server109 - ) - } -} From fe8386c3ceedad9f901a69a83874ef97b5ff8386 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 26 Jul 2022 17:56:07 -0600 Subject: [PATCH 1289/1722] last signed in user --- .../AuthenticationExample/SigninView.swift | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/SigninView.swift b/AuthenticationExample/AuthenticationExample/SigninView.swift index 826ae35a7..e93b8e3bb 100644 --- a/AuthenticationExample/AuthenticationExample/SigninView.swift +++ b/AuthenticationExample/AuthenticationExample/SigninView.swift @@ -30,6 +30,10 @@ struct SigninView: View { /// The portal that the user successfully signed in to. @Binding var portal: Portal? + /// The last signed in user name. If the property is `nil` then there was no previous effective + /// credential. If the property is non-nil and empty, then the previously persisted and effective + /// credential did not have a username. If the propert is non-nil and non-empty, then it contains + /// the previously used and persisted username. @State var lastSignedInUser: String? var body: some View { @@ -51,12 +55,28 @@ struct SigninView: View { } } .task { + guard lastSignedInUser == nil else { + return + } + if let arcGISCredential = await ArcGISRuntimeEnvironment.credentialStore.credential(for: .portal) { lastSignedInUser = arcGISCredential.username ?? "" + } else { + let networkCredentials = await ArcGISRuntimeEnvironment.networkCredentialStore.credentials(forHost: URL.portal.host!) + if !networkCredentials.isEmpty { + lastSignedInUser = networkCredentials.compactMap { credential in + switch credential { + case .password(let passwordCredential): + return passwordCredential.username + case .certificate: + return "" + case .serverTrust: + return nil + } + } + .first + } } -// else if let networkCredential = await ArcGISRuntimeEnvironment.networkCredentialStore.credential(for: ) { -// -// } } .padding() } @@ -64,11 +84,15 @@ struct SigninView: View { var signInButtonText: String { if let lastSignedInUser = lastSignedInUser { if lastSignedInUser.isEmpty { + // Non-nil but empty, can't offer the username. return "Sign in again" } else { + // Non-nil and non-empty, show the username in the button text. return "Sign in with \(lastSignedInUser)" } } else { + // For nil username, then just use default text that implies there was no previous + // used credential persisted. return "Sign in" } } From 8438b5387128385c25df0eb2dcddebba630900e5 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 26 Jul 2022 17:57:09 -0600 Subject: [PATCH 1290/1722] add doc --- AuthenticationExample/AuthenticationExample/SigninView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AuthenticationExample/AuthenticationExample/SigninView.swift b/AuthenticationExample/AuthenticationExample/SigninView.swift index e93b8e3bb..6e04e79ad 100644 --- a/AuthenticationExample/AuthenticationExample/SigninView.swift +++ b/AuthenticationExample/AuthenticationExample/SigninView.swift @@ -127,6 +127,7 @@ struct SigninView: View { } private extension ArcGISCredential { + /// The username, if any, associated with this credential. var username: String? { get { switch self { @@ -142,6 +143,7 @@ private extension ArcGISCredential { } private extension NetworkCredential { + /// The username, if any, associated with this credential. var username: String? { switch self { case .serverTrust, .certificate: From 7499879dd4909b4d2c83fac63f930168d3f59f10 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 27 Jul 2022 07:51:12 -0600 Subject: [PATCH 1291/1722] Update AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj Co-authored-by: David Feinzimer --- .../AuthenticationExample.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj b/AuthenticationExample/AuthenticationExample.xcodeproj/project.pbxproj index 917f59a2e..bcd419cde 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-runtime-toolkit-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "arcgis-runtime-toolkit-swift"; path = "../../../../Source/ArcGIS/arcgis-runtime-toolkit-swift"; sourceTree = ""; }; + 88AD13862834355C00500B2E /* arcgis-runtime-toolkit-swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "arcgis-runtime-toolkit-swift"; 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 = ""; }; From ab66aa6a3d89a6994ab6ec0ddbee5424fc2e831a Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 09:11:56 -0600 Subject: [PATCH 1292/1722] fix warnings --- Sources/ArcGISToolkit/Components/Popups/PopupView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 1a4e64edf..265b18ba9 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -71,15 +71,15 @@ public struct PopupView: View { VStack(alignment: .leading) { ForEach(popup.evaluatedElements) { popupElement in switch popupElement { - case let popupElement as AttachmentsPopupElement: + case is AttachmentsPopupElement: Text("AttachmentsPopupElementView implementation coming soon.") .font(.caption) case let popupElement as FieldsPopupElement: FieldsPopupElementView(popupElement: popupElement) - case let popupElement as MediaPopupElement: + case is MediaPopupElement: Text("MediaPopupElementView implementation coming soon.") .font(.caption) - case let popupElement as TextPopupElement: + case is TextPopupElement: Text("TextPopupElementView implementation coming soon.") .font(.caption) default: From 93059fa6c03c9ff71ec7dadee5736f29d3befbef Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 27 Jul 2022 10:50:13 -0500 Subject: [PATCH 1293/1722] Updates to size calculation for HTMLTxtView. --- .../Components/Popups/HTMLTextView.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index dc405d102..97314be15 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -49,6 +49,8 @@ struct HTMLTextView: UIViewRepresentable { class Coordinator: NSObject, WKNavigationDelegate { var parent: HTMLTextView + private var runDidFinishAgain = true + init(_ parent: HTMLTextView) { self.parent = parent } @@ -74,6 +76,17 @@ struct HTMLTextView: UIViewRepresentable { // WKNavigationDelegate method where the size calculation happens. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){ self.parent.dynamicHeight = webView.scrollView.contentSize.height + + // Sometimes the contentSize has not been updated yet, probably + // because the view has not been rendered yet. So rerun this again + // after a delay. This fixes the issue. + if runDidFinishAgain { + runDidFinishAgain = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + self?.webView(webView, didFinish: navigation) + self?.runDidFinishAgain = true + } + } } } From cfe63f3896fafb1487d8ee262f49a60c99cbd7e2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 09:29:52 -0700 Subject: [PATCH 1294/1722] Update UtilityNetworkTraceViewModelTests.swift --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a92a30db8..c689e46cd 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ To use Toolkit in your project: * **[OverviewMap](Documentation/OverviewMap)** - Displays the visible extent of a geo view in a small "inset" map. * **[Scalebar](Documentation/Scalebar)** - Displays current scale reference. * **[Search](Documentation/Search)** - Displays a search experience for geo views. +* **[UtilityNetworkTrace](Documentation/UtilityNetworkTrace)** - Run traces on a web map published with a utility network and trace configurations. ## Requirements From f877ff741b0950fe683fc9eacdd91c2dcbe55fb0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 09:37:03 -0700 Subject: [PATCH 1295/1722] Add doc --- Documentation/README.md | 1 + Documentation/UtilityNetworkTrace/README.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 Documentation/UtilityNetworkTrace/README.md diff --git a/Documentation/README.md b/Documentation/README.md index aa040934e..4b36e3aac 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -8,3 +8,4 @@ * **[Overview Map](OverviewMap)** - Displays an "overview" (or "inset") map on top of an existing map or scene view. * **[Scalebar](Scalebar)** - Displays current scale reference. * **[Search](Search)** - Displays a search experience for geo views. +* **[UtilityNetworkTrace](UtilityNetworkTrace)** - Run traces on a web map published with a utility network and trace configurations. diff --git a/Documentation/UtilityNetworkTrace/README.md b/Documentation/UtilityNetworkTrace/README.md new file mode 100644 index 000000000..e7107af8e --- /dev/null +++ b/Documentation/UtilityNetworkTrace/README.md @@ -0,0 +1,17 @@ +# UtilityNetworkTrace + +`UtilityNetworkTrace` runs traces on a web map published with a utility network and trace configurations. + +## Features + +## Key properties + +## Behavior: + +## Usage + +```swift +} +``` + +To see the `UtilityNetworkTrace` in action, check out the [Examples](../../Examples) and refer to [UtilityNetworkTraceExampleView.swift](../../Examples/Examples/UtilityNetworkTraceExampleView.swift) in the project. From a31d826c3b845865cd94694d573cfac6d6875a7b Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 27 Jul 2022 10:40:22 -0600 Subject: [PATCH 1296/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../AuthenticationExample/AuthenticationApp.swift | 2 +- .../AuthenticationExample/FeaturedMapsView.swift | 2 +- .../AuthenticationExample/PortalItemsView.swift | 2 ++ AuthenticationExample/AuthenticationExample/ProfileView.swift | 2 +- AuthenticationExample/AuthenticationExample/SigninView.swift | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 430242e2a..25368315f 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -56,7 +56,7 @@ struct AuthenticationApp: App { } } -// If want to use OAuth, you can uncomment this code: +// If you want to use OAuth, you can uncomment this code: //private extension OAuthConfiguration { // static let arcgisDotCom = OAuthConfiguration( // portalURL: .portal, diff --git a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift index a2c6f2436..73af71957 100644 --- a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift +++ b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift @@ -52,7 +52,7 @@ struct FeaturedMapsView: View { } } -/// A view that displays information about a portal item for viewing that informationwithin a list. +/// A view that displays information about a portal item for viewing that information within a list. struct PortalItemView: View { /// The portal item to display information about. var item: PortalItem diff --git a/AuthenticationExample/AuthenticationExample/PortalItemsView.swift b/AuthenticationExample/AuthenticationExample/PortalItemsView.swift index f3d113458..452635fbf 100644 --- a/AuthenticationExample/AuthenticationExample/PortalItemsView.swift +++ b/AuthenticationExample/AuthenticationExample/PortalItemsView.swift @@ -18,7 +18,9 @@ import ArcGISToolkit /// A view that displays portal items. struct FeaturedMapsView: View { var portal: Portal + @State var isLoading = true + @State var featuredItems = [PortalItem]() var body: some View { diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift index dd8257fca..a2f263e41 100644 --- a/AuthenticationExample/AuthenticationExample/ProfileView.swift +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -27,7 +27,7 @@ struct ProfileView: View { @State var isSigningOut: Bool = false /// The closure to call once the user has signed out. - var signoutAction: () -> Void + var signOutAction: () -> Void var body: some View { VStack { diff --git a/AuthenticationExample/AuthenticationExample/SigninView.swift b/AuthenticationExample/AuthenticationExample/SigninView.swift index 3636dc8d0..f5acb3887 100644 --- a/AuthenticationExample/AuthenticationExample/SigninView.swift +++ b/AuthenticationExample/AuthenticationExample/SigninView.swift @@ -80,7 +80,7 @@ struct SigninView: View { } private extension Error { - /// Returns a Bool value indicating whether the error is the result of cancelling an + /// Returns a Boolean value indicating whether the error is the result of cancelling an /// authentication challenge. var isChallengeCancellationError: Bool { switch self { From 2cd1efe11f24c4ce8eb418b7d166f6d3c96cf629 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 10:41:16 -0600 Subject: [PATCH 1297/1722] pr feedback: circle --- AuthenticationExample/AuthenticationExample/UserView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/AuthenticationExample/AuthenticationExample/UserView.swift b/AuthenticationExample/AuthenticationExample/UserView.swift index 1e3e84ccd..0d4ac7c0d 100644 --- a/AuthenticationExample/AuthenticationExample/UserView.swift +++ b/AuthenticationExample/AuthenticationExample/UserView.swift @@ -25,6 +25,7 @@ struct UserView: View { if let thumbnail = user.thumbnail { LoadableImageView(loadableImage: thumbnail) .frame(width: 100, height: 100, alignment: .center) + .clipShape(Circle()) } else { Image(systemName: "person.circle") .resizable() From 861910de0a507fef746f075d6c1cfed2386f2d12 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 10:41:58 -0600 Subject: [PATCH 1298/1722] pr feedback --- AuthenticationExample/AuthenticationExample/HomeView.swift | 2 +- AuthenticationExample/AuthenticationExample/ProfileView.swift | 2 +- .../{SigninView.swift => SignInView.swift} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename AuthenticationExample/AuthenticationExample/{SigninView.swift => SignInView.swift} (99%) diff --git a/AuthenticationExample/AuthenticationExample/HomeView.swift b/AuthenticationExample/AuthenticationExample/HomeView.swift index ecefb3d6b..3befeef45 100644 --- a/AuthenticationExample/AuthenticationExample/HomeView.swift +++ b/AuthenticationExample/AuthenticationExample/HomeView.swift @@ -40,7 +40,7 @@ struct HomeView: View { } } } else { - SigninView(portal: $portal) + SignInView(portal: $portal) } } } diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift index a2f263e41..c89fa9c21 100644 --- a/AuthenticationExample/AuthenticationExample/ProfileView.swift +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -63,7 +63,7 @@ struct ProfileView: View { Task { await authenticator.clearCredentialStores() isSigningOut = false - signoutAction() + signOutAction() } } } diff --git a/AuthenticationExample/AuthenticationExample/SigninView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift similarity index 99% rename from AuthenticationExample/AuthenticationExample/SigninView.swift rename to AuthenticationExample/AuthenticationExample/SignInView.swift index f5acb3887..2506d6dfe 100644 --- a/AuthenticationExample/AuthenticationExample/SigninView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -16,7 +16,7 @@ import ArcGIS import ArcGISToolkit /// A view that allows the user to sign in to a portal. -struct SigninView: View { +struct SignInView: View { /// The authenticator which has been passed from the app through the environment. @EnvironmentObject var authenticator: Authenticator From 4e0e6fe1549dbe7e10969fda6cd3b3c896c542b8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 09:43:08 -0700 Subject: [PATCH 1299/1722] Remove programatic starting points from sample --- .../UtilityNetworkTraceExampleView.swift | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 22e266db7..fbc87f9bb 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -33,9 +33,6 @@ struct UtilityNetworkTraceExampleView: View { /// A container for graphical trace results. @State var resultGraphicsOverlay = GraphicsOverlay() - /// Optional pre-defined starting points for the utility network trace. - @State var startingPoints: [UtilityNetworkTraceSimpleStartingPoint] = [] - /// The map viewpoint used by the `UtilityNetworkTrace` to pan/zoom the map to selected features. @State var viewpoint: Viewpoint? @@ -62,8 +59,7 @@ struct UtilityNetworkTraceExampleView: View { mapPoint: $mapPoint, viewPoint: $viewPoint, mapViewProxy: $mapViewProxy, - viewpoint: $viewpoint, - startingPoints: $startingPoints + viewpoint: $viewpoint ) .task { await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) @@ -72,17 +68,6 @@ struct UtilityNetworkTraceExampleView: View { .padding() .frame(width: 360) } - .overlay(alignment: .topLeading) { - Button { - Task { - await setPredefinedStartingPoints() - } - } label: { - Text("Set predefined starting points") - } - .buttonStyle(.borderedProminent) - .padding() - } } } @@ -107,27 +92,3 @@ private extension ArcGISCredential { } } } - -extension UtilityNetworkTraceExampleView { - /// Queries the map for a feature with a certain ID and sets the list of starting points. - func setPredefinedStartingPoints() async { - let targetID = UUID(uuidString: "2A6D25D5-8B9E-400A-BC07-4A11BD8B6C82") - let groupLayer = map.operationalLayers.first as? GroupLayer - let parameters = QueryParameters() - parameters.addObjectId(1740) - for layer in groupLayer?.layers ?? [] { - guard let layer = layer as? FeatureLayer, - let table = layer.featureTable else { continue } - let query = try? await table.queryFeatures(parameters: parameters) - query?.features().forEach { element in - if let feature = element as? ArcGISFeature, - let id = feature.attributes["globalid"] as? UUID, - id == targetID { - startingPoints = [ - UtilityNetworkTraceSimpleStartingPoint(geoElement: element) - ] - } - } - } - } -} From a2bf7500ced283547a265c70dbb30d3a09ad95d5 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 27 Jul 2022 10:45:17 -0600 Subject: [PATCH 1300/1722] Update AuthenticationExample/AuthenticationExample/AuthenticationApp.swift Co-authored-by: David Feinzimer --- .../AuthenticationExample/AuthenticationApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 25368315f..ca5644be8 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -47,7 +47,7 @@ struct AuthenticationApp: App { .task { // Here we make the authenticator persistent, which means that it will synchronize // with they keychain for storing credentials. - // It also means that a user can sign in without having to be promped for + // 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.makePersistent(access: .whenUnlockedThisDeviceOnly) From af9e9f8d770e385b13a081cd4f88cb5710e22b6d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 10:45:49 -0600 Subject: [PATCH 1301/1722] pr feedback: extra file --- .../PortalItemsView.swift | 75 ------------------- 1 file changed, 75 deletions(-) delete mode 100644 AuthenticationExample/AuthenticationExample/PortalItemsView.swift diff --git a/AuthenticationExample/AuthenticationExample/PortalItemsView.swift b/AuthenticationExample/AuthenticationExample/PortalItemsView.swift deleted file mode 100644 index 452635fbf..000000000 --- a/AuthenticationExample/AuthenticationExample/PortalItemsView.swift +++ /dev/null @@ -1,75 +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 -import ArcGIS -import ArcGISToolkit - -/// A view that displays portal items. -struct FeaturedMapsView: View { - var portal: Portal - - @State var isLoading = true - - @State var featuredItems = [PortalItem]() - - var body: some View { - VStack { - if isLoading { - ProgressView() - } else { - List(featuredItems) { item in - NavigationLink { - MapItemView(map: Map(item: item)) - } label: { - ItemView(item: item) - } - } - } - } - .task { - guard featuredItems.isEmpty else { return } - do { - featuredItems = try await portal.homePageFeaturedContent - .filter { $0.kind == .webMap } - } catch {} - - isLoading = false - } - .navigationTitle("Featured Maps") - } -} - -struct ItemView: View { - var item: PortalItem - - var body: some View { - HStack { - if let thumbnail = item.thumbnail { - LoadableImageView(loadableImage: thumbnail) - .frame(width: 64, height: 44) - } - VStack(alignment: .leading, spacing: 2) { - Text(item.title) - .font(.headline) - .lineLimit(2) - Text("Owner: \(item.owner), Views: \(item.viewCount)") - .font(.caption) - Text(item.snippet) - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(2) - } - } - } -} From 6885fb838c907b060639cbcf3bfae51ef17b9115 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 09:55:26 -0700 Subject: [PATCH 1302/1722] Update README.md Update README.md --- Documentation/UtilityNetworkTrace/README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Documentation/UtilityNetworkTrace/README.md b/Documentation/UtilityNetworkTrace/README.md index e7107af8e..04716ff92 100644 --- a/Documentation/UtilityNetworkTrace/README.md +++ b/Documentation/UtilityNetworkTrace/README.md @@ -1,17 +1,28 @@ # UtilityNetworkTrace -`UtilityNetworkTrace` runs traces on a web map published with a utility network and trace configurations. +`UtilityNetworkTrace` runs traces on a webmap published with a utility network and trace configurations. ## Features -## Key properties +The utility network trace tool displays a list of named trace configurations defined for utility networks in a web map. It enables users to add starting points and perform trace analysis from the selected named trace configuration. + +A named trace configuration defined for a utility network in a webmap comprises the parameters used for a utility network trace. ## Behavior: +The tool allows users to choose between multiple networks (if more than one is defined in a webmap), choose between named trace configurations, add trace starting points either programmatically or by tapping on a map view and view trace results. + ## Usage ```swift -} +UtilityNetworkTrace( + graphicsOverlay: $resultGraphicsOverlay, + map: map, + mapPoint: $mapPoint, + viewPoint: $viewPoint, + mapViewProxy: $mapViewProxy, + viewpoint: $viewpoint +) ``` To see the `UtilityNetworkTrace` in action, check out the [Examples](../../Examples) and refer to [UtilityNetworkTraceExampleView.swift](../../Examples/Examples/UtilityNetworkTraceExampleView.swift) in the project. From fdd98734b2e8fe0d87d048a8c6274e256ffe546e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 10:01:53 -0700 Subject: [PATCH 1303/1722] Update README.md --- Documentation/UtilityNetworkTrace/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Documentation/UtilityNetworkTrace/README.md b/Documentation/UtilityNetworkTrace/README.md index 04716ff92..111f8c04e 100644 --- a/Documentation/UtilityNetworkTrace/README.md +++ b/Documentation/UtilityNetworkTrace/README.md @@ -10,7 +10,11 @@ A named trace configuration defined for a utility network in a webmap comprises ## Behavior: -The tool allows users to choose between multiple networks (if more than one is defined in a webmap), choose between named trace configurations, add trace starting points either programmatically or by tapping on a map view and view trace results. +The tool allows users to: + - Choose between multiple networks (if more than one is defined in a webmap) + - Choose between named trace configurations + - Add trace starting points either programmatically or by tapping on a map view + - View trace results ## Usage From b0cd81f84196f7c023454edc65c1da5c5f3dca69 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 11:12:19 -0600 Subject: [PATCH 1304/1722] pr feedback --- .../AuthenticationApp.swift | 4 +-- .../AuthenticationExample/HomeView.swift | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index ca5644be8..c3bf7fef2 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -33,9 +33,7 @@ struct AuthenticationApp: App { var body: some SwiftUI.Scene { WindowGroup { - NavigationView { - HomeView() - } + HomeView() // Using this view modifier will cause a prompt when the authenticator is asked // to handle an authentication challenge. // This will handle many different types of authentication, for example: diff --git a/AuthenticationExample/AuthenticationExample/HomeView.swift b/AuthenticationExample/AuthenticationExample/HomeView.swift index 3befeef45..1d91fe654 100644 --- a/AuthenticationExample/AuthenticationExample/HomeView.swift +++ b/AuthenticationExample/AuthenticationExample/HomeView.swift @@ -24,21 +24,24 @@ struct HomeView: View { var body: some View { if let portal = portal { - FeaturedMapsView(portal: portal) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - showProfile = true - } label: { - Image(systemName: "person.circle") + NavigationView{ + FeaturedMapsView(portal: portal) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showProfile = true + } label: { + Image(systemName: "person.circle") + } } } - } - .sheet(isPresented: $showProfile) { - ProfileView(portal: portal) { - self.portal = nil + .sheet(isPresented: $showProfile) { + ProfileView(portal: portal) { + self.portal = nil + } } - } + } + .navigationViewStyle(.stack) } else { SignInView(portal: $portal) } From 2e2ad66bece95d40f7fb11e927a056c6bd48b927 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 12:35:06 -0600 Subject: [PATCH 1305/1722] remove unused file --- .../AuthenticationExample/ItemsView.swift | 73 ------------------- 1 file changed, 73 deletions(-) delete mode 100644 AuthenticationExample/AuthenticationExample/ItemsView.swift diff --git a/AuthenticationExample/AuthenticationExample/ItemsView.swift b/AuthenticationExample/AuthenticationExample/ItemsView.swift deleted file mode 100644 index 047faa50b..000000000 --- a/AuthenticationExample/AuthenticationExample/ItemsView.swift +++ /dev/null @@ -1,73 +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 -import ArcGIS -import ArcGISToolkit - -/// A view that displays portal items. -struct PortalItemsView: View { - var portal: Portal - @State var isLoading = true - @State var featuredItems = [PortalItem]() - - var body: some View { - VStack { - if isLoading { - ProgressView() - } else { - List(featuredItems) { item in - NavigationLink { - MapItemView(map: Map(item: item)) - } label: { - ItemView(item: item) - } - } - } - } - .task { - guard featuredItems.isEmpty else { return } - do { - featuredItems = try await portal.homePageFeaturedContent - .filter { $0.kind == .webMap } - } catch {} - - isLoading = false - } - .navigationTitle("Featured Maps") - } -} - -struct ItemView: View { - var item: PortalItem - - var body: some View { - HStack { - if let thumbnail = item.thumbnail { - LoadableImageView(loadableImage: thumbnail) - .frame(width: 64, height: 44) - } - VStack(alignment: .leading, spacing: 2) { - Text(item.title) - .font(.headline) - .lineLimit(2) - Text("Owner: \(item.owner), Views: \(item.viewCount)") - .font(.caption) - Text(item.snippet) - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(2) - } - } - } -} From 00552f94a8f5412a4c20e06bfa94706de3043eb8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 12:21:29 -0700 Subject: [PATCH 1306/1722] Remove `UtilityNetworkTraceSimpleStartingPoint` --- .../UtilityNetworkTraceExampleView.swift | 4 +- .../UtilityNetworkTrace.swift | 47 +++++++------- ...ilityNetworkTraceSimpleStartingPoint.swift | 44 ------------- .../UtilityNetworkTraceStartingPoint.swift | 39 ++++++++---- .../UtilityNetworkTraceViewModel.swift | 63 +++++++++---------- .../UtilityNetworkTraceViewModelTrace.swift | 2 +- 6 files changed, 85 insertions(+), 114 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 22e266db7..06afb08fa 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -34,7 +34,7 @@ struct UtilityNetworkTraceExampleView: View { @State var resultGraphicsOverlay = GraphicsOverlay() /// Optional pre-defined starting points for the utility network trace. - @State var startingPoints: [UtilityNetworkTraceSimpleStartingPoint] = [] + @State var startingPoints: [UtilityNetworkTraceStartingPoint] = [] /// The map viewpoint used by the `UtilityNetworkTrace` to pan/zoom the map to selected features. @State var viewpoint: Viewpoint? @@ -124,7 +124,7 @@ extension UtilityNetworkTraceExampleView { let id = feature.attributes["globalid"] as? UUID, id == targetID { startingPoints = [ - UtilityNetworkTraceSimpleStartingPoint(geoElement: element) + UtilityNetworkTraceStartingPoint(geoElement: element) ] } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 96f74f7bf..a745b419a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -71,7 +71,7 @@ public struct UtilityNetworkTrace: View { // MARK: Bindings /// Starting points programmatically provided to the trace tool. - @Binding private var externalStartingPoints: [UtilityNetworkTraceSimpleStartingPoint] + @Binding private var externalStartingPoints: [UtilityNetworkTraceStartingPoint] /// The graphics overlay to hold generated starting point and trace graphics. @Binding private var graphicsOverlay: GraphicsOverlay @@ -391,15 +391,15 @@ public struct UtilityNetworkTrace: View { currentActivity = .creatingTrace(.viewingStartingPoints) } makeDetailSectionHeader( - title: selectedStartingPoint?.utilityElement.assetType.name ?? "Unnamed Asset Type" + title: selectedStartingPoint?.utilityElement?.assetType.name ?? "Unnamed Asset Type" ) List { - if selectedStartingPoint?.utilityElement.networkSource.kind == .edge { + if selectedStartingPoint?.utilityElement?.networkSource.kind == .edge { Section("Fraction Along Edge") { Slider(value: Binding(get: { - viewModel.pendingTrace.startingPoints.first { sp in - sp.utilityElement.globalID == selectedStartingPoint?.utilityElement.globalID - }?.utilityElement.fractionAlongEdge ?? .zero + viewModel.pendingTrace.startingPoints.first { + $0 == selectedStartingPoint + }?.utilityElement?.fractionAlongEdge ?? .zero }, set: { newValue in if let selectedStartingPoint = selectedStartingPoint { viewModel.setFractionAlongEdgeFor( @@ -409,21 +409,21 @@ public struct UtilityNetworkTrace: View { } })) } - } else if selectedStartingPoint?.utilityElement.networkSource.kind == .junction && - selectedStartingPoint?.utilityElement.terminal != nil && - !(selectedStartingPoint?.utilityElement.assetType.terminalConfiguration?.terminals.isEmpty ?? true) { + } else if selectedStartingPoint?.utilityElement?.networkSource.kind == .junction && + selectedStartingPoint?.utilityElement?.terminal != nil && + !(selectedStartingPoint?.utilityElement?.assetType.terminalConfiguration?.terminals.isEmpty ?? true) { Section { Picker( "Terminal Configuration", selection: Binding(get: { - selectedStartingPoint!.utilityElement.terminal! + selectedStartingPoint!.utilityElement!.terminal! }, set: { newValue in viewModel.setTerminalConfigurationFor(startingPoint: selectedStartingPoint!, to: newValue) }) ) { - ForEach(viewModel.pendingTrace.startingPoints.first { sp in - sp.utilityElement.globalID == selectedStartingPoint?.utilityElement.globalID - }?.utilityElement.assetType.terminalConfiguration?.terminals ?? [], id: \.self) { + ForEach(viewModel.pendingTrace.startingPoints.first { + $0 == selectedStartingPoint + }?.utilityElement?.assetType.terminalConfiguration?.terminals ?? [], id: \.self) { Text($0.name) } } @@ -439,28 +439,31 @@ public struct UtilityNetworkTrace: View { } } makeZoomToButton { - if let selectedStartingPoint = selectedStartingPoint { - viewpoint = Viewpoint(targetExtent: selectedStartingPoint.extent) + if let selectedStartingPoint = selectedStartingPoint, + let extent = selectedStartingPoint.geoElement.geometry?.extent { + viewpoint = Viewpoint(targetExtent: extent) } } } /// Displays the chosen starting points for the new trace. private var startingPointsList: some View { - ForEach(viewModel.pendingTrace.startingPoints, id: \.utilityElement.globalID) { startingPoint in + ForEach(viewModel.pendingTrace.startingPoints, id: \.self) { startingPoint in Button { currentActivity = .creatingTrace( .inspectingStartingPoint(startingPoint) ) } label: { Label { - Text(startingPoint.utilityElement.assetType.name) + Text(startingPoint.utilityElement?.assetType.name ?? "") .lineLimit(1) } icon: { - Image(uiImage: startingPoint.image) - .frame(width: 25, height: 25) - .background(Color.secondary) - .cornerRadius(5) + if let image = startingPoint.image { + Image(uiImage: image) + .frame(width: 25, height: 25) + .background(Color.secondary) + .cornerRadius(5) + } } } .swipeActions { @@ -492,7 +495,7 @@ public struct UtilityNetworkTrace: View { viewPoint: Binding, mapViewProxy: Binding, viewpoint: Binding, - startingPoints: Binding<[UtilityNetworkTraceSimpleStartingPoint]> = .constant([]) + startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) ) { _viewPoint = viewPoint _mapPoint = mapPoint diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift deleted file mode 100644 index 1de26e54b..000000000 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceSimpleStartingPoint.swift +++ /dev/null @@ -1,44 +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 -import UIKit - -/// A simplified starting point of a utility network trace. -public struct UtilityNetworkTraceSimpleStartingPoint { - /// The underlying geo element. - let geoElement: GeoElement - - /// A unique identifier for the simple starting point. - let id = UUID() - - /// A map point (useful for specifying a fractional starting location along an edge element). - let point: Point? - - /// - Parameters: - /// - geoElement: The underlying geo element. - /// - point: A map point (useful for specifying a fractional starting location along an edge element). - public init( - geoElement: GeoElement, - point: Point? = nil - ) { - self.geoElement = geoElement - self.point = point - } -} - -extension UtilityNetworkTraceSimpleStartingPoint: Equatable { - public static func == (lhs: UtilityNetworkTraceSimpleStartingPoint, rhs: UtilityNetworkTraceSimpleStartingPoint) -> Bool { - lhs.id == rhs.id - } -} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift index df8af957a..d4d13fc77 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceStartingPoint.swift @@ -15,29 +15,42 @@ import ArcGIS import UIKit /// A starting point of a utility network trace. -struct UtilityNetworkTraceStartingPoint { - /// The starting point's geometry. - var extent: Envelope - - /// The underlying geo element. +public struct UtilityNetworkTraceStartingPoint { + /// The geo element to be used as a starting point. var geoElement: GeoElement - /// The generated marker graphic. - var graphic: Graphic + /// The marker graphic added to the map view's graphics overlay. + var graphic: Graphic? + + /// The ID of this starting point. + let id = UUID() /// The symbol of the starting point as shown on the map. - var image: UIImage + var image: UIImage? + + /// A point on the map indicated as the location of this starting point. This can be used to indicate a + /// specific location along an edge element. + var mapPoint: Point? /// The underlying utility element as generated by the utility network. - var utilityElement: UtilityElement + var utilityElement: UtilityElement? + + /// A starting point of a utility network trace. + /// - Parameters: + /// - geoElement: The geo element to be used as a starting point. + /// - mapPoint: A point on the map indicated as the location of this starting point. + public init(geoElement: GeoElement, mapPoint: Point? = nil) { + self.geoElement = geoElement + self.mapPoint = mapPoint + } } extension UtilityNetworkTraceStartingPoint: Hashable { - static func == (lhs: UtilityNetworkTraceStartingPoint, rhs: UtilityNetworkTraceStartingPoint) -> Bool { - lhs.utilityElement.globalID == rhs.utilityElement.globalID + public static func == (lhs: UtilityNetworkTraceStartingPoint, rhs: UtilityNetworkTraceStartingPoint) -> Bool { + lhs.id == rhs.id } - func hash(into hasher: inout Hasher) { - hasher.combine(utilityElement.globalID) + public func hash(into hasher: inout Hasher) { + hasher.combine(id) } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 20a60c367..6ea147a2f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -70,7 +70,7 @@ import SwiftUI private var map: Map /// Starting points programmatically provided to the trace tool. - var externalStartingPoints = [UtilityNetworkTraceSimpleStartingPoint]() { + var externalStartingPoints = [UtilityNetworkTraceStartingPoint]() { didSet { addExternalStartingPoints() } @@ -94,7 +94,7 @@ import SwiftUI init( map: Map, graphicsOverlay: GraphicsOverlay, - startingPoints: [UtilityNetworkTraceSimpleStartingPoint] + startingPoints: [UtilityNetworkTraceStartingPoint] ) { self.map = map self.graphicsOverlay = graphicsOverlay @@ -117,16 +117,18 @@ import SwiftUI /// - Parameter startingPoint: The starting point to be deleted. func delete(_ startingPoint: UtilityNetworkTraceStartingPoint) { pendingTrace.startingPoints.removeAll { - $0.utilityElement.globalID == startingPoint.utilityElement.globalID + $0 == startingPoint + } + if let graphic = startingPoint.graphic { + graphicsOverlay.removeGraphic(graphic) } - graphicsOverlay.removeGraphic(startingPoint.graphic) } /// Deletes all of the completed traces. func deleteAllTraces() { selectedTraceIndex = nil completedTraces.forEach { traceResult in - graphicsOverlay.removeGraphics(traceResult.startingPoints.map { $0.graphic }) + graphicsOverlay.removeGraphics(traceResult.startingPoints.compactMap { $0.graphic }) graphicsOverlay.removeGraphics(traceResult.graphics) } completedTraces.removeAll() @@ -175,11 +177,11 @@ import SwiftUI to newValue: Double ) { pendingTrace.startingPoints.first { - $0.utilityElement.globalID == startingPoint.utilityElement.globalID - }?.utilityElement.fractionAlongEdge = newValue + $0 == startingPoint + }?.utilityElement?.fractionAlongEdge = newValue if let geometry = startingPoint.geoElement.geometry, let polyline = geometry as? Polyline { - startingPoint.graphic.geometry = GeometryEngine.point( + startingPoint.graphic?.geometry = GeometryEngine.point( along: polyline, atDistance: GeometryEngine.length(of: geometry) * newValue ) @@ -226,7 +228,11 @@ import SwiftUI ) identifyLayerResults?.forEach { identifyLayerResult in identifyLayerResult.geoElements.forEach { geoElement in - setStartingPoint(geoElement: geoElement, mapPoint: mapPoint) + let startingPoint = UtilityNetworkTraceStartingPoint( + geoElement: geoElement, + mapPoint: mapPoint + ) + setStartingPoint(startingPoint: startingPoint) } } } @@ -235,12 +241,10 @@ import SwiftUI /// - Parameters: /// - geoElement: An element that corresponds to another within the utility network. /// - mapPoint: A point on the map in map coordinates. - func setStartingPoint( - geoElement: GeoElement, - mapPoint: Point? = nil - ) { + /// - startingPoint: <#startingPoint description#> + func setStartingPoint(startingPoint: UtilityNetworkTraceStartingPoint) { Task { - guard let feature = geoElement as? ArcGISFeature, + guard let feature = startingPoint.geoElement as? ArcGISFeature, let globalid = feature.attributes["globalid"] as? UUID else { userWarning = "Element could not be identified" return @@ -248,7 +252,7 @@ import SwiftUI // Block duplicate starting point selection guard !pendingTrace.startingPoints.contains(where: { startingPoint in - return startingPoint.utilityElement.globalID == globalid + return startingPoint.utilityElement?.globalID == globalid }) else { userWarning = "Duplicate starting points cannot be added" return @@ -263,7 +267,7 @@ import SwiftUI let utilityElement = network.createElement(arcGISFeature: feature) else { return } if utilityElement.networkSource.kind == .edge && geometry is Polyline { - if let mapPoint = mapPoint { + if let mapPoint = startingPoint.mapPoint { utilityElement.fractionAlongEdge = fractionAlongEdge( of: geometry, at: mapPoint @@ -278,7 +282,7 @@ import SwiftUI } let graphic = Graphic( - geometry: mapPoint ?? feature.geometry?.extent.center, + geometry: startingPoint.mapPoint ?? feature.geometry?.extent.center, symbol: SimpleMarkerSymbol( style: .cross, color: UIColor(self.pendingTrace.color), @@ -286,15 +290,13 @@ import SwiftUI ) ) - let startingPoint = UtilityNetworkTraceStartingPoint( - extent: geometry.extent, - geoElement: geoElement, - graphic: graphic, - image: symbol, - utilityElement: utilityElement - ) + var newStartingPoint = startingPoint + newStartingPoint.graphic = graphic + newStartingPoint.image = symbol + newStartingPoint.utilityElement = utilityElement + graphicsOverlay.addGraphic(graphic) - pendingTrace.startingPoints.append(startingPoint) + pendingTrace.startingPoints.append(newStartingPoint) } } @@ -303,8 +305,8 @@ import SwiftUI to newValue: UtilityTerminal ) { pendingTrace.startingPoints.first { - $0.utilityElement.globalID == startingPoint.utilityElement.globalID - }?.utilityElement.terminal = newValue + $0 == startingPoint + }?.utilityElement?.terminal = newValue objectWillChange.send() } @@ -416,10 +418,7 @@ import SwiftUI /// Adds programatic starting points to the pending trace. private func addExternalStartingPoints() { externalStartingPoints.forEach { - setStartingPoint( - geoElement: $0.geoElement, - mapPoint: $0.point - ) + setStartingPoint(startingPoint: $0) } } @@ -433,7 +432,7 @@ import SwiftUI ) { guard index >= 0, index <= completedTraces.count - 1 else { return } _ = completedTraces[index].graphics.map { $0.isSelected = isSelected } - _ = completedTraces[index].startingPoints.map { $0.graphic.isSelected = isSelected } + _ = completedTraces[index].startingPoints.map { $0.graphic?.isSelected = isSelected } } /// Loads the named trace configurations in the network. diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index d1aea2b3e..90944e39d 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -28,7 +28,7 @@ extension UtilityNetworkTraceViewModel { didSet { // Update the color of any pre-existing starting points startingPoints.forEach { startingPoint in - guard let symbol = startingPoint.graphic.symbol as? SimpleMarkerSymbol else { return } + guard let symbol = startingPoint.graphic?.symbol as? SimpleMarkerSymbol else { return } symbol.color = UIColor(color) } graphics.forEach { graphic in From 74bf7b09b4a669283ad38431a1d8662bb55e0eef Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 12:40:39 -0700 Subject: [PATCH 1307/1722] Renaming for accuracy --- .../UtilityNetworkTrace.swift | 2 +- .../UtilityNetworkTraceViewModel.swift | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index a745b419a..29cd9483f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -548,7 +548,7 @@ public struct UtilityNetworkTrace: View { } currentActivity = .creatingTrace(.viewingStartingPoints) Task { - await viewModel.setStartingPoint( + await viewModel.createStartingPoint( at: viewPoint, mapPoint: mapPoint, with: mapViewProxy diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 6ea147a2f..2050790ad 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -217,7 +217,7 @@ import SwiftUI /// - 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 setStartingPoint( + func createStartingPoint( at point: CGPoint, mapPoint: Point, with proxy: MapViewProxy @@ -232,17 +232,16 @@ import SwiftUI geoElement: geoElement, mapPoint: mapPoint ) - setStartingPoint(startingPoint: startingPoint) + processAndAdd(startingPoint) } } } - /// Adds a new starting point to the pending trace. + /// Asynchronously sets the nullable members of the provided starting point and adds it to the pending + /// trace. /// - Parameters: - /// - geoElement: An element that corresponds to another within the utility network. - /// - mapPoint: A point on the map in map coordinates. - /// - startingPoint: <#startingPoint description#> - func setStartingPoint(startingPoint: UtilityNetworkTraceStartingPoint) { + /// - startingPoint: The starting point to be processed and added to the pending trace. + func processAndAdd(_ startingPoint: UtilityNetworkTraceStartingPoint) { Task { guard let feature = startingPoint.geoElement as? ArcGISFeature, let globalid = feature.attributes["globalid"] as? UUID else { @@ -275,7 +274,6 @@ import SwiftUI } else { utilityElement.fractionAlongEdge = 0.5 } - } else if utilityElement.networkSource.kind == .junction && utilityElement.assetType.terminalConfiguration?.terminals.count ?? 0 > 1 { utilityElement.terminal = utilityElement.assetType.terminalConfiguration?.terminals.first @@ -418,7 +416,7 @@ import SwiftUI /// Adds programatic starting points to the pending trace. private func addExternalStartingPoints() { externalStartingPoints.forEach { - setStartingPoint(startingPoint: $0) + processAndAdd($0) } } From bc4f5d637ebde5936527b29ce534cc0732f4e5c3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 13:32:01 -0700 Subject: [PATCH 1308/1722] Fix SDK change build error --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 2050790ad..24704c8f1 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -263,7 +263,7 @@ import SwiftUI .renderer? .symbol(for: feature)? .makeSwatch(scale: 1.0), - let utilityElement = network.createElement(arcGISFeature: feature) else { return } + let utilityElement = network.makeElement(arcGISFeature: feature) else { return } if utilityElement.networkSource.kind == .edge && geometry is Polyline { if let mapPoint = startingPoint.mapPoint { From 0b2298f0754092becf787e7e43e3e5bb240d631b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 13:56:48 -0700 Subject: [PATCH 1309/1722] Make starting point icon background colors static white --- .../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 29cd9483f..2e6dbd4b5 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -461,7 +461,7 @@ public struct UtilityNetworkTrace: View { if let image = startingPoint.image { Image(uiImage: image) .frame(width: 25, height: 25) - .background(Color.secondary) + .background(.white) .cornerRadius(5) } } From 1d9882ca519b95e73aa2a153ee6811174049afda Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 14:02:30 -0700 Subject: [PATCH 1310/1722] Increase sample width to 400 --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index fbc87f9bb..9c1a77f80 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -66,7 +66,7 @@ struct UtilityNetworkTraceExampleView: View { } } .padding() - .frame(width: 360) + .frame(width: 400) } } } From d8179fde14845264cdad11b6d209a4ddc0a0a06b Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 27 Jul 2022 15:04:17 -0600 Subject: [PATCH 1311/1722] Update AuthenticationExample/AuthenticationExample/SignInView.swift Co-authored-by: Nimesh Jarecha --- .../AuthenticationExample/SignInView.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index 3495e78ba..522783280 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -30,10 +30,12 @@ struct SignInView: View { /// The portal that the user successfully signed in to. @Binding var portal: Portal? - /// The last signed in user name. If the property is `nil` then there was no previous effective - /// credential. If the property is non-nil and empty, then the previously persisted and effective - /// credential did not have a username. If the propert is non-nil and non-empty, then it contains - /// the previously used and persisted username. + /// The last signed in user name. + /// - If the property is `nil` then there was no previous effective credential. + /// - If the property is non-nil and empty, then the previously persisted and effective + /// credential did not have a username. + /// - If the property is non-nil and non-empty, then it contains the previously used and + /// persisted username. @State var lastSignedInUser: String? var body: some View { From 62bd6815ec46737c02c56d04997c9655acce80af Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 15:07:19 -0600 Subject: [PATCH 1312/1722] remove unused code --- .../AuthenticationExample/SignInView.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index 522783280..7f94a019b 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -144,18 +144,6 @@ private extension ArcGISCredential { } } -private extension NetworkCredential { - /// The username, if any, associated with this credential. - var username: String? { - switch self { - case .serverTrust, .certificate: - return nil - case .password(let credential): - return credential.username - } - } -} - private extension Error { /// Returns a Boolean value indicating whether the error is the result of cancelling an /// authentication challenge. From 18cb9190519f2bd68c32ebe706599f2ae558594f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 14:23:27 -0700 Subject: [PATCH 1313/1722] Add Attributes section header --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 2e6dbd4b5..942bcab28 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -430,11 +430,13 @@ public struct UtilityNetworkTrace: View { .foregroundColor(.blue) } } - ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in - HStack{ - Text(item.key) - Spacer() - Text(item.value as? String ?? "") + Section("Attributes") { + ForEach(Array(selectedStartingPoint!.geoElement.attributes.sorted(by: { $0.key < $1.key})), id: \.key) { item in + HStack{ + Text(item.key) + Spacer() + Text(item.value as? String ?? "") + } } } } From aad0b448f8b658ae521bac9f0f2bd8a6b719fee1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 14:44:45 -0700 Subject: [PATCH 1314/1722] Move starting point inspector zoom to button to the header --- .../UtilityNetworkTrace.swift | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 942bcab28..963cb8aa0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -390,9 +390,21 @@ public struct UtilityNetworkTrace: View { makeBackButton(title: startingPointsTitle) { currentActivity = .creatingTrace(.viewingStartingPoints) } - makeDetailSectionHeader( - title: selectedStartingPoint?.utilityElement?.assetType.name ?? "Unnamed Asset Type" - ) + Menu(selectedStartingPoint?.utilityElement?.assetType.name ?? "Unnamed Asset Type") { + Button("Zoom To") { + if let selectedStartingPoint = selectedStartingPoint, + let extent = selectedStartingPoint.geoElement.geometry?.extent { + viewpoint = Viewpoint(targetExtent: extent) + } + } + Button("Delete", role: .destructive) { + if let startingPoint = selectedStartingPoint { + viewModel.delete(startingPoint) + currentActivity = .creatingTrace(.viewingStartingPoints) + } + } + } + .font(.title3) List { if selectedStartingPoint?.utilityElement?.networkSource.kind == .edge { Section("Fraction Along Edge") { @@ -440,12 +452,6 @@ public struct UtilityNetworkTrace: View { } } } - makeZoomToButton { - if let selectedStartingPoint = selectedStartingPoint, - let extent = selectedStartingPoint.geoElement.geometry?.extent { - viewpoint = Viewpoint(targetExtent: extent) - } - } } /// Displays the chosen starting points for the new trace. From 054eb4eb77c4cf71363424c028f75635751e9c3e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 14:49:16 -0700 Subject: [PATCH 1315/1722] Moves trace name to the top of the advanced options --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 963cb8aa0..4ec23e4b0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -238,9 +238,6 @@ public struct UtilityNetworkTrace: View { set: { currentActivity = .creatingTrace($0 ? .viewingAdvancedOptions : nil) } ) ) { - ColorPicker(selection: $viewModel.pendingTrace.color) { - Text("Trace Color") - } TextField( "Trace Name", text: $viewModel.pendingTrace.name @@ -248,6 +245,9 @@ public struct UtilityNetworkTrace: View { .onSubmit { viewModel.pendingTrace.userDidSpecifyName = true } + ColorPicker(selection: $viewModel.pendingTrace.color) { + Text("Trace Color") + } Toggle(isOn: $shouldZoomOnTraceCompletion) { Text("Zoom to result") } From ccb2dd829f26c64a8e31a83bfed0f8ae4a0fa661 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 15:07:57 -0700 Subject: [PATCH 1316/1722] Add label for trace name in advanced options --- .../UtilityNetworkTrace.swift | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 4ec23e4b0..a0f95f337 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -238,15 +238,21 @@ public struct UtilityNetworkTrace: View { set: { currentActivity = .creatingTrace($0 ? .viewingAdvancedOptions : nil) } ) ) { - TextField( - "Trace Name", - text: $viewModel.pendingTrace.name - ) - .onSubmit { - viewModel.pendingTrace.userDidSpecifyName = true + HStack { + Text("Name") + Spacer() + TextField( + "Name", + text: $viewModel.pendingTrace.name + ) + .onSubmit { + viewModel.pendingTrace.userDidSpecifyName = true + } + .multilineTextAlignment(.trailing) + .foregroundColor(.blue) } ColorPicker(selection: $viewModel.pendingTrace.color) { - Text("Trace Color") + Text("Color") } Toggle(isOn: $shouldZoomOnTraceCompletion) { Text("Zoom to result") @@ -356,7 +362,7 @@ public struct UtilityNetworkTrace: View { } }) ) { - Text("Trace Color") + Text("Color") } } } From 364b659556eec6f3c5262cb9ae3b05509f8a7858 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 15:12:15 -0700 Subject: [PATCH 1317/1722] Prepend "Object ID" to asset group item numbers --- .../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 a0f95f337..50109d012 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -140,7 +140,7 @@ public struct UtilityNetworkTrace: View { $0.objectID < $1.objectID } ForEach(elements) { element in - makeZoomToButton(text: element.objectID.description) { + makeZoomToButton(text: "Object ID \(element.objectID.description)") { Task { if let feature = await viewModel.getFeatureFor(element: element), let geometry = feature.geometry { From c1397341bdfb6351105a13204bcb9e14ebe0d1de Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 16:24:16 -0600 Subject: [PATCH 1318/1722] fix nullability --- .../Components/Authentication/Authenticator.swift | 4 +--- .../Authentication/QueuedNetworkChallenge.swift | 9 +++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 8eaaf6b0b..503365209 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -159,9 +159,7 @@ extension Authenticator: AuthenticationChallengeHandler { return .performDefaultHandling } - guard let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) else { - return .performDefaultHandling - } + let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) // Queue up the challenge. subject.send(queuedChallenge) diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index dca1de5b6..e9c638ee7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -60,11 +60,8 @@ final class QueuedNetworkChallenge: QueuedChallenge { extension QueuedNetworkChallenge { /// Creates a `QueuedNetworkChallenge`. /// - Parameter networkChallenge: The associated network authentication challenge. - convenience init?(networkChallenge: NetworkAuthenticationChallenge) { - guard let kind = Kind(networkChallenge.kind) else { - return nil - } - self.init(host: networkChallenge.host, kind: kind) + convenience init(networkChallenge: NetworkAuthenticationChallenge) { + self.init(host: networkChallenge.host, kind: Kind(networkChallenge.kind)) } } @@ -84,7 +81,7 @@ extension QueuedNetworkChallenge.Kind { /// Creates an instance. /// - Parameter networkAuthenticationChallengeKind: The kind of network authentication /// challenge. - init?(_ networkAuthenticationChallengeKind: NetworkAuthenticationChallenge.Kind) { + init(_ networkAuthenticationChallengeKind: NetworkAuthenticationChallenge.Kind) { switch networkAuthenticationChallengeKind { case .serverTrust: self = .serverTrust From f8f5bbbbba9283951f684970b2a99b3fe25e4677 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 15:32:17 -0700 Subject: [PATCH 1319/1722] Adds parentheses around quantitative values --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 50109d012..55517469f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -305,7 +305,7 @@ public struct UtilityNetworkTrace: View { List { Section(elementResultsTitle) { DisclosureGroup( - viewModel.selectedTrace?.assetCount.description ?? "0", + "(\(viewModel.selectedTrace?.assetCount ?? 0))", isExpanded: Binding( get: { isFocused(traceViewingActivity: .viewingElementResults) }, set: { currentActivity = .viewingTraces($0 ? .viewingElementResults : nil) } @@ -317,7 +317,7 @@ public struct UtilityNetworkTrace: View { HStack { Text(assetGroup.key) Spacer() - Text(assetGroup.value.compactMap({ $0.value.count }).reduce(0, +).description) + Text("(\(assetGroup.value.compactMap({ $0.value.count }).reduce(0, +)))") } .foregroundColor(.blue) .contentShape(Rectangle()) @@ -327,9 +327,9 @@ public struct UtilityNetworkTrace: View { } } } - Section("Function Result") { + Section("Function Results") { DisclosureGroup( - viewModel.selectedTrace?.utilityFunctionTraceResult?.functionOutputs.count.description ?? "0", + "(\(viewModel.selectedTrace?.utilityFunctionTraceResult?.functionOutputs.count ?? 0))", isExpanded: Binding( get: { isFocused(traceViewingActivity: .viewingFunctionResults) }, set: { currentActivity = .viewingTraces($0 ? .viewingFunctionResults : nil) } From 6bdacd3a5516dbea79cee609d87aaedec32f4898 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 15:35:19 -0700 Subject: [PATCH 1320/1722] Renames element results to feature results --- .../UtilityNetworkTrace.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 55517469f..ac15f5454 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -39,8 +39,8 @@ public struct UtilityNetworkTrace: View { case viewingAdvancedOptions /// The user is viewing a list of element results, grouped by asset group and asset type. case viewingElementGroup([String: [UtilityElement]]) - /// The user is viewing the list of element results. - case viewingElementResults + /// The user is viewing the list of feature results. + case viewingFeatureResults /// The user is viewing the list of function results. case viewingFunctionResults } @@ -127,8 +127,8 @@ public struct UtilityNetworkTrace: View { /// Displays information about a chosen asset group. @ViewBuilder private var assetGroupDetail: some View { if let assetGroup = selectedAssetGroup { - makeBackButton(title: elementResultsTitle) { - currentActivity = .viewingTraces(.viewingElementResults) + makeBackButton(title: featureResultsTitle) { + currentActivity = .viewingTraces(.viewingFeatureResults) } makeDetailSectionHeader( title: assetGroup.first?.value.first?.assetGroup.name ?? "Unnamed Asset Group" @@ -303,12 +303,12 @@ public struct UtilityNetworkTrace: View { Text(traceName) } List { - Section(elementResultsTitle) { + Section(featureResultsTitle) { DisclosureGroup( "(\(viewModel.selectedTrace?.assetCount ?? 0))", isExpanded: Binding( - get: { isFocused(traceViewingActivity: .viewingElementResults) }, - set: { currentActivity = .viewingTraces($0 ? .viewingElementResults : nil) } + get: { isFocused(traceViewingActivity: .viewingFeatureResults) }, + set: { currentActivity = .viewingTraces($0 ? .viewingFeatureResults : nil) } ) ) { ForEach( @@ -674,8 +674,8 @@ public struct UtilityNetworkTrace: View { } } - /// Title for the element results section - private let elementResultsTitle = "Element Results" + /// Title for the feature results section + private let featureResultsTitle = "Feature Results" /// Title for the starting points section private let startingPointsTitle = "Starting Points" From d510c54af97b4ee1131c3718a3700c27f07bc1f0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 15:43:27 -0700 Subject: [PATCH 1321/1722] Swaps the font size of the trace result picker and the current trace result name --- .../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 ac15f5454..63ad9dd69 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -297,10 +297,10 @@ public struct UtilityNetworkTrace: View { } } } - .font(.title3) .padding(2.5) if let traceName = viewModel.selectedTrace?.name, !traceName.isEmpty { Text(traceName) + .font(.title3) } List { Section(featureResultsTitle) { From 91ae7c1a9b4be7c5f2fa4294aa5a9550e30b20fe Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 27 Jul 2022 15:53:01 -0700 Subject: [PATCH 1322/1722] Moves asset types into disclosure groups --- .../UtilityNetworkTrace.swift | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 63ad9dd69..fb2244965 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -135,19 +135,23 @@ public struct UtilityNetworkTrace: View { ) List { ForEach(assetGroup.sorted(by: { $0.key < $1.key }), id: \.key) { assetTypeGroup in + let elements = assetTypeGroup.value.sorted { + $0.objectID < $1.objectID + } Section(assetTypeGroup.key) { - let elements = assetTypeGroup.value.sorted { - $0.objectID < $1.objectID - } - ForEach(elements) { element in - makeZoomToButton(text: "Object ID \(element.objectID.description)") { - Task { - if let feature = await viewModel.getFeatureFor(element: element), - let geometry = feature.geometry { - viewpoint = Viewpoint(targetExtent: geometry.extent) + DisclosureGroup { + ForEach(elements) { element in + makeZoomToButton(text: "Object ID \(element.objectID.description)") { + Task { + if let feature = await viewModel.getFeatureFor(element: element), + let geometry = feature.geometry { + viewpoint = Viewpoint(targetExtent: geometry.extent) + } } } } + } label: { + Text("(\(elements.count))") } } } From b929b9a5da3096a448f3758c15f6be173920fcfe Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 27 Jul 2022 17:45:09 -0600 Subject: [PATCH 1323/1722] Update AuthenticationExample/AuthenticationExample/ProfileView.swift Co-authored-by: David Feinzimer --- AuthenticationExample/AuthenticationExample/ProfileView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AuthenticationExample/AuthenticationExample/ProfileView.swift b/AuthenticationExample/AuthenticationExample/ProfileView.swift index c89fa9c21..1dbb55985 100644 --- a/AuthenticationExample/AuthenticationExample/ProfileView.swift +++ b/AuthenticationExample/AuthenticationExample/ProfileView.swift @@ -41,7 +41,7 @@ struct ProfileView: View { } var signOutButton: some View { - Button { + Button(role: .destructive) { signOut() } label: { if isSigningOut { From 7b85c0278e004c505b3b0e76636902bdf9d31c8d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 27 Jul 2022 17:49:37 -0600 Subject: [PATCH 1324/1722] change to featuredItems --- .../AuthenticationExample/FeaturedMapsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift index 73af71957..38522f506 100644 --- a/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift +++ b/AuthenticationExample/AuthenticationExample/FeaturedMapsView.swift @@ -42,7 +42,7 @@ struct FeaturedMapsView: View { .task { guard featuredItems.isEmpty else { return } do { - featuredItems = try await portal.homePageFeaturedContent + featuredItems = try await portal.featuredItems .filter { $0.kind == .webMap } } catch {} From bcf0909c43b661e229f8fb8b7491dc752ae47b4e Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 28 Jul 2022 13:07:37 -0600 Subject: [PATCH 1325/1722] fix build --- .../ArcGISToolkit/Components/Authentication/Authenticator.swift | 2 +- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 503365209..a3650f644 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -156,7 +156,7 @@ extension Authenticator: AuthenticationChallengeHandler { // If `promptForUntrustedHosts` is `false` then perform default handling // for server trust challenges. guard promptForUntrustedHosts || challenge.kind != .serverTrust else { - return .performDefaultHandling + return .allowRequestToFail } let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index ebd9ea5a9..7ac2ee6a8 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -144,7 +144,7 @@ import SwiftUI .renderer? .symbol(for: feature)? .makeSwatch(scale: 1.0), - let utilityElement = network.createElement(arcGISFeature: feature) else { return } + let utilityElement = network.makeElement(arcGISFeature: feature) else { return } let graphic = Graphic( geometry: mapPoint, symbol: SimpleMarkerSymbol( From f741662286c8998b2c9b59c71fef2372d28306a7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Jul 2022 14:08:22 -0700 Subject: [PATCH 1326/1722] `createStartingPoint` -> `makeStartingPoint` --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 2 +- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 29cd9483f..0115106d5 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -548,7 +548,7 @@ public struct UtilityNetworkTrace: View { } currentActivity = .creatingTrace(.viewingStartingPoints) Task { - await viewModel.createStartingPoint( + await viewModel.makeStartingPoint( at: viewPoint, mapPoint: mapPoint, with: mapViewProxy diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 24704c8f1..93cd0af34 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -217,7 +217,7 @@ import SwiftUI /// - 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 createStartingPoint( + func makeStartingPoint( at point: CGPoint, mapPoint: Point, with proxy: MapViewProxy From fd3634227104712e6ae55e42539e33fe9327f0bb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Jul 2022 14:55:59 -0700 Subject: [PATCH 1327/1722] Shows function type --- .../UtilityNetworkTrace.swift | 7 ++++- .../Extensions/UtilityTraceFunctionType.swift | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionType.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index aa50b9257..40d4d46c4 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -343,7 +343,12 @@ public struct UtilityNetworkTrace: View { HStack { Text(item.function.networkAttribute?.name ?? "Unnamed") Spacer() - Text((item.result as? Double)?.description ?? "N/A") + VStack(alignment: .trailing) { + Text(item.function.functionType.description) + .font(.caption) + .foregroundColor(.secondary) + Text((item.result as? Double)?.description ?? "N/A") + } } } } diff --git a/Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionType.swift b/Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionType.swift new file mode 100644 index 000000000..da1448eb7 --- /dev/null +++ b/Sources/ArcGISToolkit/Extensions/UtilityTraceFunctionType.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 ArcGIS + +extension UtilityTraceFunctionType: CustomStringConvertible { + public var description: String { + switch self { + case .add: return "Add" + case .average: return "Average" + case .count: return "Count" + case .max: return "Max" + case .min: return "Min" + case .subtract: return "Subtract" + } + } +} From 9d68b7752e8c046f657c855c2a818feb1c529642 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Jul 2022 15:01:59 -0700 Subject: [PATCH 1328/1722] Matches "Clear all results" button to "Trace" button --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 40d4d46c4..849cd555f 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -382,13 +382,13 @@ public struct UtilityNetworkTrace: View { } } .padding([.vertical], 2) - Button { + Button(role: .destructive) { showWarningAlert.toggle() } label: { - Text("Clear All Results") - .tint(.red) + Text(clearResultsTitle) } - .alert("Clear All Results", isPresented: $showWarningAlert) { + .buttonStyle(.bordered) + .alert(clearResultsTitle, isPresented: $showWarningAlert) { Button(role: .destructive) { viewModel.deleteAllTraces() currentActivity = .creatingTrace(nil) @@ -683,6 +683,9 @@ public struct UtilityNetworkTrace: View { } } + /// Title for the clear all results feature + private let clearResultsTitle = "Clear All Results" + /// Title for the feature results section private let featureResultsTitle = "Feature Results" From ad204520463eea99095ad101712d8f29c854df19 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Jul 2022 15:08:49 -0700 Subject: [PATCH 1329/1722] Only shows the results navigator if there's more than one completed trace --- .../UtilityNetworkTrace.swift | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 849cd555f..05f197b97 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -281,27 +281,34 @@ public struct UtilityNetworkTrace: View { .disabled(!viewModel.canRunTrace) } - /// The tab that allows for viewing completed traces. - @ViewBuilder private var resultsTab: some View { - HStack { - if viewModel.completedTraces.count > 1 { - Button { - viewModel.selectPreviousTrace() - } label: { - Image(systemName: "chevron.backward") + /// Navigator at the top of the results tab that allows users to cycle through completed traces. + @ViewBuilder private var resultsNavigator: some View { + if viewModel.completedTraces.count > 1 { + HStack { + if viewModel.completedTraces.count > 1 { + Button { + viewModel.selectPreviousTrace() + } label: { + Image(systemName: "chevron.backward") + } } - } - Text(currentTraceLabel) - .padding(.horizontal) - if viewModel.completedTraces.count > 1 { - Button { - viewModel.selectNextTrace() - } label: { - Image(systemName: "chevron.forward") + Text(currentTraceLabel) + .padding(.horizontal) + if viewModel.completedTraces.count > 1 { + Button { + viewModel.selectNextTrace() + } label: { + Image(systemName: "chevron.forward") + } } } } - .padding(2.5) + } + + /// The tab that allows for viewing completed traces. + @ViewBuilder private var resultsTab: some View { + resultsNavigator + .padding(2.5) if let traceName = viewModel.selectedTrace?.name, !traceName.isEmpty { Text(traceName) .font(.title3) From b002570424ac055d1fdc77196386a2083e42a95b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Jul 2022 16:14:02 -0700 Subject: [PATCH 1330/1722] Makes trace name tappable menu with options --- .../UtilityNetworkTrace.swift | 23 ++++++++++++------- .../UtilityNetworkTraceViewModel.swift | 21 ++++++++++++++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 05f197b97..0a6221f9e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -309,9 +309,21 @@ public struct UtilityNetworkTrace: View { @ViewBuilder private var resultsTab: some View { resultsNavigator .padding(2.5) - if let traceName = viewModel.selectedTrace?.name, !traceName.isEmpty { - Text(traceName) - .font(.title3) + if let selectedTrace = viewModel.selectedTrace { + Menu(selectedTrace.name) { + if let resultExtent = selectedTrace.resultExtent { + Button("Zoom To") { + viewpoint = Viewpoint(targetExtent: resultExtent) + } + } + Button("Delete", role: .destructive) { + if viewModel.completedTraces.count == 1 { + currentActivity = .creatingTrace(nil) + } + viewModel.delete(selectedTrace) + } + } + .font(.title3) } List { Section(featureResultsTitle) { @@ -383,11 +395,6 @@ public struct UtilityNetworkTrace: View { } } } - makeZoomToButton { - if let extent = viewModel.selectedTrace?.resultExtent { - viewpoint = Viewpoint(targetExtent: extent) - } - } .padding([.vertical], 2) Button(role: .destructive) { showWarningAlert.toggle() diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 1c61b152f..12bafd5cd 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -86,7 +86,8 @@ import SwiftUI /// The selected trace. var selectedTrace: Trace? { - if let index = selectedTraceIndex { + if let index = selectedTraceIndex, + index >= 0, index < completedTraces.count { return completedTraces[index] } else { return nil @@ -125,12 +126,19 @@ import SwiftUI } } + /// Deletes the provided trace from the list of completed traces. + /// - Parameter trace: The trace to be deleted. + func delete(_ trace: Trace) { + deleteGraphics(for: trace) + completedTraces.removeAll { $0 == trace } + selectPreviousTrace() + } + /// Deletes all of the completed traces. func deleteAllTraces() { selectedTraceIndex = nil completedTraces.forEach { traceResult in - graphicsOverlay.removeGraphics(traceResult.startingPoints.compactMap { $0.graphic }) - graphicsOverlay.removeGraphics(traceResult.graphics) + deleteGraphics(for: traceResult) } completedTraces.removeAll() } @@ -450,6 +458,13 @@ import SwiftUI _ = completedTraces[index].startingPoints.map { $0.graphic?.isSelected = isSelected } } + /// Deletes all graphics for the provided trace. + /// - Parameter trace: The trace to which delete graphics for. + private func deleteGraphics(for trace: Trace) { + graphicsOverlay.removeGraphics(trace.startingPoints.compactMap { $0.graphic }) + graphicsOverlay.removeGraphics(trace.graphics) + } + /// Loads the named trace configurations in the network. /// Returns the named trace configurations in the network on the provided map. /// - Parameter map: A web map containing one or more utility networks. From 9dac46551fd9760c1209f1ee9894160faec2da19 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Jul 2022 16:17:35 -0700 Subject: [PATCH 1331/1722] Remove func `makeZoomToButton` as it was down to 1 caller --- .../UtilityNetworkTrace.swift | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 0a6221f9e..257a5bb83 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -141,13 +141,19 @@ public struct UtilityNetworkTrace: View { Section(assetTypeGroup.key) { DisclosureGroup { ForEach(elements) { element in - makeZoomToButton(text: "Object ID \(element.objectID.description)") { + Button { Task { if let feature = await viewModel.getFeatureFor(element: element), let geometry = feature.geometry { viewpoint = Viewpoint(targetExtent: geometry.extent) } } + } label: { + Label { + Text("Object ID \(element.objectID.description)") + } icon: { + Image(systemName: "scope") + } } } } label: { @@ -680,23 +686,6 @@ public struct UtilityNetworkTrace: View { .frame(maxWidth: .infinity, alignment: .center) } - /// Returns a "Zoom To" button that performs a specified action when pressed. - /// - Parameter text: The custom text to be displayed within the button. - /// - Parameter action: The action to be performed. - /// - Returns: The configured button. - private func makeZoomToButton( - text: String = "Zoom To", - _ action: @escaping () -> Void - ) -> some View { - Button { action() } label: { - Label { - Text(text) - } icon: { - Image(systemName: "scope") - } - } - } - /// Title for the clear all results feature private let clearResultsTitle = "Clear All Results" From bdb639de34ffc8ba5fb08c441d58af4a1e237dcb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 28 Jul 2022 18:28:37 -0700 Subject: [PATCH 1332/1722] Start --- .../Examples/FloatingPanelExampleView.swift | 9 +-- .../UtilityNetworkTraceExampleView.swift | 26 ++++---- .../FloatingPanel/FloatingPanel.swift | 62 +++++++++++++++---- .../FloatingPanel/FloatingPanelModifier.swift | 60 ++++++++++++++++++ 4 files changed, 122 insertions(+), 35 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 9e73fc763..b417926c5 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -16,8 +16,6 @@ import ArcGISToolkit import ArcGIS struct FloatingPanelExampleView: View { - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @StateObject private var map = Map(basemapStyle: .arcGISImagery) private let initialViewpoint = Viewpoint( @@ -30,11 +28,8 @@ struct FloatingPanelExampleView: View { map: map, viewpoint: initialViewpoint ) - .overlay(alignment: .topTrailing) { - FloatingPanel { - SampleContent() - } - .frame(maxWidth: horizontalSizeClass == .regular ? 360 : .infinity) + .floatingPanel(isPresented: .constant(true)) { + SampleContent() } } } diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 93b338380..85e1386b6 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -47,22 +47,18 @@ struct UtilityNetworkTraceExampleView: View { self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } - .overlay(alignment: .topTrailing) { - FloatingPanel { - UtilityNetworkTrace( - graphicsOverlay: $resultGraphicsOverlay, - map: map, - mapPoint: $mapPoint, - viewPoint: $viewPoint, - mapViewProxy: $mapViewProxy, - viewpoint: $viewpoint - ) - .task { - await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) - } + .floatingPanel(isPresented: .constant(true)) { + UtilityNetworkTrace( + graphicsOverlay: $resultGraphicsOverlay, + map: map, + mapPoint: $mapPoint, + viewPoint: $viewPoint, + mapViewProxy: $mapViewProxy, + viewpoint: $viewpoint + ) + .task { + await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) } - .padding() - .frame(width: 360) } } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 52ffce4d2..b9ffd6f6c 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -23,7 +23,7 @@ import SwiftUI /// or persistent, where the information is always displayed, for example a /// dedicated search panel. They will also be primarily simple containers /// that clients will fill with their own content. -public struct FloatingPanel: View where Content: View { +struct FloatingPanel: View where Content: View { // Note: instead of the FloatingPanel being a view, it might be preferable // to have it be a view modifier, similar to how SwiftUI doesn't have a // SheetView, but a modifier that presents a sheet. @@ -35,7 +35,7 @@ public struct FloatingPanel: View where Content: View { /// Creates a `FloatingPanel` /// - Parameter content: The view shown in the floating panel. - public init(@ViewBuilder content: () -> Content) { + init(@ViewBuilder content: () -> Content) { self.content = content() } @@ -57,20 +57,26 @@ public struct FloatingPanel: View where Content: View { GeometryReader { geometryProxy in VStack { if isCompact { - Handle(color: handleColor) - .gesture(drag) - Divider() - content - .frame(minHeight: .minHeight, maxHeight: height) + VStack { + Handle(color: handleColor) + .gesture(drag) + Divider() + content + .frame(minHeight: .minHeight, maxHeight: height) + } + .background(Color(uiColor: .systemBackground)) + .cornerRadius(10, corners: [.topLeft, .topRight]) } else { - content - .frame(minHeight: .minHeight, maxHeight: height) - Divider() - Handle(color: handleColor) - .gesture(drag) + VStack { + content + .frame(minHeight: .minHeight, maxHeight: height) + Divider() + Handle(color: handleColor) + .gesture(drag) + } + .esriBorder() } } - .esriBorder() .padding(isCompact ? [] : [.leading, .top, .trailing]) .padding(.bottom, isCompact ? 0 : 50) .frame( @@ -131,3 +137,33 @@ private extension Color { static var defaultHandleColor: Color { .secondary } static var activeHandleColor: Color { .primary } } + +private struct RoundedCorner: Shape { + var corners: UIRectCorner + + var radius: CGFloat + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize( + width: radius, + height: radius + ) + ) + return Path(path.cgPath) + } +} + +private extension View { + func cornerRadius( + _ radius: CGFloat, + corners: UIRectCorner + ) -> some View { + clipShape(RoundedCorner( + corners: corners, + radius: radius + )) + } +} diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift new file mode 100644 index 000000000..d877a70d9 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -0,0 +1,60 @@ +// 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 + +public extension View { + /// Presents a dynamic view with a presentation style similar to that of a sheet in compact + /// environments and a popover otherwise. + /// The resulting view allows for interaction with background contents. + /// - Parameters: + /// - isPresented: <#isPresented description#> + /// - content: <#content description#> + /// - Returns: <#description#> + func floatingPanel( + isPresented: Binding, + _ content: @escaping () -> Content + ) -> some View where Content: View { + modifier( + FloatingPanelModifier( + isPresented: isPresented, + innerContent: content() + ) + ) + } +} + +/// <#Description#> +private struct FloatingPanelModifier: ViewModifier where InnerContent: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + /// <#Description#> + @Binding var isPresented: Bool + + /// <#Description#> + let innerContent: InnerContent + + /// <#Description#> + /// - Parameter content: <#content description#> + /// - Returns: <#description#> + func body(content: Content) -> some View { + content + .overlay(alignment: .trailing) { + FloatingPanel { + innerContent + } + .edgesIgnoringSafeArea(.bottom) + .frame(maxWidth: horizontalSizeClass == .regular ? 360 : .infinity) + } + } +} From 30e0f1d29cb02d0aa13de57c36a50bd0e66f3261 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 10:48:43 -0500 Subject: [PATCH 1333/1722] Update html text header for better resizing. --- .../Components/Popups/HTMLTextView.swift | 86 +++++++++++++++---- .../Popups/TextPopupElementView.swift | 2 +- 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 97314be15..2f93379b0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -16,22 +16,62 @@ import SwiftUI /// A SwiftUI view to display an HTML string in a `WKWebView`. struct HTMLTextView: UIViewRepresentable { + /// The user-defined HTML string. + var userHTML: String = "" + /// The html string to dispay, including the header. - var displayHTMLString: String + var displayHTML: String { + // Set the initial scale to 1, don't allow user scaling. + // When we went to WKWebView from UIWebView, the content got smaller, + // this fixes that and also doesn't allow the user to pinch to zoom. + var header = "
" + + // Inject css in a head element to: + // - word wrap long content such as urls + // - set font family to default apple font + // - set font size to subheadline + // - remove padding from the body. Add some margin to separate from the border of the webview. + // - limit images to a maximum width of 100% + // + // Also wrap the passed html fragment inside + // open/close tags so that these css styles will apply + // + // In order to make hyperlinks match our + // app tint we need to be passed a color + // and to dynamically inject it into the + // HTML/CSS + // + header = header.appending(""" + + + + + + """) + + // The final string is the header + userHTML + closing + return header.appending(userHTML.trimmingCharacters(in: .whitespacesAndNewlines)).appending("") + } /// The height of the view, calculated in the `webView(didFinish:)` delegate method. @Binding private var dynamicHeight: CGFloat - - /// The static header string for displaying html strings in a readable size. - static var headerString = "
" - static var footerString = "

" /// Creates an `HTMLTextView`. /// - Parameters: - /// - htmlString: The html string to be displayed. + /// - html: The html string to be displayed. /// - dynamicHeight: A binding to the calculated height of the `WKWebView`. - init(htmlString: String, dynamicHeight: Binding) { - displayHTMLString = HTMLTextView.headerString + htmlString + HTMLTextView.footerString + init(html: String, dynamicHeight: Binding) { + userHTML = html _dynamicHeight = dynamicHeight } @@ -40,31 +80,39 @@ struct HTMLTextView: UIViewRepresentable { } func updateUIView(_ uiView: WKWebView, context: Context) { - uiView.backgroundColor = .clear - uiView.navigationDelegate = context.coordinator - uiView.loadHTMLString(displayHTMLString, baseURL: nil) + uiView.isOpaque = false + // This is a case where we always want the background to be white regardless of light/dark mode. If the user wants to implement dark mode, + // within their HTML, the background of the HTML will be shown over this background. + uiView.backgroundColor = .white + uiView.scrollView.backgroundColor = .white uiView.scrollView.isScrollEnabled = false + uiView.loadHTMLString(displayHTML, baseURL: nil) + uiView.navigationDelegate = context.coordinator } class Coordinator: NSObject, WKNavigationDelegate { var parent: HTMLTextView + private var hasCommitted = false + private var runDidFinishAgain = true init(_ parent: HTMLTextView) { self.parent = parent } + public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + hasCommitted = true + } + // WKNavigationDelegate method for navigation actions. func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { - if navigationAction.navigationType == .linkActivated && - (navigationAction.request.url?.scheme?.lowercased() == "http" || - navigationAction.request.url?.scheme?.lowercased() == "https") { - - if let url = navigationAction.request.url { - DispatchQueue.main.async { - UIApplication.shared.open(url) - } + if navigationAction.navigationType == .linkActivated, + (navigationAction.request.url?.scheme?.lowercased() == "http" || + navigationAction.request.url?.scheme?.lowercased() == "https"), + let url = navigationAction.request.url { + DispatchQueue.main.async { + UIApplication.shared.open(url) } return .cancel } diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index 0d984d846..b2cd25fe1 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -26,7 +26,7 @@ struct TextPopupElementView: View { let roundedRect = RoundedRectangle(cornerRadius: 8) if !popupElement.text.isEmpty { ZStack { - HTMLTextView(htmlString: popupElement.text, dynamicHeight: $webViewHeight) + HTMLTextView(html: popupElement.text, dynamicHeight: $webViewHeight) .clipShape(roundedRect) .frame(height: webViewHeight) roundedRect From a20476e4e0b83e6cbd465749f77614ef432630bc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 11:09:06 -0500 Subject: [PATCH 1334/1722] Improved htmltextview size calculation --- .../Components/Popups/HTMLTextView.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 2f93379b0..5007d4bbd 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -95,8 +95,6 @@ struct HTMLTextView: UIViewRepresentable { private var hasCommitted = false - private var runDidFinishAgain = true - init(_ parent: HTMLTextView) { self.parent = parent } @@ -123,16 +121,18 @@ struct HTMLTextView: UIViewRepresentable { // WKNavigationDelegate method where the size calculation happens. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){ - self.parent.dynamicHeight = webView.scrollView.contentSize.height - - // Sometimes the contentSize has not been updated yet, probably - // because the view has not been rendered yet. So rerun this again - // after a delay. This fixes the issue. - if runDidFinishAgain { - runDidFinishAgain = false - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.webView(webView, didFinish: navigation) - self?.runDidFinishAgain = true + webView.evaluateJavaScript("document.readyState") { [weak self] complete, _ in + guard complete != nil, let webView = webView else { + return + } + webView.evaluateJavaScript("document.body.scrollHeight") { height, _ in + // Pass the new height to the delegate so that is can change the + // cell height with performBatchUpdates + // + guard let height = height as? CGFloat else { + return + } + self?.parent.dynamicHeight = height } } } From f2e64b8a45e278e48d84b3909c12f62332991a24 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 11:11:08 -0500 Subject: [PATCH 1335/1722] Better Divider() display when there's no title and after last element. --- .../Components/Popups/PopupView.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index b154b3a58..229cd4404 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -34,10 +34,12 @@ public struct PopupView: View { public var body: some View { VStack(alignment: .leading) { - Text(popup.title) - .font(.title) - .fontWeight(.bold) - Divider() + if !popup.title.isEmpty { + Text(popup.title) + .font(.title) + .fontWeight(.bold) + Divider() + } Group { if let isPopupEvaluated = isPopupEvaluated { if isPopupEvaluated { @@ -58,7 +60,6 @@ public struct PopupView: View { do { expressionEvaluations = try await popup.evaluateExpressions() isPopupEvaluated = true - print("EE: \(expressionEvaluations?.count); first = \(expressionEvaluations?.first?.error)") } catch { isPopupEvaluated = false } @@ -70,7 +71,7 @@ public struct PopupView: View { var body: some View { ScrollView { VStack(alignment: .leading) { - ForEach(popup.evaluatedElements) { popupElement in + ForEach(Array(popup.evaluatedElements.enumerated()), id: \.offset) { index, popupElement in switch popupElement { case let popupElement as AttachmentsPopupElement: Text("AttachmentsPopupElementView implementation coming soon.") @@ -86,7 +87,9 @@ public struct PopupView: View { EmptyView() } - Divider() + if index < popup.evaluatedElements.count - 1 { + Divider() + } } } } From 60c5839c36cec0b400fa883d026870f5b4fcf867 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 08:31:44 -0700 Subject: [PATCH 1336/1722] Progress --- .../FloatingPanel/FloatingPanel.swift | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index b9ffd6f6c..aa1749c57 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -57,28 +57,24 @@ struct FloatingPanel: View where Content: View { GeometryReader { geometryProxy in VStack { if isCompact { - VStack { - Handle(color: handleColor) - .gesture(drag) - Divider() - content - .frame(minHeight: .minHeight, maxHeight: height) - } - .background(Color(uiColor: .systemBackground)) - .cornerRadius(10, corners: [.topLeft, .topRight]) - } else { - VStack { - content - .frame(minHeight: .minHeight, maxHeight: height) - Divider() - Handle(color: handleColor) - .gesture(drag) - } - .esriBorder() + Handle(color: handleColor) + .gesture(drag) + Divider() + } + content + .frame(minHeight: .minHeight, maxHeight: height) + if !isCompact { + Divider() + Handle(color: handleColor) + .gesture(drag) } } - .padding(isCompact ? [] : [.leading, .top, .trailing]) - .padding(.bottom, isCompact ? 0 : 50) + .padding([.top, .bottom], 10) + .background(Color(uiColor: .systemGroupedBackground)) + .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) + .shadow(radius: 10) + .padding([.leading, .top, .trailing], isCompact ? 0 : 10) + .padding([.bottom], isCompact ? 0 : 50) .frame( width: geometryProxy.size.width, height: geometryProxy.size.height, From a9660679e6fedd9a98511391f6d1bef4b059f284 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 09:26:57 -0700 Subject: [PATCH 1337/1722] Start --- .../UtilityNetworkTraceExampleView.swift | 2 +- .../UtilityNetworkTrace.swift | 73 ++++++++++--------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 85e1386b6..b17209bfa 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -47,7 +47,7 @@ struct UtilityNetworkTraceExampleView: View { self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } - .floatingPanel(isPresented: .constant(true)) { + .overlay { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, map: map, diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index b9f9711d4..455acaa40 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -335,43 +335,46 @@ public struct UtilityNetworkTrace: View { } public var body: some View { - VStack { - if !viewModel.completedTraces.isEmpty && - !isFocused(traceCreationActivity: .addingStartingPoints) { - activityPicker - } - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .addingStartingPoints: - cancelAddStartingPoints - case .inspectingStartingPoint: - startingPointDetail - default: - newTraceTab + Color.clear + .floatingPanel(isPresented: .constant(true)) { + VStack { + if !viewModel.completedTraces.isEmpty && + !isFocused(traceCreationActivity: .addingStartingPoints) { + activityPicker + } + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .addingStartingPoints: + cancelAddStartingPoints + case .inspectingStartingPoint: + startingPointDetail + default: + newTraceTab + } + case .viewingTraces: + resultsTab + } + } + .background(Color(uiColor: .systemGroupedBackground)) + .animation(.default, value: currentActivity) + .onChange(of: viewPoint) { newValue in + guard isFocused(traceCreationActivity: .addingStartingPoints), + let mapViewProxy = mapViewProxy, + let mapPoint = mapPoint, + let viewPoint = viewPoint else { + return + } + currentActivity = .creatingTrace(.viewingStartingPoints) + Task { + await viewModel.setStartingPoint( + at: viewPoint, + mapPoint: mapPoint, + with: mapViewProxy + ) + } } - case .viewingTraces: - resultsTab - } - } - .background(Color(uiColor: .systemGroupedBackground)) - .animation(.default, value: currentActivity) - .onChange(of: viewPoint) { newValue in - guard isFocused(traceCreationActivity: .addingStartingPoints), - let mapViewProxy = mapViewProxy, - let mapPoint = mapPoint, - let viewPoint = viewPoint else { - return - } - currentActivity = .creatingTrace(.viewingStartingPoints) - Task { - await viewModel.setStartingPoint( - at: viewPoint, - mapPoint: mapPoint, - with: mapViewProxy - ) } - } } // MARK: Computed Properties From f5952d83967a3576ac343c5d12eee14df0348bbd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 10:29:12 -0700 Subject: [PATCH 1338/1722] Progress Update FloatingPanel.swift Update FloatingPanelModifier.swift Update UtilityNetworkTraceExampleView.swift Add FloatingPanelDetent --- .../Examples/FloatingPanelExampleView.swift | 2 +- .../UtilityNetworkTraceExampleView.swift | 2 +- .../FloatingPanel/FloatingPanel.swift | 19 ++++++++++++++++- .../FloatingPanel/FloatingPanelDetent.swift | 21 +++++++++++++++++++ .../FloatingPanel/FloatingPanelModifier.swift | 10 +++++---- 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index b417926c5..838f97499 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -28,7 +28,7 @@ struct FloatingPanelExampleView: View { map: map, viewpoint: initialViewpoint ) - .floatingPanel(isPresented: .constant(true)) { + .floatingPanel(isPresented: .constant(true), detent: .constant(.mid)) { SampleContent() } } diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 85e1386b6..7790eb8eb 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -47,7 +47,7 @@ struct UtilityNetworkTraceExampleView: View { self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } - .floatingPanel(isPresented: .constant(true)) { + .floatingPanel(isPresented: .constant(true), detent: .constant(.mid)) { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, map: map, diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index aa1749c57..efdfb3ed7 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -35,8 +35,13 @@ struct FloatingPanel: View where Content: View { /// Creates a `FloatingPanel` /// - Parameter content: The view shown in the floating panel. - init(@ViewBuilder content: () -> Content) { + /// - Parameter detent: <#detent description#> + init( + detent: Binding, + @ViewBuilder content: () -> Content + ) { self.content = content() + _detent = detent } /// The color of the handle. @@ -48,6 +53,9 @@ struct FloatingPanel: View where Content: View { /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity + /// <#Description#> + @Binding var detent: FloatingPanelDetent + /// A Boolean value indicating whether the panel should be configured for a compact environment. private var isCompact: Bool { horizontalSizeClass == .compact @@ -86,6 +94,15 @@ struct FloatingPanel: View where Content: View { height = maximumHeight } } + .onChange(of: detent) { newValue in + withAnimation { + switch newValue { + case .min: height = .minHeight + 75 + case .mid: height = geometryProxy.size.height / 2.0 + case .max: height = geometryProxy.size.height - 75 + } + } + } } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift new file mode 100644 index 000000000..80b4b3ac9 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift @@ -0,0 +1,21 @@ +// 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 Foundation + +/// <#Description#> +public enum FloatingPanelDetent { + case min + case mid + case max +} diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index d877a70d9..1ea6f8bf9 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -23,11 +23,13 @@ public extension View { /// - Returns: <#description#> func floatingPanel( isPresented: Binding, + detent: Binding, _ content: @escaping () -> Content ) -> some View where Content: View { modifier( FloatingPanelModifier( isPresented: isPresented, + detent: detent, innerContent: content() ) ) @@ -42,15 +44,15 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont @Binding var isPresented: Bool /// <#Description#> - let innerContent: InnerContent + var detent: Binding /// <#Description#> - /// - Parameter content: <#content description#> - /// - Returns: <#description#> + let innerContent: InnerContent + func body(content: Content) -> some View { content .overlay(alignment: .trailing) { - FloatingPanel { + FloatingPanel(detent: detent) { innerContent } .edgesIgnoringSafeArea(.bottom) From 934595d4d2f01ab8c41673e182fd014966fe5e59 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 11:33:33 -0700 Subject: [PATCH 1339/1722] Progress --- .../Examples/FloatingPanelExampleView.swift | 2 +- .../UtilityNetworkTraceExampleView.swift | 2 +- .../FloatingPanel/FloatingPanel.swift | 30 +++++++++++++++---- .../FloatingPanel/FloatingPanelDetent.swift | 4 ++- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 838f97499..72f09e59c 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -28,7 +28,7 @@ struct FloatingPanelExampleView: View { map: map, viewpoint: initialViewpoint ) - .floatingPanel(isPresented: .constant(true), detent: .constant(.mid)) { + .floatingPanel(isPresented: .constant(true), detent: .constant(.half)) { SampleContent() } } diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 7790eb8eb..72099c7ee 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -47,7 +47,7 @@ struct UtilityNetworkTraceExampleView: View { self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } - .floatingPanel(isPresented: .constant(true), detent: .constant(.mid)) { + .floatingPanel(isPresented: .constant(true), detent: .constant(.half)) { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, map: map, diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index efdfb3ed7..7d5f2476d 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -48,7 +48,7 @@ struct FloatingPanel: View where Content: View { @State private var handleColor: Color = .defaultHandleColor /// The height of the content. - @State private var height: CGFloat = .infinity + @State private var height: CGFloat = .minHeight /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity @@ -96,11 +96,12 @@ struct FloatingPanel: View where Content: View { } .onChange(of: detent) { newValue in withAnimation { - switch newValue { - case .min: height = .minHeight + 75 - case .mid: height = geometryProxy.size.height / 2.0 - case .max: height = geometryProxy.size.height - 75 - } + height = heightWithDetent(given: geometryProxy.size.height) + } + } + .onAppear { + withAnimation { + height = heightWithDetent(given: geometryProxy.size.height) } } } @@ -128,6 +129,23 @@ struct FloatingPanel: View where Content: View { handleColor = .defaultHandleColor } } + + /// - Parameter height: Maximum available height + /// - Returns: Height given the current detent + func heightWithDetent(given height: CGFloat) -> CGFloat { + switch detent { + case .min: + return .minHeight + case .oneQuarter: + return height * 0.25 + case .half: + return height * 0.5 + case .threeQuarters: + return height * 0.75 + case .max: + return height + } + } } /// The "Handle" view of the floating panel. diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift index 80b4b3ac9..fd41d2a63 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift @@ -16,6 +16,8 @@ import Foundation /// <#Description#> public enum FloatingPanelDetent { case min - case mid + case oneQuarter + case half + case threeQuarters case max } From 1f345021af893e0b140b58b8d62721600996c1ef Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 11:45:08 -0700 Subject: [PATCH 1340/1722] Update FloatingPanel.swift --- .../FloatingPanel/FloatingPanel.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 7d5f2476d..f78cbf2c5 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -96,12 +96,12 @@ struct FloatingPanel: View where Content: View { } .onChange(of: detent) { newValue in withAnimation { - height = heightWithDetent(given: geometryProxy.size.height) + height = heightWithDetent } } .onAppear { withAnimation { - height = heightWithDetent(given: geometryProxy.size.height) + height = heightWithDetent } } } @@ -127,23 +127,25 @@ struct FloatingPanel: View where Content: View { } .onEnded { _ in handleColor = .defaultHandleColor + withAnimation { + height = heightWithDetent + } } } - /// - Parameter height: Maximum available height - /// - Returns: Height given the current detent - func heightWithDetent(given height: CGFloat) -> CGFloat { + /// - Returns: Displayable height given the current detent + var heightWithDetent: CGFloat { switch detent { case .min: return .minHeight case .oneQuarter: - return height * 0.25 + return maximumHeight * 0.25 case .half: - return height * 0.5 + return maximumHeight * 0.5 case .threeQuarters: - return height * 0.75 + return maximumHeight * 0.75 case .max: - return height + return maximumHeight } } } From 49a469a140e21346296d372ea20c3afb2ecdfede Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 13:54:10 -0500 Subject: [PATCH 1341/1722] Fix height calculation; documentation. --- .../Components/Popups/HTMLTextView.swift | 82 +++++++++++-------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 5007d4bbd..05a3cfed4 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -19,28 +19,23 @@ struct HTMLTextView: UIViewRepresentable { /// The user-defined HTML string. var userHTML: String = "" - /// The html string to dispay, including the header. + /// The HTML string to dispay, including the header. var displayHTML: String { // Set the initial scale to 1, don't allow user scaling. - // When we went to WKWebView from UIWebView, the content got smaller, - // this fixes that and also doesn't allow the user to pinch to zoom. + // This fixess small text with WKWebView and also doesn't allow the + // user to pinch to zoom. var header = "
" // Inject css in a head element to: // - word wrap long content such as urls // - set font family to default apple font // - set font size to subheadline - // - remove padding from the body. Add some margin to separate from the border of the webview. + // - remove padding from the body. Add some margin to separate from the + // border of the webview. // - limit images to a maximum width of 100% // - // Also wrap the passed html fragment inside + // Also wrap the passed HTML fragment inside // open/close tags so that these css styles will apply - // - // In order to make hyperlinks match our - // app tint we need to be passed a color - // and to dynamically inject it into the - // HTML/CSS - // header = header.appending(""" @@ -59,20 +54,22 @@ struct HTMLTextView: UIViewRepresentable { """) - // The final string is the header + userHTML + closing - return header.appending(userHTML.trimmingCharacters(in: .whitespacesAndNewlines)).appending("") + // The final string is the header + userHTML + "" + return header.appending( + userHTML.trimmingCharacters(in: .whitespacesAndNewlines) + ).appending("") } /// The height of the view, calculated in the `webView(didFinish:)` delegate method. - @Binding private var dynamicHeight: CGFloat - + @Binding private var height: CGFloat + /// Creates an `HTMLTextView`. /// - Parameters: - /// - html: The html string to be displayed. - /// - dynamicHeight: A binding to the calculated height of the `WKWebView`. - init(html: String, dynamicHeight: Binding) { + /// - html: The HTML string to be displayed. + /// - height: A binding to the calculated height of the `WKWebView`. + init(html: String, height: Binding) { userHTML = html - _dynamicHeight = dynamicHeight + _height = height } func makeUIView(context: Context) -> WKWebView { @@ -81,8 +78,10 @@ struct HTMLTextView: UIViewRepresentable { func updateUIView(_ uiView: WKWebView, context: Context) { uiView.isOpaque = false - // This is a case where we always want the background to be white regardless of light/dark mode. If the user wants to implement dark mode, - // within their HTML, the background of the HTML will be shown over this background. + // This is a case where we always want the background to be white + // regardless of light/dark mode. If the user wants to implement dark + // mode, within their HTML, the background of the HTML will be shown + // over this background. uiView.backgroundColor = .white uiView.scrollView.backgroundColor = .white uiView.scrollView.isScrollEnabled = false @@ -93,18 +92,27 @@ struct HTMLTextView: UIViewRepresentable { class Coordinator: NSObject, WKNavigationDelegate { var parent: HTMLTextView + /// A Boolean value specifying when content starts arriving for the main frame. private var hasCommitted = false - + + /// A Boolean value specifying whether we've calculated a height for the web view or not. + private var hasHeight = false + init(_ parent: HTMLTextView) { self.parent = parent } + // WKNavigationDelegate method invoked when content starts + // arriving for the main frame. public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { hasCommitted = true } // WKNavigationDelegate method for navigation actions. - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction + ) async -> WKNavigationActionPolicy { if navigationAction.navigationType == .linkActivated, (navigationAction.request.url?.scheme?.lowercased() == "http" || navigationAction.request.url?.scheme?.lowercased() == "https"), @@ -119,20 +127,30 @@ struct HTMLTextView: UIViewRepresentable { } } - // WKNavigationDelegate method where the size calculation happens. + // WKNavigationDelegate method invoked when a main frame navigation + // completes. This is where the height calculation happens. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){ webView.evaluateJavaScript("document.readyState") { [weak self] complete, _ in - guard complete != nil, let webView = webView else { - return - } + guard complete != nil, + let webView = webView, + self?.hasCommitted ?? false + else { return } + webView.evaluateJavaScript("document.body.scrollHeight") { height, _ in - // Pass the new height to the delegate so that is can change the - // cell height with performBatchUpdates - // - guard let height = height as? CGFloat else { + guard let self = self else { return } + guard let height = height as? CGFloat, + !self.hasHeight else { return } - self?.parent.dynamicHeight = height + // Set the new height, if we have one. + self.parent.height = height + + // With certain HTML strings, the JavaScript above kept + // getting called, with increasingly large heights. This + // prevents that from happening. As this block is only + // called after the `document.readyState` is "complete", + // this should be OK. + self.hasHeight = true } } } From b68d0d8ab503944b039044b77ac871a7ea3f9890 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 13:54:41 -0500 Subject: [PATCH 1342/1722] Cleanup; show progress until web view calculates it's height. --- .../Components/Popups/TextPopupElementView.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index b2cd25fe1..dff9440d1 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -26,11 +26,17 @@ struct TextPopupElementView: View { let roundedRect = RoundedRectangle(cornerRadius: 8) if !popupElement.text.isEmpty { ZStack { - HTMLTextView(html: popupElement.text, dynamicHeight: $webViewHeight) + HTMLTextView(html: popupElement.text, height: $webViewHeight) .clipShape(roundedRect) .frame(height: webViewHeight) - roundedRect - .stroke(Color.black, lineWidth: 1) + if webViewHeight == .zero { + // Show ProgressView until HTMLTextView has set the height. + ProgressView() + } + else { + roundedRect + .stroke(Color.black, lineWidth: 1) + } } } } From 1c28a801420881881e78838217594d03dea6a1c8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 12:08:09 -0700 Subject: [PATCH 1343/1722] Progress --- .../Components/FloatingPanel/FloatingPanel.swift | 16 +++++++++++++--- .../FloatingPanel/FloatingPanelDetent.swift | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index f78cbf2c5..ccc210340 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -128,13 +128,18 @@ struct FloatingPanel: View where Content: View { .onEnded { _ in handleColor = .defaultHandleColor withAnimation { - height = heightWithDetent + height = heightFor(detent: closestDetent) } } } - /// - Returns: Displayable height given the current detent - var heightWithDetent: CGFloat { + var closestDetent: FloatingPanelDetent { + return FloatingPanelDetent.allCases.min { d1, d2 in + abs(heightFor(detent: d1) - height) < abs(heightFor(detent: d2) - height) + } ?? .half + } + + func heightFor(detent: FloatingPanelDetent) -> CGFloat { switch detent { case .min: return .minHeight @@ -148,6 +153,11 @@ struct FloatingPanel: View where Content: View { return maximumHeight } } + + /// - Returns: Displayable height given the current detent + var heightWithDetent: CGFloat { + return heightFor(detent: detent) + } } /// The "Handle" view of the floating panel. diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift index fd41d2a63..4d22881dc 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift @@ -14,7 +14,7 @@ import Foundation /// <#Description#> -public enum FloatingPanelDetent { +public enum FloatingPanelDetent: CaseIterable { case min case oneQuarter case half From 61e27e79974c16e03f6078eb650f7285d4213743 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 12:44:07 -0700 Subject: [PATCH 1344/1722] Progress --- .../FloatingPanel/FloatingPanel.swift | 34 ++++++++----------- .../FloatingPanel/FloatingPanelDetent.swift | 4 +-- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index ccc210340..e6f59f1d2 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -24,10 +24,6 @@ import SwiftUI /// dedicated search panel. They will also be primarily simple containers /// that clients will fill with their own content. struct FloatingPanel: View where Content: View { - // Note: instead of the FloatingPanel being a view, it might be preferable - // to have it be a view modifier, similar to how SwiftUI doesn't have a - // SheetView, but a modifier that presents a sheet. - @Environment(\.horizontalSizeClass) private var horizontalSizeClass /// The content shown in the floating panel. @@ -35,15 +31,18 @@ struct FloatingPanel: View where Content: View { /// Creates a `FloatingPanel` /// - Parameter content: The view shown in the floating panel. - /// - Parameter detent: <#detent description#> + /// - Parameter detent: Controls the height of the panel. init( detent: Binding, @ViewBuilder content: () -> Content ) { self.content = content() - _detent = detent + _activeDetent = detent } + /// The detent that is currently set. + @Binding private var activeDetent: FloatingPanelDetent + /// The color of the handle. @State private var handleColor: Color = .defaultHandleColor @@ -53,9 +52,6 @@ struct FloatingPanel: View where Content: View { /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity - /// <#Description#> - @Binding var detent: FloatingPanelDetent - /// A Boolean value indicating whether the panel should be configured for a compact environment. private var isCompact: Bool { horizontalSizeClass == .compact @@ -94,14 +90,14 @@ struct FloatingPanel: View where Content: View { height = maximumHeight } } - .onChange(of: detent) { newValue in + .onChange(of: activeDetent) { _ in withAnimation { - height = heightWithDetent + height = heightFor(detent: activeDetent) } } .onAppear { withAnimation { - height = heightWithDetent + height = heightFor(detent: activeDetent) } } } @@ -133,12 +129,17 @@ struct FloatingPanel: View where Content: View { } } + /// The detent that would produce a height that is closest to the current height var closestDetent: FloatingPanelDetent { - return FloatingPanelDetent.allCases.min { d1, d2 in - abs(heightFor(detent: d1) - height) < abs(heightFor(detent: d2) - height) + let choices: [FloatingPanelDetent] = [.oneQuarter, .half, .threeQuarters] + return choices.min { + abs(heightFor(detent: $0) - height) < + abs(heightFor(detent: $1) - height) } ?? .half } + /// - Parameter detent: The detent to use when calculating height + /// - Returns: A height for the provided detent based on the current maximum height func heightFor(detent: FloatingPanelDetent) -> CGFloat { switch detent { case .min: @@ -153,11 +154,6 @@ struct FloatingPanel: View where Content: View { return maximumHeight } } - - /// - Returns: Displayable height given the current detent - var heightWithDetent: CGFloat { - return heightFor(detent: detent) - } } /// The "Handle" view of the floating panel. diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift index 4d22881dc..054da3c5e 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift @@ -13,8 +13,8 @@ import Foundation -/// <#Description#> -public enum FloatingPanelDetent: CaseIterable { +/// A value that represents a height where a sheet naturally rests. +public enum FloatingPanelDetent { case min case oneQuarter case half From 2bd0f952c3f4571fc816774b18eaa959750eb045 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 13:34:18 -0700 Subject: [PATCH 1345/1722] Progress --- .../FloatingPanel/FloatingPanel.swift | 22 +++++++------------ .../FloatingPanel/FloatingPanelDetent.swift | 8 +++---- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index e6f59f1d2..78a813672 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -62,7 +62,6 @@ struct FloatingPanel: View where Content: View { VStack { if isCompact { Handle(color: handleColor) - .gesture(drag) Divider() } content @@ -70,12 +69,12 @@ struct FloatingPanel: View where Content: View { if !isCompact { Divider() Handle(color: handleColor) - .gesture(drag) } } .padding([.top, .bottom], 10) .background(Color(uiColor: .systemGroupedBackground)) .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) + .gesture(drag) .shadow(radius: 10) .padding([.leading, .top, .trailing], isCompact ? 0 : 10) .padding([.bottom], isCompact ? 0 : 50) @@ -104,7 +103,7 @@ struct FloatingPanel: View where Content: View { } var drag: some Gesture { - DragGesture(minimumDistance: 0) + DragGesture(minimumDistance: 1) .onChanged { value in handleColor = .activeHandleColor let proposedHeight: CGFloat @@ -131,8 +130,7 @@ struct FloatingPanel: View where Content: View { /// The detent that would produce a height that is closest to the current height var closestDetent: FloatingPanelDetent { - let choices: [FloatingPanelDetent] = [.oneQuarter, .half, .threeQuarters] - return choices.min { + return FloatingPanelDetent.allCases.min { abs(heightFor(detent: $0) - height) < abs(heightFor(detent: $1) - height) } ?? .half @@ -142,16 +140,12 @@ struct FloatingPanel: View where Content: View { /// - Returns: A height for the provided detent based on the current maximum height func heightFor(detent: FloatingPanelDetent) -> CGFloat { switch detent { - case .min: - return .minHeight - case .oneQuarter: - return maximumHeight * 0.25 + case .summary: + return maximumHeight * 0.15 case .half: - return maximumHeight * 0.5 - case .threeQuarters: - return maximumHeight * 0.75 - case .max: - return maximumHeight + return maximumHeight * 0.4 + case .full: + return maximumHeight * 0.90 } } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift index 054da3c5e..e9d4c2f43 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift @@ -14,10 +14,8 @@ import Foundation /// A value that represents a height where a sheet naturally rests. -public enum FloatingPanelDetent { - case min - case oneQuarter +public enum FloatingPanelDetent: CaseIterable { + case summary case half - case threeQuarters - case max + case full } From 39a392a4ec4c3207d9f3d91073a6aef221fa5f94 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 13:40:33 -0700 Subject: [PATCH 1346/1722] 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 d947c1d93..e9a54fd01 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -139,7 +139,7 @@ public struct UtilityNetworkTrace: View { Section("Starting Points") { Button { currentActivity = .creatingTrace(.addingStartingPoints) - activeDetent = .min + activeDetent = .summary } label: { Text("Add new starting point") } From 1647d8f5552a47ed577ecb6937c0d73128f5f98e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 13:41:30 -0700 Subject: [PATCH 1347/1722] Update FloatingPanel.swift --- .../Components/FloatingPanel/FloatingPanel.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 78a813672..2b7234212 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -62,19 +62,20 @@ struct FloatingPanel: View where Content: View { VStack { if isCompact { Handle(color: handleColor) + .gesture(drag) Divider() } content .frame(minHeight: .minHeight, maxHeight: height) if !isCompact { Divider() + .gesture(drag) Handle(color: handleColor) } } .padding([.top, .bottom], 10) .background(Color(uiColor: .systemGroupedBackground)) .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) - .gesture(drag) .shadow(radius: 10) .padding([.leading, .top, .trailing], isCompact ? 0 : 10) .padding([.bottom], isCompact ? 0 : 50) @@ -103,7 +104,7 @@ struct FloatingPanel: View where Content: View { } var drag: some Gesture { - DragGesture(minimumDistance: 1) + DragGesture() .onChanged { value in handleColor = .activeHandleColor let proposedHeight: CGFloat From 100febbedd8f9d5ffe2621bc78f580237c7d7f39 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 14:02:47 -0700 Subject: [PATCH 1348/1722] Update FloatingPanel.swift --- .../Components/FloatingPanel/FloatingPanel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 2b7234212..07572630e 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -69,8 +69,8 @@ struct FloatingPanel: View where Content: View { .frame(minHeight: .minHeight, maxHeight: height) if !isCompact { Divider() - .gesture(drag) Handle(color: handleColor) + .gesture(drag) } } .padding([.top, .bottom], 10) @@ -104,7 +104,7 @@ struct FloatingPanel: View where Content: View { } var drag: some Gesture { - DragGesture() + DragGesture(minimumDistance: 0) .onChanged { value in handleColor = .activeHandleColor let proposedHeight: CGFloat @@ -142,7 +142,7 @@ struct FloatingPanel: View where Content: View { func heightFor(detent: FloatingPanelDetent) -> CGFloat { switch detent { case .summary: - return maximumHeight * 0.15 + return max(.minHeight, maximumHeight * 0.15) case .half: return maximumHeight * 0.4 case .full: From df9303596054866300c2edd1e75938ac6b8004a5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 16:25:02 -0500 Subject: [PATCH 1349/1722] Initial MediaPopupElementView --- .../Popups/PopupMedia/ChartMediaView.swift | 8 ++ .../Popups/PopupMedia/ImageMediaView.swift | 92 +++++++++++++++++++ .../Popups/PopupMedia/PopupMediaFooter.swift | 26 ++++++ .../Components/Popups/PopupView.swift | 5 +- .../Popups/QuickLookViewController.swift | 51 ++++++++++ 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift new file mode 100644 index 000000000..d5039c5ef --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Mark on 7/29/22. +// + +import Foundation diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift new file mode 100644 index 000000000..2507f8b73 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -0,0 +1,92 @@ +// 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 +struct ImageMediaView: View { + let popupMedia: PopupMedia + // TODO: pass in url so we're sure it's available + + // TODO: general: clean up main MediaPopupElementView and create sub view files. + @State private var showingFullScreen = false + @State private var fullScreenURL: URL! + + var body: some View { + VStack(alignment: .leading) { + // Use popupMedia.sourceURL for thumbnail + // Use popupMedia.linkURL for full-screen link + Group { + if let sourceURL = popupMedia.value?.sourceURL { + AsyncImage(url: sourceURL) { phase in + if let image = phase.image { + image // Displays the loaded image. + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxHeight: 250) + .clipped() // <- need maxWidth as well for this to work. Use GeometryReader somewhere above. + } else if let error = phase.error { + Text("sourceURL: \(sourceURL.absoluteString); An error occurred loading the image: \(error.localizedDescription)") // Indicates an error. + } else { + ProgressView() // Acts as a placeholder. + } + } + .onTapGesture { + showingFullScreen = true + } + } + else { + // TODO: better error (if any at all) + Text("No URL for the image.") + } + } + + PopupMediaFooter(popupMedia: popupMedia) + } + .sheet(isPresented: $showingFullScreen) { + if let url = popupMedia.value?.sourceURL { + VStack { + HStack { + Button("Done") { + showingFullScreen = false + } + Spacer() + } + .padding() + AsyncImage(url: url) { phase in + if let image = phase.image { + // Displays the loaded image. + image + .resizable() + .aspectRatio(contentMode: .fit) + } else if phase.error != nil { + // Displays an error image. + Image(systemName: "exclamationmark.circle") + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + } else { + // Display the progress view until image loads. + ProgressView() + } + } + .padding() + .onTapGesture { + if let url = popupMedia.value?.linkURL { + UIApplication.shared.open(url) + } + } + Spacer() + } + } + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift new file mode 100644 index 000000000..759a10d33 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift @@ -0,0 +1,26 @@ +// 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 + +struct PopupMediaFooter: View { + let popupMedia: PopupMedia + var body: some View { + Text(popupMedia.title) + .font(.body) + Text(popupMedia.caption) + .font(.subheadline) + .foregroundColor(.secondary) + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index eea73ccee..dfc5df6fd 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -78,9 +78,8 @@ public struct PopupView: View { .font(.caption) case let popupElement as FieldsPopupElement: FieldsPopupElementView(popupElement: popupElement) - case is MediaPopupElement: - Text("MediaPopupElementView implementation coming soon.") - .font(.caption) + case let popupElement as MediaPopupElement: + MediaPopupElementView(popupElement: popupElement) case let popupElement as TextPopupElement: TextPopupElementView(popupElement: popupElement) default: diff --git a/Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift b/Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift new file mode 100644 index 000000000..5cb3f73d6 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift @@ -0,0 +1,51 @@ +// 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 +import QuickLook + +struct QuickLookViewController: UIViewControllerRepresentable { + let url: URL + + func makeUIViewController(context: Context) -> QLPreviewController { + let controller = QLPreviewController() + return controller + } + + func makeCoordinator() -> Coordinator { + return Coordinator(parent: self) + } + + func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) {} + + + class Coordinator: QLPreviewControllerDataSource { + let parent: QuickLookViewController + + init(parent: QuickLookViewController) { + self.parent = parent + } + + func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + return 1 + } + + func previewController( + _ controller: QLPreviewController, + previewItemAt index: Int + ) -> QLPreviewItem { + return parent.url as NSURL + } + } +} From 94b4e0222022b2caf3c56f25c13d2253980b2228 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 16:25:24 -0500 Subject: [PATCH 1350/1722] Initial view --- .../Popups/MediaPopupElementView.swift | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift new file mode 100644 index 000000000..78f30e288 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -0,0 +1,78 @@ +// 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 +import Charts + +/// A view displaying a `MediaPopupElement`. +struct MediaPopupElementView: View { + /// The `PopupElement` to display. + var popupElement: MediaPopupElement + + var body: some View { + PopupElementHeader( + title: popupElement.title, + description: popupElement.description + ) + .padding([.bottom], 4) + PopupMediaView(popupMedia: popupElement.media) + } + + /// A view to display an array of `PopupMedia`. + struct PopupMediaView: View { + /// The popup media to display. + let popupMedia: [PopupMedia] + + var body: some View { + TabView { + ForEach(popupMedia) { media in + Group { + switch media.kind { + case .image: + ImageMediaView(popupMedia: media) + case .barChart, + .columnChart, + .lineChart, + .pieChart: + ChartMediaView(popupMedia: media) + default: + EmptyView() + } + } + .padding( + EdgeInsets( + top: 8, + leading: 8, + bottom: popupMedia.count > 1 ? 48: 8, + trailing: 8 + ) + ) + .tabItem { + Text(media.title) + } + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: popupMedia.count > 1 ? .always : .never)) + .indexViewStyle( + PageIndexViewStyle( + backgroundDisplayMode: .always + ) + ) + .frame(maxWidth: .infinity, minHeight: 250, maxHeight: .infinity) +// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity) + } + } +} + +extension PopupMedia: Identifiable {} From 17d74cbdf877634a2f8e83c25c56397f835e8265 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 14:28:33 -0700 Subject: [PATCH 1351/1722] Progress --- .../FloatingPanel/FloatingPanel.swift | 8 +++++++- .../FloatingPanel/FloatingPanelModifier.swift | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 07572630e..0be873493 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -29,13 +29,19 @@ struct FloatingPanel: View where Content: View { /// The content shown in the floating panel. let content: Content + /// <#Description#> + let backgroundColor: Color + /// Creates a `FloatingPanel` /// - Parameter content: The view shown in the floating panel. /// - Parameter detent: Controls the height of the panel. + /// - Parameter backgroundColor: <#backgroundColor description#> init( + backgroundColor: Color, detent: Binding, @ViewBuilder content: () -> Content ) { + self.backgroundColor = backgroundColor self.content = content() _activeDetent = detent } @@ -74,7 +80,7 @@ struct FloatingPanel: View where Content: View { } } .padding([.top, .bottom], 10) - .background(Color(uiColor: .systemGroupedBackground)) + .background(backgroundColor) .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) .shadow(radius: 10) .padding([.leading, .top, .trailing], isCompact ? 0 : 10) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index 1ea6f8bf9..60ef91a00 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -20,15 +20,21 @@ public extension View { /// - Parameters: /// - isPresented: <#isPresented description#> /// - content: <#content description#> + /// - horizontalAlignment: <#horizontalAlignment description#> + /// - detent: <#detent description#> /// - Returns: <#description#> func floatingPanel( isPresented: Binding, + backgroundColor: Color = Color(uiColor: .systemBackground), + horizontalAlignment: HorizontalAlignment = .trailing, detent: Binding, _ content: @escaping () -> Content ) -> some View where Content: View { modifier( FloatingPanelModifier( isPresented: isPresented, + backgroundColor: backgroundColor, + horizontalAlignment: horizontalAlignment, detent: detent, innerContent: content() ) @@ -43,6 +49,12 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont /// <#Description#> @Binding var isPresented: Bool + /// <#Description#> + let backgroundColor: Color + + /// <#Description#> + let horizontalAlignment: HorizontalAlignment + /// <#Description#> var detent: Binding @@ -51,8 +63,11 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont func body(content: Content) -> some View { content - .overlay(alignment: .trailing) { - FloatingPanel(detent: detent) { + .overlay(alignment: Alignment(horizontal: horizontalAlignment, vertical: .top)) { + FloatingPanel( + backgroundColor: backgroundColor, + detent: detent + ) { innerContent } .edgesIgnoringSafeArea(.bottom) From 02c35d111940f8fc6eccd3a4b12490b65191e713 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 14:30:16 -0700 Subject: [PATCH 1352/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index e9a54fd01..425edbb7b 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -342,6 +342,7 @@ public struct UtilityNetworkTrace: View { Color.clear .floatingPanel( isPresented: .constant(true), + backgroundColor: Color(uiColor: .systemGroupedBackground), detent: $activeDetent ) { VStack { From d94f9c7b6576f45d4b6180ae995d57c8a230cbec Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 14:38:17 -0700 Subject: [PATCH 1353/1722] Progress --- .../Components/FloatingPanel/FloatingPanel.swift | 3 ++- .../Components/FloatingPanel/FloatingPanelModifier.swift | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 07572630e..b2009907e 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -25,6 +25,7 @@ import SwiftUI /// that clients will fill with their own content. struct FloatingPanel: View where Content: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass /// The content shown in the floating panel. let content: Content @@ -54,7 +55,7 @@ struct FloatingPanel: View where Content: View { /// A Boolean value indicating whether the panel should be configured for a compact environment. private var isCompact: Bool { - horizontalSizeClass == .compact + horizontalSizeClass == .compact && verticalSizeClass == .regular } public var body: some View { diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index 1ea6f8bf9..416cb0ce3 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -39,6 +39,12 @@ public extension View { /// <#Description#> private struct FloatingPanelModifier: ViewModifier where InnerContent: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass + + /// A Boolean value indicating whether the environment is compact. + private var isCompact: Bool { + horizontalSizeClass == .compact && verticalSizeClass == .regular + } /// <#Description#> @Binding var isPresented: Bool @@ -56,7 +62,7 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont innerContent } .edgesIgnoringSafeArea(.bottom) - .frame(maxWidth: horizontalSizeClass == .regular ? 360 : .infinity) + .frame(maxWidth: isCompact ? .infinity : 400) } } } From 687e3984eb5c715a0856f92740bd78d976d37421 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 15:03:50 -0700 Subject: [PATCH 1354/1722] Progress --- Examples/Examples/FloatingPanelExampleView.swift | 2 +- .../Examples/UtilityNetworkTraceExampleView.swift | 2 +- .../FloatingPanel/FloatingPanelModifier.swift | 15 +++++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 72f09e59c..6d22ec01e 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -28,7 +28,7 @@ struct FloatingPanelExampleView: View { map: map, viewpoint: initialViewpoint ) - .floatingPanel(isPresented: .constant(true), detent: .constant(.half)) { + .floatingPanel(detent: .constant(.half), isPresented: .constant(true)) { SampleContent() } } diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 72099c7ee..ddddc5e03 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -47,7 +47,7 @@ struct UtilityNetworkTraceExampleView: View { self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } - .floatingPanel(isPresented: .constant(true), detent: .constant(.half)) { + .floatingPanel(detent: .constant(.half), isPresented: .constant(true)) { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, map: map, diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index 416cb0ce3..a0079cf20 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -18,18 +18,22 @@ public extension View { /// environments and a popover otherwise. /// The resulting view allows for interaction with background contents. /// - Parameters: + /// - detent: <#detent description#> /// - isPresented: <#isPresented description#> + /// - maxWidth: <#maxWidth description#> /// - content: <#content description#> /// - Returns: <#description#> func floatingPanel( - isPresented: Binding, detent: Binding, + isPresented: Binding, + maxWidth: CGFloat = 400, _ content: @escaping () -> Content ) -> some View where Content: View { modifier( FloatingPanelModifier( - isPresented: isPresented, detent: detent, + isPresented: isPresented, + maxWidth: maxWidth, innerContent: content() ) ) @@ -46,11 +50,14 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont horizontalSizeClass == .compact && verticalSizeClass == .regular } + /// <#Description#> + let detent: Binding + /// <#Description#> @Binding var isPresented: Bool /// <#Description#> - var detent: Binding + let maxWidth: CGFloat /// <#Description#> let innerContent: InnerContent @@ -62,7 +69,7 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont innerContent } .edgesIgnoringSafeArea(.bottom) - .frame(maxWidth: isCompact ? .infinity : 400) + .frame(maxWidth: isCompact ? .infinity : maxWidth) } } } From 5b775ae8f48871781f3a7b96fb11b4d179b56e99 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 15:31:38 -0700 Subject: [PATCH 1355/1722] Merge in latest --- .../FloatingPanel/FloatingPanel.swift | 7 ++++++- .../FloatingPanel/FloatingPanelModifier.swift | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index b2009907e..1c71aa801 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -27,6 +27,9 @@ struct FloatingPanel: View where Content: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass + /// <#Description#> + let backgroundColor: Color + /// The content shown in the floating panel. let content: Content @@ -34,9 +37,11 @@ struct FloatingPanel: View where Content: View { /// - Parameter content: The view shown in the floating panel. /// - Parameter detent: Controls the height of the panel. init( + backgroundColor: Color, detent: Binding, @ViewBuilder content: () -> Content ) { + self.backgroundColor = backgroundColor self.content = content() _activeDetent = detent } @@ -75,7 +80,7 @@ struct FloatingPanel: View where Content: View { } } .padding([.top, .bottom], 10) - .background(Color(uiColor: .systemGroupedBackground)) + .background(backgroundColor) .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) .shadow(radius: 10) .padding([.leading, .top, .trailing], isCompact ? 0 : 10) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index a0079cf20..ff7fc1fbf 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -24,14 +24,18 @@ public extension View { /// - content: <#content description#> /// - Returns: <#description#> func floatingPanel( + backgroundColor: Color = Color(uiColor: .systemBackground), detent: Binding, + horizontalAlignment: HorizontalAlignment = .trailing, isPresented: Binding, maxWidth: CGFloat = 400, _ content: @escaping () -> Content ) -> some View where Content: View { modifier( FloatingPanelModifier( + backgroundColor: backgroundColor, detent: detent, + horizontalAlignment: horizontalAlignment, isPresented: isPresented, maxWidth: maxWidth, innerContent: content() @@ -50,9 +54,15 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont horizontalSizeClass == .compact && verticalSizeClass == .regular } + /// <#Description#> + let backgroundColor: Color + /// <#Description#> let detent: Binding + /// <#Description#> + let horizontalAlignment: HorizontalAlignment + /// <#Description#> @Binding var isPresented: Bool @@ -64,8 +74,11 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont func body(content: Content) -> some View { content - .overlay(alignment: .trailing) { - FloatingPanel(detent: detent) { + .overlay(alignment: Alignment(horizontal: horizontalAlignment, vertical: .top)) { + FloatingPanel( + backgroundColor: backgroundColor, + detent: detent + ) { innerContent } .edgesIgnoringSafeArea(.bottom) From 2ef0f50ffc433ae26cb0ad0fa7a76d6d04158cdb Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 16:27:09 -0700 Subject: [PATCH 1356/1722] Default detent --- Examples/Examples/FloatingPanelExampleView.swift | 2 +- Examples/Examples/UtilityNetworkTraceExampleView.swift | 2 +- .../Components/FloatingPanel/FloatingPanelModifier.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/FloatingPanelExampleView.swift b/Examples/Examples/FloatingPanelExampleView.swift index 6d22ec01e..b417926c5 100644 --- a/Examples/Examples/FloatingPanelExampleView.swift +++ b/Examples/Examples/FloatingPanelExampleView.swift @@ -28,7 +28,7 @@ struct FloatingPanelExampleView: View { map: map, viewpoint: initialViewpoint ) - .floatingPanel(detent: .constant(.half), isPresented: .constant(true)) { + .floatingPanel(isPresented: .constant(true)) { SampleContent() } } diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index ddddc5e03..85e1386b6 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -47,7 +47,7 @@ struct UtilityNetworkTraceExampleView: View { self.mapPoint = mapPoint self.mapViewProxy = mapViewProxy } - .floatingPanel(detent: .constant(.half), isPresented: .constant(true)) { + .floatingPanel(isPresented: .constant(true)) { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, map: map, diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index ff7fc1fbf..f29fa1ab0 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -25,7 +25,7 @@ public extension View { /// - Returns: <#description#> func floatingPanel( backgroundColor: Color = Color(uiColor: .systemBackground), - detent: Binding, + detent: Binding = .constant(.half), horizontalAlignment: HorizontalAlignment = .trailing, isPresented: Binding, maxWidth: CGFloat = 400, From 0c04f2861d797a9203949d7f7ffc3f44af49f99b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 17:07:56 -0700 Subject: [PATCH 1357/1722] Progress --- .../FloatingPanel/FloatingPanel.swift | 20 +++++++++++++++---- .../FloatingPanel/FloatingPanelModifier.swift | 7 ++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 1c71aa801..18c0c77ba 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -34,16 +34,20 @@ struct FloatingPanel: View where Content: View { let content: Content /// Creates a `FloatingPanel` + /// - Parameter backgroundColor: <#backgroundColor description#> /// - Parameter content: The view shown in the floating panel. /// - Parameter detent: Controls the height of the panel. + /// - Parameter isPresented: <#isPresented description#> init( backgroundColor: Color, detent: Binding, + isPresented: Binding, @ViewBuilder content: () -> Content ) { self.backgroundColor = backgroundColor self.content = content() _activeDetent = detent + _isPresented = isPresented } /// The detent that is currently set. @@ -55,9 +59,12 @@ struct FloatingPanel: View where Content: View { /// The height of the content. @State private var height: CGFloat = .minHeight + /// <#Description#> + @Binding private var isPresented: Bool + /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity - + /// A Boolean value indicating whether the panel should be configured for a compact environment. private var isCompact: Bool { horizontalSizeClass == .compact && verticalSizeClass == .regular @@ -66,14 +73,14 @@ struct FloatingPanel: View where Content: View { public var body: some View { GeometryReader { geometryProxy in VStack { - if isCompact { + if isCompact && isPresented { Handle(color: handleColor) .gesture(drag) Divider() } content - .frame(minHeight: .minHeight, maxHeight: height) - if !isCompact { + .frame(minHeight: .zero, maxHeight: height) + if !isCompact && isPresented { Divider() Handle(color: handleColor) .gesture(drag) @@ -83,6 +90,7 @@ struct FloatingPanel: View where Content: View { .background(backgroundColor) .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) .shadow(radius: 10) + .opacity(isPresented ? 1.0 : .zero) .padding([.leading, .top, .trailing], isCompact ? 0 : 10) .padding([.bottom], isCompact ? 0 : 50) .frame( @@ -101,11 +109,15 @@ struct FloatingPanel: View where Content: View { height = heightFor(detent: activeDetent) } } + .onChange(of: isPresented) { + height = $0 ? heightFor(detent: activeDetent) : .zero + } .onAppear { withAnimation { height = heightFor(detent: activeDetent) } } + .animation(.default, value: isPresented) } } diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index f29fa1ab0..fb2a87138 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -27,7 +27,7 @@ public extension View { backgroundColor: Color = Color(uiColor: .systemBackground), detent: Binding = .constant(.half), horizontalAlignment: HorizontalAlignment = .trailing, - isPresented: Binding, + isPresented: Binding = .constant(true), maxWidth: CGFloat = 400, _ content: @escaping () -> Content ) -> some View where Content: View { @@ -64,7 +64,7 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont let horizontalAlignment: HorizontalAlignment /// <#Description#> - @Binding var isPresented: Bool + let isPresented: Binding /// <#Description#> let maxWidth: CGFloat @@ -77,7 +77,8 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont .overlay(alignment: Alignment(horizontal: horizontalAlignment, vertical: .top)) { FloatingPanel( backgroundColor: backgroundColor, - detent: detent + detent: detent, + isPresented: isPresented ) { innerContent } From 0cf61b20c724f75e754886707dd5310243b48830 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 17:14:16 -0700 Subject: [PATCH 1358/1722] Add doc --- .../Components/FloatingPanel/FloatingPanel.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 18c0c77ba..99a0c0959 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -27,17 +27,17 @@ struct FloatingPanel: View where Content: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass - /// <#Description#> + /// The background color of the floating panel. let backgroundColor: Color /// The content shown in the floating panel. let content: Content /// Creates a `FloatingPanel` - /// - Parameter backgroundColor: <#backgroundColor description#> - /// - Parameter content: The view shown in the floating panel. + /// - Parameter backgroundColor: The background color of the floating panel. /// - Parameter detent: Controls the height of the panel. - /// - Parameter isPresented: <#isPresented description#> + /// - Parameter isPresented: A Boolean value indicating if the view is presented. + /// - Parameter content: The view shown in the floating panel. init( backgroundColor: Color, detent: Binding, @@ -59,7 +59,7 @@ struct FloatingPanel: View where Content: View { /// The height of the content. @State private var height: CGFloat = .minHeight - /// <#Description#> + /// A Boolean value indicating if the view is presented. @Binding private var isPresented: Bool /// The maximum allowed height of the content. From 51950f3b6aaf806c358ed3e2823686c3b9f64306 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 29 Jul 2022 17:33:43 -0700 Subject: [PATCH 1359/1722] Finish doc --- .../FloatingPanel/FloatingPanel.swift | 4 +- .../FloatingPanel/FloatingPanelModifier.swift | 49 ++++++++++++------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 99a0c0959..5c8042322 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -50,7 +50,7 @@ struct FloatingPanel: View where Content: View { _isPresented = isPresented } - /// The detent that is currently set. + /// A binding to the currently selected detent. @Binding private var activeDetent: FloatingPanelDetent /// The color of the handle. @@ -59,7 +59,7 @@ struct FloatingPanel: View where Content: View { /// The height of the content. @State private var height: CGFloat = .minHeight - /// A Boolean value indicating if the view is presented. + /// A binding to a Boolean value that determines whether the view is presented. @Binding private var isPresented: Bool /// The maximum allowed height of the content. diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index fb2a87138..14653ea9c 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -14,15 +14,28 @@ import SwiftUI public extension View { - /// Presents a dynamic view with a presentation style similar to that of a sheet in compact - /// environments and a popover otherwise. - /// The resulting view allows for interaction with background contents. + /// A floating panel is a view that overlays a view and supplies view-related + /// content. For a map view, for instance, it could display a legend, bookmarks, search results, etc.. + /// Apple Maps, Google Maps, Windows 10, and Collector have floating panel + /// implementations, sometimes referred to as a "bottom sheet". + /// + /// Floating panels are non-modal and can be transient, only displaying + /// information for a short period of time like identify results, + /// or persistent, where the information is always displayed, for example a + /// dedicated search panel. They will also be primarily simple containers + /// that clients will fill with their own content. + /// + /// The floating panel allows for interaction with background contents, unlike native sheets or popovers. + /// /// - Parameters: - /// - detent: <#detent description#> - /// - isPresented: <#isPresented description#> - /// - maxWidth: <#maxWidth description#> - /// - content: <#content description#> - /// - Returns: <#description#> + /// - backgroundColor: The background color of the floating panel. + /// - detent: A binding to the currently selected detent. + /// - horizontalAlignment: The horizontal alignment of the floating panel. + /// - isPresented: A binding to a Boolean value that determines whether the view is presented. + /// - maxWidth: The maximum width of the floating panel. + /// - content: A closure that returns the content of the floating panel. + /// - Returns: A dynamic view with a presentation style similar to that of a sheet in compact + /// environments and a popover otherwise. func floatingPanel( backgroundColor: Color = Color(uiColor: .systemBackground), detent: Binding = .constant(.half), @@ -38,13 +51,13 @@ public extension View { horizontalAlignment: horizontalAlignment, isPresented: isPresented, maxWidth: maxWidth, - innerContent: content() + content: content() ) ) } } -/// <#Description#> +/// Overlays a floating panel on the parent content. private struct FloatingPanelModifier: ViewModifier where InnerContent: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -54,23 +67,23 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont horizontalSizeClass == .compact && verticalSizeClass == .regular } - /// <#Description#> + /// The background color of the floating panel. let backgroundColor: Color - /// <#Description#> + /// A binding to the currently selected detent. let detent: Binding - /// <#Description#> + /// The horizontal alignment of the floating panel. let horizontalAlignment: HorizontalAlignment - /// <#Description#> + /// A binding to a Boolean value that determines whether the view is presented. let isPresented: Binding - /// <#Description#> + /// The maximum width of the floating panel. let maxWidth: CGFloat - /// <#Description#> - let innerContent: InnerContent + /// The content to be displayed within the floating panel. + let content: InnerContent func body(content: Content) -> some View { content @@ -80,7 +93,7 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont detent: detent, isPresented: isPresented ) { - innerContent + content } .edgesIgnoringSafeArea(.bottom) .frame(maxWidth: isCompact ? .infinity : maxWidth) From 094b869c32e65da390651ff5f5834181a4de20a0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 20:10:59 -0500 Subject: [PATCH 1360/1722] fix sizing and padding, documentation, new full screen view for image media. --- .../Popups/MediaPopupElementView.swift | 37 +++----- .../Popups/PopupMedia/ChartMediaView.swift | 43 +++++++-- .../PopupMedia/FullScreenImageView.swift | 74 +++++++++++++++ .../Popups/PopupMedia/ImageMediaView.swift | 94 +++++++------------ .../Popups/PopupMedia/PopupMediaFooter.swift | 20 +++- 5 files changed, 173 insertions(+), 95 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 78f30e288..20c9a6c86 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -19,7 +19,7 @@ import Charts struct MediaPopupElementView: View { /// The `PopupElement` to display. var popupElement: MediaPopupElement - + var body: some View { PopupElementHeader( title: popupElement.title, @@ -29,11 +29,14 @@ struct MediaPopupElementView: View { PopupMediaView(popupMedia: popupElement.media) } - /// A view to display an array of `PopupMedia`. + /// A view displaying an array of `PopupMedia`. struct PopupMediaView: View { /// The popup media to display. let popupMedia: [PopupMedia] - + + /// The width of the view content. + @State private var viewWidth: CGFloat = .zero + var body: some View { TabView { ForEach(popupMedia) { media in @@ -41,36 +44,24 @@ struct MediaPopupElementView: View { switch media.kind { case .image: ImageMediaView(popupMedia: media) - case .barChart, - .columnChart, - .lineChart, - .pieChart: + case .barChart, .columnChart, .lineChart, .pieChart: ChartMediaView(popupMedia: media) default: EmptyView() } } - .padding( - EdgeInsets( - top: 8, - leading: 8, - bottom: popupMedia.count > 1 ? 48: 8, - trailing: 8 - ) - ) + .padding([.bottom], popupMedia.count > 1 ? 48: 8) .tabItem { Text(media.title) } } } - .tabViewStyle(PageTabViewStyle(indexDisplayMode: popupMedia.count > 1 ? .always : .never)) - .indexViewStyle( - PageIndexViewStyle( - backgroundDisplayMode: .always - ) - ) - .frame(maxWidth: .infinity, minHeight: 250, maxHeight: .infinity) -// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity) + .tabViewStyle(PageTabViewStyle()) + .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) + .frame(height: viewWidth * 0.75) + .onSizeChange { + viewWidth = $0.width + } } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift index d5039c5ef..993fb324f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift @@ -1,8 +1,35 @@ -// -// File.swift -// -// -// Created by Mark on 7/29/22. -// - -import Foundation +// 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 view displaying chart popup media. +struct ChartMediaView: View { + /// The popup media to display. + let popupMedia: PopupMedia + + var body: some View { + ZStack { + VStack(alignment: .center) { + Image(systemName: "chart.bar") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + Text("Charts coming soon!") + } + RoundedRectangle(cornerRadius: 8) + .stroke(.black, lineWidth: 1) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift new file mode 100644 index 000000000..de85ba643 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift @@ -0,0 +1,74 @@ +// 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 view displaying a popup media image in full screen. +struct FullScreenImageView: View { + /// The popup media to display. + let popupMedia: PopupMedia + + /// The sourceURL of the image media. + let sourceURL: URL + + /// A Boolean value specifying whether the media should be shown full screen. + @Binding var showingFullScreen: Bool + + var body: some View { + VStack() { + HStack { + Button("Done") { + showingFullScreen = false + } + Spacer() + } + Text("\(popupMedia.title)") + .font(.title2) + AsyncImage(url: sourceURL) { phase in + if let image = phase.image { + // Displays the loaded image. + image + .resizable() + .aspectRatio(contentMode: .fit) + } else if phase.error != nil { + // Displays an error image. + HStack { + Image(systemName: "exclamationmark.circle") + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + Text("An error occurred loading the image.") + } + .padding([.top]) + } else { + // Display the progress view until image loads. + ProgressView() + } + } + .onTapGesture { + if let url = popupMedia.value?.linkURL { + UIApplication.shared.open(url) + } + } + if popupMedia.value?.linkURL != nil { + HStack { + Text("Tap on the image for more information.") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + } + } + Spacer() + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 2507f8b73..6c179b89d 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -13,79 +13,55 @@ import SwiftUI import ArcGIS + +/// A view displaying image popup media. struct ImageMediaView: View { + /// The popup media to display. let popupMedia: PopupMedia - // TODO: pass in url so we're sure it's available - // TODO: general: clean up main MediaPopupElementView and create sub view files. + /// A Boolean value specifying whether the media should be shown full screen. @State private var showingFullScreen = false - @State private var fullScreenURL: URL! var body: some View { - VStack(alignment: .leading) { - // Use popupMedia.sourceURL for thumbnail - // Use popupMedia.linkURL for full-screen link - Group { - if let sourceURL = popupMedia.value?.sourceURL { - AsyncImage(url: sourceURL) { phase in - if let image = phase.image { - image // Displays the loaded image. - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxHeight: 250) - .clipped() // <- need maxWidth as well for this to work. Use GeometryReader somewhere above. - } else if let error = phase.error { - Text("sourceURL: \(sourceURL.absoluteString); An error occurred loading the image: \(error.localizedDescription)") // Indicates an error. - } else { - ProgressView() // Acts as a placeholder. + VStack { + if let sourceURL = popupMedia.value?.sourceURL { + AsyncImage(url: sourceURL) { phase in + if let image = phase.image { + // Display the loaded image. + image + .resizable() + .aspectRatio(contentMode: .fit) + } else if phase.error != nil { + // Display an error message. + HStack { + Image(systemName: "exclamationmark.circle") + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + Text("An error occurred loading the image.") } - } - .onTapGesture { - showingFullScreen = true + } else { + // Display a placeholder while the image is loading. + ProgressView() } } - else { - // TODO: better error (if any at all) - Text("No URL for the image.") + .onTapGesture { + showingFullScreen = true } + .padding() + } + HStack { + PopupMediaFooter(popupMedia: popupMedia) + Spacer() } - - PopupMediaFooter(popupMedia: popupMedia) } .sheet(isPresented: $showingFullScreen) { if let url = popupMedia.value?.sourceURL { - VStack { - HStack { - Button("Done") { - showingFullScreen = false - } - Spacer() - } - .padding() - AsyncImage(url: url) { phase in - if let image = phase.image { - // Displays the loaded image. - image - .resizable() - .aspectRatio(contentMode: .fit) - } else if phase.error != nil { - // Displays an error image. - Image(systemName: "exclamationmark.circle") - .aspectRatio(contentMode: .fit) - .foregroundColor(.red) - } else { - // Display the progress view until image loads. - ProgressView() - } - } - .padding() - .onTapGesture { - if let url = popupMedia.value?.linkURL { - UIApplication.shared.open(url) - } - } - Spacer() - } + FullScreenImageView( + popupMedia: popupMedia, + sourceURL: url, + showingFullScreen: $showingFullScreen + ) + .padding() } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift index 759a10d33..e750facf4 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift @@ -14,13 +14,23 @@ import SwiftUI import ArcGIS +/// A view displaying details for popup media. struct PopupMediaFooter: View { + /// The popup media to display. let popupMedia: PopupMedia + var body: some View { - Text(popupMedia.title) - .font(.body) - Text(popupMedia.caption) - .font(.subheadline) - .foregroundColor(.secondary) + VStack(alignment: .leading) { + if !popupMedia.title.isEmpty { + Text(popupMedia.title) + .font(.body) + } + + if !popupMedia.caption.isEmpty { + Text(popupMedia.caption) + .font(.subheadline) + .foregroundColor(.secondary) + } + } } } From 7b45e0bb496220fa8fd99ec098106fce3fa6d2ea Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 29 Jul 2022 20:11:42 -0500 Subject: [PATCH 1361/1722] Don't need to import Charts here. --- .../ArcGISToolkit/Components/Popups/MediaPopupElementView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 20c9a6c86..082f82fae 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -13,7 +13,6 @@ import SwiftUI import ArcGIS -import Charts /// A view displaying a `MediaPopupElement`. struct MediaPopupElementView: View { From 528352ea92e09d6f55d0e66f4ef7a4fa66dd4a14 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:53:12 -0500 Subject: [PATCH 1362/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 05a3cfed4..7aea285ea 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -22,7 +22,7 @@ struct HTMLTextView: UIViewRepresentable { /// The HTML string to dispay, including the header. var displayHTML: String { // Set the initial scale to 1, don't allow user scaling. - // This fixess small text with WKWebView and also doesn't allow the + // This fixess small text with `WKWebView` and also doesn't allow the // user to pinch to zoom. var header = "
" From 9386c4b9fc9354cf83ae42e1f91abe40999f5983 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:53:19 -0500 Subject: [PATCH 1363/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 7aea285ea..ed5c44c51 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -26,7 +26,7 @@ struct HTMLTextView: UIViewRepresentable { // user to pinch to zoom. var header = "
" - // Inject css in a head element to: + // Inject CSS in a head element to: // - word wrap long content such as urls // - set font family to default apple font // - set font size to subheadline From 9ed64fce798b70d766333ec2a9f9cb635e47a17c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:53:24 -0500 Subject: [PATCH 1364/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index ed5c44c51..8eb81b948 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -35,7 +35,7 @@ struct HTMLTextView: UIViewRepresentable { // - limit images to a maximum width of 100% // // Also wrap the passed HTML fragment inside - // open/close tags so that these css styles will apply + // open/close tags so that these CSS styles will apply header = header.appending(""" From 5ede1a3d8c2fd0b0854d87b5aa654cc0d9eb6503 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:53:32 -0500 Subject: [PATCH 1365/1722] Update Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift Co-authored-by: Nimesh Jarecha --- .../ArcGISToolkit/Components/Popups/TextPopupElementView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index dff9440d1..422763654 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -30,7 +30,7 @@ struct TextPopupElementView: View { .clipShape(roundedRect) .frame(height: webViewHeight) if webViewHeight == .zero { - // Show ProgressView until HTMLTextView has set the height. + // Show `ProgressView` until `HTMLTextView` has set the height. ProgressView() } else { From 6d053bf9f54d373e646d813eb9b43f0369520d7e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:53:43 -0500 Subject: [PATCH 1366/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 8eb81b948..728aff8b4 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -92,7 +92,7 @@ struct HTMLTextView: UIViewRepresentable { class Coordinator: NSObject, WKNavigationDelegate { var parent: HTMLTextView - /// A Boolean value specifying when content starts arriving for the main frame. + /// A Boolean value indicating whether the content started arriving for the main frame. private var hasCommitted = false /// A Boolean value specifying whether we've calculated a height for the web view or not. From fcc206952f29a0acbd45d286fc834988325ba7b7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 08:53:45 -0700 Subject: [PATCH 1367/1722] Update UtilityNetworkTraceExampleView.swift --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 06afb08fa..135eeeb3a 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -114,7 +114,7 @@ extension UtilityNetworkTraceExampleView { let targetID = UUID(uuidString: "2A6D25D5-8B9E-400A-BC07-4A11BD8B6C82") let groupLayer = map.operationalLayers.first as? GroupLayer let parameters = QueryParameters() - parameters.addObjectId(1740) + parameters.addObjectID(1740) for layer in groupLayer?.layers ?? [] { guard let layer = layer as? FeatureLayer, let table = layer.featureTable else { continue } From 46a64239bf5a56e2fedf0206003bdbbb77ffec42 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:53:56 -0500 Subject: [PATCH 1368/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 728aff8b4..31320b529 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -102,8 +102,7 @@ struct HTMLTextView: UIViewRepresentable { self.parent = parent } - // WKNavigationDelegate method invoked when content starts - // arriving for the main frame. + // `WKNavigationDelegate` method invoked when content starts arriving for the main frame. public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { hasCommitted = true } From 7490245b02fe3f81544eb0c42217a3b273e7717d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:54:03 -0500 Subject: [PATCH 1369/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 31320b529..a7cca6643 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -107,7 +107,7 @@ struct HTMLTextView: UIViewRepresentable { hasCommitted = true } - // WKNavigationDelegate method for navigation actions. + // `WKNavigationDelegate` method for navigation actions. func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction From 5e79188f92208c9f568c5d516d5b8059a119e73d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:54:17 -0500 Subject: [PATCH 1370/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index a7cca6643..f2b0135a1 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -95,7 +95,7 @@ struct HTMLTextView: UIViewRepresentable { /// A Boolean value indicating whether the content started arriving for the main frame. private var hasCommitted = false - /// A Boolean value specifying whether we've calculated a height for the web view or not. + /// A Boolean value indicating whether the height is calculated for the web view. private var hasHeight = false init(_ parent: HTMLTextView) { From 05de853dd67c7e81ce93e656299eb603af58c729 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:54:28 -0500 Subject: [PATCH 1371/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index f2b0135a1..ecc26cffb 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -126,8 +126,8 @@ struct HTMLTextView: UIViewRepresentable { } } - // WKNavigationDelegate method invoked when a main frame navigation - // completes. This is where the height calculation happens. + // `WKNavigationDelegate` method invoked when a main frame navigation completes. This is + // where the height calculation happens. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){ webView.evaluateJavaScript("document.readyState") { [weak self] complete, _ in guard complete != nil, From a4753f515e51449b4c83333121dd236c16f4fbcd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:54:38 -0500 Subject: [PATCH 1372/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index ecc26cffb..7954093d1 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -145,8 +145,8 @@ struct HTMLTextView: UIViewRepresentable { self.parent.height = height // With certain HTML strings, the JavaScript above kept - // getting called, with increasingly large heights. This - // prevents that from happening. As this block is only + // getting called, with increasingly large heights. This + // prevents that from happening. As this block is only // called after the `document.readyState` is "complete", // this should be OK. self.hasHeight = true From 470fb49f4103bad74a3c65ef48c2345416747204 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 10:55:11 -0500 Subject: [PATCH 1373/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 7954093d1..8f819fd50 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -131,7 +131,6 @@ struct HTMLTextView: UIViewRepresentable { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){ webView.evaluateJavaScript("document.readyState") { [weak self] complete, _ in guard complete != nil, - let webView = webView, self?.hasCommitted ?? false else { return } From 8a64b273b6bb007dcbdf01d613b5cc847d81faec Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 08:56:08 -0700 Subject: [PATCH 1374/1722] Update UtilityNetworkTraceExampleView.swift --- Examples/Examples/UtilityNetworkTraceExampleView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 135eeeb3a..c0cdb51e2 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -112,10 +112,10 @@ extension UtilityNetworkTraceExampleView { /// Queries the map for a feature with a certain ID and sets the list of starting points. func setPredefinedStartingPoints() async { let targetID = UUID(uuidString: "2A6D25D5-8B9E-400A-BC07-4A11BD8B6C82") - let groupLayer = map.operationalLayers.first as? GroupLayer + guard let groupLayer = map.operationalLayers.first as? GroupLayer else { return } let parameters = QueryParameters() parameters.addObjectID(1740) - for layer in groupLayer?.layers ?? [] { + for layer in groupLayer.layers { guard let layer = layer as? FeatureLayer, let table = layer.featureTable else { continue } let query = try? await table.queryFeatures(parameters: parameters) From f39f5e3c17f9d4b0ba3c830e76ced69ebcc6779a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 08:59:45 -0700 Subject: [PATCH 1375/1722] `getFeatureFor` -> `feature(for:)` --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 2 +- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 0115106d5..0a4a9b166 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -142,7 +142,7 @@ public struct UtilityNetworkTrace: View { ForEach(elements) { element in makeZoomToButton(text: element.objectID.description) { Task { - if let feature = await viewModel.getFeatureFor(element: element), + if let feature = await viewModel.feature(for: element), let geometry = feature.geometry { viewpoint = Viewpoint(targetExtent: geometry.extent) } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 93cd0af34..f1eb96377 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -137,7 +137,7 @@ import SwiftUI /// Returns a feature for the given utility element /// - Parameter element: The utility element to query the network for /// - Returns: A feature for the given element - func getFeatureFor(element: UtilityElement) async -> ArcGISFeature? { + func feature(for element: UtilityElement) async -> ArcGISFeature? { do { return try await network?.getFeatures(for: [element]).first ?? nil } catch { From 1d8eb1fdde31cec8c88010628caa8e423b068051 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 11:01:37 -0500 Subject: [PATCH 1376/1722] typo --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 8f819fd50..06e7b2551 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -22,7 +22,7 @@ struct HTMLTextView: UIViewRepresentable { /// The HTML string to dispay, including the header. var displayHTML: String { // Set the initial scale to 1, don't allow user scaling. - // This fixess small text with `WKWebView` and also doesn't allow the + // This fixes small text with `WKWebView` and also doesn't allow the // user to pinch to zoom. var header = "
" From b9b8be26e91e474588d69172141736cfe1da4670 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 09:11:13 -0700 Subject: [PATCH 1377/1722] Test repair --- .../Test Support/Utility/ChallengeHandler.swift | 2 +- .../UtilityNetworkTraceViewModelTests.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift index 3ae7d970c..3d6fa6e76 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift @@ -65,7 +65,7 @@ class ChallengeHandler: AuthenticationChallengeHandler { // This will cause a self-signed certificate to be trusted. return .useCredential(.serverTrust) } else { - return .performDefaultHandling + return .allowRequestToFail } } else if let networkCredentialProvider = networkCredentialProvider, let networkCredential = await networkCredentialProvider(challenge) { diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 55f5f1184..8e8e0e54d 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -160,7 +160,7 @@ import XCTest } as? FeatureLayer) let parameters = QueryParameters() - parameters.addObjectId(171) + parameters.addObjectID(171) let result = try await layer.featureTable?.queryFeatures(parameters: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) @@ -210,7 +210,7 @@ import XCTest } as? FeatureLayer) let parameters = QueryParameters() - parameters.addObjectId(463) + parameters.addObjectID(463) let result = try await layer.featureTable?.queryFeatures(parameters: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) @@ -267,7 +267,7 @@ import XCTest } as? FeatureLayer) let parameters = QueryParameters() - parameters.addObjectId(177) + parameters.addObjectID(177) let result = try await layer.featureTable?.queryFeatures(parameters: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) @@ -317,7 +317,7 @@ import XCTest } as? FeatureLayer) let parameters = QueryParameters() - parameters.addObjectId(171) + parameters.addObjectID(171) let result = try await layer.featureTable?.queryFeatures(parameters: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) @@ -371,7 +371,7 @@ import XCTest } as? FeatureLayer) let parameters = QueryParameters() - parameters.addObjectId(171) + parameters.addObjectID(171) let result = try await layer.featureTable?.queryFeatures(parameters: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) From a6c709f0e0369650d4627717addcb8a9fc4ce81d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 09:14:04 -0700 Subject: [PATCH 1378/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index f1eb96377..6e690ddec 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -189,10 +189,10 @@ import SwiftUI } /// Changes the selected network. - /// - Parameter network: The new utility network to be selected. /// /// This function also clears any set starting points in the pending trace and reloads the list of available /// trace configurations. + /// - Parameter network: The new utility network to be selected. func setNetwork(_ network: UtilityNetwork) { self.network = network pendingTrace.startingPoints.removeAll() From edb1be3e64cd3d7ccd564a680d35f0b2a73b5587 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 09:19:02 -0700 Subject: [PATCH 1379/1722] `makeStartingPoint` -> `addStartingPoint` --- .../UtilityNetworkTrace.swift | 2 +- .../UtilityNetworkTraceViewModel.swift | 50 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 0a4a9b166..c4de8b51e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -548,7 +548,7 @@ public struct UtilityNetworkTrace: View { } currentActivity = .creatingTrace(.viewingStartingPoints) Task { - await viewModel.makeStartingPoint( + await viewModel.addStartingPoint( at: viewPoint, mapPoint: mapPoint, with: mapViewProxy diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 6e690ddec..33d72d960 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -113,6 +113,31 @@ import SwiftUI } } + /// Adds a new starting point 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 identifyLayerResults = try? await proxy.identifyLayers( + screenPoint: point, + tolerance: 10 + ) + identifyLayerResults?.forEach { identifyLayerResult in + identifyLayerResult.geoElements.forEach { geoElement in + let startingPoint = UtilityNetworkTraceStartingPoint( + geoElement: geoElement, + mapPoint: mapPoint + ) + processAndAdd(startingPoint) + } + } + } + /// Deletes the provided starting point from the pending trace. /// - Parameter startingPoint: The starting point to be deleted. func delete(_ startingPoint: UtilityNetworkTraceStartingPoint) { @@ -212,31 +237,6 @@ import SwiftUI } } - /// Adds a new starting point 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 makeStartingPoint( - at point: CGPoint, - mapPoint: Point, - with proxy: MapViewProxy - ) async { - let identifyLayerResults = try? await proxy.identifyLayers( - screenPoint: point, - tolerance: 10 - ) - identifyLayerResults?.forEach { identifyLayerResult in - identifyLayerResult.geoElements.forEach { geoElement in - let startingPoint = UtilityNetworkTraceStartingPoint( - geoElement: geoElement, - mapPoint: mapPoint - ) - processAndAdd(startingPoint) - } - } - } - /// Asynchronously sets the nullable members of the provided starting point and adds it to the pending /// trace. /// - Parameters: From 8143f08a7f5d5bab17fd6e815695e0bc99405769 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 10:26:00 -0700 Subject: [PATCH 1380/1722] Update FloatingPanelModifier.swift --- .../Components/FloatingPanel/FloatingPanelModifier.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index 14653ea9c..aef6690a5 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -51,14 +51,14 @@ public extension View { horizontalAlignment: horizontalAlignment, isPresented: isPresented, maxWidth: maxWidth, - content: content() + panelContent: content() ) ) } } /// Overlays a floating panel on the parent content. -private struct FloatingPanelModifier: ViewModifier where InnerContent: View { +private struct FloatingPanelModifier: ViewModifier where PanelContent: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -83,7 +83,7 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont let maxWidth: CGFloat /// The content to be displayed within the floating panel. - let content: InnerContent + let panelContent: PanelContent func body(content: Content) -> some View { content @@ -93,7 +93,7 @@ private struct FloatingPanelModifier: ViewModifier where InnerCont detent: detent, isPresented: isPresented ) { - content + panelContent } .edgesIgnoringSafeArea(.bottom) .frame(maxWidth: isCompact ? .infinity : maxWidth) From f9252c88a277ba0b5ac5e21dc01613c67743d5e4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 10:28:44 -0700 Subject: [PATCH 1381/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 33d72d960..27bde8866 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -237,8 +237,7 @@ import SwiftUI } } - /// Asynchronously sets the nullable members of the provided starting point and adds it to the pending - /// trace. + /// Sets the nullable members of the provided starting point and adds it to the pending trace. /// - Parameters: /// - startingPoint: The starting point to be processed and added to the pending trace. func processAndAdd(_ startingPoint: UtilityNetworkTraceStartingPoint) { From e73f530cecd7a7b24d3b6475b9855edfedf4f238 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 10:30:55 -0700 Subject: [PATCH 1382/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 27bde8866..203e4ff17 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -133,7 +133,7 @@ import SwiftUI geoElement: geoElement, mapPoint: mapPoint ) - processAndAdd(startingPoint) + processAndAdd(startingPoint: startingPoint) } } } @@ -240,7 +240,7 @@ import SwiftUI /// Sets the nullable members of the provided starting point and adds it to the pending trace. /// - Parameters: /// - startingPoint: The starting point to be processed and added to the pending trace. - func processAndAdd(_ startingPoint: UtilityNetworkTraceStartingPoint) { + func processAndAdd(startingPoint: UtilityNetworkTraceStartingPoint) { Task { guard let feature = startingPoint.geoElement as? ArcGISFeature, let globalid = feature.attributes["globalid"] as? UUID else { @@ -415,7 +415,7 @@ import SwiftUI /// Adds programatic starting points to the pending trace. private func addExternalStartingPoints() { externalStartingPoints.forEach { - processAndAdd($0) + processAndAdd(startingPoint: $0) } } From 5cb257349d8f5aba48435d63723065cbf1f9aea9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 12:35:31 -0500 Subject: [PATCH 1383/1722] Right-align done button in FullScreen view. --- .../Components/Popups/PopupMedia/FullScreenImageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift index de85ba643..1e245501b 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift @@ -28,10 +28,10 @@ struct FullScreenImageView: View { var body: some View { VStack() { HStack { + Spacer() Button("Done") { showingFullScreen = false } - Spacer() } Text("\(popupMedia.title)") .font(.title2) From ce3a6c78133079dfda830fefa8eed43c285bad4a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 12:36:21 -0500 Subject: [PATCH 1384/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 06e7b2551..7023c94ee 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -35,7 +35,7 @@ struct HTMLTextView: UIViewRepresentable { // - limit images to a maximum width of 100% // // Also wrap the passed HTML fragment inside - // open/close tags so that these CSS styles will apply + // open/close tags so that these CSS styles will apply. header = header.appending(""" From a6546484071c59d2aa6a40f72bdd7fc6fd67d533 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 12:36:34 -0500 Subject: [PATCH 1385/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 7023c94ee..6bddc4359 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -54,7 +54,7 @@ struct HTMLTextView: UIViewRepresentable { """) - // The final string is the header + userHTML + "" + // The final string is the header + userHTML + "". return header.appending( userHTML.trimmingCharacters(in: .whitespacesAndNewlines) ).appending("") From 7f75765a032022c054bf33a1c990c9730dcfc7e9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 12:37:19 -0500 Subject: [PATCH 1386/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: Nimesh Jarecha --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 6bddc4359..576baf145 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -113,9 +113,8 @@ struct HTMLTextView: UIViewRepresentable { decidePolicyFor navigationAction: WKNavigationAction ) async -> WKNavigationActionPolicy { if navigationAction.navigationType == .linkActivated, - (navigationAction.request.url?.scheme?.lowercased() == "http" || - navigationAction.request.url?.scheme?.lowercased() == "https"), - let url = navigationAction.request.url { + let url = navigationAction.request.url, + (url.isHTTP || url.isHTTPS) { DispatchQueue.main.async { UIApplication.shared.open(url) } From 3703a97c0c3d7403029b437d94b44f53d96b496f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 10:37:22 -0700 Subject: [PATCH 1387/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 203e4ff17..33e071935 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -297,6 +297,10 @@ import SwiftUI } } + /// Sets the terminal configuration of the provided starting point. + /// - Parameters: + /// - startingPoint: The starting point to be modified. + /// - newValue: The new utility terminal to be set on the provided starting point. func setTerminalConfigurationFor( startingPoint: UtilityNetworkTraceStartingPoint, to newValue: UtilityTerminal From a5b7b3def2645ef95bf435c1463a289b213d067c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 12:39:08 -0500 Subject: [PATCH 1388/1722] add URL extensions to check for http/https --- .../Components/Popups/HTMLTextView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 576baf145..25a4b668c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -157,3 +157,15 @@ struct HTMLTextView: UIViewRepresentable { Coordinator(self) } } + +private extension URL { + /// A Boolean value indicating whether the scheme is HTTP (case-insensitive). + var isHTTP: Bool { + scheme?.caseInsensitiveCompare("http") == .orderedSame + } + + /// A Boolean value indicating whether the scheme is HTTPS (case-insensitive). + var isHTTPS: Bool { + scheme?.caseInsensitiveCompare("https") == .orderedSame + } +} From 0501a0fdbbefe6e9850e1a04d3d5aa2937d971e0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 10:44:19 -0700 Subject: [PATCH 1389/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTraceViewModel.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 33e071935..7d4345a9e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -436,9 +436,8 @@ import SwiftUI _ = completedTraces[index].startingPoints.map { $0.graphic?.isSelected = isSelected } } - /// Loads the named trace configurations in the network. - /// Returns the named trace configurations in the network on the provided map. /// - Parameter map: A web map containing one or more utility networks. + /// - Returns: The named trace configurations in the network on the provided map. private func utilityNamedTraceConfigurations(from map: Map) async -> [UtilityNamedTraceConfiguration] { guard let network = network else { return [] } return (try? await map.getNamedTraceConfigurations(from: network)) ?? [] @@ -446,16 +445,17 @@ import SwiftUI } extension UtilityNetworkTraceViewModel { - /// Finds the location on the line nearest the input point, expressed as the fraction along the line’s total - /// geodesic length. + /// Finds the location on the line nearest the input point. /// - Parameters: /// - inputGeometry: The line to be measured. /// - point: A location along the line. + /// - Returns: A location along the line expressed as a fraction of its total length. + /// - Precondition: `inputGeometry` is a `Polyline`. private func fractionAlongEdge( of inputGeometry: Geometry, at point: Point ) -> Double { - var geometry = inputGeometry + guard var geometry = inputGeometry as? Polyline else { return .zero } // Remove Z if geometry.hasZ { geometry = GeometryEngine.makeGeometry( @@ -475,7 +475,7 @@ extension UtilityNetworkTraceViewModel { } return GeometryEngine.polyline( - geometry as! Polyline, + geometry, fractionalLengthClosestTo: point, tolerance: 10 ) From b205a6485cbf7eaf4de01ead9f4c00b2a9cb0e58 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 10:46:58 -0700 Subject: [PATCH 1390/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index fc7914e0a..4caaa0a0a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -454,7 +454,7 @@ import SwiftUI } /// - Parameter map: A web map containing one or more utility networks. - /// /// - Returns: The named trace configurations in the network on the provided map. + /// - Returns: The named trace configurations in the network on the provided map. func utilityNamedTraceConfigurations(from map: Map) async -> [UtilityNamedTraceConfiguration] { guard let network = network else { return [] } do { From 8fcc61497247a980086c9afe5c7fe15315fd9cb7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 10:55:27 -0700 Subject: [PATCH 1391/1722] Update UtilityNetworkTraceViewModelTrace.swift --- .../UtilityNetworkTraceViewModelTrace.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift index 90944e39d..b78ae39f4 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModelTrace.swift @@ -18,7 +18,7 @@ extension UtilityNetworkTraceViewModel { /// A trace performed on a utility network. struct Trace { /// The number of assets returned by the trace. - var assetCount: Int = 0 + var assetCount = 0 /// A collection of all elements returned in the trace, grouped by asset group and asset type. var assets = [String: [String: [UtilityElement]]]() @@ -73,8 +73,8 @@ extension UtilityNetworkTraceViewModel { /// A collection of starting points for the trace. var startingPoints = [UtilityNetworkTraceStartingPoint]() - /// Indicates that the user has specified a name for the trace. - var userDidSpecifyName: Bool = false + /// A Boolean value that indicates that the user has specified a name for the trace. + var userDidSpecifyName = false /// A trace result comprised of a collection of UtilityElement objects. var utilityElementTraceResult: UtilityElementTraceResult? From 07a39de201d46e02c87d4114862d0296fa157b4a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 11:15:03 -0700 Subject: [PATCH 1392/1722] Update FloatingPanel.swift --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 5c8042322..66077c4c2 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -64,7 +64,7 @@ struct FloatingPanel: View where Content: View { /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity - + /// A Boolean value indicating whether the panel should be configured for a compact environment. private var isCompact: Bool { horizontalSizeClass == .compact && verticalSizeClass == .regular From 4d6de42585a15efdf942765397746afb08c7d7c9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 11:22:49 -0700 Subject: [PATCH 1393/1722] Update FloatingPanel.swift --- .../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 66077c4c2..2f7a27a38 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -190,7 +190,7 @@ private extension Color { static var activeHandleColor: Color { .primary } } -private struct RoundedCorner: Shape { +private struct RoundedCorners: Shape { var corners: UIRectCorner var radius: CGFloat @@ -213,7 +213,7 @@ private extension View { _ radius: CGFloat, corners: UIRectCorner ) -> some View { - clipShape(RoundedCorner( + clipShape(RoundedCorners( corners: corners, radius: radius )) From 6ff61953a657f8d2c2844ea7224bd8f165d795cf Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 11:26:12 -0700 Subject: [PATCH 1394/1722] Add doc --- .../Components/FloatingPanel/FloatingPanel.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 2f7a27a38..fa2f5b1ec 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -209,6 +209,11 @@ private struct RoundedCorners: Shape { } private extension View { + /// Clips this view to its bounding frame, with the specified corner radius, on the specified corners. + /// - Parameters: + /// - 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 From 9503df6fdcac0c6b0a36d7fb271e923babc2fffe Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 13:36:28 -0500 Subject: [PATCH 1395/1722] Refactor async image stuff into AsyncImageView. Padding/spacing/font updates. --- .../Popups/PopupMedia/AsyncImageView.swift | 44 +++++++++++++++++++ .../PopupMedia/FullScreenImageView.swift | 36 +++++---------- .../Popups/PopupMedia/ImageMediaView.swift | 21 +-------- 3 files changed, 56 insertions(+), 45 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift new file mode 100644 index 000000000..f3949d11c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift @@ -0,0 +1,44 @@ +// 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 view displaying an async image, with error display and progress view. +struct AsyncImageView: View { + /// The `URL` of the image. + let url: URL + + var body: some View { + AsyncImage(url: url) { phase in + if let image = phase.image { + // Displays the loaded image. + image + .resizable() + .aspectRatio(contentMode: .fit) + } else if phase.error != nil { + // Displays an error image. + HStack(alignment: .center) { + Image(systemName: "exclamationmark.circle") + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + Text("An error occurred loading the image.") + } + .padding([.top, .bottom]) + } else { + // Display the progress view until image loads. + ProgressView() + } + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift index 1e245501b..f9b934e93 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift @@ -29,37 +29,23 @@ struct FullScreenImageView: View { VStack() { HStack { Spacer() - Button("Done") { + Button { showingFullScreen = false + } label: { + Text("Done") + .fontWeight(.semibold) } + .padding([.bottom], 4) } Text("\(popupMedia.title)") - .font(.title2) - AsyncImage(url: sourceURL) { phase in - if let image = phase.image { - // Displays the loaded image. - image - .resizable() - .aspectRatio(contentMode: .fit) - } else if phase.error != nil { - // Displays an error image. - HStack { - Image(systemName: "exclamationmark.circle") - .aspectRatio(contentMode: .fit) - .foregroundColor(.red) - Text("An error occurred loading the image.") + .font(.title3) + .fontWeight(.bold) + AsyncImageView(url: sourceURL) + .onTapGesture { + if let url = popupMedia.value?.linkURL { + UIApplication.shared.open(url) } - .padding([.top]) - } else { - // Display the progress view until image loads. - ProgressView() } - } - .onTapGesture { - if let url = popupMedia.value?.linkURL { - UIApplication.shared.open(url) - } - } if popupMedia.value?.linkURL != nil { HStack { Text("Tap on the image for more information.") diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 6c179b89d..a089b9d70 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -25,29 +25,10 @@ struct ImageMediaView: View { var body: some View { VStack { if let sourceURL = popupMedia.value?.sourceURL { - AsyncImage(url: sourceURL) { phase in - if let image = phase.image { - // Display the loaded image. - image - .resizable() - .aspectRatio(contentMode: .fit) - } else if phase.error != nil { - // Display an error message. - HStack { - Image(systemName: "exclamationmark.circle") - .aspectRatio(contentMode: .fit) - .foregroundColor(.red) - Text("An error occurred loading the image.") - } - } else { - // Display a placeholder while the image is loading. - ProgressView() - } - } + AsyncImageView(url: sourceURL) .onTapGesture { showingFullScreen = true } - .padding() } HStack { PopupMediaFooter(popupMedia: popupMedia) From 999a2ce3cd21e89942a633a50cede9288b830a62 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 1 Aug 2022 13:33:40 -0700 Subject: [PATCH 1396/1722] Doc --- .../UtilityNetworkTraceViewModel.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 4caaa0a0a..b08356e0b 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -470,12 +470,11 @@ import SwiftUI } extension UtilityNetworkTraceViewModel { - /// Finds the location on the line nearest the input point. + /// Finds the location on a polyline nearest the point. /// - Parameters: - /// - inputGeometry: The line to be measured. - /// - point: A location along the line. - /// - Returns: A location along the line expressed as a fraction of its total length. - /// - Precondition: `inputGeometry` is a `Polyline`. + /// - inputGeometry: The polyline to be evaluated. + /// - point: A location along the polyline. + /// - Returns: A location along the polyline expressed as a fraction of its total length. private func fractionAlongEdge( of inputGeometry: Geometry, at point: Point From 040398bddd0e255de5a3c7fba9318adadba1d4d7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 15:45:22 -0500 Subject: [PATCH 1397/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 4 ++-- .../Components/Popups/TextPopupElementView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 25a4b668c..2d8abdbe2 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -19,7 +19,7 @@ struct HTMLTextView: UIViewRepresentable { /// The user-defined HTML string. var userHTML: String = "" - /// The HTML string to dispay, including the header. + /// The HTML string to display, including the header. var displayHTML: String { // Set the initial scale to 1, don't allow user scaling. // This fixes small text with `WKWebView` and also doesn't allow the @@ -28,7 +28,7 @@ struct HTMLTextView: UIViewRepresentable { // Inject CSS in a head element to: // - word wrap long content such as urls - // - set font family to default apple font + // - set font family to default Apple font // - set font size to subheadline // - remove padding from the body. Add some margin to separate from the // border of the webview. diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index 422763654..bba1cbd2e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -19,9 +19,9 @@ struct TextPopupElementView: View { /// The `PopupElement` to display. var popupElement: TextPopupElement - /// The calcuated height of the `HTMLTextView`. + /// The calculated height of the `HTMLTextView`. @State private var webViewHeight: CGFloat = .zero - + var body: some View { let roundedRect = RoundedRectangle(cornerRadius: 8) if !popupElement.text.isEmpty { From 6c121794e0f8d7a84f9117c98e799deccfd9717b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 1 Aug 2022 16:04:11 -0500 Subject: [PATCH 1398/1722] Very minor cleanup. --- .../Components/Popups/PopupMedia/AsyncImageView.swift | 2 +- .../Components/Popups/PopupMedia/FullScreenImageView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift index f3949d11c..0bed4857a 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift @@ -27,7 +27,7 @@ struct AsyncImageView: View { .resizable() .aspectRatio(contentMode: .fit) } else if phase.error != nil { - // Displays an error image. + // Displays an error notification. HStack(alignment: .center) { Image(systemName: "exclamationmark.circle") .aspectRatio(contentMode: .fit) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift index f9b934e93..d99490573 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift @@ -37,7 +37,7 @@ struct FullScreenImageView: View { } .padding([.bottom], 4) } - Text("\(popupMedia.title)") + Text(popupMedia.title) .font(.title3) .fontWeight(.bold) AsyncImageView(url: sourceURL) From 9bb1944de6b0b9fc8da69bd21e4d9040200fe576 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 2 Aug 2022 12:08:47 -0600 Subject: [PATCH 1399/1722] Remove queue --- .../Authentication/Authenticator.swift | 50 ++++--------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index a3650f644..86337888b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -24,13 +24,6 @@ public final class Authenticator: ObservableObject { /// A value indicating whether we should prompt the user when encountering an untrusted host. var promptForUntrustedHosts: Bool - deinit { - observationTask?.cancel() - } - - /// The task for the observation of the challenge queue. - private var observationTask: Task? - /// Creates an authenticator. /// - Parameters: /// - promptForUntrustedHosts: A value indicating whether we should prompt the user when @@ -42,25 +35,6 @@ public final class Authenticator: ObservableObject { ) { self.promptForUntrustedHosts = promptForUntrustedHosts self.oAuthConfigurations = oAuthConfigurations - observationTask = Task { [weak self] in - // Cannot unwrap self on the `for await` line or it will introduce a retain cycle. - guard let challengeQueue = self?.challengeQueue else { return } - for await queuedChallenge in challengeQueue { - guard let self = self else { break } - - // A yield here helps alleviate the already presenting bug. - await Task.yield() - - // Set the current challenge, this should present the appropriate view. - self.currentChallenge = queuedChallenge - - // Wait for the queued challenge to finish. - await queuedChallenge.complete() - - // Reset the current challenge to `nil`, that will dismiss the view. - self.currentChallenge = nil - } - } } /// Sets up new credential stores that will be persisted to the keychain. @@ -112,17 +86,6 @@ public final class Authenticator: ObservableObject { ) } - var subject = PassthroughSubject() - - /// A serial queue for authentication challenges. - private var challengeQueue: AsyncPublisher> { - AsyncPublisher( - subject - .buffer(size: .max, prefetch: .byRequest, whenFull: .dropOldest) - .eraseToAnyPublisher() - ) - } - /// The current queued challenge. @Published var currentChallenge: QueuedChallenge? } @@ -143,8 +106,9 @@ extension Authenticator: AuthenticationChallengeHandler { queuedChallenge = QueuedTokenChallenge(arcGISChallenge: challenge) } - // Queue up the challenge. - subject.send(queuedChallenge) + // Set the current challenge, which will present the UX. + self.currentChallenge = queuedChallenge + defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. return try await queuedChallenge.result.get() @@ -161,8 +125,12 @@ extension Authenticator: AuthenticationChallengeHandler { let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) - // Queue up the challenge. - subject.send(queuedChallenge) + // Alleviates an error with "already presenting". + await Task.yield() + + // Set the current challenge, which will present the UX. + self.currentChallenge = queuedChallenge + defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. return await queuedChallenge.disposition From f8e866be124e0543f10ddbdff5f3e56b03befb1f Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 2 Aug 2022 14:32:06 -0600 Subject: [PATCH 1400/1722] add value continuation --- .../Authentication/Authenticator.swift | 7 +- .../Authentication/QueuedChallenge.swift | 11 +-- .../QueuedNetworkChallenge.swift | 22 +----- .../Authentication/QueuedOAuthChallenge.swift | 23 +----- .../Authentication/QueuedTokenChallenge.swift | 27 +------ .../Authentication/ValueContinuation.swift | 45 ++++++++++++ .../AuthenticatorTests.swift | 73 ------------------- .../CertificatePickerViewModelTests.swift | 2 +- .../QueuedNetworkChallengeTests.swift | 8 +- .../QueuedTokenChallengeTests.swift | 10 +-- .../ValueContinuationTests.swift | 31 ++++++++ 11 files changed, 97 insertions(+), 162 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Authentication/ValueContinuation.swift create mode 100644 Tests/ArcGISToolkitTests/ValueContinuationTests.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 86337888b..a587a9b1e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -106,12 +106,15 @@ extension Authenticator: AuthenticationChallengeHandler { queuedChallenge = QueuedTokenChallenge(arcGISChallenge: challenge) } + // Alleviates an error with "already presenting". + await Task.yield() + // Set the current challenge, which will present the UX. self.currentChallenge = queuedChallenge defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. - return try await queuedChallenge.result.get() + return try await queuedChallenge.value.get() } public func handleNetworkChallenge( @@ -133,6 +136,6 @@ extension Authenticator: AuthenticationChallengeHandler { defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. - return await queuedChallenge.disposition + return await queuedChallenge.value } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift index fd86d6961..51ec22772 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift @@ -15,12 +15,7 @@ import Foundation import ArcGIS /// A type that represents a challenge in the queue of authentication challenges. -protocol QueuedChallenge: AnyObject { - /// Waits for the challenge to complete. - func complete() async -} +protocol QueuedChallenge: AnyObject {} -protocol QueuedArcGISChallenge: QueuedChallenge { - /// The result of the challenge. - var result: Result { get async } -} +/// A type that represents a continuation for an ArcGIS challenge. +protocol QueuedArcGISChallenge: ValueContinuation>, QueuedChallenge {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift index e9c638ee7..412b9c175 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents a network authentication challenge in the queue of challenges. @MainActor -final class QueuedNetworkChallenge: QueuedChallenge { +final class QueuedNetworkChallenge: ValueContinuation, QueuedChallenge { /// The host that prompted the challenge. let host: String @@ -35,25 +35,7 @@ final class QueuedNetworkChallenge: QueuedChallenge { /// Resumes the queued challenge. /// - Parameter disposition: The disposition to resume with. func resume(with disposition: NetworkAuthenticationChallengeDisposition) { - guard _disposition == nil else { return } - _disposition = disposition - } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed private var _disposition: (NetworkAuthenticationChallengeDisposition)? - - /// The resulting disposition of the challenge. - var disposition: NetworkAuthenticationChallengeDisposition { - get async { - await $_disposition - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await disposition + setValue(disposition) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift index 5cf360828..e7ab6af70 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedOAuthChallenge.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents an ArcGIS OAuth authentication challenge in the queue of challenges. @MainActor -final class QueuedOAuthChallenge: QueuedArcGISChallenge { +final class QueuedOAuthChallenge: ValueContinuation>, QueuedArcGISChallenge { /// The OAuth configuration to be used for this challenge. let configuration: OAuthConfiguration @@ -30,26 +30,9 @@ final class QueuedOAuthChallenge: QueuedArcGISChallenge { /// credential. func presentPrompt() { Task { - _result = await Result { + setValue(await Result { .useCredential(try await .oauth(configuration: configuration)) - } + }) } } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed private var _result: Result? - - /// The result of the challenge. - var result: Result { - get async { - await $_result - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await result - } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift index 246521976..a14feaa57 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents an ArcGIS token authentication challenge in the queue of challenges. @MainActor -final class QueuedTokenChallenge: QueuedArcGISChallenge { +final class QueuedTokenChallenge: ValueContinuation>, QueuedArcGISChallenge { /// The host that prompted the challenge. let host: String @@ -52,33 +52,14 @@ final class QueuedTokenChallenge: QueuedArcGISChallenge { /// - loginCredential: The username and password. func resume(with loginCredential: LoginCredential) { Task { - guard _result == nil else { return } - _result = await Result { + setValue(await Result { .useCredential(try await tokenCredentialProvider(loginCredential)) - } + }) } } /// Cancels the challenge. func cancel() { - guard _result == nil else { return } - _result = .success(.cancelAuthenticationChallenge) - } - - /// Use a streamed property because we need to support multiple listeners - /// to know when the challenge completed. - @Streamed private var _result: Result? - - /// The result of the challenge. - var result: Result { - get async { - await $_result - .compactMap({ $0 }) - .first(where: { _ in true })! - } - } - - public func complete() async { - _ = await result + setValue(.success(.cancelAuthenticationChallenge)) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/ValueContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/ValueContinuation.swift new file mode 100644 index 000000000..4d18bc03c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Authentication/ValueContinuation.swift @@ -0,0 +1,45 @@ +// 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 Foundation + +/// An object that allows a consumer to await on a value and a provider to provide a value +/// asynchronously. This is effectively a continuation that holds a value. +@MainActor class ValueContinuation { + /// The value. + private var _value: Value? + + /// A continuation if the resulting disposition is asked for before it is available. + private var continuation: UnsafeContinuation? + + /// Sets the value. + /// - Parameter value: The value. + func setValue(_ value: Value) { + guard _value == nil else { return } + _value = value + continuation?.resume(returning: value) + } + + /// The value. This property supports only one consumer. + var value: Value { + get async { + guard _value == nil else { + return _value! + } + precondition(continuation == nil) + return await withUnsafeContinuation { continuation in + self.continuation = continuation + } + } + } +} diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index a0c920892..3787fc795 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -65,77 +65,4 @@ import Combine arcGISCreds = await ArcGISRuntimeEnvironment.credentialStore.credentials XCTAssertTrue(arcGISCreds.isEmpty) } - - func testChallengeQueue() async throws { - actor MockQueuedChallenge: QueuedChallenge { - nonisolated let host: String - - init(host: String) { - self.host = host - } - - private var isComplete: Bool = false - - func setCompleted() { - isComplete = true - } - - func complete() async { - while !isComplete { - await Task.yield() - } - } - } - - let authenticator = Authenticator() - - // Give chance for authenticator to start observation - await Task.yield() - - XCTAssertNil(authenticator.currentChallenge) - - // Create and enqueue first challenge. - let challenge = MockQueuedChallenge(host: "host1") - authenticator.subject.send(challenge) - - // Make sure first challenge is published as the current challenge. - let currentChallenge = await AsyncPublisher(authenticator.$currentChallenge) - .compactMap( { $0 as? MockQueuedChallenge }) - .first(where: { _ in true }) - - XCTAssertEqual(currentChallenge?.host, "host1") - XCTAssertNotNil(authenticator.currentChallenge) - - // Create and enqueue second challenge. - let challenge2 = MockQueuedChallenge(host: "host2") - authenticator.subject.send(challenge2) - - // Make sure first challenge is still the current challenge - let mockedCurrentChallenge = try XCTUnwrap(authenticator.currentChallenge as? MockQueuedChallenge) - XCTAssertEqual(mockedCurrentChallenge.host, "host1") - - Task(priority: .low) { - // Complete first challenge. - await challenge.setCompleted() - } - - // Check next queued challenge - let currentChallenge2 = await AsyncPublisher(authenticator.$currentChallenge) - .compactMap( { $0 as? MockQueuedChallenge }) - .dropFirst() - .first(where: { _ in true }) - - XCTAssertEqual(currentChallenge2?.host, "host2") - XCTAssertNotNil(authenticator.currentChallenge) - - // Complete second challenge. - await challenge2.setCompleted() - - // Check next queued challenge, should be nil - let currentChallenge3 = await AsyncPublisher(authenticator.$currentChallenge) - .dropFirst() - .first(where: { _ in true }) - // nil coalescing seems required because currentChallenge3 is Optional> - XCTAssertNil(currentChallenge3 ?? nil) - } } diff --git a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift index ad3421d3e..85c8d515c 100644 --- a/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/CertificatePickerViewModelTests.swift @@ -45,7 +45,7 @@ import XCTest XCTAssertTrue(model.showCertificateImportError) model.cancel() - let disposition = await challenge.disposition + let disposition = await challenge.value XCTAssertEqual(disposition, .cancelAuthenticationChallenge) } } diff --git a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift index b43e520be..b985ae38e 100644 --- a/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedNetworkChallengeTests.swift @@ -25,13 +25,7 @@ import XCTest func testResumeAndComplete() async { let challenge = QueuedNetworkChallenge(host: "host.com", kind: .serverTrust) challenge.resume(with: .useCredential(.serverTrust)) - let disposition = await challenge.disposition + let disposition = await challenge.value XCTAssertEqual(disposition, .useCredential(.serverTrust)) - - // Make sure multiple simultaneous listeners can await the completion. - let t1 = Task { await challenge.complete() } - let t2 = Task { await challenge.complete() } - await t1.value - await t2.value } } diff --git a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift index 07c6ead17..50729b94e 100644 --- a/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift +++ b/Tests/ArcGISToolkitTests/QueuedTokenChallengeTests.swift @@ -33,14 +33,8 @@ import XCTest } challenge.resume(with: .init(username: "user1", password: "1234")) - let result = await challenge.result + let result = await challenge.value XCTAssertTrue(result.error is MockError) - - // Make sure multiple simultaneous listeners can await the completion. - let t1 = Task { await challenge.complete() } - let t2 = Task { await challenge.complete() } - await t1.value - await t2.value } func testCancel() async { @@ -49,7 +43,7 @@ import XCTest } challenge.cancel() - let result = await challenge.result + let result = await challenge.value XCTAssertEqual(result.value, .cancelAuthenticationChallenge) } } diff --git a/Tests/ArcGISToolkitTests/ValueContinuationTests.swift b/Tests/ArcGISToolkitTests/ValueContinuationTests.swift new file mode 100644 index 000000000..141fcda2d --- /dev/null +++ b/Tests/ArcGISToolkitTests/ValueContinuationTests.swift @@ -0,0 +1,31 @@ +// 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 XCTest +@testable import ArcGISToolkit + +@MainActor final class ValueContinuationTests: XCTestCase { + func testValueContinuation() async { + // Tests setting value before awaiting. + let valueContinuation = ValueContinuation() + valueContinuation.setValue("hello") + let value = await valueContinuation.value + XCTAssertEqual(value, "hello") + + // Tests setting value after awaiting. + let valueContinuation2 = ValueContinuation() + Task { valueContinuation2.setValue(1) } + let value2 = await valueContinuation2.value + XCTAssertEqual(value2, 1) + } +} From f9ac4646d3af30f21590d9392dcd87d3035b11a2 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Tue, 2 Aug 2022 16:18:50 -0600 Subject: [PATCH 1401/1722] queued challenge -> challenge continuation --- .../Authentication/Authenticator.swift | 24 +++++++++---------- .../AuthenticatorModifier.swift | 6 ++--- .../CertificatePickerViewModifier.swift | 6 ++--- ...enge.swift => ChallengeContinuation.swift} | 6 ++--- .../Components/Authentication/Login.swift | 8 +++---- ...ift => NetworkChallengeContinuation.swift} | 16 ++++++------- ...swift => OAuthChallengeContinuation.swift} | 6 ++--- ...swift => TokenChallengeContinuation.swift} | 8 +++---- .../TrustHostViewModifier.swift | 4 ++-- .../CertificatePickerViewModelTests.swift | 2 +- ...> NetworkChallengeContinuationTests.swift} | 6 ++--- ... => OAuthChallengeContinuationTests.swift} | 4 ++-- ... => TokenChallengeContinuationTests.swift} | 8 +++---- .../TrustHostViewModifierTests.swift | 2 +- 14 files changed, 53 insertions(+), 53 deletions(-) rename Sources/ArcGISToolkit/Components/Authentication/{QueuedChallenge.swift => ChallengeContinuation.swift} (71%) rename Sources/ArcGISToolkit/Components/Authentication/{QueuedNetworkChallenge.swift => NetworkChallengeContinuation.swift} (84%) rename Sources/ArcGISToolkit/Components/Authentication/{QueuedOAuthChallenge.swift => OAuthChallengeContinuation.swift} (85%) rename Sources/ArcGISToolkit/Components/Authentication/{QueuedTokenChallenge.swift => TokenChallengeContinuation.swift} (89%) rename Tests/ArcGISToolkitTests/{QueuedNetworkChallengeTests.swift => NetworkChallengeContinuationTests.swift} (79%) rename Tests/ArcGISToolkitTests/{QueuedOAuthChallengeTests.swift => OAuthChallengeContinuationTests.swift} (88%) rename Tests/ArcGISToolkitTests/{QueuedTokenChallengeTests.swift => TokenChallengeContinuationTests.swift} (86%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index a587a9b1e..43e79725f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -86,35 +86,35 @@ public final class Authenticator: ObservableObject { ) } - /// The current queued challenge. - @Published var currentChallenge: QueuedChallenge? + /// The current challenge. + @Published var currentChallenge: ChallengeContinuation? } extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { - let queuedChallenge: QueuedArcGISChallenge + let challengeContinuation: ArcGISChallengeContinuation // Create the correct challenge type. if let url = challenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { - let oAuthChallenge = QueuedOAuthChallenge(configuration: config) - queuedChallenge = oAuthChallenge + let oAuthChallenge = OAuthChallengeContinuation(configuration: config) + challengeContinuation = oAuthChallenge oAuthChallenge.presentPrompt() } else { - queuedChallenge = QueuedTokenChallenge(arcGISChallenge: challenge) + challengeContinuation = TokenChallengeContinuation(arcGISChallenge: challenge) } // Alleviates an error with "already presenting". await Task.yield() // Set the current challenge, which will present the UX. - self.currentChallenge = queuedChallenge + self.currentChallenge = challengeContinuation defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. - return try await queuedChallenge.value.get() + return try await challengeContinuation.value.get() } public func handleNetworkChallenge( @@ -125,17 +125,17 @@ extension Authenticator: AuthenticationChallengeHandler { guard promptForUntrustedHosts || challenge.kind != .serverTrust else { return .allowRequestToFail } - - let queuedChallenge = QueuedNetworkChallenge(networkChallenge: challenge) + + let challengeContinuation = NetworkChallengeContinuation(networkChallenge: challenge) // Alleviates an error with "already presenting". await Task.yield() // Set the current challenge, which will present the UX. - self.currentChallenge = queuedChallenge + self.currentChallenge = challengeContinuation defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. - return await queuedChallenge.value + return await challengeContinuation.value } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 44277a543..74f8594d7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -47,13 +47,13 @@ private struct AuthenticatorModifier: ViewModifier { @ViewBuilder func body(content: Content) -> some View { switch authenticator.currentChallenge { - case is QueuedOAuthChallenge: + case is OAuthChallengeContinuation: // OAuth is handled by the authenticator itself // so just return the unmodified content here. content - case let challenge as QueuedTokenChallenge: + case let challenge as TokenChallengeContinuation: content.modifier(LoginViewModifier(challenge: challenge)) - case let challenge as QueuedNetworkChallenge: + case let challenge as NetworkChallengeContinuation: switch challenge.kind { case .serverTrust: content.modifier(TrustHostViewModifier(challenge: challenge)) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index cb0918d21..b16eaa526 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -19,7 +19,7 @@ import ArcGIS /// certificate and a password. @MainActor final class CertificatePickerViewModel: ObservableObject { /// The challenge that requires a certificate to proceed. - let challenge: QueuedNetworkChallenge + let challenge: NetworkChallengeContinuation /// The URL of the certificate that the user chose. var certificateURL: URL? @@ -46,7 +46,7 @@ import ArcGIS /// Creates a certificate picker view model. /// - Parameter challenge: The challenge that requires a certificate. - init(challenge: QueuedNetworkChallenge) { + init(challenge: NetworkChallengeContinuation) { self.challenge = challenge } @@ -93,7 +93,7 @@ import ArcGIS struct CertificatePickerViewModifier: ViewModifier { /// Creates a certificate picker view modifier. /// - Parameter challenge: The challenge that requires a certificate. - init(challenge: QueuedNetworkChallenge) { + init(challenge: NetworkChallengeContinuation) { viewModel = CertificatePickerViewModel(challenge: challenge) } diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift similarity index 71% rename from Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift rename to Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift index 51ec22772..722bc5d49 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift @@ -14,8 +14,8 @@ import Foundation import ArcGIS -/// A type that represents a challenge in the queue of authentication challenges. -protocol QueuedChallenge: AnyObject {} +/// A type that represents an authentication challenge that can be continued.. +protocol ChallengeContinuation: AnyObject {} /// A type that represents a continuation for an ArcGIS challenge. -protocol QueuedArcGISChallenge: ValueContinuation>, QueuedChallenge {} +protocol ArcGISChallengeContinuation: ValueContinuation>, ChallengeContinuation {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index b86f76eab..63a27a062 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -104,8 +104,8 @@ struct LoginViewModifier: ViewModifier { } extension LoginViewModifier { - /// Creates a `LoginViewModifier` with a queued network challenge. - @MainActor init(challenge: QueuedNetworkChallenge) { + /// Creates a `LoginViewModifier` with a network challenge continuation. + @MainActor init(challenge: NetworkChallengeContinuation) { self.init( viewModel: LoginViewModel( challengingHost: challenge.host, @@ -123,8 +123,8 @@ extension LoginViewModifier { ) } - /// Creates a `LoginViewModifier` with a queued ArcGIS challenge. - @MainActor init(challenge: QueuedTokenChallenge) { + /// Creates a `LoginViewModifier` with an ArcGIS challenge continuation. + @MainActor init(challenge: TokenChallengeContinuation) { self.init( viewModel: LoginViewModel( challengingHost: challenge.host, diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/NetworkChallengeContinuation.swift similarity index 84% rename from Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift rename to Sources/ArcGISToolkit/Components/Authentication/NetworkChallengeContinuation.swift index 412b9c175..01093f691 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedNetworkChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/NetworkChallengeContinuation.swift @@ -14,16 +14,16 @@ import Foundation import ArcGIS -/// An object that represents a network authentication challenge in the queue of challenges. +/// An object that represents a network authentication challenge continuation. @MainActor -final class QueuedNetworkChallenge: ValueContinuation, QueuedChallenge { +final class NetworkChallengeContinuation: ValueContinuation, ChallengeContinuation { /// The host that prompted the challenge. let host: String /// The kind of challenge. let kind: Kind - /// Creates a `QueuedNetworkChallenge`. + /// Creates a `NetworkChallengeContinuation`. /// - Parameters: /// - host: The host that prompted the challenge. /// - kind: The kind of challenge. @@ -32,22 +32,22 @@ final class QueuedNetworkChallenge: ValueContinuation>, QueuedArcGISChallenge { +final class OAuthChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { /// The OAuth configuration to be used for this challenge. let configuration: OAuthConfiguration - /// Creates a `QueuedOAuthChallenge`. + /// Creates a `OAuthChallengeContinuation`. /// - Parameter configuration: The OAuth configuration to be used for this challenge. init(configuration: OAuthConfiguration) { self.configuration = configuration diff --git a/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift similarity index 89% rename from Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift rename to Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift index a14feaa57..db0d7de1a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/QueuedTokenChallenge.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift @@ -14,16 +14,16 @@ import Foundation import ArcGIS -/// An object that represents an ArcGIS token authentication challenge in the queue of challenges. +/// An object that represents an ArcGIS token authentication challenge continuation. @MainActor -final class QueuedTokenChallenge: ValueContinuation>, QueuedArcGISChallenge { +final class TokenChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { /// The host that prompted the challenge. let host: String /// A closure that provides a token credential from a username and password. let tokenCredentialProvider: (LoginCredential) async throws -> ArcGISCredential - /// Creates a `QueuedArcGISChallenge`. + /// Creates a `ArcGISChallengeContinuation`. /// - Parameters: /// - host: The host that prompted the challenge. /// - tokenCredentialProvider: A closure that provides a token credential from a username and password. @@ -35,7 +35,7 @@ final class QueuedTokenChallenge: ValueContinuation Date: Tue, 2 Aug 2022 16:25:43 -0600 Subject: [PATCH 1402/1722] do not allow untrusted host prompts in example --- .../AuthenticationExample/AuthenticationApp.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 10717a249..21bce0ff0 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -23,9 +23,7 @@ struct AuthenticationApp: App { init() { // Create an authenticator. authenticator = Authenticator( - promptForUntrustedHosts: true // If you want to use OAuth, uncomment this code: - //, //oAuthConfigurations: [.arcgisDotCom] ) // Set the challenge handler to be the authenticator we just created. From 7ccb408e6ae14aa0be7649f03db6004c7548d07c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 2 Aug 2022 15:35:32 -0700 Subject: [PATCH 1403/1722] PR change requests --- Documentation/README.md | 2 +- README.md | 2 +- .../UtilityNetworkTraceViewModel.swift | 5 +- .../Extensions/ArcGISFeature.swift | 5 +- .../UtilityNetworkTraceViewModelTests.swift | 144 +++++++++--------- 5 files changed, 77 insertions(+), 81 deletions(-) diff --git a/Documentation/README.md b/Documentation/README.md index 4b36e3aac..9ae2d792e 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -8,4 +8,4 @@ * **[Overview Map](OverviewMap)** - Displays an "overview" (or "inset") map on top of an existing map or scene view. * **[Scalebar](Scalebar)** - Displays current scale reference. * **[Search](Search)** - Displays a search experience for geo views. -* **[UtilityNetworkTrace](UtilityNetworkTrace)** - Run traces on a web map published with a utility network and trace configurations. +* **[UtilityNetworkTrace](UtilityNetworkTrace)** - Runs traces on a web map published with a utility network and trace configurations. diff --git a/README.md b/README.md index c689e46cd..3cf64ce44 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ To use Toolkit in your project: * **[OverviewMap](Documentation/OverviewMap)** - Displays the visible extent of a geo view in a small "inset" map. * **[Scalebar](Documentation/Scalebar)** - Displays current scale reference. * **[Search](Documentation/Search)** - Displays a search experience for geo views. -* **[UtilityNetworkTrace](Documentation/UtilityNetworkTrace)** - Run traces on a web map published with a utility network and trace configurations. +* **[UtilityNetworkTrace](Documentation/UtilityNetworkTrace)** - Runs traces on a web map published with a utility network and trace configurations. ## Requirements diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index b08356e0b..e2aef3d53 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -103,7 +103,7 @@ import SwiftUI init( map: Map, graphicsOverlay: GraphicsOverlay, - startingPoints: [UtilityNetworkTraceStartingPoint], + startingPoints: [UtilityNetworkTraceStartingPoint] = [], autoLoad: Bool = true ) { self.map = map @@ -257,8 +257,7 @@ import SwiftUI } /// Sets the nullable members of the provided starting point and adds it to the pending trace. - /// - Parameters: - /// - startingPoint: The starting point to be processed and added to the pending trace. + /// - Parameter startingPoint: The starting point to be processed and added to the pending trace. func processAndAdd(startingPoint: UtilityNetworkTraceStartingPoint) async { guard let feature = startingPoint.geoElement as? ArcGISFeature, let globalid = feature.globalID else { diff --git a/Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift b/Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift index 0e84c8b71..53237a2c6 100644 --- a/Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift +++ b/Sources/ArcGISToolkit/Extensions/ArcGISFeature.swift @@ -14,9 +14,10 @@ import ArcGIS import Foundation - extension ArcGISFeature { - /// A case invariant method of accessing the global ID attribute. + /// The global ID of the feature. + /// + /// This property is `nil` if there is no global ID. var globalID: UUID? { if let id = attributes["globalid"] as? UUID { return id diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 8e8e0e54d..e7df4ab37 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -12,7 +12,6 @@ // limitations under the License. import ArcGIS -import Combine import XCTest @testable import ArcGISToolkit @@ -36,11 +35,10 @@ import XCTest await ArcGISRuntimeEnvironment.credentialStore.removeAll() } - func testCase_1_1() async { + func testCase_1_1() async throws { let viewModel = UtilityNetworkTraceViewModel( - map: await makeMapWithNoUtilityNetworks(), + map: try await makeMap(), graphicsOverlay: GraphicsOverlay(), - startingPoints: [], autoLoad: false ) @@ -58,17 +56,16 @@ import XCTest try XCTSkipIf(passwordFor_sampleServer7 == nil) setChallengeHandler(ChallengeHandler(trustedHosts: [URL.sampleServer7.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_sampleServer7() + try await tokenForSampleServer7 ) - let map = await makeMapWithNoUtilityNetworks() + let map = try await makeMap() map.addUtilityNetwork( - await makeNetworkWith(url: .sampleServer7) + try await makeNetwork(url: .sampleServer7) ) let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), - startingPoints: [], autoLoad: false ) @@ -85,12 +82,12 @@ import XCTest func testCase_1_3() async throws { try XCTSkipIf(passwordFor_rtc_100_8 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtc_100_8.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.RTC100_8.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_rtc_100_8() + try await tokenForRTC100_8 ) - guard let map = Map(url: .rtc_100_8) else { + guard let map = Map(url: .RTC100_8) else { XCTFail("Failed to load map") return } @@ -98,7 +95,6 @@ import XCTest let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), - startingPoints: [], autoLoad: false ) @@ -115,12 +111,12 @@ import XCTest func testCase_1_4() async throws { try XCTSkipIf(passwordFor_rt_server109 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtServer109.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_rt_server109() + try await tokenForRTServer109 ) - guard let map = Map(url: .rt_server109) else { + guard let map = Map(url: .rtServer109) else { XCTFail("Failed to load map") return } @@ -128,7 +124,6 @@ import XCTest let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), - startingPoints: [], autoLoad: false ) @@ -145,12 +140,12 @@ import XCTest func testCase_2_1() async throws { try XCTSkipIf(passwordFor_rt_server109 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtServer109.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_rt_server109() + try await tokenForRTServer109 ) - guard let map = await makeMapWith(url: .rt_server109) else { + guard let map = try await makeMap(url: .rtServer109) else { XCTFail() return } @@ -195,12 +190,12 @@ import XCTest func testCase_2_2() async throws { try XCTSkipIf(passwordFor_rt_server109 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtServer109.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_rt_server109() + try await tokenForRTServer109 ) - guard let map = await makeMapWith(url: .rt_server109) else { + guard let map = try await makeMap(url: .rtServer109) else { XCTFail() return } @@ -252,12 +247,12 @@ import XCTest func testCase_2_3() async throws { try XCTSkipIf(passwordFor_rt_server109 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtServer109.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_rt_server109() + try await tokenForRTServer109 ) - guard let map = await makeMapWith(url: .rt_server109) else { + guard let map = try await makeMap(url: .rtServer109) else { XCTFail() return } @@ -302,12 +297,12 @@ import XCTest func testCase_3_1() async throws { try XCTSkipIf(passwordFor_rt_server109 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtServer109.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_rt_server109() + try await tokenForRTServer109 ) - guard let map = await makeMapWith(url: .rt_server109) else { + guard let map = try await makeMap(url: .rtServer109) else { XCTFail() return } @@ -345,7 +340,7 @@ import XCTest XCTAssertTrue(viewModel.canRunTrace) let success = await viewModel.trace() - let functionOutput = try XCTUnwrap( viewModel.completedTraces.first?.functionOutputs.first) + let functionOutput = try XCTUnwrap(viewModel.completedTraces.first?.functionOutputs.first) XCTAssertTrue(success) XCTAssertFalse(viewModel.canRunTrace) @@ -356,12 +351,12 @@ import XCTest func testCase_3_2() async throws { try XCTSkipIf(passwordFor_rt_server109 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rt_server109.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtServer109.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenFor_rt_server109() + try await tokenForRTServer109 ) - guard let map = await makeMapWith(url: .rt_server109) else { + guard let map = try await makeMap(url: .rtServer109) else { XCTFail() return } @@ -406,68 +401,69 @@ import XCTest } extension UtilityNetworkTraceViewModelTests { - /// - Returns: A loaded map that contains no utility networks. - func makeMapWithNoUtilityNetworks() async -> Map { + /// Initializes and loads a topographic map. + /// - Returns: A loaded map. + /// + /// The returned map contains no utility networks. + func makeMap() async throws -> Map { let map = Map(basemapStyle: .arcGISTopographic) - do { - try await map.load() - } catch { - XCTFail(error.localizedDescription) - } + try await map.load() return map } - /// - Returns: A loaded map that contains no utility networks. - func makeMapWith(url: URL) async -> Map? { - let map = Map(url: url) - do { - try await map?.load() - } catch { - XCTFail(error.localizedDescription) - } + /// Initializes and loads a map at the provided URL. + /// - Parameter url: The address of the map. + /// - Returns: A loaded map. + func makeMap(url: URL) async throws -> Map? { + let map = try XCTUnwrap(Map(url: url)) + try await map.load() return map } + /// Initializes and loads a utility network at the provided URL. + /// - Parameter url: The address of the utility network. /// - Returns: A loaded utility network. - func makeNetworkWith(url: URL) async -> UtilityNetwork { + func makeNetwork(url: URL) async throws -> UtilityNetwork { let network = UtilityNetwork(url: url) - do { - try await network.load() - } catch { - XCTFail(error.localizedDescription) - } + try await network.load() return network } - func tokenFor_rtc_100_8() async throws -> ArcGISCredential { - return try await ArcGISCredential.token( - url: URL.rtc_100_8, - username: "publisher1", - password: passwordFor_rtc_100_8! - ) + var tokenForRTC100_8: ArcGISCredential { + get async throws { + try await ArcGISCredential.token( + url: URL.RTC100_8, + username: "publisher1", + password: passwordFor_rtc_100_8! + ) + } } - func tokenFor_rt_server109() async throws -> ArcGISCredential { - return try await ArcGISCredential.token( - url: URL.rt_server109, - username: "publisher1", - password: passwordFor_rt_server109! - ) + var tokenForRTServer109: ArcGISCredential { + get async throws { + try await ArcGISCredential.token( + url: URL.rtServer109, + username: "publisher1", + password: passwordFor_rt_server109! + ) + } } - func tokenFor_sampleServer7() async throws -> ArcGISCredential { - return try await ArcGISCredential.token( - url: URL.sampleServer7, - username: "viewer01", - password: passwordFor_sampleServer7! - ) + var tokenForSampleServer7: ArcGISCredential { + get async throws { + try await ArcGISCredential.token( + url: URL.sampleServer7, + username: "viewer01", + password: passwordFor_sampleServer7! + ) + } } } private extension URL { - static var rt_server109 = URL(string: "https://rt-server109.esri.com/portal/home/item.html?id=54fa9aadf6c645d39f006cf279147204")! + static let rtServer109 = URL(string: "https://rt-server109.esri.com/portal/home/item.html?id=54fa9aadf6c645d39f006cf279147204")! - static var rtc_100_8 = URL(string: "http://rtc-100-8.esri.com/portal/home/webmap/viewer.html?webmap=78f993b89bad4ba0a8a22ce2e0bcfbd0")! + static let RTC100_8 = URL(string: "http://rtc-100-8.esri.com/portal/home/webmap/viewer.html?webmap=78f993b89bad4ba0a8a22ce2e0bcfbd0")! - static var sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! + static let sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! } From 7052126f8196ab5e26922e09501a9423df8b88c9 Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 3 Aug 2022 08:50:09 -0600 Subject: [PATCH 1404/1722] Update Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift Co-authored-by: David Feinzimer --- .../Components/Authentication/ChallengeContinuation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift index 722bc5d49..189d18970 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift @@ -14,7 +14,7 @@ import Foundation import ArcGIS -/// A type that represents an authentication challenge that can be continued.. +/// A type that represents an authentication challenge that can be continued. protocol ChallengeContinuation: AnyObject {} /// A type that represents a continuation for an ArcGIS challenge. From 7e8d8697c45baee2e30199fd49733e6809ed226b Mon Sep 17 00:00:00 2001 From: R Olson Date: Wed, 3 Aug 2022 08:50:20 -0600 Subject: [PATCH 1405/1722] Update Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift Co-authored-by: David Feinzimer --- .../Components/Authentication/OAuthChallengeContinuation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift index 408dc9c54..abbaca3c7 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift @@ -14,7 +14,7 @@ import Foundation import ArcGIS -/// An object that represents an ArcGIS OAuth authentication challenge continuation. +/// An object that represents an ArcGIS OAuth challenge continuation. @MainActor final class OAuthChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { /// The OAuth configuration to be used for this challenge. From 902f796646a0f0c98f3447af0cdb2dcd95f0fee2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 3 Aug 2022 14:06:24 -0700 Subject: [PATCH 1406/1722] Add padding to content bottom in compact envs --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index fa2f5b1ec..3185e6a2d 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -80,6 +80,7 @@ struct FloatingPanel: View where Content: View { } content .frame(minHeight: .zero, maxHeight: height) + .padding(.bottom, isCompact ? 15 : .zero) if !isCompact && isPresented { Divider() Handle(color: handleColor) From 060d0f536c0d7140c65e25d7d730999aa4beb77b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 3 Aug 2022 17:44:27 -0700 Subject: [PATCH 1407/1722] Address hidden handle --- .../FloatingPanel/FloatingPanel.swift | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 3185e6a2d..3ae0fdecf 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -33,6 +33,9 @@ struct FloatingPanel: View where Content: View { /// The content shown in the floating panel. let content: Content + /// The height of the handle area. + let handleAreaHeight: CGFloat = 20 + /// Creates a `FloatingPanel` /// - Parameter backgroundColor: The background color of the floating panel. /// - Parameter detent: Controls the height of the panel. @@ -74,20 +77,16 @@ struct FloatingPanel: View where Content: View { GeometryReader { geometryProxy in VStack { if isCompact && isPresented { - Handle(color: handleColor) - .gesture(drag) - Divider() + makeHandleArea() } content - .frame(minHeight: .zero, maxHeight: height) + .frame(minHeight: .zero, maxHeight: height - handleAreaHeight) .padding(.bottom, isCompact ? 15 : .zero) if !isCompact && isPresented { - Divider() - Handle(color: handleColor) - .gesture(drag) + makeHandleArea() } } - .padding([.top, .bottom], 10) + .padding(.bottom, isCompact ? 10 : .zero) .background(backgroundColor) .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) .shadow(radius: 10) @@ -168,6 +167,19 @@ struct FloatingPanel: View where Content: View { return maximumHeight * 0.90 } } + + /// Configures a handle area. + /// - Returns: A configured handle area, suitable for placement in the panel. + @ViewBuilder func makeHandleArea() -> some View { + ZStack { + backgroundColor + Handle(color: handleColor) + .gesture(drag) + .padding(.vertical, 5) + } + .frame(height: handleAreaHeight) + .zIndex(1) + } } /// The "Handle" view of the floating panel. From d1ade565c4fa49818c293625f67c7254744c7a0e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 3 Aug 2022 18:09:37 -0700 Subject: [PATCH 1408/1722] Apply feedback --- .../UtilityNetworkTraceViewModelTests.swift | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index e7df4ab37..423fc32ef 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -18,7 +18,6 @@ import XCTest /// - See Also: [Test Design](https://devtopia.esri.com/runtime/common-toolkit/blob/main/designs/UtilityNetworkTraceTool/UtilityNetworkTraceTool_Test_Design.md) @MainActor final class UtilityNetworkTraceViewModelTests: XCTestCase { - private let apiKey = APIKey("<#API Key#>") private let passwordFor_rtc_100_8: String? = nil private let passwordFor_rt_server109: String? = nil @@ -82,15 +81,12 @@ import XCTest func testCase_1_3() async throws { try XCTSkipIf(passwordFor_rtc_100_8 == nil) - setChallengeHandler(ChallengeHandler(trustedHosts: [URL.RTC100_8.host!])) + setChallengeHandler(ChallengeHandler(trustedHosts: [URL.rtc1008.host!])) await ArcGISRuntimeEnvironment.credentialStore.add( - try await tokenForRTC100_8 + try await tokenForRTC1008 ) - guard let map = Map(url: .RTC100_8) else { - XCTFail("Failed to load map") - return - } + let map = try XCTUnwrap(Map(url: .rtc1008)) let viewModel = UtilityNetworkTraceViewModel( map: map, @@ -162,11 +158,13 @@ import XCTest XCTAssertEqual(features.count, 1) + let feature = try XCTUnwrap(features.first) + let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), startingPoints: [ - UtilityNetworkTraceStartingPoint(geoElement: features.first!) + UtilityNetworkTraceStartingPoint(geoElement: feature) ], autoLoad: false ) @@ -212,11 +210,13 @@ import XCTest XCTAssertEqual(features.count, 1) + let feature = try XCTUnwrap(features.first) + let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), startingPoints: [ - UtilityNetworkTraceStartingPoint(geoElement: features.first!) + UtilityNetworkTraceStartingPoint(geoElement: feature) ], autoLoad: false ) @@ -226,7 +226,10 @@ import XCTest XCTAssertEqual(viewModel.pendingTrace.startingPoints.count, 1) XCTAssertFalse(viewModel.canRunTrace) - let terminal = try XCTUnwrap(viewModel.pendingTrace.startingPoints.first?.utilityElement?.assetType.terminalConfiguration?.terminals.first { $0.name == "Low" }) + let startingPoint = try XCTUnwrap(viewModel.pendingTrace.startingPoints.first) + let assetType = try XCTUnwrap(startingPoint.utilityElement?.assetType) + let terminals = try XCTUnwrap(assetType.terminalConfiguration?.terminals) + let terminal = try XCTUnwrap(terminals.first { $0.name == "Low" }) let configuration = try XCTUnwrap( viewModel.configurations.first { $0.name == "ConnectedWithResultTypes" @@ -269,11 +272,13 @@ import XCTest XCTAssertEqual(features.count, 1) + let feature = try XCTUnwrap(features.first) + let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), startingPoints: [ - UtilityNetworkTraceStartingPoint(geoElement: features.first!) + UtilityNetworkTraceStartingPoint(geoElement: feature) ], autoLoad: false ) @@ -319,11 +324,13 @@ import XCTest XCTAssertEqual(features.count, 1) + let feature = try XCTUnwrap(features.first) + let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), startingPoints: [ - UtilityNetworkTraceStartingPoint(geoElement: features.first!) + UtilityNetworkTraceStartingPoint(geoElement: feature) ], autoLoad: false ) @@ -373,11 +380,13 @@ import XCTest XCTAssertEqual(features.count, 1) + let feature = try XCTUnwrap(features.first) + let viewModel = UtilityNetworkTraceViewModel( map: map, graphicsOverlay: GraphicsOverlay(), startingPoints: [ - UtilityNetworkTraceStartingPoint(geoElement: features.first!) + UtilityNetworkTraceStartingPoint(geoElement: feature) ], autoLoad: false ) @@ -401,17 +410,17 @@ import XCTest } extension UtilityNetworkTraceViewModelTests { - /// Initializes and loads a topographic map. - /// - Returns: A loaded map. + /// Creates and loads a topographic map. /// /// The returned map contains no utility networks. + /// - Returns: A loaded map. func makeMap() async throws -> Map { let map = Map(basemapStyle: .arcGISTopographic) try await map.load() return map } - /// Initializes and loads a map at the provided URL. + /// Creates and loads a map at the provided URL. /// - Parameter url: The address of the map. /// - Returns: A loaded map. func makeMap(url: URL) async throws -> Map? { @@ -420,7 +429,7 @@ extension UtilityNetworkTraceViewModelTests { return map } - /// Initializes and loads a utility network at the provided URL. + /// Creates and loads a utility network at the provided URL. /// - Parameter url: The address of the utility network. /// - Returns: A loaded utility network. func makeNetwork(url: URL) async throws -> UtilityNetwork { @@ -429,10 +438,10 @@ extension UtilityNetworkTraceViewModelTests { return network } - var tokenForRTC100_8: ArcGISCredential { + var tokenForRTC1008: ArcGISCredential { get async throws { try await ArcGISCredential.token( - url: URL.RTC100_8, + url: URL.rtc1008, username: "publisher1", password: passwordFor_rtc_100_8! ) @@ -463,7 +472,7 @@ extension UtilityNetworkTraceViewModelTests { private extension URL { static let rtServer109 = URL(string: "https://rt-server109.esri.com/portal/home/item.html?id=54fa9aadf6c645d39f006cf279147204")! - static let RTC100_8 = URL(string: "http://rtc-100-8.esri.com/portal/home/webmap/viewer.html?webmap=78f993b89bad4ba0a8a22ce2e0bcfbd0")! + static let rtc1008 = URL(string: "http://rtc-100-8.esri.com/portal/home/webmap/viewer.html?webmap=78f993b89bad4ba0a8a22ce2e0bcfbd0")! static let sampleServer7 = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")! } From 5eff3181e453c8333347065408196e37dda34838 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 4 Aug 2022 14:50:25 -0600 Subject: [PATCH 1409/1722] remove workaround --- .../Components/Authentication/Authenticator.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 43e79725f..d94d00af4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -58,8 +58,9 @@ public final class Authenticator: ObservableObject { do { // Set a persistent network credential store on the ArcGIS environment. - ArcGISRuntimeEnvironment.networkCredentialStore = + await ArcGISRuntimeEnvironment.setNetworkCredentialStore( try await .makePersistent(access: access, isSynchronizable: isSynchronizable) + ) } catch { // If making the shared network credential store persistent fails, // then restore the ArcGIS credential store. @@ -77,13 +78,6 @@ public final class Authenticator: ObservableObject { // Clear network credentials. await ArcGISRuntimeEnvironment.networkCredentialStore.removeAll() - - // We have to set new sessions for URLCredential storage to respect the removed credentials - // right away. - ArcGISRuntimeEnvironment.urlSession = ArcGISURLSession(configuration: .default) - ArcGISRuntimeEnvironment.backgroundURLSession = ArcGISURLSession( - configuration: .background(withIdentifier: "com.esri.arcgis.toolkit." + UUID().uuidString) - ) } /// The current challenge. @@ -125,7 +119,7 @@ extension Authenticator: AuthenticationChallengeHandler { guard promptForUntrustedHosts || challenge.kind != .serverTrust else { return .allowRequestToFail } - + let challengeContinuation = NetworkChallengeContinuation(networkChallenge: challenge) // Alleviates an error with "already presenting". From 0add8d11d6b28cc781cf360568c6c29ccbf93f19 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 4 Aug 2022 16:23:52 -0700 Subject: [PATCH 1410/1722] Hide site/facility selector by default --- 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 f2c7b1331..ba02c3d05 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -52,7 +52,7 @@ public struct FloorFilter: View { @State private var isLevelsViewCollapsed = false /// A Boolean value that indicates whether the site and facility selector is presented. - @State private var isSitesAndFacilitiesHidden = false + @State private var isSitesAndFacilitiesHidden = true /// The alignment configuration. private let alignment: Alignment From 6e8e6438d9fab1ecb07085d30e243111d8b13cd7 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 4 Aug 2022 16:24:01 -0700 Subject: [PATCH 1411/1722] Spelling fixes --- .../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 ba02c3d05..f5081bc97 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -60,7 +60,7 @@ public struct FloorFilter: View { /// The width of the level selector. private let filterWidth: CGFloat = 60 - /// 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 or automatic selection support. private var viewpoint: Binding @@ -103,7 +103,7 @@ public struct FloorFilter: View { /// A Boolean value indicating whether the map is currently being navigated. private var isNavigating: Binding - /// Indicates that the selector should be presented with a top oriented aligment configuration. + /// Indicates that the selector should be presented with a top oriented alignment configuration. private var isTopAligned: Bool { alignment.vertical == .top } From 9683d3de9118dc3df895c5e2189513bbc2ce559b Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Thu, 4 Aug 2022 18:29:37 -0600 Subject: [PATCH 1412/1722] fix test --- 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 3787fc795..6b85fc8dd 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -32,7 +32,7 @@ import Combine // Make sure credential stores are restored. addTeardownBlock { ArcGISRuntimeEnvironment.credentialStore = ArcGISCredentialStore() - ArcGISRuntimeEnvironment.networkCredentialStore = NetworkCredentialStore() + await ArcGISRuntimeEnvironment.setNetworkCredentialStore(NetworkCredentialStore()) } // This tests that calling makePersistent tries to sync with the keychain. From 07cff79366523b6286b8a33382c30969b90dddf0 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 5 Aug 2022 09:59:11 -0700 Subject: [PATCH 1413/1722] Simplification --- .../Components/FloatingPanel/FloatingPanel.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 3ae0fdecf..8876ea6d4 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -33,9 +33,6 @@ struct FloatingPanel: View where Content: View { /// The content shown in the floating panel. let content: Content - /// The height of the handle area. - let handleAreaHeight: CGFloat = 20 - /// Creates a `FloatingPanel` /// - Parameter backgroundColor: The background color of the floating panel. /// - Parameter detent: Controls the height of the panel. @@ -75,18 +72,17 @@ struct FloatingPanel: View where Content: View { public var body: some View { GeometryReader { geometryProxy in - VStack { + VStack(spacing: 0) { if isCompact && isPresented { makeHandleArea() } content - .frame(minHeight: .zero, maxHeight: height - handleAreaHeight) - .padding(.bottom, isCompact ? 15 : .zero) + .frame(height: height) + .padding(.bottom, isCompact ? 25 : .zero) if !isCompact && isPresented { makeHandleArea() } } - .padding(.bottom, isCompact ? 10 : .zero) .background(backgroundColor) .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) .shadow(radius: 10) @@ -174,10 +170,9 @@ struct FloatingPanel: View where Content: View { ZStack { backgroundColor Handle(color: handleColor) - .gesture(drag) - .padding(.vertical, 5) } - .frame(height: handleAreaHeight) + .frame(height: 20) + .gesture(drag) .zIndex(1) } } From 5123d9b89a85d3304dca8ccd2a934483f24e9cce Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 5 Aug 2022 10:50:05 -0700 Subject: [PATCH 1414/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace.swift | 198 +++++++++--------- 1 file changed, 101 insertions(+), 97 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index f6865ef5f..20d6161eb 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -92,30 +92,6 @@ public struct UtilityNetworkTrace: View { // MARK: Subviews - /// Allows the user to switch between the trace creation and viewing tabs. - private var activityPicker: some View { - Picker( - "Mode", - selection: Binding( - get: { - switch currentActivity { - case .creatingTrace(_): - return UserActivity.creatingTrace(nil) - case .viewingTraces: - return UserActivity.viewingTraces(nil) - } - }, set: { newActivity, _ in - currentActivity = newActivity - } - ) - ) { - Text("New trace").tag(UserActivity.creatingTrace(nil)) - Text("Results").tag(UserActivity.viewingTraces(nil)) - } - .pickerStyle(.segmented) - .padding() - } - /// Allows the user to cancel out of selecting a new starting point. private var cancelAddStartingPoints: some View { Button(role: .destructive) { @@ -127,6 +103,32 @@ public struct UtilityNetworkTrace: View { .buttonStyle(.bordered) } + /// Allows the user to switch between the trace creation and viewing tabs. + @ViewBuilder private var activityPicker: some View { + if activeDetent != .summary { + Picker( + "Mode", + selection: Binding( + get: { + switch currentActivity { + case .creatingTrace(_): + return UserActivity.creatingTrace(nil) + case .viewingTraces: + return UserActivity.viewingTraces(nil) + } + }, set: { newActivity, _ in + currentActivity = newActivity + } + ) + ) { + Text("New trace").tag(UserActivity.creatingTrace(nil)) + Text("Results").tag(UserActivity.viewingTraces(nil)) + } + .pickerStyle(.segmented) + .padding() + } + } + /// Displays information about a chosen asset group. @ViewBuilder private var assetGroupDetail: some View { if let assetGroup = selectedAssetGroup { @@ -300,92 +302,94 @@ public struct UtilityNetworkTrace: View { if let traceName = viewModel.selectedTrace?.name, !traceName.isEmpty { Text(traceName) } - List { - Section(elementResultsTitle) { - DisclosureGroup( - viewModel.selectedTrace?.assetCount.description ?? "0", - isExpanded: Binding( - get: { isFocused(traceViewingActivity: .viewingElementResults) }, - set: { currentActivity = .viewingTraces($0 ? .viewingElementResults : nil) } - ) - ) { - ForEach( - (viewModel.selectedTrace?.assets ?? [:]).sorted(by: { $0.key < $1.key }), id: \.key - ) { assetGroup in - HStack { - Text(assetGroup.key) - Spacer() - Text(assetGroup.value.compactMap({ $0.value.count }).reduce(0, +).description) - } - .foregroundColor(.blue) - .contentShape(Rectangle()) - .onTapGesture { - currentActivity = .viewingTraces(.viewingElementGroup(assetGroup.value)) + if activeDetent != .summary { + List { + Section(elementResultsTitle) { + DisclosureGroup( + viewModel.selectedTrace?.assetCount.description ?? "0", + isExpanded: Binding( + get: { isFocused(traceViewingActivity: .viewingElementResults) }, + set: { currentActivity = .viewingTraces($0 ? .viewingElementResults : nil) } + ) + ) { + ForEach( + (viewModel.selectedTrace?.assets ?? [:]).sorted(by: { $0.key < $1.key }), id: \.key + ) { assetGroup in + HStack { + Text(assetGroup.key) + Spacer() + Text(assetGroup.value.compactMap({ $0.value.count }).reduce(0, +).description) + } + .foregroundColor(.blue) + .contentShape(Rectangle()) + .onTapGesture { + currentActivity = .viewingTraces(.viewingElementGroup(assetGroup.value)) + } } } } - } - Section("Function Result") { - DisclosureGroup( - viewModel.selectedTrace?.utilityFunctionTraceResult?.functionOutputs.count.description ?? "0", - isExpanded: Binding( - get: { isFocused(traceViewingActivity: .viewingFunctionResults) }, - set: { currentActivity = .viewingTraces($0 ? .viewingFunctionResults : nil) } - ) - ) { - ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.id) { item in - HStack { - Text(item.function.networkAttribute?.name ?? "Unnamed") - Spacer() - Text((item.result as? Double)?.description ?? "N/A") + Section("Function Result") { + DisclosureGroup( + viewModel.selectedTrace?.utilityFunctionTraceResult?.functionOutputs.count.description ?? "0", + isExpanded: Binding( + get: { isFocused(traceViewingActivity: .viewingFunctionResults) }, + set: { currentActivity = .viewingTraces($0 ? .viewingFunctionResults : nil) } + ) + ) { + ForEach(viewModel.selectedTrace?.functionOutputs ?? [], id: \.id) { item in + HStack { + Text(item.function.networkAttribute?.name ?? "Unnamed") + Spacer() + Text((item.result as? Double)?.description ?? "N/A") + } } } } - } - Section { - DisclosureGroup( - "Advanced Options", - isExpanded: Binding( - get: { isFocused(traceViewingActivity: .viewingAdvancedOptions) }, - set: { currentActivity = .viewingTraces($0 ? .viewingAdvancedOptions : nil) } - ) - ) { - ColorPicker( - selection: Binding(get: { - viewModel.selectedTrace?.color ?? Color.clear - }, set: { newValue in - if var trace = viewModel.selectedTrace { - trace.color = newValue - viewModel.update(completedTrace: trace) - } - }) + Section { + DisclosureGroup( + "Advanced Options", + isExpanded: Binding( + get: { isFocused(traceViewingActivity: .viewingAdvancedOptions) }, + set: { currentActivity = .viewingTraces($0 ? .viewingAdvancedOptions : nil) } + ) ) { - Text("Trace Color") + ColorPicker( + selection: Binding(get: { + viewModel.selectedTrace?.color ?? Color.clear + }, set: { newValue in + if var trace = viewModel.selectedTrace { + trace.color = newValue + viewModel.update(completedTrace: trace) + } + }) + ) { + Text("Trace Color") + } } } } - } - makeZoomToButton { - if let extent = viewModel.selectedTrace?.resultExtent { - viewpoint = Viewpoint(targetExtent: extent) + makeZoomToButton { + if let extent = viewModel.selectedTrace?.resultExtent { + viewpoint = Viewpoint(targetExtent: extent) + } } - } - .padding([.vertical], 2) - Button { - showWarningAlert.toggle() - } label: { - Text("Clear All Results") - .tint(.red) - } - .alert("Clear All Results", isPresented: $showWarningAlert) { - Button(role: .destructive) { - viewModel.deleteAllTraces() - currentActivity = .creatingTrace(nil) + .padding([.vertical], 2) + Button { + showWarningAlert.toggle() } label: { - Text("OK") + Text("Clear All Results") + .tint(.red) + } + .alert("Clear All Results", isPresented: $showWarningAlert) { + Button(role: .destructive) { + viewModel.deleteAllTraces() + currentActivity = .creatingTrace(nil) + } label: { + Text("OK") + } + } message: { + Text("Are you sure? All the trace inputs and results will be lost.") } - } message: { - Text("Are you sure? All the trace inputs and results will be lost.") } } From bce92eb95f823225ba6229d960c18c51a2243ffa Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 5 Aug 2022 10:51:27 -0700 Subject: [PATCH 1415/1722] Update FloatingPanel.swift --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 8876ea6d4..b45566fab 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -138,6 +138,7 @@ struct FloatingPanel: View where Content: View { .onEnded { _ in handleColor = .defaultHandleColor withAnimation { + activeDetent = closestDetent height = heightFor(detent: closestDetent) } } From bf1a8754cb423ce8c2e79ac456df7a94387abf52 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 5 Aug 2022 13:40:11 -0700 Subject: [PATCH 1416/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace.swift | 31 ++++++++-------- .../UtilityNetworkTraceViewModel.swift | 35 ++++++++++++++----- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 58eff0bbf..29e889fca 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -403,21 +403,18 @@ public struct UtilityNetworkTrace: View { } .padding([.vertical], 2) Button(role: .destructive) { - showWarningAlert.toggle() + viewModel.userAlert = UtilityNetworkTraceViewModel.UserAlert( + description: "Are you sure? All the trace inputs and results will be lost.", + button: Button(role: .destructive) { + viewModel.deleteAllTraces() + currentActivity = .creatingTrace(nil) + } label: { + Text("OK") + }) } label: { Text(clearResultsTitle) } .buttonStyle(.bordered) - .alert(clearResultsTitle, isPresented: $showWarningAlert) { - Button(role: .destructive) { - viewModel.deleteAllTraces() - currentActivity = .creatingTrace(nil) - } label: { - Text("OK") - } - } message: { - Text("Are you sure? All the trace inputs and results will be lost.") - } } /// Displays information about a chosen starting point. @@ -602,13 +599,15 @@ public struct UtilityNetworkTrace: View { viewModel.externalStartingPoints = externalStartingPoints } .alert( - "Warning", + viewModel.userAlert?.title ?? "", isPresented: Binding( - get: { !viewModel.userWarning.isEmpty }, - set: { _ in viewModel.userWarning = "" } + get: { viewModel.userAlert != nil }, + set: { _ in viewModel.userAlert = nil } ) - ) { } message: { - Text(viewModel.userWarning) + ) { + viewModel.userAlert?.button + } message: { + Text(viewModel.userAlert?.description ?? "") } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 95b1c437a..8503fa08c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -25,7 +25,7 @@ import SwiftUI @Published private(set) var configurations = [UtilityNamedTraceConfiguration]() { didSet { if configurations.isEmpty { - userWarning = "No trace types found." + userAlert = UserAlert(description: "No trace types found.") } } } @@ -54,8 +54,8 @@ import SwiftUI } } - /// Warning message presented to the user - @Published var userWarning = "" + /// Alert presented to the user + @Published var userAlert: UserAlert? /// A Boolean value indicating if the pending trace is configured to the point that it can be run. var canRunTrace: Bool { @@ -193,7 +193,7 @@ import SwiftUI network = map.utilityNetworks.first configurations = await utilityNamedTraceConfigurations(from: map) if map.utilityNetworks.isEmpty { - userWarning = "No utility networks found." + userAlert = UserAlert(description: "No utility networks found.") } await addExternalStartingPoints() } @@ -269,7 +269,7 @@ import SwiftUI func processAndAdd(startingPoint: UtilityNetworkTraceStartingPoint) async { guard let feature = startingPoint.geoElement as? ArcGISFeature, let globalid = feature.globalID else { - userWarning = "Element could not be identified" + userAlert = UserAlert(description: "Element could not be identified") return } @@ -277,7 +277,10 @@ import SwiftUI guard !pendingTrace.startingPoints.contains(where: { startingPoint in return startingPoint.utilityElement?.globalID == globalid }) else { - userWarning = "Duplicate starting points cannot be added" + userAlert = UserAlert( + title: "Failed to set starting point", + description: "Duplicate starting points cannot be added" + ) return } @@ -344,7 +347,7 @@ import SwiftUI let minStartingPoints = configuration.minimumStartingLocations.rawValue guard pendingTrace.startingPoints.count >= minStartingPoints else { - userWarning = "Please set at least \(minStartingPoints) starting location\(minStartingPoints > 1 ? "s" : "")." + userAlert = UserAlert(description: "Please set at least \(minStartingPoints) starting location\(minStartingPoints > 1 ? "s" : "").") return false } @@ -359,11 +362,11 @@ import SwiftUI traceResults = try await network.trace(traceParameters: parameters) } catch(let serviceError as ServiceError) { if let reason = serviceError.failureReason { - userWarning = reason + userAlert = UserAlert(description: reason) } return false } catch { - userWarning = "An unknown error occurred" + userAlert = UserAlert(description: error.localizedDescription) return false } @@ -519,3 +522,17 @@ extension UtilityNetworkTraceViewModel { ) } } + +extension UtilityNetworkTraceViewModel { + /// A user presentable alert. + struct UserAlert { + /// Title of the alert. + var title: String? = "Error" + + /// Description of the alert. + var description: String + + /// An additional action to be taken on the alert. + var button: Button? + } +} From da0828c948e0dc12412f5379c3208a28c844c062 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 5 Aug 2022 14:19:24 -0700 Subject: [PATCH 1417/1722] Apply feedback --- .../UtilityNetworkTraceViewModelTests.swift | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 423fc32ef..67de34c84 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -112,10 +112,7 @@ import XCTest try await tokenForRTServer109 ) - guard let map = Map(url: .rtServer109) else { - XCTFail("Failed to load map") - return - } + let map = Map(url: .rtServer109)! let viewModel = UtilityNetworkTraceViewModel( map: map, @@ -141,10 +138,7 @@ import XCTest try await tokenForRTServer109 ) - guard let map = try await makeMap(url: .rtServer109) else { - XCTFail() - return - } + let map = try await makeMap(url: .rtServer109) let layer = try XCTUnwrap(map.operationalLayers.first { $0.name == "ElecDist Device" @@ -193,10 +187,7 @@ import XCTest try await tokenForRTServer109 ) - guard let map = try await makeMap(url: .rtServer109) else { - XCTFail() - return - } + let map = try await makeMap(url: .rtServer109) let layer = try XCTUnwrap(map.operationalLayers.first { $0.name == "ElecDist Device" @@ -255,10 +246,7 @@ import XCTest try await tokenForRTServer109 ) - guard let map = try await makeMap(url: .rtServer109) else { - XCTFail() - return - } + let map = try await makeMap(url: .rtServer109) let layer = try XCTUnwrap(map.operationalLayers.first { $0.name == "ElecDist Line" @@ -307,10 +295,7 @@ import XCTest try await tokenForRTServer109 ) - guard let map = try await makeMap(url: .rtServer109) else { - XCTFail() - return - } + let map = try await makeMap(url: .rtServer109) let layer = try XCTUnwrap(map.operationalLayers.first { $0.name == "ElecDist Device" @@ -363,10 +348,7 @@ import XCTest try await tokenForRTServer109 ) - guard let map = try await makeMap(url: .rtServer109) else { - XCTFail() - return - } + let map = try await makeMap(url: .rtServer109) let layer = try XCTUnwrap(map.operationalLayers.first { $0.name == "ElecDist Device" @@ -423,7 +405,7 @@ extension UtilityNetworkTraceViewModelTests { /// Creates and loads a map at the provided URL. /// - Parameter url: The address of the map. /// - Returns: A loaded map. - func makeMap(url: URL) async throws -> Map? { + func makeMap(url: URL) async throws -> Map { let map = try XCTUnwrap(Map(url: url)) try await map.load() return map From b8e4035d89ed8cee023c52a04306f9097586f574 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 5 Aug 2022 16:00:28 -0700 Subject: [PATCH 1418/1722] Stabilize tests --- Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift | 6 +++++- Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift index 07d917bf7..e5305d750 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryItemTests.swift @@ -23,7 +23,7 @@ import Combine // required a bit more properties/logic in the 'BasemapGalleryItem' (such // as the 'loadBasemapError' and 'spatialReferenceStatus' properties than // the 'BasemapGallery' design specifies. Tests not present in the -// test design have been added to accomodate those differences. +// test design have been added to accommodate those differences. @MainActor final class BasemapGalleryItemTests: XCTestCase { override func setUp() async throws { @@ -31,6 +31,10 @@ final class BasemapGalleryItemTests: XCTestCase { try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) } + override func tearDown() { + ArcGISRuntimeEnvironment.apiKey = nil + } + func testInit() async throws { let basemap = Basemap(style: .arcGISLightGray) let item = BasemapGalleryItem(basemap: basemap) diff --git a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift index 3accaa95a..2571a5afe 100644 --- a/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/BasemapGalleryViewModelTests.swift @@ -23,7 +23,7 @@ import Combine // required a bit more properties/logic in the 'BasemapGalleryViewModel' (such // as 'geoModel.actualSpatialReference') than the 'BasemapGallery' design // specifies. Tests not present in the test design have been added to -// accomodate those differences. +// accommodate those differences. @MainActor class BasemapGalleryViewModelTests: XCTestCase { override func setUp() async throws { @@ -31,6 +31,10 @@ class BasemapGalleryViewModelTests: XCTestCase { try XCTSkipIf(ArcGISRuntimeEnvironment.apiKey == .placeholder) } + override func tearDown() { + ArcGISRuntimeEnvironment.apiKey = nil + } + let defaultBasemapGalleryItems: [BasemapGalleryItem] = [ BasemapGalleryItem( basemap: Basemap( From 5f56ab7d0d05ad0fb1122d8feb4251ee182a8851 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 8 Aug 2022 13:27:41 -0700 Subject: [PATCH 1419/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index e2aef3d53..072adddb6 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -165,7 +165,7 @@ import SwiftUI /// - Returns: A feature for the given element func feature(for element: UtilityElement) async -> ArcGISFeature? { do { - return try await network?.getFeatures(for: [element]).first ?? nil + return try await network?.features(for: [element]).first ?? nil } catch { print(error.localizedDescription) return nil @@ -457,7 +457,7 @@ import SwiftUI func utilityNamedTraceConfigurations(from map: Map) async -> [UtilityNamedTraceConfiguration] { guard let network = network else { return [] } do { - return try await map.getNamedTraceConfigurations(from: network) + return try await map.namedTraceConfigurations(from: network) } catch { print( "Failed to retrieve configurations.", From b1fe4c7d1008e871cc926917d632eefc29bcf22d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 9 Aug 2022 11:18:17 -0700 Subject: [PATCH 1420/1722] Apply feedback --- .../UtilityNetworkTrace.swift | 6 ++--- .../UtilityNetworkTraceViewModel.swift | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 58eff0bbf..4784b44cb 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -326,7 +326,7 @@ public struct UtilityNetworkTrace: View { if viewModel.completedTraces.count == 1 { currentActivity = .creatingTrace(nil) } - viewModel.delete(selectedTrace) + viewModel.deleteTrace(selectedTrace) } } .font(.title3) @@ -434,7 +434,7 @@ public struct UtilityNetworkTrace: View { } Button("Delete", role: .destructive) { if let startingPoint = selectedStartingPoint { - viewModel.delete(startingPoint) + viewModel.deleteStartingPoint(startingPoint) currentActivity = .creatingTrace(.viewingStartingPoints) } } @@ -511,7 +511,7 @@ public struct UtilityNetworkTrace: View { } .swipeActions { Button(role: .destructive) { - viewModel.delete(startingPoint) + viewModel.deleteStartingPoint(startingPoint) } label: { Image(systemName: "trash") } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 7f329e506..35b8e4816 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -140,9 +140,18 @@ import SwiftUI } } + /// Deletes all of the completed traces. + func deleteAllTraces() { + selectedTraceIndex = nil + completedTraces.forEach { traceResult in + deleteGraphics(for: traceResult) + } + completedTraces.removeAll() + } + /// Deletes the provided starting point from the pending trace. /// - Parameter startingPoint: The starting point to be deleted. - func delete(_ startingPoint: UtilityNetworkTraceStartingPoint) { + func deleteStartingPoint(_ startingPoint: UtilityNetworkTraceStartingPoint) { pendingTrace.startingPoints.removeAll { $0 == startingPoint } @@ -153,21 +162,12 @@ import SwiftUI /// Deletes the provided trace from the list of completed traces. /// - Parameter trace: The trace to be deleted. - func delete(_ trace: Trace) { + func deleteTrace(_ trace: Trace) { deleteGraphics(for: trace) completedTraces.removeAll { $0 == trace } selectPreviousTrace() } - /// Deletes all of the completed traces. - func deleteAllTraces() { - selectedTraceIndex = nil - completedTraces.forEach { traceResult in - deleteGraphics(for: traceResult) - } - completedTraces.removeAll() - } - /// Returns a feature for the given utility element /// - Parameter element: The utility element to query the network for /// - Returns: A feature for the given element From 12d2262c21620f54536efa2dc79be5cad93595ce Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 9 Aug 2022 16:58:53 -0700 Subject: [PATCH 1421/1722] Update UtilityNetworkTrace.swift --- .../Components/UtilityNetworkTrace/UtilityNetworkTrace.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 545817906..1ff71104b 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -61,9 +61,6 @@ public struct UtilityNetworkTrace: View { /// A Boolean value indicating whether the map should be zoomed to the extent of the trace result. @State private var shouldZoomOnTraceCompletion = false - /// A Boolean value indicating if the warning that all traces will be deleted is presented. - @State private var showWarningAlert = false - /// The view model used by the view. The `UtilityNetworkTraceViewModel` manages state. /// The view observes `UtilityNetworkTraceViewModel` for changes in state. @StateObject private var viewModel: UtilityNetworkTraceViewModel From aeb7428df4df557a0048528f319039f96ec921ed Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 10 Aug 2022 14:20:05 -0700 Subject: [PATCH 1422/1722] Update usage --- .../Components/Authentication/Authenticator.swift | 2 +- .../Authentication/NetworkChallengeContinuation.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index d94d00af4..803a6a34b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -113,7 +113,7 @@ extension Authenticator: AuthenticationChallengeHandler { public func handleNetworkChallenge( _ challenge: NetworkAuthenticationChallenge - ) async -> NetworkAuthenticationChallengeDisposition { + ) async -> NetworkAuthenticationChallenge.Disposition { // If `promptForUntrustedHosts` is `false` then perform default handling // for server trust challenges. guard promptForUntrustedHosts || challenge.kind != .serverTrust else { diff --git a/Sources/ArcGISToolkit/Components/Authentication/NetworkChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/NetworkChallengeContinuation.swift index 01093f691..5c830fcf4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/NetworkChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/NetworkChallengeContinuation.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents a network authentication challenge continuation. @MainActor -final class NetworkChallengeContinuation: ValueContinuation, ChallengeContinuation { +final class NetworkChallengeContinuation: ValueContinuation, ChallengeContinuation { /// The host that prompted the challenge. let host: String @@ -34,7 +34,7 @@ final class NetworkChallengeContinuation: ValueContinuation Date: Wed, 10 Aug 2022 14:21:57 -0700 Subject: [PATCH 1423/1722] Update ChallengeHandler.swift --- .../Test Support/Utility/ChallengeHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift index 3d6fa6e76..d845f999a 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift @@ -54,7 +54,7 @@ class ChallengeHandler: AuthenticationChallengeHandler { self.init(trustedHosts: trustedHosts, networkCredentialProvider: { _ in networkCredential }) } - func handleNetworkChallenge(_ challenge: NetworkAuthenticationChallenge) async -> NetworkAuthenticationChallengeDisposition { + func handleNetworkChallenge(_ challenge: NetworkAuthenticationChallenge) async -> NetworkAuthenticationChallenge.Disposition { // Record challenge only if it is not a server trust. if challenge.kind != .serverTrust { networkChallenges.append(challenge) From 58d57d39e273f63ce400d503548b48d89badd104 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 10 Aug 2022 14:49:21 -0700 Subject: [PATCH 1424/1722] Update Authenticator.swift --- .../Components/Authentication/Authenticator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 803a6a34b..932b76329 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -85,7 +85,7 @@ public final class Authenticator: ObservableObject { } extension Authenticator: AuthenticationChallengeHandler { - public func handleArcGISChallenge( + public func handleArcGISAuthenticationChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { let challengeContinuation: ArcGISChallengeContinuation @@ -111,7 +111,7 @@ extension Authenticator: AuthenticationChallengeHandler { return try await challengeContinuation.value.get() } - public func handleNetworkChallenge( + public func handleNetworkAuthenticationChallenge( _ challenge: NetworkAuthenticationChallenge ) async -> NetworkAuthenticationChallenge.Disposition { // If `promptForUntrustedHosts` is `false` then perform default handling From 4f771c7e7548474411ee5a2cb2bd3f05ca2f58d1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 11 Aug 2022 16:59:36 -0700 Subject: [PATCH 1425/1722] Apply feedback --- .../UtilityNetworkTrace.swift | 2 +- .../UtilityNetworkTraceUserAlert.swift | 26 ++++++++++++++++ .../UtilityNetworkTraceViewModel.swift | 30 +++++-------------- 3 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 1ff71104b..32ba4a5a0 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -400,7 +400,7 @@ public struct UtilityNetworkTrace: View { } .padding([.vertical], 2) Button(role: .destructive) { - viewModel.userAlert = UtilityNetworkTraceViewModel.UserAlert( + viewModel.userAlert = .init( description: "Are you sure? All the trace inputs and results will be lost.", button: Button(role: .destructive) { viewModel.deleteAllTraces() diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift new file mode 100644 index 000000000..315894ac0 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift @@ -0,0 +1,26 @@ +// 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 user presentable alert. +struct UtilityNetworkTraceUserAlert { + /// Title of the alert. + var title: String? = "Error" + + /// Description of the alert. + var description: String + + /// An additional action to be taken on the alert. + var button: Button? +} diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 0eb8c3c64..bc5cc313c 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -25,7 +25,7 @@ import SwiftUI @Published private(set) var configurations = [UtilityNamedTraceConfiguration]() { didSet { if configurations.isEmpty { - userAlert = UserAlert(description: "No trace types found.") + userAlert = .init(description: "No trace types found.") } } } @@ -55,7 +55,7 @@ import SwiftUI } /// Alert presented to the user - @Published var userAlert: UserAlert? + @Published var userAlert: UtilityNetworkTraceUserAlert? /// A Boolean value indicating if the pending trace is configured to the point that it can be run. var canRunTrace: Bool { @@ -193,7 +193,7 @@ import SwiftUI network = map.utilityNetworks.first configurations = await utilityNamedTraceConfigurations(from: map) if map.utilityNetworks.isEmpty { - userAlert = UserAlert(description: "No utility networks found.") + userAlert = .init(description: "No utility networks found.") } await addExternalStartingPoints() } @@ -269,7 +269,7 @@ import SwiftUI func processAndAdd(startingPoint: UtilityNetworkTraceStartingPoint) async { guard let feature = startingPoint.geoElement as? ArcGISFeature, let globalid = feature.globalID else { - userAlert = UserAlert(description: "Element could not be identified") + userAlert = .init(description: "Element could not be identified") return } @@ -277,7 +277,7 @@ import SwiftUI guard !pendingTrace.startingPoints.contains(where: { startingPoint in return startingPoint.utilityElement?.globalID == globalid }) else { - userAlert = UserAlert( + userAlert = .init( title: "Failed to set starting point", description: "Duplicate starting points cannot be added" ) @@ -347,7 +347,7 @@ import SwiftUI let minStartingPoints = configuration.minimumStartingLocations.rawValue guard pendingTrace.startingPoints.count >= minStartingPoints else { - userAlert = UserAlert(description: "Please set at least \(minStartingPoints) starting location\(minStartingPoints > 1 ? "s" : "").") + userAlert = .init(description: "Please set at least \(minStartingPoints) starting location\(minStartingPoints > 1 ? "s" : "").") return false } @@ -362,11 +362,11 @@ import SwiftUI traceResults = try await network.trace(traceParameters: parameters) } catch(let serviceError as ServiceError) { if let reason = serviceError.failureReason { - userAlert = UserAlert(description: reason) + userAlert = .init(description: reason) } return false } catch { - userAlert = UserAlert(description: error.localizedDescription) + userAlert = .init(description: error.localizedDescription) return false } @@ -522,17 +522,3 @@ extension UtilityNetworkTraceViewModel { ) } } - -extension UtilityNetworkTraceViewModel { - /// A user presentable alert. - struct UserAlert { - /// Title of the alert. - var title: String? = "Error" - - /// Description of the alert. - var description: String - - /// An additional action to be taken on the alert. - var button: Button? - } -} From be9913ba25f06183fbb4024557dde4b2f683e97e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 12 Aug 2022 09:15:44 -0700 Subject: [PATCH 1426/1722] Update UtilityNetworkTraceUserAlert.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift index 315894ac0..cba8cb974 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceUserAlert.swift @@ -16,7 +16,7 @@ import SwiftUI /// A user presentable alert. struct UtilityNetworkTraceUserAlert { /// Title of the alert. - var title: String? = "Error" + var title: String = "Error" /// Description of the alert. var description: String From 486762abe801fe1eb3580037191a1c200aba0b8e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 12 Aug 2022 17:11:51 -0700 Subject: [PATCH 1427/1722] Update PopupExampleView.swift --- Examples/Examples/PopupExampleView.swift | 42 ++++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index 19616b70e..27428a3b1 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -21,28 +21,28 @@ struct PopupExampleView: View { // Popups example map. - 4 types: text, media image, media chart, fields list let portalItem1 = PortalItem(portal: portal, id: Item.ID(rawValue: "10b79e7ad1944422b87f73da86dcf752")!) - + // Popups example map. - Arcade - //Test Case 3.5 Display popup with expression elements defining media [SDK] - //https://runtimecoretest.maps.arcgis.com/home/item.html?id=34752f1d149f4b2db96f7a1637767173 - let portalItem2 = PortalItem(portal: portal, id: Item.ID(rawValue: "34752f1d149f4b2db96f7a1637767173")!) - - //Test Case 3.2 Display popup with multiple fields elements [FT-SDK] - //https://runtimecoretest.maps.arcgis.com/home/item.html?id=8d75d1dbdb5c4ad5849abb26b783987e **Modified** - let portalItem3 = PortalItem(portal: portal, id: Item.ID(rawValue: "8d75d1dbdb5c4ad5849abb26b783987e")!) - - //Recreation Map with Attachments. - //https://runtimecoretest.maps.arcgis.com/home/item.html?id=2afef81236db4eabbbae357e4f990039 - let portalItem4 = PortalItem(portal: portal, id: Item.ID(rawValue: "2afef81236db4eabbbae357e4f990039")!) - - //Recreation Map with Attachments - New - //https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=79c995874bea47d08aab5a2c85120e7f - let portalItem5 = PortalItem(portal: portal, id: Item.ID(rawValue: "79c995874bea47d08aab5a2c85120e7f")!) - - //Attachments - //https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=9e3baeb5dcd4473aa13e0065d7794ca6 - let portalItem6 = PortalItem(portal: portal, id: Item.ID(rawValue: "9e3baeb5dcd4473aa13e0065d7794ca6")!) - + // Test Case 3.5 Display popup with expression elements defining media [SDK] + // https://runtimecoretest.maps.arcgis.com/home/item.html?id=34752f1d149f4b2db96f7a1637767173 + //let portalItem2 = PortalItem(portal: portal, id: Item.ID(rawValue: "34752f1d149f4b2db96f7a1637767173")!) + + // Test Case 3.2 Display popup with multiple fields elements [FT-SDK] + // https://runtimecoretest.maps.arcgis.com/home/item.html?id=8d75d1dbdb5c4ad5849abb26b783987e **Modified** + //let portalItem3 = PortalItem(portal: portal, id: Item.ID(rawValue: "8d75d1dbdb5c4ad5849abb26b783987e")!) + + // Recreation Map with Attachments. + // https://runtimecoretest.maps.arcgis.com/home/item.html?id=2afef81236db4eabbbae357e4f990039 + //let portalItem4 = PortalItem(portal: portal, id: Item.ID(rawValue: "2afef81236db4eabbbae357e4f990039")!) + + // Recreation Map with Attachments - New + // https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=79c995874bea47d08aab5a2c85120e7f + //let portalItem5 = PortalItem(portal: portal, id: Item.ID(rawValue: "79c995874bea47d08aab5a2c85120e7f")!) + + // Attachments + // https://runtimecoretest.maps.arcgis.com/apps/mapviewer/index.html?webmap=9e3baeb5dcd4473aa13e0065d7794ca6 + //let portalItem6 = PortalItem(portal: portal, id: Item.ID(rawValue: "9e3baeb5dcd4473aa13e0065d7794ca6")!) + return Map(item: portalItem1) } From e77010dd4fb327424dbd525e7d8e5532696e9a32 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 15 Aug 2022 11:53:08 -0700 Subject: [PATCH 1428/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index f5081bc97..0afe736cb 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -69,13 +69,18 @@ public struct FloorFilter: View { Button { isSitesAndFacilitiesHidden.toggle() } label: { - Image(systemName: "building.2") - .padding(.toolkitDefault) + if viewModel.isLoading { + ProgressView() + .progressViewStyle(.circular) + } else { + Image(systemName: "building.2") + .padding(.toolkitDefault) + } } } - /// A view that allows selecting between levels. - private var floorFilter: some View { + /// A view that displays the level selector and the sites and facilites button. + private var levelSelectorContainer: some View { VStack { if isTopAligned { sitesAndFacilitiesButton @@ -114,7 +119,7 @@ public struct FloorFilter: View { viewModel.onViewpointChanged(viewpoint) } - /// Displays the available levels. + /// A view that allows selecting between levels. @ViewBuilder private var levelSelector: some View { LevelSelector( isTopAligned: isTopAligned, @@ -157,9 +162,9 @@ public struct FloorFilter: View { HStack(alignment: .bottom) { if alignment.horizontal == .trailing { siteAndFacilitySelector - floorFilter + levelSelectorContainer } else { - floorFilter + levelSelectorContainer siteAndFacilitySelector } } From ad69e40059fc26f3163a6a08e1af0fb79db3fc07 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 15 Aug 2022 13:48:11 -0700 Subject: [PATCH 1429/1722] Update Login.swift --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 63a27a062..dc52f8393 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -196,6 +196,7 @@ private struct LoginView: View { .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { + focusedField = nil dismissAction() viewModel.cancel() } From caf15c6b77706f55464cd68ceb5cbe0cce9012cd Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 15 Aug 2022 14:54:24 -0700 Subject: [PATCH 1430/1722] Update FloorFilter.swift --- .../Components/FloorFilter/FloorFilter.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift index 0afe736cb..6c6fea8a5 100644 --- a/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift +++ b/Sources/ArcGISToolkit/Components/FloorFilter/FloorFilter.swift @@ -69,13 +69,15 @@ public struct FloorFilter: View { Button { isSitesAndFacilitiesHidden.toggle() } label: { - if viewModel.isLoading { - ProgressView() - .progressViewStyle(.circular) - } else { - Image(systemName: "building.2") - .padding(.toolkitDefault) - } + Image(systemName: "building.2") + .padding(.toolkitDefault) + .opacity(viewModel.isLoading ? .zero : 1) + .overlay { + if viewModel.isLoading { + ProgressView() + .progressViewStyle(.circular) + } + } } } From 21f0012754dc0f2ac79af4e1a30a82b7cfa577ba Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 16 Aug 2022 12:11:53 -0700 Subject: [PATCH 1431/1722] Update Login.swift --- .../Components/Authentication/Login.swift | 226 ++++++++++-------- 1 file changed, 127 insertions(+), 99 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index dc52f8393..8a6ed57fb 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -97,8 +97,11 @@ struct LoginViewModifier: ViewModifier { func body(content: Content) -> some View { content .task { isPresented = true } - .sheet(isPresented: $isPresented) { - LoginView(viewModel: viewModel) + .overlay { + LoginView( + viewModel: viewModel, + isPresented: $isPresented + ) } } } @@ -140,117 +143,142 @@ extension LoginViewModifier { } /// A view that prompts a user to login with a username and password. -private struct LoginView: View { +/// +/// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. +private struct LoginView: UIViewControllerRepresentable { + /// The view model. + @ObservedObject var viewModel: LoginViewModel + + /// A Boolean value indicating whether or not the view is displayed. + @Binding var isPresented: Bool + + /// The cancel action for the `UIAlertController`. + let cancelAction: UIAlertAction + + /// The sign in action for the `UIAlertController`. + let signInAction: UIAlertAction + /// Creates the view. /// - Parameters: /// - viewModel: The view model. - init(viewModel: LoginViewModel) { - _viewModel = ObservedObject(initialValue: viewModel) + /// - isPresented: A Boolean value indicating whether or not the view is displayed. + init(viewModel: LoginViewModel, isPresented: Binding) { + self.viewModel = viewModel + + _isPresented = isPresented + + cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in + viewModel.cancel() + } + signInAction = UIAlertAction(title: "Sign In", style: .default) { _ in + viewModel.signIn() + } + + cancelAction.isEnabled = true + signInAction.isEnabled = false } - @Environment(\.dismiss) var dismissAction - - /// The view model. - @ObservedObject private var viewModel: LoginViewModel - - /// The focused field. - @FocusState private var focusedField: Field? + func makeCoordinator() -> Coordinator { + Coordinator(self) + } - var body: some View { - NavigationView { - Form { - Section { - VStack { - person - Text("You must sign in to access '\(viewModel.challengingHost)'") - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) - } - - Section { - TextField("Username", text: $viewModel.username) - .focused($focusedField, equals: .username) - .textContentType(.username) - .submitLabel(.next) - .onSubmit { focusedField = .password } - SecureField("Password", text: $viewModel.password) - .focused($focusedField, equals: .password) - .textContentType(.password) - .submitLabel(.go) - .onSubmit { viewModel.signIn() } - } - .autocapitalization(.none) - .disableAutocorrection(true) - - Section { - signInButton - } - } - .disabled(!viewModel.formEnabled) - .navigationTitle("Sign In") - .navigationBarTitleDisplayMode(.inline) - .interactiveDismissDisabled() - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - focusedField = nil - dismissAction() - viewModel.cancel() - } - } - } - .onAppear { - // Workaround for Apple bug - FB9676178. - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - focusedField = .username - } - } + /// Creates the alert controller object and configures its initial state. + /// - Parameter context: A context structure containing information about the current state of the + /// system. + /// - Returns: A configured alert controller. + func makeAlertController(context: Context) -> UIAlertController { + let uiAlertController = UIAlertController( + title: "You must sign in to access '\(viewModel.challengingHost)'", + message: nil, + preferredStyle: .alert + ) + + uiAlertController.addTextField { textField in + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.delegate = context.coordinator + textField.placeholder = "Username" + textField.returnKeyType = .next + textField.textContentType = .username } + + uiAlertController.addTextField { textField in + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.delegate = context.coordinator + textField.isSecureTextEntry = true + textField.placeholder = "Password" + textField.returnKeyType = .go + textField.textContentType = .password + } + + uiAlertController.addAction(cancelAction) + uiAlertController.addAction(signInAction) + + return uiAlertController } - /// An image used in the form. - private var person: some View { - Image(systemName: "person.circle") - .resizable() - .frame(width: 150, height: 150) - .shadow( - color: .gray.opacity(0.4), - radius: 3, - x: 1, - y: 2 - ) + func makeUIViewController(context: Context) -> UIViewController { + return UIViewController() } - /// The sign-in button. - private var signInButton: some View { - Button(action: { - dismissAction() - viewModel.signIn() - }, label: { - if viewModel.formEnabled { - Text("Sign In") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundColor(.white) - } else { - ProgressView() - .frame(maxWidth: .infinity, alignment: .center) - .tint(.white) + func updateUIViewController( + _ uiViewController: UIViewControllerType, + context: Context + ) { + if isPresented { + let alertController = makeAlertController(context: context) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + uiViewController.present(alertController, animated: true) { + isPresented = false + } } - }) - .disabled(!viewModel.signInButtonEnabled) - .listRowBackground(viewModel.signInButtonEnabled ? Color.accentColor : Color.gray) + } } } -private extension LoginView { - /// A type that represents the fields in the user name and password sign-in form. - enum Field: Hashable { - /// The username field. - case username - /// The password field. - case password +extension LoginView { + /// The coordinator for the login view that acts as a delegate to the underlying + /// `UIAlertViewController`. + final class Coordinator: NSObject, UITextFieldDelegate { + /// The view that owns this coordinator. + let parent: LoginView + + /// Creates the coordinator. + /// - Parameter parent: The view that owns this coordinator. + init(_ parent: LoginView) { + self.parent = parent + } + + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.updateValues(with: textField) + } + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if textField.textContentType == .password && + parent.viewModel.signInButtonEnabled { + parent.viewModel.signIn() + } + return true + } + + /// Updates the view model with the latest text field values and the enabled state of the sign in + /// button. + /// - Parameter textField: The text field who's value recently changed. + func updateValues(with textField: UITextField) { + if textField.textContentType == .username { + parent.viewModel.username = textField.text ?? "" + } else { + parent.viewModel.password = textField.text ?? "" + } + parent.signInAction.isEnabled = parent.viewModel.signInButtonEnabled + } } } From a6d9d6164b2c4db292368cfb6b5970c6e8f06d78 Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 17 Aug 2022 14:09:37 -0700 Subject: [PATCH 1432/1722] fix authenticator for the changes in the SDK --- .../Components/Authentication/Authenticator.swift | 4 ++-- .../Authentication/ChallengeContinuation.swift | 2 +- .../Authentication/OAuthChallengeContinuation.swift | 11 +++++++---- .../Authentication/TokenChallengeContinuation.swift | 13 ++++++++----- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 932b76329..9d8fa0d24 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -87,7 +87,7 @@ public final class Authenticator: ObservableObject { extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISAuthenticationChallenge( _ challenge: ArcGISAuthenticationChallenge - ) async throws -> ArcGISAuthenticationChallenge.Disposition { + ) async -> ArcGISAuthenticationChallenge.Disposition { let challengeContinuation: ArcGISChallengeContinuation // Create the correct challenge type. @@ -108,7 +108,7 @@ extension Authenticator: AuthenticationChallengeHandler { defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. - return try await challengeContinuation.value.get() + return await challengeContinuation.value } public func handleNetworkAuthenticationChallenge( diff --git a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift index 189d18970..b6cc27eea 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift @@ -18,4 +18,4 @@ import ArcGIS protocol ChallengeContinuation: AnyObject {} /// A type that represents a continuation for an ArcGIS challenge. -protocol ArcGISChallengeContinuation: ValueContinuation>, ChallengeContinuation {} +protocol ArcGISChallengeContinuation: ValueContinuation, ChallengeContinuation {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift index abbaca3c7..90eb706e5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents an ArcGIS OAuth challenge continuation. @MainActor -final class OAuthChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { +final class OAuthChallengeContinuation: ValueContinuation, ArcGISChallengeContinuation { /// The OAuth configuration to be used for this challenge. let configuration: OAuthConfiguration @@ -30,9 +30,12 @@ final class OAuthChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { +final class TokenChallengeContinuation: ValueContinuation, ArcGISChallengeContinuation { /// The host that prompted the challenge. let host: String @@ -52,14 +52,17 @@ final class TokenChallengeContinuation: ValueContinuation Date: Wed, 17 Aug 2022 13:44:45 -0700 Subject: [PATCH 1433/1722] Update CertificatePickerViewModifier.swift --- .../CertificatePickerViewModifier.swift | 204 +++++++++++------- 1 file changed, 127 insertions(+), 77 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index b16eaa526..7f872fc0d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -24,6 +24,9 @@ import ArcGIS /// The URL of the certificate that the user chose. var certificateURL: URL? + /// The password. + @Published var password = "" + /// A Boolean value indicating whether to show the prompt. @Published var showPrompt = true @@ -65,9 +68,8 @@ import ArcGIS } /// Attempts to use the certificate and password to respond to the challenge. - /// - Parameter password: The password for the certificate. - func proceed(withPassword password: String) { - guard let certificateURL = certificateURL else { + func proceedWithPassword() { + guard let certificateURL = certificateURL, !password.isEmpty else { preconditionFailure() } @@ -99,7 +101,7 @@ struct CertificatePickerViewModifier: ViewModifier { /// The view model. @ObservedObject private var viewModel: CertificatePickerViewModel - + func body(content: Content) -> some View { content .promptBrowseCertificate( @@ -178,14 +180,11 @@ private extension View { isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { - sheet(isPresented: isPresented) { - EnterPasswordView() { password in - viewModel.proceed(withPassword: password) - } onCancel: { - viewModel.cancel() - } - .edgesIgnoringSafeArea(.bottom) - .interactiveDismissDisabled() + overlay { + EnterPasswordView( + viewModel: viewModel, + isPresented: isPresented + ) } } } @@ -230,80 +229,131 @@ private extension View { } /// A view that allows the user to enter a password. -struct EnterPasswordView: View { - @Environment(\.dismiss) var dismissAction +/// +/// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. +struct EnterPasswordView: UIViewControllerRepresentable { + /// The view model. + @ObservedObject var viewModel: CertificatePickerViewModel + + /// A Boolean value indicating whether or not the view is displayed. + @Binding var isPresented: Bool - /// The password that the user entered. - @State var password: String = "" + /// The cancel action for the `UIAlertController`. + let cancelAction: UIAlertAction - /// The action to call once the user has completed entering the password. - var onContinue: (String) -> Void + /// The continue action for the `UIAlertController`. + let continueAction: UIAlertAction - /// The action to call if the user cancels. - var onCancel: () -> Void + /// Creates the view. + /// - Parameters: + /// - viewModel: The view model. + /// - isPresented: A Boolean value indicating whether or not the view is displayed. + init( + viewModel: CertificatePickerViewModel, + isPresented: Binding + ) { + self.viewModel = viewModel + + _isPresented = isPresented + + cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in + viewModel.cancel() + } + continueAction = UIAlertAction(title: "OK", style: .default) { _ in + viewModel.proceedWithPassword() + } + + cancelAction.isEnabled = true + continueAction.isEnabled = false + } - /// A Boolean value indicating whether the password field has focus. - @FocusState var isPasswordFocused: Bool + func makeCoordinator() -> Coordinator { + Coordinator(self) + } - var body: some View { - NavigationView { - Form { - Section { - VStack { - Text("Please enter a password for the chosen certificate.") - .fixedSize(horizontal: false, vertical: true) - } - .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) - } - - Section { - SecureField("Password", text: $password) - .focused($isPasswordFocused) - .textContentType(.password) - .submitLabel(.go) - .onSubmit { - dismissAction() - onContinue(password) - } - } - .autocapitalization(.none) - .disableAutocorrection(true) - - Section { - okButton + /// Creates the alert controller object and configures its initial state. + /// - Parameter context: A context structure containing information about the current state of the + /// system. + /// - Returns: A configured alert controller. + func makeAlertController(context: Context) -> UIAlertController { + let uiAlertController = UIAlertController( + title: "Please enter a password for the chosen certificate.", + message: nil, + preferredStyle: .alert + ) + + uiAlertController.addTextField { textField in + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.delegate = context.coordinator + textField.isSecureTextEntry = true + textField.placeholder = "Password" + textField.returnKeyType = .go + textField.textContentType = .password + } + + uiAlertController.addAction(cancelAction) + uiAlertController.addAction(continueAction) + + return uiAlertController + } + + func makeUIViewController(context: Context) -> UIViewController { + return UIViewController() + } + + func updateUIViewController( + _ uiViewController: UIViewControllerType, + context: Context + ) { + if isPresented { + let alertController = makeAlertController(context: context) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + uiViewController.present(alertController, animated: true) { + isPresented = false } } - .navigationTitle("Certificate") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - dismissAction() - onCancel() - } - } + } + } +} + +extension EnterPasswordView { + /// The coordinator for the password view that acts as a delegate to the underlying + /// `UIAlertViewController`. + final class Coordinator: NSObject, UITextFieldDelegate { + /// The view that owns this coordinator. + let parent: EnterPasswordView + + /// Creates the coordinator. + /// - Parameter parent: The view that owns this coordinator. + init(_ parent: EnterPasswordView) { + self.parent = parent + } + + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.updateValues(with: textField) } - .onAppear { - // Workaround for Apple bug - FB9676178. - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - isPasswordFocused = true - } + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if !parent.viewModel.password.isEmpty { + parent.viewModel.proceedWithPassword() } + return true + } + + /// Updates the view model with the latest text field values and the enabled state of the continue + /// button. + /// - Parameter textField: The text field who's value recently changed. + func updateValues(with textField: UITextField) { + parent.viewModel.password = textField.text ?? "" + parent.continueAction.isEnabled = !parent.viewModel.password.isEmpty } - } - - /// The "OK" button. - private var okButton: some View { - Button(action: { - dismissAction() - onContinue(password) - }, label: { - Text("OK") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundColor(.white) - }) - .disabled(password.isEmpty) - .listRowBackground(!password.isEmpty ? Color.accentColor : Color.gray) } } From 1497f4db0af76aaf60d91c0eee77c1b5825e1ab1 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 17 Aug 2022 14:58:48 -0700 Subject: [PATCH 1434/1722] Update Login.swift --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 8a6ed57fb..da3838b8c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -40,9 +40,6 @@ final class LoginViewModel: ObservableObject { /// A Boolean value indicating if the sign-in button is enabled. @Published var signInButtonEnabled = false - /// A Boolean value indicating if the form is enabled. - @Published var formEnabled: Bool = true - /// The action to perform when the user signs in. This is a closure that takes a username /// and password, respectively. var signInAction: (LoginCredential) -> Void @@ -75,13 +72,11 @@ final class LoginViewModel: ObservableObject { /// Attempts to log in with a username and password. func signIn() { - formEnabled = false signInAction(LoginCredential(username: username, password: password)) } /// Cancels the challenge. func cancel() { - formEnabled = false cancelAction() } } From 3764a1d45c26cff5a29d3670d6726490e85e324e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 17 Aug 2022 16:59:44 -0700 Subject: [PATCH 1435/1722] Update Login.swift --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index da3838b8c..b3e8cdc8b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -148,10 +148,10 @@ private struct LoginView: UIViewControllerRepresentable { @Binding var isPresented: Bool /// The cancel action for the `UIAlertController`. - let cancelAction: UIAlertAction + @State var cancelAction: UIAlertAction /// The sign in action for the `UIAlertController`. - let signInAction: UIAlertAction + @State var signInAction: UIAlertAction /// Creates the view. /// - Parameters: From 6747c482e76d5f9d176b88083b3f0035175d0b29 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 17 Aug 2022 16:59:48 -0700 Subject: [PATCH 1436/1722] Update CertificatePickerViewModifier.swift --- .../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 7f872fc0d..54b19a88a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -239,10 +239,10 @@ struct EnterPasswordView: UIViewControllerRepresentable { @Binding var isPresented: Bool /// The cancel action for the `UIAlertController`. - let cancelAction: UIAlertAction + @State var cancelAction: UIAlertAction /// The continue action for the `UIAlertController`. - let continueAction: UIAlertAction + @State var continueAction: UIAlertAction /// Creates the view. /// - Parameters: From 3a4f57706b5464cb767a3a71d0c2d045fd4cb94c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 18 Aug 2022 08:41:51 -0700 Subject: [PATCH 1437/1722] Split title and message --- .../Authentication/CertificatePickerViewModifier.swift | 4 ++-- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 54b19a88a..8c0d1c3f4 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -277,8 +277,8 @@ struct EnterPasswordView: UIViewControllerRepresentable { /// - Returns: A configured alert controller. func makeAlertController(context: Context) -> UIAlertController { let uiAlertController = UIAlertController( - title: "Please enter a password for the chosen certificate.", - message: nil, + title: "Password Required", + message: "Please enter a password for the chosen certificate.", preferredStyle: .alert ) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index b3e8cdc8b..27965930e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -183,8 +183,8 @@ private struct LoginView: UIViewControllerRepresentable { /// - Returns: A configured alert controller. func makeAlertController(context: Context) -> UIAlertController { let uiAlertController = UIAlertController( - title: "You must sign in to access '\(viewModel.challengingHost)'", - message: nil, + title: "Authentication Required", + message: "You must sign in to access '\(viewModel.challengingHost)'", preferredStyle: .alert ) From 59896e34dae626adf499e1cf7fd415b63c292241 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 18 Aug 2022 08:50:26 -0700 Subject: [PATCH 1438/1722] if isPresented -> guard isPresented --- .../CertificatePickerViewModifier.swift | 11 +++++------ .../Components/Authentication/Login.swift | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 8c0d1c3f4..e9c8e00ac 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -306,12 +306,11 @@ struct EnterPasswordView: UIViewControllerRepresentable { _ uiViewController: UIViewControllerType, context: Context ) { - if isPresented { - let alertController = makeAlertController(context: context) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - uiViewController.present(alertController, animated: true) { - isPresented = false - } + guard isPresented else { return } + let alertController = makeAlertController(context: context) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + uiViewController.present(alertController, animated: true) { + isPresented = false } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 27965930e..78872ee5b 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -221,12 +221,11 @@ private struct LoginView: UIViewControllerRepresentable { _ uiViewController: UIViewControllerType, context: Context ) { - if isPresented { - let alertController = makeAlertController(context: context) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - uiViewController.present(alertController, animated: true) { - isPresented = false - } + guard isPresented else { return } + let alertController = makeAlertController(context: context) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + uiViewController.present(alertController, animated: true) { + isPresented = false } } } From 2b617a6004acbc37c127927496057d3c106b5fdf Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Aug 2022 13:10:13 -0500 Subject: [PATCH 1439/1722] Add AttachmentsPopupElementView. --- .../Popups/AttachmentsPopupElementView.swift | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift new file mode 100644 index 000000000..457281584 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -0,0 +1,202 @@ +// 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 + +struct AttachmentsPopupElementView: View { + typealias AttachmentImage = (attachment:PopupAttachment, image:UIImage?) + + var popupElement: AttachmentsPopupElement + + @State var attachmentImages = [AttachmentImage]() + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass + + /// If `true`, the gallery will display as if the device is in a regular-width orientation. + /// If `false`, the gallery will display as if the device is in a compact-width orientation. + var isRegularWidth: Bool { + !(horizontalSizeClass == .compact && verticalSizeClass == .regular) + } + + @State var loadingAttachments = true + + init( + popupElement: AttachmentsPopupElement, + popup: Popup + ) { + self.popupElement = popupElement + // self.popup = popup + // _attachmentHelper = StateObject(wrappedValue: AttachmentHelper(feature: popup.geoElement as? ArcGISFeature)) + } + + var body: some View { + VStack { + PopupElementHeader( + title: popupElement.title, + description: popupElement.description + ) + Spacer() + if loadingAttachments { + ProgressView() + } else if popupElement.attachments.count == 0 { + Text("No attachments.") + } + else { + switch popupElement.displayType { + case .list: + // AttachmentPreview(attachments: attachmentHelper.attachments, images: attachmentHelper.attachmentImages) + AttachmentList(attachmentImages: attachmentImages) + case.preview: + // AttachmentPreview(attachments: attachmentHelper.attachments, images: attachmentHelper.attachmentImages) + AttachmentList(attachmentImages: attachmentImages) + case .auto: + if isRegularWidth { + AttachmentPreview(attachmentImages: attachmentImages) + } else { + AttachmentList(attachmentImages: attachmentImages) + } + } + } + } + .task { + try? await popupElement.fetchAttachments() + + await withTaskGroup(of: AttachmentImage.self) { group in + for attachment in popupElement.attachments { + group.addTask { + if attachment.kind == .image { + let image = try? await attachment.makeFullImage() + return (attachment,image) + } + else { + + } +// let data = try? await attachment.fetchData() +// if let data = data, let image = UIImage(data: data) { +// return (attachment,image) +// } + return (attachment, nil) + } + } + for await pair in group { + attachmentImages.append(pair) + } + } + + loadingAttachments = false + + + // attachments.forEach { attachment in + // Task { + // let data = try? await attachment.fetchData() + // if let data = data, let image = UIImage(data: data) { + // attachmentImages.append(image) + // } + // } + // } + // loadingAttachments = false + } + } + + struct AttachmentList: View { + var attachmentImages: [AttachmentImage] + // let attachments: [Attachment] + // let images: [UIImage] + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + // if images.count != 2 { + // EmptyView() + // } + // else { + // ForEach(0..<2) { i in + // ForEach(attachmentImages) { attachmentImage in + // HStack { + // Image(uiImage: attachmentImage.image) + // .resizable() + // .aspectRatio(contentMode: .fit) + // .clipShape(RoundedRectangle(cornerRadius: 8)) + // .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) + // .frame(width: 75, height: 75, alignment: .center) + // Text(attachmentImage.attachment.name) + // } + // } + // } + } + } + } + + struct AttachmentPreview: View { + var attachmentImages: [AttachmentImage] + + var body: some View { + VStack(alignment: .center) { + // ForEach(0.. Date: Thu, 18 Aug 2022 11:12:02 -0700 Subject: [PATCH 1440/1722] Simplify Coordinators --- .../CertificatePickerViewModifier.swift | 31 +++++-------- .../Components/Authentication/Login.swift | 44 +++++++------------ 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index e9c8e00ac..a75e45f67 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -283,6 +283,13 @@ struct EnterPasswordView: UIViewControllerRepresentable { ) uiAlertController.addTextField { textField in + textField.addAction( + UIAction { _ in + viewModel.password = textField.text ?? "" + continueAction.isEnabled = !viewModel.password.isEmpty + }, + for: .editingChanged + ) textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.delegate = context.coordinator @@ -329,30 +336,12 @@ extension EnterPasswordView { self.parent = parent } - func textField( - _ textField: UITextField, - shouldChangeCharactersIn range: NSRange, - replacementString string: String - ) -> Bool { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.updateValues(with: textField) - } - return true - } - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if !parent.viewModel.password.isEmpty { - parent.viewModel.proceedWithPassword() + guard !parent.viewModel.password.isEmpty else { + return false } + parent.viewModel.proceedWithPassword() return true } - - /// Updates the view model with the latest text field values and the enabled state of the continue - /// button. - /// - Parameter textField: The text field who's value recently changed. - func updateValues(with textField: UITextField) { - parent.viewModel.password = textField.text ?? "" - parent.continueAction.isEnabled = !parent.viewModel.password.isEmpty - } } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 78872ee5b..a9aa12440 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -189,15 +189,28 @@ private struct LoginView: UIViewControllerRepresentable { ) uiAlertController.addTextField { textField in + textField.addAction( + UIAction { _ in + viewModel.username = textField.text ?? "" + signInAction.isEnabled = viewModel.signInButtonEnabled + }, + for: .editingChanged + ) textField.autocapitalizationType = .none textField.autocorrectionType = .no - textField.delegate = context.coordinator textField.placeholder = "Username" textField.returnKeyType = .next textField.textContentType = .username } uiAlertController.addTextField { textField in + textField.addAction( + UIAction { _ in + viewModel.password = textField.text ?? "" + signInAction.isEnabled = viewModel.signInButtonEnabled + }, + for: .editingChanged + ) textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.delegate = context.coordinator @@ -244,35 +257,12 @@ extension LoginView { self.parent = parent } - func textField( - _ textField: UITextField, - shouldChangeCharactersIn range: NSRange, - replacementString string: String - ) -> Bool { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.updateValues(with: textField) - } - return true - } - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField.textContentType == .password && - parent.viewModel.signInButtonEnabled { - parent.viewModel.signIn() + guard parent.viewModel.signInButtonEnabled else { + return false } + parent.viewModel.signIn() return true } - - /// Updates the view model with the latest text field values and the enabled state of the sign in - /// button. - /// - Parameter textField: The text field who's value recently changed. - func updateValues(with textField: UITextField) { - if textField.textContentType == .username { - parent.viewModel.username = textField.text ?? "" - } else { - parent.viewModel.password = textField.text ?? "" - } - parent.signInAction.isEnabled = parent.viewModel.signInButtonEnabled - } } } From 4fa7693bdb576003a2cfc2e132413ed1b243d25c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Aug 2022 13:55:38 -0500 Subject: [PATCH 1441/1722] Fixes for changes from the `Prune parent name from head of property names` SDK issue. --- .../Components/BasemapGallery/BasemapGalleryViewModel.swift | 2 +- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift index edf0e9cb8..6cb0b4c1b 100644 --- a/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift +++ b/Sources/ArcGISToolkit/Components/BasemapGallery/BasemapGalleryViewModel.swift @@ -216,7 +216,7 @@ extension GeoModel { /// is `webMercator`. Otherwise scene's `spatialReference`. var actualSpatialReference: SpatialReference? { if let scene = self as? ArcGIS.Scene, - scene.sceneViewTilingScheme == .webMercator { + scene.tilingScheme == .webMercator { return .webMercator } else { return spatialReference diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index bc5cc313c..166eb68de 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -286,7 +286,7 @@ import SwiftUI guard let network = self.network, let geometry = feature.geometry, - let symbol = try? await (feature.featureTable?.layer as? FeatureLayer)? + let symbol = try? await (feature.table?.layer as? FeatureLayer)? .renderer? .symbol(for: feature)? .makeSwatch(scale: 1.0), From 04f0e2e1ed22a2f04920d32925940f5400ee956c Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 18 Aug 2022 12:06:14 -0700 Subject: [PATCH 1442/1722] `@State var` -> `let` --- .../Authentication/CertificatePickerViewModifier.swift | 4 ++-- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index a75e45f67..96d56f026 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -239,10 +239,10 @@ struct EnterPasswordView: UIViewControllerRepresentable { @Binding var isPresented: Bool /// The cancel action for the `UIAlertController`. - @State var cancelAction: UIAlertAction + let cancelAction: UIAlertAction /// The continue action for the `UIAlertController`. - @State var continueAction: UIAlertAction + let continueAction: UIAlertAction /// Creates the view. /// - Parameters: diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index a9aa12440..69cc218ee 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -148,10 +148,10 @@ private struct LoginView: UIViewControllerRepresentable { @Binding var isPresented: Bool /// The cancel action for the `UIAlertController`. - @State var cancelAction: UIAlertAction + let cancelAction: UIAlertAction /// The sign in action for the `UIAlertController`. - @State var signInAction: UIAlertAction + let signInAction: UIAlertAction /// Creates the view. /// - Parameters: From d383c4b524578629e1978131a55eb1ff6ddee13f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 18 Aug 2022 14:05:13 -0700 Subject: [PATCH 1443/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 67de34c84..641e28bdf 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -46,7 +46,7 @@ import XCTest XCTAssertNil(viewModel.network) XCTAssertFalse(viewModel.canRunTrace) XCTAssertEqual( - viewModel.userWarning, + viewModel.userAlert?.description, "No utility networks found." ) } @@ -74,7 +74,7 @@ import XCTest XCTAssertFalse(viewModel.canRunTrace) XCTAssertTrue(viewModel.configurations.isEmpty) XCTAssertEqual( - viewModel.userWarning, + viewModel.userAlert?.description, "No trace types found." ) } @@ -100,7 +100,7 @@ import XCTest XCTAssertFalse(viewModel.canRunTrace) XCTAssertTrue(viewModel.configurations.isEmpty) XCTAssertEqual( - viewModel.userWarning, + viewModel.userAlert?.description, "No trace types found." ) } From d72b02bda437c0cbb33b335ccd9e2c678beab809 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 18 Aug 2022 14:05:32 -0700 Subject: [PATCH 1444/1722] Update XCTestCase+ArcGISURLSession.swift --- .../Extensions/XCTest/XCTestCase+ArcGISURLSession.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift index 7ba7d9ba0..ed376b666 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Extensions/XCTest/XCTestCase+ArcGISURLSession.swift @@ -20,7 +20,7 @@ import XCTest import ArcGIS extension XCTestCase { - func setChallengeHandler(_ challengeHandler: AuthenticationChallengeHandler?) { + func setChallengeHandler(_ challengeHandler: AuthenticationChallengeHandler) { let previous = ArcGISRuntimeEnvironment.authenticationChallengeHandler ArcGISRuntimeEnvironment.authenticationChallengeHandler = challengeHandler addTeardownBlock { ArcGISRuntimeEnvironment.authenticationChallengeHandler = previous } From 929e52ca281bd988cd77c67b1164b33bf92367c3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 18 Aug 2022 14:05:35 -0700 Subject: [PATCH 1445/1722] Update ChallengeHandler.swift --- .../Test Support/Utility/ChallengeHandler.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift index d845f999a..e88e3cac2 100644 --- a/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift +++ b/Tests/ArcGISToolkitTests/Test Support/Utility/ChallengeHandler.swift @@ -17,7 +17,7 @@ // import Foundation -import ArcGIS +@testable import ArcGIS /// A `ChallengeHandler` that that can handle trusting hosts with a self-signed certificate, the URL credential, /// and the token credential. @@ -54,7 +54,9 @@ class ChallengeHandler: AuthenticationChallengeHandler { self.init(trustedHosts: trustedHosts, networkCredentialProvider: { _ in networkCredential }) } - func handleNetworkChallenge(_ challenge: NetworkAuthenticationChallenge) async -> NetworkAuthenticationChallenge.Disposition { + func handleNetworkAuthenticationChallenge( + _ challenge: NetworkAuthenticationChallenge + ) async -> NetworkAuthenticationChallenge.Disposition { // Record challenge only if it is not a server trust. if challenge.kind != .serverTrust { networkChallenges.append(challenge) @@ -75,7 +77,7 @@ class ChallengeHandler: AuthenticationChallengeHandler { } } - func handleArcGISChallenge( + func handleArcGISAuthenticationChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { arcGISChallenges.append(challenge) From 6d823a69edea3b1a24490fef1ba10f0beed03087 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Aug 2022 16:40:23 -0500 Subject: [PATCH 1446/1722] Attachments --- .../Popups/AttachmentsPopupElementView.swift | 118 +++++------------- .../Components/Popups/PopupView.swift | 5 +- 2 files changed, 34 insertions(+), 89 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 457281584..c13f29512 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -14,31 +14,31 @@ import SwiftUI import ArcGIS -struct AttachmentsPopupElementView: View { - typealias AttachmentImage = (attachment:PopupAttachment, image:UIImage?) +// TODO: taking info from Ryan and come up with API for fetching attachment file urls +// TODO: update Visual Code tooling from README and generate stuff for all but attachments(?) +// TODO: look at notes and follow up +// TODO: look at prototype implementations and update if necessary +// TODO: Goal is to have all done on Monday (or at least all but attachments). +struct AttachmentsPopupElementView: View { + typealias AttachmentImage = (attachment:PopupAttachment, image:UIImage) + var popupElement: AttachmentsPopupElement - + @State var attachmentImages = [AttachmentImage]() @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass - /// If `true`, the gallery will display as if the device is in a regular-width orientation. - /// If `false`, the gallery will display as if the device is in a compact-width orientation. + /// A Boolean value denoting if the view should be shown as regular width. var isRegularWidth: Bool { !(horizontalSizeClass == .compact && verticalSizeClass == .regular) } @State var loadingAttachments = true - init( - popupElement: AttachmentsPopupElement, - popup: Popup - ) { + init(popupElement: AttachmentsPopupElement) { self.popupElement = popupElement - // self.popup = popup - // _attachmentHelper = StateObject(wrappedValue: AttachmentHelper(feature: popup.geoElement as? ArcGISFeature)) } var body: some View { @@ -56,14 +56,14 @@ struct AttachmentsPopupElementView: View { else { switch popupElement.displayType { case .list: - // AttachmentPreview(attachments: attachmentHelper.attachments, images: attachmentHelper.attachmentImages) AttachmentList(attachmentImages: attachmentImages) case.preview: - // AttachmentPreview(attachments: attachmentHelper.attachments, images: attachmentHelper.attachmentImages) AttachmentList(attachmentImages: attachmentImages) +// AttachmentPreview(attachmentImages: attachmentImages) case .auto: if isRegularWidth { - AttachmentPreview(attachmentImages: attachmentImages) + AttachmentList(attachmentImages: attachmentImages) +// AttachmentPreview(attachmentImages: attachmentImages) } else { AttachmentList(attachmentImages: attachmentImages) } @@ -77,17 +77,16 @@ struct AttachmentsPopupElementView: View { for attachment in popupElement.attachments { group.addTask { if attachment.kind == .image { - let image = try? await attachment.makeFullImage() - return (attachment,image) + do { + let image = try await attachment.makeFullImage() + return (attachment, image) + } catch { + return (attachment, UIImage(systemName: "photo")!) + } } else { - + return(attachment, UIImage(systemName: "doc")!) } -// let data = try? await attachment.fetchData() -// if let data = data, let image = UIImage(data: data) { -// return (attachment,image) -// } - return (attachment, nil) } } for await pair in group { @@ -96,44 +95,25 @@ struct AttachmentsPopupElementView: View { } loadingAttachments = false - - - // attachments.forEach { attachment in - // Task { - // let data = try? await attachment.fetchData() - // if let data = data, let image = UIImage(data: data) { - // attachmentImages.append(image) - // } - // } - // } - // loadingAttachments = false } } struct AttachmentList: View { var attachmentImages: [AttachmentImage] - // let attachments: [Attachment] - // let images: [UIImage] var body: some View { VStack(alignment: .leading, spacing: 6) { - // if images.count != 2 { - // EmptyView() - // } - // else { - // ForEach(0..<2) { i in - // ForEach(attachmentImages) { attachmentImage in - // HStack { - // Image(uiImage: attachmentImage.image) - // .resizable() - // .aspectRatio(contentMode: .fit) - // .clipShape(RoundedRectangle(cornerRadius: 8)) - // .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) - // .frame(width: 75, height: 75, alignment: .center) - // Text(attachmentImage.attachment.name) - // } - // } - // } + ForEach(attachmentImages, id: \.image) { attachmentImage in + HStack { + Image(uiImage: attachmentImage.image) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) + .frame(width: 75, height: 75, alignment: .center) + Text(attachmentImage.attachment.name) + } + } } } } @@ -166,37 +146,3 @@ struct AttachmentsPopupElementView: View { } extension Attachment: Identifiable {} - -@MainActor class AttachmentHelper: ObservableObject { - var feature: ArcGISFeature? - @Published var attachments = [Attachment]() - @Published var attachmentImages = [UIImage]() - - init(feature: ArcGISFeature?) { - self.feature = feature - if let feature = feature { - Task { - try await fetchAttachments(for: feature) - attachments.forEach { attachment in - Task { - let data = try? await attachment.fetchData() - if let data = data, let image = UIImage(data: data) { - attachmentImages.append(image) - } - } - } - } - } - } - - // TODO: taking info from Ryan and come up with API for fetching attachment file urls - // TODO: update Visual Code tooling from README and generate stuff for all but attachments(?) - // TODO: look at notes and follow up - // TODO: look at prototype implementations and update if necessary - // TODO: Goal is to have all done on Monday (or at least all but attachments). - - func fetchAttachments(for feature: ArcGISFeature) async throws { - print("fetching attachments") - attachments = try await feature.fetchAttachments() - } -} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index eea73ccee..da522d4be 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -73,9 +73,8 @@ public struct PopupView: View { VStack(alignment: .leading) { ForEach(Array(popup.evaluatedElements.enumerated()), id: \.offset) { index, popupElement in switch popupElement { - case is AttachmentsPopupElement: - Text("AttachmentsPopupElementView implementation coming soon.") - .font(.caption) + case let popupElement as AttachmentsPopupElement: + AttachmentsPopupElementView(popupElement: popupElement) case let popupElement as FieldsPopupElement: FieldsPopupElementView(popupElement: popupElement) case is MediaPopupElement: From 9efac936d39bd0a18ba8ee0d8d36677b6c2a6121 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 18 Aug 2022 16:37:06 -0700 Subject: [PATCH 1447/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace/UtilityNetworkTrace.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index b02375eee..f5072941e 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -413,11 +413,13 @@ public struct UtilityNetworkTrace: View { viewModel.deleteAllTraces() currentActivity = .creatingTrace(nil) } label: { - Text(clearResultsTitle) - } - ) + Text("OK") + }) + } label: { + Text(clearResultsTitle) } .buttonStyle(.bordered) + } } /// Displays information about a chosen starting point. From 46eea7cc82cec86164fc0a03e878d5fa72b4d6e6 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 19 Aug 2022 09:49:35 -0700 Subject: [PATCH 1448/1722] `passwordSheet` -> `passwordAlert` --- .../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 96d56f026..cca0da777 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -112,7 +112,7 @@ struct CertificatePickerViewModifier: ViewModifier { isPresented: $viewModel.showPicker, viewModel: viewModel ) - .passwordSheet( + .passwordAlert( isPresented: $viewModel.showPassword, viewModel: viewModel ) @@ -176,7 +176,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 passwordSheet( + @MainActor @ViewBuilder func passwordAlert( isPresented: Binding, viewModel: CertificatePickerViewModel ) -> some View { From 84ff1937102d87d1d2e087857388ddcf04ce64e9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 19 Aug 2022 09:51:01 -0700 Subject: [PATCH 1449/1722] Update Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift Co-authored-by: R Olson --- .../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 cca0da777..d3dec5641 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -228,7 +228,7 @@ private extension View { } } -/// A view that allows the user to enter a password. +/// A view that allows the user to enter a password via an alert. /// /// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. struct EnterPasswordView: UIViewControllerRepresentable { From cd896450a657e96e9a6d54c037d9dff835cdd295 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 19 Aug 2022 10:06:47 -0700 Subject: [PATCH 1450/1722] Mark members private --- .../Authentication/CertificatePickerViewModifier.swift | 8 ++++---- .../ArcGISToolkit/Components/Authentication/Login.swift | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index d3dec5641..4a7a7b61e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -233,16 +233,16 @@ private extension View { /// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. struct EnterPasswordView: UIViewControllerRepresentable { /// The view model. - @ObservedObject var viewModel: CertificatePickerViewModel + @ObservedObject private var viewModel: CertificatePickerViewModel /// A Boolean value indicating whether or not the view is displayed. - @Binding var isPresented: Bool + @Binding private var isPresented: Bool /// The cancel action for the `UIAlertController`. - let cancelAction: UIAlertAction + private let cancelAction: UIAlertAction /// The continue action for the `UIAlertController`. - let continueAction: UIAlertAction + private let continueAction: UIAlertAction /// Creates the view. /// - Parameters: diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 69cc218ee..37b17cb5a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -142,16 +142,16 @@ extension LoginViewModifier { /// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. private struct LoginView: UIViewControllerRepresentable { /// The view model. - @ObservedObject var viewModel: LoginViewModel + @ObservedObject private var viewModel: LoginViewModel /// A Boolean value indicating whether or not the view is displayed. - @Binding var isPresented: Bool + @Binding private var isPresented: Bool /// The cancel action for the `UIAlertController`. - let cancelAction: UIAlertAction + private let cancelAction: UIAlertAction /// The sign in action for the `UIAlertController`. - let signInAction: UIAlertAction + private let signInAction: UIAlertAction /// Creates the view. /// - Parameters: From 9f11b2c96d8a8da94518c07e9004725de2a2ebf5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 19 Aug 2022 10:31:03 -0700 Subject: [PATCH 1451/1722] Add interim doc for presentation delay Better solutions will be investigated --- .../Authentication/CertificatePickerViewModifier.swift | 2 ++ Sources/ArcGISToolkit/Components/Authentication/Login.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 4a7a7b61e..17179a330 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -315,6 +315,8 @@ struct EnterPasswordView: UIViewControllerRepresentable { ) { guard isPresented else { return } let alertController = makeAlertController(context: context) + // On a physical iOS 16 device, without the following delay, the + // presentation fails and an error is thrown. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { uiViewController.present(alertController, animated: true) { isPresented = false diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 37b17cb5a..e8516753c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -236,6 +236,8 @@ private struct LoginView: UIViewControllerRepresentable { ) { guard isPresented else { return } let alertController = makeAlertController(context: context) + // On a physical iOS 16 device, without the following delay, the + // presentation fails and an error is thrown. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { uiViewController.present(alertController, animated: true) { isPresented = false From 9c1c98e13a9fb96a91f2945e2b39b8c774205466 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Aug 2022 15:01:54 -0500 Subject: [PATCH 1452/1722] Add QuickLook, model, loading attachments improvements. --- .../Popups/AttachmentsPopupElementView.swift | 160 +++++++++++++----- 1 file changed, 120 insertions(+), 40 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index c13f29512..429539f9c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -13,6 +13,7 @@ import SwiftUI import ArcGIS +import QuickLook // TODO: taking info from Ryan and come up with API for fetching attachment file urls // TODO: update Visual Code tooling from README and generate stuff for all but attachments(?) @@ -20,12 +21,26 @@ import ArcGIS // TODO: look at prototype implementations and update if necessary // TODO: Goal is to have all done on Monday (or at least all but attachments). -struct AttachmentsPopupElementView: View { - typealias AttachmentImage = (attachment:PopupAttachment, image:UIImage) +class AttachmentImage: ObservableObject, Identifiable { + @Published var attachment: PopupAttachment + @Published var image: UIImage? + @Published var isLoaded = false + var id = UUID() + init(attachment: PopupAttachment, image: UIImage? = nil) { + self.attachment = attachment + self.image = image + } +} + +@MainActor +class AttachmentModel: ObservableObject { + @Published var attachmentImages = [AttachmentImage]() +} + +struct AttachmentsPopupElementView: View { var popupElement: AttachmentsPopupElement - - @State var attachmentImages = [AttachmentImage]() + @StateObject private var viewModel: AttachmentModel @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -39,10 +54,13 @@ struct AttachmentsPopupElementView: View { init(popupElement: AttachmentsPopupElement) { self.popupElement = popupElement + _viewModel = StateObject( + wrappedValue: AttachmentModel() + ) } var body: some View { - VStack { + VStack(alignment: .leading) { PopupElementHeader( title: popupElement.title, description: popupElement.description @@ -56,43 +74,46 @@ struct AttachmentsPopupElementView: View { else { switch popupElement.displayType { case .list: - AttachmentList(attachmentImages: attachmentImages) + AttachmentList(attachmentImages: viewModel.attachmentImages) case.preview: - AttachmentList(attachmentImages: attachmentImages) -// AttachmentPreview(attachmentImages: attachmentImages) + AttachmentList(attachmentImages: viewModel.attachmentImages) +// AttachmentPreview(attachmentImages: viewModel.attachmentImages) case .auto: if isRegularWidth { - AttachmentList(attachmentImages: attachmentImages) -// AttachmentPreview(attachmentImages: attachmentImages) + AttachmentList(attachmentImages: viewModel.attachmentImages) +// AttachmentPreview(attachmentImages: viewModel.attachmentImages) } else { - AttachmentList(attachmentImages: attachmentImages) + AttachmentList(attachmentImages: viewModel.attachmentImages) } } } } .task { try? await popupElement.fetchAttachments() - - await withTaskGroup(of: AttachmentImage.self) { group in - for attachment in popupElement.attachments { - group.addTask { - if attachment.kind == .image { - do { - let image = try await attachment.makeFullImage() - return (attachment, image) - } catch { - return (attachment, UIImage(systemName: "photo")!) - } - } - else { - return(attachment, UIImage(systemName: "doc")!) - } - } - } - for await pair in group { - attachmentImages.append(pair) - } + let attachmentImages = popupElement.attachments.map { attachment in + AttachmentImage(attachment: attachment) } + viewModel.attachmentImages.append(contentsOf: attachmentImages) +// await withTaskGroup(of: AttachmentImage.self) { group in +// for attachment in popupElement.attachments { +// group.addTask { +// if attachment.kind == .image { +// do { +// let image = try await attachment.makeFullImage() +// return (attachment, image) +// } catch { +// return (attachment, UIImage(systemName: "photo")!) +// } +// } +// else { +// return(attachment, UIImage(systemName: "doc")!) +// } +// } +// } +// for await pair in group { +// attachmentImages.append(pair) +// } +// } loadingAttachments = false } @@ -100,20 +121,79 @@ struct AttachmentsPopupElementView: View { struct AttachmentList: View { var attachmentImages: [AttachmentImage] - + @State var url: URL? var body: some View { VStack(alignment: .leading, spacing: 6) { - ForEach(attachmentImages, id: \.image) { attachmentImage in - HStack { - Image(uiImage: attachmentImage.image) - .resizable() - .aspectRatio(contentMode: .fit) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) - .frame(width: 75, height: 75, alignment: .center) + ForEach(attachmentImages) { attachmentImage in + makeAttachmentView(for: attachmentImage) + .onTapGesture { + //QuickLook + } + } + } + } + + func makeImage(for attachmentImage: AttachmentImage) -> Image { + switch attachmentImage.attachment.kind { + case .image: + if let image = attachmentImage.image { + return Image(uiImage: image) + } + else { + return Image(systemName: "photo") + } + case .video: + return Image(systemName: "video") + case .document, .other: + return Image(systemName: "doc") + } + } + + @ViewBuilder func makeAttachmentView(for attachmentImage: AttachmentImage) -> some View { + HStack { + HStack { + makeImage(for: attachmentImage) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) + .frame(width: 60, height: 40, alignment: .center) + VStack(alignment: .leading) { Text(attachmentImage.attachment.name) + .lineLimit(1) + .truncationMode(.middle) + Text("\(attachmentImage.attachment.size.formatted(.byteCount(style: .file)))") + .font(.caption) + .foregroundColor(.secondary) } + Spacer() } + .onTapGesture { + if attachmentImage.attachment.loadStatus == .loaded { + url = attachmentImage.attachment.fileURL + } + } + .quickLookPreview($url) + Button { + Task { + try await attachmentImage.attachment.load() + let image = try await attachmentImage.attachment.makeThumbnail(width: 60, height: 60) + attachmentImage.image = image + } + } label: { + Image(systemName: "square.and.arrow.down") + .padding(.leading) + } + .opacity(showDownloadButton(attachmentImage) ? 1 : 0) + } + } + + func showDownloadButton(_ attachmentImage: AttachmentImage) -> Bool { + print("loadStatus = \(attachmentImage.attachment.loadStatus)") + if attachmentImage.attachment.kind == .image { + return attachmentImage.image == nil + } else { + return attachmentImage.attachment.loadStatus != .loaded } } } From e4b9cf75362d200f993bb0e57c563ac02f1ce89d Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Fri, 19 Aug 2022 16:22:55 -0700 Subject: [PATCH 1453/1722] rename allowRequestToFail to continueWithoutCredential --- .../ArcGISToolkit/Components/Authentication/Authenticator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 9d8fa0d24..160852234 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -117,7 +117,7 @@ extension Authenticator: AuthenticationChallengeHandler { // If `promptForUntrustedHosts` is `false` then perform default handling // for server trust challenges. guard promptForUntrustedHosts || challenge.kind != .serverTrust else { - return .allowRequestToFail + return .continueWithoutCredential } let challengeContinuation = NetworkChallengeContinuation(networkChallenge: challenge) From e82a7709a1d0bebf2c071cbd5e34108d0f4df319 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 19 Aug 2022 17:07:03 -0700 Subject: [PATCH 1454/1722] Create RequiredInputAlertView.swift --- .../Utility/RequiredInputAlertView.swift | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift diff --git a/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift b/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift new file mode 100644 index 000000000..c7b2078c3 --- /dev/null +++ b/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift @@ -0,0 +1,187 @@ +// 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 Foundation +import SwiftUI + +/// A view that prompts a user to login with a username and password. +/// +/// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. +struct RequiredInputAlertView: UIViewControllerRepresentable { + /// A Boolean value indicating whether or not the view is displayed. + @Binding private var isPresented: Bool + + /// <#Description#> + @State var fieldOne = "" + + /// <#Description#> + @State var fieldTwo = "" + + /// <#Description#> + private let title: String + + /// <#Description#> + private let message: String + + /// <#Description#> + private let cancelConfiguration: ActionConfiguration + + /// <#Description#> + private let continueConfiguration: ActionConfiguration + + /// <#Description#> + /// - Parameters: + /// - isPresented: <#isPresented description#> + /// - title: <#title description#> + /// - message: <#message description#> + /// - cancelConfiguration: <#cancelConfiguration description#> + /// - continueConfiguration: <#continueConfiguration description#> + init( + isPresented: Binding, + title: String, + message: String, + cancelConfiguration: ActionConfiguration, + continueConfiguration: ActionConfiguration + ) { + _isPresented = isPresented + + self.cancelConfiguration = cancelConfiguration + + self.continueConfiguration = continueConfiguration + + self.title = title + + self.message = message + } + + /// <#Description#> + var isContinueEnabled: Bool { + !fieldOne.isEmpty && !fieldTwo.isEmpty + } + + /// <#Description#> + /// - Parameter context: <#context description#> + /// - Returns: <#description#> + func makeAlertController(context: Context) -> UIAlertController { + let uiAlertController = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + + let cancelAction = UIAlertAction(title: cancelConfiguration.title, style: .cancel) { _ in + cancelConfiguration.handler(fieldOne, fieldTwo) + } + + let continueAction = UIAlertAction(title: continueConfiguration.title, style: .default) { _ in + continueConfiguration.handler(fieldOne, fieldTwo) + } + + uiAlertController.addTextField { textField in + textField.addAction( + UIAction { _ in + fieldOne = textField.text ?? "" + continueAction.isEnabled = isContinueEnabled + }, + for: .editingChanged + ) + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.placeholder = "Username" + textField.returnKeyType = .next + textField.textContentType = .username + } + + uiAlertController.addTextField { textField in + textField.addAction( + UIAction { _ in + fieldTwo = textField.text ?? "" + continueAction.isEnabled = isContinueEnabled + }, + for: .editingChanged + ) + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.delegate = context.coordinator + textField.isSecureTextEntry = true + textField.placeholder = "Password" + textField.returnKeyType = .go + textField.textContentType = .password + } + + cancelAction.isEnabled = true + continueAction.isEnabled = false + + uiAlertController.addAction(cancelAction) + uiAlertController.addAction(continueAction) + + return uiAlertController + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: Context) -> some UIViewController { + return UIViewController() + } + + func updateUIViewController( + _ uiViewController: UIViewControllerType, + context: Context + ) { + guard isPresented else { return } + let alertController = makeAlertController(context: context) + // On a physical iOS 16 device, without the following delay, the + // presentation fails and an error is thrown. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + uiViewController.present(alertController, animated: true) { + isPresented = false + } + } + } +} + +extension RequiredInputAlertView { + /// The coordinator for the login view that acts as a delegate to the underlying + /// `UIAlertViewController`. + final class Coordinator: NSObject, UITextFieldDelegate { + /// The view that owns this coordinator. + let parent: RequiredInputAlertView + + /// Creates the coordinator. + /// - Parameter parent: The view that owns this coordinator. + init(_ parent: RequiredInputAlertView) { + self.parent = parent + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard !parent.fieldTwo.isEmpty else { + return false + } + parent.continueConfiguration.handler(parent.fieldOne, parent.fieldTwo) + return true + } + } +} + +extension RequiredInputAlertView { + /// <#Description#> + struct ActionConfiguration { + /// <#Description#> + let title: String + + /// <#Description#> + let handler: (String, String) -> Void + } +} From fa438312a3d3586b3f0647f0e5ff422b1a9ddda9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 19 Aug 2022 17:07:14 -0700 Subject: [PATCH 1455/1722] Update Login.swift --- .../Components/Authentication/Login.swift | 153 +++--------------- 1 file changed, 18 insertions(+), 135 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index e8516753c..f067a289f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -93,9 +93,24 @@ struct LoginViewModifier: ViewModifier { content .task { isPresented = true } .overlay { - LoginView( - viewModel: viewModel, - isPresented: $isPresented + RequiredInputAlertView( + isPresented: $isPresented, + title: "Authentication Required", + message: "You must sign in to access '\(viewModel.challengingHost)'", + cancelConfiguration: .init( + title: "Cancel", + handler: { _, _ in + viewModel.cancel() + } + ), + continueConfiguration: .init( + title: "Continue", + handler: { u, p in + viewModel.username = u + viewModel.password = p + viewModel.signIn() + } + ) ) } } @@ -136,135 +151,3 @@ extension LoginViewModifier { ) } } - -/// A view that prompts a user to login with a username and password. -/// -/// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. -private struct LoginView: UIViewControllerRepresentable { - /// The view model. - @ObservedObject private var viewModel: LoginViewModel - - /// A Boolean value indicating whether or not the view is displayed. - @Binding private var isPresented: Bool - - /// The cancel action for the `UIAlertController`. - private let cancelAction: UIAlertAction - - /// The sign in action for the `UIAlertController`. - private let signInAction: UIAlertAction - - /// Creates the view. - /// - Parameters: - /// - viewModel: The view model. - /// - isPresented: A Boolean value indicating whether or not the view is displayed. - init(viewModel: LoginViewModel, isPresented: Binding) { - self.viewModel = viewModel - - _isPresented = isPresented - - cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in - viewModel.cancel() - } - signInAction = UIAlertAction(title: "Sign In", style: .default) { _ in - viewModel.signIn() - } - - cancelAction.isEnabled = true - signInAction.isEnabled = false - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - /// Creates the alert controller object and configures its initial state. - /// - Parameter context: A context structure containing information about the current state of the - /// system. - /// - Returns: A configured alert controller. - func makeAlertController(context: Context) -> UIAlertController { - let uiAlertController = UIAlertController( - title: "Authentication Required", - message: "You must sign in to access '\(viewModel.challengingHost)'", - preferredStyle: .alert - ) - - uiAlertController.addTextField { textField in - textField.addAction( - UIAction { _ in - viewModel.username = textField.text ?? "" - signInAction.isEnabled = viewModel.signInButtonEnabled - }, - for: .editingChanged - ) - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.placeholder = "Username" - textField.returnKeyType = .next - textField.textContentType = .username - } - - uiAlertController.addTextField { textField in - textField.addAction( - UIAction { _ in - viewModel.password = textField.text ?? "" - signInAction.isEnabled = viewModel.signInButtonEnabled - }, - for: .editingChanged - ) - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.delegate = context.coordinator - textField.isSecureTextEntry = true - textField.placeholder = "Password" - textField.returnKeyType = .go - textField.textContentType = .password - } - - uiAlertController.addAction(cancelAction) - uiAlertController.addAction(signInAction) - - return uiAlertController - } - - func makeUIViewController(context: Context) -> UIViewController { - return UIViewController() - } - - func updateUIViewController( - _ uiViewController: UIViewControllerType, - context: Context - ) { - guard isPresented else { return } - let alertController = makeAlertController(context: context) - // On a physical iOS 16 device, without the following delay, the - // presentation fails and an error is thrown. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - uiViewController.present(alertController, animated: true) { - isPresented = false - } - } - } -} - -extension LoginView { - /// The coordinator for the login view that acts as a delegate to the underlying - /// `UIAlertViewController`. - final class Coordinator: NSObject, UITextFieldDelegate { - /// The view that owns this coordinator. - let parent: LoginView - - /// Creates the coordinator. - /// - Parameter parent: The view that owns this coordinator. - init(_ parent: LoginView) { - self.parent = parent - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - guard parent.viewModel.signInButtonEnabled else { - return false - } - parent.viewModel.signIn() - return true - } - } -} From daed8d27c707a29989005c1f53f8e007234a49af Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 20 Aug 2022 11:30:27 -0500 Subject: [PATCH 1456/1722] Cleanup, comments. --- .../Popups/AttachmentsPopupElementView.swift | 232 +++++++++++------- 1 file changed, 146 insertions(+), 86 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 429539f9c..d7aa9bf99 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -15,26 +15,38 @@ import SwiftUI import ArcGIS import QuickLook -// TODO: taking info from Ryan and come up with API for fetching attachment file urls -// TODO: update Visual Code tooling from README and generate stuff for all but attachments(?) // TODO: look at notes and follow up -// TODO: look at prototype implementations and update if necessary -// TODO: Goal is to have all done on Monday (or at least all but attachments). -class AttachmentImage: ObservableObject, Identifiable { +// TODO: Add alert for when attachments fail to load... +// TODO: Move classes into separate file; maybe structs too. + +@MainActor class AttachmentImage: ObservableObject { @Published var attachment: PopupAttachment @Published var image: UIImage? - @Published var isLoaded = false - var id = UUID() + @Published var loadStatus: LoadStatus = .notLoaded init(attachment: PopupAttachment, image: UIImage? = nil) { self.attachment = attachment self.image = image } + + func load() async throws { + loadStatus = .loading + try? await self.attachment.load() + loadStatus = self.attachment.loadStatus + } +} + +extension AttachmentImage: Identifiable {} + +extension AttachmentImage: Equatable { + static func == (lhs: AttachmentImage, rhs: AttachmentImage) -> Bool { + lhs.attachment === rhs.attachment && + lhs.image === rhs.image + } } -@MainActor -class AttachmentModel: ObservableObject { +@MainActor class AttachmentModel: ObservableObject { @Published var attachmentImages = [AttachmentImage]() } @@ -59,6 +71,7 @@ struct AttachmentsPopupElementView: View { ) } + // TODO: Move VStack into final else so that we won't show anything if there are no attachments????? var body: some View { VStack(alignment: .leading) { PopupElementHeader( @@ -68,8 +81,10 @@ struct AttachmentsPopupElementView: View { Spacer() if loadingAttachments { ProgressView() + .padding() } else if popupElement.attachments.count == 0 { Text("No attachments.") + .padding() } else { switch popupElement.displayType { @@ -94,106 +109,153 @@ struct AttachmentsPopupElementView: View { AttachmentImage(attachment: attachment) } viewModel.attachmentImages.append(contentsOf: attachmentImages) -// await withTaskGroup(of: AttachmentImage.self) { group in -// for attachment in popupElement.attachments { -// group.addTask { -// if attachment.kind == .image { -// do { -// let image = try await attachment.makeFullImage() -// return (attachment, image) -// } catch { -// return (attachment, UIImage(systemName: "photo")!) -// } -// } -// else { -// return(attachment, UIImage(systemName: "doc")!) -// } -// } -// } -// for await pair in group { -// attachmentImages.append(pair) -// } -// } - loadingAttachments = false } } struct AttachmentList: View { var attachmentImages: [AttachmentImage] - @State var url: URL? var body: some View { VStack(alignment: .leading, spacing: 6) { ForEach(attachmentImages) { attachmentImage in - makeAttachmentView(for: attachmentImage) - .onTapGesture { - //QuickLook + AttachmentRow(attachmentImage: attachmentImage) + if attachmentImage != attachmentImages.last { + Divider() } } } } - func makeImage(for attachmentImage: AttachmentImage) -> Image { - switch attachmentImage.attachment.kind { - case .image: + struct ImageView: View { + @ObservedObject var attachmentImage: AttachmentImage + + // TODO: instead of this big block, pre-load attachmentImage.image with correct placeholder image + // which will get overwritten when the preview image loads. + var body: some View { if let image = attachmentImage.image { - return Image(uiImage: image) + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .padding(2) + .frame(width: 40, height: 40, alignment: .center) } else { - return Image(systemName: "photo") + switch attachmentImage.attachment.kind { + case .image: + if let image = attachmentImage.image { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) + .frame(width: 40, height: 40, alignment: .center) + } + else { + Image(systemName: "photo") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.accentColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) + .frame(width: 40, height: 40, alignment: .center) + } + case .video: + Image(systemName: "video") + .resizable() + .foregroundColor(.accentColor) + .aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) + .frame(width: 40, height: 40, alignment: .center) + case .document, .other: + Image(systemName: "doc") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.accentColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) + .frame(width: 40, height: 30, alignment: .center) + } } - case .video: - return Image(systemName: "video") - case .document, .other: - return Image(systemName: "doc") } } - @ViewBuilder func makeAttachmentView(for attachmentImage: AttachmentImage) -> some View { - HStack { + struct AttachmentRow: View { + @ObservedObject var attachmentImage: AttachmentImage + @State var url: URL? + @Environment(\.displayScale) var displayScale + + var body: some View { HStack { - makeImage(for: attachmentImage) - .resizable() - .aspectRatio(contentMode: .fit) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) - .frame(width: 60, height: 40, alignment: .center) - VStack(alignment: .leading) { - Text(attachmentImage.attachment.name) - .lineLimit(1) - .truncationMode(.middle) - Text("\(attachmentImage.attachment.size.formatted(.byteCount(style: .file)))") - .font(.caption) - .foregroundColor(.secondary) + HStack { + ImageView(attachmentImage: attachmentImage) + VStack(alignment: .leading) { + Text(attachmentImage.attachment.name) + .lineLimit(1) + .truncationMode(.middle) + Text("\(attachmentImage.attachment.size.formatted(.byteCount(style: .file)))") + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) } - Spacer() - } - .onTapGesture { - if attachmentImage.attachment.loadStatus == .loaded { - url = attachmentImage.attachment.fileURL + .onTapGesture { + if attachmentImage.attachment.loadStatus == .loaded { + url = attachmentImage.attachment.fileURL + } } - } - .quickLookPreview($url) - Button { - Task { - try await attachmentImage.attachment.load() - let image = try await attachmentImage.attachment.makeThumbnail(width: 60, height: 60) - attachmentImage.image = image + .quickLookPreview($url) + if attachmentImage.loadStatus != .loaded { + Button { + Task { + // TODO: Move this into a separate function + try await attachmentImage.load() + + let request = QLThumbnailGenerator.Request( + fileAt: attachmentImage.attachment.fileURL, + size: CGSize(width: 40, height: 40), + scale: displayScale, + representationTypes: .thumbnail) + + // 2 + let generator = QLThumbnailGenerator.shared + generator.generateRepresentations(for: request) { thumbnail, _, error in + // 3 + Task { + await MainActor.run { + if let thumbnail = thumbnail { + attachmentImage.image = thumbnail.uiImage + } + } + } + } + } + } label: { + // TODO: Move this into a separate view + Group { + switch attachmentImage.loadStatus { + case .notLoaded: + Image(systemName: "square.and.arrow.down") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + case .loading: + ProgressView() + case .loaded: + EmptyView() + case .failed: + Image(systemName: "exclamationmark.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + .background(Color.clear) + .frame(width: 24, height: 24) + } + } + .padding(.leading) + } } - } label: { - Image(systemName: "square.and.arrow.down") - .padding(.leading) } - .opacity(showDownloadButton(attachmentImage) ? 1 : 0) - } - } - - func showDownloadButton(_ attachmentImage: AttachmentImage) -> Bool { - print("loadStatus = \(attachmentImage.attachment.loadStatus)") - if attachmentImage.attachment.kind == .image { - return attachmentImage.image == nil - } else { - return attachmentImage.attachment.loadStatus != .loaded } } } @@ -212,7 +274,7 @@ struct AttachmentsPopupElementView: View { // Image(uiImage: images[i]) // .resizable() // .aspectRatio(contentMode: .fit) - // .clipShape(RoundedRectangle(cornerRadius: 8)) + // .clipShape(RoundedRectangle(cornerRadius: 4)) //// .frame(width: 75, height: 75, alignment: .center) // } // Text(attachments[i].name) @@ -224,5 +286,3 @@ struct AttachmentsPopupElementView: View { } } } - -extension Attachment: Identifiable {} From b01d2a105b191442d0a68e698bf96e83ab9e2b34 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 11:20:33 -0700 Subject: [PATCH 1457/1722] Add styles, use in cert picker --- .../CertificatePickerViewModifier.swift | 141 +++--------------- .../Components/Authentication/Login.swift | 7 +- .../Utility/RequiredInputAlertView.swift | 46 ++++-- 3 files changed, 55 insertions(+), 139 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 17179a330..c8020e13d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -181,9 +181,24 @@ private extension View { viewModel: CertificatePickerViewModel ) -> some View { overlay { - EnterPasswordView( - viewModel: viewModel, - isPresented: isPresented + RequiredInputAlertView( + isPresented: isPresented, + style: .passwordOnly, + title: "Password Required", + message: "Please enter a password for the chosen certificate.", + cancelConfiguration: .init( + title: "Cancel", + handler: { _, _ in + viewModel.cancel() + } + ), + continueConfiguration: .init( + title: "OK", + handler: { _, password in + viewModel.password = password + viewModel.proceedWithPassword() + } + ) ) } } @@ -227,123 +242,3 @@ private extension View { } } } - -/// A view that allows the user to enter a password via an alert. -/// -/// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. -struct EnterPasswordView: UIViewControllerRepresentable { - /// The view model. - @ObservedObject private var viewModel: CertificatePickerViewModel - - /// A Boolean value indicating whether or not the view is displayed. - @Binding private var isPresented: Bool - - /// The cancel action for the `UIAlertController`. - private let cancelAction: UIAlertAction - - /// The continue action for the `UIAlertController`. - private let continueAction: UIAlertAction - - /// Creates the view. - /// - Parameters: - /// - viewModel: The view model. - /// - isPresented: A Boolean value indicating whether or not the view is displayed. - init( - viewModel: CertificatePickerViewModel, - isPresented: Binding - ) { - self.viewModel = viewModel - - _isPresented = isPresented - - cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in - viewModel.cancel() - } - continueAction = UIAlertAction(title: "OK", style: .default) { _ in - viewModel.proceedWithPassword() - } - - cancelAction.isEnabled = true - continueAction.isEnabled = false - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - /// Creates the alert controller object and configures its initial state. - /// - Parameter context: A context structure containing information about the current state of the - /// system. - /// - Returns: A configured alert controller. - func makeAlertController(context: Context) -> UIAlertController { - let uiAlertController = UIAlertController( - title: "Password Required", - message: "Please enter a password for the chosen certificate.", - preferredStyle: .alert - ) - - uiAlertController.addTextField { textField in - textField.addAction( - UIAction { _ in - viewModel.password = textField.text ?? "" - continueAction.isEnabled = !viewModel.password.isEmpty - }, - for: .editingChanged - ) - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.delegate = context.coordinator - textField.isSecureTextEntry = true - textField.placeholder = "Password" - textField.returnKeyType = .go - textField.textContentType = .password - } - - uiAlertController.addAction(cancelAction) - uiAlertController.addAction(continueAction) - - return uiAlertController - } - - func makeUIViewController(context: Context) -> UIViewController { - return UIViewController() - } - - func updateUIViewController( - _ uiViewController: UIViewControllerType, - context: Context - ) { - guard isPresented else { return } - let alertController = makeAlertController(context: context) - // On a physical iOS 16 device, without the following delay, the - // presentation fails and an error is thrown. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - uiViewController.present(alertController, animated: true) { - isPresented = false - } - } - } -} - -extension EnterPasswordView { - /// The coordinator for the password view that acts as a delegate to the underlying - /// `UIAlertViewController`. - final class Coordinator: NSObject, UITextFieldDelegate { - /// The view that owns this coordinator. - let parent: EnterPasswordView - - /// Creates the coordinator. - /// - Parameter parent: The view that owns this coordinator. - init(_ parent: EnterPasswordView) { - self.parent = parent - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - guard !parent.viewModel.password.isEmpty else { - return false - } - parent.viewModel.proceedWithPassword() - return true - } - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index f067a289f..3ed650bd6 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -95,6 +95,7 @@ struct LoginViewModifier: ViewModifier { .overlay { RequiredInputAlertView( isPresented: $isPresented, + style: .identityAndPassword, title: "Authentication Required", message: "You must sign in to access '\(viewModel.challengingHost)'", cancelConfiguration: .init( @@ -105,9 +106,9 @@ struct LoginViewModifier: ViewModifier { ), continueConfiguration: .init( title: "Continue", - handler: { u, p in - viewModel.username = u - viewModel.password = p + handler: { username, password in + viewModel.username = username + viewModel.password = password viewModel.signIn() } ) diff --git a/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift b/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift index c7b2078c3..429976765 100644 --- a/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift +++ b/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift @@ -27,6 +27,9 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { /// <#Description#> @State var fieldTwo = "" + /// <#Description#> + private let style: Style + /// <#Description#> private let title: String @@ -42,12 +45,14 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { /// <#Description#> /// - Parameters: /// - isPresented: <#isPresented description#> + /// - style: <#style description#> /// - title: <#title description#> /// - message: <#message description#> /// - cancelConfiguration: <#cancelConfiguration description#> /// - continueConfiguration: <#continueConfiguration description#> init( isPresented: Binding, + style: Style, title: String, message: String, cancelConfiguration: ActionConfiguration, @@ -55,6 +60,8 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { ) { _isPresented = isPresented + self.style = style + self.cancelConfiguration = cancelConfiguration self.continueConfiguration = continueConfiguration @@ -87,19 +94,21 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { continueConfiguration.handler(fieldOne, fieldTwo) } - uiAlertController.addTextField { textField in - textField.addAction( - UIAction { _ in - fieldOne = textField.text ?? "" - continueAction.isEnabled = isContinueEnabled - }, - for: .editingChanged - ) - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.placeholder = "Username" - textField.returnKeyType = .next - textField.textContentType = .username + if style == .identityAndPassword { + uiAlertController.addTextField { textField in + textField.addAction( + UIAction { _ in + fieldOne = textField.text ?? "" + continueAction.isEnabled = isContinueEnabled + }, + for: .editingChanged + ) + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.placeholder = "Username" + textField.returnKeyType = .next + textField.textContentType = .username + } } uiAlertController.addTextField { textField in @@ -185,3 +194,14 @@ extension RequiredInputAlertView { let handler: (String, String) -> Void } } + +extension RequiredInputAlertView { + /// <#Description#> + enum Style { + /// + case identityAndPassword + + /// + case passwordOnly + } +} From c226171dfb760c27431f9b3013f2278f5eaf37b4 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 11:52:48 -0700 Subject: [PATCH 1458/1722] `style` -> `usage` --- .../CertificatePickerViewModifier.swift | 4 +-- .../Components/Authentication/Login.swift | 4 +-- .../Utility/RequiredInputAlertView.swift | 30 ++++++++++--------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index c8020e13d..a52b4d9b5 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -183,9 +183,9 @@ private extension View { overlay { RequiredInputAlertView( isPresented: isPresented, - style: .passwordOnly, - title: "Password Required", message: "Please enter a password for the chosen certificate.", + title: "Password Required", + usage: .passwordOnly, cancelConfiguration: .init( title: "Cancel", handler: { _, _ in diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 3ed650bd6..19021d8b1 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -95,9 +95,9 @@ struct LoginViewModifier: ViewModifier { .overlay { RequiredInputAlertView( isPresented: $isPresented, - style: .identityAndPassword, - title: "Authentication Required", message: "You must sign in to access '\(viewModel.challengingHost)'", + title: "Authentication Required", + usage: .identityAndPassword, cancelConfiguration: .init( title: "Cancel", handler: { _, _ in diff --git a/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift b/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift index 429976765..ed30e73d5 100644 --- a/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift +++ b/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift @@ -28,7 +28,7 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { @State var fieldTwo = "" /// <#Description#> - private let style: Style + private let usage: Usage /// <#Description#> private let title: String @@ -45,35 +45,37 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { /// <#Description#> /// - Parameters: /// - isPresented: <#isPresented description#> - /// - style: <#style description#> - /// - title: <#title description#> /// - message: <#message description#> + /// - title: <#title description#> + /// - usage: <#usage description#> /// - cancelConfiguration: <#cancelConfiguration description#> /// - continueConfiguration: <#continueConfiguration description#> init( isPresented: Binding, - style: Style, - title: String, message: String, + title: String, + usage: Usage, cancelConfiguration: ActionConfiguration, continueConfiguration: ActionConfiguration ) { _isPresented = isPresented - self.style = style + self.message = message + self.title = title + self.usage = usage self.cancelConfiguration = cancelConfiguration - self.continueConfiguration = continueConfiguration - - self.title = title - - self.message = message } /// <#Description#> var isContinueEnabled: Bool { - !fieldOne.isEmpty && !fieldTwo.isEmpty + switch usage { + case .identityAndPassword: + return !fieldOne.isEmpty && !fieldTwo.isEmpty + case .passwordOnly: + return !fieldTwo.isEmpty + } } /// <#Description#> @@ -94,7 +96,7 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { continueConfiguration.handler(fieldOne, fieldTwo) } - if style == .identityAndPassword { + if usage == .identityAndPassword { uiAlertController.addTextField { textField in textField.addAction( UIAction { _ in @@ -197,7 +199,7 @@ extension RequiredInputAlertView { extension RequiredInputAlertView { /// <#Description#> - enum Style { + enum Usage { /// case identityAndPassword From a656f20b67c518c0331d4503fc55b28380ecf3ac Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 12:13:53 -0700 Subject: [PATCH 1459/1722] Documentation --- .../CertificatePickerViewModifier.swift | 2 +- .../Components/Authentication/Login.swift | 2 +- ...rtView.swift => CredentialInputView.swift} | 143 +++++++++++------- 3 files changed, 87 insertions(+), 60 deletions(-) rename Sources/ArcGISToolkit/Utility/{RequiredInputAlertView.swift => CredentialInputView.swift} (55%) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index a52b4d9b5..558aeec21 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -181,7 +181,7 @@ private extension View { viewModel: CertificatePickerViewModel ) -> some View { overlay { - RequiredInputAlertView( + CredentialInputView( isPresented: isPresented, message: "Please enter a password for the chosen certificate.", title: "Password Required", diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 19021d8b1..0353f51ec 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -93,7 +93,7 @@ struct LoginViewModifier: ViewModifier { content .task { isPresented = true } .overlay { - RequiredInputAlertView( + CredentialInputView( isPresented: $isPresented, message: "You must sign in to access '\(viewModel.challengingHost)'", title: "Authentication Required", diff --git a/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift similarity index 55% rename from Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift rename to Sources/ArcGISToolkit/Utility/CredentialInputView.swift index ed30e73d5..93cee90ce 100644 --- a/Sources/ArcGISToolkit/Utility/RequiredInputAlertView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -14,42 +14,46 @@ import Foundation import SwiftUI -/// A view that prompts a user to login with a username and password. +/// A view that prompts a user to provide credentials. It can be configured to require either an identity and +/// password, or a password only. /// -/// Implemented in UIKit because as of iOS 16, SwiftUI alerts don't support visible but disabled buttons. -struct RequiredInputAlertView: UIViewControllerRepresentable { - /// A Boolean value indicating whether or not the view is displayed. - @Binding private var isPresented: Bool - - /// <#Description#> - @State var fieldOne = "" +/// The view is implemented as a wrapper for a UIKit `UIAlertController` because as of iOS 16, +/// SwiftUI alerts don't support visible but disabled buttons. +struct CredentialInputView: UIViewControllerRepresentable { + /// The cancel action configuration. + private let cancelConfiguration: ActionConfiguration - /// <#Description#> - @State var fieldTwo = "" + /// The continue action configuration. + private let continueConfiguration: ActionConfiguration - /// <#Description#> - private let usage: Usage + /// The value in the identity field. + /// + /// This member is unused when usage is set to `Usage.passwordOnly`. + @State private var identity = "" - /// <#Description#> - private let title: String + /// A Boolean value indicating whether or not the view is displayed. + @Binding private var isPresented: Bool - /// <#Description#> + /// Descriptive text that provides more details about the reason for the alert. private let message: String - /// <#Description#> - private let cancelConfiguration: ActionConfiguration + /// The value in the password field. + @State private var password = "" - /// <#Description#> - private let continueConfiguration: ActionConfiguration + /// The title of the alert. + private let title: String - /// <#Description#> + /// The usage of the alert. + private let usage: Usage + + /// Creates the view. /// - Parameters: - /// - isPresented: <#isPresented description#> - /// - message: <#message description#> - /// - title: <#title description#> - /// - usage: <#usage description#> - /// - cancelConfiguration: <#cancelConfiguration description#> - /// - continueConfiguration: <#continueConfiguration description#> + /// - isPresented: A Boolean value indicating whether or not the view is displayed. + /// - message: Descriptive text that provides more details about the reason for the alert. + /// - title: The title of the alert. + /// - usage: The usage of the alert. + /// - cancelConfiguration: The cancel action configuration. + /// - continueConfiguration: The continue action configuration. init( isPresented: Binding, message: String, @@ -58,49 +62,56 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { cancelConfiguration: ActionConfiguration, continueConfiguration: ActionConfiguration ) { + self.cancelConfiguration = cancelConfiguration + self.continueConfiguration = continueConfiguration + _isPresented = isPresented self.message = message self.title = title self.usage = usage - - self.cancelConfiguration = cancelConfiguration - self.continueConfiguration = continueConfiguration } - /// <#Description#> - var isContinueEnabled: Bool { + /// A Boolean value indicating whether the alert should allow the continue action to proceed. + private var isContinueEnabled: Bool { switch usage { case .identityAndPassword: - return !fieldOne.isEmpty && !fieldTwo.isEmpty + return !identity.isEmpty && !password.isEmpty case .passwordOnly: - return !fieldTwo.isEmpty + return !password.isEmpty } } - /// <#Description#> - /// - Parameter context: <#context description#> - /// - Returns: <#description#> - func makeAlertController(context: Context) -> UIAlertController { + /// Creates the alert controller. + /// - Parameter context: A context structure containing information about the current state of the + /// system. + /// - Returns: The alert controller displayed to the user. + private func makeAlertController(context: Context) -> UIAlertController { let uiAlertController = UIAlertController( title: title, message: message, preferredStyle: .alert ) - let cancelAction = UIAlertAction(title: cancelConfiguration.title, style: .cancel) { _ in - cancelConfiguration.handler(fieldOne, fieldTwo) + let cancelAction = UIAlertAction( + title: cancelConfiguration.title, + style: .cancel + ) { _ in + cancelConfiguration.handler(identity, password) } - let continueAction = UIAlertAction(title: continueConfiguration.title, style: .default) { _ in - continueConfiguration.handler(fieldOne, fieldTwo) + let continueAction = UIAlertAction( + title: continueConfiguration.title, + style: .default + ) { _ in + continueConfiguration.handler(identity, password) } if usage == .identityAndPassword { uiAlertController.addTextField { textField in textField.addAction( UIAction { _ in - fieldOne = textField.text ?? "" + identity = textField.text ?? "" continueAction.isEnabled = isContinueEnabled }, for: .editingChanged @@ -116,14 +127,18 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { uiAlertController.addTextField { textField in textField.addAction( UIAction { _ in - fieldTwo = textField.text ?? "" + password = textField.text ?? "" continueAction.isEnabled = isContinueEnabled }, for: .editingChanged ) textField.autocapitalizationType = .none textField.autocorrectionType = .no + + // Add a coordinator to the password field so that the primary + // keyboard action can be disabled when the field is empty. textField.delegate = context.coordinator + textField.isSecureTextEntry = true textField.placeholder = "Password" textField.returnKeyType = .go @@ -163,47 +178,59 @@ struct RequiredInputAlertView: UIViewControllerRepresentable { } } -extension RequiredInputAlertView { +extension CredentialInputView { /// The coordinator for the login view that acts as a delegate to the underlying /// `UIAlertViewController`. final class Coordinator: NSObject, UITextFieldDelegate { /// The view that owns this coordinator. - let parent: RequiredInputAlertView + let parent: CredentialInputView /// Creates the coordinator. /// - Parameter parent: The view that owns this coordinator. - init(_ parent: RequiredInputAlertView) { + init(_ parent: CredentialInputView) { self.parent = parent } func textFieldShouldReturn(_ textField: UITextField) -> Bool { - guard !parent.fieldTwo.isEmpty else { - return false - } - parent.continueConfiguration.handler(parent.fieldOne, parent.fieldTwo) + guard !parent.password.isEmpty else { return false } + parent.continueConfiguration.handler( + parent.identity, + parent.password + ) return true } } } -extension RequiredInputAlertView { - /// <#Description#> +extension CredentialInputView { + /// A configuration for an alert action. struct ActionConfiguration { - /// <#Description#> + /// The title of the action. let title: String - /// <#Description#> + /// The block to execute when the action is triggered. let handler: (String, String) -> Void + + /// Makes the action configuration. + /// - Parameters: + /// - title: The title of the action. + /// - handler: The block to execute when the action is triggered. The first and second + /// parameters correspond to the identity and password field values. + init(title: String, handler: @escaping (String, String) -> Void) { + self.title = title + self.handler = handler + } } } -extension RequiredInputAlertView { - /// <#Description#> +extension CredentialInputView { + /// The usage of the view. This determines if the view is intended to require either an identity and + /// password, or a password only. enum Usage { - /// + /// Indicates the view is intended to collect an identity and password. case identityAndPassword - /// + /// Indicates the view is intended to collect a password only. case passwordOnly } } From 6d0175c551f15eb33cd8be87165c11b0ed49b328 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 13:22:53 -0700 Subject: [PATCH 1460/1722] `makeHandleArea` -> `makeHandleView` --- .../Components/FloatingPanel/FloatingPanel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index b45566fab..524772b7e 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -74,13 +74,13 @@ struct FloatingPanel: View where Content: View { GeometryReader { geometryProxy in VStack(spacing: 0) { if isCompact && isPresented { - makeHandleArea() + makeHandleView() } content .frame(height: height) .padding(.bottom, isCompact ? 25 : .zero) if !isCompact && isPresented { - makeHandleArea() + makeHandleView() } } .background(backgroundColor) @@ -167,7 +167,7 @@ struct FloatingPanel: View where Content: View { /// Configures a handle area. /// - Returns: A configured handle area, suitable for placement in the panel. - @ViewBuilder func makeHandleArea() -> some View { + @ViewBuilder func makeHandleView() -> some View { ZStack { backgroundColor Handle(color: handleColor) From 5352247eb85c8e4a1a876037d65275c0d90a47ce Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 13:25:24 -0700 Subject: [PATCH 1461/1722] Doc simplification --- .../Components/FloatingPanel/FloatingPanel.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 524772b7e..063182c6d 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -34,10 +34,11 @@ struct FloatingPanel: View where Content: View { let content: Content /// Creates a `FloatingPanel` - /// - Parameter backgroundColor: The background color of the floating panel. - /// - Parameter detent: Controls the height of the panel. - /// - Parameter isPresented: A Boolean value indicating if the view is presented. - /// - Parameter content: The view shown in the floating panel. + /// - Parameters: + /// - backgroundColor: The background color of the floating panel. + /// - detent: Controls the height of the panel. + /// - isPresented: A Boolean value indicating if the view is presented. + /// - content: The view shown in the floating panel. init( backgroundColor: Color, detent: Binding, From c75e7e39e52fe818c6f0b0c8c3e858eb37e9be26 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 13:26:11 -0700 Subject: [PATCH 1462/1722] Update FloatingPanel.swift --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 063182c6d..43e90aaf7 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -33,7 +33,7 @@ struct FloatingPanel: View where Content: View { /// The content shown in the floating panel. let content: Content - /// Creates a `FloatingPanel` + /// Creates a `FloatingPanel`. /// - Parameters: /// - backgroundColor: The background color of the floating panel. /// - detent: Controls the height of the panel. From 2b82038796373c5e1b78a53139f8f27368666a27 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 13:30:57 -0700 Subject: [PATCH 1463/1722] Doc updates --- .../Components/FloatingPanel/FloatingPanel.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 43e90aaf7..24f796842 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -145,7 +145,7 @@ struct FloatingPanel: View where Content: View { } } - /// The detent that would produce a height that is closest to the current height + /// The detent that would produce a height that is closest to the current height. var closestDetent: FloatingPanelDetent { return FloatingPanelDetent.allCases.min { abs(heightFor(detent: $0) - height) < @@ -153,6 +153,7 @@ struct FloatingPanel: View where Content: View { } ?? .half } + /// Calculates the height for the `detent`. /// - Parameter detent: The detent to use when calculating height /// - Returns: A height for the provided detent based on the current maximum height func heightFor(detent: FloatingPanelDetent) -> CGFloat { @@ -166,8 +167,8 @@ struct FloatingPanel: View where Content: View { } } - /// Configures a handle area. - /// - Returns: A configured handle area, suitable for placement in the panel. + /// Configures a handle view. + /// - Returns: A configured handle view, suitable for placement in the panel. @ViewBuilder func makeHandleView() -> some View { ZStack { backgroundColor @@ -221,6 +222,7 @@ private struct RoundedCorners: Shape { 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. From d695cbb9bdc722c2ee8156b0d9e9ee1cac8bd405 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 13:33:28 -0700 Subject: [PATCH 1464/1722] Update FloatingPanelDetent.swift --- .../Components/FloatingPanel/FloatingPanelDetent.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift index e9d4c2f43..4502b6544 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelDetent.swift @@ -15,7 +15,10 @@ import Foundation /// A value that represents a height where a sheet naturally rests. public enum FloatingPanelDetent: CaseIterable { + /// A height large enough to display a short summary. case summary + /// A height roughly half of the maximum height. case half + /// A height just less than the maximum height. case full } From efe710227f72f44b1225aed8b8d0d46dcfe28df8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 13:40:33 -0700 Subject: [PATCH 1465/1722] Update FloatingPanel.swift --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 24f796842..3ebd8e6e1 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -174,7 +174,7 @@ struct FloatingPanel: View where Content: View { backgroundColor Handle(color: handleColor) } - .frame(height: 20) + .frame(height: 30) .gesture(drag) .zIndex(1) } From 9c76d72adb838235ae487d0b28935fbc09989255 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 14:27:55 -0700 Subject: [PATCH 1466/1722] Remove deprecated method --- .../Components/FloatingPanel/FloatingPanelModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift index aef6690a5..b01a3f5ab 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanelModifier.swift @@ -95,7 +95,7 @@ private struct FloatingPanelModifier: ViewModifier where PanelCont ) { panelContent } - .edgesIgnoringSafeArea(.bottom) + .ignoresSafeArea(.all, edges: .bottom) .frame(maxWidth: isCompact ? .infinity : maxWidth) } } From 77e686740ae56712c6ae2e9427d110f12b6dc134 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Mon, 22 Aug 2022 14:44:25 -0700 Subject: [PATCH 1467/1722] Move padding location --- .../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 3ebd8e6e1..9dd50f4d7 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -88,8 +88,6 @@ struct FloatingPanel: View where Content: View { .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) .shadow(radius: 10) .opacity(isPresented ? 1.0 : .zero) - .padding([.leading, .top, .trailing], isCompact ? 0 : 10) - .padding([.bottom], isCompact ? 0 : 50) .frame( width: geometryProxy.size.width, height: geometryProxy.size.height, @@ -116,6 +114,8 @@ struct FloatingPanel: View where Content: View { } .animation(.default, value: isPresented) } + .padding([.leading, .top, .trailing], isCompact ? 0 : 10) + .padding([.bottom], isCompact ? 0 : 50) } var drag: some Gesture { From 636f7f4eb5c50c4b502f05c561802c72a122fe5c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Aug 2022 18:12:17 -0500 Subject: [PATCH 1468/1722] Refactor and implement Preview type. --- .../Popups/AttachmentsPopupElementModel.swift | 90 +++++ .../Popups/AttachmentsPopupElementView.swift | 315 ++++++++---------- 2 files changed, 228 insertions(+), 177 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementModel.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementModel.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementModel.swift new file mode 100644 index 000000000..601764e2c --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementModel.swift @@ -0,0 +1,90 @@ +// 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 +import QuickLook + +/// The view model for +@MainActor class AttachmentsPopupElementModel: ObservableObject { + @Published var attachmentModels = [AttachmentModel]() +} + +/// A view model representing the combination of a `PopupAttachment` and +/// an associated `UIImage` used as a thumbnail. +@MainActor class AttachmentModel: ObservableObject { + @Published var attachment: PopupAttachment + @Published var thumbnail: UIImage? { + didSet { + usingDefaultImage = false + } + } + @Published var loadStatus: LoadStatus = .notLoaded + var usingDefaultImage: Bool + + @Environment(\.displayScale) var displayScale + + init(attachment: PopupAttachment) { + self.attachment = attachment + + switch attachment.kind { + case .image: + thumbnail = UIImage(systemName: "photo") + case .video: + thumbnail = UIImage(systemName: "film") + case .document, .other: + thumbnail = UIImage(systemName: "doc") + } + usingDefaultImage = true + } + + + + func load(thumbnailSize: CGSize = CGSize(width: 40, height: 40)) { + Task { + loadStatus = .loading + try await self.attachment.load() + + let request = QLThumbnailGenerator.Request( + fileAt: attachment.fileURL, + size: CGSize(width: thumbnailSize.width, height: thumbnailSize.height), + scale: displayScale, + representationTypes: .thumbnail) + + let generator = QLThumbnailGenerator.shared + generator.generateRepresentations(for: request) { [weak self] thumbnail, _, error in + guard let self = self else { return } + DispatchQueue.main.async { + if let thumbnail = thumbnail { + self.thumbnail = thumbnail.uiImage + } + else if self.attachment.loadStatus == .failed { + self.thumbnail = UIImage(systemName: "exclamationmark.circle.fill") + self.usingDefaultImage = true + } + + self.loadStatus = self.attachment.loadStatus + } + } + } + } +} + +extension AttachmentModel: Identifiable {} + +extension AttachmentModel: Equatable { + static func == (lhs: AttachmentModel, rhs: AttachmentModel) -> Bool { + lhs.attachment === rhs.attachment && + lhs.thumbnail === rhs.thumbnail + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index d7aa9bf99..7a130ff20 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -20,39 +20,9 @@ import QuickLook // TODO: Add alert for when attachments fail to load... // TODO: Move classes into separate file; maybe structs too. -@MainActor class AttachmentImage: ObservableObject { - @Published var attachment: PopupAttachment - @Published var image: UIImage? - @Published var loadStatus: LoadStatus = .notLoaded - - init(attachment: PopupAttachment, image: UIImage? = nil) { - self.attachment = attachment - self.image = image - } - - func load() async throws { - loadStatus = .loading - try? await self.attachment.load() - loadStatus = self.attachment.loadStatus - } -} - -extension AttachmentImage: Identifiable {} - -extension AttachmentImage: Equatable { - static func == (lhs: AttachmentImage, rhs: AttachmentImage) -> Bool { - lhs.attachment === rhs.attachment && - lhs.image === rhs.image - } -} - -@MainActor class AttachmentModel: ObservableObject { - @Published var attachmentImages = [AttachmentImage]() -} - struct AttachmentsPopupElementView: View { var popupElement: AttachmentsPopupElement - @StateObject private var viewModel: AttachmentModel + @StateObject private var viewModel: AttachmentsPopupElementModel @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass @@ -67,7 +37,7 @@ struct AttachmentsPopupElementView: View { init(popupElement: AttachmentsPopupElement) { self.popupElement = popupElement _viewModel = StateObject( - wrappedValue: AttachmentModel() + wrappedValue: AttachmentsPopupElementModel() ) } @@ -89,199 +59,190 @@ struct AttachmentsPopupElementView: View { else { switch popupElement.displayType { case .list: - AttachmentList(attachmentImages: viewModel.attachmentImages) +// AttachmentList(attachmentModels: viewModel.attachmentModels) + AttachmentPreview(attachmentModels: viewModel.attachmentModels) case.preview: - AttachmentList(attachmentImages: viewModel.attachmentImages) -// AttachmentPreview(attachmentImages: viewModel.attachmentImages) +// AttachmentList(attachmentModels: viewModel.attachmentModels) + AttachmentPreview(attachmentModels: viewModel.attachmentModels) case .auto: if isRegularWidth { - AttachmentList(attachmentImages: viewModel.attachmentImages) -// AttachmentPreview(attachmentImages: viewModel.attachmentImages) +// AttachmentList(attachmentModels: viewModel.attachmentModels) + AttachmentPreview(attachmentModels: viewModel.attachmentModels) } else { - AttachmentList(attachmentImages: viewModel.attachmentImages) + AttachmentList(attachmentModels: viewModel.attachmentModels) + AttachmentPreview(attachmentModels: viewModel.attachmentModels) } } } } .task { try? await popupElement.fetchAttachments() - let attachmentImages = popupElement.attachments.map { attachment in - AttachmentImage(attachment: attachment) + let attachmentModels = popupElement.attachments.map { attachment in + AttachmentModel(attachment: attachment) } - viewModel.attachmentImages.append(contentsOf: attachmentImages) + viewModel.attachmentModels.append(contentsOf: attachmentModels) loadingAttachments = false } } struct AttachmentList: View { - var attachmentImages: [AttachmentImage] + var attachmentModels: [AttachmentModel] var body: some View { VStack(alignment: .leading, spacing: 6) { - ForEach(attachmentImages) { attachmentImage in - AttachmentRow(attachmentImage: attachmentImage) - if attachmentImage != attachmentImages.last { + ForEach(attachmentModels) { attachmentModel in + AttachmentRow(attachmentModel: attachmentModel) + if attachmentModel != attachmentModels.last { Divider() } } } } - struct ImageView: View { - @ObservedObject var attachmentImage: AttachmentImage - - // TODO: instead of this big block, pre-load attachmentImage.image with correct placeholder image - // which will get overwritten when the preview image loads. - var body: some View { - if let image = attachmentImage.image { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .padding(2) - .frame(width: 40, height: 40, alignment: .center) - } - else { - switch attachmentImage.attachment.kind { - case .image: - if let image = attachmentImage.image { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) - .frame(width: 40, height: 40, alignment: .center) - } - else { - Image(systemName: "photo") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.accentColor) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) - .frame(width: 40, height: 40, alignment: .center) - } - case .video: - Image(systemName: "video") - .resizable() - .foregroundColor(.accentColor) - .aspectRatio(contentMode: .fit) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) - .frame(width: 40, height: 40, alignment: .center) - case .document, .other: - Image(systemName: "doc") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.accentColor) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)) - .frame(width: 40, height: 30, alignment: .center) - } - } - } - } - struct AttachmentRow: View { - @ObservedObject var attachmentImage: AttachmentImage + @ObservedObject var attachmentModel: AttachmentModel @State var url: URL? - @Environment(\.displayScale) var displayScale - + var body: some View { HStack { HStack { - ImageView(attachmentImage: attachmentImage) + ThumbnailView(attachmentModel: attachmentModel) + .padding(2) VStack(alignment: .leading) { - Text(attachmentImage.attachment.name) + Text(attachmentModel.attachment.name) .lineLimit(1) .truncationMode(.middle) - Text("\(attachmentImage.attachment.size.formatted(.byteCount(style: .file)))") + Text("\(attachmentModel.attachment.size.formatted(.byteCount(style: .file)))") .foregroundColor(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } .onTapGesture { - if attachmentImage.attachment.loadStatus == .loaded { - url = attachmentImage.attachment.fileURL + if attachmentModel.attachment.loadStatus == .loaded { + url = attachmentModel.attachment.fileURL } } - .quickLookPreview($url) - if attachmentImage.loadStatus != .loaded { - Button { - Task { - // TODO: Move this into a separate function - try await attachmentImage.load() - - let request = QLThumbnailGenerator.Request( - fileAt: attachmentImage.attachment.fileURL, - size: CGSize(width: 40, height: 40), - scale: displayScale, - representationTypes: .thumbnail) - - // 2 - let generator = QLThumbnailGenerator.shared - generator.generateRepresentations(for: request) { thumbnail, _, error in - // 3 - Task { - await MainActor.run { - if let thumbnail = thumbnail { - attachmentImage.image = thumbnail.uiImage - } - } - } - } - } - } label: { - // TODO: Move this into a separate view - Group { - switch attachmentImage.loadStatus { - case .notLoaded: - Image(systemName: "square.and.arrow.down") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 24, height: 24) - case .loading: - ProgressView() - case .loaded: - EmptyView() - case .failed: - Image(systemName: "exclamationmark.circle.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.red) - .background(Color.clear) - .frame(width: 24, height: 24) - } - } - .padding(.leading) - } + if attachmentModel.loadStatus != .loaded { + AttachmentLoadButton(attachmentModel: attachmentModel) + } + } + .quickLookPreview($url) + } + } + } + + struct ThumbnailView: View { + @ObservedObject var attachmentModel: AttachmentModel + var size: CGSize = CGSize(width: 40, height: 40) + + var body: some View { + if let image = attachmentModel.thumbnail { + Image(uiImage: image) + .resizable() + .renderingMode(attachmentModel.usingDefaultImage ? .template : .original) + .aspectRatio(contentMode: attachmentModel.usingDefaultImage ? .fit : .fill) + .frame(width: size.width, height: size.height, alignment: .center) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .foregroundColor(foregroundColor(for: attachmentModel)) + } + } + + func foregroundColor(for attachmentModel: AttachmentModel) -> Color { + attachmentModel.loadStatus == .failed ? .red : + (attachmentModel.usingDefaultImage ? .accentColor : .primary) + } + } + + struct AttachmentLoadButton: View { + @ObservedObject var attachmentModel: AttachmentModel + + var body: some View { + Button { + if attachmentModel.loadStatus == .notLoaded { + attachmentModel.load() + } + else if attachmentModel.loadStatus == .failed { + // TODO: Show error alert, similar to BasemapGallery. + } + } label: { + Group { + switch attachmentModel.loadStatus { + case .notLoaded: + Image(systemName: "square.and.arrow.down") + .resizable() + .aspectRatio(contentMode: .fit) + case .loading: + ProgressView() + case .loaded: + EmptyView() + case .failed: + Image(systemName: "exclamationmark.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + .background(Color.clear) } } + .frame(width: 24, height: 24) + .padding(.leading) } } } struct AttachmentPreview: View { - var attachmentImages: [AttachmentImage] + var attachmentModels: [AttachmentModel] var body: some View { - VStack(alignment: .center) { - // ForEach(0.. Date: Mon, 22 Aug 2022 16:32:32 -0700 Subject: [PATCH 1469/1722] Update SearchView.swift --- .../Components/Search/SearchView.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 5140ee834..15a7ad7b4 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -55,7 +55,10 @@ public struct SearchView: View { /// /// If `nil`, does not prioritize the search results around any point. @Binding var queryCenter: Point? - + + /// A closure to performed when the query has changed. + private var onQueryChangedAction: ((String) -> Void)? + /// The current map/scene view extent. Defaults to `nil`. /// /// This will be used to determine the value of `isEligibleForRequery` for the 'Repeat @@ -166,6 +169,7 @@ public struct SearchView: View { } .listStyle(.plain) .onReceive(viewModel.$currentQuery) { _ in + onQueryChangedAction?(viewModel.currentQuery) viewModel.updateSuggestions() } .onChange(of: geoViewExtent) { _ in @@ -232,6 +236,16 @@ extension SearchView { return copy } + /// Sets a closure to perform when the query changes. + /// - Parameters: + /// - action: The closure to performed when the query has changed. + /// - query: The new query. + public func onQueryChanged(perform action: @escaping (_ query: String) -> Void) -> Self { + var copy = self + copy.onQueryChangedAction = action + return copy + } + /// The `GraphicsOverlay` used to display results. If `nil`, no results will be displayed. /// - Parameter newResultsOverlay: The new value. /// - Returns: The `SearchView`. From e1dbd9d2fec677ed89aa480bb35cdae638d7094e Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Mon, 22 Aug 2022 17:00:13 -0700 Subject: [PATCH 1470/1722] changes for renaming of cancelAuthenticationChallenge to cancel --- .../Authentication/CertificatePickerViewModifier.swift | 2 +- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 2 +- .../Authentication/TokenChallengeContinuation.swift | 2 +- .../Components/Authentication/TrustHostViewModifier.swift | 2 +- .../CertificatePickerViewModelTests.swift | 2 +- .../Test Support/Utility/ChallengeHandler.swift | 6 +++--- .../TokenChallengeContinuationTests.swift | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index b16eaa526..e34b17e80 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -85,7 +85,7 @@ import ArcGIS /// Cancels the challenge. func cancel() { - challenge.resume(with: .cancelAuthenticationChallenge) + challenge.resume(with: .cancel) } } diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index dc52f8393..1d4164f19 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -117,7 +117,7 @@ extension LoginViewModifier { ) }, onCancel: { - challenge.resume(with: .cancelAuthenticationChallenge) + challenge.resume(with: .cancel) } ) ) diff --git a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift index e67ba59b4..a23f89e28 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift @@ -63,6 +63,6 @@ final class TokenChallengeContinuation: ValueContinuation Date: Tue, 23 Aug 2022 09:34:00 -0700 Subject: [PATCH 1471/1722] `ActionConfiguration` -> `Action` --- .../ArcGISToolkit/Utility/CredentialInputView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index 93cee90ce..021059a7c 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -21,10 +21,10 @@ import SwiftUI /// SwiftUI alerts don't support visible but disabled buttons. struct CredentialInputView: UIViewControllerRepresentable { /// The cancel action configuration. - private let cancelConfiguration: ActionConfiguration + private let cancelConfiguration: Action /// The continue action configuration. - private let continueConfiguration: ActionConfiguration + private let continueConfiguration: Action /// The value in the identity field. /// @@ -59,8 +59,8 @@ struct CredentialInputView: UIViewControllerRepresentable { message: String, title: String, usage: Usage, - cancelConfiguration: ActionConfiguration, - continueConfiguration: ActionConfiguration + cancelConfiguration: Action, + continueConfiguration: Action ) { self.cancelConfiguration = cancelConfiguration self.continueConfiguration = continueConfiguration @@ -204,7 +204,7 @@ extension CredentialInputView { extension CredentialInputView { /// A configuration for an alert action. - struct ActionConfiguration { + struct Action { /// The title of the action. let title: String From bea4c4b982853265569a0a21e361ef5fd8299b06 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 09:35:26 -0700 Subject: [PATCH 1472/1722] Remove custom `Action` initializer --- .../ArcGISToolkit/Utility/CredentialInputView.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index 021059a7c..af10aa5b7 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -210,16 +210,6 @@ extension CredentialInputView { /// The block to execute when the action is triggered. let handler: (String, String) -> Void - - /// Makes the action configuration. - /// - Parameters: - /// - title: The title of the action. - /// - handler: The block to execute when the action is triggered. The first and second - /// parameters correspond to the identity and password field values. - init(title: String, handler: @escaping (String, String) -> Void) { - self.title = title - self.handler = handler - } } } From 4ab48eaf82132662b29bc6c5ef31f48af36a320e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 09:46:15 -0700 Subject: [PATCH 1473/1722] `usage` -> `fields` --- .../CertificatePickerViewModifier.swift | 2 +- .../Components/Authentication/Login.swift | 2 +- .../Utility/CredentialInputView.swift | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 558aeec21..06551ada8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -182,10 +182,10 @@ private extension View { ) -> some View { overlay { CredentialInputView( + fields: .passwordOnly, isPresented: isPresented, message: "Please enter a password for the chosen certificate.", title: "Password Required", - usage: .passwordOnly, cancelConfiguration: .init( title: "Cancel", handler: { _, _ in diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 0353f51ec..73471df4f 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -94,10 +94,10 @@ struct LoginViewModifier: ViewModifier { .task { isPresented = true } .overlay { CredentialInputView( + fields: .identityAndPassword, isPresented: $isPresented, message: "You must sign in to access '\(viewModel.challengingHost)'", title: "Authentication Required", - usage: .identityAndPassword, cancelConfiguration: .init( title: "Cancel", handler: { _, _ in diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index af10aa5b7..a4f9c3009 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -43,22 +43,22 @@ struct CredentialInputView: UIViewControllerRepresentable { /// The title of the alert. private let title: String - /// The usage of the alert. - private let usage: Usage + /// The fields shown in the alert. + private let fields: Fields /// Creates the view. /// - Parameters: /// - isPresented: A Boolean value indicating whether or not the view is displayed. /// - message: Descriptive text that provides more details about the reason for the alert. /// - title: The title of the alert. - /// - usage: The usage of the alert. + /// - fields: The fields shown in the alert. /// - cancelConfiguration: The cancel action configuration. /// - continueConfiguration: The continue action configuration. init( + fields: Fields, isPresented: Binding, message: String, title: String, - usage: Usage, cancelConfiguration: Action, continueConfiguration: Action ) { @@ -67,14 +67,14 @@ struct CredentialInputView: UIViewControllerRepresentable { _isPresented = isPresented + self.fields = fields self.message = message self.title = title - self.usage = usage } /// A Boolean value indicating whether the alert should allow the continue action to proceed. private var isContinueEnabled: Bool { - switch usage { + switch fields { case .identityAndPassword: return !identity.isEmpty && !password.isEmpty case .passwordOnly: @@ -107,7 +107,7 @@ struct CredentialInputView: UIViewControllerRepresentable { continueConfiguration.handler(identity, password) } - if usage == .identityAndPassword { + if fields == .identityAndPassword { uiAlertController.addTextField { textField in textField.addAction( UIAction { _ in @@ -216,7 +216,7 @@ extension CredentialInputView { extension CredentialInputView { /// The usage of the view. This determines if the view is intended to require either an identity and /// password, or a password only. - enum Usage { + enum Fields { /// Indicates the view is intended to collect an identity and password. case identityAndPassword From e9cd90cba7c567f87334e1fbb695806325e56e5d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 09:47:11 -0700 Subject: [PATCH 1474/1722] `passwordOnly` -> `password` --- .../Authentication/CertificatePickerViewModifier.swift | 2 +- Sources/ArcGISToolkit/Utility/CredentialInputView.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 06551ada8..33856b3aa 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -182,7 +182,7 @@ private extension View { ) -> some View { overlay { CredentialInputView( - fields: .passwordOnly, + fields: .password, isPresented: isPresented, message: "Please enter a password for the chosen certificate.", title: "Password Required", diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index a4f9c3009..a48c0d4f0 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -77,7 +77,7 @@ struct CredentialInputView: UIViewControllerRepresentable { switch fields { case .identityAndPassword: return !identity.isEmpty && !password.isEmpty - case .passwordOnly: + case .password: return !password.isEmpty } } @@ -221,6 +221,6 @@ extension CredentialInputView { case identityAndPassword /// Indicates the view is intended to collect a password only. - case passwordOnly + case password } } From 1223f39d4b40ebd78a9a36c67979aeceeb02b2c2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 09:48:22 -0700 Subject: [PATCH 1475/1722] `identityAndPassword` -> `usernamePassword` --- .../Components/Authentication/Login.swift | 2 +- .../ArcGISToolkit/Utility/CredentialInputView.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 73471df4f..66698b0a8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -94,7 +94,7 @@ struct LoginViewModifier: ViewModifier { .task { isPresented = true } .overlay { CredentialInputView( - fields: .identityAndPassword, + fields: .usernamePassword, isPresented: $isPresented, message: "You must sign in to access '\(viewModel.challengingHost)'", title: "Authentication Required", diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index a48c0d4f0..4a0340f44 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -75,7 +75,7 @@ struct CredentialInputView: UIViewControllerRepresentable { /// A Boolean value indicating whether the alert should allow the continue action to proceed. private var isContinueEnabled: Bool { switch fields { - case .identityAndPassword: + case .usernamePassword: return !identity.isEmpty && !password.isEmpty case .password: return !password.isEmpty @@ -107,7 +107,7 @@ struct CredentialInputView: UIViewControllerRepresentable { continueConfiguration.handler(identity, password) } - if fields == .identityAndPassword { + if fields == .usernamePassword { uiAlertController.addTextField { textField in textField.addAction( UIAction { _ in @@ -214,11 +214,11 @@ extension CredentialInputView { } extension CredentialInputView { - /// The usage of the view. This determines if the view is intended to require either an identity and + /// The usage of the view. This determines if the view is intended to require either a username and /// password, or a password only. enum Fields { - /// Indicates the view is intended to collect an identity and password. - case identityAndPassword + /// Indicates the view is intended to collect a username and password. + case usernamePassword /// Indicates the view is intended to collect a password only. case password From 2cf25ddfc51cbf595353562f423c5b746f2acfc5 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 09:50:22 -0700 Subject: [PATCH 1476/1722] Update CredentialInputView.swift --- Sources/ArcGISToolkit/Utility/CredentialInputView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index 4a0340f44..3cddabb1e 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -14,7 +14,7 @@ import Foundation import SwiftUI -/// A view that prompts a user to provide credentials. It can be configured to require either an identity and +/// A view that prompts a user to provide credentials. It can be configured to require either a username and /// password, or a password only. /// /// The view is implemented as a wrapper for a UIKit `UIAlertController` because as of iOS 16, @@ -214,8 +214,8 @@ extension CredentialInputView { } extension CredentialInputView { - /// The usage of the view. This determines if the view is intended to require either a username and - /// password, or a password only. + /// The fields shown in the alert. This determines if the view is intended to require either a username + /// and password, or a password only. enum Fields { /// Indicates the view is intended to collect a username and password. case usernamePassword From 73a437247e9f94ceda6d11cbe556330aac79e424 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 09:55:19 -0700 Subject: [PATCH 1477/1722] Replace "configuration" with "action" --- .../CertificatePickerViewModifier.swift | 4 +- .../Components/Authentication/Login.swift | 4 +- .../Utility/CredentialInputView.swift | 46 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 33856b3aa..14d750b22 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -186,13 +186,13 @@ private extension View { isPresented: isPresented, message: "Please enter a password for the chosen certificate.", title: "Password Required", - cancelConfiguration: .init( + cancelAction: .init( title: "Cancel", handler: { _, _ in viewModel.cancel() } ), - continueConfiguration: .init( + continueAction: .init( title: "OK", handler: { _, password in viewModel.password = password diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 66698b0a8..6308cad76 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -98,13 +98,13 @@ struct LoginViewModifier: ViewModifier { isPresented: $isPresented, message: "You must sign in to access '\(viewModel.challengingHost)'", title: "Authentication Required", - cancelConfiguration: .init( + cancelAction: .init( title: "Cancel", handler: { _, _ in viewModel.cancel() } ), - continueConfiguration: .init( + continueAction: .init( title: "Continue", handler: { username, password in viewModel.username = username diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index 3cddabb1e..ad546fd2e 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -20,11 +20,11 @@ import SwiftUI /// The view is implemented as a wrapper for a UIKit `UIAlertController` because as of iOS 16, /// SwiftUI alerts don't support visible but disabled buttons. struct CredentialInputView: UIViewControllerRepresentable { - /// The cancel action configuration. - private let cancelConfiguration: Action + /// The cancel action. + private let cancelAction: Action - /// The continue action configuration. - private let continueConfiguration: Action + /// The continue action. + private let continueAction: Action /// The value in the identity field. /// @@ -52,18 +52,18 @@ struct CredentialInputView: UIViewControllerRepresentable { /// - message: Descriptive text that provides more details about the reason for the alert. /// - title: The title of the alert. /// - fields: The fields shown in the alert. - /// - cancelConfiguration: The cancel action configuration. - /// - continueConfiguration: The continue action configuration. + /// - cancelAction: The cancel action. + /// - continueAction: The continue action. init( fields: Fields, isPresented: Binding, message: String, title: String, - cancelConfiguration: Action, - continueConfiguration: Action + cancelAction: Action, + continueAction: Action ) { - self.cancelConfiguration = cancelConfiguration - self.continueConfiguration = continueConfiguration + self.cancelAction = cancelAction + self.continueAction = continueAction _isPresented = isPresented @@ -93,18 +93,18 @@ struct CredentialInputView: UIViewControllerRepresentable { preferredStyle: .alert ) - let cancelAction = UIAlertAction( - title: cancelConfiguration.title, + let cancelUIAlertAction = UIAlertAction( + title: cancelAction.title, style: .cancel ) { _ in - cancelConfiguration.handler(identity, password) + cancelAction.handler(identity, password) } - let continueAction = UIAlertAction( - title: continueConfiguration.title, + let continueUIAlertAction = UIAlertAction( + title: continueAction.title, style: .default ) { _ in - continueConfiguration.handler(identity, password) + continueAction.handler(identity, password) } if fields == .usernamePassword { @@ -112,7 +112,7 @@ struct CredentialInputView: UIViewControllerRepresentable { textField.addAction( UIAction { _ in identity = textField.text ?? "" - continueAction.isEnabled = isContinueEnabled + continueUIAlertAction.isEnabled = isContinueEnabled }, for: .editingChanged ) @@ -128,7 +128,7 @@ struct CredentialInputView: UIViewControllerRepresentable { textField.addAction( UIAction { _ in password = textField.text ?? "" - continueAction.isEnabled = isContinueEnabled + continueUIAlertAction.isEnabled = isContinueEnabled }, for: .editingChanged ) @@ -145,11 +145,11 @@ struct CredentialInputView: UIViewControllerRepresentable { textField.textContentType = .password } - cancelAction.isEnabled = true - continueAction.isEnabled = false + cancelUIAlertAction.isEnabled = true + continueUIAlertAction.isEnabled = false - uiAlertController.addAction(cancelAction) - uiAlertController.addAction(continueAction) + uiAlertController.addAction(cancelUIAlertAction) + uiAlertController.addAction(continueUIAlertAction) return uiAlertController } @@ -193,7 +193,7 @@ extension CredentialInputView { func textFieldShouldReturn(_ textField: UITextField) -> Bool { guard !parent.password.isEmpty else { return false } - parent.continueConfiguration.handler( + parent.continueAction.handler( parent.identity, parent.password ) From 695a185c67ebc802b39f8acf36b7995f85fb00b9 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 09:59:40 -0700 Subject: [PATCH 1478/1722] Add warning text --- Sources/ArcGISToolkit/Utility/CredentialInputView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index ad546fd2e..c7b30ccf8 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -169,7 +169,9 @@ struct CredentialInputView: UIViewControllerRepresentable { guard isPresented else { return } let alertController = makeAlertController(context: context) // On a physical iOS 16 device, without the following delay, the - // presentation fails and an error is thrown. + // presentation fails and the following warning is logged: "Attempt to + // present UIAlertController on UIViewController whose view is not in + // the window hierarchy." DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { uiViewController.present(alertController, animated: true) { isPresented = false From bd4f3ba63a98bd0e86de23280b47f5f2b4ea3488 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 11:25:47 -0700 Subject: [PATCH 1479/1722] Add credential input modifier --- .../CertificatePickerViewModifier.swift | 52 +++++---------- .../Components/Authentication/Login.swift | 40 ++++++----- .../Utility/CredentialInputView.swift | 66 +++++++++++++++++++ 3 files changed, 102 insertions(+), 56 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index 14d750b22..47918521d 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -112,9 +112,24 @@ struct CertificatePickerViewModifier: ViewModifier { isPresented: $viewModel.showPicker, viewModel: viewModel ) - .passwordAlert( + .credentialInput( + fields: .password, isPresented: $viewModel.showPassword, - viewModel: viewModel + message: "Please enter a password for the chosen certificate.", + title: "Password Required", + cancelAction: .init( + title: "Cancel", + handler: { _, _ in + viewModel.cancel() + } + ), + continueAction: .init( + title: "OK", + handler: { _, password in + viewModel.password = password + viewModel.proceedWithPassword() + } + ) ) .alertCertificateImportError( isPresented: $viewModel.showCertificateImportError, @@ -171,39 +186,6 @@ private extension View { } } -private extension View { - /// Displays a sheet that allows the user to enter a password. - /// - Parameters: - /// - isPresented: A Boolean value indicating if the view is presented. - /// - viewModel: The view model associated with the view. - @MainActor @ViewBuilder func passwordAlert( - isPresented: Binding, - viewModel: CertificatePickerViewModel - ) -> some View { - overlay { - CredentialInputView( - fields: .password, - isPresented: isPresented, - message: "Please enter a password for the chosen certificate.", - title: "Password Required", - cancelAction: .init( - title: "Cancel", - handler: { _, _ in - viewModel.cancel() - } - ), - continueAction: .init( - title: "OK", - handler: { _, password in - viewModel.password = password - viewModel.proceedWithPassword() - } - ) - ) - } - } -} - private extension View { /// Displays an alert to notify that there was an error importing the certificate. /// - Parameters: diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 6308cad76..548cd8a8a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -92,28 +92,26 @@ struct LoginViewModifier: ViewModifier { func body(content: Content) -> some View { content .task { isPresented = true } - .overlay { - CredentialInputView( - fields: .usernamePassword, - isPresented: $isPresented, - message: "You must sign in to access '\(viewModel.challengingHost)'", - title: "Authentication Required", - cancelAction: .init( - title: "Cancel", - handler: { _, _ in - viewModel.cancel() - } - ), - continueAction: .init( - title: "Continue", - handler: { username, password in - viewModel.username = username - viewModel.password = password - viewModel.signIn() - } - ) + .credentialInput( + fields: .usernamePassword, + isPresented: $isPresented, + message: "You must sign in to access '\(viewModel.challengingHost)'", + title: "Authentication Required", + cancelAction: .init( + title: "Cancel", + handler: { _, _ in + viewModel.cancel() + } + ), + continueAction: .init( + title: "Continue", + handler: { username, password in + viewModel.username = username + viewModel.password = password + viewModel.signIn() + } ) - } + ) } } diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index c7b30ccf8..44866d759 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -226,3 +226,69 @@ extension CredentialInputView { case password } } + +extension View { + /// Presents user experiences for collecting credentials from the user. + /// - Parameters: + /// - fields: The fields shown in the view. + /// - isPresented: A Boolean value indicating whether or not the view is displayed. + /// - message: Descriptive text that provides more details about the reason for the alert. + /// - title: The title of the alert. + /// - cancelAction: The cancel action. + /// - continueAction: The continue action. + @ViewBuilder func credentialInput( + fields: CredentialInputView.Fields, + isPresented: Binding, + message: String, + title: String, + cancelAction: CredentialInputView.Action, + continueAction: CredentialInputView.Action + ) -> some View { + modifier( + CredentialInputModifier( + fields: fields, + isPresented: isPresented, + message: message, + title: title, + cancelAction: cancelAction, + continueAction: continueAction + ) + ) + } +} + +/// A view modifier that prompts for credentials. +struct CredentialInputModifier: ViewModifier { + + /// The fields shown in the view. + let fields: CredentialInputView.Fields + + /// A Boolean value indicating whether or not the view is displayed. + @Binding var isPresented: Bool + + /// Descriptive text that provides more details about the reason for the alert. + let message: String + + /// The title of the alert. + let title: String + + /// The cancel action. + let cancelAction: CredentialInputView.Action + + /// The continue action. + let continueAction: CredentialInputView.Action + + @ViewBuilder func body(content: Content) -> some View { + ZStack { + content + CredentialInputView( + fields: fields, + isPresented: $isPresented, + message: message, + title: title, + cancelAction: cancelAction, + continueAction: continueAction + ) + } + } +} From fd93097f53bd0a87a026bd24d2746e3b6a3468d8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 11:28:06 -0700 Subject: [PATCH 1480/1722] Update CredentialInputView.swift --- Sources/ArcGISToolkit/Utility/CredentialInputView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index 44866d759..80794f305 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -219,11 +219,11 @@ extension CredentialInputView { /// The fields shown in the alert. This determines if the view is intended to require either a username /// and password, or a password only. enum Fields { - /// Indicates the view is intended to collect a username and password. - case usernamePassword - /// Indicates the view is intended to collect a password only. case password + + /// Indicates the view is intended to collect a username and password. + case usernamePassword } } From 25d7bf477bed2e9fc493a7bdaeeda8329bc63e1e Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 13:05:36 -0700 Subject: [PATCH 1481/1722] Update Sources/ArcGISToolkit/Utility/CredentialInputView.swift Co-authored-by: R Olson --- Sources/ArcGISToolkit/Utility/CredentialInputView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index 80794f305..aff3226f2 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -29,7 +29,7 @@ struct CredentialInputView: UIViewControllerRepresentable { /// The value in the identity field. /// /// This member is unused when usage is set to `Usage.passwordOnly`. - @State private var identity = "" + @State private var username = "" /// A Boolean value indicating whether or not the view is displayed. @Binding private var isPresented: Bool From 735dac290866c8701d8a965dbf1feb234595ed6d Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 13:07:53 -0700 Subject: [PATCH 1482/1722] `identity` -> `username` --- .../ArcGISToolkit/Utility/CredentialInputView.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index aff3226f2..81bd11ee6 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -26,7 +26,7 @@ struct CredentialInputView: UIViewControllerRepresentable { /// The continue action. private let continueAction: Action - /// The value in the identity field. + /// The value in the username field. /// /// This member is unused when usage is set to `Usage.passwordOnly`. @State private var username = "" @@ -76,7 +76,7 @@ struct CredentialInputView: UIViewControllerRepresentable { private var isContinueEnabled: Bool { switch fields { case .usernamePassword: - return !identity.isEmpty && !password.isEmpty + return !username.isEmpty && !password.isEmpty case .password: return !password.isEmpty } @@ -97,21 +97,21 @@ struct CredentialInputView: UIViewControllerRepresentable { title: cancelAction.title, style: .cancel ) { _ in - cancelAction.handler(identity, password) + cancelAction.handler(username, password) } let continueUIAlertAction = UIAlertAction( title: continueAction.title, style: .default ) { _ in - continueAction.handler(identity, password) + continueAction.handler(username, password) } if fields == .usernamePassword { uiAlertController.addTextField { textField in textField.addAction( UIAction { _ in - identity = textField.text ?? "" + username = textField.text ?? "" continueUIAlertAction.isEnabled = isContinueEnabled }, for: .editingChanged @@ -196,7 +196,7 @@ extension CredentialInputView { func textFieldShouldReturn(_ textField: UITextField) -> Bool { guard !parent.password.isEmpty else { return false } parent.continueAction.handler( - parent.identity, + parent.username, parent.password ) return true From ed026b0fa144e124fb487617c68f5f677fb04857 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 23 Aug 2022 16:25:23 -0700 Subject: [PATCH 1483/1722] Updates --- .../Components/Search/LocatorSearchSource.swift | 6 +++--- .../Components/Search/SmartLocatorSearchSource.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index 39607f2c3..e057f5774 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -100,7 +100,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { geocodeParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.geocode( - suggestResult: suggestResult, + forSuggestResult: suggestResult, parameters: geocodeParameters ) @@ -119,7 +119,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { suggestParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.suggest( - searchText: query, + forSearchText: query, parameters: suggestParameters ) // Convert to an array of `SearchSuggestion` objects and return. @@ -139,7 +139,7 @@ private extension LocatorSearchSource { geocodeParameters.preferredSearchLocation = preferredSearchLocation let geocodeResults = try await locatorTask.geocode( - searchText: query, + forSearchText: query, parameters: geocodeParameters ) diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 053f7c227..235e5e50d 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -80,7 +80,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil let geocodeResults = try await locatorTask.geocode( - searchText: query, + forSearchText: query, parameters: geocodeParameters ) @@ -119,7 +119,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. geocodeParameters.searchArea = nil let geocodeResults = try await locatorTask.geocode( - suggestResult: suggestResult, + forSuggestResult: suggestResult, parameters: geocodeParameters ) @@ -154,7 +154,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { // Remove geographic constraints and re-run search. suggestParameters.searchArea = nil let geocodeResults = try await locatorTask.suggest( - searchText: query, + forSearchText: query, parameters: suggestParameters ) From 4781fce93beb37058ba726ee00e55b1a4ed5cd22 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 24 Aug 2022 11:45:13 -0700 Subject: [PATCH 1484/1722] Update UtilityNetworkTrace.swift --- .../UtilityNetworkTrace.swift | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index f5072941e..aa764af36 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -55,6 +55,7 @@ public struct UtilityNetworkTrace: View { // MARK: States + /// The current detent of the floating panel. @State private var activeDetent: FloatingPanelDetent = .half /// The current user activity. @@ -89,6 +90,30 @@ public struct UtilityNetworkTrace: View { // MARK: Subviews + /// Allows the user to switch between the trace creation and viewing tabs. + @ViewBuilder private var activityPicker: some View { + Picker( + "Mode", + selection: Binding( + get: { + switch currentActivity { + case .creatingTrace(_): + return UserActivity.creatingTrace(nil) + case .viewingTraces: + return UserActivity.viewingTraces(nil) + } + }, set: { newActivity, _ in + currentActivity = newActivity + } + ) + ) { + Text("New trace").tag(UserActivity.creatingTrace(nil)) + Text("Results").tag(UserActivity.viewingTraces(nil)) + } + .pickerStyle(.segmented) + .padding() + } + /// Allows the user to cancel out of selecting a new starting point. private var cancelAddStartingPoints: some View { Button(role: .destructive) { @@ -100,32 +125,6 @@ public struct UtilityNetworkTrace: View { .buttonStyle(.bordered) } - /// Allows the user to switch between the trace creation and viewing tabs. - @ViewBuilder private var activityPicker: some View { - if activeDetent != .summary { - Picker( - "Mode", - selection: Binding( - get: { - switch currentActivity { - case .creatingTrace(_): - return UserActivity.creatingTrace(nil) - case .viewingTraces: - return UserActivity.viewingTraces(nil) - } - }, set: { newActivity, _ in - currentActivity = newActivity - } - ) - ) { - Text("New trace").tag(UserActivity.creatingTrace(nil)) - Text("Results").tag(UserActivity.viewingTraces(nil)) - } - .pickerStyle(.segmented) - .padding() - } - } - /// Displays information about a chosen asset group. @ViewBuilder private var assetGroupDetail: some View { if let assetGroup = selectedAssetGroup { @@ -567,7 +566,8 @@ public struct UtilityNetworkTrace: View { ) { VStack { if !viewModel.completedTraces.isEmpty && - !isFocused(traceCreationActivity: .addingStartingPoints) { + !isFocused(traceCreationActivity: .addingStartingPoints) && + activeDetent != .summary { activityPicker } switch currentActivity { From 1916e12090b2b59ed50db41e452b0bb69d12a050 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 24 Aug 2022 12:05:19 -0700 Subject: [PATCH 1485/1722] 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 aa764af36..bd3dd4364 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -91,7 +91,7 @@ public struct UtilityNetworkTrace: View { // MARK: Subviews /// Allows the user to switch between the trace creation and viewing tabs. - @ViewBuilder private var activityPicker: some View { + private var activityPicker: some View { Picker( "Mode", selection: Binding( From 9c1227781e92fdb4795f454c32896e315cd66c2a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 24 Aug 2022 12:50:29 -0700 Subject: [PATCH 1486/1722] Update Sources/ArcGISToolkit/Components/Search/SearchView.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Components/Search/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Search/SearchView.swift b/Sources/ArcGISToolkit/Components/Search/SearchView.swift index 15a7ad7b4..194cf0aca 100644 --- a/Sources/ArcGISToolkit/Components/Search/SearchView.swift +++ b/Sources/ArcGISToolkit/Components/Search/SearchView.swift @@ -56,7 +56,7 @@ public struct SearchView: View { /// If `nil`, does not prioritize the search results around any point. @Binding var queryCenter: Point? - /// A closure to performed when the query has changed. + /// A closure performed when the query has changed. private var onQueryChangedAction: ((String) -> Void)? /// The current map/scene view extent. Defaults to `nil`. From 96fd28f45eb94447104c0af491743451d9be15d8 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 24 Aug 2022 13:43:49 -0700 Subject: [PATCH 1487/1722] Expose as modifier --- .../UtilityNetworkTraceExampleView.swift | 19 ++- .../UtilityNetworkTrace.swift | 133 +++++++++--------- .../UtilityNetworkTraceModifier.swift | 103 ++++++++++++++ 3 files changed, 175 insertions(+), 80 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 76a987a9b..f12a2e755 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -51,16 +51,15 @@ struct UtilityNetworkTraceExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .overlay { - UtilityNetworkTrace( - graphicsOverlay: $resultGraphicsOverlay, - map: map, - mapPoint: $mapPoint, - viewPoint: $viewPoint, - mapViewProxy: $mapViewProxy, - viewpoint: $viewpoint - ) - } + .utilityNetworkTrace( + isPresented: .constant(true), + graphicsOverlay: $resultGraphicsOverlay, + map: map, + mapPoint: $mapPoint, + viewPoint: $viewPoint, + mapViewProxy: $mapViewProxy, + viewpoint: $viewpoint + ) .task { await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index bd3dd4364..01639cc3a 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -14,7 +14,7 @@ import ArcGIS import SwiftUI -public struct UtilityNetworkTrace: View { +struct UtilityNetworkTrace: View { // MARK: Enums /// Activities users will perform while creating a new trace. @@ -56,7 +56,7 @@ public struct UtilityNetworkTrace: View { // MARK: States /// The current detent of the floating panel. - @State private var activeDetent: FloatingPanelDetent = .half + @Binding private var activeDetent: FloatingPanelDetent /// The current user activity. @State private var currentActivity: UserActivity = .creatingTrace(nil) @@ -522,17 +522,17 @@ public struct UtilityNetworkTrace: View { /// A graphical interface to run pre-configured traces on a map's utility networks. /// - Parameters: - /// - graphicsOverlay: The graphics overlay to hold generated starting point and trace - /// graphics. + /// - 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. + /// - 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. /// - 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. /// - startingPoints: An optional list of programmatically provided starting points. - public init( + init( + activeDetent: Binding, graphicsOverlay: Binding, map: Map, mapPoint: Binding, @@ -541,6 +541,7 @@ public struct UtilityNetworkTrace: View { viewpoint: Binding, startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) ) { + _activeDetent = activeDetent _viewPoint = viewPoint _mapPoint = mapPoint _mapViewProxy = mapViewProxy @@ -557,72 +558,64 @@ public struct UtilityNetworkTrace: View { } public var body: some View { - Color.clear - .floatingPanel( - backgroundColor: Color(uiColor: .systemGroupedBackground), - detent: $activeDetent, - horizontalAlignment: .trailing, - isPresented: .constant(true) - ) { - VStack { - if !viewModel.completedTraces.isEmpty && - !isFocused(traceCreationActivity: .addingStartingPoints) && - activeDetent != .summary { - activityPicker - } - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .addingStartingPoints: - cancelAddStartingPoints - case .inspectingStartingPoint: - startingPointDetail - default: - newTraceTab - } - case .viewingTraces(let activity): - switch activity { - case .viewingElementGroup: - assetGroupDetail - default: - resultsTab - } - } - } - .background(Color(uiColor: .systemGroupedBackground)) - .animation(.default, value: currentActivity) - .onChange(of: viewPoint) { newValue in - guard isFocused(traceCreationActivity: .addingStartingPoints), - let mapViewProxy = mapViewProxy, - let mapPoint = mapPoint, - let viewPoint = viewPoint else { - return - } - currentActivity = .creatingTrace(.viewingStartingPoints) - activeDetent = .half - Task { - await viewModel.addStartingPoint( - at: viewPoint, - mapPoint: mapPoint, - with: mapViewProxy - ) - } - } - .onChange(of: externalStartingPoints) { _ in - viewModel.externalStartingPoints = externalStartingPoints + VStack { + if !viewModel.completedTraces.isEmpty && + !isFocused(traceCreationActivity: .addingStartingPoints) && + activeDetent != .summary { + activityPicker + } + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .addingStartingPoints: + cancelAddStartingPoints + case .inspectingStartingPoint: + startingPointDetail + default: + newTraceTab } - .alert( - viewModel.userAlert?.title ?? "", - isPresented: Binding( - get: { viewModel.userAlert != nil }, - set: { _ in viewModel.userAlert = nil } - ) - ) { - viewModel.userAlert?.button - } message: { - Text(viewModel.userAlert?.description ?? "") + case .viewingTraces(let activity): + switch activity { + case .viewingElementGroup: + assetGroupDetail + default: + resultsTab } } + } + .background(Color(uiColor: .systemGroupedBackground)) + .animation(.default, value: currentActivity) + .onChange(of: viewPoint) { newValue in + guard isFocused(traceCreationActivity: .addingStartingPoints), + let mapViewProxy = mapViewProxy, + let mapPoint = mapPoint, + let viewPoint = viewPoint else { + return + } + currentActivity = .creatingTrace(.viewingStartingPoints) + activeDetent = .half + Task { + await viewModel.addStartingPoint( + at: viewPoint, + mapPoint: mapPoint, + with: mapViewProxy + ) + } + } + .onChange(of: externalStartingPoints) { _ in + viewModel.externalStartingPoints = externalStartingPoints + } + .alert( + viewModel.userAlert?.title ?? "", + isPresented: Binding( + get: { viewModel.userAlert != nil }, + set: { _ in viewModel.userAlert = nil } + ) + ) { + viewModel.userAlert?.button + } message: { + Text(viewModel.userAlert?.description ?? "") + } } // MARK: Computed Properties diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift new file mode 100644 index 000000000..a25eb481b --- /dev/null +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift @@ -0,0 +1,103 @@ +// 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 +import SwiftUI + +public extension MapView { + /// Adds a graphical interface to run pre-configured traces on a map's utility networks. + /// - Parameters: + /// - isPresented: A Boolean value indicating if the view is presented. + /// - 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. + /// - 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. + /// - startingPoints: An optional list of programmatically provided starting points. + @ViewBuilder func utilityNetworkTrace( + isPresented: Binding, + graphicsOverlay: Binding, + map: Map, + mapPoint: Binding, + viewPoint: Binding, + mapViewProxy: Binding, + viewpoint: Binding, + startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) + ) -> some View { + modifier( + UtilityNetworkTraceModifier( + isPresented: isPresented, + graphicsOverlay: graphicsOverlay, + map: map, + mapPoint: mapPoint, + viewPoint: viewPoint, + mapViewProxy: mapViewProxy, + startingPoints: startingPoints, + viewpoint: viewpoint + ) + ) + } +} + +/// Overlays the provided content with a Floating Panel that contains a Utility Network Trace. +struct UtilityNetworkTraceModifier: ViewModifier { + /// A Boolean value indicating if the view is presented. + @Binding var isPresented: Bool + + /// The current detent of the floating panel. + @State var activeDetent: FloatingPanelDetent = .half + + /// The graphics overlay to hold generated starting point and trace graphics. + @Binding var graphicsOverlay: GraphicsOverlay + + /// The map containing the utility network(s). + let map: Map + + /// Acts as the point at which newly selected starting point graphics will be created + @Binding var mapPoint: Point? + + /// Acts as the point of identification for items tapped in the utility network. + @Binding var viewPoint: CGPoint? + + /// Provides a method of layer identification when starting points are being chosen. + @Binding var mapViewProxy: MapViewProxy? + + /// An optional list of programmatically provided starting points. + @Binding var startingPoints: [UtilityNetworkTraceStartingPoint] + + /// Allows the utility network trace tool to update the parent map view's viewpoint. + @Binding var viewpoint: Viewpoint? + + @ViewBuilder func body(content: Content) -> some View { + content + .floatingPanel( + backgroundColor: Color(uiColor: .systemGroupedBackground), + detent: $activeDetent, + horizontalAlignment: .trailing, + isPresented: $isPresented + ) { + UtilityNetworkTrace( + activeDetent: $activeDetent, + graphicsOverlay: $graphicsOverlay, + map: map, + mapPoint: $mapPoint, + viewPoint: $viewPoint, + mapViewProxy: $mapViewProxy, + viewpoint: $viewpoint, + startingPoints: $startingPoints + ) + } + } +} From 3ee8b2f668494a654581267d436facc92e866317 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 24 Aug 2022 16:04:06 -0500 Subject: [PATCH 1488/1722] Clip with rounded corners when displayed in the view. --- .../Components/Popups/PopupMedia/ImageMediaView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index a089b9d70..206db8727 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -26,6 +26,7 @@ struct ImageMediaView: View { VStack { if let sourceURL = popupMedia.value?.sourceURL { AsyncImageView(url: sourceURL) + .clipShape(RoundedRectangle(cornerRadius: 8)) .onTapGesture { showingFullScreen = true } From 54eeb4428745974b0b5b2ad6b3019a292eaf2bc3 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 24 Aug 2022 14:05:52 -0700 Subject: [PATCH 1489/1722] Update Sources/ArcGISToolkit/Utility/CredentialInputView.swift Co-authored-by: Mark Dostal --- Sources/ArcGISToolkit/Utility/CredentialInputView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift index 81bd11ee6..480e51578 100644 --- a/Sources/ArcGISToolkit/Utility/CredentialInputView.swift +++ b/Sources/ArcGISToolkit/Utility/CredentialInputView.swift @@ -48,10 +48,10 @@ struct CredentialInputView: UIViewControllerRepresentable { /// Creates the view. /// - Parameters: + /// - fields: The fields shown in the alert. /// - isPresented: A Boolean value indicating whether or not the view is displayed. /// - message: Descriptive text that provides more details about the reason for the alert. /// - title: The title of the alert. - /// - fields: The fields shown in the alert. /// - cancelAction: The cancel action. /// - continueAction: The continue action. init( From 6a8bdfa4b15b9d8f9efcaba7b8b5068b2625d98a Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 24 Aug 2022 14:18:07 -0700 Subject: [PATCH 1490/1722] Update Login.swift --- Sources/ArcGISToolkit/Components/Authentication/Login.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Login.swift b/Sources/ArcGISToolkit/Components/Authentication/Login.swift index 18dd20e0a..1788c9ba8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Login.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Login.swift @@ -91,7 +91,7 @@ struct LoginViewModifier: ViewModifier { func body(content: Content) -> some View { content - .task { isPresented = true } + .onAppear { isPresented = true } .credentialInput( fields: .usernamePassword, isPresented: $isPresented, From 11d543377a831d80f06a4254e65f361f7a8ff65c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 25 Aug 2022 09:48:25 -0500 Subject: [PATCH 1491/1722] Improvements to image rendering; change to horizontal scroll view instead of page control. --- .../Popups/MediaPopupElementView.swift | 40 +++++++++++-------- .../Popups/PopupMedia/AsyncImageView.swift | 14 ++++++- .../PopupMedia/FullScreenImageView.swift | 17 +++++--- .../Popups/PopupMedia/ImageMediaView.swift | 19 +++++---- 4 files changed, 57 insertions(+), 33 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 082f82fae..bd05ab158 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -37,31 +37,37 @@ struct MediaPopupElementView: View { @State private var viewWidth: CGFloat = .zero var body: some View { - TabView { - ForEach(popupMedia) { media in - Group { - switch media.kind { - case .image: - ImageMediaView(popupMedia: media) - case .barChart, .columnChart, .lineChart, .pieChart: - ChartMediaView(popupMedia: media) - default: - EmptyView() + ScrollView(.horizontal) { + HStack(alignment: .top, spacing: 8) { + ForEach(popupMedia) { media in + Group { + switch media.kind { + case .image: + ImageMediaView(popupMedia: media) + case .barChart, .columnChart, .lineChart, .pieChart: + ChartMediaView(popupMedia: media) + default: + EmptyView() + } } - } - .padding([.bottom], popupMedia.count > 1 ? 48: 8) - .tabItem { - Text(media.title) + .frame(width: viewWidth * widthScaleFactor) + .clipped() } } } - .tabViewStyle(PageTabViewStyle()) - .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) - .frame(height: viewWidth * 0.75) .onSizeChange { viewWidth = $0.width } } + + /// The scale factor for specifying the width of popup media. If there is only one popup media, + /// the scale is 1.0 (full width); if there is more than one, the scale is 0.85, which allows for + /// second and subsequent media to be partially visible, to indicate there is more than one. + var widthScaleFactor: Double { + get { + popupMedia.count > 1 ? 0.85 : 1 + } + } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift index 0bed4857a..73fa96d43 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift @@ -19,13 +19,25 @@ struct AsyncImageView: View { /// The `URL` of the image. let url: URL + /// The `ContentMode` defining how the image fills the available space. + let contentMode: ContentMode + + /// Creates an `AsyncImageView`. + /// - Parameters: + /// - url: The `URL` of the image. + /// - contentMode: The `ContentMode` defining how the image fills the available space. + public init(url: URL, contentMode: ContentMode = .fit) { + self.url = url + self.contentMode = contentMode + } + var body: some View { AsyncImage(url: url) { phase in if let image = phase.image { // Displays the loaded image. image .resizable() - .aspectRatio(contentMode: .fit) + .aspectRatio(contentMode: contentMode) } else if phase.error != nil { // Displays an error notification. HStack(alignment: .center) { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift index d99490573..827515b3f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift @@ -26,7 +26,7 @@ struct FullScreenImageView: View { @Binding var showingFullScreen: Bool var body: some View { - VStack() { + VStack { HStack { Spacer() Button { @@ -37,9 +37,16 @@ struct FullScreenImageView: View { } .padding([.bottom], 4) } - Text(popupMedia.title) - .font(.title3) - .fontWeight(.bold) + HStack { + VStack(alignment: .leading) { + Text(popupMedia.title) + .font(.title2) + Text(popupMedia.caption) + .font(.title3) + .foregroundColor(.secondary) + } + Spacer() + } AsyncImageView(url: sourceURL) .onTapGesture { if let url = popupMedia.value?.linkURL { @@ -49,7 +56,7 @@ struct FullScreenImageView: View { if popupMedia.value?.linkURL != nil { HStack { Text("Tap on the image for more information.") - .font(.caption) + .font(.subheadline) .foregroundColor(.secondary) Spacer() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 206db8727..8b47b18f6 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -23,18 +23,17 @@ struct ImageMediaView: View { @State private var showingFullScreen = false var body: some View { - VStack { + VStack(alignment: .leading) { if let sourceURL = popupMedia.value?.sourceURL { - AsyncImageView(url: sourceURL) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .onTapGesture { - showingFullScreen = true - } - } - HStack { - PopupMediaFooter(popupMedia: popupMedia) - Spacer() + AsyncImageView(url: sourceURL, contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: 200) + .clipped() + .cornerRadius(8) + .onTapGesture { + showingFullScreen = true + } } + PopupMediaFooter(popupMedia: popupMedia) } .sheet(isPresented: $showingFullScreen) { if let url = popupMedia.value?.sourceURL { From bd81de9acb3d381763ff6f27f7d3cfcfd6ff53d0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 25 Aug 2022 13:06:40 -0500 Subject: [PATCH 1492/1722] Fix MediaPopup footer alignment. --- .../Components/Popups/PopupMedia/ImageMediaView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 8b47b18f6..2c137ac3e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -23,7 +23,7 @@ struct ImageMediaView: View { @State private var showingFullScreen = false var body: some View { - VStack(alignment: .leading) { + VStack { if let sourceURL = popupMedia.value?.sourceURL { AsyncImageView(url: sourceURL, contentMode: .fill) .frame(maxWidth: .infinity, maxHeight: 200) @@ -33,7 +33,10 @@ struct ImageMediaView: View { showingFullScreen = true } } - PopupMediaFooter(popupMedia: popupMedia) + HStack { + PopupMediaFooter(popupMedia: popupMedia) + Spacer() + } } .sheet(isPresented: $showingFullScreen) { if let url = popupMedia.value?.sourceURL { From a56160fda4c59c6fc8e86d43c3f54712f0ff7f6f Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 25 Aug 2022 13:40:03 -0700 Subject: [PATCH 1493/1722] `makePersistent` -> `setupPersistentCredentialStorage` --- .../Components/Authentication/Authenticator.swift | 2 +- Tests/ArcGISToolkitTests/AuthenticatorTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 160852234..525e85380 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -44,7 +44,7 @@ public final class Authenticator: ObservableObject { /// - Parameters: /// - access: When the credentials stored in the keychain can be accessed. /// - isSynchronizable: A value indicating whether the credentials are synchronized with iCloud. - public func makePersistent( + public func setupPersistentCredentialStorage( access: ArcGIS.KeychainAccess, isSynchronizable: Bool = false ) async throws { diff --git a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift index 6b85fc8dd..20ed6d2ab 100644 --- a/Tests/ArcGISToolkitTests/AuthenticatorTests.swift +++ b/Tests/ArcGISToolkitTests/AuthenticatorTests.swift @@ -35,10 +35,10 @@ import Combine await ArcGISRuntimeEnvironment.setNetworkCredentialStore(NetworkCredentialStore()) } - // This tests that calling makePersistent tries to sync with the keychain. + // This tests that calling setupPersistentCredentialStorage tries to sync with the keychain. let authenticator = Authenticator() do { - try await authenticator.makePersistent(access: .whenUnlocked) + try await authenticator.setupPersistentCredentialStorage(access: .whenUnlocked) XCTFail("Expected an error to be thrown as unit tests should not have access to the keychain") } catch {} } From 6f54fa34a8ac6cdd71b848c295c901351efc9c58 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 25 Aug 2022 13:52:10 -0700 Subject: [PATCH 1494/1722] Update CertificatePickerViewModifier.swift --- .../CertificatePickerViewModifier.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift index a4ea098d3..00478596e 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/CertificatePickerViewModifier.swift @@ -24,9 +24,6 @@ import ArcGIS /// The URL of the certificate that the user chose. var certificateURL: URL? - /// The password. - @Published var password = "" - /// A Boolean value indicating whether to show the prompt. @Published var showPrompt = true @@ -68,8 +65,9 @@ import ArcGIS } /// Attempts to use the certificate and password to respond to the challenge. - func proceedWithPassword() { - guard let certificateURL = certificateURL, !password.isEmpty else { + /// - Parameter password: The password for the certificate. + func proceed(withPassword password: String) { + guard let certificateURL = certificateURL else { preconditionFailure() } @@ -126,8 +124,7 @@ struct CertificatePickerViewModifier: ViewModifier { continueAction: .init( title: "OK", handler: { _, password in - viewModel.password = password - viewModel.proceedWithPassword() + viewModel.proceed(withPassword: password) } ) ) From 0dc2091428291bb04fbc642acfb69e10f83ce28e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 25 Aug 2022 16:10:43 -0500 Subject: [PATCH 1495/1722] Fix clipping and ContentShape to limit tap gestures to the exact image media --- .../Popups/MediaPopupElementView.swift | 18 +++++++++++++----- .../Popups/PopupMedia/ImageMediaView.swift | 6 +++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index bd05ab158..0a489219e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -34,7 +34,7 @@ struct MediaPopupElementView: View { let popupMedia: [PopupMedia] /// The width of the view content. - @State private var viewWidth: CGFloat = .zero + @State private var width: CGFloat = .zero var body: some View { ScrollView(.horizontal) { @@ -43,20 +43,23 @@ struct MediaPopupElementView: View { Group { switch media.kind { case .image: - ImageMediaView(popupMedia: media) + ImageMediaView( + popupMedia: media, + mediaSize: mediaSize + ) case .barChart, .columnChart, .lineChart, .pieChart: ChartMediaView(popupMedia: media) default: EmptyView() } } - .frame(width: viewWidth * widthScaleFactor) - .clipped() + .frame(width: width) + .contentShape(Rectangle()) } } } .onSizeChange { - viewWidth = $0.width + width = $0.width * widthScaleFactor } } @@ -68,6 +71,11 @@ struct MediaPopupElementView: View { popupMedia.count > 1 ? 0.85 : 1 } } + + /// The size of the image or chart media, not counting descriptive text. + var mediaSize: CGSize { + CGSize(width: width, height: 200) + } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 2c137ac3e..ab34af73d 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -18,6 +18,7 @@ import ArcGIS struct ImageMediaView: View { /// The popup media to display. let popupMedia: PopupMedia + let mediaSize: CGSize /// A Boolean value specifying whether the media should be shown full screen. @State private var showingFullScreen = false @@ -26,9 +27,8 @@ struct ImageMediaView: View { VStack { if let sourceURL = popupMedia.value?.sourceURL { AsyncImageView(url: sourceURL, contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: 200) - .clipped() - .cornerRadius(8) + .frame(width: mediaSize.width, height: mediaSize.height) + .clipShape(RoundedRectangle(cornerRadius: 8)) .onTapGesture { showingFullScreen = true } From 0092bd46cd689641350bc9d6b56cdcd9fd8b75ac Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Thu, 25 Aug 2022 16:56:45 -0700 Subject: [PATCH 1496/1722] Remove UN trace modifier --- .../UtilityNetworkTraceExampleView.swift | 19 +-- .../UtilityNetworkTrace.swift | 126 +++++++++--------- .../UtilityNetworkTraceModifier.swift | 103 -------------- 3 files changed, 76 insertions(+), 172 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index f12a2e755..76a987a9b 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -51,15 +51,16 @@ struct UtilityNetworkTraceExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .utilityNetworkTrace( - isPresented: .constant(true), - graphicsOverlay: $resultGraphicsOverlay, - map: map, - mapPoint: $mapPoint, - viewPoint: $viewPoint, - mapViewProxy: $mapViewProxy, - viewpoint: $viewpoint - ) + .overlay { + UtilityNetworkTrace( + graphicsOverlay: $resultGraphicsOverlay, + map: map, + mapPoint: $mapPoint, + viewPoint: $viewPoint, + mapViewProxy: $mapViewProxy, + viewpoint: $viewpoint + ) + } .task { await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 01639cc3a..5a281b4a8 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -14,7 +14,7 @@ import ArcGIS import SwiftUI -struct UtilityNetworkTrace: View { +public struct UtilityNetworkTrace: View { // MARK: Enums /// Activities users will perform while creating a new trace. @@ -56,7 +56,7 @@ struct UtilityNetworkTrace: View { // MARK: States /// The current detent of the floating panel. - @Binding private var activeDetent: FloatingPanelDetent + @State private var activeDetent: FloatingPanelDetent = .half /// The current user activity. @State private var currentActivity: UserActivity = .creatingTrace(nil) @@ -531,8 +531,7 @@ struct UtilityNetworkTrace: View { /// chosen. /// - viewpoint: Allows the utility network trace tool to update the parent map view's viewpoint. /// - startingPoints: An optional list of programmatically provided starting points. - init( - activeDetent: Binding, + public init( graphicsOverlay: Binding, map: Map, mapPoint: Binding, @@ -541,7 +540,6 @@ struct UtilityNetworkTrace: View { viewpoint: Binding, startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) ) { - _activeDetent = activeDetent _viewPoint = viewPoint _mapPoint = mapPoint _mapViewProxy = mapViewProxy @@ -558,64 +556,72 @@ struct UtilityNetworkTrace: View { } public var body: some View { - VStack { - if !viewModel.completedTraces.isEmpty && - !isFocused(traceCreationActivity: .addingStartingPoints) && - activeDetent != .summary { - activityPicker - } - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .addingStartingPoints: - cancelAddStartingPoints - case .inspectingStartingPoint: - startingPointDetail - default: - newTraceTab + Color.clear + .floatingPanel( + backgroundColor: Color(uiColor: .systemGroupedBackground), + detent: $activeDetent, + horizontalAlignment: .trailing, + isPresented: .constant(true) + ) { + VStack { + if !viewModel.completedTraces.isEmpty && + !isFocused(traceCreationActivity: .addingStartingPoints) && + activeDetent != .summary { + activityPicker + } + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .addingStartingPoints: + cancelAddStartingPoints + case .inspectingStartingPoint: + startingPointDetail + default: + newTraceTab + } + case .viewingTraces(let activity): + switch activity { + case .viewingElementGroup: + assetGroupDetail + default: + resultsTab + } + } } - case .viewingTraces(let activity): - switch activity { - case .viewingElementGroup: - assetGroupDetail - default: - resultsTab + .background(Color(uiColor: .systemGroupedBackground)) + .animation(.default, value: currentActivity) + .onChange(of: viewPoint) { newValue in + guard isFocused(traceCreationActivity: .addingStartingPoints), + let mapViewProxy = mapViewProxy, + let mapPoint = mapPoint, + let viewPoint = viewPoint else { + return + } + currentActivity = .creatingTrace(.viewingStartingPoints) + activeDetent = .half + Task { + await viewModel.addStartingPoint( + at: viewPoint, + mapPoint: mapPoint, + with: mapViewProxy + ) + } + } + .onChange(of: externalStartingPoints) { _ in + viewModel.externalStartingPoints = externalStartingPoints + } + .alert( + viewModel.userAlert?.title ?? "", + isPresented: Binding( + get: { viewModel.userAlert != nil }, + set: { _ in viewModel.userAlert = nil } + ) + ) { + viewModel.userAlert?.button + } message: { + Text(viewModel.userAlert?.description ?? "") } } - } - .background(Color(uiColor: .systemGroupedBackground)) - .animation(.default, value: currentActivity) - .onChange(of: viewPoint) { newValue in - guard isFocused(traceCreationActivity: .addingStartingPoints), - let mapViewProxy = mapViewProxy, - let mapPoint = mapPoint, - let viewPoint = viewPoint else { - return - } - currentActivity = .creatingTrace(.viewingStartingPoints) - activeDetent = .half - Task { - await viewModel.addStartingPoint( - at: viewPoint, - mapPoint: mapPoint, - with: mapViewProxy - ) - } - } - .onChange(of: externalStartingPoints) { _ in - viewModel.externalStartingPoints = externalStartingPoints - } - .alert( - viewModel.userAlert?.title ?? "", - isPresented: Binding( - get: { viewModel.userAlert != nil }, - set: { _ in viewModel.userAlert = nil } - ) - ) { - viewModel.userAlert?.button - } message: { - Text(viewModel.userAlert?.description ?? "") - } } // MARK: Computed Properties diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift deleted file mode 100644 index a25eb481b..000000000 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceModifier.swift +++ /dev/null @@ -1,103 +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 -import SwiftUI - -public extension MapView { - /// Adds a graphical interface to run pre-configured traces on a map's utility networks. - /// - Parameters: - /// - isPresented: A Boolean value indicating if the view is presented. - /// - 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. - /// - 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. - /// - startingPoints: An optional list of programmatically provided starting points. - @ViewBuilder func utilityNetworkTrace( - isPresented: Binding, - graphicsOverlay: Binding, - map: Map, - mapPoint: Binding, - viewPoint: Binding, - mapViewProxy: Binding, - viewpoint: Binding, - startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) - ) -> some View { - modifier( - UtilityNetworkTraceModifier( - isPresented: isPresented, - graphicsOverlay: graphicsOverlay, - map: map, - mapPoint: mapPoint, - viewPoint: viewPoint, - mapViewProxy: mapViewProxy, - startingPoints: startingPoints, - viewpoint: viewpoint - ) - ) - } -} - -/// Overlays the provided content with a Floating Panel that contains a Utility Network Trace. -struct UtilityNetworkTraceModifier: ViewModifier { - /// A Boolean value indicating if the view is presented. - @Binding var isPresented: Bool - - /// The current detent of the floating panel. - @State var activeDetent: FloatingPanelDetent = .half - - /// The graphics overlay to hold generated starting point and trace graphics. - @Binding var graphicsOverlay: GraphicsOverlay - - /// The map containing the utility network(s). - let map: Map - - /// Acts as the point at which newly selected starting point graphics will be created - @Binding var mapPoint: Point? - - /// Acts as the point of identification for items tapped in the utility network. - @Binding var viewPoint: CGPoint? - - /// Provides a method of layer identification when starting points are being chosen. - @Binding var mapViewProxy: MapViewProxy? - - /// An optional list of programmatically provided starting points. - @Binding var startingPoints: [UtilityNetworkTraceStartingPoint] - - /// Allows the utility network trace tool to update the parent map view's viewpoint. - @Binding var viewpoint: Viewpoint? - - @ViewBuilder func body(content: Content) -> some View { - content - .floatingPanel( - backgroundColor: Color(uiColor: .systemGroupedBackground), - detent: $activeDetent, - horizontalAlignment: .trailing, - isPresented: $isPresented - ) { - UtilityNetworkTrace( - activeDetent: $activeDetent, - graphicsOverlay: $graphicsOverlay, - map: map, - mapPoint: $mapPoint, - viewPoint: $viewPoint, - mapViewProxy: $mapViewProxy, - viewpoint: $viewpoint, - startingPoints: $startingPoints - ) - } - } -} From 944918c9130d3ea07598b82ea66a0e82369cb551 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 26 Aug 2022 09:44:09 -0500 Subject: [PATCH 1497/1722] Remove unused quicklook vc --- .../Popups/QuickLookViewController.swift | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift b/Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift deleted file mode 100644 index 5cb3f73d6..000000000 --- a/Sources/ArcGISToolkit/Components/Popups/QuickLookViewController.swift +++ /dev/null @@ -1,51 +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 -import ArcGIS -import QuickLook - -struct QuickLookViewController: UIViewControllerRepresentable { - let url: URL - - func makeUIViewController(context: Context) -> QLPreviewController { - let controller = QLPreviewController() - return controller - } - - func makeCoordinator() -> Coordinator { - return Coordinator(parent: self) - } - - func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) {} - - - class Coordinator: QLPreviewControllerDataSource { - let parent: QuickLookViewController - - init(parent: QuickLookViewController) { - self.parent = parent - } - - func numberOfPreviewItems(in controller: QLPreviewController) -> Int { - return 1 - } - - func previewController( - _ controller: QLPreviewController, - previewItemAt index: Int - ) -> QLPreviewItem { - return parent.url as NSURL - } - } -} From 2be9ff8953eb0d0ad347fc2ddbf5183c9d4b58a8 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 26 Aug 2022 11:19:23 -0600 Subject: [PATCH 1498/1722] properly handle cancellation error --- .../Authentication/OAuthChallengeContinuation.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift index 90eb706e5..e1d4181de 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift @@ -34,7 +34,12 @@ final class OAuthChallengeContinuation: ValueContinuation Date: Fri, 26 Aug 2022 11:19:53 -0600 Subject: [PATCH 1499/1722] fix build error --- .../AuthenticationExample/AuthenticationApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift index 21bce0ff0..78fb9db16 100644 --- a/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift +++ b/AuthenticationExample/AuthenticationExample/AuthenticationApp.swift @@ -54,7 +54,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.makePersistent(access: .whenUnlockedThisDeviceOnly) + try? await authenticator.setupPersistentCredentialStorage(access: .whenUnlockedThisDeviceOnly) isSettingUp = false } } From 8a31314f05524bcfa808096f9365d4dcb5384936 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Fri, 26 Aug 2022 10:33:30 -0700 Subject: [PATCH 1500/1722] Add modifier --- .../UtilityNetworkTraceExampleView.swift | 17 ++- .../UtilityNetworkTrace.swift | 134 +++++++++--------- 2 files changed, 83 insertions(+), 68 deletions(-) diff --git a/Examples/Examples/UtilityNetworkTraceExampleView.swift b/Examples/Examples/UtilityNetworkTraceExampleView.swift index 76a987a9b..9971c1f44 100644 --- a/Examples/Examples/UtilityNetworkTraceExampleView.swift +++ b/Examples/Examples/UtilityNetworkTraceExampleView.swift @@ -21,6 +21,9 @@ struct UtilityNetworkTraceExampleView: View { /// The map containing the utility networks. @StateObject private var map = makeMap() + /// 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? @@ -51,7 +54,15 @@ struct UtilityNetworkTraceExampleView: View { .onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } - .overlay { + .task { + await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) + } + .floatingPanel( + backgroundColor: Color(uiColor: .systemGroupedBackground), + detent: $activeDetent, + horizontalAlignment: .trailing, + isPresented: .constant(true) + ) { UtilityNetworkTrace( graphicsOverlay: $resultGraphicsOverlay, map: map, @@ -60,9 +71,7 @@ struct UtilityNetworkTraceExampleView: View { mapViewProxy: $mapViewProxy, viewpoint: $viewpoint ) - } - .task { - await ArcGISRuntimeEnvironment.credentialStore.add(try! await .publicSample) + .floatingPanelDetent($activeDetent) } } } diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift index 5a281b4a8..185ac4ea8 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTrace.swift @@ -56,7 +56,7 @@ public struct UtilityNetworkTrace: View { // MARK: States /// The current detent of the floating panel. - @State private var activeDetent: FloatingPanelDetent = .half + @Binding private var activeDetent: FloatingPanelDetent? /// The current user activity. @State private var currentActivity: UserActivity = .creatingTrace(nil) @@ -540,6 +540,7 @@ public struct UtilityNetworkTrace: View { viewpoint: Binding, startingPoints: Binding<[UtilityNetworkTraceStartingPoint]> = .constant([]) ) { + _activeDetent = .constant(nil) _viewPoint = viewPoint _mapPoint = mapPoint _mapViewProxy = mapViewProxy @@ -555,73 +556,78 @@ public struct UtilityNetworkTrace: View { ) } + /// Sets the active detent for a hosting floating panel. + /// - Parameter detent: A binding to a value that determines the height of a hosting + /// floating panel. + /// - Returns: A trace tool that automatically sets and responds to detent values to improve user + /// experience. + public func floatingPanelDetent( + _ detent: Binding + ) -> UtilityNetworkTrace { + var copy = self + copy._activeDetent = Binding(detent) + return copy + } + public var body: some View { - Color.clear - .floatingPanel( - backgroundColor: Color(uiColor: .systemGroupedBackground), - detent: $activeDetent, - horizontalAlignment: .trailing, - isPresented: .constant(true) - ) { - VStack { - if !viewModel.completedTraces.isEmpty && - !isFocused(traceCreationActivity: .addingStartingPoints) && - activeDetent != .summary { - activityPicker - } - switch currentActivity { - case .creatingTrace(let activity): - switch activity { - case .addingStartingPoints: - cancelAddStartingPoints - case .inspectingStartingPoint: - startingPointDetail - default: - newTraceTab - } - case .viewingTraces(let activity): - switch activity { - case .viewingElementGroup: - assetGroupDetail - default: - resultsTab - } - } - } - .background(Color(uiColor: .systemGroupedBackground)) - .animation(.default, value: currentActivity) - .onChange(of: viewPoint) { newValue in - guard isFocused(traceCreationActivity: .addingStartingPoints), - let mapViewProxy = mapViewProxy, - let mapPoint = mapPoint, - let viewPoint = viewPoint else { - return - } - currentActivity = .creatingTrace(.viewingStartingPoints) - activeDetent = .half - Task { - await viewModel.addStartingPoint( - at: viewPoint, - mapPoint: mapPoint, - with: mapViewProxy - ) - } - } - .onChange(of: externalStartingPoints) { _ in - viewModel.externalStartingPoints = externalStartingPoints + VStack { + if !viewModel.completedTraces.isEmpty && + !isFocused(traceCreationActivity: .addingStartingPoints) && + activeDetent != .summary { + activityPicker + } + switch currentActivity { + case .creatingTrace(let activity): + switch activity { + case .addingStartingPoints: + cancelAddStartingPoints + case .inspectingStartingPoint: + startingPointDetail + default: + newTraceTab } - .alert( - viewModel.userAlert?.title ?? "", - isPresented: Binding( - get: { viewModel.userAlert != nil }, - set: { _ in viewModel.userAlert = nil } - ) - ) { - viewModel.userAlert?.button - } message: { - Text(viewModel.userAlert?.description ?? "") + case .viewingTraces(let activity): + switch activity { + case .viewingElementGroup: + assetGroupDetail + default: + resultsTab } } + } + .background(Color(uiColor: .systemGroupedBackground)) + .animation(.default, value: currentActivity) + .onChange(of: viewPoint) { newValue in + guard isFocused(traceCreationActivity: .addingStartingPoints), + let mapViewProxy = mapViewProxy, + let mapPoint = mapPoint, + let viewPoint = viewPoint else { + return + } + currentActivity = .creatingTrace(.viewingStartingPoints) + activeDetent = .half + Task { + await viewModel.addStartingPoint( + at: viewPoint, + mapPoint: mapPoint, + with: mapViewProxy + ) + } + } + .onChange(of: externalStartingPoints) { _ in + viewModel.externalStartingPoints = externalStartingPoints + } + .alert( + viewModel.userAlert?.title ?? "", + isPresented: Binding( + get: { viewModel.userAlert != nil }, + set: { _ in viewModel.userAlert = nil } + ) + ) { + viewModel.userAlert?.button + } message: { + Text(viewModel.userAlert?.description ?? "") + } } // MARK: Computed Properties From 807600dabf29e89a2e2ea963f71f253089185680 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 27 Aug 2022 13:12:15 -0500 Subject: [PATCH 1501/1722] Visual improvements to match apps team design --- .../Popups/MediaPopupElementView.swift | 7 ++-- .../Popups/PopupMedia/ImageMediaView.swift | 23 ++++++++---- .../Popups/PopupMedia/PopupMediaFooter.swift | 36 +++++++++++++------ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 0a489219e..6d73db0a4 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -25,6 +25,7 @@ struct MediaPopupElementView: View { description: popupElement.description ) .padding([.bottom], 4) + Divider() PopupMediaView(popupMedia: popupElement.media) } @@ -53,8 +54,8 @@ struct MediaPopupElementView: View { EmptyView() } } - .frame(width: width) - .contentShape(Rectangle()) + .frame(width: mediaSize.width, height: mediaSize.height) + .contentShape(RoundedRectangle(cornerRadius: 8)) } } } @@ -68,7 +69,7 @@ struct MediaPopupElementView: View { /// second and subsequent media to be partially visible, to indicate there is more than one. var widthScaleFactor: Double { get { - popupMedia.count > 1 ? 0.85 : 1 + popupMedia.count > 1 ? 0.75 : 1 } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index ab34af73d..74fc44ec0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -22,22 +22,33 @@ struct ImageMediaView: View { /// A Boolean value specifying whether the media should be shown full screen. @State private var showingFullScreen = false - + private let cornerRadius: CGFloat = 8 + var body: some View { - VStack { + ZStack { if let sourceURL = popupMedia.value?.sourceURL { AsyncImageView(url: sourceURL, contentMode: .fill) - .frame(width: mediaSize.width, height: mediaSize.height) - .clipShape(RoundedRectangle(cornerRadius: 8)) .onTapGesture { showingFullScreen = true } + .frame(width: mediaSize.width, height: mediaSize.height) } - HStack { - PopupMediaFooter(popupMedia: popupMedia) + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(.gray, lineWidth: 1) + .frame(width: mediaSize.width, height: mediaSize.height) + VStack { Spacer() + HStack { + PopupMediaFooter( + popupMedia: popupMedia, + mediaSize: mediaSize + ) + Spacer() + } } } + .frame(width: mediaSize.width, height: mediaSize.height) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .sheet(isPresented: $showingFullScreen) { if let url = popupMedia.value?.sourceURL { FullScreenImageView( diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift index e750facf4..2560ed376 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift @@ -19,18 +19,34 @@ struct PopupMediaFooter: View { /// The popup media to display. let popupMedia: PopupMedia + let mediaSize: CGSize + var body: some View { - VStack(alignment: .leading) { - if !popupMedia.title.isEmpty { - Text(popupMedia.title) - .font(.body) - } - - if !popupMedia.caption.isEmpty { - Text(popupMedia.caption) - .font(.subheadline) - .foregroundColor(.secondary) + ZStack { + let gradient = Gradient(colors: [.black, .black.opacity(0.35)]) + Rectangle() + .fill( + LinearGradient(gradient: gradient, startPoint: .bottom, endPoint: .top) + ) + .frame(maxWidth: .infinity) + .frame(height: mediaSize.height * 0.25) + HStack { + VStack(alignment: .leading) { + if !popupMedia.title.isEmpty { + Text(popupMedia.title) + .foregroundColor(.white) + .font(.body) + } + + if !popupMedia.caption.isEmpty { + Text(popupMedia.caption) + .font(.subheadline) + .foregroundColor(.init(white: 0.75)) + } + } + Spacer() } + .padding([.leading], 20) } } } From 71d4378d728c0f71abc1f08ffd7674569b15992b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 27 Aug 2022 16:40:10 -0500 Subject: [PATCH 1502/1722] Change opacity of end gradient; reorganize ImageMediaView --- .../Popups/PopupMedia/ImageMediaView.swift | 17 +++++++---------- .../Popups/PopupMedia/PopupMediaFooter.swift | 3 +-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 74fc44ec0..62fd9e2ab 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -33,19 +33,16 @@ struct ImageMediaView: View { } .frame(width: mediaSize.width, height: mediaSize.height) } - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(.gray, lineWidth: 1) - .frame(width: mediaSize.width, height: mediaSize.height) VStack { Spacer() - HStack { - PopupMediaFooter( - popupMedia: popupMedia, - mediaSize: mediaSize - ) - Spacer() - } + PopupMediaFooter( + popupMedia: popupMedia, + mediaSize: mediaSize + ) } + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(.gray, lineWidth: 1) + .frame(width: mediaSize.width, height: mediaSize.height) } .frame(width: mediaSize.width, height: mediaSize.height) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift index 2560ed376..36575755f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift @@ -23,12 +23,11 @@ struct PopupMediaFooter: View { var body: some View { ZStack { - let gradient = Gradient(colors: [.black, .black.opacity(0.35)]) + let gradient = Gradient(colors: [.black, .black.opacity(0.15)]) Rectangle() .fill( LinearGradient(gradient: gradient, startPoint: .bottom, endPoint: .top) ) - .frame(maxWidth: .infinity) .frame(height: mediaSize.height * 0.25) HStack { VStack(alignment: .leading) { From fa79577a0de744e82494357657c61cca7381e4df Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 27 Aug 2022 16:54:27 -0500 Subject: [PATCH 1503/1722] Adjust font used in popupelement header. --- .../ArcGISToolkit/Components/Popups/PopupElementHeader.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index 6eab4c4da..c0dede808 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -25,8 +25,7 @@ struct PopupElementHeader: View { // a view, so conditionally check for an empty title and description. if !title.isEmpty { Text(title) - .font(.title3) - .fontWeight(.bold) + .font(.title2) .foregroundColor(.primary) } From 78f2ae8c757314d9a969018b644c46a1161149f3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 27 Aug 2022 17:03:18 -0500 Subject: [PATCH 1504/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Components/Popups/PopupMedia/FullScreenImageView.swift | 2 +- .../Components/Popups/PopupMedia/ImageMediaView.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift index 827515b3f..8ee2d79fe 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift @@ -15,7 +15,7 @@ import SwiftUI import ArcGIS /// A view displaying a popup media image in full screen. -struct FullScreenImageView: View { +struct DetailImageView: View { /// The popup media to display. let popupMedia: PopupMedia diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 62fd9e2ab..902ebd4ec 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -18,6 +18,8 @@ import ArcGIS struct ImageMediaView: View { /// The popup media to display. let popupMedia: PopupMedia + + /// The size of the media's frame. let mediaSize: CGSize /// A Boolean value specifying whether the media should be shown full screen. From 501c7b79b9c66c76009689611b4dda02bd50ef5d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Sat, 27 Aug 2022 17:10:27 -0500 Subject: [PATCH 1505/1722] Cleanup and name change to "DetailImageView". --- .../{FullScreenImageView.swift => DetailImageView.swift} | 0 .../Components/Popups/PopupMedia/ImageMediaView.swift | 2 +- .../Components/Popups/PopupMedia/PopupMediaFooter.swift | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{FullScreenImageView.swift => DetailImageView.swift} (100%) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/DetailImageView.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/FullScreenImageView.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/DetailImageView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 902ebd4ec..2986be6d2 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -50,7 +50,7 @@ struct ImageMediaView: View { .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .sheet(isPresented: $showingFullScreen) { if let url = popupMedia.value?.sourceURL { - FullScreenImageView( + DetailImageView( popupMedia: popupMedia, sourceURL: url, showingFullScreen: $showingFullScreen diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift index 36575755f..a4edd2eef 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift @@ -19,6 +19,7 @@ struct PopupMediaFooter: View { /// The popup media to display. let popupMedia: PopupMedia + /// The size of the media's frame. let mediaSize: CGSize var body: some View { From 2497d904e1f746326494d50568811b9dd99dd1db Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Aug 2022 13:03:26 -0500 Subject: [PATCH 1506/1722] Refactor attachment code. --- .../Popups/Attachments/AttachmentList.swift | 96 ++++++++++ .../Attachments/AttachmentPreview.swift | 76 ++++++++ .../AttachmentsPopupElementModel.swift | 15 +- .../Popups/Attachments/ThumbnailView.swift | 38 ++++ .../Popups/AttachmentsPopupElementView.swift | 168 +----------------- 5 files changed, 225 insertions(+), 168 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift rename Sources/ArcGISToolkit/Components/Popups/{ => Attachments}/AttachmentsPopupElementModel.swift (85%) create mode 100644 Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift new file mode 100644 index 000000000..9afb4fae0 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift @@ -0,0 +1,96 @@ +// 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 + +struct AttachmentList: View { + var attachmentModels: [AttachmentModel] + var body: some View { + VStack(alignment: .leading, spacing: 6) { + ForEach(attachmentModels) { attachmentModel in + AttachmentRow(attachmentModel: attachmentModel) + if attachmentModel != attachmentModels.last { + Divider() + } + } + } + } +} + +struct AttachmentRow: View { + @ObservedObject var attachmentModel: AttachmentModel + @State var url: URL? + + var body: some View { + HStack { + HStack { + ThumbnailView(attachmentModel: attachmentModel) + .padding(2) + VStack(alignment: .leading) { + Text(attachmentModel.attachment.name) + .lineLimit(1) + .truncationMode(.middle) + Text("\(attachmentModel.attachment.size.formatted(.byteCount(style: .file)))") + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .onTapGesture { + if attachmentModel.attachment.loadStatus == .loaded { + url = attachmentModel.attachment.fileURL + } + } + if attachmentModel.loadStatus != .loaded { + AttachmentLoadButton(attachmentModel: attachmentModel) + } + } + .quickLookPreview($url) + } +} + +struct AttachmentLoadButton: View { + @ObservedObject var attachmentModel: AttachmentModel + + var body: some View { + Button { + if attachmentModel.loadStatus == .notLoaded { + attachmentModel.load() + } + else if attachmentModel.loadStatus == .failed { + // TODO: Show error alert, similar to BasemapGallery. + } + } label: { + Group { + switch attachmentModel.loadStatus { + case .notLoaded: + Image(systemName: "square.and.arrow.down") + .resizable() + .aspectRatio(contentMode: .fit) + case .loading: + ProgressView() + case .loaded: + EmptyView() + case .failed: + Image(systemName: "exclamationmark.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.red) + .background(Color.clear) + } + } + .frame(width: 24, height: 24) + .padding(.leading) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift new file mode 100644 index 000000000..da26f030d --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift @@ -0,0 +1,76 @@ +// 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 + +struct AttachmentPreview: View { + var attachmentModels: [AttachmentModel] + + var body: some View { + ScrollView(.horizontal) { + HStack(alignment: .top, spacing: 8) { + ForEach(attachmentModels) { attachmentModel in + AttachmentCell(attachmentModel: attachmentModel) + } + } + } + } + + struct AttachmentCell: View { + @ObservedObject var attachmentModel: AttachmentModel + @State var url: URL? + + var body: some View { + VStack(alignment: .center) { + ZStack { + if attachmentModel.loadStatus != .loading { + ThumbnailView( + attachmentModel: attachmentModel, + size: attachmentModel.usingDefaultImage ? + CGSize(width: 36, height: 36) : + CGSize(width: 120, height: 120) + ) + } else { + ProgressView() + .padding(8) + .background(Color.white.opacity(0.75)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + if attachmentModel.usingDefaultImage { + Text(attachmentModel.attachment.name) + .lineLimit(1) + .truncationMode(.middle) + .padding([.leading, .trailing], 4) + Text("\(attachmentModel.attachment.size.formatted(.byteCount(style: .file)))") + .foregroundColor(.secondary) + .padding([.leading, .trailing], 4) + } + } + .font(.caption) + .frame(width: 120, height: 120) + .background(Color.gray.opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .onTapGesture { + if attachmentModel.attachment.loadStatus == .loaded { + url = attachmentModel.attachment.fileURL + } + else if attachmentModel.attachment.loadStatus == .notLoaded { + attachmentModel.load(thumbnailSize: CGSize(width: 120, height: 120)) + } + } + .quickLookPreview($url) + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementModel.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift similarity index 85% rename from Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementModel.swift rename to Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift index 601764e2c..da1f55627 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementModel.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift @@ -15,21 +15,30 @@ import SwiftUI import ArcGIS import QuickLook -/// The view model for +/// The view model for an `AttachmentPopupElement`. @MainActor class AttachmentsPopupElementModel: ObservableObject { + /// The array of `AttachmentModels`, one for each popup attachment. @Published var attachmentModels = [AttachmentModel]() } /// A view model representing the combination of a `PopupAttachment` and /// an associated `UIImage` used as a thumbnail. @MainActor class AttachmentModel: ObservableObject { + /// The `PopupAttachment`. @Published var attachment: PopupAttachment + + /// The thumbnail representing the attachment. @Published var thumbnail: UIImage? { didSet { usingDefaultImage = false } } + + /// The `LoadStatus` of the popup attachment. @Published var loadStatus: LoadStatus = .notLoaded + + /// A Boolean value specifying whether the thumbnails is the default image + /// or an image generated from the popup attachment. var usingDefaultImage: Bool @Environment(\.displayScale) var displayScale @@ -48,8 +57,8 @@ import QuickLook usingDefaultImage = true } - - + /// Loads the popup attachment and generates a thumbnail image. + /// - Parameter thumbnailSize: The size for the generated thumbnail. func load(thumbnailSize: CGSize = CGSize(width: 40, height: 40)) { Task { loadStatus = .loading diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift new file mode 100644 index 000000000..ae41bd844 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift @@ -0,0 +1,38 @@ +// 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 + +struct ThumbnailView: View { + @ObservedObject var attachmentModel: AttachmentModel + var size: CGSize = CGSize(width: 36, height: 36) + + var body: some View { + if let image = attachmentModel.thumbnail { + Image(uiImage: image) + .resizable() + .renderingMode(attachmentModel.usingDefaultImage ? .template : .original) + .aspectRatio(contentMode: attachmentModel.usingDefaultImage ? .fit : .fill) + .frame(width: size.width, height: size.height, alignment: .center) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .contentShape(RoundedRectangle(cornerRadius: 4)) + .foregroundColor(foregroundColor(for: attachmentModel)) + } + } + + func foregroundColor(for attachmentModel: AttachmentModel) -> Color { + attachmentModel.loadStatus == .failed ? .red : + (attachmentModel.usingDefaultImage ? .accentColor : .primary) + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 7a130ff20..5a60dc983 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -48,7 +48,7 @@ struct AttachmentsPopupElementView: View { title: popupElement.title, description: popupElement.description ) - Spacer() + Divider() if loadingAttachments { ProgressView() .padding() @@ -59,8 +59,8 @@ struct AttachmentsPopupElementView: View { else { switch popupElement.displayType { case .list: -// AttachmentList(attachmentModels: viewModel.attachmentModels) - AttachmentPreview(attachmentModels: viewModel.attachmentModels) + AttachmentList(attachmentModels: viewModel.attachmentModels) +// AttachmentPreview(attachmentModels: viewModel.attachmentModels) case.preview: // AttachmentList(attachmentModels: viewModel.attachmentModels) AttachmentPreview(attachmentModels: viewModel.attachmentModels) @@ -84,166 +84,4 @@ struct AttachmentsPopupElementView: View { loadingAttachments = false } } - - struct AttachmentList: View { - var attachmentModels: [AttachmentModel] - var body: some View { - VStack(alignment: .leading, spacing: 6) { - ForEach(attachmentModels) { attachmentModel in - AttachmentRow(attachmentModel: attachmentModel) - if attachmentModel != attachmentModels.last { - Divider() - } - } - } - } - - struct AttachmentRow: View { - @ObservedObject var attachmentModel: AttachmentModel - @State var url: URL? - - var body: some View { - HStack { - HStack { - ThumbnailView(attachmentModel: attachmentModel) - .padding(2) - VStack(alignment: .leading) { - Text(attachmentModel.attachment.name) - .lineLimit(1) - .truncationMode(.middle) - Text("\(attachmentModel.attachment.size.formatted(.byteCount(style: .file)))") - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .onTapGesture { - if attachmentModel.attachment.loadStatus == .loaded { - url = attachmentModel.attachment.fileURL - } - } - if attachmentModel.loadStatus != .loaded { - AttachmentLoadButton(attachmentModel: attachmentModel) - } - } - .quickLookPreview($url) - } - } - } - - struct ThumbnailView: View { - @ObservedObject var attachmentModel: AttachmentModel - var size: CGSize = CGSize(width: 40, height: 40) - - var body: some View { - if let image = attachmentModel.thumbnail { - Image(uiImage: image) - .resizable() - .renderingMode(attachmentModel.usingDefaultImage ? .template : .original) - .aspectRatio(contentMode: attachmentModel.usingDefaultImage ? .fit : .fill) - .frame(width: size.width, height: size.height, alignment: .center) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .foregroundColor(foregroundColor(for: attachmentModel)) - } - } - - func foregroundColor(for attachmentModel: AttachmentModel) -> Color { - attachmentModel.loadStatus == .failed ? .red : - (attachmentModel.usingDefaultImage ? .accentColor : .primary) - } - } - - struct AttachmentLoadButton: View { - @ObservedObject var attachmentModel: AttachmentModel - - var body: some View { - Button { - if attachmentModel.loadStatus == .notLoaded { - attachmentModel.load() - } - else if attachmentModel.loadStatus == .failed { - // TODO: Show error alert, similar to BasemapGallery. - } - } label: { - Group { - switch attachmentModel.loadStatus { - case .notLoaded: - Image(systemName: "square.and.arrow.down") - .resizable() - .aspectRatio(contentMode: .fit) - case .loading: - ProgressView() - case .loaded: - EmptyView() - case .failed: - Image(systemName: "exclamationmark.circle.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.red) - .background(Color.clear) - } - } - .frame(width: 24, height: 24) - .padding(.leading) - } - } - } - - struct AttachmentPreview: View { - var attachmentModels: [AttachmentModel] - - var body: some View { - ScrollView(.horizontal) { - HStack(alignment: .top, spacing: 8) { - ForEach(attachmentModels) { attachmentModel in - AttachmentCell(attachmentModel: attachmentModel) - } - } - } - } - - struct AttachmentCell: View { - @ObservedObject var attachmentModel: AttachmentModel - @State var url: URL? - - var body: some View { - VStack(alignment: .center) { - ZStack { - if attachmentModel.loadStatus != .loading { - ThumbnailView( - attachmentModel: attachmentModel, - size: attachmentModel.usingDefaultImage ? - CGSize(width: 40, height: 40) : - CGSize(width: 120, height: 120) - ) - } else { - ProgressView() - .padding(8) - .background(Color.white.opacity(0.75)) - .clipShape(RoundedRectangle(cornerRadius: 8)) - } - } - if attachmentModel.usingDefaultImage { - Text(attachmentModel.attachment.name) - .lineLimit(1) - .truncationMode(.middle) - Text("\(attachmentModel.attachment.size.formatted(.byteCount(style: .file)))") - .foregroundColor(.secondary) - } - } - .font(.caption) - .frame(width: 120, height: 120) - .background(Color.gray.opacity(0.2)) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .onTapGesture { - if attachmentModel.attachment.loadStatus == .loaded { - url = attachmentModel.attachment.fileURL - } - else if attachmentModel.attachment.loadStatus == .notLoaded { - attachmentModel.load(thumbnailSize: CGSize(width: 120, height: 120)) - } - } - .quickLookPreview($url) - } - } - } } From 6cd9252ab50534cf9800046b531ff56df379b6ad Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Aug 2022 16:34:51 -0500 Subject: [PATCH 1507/1722] doc --- .../Popups/AttachmentsPopupElementView.swift | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 5a60dc983..fe8574c5e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -18,10 +18,13 @@ import QuickLook // TODO: look at notes and follow up // TODO: Add alert for when attachments fail to load... -// TODO: Move classes into separate file; maybe structs too. +/// A view displaying an `AttachmentsPopupElement`. struct AttachmentsPopupElementView: View { + /// The `PopupElement` to display. var popupElement: AttachmentsPopupElement + + /// The model for the view. @StateObject private var viewModel: AttachmentsPopupElementModel @Environment(\.horizontalSizeClass) var horizontalSizeClass @@ -32,8 +35,11 @@ struct AttachmentsPopupElementView: View { !(horizontalSizeClass == .compact && verticalSizeClass == .regular) } - @State var loadingAttachments = true + /// A Boolean value specifying whether the attachments are currently being loaded. + @State var loadingAttachments = false + /// Creates a new `AttachmentsPopupElementView`. + /// - Parameter popupElement: The `AttachmentsPopupElement`. init(popupElement: AttachmentsPopupElement) { self.popupElement = popupElement _viewModel = StateObject( @@ -41,47 +47,40 @@ struct AttachmentsPopupElementView: View { ) } - // TODO: Move VStack into final else so that we won't show anything if there are no attachments????? var body: some View { - VStack(alignment: .leading) { - PopupElementHeader( - title: popupElement.title, - description: popupElement.description - ) - Divider() - if loadingAttachments { - ProgressView() - .padding() - } else if popupElement.attachments.count == 0 { - Text("No attachments.") - .padding() - } - else { + if loadingAttachments { + ProgressView() + .padding() + } else if popupElement.attachments.count > 0 { + VStack(alignment: .leading) { + PopupElementHeader( + title: popupElement.title, + description: popupElement.description + ) + Divider() + switch popupElement.displayType { case .list: AttachmentList(attachmentModels: viewModel.attachmentModels) -// AttachmentPreview(attachmentModels: viewModel.attachmentModels) case.preview: -// AttachmentList(attachmentModels: viewModel.attachmentModels) AttachmentPreview(attachmentModels: viewModel.attachmentModels) case .auto: if isRegularWidth { -// AttachmentList(attachmentModels: viewModel.attachmentModels) AttachmentPreview(attachmentModels: viewModel.attachmentModels) } else { AttachmentList(attachmentModels: viewModel.attachmentModels) - AttachmentPreview(attachmentModels: viewModel.attachmentModels) } } } - } - .task { - try? await popupElement.fetchAttachments() - let attachmentModels = popupElement.attachments.map { attachment in - AttachmentModel(attachment: attachment) + .task { + loadingAttachments = true + try? await popupElement.fetchAttachments() + let attachmentModels = popupElement.attachments.map { attachment in + AttachmentModel(attachment: attachment) + } + viewModel.attachmentModels.append(contentsOf: attachmentModels) + loadingAttachments = false } - viewModel.attachmentModels.append(contentsOf: attachmentModels) - loadingAttachments = false } } } From 94b74f68a8a48957c502e415617a4caab8d3d433 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Aug 2022 16:39:21 -0500 Subject: [PATCH 1508/1722] .lineLimit(1) for footer text; even up padding around footer text. --- .../Components/Popups/PopupMedia/PopupMediaFooter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift index a4edd2eef..2b233aacc 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PopupMediaFooter.swift @@ -44,9 +44,10 @@ struct PopupMediaFooter: View { .foregroundColor(.init(white: 0.75)) } } + .lineLimit(1) Spacer() } - .padding([.leading], 20) + .padding([.leading, .trailing], 12) } } } From e85ac6c843fd6b8f0bf30b8b4fe9a66c75cdd232 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Aug 2022 18:07:28 -0500 Subject: [PATCH 1509/1722] redo "Divider()" responsibilities; don't show an attachment section if there are no attachments. --- .../Popups/AttachmentsPopupElementView.swift | 61 +++++++++---------- .../Popups/FieldsPopupElementView.swift | 1 + .../Components/Popups/PopupView.swift | 5 -- .../Popups/TextPopupElementView.swift | 3 +- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index fe8574c5e..8e66d568c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -15,10 +15,6 @@ import SwiftUI import ArcGIS import QuickLook -// TODO: look at notes and follow up - -// TODO: Add alert for when attachments fail to load... - /// A view displaying an `AttachmentsPopupElement`. struct AttachmentsPopupElementView: View { /// The `PopupElement` to display. @@ -48,39 +44,42 @@ struct AttachmentsPopupElementView: View { } var body: some View { - if loadingAttachments { - ProgressView() - .padding() - } else if popupElement.attachments.count > 0 { - VStack(alignment: .leading) { - PopupElementHeader( - title: popupElement.title, - description: popupElement.description - ) + Group { + if loadingAttachments { + ProgressView() + .padding() + } else if popupElement.attachments.count > 0 { Divider() - - switch popupElement.displayType { - case .list: - AttachmentList(attachmentModels: viewModel.attachmentModels) - case.preview: - AttachmentPreview(attachmentModels: viewModel.attachmentModels) - case .auto: - if isRegularWidth { - AttachmentPreview(attachmentModels: viewModel.attachmentModels) - } else { + VStack(alignment: .leading) { + PopupElementHeader( + title: popupElement.title, + description: popupElement.description + ) + Divider() + + switch popupElement.displayType { + case .list: AttachmentList(attachmentModels: viewModel.attachmentModels) + case.preview: + AttachmentPreview(attachmentModels: viewModel.attachmentModels) + case .auto: + if isRegularWidth { + AttachmentPreview(attachmentModels: viewModel.attachmentModels) + } else { + AttachmentList(attachmentModels: viewModel.attachmentModels) + } } } } - .task { - loadingAttachments = true - try? await popupElement.fetchAttachments() - let attachmentModels = popupElement.attachments.map { attachment in - AttachmentModel(attachment: attachment) - } - viewModel.attachmentModels.append(contentsOf: attachmentModels) - loadingAttachments = false + } + .task { + loadingAttachments = true + try? await popupElement.fetchAttachments() + let attachmentModels = popupElement.attachments.map { attachment in + AttachmentModel(attachment: attachment) } + viewModel.attachmentModels.append(contentsOf: attachmentModels) + loadingAttachments = false } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 2f03fb809..527092f7f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -32,6 +32,7 @@ struct FieldsPopupElementView: View { private let displayFields: [DisplayField] var body: some View { + Divider() PopupElementHeader( title: popupElement.displayTitle, description: popupElement.description diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index e8fbe495d..7e77f3a32 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -38,7 +38,6 @@ public struct PopupView: View { Text(popup.title) .font(.title) .fontWeight(.bold) - Divider() } Group { if let isPopupEvaluated = isPopupEvaluated { @@ -84,10 +83,6 @@ public struct PopupView: View { default: EmptyView() } - - if index < popup.evaluatedElements.count - 1 { - Divider() - } } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index bba1cbd2e..f0e850100 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -23,8 +23,9 @@ struct TextPopupElementView: View { @State private var webViewHeight: CGFloat = .zero var body: some View { - let roundedRect = RoundedRectangle(cornerRadius: 8) if !popupElement.text.isEmpty { + let roundedRect = RoundedRectangle(cornerRadius: 8) + Divider() ZStack { HTMLTextView(html: popupElement.text, height: $webViewHeight) .clipShape(roundedRect) From ff61f664fec26f3bfa60d02013456bf6d8e49a37 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Aug 2022 18:35:50 -0500 Subject: [PATCH 1510/1722] Doc & divider() fixes. --- .../Popups/Attachments/AttachmentList.swift | 12 +++++++++--- .../Popups/Attachments/AttachmentPreview.swift | 11 +++++++++-- .../Popups/Attachments/ThumbnailView.swift | 7 +++++++ .../Components/Popups/MediaPopupElementView.swift | 6 +++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift index 9afb4fae0..1cf6a6b92 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentList.swift @@ -14,8 +14,11 @@ import SwiftUI import ArcGIS +/// A view displaying a list of attachments, with a thumbnail, title, and download button. struct AttachmentList: View { + /// The attachment models displayed in the list. var attachmentModels: [AttachmentModel] + var body: some View { VStack(alignment: .leading, spacing: 6) { ForEach(attachmentModels) { attachmentModel in @@ -28,8 +31,12 @@ struct AttachmentList: View { } } +/// A view representing a single row in an `AttachmentList`. struct AttachmentRow: View { + /// The model representing the attachment to display. @ObservedObject var attachmentModel: AttachmentModel + + /// The url of the the attachment, used to display the attachment via `QuickLook`. @State var url: URL? var body: some View { @@ -48,6 +55,7 @@ struct AttachmentRow: View { } .onTapGesture { if attachmentModel.attachment.loadStatus == .loaded { + // Set the url to trigger `.quickLookPreview`. url = attachmentModel.attachment.fileURL } } @@ -59,6 +67,7 @@ struct AttachmentRow: View { } } +/// View displaying a button used to load an attachment. struct AttachmentLoadButton: View { @ObservedObject var attachmentModel: AttachmentModel @@ -67,9 +76,6 @@ struct AttachmentLoadButton: View { if attachmentModel.loadStatus == .notLoaded { attachmentModel.load() } - else if attachmentModel.loadStatus == .failed { - // TODO: Show error alert, similar to BasemapGallery. - } } label: { Group { switch attachmentModel.loadStatus { diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift index da26f030d..3050232c2 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift @@ -14,7 +14,9 @@ import SwiftUI import ArcGIS +/// A view displaying a list of attachments in a "carousel", with a thumbnail and title. struct AttachmentPreview: View { + /// The attachment models displayed in the list. var attachmentModels: [AttachmentModel] var body: some View { @@ -27,8 +29,12 @@ struct AttachmentPreview: View { } } + /// A view representing a single cell in an `AttachmentPreview`. struct AttachmentCell: View { + /// The model representing the attachment to display. @ObservedObject var attachmentModel: AttachmentModel + + /// The url of the the attachment, used to display the attachment via `QuickLook`. @State var url: URL? var body: some View { @@ -64,9 +70,10 @@ struct AttachmentPreview: View { .clipShape(RoundedRectangle(cornerRadius: 8)) .onTapGesture { if attachmentModel.attachment.loadStatus == .loaded { + // Set the url to trigger `.quickLookPreview`. url = attachmentModel.attachment.fileURL - } - else if attachmentModel.attachment.loadStatus == .notLoaded { + } else if attachmentModel.attachment.loadStatus == .notLoaded { + // Load the attachment model with the given size. attachmentModel.load(thumbnailSize: CGSize(width: 120, height: 120)) } } diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift index ae41bd844..555fc7cbf 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift @@ -14,8 +14,12 @@ import SwiftUI import ArcGIS +/// A view displaying a thumbnail image for an attachment. struct ThumbnailView: View { + /// The model represented by the thumbnail. @ObservedObject var attachmentModel: AttachmentModel + + /// The display size of the thumbnail. var size: CGSize = CGSize(width: 36, height: 36) var body: some View { @@ -31,6 +35,9 @@ struct ThumbnailView: View { } } + /// The foreground color of the thumbnail image. + /// - Parameter attachmentModel: The model for the associated attachment. + /// - Returns: A color to be used as the foreground color. func foregroundColor(for attachmentModel: AttachmentModel) -> Color { attachmentModel.loadStatus == .failed ? .red : (attachmentModel.usingDefaultImage ? .accentColor : .primary) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 6d73db0a4..da726481f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -20,12 +20,16 @@ struct MediaPopupElementView: View { var popupElement: MediaPopupElement var body: some View { + Divider() PopupElementHeader( title: popupElement.title, description: popupElement.description ) .padding([.bottom], 4) - Divider() + if !popupElement.title.isEmpty || + !popupElement.description.isEmpty { + Divider() + } PopupMediaView(popupMedia: popupElement.media) } From 79e6039a6d2c6fbd8453ae3ab750cd2e98859514 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Aug 2022 18:49:41 -0500 Subject: [PATCH 1511/1722] Fix loading. --- .../Components/Popups/AttachmentsPopupElementView.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 8e66d568c..ddf928416 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -32,7 +32,7 @@ struct AttachmentsPopupElementView: View { } /// A Boolean value specifying whether the attachments are currently being loaded. - @State var loadingAttachments = false + @State var loadingAttachments = true /// Creates a new `AttachmentsPopupElementView`. /// - Parameter popupElement: The `AttachmentsPopupElement`. @@ -48,7 +48,7 @@ struct AttachmentsPopupElementView: View { if loadingAttachments { ProgressView() .padding() - } else if popupElement.attachments.count > 0 { + } else if viewModel.attachmentModels.count > 0 { Divider() VStack(alignment: .leading) { PopupElementHeader( @@ -73,7 +73,6 @@ struct AttachmentsPopupElementView: View { } } .task { - loadingAttachments = true try? await popupElement.fetchAttachments() let attachmentModels = popupElement.attachments.map { attachment in AttachmentModel(attachment: attachment) From 41c9bfad386caddc7dc0fec286b7dd4680d6976c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Aug 2022 12:13:53 -0500 Subject: [PATCH 1512/1722] fix typo. --- .../Components/Popups/AttachmentsPopupElementView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index ddf928416..ff4b7c01b 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -60,7 +60,7 @@ struct AttachmentsPopupElementView: View { switch popupElement.displayType { case .list: AttachmentList(attachmentModels: viewModel.attachmentModels) - case.preview: + case .preview: AttachmentPreview(attachmentModels: viewModel.attachmentModels) case .auto: if isRegularWidth { From f02150303de8a9ba06cc755a342b9627be22672b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Aug 2022 14:16:55 -0500 Subject: [PATCH 1513/1722] Fixes mis-proportioned system SFSymbols images. Uses `Image(systemName:)` instead of `Image(uiImage:)` for those images. --- .../AttachmentsPopupElementModel.swift | 18 ++++++------ .../Popups/Attachments/ThumbnailView.swift | 28 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift index da1f55627..6de774b44 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift @@ -30,16 +30,20 @@ import QuickLook /// The thumbnail representing the attachment. @Published var thumbnail: UIImage? { didSet { - usingDefaultImage = false + defaultSystemName = nil } } + @Published var defaultSystemName: String? + /// The `LoadStatus` of the popup attachment. @Published var loadStatus: LoadStatus = .notLoaded /// A Boolean value specifying whether the thumbnails is the default image /// or an image generated from the popup attachment. - var usingDefaultImage: Bool + var usingDefaultImage: Bool { + defaultSystemName != nil + } @Environment(\.displayScale) var displayScale @@ -48,13 +52,12 @@ import QuickLook switch attachment.kind { case .image: - thumbnail = UIImage(systemName: "photo") + defaultSystemName = "photo" case .video: - thumbnail = UIImage(systemName: "film") + defaultSystemName = "film" case .document, .other: - thumbnail = UIImage(systemName: "doc") + defaultSystemName = "doc" } - usingDefaultImage = true } /// Loads the popup attachment and generates a thumbnail image. @@ -78,8 +81,7 @@ import QuickLook self.thumbnail = thumbnail.uiImage } else if self.attachment.loadStatus == .failed { - self.thumbnail = UIImage(systemName: "exclamationmark.circle.fill") - self.usingDefaultImage = true + self.defaultSystemName = "exclamationmark.circle.fill" } self.loadStatus = self.attachment.loadStatus diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift index 555fc7cbf..da3aab093 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift @@ -23,16 +23,24 @@ struct ThumbnailView: View { var size: CGSize = CGSize(width: 36, height: 36) var body: some View { - if let image = attachmentModel.thumbnail { - Image(uiImage: image) - .resizable() - .renderingMode(attachmentModel.usingDefaultImage ? .template : .original) - .aspectRatio(contentMode: attachmentModel.usingDefaultImage ? .fit : .fill) - .frame(width: size.width, height: size.height, alignment: .center) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .contentShape(RoundedRectangle(cornerRadius: 4)) - .foregroundColor(foregroundColor(for: attachmentModel)) + Group { + if attachmentModel.usingDefaultImage, + let systemName = attachmentModel.defaultSystemName { + Image(systemName: systemName) + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + } else if let image = attachmentModel.thumbnail { + Image(uiImage: image) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fill) + } } + .frame(width: size.width, height: size.height, alignment: .center) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .contentShape(RoundedRectangle(cornerRadius: 4)) + .foregroundColor(foregroundColor(for: attachmentModel)) } /// The foreground color of the thumbnail image. @@ -40,6 +48,6 @@ struct ThumbnailView: View { /// - Returns: A color to be used as the foreground color. func foregroundColor(for attachmentModel: AttachmentModel) -> Color { attachmentModel.loadStatus == .failed ? .red : - (attachmentModel.usingDefaultImage ? .accentColor : .primary) + (attachmentModel.usingDefaultImage ? .gray : .primary) } } From f525d39e8d0f1a19a282ae7b67bd1d8562f87ca1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Aug 2022 14:18:27 -0500 Subject: [PATCH 1514/1722] Doc. --- .../Popups/Attachments/AttachmentsPopupElementModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift index 6de774b44..6a6e970e3 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentsPopupElementModel.swift @@ -34,6 +34,7 @@ import QuickLook } } + /// The name of the system SF symbol used instead of `thumbnail`. @Published var defaultSystemName: String? /// The `LoadStatus` of the popup attachment. From 5286b454986130399ce50ec117d38cfada7fd0cd Mon Sep 17 00:00:00 2001 From: Nimesh Jarecha Date: Wed, 31 Aug 2022 12:31:57 -0700 Subject: [PATCH 1515/1722] changes for making the handleArcGISAuthenticationChallenge a throwing function --- .../Authentication/Authenticator.swift | 6 ++--- .../ChallengeContinuation.swift | 2 +- .../OAuthChallengeContinuation.swift | 24 +++++++++---------- .../TokenChallengeContinuation.swift | 13 ++++------ .../LoginViewModelTests.swift | 2 -- .../TokenChallengeContinuationTests.swift | 4 ++-- 6 files changed, 22 insertions(+), 29 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 525e85380..82e4df8d3 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -87,7 +87,7 @@ public final class Authenticator: ObservableObject { extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISAuthenticationChallenge( _ challenge: ArcGISAuthenticationChallenge - ) async -> ArcGISAuthenticationChallenge.Disposition { + ) async throws -> ArcGISAuthenticationChallenge.Disposition { let challengeContinuation: ArcGISChallengeContinuation // Create the correct challenge type. @@ -95,7 +95,7 @@ extension Authenticator: AuthenticationChallengeHandler { let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { let oAuthChallenge = OAuthChallengeContinuation(configuration: config) challengeContinuation = oAuthChallenge - oAuthChallenge.presentPrompt() + await oAuthChallenge.presentPrompt() } else { challengeContinuation = TokenChallengeContinuation(arcGISChallenge: challenge) } @@ -108,7 +108,7 @@ extension Authenticator: AuthenticationChallengeHandler { defer { self.currentChallenge = nil } // Wait for it to complete and return the resulting disposition. - return await challengeContinuation.value + return try await challengeContinuation.value.get() } public func handleNetworkAuthenticationChallenge( diff --git a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift index b6cc27eea..189d18970 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift @@ -18,4 +18,4 @@ import ArcGIS protocol ChallengeContinuation: AnyObject {} /// A type that represents a continuation for an ArcGIS challenge. -protocol ArcGISChallengeContinuation: ValueContinuation, ChallengeContinuation {} +protocol ArcGISChallengeContinuation: ValueContinuation>, ChallengeContinuation {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift index e1d4181de..584099b3c 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents an ArcGIS OAuth challenge continuation. @MainActor -final class OAuthChallengeContinuation: ValueContinuation, ArcGISChallengeContinuation { +final class OAuthChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { /// The OAuth configuration to be used for this challenge. let configuration: OAuthConfiguration @@ -28,18 +28,16 @@ final class OAuthChallengeContinuation: ValueContinuation, ArcGISChallengeContinuation { +final class TokenChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { /// The host that prompted the challenge. let host: String @@ -52,17 +52,14 @@ final class TokenChallengeContinuation: ValueContinuation Date: Wed, 31 Aug 2022 18:52:24 -0600 Subject: [PATCH 1516/1722] change where cancellation recognition occurs --- .../AuthenticationExample/SignInView.swift | 7 +++++++ .../Components/Authentication/Authenticator.swift | 6 +++--- .../OAuthChallengeContinuation.swift | 14 +++----------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/AuthenticationExample/AuthenticationExample/SignInView.swift b/AuthenticationExample/AuthenticationExample/SignInView.swift index 7f94a019b..d6258db7f 100644 --- a/AuthenticationExample/AuthenticationExample/SignInView.swift +++ b/AuthenticationExample/AuthenticationExample/SignInView.swift @@ -156,6 +156,13 @@ private extension Error { default: return false } + case let error as OAuthCredential.AuthorizationError: + switch error { + case .userCancelled: + return true + default: + return false + } case let error as NSError: return error.domain == NSURLErrorDomain && error.code == -999 default: diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 82e4df8d3..99e382680 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -90,6 +90,9 @@ extension Authenticator: AuthenticationChallengeHandler { ) async throws -> ArcGISAuthenticationChallenge.Disposition { let challengeContinuation: ArcGISChallengeContinuation + // Alleviates an error with "already presenting". + await Task.yield() + // Create the correct challenge type. if let url = challenge.request.url, let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { @@ -100,9 +103,6 @@ extension Authenticator: AuthenticationChallengeHandler { challengeContinuation = TokenChallengeContinuation(arcGISChallenge: challenge) } - // Alleviates an error with "already presenting". - await Task.yield() - // Set the current challenge, which will present the UX. self.currentChallenge = challengeContinuation defer { self.currentChallenge = nil } diff --git a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift index 584099b3c..9a52e5ac8 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift @@ -29,16 +29,8 @@ final class OAuthChallengeContinuation: ValueContinuation Date: Wed, 31 Aug 2022 19:05:31 -0600 Subject: [PATCH 1517/1722] simplify oauth challenge --- .../Authentication/Authenticator.swift | 25 ++++++------- .../AuthenticatorModifier.swift | 4 --- .../ChallengeContinuation.swift | 3 -- .../OAuthChallengeContinuation.swift | 36 ------------------- .../TokenChallengeContinuation.swift | 2 +- .../OAuthChallengeContinuationTests.swift | 34 ------------------ 6 files changed, 12 insertions(+), 92 deletions(-) delete mode 100644 Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift delete mode 100644 Tests/ArcGISToolkitTests/OAuthChallengeContinuationTests.swift diff --git a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift index 99e382680..0fc87ac11 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/Authenticator.swift @@ -81,6 +81,7 @@ public final class Authenticator: ObservableObject { } /// The current challenge. + /// This property is not set for OAuth challenges. @Published var currentChallenge: ChallengeContinuation? } @@ -88,27 +89,23 @@ extension Authenticator: AuthenticationChallengeHandler { public func handleArcGISAuthenticationChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { - let challengeContinuation: ArcGISChallengeContinuation - // Alleviates an error with "already presenting". await Task.yield() // Create the correct challenge type. if let url = challenge.request.url, - let config = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { - let oAuthChallenge = OAuthChallengeContinuation(configuration: config) - challengeContinuation = oAuthChallenge - await oAuthChallenge.presentPrompt() + let configuration = oAuthConfigurations.first(where: { $0.canBeUsed(for: url) }) { + return .useCredential(try await ArcGISCredential.oauth(configuration: configuration)) } else { - challengeContinuation = TokenChallengeContinuation(arcGISChallenge: challenge) + let tokenChallengeContinuation = TokenChallengeContinuation(arcGISChallenge: challenge) + + // Set the current challenge, which will present the UX. + self.currentChallenge = tokenChallengeContinuation + defer { self.currentChallenge = nil } + + // Wait for it to complete and return the resulting disposition. + return try await tokenChallengeContinuation.value.get() } - - // Set the current challenge, which will present the UX. - self.currentChallenge = challengeContinuation - defer { self.currentChallenge = nil } - - // Wait for it to complete and return the resulting disposition. - return try await challengeContinuation.value.get() } public func handleNetworkAuthenticationChallenge( diff --git a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift index 74f8594d7..2de80b6b2 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/AuthenticatorModifier.swift @@ -47,10 +47,6 @@ private struct AuthenticatorModifier: ViewModifier { @ViewBuilder func body(content: Content) -> some View { switch authenticator.currentChallenge { - case is OAuthChallengeContinuation: - // OAuth is handled by the authenticator itself - // so just return the unmodified content here. - content case let challenge as TokenChallengeContinuation: content.modifier(LoginViewModifier(challenge: challenge)) case let challenge as NetworkChallengeContinuation: diff --git a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift index 189d18970..7583b5e99 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/ChallengeContinuation.swift @@ -16,6 +16,3 @@ import ArcGIS /// A type that represents an authentication challenge that can be continued. protocol ChallengeContinuation: AnyObject {} - -/// A type that represents a continuation for an ArcGIS challenge. -protocol ArcGISChallengeContinuation: ValueContinuation>, ChallengeContinuation {} diff --git a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift deleted file mode 100644 index 9a52e5ac8..000000000 --- a/Sources/ArcGISToolkit/Components/Authentication/OAuthChallengeContinuation.swift +++ /dev/null @@ -1,36 +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 Foundation -import ArcGIS - -/// An object that represents an ArcGIS OAuth challenge continuation. -@MainActor -final class OAuthChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { - /// The OAuth configuration to be used for this challenge. - let configuration: OAuthConfiguration - - /// Creates a `OAuthChallengeContinuation`. - /// - Parameter configuration: The OAuth configuration to be used for this challenge. - init(configuration: OAuthConfiguration) { - self.configuration = configuration - } - - /// Presents the user with a prompt to login with OAuth. This is done by creating the OAuth - /// credential. - func presentPrompt() async { - await setValue( - Result { .useCredential(try await ArcGISCredential.oauth(configuration: configuration)) } - ) - } -} diff --git a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift index 592aa99d9..d6340ab8a 100644 --- a/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift +++ b/Sources/ArcGISToolkit/Components/Authentication/TokenChallengeContinuation.swift @@ -16,7 +16,7 @@ import ArcGIS /// An object that represents an ArcGIS token authentication challenge continuation. @MainActor -final class TokenChallengeContinuation: ValueContinuation>, ArcGISChallengeContinuation { +final class TokenChallengeContinuation: ValueContinuation>, ChallengeContinuation { /// The host that prompted the challenge. let host: String diff --git a/Tests/ArcGISToolkitTests/OAuthChallengeContinuationTests.swift b/Tests/ArcGISToolkitTests/OAuthChallengeContinuationTests.swift deleted file mode 100644 index 21994eded..000000000 --- a/Tests/ArcGISToolkitTests/OAuthChallengeContinuationTests.swift +++ /dev/null @@ -1,34 +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 -import XCTest -@testable import ArcGISToolkit -import ArcGIS - -@MainActor final class OAuthChallengeContinuationTests: XCTestCase { - func testInit() { - let portalURL = URL(string: "www.test-portal.com")! - let clientID = "client id" - let redirectURL = URL(string: "test-app://redirect")! - - let config = OAuthConfiguration( - portalURL: portalURL, - clientID: clientID, - redirectURL: redirectURL - ) - - let challenge = OAuthChallengeContinuation(configuration: config) - XCTAssertEqual(challenge.configuration, config) - } -} From f674099870e87fe0cc0a326d79496b3d414c9386 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 1 Sep 2022 09:19:40 -0500 Subject: [PATCH 1518/1722] initial chart implementation. --- .../Popups/AttachmentsPopupElementView.swift | 1 - .../Popups/MediaPopupElementView.swift | 10 ++- .../Popups/PopupElementHeader.swift | 6 ++ .../Popups/PopupMedia/BarChart.swift | 39 +++++++++++ .../Popups/PopupMedia/ChartData.swift | 48 ++++++++++++++ .../Popups/PopupMedia/ChartMediaView.swift | 64 ++++++++++++++++--- .../Popups/PopupMedia/ImageMediaView.swift | 5 +- .../Popups/PopupMedia/LineChart.swift | 43 +++++++++++++ ...lImageView.swift => MediaDetailView.swift} | 45 +++++++------ .../Popups/PopupMedia/PieChart.swift | 25 ++++++++ 10 files changed, 249 insertions(+), 37 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartData.swift create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{DetailImageView.swift => MediaDetailView.swift} (55%) create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index ff4b7c01b..f892ae330 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -55,7 +55,6 @@ struct AttachmentsPopupElementView: View { title: popupElement.title, description: popupElement.description ) - Divider() switch popupElement.displayType { case .list: diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index da726481f..473e8d787 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -25,11 +25,6 @@ struct MediaPopupElementView: View { title: popupElement.title, description: popupElement.description ) - .padding([.bottom], 4) - if !popupElement.title.isEmpty || - !popupElement.description.isEmpty { - Divider() - } PopupMediaView(popupMedia: popupElement.media) } @@ -53,7 +48,10 @@ struct MediaPopupElementView: View { mediaSize: mediaSize ) case .barChart, .columnChart, .lineChart, .pieChart: - ChartMediaView(popupMedia: media) + ChartMediaView( + popupMedia: media, + mediaSize: mediaSize + ) default: EmptyView() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index c0dede808..b6ddcdaf2 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -35,5 +35,11 @@ struct PopupElementHeader: View { .foregroundColor(.secondary) } } + .padding([.bottom], 4) + + if !title.isEmpty || + !description.isEmpty { + Divider() + } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift new file mode 100644 index 000000000..04c8ad3fe --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift @@ -0,0 +1,39 @@ +// 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 Charts + +/// A view displaying details for popup media. +struct BarChart: View { + /// The chart data to display. + let chartData: [ChartData] + + var body: some View { + if #available(iOS 16, *) { + Group { + Chart(chartData) { + BarMark( + x: .value("Field", $0.label), + y: .value("Value", $0.value) + ) + } + .chartXAxis { + AxisMarks { _ in + AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) + } + } + } + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartData.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartData.swift new file mode 100644 index 000000000..475ebc130 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartData.swift @@ -0,0 +1,48 @@ +// 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 + +internal struct ChartData: Identifiable { + var label: String + var value: Double + var id = UUID() + + 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 { + self.value = Double(int) + } else if let float = value as? Float { + self.value = Double(float) + } else if let double = value as? Double { + self.value = double + } else { + self.value = 0 + } + } + + static func getChartData(popupMedia: PopupMedia) -> [ChartData] { + guard let labels = popupMedia.value?.labels, + let data = popupMedia.value?.data else { return [] } + + let chartRawData = zip(labels, data).map { ($0, $1) } + let chartData = chartRawData.map { + ChartData(label: $0.0, value: $0.1) + } + + return chartData + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift index 993fb324f..9704fbb72 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift @@ -19,17 +19,65 @@ struct ChartMediaView: View { /// The popup media to display. let popupMedia: PopupMedia + /// The size of the media's frame. + let mediaSize: CGSize + + let chartData: [ChartData] + + init(popupMedia: PopupMedia, mediaSize: CGSize) { + self.popupMedia = popupMedia + self.mediaSize = mediaSize + self.chartData = ChartData.getChartData(popupMedia: popupMedia) + } + + /// A Boolean value specifying whether the media should be shown full screen. + @State private var showingFullScreen = false + + private let cornerRadius: CGFloat = 8 + var body: some View { ZStack { - VStack(alignment: .center) { - Image(systemName: "chart.bar") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - Text("Charts coming soon!") + ChartView(popupMedia: popupMedia, data: chartData) + VStack { + Spacer() + PopupMediaFooter( + popupMedia: popupMedia, + mediaSize: mediaSize + ) } - RoundedRectangle(cornerRadius: 8) - .stroke(.black, lineWidth: 1) + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(.gray, lineWidth: 1) + .frame(width: mediaSize.width, height: mediaSize.height) + } + .frame(width: mediaSize.width, height: mediaSize.height) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .onTapGesture { + showingFullScreen = true + } + .sheet(isPresented: $showingFullScreen) { + MediaDetailView( + popupMedia: popupMedia, + showingFullScreen: $showingFullScreen + ) + .padding() + } + } +} + +struct ChartView: View { + let popupMedia: PopupMedia + let data: [ChartData] + + var body: some View { + switch popupMedia.kind { + case .barChart, .columnChart: + BarChart(chartData: data) + case .pieChart: + PieChart(chartData: data) + case .lineChart: + LineChart(chartData: data) + default: + EmptyView() } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 2986be6d2..2bba3d563 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -49,10 +49,9 @@ struct ImageMediaView: View { .frame(width: mediaSize.width, height: mediaSize.height) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .sheet(isPresented: $showingFullScreen) { - if let url = popupMedia.value?.sourceURL { - DetailImageView( + if popupMedia.value?.sourceURL != nil { + MediaDetailView( popupMedia: popupMedia, - sourceURL: url, showingFullScreen: $showingFullScreen ) .padding() diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift new file mode 100644 index 000000000..bab23d0b3 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift @@ -0,0 +1,43 @@ +// 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 Charts + +/// A view displaying details for popup media. +struct LineChart: View { + /// The chart data to display. + let chartData: [ChartData] + + var body: some View { + if #available(iOS 16, *) { + Group { + Chart(chartData) { + LineMark( + x: .value("Field", $0.label), + y: .value("Value", $0.value) + ) + PointMark( + x: .value("Field", $0.label), + y: .value("Value", $0.value) + ) + } + .chartXAxis { + AxisMarks { _ in + AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) + } + } + } + } + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/DetailImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift similarity index 55% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/DetailImageView.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift index 8ee2d79fe..114efae49 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/DetailImageView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift @@ -14,23 +14,20 @@ import SwiftUI import ArcGIS -/// A view displaying a popup media image in full screen. -struct DetailImageView: View { +/// A view displaying a popup media in full screen. +struct MediaDetailView : View { /// The popup media to display. let popupMedia: PopupMedia - - /// The sourceURL of the image media. - let sourceURL: URL - + /// A Boolean value specifying whether the media should be shown full screen. - @Binding var showingFullScreen: Bool + var showingFullScreen: Binding var body: some View { VStack { HStack { Spacer() Button { - showingFullScreen = false + showingFullScreen.wrappedValue = false } label: { Text("Done") .fontWeight(.semibold) @@ -47,19 +44,29 @@ struct DetailImageView: View { } Spacer() } - AsyncImageView(url: sourceURL) - .onTapGesture { - if let url = popupMedia.value?.linkURL { - UIApplication.shared.open(url) + + switch popupMedia.kind { + case .image: + if let sourceURL = popupMedia.value?.sourceURL { + AsyncImageView(url: sourceURL) + .onTapGesture { + if let linkURL = popupMedia.value?.linkURL { + UIApplication.shared.open(linkURL) + } + } + if popupMedia.value?.linkURL != nil { + HStack { + Text("Tap on the image for more information.") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + } } } - if popupMedia.value?.linkURL != nil { - HStack { - Text("Tap on the image for more information.") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - } + case .barChart, .columnChart, .pieChart, .lineChart: + ChartView(popupMedia: popupMedia, data: ChartData.getChartData(popupMedia: popupMedia)) + default: + EmptyView() } Spacer() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift new file mode 100644 index 000000000..2d7d9757d --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift @@ -0,0 +1,25 @@ +// 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 Charts + +/// A view displaying details for popup media. +struct PieChart: View { + /// The chart data to display. + let chartData: [ChartData] + + var body: some View { + Text("Pie Chart") + } +} From daabeace0f1c953458f7f039214d8019a6513b15 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Sep 2022 12:12:44 -0500 Subject: [PATCH 1519/1722] Pie chart implementation with legend and full screen. --- .../Popups/PopupMedia/ChartMediaView.swift | 9 +- .../Popups/PopupMedia/LineChart.swift | 1 + .../Popups/PopupMedia/MediaDetailView.swift | 6 +- .../Popups/PopupMedia/PieChart.swift | 73 +++++++++++++++- .../Popups/PopupMedia/PieChartModel.swift | 86 +++++++++++++++++++ 5 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift index 9704fbb72..907da2ac9 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift @@ -67,13 +67,20 @@ struct ChartMediaView: View { struct ChartView: View { let popupMedia: PopupMedia let data: [ChartData] + let isFullScreen: Bool + + init(popupMedia: PopupMedia, data: [ChartData], isFullScreen: Bool = false) { + self.popupMedia = popupMedia + self.data = data + self.isFullScreen = isFullScreen + } var body: some View { switch popupMedia.kind { case .barChart, .columnChart: BarChart(chartData: data) case .pieChart: - PieChart(chartData: data) + PieChart(chartData: data, showLegend: isFullScreen) case .lineChart: LineChart(chartData: data) default: diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift index bab23d0b3..9a28cb51e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift @@ -35,6 +35,7 @@ struct LineChart: View { .chartXAxis { AxisMarks { _ in AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) + AxisGridLine() } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift index 114efae49..38274771b 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift @@ -64,7 +64,11 @@ struct MediaDetailView : View { } } case .barChart, .columnChart, .pieChart, .lineChart: - ChartView(popupMedia: popupMedia, data: ChartData.getChartData(popupMedia: popupMedia)) + ChartView( + popupMedia: popupMedia, + data: ChartData.getChartData(popupMedia: popupMedia), + isFullScreen: true + ) default: EmptyView() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift index 2d7d9757d..be2e4fe08 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift @@ -18,8 +18,79 @@ import Charts struct PieChart: View { /// The chart data to display. let chartData: [ChartData] + let showLegend: Bool + + @ObservedObject private var viewModel: PieChartModel + + init(chartData: [ChartData], showLegend: Bool = false) { + self.chartData = chartData + self.showLegend = showLegend + _viewModel = ObservedObject(wrappedValue: PieChartModel(chartData: chartData)) + } + + var body: some View { + Pie(viewModel: viewModel) + .padding() + if showLegend { + makeLegend(slices: viewModel.pieSlices) + } + } + + func makeLegend(slices: [PieSlice]) -> some View { + LazyVGrid( + columns: Array( + repeating: GridItem(.flexible(), alignment: .top), + count: 3 + ), + alignment: .leading + ) { + ForEach(slices) { slice in + HStack { + Rectangle() + .stroke(.gray, lineWidth: 1) + .frame(width: 20, height: 20) + .background(slice.color) + Text(slice.name) + Spacer() + } + } + } + } +} + +struct Pie: View { + @ObservedObject private var viewModel: PieChartModel + init(viewModel: PieChartModel) { + self.viewModel = viewModel + } + var body: some View { - Text("Pie Chart") + GeometryReader { geometry in + let radius = min(geometry.size.width, geometry.size.height) / 2.0 + let center = CGPoint( + x: geometry.size.width / 2.0, + y: geometry.size.height / 2.0 + ) + var startAngle: Double = -90 + ForEach(viewModel.pieSlices, id: \.self) { slice in + let endAngle = startAngle + slice.fraction * 360.0 + let path = Path { pieChart in + pieChart.move(to: center) + pieChart.addArc( + center: center, + radius: radius, + startAngle: .degrees(startAngle), + endAngle: .degrees(endAngle), + clockwise: false + ) + + pieChart.closeSubpath() + startAngle = endAngle + } + path.fill(slice.color) + path.stroke(.gray, lineWidth: 1) + } + } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift new file mode 100644 index 000000000..358e9f750 --- /dev/null +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift @@ -0,0 +1,86 @@ +// 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 + +final class PieChartModel: ObservableObject { + let pieSlices: [PieSlice] + + init(chartData: [ChartData]) { + var slices = [PieSlice]() + var dataTotal: Double = 0 + _ = chartData.map { dataTotal += $0.value } + for i in 0.. Color { + // We don't want to just wrap color indices because we don't want + // two adjacent slices to have the same color. "extra" will skip the + // the first color for the 2nd time through the list, skip the second + // color the second time through the list, etc. + let extra = index / sliceColors.count + return sliceColors[(index + extra) % sliceColors.count].opacity(0.75) + } +} + +class PieSlice: Identifiable { + let fraction: Double + let color: Color + let name: String + + init(fraction: Double, color: Color, name: String) { + self.fraction = fraction + self.color = color + self.name = name + } +} + +extension PieSlice: Equatable { + static func == (lhs: PieSlice, rhs: PieSlice) -> Bool { + lhs.fraction == rhs.fraction && + lhs.color == rhs.color && + lhs.name == rhs.name + } +} + +extension PieSlice: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(self.fraction) + hasher.combine(self.color) + hasher.combine(self.name) + } +} From 7eea977e2273881cc2e8faf878e5ce953e8f80bd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Sep 2022 13:24:43 -0500 Subject: [PATCH 1520/1722] legend for Pie Charts --- .../Popups/PopupMedia/PieChart.swift | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift index be2e4fe08..33c38b15f 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift @@ -16,14 +16,19 @@ import Charts /// A view displaying details for popup media. struct PieChart: View { - /// The chart data to display. - let chartData: [ChartData] + @ObservedObject private var viewModel: PieChartModel + let showLegend: Bool - @ObservedObject private var viewModel: PieChartModel + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass + /// A Boolean value denoting if the view should be shown as regular width. + var isRegularWidth: Bool { + !(horizontalSizeClass == .compact && verticalSizeClass == .regular) + } + init(chartData: [ChartData], showLegend: Bool = false) { - self.chartData = chartData self.showLegend = showLegend _viewModel = ObservedObject(wrappedValue: PieChartModel(chartData: chartData)) } @@ -36,13 +41,12 @@ struct PieChart: View { } } - func makeLegend(slices: [PieSlice]) -> some View { + @ViewBuilder func makeLegend(slices: [PieSlice]) -> some View { LazyVGrid( columns: Array( repeating: GridItem(.flexible(), alignment: .top), - count: 3 - ), - alignment: .leading + count: isRegularWidth ? 3 : 2 + ) ) { ForEach(slices) { slice in HStack { @@ -51,6 +55,7 @@ struct PieChart: View { .frame(width: 20, height: 20) .background(slice.color) Text(slice.name) + .font(.footnote) Spacer() } } From 4ee3300f764ed84eac60a77f26634652fd44c9e6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Sep 2022 15:59:45 -0500 Subject: [PATCH 1521/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Components/Popups/Attachments/AttachmentPreview.swift | 3 +-- .../Components/Popups/AttachmentsPopupElementView.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift index 3050232c2..2f174abc9 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/AttachmentPreview.swift @@ -50,8 +50,7 @@ struct AttachmentPreview: View { } else { ProgressView() .padding(8) - .background(Color.white.opacity(0.75)) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .background(Material.thin, in: RoundedRectangle(cornerRadius: 8)) } } if attachmentModel.usingDefaultImage { diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index ff4b7c01b..1b2aebea5 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -32,7 +32,7 @@ struct AttachmentsPopupElementView: View { } /// A Boolean value specifying whether the attachments are currently being loaded. - @State var loadingAttachments = true + @State var isLoadingAttachments = true /// Creates a new `AttachmentsPopupElementView`. /// - Parameter popupElement: The `AttachmentsPopupElement`. From 068aa9479181706e7501c3ff8268aadd82c15140 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Sep 2022 16:15:27 -0500 Subject: [PATCH 1522/1722] Fix build. --- .../Components/Popups/AttachmentsPopupElementView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 1b2aebea5..c4878baa2 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -45,7 +45,7 @@ struct AttachmentsPopupElementView: View { var body: some View { Group { - if loadingAttachments { + if isLoadingAttachments { ProgressView() .padding() } else if viewModel.attachmentModels.count > 0 { @@ -78,7 +78,7 @@ struct AttachmentsPopupElementView: View { AttachmentModel(attachment: attachment) } viewModel.attachmentModels.append(contentsOf: attachmentModels) - loadingAttachments = false + isLoadingAttachments = false } } } From ea87d2e1b5d62900c19036bb03a4375dd2134c04 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Sep 2022 17:07:29 -0500 Subject: [PATCH 1523/1722] `.light` for system icons. --- .../Popups/Attachments/ThumbnailView.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift index da3aab093..52cf98510 100644 --- a/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/Attachments/ThumbnailView.swift @@ -26,10 +26,18 @@ struct ThumbnailView: View { Group { if attachmentModel.usingDefaultImage, let systemName = attachmentModel.defaultSystemName { - Image(systemName: systemName) - .resizable() - .renderingMode(.template) - .aspectRatio(contentMode: .fit) + if #available(iOS 16, *) { + Image(systemName: systemName) + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + .fontWeight(.light) + } else { + Image(systemName: systemName) + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + } } else if let image = attachmentModel.thumbnail { Image(uiImage: image) .resizable() From 3f5e98a0344744ea9c94677cc8271be006a1a93c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 6 Sep 2022 15:28:02 -0500 Subject: [PATCH 1524/1722] Handle `.columnChart` --- .../Popups/PopupMedia/BarChart.swift | 18 ++++++++++++++---- .../Popups/PopupMedia/ChartMediaView.swift | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift index 04c8ad3fe..7ca110e36 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift @@ -19,14 +19,24 @@ struct BarChart: View { /// The chart data to display. let chartData: [ChartData] + let isColumnChart: Bool + var body: some View { if #available(iOS 16, *) { Group { Chart(chartData) { - BarMark( - x: .value("Field", $0.label), - y: .value("Value", $0.value) - ) + if isColumnChart { + // reverse x/y values + BarMark( + x: .value("Field", $0.label), + y: .value("Value", $0.value) + ) + } else { + BarMark( + x: .value("Value", $0.value), + y: .value("Field", $0.label) + ) + } } .chartXAxis { AxisMarks { _ in diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift index 907da2ac9..8c0c4a25c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift @@ -78,7 +78,7 @@ struct ChartView: View { var body: some View { switch popupMedia.kind { case .barChart, .columnChart: - BarChart(chartData: data) + BarChart(chartData: data, isColumnChart: (popupMedia.kind == .columnChart)) case .pieChart: PieChart(chartData: data, showLegend: isFullScreen) case .lineChart: From 459b4ad9710f3d955a62f26d1c2e74c372e35f31 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 8 Sep 2022 11:35:01 -0500 Subject: [PATCH 1525/1722] Pie charts, general chart updates. --- .../Popups/MediaPopupElementView.swift | 30 +++++++-- .../Popups/PopupMedia/BarChart.swift | 40 +++++------ .../Popups/PopupMedia/ChartMediaView.swift | 66 ++++++++++++------- .../Popups/PopupMedia/ImageMediaView.swift | 8 +-- .../Popups/PopupMedia/LineChart.swift | 35 +++++----- .../Popups/PopupMedia/MediaDetailView.swift | 18 +++-- .../Popups/PopupMedia/PieChartModel.swift | 24 ++++++- 7 files changed, 142 insertions(+), 79 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 473e8d787..d089c108e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -20,12 +20,30 @@ struct MediaPopupElementView: View { var popupElement: MediaPopupElement var body: some View { - Divider() - PopupElementHeader( - title: popupElement.title, - description: popupElement.description - ) - PopupMediaView(popupMedia: popupElement.media) + if hasDisplayableMedia { + Divider() + PopupElementHeader( + title: popupElement.title, + description: popupElement.description + ) + PopupMediaView(popupMedia: popupElement.media) + } + } + + /// A Boolean value specifying if there is available media to display. Available media would + /// be image media or chart media when running on iOS 16 or newer. + var hasDisplayableMedia: Bool { + let media = popupElement.media + let imageMedia = media.filter { $0.kind == .image } + if imageMedia.count > 0 { + // We have image media to display. + return true + } + if #available(iOS 16, *) { + // We're on iOS 16 and we have more media than just images. + return media.count > imageMedia.count + } + return false } /// A view displaying an array of `PopupMedia`. diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift index 7ca110e36..348e9e912 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift @@ -15,33 +15,35 @@ import SwiftUI import Charts /// A view displaying details for popup media. +@available(iOS 16, *) struct BarChart: View { /// The chart data to display. let chartData: [ChartData] + /// A Boolean value specifying whether the chart is a "column" chart, with vertical bars. If it's + /// not a "column" chart, then the bars are horizontal. let isColumnChart: Bool var body: some View { - if #available(iOS 16, *) { - Group { - Chart(chartData) { - if isColumnChart { - // reverse x/y values - BarMark( - x: .value("Field", $0.label), - y: .value("Value", $0.value) - ) - } else { - BarMark( - x: .value("Value", $0.value), - y: .value("Field", $0.label) - ) - } + Group { + Chart(chartData) { + if isColumnChart { + // Vertical bars. + BarMark( + x: .value("Field", $0.label), + y: .value("Value", $0.value) + ) + } else { + // Horizontal bars. + BarMark( + x: .value("Value", $0.value), + y: .value("Field", $0.label) + ) } - .chartXAxis { - AxisMarks { _ in - AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) - } + } + .chartXAxis { + AxisMarks { _ in + AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift index 8c0c4a25c..f3b4a44e1 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift @@ -22,8 +22,13 @@ struct ChartMediaView: View { /// The size of the media's frame. let mediaSize: CGSize + /// The data to display in the chart. let chartData: [ChartData] + /// Creates a `ChartMediaView`. + /// - Parameters: + /// - popupMedia: The popup media to display. + /// - mediaSize: The size of the media's frame. init(popupMedia: PopupMedia, mediaSize: CGSize) { self.popupMedia = popupMedia self.mediaSize = mediaSize @@ -31,44 +36,59 @@ struct ChartMediaView: View { } /// A Boolean value specifying whether the media should be shown full screen. - @State private var showingFullScreen = false + @State private var isShowingDetailView = false + /// The corner radius for the view. private let cornerRadius: CGFloat = 8 var body: some View { - ZStack { - ChartView(popupMedia: popupMedia, data: chartData) - VStack { - Spacer() - PopupMediaFooter( + if #available(iOS 16, *) { + ZStack { + ChartView(popupMedia: popupMedia, data: chartData) + VStack { + Spacer() + PopupMediaFooter( + popupMedia: popupMedia, + mediaSize: mediaSize + ) + } + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(.gray, lineWidth: 1) + .frame(width: mediaSize.width, height: mediaSize.height) + } + .frame(width: mediaSize.width, height: mediaSize.height) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .onTapGesture { + isShowingDetailView = true + } + .sheet(isPresented: $isShowingDetailView) { + MediaDetailView( popupMedia: popupMedia, - mediaSize: mediaSize + isShowingDetalView: $isShowingDetailView ) + .padding() } - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(.gray, lineWidth: 1) - .frame(width: mediaSize.width, height: mediaSize.height) - } - .frame(width: mediaSize.width, height: mediaSize.height) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .onTapGesture { - showingFullScreen = true - } - .sheet(isPresented: $showingFullScreen) { - MediaDetailView( - popupMedia: popupMedia, - showingFullScreen: $showingFullScreen - ) - .padding() } } } +@available(iOS 16, *) +/// A view describing a chart. struct ChartView: View { + /// The popup media to display. let popupMedia: PopupMedia + + /// The data to display in the chart. let data: [ChartData] + + /// A Boolean value specifying whether the chart is being draw full screen. let isFullScreen: Bool - + + /// Creates a `ChartView`. + /// - Parameters: + /// - popupMedia: The popup media to display. + /// - data: The data to display in the chart. + /// - isFullScreen: Specifies whether the chart is being draw full screen. init(popupMedia: PopupMedia, data: [ChartData], isFullScreen: Bool = false) { self.popupMedia = popupMedia self.data = data diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift index 2bba3d563..d3e6dcc96 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift @@ -23,7 +23,7 @@ struct ImageMediaView: View { let mediaSize: CGSize /// A Boolean value specifying whether the media should be shown full screen. - @State private var showingFullScreen = false + @State private var isShowingDetalView = false private let cornerRadius: CGFloat = 8 var body: some View { @@ -31,7 +31,7 @@ struct ImageMediaView: View { if let sourceURL = popupMedia.value?.sourceURL { AsyncImageView(url: sourceURL, contentMode: .fill) .onTapGesture { - showingFullScreen = true + isShowingDetalView = true } .frame(width: mediaSize.width, height: mediaSize.height) } @@ -48,11 +48,11 @@ struct ImageMediaView: View { } .frame(width: mediaSize.width, height: mediaSize.height) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .sheet(isPresented: $showingFullScreen) { + .sheet(isPresented: $isShowingDetalView) { if popupMedia.value?.sourceURL != nil { MediaDetailView( popupMedia: popupMedia, - showingFullScreen: $showingFullScreen + isShowingDetalView: $isShowingDetalView ) .padding() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift index 9a28cb51e..2f873b483 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift @@ -15,28 +15,27 @@ import SwiftUI import Charts /// A view displaying details for popup media. +@available(iOS 16, *) struct LineChart: View { /// The chart data to display. let chartData: [ChartData] - + var body: some View { - if #available(iOS 16, *) { - Group { - Chart(chartData) { - LineMark( - x: .value("Field", $0.label), - y: .value("Value", $0.value) - ) - PointMark( - x: .value("Field", $0.label), - y: .value("Value", $0.value) - ) - } - .chartXAxis { - AxisMarks { _ in - AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) - AxisGridLine() - } + Group { + Chart(chartData) { + LineMark( + x: .value("Field", $0.label), + y: .value("Value", $0.value) + ) + PointMark( + x: .value("Field", $0.label), + y: .value("Value", $0.value) + ) + } + .chartXAxis { + AxisMarks { _ in + AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) + AxisGridLine() } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift index 38274771b..687196038 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift @@ -20,14 +20,14 @@ struct MediaDetailView : View { let popupMedia: PopupMedia /// A Boolean value specifying whether the media should be shown full screen. - var showingFullScreen: Binding + var isShowingDetalView: Binding var body: some View { VStack { HStack { Spacer() Button { - showingFullScreen.wrappedValue = false + isShowingDetalView.wrappedValue = false } label: { Text("Done") .fontWeight(.semibold) @@ -64,11 +64,15 @@ struct MediaDetailView : View { } } case .barChart, .columnChart, .pieChart, .lineChart: - ChartView( - popupMedia: popupMedia, - data: ChartData.getChartData(popupMedia: popupMedia), - isFullScreen: true - ) + if #available(iOS 16, *) { + ChartView( + popupMedia: popupMedia, + data: ChartData.getChartData(popupMedia: popupMedia), + isFullScreen: true + ) + } else { + EmptyView() + } default: EmptyView() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift index 358e9f750..ea758164c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift @@ -13,9 +13,13 @@ import SwiftUI +/// The view model for pie charts. final class PieChartModel: ObservableObject { + /// The slices that make up the pie chart. let pieSlices: [PieSlice] + /// Creates a `PieChartModel` + /// - Parameter chartData: The data used for the pie chart. init(chartData: [ChartData]) { var slices = [PieSlice]() var dataTotal: Double = 0 @@ -32,6 +36,7 @@ final class PieChartModel: ObservableObject { pieSlices = slices } + /// The pre-defined colors for the pie slices. static var sliceColors: [Color] = [ .mint, .teal, @@ -47,21 +52,36 @@ final class PieChartModel: ObservableObject { .brown ] + /// Calculates a slice color for the given slice index. + /// - Parameter index: The index of the slice. + /// - Returns: The color for the slice at `index`. static func color(for index: Int) -> Color { // We don't want to just wrap color indices because we don't want // two adjacent slices to have the same color. "extra" will skip the - // the first color for the 2nd time through the list, skip the second - // color the second time through the list, etc. + // the 1st color for the second time through the list, skip the 2nd + // color the thrid time through the list, etc., ensuring that we + // don't get adjacent colors. let extra = index / sliceColors.count return sliceColors[(index + extra) % sliceColors.count].opacity(0.75) } } +/// A single slice of a pie chart. class PieSlice: Identifiable { + /// The fraction of the whole the slice represents. let fraction: Double + + /// The color of the slice. let color: Color + + /// The name of the slice. let name: String + /// Creates a `PieSlice`. + /// - Parameters: + /// - fraction: The fraction of the whole the slice represents. + /// - color: The color of the slice. + /// - name: The name of the slice. init(fraction: Double, color: Color, name: String) { self.fraction = fraction self.color = color From 8666ebb2dd57afded00059ea536245856a145d61 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 8 Sep 2022 16:06:43 -0500 Subject: [PATCH 1526/1722] Refactor, more doc, file location changes for organization. --- .../Popups/MediaPopupElementView.swift | 35 ++++++++++--------- .../PopupMedia/{ => Charts}/BarChart.swift | 2 +- .../PopupMedia/{ => Charts}/ChartData.swift | 2 +- .../{ => Charts}/ChartMediaView.swift | 18 +++++----- .../PopupMedia/{ => Charts}/LineChart.swift | 2 +- .../PopupMedia/{ => Charts}/PieChart.swift | 14 +++++--- .../{ => Charts}/PieChartModel.swift | 0 .../{ => Images}/AsyncImageView.swift | 0 .../{ => Images}/ImageMediaView.swift | 2 +- .../Popups/PopupMedia/MediaDetailView.swift | 8 ++--- 10 files changed, 46 insertions(+), 37 deletions(-) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Charts}/BarChart.swift (96%) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Charts}/ChartData.swift (95%) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Charts}/ChartMediaView.swift (85%) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Charts}/LineChart.swift (95%) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Charts}/PieChart.swift (86%) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Charts}/PieChartModel.swift (100%) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Images}/AsyncImageView.swift (100%) rename Sources/ArcGISToolkit/Components/Popups/PopupMedia/{ => Images}/ImageMediaView.swift (95%) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index d089c108e..cb5c1b677 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -20,30 +20,30 @@ struct MediaPopupElementView: View { var popupElement: MediaPopupElement var body: some View { - if hasDisplayableMedia { + if displayableMediaCount > 0 { Divider() PopupElementHeader( title: popupElement.title, description: popupElement.description ) - PopupMediaView(popupMedia: popupElement.media) + PopupMediaView( + popupMedia: popupElement.media, + displayableMediaCount: displayableMediaCount + ) } } - /// A Boolean value specifying if there is available media to display. Available media would - /// be image media or chart media when running on iOS 16 or newer. - var hasDisplayableMedia: Bool { - let media = popupElement.media - let imageMedia = media.filter { $0.kind == .image } - if imageMedia.count > 0 { - // We have image media to display. - return true - } + /// The number of popup media that can be displayed. The count includes + /// all image media and chart media when running on iOS 16 or newer. + var displayableMediaCount: Int { if #available(iOS 16, *) { - // We're on iOS 16 and we have more media than just images. - return media.count > imageMedia.count + // Include all images and charts. + return popupElement.media.count + } else { + // Only include image media. + let imageMedia = popupElement.media.filter { $0.kind == .image } + return imageMedia.count } - return false } /// A view displaying an array of `PopupMedia`. @@ -51,6 +51,9 @@ struct MediaPopupElementView: View { /// The popup media to display. let popupMedia: [PopupMedia] + /// The number of popup media that can be displayed. + let displayableMediaCount: Int + /// The width of the view content. @State private var width: CGFloat = .zero @@ -89,11 +92,11 @@ struct MediaPopupElementView: View { /// second and subsequent media to be partially visible, to indicate there is more than one. var widthScaleFactor: Double { get { - popupMedia.count > 1 ? 0.75 : 1 + displayableMediaCount > 1 ? 0.75 : 1 } } - /// The size of the image or chart media, not counting descriptive text. + /// The size of the image or chart media. var mediaSize: CGSize { CGSize(width: width, height: 200) } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift similarity index 96% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift index 348e9e912..a9670a712 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/BarChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift @@ -14,7 +14,7 @@ import SwiftUI import Charts -/// A view displaying details for popup media. +/// A view displaying details for bar chart popup media. @available(iOS 16, *) struct BarChart: View { /// The chart data to display. diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartData.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift similarity index 95% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartData.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift index 475ebc130..bf87a6cb9 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartData.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift @@ -34,7 +34,7 @@ internal struct ChartData: Identifiable { } } - static func getChartData(popupMedia: PopupMedia) -> [ChartData] { + static func getChartData(from popupMedia: PopupMedia) -> [ChartData] { guard let labels = popupMedia.value?.labels, let data = popupMedia.value?.data else { return [] } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift similarity index 85% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift index f3b4a44e1..1b0d38c03 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift @@ -32,15 +32,15 @@ struct ChartMediaView: View { init(popupMedia: PopupMedia, mediaSize: CGSize) { self.popupMedia = popupMedia self.mediaSize = mediaSize - self.chartData = ChartData.getChartData(popupMedia: popupMedia) + self.chartData = ChartData.getChartData(from: popupMedia) } - /// A Boolean value specifying whether the media should be shown full screen. + /// A Boolean value specifying whether the media should be drawn in a larger format. @State private var isShowingDetailView = false /// The corner radius for the view. private let cornerRadius: CGFloat = 8 - + var body: some View { if #available(iOS 16, *) { ZStack { @@ -81,18 +81,18 @@ struct ChartView: View { /// The data to display in the chart. let data: [ChartData] - /// A Boolean value specifying whether the chart is being draw full screen. - let isFullScreen: Bool + /// A Boolean value specifying whether the chart is being drawn in a larger format. + let isShowingDetalView: Bool /// Creates a `ChartView`. /// - Parameters: /// - popupMedia: The popup media to display. /// - data: The data to display in the chart. - /// - isFullScreen: Specifies whether the chart is being draw full screen. - init(popupMedia: PopupMedia, data: [ChartData], isFullScreen: Bool = false) { + /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. + init(popupMedia: PopupMedia, data: [ChartData], isShowingDetalView: Bool = false) { self.popupMedia = popupMedia self.data = data - self.isFullScreen = isFullScreen + self.isShowingDetalView = isShowingDetalView } var body: some View { @@ -100,7 +100,7 @@ struct ChartView: View { case .barChart, .columnChart: BarChart(chartData: data, isColumnChart: (popupMedia.kind == .columnChart)) case .pieChart: - PieChart(chartData: data, showLegend: isFullScreen) + PieChart(chartData: data, isShowingDetalView: isShowingDetalView) case .lineChart: LineChart(chartData: data) default: diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift similarity index 95% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift index 2f873b483..51baf99f0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/LineChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift @@ -14,7 +14,7 @@ import SwiftUI import Charts -/// A view displaying details for popup media. +/// A view displaying details for line chart popup media. @available(iOS 16, *) struct LineChart: View { /// The chart data to display. diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift similarity index 86% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift index 33c38b15f..98e40b470 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift @@ -14,10 +14,12 @@ import SwiftUI import Charts -/// A view displaying details for popup media. +/// A view displaying details for pie chart popup media. struct PieChart: View { + /// The view model for the pie chart. @ObservedObject private var viewModel: PieChartModel + /// A Boolean value determining whether to show the legend for the chart. let showLegend: Bool @Environment(\.horizontalSizeClass) var horizontalSizeClass @@ -27,9 +29,13 @@ struct PieChart: View { var isRegularWidth: Bool { !(horizontalSizeClass == .compact && verticalSizeClass == .regular) } - - init(chartData: [ChartData], showLegend: Bool = false) { - self.showLegend = showLegend + + /// Creates a `PieChart`. + /// - Parameters: + /// - chartData: The data to display in the chart. + /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. + init(chartData: [ChartData], isShowingDetalView: Bool = false) { + showLegend = isShowingDetalView _viewModel = ObservedObject(wrappedValue: PieChartModel(chartData: chartData)) } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/PieChartModel.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/AsyncImageView.swift similarity index 100% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/AsyncImageView.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/AsyncImageView.swift diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift similarity index 95% rename from Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift rename to Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift index d3e6dcc96..ee8a0f46a 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift @@ -22,7 +22,7 @@ struct ImageMediaView: View { /// The size of the media's frame. let mediaSize: CGSize - /// A Boolean value specifying whether the media should be shown full screen. + /// A Boolean value specifying whether the media should be drawn in a larger format. @State private var isShowingDetalView = false private let cornerRadius: CGFloat = 8 diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift index 687196038..0a0572b1e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift @@ -14,12 +14,12 @@ import SwiftUI import ArcGIS -/// A view displaying a popup media in full screen. +/// A view displaying a popup media in a large format. struct MediaDetailView : View { /// The popup media to display. let popupMedia: PopupMedia - /// A Boolean value specifying whether the media should be shown full screen. + /// A Boolean value specifying whether the media should be drawn in a larger format. var isShowingDetalView: Binding var body: some View { @@ -67,8 +67,8 @@ struct MediaDetailView : View { if #available(iOS 16, *) { ChartView( popupMedia: popupMedia, - data: ChartData.getChartData(popupMedia: popupMedia), - isFullScreen: true + data: ChartData.getChartData(from: popupMedia), + isShowingDetalView: true ) } else { EmptyView() From fda4b6819b5d6d954cbefae696b35e3cb9b7f6a0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 8 Sep 2022 16:40:11 -0500 Subject: [PATCH 1527/1722] Doc and minor refactoring. --- .../Popups/MediaPopupElementView.swift | 2 +- .../PopupMedia/Charts/ChartMediaView.swift | 6 +-- .../Popups/PopupMedia/Charts/PieChart.swift | 9 +++- .../PopupMedia/Charts/PieChartModel.swift | 4 +- .../PopupMedia/Images/ImageMediaView.swift | 44 +++++++++---------- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index cb5c1b677..44b9d6e73 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -56,7 +56,7 @@ struct MediaPopupElementView: View { /// The width of the view content. @State private var width: CGFloat = .zero - + var body: some View { ScrollView(.horizontal) { HStack(alignment: .top, spacing: 8) { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift index 1b0d38c03..e2e998149 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift @@ -35,12 +35,12 @@ struct ChartMediaView: View { self.chartData = ChartData.getChartData(from: popupMedia) } - /// A Boolean value specifying whether the media should be drawn in a larger format. - @State private var isShowingDetailView = false - /// The corner radius for the view. private let cornerRadius: CGFloat = 8 + /// A Boolean value specifying whether the media should be drawn in a larger format. + @State private var isShowingDetailView = false + var body: some View { if #available(iOS 16, *) { ZStack { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift index 98e40b470..b4cc0e888 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift @@ -47,6 +47,9 @@ struct PieChart: View { } } + /// Creates a legend view for a pie chart. + /// - Parameter slices: The slices that make up the pie chart. + /// - Returns: A view representing a pie chart legend. @ViewBuilder func makeLegend(slices: [PieSlice]) -> some View { LazyVGrid( columns: Array( @@ -69,9 +72,13 @@ struct PieChart: View { } } +/// A view representing a pie chart. struct Pie: View { + /// The view model for the pie chart. @ObservedObject private var viewModel: PieChartModel - + + /// Creates a Pie view. + /// - Parameter viewModel: The view model for the pie chart. init(viewModel: PieChartModel) { self.viewModel = viewModel } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift index ea758164c..c719157e8 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift @@ -67,10 +67,10 @@ final class PieChartModel: ObservableObject { } /// A single slice of a pie chart. -class PieSlice: Identifiable { +final class PieSlice: Identifiable { /// The fraction of the whole the slice represents. let fraction: Double - + /// The color of the slice. let color: Color diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift index ee8a0f46a..9f136f386 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift @@ -22,37 +22,37 @@ struct ImageMediaView: View { /// The size of the media's frame. let mediaSize: CGSize - /// A Boolean value specifying whether the media should be drawn in a larger format. - @State private var isShowingDetalView = false + /// The corner radius for the view. private let cornerRadius: CGFloat = 8 + + /// A Boolean value specifying whether the media should be drawn in a larger format. + @State private var isShowingDetailView = false var body: some View { - ZStack { - if let sourceURL = popupMedia.value?.sourceURL { + if let sourceURL = popupMedia.value?.sourceURL { + ZStack { AsyncImageView(url: sourceURL, contentMode: .fill) - .onTapGesture { - isShowingDetalView = true - } + .frame(width: mediaSize.width, height: mediaSize.height) + VStack { + Spacer() + PopupMediaFooter( + popupMedia: popupMedia, + mediaSize: mediaSize + ) + } + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(.gray, lineWidth: 1) .frame(width: mediaSize.width, height: mediaSize.height) } - VStack { - Spacer() - PopupMediaFooter( - popupMedia: popupMedia, - mediaSize: mediaSize - ) + .frame(width: mediaSize.width, height: mediaSize.height) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .onTapGesture { + isShowingDetailView = true } - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(.gray, lineWidth: 1) - .frame(width: mediaSize.width, height: mediaSize.height) - } - .frame(width: mediaSize.width, height: mediaSize.height) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .sheet(isPresented: $isShowingDetalView) { - if popupMedia.value?.sourceURL != nil { + .sheet(isPresented: $isShowingDetailView) { MediaDetailView( popupMedia: popupMedia, - isShowingDetalView: $isShowingDetalView + isShowingDetalView: $isShowingDetailView ) .padding() } From b8052ea75cb81d860ffe421c1d02b3b47c61311d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 8 Sep 2022 16:57:33 -0500 Subject: [PATCH 1528/1722] Don't show x axis labels in bar or pie charts unless we're in a detail view. --- .../Popups/PopupMedia/Charts/BarChart.swift | 23 ++++++++++++++++++- .../PopupMedia/Charts/ChartMediaView.swift | 16 ++++++++++--- .../Popups/PopupMedia/Charts/LineChart.swift | 21 ++++++++++++++++- .../Popups/PopupMedia/Charts/PieChart.swift | 4 +++- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift index a9670a712..1a57d21ad 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift @@ -24,6 +24,22 @@ struct BarChart: View { /// not a "column" chart, then the bars are horizontal. let isColumnChart: Bool + /// A Boolean value determining whether to show the x axis labels for the chart. + let showXAxisLabels: Bool + + /// Creates a `BarChart`. + /// - Parameters: + /// - chartData: The data to display in the chart. + /// - isColumnChart: Specifying whether the chart is a "column" chart, with vertical bars. + /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. + init(chartData: [ChartData], isColumnChart: Bool, isShowingDetalView: Bool = false) { + self.chartData = chartData + self.isColumnChart = isColumnChart + + // Only show the x axis labels if we're being show in a detail view. + showXAxisLabels = isShowingDetalView + } + var body: some View { Group { Chart(chartData) { @@ -43,7 +59,12 @@ struct BarChart: View { } .chartXAxis { AxisMarks { _ in - AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) + if showXAxisLabels { + AxisValueLabel( + collisionResolution: .greedy, + orientation: .verticalReversed + ) + } } } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift index e2e998149..aee95cd83 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift @@ -98,11 +98,21 @@ struct ChartView: View { var body: some View { switch popupMedia.kind { case .barChart, .columnChart: - BarChart(chartData: data, isColumnChart: (popupMedia.kind == .columnChart)) + BarChart( + chartData: data, + isColumnChart: (popupMedia.kind == .columnChart), + isShowingDetalView: isShowingDetalView + ) case .pieChart: - PieChart(chartData: data, isShowingDetalView: isShowingDetalView) + PieChart( + chartData: data, + isShowingDetalView: isShowingDetalView + ) case .lineChart: - LineChart(chartData: data) + LineChart( + chartData: data, + isShowingDetalView: isShowingDetalView + ) default: EmptyView() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift index 51baf99f0..f5a929cf2 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift @@ -20,6 +20,20 @@ struct LineChart: View { /// The chart data to display. let chartData: [ChartData] + /// A Boolean value determining whether to show the x axis labels for the chart. + let showXAxisLabels: Bool + + /// Creates a `BarChart`. + /// - Parameters: + /// - chartData: The data to display in the chart. + /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. + init(chartData: [ChartData], isShowingDetalView: Bool = false) { + self.chartData = chartData + + // Only show the x axis labels if we're being show in a detail view. + showXAxisLabels = isShowingDetalView + } + var body: some View { Group { Chart(chartData) { @@ -34,7 +48,12 @@ struct LineChart: View { } .chartXAxis { AxisMarks { _ in - AxisValueLabel(collisionResolution: .greedy, orientation: .verticalReversed) + if showXAxisLabels { + AxisValueLabel( + collisionResolution: .greedy, + orientation: .verticalReversed + ) + } AxisGridLine() } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift index b4cc0e888..db1dd0f4a 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift @@ -35,8 +35,10 @@ struct PieChart: View { /// - chartData: The data to display in the chart. /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. init(chartData: [ChartData], isShowingDetalView: Bool = false) { - showLegend = isShowingDetalView _viewModel = ObservedObject(wrappedValue: PieChartModel(chartData: chartData)) + + // Only show the legend if we're being show in a detail view. + showLegend = isShowingDetalView } var body: some View { From 63058c54802b5397bb4e4b33e30cabaabfdaa1ce Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 12 Sep 2022 09:43:14 -0500 Subject: [PATCH 1529/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Components/Popups/MediaPopupElementView.swift | 2 +- .../Components/Popups/PopupMedia/Charts/BarChart.swift | 2 +- .../Components/Popups/PopupMedia/Charts/ChartData.swift | 2 ++ .../Components/Popups/PopupMedia/Charts/ChartMediaView.swift | 2 +- .../Components/Popups/PopupMedia/Charts/LineChart.swift | 4 ++-- .../Components/Popups/PopupMedia/Charts/PieChartModel.swift | 2 +- .../Components/Popups/PopupMedia/Images/ImageMediaView.swift | 2 +- .../Components/Popups/PopupMedia/MediaDetailView.swift | 2 +- 8 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 44b9d6e73..cb5c1b677 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -56,7 +56,7 @@ struct MediaPopupElementView: View { /// The width of the view content. @State private var width: CGFloat = .zero - + var body: some View { ScrollView(.horizontal) { HStack(alignment: .top, spacing: 8) { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift index 1a57d21ad..4917422cb 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift @@ -39,7 +39,7 @@ struct BarChart: View { // Only show the x axis labels if we're being show in a detail view. showXAxisLabels = isShowingDetalView } - + var body: some View { Group { Chart(chartData) { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift index bf87a6cb9..2c442f94a 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift @@ -15,7 +15,9 @@ import SwiftUI import ArcGIS internal struct ChartData: Identifiable { + /// A label for the data. var label: String + /// The value of the data. var value: Double var id = UUID() diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift index aee95cd83..4534e7f0e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift @@ -40,7 +40,7 @@ struct ChartMediaView: View { /// A Boolean value specifying whether the media should be drawn in a larger format. @State private var isShowingDetailView = false - + var body: some View { if #available(iOS 16, *) { ZStack { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift index f5a929cf2..f1bd5a450 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift @@ -22,7 +22,7 @@ struct LineChart: View { /// A Boolean value determining whether to show the x axis labels for the chart. let showXAxisLabels: Bool - + /// Creates a `BarChart`. /// - Parameters: /// - chartData: The data to display in the chart. @@ -33,7 +33,7 @@ struct LineChart: View { // Only show the x axis labels if we're being show in a detail view. showXAxisLabels = isShowingDetalView } - + var body: some View { Group { Chart(chartData) { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift index c719157e8..144e28a00 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift @@ -59,7 +59,7 @@ final class PieChartModel: ObservableObject { // We don't want to just wrap color indices because we don't want // two adjacent slices to have the same color. "extra" will skip the // the 1st color for the second time through the list, skip the 2nd - // color the thrid time through the list, etc., ensuring that we + // color the third time through the list, etc., ensuring that we // don't get adjacent colors. let extra = index / sliceColors.count return sliceColors[(index + extra) % sliceColors.count].opacity(0.75) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift index 9f136f386..7e55439e0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift @@ -27,7 +27,7 @@ struct ImageMediaView: View { /// A Boolean value specifying whether the media should be drawn in a larger format. @State private var isShowingDetailView = false - + var body: some View { if let sourceURL = popupMedia.value?.sourceURL { ZStack { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift index 0a0572b1e..015e040a6 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift @@ -18,7 +18,7 @@ import ArcGIS struct MediaDetailView : View { /// The popup media to display. let popupMedia: PopupMedia - + /// A Boolean value specifying whether the media should be drawn in a larger format. var isShowingDetalView: Binding From 98255c98095f2f743e5d136d465d6f6c4b100517 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 12 Sep 2022 09:43:57 -0500 Subject: [PATCH 1530/1722] PR review changes. --- .../Popups/PopupMedia/Charts/BarChart.swift | 6 +++--- .../PopupMedia/Charts/ChartMediaView.swift | 16 ++++++++-------- .../Popups/PopupMedia/Charts/LineChart.swift | 6 +++--- .../Popups/PopupMedia/Charts/PieChart.swift | 6 +++--- .../PopupMedia/Images/ImageMediaView.swift | 2 +- .../Popups/PopupMedia/MediaDetailView.swift | 6 +++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift index 4917422cb..1ca2f316e 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift @@ -31,13 +31,13 @@ struct BarChart: View { /// - Parameters: /// - chartData: The data to display in the chart. /// - isColumnChart: Specifying whether the chart is a "column" chart, with vertical bars. - /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. - init(chartData: [ChartData], isColumnChart: Bool, isShowingDetalView: Bool = false) { + /// - isShowingDetailView: Specifies whether the chart is being drawn in a larger format. + init(chartData: [ChartData], isColumnChart: Bool, isShowingDetailView: Bool = false) { self.chartData = chartData self.isColumnChart = isColumnChart // Only show the x axis labels if we're being show in a detail view. - showXAxisLabels = isShowingDetalView + showXAxisLabels = isShowingDetailView } var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift index 4534e7f0e..d88084cb4 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartMediaView.swift @@ -64,7 +64,7 @@ struct ChartMediaView: View { .sheet(isPresented: $isShowingDetailView) { MediaDetailView( popupMedia: popupMedia, - isShowingDetalView: $isShowingDetailView + isShowingDetailView: $isShowingDetailView ) .padding() } @@ -82,17 +82,17 @@ struct ChartView: View { let data: [ChartData] /// A Boolean value specifying whether the chart is being drawn in a larger format. - let isShowingDetalView: Bool + let isShowingDetailView: Bool /// Creates a `ChartView`. /// - Parameters: /// - popupMedia: The popup media to display. /// - data: The data to display in the chart. - /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. - init(popupMedia: PopupMedia, data: [ChartData], isShowingDetalView: Bool = false) { + /// - isShowingDetailView: Specifies whether the chart is being drawn in a larger format. + init(popupMedia: PopupMedia, data: [ChartData], isShowingDetailView: Bool = false) { self.popupMedia = popupMedia self.data = data - self.isShowingDetalView = isShowingDetalView + self.isShowingDetailView = isShowingDetailView } var body: some View { @@ -101,17 +101,17 @@ struct ChartView: View { BarChart( chartData: data, isColumnChart: (popupMedia.kind == .columnChart), - isShowingDetalView: isShowingDetalView + isShowingDetailView: isShowingDetailView ) case .pieChart: PieChart( chartData: data, - isShowingDetalView: isShowingDetalView + isShowingDetailView: isShowingDetailView ) case .lineChart: LineChart( chartData: data, - isShowingDetalView: isShowingDetalView + isShowingDetailView: isShowingDetailView ) default: EmptyView() diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift index f1bd5a450..2bbcfe47c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift @@ -26,12 +26,12 @@ struct LineChart: View { /// Creates a `BarChart`. /// - Parameters: /// - chartData: The data to display in the chart. - /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. - init(chartData: [ChartData], isShowingDetalView: Bool = false) { + /// - isShowingDetailView: Specifies whether the chart is being drawn in a larger format. + init(chartData: [ChartData], isShowingDetailView: Bool = false) { self.chartData = chartData // Only show the x axis labels if we're being show in a detail view. - showXAxisLabels = isShowingDetalView + showXAxisLabels = isShowingDetailView } var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift index db1dd0f4a..b82023c44 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift @@ -33,12 +33,12 @@ struct PieChart: View { /// Creates a `PieChart`. /// - Parameters: /// - chartData: The data to display in the chart. - /// - isShowingDetalView: Specifies whether the chart is being drawn in a larger format. - init(chartData: [ChartData], isShowingDetalView: Bool = false) { + /// - isShowingDetailView: Specifies whether the chart is being drawn in a larger format. + init(chartData: [ChartData], isShowingDetailView: Bool = false) { _viewModel = ObservedObject(wrappedValue: PieChartModel(chartData: chartData)) // Only show the legend if we're being show in a detail view. - showLegend = isShowingDetalView + showLegend = isShowingDetailView } var body: some View { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift index 7e55439e0..bdef82115 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Images/ImageMediaView.swift @@ -52,7 +52,7 @@ struct ImageMediaView: View { .sheet(isPresented: $isShowingDetailView) { MediaDetailView( popupMedia: popupMedia, - isShowingDetalView: $isShowingDetailView + isShowingDetailView: $isShowingDetailView ) .padding() } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift index 015e040a6..162367857 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/MediaDetailView.swift @@ -20,14 +20,14 @@ struct MediaDetailView : View { let popupMedia: PopupMedia /// A Boolean value specifying whether the media should be drawn in a larger format. - var isShowingDetalView: Binding + var isShowingDetailView: Binding var body: some View { VStack { HStack { Spacer() Button { - isShowingDetalView.wrappedValue = false + isShowingDetailView.wrappedValue = false } label: { Text("Done") .fontWeight(.semibold) @@ -68,7 +68,7 @@ struct MediaDetailView : View { ChartView( popupMedia: popupMedia, data: ChartData.getChartData(from: popupMedia), - isShowingDetalView: true + isShowingDetailView: true ) } else { EmptyView() From 8133852a829b1ac3437dc656e2db12293e7d41c7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 12 Sep 2022 09:57:53 -0500 Subject: [PATCH 1531/1722] Add doc for struct and methods/props. --- .../Popups/PopupMedia/Charts/ChartData.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift index 2c442f94a..1ee785c19 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift @@ -14,13 +14,18 @@ import SwiftUI import ArcGIS -internal struct ChartData: Identifiable { +/// Data for a chart, representing a label and value pair. +struct ChartData: Identifiable { /// A label for the data. var label: String /// The value of the data. var value: Double var id = UUID() + /// Creates a `ChartData`. + /// - Parameters: + /// - label: The label for the data. + /// - value: The value of the data. init(label: String, value: Any) { self.label = label if let int32 = value as? Int32 { @@ -36,6 +41,9 @@ internal struct ChartData: Identifiable { } } + /// Gets the chart data for a `PopupMedia`. + /// - Parameter popupMedia: The popup media to get the data for. + /// - Returns: The array of chart data for the popup media. static func getChartData(from popupMedia: PopupMedia) -> [ChartData] { guard let labels = popupMedia.value?.labels, let data = popupMedia.value?.data else { return [] } From c31bc4faa1bf9ea714d85da549eff9131222438c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 12 Sep 2022 15:54:13 -0500 Subject: [PATCH 1532/1722] Disclosure groups and clean up Dividers() --- .../Popups/AttachmentsPopupElementView.swift | 27 +++++++++++---- .../Popups/FieldsPopupElementView.swift | 22 ++++++++---- .../Popups/MediaPopupElementView.swift | 34 ++++++++++++++----- .../Popups/PopupElementHeader.swift | 5 --- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 8d0b03cbb..5448dfb80 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -43,19 +43,17 @@ struct AttachmentsPopupElementView: View { ) } + @State var isExpanded: Bool = true + var body: some View { Group { if isLoadingAttachments { ProgressView() .padding() } else if viewModel.attachmentModels.count > 0 { - Divider() - VStack(alignment: .leading) { - PopupElementHeader( - title: popupElement.title, - description: popupElement.description - ) - + DisclosureGroup(isExpanded: $isExpanded) { + Divider() + .padding(.bottom, 4) switch popupElement.displayType { case .list: AttachmentList(attachmentModels: viewModel.attachmentModels) @@ -68,6 +66,14 @@ struct AttachmentsPopupElementView: View { AttachmentList(attachmentModels: viewModel.attachmentModels) } } + } label: { + VStack(alignment: .leading) { + Divider() + PopupElementHeader( + title: popupElement.displayTitle, + description: popupElement.description + ) + } } } } @@ -81,3 +87,10 @@ struct AttachmentsPopupElementView: View { } } } + +private extension AttachmentsPopupElement { + /// Provides a default title to display if `title` is empty. + var displayTitle: String { + title.isEmpty ? "Attachments" : title + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 527092f7f..627f65631 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -31,14 +31,22 @@ struct FieldsPopupElementView: View { /// The labels and values to display, as an array of `DisplayField`s. private let displayFields: [DisplayField] + @State var isExpanded: Bool = true + var body: some View { - Divider() - PopupElementHeader( - title: popupElement.displayTitle, - description: popupElement.description - ) - .padding([.bottom], 4) - FieldsList(fields: displayFields) + DisclosureGroup(isExpanded: $isExpanded) { + Divider() + .padding(.bottom, 4) + FieldsList(fields: displayFields) + } label: { + VStack(alignment: .leading) { + Divider() + PopupElementHeader( + title: popupElement.displayTitle, + description: popupElement.description + ) + } + } } /// A view displaying the labels and values. diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index cb5c1b677..8532ea499 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -19,17 +19,26 @@ struct MediaPopupElementView: View { /// The `PopupElement` to display. var popupElement: MediaPopupElement + @State var isExpanded: Bool = true + var body: some View { if displayableMediaCount > 0 { - Divider() - PopupElementHeader( - title: popupElement.title, - description: popupElement.description - ) - PopupMediaView( - popupMedia: popupElement.media, - displayableMediaCount: displayableMediaCount - ) + DisclosureGroup(isExpanded: $isExpanded) { + Divider() + .padding(.bottom, 4) + PopupMediaView( + popupMedia: popupElement.media, + displayableMediaCount: displayableMediaCount + ) + } label: { + VStack(alignment: .leading) { + Divider() + PopupElementHeader( + title: popupElement.displayTitle, + description: popupElement.description + ) + } + } } } @@ -104,3 +113,10 @@ struct MediaPopupElementView: View { } extension PopupMedia: Identifiable {} + +private extension MediaPopupElement { + /// Provides a default title to display if `title` is empty. + var displayTitle: String { + title.isEmpty ? "Media" : title + } +} diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index b6ddcdaf2..6aa724785 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -36,10 +36,5 @@ struct PopupElementHeader: View { } } .padding([.bottom], 4) - - if !title.isEmpty || - !description.isEmpty { - Divider() - } } } From 0e51d34b911a5e278a608602ce8e4585ceb95864 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 12 Sep 2022 16:01:45 -0500 Subject: [PATCH 1533/1722] Move position of Dividers() from top of element to the bottom. --- .../Components/Popups/AttachmentsPopupElementView.swift | 2 +- .../Components/Popups/FieldsPopupElementView.swift | 2 +- .../ArcGISToolkit/Components/Popups/MediaPopupElementView.swift | 2 +- Sources/ArcGISToolkit/Components/Popups/PopupView.swift | 1 + .../ArcGISToolkit/Components/Popups/TextPopupElementView.swift | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift index 5448dfb80..113c202f0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/AttachmentsPopupElementView.swift @@ -68,13 +68,13 @@ struct AttachmentsPopupElementView: View { } } label: { VStack(alignment: .leading) { - Divider() PopupElementHeader( title: popupElement.displayTitle, description: popupElement.description ) } } + Divider() } } .task { diff --git a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift index 627f65631..a27eda597 100644 --- a/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/FieldsPopupElementView.swift @@ -40,13 +40,13 @@ struct FieldsPopupElementView: View { FieldsList(fields: displayFields) } label: { VStack(alignment: .leading) { - Divider() PopupElementHeader( title: popupElement.displayTitle, description: popupElement.description ) } } + Divider() } /// A view displaying the labels and values. diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 8532ea499..3c7beb5ae 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -32,13 +32,13 @@ struct MediaPopupElementView: View { ) } label: { VStack(alignment: .leading) { - Divider() PopupElementHeader( title: popupElement.displayTitle, description: popupElement.description ) } } + Divider() } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 7e77f3a32..a1481c43c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -38,6 +38,7 @@ public struct PopupView: View { Text(popup.title) .font(.title) .fontWeight(.bold) + Divider() } Group { if let isPopupEvaluated = isPopupEvaluated { diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index f0e850100..8c64f8ae5 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -25,7 +25,6 @@ struct TextPopupElementView: View { var body: some View { if !popupElement.text.isEmpty { let roundedRect = RoundedRectangle(cornerRadius: 8) - Divider() ZStack { HTMLTextView(html: popupElement.text, height: $webViewHeight) .clipShape(roundedRect) @@ -39,6 +38,7 @@ struct TextPopupElementView: View { .stroke(Color.black, lineWidth: 1) } } + Divider() } } } From 636f2a0ce89d08a51bddcd9adb486bfbb80c9733 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Sep 2022 15:41:02 -0500 Subject: [PATCH 1534/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Components/Popups/PopupMedia/Charts/BarChart.swift | 4 ++-- .../Components/Popups/PopupMedia/Charts/LineChart.swift | 2 +- .../Components/Popups/PopupMedia/Charts/PieChart.swift | 2 +- .../Components/Popups/PopupMedia/Charts/PieChartModel.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift index 1ca2f316e..7b0b2b9e7 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/BarChart.swift @@ -20,7 +20,7 @@ struct BarChart: View { /// The chart data to display. let chartData: [ChartData] - /// A Boolean value specifying whether the chart is a "column" chart, with vertical bars. If it's + /// A Boolean value specifying whether the chart is a "column" chart, with vertical bars. If it's /// not a "column" chart, then the bars are horizontal. let isColumnChart: Bool @@ -36,7 +36,7 @@ struct BarChart: View { self.chartData = chartData self.isColumnChart = isColumnChart - // Only show the x axis labels if we're being show in a detail view. + // Only show the x axis labels if we're being shown in a detail view. showXAxisLabels = isShowingDetailView } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift index 2bbcfe47c..030b3cad7 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/LineChart.swift @@ -30,7 +30,7 @@ struct LineChart: View { init(chartData: [ChartData], isShowingDetailView: Bool = false) { self.chartData = chartData - // Only show the x axis labels if we're being show in a detail view. + // Only show the x axis labels if we're being shown in a detail view. showXAxisLabels = isShowingDetailView } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift index b82023c44..4dfb79017 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift @@ -37,7 +37,7 @@ struct PieChart: View { init(chartData: [ChartData], isShowingDetailView: Bool = false) { _viewModel = ObservedObject(wrappedValue: PieChartModel(chartData: chartData)) - // Only show the legend if we're being show in a detail view. + // Only show the legend if we're being shown in a detail view. showLegend = isShowingDetailView } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift index 144e28a00..23da66c52 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChartModel.swift @@ -57,7 +57,7 @@ final class PieChartModel: ObservableObject { /// - Returns: The color for the slice at `index`. static func color(for index: Int) -> Color { // We don't want to just wrap color indices because we don't want - // two adjacent slices to have the same color. "extra" will skip the + // two adjacent slices to have the same color. "extra" will skip the // the 1st color for the second time through the list, skip the 2nd // color the third time through the list, etc., ensuring that we // don't get adjacent colors. From a0d74571e8187ec55e62d6f9467c51077f218a21 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Sep 2022 16:04:55 -0500 Subject: [PATCH 1535/1722] Update Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift I like having some border, so I think 0.5 is fine. Co-authored-by: David Feinzimer --- .../Components/Popups/PopupMedia/Charts/PieChart.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift index 4dfb79017..ff90c69af 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift @@ -109,7 +109,7 @@ struct Pie: View { startAngle = endAngle } path.fill(slice.color) - path.stroke(.gray, lineWidth: 1) + path.stroke(.gray, lineWidth: 0.5) } } } From 4c892f63d599f68c5fefbc092f10d92d0639201e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Sep 2022 16:07:32 -0500 Subject: [PATCH 1536/1722] Popup Chart Pr review changes. --- .../Components/Popups/PopupElementHeader.swift | 4 ++-- .../Components/Popups/PopupMedia/Charts/ChartData.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index b6ddcdaf2..153f84174 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -16,8 +16,8 @@ import ArcGIS /// A view displaying a title and description of a `PopupElement`. struct PopupElementHeader: View { - var title: String - var description: String + let title: String + let description: String var body: some View { VStack(alignment: .leading) { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift index 1ee785c19..7ac243186 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/ChartData.swift @@ -17,10 +17,10 @@ import ArcGIS /// Data for a chart, representing a label and value pair. struct ChartData: Identifiable { /// A label for the data. - var label: String + let label: String /// The value of the data. - var value: Double - var id = UUID() + let value: Double + let id = UUID() /// Creates a `ChartData`. /// - Parameters: From cc605f7ea8adc0d3ee62eec7f4ed5d30fa41cb8e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Sep 2022 14:47:55 -0500 Subject: [PATCH 1537/1722] fix multi-line leading --- .../ArcGISToolkit/Components/Popups/PopupElementHeader.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift index f04f08df1..b222ee461 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupElementHeader.swift @@ -25,12 +25,14 @@ struct PopupElementHeader: View { // a view, so conditionally check for an empty title and description. if !title.isEmpty { Text(title) + .multilineTextAlignment(.leading) .font(.title2) .foregroundColor(.primary) } if !description.isEmpty { Text(description) + .multilineTextAlignment(.leading) .font(.subheadline) .foregroundColor(.secondary) } From 871a295c34676b7218cc9b1ebf08baca75bc908b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Sep 2022 14:49:09 -0500 Subject: [PATCH 1538/1722] Remove unneeded Chart import. --- .../Components/Popups/PopupMedia/Charts/PieChart.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift index ff90c69af..bd93d111c 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupMedia/Charts/PieChart.swift @@ -12,7 +12,6 @@ // limitations under the License. import SwiftUI -import Charts /// A view displaying details for pie chart popup media. struct PieChart: View { From e2c6ad21491357c36b6b4ed5ac561485916aef5c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 16 Sep 2022 14:49:51 -0500 Subject: [PATCH 1539/1722] Support for a "close" button in the popup, displaying the popup in a floating panel, and some minor floating panel enhancements. --- Examples/Examples/PopupExampleView.swift | 68 +++++--------- .../FloatingPanel/FloatingPanel.swift | 91 ++++++++++--------- .../Components/Popups/PopupView.swift | 51 +++++++++-- 3 files changed, 112 insertions(+), 98 deletions(-) diff --git a/Examples/Examples/PopupExampleView.swift b/Examples/Examples/PopupExampleView.swift index 27428a3b1..fa86084a6 100644 --- a/Examples/Examples/PopupExampleView.swift +++ b/Examples/Examples/PopupExampleView.swift @@ -45,16 +45,22 @@ struct PopupExampleView: View { return Map(item: portalItem1) } - + /// The map displayed in the map view. @StateObject private var map = makeMap() /// The point on the screen the user tapped on to identify a feature. @State private var identifyScreenPoint: CGPoint? - - /// The result of the layer identify operation. - @State private var identifyResult: Result<[IdentifyLayerResult], Error>? - + + /// The popup to be shown as the result of the layer identify operation. + @State private var popup: Popup? + + /// A Boolean value specifying whether the popup view should be shown or not. + @State private var showPopup = false + + /// The detent value specifying the initial `FloatingPanelDetent`. Defaults to "full". + @State private var floatingPanelDetent: FloatingPanelDetent = .full + var body: some View { MapViewReader { proxy in VStack { @@ -76,16 +82,19 @@ struct PopupExampleView: View { return } - self.identifyResult = identifyResult self.identifyScreenPoint = nil + self.popup = try? identifyResult.get().first?.popups.first + self.showPopup = self.popup != nil } - .overlay(alignment: .topLeading) { + .floatingPanel( + detent: $floatingPanelDetent, + horizontalAlignment: .leading, + isPresented: $showPopup + ) { Group { - if identifyScreenPoint != nil { - ProgressView() - .esriBorder() - } else if let identifyResult = identifyResult { - IdentifyResultView(identifyResult: identifyResult) + if let popup = popup { + PopupView(popup: popup, isPresented: $showPopup) + .showCloseButton(true) } } .padding() @@ -94,38 +103,3 @@ struct PopupExampleView: View { } } } - -/// A view displaying the results of an identify operation. -private struct IdentifyResultView: View { - var identifyResult: Result<[IdentifyLayerResult], Error> - - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass - - /// If `true`, will draw the popup view at half height, exposing a portion of the - /// underlying map below the view on an iPhone in portrait orientation (and certain iPad multitasking - /// configurations). If `false`, will draw the popup view full size. - private var useHalfHeightResults: Bool { - horizontalSizeClass == .compact && verticalSizeClass == .regular - } - - var body: some View { - switch identifyResult { - case .success(let identifyLayerResults): - // Get the first popup from the first layer result. - if let popup = identifyLayerResults.first?.popups.first { - GeometryReader { geometry in - PopupView(popup: popup) - .frame( - maxWidth: useHalfHeightResults ? .infinity : 400, - maxHeight: useHalfHeightResults ? geometry.size.height / 2 : nil - ) - .esriBorder() - } - } - case .failure(let error): - Text("Identify error: \(error.localizedDescription).") - .esriBorder() - } - } -} diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index 9dd50f4d7..dd6cf8897 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -46,22 +46,22 @@ struct FloatingPanel: View where Content: View { @ViewBuilder content: () -> Content ) { self.backgroundColor = backgroundColor + self.activeDetent = detent + self.isPresented = isPresented self.content = content() - _activeDetent = detent - _isPresented = isPresented } /// A binding to the currently selected detent. - @Binding private var activeDetent: FloatingPanelDetent + private var activeDetent: Binding /// The color of the handle. @State private var handleColor: Color = .defaultHandleColor /// The height of the content. @State private var height: CGFloat = .minHeight - + /// A binding to a Boolean value that determines whether the view is presented. - @Binding private var isPresented: Bool + private var isPresented: Binding /// The maximum allowed height of the content. @State private var maximumHeight: CGFloat = .infinity @@ -72,50 +72,53 @@ struct FloatingPanel: View where Content: View { } public var body: some View { - GeometryReader { geometryProxy in - VStack(spacing: 0) { - if isCompact && isPresented { - makeHandleView() + if isPresented.wrappedValue { + GeometryReader { geometryProxy in + VStack(spacing: 0) { + if isCompact { + makeHandleView() + } + content + .frame(height: height) + .padding(.bottom, isCompact ? 25 : .zero) + if !isCompact { + Divider() + makeHandleView() + } } - content - .frame(height: height) - .padding(.bottom, isCompact ? 25 : .zero) - if !isCompact && isPresented { - 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 + ) + .onSizeChange { + maximumHeight = $0.height + if height > maximumHeight { + height = maximumHeight + } } - } - .background(backgroundColor) - .cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners]) - .shadow(radius: 10) - .opacity(isPresented ? 1.0 : .zero) - .frame( - width: geometryProxy.size.width, - height: geometryProxy.size.height, - alignment: isCompact ? .bottom : .top - ) - .onSizeChange { - maximumHeight = $0.height - if height > maximumHeight { - height = maximumHeight + .onChange(of: activeDetent.wrappedValue) { _ in + withAnimation { + height = heightFor(detent: activeDetent.wrappedValue) + } } - } - .onChange(of: activeDetent) { _ in - withAnimation { - height = heightFor(detent: activeDetent) + .onChange(of: isPresented.wrappedValue) { + height = $0 ? heightFor(detent: activeDetent.wrappedValue) : .zero } - } - .onChange(of: isPresented) { - height = $0 ? heightFor(detent: activeDetent) : .zero - } - .onAppear { - withAnimation { - height = heightFor(detent: activeDetent) + .onAppear { + withAnimation { + height = heightFor(detent: activeDetent.wrappedValue) + } } + .animation(.default, value: isPresented.wrappedValue) } - .animation(.default, value: isPresented) + .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 { @@ -139,7 +142,7 @@ struct FloatingPanel: View where Content: View { .onEnded { _ in handleColor = .defaultHandleColor withAnimation { - activeDetent = closestDetent + activeDetent.wrappedValue = closestDetent height = heightFor(detent: closestDetent) } } @@ -163,7 +166,7 @@ struct FloatingPanel: View where Content: View { case .half: return maximumHeight * 0.4 case .full: - return maximumHeight * 0.90 + return maximumHeight * (isCompact ? 0.90 : 1.0) } } diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index a1481c43c..2909a39d1 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -17,14 +17,22 @@ import ArcGIS /// A view displaying the elements of a single Popup. public struct PopupView: View { /// Creates a `PopupView` with the given popup. - /// - Parameter popup: The popup to display. - public init(popup: Popup) { + /// - Parameters + /// popup: The popup to display. + /// - isPresented: A Boolean value indicating if the view is presented. + public init(popup: Popup, isPresented: Binding? = nil) { self.popup = popup + self.isPresented = isPresented } /// The `Popup` to display. private var popup: Popup + /// A Boolean value specifying whether a "close" button should be shown or not. If the "close" + /// button is shown, you should pass in the `isPresented` argument to the initializer, + /// so that the the "close" button can close the view. + private var showCloseButton = false + /// A Boolean value indicating whether the popup's elements have been evaluated via /// the `popup.evaluateExpressions()` method. @State private var isPopupEvaluated: Bool? = nil @@ -32,14 +40,29 @@ public struct PopupView: View { /// The results of calling the `popup.evaluateExpressions()` method. @State private var expressionEvaluations: [PopupExpressionEvaluation]? = nil + /// A binding to a Boolean value that determines whether the view is presented. + private var isPresented: Binding? + public var body: some View { VStack(alignment: .leading) { - if !popup.title.isEmpty { - Text(popup.title) - .font(.title) - .fontWeight(.bold) - Divider() + HStack { + if !popup.title.isEmpty { + Text(popup.title) + .font(.title) + .fontWeight(.bold) + } + Spacer() + if showCloseButton { + Button(action: { + isPresented?.wrappedValue = false + }, label: { + Image(systemName: "xmark.circle") + .foregroundColor(.secondary) + .padding([.top, .bottom, .trailing], 4) + }) + } } + Divider() Group { if let isPopupEvaluated = isPopupEvaluated { if isPopupEvaluated { @@ -90,3 +113,17 @@ public struct PopupView: View { } } } + +extension PopupView { + /// Specifies whether a "close" button should be shown to the right of the popup title. If the "close" + /// button is shown, you should pass in the `isPresented` argument to the `PopupView` + /// initializer, so that the the "close" button can close the view. + /// Defaults to `false`. + /// - Parameter newShowCloseButton: The new value. + /// - Returns: A new `PopupView`. + public func showCloseButton(_ newShowCloseButton: Bool) -> Self { + var copy = self + copy.showCloseButton = newShowCloseButton + return copy + } +} From 7dd6ec542535e07dca44409312b64dc8428d25ae Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 19 Sep 2022 11:09:36 -0500 Subject: [PATCH 1540/1722] Dark mode support through CSS. --- .../Components/Popups/HTMLTextView.swift | 22 +++++++++++++++++++ .../Popups/TextPopupElementView.swift | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 2d8abdbe2..d66078537 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -45,10 +45,32 @@ struct HTMLTextView: UIViewRepresentable { } body { margin:10px; padding:0px; + background: var(--body-bg); + color: var(--body-color); } img { max-width: 100%; } + a { + color: var(--link-color); + } + + diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index 8c64f8ae5..60336b8e1 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -35,7 +35,7 @@ struct TextPopupElementView: View { } else { roundedRect - .stroke(Color.black, lineWidth: 1) + .stroke(Color.primary, lineWidth: 1) } } Divider() From 8e7a539e79f600a8c7b3c9d867c8fd7258328eaf Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 19 Sep 2022 13:44:36 -0500 Subject: [PATCH 1541/1722] Apply suggestions from code review Co-authored-by: David Feinzimer --- .../Components/FloatingPanel/FloatingPanel.swift | 2 +- .../Components/Popups/MediaPopupElementView.swift | 2 +- Sources/ArcGISToolkit/Components/Popups/PopupView.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index dd6cf8897..bf43cf5eb 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -59,7 +59,7 @@ struct FloatingPanel: View where Content: View { /// The height of the content. @State private var height: CGFloat = .minHeight - + /// A binding to a Boolean value that determines whether the view is presented. private var isPresented: Binding diff --git a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift index 3c7beb5ae..ac9c4a7d0 100644 --- a/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/MediaPopupElementView.swift @@ -20,7 +20,7 @@ struct MediaPopupElementView: View { var popupElement: MediaPopupElement @State var isExpanded: Bool = true - + var body: some View { if displayableMediaCount > 0 { DisclosureGroup(isExpanded: $isExpanded) { diff --git a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift index 2909a39d1..6af0c8315 100644 --- a/Sources/ArcGISToolkit/Components/Popups/PopupView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/PopupView.swift @@ -28,7 +28,7 @@ public struct PopupView: View { /// The `Popup` to display. private var popup: Popup - /// A Boolean value specifying whether a "close" button should be shown or not. If the "close" + /// A Boolean value specifying whether a "close" button should be shown or not. If the "close" /// button is shown, you should pass in the `isPresented` argument to the initializer, /// so that the the "close" button can close the view. private var showCloseButton = false @@ -115,7 +115,7 @@ public struct PopupView: View { } extension PopupView { - /// Specifies whether a "close" button should be shown to the right of the popup title. If the "close" + /// Specifies whether a "close" button should be shown to the right of the popup title. If the "close" /// button is shown, you should pass in the `isPresented` argument to the `PopupView` /// initializer, so that the the "close" button can close the view. /// Defaults to `false`. From d3abc16555659b62a64b0171c9c664e48cc38f85 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 19 Sep 2022 13:51:41 -0500 Subject: [PATCH 1542/1722] Update Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift Co-authored-by: David Feinzimer --- .../ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift index bf43cf5eb..c5078a47d 100644 --- a/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift +++ b/Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift @@ -77,6 +77,7 @@ struct FloatingPanel: View where Content: View { VStack(spacing: 0) { if isCompact { makeHandleView() + Divider() } content .frame(height: height) From d1d98953831296022da79d3acc743f49dfd94aa5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 19 Sep 2022 17:47:34 -0500 Subject: [PATCH 1543/1722] Update Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift Co-authored-by: David Feinzimer --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index d66078537..c873938be 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -44,7 +44,8 @@ struct HTMLTextView: UIViewRepresentable { word-wrap:break-word; font-family:-apple-system; font:-apple-system-subheadline; } body { - margin:10px; padding:0px; + margin: 10px; + padding:0px; background: var(--body-bg); color: var(--body-color); } From 0a272530ba0c19acb483c5e0ab907c2beed24296 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 21 Sep 2022 09:42:09 -0700 Subject: [PATCH 1544/1722] Changes --- .../ArcGISToolkit/Components/Search/LocatorSearchSource.swift | 4 ++-- .../Components/Search/SmartLocatorSearchSource.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift index e057f5774..5601e4745 100644 --- a/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/LocatorSearchSource.swift @@ -101,7 +101,7 @@ public class LocatorSearchSource: ObservableObject, SearchSource { let geocodeResults = try await locatorTask.geocode( forSuggestResult: suggestResult, - parameters: geocodeParameters + using: geocodeParameters ) // Convert to an array of `SearchResult` objects and return. @@ -140,7 +140,7 @@ private extension LocatorSearchSource { let geocodeResults = try await locatorTask.geocode( forSearchText: query, - parameters: geocodeParameters + using: geocodeParameters ) // Convert to SearchResults and return. diff --git a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift index 235e5e50d..97b7ec270 100644 --- a/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift +++ b/Sources/ArcGISToolkit/Components/Search/SmartLocatorSearchSource.swift @@ -81,7 +81,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { geocodeParameters.searchArea = nil let geocodeResults = try await locatorTask.geocode( forSearchText: query, - parameters: geocodeParameters + using: geocodeParameters ) // Union results and return. @@ -120,7 +120,7 @@ public class SmartLocatorSearchSource: LocatorSearchSource { geocodeParameters.searchArea = nil let geocodeResults = try await locatorTask.geocode( forSuggestResult: suggestResult, - parameters: geocodeParameters + using: geocodeParameters ) // Union results and return. From bff95ea40678ad4f40ab2769399e28750fe082d3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 21 Sep 2022 14:13:07 -0500 Subject: [PATCH 1545/1722] Turns out black isn't black, but #1C1C1E, when in DarkMode. --- Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index c873938be..9d5c2d922 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -60,14 +60,14 @@ struct HTMLTextView: UIViewRepresentable { /* Light mode */ :root { --body-bg: #FFFFFF; - --body-color: #000000; + --body-color: #1C1C1E; --link-color: #0164C8; } /* Dark mode */ @media (prefers-color-scheme: dark) { :root { - --body-bg: #000000; + --body-bg: #1C1C1E; --body-color: #FFFFFF; --link-color: #1796FA; } From 483894bf7ea434c838e7cb251864be6e1eec9ee2 Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Wed, 21 Sep 2022 15:48:48 -0700 Subject: [PATCH 1546/1722] Update UtilityNetworkTraceViewModelTests.swift --- .../UtilityNetworkTraceViewModelTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift index 641e28bdf..a1caef110 100644 --- a/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/UtilityNetworkTraceViewModelTests.swift @@ -147,7 +147,7 @@ import XCTest let parameters = QueryParameters() parameters.addObjectID(171) - let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let result = try await layer.featureTable?.queryFeatures(using: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) XCTAssertEqual(features.count, 1) @@ -196,7 +196,7 @@ import XCTest let parameters = QueryParameters() parameters.addObjectID(463) - let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let result = try await layer.featureTable?.queryFeatures(using: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) XCTAssertEqual(features.count, 1) @@ -255,7 +255,7 @@ import XCTest let parameters = QueryParameters() parameters.addObjectID(177) - let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let result = try await layer.featureTable?.queryFeatures(using: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) XCTAssertEqual(features.count, 1) @@ -304,7 +304,7 @@ import XCTest let parameters = QueryParameters() parameters.addObjectID(171) - let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let result = try await layer.featureTable?.queryFeatures(using: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) XCTAssertEqual(features.count, 1) @@ -357,7 +357,7 @@ import XCTest let parameters = QueryParameters() parameters.addObjectID(171) - let result = try await layer.featureTable?.queryFeatures(parameters: parameters) + let result = try await layer.featureTable?.queryFeatures(using: parameters) let features = try XCTUnwrap(result?.features().compactMap { $0 }) XCTAssertEqual(features.count, 1) From 29e7e5c7c2f3f966ab38087fe23b7f3b5ae919df Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 27 Sep 2022 12:40:26 -0700 Subject: [PATCH 1547/1722] Update UtilityNetworkTraceViewModel.swift --- .../UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift index 166eb68de..7e9ebba28 100644 --- a/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift +++ b/Sources/ArcGISToolkit/Components/UtilityNetworkTrace/UtilityNetworkTraceViewModel.swift @@ -359,7 +359,7 @@ import SwiftUI let traceResults: [UtilityTraceResult] do { - traceResults = try await network.trace(traceParameters: parameters) + traceResults = try await network.trace(using: parameters) } catch(let serviceError as ServiceError) { if let reason = serviceError.failureReason { userAlert = .init(description: reason) From 6969002dbba81edc6b264db2b6385c90c6cc5c1b Mon Sep 17 00:00:00 2001 From: David Feinzimer Date: Tue, 27 Sep 2022 12:40:30 -0700 Subject: [PATCH 1548/1722] Update SearchViewModelTests.swift --- .../SearchViewModelTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift index 5067c09e1..1204bd749 100644 --- a/Tests/ArcGISToolkitTests/SearchViewModelTests.swift +++ b/Tests/ArcGISToolkitTests/SearchViewModelTests.swift @@ -452,19 +452,19 @@ extension SearchViewModelTests { extension Polygon { static let chippewaFalls: Polygon = { let builder = PolygonBuilder(spatialReference: .wgs84) - _ = builder.add(point: Point(x: -91.59127653822401, y: 44.74770908213401, spatialReference: .wgs84)) - _ = builder.add(point: Point(x: -91.19322516572637, y: 44.74770908213401, spatialReference: .wgs84)) - _ = builder.add(point: Point(x: -91.19322516572637, y: 45.116100854348254, spatialReference: .wgs84)) - _ = builder.add(point: Point(x: -91.59127653822401, y: 45.116100854348254, 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() }() static let minneapolis: Polygon = { let builder = PolygonBuilder(spatialReference: .wgs84) - _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - _ = builder.add(point: Point(x: -94.170821328662, y: 44.13656401114444, spatialReference: .wgs84)) - _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, spatialReference: .wgs84)) - _ = builder.add(point: Point(x: -92.34544467133114, y: 45.824325577904446, 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() }() } From f74d34548ff323fd5104657e20dfe58440756775 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 27 Sep 2022 16:13:20 -0500 Subject: [PATCH 1549/1722] Remove RoundRectangle border from around HTML text element. This is based on feedback from the team. --- .../Components/Popups/TextPopupElementView.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift index 60336b8e1..ab104c690 100644 --- a/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/TextPopupElementView.swift @@ -24,19 +24,13 @@ struct TextPopupElementView: View { var body: some View { if !popupElement.text.isEmpty { - let roundedRect = RoundedRectangle(cornerRadius: 8) ZStack { HTMLTextView(html: popupElement.text, height: $webViewHeight) - .clipShape(roundedRect) .frame(height: webViewHeight) if webViewHeight == .zero { // Show `ProgressView` until `HTMLTextView` has set the height. ProgressView() } - else { - roundedRect - .stroke(Color.primary, lineWidth: 1) - } } Divider() } From 923db8868cf895f2a83cf313bace974000f2f5be Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 28 Sep 2022 16:31:34 -0500 Subject: [PATCH 1550/1722] Make HTML text's background transparent --- .../ArcGISToolkit/Components/Popups/HTMLTextView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift index 9d5c2d922..d79d71089 100644 --- a/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift +++ b/Sources/ArcGISToolkit/Components/Popups/HTMLTextView.swift @@ -59,15 +59,15 @@ struct HTMLTextView: UIViewRepresentable {

`$G1(;r~^2=jgs8PSN^F~(!)?fKK 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 literal 0 HcmV?d00001 diff --git a/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json b/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json new file mode 100644 index 000000000..17f6fd948 --- /dev/null +++ b/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Image.png b/Examples/ExamplesApp/Assets.xcassets/Terrain.imageset/Image.png new file mode 100644 index 0000000000000000000000000000000000000000..a719c929c15a27a026b242d71e3d84ee8b1f73d4 GIT binary patch literal 52275 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 z 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

`eX2V(OTYn&&pJO+p9?Bs0k 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 TM&jjg00000NkvXXu0mjfCE%H{ literal 0 HcmV?d00001 diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index c2b3f87db..1278ac3d2 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -1,8 +1,13 @@ # OverviewMap -`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be superimposed on an existing `MapView`/`SceneView`. `OverviewMap` shows shows a representation of the current `Viewpoint` of the `GeoView` it is connected to. +`OverviewMap` is a small, secondary `MapView` (sometimes called an inset map), that can be overlayed on an existing `GeoView` (`MapView` or `SceneView`). `OverviewMap` shows shows a representation of the current `visibleArea` (for a `MapView`) or `viewpoint` (for a `SceneView`) of the `GeoView` it is connected to. + +Map View +![OverviewMap - MapView](./OverviewMap_MapView.png) + +SceneView +![OverviewMap - SceneView](./OverviewMap_SceneView.png.png) -![OverviewMap](https://user-images.githubusercontent.com/29742178/121975740-34f07000-cd37-11eb-9162-462925cb3fe7.png) > **NOTE**: OverviewMap uses metered ArcGIS basemaps by default, so you will need to configure an API key. See [Security and authentication documentation](https://developers.arcgis.com/documentation/mapping-apis-and-services/security/#api-keys) for more information. @@ -10,18 +15,17 @@ OverviewMap: -- Displays a representation of the current `Viewpoint` for a connected `GeoView`. +- 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 `Viewpoint` representation (a `FillSymbol` for a connected `MapView`; a `MarkerSymbol` for a connected `SceneView`). +- Supports a configurable symbol for visualizing the current `VisibleArea`/`Viewpoint` representation (a `FillSymbol` for a connected `MapView`; a `MarkerSymbol` for a connected `SceneView`). ## Key properties `OverviewMap` has the following customizable properties: -- `geoView` - References the connected `MapView` or `SceneView`. -- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. -- `scaleFactor` - Defines the scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The default is 25. - `map` - the `Map` displayed in the `OverviewMap`. For example, you can use `map` to change the base map displayed by the `OverviewMap`. +- `scaleFactor` - Defines the scale of the `OverviewMap` relative to the scale of the connected `GeoView`. The default is 25. +- `symbol` - Defines the symbol used to visualize the current `Viewpoint` . This is a red rectangle by default for a `MapView`; for a `SceneView`, this is a red cross. ## Behavior: @@ -31,8 +35,114 @@ For an `OverviewMap` on a `SceneView`, the center point of the `SceneView`'s `cu ## Usage +###Basic usage for overlaying a `MapView` + +Note that for `MapView`'s, you need to provide the `OverviewMap both a viewpoint and visibleArea, as well as providing a `FillSymbol` if you want to customize the display of the visible area. + +```swift +let map = Map(basemap: .imageryWithLabels()) + +@State +private var viewpoint: Viewpoint? + +@State +private var visibleArea: ArcGIS.Polygon? + +var body: some View { + MapView(map: map) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onVisibleAreaChanged { visibleArea = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint, + visibleArea: visibleArea + ) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) +} +``` + +### Displaying a custom fill symbol for a `MapView`'s visible area. + +```swift +let map = Map(basemap: .imageryWithLabels()) + +@State +private var viewpoint: Viewpoint? + +@State +private var visibleArea: ArcGIS.Polygon? + +var body: some View { + MapView(map: map) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .onVisibleAreaChanged { visibleArea = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint, + visibleArea: visibleArea, + symbol: SimpleFillSymbol( + style: .solid, + color: .clear, + outline: SimpleLineSymbol( + style: .solid, + color: .blue, + width: 2.0 + ) + ) + ) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) +} +``` + +###Basic usage for overlaying a `SceneView` + +Note that for `SceneView`'s, you need to provide the `OverviewMap only a viewpoint, as well as providing a `MarkerSymbol` if you want to customize the display of the viewpoint. + +```swift +let scene = Scene(basemap: .imageryWithLabels()) + +@State +private var viewpoint: Viewpoint? + +var body: some View { + SceneView(scene: scene) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) +} +``` + +### Displaying a custom marker symbol for a `SceneView`'s viewpoint. + ```swift -// Waiting on final implementation details +let scene = Scene(basemap: .imageryWithLabels()) + +@State +private var viewpoint: Viewpoint? + +var body: some View { + SceneView(scene: scene) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint, + symbol: SimpleMarkerSymbol(style: .x, + color: .blue, + size: 24.0 + ) + ) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) +} ``` To see it in action, try out the [Examples](../../Examples) and refer to [OverviewMapExampleView.swift](../../Examples/Examples/OverviewMapExampleView.swift) in the project. diff --git a/README.md b/README.md index 75286681e..c171ed48d 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ The ArcGIS Runtime SDK for Swift Toolkit contains components that will simplify your iOS app development. It is built off of the new Swift ArcGIS Runtime SDK. -Examples and Documentation are coming soon. - To use Toolkit in your project: * **[Install with Swift Package Manager](#swift-package-manager)** - Add `https://github.com/ArcGIS/arcgis-runtime-toolkit-swift` as the package repository URL. @@ -16,9 +14,9 @@ To use Toolkit in your project: ## Requirements * ArcGIS Runtime SDK for Swift -* Xcode 12.0 (or higher) +* Xcode 13.0 (or higher) -The *ArcGIS Runtime Toolkit for Swift* has a *Target SDK* version of *13.0*, meaning that it can run on devices with *iOS 13.0* or newer. +The *ArcGIS Runtime Toolkit for Swift* has a *Target SDK* version of *15.0*, meaning that it can run on devices with *iOS 15.0* or newer. ## Instructions @@ -29,7 +27,7 @@ The *ArcGIS Runtime Toolkit for Swift* has a *Target SDK* version of *13.0*, mea 3. Enter `https://github.com/ArcGIS/arcgis-runtime-toolkit-swift` as the package repository URL 4. ~Choose version 100.12.0 or a later version. Click Next. Only version 100.12.0 or newer supports Swift Package Manager.~ - Note: The Toolkit Swift Package adds the ArcGIS SDK Swift Package as a dependency so no need to add both separately. If you already have the ArcGIS SDK Swift Package delete that and just add the Toolkit Swift Package. + Note: The Toolkit Swift Package adds the ArcGIS SDK Swift Package as a dependency so there's no need to add both separately. If you already have the ArcGIS SDK Swift Package, delete it and just add the Toolkit Swift Package. New to Swift Package Manager? Visit [swift.org/package-manager/](https://swift.org/package-manager/). diff --git a/Sources/ArcGISToolkit/Components/OverviewMap.swift b/Sources/ArcGISToolkit/Components/OverviewMap.swift index 9ff039160..0ef12b000 100644 --- a/Sources/ArcGISToolkit/Components/OverviewMap.swift +++ b/Sources/ArcGISToolkit/Components/OverviewMap.swift @@ -24,15 +24,6 @@ public struct OverviewMap: View { /// The visible area of the main `GeoView`. let visibleArea: Polygon? - /// The `Graphic` displaying the visible area of the main `GeoView`. - @StateObject var graphic: Graphic - - /// The `GraphicsOverlay` used to display the visible area graphic. - @StateObject var graphicsOverlay: GraphicsOverlay - - /// The `Map` displayed in the `OverviewMap`. - @StateObject var map: Map = Map(basemap: .topographic()) - /// The `Symbol` used to display the main `GeoView` visible area. For MapViews, /// the symbol will be a FillSymbol used to display the GeoView visible area. For SceneViews, /// the symbol will be a MarkerSymbol, used to display the current viewpoint's center. @@ -44,6 +35,19 @@ public struct OverviewMap: View { /// at the a scale equal to: `viewpoint.targetscale` x `scaleFactor. private(set) var scaleFactor: Double + /// The `Map` displayed in the `OverviewMap`. + @StateObject + var map: Map = Map(basemap: .topographic()) + + /// The `Graphic` displaying the visible area of the main `GeoView`. + @StateObject + private var graphic: Graphic + + /// The `GraphicsOverlay` used to display the visible area graphic. + @StateObject + private var graphicsOverlay: GraphicsOverlay + + /// The default fill symbol. private let fillSymbol: FillSymbol = SimpleFillSymbol( style: .solid, color: .clear, @@ -54,6 +58,7 @@ public struct OverviewMap: View { ) ) + /// The default marker symbol. private let markerSymbol: MarkerSymbol = SimpleMarkerSymbol(style: .cross, color: .red, size: 12.0 From 59b51057f91e1109a6429239b8232d98becd4f9c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Jul 2021 16:23:27 -0500 Subject: [PATCH 0075/1722] doc fixes; add info on scaleFactor and .map modifier --- Documentation/OverviewMap/README.md | 56 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 1278ac3d2..88ff9d293 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -35,9 +35,9 @@ For an `OverviewMap` on a `SceneView`, the center point of the `SceneView`'s `cu ## Usage -###Basic usage for overlaying a `MapView` +### Basic usage for overlaying a `MapView` -Note that for `MapView`'s, you need to provide the `OverviewMap both a viewpoint and visibleArea, as well as providing a `FillSymbol` if you want to customize the display of the visible area. +Note that for `MapView`'s, you need to provide the `OverviewMap` both a viewpoint and visibleArea, as well as providing a `FillSymbol` if you want to customize the display of the visible area. ```swift let map = Map(basemap: .imageryWithLabels()) @@ -63,7 +63,7 @@ var body: some View { } ``` -### Displaying a custom fill symbol for a `MapView`'s visible area. +### Displaying a custom fill symbol and scale factor for a `MapView`'s visible area. ```swift let map = Map(basemap: .imageryWithLabels()) @@ -89,7 +89,8 @@ var body: some View { color: .blue, width: 2.0 ) - ) + ), + scaleFactor: 32.0 ) .frame(width: 200, height: 132) .padding(), @@ -98,9 +99,9 @@ var body: some View { } ``` -###Basic usage for overlaying a `SceneView` +### Basic usage for overlaying a `SceneView` -Note that for `SceneView`'s, you need to provide the `OverviewMap only a viewpoint, as well as providing a `MarkerSymbol` if you want to customize the display of the viewpoint. +Note that for `SceneView`'s, you need to provide the `OverviewMap` only a viewpoint, as well as providing a `MarkerSymbol` if you want to customize the display of the viewpoint. ```swift let scene = Scene(basemap: .imageryWithLabels()) @@ -120,29 +121,32 @@ var body: some View { } ``` -### Displaying a custom marker symbol for a `SceneView`'s viewpoint. +### Displaying a custom marker symbol and scale factor for a `SceneView`'s viewpoint. ```swift -let scene = Scene(basemap: .imageryWithLabels()) - -@State -private var viewpoint: Viewpoint? +SceneView(scene: scene) + .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } + .overlay( + OverviewMap(viewpoint: viewpoint, + symbol: SimpleMarkerSymbol(style: .x, + color: .blue, + size: 24.0 + ), + scaleFactor: 32.0 + ) + .frame(width: 200, height: 132) + .padding(), + alignment: .topTrailing + ) +``` -var body: some View { - SceneView(scene: scene) - .onViewpointChanged(type: .centerAndScale) { viewpoint = $0 } - .overlay( - OverviewMap(viewpoint: viewpoint, - symbol: SimpleMarkerSymbol(style: .x, - color: .blue, - size: 24.0 - ) - ) - .frame(width: 200, height: 132) - .padding(), - alignment: .topTrailing - ) -} +### Changing the map used by the `OverviewMap` + +To change the `OverviewMap`'s `map`, you use the `.map()` modifier on `OverviewMap` + +```swift +OverviewMap(viewpoint: viewpoint) + .map(Map(basemap: .darkGrayCanvasVector())) ``` To see it in action, try out the [Examples](../../Examples) and refer to [OverviewMapExampleView.swift](../../Examples/Examples/OverviewMapExampleView.swift) in the project. From 0f6748d808bba57463d7e02199b44083fbe75e34 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Jul 2021 16:26:16 -0500 Subject: [PATCH 0076/1722] Periods and typos and minor updates. --- Documentation/OverviewMap/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/OverviewMap/README.md b/Documentation/OverviewMap/README.md index 88ff9d293..777e7a15f 100644 --- a/Documentation/OverviewMap/README.md +++ b/Documentation/OverviewMap/README.md @@ -63,7 +63,7 @@ var body: some View { } ``` -### Displaying a custom fill symbol and scale factor for a `MapView`'s visible area. +### Displaying a custom fill symbol and scale factor for a `MapView`'s visible area ```swift let map = Map(basemap: .imageryWithLabels()) @@ -121,7 +121,7 @@ var body: some View { } ``` -### Displaying a custom marker symbol and scale factor for a `SceneView`'s viewpoint. +### Displaying a custom marker symbol and scale factor for a `SceneView`'s viewpoint ```swift SceneView(scene: scene) @@ -142,12 +142,12 @@ SceneView(scene: scene) ### Changing the map used by the `OverviewMap` -To change the `OverviewMap`'s `map`, you use the `.map()` modifier on `OverviewMap` +To change the `OverviewMap`'s `map`, you use the `.map()` modifier on `OverviewMap`. ```swift OverviewMap(viewpoint: viewpoint) .map(Map(basemap: .darkGrayCanvasVector())) ``` -To see it in action, try out the [Examples](../../Examples) and refer to [OverviewMapExampleView.swift](../../Examples/Examples/OverviewMapExampleView.swift) in the project. +To see the `OverviewMap` in action, try out the [Examples](../../Examples) and refer to [OverviewMapExampleView.swift](../../Examples/Examples/OverviewMapExampleView.swift) in the project. From 228ce8efe494451b449f3f5aa3cdbbea928d59cb Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 6 Jul 2021 10:32:44 -0500 Subject: [PATCH 0077/1722] Remove watermark from screenshots --- .../OverviewMap/OverviewMap_MapView.png | Bin 885203 -> 142295 bytes .../OverviewMap/OverviewMap_SceneView.png | Bin 888604 -> 137336 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Documentation/OverviewMap/OverviewMap_MapView.png b/Documentation/OverviewMap/OverviewMap_MapView.png index 56353d45835178491e2c5ac08bdfbc3ba36927c7..496e5d7f26aa11a18dcab3425c967e68d37bf9ab 100644 GIT binary patch literal 142295 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 z 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