I have a game where is uses a countdown timer and when the timer is up, you are brougt to a Game over view. I want to add a feature were if they tap a button, it will add like 1, 2 or 3 more seconds to the timer. I already have the code for the timer (Below), but i just need to know how to add more time to the counter. I thought of it and i have to say when the views will switch and it would need to take into a count the added time.
Code:
-(IBAction)Ready:(id)sender {
[self performSelector:#selector(TimerDelay) withObject:nil afterDelay:1.0];
[self performSelector:#selector(delay) withObject:nil afterDelay:36.5];
}
-(void)TimerDelay {
MainInt = 36;
timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(countDownDuration)
userInfo:nil
repeats:YES];
if (timer == 0)
{
[timer invalidate];
}
}
-(void)countDownDuration {
MainInt -= 1;
seconds.text = [NSString stringWithFormat:#"%i", MainInt];
}
-(void)delay {
GameOverView1_4inch *second= [self.storyboard instantiateViewControllerWithIdentifier:#"GameOverView1"];
second.finalScore = self.currentScore;
[self presentViewController:second animated:YES completion:nil];
}
If you're using a timer to manage the game, using a perform selector at the same time (to end the game or anything else) kind of defeats the point and makes the management very complex. Choose one route and stick with it.
When the timer is running, you can change it's fire date using setFireDate:. So, you could get the current fire date, add your time to it and then set the new fire date:
- (void)extendByTime:(NSInteger)seconds
{
NSDate *newFireDate = [[self.timer fireDate] dateByAddingTimeInterval:seconds];
[self.timer setFireDate:newFireDate];
}
Then, your button callbacks are something like:
- (void)buttonOnePressed:(id)sender
{
[self extendByTime:1];
}
- (void)buttonFivePressed:(id)sender
{
[self extendByTime:5];
}
Once you've removed the performSelector which calls delay your game end will be defined by
MainInt reaching zero.
As an aside, don't do this:
if (timer == 0)
The correct approach is:
if (timer == nil)
And if the timer is nil, there's no point in trying to invalidate it...
Also a good idea to take a look at the Objective-C naming guidelines.
Based on your recent comment, it seems that you actually want the timer to continue counting at a second interval, but to add time only to the number of seconds remaining. That's even easier and doesn't require any change to the timer fire date.
- (void)extendByTime:(NSInteger)seconds {
MainInt += seconds;
seconds.text = [NSString stringWithFormat:#"%i", MainInt];
}
And you need to add a check in 'countDownDuration':
if (MainInt <= 0) {
[timer invalidate];
[self delay];
}
To determine when you're done.
You can keep reference of the time you start the timer. Then, when you want to add extra time, calculate how much time has passed since the timer started, invalidate the timer and create a new one passing as time interval the difference between the time left of the previous timer and the extra seconds.
Hy, try to use this:
Put this in -(void)viewDidLoad method
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self
selector:#selector(countDownTimer) userInfo:nil repeats:YES];
then create the -(void)countDownTimer method
- (void)countDownTimer {
// my method which returns the differences between two dates in my case
double diff = [self getDateDifference];
double days = trunc(diff / (60 * 60 * 24));
double seconds = fmod(diff, 60.0);
double minutes = fmod(trunc(diff / 60.0), 60.0);
double hours = fmodf(trunc(diff / 3600.0), 24);
if(diff > 0) {
NSString *countDownString = [NSString stringWithFormat:#"%02.0f day(s)\n%02.0f:%02.0f:%02.0f",
days, hours, minutes, seconds];
// IBOutlet label, added in .h
self.labelCountDown.text= countDownString;
} else {
// stoping the timer
[timer invalidate];
timer = nil;
// do something after countdown ...
}
}
and you can add a - (double)getDateDifference method which returns the difference between two dates in my case
- (double)getDateDifference {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
NSDate *dateFromString = [[NSDate alloc] init];
NSDate *now = [NSDate date];
NSString *myDateString = #"2013-10-10 10:10:10"; // my initial date with time
// if you want to use only time, than delete the
// date in myDateString and setDateFormat:#"HH:mm:ss"
// this line is not required, I used it, because I need GMT+2
[dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:+2]];
[dateFormatter setDateFormat:#"yyyy-MM-dd HH:mm:ss"];
// get the date
dateFromString = [dateFormatter dateFromString:myDateString];
// this line is also not required, I used it because I need GMT+2
// so I added two hours in seconds to 'now'
now = [now dateByAddingTimeInterval:60*60*2];
// getting the difference
double diff = [dateFromString timeIntervalSinceDate:now];
NSLog(#"dateString: %#", dateString);
NSLog(#"now: %#", now);
NSLog(#"targetDate: %#", dateFromString);
NSLog(#"diff: %f", diff);
return diff;
}
the output is similar to this
dateString: 2013-10-10 00:20:00
now: 2013-10-10 00:20:00 +0000
target: 2013-10-10 00:20:28 +0000
diff: 28.382786
I hope it was helpfull
Here's a library that you can use.
https://github.com/akmarinov/AMClock
Related
I have a label that displays the time; however, the time is not updated. The time is displayed, but it does not count up. The time at which the button was pressed is displayed and that time does not change. Here is my code
- (IBAction)startCamera:(id)sender
{
[self.videoCamera start];
NSDate *today = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"HH:mm:ss"];
NSString *currentTime = [dateFormatter stringFromDate:today];
[dateFormatter setDateFormat:#"dd.MM.yyyy"];
NSString *currentDate = [dateFormatter stringFromDate:today];
for (int i = 1; i <= 10; i--) {
Label1.text = [NSString stringWithFormat:#"%#", currentTime];
Label2.text = [NSString stringWithFormat:#"%#", currentDate];
}
}
I tried a for loop but that does not update the time. Any suggestions?
UI updates are performed using an event loop that runs on the main thread. Your for-loop is hogging the main thread and never returns from you start function. Whatever you set in labelx.text never gets refreshed on screen because the run loop is waiting for your start function to finish.
You should read up on NSTimer to implement this using best practices.
There is also a way to do this using a delayed dispatch:
(sorry that this is in Swift, I don't know objective-C, but I'm sure you'll get the idea)
// add this function and call it in your start function
func updateTime()
{
// update label1 and label2 here
// also add an exit condition to eventually stop
let waitTime = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC ) // one second wait duration
dispatch_after(waitTime, dispatch_get_main_queue(), {self.updateTime() }) // updateTime() calls itself every 1 second
}
NSTimer works but its not very accurate.
When I need accurate timers I use CADisplaylink, especially when working with animations. This reduces visual stutter.
Using the display refresh is accurate and reliable. However you don't want to be doing heavy calculations using this method.
- (void)startUpdateLoop {
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(update:)];
displayLink.frameInterval = 60;
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)update {
// set you label text here.
}
I have a camera app where I am trying to limit the capture length to exactly 15 seconds.
I have tried two different approaches, and neither of them are working to my satisfaction.
The first approach is to fire a repeating timer every second:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(countTime:) userInfo:[NSDate date] repeats:YES];
- (void)countTime:(NSTimer*)sender {
NSDate *start = sender.userInfo;
NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:start];
NSInteger time = round(duration);
if (time > 15) {
[self capture:nil]; // this stops capture
}
}
this gives me a 15 second video 8/10 times, with a periodic 16 second one... and I have tried a mixture of the NSTimeInterval double and the rounded integer here, with no apparent difference...
The second approach is to fire a selector once after the desired duration, like so:
self.timer = [NSTimer scheduledTimerWithTimeInterval:15.0f target:self selector:#selector(capture:) userInfo:nil repeats:NO];
this just calls the capture method - which stops camera capture - directly, and gives me the same results...
Is there something that I am overlooking here?
Now, because I have tested with a number of tweaked floating point values as the cap (14.5, 15.0, 15.1, 15.5, 16.0 etc) and I almost always see a 16 second video after a few tries, I am starting to wonder whether it's just the AVFoundation taking a second to flush the buffer... ???
NSTimer is not guaranteed to fire when you want it to, just after you want it to fire:
From Apple's docs:
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time. See also Timer Tolerance.
But to answer your question, I used to work for a company that had a max 15 seconds video. I didn't write the video code but I think we used AVComposition after the fact to ensure that the video was no more than 15 seconds. And even then it could be a frame shorter sometimes. See How do I use AVFoundation to crop a video
Thanks to Paul and Linuxious for their comments and answers... and Rory for thinking outside the box (intriguing option).
And yes, in the end it is clear that NSTimer isn't sufficient by itself for this.
In the end, I listen for the captureOutput delegate method to fire, test for the length of the asset, and trim the composition appropriately.
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
fromConnections:(NSArray *)connections error:(NSError *)error
{
_isRecording = NO;
AVURLAsset *videoAsset = [AVURLAsset assetWithURL:outputFileURL];
CMTime length = [videoAsset duration];
CMTimeShow(length);
if(CMTimeGetSeconds(length) > 15)
{
NSLog(#"Capture Longer Than 15 Seconds - Attempting to Trim");
Float64 preferredDuration = 15;
int32_t preferredTimeScale = 30;
CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, CMTimeMakeWithSeconds(preferredDuration, preferredTimeScale));
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:videoAsset presetName:AVAssetExportPresetHighestQuality];
exportSession.outputURL = outputFileURL;
exportSession.outputFileType = AVFileTypeQuickTimeMovie;
exportSession.timeRange = timeRange;
NSError *err = nil;
[[NSFileManager defaultManager] removeItemAtURL:outputFileURL error:&err];
if (err) {
NSLog(#"Error deleting File: %#", [err localizedDescription]);
}
else {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
if (exportSession.status == AVAssetExportSessionStatusCompleted) {
NSLog(#"Export Completed - Passing URL to Delegate");
if ([self.delegate respondsToSelector:#selector(didFinishRecordingToOutputFileAtURL:error:)]) {
[self.delegate didFinishRecordingToOutputFileAtURL:outputFileURL error:error];
}
}
else if(exportSession.status == AVAssetExportSessionStatusFailed) {
NSLog(#"Export Error: %#", [exportSession.error localizedDescription]);
if ([self.delegate respondsToSelector:#selector(didFinishRecordingToOutputFileAtURL:error:)]) {
[self.delegate didFinishRecordingToOutputFileAtURL:outputFileURL error:exportSession.error ];
}
}
}];
}
}
}
I use zxing to scan barcodes. But the camera scans real quick ,so that my method gets overloaded with the result. How to slow down it or create a delay to scan the barcodes?
Here is my result method:
- (void)captureResult:(ZXCapture *)capture result:(ZXResult *)result {
if (!result) return;
// We got a result. Display information about the result onscreen.
NSString *formatString = [self barcodeFormatToString:result.barcodeFormat];
NSString *display = [NSString stringWithFormat:#"Scanned!\n\nFormat: %#\n\nContents:\n%#", formatString, result.text];
[self.decodedLabel performSelectorOnMainThread:#selector(setText:) withObject:display waitUntilDone:YES];
// Vibrate
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
You could record an NSTimeInterval and reject all results for the next 'x' seconds. Example of detecting at most once every half second:
- (void)captureResult:(ZXCapture *)capture result:(ZXResult *)result {
if ([[NSDate date] timeIntervalSince1970] < _nextUpdateTime) {
return;
}
_nextUpdateTime = [[NSDate date] timeIntervalSince1970] + 0.5;
// remainder of function.
}
I would suggest you to use sleep function. Try to use sleep(timeInSeconds) ,so it will delay the scanner by the seconds you enter.
I have an app where content is displayed to the user. I now want to find out how many seconds a user actually views that content for. So in my header file, I've declared an
NSDate *startTime;
NSDate *endTime;
Then in my viewWillAppear
startTime = [NSDate date];
Then in my viewWillDisappear
endTime = [NSDate date];
NSTimeInterval secs = [endTime timeIntervalSinceDate:startTime];
NSLog(#"Seconds --------> %f", secs);
However, the app crashes, with different errors sometimes. Sometimes it's a memory leak, sometimes it's a problem with the NSTimeInterval, and sometimes it crashes after going back to the content for a second time.
Any ideas on to fix this?
since you are not using ARC, when you write
startTime = [NSDate date];
you do not retain startTime, so it is deallocated before -viewWillDisappear is called. Try
startTime = [[NSDate date] retain];
Also, I recommend to use ARC. There should be much less errors with memory management with it, than without it
You should declare a property with retain for the start date. Your date is getting released before you can calculate the time difference.
So declare
#property (nonatomic, retain) NSDate *startDate
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setStartDate: [NSDate date]];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
NSLog(#"Seconds --------> %f",[[NSDate date] timeIntervalSinceDate: self.startDate]);
}
Don't forget to cleanup.
- (void)dealloc
{
[self.startDate release];
[super dealloc];
}
Trying to follow the best practices of ReactiveCocoa to update my UI on the hour, every hour. This is what I've got:
NSDateComponents *components = [[[NSCalendar sharedCalendar] calendar] components:NSMinuteCalendarUnit fromDate:[NSDate date]];
// Generalization, I know (not every hour has 60 minutes, but bear with me).
NSInteger minutesToNextHour = 60 - components.minute;
RACSubject *updateEventSignal = [RACSubject subject];
[updateEventSignal subscribeNext:^(NSDate *now) {
// Update some UI
}];
[[[RACSignal interval:(60 * minutesToNextHour)] take:1] subscribeNext:^(id x) {
[updateEventSignal sendNext:x];
[[RACSignal interval:3600] subscribeNext:^(id x) {
[updateEventSignal sendNext:x];
}];
}];
This has some obvious flaws: manual subscription and sending, and it just "feels wrong." Any ideas on how to make this more "reactive"?
You can do this using completely vanilla operators. It's just a matter of chaining the two intervals together while still passing through both of their values, which is exactly what -concat: does.
I would rewrite the subject as follows:
RACSignal *updateEventSignal = [[[RACSignal
interval:(60 * minutesToNextHour)]
take:1]
concat:[RACSignal interval:3600]];
This may not give you super ultra exact precision (because there might be a minuscule hiccup between the two signals), but it should be Good Enough™ for any UI work.
Sounds like you need something like +interval:startingIn:.
With that thought, you could make your own version of +interval:startingIn: by slightly tweaking the implementation of +interval:.
+ (RACSignal *)interval:(NSTimeInterval)interval startingIn:(NSTimeInterval)delay {
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
int64_t intervalInNanoSecs = (int64_t)(interval * NSEC_PER_SEC);
int64_t delayInNanoSecs = (int64_t)(delay * NSEC_PER_SEC);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, delayInNanoSecs), (uint64_t)intervalInNanoSecs, (uint64_t)0);
dispatch_source_set_event_handler(timer, ^{
[subscriber sendNext:[NSDate date]];
});
dispatch_resume(timer);
return [RACDisposable disposableWithBlock:^{
dispatch_source_cancel(timer);
dispatch_release(timer);
}];
}] setNameWithFormat:#"+interval: %f startingIn: %f", (double)interval, (double)delay];
}
With this in place, your code could be refactored to:
NSDateComponents *components = [[[NSCalendar sharedCalendar] calendar] components:NSMinuteCalendarUnit fromDate:[NSDate date]];
// Generalization, I know (not every hour has 60 minutes, but bear with me).
NSInteger minutesToNextHour = 60 - components.minute;
RACSubject *updateEventSignal = [[RACSignal interval:3600 startingIn:(minutesToNextHour * 60)];
[updateEventSignal subscribeNext:^(NSDate *now) {
// Update some UI
}];