Timer being reused in UItableview cell

576 Views Asked by At

I am working on my UITableviewcell where I have a UIView for user to "tap" and start timer. I am creating my UItableview cell components completely programmatically and adding timer components within UITableviewcell class instead of the parent view controller.

The problem I am having is that my timer is supposed to be distinct for each UITableviewcell. User should be able to start timer in one cell and scroll through other cells to start/stop another. However, when I "tap" on my first cell, the timer gets started not only on the first one, but I can also see that it is starting on the 3rd cell.

I tried to rectify this issue by using PrepareForReuse and invalidate timer, but it also result in the 1st cell's timer to be invalidated.

I tried to research on this issue, but have found no exact match. Could anyone kindly advise how I can resolve this issue?

class ActiveExerciseTableViewCell: UITableViewCell, UITextFieldDelegate {
    
    static let tableviewidentifier = "activeExerciseTableViewCell"
    
    var tableviewContentViewTabBarHeight = CGFloat()
    
    var exerciseCellKeyboardHeight = CGFloat()
    
    var restTimer = Timer()
    var restTimeRemaining: Int = 180
    var isRestTimerRunning: Bool = false

    let activeExerciseTimerUIView: UIView = {
        let activeExerciseTimerUIView = UIView()
        activeExerciseTimerUIView.backgroundColor = .darkGray
        return activeExerciseTimerUIView
    }()
    
    let timerLabel: UILabel = {
        let timerLabel = UILabel()
        timerLabel.text = "180"
        timerLabel.textColor = .black
        timerLabel.adjustsFontSizeToFitWidth = true
        return timerLabel
    }()


    override func prepareForReuse() {
        super.prepareForReuse()
        
        if self.isRestTimerRunning == false {
            restTimer.invalidate()
            restTimeRemaining = 180
            timerLabel.text = prodTimeString(time: TimeInterval(restTimeRemaining))
        }

    }


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 isRestTimerRunning == false {
            restTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(step), userInfo: nil, repeats: true)
            self.isRestTimerRunning = true
        }
      
    }
    
    
    @IBAction func pauseTapped(_ sender: Any) {
        restTimer.invalidate()
    }
    

    @IBAction func resetTapped(_ sender: Any) {
        restTimer.invalidate()
        restTimeRemaining = 180
        timerLabel.text = prodTimeString(time: TimeInterval(restTimeRemaining))
    }
    
    
    @objc func step() {
        if restTimeRemaining > 0 {
            restTimeRemaining -= 1
        } else {
            restTimer.invalidate()
            restTimeRemaining = 180
        }
        timerLabel.text = prodTimeString(time: TimeInterval(restTimeRemaining))
    }
    
    public func prodTimeString(time: TimeInterval) -> String {
        let Minutes = Int(time) / 60 % 60
        let Seconds = Int(time) % 60

        return String(format: "%02d:%02d", Minutes, Seconds)
    }
1

There are 1 best solutions below

0
Michael On BEST ANSWER

Iam posting my own answer, issue had been resolved the issue by

  1. Creating a separate view model that stores timer.
  2. Initializing UITableView cell by using unique "reuseID" from tableview delegate.
SetViewModel

class SetViewModel {
    
    init(set: SetInformation) {
        self.set = set
        self.secondsRemaining = set.restTime ?? 0
        self.elapsedSeconds = 0
        self.isTimerRunning = false
        self.currentBackgroundDate = nil
        self.buttonPressCount = 0

    }
    
    var set: SetInformation
    var secondsRemaining: Int
    var elapsedSeconds: Int
    var isTimerRunning: Bool
    var currentBackgroundDate: Date?
    var buttonPressCount: Int
    
    weak var delegate: SetViewModelDelegate?
    
    // MARK: - Timer
    private var timer: Timer?
    
    func startTimer() {
        //stopTimer()
        isTimerRunning = true
        timer = .scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] timer in
            guard let self = self else {
                // If the model was discarded during the runtime of the timer
                timer.invalidate()
                return
            }
            
            if self.secondsRemaining > 0 && self.elapsedSeconds < (self.secondsRemaining + self.elapsedSeconds) {
                self.secondsRemaining -= 1
                self.elapsedSeconds += 1
            } else {
                timer.invalidate()
            }
            
            self.delegate?.setTimerUpdated(model: self)
            
        })
    }
    
    func stopTimer() {
        timer?.invalidate()
        isTimerRunning = false
        self.delegate?.setTimerUpdated(model: self)
    }

UItableView Delegate


 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let reuseID = String(indexPath.row) + ActiveExerciseTableViewCell.tableviewidentifier
        
        activeExerciseTableView.register(ActiveExerciseTableViewCell.self, forCellReuseIdentifier: reuseID)
        
        let cell = activeExerciseTableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as! ActiveExerciseTableViewCell
        
        //configure set information for the tableview cell based on setviewmodel
        cell.configure(setViewModel: setViewModels[indexPath.row], currentHeartRate: userCurrentHeartRate ?? 00)
        
        cell.setDescriptionLabel.text = "Set \(String(indexPath.row + 1)) of \(activeExerciseList[activeDisplayedExericseRow].setInformation!.count)"
        
        cell.tableviewContentViewTabBarHeight = contentViewTabBarHeight
        cell.delegate = self

        return cell
    }