One timer per table view cell

Solution 1:

This is how I would do it:

  • Create a struct to represent each row containing a start time and whether that row is currently running
  • Schedule a repeating timer for 0.5 seconds to account for the jitter in Timer
  • Each time the timer fires, update the visible rows that have "running" timers

import UIKit

struct TimerModel {
    private var startTime: Date?
    private var offset: TimeInterval = 0

    var elapsed : TimeInterval {
        get {
            return self.elapsed(since:Date())
        }
    }

    var isRunning = false {
        didSet {
            if isRunning  {
                self.startTime = Date()
            } else {
                if self.startTime != nil{
                    self.offset = self.elapsed
                    self.startTime = nil
                }
            }
        }
    }

    func elapsed(since: Date) -> TimeInterval {
        var elapsed = offset
        if let startTime = self.startTime {
            elapsed += -startTime.timeIntervalSince(since)
        }
        return elapsed
    }
}

class ViewController: UIViewController {

    @IBOutlet weak var tableview: UITableView!

    var timer: Timer?
    var timersActive = 0

    let formatter: DateComponentsFormatter = {
        let formatter = DateComponentsFormatter()
        formatter.zeroFormattingBehavior = .pad
        formatter.allowedUnits = [.hour, .minute, .second]
        return formatter
    }()

    var timers = [TimerModel](repeating:TimerModel(), count:30)


    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func elapsedTimeSince(_ startTime: Date) -> String {
        let elapsed = -startTime.timeIntervalSinceNow

        return self.formatter.string(from: elapsed) ?? "0:00:00"
    }

    func startTimer() {
        self.timersActive += 1
        guard self.timer == nil else {
            return
        }

        self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] (timer) in
           if let me = self {
            for indexPath in me.tableview.indexPathsForVisibleRows ?? [] {
                let timer = me.timers[indexPath.row]
                if timer.isRunning {
                    if let cell = me.tableview.cellForRow(at: indexPath) {
                        cell.textLabel?.text = me.formatter.string(from: timer.elapsed) ?? "0:00:00"
                    }
                }
            }
           }
        })
    }

    func stopTimer() {
        self.timersActive -= 1
        if self.timersActive == 0 {
            self.timer?.invalidate()
            self.timer = nil
        }
    }

}

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.timers.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let timer = self.timers[indexPath.row]

        cell.imageView?.image = timer.isRunning ? #imageLiteral(resourceName: "GreenDot") : #imageLiteral(resourceName: "RedDot")
        cell.textLabel?.text = self.formatter.string(from: timer.elapsed) ?? "0:00:00"

        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.timers[indexPath.row].isRunning = !self.timers[indexPath.row].isRunning
        self.tableview.reloadRows(at: [indexPath], with: .none)
        if self.timers[indexPath.row].isRunning {
            self.startTimer()
        } else {
            self.stopTimer()
        }
    }
}