A simplified set of methods to demonstrate what's happening:
- (void)timerDidFire {
NSLog(#"fire");
}
- (void)resetTimer:(NSTimer *)timer {
if (timer) [timer invalidate]; // timer = nil; here doesn't change anything
NSLog(#"%#", timer);
timer = [NSTimer ...Interval:1 ... repeats:YES];
}
- (IBAction)pressButton {
[self resetTimer:myTimer];
}
Clearing I'm doing something wrong, but what? Why do I get an extra timer with every press?
Each time the resetTimer: method is called you create a new NSTimer instance. Unfortunately after the execution of this method is finished you lost all your references to the new instance because it was assigned to a local variable.
The timer you create inside the method is not assigned to the myTimer variable. Whatever myTimer is, it is not the new created timer.
You could dump all those local variables and simply use something like this:
- (void)resetTimer {
[myTimer invalidate]; // calls to nil are legal, so no need to check before calling invalidate
NSLog(#"%#", myTimer);
myTimer = [NSTimer ...Interval:1 ... repeats:YES];
}
- (IBAction)pressButton {
[self resetTimer];
}
Related
I am writing a Cocoa UI and am calling NSView's scrollRectToVisible repeatedly in a short space of time as a result of the user holding down a certain key (and hence repeatedly firing events on the main queue).
Through logging I can see that successive keyDown() events are firing prior to the scrollRectToVisible having completed its prior changing of the visibleRect. This is resulting in the subsequent scrollRectToVisible being called with incorrect inputs (namely the wrong starting visibleRect) and is leading to non-sensical UI behaviour. This happens about 50% of the time which I guess is to be expected dealing with an asynchronous problem.
How can I address this?
One way I can think of is by somehow turning scrollRectToVisible into a synchronous call. The problem is that it is an AppKit API and the method returns immediately with a boolean indicating whether it is going to scroll or not.
Try putting the code in the view that responds to the -keyDown:.
This example controls the scrolling with a timer instead of the repeated keyDowns from holding the key.
I couldn't get the UI unwanted behavior, so I don't know if this will help.
- (BOOL)acceptsFirstResponder {
return YES;
}
- (BOOL)canBecomeKeyView {
return YES;
}
- (void)keyDown:(NSEvent *)theEvent {
if (timer == nil) {
timer = [NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:#selector(doScroll:)
userInfo:nil
repeats:YES];
}
NSLog(#"event %# with Timer%#", theEvent, timer);
}
- (void)keyUp:(NSEvent *)theEvent {
[timer invalidate];
timer = nil;
NSLog(#"keyUp timer:%#", timer);
NSLog(#"event %#", theEvent);
}
- (void)doScroll:theTimer {
NSEvent * current = [NSApp currentEvent]; //use this to parse
NSLog(#"current press %#", current);
[self scrollRectToVisible:NSMakeRect(self.visibleRect.origin.x + 10,
self.visibleRect.origin.y + 10,
self.bounds.size.width,
self.bounds.size.height)];
}
My code is given below.I am trying to show a counter on the screen in a window and enable a button after count limit is over.
When I run the code before nstimer and after nstimer are printed only once and nothing is printed from the time countdown function.Someone pelase help me.
- (void)startTimer:(NSInteger)count;
{
NSLog(#"Entered the start timer function");
self.countLimit = count;
[self.lbCount setStringValue:[NSString stringWithFormat:#"%ld",self.countLimit]];
NSLog(#"Before nstimer");
#try{
self.timeCounter = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(timeCountDown) userInfo:nil repeats:YES];
}
#catch (NSException* e) {
NSLog(#"%#",[e description]);
}
NSLog(#"After nstimer");
}
- (void)timeCountDown
{
NSlog(#"Entered the function");
self.countLimit = self.countLimit - 1;
NSLog(#"Just entered the function time countdown");
if (self.countLimit < 0) {
NSLog(#"Entered count limit less than 0");
[self.timeCounter invalidate];
self.timeCounter = nil;
[self.btContinue setEnabled:YES];
return;
}
[self.lbCount setStringValue:[NSString stringWithFormat:#"%ld",self.countLimit]];
}
As detailed in the API docs, your scheduled method must take a NSTimer * parameter:
- (void)timeCountDown:(NSTimer *)timer
Don't forget to change your selector in the scheduledTimerWithTimeInterval call to reflect this:
// There's a colon after selector name to denote
// the method takes one parameter
self.timeCounter = [NSTimer scheduledTimerWithTimeInterval:1
target:self selector:#selector(timeCountDown:) userInfo:nil repeats:YES];
If you don't want to pass as a arguement NSTimer* then use below after your timer
[self.timeCounter fire];
I have a do loop that I want to execute a command every 1 second while a SWITCH is on.
The Code works fine ONCE, when I don't have the DO LOOP.
However, as soon as I add the LOOP, none of the labels in the view controller are updated, the back button for the storyboard doesn't work, and the SWITCH will not toggle off. Essentially, the DO LOOP keeps looping, but nothing on the screen will work, nor can I back out.
I know I'm doing it wrong. But, I don't now what. Any thoughts would be appreciated.
I attached the code that gets me in trouble.
Thanks,
- (IBAction)roaming:(id)sender {
UISwitch *roamingswitch = (UISwitch *)sender;
BOOL isOn = roamingswitch.isOn;
if (isOn) {
last=[NSDate date];
while (isOn)
{
current = [NSDate date];
interval = [current timeIntervalSinceDate:last];
if (interval>10) {
TheCommand.text=#"ON";
[self Combo:sendcommand];
last=current;
}
}
}
else
{
TheCommand.text=#"OFF";
}
}
iOS and OSX are event based systems and you cannot use loops like this in the main (UI) thread to do what you want to do, otherwise you don't allow the run loop to run and events stop being processed.
See: Mac App Programming Guide section "The App’s Main Event Loop Drives Interactions".
What you need to do is set-up a timer (NSTimer) which will fire every second:
.h file:
#interface MyClass : NSView // Or whatever the base class is
{
NSTimer *_timer;
}
#end
.m file:
#implementation MyClass
- (id)initWithFrame:(NSRect)frame // Or whatever the designated initializier is for your class
{
self = [super initInitWithFrame:frame];
if (self != nil)
{
_timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:#selector(timerFired:)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)dealloc
{
[_timer invalidate];
// If using MRR ONLY!
[super dealloc];
}
- (void)timerFired:(NSTimer*)timer
{
if (roamingswitch.isOn)
{
TheCommand.text=#"ON";
[self Combo:sendcommand];
}
}
#end
Give your processor enough time to update your view controller and not be interrupted by other processes. I give it 0.5 second before and after the view controller update signal.
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
self.CrashingTime.text = [NSString stringWithFormat: #"Crash Time = %f ms", outputOfCrashTime];
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
-(void) test{
for(Person *person in persons){
__block CGPoint point;
dispatch_async(dispatch_get_main_queue(), ^{
point = [self.myview personToPoint:person];
});
usePoint(point); // take a long time to run
}
}
I need to run personToPoint() in the main queue to get the point, and usePoint() method doesn't need to run in main queue and take a long time to run. However, when running usePoint(point), point has not been assigned value because using dispatch_async. If using dispatch_sync method, the program will be blocked. the how can I using point after it has been assigned?
UPDATE:
how to implement the pattern of the following code:
-(void) test{
NSMutableArray *points = [NSMutableArray array];
for(Person *person in persons){
__block CGPoint point;
dispatch_async(dispatch_get_main_queue(), ^{
point = [self.myview personToPoint:person];
[points addObject:point];
});
}
usePoint(points); // take a long time to run
}
Something like the following would work. You might also put the entire for loop inside one dispatch_async() and let the main thread dispatch all the usePoint() functions at once.
-(void) test{
for(Person *person in persons){
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint point = [self.myview personToPoint:person];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
usePoint(point); // take a long time to run
});
});
}
}
Solution for Updated question:
You use the same basic pattern as suggested above. That is you dispatch the stuff you need to do on the main thread to the main thread and then nest a dispatch back to a default work queue inside the main thread dispatch. Thus when the main thread finishes its work it will dispatch off the time consuming parts to be done elsewhere.
-(void) test{
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray *points = [NSMutableArray array];
for (Person *person in persons){
CGPoint point = [self.myview personToPoint:person];
[points addObject:[NSValue valueWithCGPoint:point]];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
usePoint(points); // take a long time to run
});
});
}
Note that there was an error in your code as you can't add CGPoint's to an NSArray since they are not objects. You have to wrap them in an NSValue and then unwrap them in usePoint(). I used an extension to NSValue that only works on iOS. On Mac OS X you'd need to replace this with [NSValue valueWithPoint:NSPointToCGPoint(point)].
Is there a way to control the update interval of the location manager? I used the nearest 10 meter setting for accuracy but it appears that my application gets one update every second, which I think is too often. I want the good accuracy but not the frequency.
Thanks.
I did it with a timer:
in the header add:
ServerInterface *serverInterface;
in the .m file:
- (void)viewDidLoad
{
...
[frequentLocationUpdateTimer invalidate];
frequentLocationUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:
#selector(reEnableLocationTick:) userInfo:nil repeats:YES];
...
}
- (void)reEnableLocationTick:(NSTimer *)theTimer
{
[locationGetter startUpdates]; //Maybe you have to change this line according to your code
}
and in your locationmanager...:
- (void)locationManager:(CLLocationManager *)manage didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
...
if (newLocation.horizontalAccuracy < 100)
{
[locationManager stopUpdatingLocation];
}
...
// let our delegate know we're done
[delegate newPhysicalLocation:newLocation];
}
The code simply stops updating the location after reaching a minimum accuracy of 100m. Then, every 30sec a timer reenables the locationmanager again.