tableView 中几个 NSTimer 对象的问题
Problem with Several NSTimer objects in tableView
我有一个 TableView,其中每个单元格都包含一个 NSTimer 对象。
我已经设法让它工作,我可以 运行 几个计时器,更新它们或删除它们。
但是当我删除 运行ning 计时器时出现问题,
当我这样做时,我的计数器列表(计时器对象)中的下一个对象将替换这个对象,但是与之相关的 UILabel 仍然引用旧索引,因此 UI 不再更新。
在这一点上我真的卡住了,我应该使所有计时器失效并在删除一个后再次触发()它们还是有更好的方法来做到这一点?
ViewController Class(主要问题在 "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)
}
}
型号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?
}
对于以后遇到这个问题的人,我使用了 Paulw11 建议的提示,只创建了一个计时器,而不是在数组中手动保存索引,每次 cellForRowAt 时都将所有当前单元格保存在 tableView 中函数被调用。
这是我的解决方案的修改代码:
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)
}
}
我有一个 TableView,其中每个单元格都包含一个 NSTimer 对象。
我已经设法让它工作,我可以 运行 几个计时器,更新它们或删除它们。 但是当我删除 运行ning 计时器时出现问题,
当我这样做时,我的计数器列表(计时器对象)中的下一个对象将替换这个对象,但是与之相关的 UILabel 仍然引用旧索引,因此 UI 不再更新。
在这一点上我真的卡住了,我应该使所有计时器失效并在删除一个后再次触发()它们还是有更好的方法来做到这一点?
ViewController Class(主要问题在 "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)
}
}
型号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?
}
对于以后遇到这个问题的人,我使用了 Paulw11 建议的提示,只创建了一个计时器,而不是在数组中手动保存索引,每次 cellForRowAt 时都将所有当前单元格保存在 tableView 中函数被调用。
这是我的解决方案的修改代码:
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)
}
}