'Problem animating with animatableData in SwiftUI
SwiftUI not only gives us get automatic animation… it lets us control how the animation occurs, using the animatableData
property. So cool!
Only I can't get it to work. The following code displays a clock face on the screen (numbers 0…11 🙂) and shows a toggle switch. Switching the toggle rotates the numbers by 180 degrees, animated. Or that is my intent. But instead of each number taking an animated path around the circle of numbers, each number moves along a straight line to its new position… so that the circle of numbers collapses to the circle's center point and then re-explodes into its new configuration:
The animatableData
property is not being referenced by SwiftUI. Any idea why?
(Note that it wouldn't work to use a standard rotation animation, because that would turn all the numbers upside down.)
import SwiftUI
struct ContentView: View {
@State var state: Bool = false
var body: some View {
VStack {
CircleView(hoursOffset: CGFloat(state ? 6 : 0 ))
.aspectRatio(contentMode: .fit)
Toggle(isOn: $state, label: { Text("Rotate 180°?") })
.frame(maxWidth: 200).padding()
}
}
}
extension CGPoint {
static func onCircle(hours: CGFloat, size: CGFloat) -> CGPoint {
let radians = (3 - hours) * CGFloat.pi / CGFloat (6)
let hypotenuse = size / 2
return CGPoint(x: size / 2 + 0.8 * hypotenuse * cos(radians),
y: size / 2 - 0.8 * hypotenuse * sin(radians))
}
}
struct CircleView: View {
var hoursOffset: CGFloat
public var animatableData: CGFloat {
get { hoursOffset }
set { self.hoursOffset = newValue }
}
var body: some View {
GeometryReader() { geo in
ForEach(0..<12) { hour in
ZStack {
Text("\(hour)")
.position(CGPoint.onCircle(hours: CGFloat(hour) + self.hoursOffset, size: geo.size.width))
.animation(Animation.easeInOut(duration: 2.0))
}
}
}
}
}
Solution 1:[1]
Here is possible approach. Tested with Xcode 11.4 / iOS 13.4. Just animatableData
in View does not work - you have to put it in AnimatableModifier
.
struct CircleMoving: AnimatableModifier {
var hoursOffset: CGFloat
var hour: CGFloat
var size: CGFloat
public var animatableData: CGFloat {
get { hoursOffset }
set { self.hoursOffset = newValue }
}
func body(content: Content) -> some View {
content
.position(CGPoint.onCircle(hours: hour + hoursOffset, size: size))
}
}
struct CircleView: View {
var hoursOffset: CGFloat
var body: some View {
GeometryReader() { geo in
ForEach(0..<12) { hour in
ZStack {
Text("\(hour)")
.modifier(CircleMoving(hoursOffset: self.hoursOffset, hour: CGFloat(hour), size: geo.size.width))
}
}
}
.animation(.easeInOut(duration: 3.0), value: hoursOffset) // !!!
}
}
Optional: also I would recommend you round point values for better integrity, however it is not much important.
extension CGPoint {
static func onCircle(hours: CGFloat, size: CGFloat) -> CGPoint {
let radians = (3 - hours) * CGFloat.pi / CGFloat (6)
let hypotenuse = size / 2
return CGPoint(x: (size / 2 + 0.8 * hypotenuse * cos(radians)).rounded(.up),
y: (size / 2 - 0.8 * hypotenuse * sin(radians)).rounded(.up))
}
}
Solution 2:[2]
the problem is the evil mathematics...you are calculating the endpositions. Apple does just adding steps from one position to another (as a line) so this is what you see. If you toggle from 0 to 1 it "looks" like a circular move, but it is also linear ;) the solution would be, to calculate the x,y positions manually ON the circle depending on your hours offset values
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |