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?
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?
An easy & simple to implement solution is to do:
NSTimer *timer = [NSTimer timerWithTimeInterval:...
target:...
selector:....
userInfo:...
repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
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)
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.
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
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.
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.
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.
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.
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.
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
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
This is the swift version.
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: aSelector, userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
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.
for anyone use Swift 4:
timer = Timer(timeInterval: 1, target: self, selector: #selector(timerUpdated), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .common)
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)