-
Notifications
You must be signed in to change notification settings - Fork 11
Mhd/refactor compass action #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7898052
5ca7dba
8e6b821
21b0a02
a6f8cfc
b7e130b
6435d32
ad90150
38c0f09
13ebe9f
e59a4f5
4d91afe
3ef79b6
2df1541
c16eb88
c2b6d55
0a202c1
a759615
a067be9
64d9acc
6788766
2bc8a8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,27 +15,121 @@ import ArcGIS | |
import ArcGISToolkit | ||
import SwiftUI | ||
|
||
/// An example demonstrating how to use a compass in three different environments. | ||
struct CompassExampleView: View { | ||
/// The data model containing the `Map` displayed in the `MapView`. | ||
@StateObject private var dataModel = MapDataModel( | ||
map: Map(basemapStyle: .arcGISImagery) | ||
) | ||
/// A scenario represents a type of environment a compass may be used in. | ||
enum Scenario: String { | ||
case map | ||
case scene | ||
} | ||
|
||
/// The active scenario. | ||
@State private var scenario = Scenario.map | ||
|
||
var body: some View { | ||
Group { | ||
switch scenario { | ||
case .map: | ||
MapWithViewpoint() | ||
case .scene: | ||
SceneWithCameraController() | ||
} | ||
} | ||
.toolbar { | ||
ToolbarItem(placement: .navigationBarTrailing) { | ||
Menu(scenario.rawValue.capitalized) { | ||
Button { | ||
scenario = .map | ||
} label: { | ||
Label("Map", systemImage: "map.fill") | ||
} | ||
|
||
Button { | ||
scenario = .scene | ||
} label: { | ||
Label("Scene", systemImage: "globe.americas.fill") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// An example demonstrating how to use a compass with a map view. | ||
struct MapWithViewpoint: View { | ||
/// The `Map` displayed in the `MapView`. | ||
@State private var map = Map(basemapStyle: .arcGISImagery) | ||
|
||
/// Allows for communication between the Compass and MapView or SceneView. | ||
@State private var viewpoint: Viewpoint? = Viewpoint( | ||
center: Point(x: -117.19494, y: 34.05723, spatialReference: .wgs84), | ||
center: .esriRedlands, | ||
scale: 10_000, | ||
rotation: -45 | ||
) | ||
|
||
var body: some View { | ||
MapView(map: dataModel.map, viewpoint: viewpoint) | ||
.onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } | ||
.overlay(alignment: .topTrailing) { | ||
Compass(viewpoint: $viewpoint) | ||
// Optionally provide a different size for the compass. | ||
// .compassSize(size: <#T##CGFloat#>) | ||
MapViewReader { proxy in | ||
MapView(map: map, viewpoint: viewpoint) | ||
.onViewpointChanged(kind: .centerAndScale) { viewpoint = $0 } | ||
.overlay(alignment: .topTrailing) { | ||
Compass(viewpoint: $viewpoint) { | ||
guard let viewpoint else { return } | ||
Task { | ||
// Animate the map view to zero when the compass is tapped. | ||
await proxy.setViewpoint( | ||
viewpoint.withRotation(0), | ||
duration: 0.25 | ||
Comment on lines
+79
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a lot of point to the compass component if the user has to animate to zero themselves? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It still draws the compass, which is non-trivial, automatically allows it to track map view rotation, and will allow tapping to perform an action. The default is to rotate the map view to zero, but without animation, as that requires a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good points. Thank you. |
||
) | ||
} | ||
} | ||
.padding() | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// An example demonstrating how to use a compass with a scene view and camera controller. | ||
struct SceneWithCameraController: View { | ||
/// The data model containing the `Scene` displayed in the `SceneView`. | ||
@State private var scene = Scene(basemapStyle: .arcGISImagery) | ||
|
||
/// The current heading as reported by the scene view. | ||
@State private var heading = Double.zero | ||
|
||
/// The orbit location camera controller used by the scene view. | ||
private let cameraController = OrbitLocationCameraController( | ||
target: .esriRedlands, | ||
distance: 10_000 | ||
) | ||
|
||
var body: some View { | ||
SceneView(scene: scene, cameraController: cameraController) | ||
.onCameraChanged { newCamera in | ||
heading = newCamera.heading.rounded() | ||
} | ||
.overlay(alignment: .topTrailing) { | ||
Compass(viewpointRotation: $heading) { | ||
// Animate the scene view when the compass is tapped. | ||
Task { | ||
await cameraController.moveCamera( | ||
distanceDelta: .zero, | ||
headingDelta: heading > 180 ? 360 - heading : -heading, | ||
pitchDelta: .zero, | ||
duration: 0.3 | ||
) | ||
} | ||
} | ||
.padding() | ||
} | ||
} | ||
} | ||
|
||
private extension Point { | ||
static var esriRedlands: Point { | ||
.init( | ||
x: -117.19494, | ||
y: 34.05723, | ||
spatialReference: .wgs84 | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,21 +17,24 @@ import SwiftUI | |
/// A `Compass` (alias North arrow) shows where north is in a `MapView` or | ||
/// `SceneView`. | ||
public struct Compass: View { | ||
/// A Boolean value indicating whether the compass should automatically | ||
/// hide/show itself when the heading is `0`. | ||
private let autoHide: Bool | ||
|
||
/// The opacity of the compass. | ||
@State private var opacity: Double = .zero | ||
|
||
/// An action to perform when the compass is tapped. | ||
private let action: (() -> Void)? | ||
|
||
/// A Boolean value indicating whether the compass should automatically | ||
/// hide/show itself when the heading is `0`. | ||
private var autoHide: Bool = true | ||
|
||
/// A Boolean value indicating whether the compass should hide based on the | ||
/// current heading and whether the compass automatically hides. | ||
var shouldHide: Bool { | ||
(heading.isZero || heading.isNaN) && autoHide | ||
} | ||
|
||
/// The width and height of the compass. | ||
var size: CGFloat = 44 | ||
private var size: CGFloat = 44 | ||
|
||
/// The heading of the compass in degrees. | ||
@Binding private var heading: Double | ||
|
@@ -41,14 +44,13 @@ public struct Compass: View { | |
/// direction toward true East, etc.). | ||
/// - Parameters: | ||
/// - heading: The heading of the compass. | ||
/// - autoHide: A Boolean value that determines whether the compass | ||
/// automatically hides itself when the heading is `0`. | ||
/// - action: An action to perform when the compass is tapped. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some doc about when you need to specify this action might be helpful to the user. For example, does the compass set the heading to zero and call the action if I have an action set? What is the behavior when no action is set? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default is to rotate the map view to zero, but without animation. We'll update the doc. |
||
public init( | ||
heading: Binding<Double>, | ||
autoHide: Bool = true | ||
action: (() -> Void)? = nil | ||
) { | ||
_heading = heading | ||
self.autoHide = autoHide | ||
self.action = action | ||
} | ||
|
||
public var body: some View { | ||
|
@@ -60,16 +62,22 @@ public struct Compass: View { | |
} | ||
.aspectRatio(1, contentMode: .fit) | ||
.opacity(opacity) | ||
.onTapGesture { heading = .zero } | ||
.frame(width: size, height: size) | ||
.onAppear { opacity = shouldHide ? 0 : 1 } | ||
.onChange(of: heading) { _ in | ||
let newOpacity: Double = shouldHide ? .zero : 1 | ||
guard opacity != newOpacity else { return } | ||
withAnimation(.default.delay(shouldHide ? 0.25 : 0)) { | ||
opacity = newOpacity | ||
} | ||
} | ||
.onAppear { opacity = shouldHide ? 0 : 1 } | ||
.onTapGesture { | ||
if let action { | ||
action() | ||
} else { | ||
heading = .zero | ||
} | ||
} | ||
.accessibilityLabel("Compass, heading \(Int(heading.rounded())) degrees \(CompassDirection(heading).rawValue)") | ||
} | ||
} | ||
|
@@ -82,28 +90,27 @@ public extension Compass { | |
/// - 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. | ||
/// - action: An action to perform when the compass is tapped. | ||
init( | ||
viewpointRotation: Binding<Double>, | ||
autoHide: Bool = true | ||
action: (() -> Void)? = nil | ||
) { | ||
let heading = Binding(get: { | ||
viewpointRotation.wrappedValue.isZero ? .zero : 360 - viewpointRotation.wrappedValue | ||
}, set: { newHeading in | ||
viewpointRotation.wrappedValue = newHeading.isZero ? .zero : 360 - newHeading | ||
}) | ||
self.init(heading: heading, autoHide: autoHide) | ||
self.init(heading: heading, action: action) | ||
} | ||
|
||
/// Creates a compass with a binding to an optional viewpoint. | ||
/// - Parameters: | ||
/// - viewpoint: The viewpoint whose rotation determines the heading of the compass. | ||
/// - autoHide: A Boolean value that determines whether the compass automatically hides itself | ||
/// - action: An action to perform when the compass is tapped. | ||
/// when the viewpoint's rotation is 0 degrees. | ||
init( | ||
viewpoint: Binding<Viewpoint?>, | ||
autoHide: Bool = true | ||
action: (() -> Void)? = nil | ||
) { | ||
let viewpointRotation = Binding { | ||
viewpoint.wrappedValue?.rotation ?? .nan | ||
|
@@ -115,7 +122,7 @@ public extension Compass { | |
rotation: newViewpointRotation | ||
) | ||
} | ||
self.init(viewpointRotation: viewpointRotation, autoHide: autoHide) | ||
self.init(viewpointRotation: viewpointRotation, action: action) | ||
} | ||
|
||
/// Define a custom size for the compass. | ||
|
@@ -125,4 +132,13 @@ public extension Compass { | |
copy.size = size | ||
return copy | ||
} | ||
|
||
/// Specifies whether the ``Compass`` should automatically hide when the heading is 0. | ||
/// - Parameter flag: A Boolean value indicating whether the compass should automatically | ||
/// hide/show itself when the heading is `0`. | ||
func automaticallyHides(_ flag: Bool) -> some View { | ||
var copy = self | ||
copy.autoHide = flag | ||
return copy | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// Copyright 2023 Esri. | ||
|
||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import ArcGIS | ||
|
||
public extension Viewpoint { | ||
/// Creates a new viewpoint with the same target geometry and scale but with a new rotation. | ||
/// - Parameter rotation: The rotation for the new viewpoint. | ||
/// - Returns: A new viewpoint. | ||
func withRotation(_ rotation: Double) -> Viewpoint { | ||
switch self.kind { | ||
case .centerAndScale: | ||
return Viewpoint( | ||
center: self.targetGeometry as! Point, | ||
scale: self.targetScale, | ||
rotation: rotation | ||
) | ||
case.boundingGeometry: | ||
return Viewpoint( | ||
boundingGeometry: self.targetGeometry, | ||
rotation: rotation | ||
) | ||
} | ||
} | ||
} | ||
mhdostal marked this conversation as resolved.
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this then be named more like how SwiftUI names modifiers like this?
maybe
disableAutoHide(:)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be OK. @dfeinzimer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#291
disableAutocorrection
was deprecated in favor ofautocorrectionDisabled
so I'll follow the new scheme. e.g.autoHideDisabled
.