UIScrollView pauses NSTimer until scrolling finishes

IosUiscrollviewNstimer

Ios Problem Overview


While a UIScrollView (or a derived class thereof) is scrolling, it seems like all the NSTimers that are running get paused until the scroll is finished.

Is there a way to get around this? Threads? A priority setting? Anything?

Ios Solutions


Solution 1 - Ios

An easy & simple to implement solution is to do:

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

Solution 2 - Ios

For anyone using Swift 3

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


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

Solution 3 - Ios

Yes, Paul is right, this is a run loop issue. Specifically, you need to make use of the NSRunLoop method:

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

Solution 4 - Ios

tl;dr the runloop is handing scroll related events. It can't handle any more events — unless you manually change the timer's config so the timer can be processed while runloop is handling touch events. OR try an alternate solution and use GCD


A must read for any iOS developer. Lots of things are ultimately executed through RunLoop.

Derived from Apple's docs.

What is a Run Loop?

> A run loop is very much like its name sounds. It is a loop your thread > enters and uses to run event handlers in response to incoming events

How delivery of events are disrupted?

> Because timers and other periodic events are delivered when you run > the run loop, circumventing that loop disrupts the delivery of those > events. The typical example of this behavior occurs whenever you > implement a mouse-tracking routine by entering a loop and repeatedly > requesting events from the application. Because your code is grabbing > events directly, rather than letting the application dispatch those > events normally, active timers would be unable to fire until after > your mouse-tracking routine exited and returned control to the > application. >

What happens if timer is fired when run loop is in the middle of execution?

This happens A LOT OF TIMES, without us ever noticing. I mean we set the timer to fire at 10:10:10:00, but the runloop is executing an event which takes till 10:10:10:05, hence the timer is fired 10:10:10:06

> Similarly, if a timer fires when the run loop is in the middle of > executing a handler routine, the timer waits until the next time > through the run loop to invoke its handler routine. If the run loop is > not running at all, the timer never fires.

Would scrolling or anything that keeps the runloop busy shift all the times my timer is going to fire?

> You can configure timers to generate events only once or repeatedly. A > repeating timer reschedules itself automatically based on the > scheduled firing time, not the actual firing time. For example, if a > timer is scheduled to fire at a particular time and every 5 seconds > after that, the scheduled firing time will always fall on the original > 5 second time intervals, even if the actual firing time gets delayed. > If the firing time is delayed so much that it misses one or more of > the scheduled firing times, the timer is fired only once for the > missed time period. After firing for the missed period, the timer is > rescheduled for the next scheduled firing time.

How can I change the RunLoops's mode?

You can't. The OS just changes itself for you. e.g. when user taps, then the mode switches to eventTracking. When the user taps are finished, the mode goes back to default. If you want something to be run in a specific mode, then it's up to you make sure that happens.


Solution:

When user is scrolling the the Run Loop Mode becomes tracking. The RunLoop is designed to shifts gears. Once the mode is set to eventTracking, then it gives priority (remember we have limited CPU cores) to touch events. This is an architectural design by the OS designers.

By default timers are NOT scheduled on the tracking mode. They are scheduled on:

> Creates a timer and schedules it on the current run loop in the > default mode.

The scheduledTimer underneath does this:

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

If you want your timer to work when scrolling then you must do either:

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

Or just do:

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

Ultimately doing one of the above means your thread is not blocked by touch events. which is equivalent to:

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 solution:

You may consider using GCD for your timer which will help you to "shield" your code from run loop management issues.

For non-repeating just use:

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

For repeating timers use:

See how to use DispatchSourceTimer


Digging deeper from a discussion I had with Daniel Jalkut:

Question: how does GCD (background threads) e.g. a asyncAfter on a background thread get executed outside of the RunLoop? My understanding from this is that everything is to be executed within a RunLoop

Not necessarily - every thread has at most one run loop, but can have zero if there's no reason to coordinate execution "ownership" of the thread.

Threads are an OS level affordance that gives your process the ability to split up its functionality across multiple parallel execution contexts. Run loops are a framework-level affordance that allows you to further split up a single thread so it can be shared efficiently by multiple code paths.

Typically if you dispatch something that gets run on a thread, it probably won't have a runloop unless something calls [NSRunLoop currentRunLoop] which would implicitly create one.

In a nutshell, modes are basically a filter mechanism for inputs and timers

Solution 5 - Ios

You have to run another thread and another run loop if you want timers to fire while scrolling; since timers are processed as part of the event loop, if you're busy processing scrolling your view, you never get around to the timers. Though the perf/battery penalty of running timers on other threads might not be worth handling this case.

Solution 6 - Ios

This is the swift version.

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

Solution 7 - Ios

for anyone use Swift 4:

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

Solution 8 - Ios

Tested in swift 5

var myTimer: Timer?

self.myTimer= Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
      //your code
}
    
RunLoop.main.add(self.myTimer!, forMode: .common)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmccccleanView Question on Stackoverflow
Solution 1 - IosKashif HisamView Answer on Stackoverflow
Solution 2 - IosIsaacView Answer on Stackoverflow
Solution 3 - IosAugustView Answer on Stackoverflow
Solution 4 - IosmfaaniView Answer on Stackoverflow
Solution 5 - IosAna BettsView Answer on Stackoverflow
Solution 6 - IosAndrew CookView Answer on Stackoverflow
Solution 7 - IosYulong XiaoView Answer on Stackoverflow
Solution 8 - IosJuan Manuel Garcia CarmonaView Answer on Stackoverflow