I have an app that shows a bunch of people who each have an origin and angle.
struct Location {
var centre:CGPoint
var facing:Angle
}
SwiftUI magically and automatically does a lot of the animation as they move from location A to location B
withAnimation {
person.location = newLocation
}
However - for the Angle (facing) property, I want the animation to go in the shortest route (bearing in mind that in the real world - angles wrap around).
e.g. Swift UI correctly animates when the angle changes 5 -> 10 (degrees)
5,6,7,8,9,10
but going from 2 to 358, it takes the long way around
SwiftUI does 2,3,4,5,6,7.......,357,358
where I would like it to do
2,1,0,359,358
how can I go about this?
thank you
update: I'm hoping for a solution which allows me to work with the animation system, perhaps using a new MyAngle struct which provides the animation steps directly, perhaps using some kind of animation modifier. .easeInOut modifies the steps - is there an equivalent approach where I can create a .goTheRightWay animation?
Ok - Posting my own answer. It works a bit like @Ben's answer - but moves the 'shadow angle' management to the rotation effect.
All you have to do is switch rotationEffect(angle:Angle) for shortRotationEffect(angle:Angle,id:UUID)
this looks like
@State private var rotationStorage = RotationStorage()
//and then in body
Image(systemName: "person.fill").resizable()
.frame(width: 50, height: 50)
.shortRotationEffect(self.person.angle,id:person.id,storage:rotationStorage)
.animation(.easeInOut)
the ShortRotationEffect uses the provided id to maintain a dictionary of previous angles. When you set a new angle, it figures out the equivalent angle which provides a short rotation and applies that with a normal rotationEffect(...)
Here it is:
class RotationStorage {
private var storage: [UUID: Angle] = [:]
fileprivate func setAngle(id:UUID,angle:Angle) {
storage[id] = angle
}
fileprivate func getAngle(_ id:UUID) -> Angle? {
return storage[id]
}
}
extension View {
/// Like RotationEffect - but when animated, the rotation moves in the shortest direction.
/// - Parameters:
/// - angle: new angle
/// - anchor: anchor point
/// - id: unique id for the item being displayed. This is used as a key to maintain the rotation history and figure out the right direction to move
func shortRotationEffect(_ angle: Angle,
anchor: UnitPoint = .center,
id: UUID,
storage:RotationStorage) -> some View {
modifier(ShortRotation(angle: angle,
anchor: anchor,
id: id,
storage:storage))
}
}
struct ShortRotation: ViewModifier {
var angle: Angle
var anchor: UnitPoint
var id: UUID
let storage:RotationStorage
func getAngle() -> Angle {
var newAngle = angle
if let lastAngle = storage.getAngle(id) {
let change: Double = (newAngle.degrees - lastAngle.degrees) %% 360.double
if change < 180 {
newAngle = lastAngle + Angle.init(degrees: change)
} else {
newAngle = lastAngle + Angle.init(degrees: change - 360)
}
}
storage.setAngle(id: id, angle: newAngle)
return newAngle
}
func body(content: Content) -> some View {
content
.rotationEffect(getAngle(), anchor: anchor)
}
}
this relies on my positive modulus function:
public extension Double {
/// Returns modulus, but forces it to be positive
/// - Parameters:
/// - left: number
/// - right: modulus
/// - Returns: positive modulus
static func %% (_ left: Double, _ right: Double) -> Double {
let truncatingRemainder = left.truncatingRemainder(dividingBy: right)
return truncatingRemainder >= 0 ? truncatingRemainder : truncatingRemainder+abs(right)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With