UIScrollView wstrzymuje NSTimer do zakończenia przewijania

Odpowiedzi:

202

Łatwe i proste do wdrożenia rozwiązanie to:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Kashif Hisam
źródło
2
UITrackingRunLoopMode to tryb - i jego publiczny - możesz użyć do tworzenia różnych zachowań timera.
Tom Andersen
Uwielbiam to, działa jak urok z GLKViewController. Po prostu ustaw kontroler wstrzymany na TAK, gdy pojawi się UIScrollView i uruchom własny licznik czasu zgodnie z opisem. Odwróć to, gdy widok przewijania zostanie odrzucony i to wszystko! Moja pętla aktualizacji / renderowania nie jest zbyt droga, więc może to pomóc.
Jeroen Bouma,
3
Niesamowite! Czy muszę usunąć licznik czasu, kiedy go unieważnię, czy nie? Dzięki
Jacopo Penzo
Działa to w przypadku przewijania, ale zatrzymuje licznik czasu, gdy aplikacja przechodzi w tło. Jakieś rozwiązanie, aby osiągnąć oba te cele?
Pulkit Sharma
23

Dla każdego, kto używa Swift 3

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


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
Izaak
źródło
7
Lepiej jest wywołać timer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)jako pierwsze polecenie zamiast Timer.scheduleTimer(), ponieważ scheduleTimer()dodaje licznik czasu do runloop, a następne wywołanie jest kolejnym dodaniem do tego samego runloop, ale z innym trybem. Nie wykonuj dwukrotnie tej samej pracy.
Accid Bright
8

Tak, Paul ma rację, to kwestia pętli uruchamiania. W szczególności musisz skorzystać z metody NSRunLoop:

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
sierpień
źródło
7

To jest szybka wersja.

timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: aSelector, userInfo: nil, repeats: true)
            NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
Andrew Cook
źródło
6

Musisz uruchomić inny wątek i kolejną pętlę uruchamiania, jeśli chcesz, aby liczniki czasu były uruchamiane podczas przewijania; Ponieważ liczniki są przetwarzane jako część pętli zdarzeń, jeśli jesteś zajęty przewijaniem widoku, nigdy nie możesz przejść do liczników. Chociaż kara za wydajność / baterię w przypadku uruchamiania timerów w innych wątkach może nie być warta zajęcia się tym przypadkiem.

Ana Betts
źródło
11
Nie wymaga kolejnego wątku, zobacz najnowszą odpowiedź od Kashif. Wypróbowałem to i działa bez dodatkowego wątku.
progrmr
3
Strzelanie z armat do wróbli. Zamiast wątku, po prostu zaplanuj czasomierz w odpowiednim trybie pętli uruchamiania.
uliwitness
2
Myślę, że odpowiedź Kashif poniżej jest zdecydowanie najlepszą odpowiedzią, ponieważ wymaga tylko dodania 1 linii kodu, aby rozwiązać ten problem.
damien murphy.
3

dla każdego używaj Swift 4:

    timer = Timer(timeInterval: 1, target: self, selector: #selector(timerUpdated), userInfo: nil, repeats: true)
    RunLoop.main.add(timer, forMode: .common)
YuLong Xiao
źródło
1

tl; dr runloop wykonuje przewijanie, więc nie może obsłużyć więcej zdarzeń - chyba że ręcznie ustawisz licznik czasu, aby mogło się to również zdarzyć, gdy runloop obsługuje zdarzenia dotykowe. Lub wypróbuj alternatywne rozwiązanie i użyj GCD


Lektura obowiązkowa dla każdego programisty iOS. Wiele rzeczy jest ostatecznie wykonywanych przez RunLoop.

Pochodzi z dokumentów Apple .

Co to jest Run Loop?

Pętla biegu jest bardzo podobna do jej nazwy. Jest to pętla, do której wchodzi wątek i której używa do uruchamiania programów obsługi zdarzeń w odpowiedzi na nadchodzące zdarzenia

W jaki sposób zakłócana jest realizacja wydarzeń?

Ponieważ liczniki czasu i inne zdarzenia okresowe są dostarczane po uruchomieniu pętli uruchamiania, obejście tej pętli zakłóca dostarczanie tych zdarzeń. Typowy przykład takiego zachowania występuje zawsze, gdy implementujesz procedurę śledzenia myszy, wprowadzając pętlę i wielokrotnie żądając zdarzeń z aplikacji. Ponieważ kod przechwytuje zdarzenia bezpośrednio, a nie pozwala aplikacji normalnie wywoływać te zdarzenia, aktywne zegary nie będą mogły zostać uruchomione, dopóki procedura śledzenia myszy nie zakończy się i nie zwróci kontroli do aplikacji.

Co się stanie, jeśli licznik czasu zostanie uruchomiony, gdy pętla uruchamiania jest w środku wykonywania?

Dzieje się to DUŻO RAZ, a my nigdy tego nie zauważamy. Mam na myśli, że ustawiliśmy zegar na uruchomienie o 10:00, ale runloop wykonuje zdarzenie, które trwa do 10:10:10:05, dlatego zegar jest uruchamiany 10: 10: 10: 06

Podobnie, jeśli licznik czasu uruchamia się, gdy pętla uruchamiania jest w trakcie wykonywania procedury obsługi, licznik czasu czeka do następnego przejścia przez pętlę wykonywania, aby wywołać procedurę obsługi. Jeśli pętla w ogóle nie działa, licznik czasu nigdy nie zostanie uruchomiony.

Czy przewijanie lub cokolwiek, co sprawia, że ​​pętla jest zajęta, zmienia się za każdym razem, gdy mój licznik czasu się uruchomi?

Możesz skonfigurować timery tak, aby generowały zdarzenia tylko raz lub wielokrotnie. Czasomierz powtarzania samoczynnie zmienia harmonogram na podstawie zaplanowanego czasu odpalania, a nie faktycznego czasu odpalania. Na przykład, jeśli zegar ma odpalić w określonym czasie i co 5 sekund po tym, zaplanowany czas strzelania będzie zawsze przypadał na pierwotne 5-sekundowe interwały, nawet jeśli rzeczywisty czas odpalania zostanie opóźniony. Jeśli czas strzelania jest tak bardzo opóźniony, że omija jeden lub więcej zaplanowanych czasów strzelania, licznik czasu jest uruchamiany tylko raz na brakujący czas. Po strzale za nieudany okres, licznik czasu zostaje przesunięty na następny zaplanowany czas strzelania.

Jak mogę zmienić tryb RunLoops?

Nie możesz. System operacyjny po prostu zmienia się dla Ciebie. np. gdy użytkownik puknie, tryb przełącza się na eventTracking. Po zakończeniu stuknięć przez użytkownika tryb powraca do default. Jeśli chcesz, aby coś działało w określonym trybie, to od Ciebie zależy, czy tak się stanie.


Rozwiązanie:

Kiedy użytkownik przewija, tryb Run Loop zmienia się na tracking. RunLoop jest przeznaczony do zmiany biegów. Gdy tryb jest ustawiony na eventTracking, daje priorytet (pamiętaj, że mamy ograniczone rdzenie procesora), aby zdarzenia dotknąć. To jest projekt architektoniczny wykonany przez projektantów systemu operacyjnego .

Domyślnie timery NIE są zaplanowane w tym trackingtrybie. Są zaplanowane na:

Tworzy licznik czasu i planuje go w bieżącej pętli uruchamiania w trybie domyślnym .

Pod scheduledTimerspodem robi to:

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

Jeśli chcesz, aby zegar działał podczas przewijania, musisz wykonać jedną z poniższych czynności:

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

Lub po prostu zrób:

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

Ostatecznie wykonanie jednego z powyższych oznacza, że ​​Twój wątek nie jest blokowany przez zdarzenia dotykowe. co jest równoważne z:

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.

Alternatywne rozwiązanie:

Możesz rozważyć użycie GCD jako timera, który pomoże ci "chronić" kod przed problemami z zarządzaniem pętlą uruchamiania.

Aby nie powtarzać, po prostu użyj:

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

Do powtarzających się timerów użyj:

Zobacz, jak używać DispatchSourceTimer


Sięgając głębiej do dyskusji, którą odbyłem z Danielem Jalkutem:

Pytanie: w jaki sposób GCD (wątki w tle), np. AsyncAfter w wątku w tle, są wykonywane poza RunLoop? Z tego rozumiem, że wszystko ma być wykonane w ramach RunLoop

Niekoniecznie - każdy wątek ma co najwyżej jedną pętlę uruchamiania, ale może mieć zero, jeśli nie ma powodu, aby koordynować wykonanie „własności” wątku.

Wątki to afordancja na poziomie systemu operacyjnego, która umożliwia procesowi dzielenie jego funkcjonalności na wiele równoległych kontekstów wykonywania. Pętle uruchamiania to afordancja na poziomie struktury, która umożliwia dalsze dzielenie pojedynczego wątku, aby można go było wydajnie udostępniać przez wiele ścieżek kodu.

Zwykle, jeśli wysyłasz coś, co jest uruchamiane w wątku, prawdopodobnie nie będzie miało pętli rozruchowej, chyba że wywoła coś, [NSRunLoop currentRunLoop]co niejawnie je utworzy.

Krótko mówiąc, tryby są w zasadzie mechanizmem filtrującym wejścia i timery

kochanie
źródło