SwiftUI withAnimation completion callback SwiftUI withAnimation completion callback ios ios

SwiftUI withAnimation completion callback


On this blog this Guy Javier describes how to use GeometryEffect in order to have animation feedback, in his example he detects when the animation is at 50% so he can flip the view and make it looks like the view has 2 sides

here is the link to the full article with a lot of explanations: https://swiftui-lab.com/swiftui-animations-part2/

I will copy the relevant snippets here so the answer can still be relevant even if the link is not valid no more:

In this example @Binding var flipped: Bool becomes true when the angle is between 90 and 270 and then false.

struct FlipEffect: GeometryEffect {    var animatableData: Double {        get { angle }        set { angle = newValue }    }    @Binding var flipped: Bool    var angle: Double    let axis: (x: CGFloat, y: CGFloat)    func effectValue(size: CGSize) -> ProjectionTransform {        // We schedule the change to be done after the view has finished drawing,        // otherwise, we would receive a runtime error, indicating we are changing        // the state while the view is being drawn.        DispatchQueue.main.async {            self.flipped = self.angle >= 90 && self.angle < 270        }        let a = CGFloat(Angle(degrees: angle).radians)        var transform3d = CATransform3DIdentity;        transform3d.m34 = -1/max(size.width, size.height)        transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)        transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)        let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))        return ProjectionTransform(transform3d).concatenating(affineTransform)    }}

You should be able to change the animation to whatever you want to achieve and then get the binding to change the state of the parent once it is done.


Here's a bit simplified and generalized version that could be used for any single value animations. This is based on some other examples I was able to find on the internet while waiting for Apple to provide a more convenient way:

struct AnimatableModifierDouble: AnimatableModifier {    var targetValue: Double    // SwiftUI gradually varies it from old value to the new value    var animatableData: Double {        didSet {            checkIfFinished()        }    }    var completion: () -> ()    // Re-created every time the control argument changes    init(bindedValue: Double, completion: @escaping () -> ()) {        self.completion = completion        // Set animatableData to the new value. But SwiftUI again directly        // and gradually varies the value while the body        // is being called to animate. Following line serves the purpose of        // associating the extenal argument with the animatableData.        self.animatableData = bindedValue        targetValue = bindedValue    }    func checkIfFinished() -> () {        //print("Current value: \(animatableData)")        if (animatableData == targetValue) {            //if animatableData.isEqual(to: targetValue) {            DispatchQueue.main.async {                self.completion()            }        }    }    // Called after each gradual change in animatableData to allow the    // modifier to animate    func body(content: Content) -> some View {        // content is the view on which .modifier is applied        content        // We don't want the system also to        // implicitly animate default system animatons it each time we set it. It will also cancel        // out other implicit animations now present on the content.            .animation(nil)    }}

And here's an example on how to use it with text opacity animation:

import SwiftUIstruct ContentView: View {    // Need to create state property    @State var textOpacity: Double = 0.0    var body: some View {        VStack {            Text("Hello world!")                .font(.largeTitle)                 // Pass generic animatable modifier for animating double values                .modifier(AnimatableModifierDouble(bindedValue: textOpacity) {                    // Finished, hurray!                    print("finished")                    // Reset opacity so that you could tap the button and animate again                    self.textOpacity = 0.0                }).opacity(textOpacity) // bind text opacity to your state property            Button(action: {                withAnimation(.easeInOut(duration: 1.0)) {                    self.textOpacity = 1.0 // Change your state property and trigger animation to start                }            }) {                Text("Animate")            }        }    }}struct HomeView_Previews: PreviewProvider {    static var previews: some View {        ContentView()    }}


You need to use a custom modifier.

I have done an example to animate the offset in the X-axis with a completion block.

struct OffsetXEffectModifier: AnimatableModifier {    var initialOffsetX: CGFloat    var offsetX: CGFloat    var onCompletion: (() -> Void)?    init(offsetX: CGFloat, onCompletion: (() -> Void)? = nil) {        self.initialOffsetX = offsetX        self.offsetX = offsetX        self.onCompletion = onCompletion    }    var animatableData: CGFloat {        get { offsetX }        set {            offsetX = newValue            checkIfFinished()        }    }    func checkIfFinished() -> () {        if let onCompletion = onCompletion, offsetX == initialOffsetX {            DispatchQueue.main.async {                onCompletion()            }        }    }    func body(content: Content) -> some View {        content.offset(x: offsetX)    }}struct OffsetXEffectModifier_Previews: PreviewProvider {  static var previews: some View {    ZStack {      Text("Hello")      .modifier(        OffsetXEffectModifier(offsetX: 10, onCompletion: {            print("Completed")        })      )    }    .frame(width: 100, height: 100, alignment: .bottomLeading)    .previewLayout(.sizeThatFits)  }}