I have following UI:
Red Rectangle
spin speed is base from slider's value. I'm acheiving this using .linear(duration:).repeatForever(autoreverses: false)
(duration base on slider's value) and .id
modifer to stop current animation whenever slider's value changed.
struct ContentView: View {
@State private var level: Double = 0
@State private var rotation: Double = 0
var body: some View {
VStack(spacing: 16) {
Rectangle().fill(.red)
.frame(width: 100, height: 100)
.id(level)
.rotationEffect(Angle(degrees: rotation))
.animation(level > 0 ? .linear(duration: 2 / level).repeatForever(autoreverses: false) : .linear(duration: 2), value: rotation)
Spacer().frame(height: 12)
Slider(value: $level, in: 0...3, step: 1)
HStack {
Text("0")
.fontWeight(.semibold)
Spacer()
Text("1")
.fontWeight(.semibold)
Spacer()
Text("2")
.fontWeight(.semibold)
Spacer()
Text("3")
.fontWeight(.semibold)
}
}
.padding(16)
.onChange(of: level, perform: { _ in
rotation += 360
})
}
}
My current issue is when user change slider's value, Rectangle
is reset to initial state because of .id
so it create a glitch.
Here is video link of how my code currently work: https://imgur.com/a/6bsUJzm
My question is how to get smooth transition between state when user change slider's value? Any insights or suggestions would be appreciated!
One way to fix is to avoid using .repeatForever
for the animation. Instead:
Since iOS 17, an animation that is triggered using withAnimation
can have a completion
callback. So this would be one way of triggering the follow-on animation. However, doing it this way gives a rather stuttering animation.
An alternative technique is to use an Animatable
ViewModifier
to track the animation and perform an action when it completes. Doing it this way works more smoothly. The answer to SwiftUI Animation finish animation cycle provides a generic modifier that can be used for this purpose (it was my answer). That post was also concerned with a continuous rotation, so it is quite a similar issue.
Here is the updated example. Some more notes:
The .id
modifier should be removed.
It is important that the animation modifier appears after the AnimationCompletionCallback
.
Since .repeatForever
is no longer used, the animation modifier can be simplified (a ternary operator is not needed).
It is simpler to apply the .fontWeight
for the slider labels to the HStack
, instead of to each individual label.
You were using a deprecated version of .onChange
before. If in fact you are targeting a version of iOS earlier than iOS 17 then use the commented version provided in the code below.
var body: some View {
VStack(spacing: 16) {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: rotation))
// See https://stackoverflow.com/a/76969841/20386264
.modifier(
AnimationCompletionCallback(animatedValue: rotation) {
if level > 0 {
rotation += 90
}
}
)
.animation(.linear(duration: 2 / max(1, level)), value: rotation)
Spacer().frame(height: 12)
Slider(value: $level, in: 0...3, step: 1)
HStack {
Text("0")
Spacer()
Text("1")
Spacer()
Text("2")
Spacer()
Text("3")
}
.fontWeight(.semibold)
}
.padding(16)
// Pre iOS 17: .onChange(of: level) { [oldVal = level] newVal in
.onChange(of: level) { oldVal, newVal in
if oldVal == 0 {
rotation += 90
}
}
}