Swiftui Withanimation Completion Callback

Adding completion Handler to withAnimation in SwiftUI

Maybe just wrap your reset function to asyncAfter DispatchQueue with length of your animation?

DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
self.viewModel.resetGame()
}

SwiftUI perform action after Spring() animation has completed

Default animation duration (for those animations which do not have explicit duration parameter) is usually 0.25-0.35 (independently of where it is started & platform), so in your case it is completely safe (tested with Xcode 11.4 / iOS 13.4) to use the following approach:

withAnimation(.spring()){
self.offset = .zero
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.animationRunning = false
}
}

Note: you can tune that 0.5 delay, but the difference is not remarkable for human eye.

How to set a property or any callbacks upon the finishing of an animation?

Use DispatchQueue to change the value to variable.

var myTestButton: some View {
HStack {
Rectangle().fill()
.opacity(showRect ? 1 : 0)
Button("Show") {
withAnimation(.linear(duration: 3)) {
showRect = true
// only do the following at the completion of the animation
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
showButtonEnabled = false
}

}
}.disabled(!showButtonEnabled)
Button("Hide") {
withAnimation(.linear(duration: 3)) {
showRect = false
// only do the following at the completion of the animation
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
showButtonEnabled = true
}

}
}.disabled(showButtonEnabled)
}
}

How to make the button inactive during animation

SwiftUI animations don't have completion handlers, but you can monitor the state of an animatable property and listen for changes to that. This does what you need and is not coupled to the timing of the animation

SwiftUI has AnimatableModifier which you can use to create a modifier that calls a function when the animation completes.

You can see the explanation of this at withAnimation completion callback with animatable modifiers

struct ContentView: View {
@State private var scale: CGFloat = 1
@State private var isDisable = false

var body: some View {
VStack {
Button(
action: {
self.isDisable = true
withAnimation(
.linear(duration: 1)
) {
scale = scale - 0.1
}
},
label: {
Text("Tap Me")
}
)
.disabled(
isDisable
)
RectangleView()
.scaleEffect(scale)
.onAnimationCompleted(for: scale) {
isDisable = false
}
}
}
}

struct RectangleView: View {
var body: some View {
Rectangle().fill(
Color.blue
)
.frame(
width:200,
height: 150
)
}
}

/// An animatable modifier that is used for observing animations for a given animatable value.
struct AnimationCompletionObserverModifier<Value>: AnimatableModifier where Value: VectorArithmetic {

/// While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes.
var animatableData: Value {
didSet {
notifyCompletionIfFinished()
}
}

/// The target value for which we're observing. This value is directly set once the animation starts. During animation, `animatableData` will hold the oldValue and is only updated to the target value once the animation completes.
private var targetValue: Value

/// The completion callback which is called once the animation completes.
private var completion: () -> Void

init(observedValue: Value, completion: @escaping () -> Void) {
self.completion = completion
self.animatableData = observedValue
targetValue = observedValue
}

/// Verifies whether the current animation is finished and calls the completion callback if true.
private func notifyCompletionIfFinished() {
guard animatableData == targetValue else { return }

/// Dispatching is needed to take the next runloop for the completion callback.
/// This prevents errors like "Modifying state during view update, this will cause undefined behavior."
DispatchQueue.main.async {
self.completion()
}
}

func body(content: Content) -> some View {
/// We're not really modifying the view so we can directly return the original input value.
return content
}
}

extension View {

/// Calls the completion handler whenever an animation on the given value completes.
/// - Parameters:
/// - value: The value to observe for animations.
/// - completion: The completion callback to call once the animation completes.
/// - Returns: A modified `View` instance with the observer attached.
func onAnimationCompleted<Value: VectorArithmetic>(for value: Value, completion: @escaping () -> Void) -> ModifiedContent<Self, AnimationCompletionObserverModifier<Value>> {
return modifier(AnimationCompletionObserverModifier(observedValue: value, completion: completion))
}
}

SwiftUI animation only works with `withAnimation` block and not with `.animation`

Here is an approach fro you:

Using Animation inside if or if let does not give you any benefit or result! because animation works with value changes, when you put it inside if, it got just initialized, do not use group unless you are sure you can replace it with group, use VStack instead.

struct ErrorForState: View {
@Binding var state: SomeStates
var body: some View {

return VStack(spacing: .zero) {
if let message = state.errorMessage {
Text(message)
.foregroundColor(Color(hex: 0x874b4b))
.padding(15)
.background(Color(hex: 0xffefef))
.cornerRadius(10)
.transition(.scale)
}
}
.animation(Animation.easeInOut(duration: 1.0), value: state.errorMessage)
}
}

Is there a way to execute a function after an animation in SwiftUI?

There is an approach to this described here:

https://www.avanderlee.com/swiftui/withanimation-completion-callback/

The approach requires the use of a custom implementation of the AnimatableModifier protocol. It's not completely trivial to implement, but it does seem to solve the problem.

SwiftUI withAnimation inside conditional not working

It is better to join animation with value which you want to animate, in your case it is radius, explicitly on container which holds animatable view.

Here is demo of approach. Tested with Xcode 13.2 / iOS 15.2

demo

struct TestButton: View {
@State var radius = 50.0
@State var running = false
let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)

var body: some View {
VStack {
Button(running ? "Stop" : "Start") {
running.toggle()
}
VStack { // responsible for animation of
// conditionally appeared/disappeared view
if running {
Circle()
.fill(.blue)
.frame(width: radius * 2, height: radius * 2)
.onAppear {
self.radius = 100
}
.onDisappear {
self.radius = 50
}
}
}
.animation(animation, value: radius) // << here !!
}
}
}


Related Topics



Leave a reply



Submit