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
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)
- 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)
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 } // <<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
}
}
}
}
}
This creates a timer publisher which asks the timer to fire every
0.1
seconds. It says the timer should run on themain
thread, It should run on thecommon
run loop, which is the one you’ll want to use most of the time. (Run loops letsiOS
handle running code while the user is actively doing something, such as scrolling in a list or tapping a button.)When you tap the circle, the timer is automatically cancelled.
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
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.
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:
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
Programmatically Select All Cells in Tableview So Next Time They Are Pressed They Call Diddeselect
iOS 10. Coredata Insert New Object Sig Abrt
Swift-Animate Cashapelayer Stroke Color
Exc_Badinstruction When Computing a Hash Value
Turn Off Splash Screen When Using Flutterviewcontroller Within Existing Native App
Characteristic.Value from Bluetooth Reading in Swift
Can't Unwrap Optional.None When Setting Window Background Color
How to Rotate Text in Nsview? (Mac Os, Cocoa, Swift)
Realitykit - Updating Entity's Translation Returns Unexpected Values
Why Can't I Get The Index of a Filtered Realm List
Type Alias Declaration with Templates in Swift
How to Custom The Image of Mkannotation Pin
Nsdocumentcontroller.Opendocument Not Allowing Selection of Custom File Type
How to Remove The Fading Animation on .Ondelete Swiftui
Issue with Optional Core Data Relationship Using Nspersistentcloudkitcontainer
Preload a Scene to Prevent Lag
Why Does Editing a Textfield Throw a Nsinvalidargumentexception