Reputation: 85
Hello i wrote a simple code where i created an animation with a Circle which changes its position. I want the animation to stop when i tap on that Circle, but it doesn't stop, no matter that i set the animation to nil. Maybe my approach is wrong in some way. I will be happy if someone helps.
struct ContentView: View {
@State private var enabled = true
@State private var offset = 0.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onAppear {
withAnimation(self.enabled ? .easeIn(duration: 5) : nil) {
self.offset = Double(-UIScreen.main.bounds.size.height + 300)
}
}
.onTapGesture {
self.enabled = false
}
}
}
Upvotes: 7
Views: 4904
Reputation: 494
After going through many things, I found out something that works for me. At the least for the time being and till I have time to figure out a better way.
struct WiggleAnimation<Content: View>: View {
var content: Content
@Binding var animate: Bool
@State private var wave = true
var body: some View {
ZStack {
content
if animate {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(.systemGray))
.offset(x: -25, y: -25)
}
}
.id(animate) //THIS IS THE MAGIC
.onChange(of: animate) { newValue in
if newValue {
let baseAnimation = Animation.linear(duration: 0.15)
withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
wave.toggle()
}
}
}
.rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
anchor: .center)
}
init(animate: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content) {
self.content = content()
self._animate = animate
}
}
Use
@State private var editMode = false
WiggleAnimation(animate: $editMode) {
VStack {
Image(systemName: image)
.resizable()
.frame(width: UIScreen.screenWidth * 0.1,
height: UIScreen.screenWidth * 0.1)
.padding()
.foregroundColor(.white)
.background(.gray)
Text(text)
.multilineTextAlignment(.center)
.font(KMFont.tiny)
.foregroundColor(.black)
}
}
What do you have to change? make an offset as @Binding, provide from outside
How does it work? .id(animate) modifier here does not refresh the view but just replaces it with a new one, and since you have offset as binding property, replaced view will have the correct offset.
Again this might not be the best solution but it works for my case.
Upvotes: 2
Reputation: 1396
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 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.)
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
Upvotes: 3
Reputation: 2024
What you need to understand is that only something that has already happened is being animated. It just takes longer to show. So in other words, when you set your offset to some value, it is going to animate towards that value, no matter what. What you, therefore, need to do is to set the offset to zero when tapping on the circle. The animation system is smart enough to deal with the 'conflicting' animations and cancel the previous one.
Also, a quick tip: You can do var offset: CGFloat = 0.0
so you don't have to cast it in the onAppear modifier
Upvotes: 1