0

I have a Timer object within my view:

@State var timer: Timer?

Inside the view's body, I start the timer when the value of popup changes to true, and I invalidate the timer when viewModel.present changes to true.

var body: some View {
  /* ... */
  .onChange(of: viewModel.popup) { popup in guard popup else { return }; setupTimer() }
  .onChange(of: viewModel.present) { present in guard present else { return }; resetTimer() }
}

In setupTimer(), I create the timer instance:

private func setupTimer() {
  guard timer == nil else { return }
  timer = Timer.scheduledTimer(withTimeInterval: 8.0, repeats: false, block: { _ in
    // This code won't stop running!
  })
}

Next, in resetTimer(), I invalidate and remove the timer:

private func resetTimer() {
  timer?.invalidate()
  timer = nil
  // ...
}

But the timer continues to run. I'm not sure why timer?.invaludate() fails to work; I've used breakpoints to assure that

  1. setupTimer() is indeed only called once such that only a single timer is running (as per the answer here)
  2. self.timer is non-nil when invalidating within resetTimer (as per the answer here)

Any ideas on what is going wrong here? I'm running this program in macOS 12.2.1.

Edit: Ok, so after some investigation, I've seemed to narrow down the problem. It seems as though the timer is successfully invalidated when setupTimer() is called within a tap gesture or action by the user. However, if setupTimer() is called "automatically" within a view (i.e. via onAppear, onChange, etc.) then it cannot be invalidated. So the new question I have is: why is this? Why can I not call setupTimer() within onAppear/onChange and cancel the timer?

Ben Myers
  • 1,173
  • 1
  • 8
  • 25
  • Your understanding about State wrapper is not 100% correct, State cannot and did not design for tracking a reference type! Your code should not and must work, because State doing its job. You should change your approach for using observed object. – ios coder Mar 19 '22 at 02:30
  • @swiftPunk Odd though, the code seems to work in some cases even when the State wrapper is used; so, I'm not sure this is the root of the issue. – Ben Myers Mar 19 '22 at 08:36
  • State wrapper is for value type designed! StateObject wrapper is for reference type! – ios coder Mar 19 '22 at 14:03

1 Answers1

2

There could be some other code (that you are not showing) affecting the timer, or maybe it does not work well on macOS 12.2.1. However, here is a simple test that works well for me, on macos 12.3 (the only one I have) using xcode 13.3, targets ios 15 and macCatalyst 12. Does this code work for you?

struct ContentView: View {
    @State var timer: Timer?
    
    var body: some View {
        VStack (spacing: 55) {
            Button(action: {setupTimer()}) {
                Text("start")
            }
            Button(action: {resetTimer()}) {
                Text("reset")
            }
        }.frame(width: 333, height: 333)
        .onAppear {
           setupTimer()
        }
    }
    
    private func setupTimer() {
        guard timer == nil else { return }
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
            // This code stop running when timer invalidate
            print("----> timer running")
        })
    }
    
    private func resetTimer() {
        timer?.invalidate()
        timer = nil
    }
}

Note that in your code, after resetTimer() is called any change in viewModel.popup may trigger setupTimer() again, and so the timer starts again since it is nil.