Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I customise the Animation of an Angle change in SwiftUI

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?

like image 458
Confused Vorlon Avatar asked Oct 30 '25 00:10

Confused Vorlon


1 Answers

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)
    }
}
like image 92
Confused Vorlon Avatar answered Nov 01 '25 14:11

Confused Vorlon



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!