Romit Kumar
Romit Kumar

Reputation: 3300

Timer not working properly in UITableView cell in iOS

I'm creating a timer in tableview cell's class and updating a label each second using the timer. But the label is getting updating multiple times each second for some cells while it is being updated at a different rate for other cells. I think the resusability of the cell might be causing the problem.

I'm creating the timer object as an instance variable:

var timer: Timer!

and calling:

runTimer(). //called in awakefromnib

while the runtimer method is:

func runTimer() {
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
}

func updateTimer() {
    seconds -= 1
    timerLabel.text = timeString(time: seconds)
}

What is the correct way to create a timer which update UILabel each second for each cell depending on some login in cell?

EDIT: This if my cellforrowAt:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "Doubts Cell", for: indexPath) as! DoubtsTableViewCell
    cell.postedQuestionAndAnswer = postedQuestionsArray[indexPath.row]

    if cell.timer != nil {
        cell.timer.invalidate()
    }

    cell.runTimer()

    return cell
}

and this is my required cell's class code:

var postedQuestionAndAnswer: PostedQuestionAndAnswer! {
    didSet {
        configureTableViewData()
    }
}

override func awakeFromNib() {
    super.awakeFromNib()

}

func configureTableViewData() {
    //.....other code
    seconds = getTimeRemaining(queryPostedTime: Double(postedQuestionAndAnswer.question!.createdAt), queryPostedDate: Double(postedQuestionAndAnswer.question!.createdAt))
}

func runTimer() {
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
}

func updateTimer() {
    seconds -= 1
    timerLabel.text = timeString(time: seconds) //timestring returns desired formatted string
}

Upvotes: 0

Views: 1680

Answers (3)

Y.Bonafons
Y.Bonafons

Reputation: 2349

It could be better to create an array of struct containing Timer and seconds in your viewController for example:

struct MyTimer {
    var timer: Timer?
    var seconds: Int
    init (seconds: Int) {
        self.seconds = seconds
    }
}

declared like that:

var timers = [[MyTimer(seconds: 5), MyTimer(seconds: 6)],
              [MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4), MyTimer(seconds: 3), MyTimer(seconds: 4)]]

Then you will need to create timer each time it doesn't exist yet.

func cellForRow(at indexPath: IndexPath) -> UITableViewCell? {

    if self.timers[indexPath.section][indexPath.row].timer == nil {
        self.timers[indexPath.section][indexPath.row].timer = self.createTimer(indexPath: indexPath)
    }

    // retrieve your cell here
    cell.timerLabel.text = String(self.timers[indexPath.section][indexPath.row].seconds)

    return cell
}

To create timer it could be like:

func createTimer (indexPath: IndexPath) -> Timer {
    let timeInterVal = self.getTimeInterval(indexPath: indexPath)
    let timer = Timer.scheduledTimer(timeInterval: timeInterVal,
                                     target: self,
                                     selector: #selector(updateTimer(sender:)),
                                     userInfo: ["indexPath": indexPath],
                                     repeats: true)
    return timer
}

func getTimeInterval (indexPath: IndexPath) -> TimeInterval {
    return 1 // or return a value depending on the indexPath
}

Then, to update timer:

@objc func updateTimer (sender: Timer) {
    guard
        let userInfo = sender.userInfo as? [String: IndexPath],
        let idx = userInfo["indexPath"] else {
            return
    }
    self.timers[idx.section][idx.row].seconds = self.timers[idx.section][idx.row].seconds - 1
    if
        let indexPathes = self.tableView.indexPathsForVisibleRows,
        indexPathes.contains(idx) {
        self.tableView.reloadRows(at: [idx], with: .none) // update directly the cell if it's visible
    }
}

Upvotes: 1

kd02
kd02

Reputation: 430

Rather have 1 timer and just reload your tableview, your tableViewCell's shouldn't have any custom logic in them, rather abstract that their parent.

Example:

class TableViewController: UITableViewController {

    var timer: Timer

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        runTimer()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        timer.invalidate()
    }

    func runTimer() {

        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
    }

    func updateTimer() {

        self.tableView.reloadData()
    }

    func getTimeIntervalForTableViewCell() -> TimeInterval { // Not sure what the 'seconds' object type is meant to be so I just used TimeInterval

        seconds = getTimeRemaining(queryPostedTime: Double(postedQuestionAndAnswer.question!.createdAt),
                                   queryPostedDate: Double(postedQuestionAndAnswer.question!.createdAt))

        return seconds
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "Doubts Cell", for: indexPath) as! DoubtsTableViewCell

        // rest of your databinding logic here

        let timeInterval = self.getTimeIntervalForTableViewCell()

        cell.timerLabel.text = timeString(time: timeInterval)

        return cell
    }
}

Upvotes: 0

pkorosec
pkorosec

Reputation: 676

If I understand correctly you only need one timer. Move it to your viewController. In updateTimer() only call tableView.reloadData(), to update all your cells at once. In your cell add timerLabel.text = timeString(time: seconds) to the end of configureTableViewData() and remove everything timer related.

This way all your cells will update every second, and configureTableViewData() will be called for every cell to update the labels accordingly.

Upvotes: 0

Related Questions