UIScrollView met NSTimer en pause jusqu'à la fin du défilement

84

Pendant qu'un UIScrollView(ou une classe dérivée de celui-ci) défile, il semble que tous ceux NSTimersqui sont en cours d'exécution soient suspendus jusqu'à ce que le défilement soit terminé.

Y a-t-il un moyen de contourner ceci? Des discussions? Un établissement de priorité? N'importe quoi?

mcccclean
la source
1
Copie
Fattie
sept ans plus tard ... stackoverflow.com/a/12625429/294884
Fattie

Réponses:

201

Une solution facile et simple à mettre en œuvre consiste à:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Kashif Hisam
la source
2
UITrackingRunLoopMode est le mode - et son public - que vous pouvez utiliser pour créer différents comportements de minuterie.
Tom Andersen
J'adore ça, ça marche comme un charme avec GLKViewController aussi. Réglez simplement controller.paused sur YES lorsque UIScrollView apparaît et démarrez votre propre minuterie comme décrit. Inversez-le lorsque la vue de défilement est rejetée et c'est tout! Ma boucle de mise à jour / rendu n'est pas très chère, donc cela pourrait aider.
Jeroen Bouma
3
Impressionnant! Dois-je retirer la minuterie lorsque je l'invalide ou non? Merci
Jacopo Penzo
Cela fonctionne pour le défilement mais arrête la minuterie lorsque l'application passe en arrière-plan. Une solution pour atteindre les deux?
Pulkit Sharma
23

Pour tous ceux qui utilisent Swift 3

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


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
Isaac
la source
7
Il est préférable d'appeler timer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)comme première commande au lieu de Timer.scheduleTimer(), car scheduleTimer()ajoute un minuteur à la boucle d'exécution, et l'appel suivant est un autre ajout à la même boucle d'exécution mais avec un mode différent. Ne faites pas le même travail deux fois.
Accid Bright
8

Oui, Paul a raison, c'est un problème de boucle d'exécution. Plus précisément, vous devez utiliser la méthode NSRunLoop:

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
août
la source
7

Ceci est la version rapide.

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

Vous devez exécuter un autre thread et une autre boucle d'exécution si vous voulez que les minuteries se déclenchent pendant le défilement; puisque les minuteries sont traitées dans le cadre de la boucle d'événements, si vous êtes occupé à traiter le défilement de votre vue, vous ne vous déplacez jamais vers les minuteries. Bien que la pénalité de performance / batterie liée à l'exécution de minuteries sur d'autres threads ne vaut pas la peine d'être traitée dans ce cas.

Ana Betts
la source
11
Cela ne nécessite pas un autre fil, voir la réponse la plus récente de Kashif. Je l'ai essayé et cela fonctionne sans fil supplémentaire requis.
programme du
3
Tir de canons sur les moineaux. Au lieu d'un thread, programmez simplement le minuteur dans le bon mode de boucle d'exécution.
uliwitness
2
Je pense que la réponse de Kashif ci-dessous est de loin la meilleure réponse car elle ne vous oblige qu'à ajouter 1 ligne de code pour résoudre ce problème.
Damien Murphy.
3

pour quiconque utilise Swift 4:

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

tl; dr le runloop effectue un défilement afin qu'il ne puisse plus gérer d'événements - à moins que vous ne régliez manuellement le minuteur pour qu'il puisse également se produire lorsque runloop gère des événements tactiles. Ou essayez une autre solution et utilisez GCD


Une lecture incontournable pour tout développeur iOS. Beaucoup de choses sont finalement exécutées via RunLoop.

Dérivé de la documentation d' Apple .

Qu'est-ce qu'une boucle de course?

Une boucle d'exécution ressemble beaucoup à son nom. C'est une boucle que votre thread entre et utilise pour exécuter des gestionnaires d'événements en réponse aux événements entrants

Comment la livraison des événements est-elle perturbée?

Étant donné que les minuteries et autres événements périodiques sont fournis lorsque vous exécutez la boucle d'exécution, le contournement de cette boucle perturbe la livraison de ces événements. L'exemple typique de ce comportement se produit chaque fois que vous implémentez une routine de suivi de la souris en entrant une boucle et en demandant à plusieurs reprises des événements à partir de l'application. Étant donné que votre code saisit directement les événements, plutôt que de laisser l'application distribuer ces événements normalement, les minuteries actives ne pourront pas se déclencher tant que votre routine de suivi de la souris n'est pas sortie et a renvoyé le contrôle à l'application.

Que se passe-t-il si le minuteur est déclenché lorsque la boucle d'exécution est en cours d'exécution?

Cela arrive BEAUCOUP DE FOIS, sans que nous nous en rendions compte. Je veux dire que nous avons réglé la minuterie pour qu'elle se déclenche à 10: 10: 10: 00, mais la boucle d'exécution exécute un événement qui prend jusqu'à 10: 10: 10: 05, donc la minuterie est déclenchée 10: 10: 10: 06

De même, si un minuteur se déclenche lorsque la boucle d'exécution est au milieu de l'exécution d'une routine de gestionnaire, le minuteur attend la prochaine fois dans la boucle d'exécution pour appeler sa routine de gestionnaire. Si la boucle d'exécution ne s'exécute pas du tout, le minuteur ne se déclenche jamais.

Le défilement ou tout autre élément qui maintient la boucle de course occupée changerait-il toutes les fois que ma minuterie va se déclencher?

Vous pouvez configurer des minuteries pour générer des événements une seule fois ou plusieurs fois. Une minuterie à répétition se replanifie automatiquement en fonction de l'heure de tir programmée, et non de l'heure de tir réelle. Par exemple, si une minuterie est programmée pour se déclencher à un moment donné et toutes les 5 secondes par la suite, l'heure de tir programmée tombera toujours sur les intervalles de temps d'origine de 5 secondes, même si l'heure de tir réelle est retardée. Si le temps de tir est tellement retardé qu'il manque un ou plusieurs des temps de tir programmés, le temporisateur est déclenché une seule fois pendant la période de temps manquée. Après le tir pour la période manquée, le chronomètre est reprogrammé pour la prochaine heure de tir programmée.

Comment puis-je changer le mode des RunLoops?

Vous ne pouvez pas. Le système d'exploitation se modifie tout seul pour vous. Par exemple, lorsque l'utilisateur appuie sur, le mode passe à eventTracking. Lorsque l'utilisateur appuie sur terminé, le mode revient à default. Si vous voulez que quelque chose soit exécuté dans un mode spécifique, c'est à vous de vous assurer que cela se produit.


Solution:

Lorsque l'utilisateur fait défiler, le mode Run Loop devient tracking. Le RunLoop est conçu pour changer de vitesse. Une fois que le mode est défini sur eventTracking, il donne la priorité (rappelez-vous que nous avons des cœurs CPU limités) pour toucher les événements. Il s'agit d'une conception architecturale des concepteurs du système d'exploitation .

Par défaut, les minuteries ne sont PAS programmées sur le trackingmode. Ils sont programmés le:

Crée une minuterie et la programme sur la boucle d'exécution actuelle dans le mode par défaut .

Le scheduledTimerdessous fait ceci:

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

Si vous voulez que votre minuterie fonctionne lors du défilement, vous devez faire soit:

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

Ou faites simplement:

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

En fin de compte, faire l'une des choses ci-dessus signifie que votre thread n'est pas bloqué par les événements tactiles. ce qui équivaut à:

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.

Solution alternative:

Vous pouvez envisager d'utiliser GCD pour votre minuterie, ce qui vous aidera à «protéger» votre code des problèmes de gestion de la boucle d'exécution.

Pour ne pas se répéter, utilisez simplement:

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

Pour les minuteries répétitives, utilisez:

Découvrez comment utiliser DispatchSourceTimer


Creuser plus profondément à partir d'une discussion que j'ai eue avec Daniel Jalkut:

Question: comment GCD (threads d'arrière-plan), par exemple un asyncAfter sur un thread d'arrière-plan, est-il exécuté en dehors du RunLoop? Je crois comprendre que tout doit être exécuté dans un RunLoop

Pas nécessairement - chaque thread a au plus une boucle d'exécution, mais peut avoir zéro s'il n'y a aucune raison de coordonner l'exécution "propriété" du thread.

Les threads sont une offre au niveau du système d'exploitation qui donne à votre processus la possibilité de répartir ses fonctionnalités sur plusieurs contextes d'exécution parallèles. Les boucles d'exécution sont une capacité au niveau de l'infrastructure qui vous permet de diviser davantage un seul thread afin qu'il puisse être partagé efficacement par plusieurs chemins de code.

En règle générale, si vous distribuez quelque chose qui s'exécute sur un thread, il n'aura probablement pas de boucle d'exécution à moins que quelque chose n'appelle [NSRunLoop currentRunLoop]qui en créerait implicitement une.

En un mot, les modes sont essentiellement un mécanisme de filtre pour les entrées et les minuteries

Mon chéri
la source