IOS Timer

Imagine que está trabajando en una aplicación en la que necesita realizar periódicamente algunas acciones. Esto es exactamente para lo que Swift usa la clase Timer .

El temporizador se usa para planificar acciones en la aplicación. Esto puede ser una acción única o un procedimiento repetitivo.

En esta guía, aprenderá cómo funciona el temporizador en iOS, cómo puede afectar la capacidad de respuesta de la interfaz de usuario, cómo optimizar el consumo de batería al usar un temporizador y cómo usar CADisplayLink para la animación.

Como sitio de prueba, utilizaremos la aplicación, un planificador de tareas primitivo.

Empezando


Descargue el proyecto fuente. Ábrelo en Xcode, mira su estructura, compila y ejecuta. Verá el programador de tareas más simple:



Agregue una nueva tarea a la misma. Toque el ícono +, ingrese el nombre de la tarea, toque Aceptar.

Las tareas agregadas tienen una marca de tiempo. La nueva tarea que acaba de crear está marcada con cero segundos. Como puede ver, este valor no aumenta.

Cada tarea se puede marcar como completada. Toca la tarea. El nombre de la tarea se tachará y se marcará como completado.

Crea nuestro primer temporizador


Creemos el temporizador principal de nuestra aplicación. La clase Timer , también conocida como NSTimer, es una forma conveniente de programar una acción para un momento específico, tanto individual como periódico.

Abra TaskListViewController.swift y agregue esta variable a TaskListViewController :

var timer: Timer? 

Luego agregue la extensión allí:

 // MARK: - Timer extension TaskListViewController { } 

Y pegue este código en la extensión:

 @objc func updateTimer() { // 1 guard let visibleRowsIndexPaths = tableView.indexPathsForVisibleRows else { return } for indexPath in visibleRowsIndexPaths { // 2 if let cell = tableView.cellForRow(at: indexPath) as? TaskTableViewCell { cell.updateTime() } } } 

En este método, nosotros:

  1. Compruebe si hay filas visibles en la tabla de tareas.
  2. Llame a updateTime para cada celda visible. Este método actualiza la marca de tiempo en la celda (consulte TaskTableViewCell.swift ).

Luego agregue este código a la extensión:

 func createTimer() { // 1 if timer == nil { // 2 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) } } 

Aquí estamos

  1. Compruebe si el temporizador contiene una instancia de la clase Timer .
  2. De lo contrario, cree un temporizador que llame a updateTimer () cada segundo.

Luego, necesitamos crear un temporizador tan pronto como el usuario agregue la primera tarea. Agregue createTimer () al principio del método presentAlertController (_ :) .

Inicie la aplicación y cree un par de tareas nuevas. Verá que la marca de tiempo para cada tarea cambia cada segundo.



Agregar tolerancia de temporizador


Aumentar la cantidad de temporizadores da como resultado una peor respuesta de la interfaz de usuario y más consumo de batería. Cada temporizador intenta ejecutarse exactamente en el momento asignado, ya que por defecto su tolerancia es cero.

Agregar una tolerancia de temporizador es una manera fácil de reducir el consumo de energía. Esto permite que el sistema realice una acción de temporizador entre el tiempo asignado y el tiempo asignado más el tiempo de tolerancia , pero nunca antes del intervalo asignado.

Para los temporizadores que se ejecutan solo una vez, se ignora el valor de tolerancia.

En el método createTimer () , inmediatamente después de la asignación del temporizador, agregue esta línea:

 timer?.tolerance = 0.1 

Inicia la aplicación. En este caso particular, el efecto no será obvio (solo tenemos un temporizador), sin embargo, en la situación real de varios temporizadores, sus usuarios obtendrán una interfaz más receptiva y la aplicación será más eficiente en términos de energía.



Temporizadores en el fondo


Curiosamente, ¿qué sucede con los temporizadores cuando una aplicación pasa a segundo plano? Para lidiar con esto, agregue este código al comienzo del método updateTimer () :

 if let fireDateDescription = timer?.fireDate.description { print(fireDateDescription) } 

Esto nos permitirá rastrear los eventos del temporizador en la consola.

Ejecute la aplicación, agregue la tarea. Ahora presione el botón Inicio en su dispositivo y luego regrese a nuestra aplicación.

En la consola, verá algo como esto:



Como puede ver, cuando la aplicación pasa a segundo plano, iOS detiene todos los temporizadores de la aplicación en ejecución. Cuando la aplicación se activa, iOS reanuda los temporizadores.

Entendiendo Run Loops


Un ciclo de ejecución es un ciclo de eventos que programa el trabajo y maneja los eventos entrantes. El ciclo mantiene ocupado el subproceso mientras se está ejecutando y lo pone en un estado de "suspensión" cuando no hay trabajo para él.

Cada vez que inicia la aplicación, el sistema crea el hilo principal de la aplicación, cada hilo tiene un bucle de ejecución creado automáticamente.

¿Pero por qué toda esta información es importante para usted ahora? Ahora cada temporizador comienza en el hilo principal y se une al bucle de ejecución. Probablemente sepa que el hilo principal se dedica a representar la interfaz de usuario, procesar toques, etc. Si el hilo principal está ocupado con algo, la interfaz de su aplicación puede dejar de responder (bloquearse).

¿Notó que la marca de tiempo en la celda no se actualiza cuando arrastra la vista de tabla?



Puede resolver este problema diciéndole al ciclo de ejecución que inicie los temporizadores en un modo diferente.

Comprender los modos de ciclo de ejecución


El modo de ciclo de ejecución es un conjunto de fuentes de entrada, como tocar la pantalla o hacer clic con el mouse, que se puede configurar para monitorear y un conjunto de "observadores" que reciben notificaciones.

Hay tres modos de tiempo de ejecución en iOS:

predeterminado : se procesan las fuentes de entrada que no son NSConnectionObjects.
común : se está procesando un conjunto de ciclos de entrada, para lo cual puede definir un conjunto de fuentes de entrada, temporizadores, "observadores".
seguimiento : la interfaz de usuario de la aplicación se está procesando.

Para nuestra aplicación, el modo más adecuado es común . Para usarlo, reemplace el contenido del método createTimer () con lo siguiente:

 if timer == nil { let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) RunLoop.current.add(timer, forMode: .common) timer.tolerance = 0.1 self.timer = timer } 

La principal diferencia con el código anterior es que antes de asignar el temporizador al TaskListViewController, agregamos este temporizador al ciclo de ejecución en modo común .

Compila y ejecuta la aplicación.



Ahora las marcas de tiempo de las celdas se actualizan incluso si se desplaza la tabla.

Agrega animación para completar todas las tareas


Ahora agregamos una animación de felicitación para que el usuario complete todas las tareas: la pelota se elevará desde la parte inferior de la pantalla hasta la parte superior.

Agregue estas variables al comienzo de TaskListViewController:

 // 1 var animationTimer: Timer? // 2 var startTime: TimeInterval?, endTime: TimeInterval? // 3 let animationDuration = 3.0 // 4 var height: CGFloat = 0 

El propósito de estas variables es:

  1. Temporizador de animación de almacenamiento.
  2. almacenar la hora de inicio y finalización de la animación
  3. duración de la animación
  4. altura de animación

Ahora agregue la siguiente extensión TaskListViewController al final del archivo TaskListViewController.swift :

 // MARK: - Animation extension TaskListViewController { func showCongratulationAnimation() { // 1 height = UIScreen.main.bounds.height + balloon.frame.size.height // 2 balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: height + balloon.frame.size.height / 2) balloon.isHidden = false // 3 startTime = Date().timeIntervalSince1970 endTime = animationDuration + startTime! // 4 animationTimer = Timer.scheduledTimer(withTimeInterval: 1 / 60, repeats: true) { timer in // TODO: Animation here } } } 

Aquí hacemos lo siguiente:

  • calcular la altura de la animación, obteniendo la altura de la pantalla del dispositivo
  • centrar la pelota fuera de la pantalla y establecer su visibilidad
  • asignar la hora de inicio y finalización de la animación
  • Iniciamos el temporizador de animación y actualizamos la animación 60 veces por segundo

Ahora necesitamos crear la lógica real para actualizar la animación de felicitación. Agregue este código después de showCongratulationAnimation () :

 func updateAnimation() { // 1 guard let endTime = endTime, let startTime = startTime else { return } // 2 let now = Date().timeIntervalSince1970 // 3 if now >= endTime { animationTimer?.invalidate() balloon.isHidden = true } // 4 let percentage = (now - startTime) * 100 / animationDuration let y = height - ((height + balloon.frame.height / 2) / 100 * CGFloat(percentage)) // 5 balloon.center = CGPoint(x: balloon.center.x + CGFloat.random(in: -0.5...0.5), y: y) } 

Lo que hacemos:

  1. compruebe que endTime y startTime están asignados
  2. guardar la hora actual en constante
  3. nos aseguramos de que la hora final aún no haya llegado. Si ya ha llegado, actualiza el temporizador y esconde nuestra pelota
  4. calcular la nueva coordenada y de la pelota
  5. la posición horizontal de la pelota se calcula en relación con la posición anterior

Ahora reemplace // TODO: Animación aquí en showCongratulationAnimation () con este código:

 self.updateAnimation() 

Ahora se llama a updateAnimation () cada vez que ocurre un evento de temporizador.

Hurra, acabamos de crear una animación. Sin embargo, cuando se inicia la aplicación, no sucede nada nuevo ...

Mostrando animación


Como probablemente haya adivinado, no hay nada para "lanzar" nuestra nueva animación. Para hacer esto, necesitamos otro método. Agregue este código a la extensión de animación TaskListViewController:

 func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { showCongratulationAnimation() } } 

Llamaremos a este método cada vez que el usuario marque la tarea completada, verificará si todas las tareas se han completado. Si es así, llamará a showCongratulationAnimation () .

En conclusión, agregue una llamada a este método al final de tableView (_: didSelectRowAt :) :

 showCongratulationsIfNeeded() 

Inicie la aplicación, cree un par de tareas, márquelas como completadas y verá nuestra animación.



Paramos el cronómetro


Si observa la consola, verá que, aunque el usuario marcó todas las tareas completadas, el temporizador continúa funcionando. Esto no tiene sentido, por lo que tiene sentido detener el temporizador cuando no es necesario.

Primero, cree un nuevo método para detener el temporizador:

 func cancelTimer() { timer?.invalidate() timer = nil } 

Esto actualizará el temporizador y lo restablecerá a cero para que podamos crearlo correctamente más tarde. invalidate () es la única forma de eliminar Timer del ciclo de ejecución. El ciclo de ejecución eliminará la referencia del temporizador fuerte inmediatamente después de llamar a invalidate () o un poco más tarde.

Ahora reemplace el método showCongratulationsIfNeeded () de la siguiente manera:

 func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { cancelTimer() showCongratulationAnimation() } else { createTimer() } } 

Ahora, si el usuario completa todas las tareas, la aplicación primero restablecerá el temporizador y luego mostrará la animación; de lo contrario, intentará crear un nuevo temporizador si aún no está allí.

Inicia la aplicación.



Ahora el temporizador se detiene y se reinicia como debería.

CADisplayLink para una animación fluida


El temporizador no es una opción ideal para controlar la animación. Es posible que haya notado algunos cuadros omitiendo la animación, especialmente si ejecuta la aplicación en el simulador.

Configuramos el temporizador a 60Hz. Por lo tanto, el temporizador actualiza la animación cada 16 ms. Considere la situación más de cerca:



Cuando usamos Timer, no sabemos la hora exacta en que comenzó la acción. Esto puede suceder al principio o al final del cuadro. Digamos que el temporizador se ejecuta en el medio de cada cuadro (puntos azules en la imagen). Lo único que sabemos con certeza es que la llamada será cada 16 ms.

Ahora solo tenemos 8 ms para ejecutar la animación, y esto puede no ser suficiente para nuestra animación. Miremos el segundo cuadro de la figura. El segundo cuadro no se puede completar en el tiempo asignado, por lo que la aplicación restablecerá el segundo cuadro de la animación.

CADisplayLink nos ayudará


CADisplayLink se llama una vez por cuadro e intenta sincronizar cuadros de animación reales tanto como sea posible. Ahora tendrá los 16 ms a su disposición y iOS no soltará un solo cuadro.

Para usar CADisplayLink , debe reemplazar animationTimer con un nuevo tipo.

Reemplace este código

 var animationTimer: Timer? 

en este:

 var displayLink: CADisplayLink? 

Ha reemplazado el temporizador con CADisplayLink . CADisplayLink es una vista de temporizador vinculada al escaneo vertical de la pantalla. Esto significa que la GPU del dispositivo se detendrá hasta que la pantalla pueda continuar procesando los comandos de la GPU. De esta manera obtenemos una animación suave.

Reemplace este código

 var startTime: TimeInterval?, endTime: TimeInterval? 

en este:

 var startTime: CFTimeInterval?, endTime: CFTimeInterval? 


Reemplazó TimeInterval con CFTimeInterval , que es necesario para trabajar con CADisplayLink.

Reemplace el texto del método showCongratulationAnimation () con esto:

 func showCongratulationAnimation() { // 1 height = UIScreen.main.bounds.height + balloon.frame.size.height balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: height + balloon.frame.size.height / 2) balloon.isHidden = false // 2 startTime = CACurrentMediaTime() endTime = animationDuration + startTime! // 3 displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation)) displayLink?.add(to: RunLoop.main, forMode: .common) } 

¿Qué estamos haciendo aquí?

  1. Establezca la altura de la animación, las coordenadas de la pelota y la visibilidad, casi lo mismo que antes.
  2. Inicialice startTime con CACurrentMediaTime () (en lugar de Date ()).
  3. Creamos una instancia de la clase CADisplayLink y la agregamos al ciclo de ejecución en modo común .

Ahora reemplace updateAnimation () con el siguiente código:

 // 1 @objc func updateAnimation() { guard let endTime = endTime, let startTime = startTime else { return } // 2 let now = CACurrentMediaTime() if now >= endTime { // 3 displayLink?.isPaused = true displayLink?.invalidate() balloon.isHidden = true } let percentage = (now - startTime) * 100 / animationDuration let y = height - ((height + balloon.frame.height / 2) / 100 * CGFloat(percentage)) balloon.center = CGPoint(x: balloon.center.x + CGFloat.random(in: -0.5...0.5), y: y) } 

  1. Agregue objc a la firma del método (para CADisplayLink, el parámetro selector requiere dicha firma).
  2. Reemplace la inicialización con Fecha () para inicializar la fecha de CoreAnimation .
  3. Reemplace la llamada animationTimer.invalidate () con una pausa de CADisplayLink e invalide. Esto también eliminará CADisplayLink del ciclo de ejecución.

¡Inicia la aplicación!


Genial Reemplazamos con éxito la animación basada en temporizador con un CADisplayLink más apropiado, y conseguimos que la animación sea más fluida, sin sacudidas.

Conclusión


En esta guía, descubriste cómo funciona la clase Timer en iOS, cuál es el ciclo de ejecución y cómo puede hacer que tu aplicación sea más receptiva en términos de interfaz y cómo usar CADisplayLink en lugar de Timer para una animación fluida.

Source: https://habr.com/ru/post/450172/


All Articles