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
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
iOS 9 '-Webkit-Overflow-Scrolling:Touch' and 'Overflow: Scroll' Breaks Scrolling Capability
Wkwebview Page Height Issue on iPhone X
Font Sizes in Uiwebview Does Not Match iOS Font Size
How to Put a Logo in Navigationview in Swiftui
Ccavenue iOS Kit Integration Kit
How to Bring Application to Foreground in iOS
Decode Base-64 Encoded Png in an Nsstring
How to Customize the Navigation Back Symbol and Navigation Back Text
How to Test My Xcode 7.2-Compiled App with iOS 9.3
Nsdateformatter: Date According to Currentlocale, Without Year
iPhone - Convert Ctfont to Uifont
Modal Segue, Navigation Bar Disappears
Ios: How to Find the Creation Date of a File
Enable or Disable iPhone Push Notifications Inside the App
How to Asynchronously Load an Image in an Uiimageview