shingo.nakanishi
shingo.nakanishi

Reputation: 2837

How to make the button inactive during animation

I want to deactivate the button until the animation is finished. I want it to be active when the animation is over.

I wrote the following, but it didn't work. It activates immediately.

import SwiftUI

struct ContentView: View {
  @State private var scale: CGFloat = 1
  @State private var isDisable = false
  
  var body: some View {
    VStack {
      Button(
        action: {
          isDisable = true
          withAnimation(
            .linear(duration: 1)
          ) {
            scale = scale - 0.1
            isDisable = false
          }
        },
        label: {
          Text("Tap Me")
        }
      )
      .disabled(
        isDisable
      )
      RectangleView().scaleEffect(
        scale
      )
    }
  }
}

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

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

Upvotes: 2

Views: 1132

Answers (3)

Abizern
Abizern

Reputation: 150745

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))
    }
}

Upvotes: 5

Sergio
Sergio

Reputation: 1932

Button {
    let duration: Double = 1
    isDisable = true
    DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
        isDisable = false
    }
    withAnimation(.linear(duration: duration)) {
        scale -= 0.1
    }
} label: {
    Text("Tap Me")
}

Upvotes: 1

Raja Kishan
Raja Kishan

Reputation: 19044

Add delay.

{
    isDisable = true
    withAnimation(
        .linear(duration: 1)
    ) {
        scale = scale - 0.1
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { //< Here
        isDisable = false
    }
}

Upvotes: 2

Related Questions