UIScrollView sospende NSTimer fino al termine dello scorrimento


84

Mentre una UIScrollView(o una sua classe derivata) sta scorrendo, sembra che tutto NSTimersciò che è in esecuzione venga messo in pausa fino al termine dello scorrimento.

C'è un modo per aggirare questo problema? Discussioni? Un'impostazione prioritaria? Nulla?



Risposte:


202

Una soluzione facile e semplice da implementare è fare:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2
UITrackingRunLoopMode è la modalità - e il suo pubblico - che puoi utilizzare per creare diversi comportamenti del timer.
Tom Andersen

Lo adoro, funziona a meraviglia anche con GLKViewController. Basta impostare controller.paused su YES quando viene visualizzato UIScrollView e avviare il proprio timer come descritto. Invertirlo quando la visualizzazione a scorrimento viene chiusa e questo è tutto! Il mio ciclo di aggiornamento / rendering non è molto costoso, quindi potrebbe aiutare.
Jeroen Bouma

3
Eccezionale! Devo rimuovere il timer quando lo invalido o no? Grazie
Jacopo Penzo

Funziona per lo scorrimento ma interrompe il timer quando l'app passa in background. Qualche soluzione per ottenere entrambi?
Pulkit Sharma

23

Per chiunque utilizzi Swift 3

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


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)

7
È meglio chiamare timer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)come primo comando invece di Timer.scheduleTimer(), perché scheduleTimer()aggiunge timer a runloop e la chiamata successiva è un'altra aggiunta allo stesso runloop ma con modalità diversa. Non fare lo stesso lavoro due volte.
Accid Bright

8

Sì, Paul ha ragione, questo è un problema di ciclo di esecuzione. In particolare, è necessario utilizzare il metodo NSRunLoop:

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

7

Questa è la versione rapida.

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

6

È necessario eseguire un altro thread e un altro ciclo di esecuzione se si desidera che i timer si attivino durante lo scorrimento; poiché i timer vengono elaborati come parte del ciclo di eventi, se sei impegnato nell'elaborazione dello scorrimento della visualizzazione, non puoi mai andare in giro per i timer. Sebbene la penalità di prestazioni / batteria di eseguire timer su altri thread potrebbe non valere la pena gestire questo caso.


11
Non richiede un altro thread, vedere la risposta più recente di Kashif. L'ho provato e funziona senza bisogno di thread aggiuntivi.
programma

3
Sparando cannoni ai passeri. Invece di un thread, pianifica il timer nella modalità ciclo di esecuzione corretta.
testimone di una persona il

2
Penso che la risposta di Kashif di seguito sia la migliore risposta in quanto richiede solo l'aggiunta di 1 riga di codice per risolvere questo problema.
damien murphy.

3

per chiunque usi Swift 4:

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

2

tl; dr il runloop sta eseguendo lo scorrimento in modo da non poter gestire altri eventi, a meno che tu non imposti manualmente il timer in modo che possa accadere anche quando runloop sta gestendo eventi di tocco. Oppure prova una soluzione alternativa e utilizza GCD


Una lettura obbligata per qualsiasi sviluppatore iOS. Molte cose vengono infine eseguite tramite RunLoop.

Derivato dai documenti di Apple .

Cos'è un ciclo di corsa?

Un ciclo di esecuzione è molto simile al suo nome. È un ciclo in cui il thread entra e utilizza per eseguire gestori di eventi in risposta agli eventi in arrivo

Come viene interrotta la consegna degli eventi?

Poiché i timer e altri eventi periodici vengono consegnati quando si esegue il ciclo di esecuzione, aggirare quel ciclo interrompe la consegna di quegli eventi. L'esempio tipico di questo comportamento si verifica ogni volta che si implementa una routine di rilevamento del mouse immettendo un ciclo e richiedendo ripetutamente eventi dall'applicazione. Poiché il codice acquisisce gli eventi direttamente, anziché lasciare che l'applicazione invii normalmente tali eventi, i timer attivi non sarebbero in grado di attivarsi fino a quando la routine di rilevamento del mouse non fosse terminata e avesse restituito il controllo all'applicazione.

Cosa succede se il timer viene attivato quando il ciclo di esecuzione è nel mezzo dell'esecuzione?

Questo accade MOLTE VOLTE, senza che noi ce ne accorgiamo. Voglio dire, impostiamo il timer in modo che si attivi alle 10:10: 10: 00, ma il runloop sta eseguendo un evento che dura fino alle 10:10: 10: 05, quindi il timer viene attivato alle 10:10: 10: 06

Allo stesso modo, se un timer si attiva quando il ciclo di esecuzione è nel mezzo dell'esecuzione di una routine del gestore, il timer attende la volta successiva del ciclo di esecuzione per richiamare la sua routine del gestore. Se il ciclo di esecuzione non è affatto in esecuzione, il timer non si attiva mai.

Lo scorrimento o qualsiasi altra cosa che tiene occupato il ciclo di esecuzione si sposterebbe tutte le volte che il mio timer scatta?

È possibile configurare i timer per generare eventi solo una o più volte. Un timer che si ripete si ripianifica automaticamente in base al tempo di accensione programmato, non al tempo di accensione effettivo. Ad esempio, se un timer è programmato per attivarsi in un determinato momento e successivamente ogni 5 secondi, l'ora di attivazione programmata cadrà sempre sugli intervalli di tempo originali di 5 secondi, anche se l'ora di attivazione effettiva viene ritardata. Se il tempo di accensione viene ritardato così tanto da perdere uno o più dei tempi di accensione programmati, il timer viene attivato solo una volta per il periodo di tempo mancato. Dopo aver sparato per il periodo mancato, il timer viene riprogrammato per il successivo orario di accensione programmato.

Come posso cambiare la modalità di RunLoops?

Non puoi. Il sistema operativo cambia solo per te. ad esempio, quando l'utente tocca, la modalità passa a eventTracking. Al termine dei tocchi dell'utente, la modalità torna a default. Se vuoi che qualcosa venga eseguito in una modalità specifica, sta a te assicurarti che accada.


Soluzione:

Quando l'utente sta scorrendo, la modalità Run Loop diventa tracking. Il RunLoop è progettato per cambiare marcia. Una volta impostata la modalità, viene data eventTrackingpriorità (ricorda che abbiamo core CPU limitati) agli eventi di tocco. Questo è un progetto architettonico dei progettisti del sistema operativo .

Per impostazione predefinita, i timer NON sono programmati nella trackingmodalità. Sono in programma:

Crea un timer e lo pianifica sul ciclo di esecuzione corrente nella modalità predefinita .

Il scheduledTimersotto fa questo:

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

Se vuoi che il timer funzioni durante lo scorrimento, devi eseguire una delle seguenti operazioni:

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

Oppure fai semplicemente:

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

In definitiva, eseguire una delle operazioni precedenti significa che il thread non è bloccato dagli eventi di tocco. che è 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.

Soluzione alternativa:

Potresti considerare di utilizzare GCD per il tuo timer che ti aiuterà a "proteggere" il tuo codice dai problemi di gestione del ciclo di esecuzione.

Per non ripetere basta usare:

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

Per la ripetizione dei timer utilizzare:

Scopri come utilizzare DispatchSourceTimer


Scavando più a fondo da una discussione che ho avuto con Daniel Jalkut:

Domanda: come viene eseguito GCD (thread in background), ad esempio un asyncAfter su un thread in background, al di fuori di RunLoop? La mia comprensione da questo è che tutto deve essere eseguito all'interno di un RunLoop

Non necessariamente - ogni thread ha al massimo un ciclo di esecuzione, ma può avere zero se non c'è motivo di coordinare la "proprietà" di esecuzione del thread.

I thread sono un'affidabilità a livello di sistema operativo che offre al processo la possibilità di suddividere la sua funzionalità in più contesti di esecuzione parallela. I cicli di esecuzione sono un vantaggio a livello di framework che consente di suddividere ulteriormente un singolo thread in modo che possa essere condiviso in modo efficiente da più percorsi di codice.

In genere, se invii qualcosa che viene eseguito su un thread, probabilmente non avrà un ciclo di esecuzione a meno che qualcosa non chiami [NSRunLoop currentRunLoop]che implicitamente ne creerebbe uno.

In poche parole, le modalità sono fondamentalmente un meccanismo di filtro per ingressi e timer

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.