Reputation: 219
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
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:
Upvotes: 4
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
Upvotes: 1