Home > Mobile >  .repeatForever() does not work when value is updated in SwiftUI
.repeatForever() does not work when value is updated in SwiftUI

Time:01-02

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:

hmm

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)
        }
    }
}
  • Related