[英]Problem with Several NSTimer objects in tableView
I have a TableView where each cell consists of a NSTimer object.我有一个 TableView,其中每个单元格都包含一个 NSTimer 对象。
I have managed to make it work where I can run several timers, update them or delete them.我设法让它在我可以运行多个计时器的地方工作,更新它们或删除它们。 But problem arises when I delete a running timer,
但是当我删除一个正在运行的计时器时出现问题,
when I do that the next object in my list of Counters (timer objects) replaces this one, but the UILabel that is related to that is still referring to the old index and therefore the UI doesn't get updated any more.当我这样做时,我的计数器(计时器对象)列表中的下一个对象替换了这个对象,但与之相关的 UILabel 仍然引用旧索引,因此 UI 不再更新。
At this point I'm really stuck, should I invalidate all timers and fire() them again after deleting one or is there a better way to do so?在这一点上,我真的被卡住了,我应该在删除一个计时器后使所有计时器无效并再次触发()它们还是有更好的方法来做到这一点?
ViewController Class (main issue is at "playPauseCounter" function) ViewController 类(主要问题在于“playPauseCounter”函数)
import UIKit
import RealmSwift
class TimerController: UITableViewController, editDataDelegate, settingVCDelegate {
// MARK: - Properties
// delegate objects
var timerArray: [String: Timer]?
var counterArray: Results<Counter>?
var counter: Counter?
var index: String?
var counters: Results<Counter>?
var timerDict = [String: Timer]()
private let reuseIdentifier = "TimerCell"
private let cellSpacingHeight: CGFloat = 10
var shared = DatabaseService.shared
var sharedNotif = NotifService.shared
// MARK: - Initializers
override func viewDidLoad() {
super.viewDidLoad()
configureNavigationBar()
tableView.register(TimerCell.self, forCellReuseIdentifier: reuseIdentifier)
// How to handle when application goes to background and then comes to foreground
NotificationCenter.default.addObserver(self, selector: #selector(pauseWhenBackground(noti:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(continueWhenForeground(noti:)), name: UIApplication.willEnterForegroundNotification, object: nil)
sharedNotif.requestLocalNotification()
if shared.getCurrentTheme() == true {
overrideUserInterfaceStyle = .dark
} else {
overrideUserInterfaceStyle = .light
}
refreshData()
}
override func viewDidAppear(_ animated: Bool) {
refreshData()
if shared.getCurrentTheme() == true {
overrideUserInterfaceStyle = .dark
} else {
overrideUserInterfaceStyle = .light
}
tableView.reloadData()
}
// MARK: - UITableView Functions
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .normal, title: "Delete") { (_, _, completion) in
guard let counter = self.counters?[indexPath.section] else { return }
self.sharedNotif.removeLocalNotificationPending(id: counter.id)
if let t = self.timerDict[counter.id] {
t.invalidate()
}
self.shared.delete(idx: indexPath.section)
self.tableView.reloadData()
completion(true)
}
action.image = #imageLiteral(resourceName: "delete")
action.backgroundColor = .textRed()
return UISwipeActionsConfiguration(actions: [action])
}
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let edit = handleEdit(at: indexPath)
return UISwipeActionsConfiguration(actions: [edit])
}
override func numberOfSections(in tableView: UITableView) -> Int {
counters = shared.fetchAllCounters()
guard let counters = counters else { return 0 } // later here load items from the database
return counters.count
}
// There is just one row in every section
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
// Set the spacing between sections
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return cellSpacingHeight
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
// Make the background color show through
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
headerView.backgroundColor = UIColor.clear
return headerView
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// swiftlint:disable force_cast
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! TimerCell
// swiftlint:enabl1e force_cast
if let counters = counters {
cell.counter = counters[indexPath.section]
changeColorsLight(cell: cell, mode: counters[indexPath.section].counterMode)
}
cell.delegate = self
return cell
}
func footerAddButton() {
let footerView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let btn = UIButton(type: .roundedRect)
btn.setTitle("ADD", for: .normal)
btn.setTitleColor(.black, for: .normal)
btn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
btn.translatesAutoresizingMaskIntoConstraints = false
btn.layer.cornerRadius = 5
btn.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
footerView.addSubview(btn)
tableView.tableFooterView = footerView
}
//MARK: - Helper Methods
func refreshData() {
counters = shared.fetchAllCounters()
if let counters = counters {
for counter in counters where timerDict[counter.id] == nil {
timerDict[counter.id] = Timer()
}
}
}
func getTimeDifference(_ startDate: Date) -> Int {
let calendar = Calendar.current
let components = calendar.dateComponents([.second], from: startDate, to: Date())
if let secs = components.second {
return abs(secs)
}
return 0
}
func configureNavigationBar() {
navigationController?.navigationBar.barTintColor = UIColor.systemGray
navigationController?.navigationBar.isTranslucent = false
navigationItem.title = "Timers"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Settings", style: .done, target: self, action: #selector(goToSettings))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Add", style: .done, target: self, action: #selector(goToNewTimer))
}
//MARK: - Event Handlers
@objc func pauseWhenBackground(noti: Notification) {
counters = shared.fetchAllCounters()
guard let counters = counters else { return }
for counter in counters where counter.counterMode == Mode.running.rawValue {
guard let idx = counters.index(of: counter) else { return }
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: nil, md: nil, timer: Date())
}
}
@objc func continueWhenForeground(noti: Notification) {
counters = shared.fetchAllCounters()
guard let counters = counters else { return }
for counter in counters where counter.counterMode == Mode.running.rawValue {
if let savedDate = counter.savedTime {
let diff = getTimeDifference(savedDate)
if diff > 0 {
guard let idx = counters.index(of: counter) else { return }
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime - diff, md: nil, timer: nil)
}
}
}
}
@objc func goToSettings() {
let settingVC = SettingController()
settingVC.delegateVC = self
timerArray = timerDict
counterArray = shared.fetchAllCounters()
navigationController?.pushViewController(settingVC, animated: true)
}
@objc func goToNewTimer() {
let newTimerVC = TimeController()
navigationController?.pushViewController(newTimerVC, animated: true)
}
func handleEdit(at indexPath: IndexPath) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Edit") { (_, _, _) in
let vc = TimeController()
vc.delegate = self
self.counter = self.counters![indexPath.section]
self.index = self.counters![indexPath.section].id
self.navigationController?.pushViewController(vc, animated: true)
}
action.image = #imageLiteral(resourceName: "edit")
action.backgroundColor = .rgb(red: 239, blue: 13, green: 155)
return action
}
}
extension TimerController: CounterDelegate {
// transfer seconds to h/m/s
func updateCounterView(seconds: Int) -> String {
let arr = secondsToDate(seconds: seconds)
return String(format: "%02i:%02i:%02i", arr[0], arr[1], arr[2])
}
func playPauseCounter(cell: TimerCell) {
let idx = self.tableView.indexPath(for: cell)
guard let index = idx?.section else { return }
guard let counter = counters?[index] else { return }
let timeLabel = cell.timeLabel
if counter.counterMode == Mode.notStarted.rawValue || counter.counterMode == Mode.paused.rawValue {
changeColorsLight(cell: cell, mode: Mode.running.rawValue)
// add local notification when user resumes or starts the timer
sharedNotif.addLocalNotificationAlert(id: counter.id, name: counter.name, seconds: counter.currentTime)
cell.backgroundColor = UIColor.rgb(red: 1, blue: 255, green: 123)
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: nil, md: .running, timer: nil)
timerDict[counter.id] = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] (_) in
if counter.currentTime > 0 {
print(counter.currentTime)
self?.shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime-1, md: nil, timer: nil)
timeLabel.text = self?.updateCounterView(seconds: counter.currentTime)
} else if counter.currentTime == 0 {
// remove all delivered local notifications left
changeColorsLight(cell: cell, mode: Mode.ended.rawValue)
self?.sharedNotif.removeLocalNotificationsDelivered()
self?.timerDict[counter.id]!.invalidate()
self?.shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime, md: .ended, timer: nil)
}
})
RunLoop.current.add(timerDict[counter.id]!, forMode: .common)
timerDict[counter.id]!.tolerance = 0.15
} else if counter.counterMode == Mode.running.rawValue {
changeColorsLight(cell: cell, mode: Mode.paused.rawValue)
sharedNotif.removeLocalNotificationPending(id: counter.id)
timerDict[counter.id]!.invalidate()
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime, md: .paused, timer: nil)
}
}
func resetCounter(cell: TimerCell) {
let idx = self.tableView.indexPath(for: cell)
guard let index = idx?.section else { return }
guard let counter = counters?[index] else { return }
timerDict[counter.id]!.invalidate()
// remove the previous notification (if exists) before resetting the timer
sharedNotif.removeLocalNotificationPending(id: counter.id)
// change currentTime back to original
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.originalTime, md: .notStarted, timer: nil)
cell.timeLabel.text = String(counter.originalTime)
changeColorsLight(cell: cell, mode: Mode.notStarted.rawValue)
}
}
model class模型类
import Foundation
import RealmSwift
class Counter: Object {
@objc dynamic var id: String = UUID().uuidString
@objc dynamic var name: String = ""
@objc dynamic var originalTime: Int = 0
@objc dynamic var currentTime: Int = 0
// Realm database doesnt work properly with objects of type enum
@objc dynamic var counterMode: Int = 0
@objc dynamic var savedTime: Date?
}
For anyone coming to this question in the future, I used the hint suggested by Paulw11 and created only one timer, and instead of saving indexes manually inside an array, saved all the current cells in the tableView each time the cellForRowAt function gets called.对于将来遇到这个问题的任何人,我使用了 Paulw11 建议的提示并只创建了一个计时器,而不是在数组中手动保存索引,而是在每次调用 cellForRowAt 函数时保存 tableView 中的所有当前单元格。
here is the modified code for my solution:这是我的解决方案的修改代码:
import UIKit
import RealmSwift
class TimerController: UITableViewController, editDataDelegate, settingVCDelegate {
// MARK: - Properties
// delegate objects
var timerArray: [String: Timer]?
var counterArray: Results<Counter>?
var counter: Counter?
var index: String?
var counters: Results<Counter>?
var timer = Timer()
var currentRunning = [TimerCell]()
private let reuseIdentifier = "TimerCell"
private let cellSpacingHeight: CGFloat = 10
var shared = DatabaseService.shared
var sharedNotif = NotifService.shared
// MARK: - Initializers
override func viewDidLoad() {
super.viewDidLoad()
shared.deleteAll()
configureNavigationBar()
tableView.register(TimerCell.self, forCellReuseIdentifier: reuseIdentifier)
// How to handle when application goes to background and then comes to foreground
NotificationCenter.default.addObserver(self, selector: #selector(pauseWhenBackground(noti:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(continueWhenForeground(noti:)), name: UIApplication.willEnterForegroundNotification, object: nil)
sharedNotif.requestLocalNotification()
if shared.getCurrentTheme() == true {
overrideUserInterfaceStyle = .dark
} else {
overrideUserInterfaceStyle = .light
}
counters = shared.fetchAllCounters()
// add this feature so timer will continue working when user drags down the list, and add tolerance to timer
fireTimer()
}
override func viewDidAppear(_ animated: Bool) {
counters = shared.fetchAllCounters()
if shared.getCurrentTheme() == true {
overrideUserInterfaceStyle = .dark
} else {
overrideUserInterfaceStyle = .light
}
tableView.reloadData()
}
// MARK: - UITableView Functions
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .normal, title: "Delete") { (_, _, completion) in
guard let counter = self.counters?[indexPath.section] else { return }
self.sharedNotif.removeLocalNotificationPending(id: counter.id)
/////////////
self.shared.delete(idx: indexPath.section)
self.tableView.reloadData()
///////////
completion(true)
}
action.image = #imageLiteral(resourceName: "delete")
action.backgroundColor = .textRed()
return UISwipeActionsConfiguration(actions: [action])
}
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let edit = handleEdit(at: indexPath)
return UISwipeActionsConfiguration(actions: [edit])
}
override func numberOfSections(in tableView: UITableView) -> Int {
print(currentRunning)
currentRunning = []
counters = shared.fetchAllCounters()
guard let counters = counters else { return 0 } // later here load items from the database
return counters.count
}
// There is just one row in every section
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
// Set the spacing between sections
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return cellSpacingHeight
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
// Make the background color show through
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
headerView.backgroundColor = UIColor.clear
return headerView
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// swiftlint:disable force_cast
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! TimerCell
// swiftlint:enabl1e force_cast
if let counters = counters {
cell.counter = counters[indexPath.section]
changeColorsLight(cell: cell, mode: counters[indexPath.section].counterMode)
if cell.counter?.counterMode == Mode.running.rawValue {
currentRunning.append(cell)
}
}
cell.delegate = self
return cell
}
func footerAddButton() {
let footerView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let btn = UIButton(type: .roundedRect)
btn.setTitle("ADD", for: .normal)
btn.setTitleColor(.black, for: .normal)
btn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
btn.translatesAutoresizingMaskIntoConstraints = false
btn.layer.cornerRadius = 5
btn.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
footerView.addSubview(btn)
tableView.tableFooterView = footerView
}
//MARK: - Helper Methods
func getTimeDifference(_ startDate: Date) -> Int {
let calendar = Calendar.current
let components = calendar.dateComponents([.second], from: startDate, to: Date())
if let secs = components.second {
return abs(secs)
}
return 0
}
func configureNavigationBar() {
navigationController?.navigationBar.barTintColor = UIColor.systemGray
navigationController?.navigationBar.isTranslucent = false
navigationItem.title = "Timers"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Settings", style: .done, target: self, action: #selector(goToSettings))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Add", style: .done, target: self, action: #selector(goToNewTimer))
}
//MARK: - Event Handlers
@objc func pauseWhenBackground(noti: Notification) {
counters = shared.fetchAllCounters()
guard let counters = counters else { return }
for counter in counters where counter.counterMode == Mode.running.rawValue {
guard let idx = counters.index(of: counter) else { return }
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: nil, md: nil, timer: Date())
}
}
@objc func continueWhenForeground(noti: Notification) {
counters = shared.fetchAllCounters()
guard let counters = counters else { return }
for counter in counters where counter.counterMode == Mode.running.rawValue {
if let savedDate = counter.savedTime {
let diff = getTimeDifference(savedDate)
if diff > 0 {
guard let idx = counters.index(of: counter) else { return }
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime - diff, md: nil, timer: nil)
}
}
}
}
@objc func goToSettings() {
let settingVC = SettingController()
settingVC.delegateVC = self
//////////////
timerArray = [String: Timer]()
/////////////
counterArray = shared.fetchAllCounters()
navigationController?.pushViewController(settingVC, animated: true)
}
@objc func goToNewTimer() {
let newTimerVC = TimeController()
navigationController?.pushViewController(newTimerVC, animated: true)
}
func handleEdit(at indexPath: IndexPath) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Edit") { (_, _, _) in
let vc = TimeController()
vc.delegate = self
self.counter = self.counters![indexPath.section]
self.index = self.counters![indexPath.section].id
self.navigationController?.pushViewController(vc, animated: true)
}
action.image = #imageLiteral(resourceName: "edit")
action.backgroundColor = .rgb(red: 239, blue: 13, green: 155)
return action
}
}
extension TimerController: CounterDelegate {
// transfer seconds to h/m/s
func updateCounterView(seconds: Int) -> String {
let arr = secondsToDate(seconds: seconds)
return String(format: "%02i:%02i:%02i", arr[0], arr[1], arr[2])
}
func playPauseCounter(cell: TimerCell) {
let idx = self.tableView.indexPath(for: cell)
guard let index = idx?.section else { return }
guard let counter = counters?[index] else { return }
let timeLabel = cell.timeLabel
if counter.counterMode == Mode.notStarted.rawValue || counter.counterMode == Mode.paused.rawValue {
changeColorsLight(cell: cell, mode: Mode.running.rawValue)
sharedNotif.addLocalNotificationAlert(id: counter.id, name: counter.name, seconds: counter.currentTime)
cell.backgroundColor = UIColor.rgb(red: 1, blue: 255, green: 123)
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: nil, md: .running, timer: nil)
///////////////
currentRunning.append(cell)
/////////////
} else if counter.counterMode == Mode.running.rawValue {
changeColorsLight(cell: cell, mode: Mode.paused.rawValue)
sharedNotif.removeLocalNotificationPending(id: counter.id)
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime, md: .paused, timer: nil)
////////////////////
guard let indexOf = currentRunning.index(of: cell) else { return }
currentRunning.remove(at: indexOf)
///////////////
}
}
func fireTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (_) in
// for each item in the current running list, update it and save it to database
for cell in self.currentRunning {
guard let counter = cell.counter else { return }
let timeLabel = cell.timeLabel
if counter.currentTime > 0 {
self.shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime-1, md: nil, timer: nil)
timeLabel.text = self.updateCounterView(seconds: counter.currentTime)
} else if counter.currentTime == 0 {
// remove all delivered local notifications left
// changeColorsLight(cell: cell, mode: Mode.ended.rawValue)
self.sharedNotif.removeLocalNotificationsDelivered()
self.shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.currentTime, md: .ended, timer: nil)
// here remove this item from the list
guard let indexOf = self.currentRunning.index(of: cell) else { return }
self.currentRunning.remove(at: indexOf)
///////////////
}
}
})
RunLoop.current.add(timer, forMode: .common)
timer.tolerance = 0.15
}
func resetCounter(cell: TimerCell) {
let idx = self.tableView.indexPath(for: cell)
guard let index = idx?.section else { return }
guard let counter = counters?[index] else { return }
// remove this index from the running list
guard let indexOf = self.currentRunning.index(of: cell) else { return }
self.currentRunning.remove(at: indexOf)
//////////
sharedNotif.removeLocalNotificationPending(id: counter.id)
shared.editCounter(idx: counter.id, name: nil, ot: nil, ct: counter.originalTime, md: .notStarted, timer: nil)
cell.timeLabel.text = String(counter.originalTime)
changeColorsLight(cell: cell, mode: Mode.notStarted.rawValue)
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.