Я работаю над отладкой, почему таймер обратного отсчета отключается в фоновом режиме.
Чтобы лучше ознакомиться, я сделал быструю и грязную реализацию таймера. Он запускает фоновую задачу, запускает стандартный таймер и добавляет его в RunLoop.
Всякий раз, когда меняются секунды в обратном отсчете, я распечатываю, сколько секунд на обратном отсчете у меня осталось и сколько секундОС дала мне (т.е. UIApplication.shared.backgroundTimeRemaining
).
Однако, когда я запускаю это в симуляторе, запускаю таймер и помещаю приложение в фоновый режим, таймер работает просто отлично и никогда не останавливается до тех пор, покаотсчитал весь путь вниз.
Некоторые вещи, которые нужно иметь в виду:
Я ХОЧУ, чтобы таймер не останавливался до тех пор, пока не будет выполнено, даже если он находится в фоновом режиме. Тем не менее, я знаю, что ОС обычно дает 3-5 минут в фоновом режиме. Отсюда и мой вопрос. Если у меня только 3-5 минут в фоновом режиме, то почему мой таймер работает в основном столько, сколько нужно? Разве симулятор не убивает приложения в то же время, что и физическое устройство в фоновом режиме?
Кроме того, я также установил обратный вызов, чтобы он срабатывал, когда и если ОС убивала меня (т. Е. Предусмотрен обратный вызов expirationHandler
). с помощью функции beginBackgroundTask(withName:
)
Любое понимание этого было бы полезно! Вот мой класс View Controller:
class TimerViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
// MARK: - Outlets
@IBOutlet weak var timeLeftLabel: UILabel!
@IBOutlet weak var timePicker: UIPickerView!
// MARK: - Properties
var timer: Timer?
var timeLeft: Int = 0 {
didSet {
DispatchQueue.main.async {
self.timeLeftLabel.text = "\(self.timeLeft.description) seconds left"
}
}
}
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
let backgroundTaskName = "bgTask"
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
@objc func applicationDidMoveToBackground() {
print("moved to backgorund")
}
@objc func applicationWillMoveToForegraund() {
print("moved to foreground")
}
// MARK: - Setup
func setupUI() {
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidMoveToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillMoveToForegraund), name: UIApplication.willEnterForegroundNotification, object: nil)
timePicker.tintColor = .white
timePicker.backgroundColor = .clear
}
func registerBackgroundTask() {
//end any bg tasks
endBackgroundTask()
//start new one
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: backgroundTaskName, expirationHandler: {
//times up, do this stuff when ios kills me
print("background task being ended by expiration handler")
self.endBackgroundTask()
})
assert(backgroundTask != UIBackgroundTaskIdentifier.invalid)
//actual meat of bg task
print("starting")
timePicker.isHidden = true
timeLeftLabel.isHidden = false
timeLeft = getCurrentPickerViewSeconds()
timeLeftLabel.text = "\(timeLeft) seconds left"
setupTimer()
}
func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = .invalid
}
func setupTimer() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fire), userInfo: nil, repeats: true)
if timer != nil {
RunLoop.current.add(timer!, forMode: .common)
} else {
print("timer is nil, didnt add to runloop")
}
}
// MARK: - Helpers
func getCurrentPickerViewSeconds() -> Int {
let mins = timePicker.selectedRow(inComponent: 0)
let seconds = timePicker.selectedRow(inComponent: 1)
let totalSeconds = seconds + (mins * 60)
return totalSeconds
}
// MARK: - Actions
@objc func fire() {
print("current time left: \(timeLeft)")
print("background time remaining: \(UIApplication.shared.backgroundTimeRemaining)")
if timeLeft > 0 {
timeLeft -= 1
} else {
print("done")
stopTimer()
}
}
@IBAction func startTimer() {
registerBackgroundTask()
}
@IBAction func stopTimer() {
print("stopping")
endBackgroundTask()
timePicker.isHidden = false
timeLeftLabel.isHidden = true
timer?.invalidate()
timer = nil
}
@IBAction func resetTimer() {
print("resetting")
stopTimer()
startTimer()
}
@IBAction func doneTapped() {
print("done-ing")
stopTimer()
dismiss(animated: true, completion: nil)
}
// MARK: - Picker View
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 59
}
func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
var label = ""
switch component {
case 0:
label = "m"
case 1:
label = "s"
default:
label = ""
}
let result = "\(row) \(label)"
let attributedResult = NSAttributedString(string: result, attributes: [NSAttributedString.Key.foregroundColor : UIColor.white])
return attributedResult
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let seconds = row + (60 * component)
timeLeft = seconds
}
}
Вот несколько скриншотов вывода (один с начала обратного отсчета, один с момента запуска expirationHandler, один с конца обратного отсчета):
![When the count down timer ended](https://i.stack.imgur.com/690ON.png)
![When the count down timer was started](https://i.stack.imgur.com/WhvHM.png)
![When the expirationHandler was fired](https://i.stack.imgur.com/1VHGu.png)