UIScrollView приостанавливает NSTimer до завершения прокрутки

84

Во время UIScrollViewпрокрутки объекта (или его производного класса) кажется, что все NSTimersвыполняемые операции приостанавливаются до завершения прокрутки.

Есть ли способ обойти это? Потоки? Установка приоритета? Что-нибудь?

Маккклин
источник
семь лет спустя ... stackoverflow.com/a/12625429/294884
Fattie

Ответы:

202

Легкое и простое в реализации решение:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Кашиф Хисам
источник
2
UITrackingRunLoopMode - это режим (и его общедоступный), который вы можете использовать для создания различного поведения таймера.
Tom Andersen
Очень нравится, отлично работает и с GLKViewController. Просто установите для controller.paused значение YES, когда появится UIScrollView, и запустите свой собственный таймер, как описано. Переверните его, когда просмотр прокрутки закрыт, и все! Мой цикл обновления / рендеринга не очень дорогой, так что это может помочь.
Jeroen Bouma
3
Потрясающе! Должен ли я снимать таймер, если я сделаю его недействительным или нет? Спасибо
Якопо Пензо
Это работает для прокрутки, но останавливает таймер, когда приложение переходит в фоновый режим. Любое решение для достижения обоих?
Pulkit Sharma 01
23

Для всех, кто использует Swift 3

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


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
Исаак
источник
7
Лучше вызывать timer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)в качестве первой команды вместо Timer.scheduleTimer(), потому что scheduleTimer()добавляет таймер к циклу выполнения, а следующий вызов - это еще одно добавление к тому же циклу выполнения, но с другим режимом. Не делайте одну и ту же работу дважды.
Accid Bright 08
8

Да, Пол прав, это проблема цикла выполнения. В частности, вам нужно использовать метод NSRunLoop:

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
Август
источник
7

Это быстрая версия.

timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: aSelector, userInfo: nil, repeats: true)
            NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
Эндрю Кук
источник
6

Вам нужно запустить другой поток и еще один цикл выполнения, если вы хотите, чтобы таймеры срабатывали во время прокрутки; поскольку таймеры обрабатываются как часть цикла событий, если вы заняты обработкой прокрутки вашего представления, вы никогда не дойдете до таймеров. Хотя штраф за производительность / расход заряда батареи из-за запуска таймеров в других потоках может не иметь смысла в этом случае.

Ана Беттс
источник
11
Для этого не требуется еще одна ветка, см. Более свежий ответ от Кашифа. Я попробовал, и он работает без необходимости в дополнительном потоке.
progrmr 02
3
Стрельба из пушек по воробьям. Вместо потока просто запланируйте таймер в правильном режиме цикла выполнения.
uliwitness
2
Я думаю, что ответ Кашифа ниже - лучший ответ, поскольку для решения этой проблемы требуется только добавить 1 строку кода.
Дэмиен Мерфи.
3

для всех, кто использует Swift 4:

    timer = Timer(timeInterval: 1, target: self, selector: #selector(timerUpdated), userInfo: nil, repeats: true)
    RunLoop.main.add(timer, forMode: .common)
ЮЛун Сяо
источник
2

tl; dr цикл выполнения выполняет прокрутку, поэтому он не может больше обрабатывать события - если вы вручную не установите таймер, чтобы это также могло произойти, когда цикл выполнения обрабатывает события касания. Или попробуйте альтернативное решение и используйте GCD


Обязательно к прочтению любому разработчику iOS. Многие вещи в конечном итоге выполняются через RunLoop.

Получено из документации Apple .

Что такое цикл выполнения?

Цикл выполнения очень похож на свое название. Это цикл, в который входит ваш поток и который он использует для запуска обработчиков событий в ответ на входящие события.

Как нарушается доставка событий?

Поскольку таймеры и другие периодические события доставляются при запуске цикла выполнения, обход этого цикла нарушает доставку этих событий. Типичный пример такого поведения возникает всякий раз, когда вы реализуете процедуру отслеживания мыши, входя в цикл и многократно запрашивая события из приложения. Поскольку ваш код захватывает события напрямую, вместо того, чтобы позволить приложению отправлять эти события в обычном режиме, активные таймеры не смогут срабатывать до тех пор, пока ваша подпрограмма отслеживания мыши не завершится и не вернет управление приложению.

Что произойдет, если таймер сработает, когда цикл выполнения находится в середине выполнения?

Это происходит МНОГО РАЗ, а мы даже не замечаем этого. Я имею в виду, что мы установили таймер на 10: 10: 10: 00, но цикл выполнения выполняет событие, которое длится до 10: 10: 10: 05, поэтому таймер запускается 10: 10: 10: 06

Точно так же, если таймер срабатывает, когда цикл выполнения находится в середине выполнения подпрограммы обработчика, таймер ожидает до следующего раза цикла выполнения, чтобы вызвать свою подпрограмму обработчика. Если цикл выполнения вообще не запущен, таймер никогда не срабатывает.

Будет ли прокрутка или что-нибудь, что заставляет runloop быть занятым, постоянно сдвигать мой таймер?

Вы можете настроить таймеры для генерации событий только один раз или несколько раз. Повторяющийся таймер автоматически меняет расписание на основе запланированного времени зажигания, а не фактического времени зажигания. Например, если таймер запланирован для срабатывания в определенное время и каждые 5 секунд после этого, запланированное время срабатывания всегда будет соответствовать исходным 5-секундным интервалам, даже если фактическое время срабатывания будет отложено. Если время срабатывания задерживается настолько, что пропускается одно или несколько запланированных времен срабатывания, таймер срабатывает только один раз на пропущенный период времени. После срабатывания за пропущенный период таймер переносится на следующее запланированное время срабатывания.

Как я могу изменить режим RunLoops?

Вы не можете. ОС просто меняется для вас. например, когда пользователь нажимает, режим переключается на eventTracking. Когда пользовательские нажатия заканчиваются, режим возвращается в default. Если вы хотите, чтобы что-то работало в определенном режиме, вы должны убедиться, что это произойдет.


Решение:

Когда пользователь прокручивает, становится режим цикла выполнения tracking. RunLoop предназначен для переключения передач. Как только режим установлен eventTracking, он дает приоритет (помните, что у нас есть ограниченное количество ядер ЦП) сенсорным событиям. Это архитектурный проект дизайнеров ОС .

По умолчанию таймеры НЕ устанавливаются в trackingрежиме. Они запланированы на:

Создает таймер и планирует его для текущего цикла выполнения в режиме по умолчанию .

Ниже scheduledTimerделается следующее:

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

Если вы хотите, чтобы ваш таймер работал при прокрутке, вы должны сделать либо:

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

Или просто сделайте:

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

В конечном итоге выполнение одного из перечисленных выше действий означает, что ваш поток не блокируется событиями касания. что эквивалентно:

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.

Альтернативное решение:

Вы можете рассмотреть возможность использования GCD для своего таймера, который поможет вам «защитить» ваш код от проблем управления циклом выполнения.

Для неповторения просто используйте:

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

Для повторяющихся таймеров используйте:

Узнайте, как использовать DispatchSourceTimer


Углубляясь в беседу с Даниэлем Ялкутом:

Вопрос: как GCD (фоновые потоки), например, asyncAfter в фоновом потоке, выполняется вне RunLoop? Насколько я понимаю, все должно выполняться в RunLoop.

Не обязательно - каждый поток имеет не более одного цикла выполнения, но может иметь ноль, если нет причин координировать выполнение «владения» потоком.

Потоки - это доступность на уровне ОС, которая дает вашему процессу возможность разделить свои функциональные возможности по нескольким контекстам параллельного выполнения. Циклы выполнения - это возможность на уровне фреймворка, которая позволяет дополнительно разделить один поток, чтобы он мог эффективно совместно использоваться несколькими путями кода.

Обычно, если вы отправляете что-то, что запускается в потоке, у него, вероятно, не будет цикла выполнения, если только что-то не вызовет, что [NSRunLoop currentRunLoop]неявно его создаст.

Вкратце, режимы - это в основном механизм фильтрации для входов и таймеров.

Мед
источник