1

Currently, I am trying to add UITapGestureRecognizer to a UIView in order to start a timer. However, whenever I tap the UIView multiple times by mistake, multiple gestures get recognized and timers run twice, or multiple times faster than usual.

I want to make sure that only 1 timer action / 1 tap gesture is recognized by the UIView and the 2nd tap onwards would be redundant (and later I will work on ensuring the 2nd tap "stops" the timer).

I tried reading this answer, but it didn't quite guide me on how I can prevent 2nd tap onwards or customize actions based on the states, and am still trying to figure it out, but I am getting stuck at this question.

Please help if you have any insights on how I can resolve this issue.

class ActiveExerciseTableViewCell: UITableViewCell, UITextFieldDelegate {
    
    var restTimer = Timer()
    var restTimeRemaining: Int = 180
    
    func setUpActiveExerciseUIViewLayout(){
        
        timerLabel.translatesAutoresizingMaskIntoConstraints = false
        timerLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        timerLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentView.frame.height-tableviewContentViewTabBarHeight)*0.55).isActive = true
        timerLabel.widthAnchor.constraint(equalToConstant: contentView.frame.width * 0.7).isActive = true
        timerLabel.heightAnchor.constraint(equalToConstant: 80).isActive = true
        timerLabel.font = .boldSystemFont(ofSize: 64)
        
        
        activeExerciseTimerUIView.translatesAutoresizingMaskIntoConstraints = false
        activeExerciseTimerUIView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        activeExerciseTimerUIView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentView.frame.height-tableviewContentViewTabBarHeight)*0.25).isActive = true
        activeExerciseTimerUIView.widthAnchor.constraint(equalToConstant: 225).isActive = true
        activeExerciseTimerUIView.heightAnchor.constraint(equalToConstant: 225).isActive = true
        
        
        let timerStartGesture = UITapGestureRecognizer(target: self, action: #selector(playTapped))
        timerStartGesture.numberOfTapsRequired = 1
        
        activeExerciseTimerUIView.addGestureRecognizer(timerStartGesture)
        activeExerciseTimerUIView.isUserInteractionEnabled = true
    }
    
    @objc func playTapped(_ sender: Any) {
        restTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(step), userInfo: nil, repeats: true)
    }
    
    @IBAction func pauseTapped(_ sender: Any) {
        restTimer.invalidate()
    }
    
    @IBAction func resetTapped(_ sender: Any) {
        restTimer.invalidate()
        restTimeRemaining = 180
        timerLabel.text = "\(restTimeRemaining)"
    }
    
    @objc func step() {
        if restTimeRemaining > 0 {
            restTimeRemaining -= 1
        } else {
            restTimer.invalidate()
            restTimeRemaining = 180
        }
        timerLabel.text = prodTimeString(time: TimeInterval(restTimeRemaining))
    }
    
    func prodTimeString(time: TimeInterval) -> String {
        let Minutes = Int(time) / 60 % 60
        let Seconds = Int(time) % 60
        
        return String(format: "%02d:%02d", Minutes, Seconds)
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
Michael
  • 370
  • 3
  • 11
  • 1
    Slightly unrelated but I recommend not using stuff like `Timer` inside a `ActiveExerciseTableViewCell`. These get reused when you scroll, so you'll probably run into some weird bugs later on. It's better to manage this in your parent view controller instead. – aheze Aug 28 '21 at 19:24
  • That's a very useful information. Right now, I am not having any bugs or issues. But I do understand your point because I had similar issue when I try to implement keyboard set-ups. At the end, I had to use my parent view. The problem is that - I am still learning and I don't quite know how to connect IBactions & objc func to UIcontrols within "UITableViewCell" classes. Perhaps, I will post another question on this later. – Michael Aug 29 '21 at 09:50
  • 1
    Hello Aheze, I encountered the exact issue you mentioned and I posted this question - https://stackoverflow.com/questions/68975261/timer-being-reused-in-uitableview-cell I would appreciate your help if you can. I tried to write the code to the parent controller, but I don't know how I can pass "UILabel parameter" via objective c method such as "playTapped" and found that doing so is complicated. – Michael Aug 29 '21 at 17:15

1 Answers1

2

Use a boolean to handle state changes:

class ActiveExerciseTableViewCell: UITableViewCell, UITextFieldDelegate {
    
    var restTimer = Timer()
    var restTimeRemaining: Int = 180
    var timerInitiated: Bool = false /// here!
    
    func setUpActiveExerciseUIViewLayout() {
        
        timerLabel.translatesAutoresizingMaskIntoConstraints = false
        timerLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        timerLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentView.frame.height-tableviewContentViewTabBarHeight)*0.55).isActive = true
        timerLabel.widthAnchor.constraint(equalToConstant: contentView.frame.width * 0.7).isActive = true
        timerLabel.heightAnchor.constraint(equalToConstant: 80).isActive = true
        timerLabel.font = .boldSystemFont(ofSize: 64)
        
        
        activeExerciseTimerUIView.translatesAutoresizingMaskIntoConstraints = false
        activeExerciseTimerUIView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        activeExerciseTimerUIView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentView.frame.height-tableviewContentViewTabBarHeight)*0.25).isActive = true
        activeExerciseTimerUIView.widthAnchor.constraint(equalToConstant: 225).isActive = true
        activeExerciseTimerUIView.heightAnchor.constraint(equalToConstant: 225).isActive = true
        
        
        let timerStartGesture = UITapGestureRecognizer(target: self, action: #selector(playTapped))
        timerStartGesture.numberOfTapsRequired = 1
        
        activeExerciseTimerUIView.addGestureRecognizer(timerStartGesture)
        activeExerciseTimerUIView.isUserInteractionEnabled = true
    }
    
    @objc func playTapped(_ sender: Any) {
        if !timerInitiated { /// check here
            restTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(step), userInfo: nil, repeats: true)
            self.timerInitiated = true
        }
    }
    
    @IBAction func pauseTapped(_ sender: Any) {
        restTimer.invalidate()
    }
    
    @IBAction func resetTapped(_ sender: Any) {
        restTimer.invalidate()
        restTimeRemaining = 180
        timerLabel.text = "\(restTimeRemaining)"
    }
    
    @objc func step() {
        if restTimeRemaining > 0 {
            restTimeRemaining -= 1
        } else {
            restTimer.invalidate()
            restTimeRemaining = 180
        }
        timerLabel.text = prodTimeString(time: TimeInterval(restTimeRemaining))
    }
    
    func prodTimeString(time: TimeInterval) -> String {
        let Minutes = Int(time) / 60 % 60
        let Seconds = Int(time) % 60
        
        return String(format: "%02d:%02d", Minutes, Seconds)
    }
}

If the timerInitiated boolean is true, It would mean that the exercise has already begun and won't schedule any more timers unless the boolean is changed.

aheze
  • 24,434
  • 8
  • 68
  • 125
excitedmicrobe
  • 2,338
  • 1
  • 14
  • 30