Проблема с несколькими объектами NSTimer в tableView - PullRequest
0 голосов
/ 08 января 2020

У меня есть TableView, где каждая ячейка состоит из объекта NSTimer.

Мне удалось заставить его работать, где я могу запустить несколько таймеров, обновить их или удалить их. Но проблема возникает, когда я удаляю работающий таймер,

, когда я делаю это, следующий объект в моем списке счетчиков (объекты таймера) заменяет этот, но связанная с ним UILabel все еще ссылается на старый индекс и, следовательно, пользовательский интерфейс больше не обновляется.

В этот момент я действительно застрял, должен ли я сделать недействительными все таймеры и запустить их снова () после удаления одного или есть лучший способ сделать это?

Класс 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)
    }
}

класс модели

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?
}

1 Ответ

0 голосов
/ 08 января 2020

Для любого, кто придет к этому вопросу в будущем, я использовал подсказку, предложенную Paulw11, и создал только один таймер, и вместо сохранения индексов вручную внутри массива сохранял все текущие ячейки в tableView каждый раз, когда функция cellForRowAt получает Позвонил.

вот модифицированный код для моего решения:

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)
    }
}
...