ios - oficial - NSTimer rápido en segundo plano
mac os mojave (2)
Desafortunadamente, no hay una forma confiable de ejecutar periódicamente algunas acciones mientras está en segundo plano. Puede hacer uso de las capturas de fondo, sin embargo, el sistema operativo no le garantiza que se ejecutarán periódicamente.
Mientras está en segundo plano, su aplicación se suspende y, por lo tanto, no se ejecuta ningún código, a excepción de las recuperaciones de fondo mencionadas anteriormente.
Me he encontrado con muchos problemas sobre cómo manejar NSTimer en segundo plano aquí en la pila o en otro lugar. He probado una de todas las opciones que realmente tenían sentido ... para detener el temporizador cuando la aplicación pasa a segundo plano con
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
y
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidBecomeActive", name: UIApplicationWillEnterForegroundNotification, object: nil)
Al principio pensé que mi problema estaba resuelto, simplemente guardé el tiempo en que la aplicación entró en segundo plano y calculé la diferencia cuando la aplicación entró en primer plano ... pero luego noté que el tiempo se pospuso en 3, 4, 5 segundos. .que en realidad no es lo mismo ... Lo he comparado con el cronómetro en otro dispositivo.
¿REALMENTE hay alguna solución SÓLIDA para ejecutar un NSTimer en segundo plano?
No debe meterse con ningún ajuste en función de cuándo ingresa al fondo o se reanuda, sino que simplemente ahorre el tiempo desde el que está contando o hacia (dependiendo de si está contando hacia arriba o hacia abajo). Luego, cuando la aplicación se inicia nuevamente, solo la usa de vez en cuando al reconstruir el temporizador.
Del mismo modo, asegúrese de que su controlador de temporizador no dependa del momento exacto en que se llama al selector de manejo (por ejemplo,
no
haga nada como
seconds++
o algo así porque puede que no se llame exactamente cuando lo espera), pero siempre regrese a eso de / a tiempo.
Aquí hay un ejemplo de un temporizador de cuenta regresiva, que ilustra que no "contamos" nada.
Tampoco nos importa el tiempo transcurrido entre
appDidEnterBackground
y
appDidBecomeActive
.
Simplemente guarde el tiempo de parada y luego el controlador del temporizador solo compara el tiempo de parada objetivo y la hora actual, y muestra el tiempo transcurrido como desee.
En Swift 3:
import UIKit
import UserNotifications
private let stopTimeKey = "stopTimeKey"
class ViewController: UIViewController {
@IBOutlet weak var datePicker: UIDatePicker!
@IBOutlet weak var timerLabel: UILabel!
private var stopTime: Date?
override func viewDidLoad() {
super.viewDidLoad()
registerForLocalNotifications()
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
if let time = stopTime {
if time > Date() {
startTimer(time, includeNotification: false)
} else {
notifyTimerCompleted()
}
}
}
private func registerForLocalNotifications() {
if #available(iOS 10, *) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
guard granted && error == nil else {
// display error
print("/(error)")
return
}
}
} else {
let types: UIUserNotificationType = [.badge, .sound, .alert]
let settings = UIUserNotificationSettings(types: types, categories: nil)
UIApplication.shared.registerUserNotificationSettings(settings)
}
}
@IBAction func didTapStartButton(_ sender: AnyObject) {
let time = datePicker.date
if time > Date() {
startTimer(time)
} else {
timerLabel.text = "timer date must be in future"
}
}
// MARK: Timer stuff
private var timer: Timer?
private func startTimer(_ stopTime: Date, includeNotification: Bool = true) {
// save `stopTime` in case app is terminated
UserDefaults.standard.set(stopTime, forKey: stopTimeKey)
self.stopTime = stopTime
// start Timer
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
guard includeNotification else { return }
// start local notification (so we''re notified if timer expires while app is not running)
if #available(iOS 10, *) {
let content = UNMutableNotificationContent()
content.title = "Timer expired"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: stopTime.timeIntervalSinceNow, repeats: false)
let notification = UNNotificationRequest(identifier: "timer", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
} else {
let notification = UILocalNotification()
notification.fireDate = stopTime
notification.alertBody = "Timer finished!"
UIApplication.shared.scheduleLocalNotification(notification)
}
}
private func stopTimer() {
timer?.invalidate()
timer = nil
}
private let dateComponentsFormatter: DateComponentsFormatter = {
let _formatter = DateComponentsFormatter()
_formatter.allowedUnits = [.hour, .minute, .second]
_formatter.unitsStyle = .positional
_formatter.zeroFormattingBehavior = .pad
return _formatter
}()
// I''m going to use `DateComponentsFormatter` to update the
// label. Update it any way you want, but the key is that
// we''re just using the scheduled stop time and the current
// time, but we''re not counting anything. If you don''t want to
// use `NSDateComponentsFormatter`, I''d suggest considering
// `NSCalendar` method `components:fromDate:toDate:options:` to
// get the number of hours, minutes, seconds, etc. between two
// dates.
func handleTimer(_ timer: Timer) {
let now = Date()
if stopTime! > now {
timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!)
} else {
stopTimer()
notifyTimerCompleted()
}
}
private func notifyTimerCompleted() {
timerLabel.text = "Timer done!"
}
}
O en Swift 2:
private let stopTimeKey = "stopTimeKey"
class ViewController: UIViewController {
@IBOutlet weak var datePicker: UIDatePicker!
@IBOutlet weak var timerLabel: UILabel!
var stopTime: NSDate?
override func viewDidLoad() {
super.viewDidLoad()
registerForLocalNotifications()
stopTime = NSUserDefaults.standardUserDefaults().objectForKey(stopTimeKey) as? NSDate
if let time = stopTime {
if time.compare(NSDate()) == .OrderedDescending {
startTimer(time)
} else {
notifyTimerCompleted()
}
}
}
func registerForLocalNotifications() {
let types: UIUserNotificationType = [.Badge, .Sound, .Alert]
let settings = UIUserNotificationSettings(forTypes: types, categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(settings)
}
@IBAction func didTapStartButton(sender: AnyObject) {
let time = datePicker.date
if time.compare(NSDate()) == .OrderedDescending {
startTimer(time)
} else {
timerLabel.text = "timer date must be in future"
}
}
// MARK: Timer stuff
var timer: NSTimer?
func startTimer(stopTime: NSDate) {
// save `stopTime` in case app is terminated
NSUserDefaults.standardUserDefaults().setObject(stopTime, forKey: stopTimeKey)
self.stopTime = stopTime
// start NSTimer
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "handleTimer:", userInfo: nil, repeats: true)
// start local notification (so we''re notified if timer expires while app is not running)
let notification = UILocalNotification()
notification.fireDate = stopTime
notification.alertBody = "Timer finished!"
UIApplication.sharedApplication().scheduleLocalNotification(notification)
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
let dateComponentsFormatter: NSDateComponentsFormatter = {
let _formatter = NSDateComponentsFormatter()
_formatter.allowedUnits = [.Hour, .Minute, .Second]
_formatter.unitsStyle = .Positional
_formatter.zeroFormattingBehavior = .Pad
return _formatter
}()
// I''m going to use `NSDateComponentsFormatter` to update the
// label. Update it any way you want, but the key is that
// we''re just using the scheduled stop time and the current
// time, but we''re not counting anything. If you don''t want to
// use `NSDateComponentsFormatter`, I''d suggest considering
// `NSCalendar` method `components:fromDate:toDate:options:` to
// get the number of hours, minutes, seconds, etc. between two
// dates.
func handleTimer(timer: NSTimer) {
let now = NSDate()
if stopTime!.compare(now) == .OrderedDescending {
timerLabel.text = dateComponentsFormatter.stringFromDate(now, toDate: stopTime!)
} else {
stopTimer()
notifyTimerCompleted()
}
}
func notifyTimerCompleted() {
timerLabel.text = "Timer done!"
}
}
Por cierto, lo anterior también ilustra el uso de una notificación local (en caso de que el temporizador caduque mientras la aplicación no se esté ejecutando actualmente).