当从 0° 转到 359° 时,如何修复我的指南针应用程序进行完整的转动?
How can I fix my compass application that makes a complete turn when going 359° from 0°?
我正在用 swiftUI 创建一个指南针应用程序。它有效,但是当我添加动画来移动指南针时,出现以下行为:
例如,当它从5°方向转到350°时,它决定做一个完整的转弯。这不是指南针的自然行为。
我的 ContentView 代码:
import SwiftUI
import CoreLocation
struct ContentView: View {
var locationManager = CLLocationManager()
@ObservedObject var location: LocationProvider = LocationProvider()
@State var angle: CGFloat = 0
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.frame(width: 10, height: 30)
.background(Color(.red))
.foregroundColor(Color(.clear))
Spacer()
Text(String(Double(self.location.currentHeading).stringWithoutZeroFraction) + "°")
.font(.system(size: 40))
.foregroundColor(Color(.black))
Spacer()
}
.frame(width: 300, height: 300, alignment: .center)
.border(Color(.black))
.onReceive(self.location.heading) { heading in
withAnimation(.easeInOut(duration: 0.2)) {
self.angle = heading
}
}
.modifier(RotationEffect(angle: self.angle))
}.background(Color(.white))
}
}
struct RotationEffect: GeometryEffect {
var angle: CGFloat
var animatableData: CGFloat {
get { angle }
set { angle = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(
CGAffineTransform(translationX: -150, y: -150)
.concatenating(CGAffineTransform(rotationAngle: -CGFloat(angle.degreesToRadians)))
.concatenating(CGAffineTransform(translationX: 150, y: 150))
)
}
}
public extension CGFloat {
var degreesToRadians: CGFloat { return self * .pi / 180 }
var radiansToDegrees: CGFloat { return self * 180 / .pi }
}
public extension Double {
var degreesToRadians: Double { return Double(CGFloat(self).degreesToRadians) }
var radiansToDegrees: Double { return Double(CGFloat(self).radiansToDegrees) }
var stringWithoutZeroFraction: String {
return truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
这里,我使用CGAffineTransform
旋转罗盘动画来解决问题。我认为这会起作用,因为当我在 UIKit 中使用此方法时,问题不存在。
我的 LocationProvider 代码:
import SwiftUI
import CoreLocation
import Combine
public class LocationProvider: NSObject, CLLocationManagerDelegate, ObservableObject {
private let locationManager: CLLocationManager
public let heading = PassthroughSubject<CGFloat, Never>()
@Published var currentHeading: CGFloat {
willSet {
heading.send(newValue)
}
}
public override init() {
currentHeading = 0
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingHeading()
locationManager.requestWhenInUseAuthorization()
}
public func updateHeading() {
locationManager.startUpdatingHeading()
}
public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
DispatchQueue.main.async {
self.currentHeading = CGFloat(newHeading.trueHeading)
}
}
}
我该如何解决这个问题?
更新:这将在多次旋转时起作用。感谢 krjw 指出问题。
您没有理由需要 angle
属性 保持在 360° 范围内。不是直接将heading
赋值给angle
,而是计算差值并相加。
这是一个工作示例。在 ContentView 的 body
属性 之外,添加以下函数:
// If you ever need the current value of angle clamped to 0..<360,
// use clampAngle(self.angle)
func clampAngle(_ angle: CGFloat) -> CGFloat {
var angle = angle
while angle < 0 {
angle += 360
}
return angle.truncatingRemainder(dividingBy: 360)
}
// Calculates the difference between heading and angle
func angleDiff(to heading: CGFloat) -> CGFloat {
return (clampAngle(heading - self.angle) + 180).truncatingRemainder(dividingBy: 360) - 180
}
然后将分配angle
的行更改为
self.angle += self.angleDiff(to: heading)
基于评论的答案有效,但是当进入正向,超过 360 度时,动画再次失败。
简单的解决方案是查找 -300 和更小的变化,然后添加 360 度。可能有更好的解决方案,但在那之前我会分享我的:
.onReceive(self.location.heading) { heading in
var diff = (heading - self.angle + 180).truncatingRemainder(dividingBy: 360) - 180
if diff < -300 {
diff += 360
}
withAnimation {
self.angle += diff
}
}
我正在用 swiftUI 创建一个指南针应用程序。它有效,但是当我添加动画来移动指南针时,出现以下行为:
例如,当它从5°方向转到350°时,它决定做一个完整的转弯。这不是指南针的自然行为。
我的 ContentView 代码:
import SwiftUI
import CoreLocation
struct ContentView: View {
var locationManager = CLLocationManager()
@ObservedObject var location: LocationProvider = LocationProvider()
@State var angle: CGFloat = 0
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.frame(width: 10, height: 30)
.background(Color(.red))
.foregroundColor(Color(.clear))
Spacer()
Text(String(Double(self.location.currentHeading).stringWithoutZeroFraction) + "°")
.font(.system(size: 40))
.foregroundColor(Color(.black))
Spacer()
}
.frame(width: 300, height: 300, alignment: .center)
.border(Color(.black))
.onReceive(self.location.heading) { heading in
withAnimation(.easeInOut(duration: 0.2)) {
self.angle = heading
}
}
.modifier(RotationEffect(angle: self.angle))
}.background(Color(.white))
}
}
struct RotationEffect: GeometryEffect {
var angle: CGFloat
var animatableData: CGFloat {
get { angle }
set { angle = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(
CGAffineTransform(translationX: -150, y: -150)
.concatenating(CGAffineTransform(rotationAngle: -CGFloat(angle.degreesToRadians)))
.concatenating(CGAffineTransform(translationX: 150, y: 150))
)
}
}
public extension CGFloat {
var degreesToRadians: CGFloat { return self * .pi / 180 }
var radiansToDegrees: CGFloat { return self * 180 / .pi }
}
public extension Double {
var degreesToRadians: Double { return Double(CGFloat(self).degreesToRadians) }
var radiansToDegrees: Double { return Double(CGFloat(self).radiansToDegrees) }
var stringWithoutZeroFraction: String {
return truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
这里,我使用CGAffineTransform
旋转罗盘动画来解决问题。我认为这会起作用,因为当我在 UIKit 中使用此方法时,问题不存在。
我的 LocationProvider 代码:
import SwiftUI
import CoreLocation
import Combine
public class LocationProvider: NSObject, CLLocationManagerDelegate, ObservableObject {
private let locationManager: CLLocationManager
public let heading = PassthroughSubject<CGFloat, Never>()
@Published var currentHeading: CGFloat {
willSet {
heading.send(newValue)
}
}
public override init() {
currentHeading = 0
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingHeading()
locationManager.requestWhenInUseAuthorization()
}
public func updateHeading() {
locationManager.startUpdatingHeading()
}
public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
DispatchQueue.main.async {
self.currentHeading = CGFloat(newHeading.trueHeading)
}
}
}
我该如何解决这个问题?
更新:这将在多次旋转时起作用。感谢 krjw 指出问题。
您没有理由需要 angle
属性 保持在 360° 范围内。不是直接将heading
赋值给angle
,而是计算差值并相加。
这是一个工作示例。在 ContentView 的 body
属性 之外,添加以下函数:
// If you ever need the current value of angle clamped to 0..<360,
// use clampAngle(self.angle)
func clampAngle(_ angle: CGFloat) -> CGFloat {
var angle = angle
while angle < 0 {
angle += 360
}
return angle.truncatingRemainder(dividingBy: 360)
}
// Calculates the difference between heading and angle
func angleDiff(to heading: CGFloat) -> CGFloat {
return (clampAngle(heading - self.angle) + 180).truncatingRemainder(dividingBy: 360) - 180
}
然后将分配angle
的行更改为
self.angle += self.angleDiff(to: heading)
简单的解决方案是查找 -300 和更小的变化,然后添加 360 度。可能有更好的解决方案,但在那之前我会分享我的:
.onReceive(self.location.heading) { heading in
var diff = (heading - self.angle + 180).truncatingRemainder(dividingBy: 360) - 180
if diff < -300 {
diff += 360
}
withAnimation {
self.angle += diff
}
}