Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI animation rotation glitch @ 360/0 degree marker

So I have a view that rotates according to a specific degree that is set by the direction the device is pointed in. The problem is when you go from 359.99 to 0.01 degree rotation (which is the smallest difference in actuality, the view chooses to rotate all the way from 360 down to 0, instead of just wrapping around. This causes problems with the animation because it makes the view spin 359 degrees back to the 0 position instead of just clicking into the 0 degree position from 359. I've tried setting animation to nil between 359-360 and 0-1 but if you move the device fast enough, you still see this bad animation. Any suggestions?

Image("arrow")
    .rotationEffect(Angle(degrees: deviceHeading))
    .animation(.easeOut)

https://youtu.be/MzpI6THHS8U

As you can see, right in that position, it rotates fully around instead of just doing a change of taking the short path to the new position.

like image 538
nickcoding2 Avatar asked Nov 19 '25 12:11

nickcoding2


1 Answers

Since iOS 17 / watchOS 10 / tvOS 17 / macOS 14 / Mac Catalyst 17 / VisionOS 1

In SwiftUI withAnimation has a completionHandler now. https://developer.apple.com/documentation/swiftui/withanimation(_:completioncriteria:_:completion:)

Using that you could temporarily calculate the new rotation degrees based on the shortest route of the animation. And then once the animation completes, you correct the rotation degrees to the actual value of the device heading.

Here is a code snippet that I think should work (not yet tested):

@available(watchOS 10.0, *)
struct CompassArrowView: View {
    var deviceHeading: CLLocationDegrees
    @State private var rotationDegrees: CLLocationDegrees = .zero

    init(deviceHeading: CLLocationDegrees) {
        self.deviceHeading = deviceHeading
        self.rotationDegrees = deviceHeading
    }

    var body: some View {
        Image("arrow")
            .rotationEffect(.degrees(rotationDegrees))
            .onChange(of: deviceHeading) { oldValue, newValue in
                if abs(newValue - oldValue) > 180.0 {
                    if newValue < oldValue {
                        withAnimation(.easeOut) { [self] in
                            rotationDegrees = newValue + 360
                        } completion: { [self] in
                            rotationDegrees = rotationDegrees - 360
                        }
                    } else {
                        rotationDegrees = oldValue + 360
                        withAnimation(.easeOut) { [self] in
                            rotationDegrees = newValue
                        }
                    }
                } else {
                    withAnimation(.easeOut) { [self] in
                        rotationDegrees = newValue
                    }
                }
            }
    }
}
like image 170
Bocaxica Avatar answered Nov 21 '25 01:11

Bocaxica