UIScrollView pausa NSTimer hasta que finaliza el desplazamiento

Respuestas:

201

Una solución fácil y sencilla de implementar es hacer:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Kashif Hisam
fuente
2
UITrackingRunLoopMode es el modo, y su público, que puede usar para crear diferentes comportamientos de temporizador.
Tom Andersen
Me encanta, funciona como un encanto con GLKViewController también. Simplemente configure controller.paused en YES cuando aparezca UIScrollView e inicie su propio temporizador como se describe. Inviértalo cuando se descarte la vista de desplazamiento y eso es todo. Mi ciclo de actualización / renderizado no es muy caro, así que eso podría ayudar.
Jeroen Bouma
3
¡Increíble! ¿Tengo que quitar el temporizador cuando lo invalido o no? Gracias
Jacopo Penzo
Esto funciona para el desplazamiento, pero detiene el temporizador cuando la aplicación pasa a segundo plano. ¿Alguna solución para lograr ambos?
Pulkit Sharma
23

Para cualquiera que use Swift 3

timer = Timer.scheduledTimer(timeInterval: 0.1,
                            target: self,
                            selector: aSelector,
                            userInfo: nil,
                            repeats: true)


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
Isaac
fuente
7
Es mejor llamar timer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)como el primer comando en lugar de Timer.scheduleTimer(), porque scheduleTimer()agrega un temporizador a runloop, y la siguiente llamada es otra adición al mismo runloop pero con un modo diferente. No hagas el mismo trabajo dos veces.
Accid Bright
8

Sí, Paul tiene razón, este es un problema de ciclo de ejecución. Específicamente, debe hacer uso del método NSRunLoop:

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
agosto
fuente
7

Esta es la versión rápida.

timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: aSelector, userInfo: nil, repeats: true)
            NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
Andrew Cook
fuente
6

Debe ejecutar otro hilo y otro ciclo de ejecución si desea que los temporizadores se activen mientras se desplaza; Dado que los temporizadores se procesan como parte del ciclo de eventos, si está ocupado procesando el desplazamiento de su vista, nunca llega a los temporizadores. Aunque la penalización de rendimiento / batería de ejecutar temporizadores en otros subprocesos podría no valer la pena manejar este caso.

Ana Betts
fuente
11
No requiere otro hilo, vea la respuesta más reciente de Kashif. Lo probé y funciona sin necesidad de subprocesos adicionales.
Progrmr
3
Disparar con cañones a los gorriones. En lugar de un hilo, simplemente programe el temporizador en el modo de ciclo de ejecución correcto.
uliwitness
2
Creo que la respuesta de Kashif a continuación es la mejor respuesta, ya que solo requiere que agregue 1 línea de código para solucionar este problema.
Damien Murphy.
3

para cualquiera que use Swift 4:

    timer = Timer(timeInterval: 1, target: self, selector: #selector(timerUpdated), userInfo: nil, repeats: true)
    RunLoop.main.add(timer, forMode: .common)
YuLong Xiao
fuente
1

tl; dr, el runloop está desplazándose, por lo que no puede manejar más eventos, a menos que configure manualmente el temporizador para que también pueda suceder cuando runloop está manejando eventos táctiles. O prueba una solución alternativa y usa GCD


Una lectura obligada para cualquier desarrollador de iOS. En última instancia, muchas cosas se ejecutan a través de RunLoop.

Derivado de los documentos de Apple .

¿Qué es un Run Loop?

Un bucle de ejecución es muy parecido al sonido de su nombre. Es un bucle que ingresa su hilo y usa para ejecutar controladores de eventos en respuesta a eventos entrantes

¿Cómo se interrumpe la entrega de eventos?

Debido a que los temporizadores y otros eventos periódicos se entregan cuando ejecuta el ciclo de ejecución, eludir ese ciclo interrumpe la entrega de esos eventos. El ejemplo típico de este comportamiento ocurre siempre que implementa una rutina de seguimiento del mouse ingresando un bucle y solicitando repetidamente eventos de la aplicación. Debido a que su código captura eventos directamente, en lugar de permitir que la aplicación distribuya esos eventos normalmente, los temporizadores activos no podrían dispararse hasta después de que su rutina de seguimiento del mouse salga y devuelva el control a la aplicación.

¿Qué sucede si el temporizador se activa cuando el bucle de ejecución está en medio de la ejecución?

Esto sucede MUCHAS VECES, sin que nos demos cuenta. Quiero decir, configuramos el temporizador para que se active a las 10: 10: 10: 00, pero el runloop está ejecutando un evento que tarda hasta las 10: 10: 10: 05, por lo que el temporizador se dispara a las 10: 10: 10: 06

De manera similar, si un temporizador se dispara cuando el ciclo de ejecución está en medio de la ejecución de una rutina de controlador, el temporizador espera hasta la próxima vez a través del ciclo de ejecución para invocar su rutina de controlador. Si el ciclo de ejecución no se está ejecutando, el temporizador nunca se dispara.

¿El desplazamiento o cualquier cosa que mantenga ocupado el runloop cambiará todas las veces que mi temporizador se disparará?

Puede configurar temporizadores para generar eventos solo una vez o repetidamente. Un temporizador de repetición se reprograma automáticamente según el tiempo de disparo programado, no el tiempo de disparo real. Por ejemplo, si un temporizador está programado para disparar a una hora en particular y cada 5 segundos después de eso, el tiempo de disparo programado siempre caerá en los intervalos de tiempo originales de 5 segundos, incluso si el tiempo de disparo real se retrasa. Si el tiempo de disparo se retrasa tanto que pierde uno o más de los tiempos de disparo programados, el temporizador se dispara solo una vez durante el período de tiempo perdido. Después de disparar durante el período perdido, el temporizador se reprograma para el siguiente tiempo de disparo programado.

¿Cómo puedo cambiar el modo de RunLoops?

No puedes. El sistema operativo simplemente cambia por usted. por ejemplo, cuando el usuario toca, el modo cambia a eventTracking. Cuando los toques del usuario terminan, el modo vuelve a default. Si desea que algo se ejecute en un modo específico, depende de usted asegurarse de que eso suceda.


Solución:

Cuando el usuario se desplaza, el modo Run Loop se convierte en tracking. El RunLoop está diseñado para cambiar de marcha. Una vez que el modo está configurado eventTracking, da prioridad (recuerde que tenemos núcleos de CPU limitados) para tocar eventos. Este es un diseño arquitectónico de los diseñadores de SO .

Por defecto, los temporizadores NO están programados en el trackingmodo. Están programados para:

Crea un temporizador y lo programa en el ciclo de ejecución actual en el modo predeterminado .

El de scheduledTimerabajo hace esto:

RunLoop.main.add(timer, forMode: .default)

Si desea que su temporizador funcione cuando se desplaza, debe hacer lo siguiente:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self,
 selector: #selector(fireTimer), userInfo: nil, repeats: true) // sets it on `.default` mode

RunLoop.main.add(timer, forMode: .tracking) // AND Do this

O simplemente haz:

RunLoop.main.add(timer, forMode: .common)

En última instancia, hacer uno de los anteriores significa que su hilo no está bloqueado por eventos táctiles. que es equivalente a:

RunLoop.main.add(timer, forMode: .default)
RunLoop.main.add(timer, forMode: .eventTracking)
RunLoop.main.add(timer, forMode: .modal) // This is more of a macOS thing for when you have a modal panel showing.

Solución alternativa:

Puede considerar usar GCD para su temporizador, lo que lo ayudará a "proteger" su código de problemas de administración de bucle de ejecución.

Para no repetir, solo use:

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    // your code here
}

Para temporizadores repetidos use:

Vea cómo usar DispatchSourceTimer


Profundizando en una discusión que tuve con Daniel Jalkut:

Pregunta: ¿cómo se ejecuta GCD (subprocesos en segundo plano), por ejemplo, un asyncAfter en un subproceso en segundo plano, fuera de RunLoop? Tengo entendido de esto que todo se debe ejecutar dentro de un RunLoop

No necesariamente, cada hilo tiene como máximo un ciclo de ejecución, pero puede tener cero si no hay razón para coordinar la "propiedad" de ejecución del hilo.

Los subprocesos son una prestación de nivel de sistema operativo que le da a su proceso la capacidad de dividir su funcionalidad en múltiples contextos de ejecución paralela. Los bucles de ejecución son una prestación de nivel de marco que le permite dividir aún más un solo hilo para que pueda ser compartido de manera eficiente por múltiples rutas de código.

Por lo general, si envía algo que se ejecuta en un hilo, probablemente no tendrá un runloop a menos que algo llame [NSRunLoop currentRunLoop] lo que crearía uno implícitamente.

En pocas palabras, los modos son básicamente un mecanismo de filtro para entradas y temporizadores.

Miel
fuente