Last active
February 6, 2024 16:35
-
-
Save bhxlla/56b5a84e92d763a76dd730cf4ebff987 to your computer and use it in GitHub Desktop.
Cell Timers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import Combine | |
/* | |
Problem: A tableview with a timer running on each cell, the timer should start only after the cell has been viewed. | |
The timer's state should be changed to pause when tapped, and back to running when tapped again. | |
*/ | |
class TimerCellViewModel { | |
enum State { case initial, running, paused } | |
lazy var timerSubject: CurrentValueSubject<Int, Never> = .init(0) | |
var timer: Int { | |
get { timerSubject.value } | |
set { timerSubject.send(newValue) } | |
} | |
var state = State.initial | |
let index: Int | |
init(_ index: Int) { | |
self.index = index | |
} | |
func ping() { | |
if state == .running { | |
self.timer = self.timer + 1 | |
} | |
} | |
func start() { | |
if state != .paused { | |
state = .running | |
} | |
} | |
func stop() { | |
state = .paused | |
} | |
func toggle () { state = state != .running ? .running : .paused } | |
} | |
class TimerViewModel { | |
lazy var cellViewModels: [TimerCellViewModel] = Array(repeating: (), count: 30).enumerated().map { | |
TimerCellViewModel($0.offset) | |
} | |
init() {} | |
lazy var timerPublisher: AnyPublisher<(),Never> = { | |
Timer.publish(every: 1, on: .main, in: .common) | |
.autoconnect() | |
.map { _ in () } | |
.share() | |
.eraseToAnyPublisher() | |
}() | |
var cancellables: Set<AnyCancellable> = .init() | |
func bindTimers() { | |
timerPublisher.sink { [weak self] _ in | |
self?.cellViewModels.forEach { tcv in tcv.ping() } | |
}.store(in: &cancellables) | |
} | |
} | |
class TimerViewController: UIViewController { | |
let vm: TimerViewModel | |
init(vm: TimerViewModel = .init()) { | |
self.vm = vm | |
super.init(nibName: nil, bundle: nil) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
lazy var listView: UITableView = { | |
let view = UITableView(frame: view.bounds) | |
view.delegate = self | |
view.dataSource = self | |
view.translatesAutoresizingMaskIntoConstraints = false | |
view.register(TimerCell.self, forCellReuseIdentifier: TimerCell.identifier) | |
return view | |
}() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
view.backgroundColor = .darkGray | |
view.addSubview(listView) | |
NSLayoutConstraint.activate([ | |
listView.topAnchor.constraint(equalTo: view.topAnchor), | |
listView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | |
listView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | |
listView.bottomAnchor.constraint(equalTo: view.bottomAnchor), | |
]) | |
vm.bindTimers() | |
} | |
} | |
extension TimerViewController: UITableViewDelegate, UITableViewDataSource { | |
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
vm.cellViewModels.count | |
} | |
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let cell = tableView.dequeueReusableCell(withIdentifier: TimerCell.identifier, for: indexPath) as! TimerCell | |
cell.config(with: vm.cellViewModels[indexPath.row]) | |
return cell | |
} | |
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
tableView.deselectRow(at: indexPath, animated: true) | |
vm.cellViewModels[indexPath.row].toggle() | |
} | |
} | |
class TimerCell: UITableViewCell { | |
static let identifier = "TimerCell" | |
var vm: TimerCellViewModel? | |
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | |
super.init(style: style, reuseIdentifier: reuseIdentifier) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
var cancellable: AnyCancellable? | |
func config(with vm: TimerCellViewModel) { | |
self.vm = vm | |
vm.start() | |
textLabel?.text = "\(vm.index), Time: \(vm.timer)" | |
cancellable = vm.timerSubject.sink(receiveValue: { [weak self] i in | |
guard let self, let vm = self.vm else { return } | |
textLabel?.text = "\(vm.index), Time: \(i)" | |
}) | |
} | |
override func prepareForReuse() { | |
super.prepareForReuse() | |
cancellable = nil | |
textLabel?.text = nil; | |
self.vm = nil | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment