Swiftui: Unexpected Animation When Using a Non @State Var

SwiftUI unexpected position changes during animation

I don't know exactly why this happens but I have a vague idea.
Because the animation starts before the view is fully rendered in the width an height, the view starts in the op left corner. The animation takes the starting position in the animation and the change to its normal position and keep repeating this part with the given animation. Which causes this unexpected behaviour.

I think it is a bug, but with the theory above I have created a workaround for now. In the view where we place the loader we have to wait till the parent view is fully rendered before adding the animated view (Loader view), we can do that like this:

struct LoaderViewer: View {

@State showLoader = false

var body: some View {
VStack {
if showLoader {
Loader().frame(width: 200, height: 200)
}
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
showLoader = true
}
}
}
}

SwiftUI animation not working using animation(_:value:)

The difference between animation(_:) and animation(_:value:) is straightforward. The former is implicit, and the latter explicit. The implicit nature of animation(_:) meant that anytime ANYTHING changed, it would react. The other issue it had was trying to guess what you wanted to animate. As a result, this could be erratic and unexpected. There were some other issues, so Apple has simply deprecated it.

animation(_:value:) is an explicit animation. It will only trigger when the value you give it changes. This means you can't just stick it on a view and expect the view to animate when it appears. You need to change the value in an .onAppear() or use some value that naturally changes when a view appears to trigger the animation. You also need to have some modifier specifically react to the changed value.

struct ContentView: View {
@State var isOn = false
//The better route is to have a separate variable to control the animations
// This prevents unpleasant side-effects.
@State private var animate = false

var body: some View {
VStack {
Text("I don't change.")
.padding()
Button("Press me, I do change") {
isOn.toggle()
animate = false
// Because .opacity is animated, we need to switch it
// back so the button shows.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
animate = true
}
}
// In this case I chose to animate .opacity
.opacity(animate ? 1 : 0)
.animation(.easeIn, value: animate)
.frame(width: 300, height: 400)
// If you want the button to animate when the view appears, you need to change the value
.onAppear { animate = true }
}
}
}

Why animation is speeding up after changing a property? SwiftUI

View should be in body, animation should be joined to corresponding trigger state.

Find below fixed parts. Tested with Xcode 13.4 / iOS 15.5

demo

    @State var colors = [Color.white]  // data !!
var body: some View {

ZStack {
// view is here !!
EmitterView(images: ["spark"], particleCount: 200, creationRange: CGSize(width: 0.4, height: 0.2), colors: colors, blendMode: .screen, angle: .degrees(0), angleRange: .degrees(360), opacityRange: 0, opacitySpeed: 15, scale: 0.5, scaleRange: 0.2, scaleSpeed: -0.2, speed: 50, speedRange: 120, animation: Animation.linear(duration: 1).repeatForever(autoreverses: false), animationDelayTreshold: 1)
.ignoresSafeArea()
}
.background(.black)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
.onTapGesture {
colors = [.red] // update !!
}
}

and animation where is trigger

    private struct ParticleView: View {

let image: Image

@State private var isActive = false
let position: ParticleState<CGPoint>
let opacity: ParticleState<Double>
let rotation: ParticleState<Angle>
let scale: ParticleState<CGFloat>

var animation: Animation
var delayTreshold = 0.0

var body: some View {
image
.opacity(isActive ? opacity.end : opacity.start)
.scaleEffect(isActive ? scale.end : scale.start)
.rotationEffect(isActive ? rotation.end : rotation.start)
.position(isActive ? position.end : position.start)
// here is animation, depends on isActive !!
.animation(self.animation.delay(Double.random(in: 0...self.delayTreshold)), value: isActive)
.onAppear{self.isActive = true}
}

}

Complete test module is here



Related Topics



Leave a reply



Submit