alexsmith
alexsmith

Reputation: 219

How to manage multiple timers in one viewController?

I'm working on Timers app and cannot understand how I can make work multiple timers for each cell.

I start and pause timers at didSelectRowAt:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let cell = tableView.cellForRow(at: indexPath) as! TimerTableViewCell
    let item = timers.items[indexPath.row]
    item.toggle()
    print(item)
    startPauseTimer(for: cell, with: item)
}

And this is my code for startPauseTimer:

   var timer = Timer()

    func startPauseTimer(for cell: TimerTableViewCell, with item: Timers) {
       if !item.isStarted {
           timer.invalidate()
           } else {    
           timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {timer in
                item.seconds -= 1
                cell.timerTime.text = self.formattedTime(time: TimeInterval(item.seconds))

               if item.seconds < 1 {
                   self.timer.invalidate()
                   cell.timerTime.text = self.formattedTime(time: TimeInterval(item.seconds))
                   item.isStarted = false
               }
        }
       }
    }

And my data model:

class Timers: NSObject, Codable {
  var name = ""
  var id = ""
  var seconds = 0
  var editSeconds = 0
  var isStarted = false

  func toggle() {
        isStarted = !isStarted
    }

Any my Cell code:

class TimerTableViewCell: UITableViewCell {

    @IBOutlet var timerLabel: UILabel!
    @IBOutlet var timerTime: UILabel
    @IBOutlet var startPauseButton: UIButton!
    @IBOutlet var resetButton: UIButton!    

    }

How I can manage multiple timers at once? When I use didSelectRowAt only the same Timer() instance is firing, so multiple timers is mixing. How I can divide multiple timers and make them work?

Upvotes: 1

Views: 1166

Answers (2)

DonMag
DonMag

Reputation: 77477

Here is a complete example, based on iOS Timer Tutorial by Fabrizio Brancati at RayWenderlich.com

Everything is done via code (no @IBOutlet or @IBAction connections needed), so just create a new UITableViewController and assign its custom class to ExampleTableViewController:


ExampleTableViewController.swift

//
//  ExampleTableViewController.swift
//  MultipleTimers
//
//  Created by Don Mag on 5/12/20.
//

import UIKit

class ExampleTableViewController: UITableViewController {

    let cellID: String = "TaskCell"

    var taskList: [Task] = []
    var timer: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()

        // -1 means use default 2-hours
        let sampleData: [(String, Double)] = [
            ("First (2 hours)", -1),
            ("Second (2 hours)", -1),
            ("Third (10 seconds)", 10),
            ("Fourth (30 seconds)", 30),
            ("Fifth (1 hour 10 minutes)", 60 * 70),
            ("Sixth (2 hours)", -1),
            ("Seventh (45 minutes)", 60 * 45),
            ("Eighth (2 hours)", -1),
            ("Ninth (1 hour 10 minutes)", 60 * 70),
            ("Tenth (2 hours)", -1),
            ("Eleventh (45 minutes)", 60 * 45),
            ("Thirteenth (2 hours)", -1),
            ("Fourteenth (2 minutes)", 60 * 2),
            ("Fifthteenth (11 minutes)", 60 * 11),
            ("Sixteenth (2 hours)", -1),
        ]

        sampleData.forEach { (s, t) in
            let task = Task(name: s, targetTime: t)
            self.taskList.append(task)
        }

        tableView.register(TaskCell.self, forCellReuseIdentifier: cellID)

        createTimer()
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

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

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! TaskCell

        cell.task = taskList[indexPath.row]

        return cell
    }

}

// MARK: - Timer
extension ExampleTableViewController {
    func createTimer() {
        if timer == nil {
            let timer = Timer(timeInterval: 1.0,
                              target: self,
                              selector: #selector(updateTimer),
                              userInfo: nil,
                              repeats: true)
            RunLoop.current.add(timer, forMode: .common)
            timer.tolerance = 0.1

            self.timer = timer
        }
    }

    func cancelTimer() {
        timer?.invalidate()
        timer = nil
    }

    @objc func updateTimer() {
        guard let visibleRowsIndexPaths = tableView.indexPathsForVisibleRows else {
            return
        }

        for indexPath in visibleRowsIndexPaths {
            if let cell = tableView.cellForRow(at: indexPath) as? TaskCell {
                cell.updateTime()
            }
        }
    }
}

TaskCell.swift

//
//  TaskCell.swift
//  MultipleTimers
//
//  Created by Don Mag on 5/12/20.
//

import UIKit

class TaskCell: UITableViewCell {

    let taskNameLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        return v
    }()

    let timerLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.font = UIFont.monospacedDigitSystemFont(ofSize: 17.0, weight: .medium)
        return v
    }()

    let actionButton: UIButton = {
        let v = UIButton()
        v.setTitle("Start", for: [])
        v.setTitleColor(.lightGray, for: .highlighted)
        v.setTitleColor(.darkGray, for: .disabled)
        v.backgroundColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0)
        return v
    }()

    let resetButton: UIButton = {
        let v = UIButton()
        v.setTitle("Reset", for: [])
        v.setTitleColor(.lightGray, for: .highlighted)
        v.setTitleColor(.darkGray, for: .disabled)
        v.backgroundColor = .red
        return v
    }()

    let buttonStack: UIStackView = {
        let v = UIStackView()
        v.axis = .horizontal
        v.distribution = .fillEqually
        v.spacing = 16
        return v
    }()

    var task: Task? {
        didSet {
            taskNameLabel.text = task?.name
            timerLabel.text = "0"
            setState()
            updateTime()
        }
    }

    func setState() -> Void {

        switch task?.state {
        case .running:
            actionButton.setTitle("Pause", for: [])
            actionButton.isEnabled = true
        case .paused:
            if task?.elapsedTime == 0 {
                actionButton.setTitle("Start", for: [])
                actionButton.isEnabled = true
            } else {
                actionButton.setTitle("Resume", for: [])
                actionButton.isEnabled = true
            }
        default: // .completed
            actionButton.setTitle("", for: [])
            actionButton.isEnabled = false
        }

    }

    func updateTime() {
        guard let task = task else {
            return
        }

        var t: Double = 0
        if task.state == .paused {
            t = task.targetTime - task.elapsedTime
        } else {
            t = task.targetTime - (Date().timeIntervalSince(task.creationDate) + task.elapsedTime)
        }

        let tm = Int(max(t, 0))

        let hours = tm / 3600
        let minutes = tm / 60 % 60
        let seconds = tm % 60

        let s = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
        timerLabel.text = s
        timerLabel.textColor = tm > 0 ? .black : .red
        if tm == 0 {
            task.state = .completed
            setState()
        }
    }

    @objc
    func buttonTapped(_ sender: UIButton) -> Void {
        guard let s = sender.currentTitle, let task = task else { return }
        switch s {

        case "Start", "Resume":
            task.state = .running
            task.creationDate = Date()

        case "Pause":
            task.state = .paused
            task.elapsedTime += Date().timeIntervalSince(task.creationDate)

        case "Reset":
            task.state = .paused
            task.elapsedTime = 0

        default:
            break
        }
        setState()
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {

        [buttonStack, resetButton, actionButton, timerLabel, taskNameLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        buttonStack.addArrangedSubview(actionButton)
        buttonStack.addArrangedSubview(resetButton)

        contentView.addSubview(buttonStack)
        contentView.addSubview(taskNameLabel)
        contentView.addSubview(timerLabel)

        let g = contentView.layoutMarginsGuide

        NSLayoutConstraint.activate([

            buttonStack.topAnchor.constraint(equalTo: g.topAnchor),
            buttonStack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            buttonStack.widthAnchor.constraint(equalToConstant: 280.0),

            taskNameLabel.topAnchor.constraint(equalTo: buttonStack.bottomAnchor, constant: 8.0),
            taskNameLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            taskNameLabel.widthAnchor.constraint(equalTo: buttonStack.widthAnchor),

            timerLabel.topAnchor.constraint(equalTo: taskNameLabel.bottomAnchor, constant: 8.0),
            timerLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            timerLabel.widthAnchor.constraint(equalTo: buttonStack.widthAnchor),

            timerLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),

        ])

        actionButton.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
        resetButton.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)

    }

}

Task.swift

//
//  Task.swift
//  MultipleTimers
//
//  Created by Don Mag on 5/12/20.
//

import Foundation

enum TimerState {
    case paused, running, completed
}

class Task {
    let name: String
    var creationDate = Date()
    var elapsedTime: Double = 0
    var state: TimerState = .paused
    var targetTime: Double = 60 * 60 * 2 // default 2 hours

    init(name: String, targetTime: Double) {
        self.name = name
        if targetTime != -1 {
            self.targetTime = targetTime
        }
    }
}

And here's how it looks while running:

enter image description here

Upvotes: 4

Dan O
Dan O

Reputation: 36

Well, there's only one instance of Timer kept, so it can be replaced with other timer when you don't want it.

And it's better to pass row instead of cell object to startPauseTimer since cells are normally reused. And then you can address required cell and change its text via func cellForRow(at indexPath: IndexPath) -> UITableViewCell?.

Let's create TimerModel class:

class TimerModel {
    let timer: Timers
    var actualTimer: Timer?

    init(_ timer: Timers) {
        self.timer = timer
        self.actualTimer = nil
    }
}

Then suppose you have timers = [TimerModel(Timers()), TimerModel(Timers())]

Selecting a row:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let cell = tableView.cellForRow(at: indexPath) as! TimerTableViewCell
    let item = timers[indexPath.row]
    item.timer.toggle()
    print(item)
    startPauseTimer(for: indexPath.row)
}

startPause:

func startPauseTimer(for row: Int) {
    let item = self.timers[row].timer
    if !item.isStarted {
        self.timers[row].actualTimer?.invalidate()
    } else {
        self.timers[row].actualTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {[weak self] timer in
            guard let self = self else { return }
            if let cell =  self.tableView.cellForRow(at: IndexPath(row: row, section: 0)) {
                item.seconds -= 1
                cell.textLabel?.text = "\(TimeInterval(item.seconds))"

                if item.seconds < 1 {
                    self.timers[row].actualTimer?.invalidate()
                    cell.textLabel?.text = "\(TimeInterval(item.seconds))"
                    item.isStarted = false
                }
            }
        }
    }
}

If you want to remove a row (to be called when row is removed by user or programmatically):

func onRemove(at row: Int) {
    timers[row].actualTimer?.invalidate()
    timers[row].actualTimer = nil
    timers.remove(at: row)
}

Please see apple docs for editing UITableView: https://developer.apple.com/documentation/uikit/uitableview

  • Putting the Table into Edit Mode if you want to edit by interaction with cells
  • Inserting, Deleting, and Moving Rows and Sections if you want to edit rows programmatically.

Upvotes: 1

Related Questions