Reputation: 2837
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
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
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
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