how to do a simple scaling animation and why isn’t this working?

Here is possible approach (based on AnimatableModifier). Actually it demonstrates how current animation end can be detected, and performed something – in this case, for your scaling scenario, just initiate reversing.

demo

Simplified & modified your example

struct TestReversingScaleAnimation: View {

    @State var scaleImage : CGFloat = 1

    var body: some View {
        VStack {
            Button("Start animation") {
                self.scaleImage = 0.01       // initiate animation
            }

            Image(systemName: "circle.fill")
                .modifier(ReversingScale(to: scaleImage) {
                    self.scaleImage = 1      // reverse set
                })
                .animation(.default)         // now can be implicit
        }
    }
}

Actually, show-maker here… important comments inline.

Updated for Xcode 13.3 (tested with iOS 15.4)

struct ReversingScale: AnimatableModifier {
    var value: CGFloat

    private let target: CGFloat
    private let onEnded: () -> ()

    init(to value: CGFloat, onEnded: @escaping () -> () = {}) {
        self.target = value
        self.value = value
        self.onEnded = onEnded // << callback
    }

    var animatableData: CGFloat {
        get { value }
        set { value = newValue
            // newValue here is interpolating by engine, so changing
            // from previous to initially set, so when they got equal
            // animation ended
            let callback = onEnded
            if newValue == target {
                DispatchQueue.main.async(execute: callback)
            }
        }
    }

    func body(content: Content) -> some View {
        content.scaleEffect(value)
    }
}

Original variant (tested with Xcode 11.4 / iOS 13.4)

struct ReversingScale: AnimatableModifier {
    var value: CGFloat

    private var target: CGFloat
    private var onEnded: () -> ()

    init(to value: CGFloat, onEnded: @escaping () -> () = {}) {
        self.target = value
        self.value = value
        self.onEnded = onEnded // << callback
    }

    var animatableData: CGFloat {
        get { value }
        set { value = newValue
            // newValue here is interpolating by engine, so changing
            // from previous to initially set, so when they got equal
            // animation ended
            if newValue == target {
                onEnded()
            }
        }
    }

    func body(content: Content) -> some View {
        content.scaleEffect(value)
    }
}

backup

Leave a Comment