UIScrollView hält NSTimer an, bis der Bildlauf abgeschlossen ist

Antworten:

201

Eine einfach zu implementierende Lösung ist:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Kashif Hisam
quelle
2
UITrackingRunLoopMode ist der Modus - und seine öffentliche -, mit dem Sie verschiedene Timer-Verhaltensweisen erstellen können.
Tom Andersen
Ich liebe es, funktioniert auch mit GLKViewController wie ein Zauber. Setzen Sie controller.paused einfach auf YES, wenn UIScrollView angezeigt wird, und starten Sie Ihren eigenen Timer wie beschrieben. Kehre es um, wenn die Bildlaufansicht geschlossen wird und das wars! Meine Update / Render-Schleife ist nicht sehr teuer, das könnte also helfen.
Jeroen Bouma
3
Genial! Muss ich den Timer entfernen, wenn ich ihn ungültig mache oder nicht? Vielen Dank
Jacopo Penzo
Dies funktioniert beim Scrollen, stoppt jedoch den Timer, wenn die App in den Hintergrund tritt. Irgendeine Lösung, um beides zu erreichen?
Pulkit Sharma
23

Für alle, die Swift 3 verwenden

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


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
Isaac
quelle
6
Es ist besser, timer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)als erster Befehl statt als aufzurufen Timer.scheduleTimer(), da scheduleTimer()der Runloop mit einem Timer versehen wird und der nächste Aufruf ein weiterer Befehl zum gleichen Runloop ist, jedoch mit einem anderen Modus. Mach nicht zweimal die gleiche Arbeit.
Accid Bright
8

Ja, Paul hat recht, dies ist ein Run-Loop-Problem. Insbesondere müssen Sie die NSRunLoop-Methode verwenden:

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

Dies ist die schnelle Version.

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

Sie müssen einen anderen Thread und eine weitere Ausführungsschleife ausführen, wenn Timer beim Scrollen ausgelöst werden sollen. Da Timer als Teil der Ereignisschleife verarbeitet werden, gelangen Sie nie zu den Timern, wenn Sie mit dem Scrollen Ihrer Ansicht beschäftigt sind. Obwohl die Perf / Batterie-Strafe für das Ausführen von Timern auf anderen Threads in diesem Fall möglicherweise nicht sinnvoll ist.

Ana Betts
quelle
11
Es ist kein weiterer Thread erforderlich, siehe die neuere Antwort von Kashif. Ich habe es versucht und es funktioniert ohne zusätzlichen Thread.
Programm
3
Kanonen auf Spatzen schießen. Planen Sie den Timer anstelle eines Threads einfach im richtigen Run-Loop-Modus.
uliwitness
2
Ich denke, Kashifs Antwort unten ist bei weitem die beste Antwort, da Sie nur 1 Codezeile hinzufügen müssen, um dieses Problem zu beheben.
Damien Murphy.
3

Verwenden Sie für alle Swift 4:

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

tl; dr Der Runloop führt einen Bildlauf durch, sodass keine Ereignisse mehr verarbeitet werden können - es sei denn, Sie stellen den Timer manuell so ein, dass dies auch passieren kann, wenn der Runloop Berührungsereignisse verarbeitet. Oder versuchen Sie es mit einer alternativen Lösung und verwenden Sie GCD


Ein Muss für jeden iOS-Entwickler. Viele Dinge werden letztendlich über RunLoop ausgeführt.

Abgeleitet von Apples Dokumenten .

Was ist eine Run-Schleife?

Eine Run-Schleife ist ihrem Namen sehr ähnlich. Es ist eine Schleife, in die Ihr Thread eintritt und die zum Ausführen von Ereignishandlern als Reaktion auf eingehende Ereignisse verwendet wird

Wie wird die Zustellung von Ereignissen gestört?

Da Timer und andere periodische Ereignisse beim Ausführen der Ausführungsschleife übermittelt werden, wird durch die Umgehung dieser Schleife die Übermittlung dieser Ereignisse unterbrochen. Das typische Beispiel für dieses Verhalten tritt immer dann auf, wenn Sie eine Mausverfolgungsroutine implementieren, indem Sie eine Schleife eingeben und wiederholt Ereignisse von der Anwendung anfordern. Da Ihr Code Ereignisse direkt erfasst, anstatt die Anwendung diese Ereignisse normal auslösen zu lassen, können aktive Timer erst ausgelöst werden, nachdem Ihre Mausverfolgungsroutine beendet und die Kontrolle an die Anwendung zurückgegeben wurde.

Was passiert, wenn der Timer ausgelöst wird, während sich die Ausführungsschleife mitten in der Ausführung befindet?

Dies geschieht VIELE MAL, ohne dass wir es jemals bemerken. Ich meine, wir setzen den Timer auf 10: 10: 10: 00, aber der Runloop führt ein Ereignis aus, das bis 10: 10: 10: 05 dauert, daher wird der Timer 10: 10: 10: 06 ausgelöst

Wenn ein Timer ausgelöst wird, während sich die Ausführungsschleife gerade in der Ausführung einer Handlerroutine befindet, wartet der Timer bis zum nächsten Durchlaufen der Ausführungsschleife, um seine Handlerroutine aufzurufen. Wenn die Run-Schleife überhaupt nicht läuft, wird der Timer nie ausgelöst.

Würde sich das Scrollen oder irgendetwas, das den Runloop beschäftigt, ständig verschieben, wenn mein Timer ausgelöst wird?

Sie können Timer so konfigurieren, dass Ereignisse nur einmal oder wiederholt generiert werden. Ein sich wiederholender Timer plant sich automatisch neu basierend auf der geplanten Zündzeit und nicht auf der tatsächlichen Zündzeit. Wenn beispielsweise ein Timer zu einem bestimmten Zeitpunkt und danach alle 5 Sekunden ausgelöst werden soll, fällt die geplante Zündzeit immer auf die ursprünglichen Zeitintervalle von 5 Sekunden, selbst wenn sich die tatsächliche Zündzeit verzögert. Wenn die Zündzeit so stark verzögert wird, dass eine oder mehrere der geplanten Zündzeiten verfehlt werden, wird der Timer für den verpassten Zeitraum nur einmal ausgelöst. Nach dem Auslösen für die versäumte Zeit wird der Timer für die nächste geplante Auslösezeit neu geplant.

Wie kann ich den RunLoops-Modus ändern?

Das kannst du nicht. Das Betriebssystem ändert sich nur für Sie. Wenn der Benutzer z. B. tippt, wechselt der Modus zu eventTracking. Wenn die Benutzertipps beendet sind, kehrt der Modus zurück zu default. Wenn Sie möchten, dass etwas in einem bestimmten Modus ausgeführt wird, müssen Sie sicherstellen, dass dies geschieht.


Lösung:

Wenn der Benutzer einen Bildlauf durchführt, wird der Run-Loop-Modus aktiviert tracking. Der RunLoop ist zum Schalten ausgelegt. Sobald der Modus eingestellt ist eventTracking, gibt es Priorität (denken Sie daran, dass wir nur begrenzte CPU-Kerne haben), um Ereignisse zu berühren. Dies ist ein architektonischer Entwurf der OS-Designer .

Standardmäßig sind Timer NICHT für den trackingModus geplant. Sie sind geplant am:

Erstellt einen Timer und plant ihn in der aktuellen Ausführungsschleife im Standardmodus .

Das scheduledTimerFolgende macht das:

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

Wenn Sie möchten, dass Ihr Timer beim Scrollen funktioniert, müssen Sie Folgendes tun:

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

Oder einfach:

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

Wenn Sie einen der oben genannten Schritte ausführen, wird Ihr Thread nicht durch Berührungsereignisse blockiert. was äquivalent ist zu:

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.

Alternative Lösung:

Sie können GCD für Ihren Timer verwenden, um Ihren Code vor Problemen mit der Verwaltung von Ausführungsschleifen zu schützen.

Für nicht wiederholte verwenden Sie einfach:

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

Verwenden Sie zum Wiederholen von Timern:

Erfahren Sie, wie Sie DispatchSourceTimer verwenden


Aus einer Diskussion mit Daniel Jalkut vertiefen:

Frage: Wie wird GCD (Hintergrundthreads), z. B. ein asynchroner After in einem Hintergrundthread, außerhalb von RunLoop ausgeführt? Mein Verständnis davon ist, dass alles innerhalb einer RunLoop ausgeführt werden soll

Nicht unbedingt - jeder Thread hat höchstens eine Ausführungsschleife, kann aber Null haben, wenn es keinen Grund gibt, die Ausführung "Besitz" des Threads zu koordinieren.

Threads sind ein Vorteil auf Betriebssystemebene, mit dem Ihr Prozess seine Funktionalität auf mehrere parallele Ausführungskontexte aufteilen kann. Ausführungsschleifen sind eine Möglichkeit auf Framework-Ebene, mit der Sie einen einzelnen Thread weiter aufteilen können, damit er von mehreren Codepfaden effizient gemeinsam genutzt werden kann.

Wenn Sie etwas versenden, das in einem Thread ausgeführt wird, hat es normalerweise keinen Runloop, es sei denn, es wird etwas aufgerufen, [NSRunLoop currentRunLoop]das implizit einen erstellen würde.

Kurz gesagt, Modi sind im Grunde genommen ein Filtermechanismus für Eingänge und Timer

Honig
quelle