I am creating a simple countdown application in Cocoa. When I click the "Start Countdown" button, I get the spinning mouse indicator. After the correct amount of time (for example, if "2" was put in the box, the application would wait 2 seconds), the TextField skips right to 0. Here is my code:
#import "APPAppDelegate.h"
#implementation APPAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
}
- (IBAction)StartCountdown:(id)sender {
int counterNow, counterLater;
counterNow = [_FieldCount intValue];
while (counterNow>1){
counterNow = [_FieldCount intValue];
counterLater = counterNow - 1;
[NSThread sleepForTimeInterval:1.0f];
[_FieldCount setIntValue:counterLater];
}
}
#end
You need to let the run loop run in order for updates to make it to the screen. You're changing the value, then sleeping the thread. A better method would be to have an NSTimer which sets the text of the text field, and then calls -setNeedsDisplay:YES on it. Something like this:
-(void)fireTimer:(NSTimer*)timer
{
counterNow = [_FieldCount intValue];
counterNow--;
[_FieldCount setIntValue:counterNow];
}
This assumes that _FieldCount is an NSTextField. You'd set this up by creating a repeating timer. If you wanted it to repeat once per second, you could do the following:
NSTimer* secondsTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:#selector(fireTimer:) userInfo:nil repeats:YES];
Related
The Problem
I am building an app where I am getting real-time data and updating a MKMapView. I get a batch of data every 10 seconds and between data sets from the webs service I am removing older data points while also adding the new ones.
Instead of updating them all at once I want spread out the animation of the new points I get from the data service over that 10 seconds so I create the 'real-time' feel and avoid as many stops and starts as I can.
Everything seems to be working great except the that the NSTimer is always finishing early... way early. It should loop through the new data over 10 seconds but it will typically finish looping through the new data set 4 to 5 seconds earlier then it should.
I have read through a lot of the Apple documentation and StackOverflow questions (below are two good ones for those that may be looking) :)
https://stackoverflow.com/a/18584973
https://developer.apple.com/library/ios/technotes/tn2169/_index.html#//apple_ref/doc/uid/DTS40013172-CH1-TNTAG8000
But it seems like most of the recommendations are made for gaming apps using CADisplayLink (but I am not building a gaming app) or that if you need to use a high performance timer that it should not be used continuously.
My timer does not need to be exact but if I could even get it within .5 seconds that would be great without having to add the overhead of some of the other options I have seen.
As always any thoughts / code / or directions you could point me would be greatly appreciated.
The Code
Once I collect the new data into arrays I create the time interval and start the timer with the code below
addCount = -1;
timerDelay = 10.0/[timerAdditions count];
delayTimer =[NSTimer scheduledTimerWithTimeInterval:timerDelay target:self selector:#selector(delayMethod) userInfo:nil repeats:YES];
That then fires this method that animates through adding and removing the my map annotations.
-(void) delayMethod {
addCount = addCount +1;
if (addCount >= [timerAdditions count]) {
[timerRemovals removeAllObjects];
[timerAdditions removeAllObjects];
addCount = -1;
[delayTimer invalidate];
delayTimer = nil;
} else {
[myMap addAnnotation:[timerAdditions objectAtIndex:addCount]];
[myMap removeAnnotation:[timerRemovals objectAtIndex:addCount]animated:YES];
}
}
UPDATE
I tried updating my timer through GCD. And what is odd is that the timing loop works every other dataset. Still do not have it working every tie but for some reason it seems to be tied to resetting the dispatch time or the timer interval.
-(void) delayMethod {
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * timerDelay); // How long
dispatch_after(delay, dispatch_get_main_queue(), ^(void){
addCount = addCount +1;
if (addCount >= [timerAdditions count]) {
[timerRemovals removeAllObjects];
[timerAdditions removeAllObjects];
addCount = -1;
//[delayTimer invalidate];
//delayTimer = nil;
} else {
NSLog(#"Delay fired count %i -- additoins %lu",addCount,(unsigned long)[timerAdditions count]);
[myMap addAnnotation:[timerAdditions objectAtIndex:addCount]];
[myMap removeAnnotation:[timerRemovals objectAtIndex:addCount]animated:YES];
[self delayMethod];
}
});
}
I am using timer as like this to do stuff with timer instance for more accurate result.
- (void)createTimer {
// start timer
if(gameTimer == nil)
gameTimer = [NSTimer timerWithTimeInterval:1.00 target:self selector:#selector(timerFired:) userInfo:nil repeats:YES] ;
[[NSRunLoop currentRunLoop] addTimer:gameTimer forMode:NSDefaultRunLoopMode];
timeCount = 725; // instance variable or you can set it as your need
}
- (void)timerFired:(NSTimer *)timer {
// update label
if(timeCount == 0){
[self timerExpired];
} else {
timeCount--;
if(timeCount == 0) {
// display correct dialog with button
[timer invalidate];
[self timerExpired];
}
}
//do your stuff here for particular time.
}
- (void) timerExpired {
// display an alert or something when the timer expires.
NSLog(#"Your time is over");
[gameTimer invalidate];
gameTimer = nil;
//do your stuff for completion of time.
}
in this take time interval you want. and also Increment decrement stuff as you require. And do stuff with timer fired and completed. In your situation if you don not want to expire timer than its ok. never invalidate timer instance and use only timer fired event to do stuff.
I went down a slightly different path thanks to another SO question I have referenced below. Basically by combining the two timers into one, setting that one timer to the fastest time interval I would need and managing the methods I need to at the changing intervals within the method called by the timer I have solved the problem I was seeing.
https://stackoverflow.com/a/25087473/2939977
timeAdder = (10.0/[timerAdditions count]);
timeCountAnimate = timeAdder;
[NSTimer scheduledTimerWithTimeInterval:0.11 target:self selector:#selector(timerFired) userInfo:nil repeats:YES];
- (void)timerFired {
timeCount += 0.111;
if (timeCount > 10.0) {
// call a method to start a new fetch
timeCount = 0.0;
timeCountAnimate =0.0;
[self timerTest];
}
if (timeCount > timeCountAnimate) {
timeCountAnimate += timeAdder;
[self delayMethod];
}
I want to manipulate views while in a for loop. I manipulate a view in the for loop, then the operations for the view are done at once after the for loop has ended. I tried to use other threads like GCD, but I noticed that a view is in the main thread. The operations are back to the main thread and they are put off after the for loop finishes.
What I want to do is update UITextView's text while in the for loop. If I can't operate the for loop in another thread, how can I do that? Are there other ways to do that?
Solution 1: Use a timer
In order to progressively add text to a textview, you can use an NSTimer.
Requirements
in your interface - the following ivars or properties:
UITextView *textView;
NSNumber *currentIndex;
NSTimer *timer;
NSString *stringForTextView;
Assuming the string is created and the textview is set up, you can create a function to create the timer and kick it off:
- (void) updateTextViewButtonPressed
{
timer = [NSTimer scheduledTimerWithTimeInterval:.5
target:self
selector:#selector(addTextToTextView)
userInfo:nil
repeats:YES];
}
- (void) addTextToTextView
{
textView.text = [string substringToIndex:currentIndex.integerValue];
currentIndex = [NSNumber numberWithInt:currentIndex.integerValue + 1];
if(currentIndex.integerValue == string.length)
{
[_timer invalidate];
timer = nil;
}
}
This is a basic working implementation, and you can vary it to pass in the string as userInfo for the timer, if it is not present at the class level. Then you could access it in your addTextToTextView selector with sender.userInfo. You can also adjust the timer interval and how exactly the text is added. I used half a second and character by character concatenation as an example.
Solution 2: Use a loop
Requirements
NSString *string
UITextview *textView
- (void) updateTextViewButtonPressed
{
// perform the actual loop on a background thread, so UI isn't blocked
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^()
{
for (int i = 0; i < string.length; i++)
{
// use main thread to update view
dispatch_async(dispatch_get_main_queue(), ^()
{
textView.text = [string substringToIndex:i];
});
// delay
[NSThread sleepForTimeInterval:.5];
}
});
}
Fistly i'm fairly new to programming in general. And very new to Objective C and IOS programming. This is just a practice programming I'm writing to learn with.
What its basically does is this :
The computer chooses a number of times to attack and then randomly choses to a number between 1 and 4 or 0 and 3 (really doesn't matter). It does this each time it goes through the for loop.
What i'm trying to accomplish is this :
If the computer picks 0 then it highlights the appropriate button and makes it active for the user to interact with, but they only have a certain amount of time to press said button. The timer is calls a function that unhighlights the button and makes in inactive.
All this works but it all happens at the same time. If the computer attacks three times all three buttons are highlighted and activated at the same time and then made inactive at the same time. I want to make the program pause for the length of the timer giving the player that amount of time to push the button. I can't figure this part out. I thought of using a while loop that kicks out when a button is pushed or when the timer calls the function but that just makes it get stuck in the while loop. I have shown this in the first if statement.
Again please keep it simple because i'm newish to programming. Thanks
if(theEnemy.attackingOrBlocking == 1)
{
int whereAttack;
int numberOfAttacks = 3; //theEnemy.numOfAttacks;
for (int i = 0; i <= numberOfAttacks; i++)
{
whereAttack = theEnemy.attackButton;
if (whereAttack == 0)
{
while (buttonPushed == NO)
{
[NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:#selector(timerCallback) userInfo:nil repeats:NO];
lowAttackBlock.userInteractionEnabled = YES;
lowAttackBlock.highlighted = YES;
}
}
if (whereAttack == 1)
{
[NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:#selector(timerCallback) userInfo:nil repeats:NO];
leftAttackBlock.userInteractionEnabled = YES;
leftAttackBlock.highlighted = YES;
}
if (whereAttack == 2)
{
[NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:#selector(timerCallback) userInfo:nil repeats:NO];
rightAttackBlock.userInteractionEnabled = YES;
rightAttackBlock.highlighted = YES;
}
if (whereAttack == 3)
{
[NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:#selector(timerCallback) userInfo:nil repeats:NO];
highAttackBlock.userInteractionEnabled = YES;
highAttackBlock.highlighted = YES;
}
if (myCharacter.block != whereAttack)
{
myCharacter.health -= 10;
[yourHealth setText:[[NSString alloc] initWithFormat:#"%d",myCharacter.health]];
}
}
}
}
Launching a timer is an Asynchrone operation, so it won't stop the normal flow of your application, so if you are in a for loop that launch 3 timer, the for loop will completely execute before the first timer will call you back.
A way do to this would be to cache the number of hit in an instance variable that you would decrease by one each time you are calling a timer.
So you launch a a sequence of 3 attacks, you set the remainingAttack instance variable to 3, call a method performAttack that will check if there is attacks left to do, if there is you perform the attack (or you want to perform it from the timer call back), decrease the number of attack by 1 (or from the timer call back), launch a timer.
In the timer callBack do your logic, then call your performAttack again as the last action of your timer.
That would be the basic logic I can think of right now, you will have to adapt it to your personal need, and there is probably a better solution. but that is one.
theTimer = [NSTimer scheduledTimerWithTimeInterval:1.00 target:self selector:#selector(sendMessageHandler:) userInfo:nil repeats:YES];
//////////////////////////////////////////////////////
- (void)sendMessageHandler (NSTimer *) timer {
}
Ok so sendMessageHandler is triggering every second. But now, I want it to check the value of "theString" and if it changed value from the previous run, do something.
Can someone help me?
Thanks!
Make another string called prevString.
Make them equal each other initially. Then in the timer:
if ([theString isEqualToString:prevString]) {
//No change.
}
else {
//Change happened.
prevString = theString;
}
And for memory management in dealloc:
[theString release];
[prevString release];
Here's the problem: I have some code that goes like this
otherWinController = [[NotificationWindowController alloc] init];
for (int i = 0; i < 10; i++) {
[otherWinController showMessage:[NSString stringWithFormat:#"%d", i]];
NSLog(#"%d", i);
sleep(1);
}
where otherWinController is a subclass of NSWindowController that I am using to update my window as changes happen in code, and the alloc init method simply opens up the nib and shows the window. showMessage is a method that changes an NSTextView to display whatever text is in the parameter.
in the NSLog, the text changes every second and just counts to ten. However for the showMessage method, the text is blank for a full ten seconds and then just displays the number 10. any thoughts??
FTR, the showMessage method is simply
- (void)showMessage:(NSString *)text {
[[self message] setStringValue:text];
}
not that it should matter, that's pretty basic.
You can probably achieve the desired effect right inside your loop, if you explicitly give the run loop some time to run:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 0.1]];
The problem is that you’re blocking the main thread in your for loop and user interface updates happen on the main thread. The main thread run loop will only spin (and consequently user interface updates will take place) after the method containing that for loop finishes executing.
If you want to update that text field every second, you should use a timer. For instance, considering otherWinController is an instance variable, declare a counter property in your class and:
otherWinController = [[NotificationWindowController alloc] init];
self.counter = 0;
[otherWinController showMessage:[NSString stringWithFormat:#"%d", self.counter]];
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(updateCounter:)
userInfo:nil
repeats:YES];
In the same class, implement the method that’s called whenever the time has been fired:
- (void)updateCounter:(NSTimer *)timer {
self.counter = self.counter + 1;
[otherWinController showMessage:[NSString stringWithFormat:#"%d", self.counter]];
if (self.counter == 9) {
[timer invalidate];
// if you want to reset the counter,
// self.counter = 0;
}
}
Views don't get updated until the end of the run loop; your for loop doesn't let the run loop continue, so all the view updates you make are just done after your for loop exits.
You should either use an NSTimer or performSelector:withObject:afterDelay: to change the display in a loop-like fashion.
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(changeTextFieldsString:)
userInfo:nil
repeats:YES];
Then your timer's action will change the view's image:
- (void)changeTextFieldsString:(NSTimer *)tim {
// currStringIdx is an ivar keeping track of our position
if( currStringIdx >= maxStringIdx ){
[tim invalidate];
return;
}
[otherWinController showMessage:[NSString stringWithFormat:#"%d", currStringIdx]]
currStringIdx++;
}
You also generally don't want to use sleep unless you're on a background thread, because it will lock up the rest of the UI; your user won't be able to do anything, and if you sleep long enough, you'll get the spinning beach ball.
I think that by calling sleep(1) you block the main thread, which must draw your changes. So the display is not updated. The task manager will not interrupt your function. You shouldn't use sleep in this case. Please take a look at NSTimer class. It has a static method scheduledTimerWithTimeInterval, which you should use.
static UIViewController *controller = nil;
.....
{
.....
otherWinController = [[NotificationWindowController alloc] init];
controller = otherWinController;
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(timerTick:) userInfo:nil repeats:YES]; //set timer with one second interval
.....
}
- (void) timerTick:(NSTimer*)theTimer {
static int i = 0;
[controller showMessage:[NSString stringWithFormat:#"%d", i]];
NSLog(#"%d", i);
if (++i == 10) {
[theTimer invalidate];
i = 0;
}
}