iosswiftuitableviewtimerprepareforreuse

Timer being reused in UItableview cell


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

Solution

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