Search code examples
iosswifttimeruikituitapgesturerecognizer

Tap Gesture Detected Multiple Times in the same UIView


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

Solution

  • 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.