Skip to content

Commit 7c63f93

Browse files
authored
Merge pull request #286 from Esri/mhd/RefactorCompass_action
Mhd/refactor compass action
2 parents 8f5dadc + 2bc8a8c commit 7c63f93

File tree

7 files changed

+260
-47
lines changed

7 files changed

+260
-47
lines changed

Documentation/Compass/README.md

+11-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ The ArcGIS Maps SDK for Swift currently supports rotating MapViews and SceneView
1010

1111
Compass:
1212

13-
- Can be configured to automatically hide when the rotation is zero.
13+
- Automatically hides when the rotation is zero.
14+
- Can be configured to be always visible.
1415
- Will reset the map/scene rotation to North when tapped on.
1516

1617
## Key properties
@@ -23,9 +24,8 @@ Compass:
2324
/// direction toward true East, etc.).
2425
/// - Parameters:
2526
/// - heading: The heading of the compass.
26-
/// - autoHide: A Boolean value that determines whether the compass
27-
/// automatically hides itself when the heading is `0`.
28-
public init(heading: Binding<Double>, autoHide: Bool = true)
27+
/// - action: An action to perform when the compass is tapped.
28+
public init(heading: Binding<Double>, action: (() -> Void)? = nil)
2929
```
3030

3131
```swift
@@ -35,27 +35,26 @@ Compass:
3535
/// - Parameters:
3636
/// - viewpointRotation: The viewpoint rotation whose value determines the
3737
/// heading of the compass.
38-
/// - autoHide: A Boolean value that determines whether the compass
39-
/// automatically hides itself when the viewpoint rotation is 0 degrees.
40-
public init(viewpointRotation: Binding<Double>, autoHide: Bool = true)
38+
/// - action: An action to perform when the compass is tapped.
39+
public init(viewpointRotation: Binding<Double>, action: (() -> Void)? = nil)
4140
```
4241

4342
```swift
4443
/// Creates a compass with a binding to an optional viewpoint.
4544
/// - Parameters:
4645
/// - viewpoint: The viewpoint whose rotation determines the heading of the compass.
47-
/// - autoHide: A Boolean value that determines whether the compass automatically hides itself
48-
/// when the viewpoint's rotation is 0 degrees.
49-
public init(viewpoint: Binding<Viewpoint?>, autoHide: Bool = true)
46+
/// - action: An action to perform when the compass is tapped.
47+
public init(viewpoint: Binding<Viewpoint?>, action: (() -> Void)? = nil)
5048
```
5149

52-
`Compass` has the following modifier:
50+
`Compass` has the following modifiers:
5351

5452
- `func compassSize(size: CGFloat)` - The size of the `Compass`, specifying both the width and height of the compass.
53+
- `func automaticallyHides(_:)` - Specifies whether the ``Compass`` should automatically hide when the heading is 0.
5554

5655
## Behavior:
5756

58-
Whenever the map is not orientated North (non-zero bearing) the compass appears. When reset to north, it disappears. An initializer argument allows you to disable the auto-hide feature so that it always appears.
57+
Whenever the map is not orientated North (non-zero bearing) the compass appears. When reset to north, it disappears. The `automaticallyHides` view modifier allows you to disable the auto-hide feature so that it is always displayed.
5958

6059
When the compass is tapped, the map orients back to north (zero bearing).
6160

Examples/Examples/CompassExampleView.swift

+105-11
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,121 @@ import ArcGIS
1515
import ArcGISToolkit
1616
import SwiftUI
1717

18+
/// An example demonstrating how to use a compass in three different environments.
1819
struct CompassExampleView: View {
19-
/// The data model containing the `Map` displayed in the `MapView`.
20-
@StateObject private var dataModel = MapDataModel(
21-
map: Map(basemapStyle: .arcGISImagery)
22-
)
20+
/// A scenario represents a type of environment a compass may be used in.
21+
enum Scenario: String {
22+
case map
23+
case scene
24+
}
25+
26+
/// The active scenario.
27+
@State private var scenario = Scenario.map
28+
29+
var body: some View {
30+
Group {
31+
switch scenario {
32+
case .map:
33+
MapWithViewpoint()
34+
case .scene:
35+
SceneWithCameraController()
36+
}
37+
}
38+
.toolbar {
39+
ToolbarItem(placement: .navigationBarTrailing) {
40+
Menu(scenario.rawValue.capitalized) {
41+
Button {
42+
scenario = .map
43+
} label: {
44+
Label("Map", systemImage: "map.fill")
45+
}
46+
47+
Button {
48+
scenario = .scene
49+
} label: {
50+
Label("Scene", systemImage: "globe.americas.fill")
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
58+
/// An example demonstrating how to use a compass with a map view.
59+
struct MapWithViewpoint: View {
60+
/// The `Map` displayed in the `MapView`.
61+
@State private var map = Map(basemapStyle: .arcGISImagery)
2362

2463
/// Allows for communication between the Compass and MapView or SceneView.
2564
@State private var viewpoint: Viewpoint? = Viewpoint(
26-
center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84),
65+
center: .esriRedlands,
2766
scale: 10_000,
2867
rotation: -45
2968
)
3069

3170
var body: some View {
32-
MapView(map: dataModel.map, viewpoint: viewpoint)
33-
.onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 }
34-
.overlay(alignment: .topTrailing) {
35-
Compass(viewpoint: $viewpoint)
36-
// Optionally provide a different size for the compass.
37-
// .compassSize(size: <#T##CGFloat#>)
71+
MapViewReader { proxy in
72+
MapView(map: map, viewpoint: viewpoint)
73+
.onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 }
74+
.overlay(alignment: .topTrailing) {
75+
Compass(viewpoint: $viewpoint) {
76+
guard let viewpoint else { return }
77+
Task {
78+
// Animate the map view to zero when the compass is tapped.
79+
await proxy.setViewpoint(
80+
viewpoint.withRotation(0),
81+
duration: 0.25
82+
)
83+
}
84+
}
3885
.padding()
86+
}
87+
}
88+
}
89+
}
90+
91+
/// An example demonstrating how to use a compass with a scene view and camera controller.
92+
struct SceneWithCameraController: View {
93+
/// The data model containing the `Scene` displayed in the `SceneView`.
94+
@State private var scene = Scene(basemapStyle: .arcGISImagery)
95+
96+
/// The current heading as reported by the scene view.
97+
@State private var heading = Double.zero
98+
99+
/// The orbit location camera controller used by the scene view.
100+
private let cameraController = OrbitLocationCameraController(
101+
target: .esriRedlands,
102+
distance: 10_000
103+
)
104+
105+
var body: some View {
106+
SceneView(scene: scene, cameraController: cameraController)
107+
.onCameraChanged { newCamera in
108+
heading = newCamera.heading.rounded()
109+
}
110+
.overlay(alignment: .topTrailing) {
111+
Compass(viewpointRotation: $heading) {
112+
// Animate the scene view when the compass is tapped.
113+
Task {
114+
await cameraController.moveCamera(
115+
distanceDelta: .zero,
116+
headingDelta: heading > 180 ? 360 - heading : -heading,
117+
pitchDelta: .zero,
118+
duration: 0.3
119+
)
120+
}
121+
}
122+
.padding()
39123
}
40124
}
41125
}
126+
127+
private extension Point {
128+
static var esriRedlands: Point {
129+
.init(
130+
x: -117.19494,
131+
y: 34.05723,
132+
spatialReference: .wgs84
133+
)
134+
}
135+
}

Examples/Examples/PopupExampleView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct PopupExampleView: View {
1919
static func makeMap() -> Map {
2020
let portalItem = PortalItem(
2121
portal: .arcGISOnline(connection: .anonymous),
22-
id: Item.ID(rawValue: "9f3a674e998f461580006e626611f9ad")!
22+
id: Item.ID("9f3a674e998f461580006e626611f9ad")!
2323
)
2424
return Map(item: portalItem)
2525
}

Sources/ArcGISToolkit/Components/Compass/Compass.swift

+34-18
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,24 @@ import SwiftUI
1717
/// A `Compass` (alias North arrow) shows where north is in a `MapView` or
1818
/// `SceneView`.
1919
public struct Compass: View {
20-
/// A Boolean value indicating whether the compass should automatically
21-
/// hide/show itself when the heading is `0`.
22-
private let autoHide: Bool
23-
2420
/// The opacity of the compass.
2521
@State private var opacity: Double = .zero
2622

23+
/// An action to perform when the compass is tapped.
24+
private let action: (() -> Void)?
25+
26+
/// A Boolean value indicating whether the compass should automatically
27+
/// hide/show itself when the heading is `0`.
28+
private var autoHide: Bool = true
29+
2730
/// A Boolean value indicating whether the compass should hide based on the
2831
/// current heading and whether the compass automatically hides.
2932
var shouldHide: Bool {
3033
(heading.isZero || heading.isNaN) && autoHide
3134
}
3235

3336
/// The width and height of the compass.
34-
var size: CGFloat = 44
37+
private var size: CGFloat = 44
3538

3639
/// The heading of the compass in degrees.
3740
@Binding private var heading: Double
@@ -41,14 +44,13 @@ public struct Compass: View {
4144
/// direction toward true East, etc.).
4245
/// - Parameters:
4346
/// - heading: The heading of the compass.
44-
/// - autoHide: A Boolean value that determines whether the compass
45-
/// automatically hides itself when the heading is `0`.
47+
/// - action: An action to perform when the compass is tapped.
4648
public init(
4749
heading: Binding<Double>,
48-
autoHide: Bool = true
50+
action: (() -> Void)? = nil
4951
) {
5052
_heading = heading
51-
self.autoHide = autoHide
53+
self.action = action
5254
}
5355

5456
public var body: some View {
@@ -60,16 +62,22 @@ public struct Compass: View {
6062
}
6163
.aspectRatio(1, contentMode: .fit)
6264
.opacity(opacity)
63-
.onTapGesture { heading = .zero }
6465
.frame(width: size, height: size)
66+
.onAppear { opacity = shouldHide ? 0 : 1 }
6567
.onChange(of: heading) { _ in
6668
let newOpacity: Double = shouldHide ? .zero : 1
6769
guard opacity != newOpacity else { return }
6870
withAnimation(.default.delay(shouldHide ? 0.25 : 0)) {
6971
opacity = newOpacity
7072
}
7173
}
72-
.onAppear { opacity = shouldHide ? 0 : 1 }
74+
.onTapGesture {
75+
if let action {
76+
action()
77+
} else {
78+
heading = .zero
79+
}
80+
}
7381
.accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)")
7482
}
7583
}
@@ -82,28 +90,27 @@ public extension Compass {
8290
/// - Parameters:
8391
/// - viewpointRotation: The viewpoint rotation whose value determines the
8492
/// heading of the compass.
85-
/// - autoHide: A Boolean value that determines whether the compass
86-
/// automatically hides itself when the viewpoint rotation is 0 degrees.
93+
/// - action: An action to perform when the compass is tapped.
8794
init(
8895
viewpointRotation: Binding<Double>,
89-
autoHide: Bool = true
96+
action: (() -> Void)? = nil
9097
) {
9198
let heading = Binding(get: {
9299
viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue
93100
}, set: { newHeading in
94101
viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading
95102
})
96-
self.init(heading: heading, autoHide: autoHide)
103+
self.init(heading: heading, action: action)
97104
}
98105

99106
/// Creates a compass with a binding to an optional viewpoint.
100107
/// - Parameters:
101108
/// - viewpoint: The viewpoint whose rotation determines the heading of the compass.
102-
/// - autoHide: A Boolean value that determines whether the compass automatically hides itself
109+
/// - action: An action to perform when the compass is tapped.
103110
/// when the viewpoint's rotation is 0 degrees.
104111
init(
105112
viewpoint: Binding<Viewpoint?>,
106-
autoHide: Bool = true
113+
action: (() -> Void)? = nil
107114
) {
108115
let viewpointRotation = Binding {
109116
viewpoint.wrappedValue?.rotation ?? .nan
@@ -115,7 +122,7 @@ public extension Compass {
115122
rotation: newViewpointRotation
116123
)
117124
}
118-
self.init(viewpointRotation: viewpointRotation, autoHide: autoHide)
125+
self.init(viewpointRotation: viewpointRotation, action: action)
119126
}
120127

121128
/// Define a custom size for the compass.
@@ -125,4 +132,13 @@ public extension Compass {
125132
copy.size = size
126133
return copy
127134
}
135+
136+
/// Specifies whether the ``Compass`` should automatically hide when the heading is 0.
137+
/// - Parameter flag: A Boolean value indicating whether the compass should automatically
138+
/// hide/show itself when the heading is `0`.
139+
func automaticallyHides(_ flag: Bool) -> some View {
140+
var copy = self
141+
copy.autoHide = flag
142+
return copy
143+
}
128144
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2023 Esri.
2+
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import ArcGIS
15+
16+
public extension Viewpoint {
17+
/// Creates a new viewpoint with the same target geometry and scale but with a new rotation.
18+
/// - Parameter rotation: The rotation for the new viewpoint.
19+
/// - Returns: A new viewpoint.
20+
func withRotation(_ rotation: Double) -> Viewpoint {
21+
switch self.kind {
22+
case .centerAndScale:
23+
return Viewpoint(
24+
center: self.targetGeometry as! Point,
25+
scale: self.targetScale,
26+
rotation: rotation
27+
)
28+
case.boundingGeometry:
29+
return Viewpoint(
30+
boundingGeometry: self.targetGeometry,
31+
rotation: rotation
32+
)
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)