I'm trying to make an idle animation in SwiftUI
that gets triggered if there's no touch in the screen for 3 seconds. I made a little animation that goes up and down (y offset 15) when there's no touch for 3 seconds and goes back to its original position when a touch occurs. But the thing is, when it goes to its original positon, autoreverses doesn't get triggered. Here's how it looks like:
Go Live button:
struct GoLiveButton: View {
@State private var animationOffset: CGFloat = 0
@Binding var isIdle: Bool
var body: some View {
ZStack {
Button(action: {} ) {
Text("Go Live")
.frame(width: 120, height: 40)
.background(Color.black)
.foregroundColor(.white)
.clipShape(Capsule())
.font(.system(size: 20))
.shadow(color: .black, radius: 4, x: 4, y: 4)
}
.offset(y: animationOffset)
.animation(.timingCurve(0.38, 0.07, 0.12, 0.93, duration: 2).repeatForever(autoreverses: true), value: isIdle)
.animation(.timingCurve(0.38, 0.07, 0.12, 0.93, duration: 2), value: !isIdle)
}
.onAppear {
self.isIdle = true
self.animationOffset = 15
}
.onChange(of: isIdle) { newValue in
if newValue {
self.animationOffset = 15
}
else {
self.animationOffset = 0
}
}
}
}
Here is the idle view:
struct StackOverflowView: View {
@State private var timer: Timer?
@State private var isIdle = false
var body: some View {
GeometryReader { geo in
GoLiveButton(isIdle: $isIdle)
}
.onTapGesture {
print("DEBUG: CustomTabView OnTapGesture Triggered")
self.isIdle = false
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
self.isIdle = true
}
}
.gesture(
DragGesture().onEnded { _ in
self.isIdle = false
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
self.isIdle = true
}
}
)
}
}
CodePudding user response:
Here is an approach without AnimatableData:
I added a second timer that just triggers the animations by offset (0, -15, 0, -15 ...) every 2 seconds, repeating forever.
If isIdle
changes to false, we just set offset to 0, and this will be animated too. We reset all timers. And again set the idle timer (3 secs) which when fires will start the animation timer (2 secs). voila.
(I also restructured the GoLiveButton
a little bit so it holds all relevant states in itself, and the parent view only has to control isIdle
)
struct GoLiveButton: View {
@Binding var isIdle: Bool
@State private var timer: Timer?
@State private var animationTimer: Timer?
@State private var animationOffset: CGFloat = 0
var body: some View {
ZStack {
Button(action: {} ) {
Text("Go Live")
.frame(width: 120, height: 40)
.background(Color.black)
.foregroundColor(.white)
.clipShape(Capsule())
.font(.system(size: 20))
.shadow(color: .black, radius: 4, x: 4, y: 4)
}
.offset(y: animationOffset)
.animation(.timingCurve(0.38, 0.07, 0.12, 0.93, duration: 2), value: animationOffset)
}
.onAppear {
self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
self.isIdle = true
animationTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in
animationOffset = (animationOffset == 15) ? 0 : 15
}
}
}
.onChange(of: isIdle) { newValue in
if newValue == false {
// reset all
self.animationTimer?.invalidate()
self.timer?.invalidate()
animationOffset = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
self.isIdle = true
animationTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in
animationOffset = (animationOffset == 15) ? 0 : 15
}
}
}
}
}
}
struct ContentView: View {
@State private var isIdle = false
var body: some View {
ZStack {
// for background tap only
Color.gray.opacity(0.2)
.onTapGesture {
print("tap")
self.isIdle = false
}
GoLiveButton(isIdle: $isIdle)
}
}
}