Animation Triggered Using a Button Stops a Repeatforever Animation Added Onappear

Animation triggered using a Button stops a repeatForever animation added onAppear

SwiftUI animations are not added(cumulated), at least for now (SwiftUI 2.0). So here is possible workaround.

Tested with Xcode 12 / iOS 14

demo

struct WaterWaveView: View {
@State var progress: CGFloat = 0.1
@State var phase: CGFloat = 0.5

var body: some View {
VStack {
WaterWave(progress: self.progress, phase: self.phase)
.fill(Color.blue)
.clipShape(Circle())
.frame(width: 250, height: 250)
.animation(phase == 0 ? .default : Animation.linear(duration: 1).repeatForever(autoreverses: false), value: phase)
.animation(.easeOut(duration: 1), value: progress)
.onAppear {
self.phase = .pi * 2
}
Button("Add") {
self.phase = 0
self.progress += 0.1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.phase = .pi * 2
}
}
Button("Reset") {
self.progress = 0.0
}
}
}
}

SwiftUI .onAppear withAnimation speeds up each time the view appears. Why?

I think it has to do with your += 360 because every time it appears the number of degrees it needs to rotate increases by another 360 degrees. Instead of adding the 360 in the appear try setting a state boolean for when the animation should run. Try the code below and see if it works for you.

struct HueRotationAnimation: ViewModifier {
@State var hueRotationValue: Double
@State private var animated = false

func body(content: Content) -> some View {
content
.hueRotation(Angle(degrees: animated ? hueRotationValue : hueRotationValue + 360)).animation(.linear(duration: 20).repeatForever(autoreverses: false))
.onAppear() {
self.animated.toggle()
}
}
}

This way the 360 animation should remain 360 degrees and the speed of the animation should not change.

SwiftUI animation not working when embedded inside another view

Animation is activate on state change. In provided code there is no change so no animation at all.

Below are main changes so I made it work. Tested with Xcode 11.4 / iOS 13.4 (with some replication of absent custom dependencies)

demo

  1. Made initial animation off
struct LoginView: View {
@State var showProgress = false

// ... other code

// no model provided so used same state for progress
ProgressButton(action: {
self.showProgress.toggle() // << activate change !!!
}, image: Image("password-lock"), text: L.Login.LoginSecurely, backgroundColor: self.isLoginButtonEnabled ? Color.primaryLightColor : Color.gray, showProgress: $showProgress)


  1. Add internal ring activating state inside progress button; same state used for hide/unhide and activating does not work, again, because when ring appeared there is no change (it is already true) so ring is not activated

    @State private var actiavteRing = false // << here !!
    var body: some View {
    Button(action: action) {
    HStack {

             if showProgress {
    RingView(color: textColor, show: self.$actiavteRing) // !!
    .frame(width: 25, height: 25)
    .transition(.opacity)
    .animation(.spring())
    .onAppear { self.actiavteRing = true } // <<
    .onDisappear { self.actiavteRing = false } // <<
  2. in Ring fixed animation deactivation to avoid cumulative effect (.none does not work here), so

    .rotationEffect(.degrees(show ? 360.0 : 0.0))
    .animation(show ? Animation.linear(duration:
    1.0).repeatForever(autoreverses: false) : .default) // << here !!

How to stop animation in the current state in SwiftUI

As @Schottky stated above, the offset value has already been set, SwiftUI is just animating the changes.

The problem is that when you tap the circle, onAppear is not called again which means enabled is not checked at all and even if it was it wouldn’t stop the offset from animating.

To solve this, we can introduce a Timer and do some small calculations, iOS comes with a built-in Timer class that lets us run code on a regular basis. So the code would look like this:

struct ContentView: View {
@State private var offset = 0.0
// 1
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
@State var animationDuration: Double = 5.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onTapGesture {
// 2
timer.upstream.connect().cancel()
}
.onReceive(timer) { _ in
// 3
animationDuration -= 0.1
if animationDuration <= 0.0 {
timer.upstream.connect().cancel()
} else {
withAnimation(.easeIn) {
self.offset += Double(-UIScreen.main.bounds.size.height + 300)/50
}
}
}
}
}
  1. This creates a timer publisher which asks the timer to fire every 0.1 seconds. It says the timer should run on the main thread, It should run on the common run loop, which is the one you’ll want to use most of the time. (Run loops lets iOS handle running code while the user is actively doing something, such as scrolling in a list or tapping a button.)

  2. When you tap the circle, the timer is automatically cancelled.

  3. We need to catch the announcements (publications) by hand using a new modifier called onReceive(). This accepts a publisher as its first parameter (in this came it's our timer) and a function to run as its second, and it will make sure that function is called whenever the publisher sends its change notification (in this case, every 0.1 seconds).

After every 0.1 seconds, we reduce the duration of our animation, when the duration is over (duration = 0.0), we stop the timer otherwise we keep decreasing the offset.

Check out triggering-events-repeatedly-using-a-timer to learn more about Timer

SwiftUI Custom View repeat forever animation show as unexpected

This looks like a bug of NavigationView: without it animation works totally fine. And it wan't fixed in iOS15.

Working solution is waiting one layout cycle using DispatchQueue.main.async before string animation:

struct LoadingView: View {
@State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.red, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
}

Offset with animation is breaking buttons SwiftUI

How Offsetting works?

First of all, this is an expected behavior. Because when you use offset, SwiftUI shifts the displayed contents. To be brief, that means, SwiftUI shifts the View itself

Since onTapGesture only recognizes the touches on the view that also explains why you can click to an offsetted View

offset

How Animation Works?

In your code, you're offsetting your View First, then you're applying your animation. When you use withAnimation, SwiftUI recomputes the view's body with provided animation, but keep in mind that it does not change anything that is applied to the View beforehand.

animation

Notice how Click Me becomes clickable when entering the red rectangle. That happens because the red rectangle indicates the final offset amount of the Click Me button. (so it is just a placeholder)

So the View itself, and the offset has to match because as you offset your View first, SwiftUI needs your view there to trigger the tap gestures.

Possible solution

Now that we understand the problem, we can solve it. So, the problem happens because we are offsetting our view first, then applying animation.

So if that does not help, one possible solution could be to change the offset in periods (for example, I used 0.1 seconds per period) with an animation, because that would result in SwiftUI repositioning the view every time we change the offset, so our weird bug should not occur.

Code:

struct ContentView: View {
@State private var increment : CGFloat = 1
@State private var offset : CGFloat = 0
var body: some View {
ZStack {
Button("Click Me") {
print("Click")
}
.fontWeight(.black)
}
.tappableOffsetAnimation(offset: $offset, animation: .linear, duration: 5, finalOffsetAmount: 300)

}
}

struct TappableAnimationModifier : ViewModifier {
@Binding var offset : CGFloat

var duration : Double
var finalOffsetAmount : Double
var animation : Animation
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()

func body(content: Content) -> some View {
content
.animation(animation, value: offset)
.offset(x: offset)
.onReceive(timer) { input in
/*
* a simple math here, we're dividing duration by 0.1 because our timer gets triggered
* in every 0.1 seconds, so dividing this result will always produce the
* proper value to finish offset animation in `x` seconds
* example: 300 / (5 / 0.1) = 300 / 50 = 6 increment per 0.1 second
*/
if (offset >= finalOffsetAmount) {
// you could implement autoReverses by not canceling the timer here
// and substracting finalOffsetAmount / (duration / 0.1) until it reaches zero
// then you can again start incrementing it.
timer.upstream.connect().cancel()
return
}
offset += finalOffsetAmount / (duration / 0.1)
}
}
}
extension View {
func tappableOffsetAnimation(offset: Binding<CGFloat>, animation: Animation, duration: Double, finalOffsetAmount: Double) -> some View {
modifier(TappableAnimationModifier(offset: offset, duration: duration, finalOffsetAmount: finalOffsetAmount, animation: animation))
}
}

Here's how it looks like:

solution

Your view is running, go catch it out x)

Repeating Action Continuously In SwiftUI

Animation.basic is deprecated. Basic animations are now named after their curve types: like linear, etc:

var foreverAnimation: Animation {
Animation.linear(duration: 0.3)
.repeatForever()
}

Source:
https://forums.swift.org/t/swiftui-animation-basic-duration-curve-deprecated/27076

SwiftUI Animation onLongPressGesture(minimumDuration: 0.5) automatically starts

Make animation active explicit per-value change, like

return Circle()
.stroke(innerColor, lineWidth: 20)
.frame(width: side, height: side)
.scaleEffect(animationEffect ? 0.9 : 1)
.opacity(animationEffect ? 0.7 : 1)
.animation(touchpulse, value: animationEffect) // << here !!

How to trigger SwiftUI animation via change of non-State variable

You can use onChange to watch throbbing and then assign an animation. If true, add a repeating animation, and if false, just animate back to the original scale size:

struct MyCircle: View {
var throbbing: Bool
@State private var scale : CGFloat = 0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.scaleEffect(1 + scale)
.foregroundColor(.blue)
.onChange(of: throbbing) { newValue in
if newValue {
withAnimation(.easeInOut.repeatForever()) {
scale = 0.2
}
} else {
withAnimation {
scale = 0
}
}
}
}
}


Related Topics



Leave a reply



Submit