Skip to content

Instantly share code, notes, and snippets.

@bhxlla
Last active February 6, 2024 16:35
Show Gist options
  • Save bhxlla/56b5a84e92d763a76dd730cf4ebff987 to your computer and use it in GitHub Desktop.
Save bhxlla/56b5a84e92d763a76dd730cf4ebff987 to your computer and use it in GitHub Desktop.
Cell Timers
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