Skip to content

Commit 6a6a288

Browse files
authored
Merge pull request #298 from Esri/df/addCompassProxyParameter
[Compass] Remove `action` parameter, add `mapViewProxy` parameter, support rotation animation
2 parents 4cd5db0 + 786b844 commit 6a6a288

File tree

4 files changed

+92
-268
lines changed

4 files changed

+92
-268
lines changed

Documentation/Compass/README.md

+16-39
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,15 @@ Compass:
1616

1717
## Key properties
1818

19-
`Compass` has the following initializers:
19+
`Compass` has the following initializer:
2020

2121
```swift
22-
/// Creates a compass with a binding to a heading based on compass
23-
/// directions (0° indicates a direction toward true North, 90° indicates a
24-
/// direction toward true East, etc.).
22+
/// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates
23+
/// a direction toward true West, etc.).
2524
/// - Parameters:
26-
/// - heading: The heading of the compass.
27-
/// - action: An action to perform when the compass is tapped.
28-
public init(heading: Binding<Double>, action: (() -> Void)? = nil)
29-
```
30-
31-
```swift
32-
/// Creates a compass with a binding to a viewpoint rotation (0° indicates
33-
/// a direction toward true North, 90° indicates a direction toward true
34-
/// West, etc.).
35-
/// - Parameters:
36-
/// - viewpointRotation: The viewpoint rotation whose value determines the
37-
/// heading of the compass.
38-
/// - action: An action to perform when the compass is tapped.
39-
public init(viewpointRotation: Binding<Double>, action: (() -> Void)? = nil)
40-
```
41-
42-
```swift
43-
/// Creates a compass with a binding to an optional viewpoint.
44-
/// - Parameters:
45-
/// - viewpoint: The viewpoint whose rotation determines the heading of the compass.
46-
/// - action: An action to perform when the compass is tapped.
47-
public init(viewpoint: Binding<Viewpoint?>, action: (() -> Void)? = nil)
25+
/// - rotation: The rotation whose value determines the heading of the compass.
26+
/// - mapViewProxy: The proxy to provide access to map view operations.
27+
public init(rotation: Double?, mapViewProxy: MapViewProxy)
4828
```
4929

5030
`Compass` has the following modifiers:
@@ -63,22 +43,19 @@ When the compass is tapped, the map orients back to north (zero bearing).
6343
### Basic usage for displaying a `Compass`.
6444

6545
```swift
66-
@StateObject var map = Map(basemapStyle: .arcGISImagery)
46+
@State private var map = Map(basemapStyle: .arcGISImagery)
6747

68-
/// Allows for communication between the Compass and MapView or SceneView.
69-
@State private var viewpoint: Viewpoint? = Viewpoint(
70-
center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84),
71-
scale: 10_000,
72-
rotation: -45
73-
)
48+
@State private var viewpoint: Viewpoint?
7449

7550
var body: some View {
76-
MapView(map: map, viewpoint: viewpoint)
77-
.onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 }
78-
.overlay(alignment: .topTrailing) {
79-
Compass(viewpoint: $viewpoint)
80-
.padding()
81-
}
51+
MapViewReader { proxy in
52+
MapView(map: map, viewpoint: viewpoint)
53+
.onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 }
54+
.overlay(alignment: .topTrailing) {
55+
Compass(rotation: viewpoint?.rotation, mapViewProxy: proxy)
56+
.padding()
57+
}
58+
}
8259
}
8360
```
8461

Examples/Examples/CompassExampleView.swift

+9-98
Original file line numberDiff line numberDiff line change
@@ -15,121 +15,32 @@ import ArcGIS
1515
import ArcGISToolkit
1616
import SwiftUI
1717

18-
/// An example demonstrating how to use a compass in three different environments.
19-
struct CompassExampleView: View {
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-
5818
/// An example demonstrating how to use a compass with a map view.
59-
struct MapWithViewpoint: View {
19+
struct CompassExampleView: View {
6020
/// The `Map` displayed in the `MapView`.
6121
@State private var map = Map(basemapStyle: .arcGISImagery)
6222

6323
/// Allows for communication between the Compass and MapView or SceneView.
64-
@State private var viewpoint: Viewpoint? = Viewpoint(
65-
center: .esriRedlands,
66-
scale: 10_000,
67-
rotation: -45
68-
)
24+
@State private var viewpoint: Viewpoint? = .esriRedlands
6925

7026
var body: some View {
7127
MapViewReader { proxy in
7228
MapView(map: map, viewpoint: viewpoint)
7329
.onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 }
7430
.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-
}
85-
.padding()
31+
Compass(rotation: viewpoint?.rotation, mapViewProxy: proxy)
32+
.padding()
8633
}
8734
}
8835
}
8936
}
9037

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()
123-
}
124-
}
125-
}
126-
127-
private extension Point {
128-
static var esriRedlands: Point {
38+
private extension Viewpoint {
39+
static var esriRedlands: Viewpoint {
12940
.init(
130-
x: -117.19494,
131-
y: 34.05723,
132-
spatialReference: .wgs84
41+
center: .init(x: -117.19494, y: 34.05723, spatialReference: .wgs84),
42+
scale: 10_000,
43+
rotation: -45
13344
)
13445
}
13546
}

Sources/ArcGISToolkit/Components/Compass/Compass.swift

+41-66
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,35 @@
1414
import ArcGIS
1515
import SwiftUI
1616

17-
/// A `Compass` (alias North arrow) shows where north is in a `MapView` or
18-
/// `SceneView`.
17+
/// A `Compass` (alias North arrow) shows where north is in a `MapView`.
1918
public struct Compass: View {
2019
/// The opacity of the compass.
2120
@State private var opacity: Double = .zero
2221

23-
/// An action to perform when the compass is tapped.
24-
private let action: (() -> Void)?
25-
2622
/// A Boolean value indicating whether the compass should automatically
2723
/// hide/show itself when the heading is `0`.
2824
private var autoHide: Bool = true
2925

30-
/// A Boolean value indicating whether the compass should hide based on the
31-
/// current heading and whether the compass automatically hides.
32-
var shouldHide: Bool {
33-
(heading.isZero || heading.isNaN) && autoHide
34-
}
26+
/// The heading of the compass in degrees.
27+
private var heading: Double
28+
29+
/// The proxy to provide access to map view operations.
30+
private var mapViewProxy: MapViewProxy?
3531

3632
/// The width and height of the compass.
3733
private var size: CGFloat = 44
3834

39-
/// The heading of the compass in degrees.
40-
@Binding private var heading: Double
41-
42-
/// Creates a compass with a binding to a heading based on compass
43-
/// directions (0° indicates a direction toward true North, 90° indicates a
44-
/// direction toward true East, etc.).
35+
/// Creates a compass with a heading based on compass directions (0° indicates a direction
36+
/// toward true North, 90° indicates a direction toward true East, etc.).
4537
/// - Parameters:
4638
/// - heading: The heading of the compass.
47-
/// - action: An action to perform when the compass is tapped.
48-
public init(
49-
heading: Binding<Double>,
50-
action: (() -> Void)? = nil
39+
/// - mapViewProxy: The proxy to provide access to map view operations.
40+
init(
41+
heading: Double,
42+
mapViewProxy: MapViewProxy? = nil
5143
) {
52-
_heading = heading
53-
self.action = action
44+
self.heading = heading
45+
self.mapViewProxy = mapViewProxy
5446
}
5547

5648
public var body: some View {
@@ -63,66 +55,49 @@ public struct Compass: View {
6355
.aspectRatio(1, contentMode: .fit)
6456
.opacity(opacity)
6557
.frame(width: size, height: size)
66-
.onAppear { opacity = shouldHide ? 0 : 1 }
67-
.onChange(of: heading) { _ in
68-
let newOpacity: Double = shouldHide ? .zero : 1
58+
.onAppear { opacity = shouldHide(forHeading: heading) ? 0 : 1 }
59+
.onChange(of: heading) { newHeading in
60+
let newOpacity: Double = shouldHide(forHeading: newHeading) ? .zero : 1
6961
guard opacity != newOpacity else { return }
70-
withAnimation(.default.delay(shouldHide ? 0.25 : 0)) {
62+
withAnimation(.default.delay(shouldHide(forHeading: newHeading) ? 0.25 : 0)) {
7163
opacity = newOpacity
7264
}
7365
}
7466
.onTapGesture {
75-
if let action {
76-
action()
77-
} else {
78-
heading = .zero
79-
}
67+
Task { await mapViewProxy?.setViewpointRotation(0) }
8068
}
8169
.accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)")
8270
}
8371
}
8472
}
8573

86-
public extension Compass {
87-
/// Creates a compass with a binding to a viewpoint rotation (0° indicates
88-
/// a direction toward true North, 90° indicates a direction toward true
89-
/// West, etc.).
90-
/// - Parameters:
91-
/// - viewpointRotation: The viewpoint rotation whose value determines the
92-
/// heading of the compass.
93-
/// - action: An action to perform when the compass is tapped.
94-
init(
95-
viewpointRotation: Binding<Double>,
96-
action: (() -> Void)? = nil
97-
) {
98-
let heading = Binding(get: {
99-
viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue
100-
}, set: { newHeading in
101-
viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading
102-
})
103-
self.init(heading: heading, action: action)
74+
extension Compass {
75+
/// Returns a Boolean value indicating whether the compass should hide based on the
76+
/// provided heading and whether the compass has been configured to automatically hide.
77+
/// - Parameter heading: The heading used to determine if the compass should hide.
78+
/// - Returns: `true` if the compass should hide, `false` otherwise.
79+
func shouldHide(forHeading heading: Double) -> Bool {
80+
(heading.isZero || heading.isNaN) && autoHide
10481
}
105-
106-
/// Creates a compass with a binding to an optional viewpoint.
82+
}
83+
84+
public extension Compass {
85+
/// Creates a compass with a rotation (0° indicates a direction toward true North, 90° indicates
86+
/// a direction toward true West, etc.).
10787
/// - Parameters:
108-
/// - viewpoint: The viewpoint whose rotation determines the heading of the compass.
109-
/// - action: An action to perform when the compass is tapped.
110-
/// when the viewpoint's rotation is 0 degrees.
88+
/// - rotation: The rotation whose value determines the heading of the compass.
89+
/// - mapViewProxy: The proxy to provide access to map view operations.
11190
init(
112-
viewpoint: Binding<Viewpoint?>,
113-
action: (() -> Void)? = nil
91+
rotation: Double?,
92+
mapViewProxy: MapViewProxy
11493
) {
115-
let viewpointRotation = Binding {
116-
viewpoint.wrappedValue?.rotation ?? .nan
117-
} set: { newViewpointRotation in
118-
guard let oldViewpoint = viewpoint.wrappedValue else { return }
119-
viewpoint.wrappedValue = Viewpoint(
120-
center: oldViewpoint.targetGeometry.extent.center,
121-
scale: oldViewpoint.targetScale,
122-
rotation: newViewpointRotation
123-
)
94+
let heading: Double
95+
if let rotation {
96+
heading = rotation.isZero ? .zero : 360 - rotation
97+
} else {
98+
heading = .nan
12499
}
125-
self.init(viewpointRotation: viewpointRotation, action: action)
100+
self.init(heading: heading, mapViewProxy: mapViewProxy)
126101
}
127102

128103
/// Define a custom size for the compass.

0 commit comments

Comments
 (0)