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

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

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

8 ответов

Решение

Простое и простое в реализации решение состоит в следующем:

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

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

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


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

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] который неявно создал бы его.

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

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

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

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

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

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

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

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

Протестировано в Swift 5

      var myTimer: Timer?

self.myTimer= Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
      //your code
}
    
RunLoop.main.add(self.myTimer!, forMode: .common)
Другие вопросы по тегам