简体   繁体   English

tableView 中几个 NSTimer 对象的问题

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM