I've made a RunLoop with a timer that updates a label that displays a countdown. I need the RunLoop to stop once the countdown reaches zero, for the case where the the timer finishes normally I could just use runUntilDate, with the date being the current date + the time on the countdown. The problem is when the user cancels the countdown from a button before it's finished. I don't know how to tell the RunLoop to stop from the cancel button action. Here's the code for the RunLoop:
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector:#selector(updateCountdownLabel:)]];
[invocation setTarget:self];
[invocation setSelector:#selector(updateCountdownLabel:)];
[[NSRunLoop mainRunLoop] addTimer:[NSTimer timerWithTimeInterval:1 invocation:invocation repeats:YES] forMode:NSRunLoopCommonModes];
The method just tells the label to reduce by 1 in each loop.
I could tell the cancel button to change the label to zero, and have the run loop selector check if the value is zero, but could the RunLoop's own selector tell it to stop?
cancelPerformSelector:target:argument:
cancelPerformSelectorsWithTarget:
These are the closest I've found but they don't seem to work from inside the RunLoops own selector, or at least not in any way I've tried them.
Basically I need to have the button tell the RunLoop to stop, or somehow stop the RunLoop from it's own selector.
Thanks.
You haven't made a run loop, you've scheduled a timer to begin on the main run loop.
What you should do is store the NSTimer object that you create as an instance variable before scheduling the timer on the run loop.
In your updateCountdownLabel: method, once your end condition has been satisfied just call -invalidate on your timer instance. This will remove the timer from the run loop, and because you never retained it, it will be released.
I've updated the methods to use a selector-based NSTimer rather than your NSInvocation-based one. This means that the callback method signature is defined as you are expecting. It also avoids the need to store the NSTimer object in an ivar:
- (void)startCountDown
{
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:#selector(updateCountdownLabel:) userInfo:nil repeats:YES]
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)updateCountdownLabel:(NSTImer*)timer
{
if(thingsAreAllDone)
{
[timer invalidate];
}
}
Related
I've made a game in Xcode 6, using several NSTimers for different things, like a scorer timer, countdown timer, and to move my objects around. The problem is that sometimes (it seems like) the NSTimers stop for like half a second which makes it look like it lags. Example: When the character is moving, it stops for a tiny second and then continues to move. It happens so fast, but it is noticable, and it annoys me so much. I want it to be completely smooth. Any help would be appreciated!
A couple of thoughts:
If you're having a small delay in the timer processing, the most likely issue is that you have something blocking the main queue. Take a careful look at your code and see if you can find anything that could block the main queue.
You can actually use Instruments to find places in your app where the thread might be blocked. If I recall correctly, WWDC 2112 video Building Concurrent User Interfaces on iOS shows the trick with Instruments to find where your app is blocked. It's a bit dated, but the techniques for finding where the main thread blocks still apply.
It's unlikely, but you might want to consider checking the run loop modes that your timer is running on. For example, the default:
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(tick:) userInfo:nil repeats:YES];
This can pause during certain animations. You might consider using a broader array of run loop modes, e.g.:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:#selector(tick:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
This results in a timer that is less susceptible to certain delays during particular types of animations. It just depends upon what else your app is doing when you see the delay in the user interface.
When using a timer to update animations, better than a NSTimer is a CADisplayLink. For example, define a few properties:
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval startTime;
Then you can write code to start and stop the display link:
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval elapsed = CACurrentMediaTime() - self.startTime;
// update your UI, not on the basis of "this is called x times per second",
// but rather, on the basis that `elapsed` seconds have passed
}
The key in good animation code is that you don't just assume that your routine will be called at a specified frequency, but rather that you update the UI based upon the number of elapsed seconds. This way, a slow device that drops a few frames and a fast device will yield the same animation, the latter would just be a little smoother than the former.
The merits of display links, though, are discussed briefly in WWDC 2014 video - Building Interruptible and Responsive Interactions. There are other longer discussions of the topic that are eluding me at this point, but this might be a good place to get introduced to the topic (even though the vast majority of that video is on other topics).
You may want to try a high resolution timer, like Timer dispatch sources. It looks a bit scary at first, but actually quite easy to use. Sample code (with comments)
dispatch_source_t CreateDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue , dispatch_block_t block) {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer) {
// Use dispatch_time instead of dispatch_walltime if the interval is small
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
void MyCreateTimer()
{
dispatch_source_t aTimer = CreateDispatchTimer(30 * NSEC_PER_SEC, 1 * NSEC_PER_SEC, dispatch_get_main_queue(), ^{
NSLog(#"Timer fired!");
});
// Keep a reference if you want to, say, stop it somewhere in the future
}
EDIT:
In XCode, if you type dispatch in the editor, it will suggest a snippet called dispatch_source timer - GCD: Dispatch Source (Timer), which will generate the template code for the timer.
I need some help with xcode...
I need to show a value on label with [label1 setIntValue: someInt] wait some secs and do again [label1 setIntValue: otherInt]. I tried with sleep() but the ui stucks and only the second int is shown. What I need to do? Thank you very much!
What you need to do is to set up a timer. After setting the first value on the label, use this:
NSTimer* t = [NSTimer scheduledTimerWithTimeInterval:1 target: self selector:#selector(timerEnded) userInfo: nil repeats:NO];
In this case you're waiting 1 second before triggering timerEnded. So, after this, create the timerEnded method. This is the method that gets called after 1 second.
-(void)timerEnded{
//set value to label
}
I ran the following code expecting to schedule three subsequent calls, at different time intervals (e.g. after 1 sec, after 2.6sec etc..) on the method "displayWarningMessage" but didn't work (it displayed the massage only the first time).
I don't find a method signature in the scheduler that would do the job of displaying it multiple times and with a specific delay. Anyone has some suggestion?
[self scheduleOnce:#selector(displayWarningMessage) delay:0.7f];
[self scheduleOnce:#selector(displayWarningMessage) delay:1.7f];
[self scheduleOnce:#selector(displayWarningMessage) delay:3.7f];
Problem here is, when you call first schedule it is scheduled successfully. But the next immediate call is throwing warning something
CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: X.2 to X.2
you can see this in the log.
What you can do is when the selector is called, at the end of the method you can schedule it again for the next time, until you are done. You may take a counter to keep track of how many times it has been called, put all of your intervals in an array and then schedule next selector for the interval at the specific index identified by counter. like this:
NSArray *intervals = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.7],[NSNumber numberWithFloat:1.7],[NSNumber numberWithFloat:3.7], nil];
int counter = 0;
//schedule it for the first time with object at index counter/index 0
[self scheduleOnce:#selector(displayWarningMessage) delay:[(NSNumber *)[intervals objectAtIndex:counter]] floatValue];
now in your selector, do something like this:
-(void)displayWarningMessage
{
//do all your stuff here
//increment counter
counter ++;
if(counter < [intervals count])
{
//schedule it for the next time with object at index counter/index
[self scheduleOnce:#selector(displayWarningMessage) delay:[(NSNumber *)[intervals objectAtIndex:counter]] floatValue];
}
}
intervals and counter should be class ivars of-course.
Try this:
- (void)displayWarningMessage {
//Stuff
}
- (void)callStuff {
CCCallFunc *call = [CCCallFunc actionWithTarget:self selector:#selector(displayWarningMessage)];
CCDelayTime *delay1 = [CCDelayTime actionWithDuration:0.7f];
CCDelayTime *delay2 = [CCDelayTime actionWithDuration:1.7f];
CCDelayTime *delay3 = [CCDelayTime actionWithDuration:3.7f];
CCSequence *actionToRun = [CCSequence actions:delay1, call, delay2, call, delay3, call, nil];
[self runAction:actionToRun];
}
That should work for what you're trying to do, at least that's how I'd imagine doing it. I'm fairly sure you can call that CCCallFunc multiple times in one CCSequence without having to create it three individual times. You could also make those delays variable based if need be, of course. Let me know how it goes.
Method is created.
[self schedule: #selector(displayWarningMessage:) interval:3.2f];
-(void) displayWarningMessage:(ccTime) delta
{
CCLOG(#"alert........!!!!!!");
}
Use the Calling method in not warning message detected.
Compiling in XCode 3.1.1 for OSX 10.5.8 target, 32-bit and i386 build.
I have a modal run loop, running in NSWindow wloop and NSView vloop. The modal loop is started first. It starts, runs and stops as expected. Here's the start:
[NSApp runModalForWindow: wloop];
Then, when the user presses the left mouse button, I do this:
if (ticking == 0) // ticking is set to zero in its definition, so starts that way
{
ticking = 1; // don't want to do this more than once per loop
tickCounter = 0;
cuckCoo = [NSTimer scheduledTimerWithTimeInterval: 1.0f / 10.0f // 10x per second
target: self // method is here in masterView
selector: #selector(onTick:) // method
userInfo: nil // not used
repeats: YES]; // should repeat
}
Checking the return of the call, I do get a timer object, and can confirm that the timer call is made when I expect it to be.
Now, according to the docs, the resulting NSTimer, stored globally as "cuckCoo", should be added to the current run loop automagically. The current run loop is definitely the modal one - at this time other windows are locked out and only the window with the intended mouse action is taking messages.
The method that this calls, "onTick", is very simple (because I can't get it to fire), located in the vloop NSView code, which is where all of this is going on:
- (void) onTick:(NSTimer*)theTimer
{
tickCounter += 1;
NSLog(#"Timer started");
}
Then when it's time to stop the modal loop (which works fine, btw), I do this:
[cuckCoo invalidate];
[NSApp stop: nil];
ticking=0;
cuckCoo = NULL;
NSLog(#"tickCounter=%ld",tickCounter);
ticking and tickCounter are both global longs.
I don't get the NSLog message from within onTick, and tickCounter remains at zero as reported by the NSLog at the close of the runloop.
All this compiles and runs fine. I just never get any ticks. I'm at a complete loss. Any ideas, anyone?
The problem is related to this statement "The current run loop is definitely the modal one". In Cocoa, each thread has at most one runloop, and each runloop can be run in a variety of "modes". Typical modes are default, event tracking, and modal. Default is the mode the loop normally runs in, while event tracking is typically used to track a drag session of the mouse, and modal is used for things like modal panels.
When you invoke -[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:] it does schedule the timer immediately, but it only schedules it for the default runloop mode, not the modal runloop mode. The idea behind this is that the app generally shouldn't continue to run behind a modal panel.
To create a timer that fires during a modal runloop, you can use -[NSTimer initWithFireDate:interval:target:selector:userInfo:repeats:] and then -[NSRunLoop addTimer:forMode:].
The answer specific to...
[NSApp runModalForWindow: wloop];
...is, after the modal run loop has been entered:
NSRunLoop *crl;
cuckCoo = [NSTimer timerWithTimeInterval: 1.0 / 10
target: self
selector: #selector(onTick:)
userInfo: nil
repeats:YES];
crl = [NSRunLoop currentRunLoop];
[crl addTimer: cuckCoo forMode: NSModalPanelRunLoopMode];
(crl obtained separately for clarity) Where the onTick method has the form:
- (void) onTick:(NSTimer*)theTimer
{
// do something tick-tocky
}
Note: It's probably worth scrolling down to read my edit.
I'm trying to setup an NSTimer in a separate thread so that it continues to fire when users interact with the UI of my application. This seems to work, but Leaks reports a number of issues - and I believe I've narrowed it down to my timer code.
Currently what's happening is that updateTimer tries to access an NSArrayController (timersController) which is bound to an NSTableView in my applications interface. From there, I grab the first selected row and alter its timeSpent column. Note: the contents of timersController is a collection of managed objects generated via Core Data.
From reading around, I believe what I should be trying to do is execute the updateTimer function on the main thread, rather than in my timers secondary thread.
I'm posting here in the hopes that someone with more experience can tell me if that's the only thing I'm doing wrong. Having read Apple's documentation on Threading, I've found it an overwhelmingly large subject area.
NSThread *timerThread = [[[NSThread alloc] initWithTarget:self selector:#selector(startTimerThread) object:nil] autorelease];
[timerThread start];
-(void)startTimerThread
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
activeTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(updateTimer:) userInfo:nil repeats:YES] retain];
[runLoop run];
[pool release];
}
-(void)updateTimer:(NSTimer *)timer
{
NSArray *selectedTimers = [timersController selectedObjects];
id selectedTimer = [selectedTimers objectAtIndex:0];
NSNumber *currentTimeSpent = [selectedTimer timeSpent];
[selectedTimer setValue:[NSNumber numberWithInt:[currentTimeSpent intValue]+1] forKey:#"timeSpent"];
}
-(void)stopTimer
{
[activeTimer invalidate];
[activeTimer release];
}
UPDATE
I'm still totally lost with regards to this leak. I know I'm obviously doing something wrong, but I've stripped my application down to its bare bones and still can't seem to find it. For simplicities sake, I've uploaded my applications controller code to: a small pastebin. Note that I've now removed the timer thread code and instead opted to run the timer in a separate runloop (as suggested here).
If I set the Leaks Call Tree to hide both Missing Symbols and System Libraries, I'm shown the following output:
EDIT: Links to screenshots broken and therefor removed.
If the only reason you are spawning a new thread is to allow your timer to run while the user is interacting with the UI you can just add it in different runloop modes:
NSTimer *uiTimer = [NSTimer timerWithTimeInterval:(1.0 / 5.0) target:self selector:#selector(uiTimerFired:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:uiTimer forMode:NSRunLoopCommonModes];
As an addendum to this answer it is now possible to schedule timers using Grand Central Dispatch and blocks:
// Update the UI 5 times per second on the main queue
// Keep a strong reference to _timer in ARC
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, (1.0 / 5.0) * NSEC_PER_SEC, 0.25 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
// Perform a periodic action
});
// Start the timer
dispatch_resume(_timer);
Later when the timer is no longer needed:
dispatch_source_cancel(_timer);