Run repeating NSTimer with GCD? - ios

I was wondering why when you create a repeating timer in a GCD block it doesen't work?
This works fine:
-(void)viewDidLoad{
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(runTimer) userInfo:nil repeats:YES];
}
-(void)runTimer{
NSLog(#"hi");
}
But this doesent work:
dispatch_queue_t myQueue;
-(void)viewDidLoad{
[super viewDidLoad];
myQueue = dispatch_queue_create("someDescription", NULL);
dispatch_async(myQueue, ^{
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(runTimer) userInfo:nil repeats:YES];
});
}
-(void)runTimer{
NSLog(#"hi");
}

NSTimers are scheduled on the current thread's run loop. However, GCD dispatch threads don't have run loops, so scheduling timers in a GCD block isn't going to do anything.
There's three reasonable alternatives:
Figure out what run loop you want to schedule the timer on, and explicitly do so. Use +[NSTimer timerWithTimeInterval:target:selector:userInfo:repeats:] to create the timer and then -[NSRunLoop addTimer:forMode:] to actually schedule it on the run loop you want to use. This requires having a handle on the run loop in question, but you may just use +[NSRunLoop mainRunLoop] if you want to do it on the main thread.
Switch over to using a timer-based dispatch source. This implements a timer in a GCD-aware mechanism, that will run a block at the interval you want on the queue of your choice.
Explicitly dispatch_async() back to the main queue before creating the timer. This is equivalent to option #1 using the main run loop (since it will also create the timer on the main thread).
Of course, the real question here is, why are you creating a timer from a GCD queue to begin with?

NSTimer is scheduled to thread’s runloop. In code of question, runloop of thread dispatched by GCD is not running. You must start it manually and there must be a way to exit run loop, so you should keep a reference to the NSTimer, and invalidate it in appropriate time.
NSTimer has strong reference to the target, so target can't has strong reference to timer, and runloop has strong reference to the timer.
weak var weakTimer: Timer?
func configurateTimerInBackgroundThread(){
DispatchQueue.global().async {
// Pause program execution in Xcode, you will find thread with this name
Thread.current.name = "BackgroundThreadWithTimer"
// This timer is scheduled to current run loop
self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
// Start current runloop manually, otherwise NSTimer won't fire.
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
}
}
#objc func runTimer(){
NSLog("Timer is running in mainThread: \(Thread.isMainThread)")
}
If timer is invalidated in future, pause program execution again in Xcode, you will find that thread is gone.
Of course, threads dispatched by GCD have runloop. GCD generate and reuse threads internally, there threads are anonymous to caller. If you don't feel safe to it, you could use Thread. Don't afraid, code is very easy.
Actually, I try same thing last week and get same fail with asker, then I found this page. I try NSThread before I give up. It works. So why NSTimer in GCD can't work? It should be. Read runloop's document to know how NSTimer works.
Use NSThread to work with NSTimer:
func configurateTimerInBackgroundThread(){
let thread = Thread.init(target: self, selector: #selector(addTimerInBackground), object: nil)
thread.name = "BackgroundThreadWithTimer"
thread.start()
}
#objc func addTimerInBackground() {
self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
}

This is a bad Idea. I was about to delete this answer, but I left it here to avoid others from doing the same mistake I did.
Thank you #Kevin_Ballard for pointing at this.
You'd only add one line to your example and it'd work just as you wrote it:
[[NSRunLoop currentRunLoop] run]
so you'd get:
-(void)viewDidLoad{
[super viewDidLoad];
myQueue = dispatch_queue_create("someDescription", NULL);
dispatch_async(myQueue, ^{
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(runTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run]
});
}
Since your queue myQueue contains a NSThread and it contains a NSRunLoop and since the code in the dispatch_async is run the the context of that NSThread, currentRunLoop would return a stopped run loop associated with the thread of your queue.

Related

Unable to stop NSTimer

I use this code for stopping NSTimer
[timer invalidate]
timer = nil;
It works fine for the first run. But, after I resume the timer with this code.
timer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(checkNewUpdates)
userInfo:nil
repeats:YES];
NSTimer won't stop anymore with [timer invalidate]
It look like multiple instance of timer is running simultaneously. You can do one thing, before start to run a new timer, check for previous instance of timer, and if timer instance is available, then invalidate it. After this start new instance
if(timer)
{
[timer invalidate];
timer = nil;
}
timer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(checkNewUpdates)
userInfo:nil
repeats:YES];
In apple's official document they said:
You must send this message from the thread on which the timer was
installed. If you send this message from another thread, the input
source associated with the timer may not be removed from its run loop,
which could prevent the thread from exiting properly.
If your timer is running on main thread, do this:
[timer performSelectorOnMainThread:#selector(invalidate) withObject:nil waitUntilDone:YES];
If it is on any other thread, lets call the thread myThread, then do this:
[timer performSelector:#selector(invalidate) onThread:myThread withObject:nil waitUntilDone:NO];
Hope this helps.. :)
Just invalidate the timer inside the selector that fires. That will ensure you have a pointer to the correct timer (which is probably why your invalidate call isn't working:
timer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(checkNewUpdates:)
userInfo:nil
repeats:YES];
Note the colon after checkNewUpdates:
Then, in your checkNewUpdates: method, do something like this:
- (void)checkNewUpdates:(NSTimer*)timer
{
// do somehting
// Then, check if the criteria for stopping the timer has been met, and invalidate it here.
if( self.shouldStopTimer ) // made up variable, use your own criteria.
{
[timer invalidate];
}
}
I know this doesnt answer your question per-se;
Can I suggest using polling mechanism instead of a timer? Ive had a world of trouble with NSTimers in the past and polling was a good alternative. Ive made a simple one below.
- (void) doPoll {
// do logic here
if (shoudStop) {
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, X * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self doPoll];
});
}
This is just a simple example, it does not stop retain cycles If you choose to try this, Yours should.
Hope it helps.

NSTimer doesn't stop with invalidate

I add timer like this
tim=[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(repeatTim) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:tim forMode:NSDefaultRunLoopMode];
tim it is NSTimer property of my class.
Then i stop it on button click like
[[fbt tim] invalidate];
[fbt setTim:nil];
fbt it is instance of my class.
if i call only invalidate then it doesn't stop, but if i set it to nil then i got EXC_BREAKPOINT
here code of repeatTim method in selector
AppDelegate *appDelegate = [[UIApplication sharedApplication]delegate];
[appDelegate.wbv stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"intal()"]];
I tried to call init and invalidate in
dispatch_async(dispatch_get_main_queue(), ^{})
it also doesn't stop timer.
You have more than one timer running . Try this:
-(void)startTimer{
[self.myTimer invalidate]; // kill old timer
self.myTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(doSomething) userInfo:nil repeats:YES];
}
-(void)stopTimer{
[self.myTimer invalidate];
self.myTimer=nil; //set pointer to nil
}
Read documentation for NSTimer:
There are three ways to create a timer:
Use the scheduledTimerWithTimeInterval:invocation:repeats: or scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: class method to create the timer and schedule it on the current run loop in the default mode.
Use the timerWithTimeInterval:invocation:repeats: or timerWithTimeInterval:target:selector:userInfo:repeats: class method to create the timer object without scheduling it on a run loop. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)
Allocate the timer and initialize it using the initWithFireDate:interval:target:selector:userInfo:repeats: method. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)
You are using method which already adds it to mainLoop from 1. - you need to remove this line or create a timer with 2. approach and leave manual adding.
Also remember that you must send invalidate message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
I have tried every possible solution found but not able to resolve that at the end I have set repeat "false" while initialising timer like below
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(viewcontroller.methodname), userInfo: nil, repeats: false)
And need to add above line in my selector method for whatever the condition for which I wanted to repeat the time.
For example:- My requirement is I want to repeatedly call some method until one condition satisfied. So instead of adding repeats true I set it false as repeat true does not invalidate timer in my case.
I have added below in my viewdidload method
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(viewcontroller.method), userInfo: nil, repeats: false)
in selector function I added below code:-
func method{
if condition matched{
// here your timer will be invalidated automatically
}
else{
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self,selector: #selector(viewcontroller.method), userInfo: nil,repeats: false)
}
}
Hope this will solve your problem.
Happy Coding :)

how to invalidate NSTimer?

self.timerProgress=[NSTimer scheduledTimerWithTimeInterval:50.0 target:self selector:#selector(stopProgressView) userInfo:nil repeats:NO];
-(void)stopProgressView
{
if ([self.timerProgress isValid]) {
[self.timerProgress invalidate];
self.timerProgress=nil;
}
}
and on a button click when i tried to invalidate NSTimer object
-(void)cancelTimer
{
if ([self.timerProgress isValid]) {
[self.timerProgress invalidate];
self.timerProgress=nil;
}
}
it don't get invalidate. It calls once stopProgressView after the interval of 50.
How to get resolve from this issue?
- (IBAction)stopTimer {
if ([timerProgress isValid]) {
[timerProgress invalidate];
}
}
Don't use self.timerProgress use just timerProgress
The most likely reason for this is that your timer scheduled on a different run loop to the one where you try and invalidate it.
Timers must be invalidated on the same thread/runloop as the run loop that they are scheduled on.
Cocoa touch isn't thread safe, so you should be running all UI related activities on the main thread. It may work if you do GUI work on different threads, but then you'll get random crashes, and you'll also generate timer problems like this.
It seems like from what you're posting it should work. This is how I have it in my apps and it works fine.
However, you could try making the selector one that takes a timer object like:
-(void)stopProgressView:(NSTimer *)timer{
//do stuff with timer here
}
Note that this would also mean that you should change #selector(stopProgressView) to #selector(stopProgressView:). Although for the record my current stop timer function just uses [self.timer invalidate] and it works fine.
My other piece of advice for debugging is to use NSLogs to make sure each of the methods are in fact getting called, and that when the method is called an NSLog within the if clause to make sure that works.
You create NSTimer with out NSRunLoop so your NSTimer not started, to add this code after
self.timerProgress = [NSTimer scheduledTimerWithTimeInterval:50.0
target:self
selector:#selector(stopProgressView)
userInfo:nil
repeats:NO];
//add
[[NSRunLoop currentRunLoop] addTimer:_tapTimer
forMode:NSDefaultRunLoopMode];

iOS Executing loop until certain condition is met of specified time has passed?

As the title states, i have a while loop that will be executed until certain condition is met, or until 5 seconds have passed.
What is the best way to solve this? I have seen some simple tutorial about NSTimer, but it seems to me that selector that is fired within NSTimer will be executed after time interval specified no matter what. I only need to execute it if condition is not met...
Just create an NSTimer scheduled action store the timer and if you reach your what you wanted to achieve deactivate this timer so that it doesn't trigger the action.
Basically:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:#selector(yourAction) userInfo:nil repeats:NO];
some code
//for deactivating the timer
[timer invalidate];
timer = nil;
You could start the NSTimer on the main thread (to ensure above code works) with this:
[self performSelectorOnMainThread:#selector(startTimerMethod) withObject:someOrNoObject waitUntilDone:NO];

NSTimer requiring me to add it to a runloop

I am wondering if someone can explain why dispatching back to the main queue and creating a repeating NSTimer I am having to add it to RUN LOOP for it too fire? Even when using performselectorOnMainThread I still have to add it to a RUN LOOP to get it to fire.
Below is an example of my question:
#define queue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
#define mainqueue dispatch_get_main_queue()
- (void)someMethodBeginCalled
{
dispatch_async(queue, ^{
int x = 0;
dispatch_async(mainqueue, ^(void){
if([_delegate respondsToSelector:#selector(complete:)])
[_delegate complete:nil];
});
});
}
- (void)compelete:(id)object
{
[self startTimer];
//[self performSelectorOnMainThread:#selector(startTimer) withObject:nil waitUntilDone:NO];
}
- (void)startTimer
{
NSTimer timer = [NSTimer timerWithTimeInterval:3 target:self selector:#selector(callsomethingelse) userInfo:nil repeats:YES];
//NSDefaultRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:_busTimer forMode:NSRunLoopCommonModes];
}
EDIT:
I believe I worded this question very poorly. I would like to know why [[NSRunLoop currentRunLoop] addTimer:_busTimer forMode:NSRunLoopCommonModes]; is necessary in startTimer if I call someMethodBeginCalled. If I don't include that line, the timer doesn't fire.
If I call startTimer from viewDidLoad for example, I can remove the NSRunLoop line and the timer will fire every 60 seconds.
And here's how to add an NSTimer to a runloop:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
You could always use this method instead:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:#selector(getBusLocation) userInfo:nil repeats:YES];
This will save you a line, as it will add it to the run loop automatically.
Because, as the docs say:
Timers work in conjunction with run loops. To use a timer effectively,
you should be aware of how run loops operate—see NSRunLoop and
Threading Programming Guide. Note in particular that run loops retain
their timers, so you can release a timer after you have added it to a
run loop.
It is a design decision that Apple made when they wrote the code for NSTimer (and I'm sure they had good reason to do so) and there is nothing we can do to get around it. Is it really that burdensome?
Like #sosborn said, NSTimers depend on NSRunLoops, and since GCD queues create threads that don't have run loops, NSTimer doesn't play well with GCD.
Check out this other StackOverflow question on the matter: Is it safe to schedule and invalidate NSTimers on a GCD serial queue?
To solve that problem, I implemented MSWeakTimer: https://github.com/mindsnacks/MSWeakTimer (and had the implementation checked by a libdispatch engineer at the last WWDC!)
Timer method won't be called since GCD queues create threads that don't have run loops
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(#"Timer method from GCD main queue");
}];
});
However when dispatched on main queue the timer method will be called as it will get added to main threads run loop.
dispatch_async(dispatch_get_main_queue(), ^{
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(#"Timer method from GCD main queue");
}];
});
Adding the timer to the runloop didn't work in my case. I had to create the timer on the main thread. I was doing this thread creation in a MultipeerConnectivity delegate.
dispatch_async(dispatch_get_main_queue(), ^{
self.timer = [NSTimer scheduledTimerWithTimeInterval:self.interval invocation: self.invocation repeats:YES];
});

Resources