My iOS app runs a repeatable timer inside a main dispatch queue to update the UI screen status periodically. It runs normally on iPhone and iPad. When it runs on a M1 MacOS, the timer seems not functioning as expected - the timer is called much much faster than the defined time interval.
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true,
block: {
// runs code to update UI
})
}
Besides, is there any good way to debug the iOS app running on M1 MacOS (like setting debug breakpoints in XCode) instead of re-installing/upgrading the iOS app from App Store after change each time?
Thank you very much for your advices.
We do not have the broader context in which you are calling the code in your question. Theoretically, there could be some race condition that performs differently on the different machines. Or perhaps when testing on the M1 device, there was a subtle difference in the testing procedure.
Regardless, there are a few defensive programming techniques that can solve this sort of problem:
I would make sure that timer is weak variable. The RunLoop will keep a strong reference to the timer for you, so you do not need to keep your own strong reference to it:
weak var timer: Timer?
By doing this, when the timer is invalidated, this reference will be set to nil automatically.
When creating a timer, invalidate the prior timer, if any. This will protect you should you ever accidentally call this routine more than once:
DispatchQueue.main.async {
self.timer?.invalidate() // invalidate prior timer, if any
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else {
timer?.invalidate()
return
}
// runs code to update UI
}
}
Note that in addition to the invalidate-before-scheduling pattern, I also
I use [weak self] pattern in closure, ensuring that the repeating timer cannot not unintentionally keep a strong reference to the current object.
I use guard let self = self else ... pattern to invalidate the timer and return should the object in question be deallocated.
But the critical note is that we want to use [weak self] with repeating timers to guard against strong reference cycles.
Optionally, while the above will stop the timer if the object has been deallocated when it next fires, I would generally also explicitly stop the timer as soon as the object is deallocated.
deinit {
timer?.invalidate()
}
The combination of the above steps will ensure that you can only have one timer per object, regardless of how often you call this routine. These steps will help prevent ever having multiple timers running at the same time.
Related
In the accepted answer for Timer.scheduledTimer not firing, it is emphasised to start the timer on the main thread to ensure that it fires. However, if I do that then I often end up with the timer being slow to initialise, and therefore failing in its purpose as a debouncer. Just wondering if there is something I am doing wrong, or a better way of doing this.
My problem (pseudocode at the bottom):
I use a JWT to authenticate my server calls, and I check this locally to see if it's expired before submitting it. However, I don't want several network calls to notice the expired JWT all at once and submit several refresh requests, so I use a semaphore to ensure only one call at a time is checking/renewing the JWT. I also use a dispatchGroup to delay the original network call until after the checking/renewing is done. However, if the refresh fails I want to avoid all the queued calls then trying it again. I don't want to block all refresh calls forever more with a boolean, so I thought I would create a scheduledTimer to block it. However, if I create it on the main thread, there's a delay before it's created and the released network calls submit a few more refresh attempts before they're blocked.
Questions
Should I just create the timer on the local thread to ensure there's no delay (I presume the main thread is occupied with some UI tasks which is why the timer doesn't get created instantly?)
More generally, is there a better way of doing this? I suspect there is - I tried playing with adding items to a queue, and then cancelling them, but then I began getting worried about creating work items with out of date values of functions, and capturing things in closures etc (it was a while ago, I can't remember the details), so I went with my current bodge.
This might all be easier if I was using await/async, but our app supports all the way back to iOS12, so I'm stuck with nests of completion handlers.
Hopefully this pseudocode is accurate enough to be helpful!
private static let requestQueue: DispatchQueue = DispatchQueue(label: "requestQueue", qos: .userInteractive, attributes: .concurrent)
public static let jwtValidityCheckSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
private static var uglyHackTimer: Timer?
#objc private class func clearUglyHackTimer(){
uglyHackTimer?.invalidate()
uglyHackTimer = nil
}
class func myNetworkCall(for: myPurposes){
let group = DispatchGroup()
jwtValidityCheckSemaphore.wait()
if (uglyHackTimer?.isValid ?? false){
jwtValidityCheckSemaphore.signal()
return
}
group.enter()
if jwtIsInvalid(){
refreshJWT(){success in
if !success{
DispatchQueue.main.async{
self.uglyHackTimer = Timer.scheduledTimer(timeInterval: TimeInterval(2), target: self, selector: #selector(clearUglyHackTimer), userInfo: nil, repeats: false)
}
}
group.leave()
jwtValidityCheckSemaphore.signal()
}
}else{
group.leave()
jwtValidityCheckSemaphore.signal()
}
// Make the original network call
newNetworkRequest = DispatchWorkItem{
// Blah, blah
}
group.notify(queue: requestQueue, work: newNetworkRequest)
}
There are lots of questions about using NSTimer for timer functions but I am instead using a CocoaPod "Countdown Label" https://github.com/suzuki-0000/CountdownLabel
My question/problem specifically is how to trigger an action/notification once it is complete I don't believe this to be the same as the many "NSTimer" questions/tutorials but please correct me if I'm wrong!
I've read the Timer documentation here https://developer.apple.com/documentation/foundation/timer but, again, don't see how it would apply to a 'custom timer library' like this Cocoapod.
I am currently trying this....
func testTimer() {
timerLabel.countdownDelegate = self
timerLabel.start()
if self.timerLabel.isFinished {
print("timer it's finished")
} else {self.timerLabel.start()
}
}
testTimer() is then called when an IBAction is pressed to star the timer.
My logic/thinking was that this function starts the timer, then checks if it finished. If it isn't finished it starts/continues the timer (timerLabel.start) and checks again basically in a loop until it is finished and then prints "timer is finished" but this isn't working and I'm not sure why? (but builds fine)
I know I could just scrap the pod and follow a Timer() tutorial but I'm trying to learn/understand how this sort of 'internal notification' for non Apple libraries would work generally at the same time as solving this specific problem. I hope this all makes sense.
NB. Some of the questions/answers I've read through that I don't believe are what I need are How to check if a NSTimer is running or not in Swift?, Check if Timer is running, Perform segue when timer is finished but they all use the Apple "Timer()".
NB. I am setting up the timer is ViewDidLoad with data from a Segue like this :
let timerLabelTime = Int(selectedWorkoutTime)
timerLabel.setCountDownTime(minutes: Double((timerLabelTime)!*60))
timerLabel.countdownDelegate = self
timerLabel.pause()
which works fine
if timerRunning == false{
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("counting"), userInfo: nil, repeats: true)
timerRunning = true
}
while timerLabel.text != "0"{
gameViewStillRunning = false
}
if gameViewStillRunning == false{
self.performSegueWithIdentifier("segue", sender: nil)
}
The purpose of this code is to display a label counting down, and then when it hits 0, the scene should switch to a different ViewController. This code doesn't get any errors but when I run it, the program does not get any further than the loading screen. Any suggestions?
It looks like you are running a while loop on the main thread which is also the thread responsible for drawing the UI. As long as that thread is stuck in your while loop there's no way for it's run loop to continue and no opporunity for it to update the display or respond to user interaction.
In addition if you look at the NSTimer documentation you might notice that scheduledTimerWithTimeInterval states that it Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode. That timer is scheduling a call to your counting function in the main thread's run loop but since that thread is stuck spinning forever in your while it will never have a chance to execute so it has no chance to update that timerLabel.text.
Rather than attempting to block execution with your while loop while polling for some condition, a better solution would be to allow the run loop to continue and to react when the timer calls your counting function. Let your application react to events (like the time remaining changing) regardless of when or how they happen rather than trying to control the exact sequence of execution.
The problem is that this is an infinite loop:
while timerLabel.text != "0"{
gameViewStillRunning = false
}
The body of the loop doesn't change the value of timerLabel.text, so the condition (the test in the first line) keeps failing, and the loop just loops forever — and the code, and so the entire app, just hangs at that point, coming to a dead stop permanently.
Can anyone explain for what is NSRunLoop? so as I know NSRunLoop is a something connected with NSThread right? So assume I create a Thread like
NSThread* th=[[NSThread alloc] initWithTarget:self selector:#selector(someMethod) object:nil];
[th start];
-(void) someMethod
{
NSLog(#"operation");
}
so after this Thread finishes his working right? why use RunLoops or where to use ? from Apple docs I have read something but its not clear for me, so please explain as simple as it possible
A run loop is an abstraction that (among other things) provides a mechanism to handle system input sources (sockets, ports, files, keyboard, mouse, timers, etc).
Each NSThread has its own run loop, which can be accessed via the currentRunLoop method.
In general, you do not need to access the run loop directly, though there are some (networking) components that may allow you to specify which run loop they will use for I/O processing.
A run loop for a given thread will wait until one or more of its input sources has some data or event, then fire the appropriate input handler(s) to process each input source that is "ready.".
After doing so, it will then return to its loop, processing input from various sources, and "sleeping" if there is no work to do.
That's a pretty high level description (trying to avoid too many details).
EDIT
An attempt to address the comment. I broke it into pieces.
it means that i can only access/run to run loop inside the thread
right?
Indeed. NSRunLoop is not thread safe, and should only be accessed from the context of the thread that is running the loop.
is there any simple example how to add event to run loop?
If you want to monitor a port, you would just add that port to the run loop, and then the run loop would watch that port for activity.
- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode
You can also add a timer explicitly with
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
what means it will then return to its loop?
The run loop will process all ready events each iteration (according to its mode). You will need to look at the documentation to discover about run modes, as that's a bit beyond the scope of a general answer.
is run loop inactive when i start the thread?
In most applications, the main run loop will run automatically. However, you are responsible for starting the run loop and responding to incoming events for threads you spin.
is it possible to add some events to Thread run loop outside the thread?
I am not sure what you mean here. You don't add events to the run loop. You add input sources and timer sources (from the thread that owns the run loop). The run loop then watches them for activity. You can, of course, provide data input from other threads and processes, but input will be processed by the run loop that is monitoring those sources on the thread that is running the run loop.
does it mean that sometimes i can use run loop to block thread for a time
Indeed. In fact, a run loop will "stay" in an event handler until that event handler has returned. You can see this in any app simply enough. Install a handler for any IO action (e.g., button press) that sleeps. You will block the main run loop (and the whole UI) until that method completes.
The same applies to any run loop.
I suggest you read the following documentation on run loops:
https://developer.apple.com/documentation/foundation/nsrunloop
and how they are used within threads:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
Run loops are what separates interactive apps from
command-line tools.
Command-line tools are launched with parameters, execute their command, then exit.
Interactive apps wait for user input, react, then resume waiting.
From here
They allow you to wait till user taps and respond accordingly, wait till you get a completionHandler and apply its results, wait till you get a timer and perform a function. If you don't have a runloop then you can't be listening/waiting for user taps, you can't wait till a network call is happening, you can't be awoken in x minutes unless you use DispatchSourceTimer or DispatchWorkItem
Also from this comment:
Background threads don't have their own runloops, but you can just add
one. E.g. AFNetworking 2.x did it. It was tried and true technique for
NSURLConnection or NSTimer on background threads, but we don't do this
ourselves much anymore, as newer APIs eliminate the need to do so. But
it appears that URLSession does, e.g., here is simple request, running [see the left panel of the image]
completion handlers on the main queue, and you can see it has a run
loop on background thread
Specifically about: "Background threads don't have their own runloops". The following timer fails to fire for an async dispatch:
class T {
var timer: Timer?
func fireWithoutAnyQueue() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { _ in
print("without any queue") // success. It's being ran on main thread, since playgrounds begin running from main thread
})
}
func fireFromQueueAsnyc() {
let queue = DispatchQueue(label: "whatever")
queue.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
print("from a queue — async") // failed to print
})
}
}
func fireFromQueueSnyc() {
let queue = DispatchQueue(label: "whatever")
queue.sync {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
print("from a queue — sync") // success. Weird. Read my possible explanation below
})
}
}
func fireFromMain() {
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
print("from main queue — sync") //success
})
}
}
}
I think the reason the sync block also runs is because:
sync blocks usually just get executed from within their source queue. In this example, source queue is main queue, the whatever queue is the destination queue.
To test that I logged RunLoop.current inside every dispatch.
The sync dispatch had the same runloop as main queue. While the RunLoop within the async block was a different instance from the others. You might be thinking how why does RunLoop.current return a different value. Isn't it a shared value!? Great question! Read further:
IMPORTANT NOTE:
The class property current is NOT a global variable.
Returns the run loop for the current thread.
It's contextual. It's visible only within the scope of the thread ie Thread-local storage. For more on that see here.
This is a known issue with timers. You don't have the same issue if you use DispatchSourceTimer
RunLoops are a bit of like a box where stuff just happens.
Basically, in a RunLoop, you go to process some events and then return. Or return if it doesn't process any events before the timeout is hit.
You can say it as similar to asynchronous NSURLConnections, Processing data in the background without interfering your current loop and but at the same time, you require data synchronously.
Which can be done with the help of RunLoop which makes your asynchronous NSURLConnection and provides data at calling time.
You can use a RunLoop like this:
NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];
while (YourBoolFlag && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil]) {
loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];
}
In this RunLoop, it will run until you complete some of your other work and set YourBoolFlag to false.
Similarly, you can use them in threads.
Hope this helps you.
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
From here
The most important feature of CFRunLoop is the CFRunLoopModes. CFRunLoop works with a system of “Run Loop Sources”. Sources are registered on a run loop for one or several modes, and the run loop itself is made to run in a given mode. When an event arrives on a source, it is only handled by the run loop if the source mode matches the run loop current mode.
From here
iOS RunLoop
RunLoop(EventLoop, Looper) is an implementation of EventLoop (event processing loop) pattern. It is based on NSRunLoop (which a wrapper of CFRunLoopRef)
Official doc
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
Single thread can have single RunLoop in a single mode. Only events with this mode will be processed all others will be waiting when RunLoop will be started at that mode
RunLoop is a mechanism (based on loop(for, while)) which move a scheduled task(e.g Callback Queue) to a thread(thread stack). RunLoop works(event processing loop) when Thread Stack is empty.
event processing loop is when RunLoop between .entry and .exit. During it RunLoop handles all scheduled task in specific mode. All others modes with their own Queues will be managed after
Application by default has a main thread with RunLoop(main loop). In other cases you should create it manually
main run loop is responsible for draining the main queue in an app.
//Run loop for the current thread
RunLoop.current
//Run loop of the main thread.
RunLoop.main
Mode
A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified.
modes:
default - used by default
tracking - for example when you scroll UITableView scrollViewDidScroll
common(is a pseudo mode like [default, tracking])
<custom> - you are able to create your own mode
//current input mode
RunLoop.current.currentMode
For example:
UIView.draw(_ rect:), button action... uses default mode
NSObject.perform(_:with:afterDelay:) uses default mode
DispatchQueue.main.async uses common mode
Timer.scheduledTimer uses default mode. That is why when UI scrolling occurring(in tracking mode) your timer is not fired(in default mode). To fix it use common mode - RunLoop.main.add(timer, forMode: .common)
Combine RunLoop.main vs DispatchQueue.main(.receive(on:, options:)). RunLoop.main uses RunLoop.perform(_:) which uses default mode, DispatchQueue.main uses DispatchQueue.main.async which uses common mode
input sources and timers
Run loop receives events:
Input sources - asynchronous events(as fired) messages
Port-based - from another thread or process. signaled automatically by the kernel
Custom Input Sources - user-initiated events - user actions, network events. must be signaled manually from another thread
performSelector: onThread
Timer sources - synchronous events(at specific time) timers
They can be added to several modes
observers
monitor RunLoop's state changes
Create RunLoop
create new thread, setup RunLoop and start the thread
create RunLoop
RunLoop.current
A run loop must have at least one input source or timer to monitor
RunLoop.add(_ timer: Timer, forMode mode: RunLoop.Mode)
RunLoop.add(_ aPort: Port, forMode mode: RunLoop.Mode)
run RunLoop
RunLoop.run()
let thread = Thread {
//1. create RunLoop
//create a new one or return existing run loop for current thread
//use RunLoop.current instead of RunLoop()
let customRunLoop = RunLoop.current
//add observer for current RunLoop for cpecufic mode
CFRunLoopAddObserver(CFRunLoopGetCurrent(), customObserver, CFRunLoopMode.commonModes)
//2. A run loop must have at least one input source or timer to monitor
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
//.default mode
}
customRunLoop.add(timer, forMode: .default)
//3. run RunLoop
//If no input sources or timers are attached to the run loop, this method exits immediately
//infinite loop that processes data from the run loop’s input sources and timers.
//calls RunLoop.run(mode:.default before:)
customRunLoop.run()
//------
//create custom mode
let customRunLoopMode = RunLoop.Mode("customeMode")
//2. A run loop must have at least one input source or timer to monitor
//Will be called when previous RunLoop.run() is done(no input sources or timers) - exit from loop
let timer2 = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
//"customeMode" mode
}
customRunLoop.add(timer2, forMode: customRunLoopMode)
//3. run RunLoop
let isInputSourcesOrTimers = customRunLoop.run(mode: customRunLoopMode, before: Date.distantFuture)
}
thread.start()
let customObserver = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.allActivities.rawValue , true, 0) { observer, activity in
switch (activity) {
case .entry:
break
case .beforeTimers:
break
case .beforeSources:
break
case .beforeWaiting:
break
case .afterWaiting:
break
case .exit:
break
case .allActivities:
break
default:
break
}
}
Swift
let runLoop = RunLoop.current
Obj-c
NSRunLoop * runloop = [NSRunLoop currentRunLoop];
A run loop is an event processing loop that is used to continuously monitor and process input events and assign them to the corresponding targets for processing.
Can anyone explain for what is NSRunLoop? so as I know NSRunLoop is a something connected with NSThread right? So assume I create a Thread like
NSThread* th=[[NSThread alloc] initWithTarget:self selector:#selector(someMethod) object:nil];
[th start];
-(void) someMethod
{
NSLog(#"operation");
}
so after this Thread finishes his working right? why use RunLoops or where to use ? from Apple docs I have read something but its not clear for me, so please explain as simple as it possible
A run loop is an abstraction that (among other things) provides a mechanism to handle system input sources (sockets, ports, files, keyboard, mouse, timers, etc).
Each NSThread has its own run loop, which can be accessed via the currentRunLoop method.
In general, you do not need to access the run loop directly, though there are some (networking) components that may allow you to specify which run loop they will use for I/O processing.
A run loop for a given thread will wait until one or more of its input sources has some data or event, then fire the appropriate input handler(s) to process each input source that is "ready.".
After doing so, it will then return to its loop, processing input from various sources, and "sleeping" if there is no work to do.
That's a pretty high level description (trying to avoid too many details).
EDIT
An attempt to address the comment. I broke it into pieces.
it means that i can only access/run to run loop inside the thread
right?
Indeed. NSRunLoop is not thread safe, and should only be accessed from the context of the thread that is running the loop.
is there any simple example how to add event to run loop?
If you want to monitor a port, you would just add that port to the run loop, and then the run loop would watch that port for activity.
- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode
You can also add a timer explicitly with
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
what means it will then return to its loop?
The run loop will process all ready events each iteration (according to its mode). You will need to look at the documentation to discover about run modes, as that's a bit beyond the scope of a general answer.
is run loop inactive when i start the thread?
In most applications, the main run loop will run automatically. However, you are responsible for starting the run loop and responding to incoming events for threads you spin.
is it possible to add some events to Thread run loop outside the thread?
I am not sure what you mean here. You don't add events to the run loop. You add input sources and timer sources (from the thread that owns the run loop). The run loop then watches them for activity. You can, of course, provide data input from other threads and processes, but input will be processed by the run loop that is monitoring those sources on the thread that is running the run loop.
does it mean that sometimes i can use run loop to block thread for a time
Indeed. In fact, a run loop will "stay" in an event handler until that event handler has returned. You can see this in any app simply enough. Install a handler for any IO action (e.g., button press) that sleeps. You will block the main run loop (and the whole UI) until that method completes.
The same applies to any run loop.
I suggest you read the following documentation on run loops:
https://developer.apple.com/documentation/foundation/nsrunloop
and how they are used within threads:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
Run loops are what separates interactive apps from
command-line tools.
Command-line tools are launched with parameters, execute their command, then exit.
Interactive apps wait for user input, react, then resume waiting.
From here
They allow you to wait till user taps and respond accordingly, wait till you get a completionHandler and apply its results, wait till you get a timer and perform a function. If you don't have a runloop then you can't be listening/waiting for user taps, you can't wait till a network call is happening, you can't be awoken in x minutes unless you use DispatchSourceTimer or DispatchWorkItem
Also from this comment:
Background threads don't have their own runloops, but you can just add
one. E.g. AFNetworking 2.x did it. It was tried and true technique for
NSURLConnection or NSTimer on background threads, but we don't do this
ourselves much anymore, as newer APIs eliminate the need to do so. But
it appears that URLSession does, e.g., here is simple request, running [see the left panel of the image]
completion handlers on the main queue, and you can see it has a run
loop on background thread
Specifically about: "Background threads don't have their own runloops". The following timer fails to fire for an async dispatch:
class T {
var timer: Timer?
func fireWithoutAnyQueue() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { _ in
print("without any queue") // success. It's being ran on main thread, since playgrounds begin running from main thread
})
}
func fireFromQueueAsnyc() {
let queue = DispatchQueue(label: "whatever")
queue.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
print("from a queue — async") // failed to print
})
}
}
func fireFromQueueSnyc() {
let queue = DispatchQueue(label: "whatever")
queue.sync {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
print("from a queue — sync") // success. Weird. Read my possible explanation below
})
}
}
func fireFromMain() {
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
print("from main queue — sync") //success
})
}
}
}
I think the reason the sync block also runs is because:
sync blocks usually just get executed from within their source queue. In this example, source queue is main queue, the whatever queue is the destination queue.
To test that I logged RunLoop.current inside every dispatch.
The sync dispatch had the same runloop as main queue. While the RunLoop within the async block was a different instance from the others. You might be thinking how why does RunLoop.current return a different value. Isn't it a shared value!? Great question! Read further:
IMPORTANT NOTE:
The class property current is NOT a global variable.
Returns the run loop for the current thread.
It's contextual. It's visible only within the scope of the thread ie Thread-local storage. For more on that see here.
This is a known issue with timers. You don't have the same issue if you use DispatchSourceTimer
RunLoops are a bit of like a box where stuff just happens.
Basically, in a RunLoop, you go to process some events and then return. Or return if it doesn't process any events before the timeout is hit.
You can say it as similar to asynchronous NSURLConnections, Processing data in the background without interfering your current loop and but at the same time, you require data synchronously.
Which can be done with the help of RunLoop which makes your asynchronous NSURLConnection and provides data at calling time.
You can use a RunLoop like this:
NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];
while (YourBoolFlag && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil]) {
loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];
}
In this RunLoop, it will run until you complete some of your other work and set YourBoolFlag to false.
Similarly, you can use them in threads.
Hope this helps you.
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
From here
The most important feature of CFRunLoop is the CFRunLoopModes. CFRunLoop works with a system of “Run Loop Sources”. Sources are registered on a run loop for one or several modes, and the run loop itself is made to run in a given mode. When an event arrives on a source, it is only handled by the run loop if the source mode matches the run loop current mode.
From here
iOS RunLoop
RunLoop(EventLoop, Looper) is an implementation of EventLoop (event processing loop) pattern. It is based on NSRunLoop (which a wrapper of CFRunLoopRef)
Official doc
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
Single thread can have single RunLoop in a single mode. Only events with this mode will be processed all others will be waiting when RunLoop will be started at that mode
RunLoop is a mechanism (based on loop(for, while)) which move a scheduled task(e.g Callback Queue) to a thread(thread stack). RunLoop works(event processing loop) when Thread Stack is empty.
event processing loop is when RunLoop between .entry and .exit. During it RunLoop handles all scheduled task in specific mode. All others modes with their own Queues will be managed after
Application by default has a main thread with RunLoop(main loop). In other cases you should create it manually
main run loop is responsible for draining the main queue in an app.
//Run loop for the current thread
RunLoop.current
//Run loop of the main thread.
RunLoop.main
Mode
A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified.
modes:
default - used by default
tracking - for example when you scroll UITableView scrollViewDidScroll
common(is a pseudo mode like [default, tracking])
<custom> - you are able to create your own mode
//current input mode
RunLoop.current.currentMode
For example:
UIView.draw(_ rect:), button action... uses default mode
NSObject.perform(_:with:afterDelay:) uses default mode
DispatchQueue.main.async uses common mode
Timer.scheduledTimer uses default mode. That is why when UI scrolling occurring(in tracking mode) your timer is not fired(in default mode). To fix it use common mode - RunLoop.main.add(timer, forMode: .common)
Combine RunLoop.main vs DispatchQueue.main(.receive(on:, options:)). RunLoop.main uses RunLoop.perform(_:) which uses default mode, DispatchQueue.main uses DispatchQueue.main.async which uses common mode
input sources and timers
Run loop receives events:
Input sources - asynchronous events(as fired) messages
Port-based - from another thread or process. signaled automatically by the kernel
Custom Input Sources - user-initiated events - user actions, network events. must be signaled manually from another thread
performSelector: onThread
Timer sources - synchronous events(at specific time) timers
They can be added to several modes
observers
monitor RunLoop's state changes
Create RunLoop
create new thread, setup RunLoop and start the thread
create RunLoop
RunLoop.current
A run loop must have at least one input source or timer to monitor
RunLoop.add(_ timer: Timer, forMode mode: RunLoop.Mode)
RunLoop.add(_ aPort: Port, forMode mode: RunLoop.Mode)
run RunLoop
RunLoop.run()
let thread = Thread {
//1. create RunLoop
//create a new one or return existing run loop for current thread
//use RunLoop.current instead of RunLoop()
let customRunLoop = RunLoop.current
//add observer for current RunLoop for cpecufic mode
CFRunLoopAddObserver(CFRunLoopGetCurrent(), customObserver, CFRunLoopMode.commonModes)
//2. A run loop must have at least one input source or timer to monitor
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
//.default mode
}
customRunLoop.add(timer, forMode: .default)
//3. run RunLoop
//If no input sources or timers are attached to the run loop, this method exits immediately
//infinite loop that processes data from the run loop’s input sources and timers.
//calls RunLoop.run(mode:.default before:)
customRunLoop.run()
//------
//create custom mode
let customRunLoopMode = RunLoop.Mode("customeMode")
//2. A run loop must have at least one input source or timer to monitor
//Will be called when previous RunLoop.run() is done(no input sources or timers) - exit from loop
let timer2 = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
//"customeMode" mode
}
customRunLoop.add(timer2, forMode: customRunLoopMode)
//3. run RunLoop
let isInputSourcesOrTimers = customRunLoop.run(mode: customRunLoopMode, before: Date.distantFuture)
}
thread.start()
let customObserver = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.allActivities.rawValue , true, 0) { observer, activity in
switch (activity) {
case .entry:
break
case .beforeTimers:
break
case .beforeSources:
break
case .beforeWaiting:
break
case .afterWaiting:
break
case .exit:
break
case .allActivities:
break
default:
break
}
}
Swift
let runLoop = RunLoop.current
Obj-c
NSRunLoop * runloop = [NSRunLoop currentRunLoop];
A run loop is an event processing loop that is used to continuously monitor and process input events and assign them to the corresponding targets for processing.