Уничтожается ли объект Timer автоматически, когда контроллер представления извлекается из стека навигации? - PullRequest
0 голосов
/ 08 мая 2020

Я создал объект Timer для многократного выполнения некоторого кода каждую секунду в одном из контроллеров представления в моем приложении. Мой вопрос: будет ли система автоматически аннулировать таймер, когда я выталкиваю контроллер представления из стека навигации? Каким-то образом моя интуиция подсказывает мне, что это не так, потому что сам объект таймера не привязан напрямую к объекту контроллера представления.

Редактировать Примечание: Ниже приведен код для файла V C swift, в котором создается таймер. Пожалуйста, не bash меня из-за моего любительского кода. Таким образом, создается V C этого типа, который помещается в стек навигации. Предполагая сценарий, когда пользователь не нажимал кнопку паузы (в этом случае таймер становится недействительным) перед тем, как вернуться к виду root, нажав кнопку возврата на панели навигации, будет ли объект таймера уничтожен?

//
//  TimerViewController.swift
//  SwiftyTimer
//
//  Created by Jiaming Zhou on 5/6/20.
//  Copyright © 2020 Jiaming Zhou. All rights reserved.
//

import UIKit

class TimerViewController: UIViewController {

    @IBOutlet var countDownLabel: UILabel!
    @IBOutlet var imageView: UIImageView!

    private var timer: Timer?
    private var timePassed = -1
    private enum status {
        case ongoing
        case paused
        case completed
    }

    private enum buttonImage {
        case cancelButton
        case pauseButton
        case resumeButton
    }

    private var state = status.ongoing

    var activity: Activity?

    override func viewDidLoad() {
        super.viewDidLoad()

        if let activity = activity {
            imageView.image = UIImage(named: activity.name)
            view.backgroundColor = UIColor(named: activity.color)
        }

        //Start a timer that increments every second
        updateTimer()
        creatTimer()
    }

    @IBAction func buttonsPressed(_ sender: UIButton) {

        switch sender.tag {
        case 0:
            timePassed = -1
            timer?.invalidate()
            state = status.ongoing
            creatTimer()
            updateTimer()
        case 1:
            if state == status.ongoing {
                timer?.invalidate()
                timer = nil
                state = status.paused
                sender.setBackgroundImage(UIImage(named: "\(buttonImage.resumeButton)"), for: .normal)
                sender.setTitle("Resume", for: .normal)
            } else if state == status.paused {
                creatTimer()
                state = status.ongoing
                sender.setBackgroundImage(UIImage(named: "\(buttonImage.pauseButton)"), for: .normal)
                sender.setTitle("Pause", for: .normal)
            }
        default:
            return
        }
    }


}

//MARK: - Timer

extension TimerViewController {

    func creatTimer() {
        let timer = Timer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
        RunLoop.current.add(timer, forMode: .common)
        self.timer = timer
    }

    @objc func updateTimer() {
        if let activity = activity {
            timePassed += 1

            if timePassed == activity.duration {
                self.timer?.invalidate()
                state = status.completed
                let alert = UIAlertController(title: "Time's Up!", message: "you have completed your activity", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "dismiss", style: .cancel, handler: nil))
                present(alert, animated: true)
            }

            let currentTime = activity.duration - timePassed
            let hours = currentTime / 3600
            let minutes = (currentTime / 60) % 60
            let seconds =  currentTime % 60

            var countDown = ""
            if hours > 0 {
                countDown += "\(hours):"
            }
            if minutes > 9 {
                countDown += "\(minutes):"
            } else {
                countDown += "0\(minutes):"
            }
            if seconds > 9 {
                countDown += "\(seconds)"
            } else {
                countDown += "0\(seconds)"
            }

            countDownLabel.text = countDown
        }
    }

}

Ответы [ 2 ]

2 голосов
/ 08 мая 2020

Timer не становится автоматически недействительным, потому что, когда вы его планируете, прогон l oop сохраняет сильную ссылку на него, независимо от того, был ли отклонен контроллер представления или нет. Есть много способов решить эту проблему, но два современных решения включают:

  1. Использовать блок завершения Timer:

    • Использовать шаблон [weak self] , поэтому таймер не будет сохранять строгую ссылку на self, тем самым нарушая цикл сильной ссылки.

    • Имейте deinit метод invalidate таймер, когда контроллер представления освобождается .

    Например:

    class ViewController: UIViewController {
        weak var timer: Timer?
    
        override viewDidLoad() {
            super.viewDidLoad()
            createTimer()
        }
    
        deinit {
            timer?.invalidate()
        }
    
        func createTimer() {
            timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in            
                self?.handleTimer(timer)
            }
        }
    
        func handleTimer(_ timer: Timer) { ... }
    }
    

    Обратите внимание, что объявление weak переменной timer не связано с нарушением цикла строгой ссылки, а скорее служит другой цели, а именно, чтобы гарантировать, что когда таймер недействителен (если вы invalidate это где-то еще), переменная timer автоматически будет установлена ​​на nil. Ключом к прерыванию цикла сильной ссылки является [weak self] в закрытии таймера.

  2. Другой подход - использовать таймер GCD, который будет отменен когда вы удаляете сильную ссылку на него:

    • Опять же, используйте шаблон [weak self] для закрытия, чтобы избежать цикла сильных ссылок.

    • Но В отличие от Timer, таймер GCD автоматически останавливается при удалении ссылки DispatchSourceTimer. Таким образом, метод deinit для остановки таймера отправки не требуется.

    Таким образом:

    class ViewController: UIViewController {
        private let timer = DispatchSource.makeTimerSource(queue: .main)
    
        override viewDidLoad() {
            super.viewDidLoad()
            configureTimer()
        }
    
        func configureTimer() {
            timer.setEventHandler { [weak self] in
                self?.handleTimer()
            }
            timer.schedule(deadline: .now(), repeating: 1)
            timer.resume()
        }
    
        func handleTimer() { ... }
    }
    

Я бы обычно использовал подход Timer, но для этого включил бы GCD DispatchSourceTimer полноты.

1 голос
/ 08 мая 2020

Система не аннулирует таймер автоматически. Контроллер представления не имеет никакого отношения к самому таймеру, поскольку на таймер ссылается (сильно) объект RunL oop.

В документации Apple также прямо говорится, что единственный способ сделать таймер недействительным - это вызвать метод:

https://developer.apple.com/documentation/foundation/timer/1415405-invalidate

Этот метод - единственный способ удалить таймер из объекта RunL oop. Объект RunL oop удаляет свою сильную ссылку на таймер либо непосредственно перед возвратом метода invalidate (), либо в какой-то более поздний момент.

Если он был настроен с объектами целевой и пользовательской информации, получатель удаляет свои сильные ссылки на эти объекты.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...