1

In a game screen, I use a background queue to count elapsed time in a game, this func is based on Daniel Galasko solution, a perfect solution for my app: it allows user to navigate through other VC, while timer is still on. The VC hierarchy is quite simple : the game settings VCs are handled in a tabBarController. The game screen is apart. User can change settings while timer is on. Settings are stored in CoreData.

In my game screen, where I need to display the timer, I have a label that displays the elapsed time and 2 buttons : Play/Pause button and Reset button.

I call my setup timer func in ViewDidLoad. The default value for my timer is the stored value in CoreData, it has been defined in settings. And this value is incremented by 1 every second when timer is on. I also have a static let shared that keep timer status (resumed / suspended).

When I'm on the game screen, and if my timer is suspended, my play/Pause button works perfect : I can navigate to others views (mean dismiss my screen game), present again my screen game and resume my counter. It updates my label correctly.

The problem is when I dismiss the game screen view while timer is running. Timer works (print func shows that timer is still running), but when I present the screen again, I'm unable to pause/resume/restart it, and my label is stopped at the second I came back... while timer is still running back.

private var counter: Int16?
var t = RepeatingTimer(timeInterval: 1)
let gameIsOn = isGameOnManager.shared

override func viewDidLoad() {
        super.viewDidLoad()
        print("is timer On ? \(String(describing: gameIsOn.isgameOn))")
        buildTimer()
        if gameIsOn.isgameOn == true {
            resumeTapped = true
            t.resume()
            PlayB.setImage(UIImage(named:"pause"), for: .normal)
        } else {
            resumeTapped = false
        }
    }

    func buildTimer(){
        self.t.eventHandler = {
            self.counter! += 1
            print("counter \(String(describing: self.counter!))")
            self.coreDataEntity?.TimeAttribute = self.counter ?? 0
            self.save()
            DispatchQueue.main.async {
                self.dataField.text = String(describing: self.counter ?? 0)
            }
        }
    }

@objc func didTapButton(_ button: UIButton) {
        if resumeTapped == false {
            t.resume()
            resumeTapped = true
            gameIsOn.isgameOn = true
            PlayB.setImage(UIImage(named:"pause"), for: .normal)
        }
        else if resumeTapped == true {
            t.suspend()
            resumeTapped = false
            gameIsOn.isgameOn = false
            PlayB.setImage(UIImage(named:"play"), for: .normal)
        }
    }
Creanomy
  • 216
  • 2
  • 12
  • What does “leave” and “come back” mean? In many cases your view controller is torn down when you leave. You need to save the current time into persistent storage when you leave so that when you return you can automatically start the timer from the time it is now. – matt Feb 02 '20 at 13:14
  • @matt, it does, when "I come back" (I mean change of view and come back to my VC), it has stored the counter value in CoreData, so the value is ok, but it doesn't actualize anymore, it doesn't restart... i did not write the entire func but my save func is OK. – Creanomy Feb 02 '20 at 13:23
  • Well, if you want help, provide sufficient code to reproduce the issue. You have lots of code that is merely referred to in your question. We cannot just imagine or guess what it does. – matt Feb 02 '20 at 13:27
  • @matt, I edited the func that store my counter value. – Creanomy Feb 02 '20 at 13:45
  • @Rob, I re-edited my question, it seems that my problem concerns more the way I can stop (kill) and restart a background timer than a problem of view hierarchy, but I agree, it was not clear.... – Creanomy Feb 04 '20 at 12:29
  • “I use a background queue to count elapsed time in a game” ... unrelated to the broader question, we tend to avoid this “increment counter in timer” pattern. We generally capture a start time, for example, and then update our UI, calculating the elapsed time. You are not guaranteed that the timer will fire when you want and you don’t want that messing up your counter. – Rob Feb 04 '20 at 22:56

1 Answers1

1

You have a strong reference cycle between your view controller, the timer, and the timer’s event handler. You should use a weak reference in the closure to break this cycle:

func buildTimer() {
    t.eventHandler = { [weak self] in
        guard let self = self else { return }

        ...
    }
}

That fixes the strong reference cycle.

But when you fix this, there is a chance that you’re going to see your timer stop running when you dismiss this view controller. If this is the case, that will indicate that the timer is not in the right object. It should be in some higher level object that persists throughout the app, not inside this view controller which is presented and dismissed.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • You made my day! it's true that DispatchSourceTimer have strong reference. So, your solution worked perfectly to keep my buttons connected and my label updated. The timer is fully functional when I dismiss the game screen... The surprise is when I re present the VC : counter label was updated but I had 2 counters in my print func... It has recreated a new timer... So, to be sure to keep only 1 dispatchSourceTimer, I assigned my timer as a shared let and each time I present it, I empty timer.eventHandler, cancel the timer and rebuild one. it's a bit tricky, but it works... – Creanomy Feb 05 '20 at 00:01