I am creating a game in Spritekit, and I am trying to set up my game in a way that when a player loses all of their lives they have to wait 30 minutes for one of their lives to be restored so that they can play again. I tried using NSTimer to do this but I figured UINotification will be more effective since I want this timer to run whether or not the app is terminated, in the background, being used or not being used. I'm having problems setting this up though.
I have the following code written thus far, when the user reaches the GameOverScene
-(instancetype)initWithSize:(CGSize)size {
if (GameLives < 5 ) {
alarm = [[UILocalNotification alloc] init];
alarm.fireDate = [NSDate dateWithTimeIntervalSinceNow:thirtyNewMinutes];
alarm.timeZone = [NSTimeZone localTimeZone];
[[UIApplication sharedApplication] scheduleLocalNotification:alarm];
alarm.repeatInterval = NSCalendarUnitHour;
NSLog(#"AlarmFireDate = %#", alarm.fireDate);
}
}
The alarm.firedate shows up correctly in the NSLog when I reach the GameOverScene but when I close down my app and restart it, it shows up as null in my view controllers and never fires. How do I get my app to automatically update the user's lives in the background once the notification is scheduled regardless of whether the user is using the app or not? Should it be run in my app delegate?
Should some type of NSDate comparison like the one below run somewhere?
if ([[NSDate date] compare:allarm.fireDate] == NSOrderedDescending) {
GameLives = GameLives + 1;
NSLog(#"SUCCESS");
NSLog(#"COUNT = %lu", (long)GameLives);
}
else if ([[NSDate date] compare:allarm.fireDate] == NSOrderedAscending){
GameLives = GameLives + 1;
NSLog(#"FAILURE");
NSLog(#"COUNT = %lu", (long)GameLives);
}
else if ([[NSDate date] compare:allarm.fireDate] == NSOrderedSame){
NSLog(#"SAME");
NSLog(#"COUNT = %lu", (long)GameLives);
}
I'd be most grateful to anybody that can offer help.
EDIT: RESPONSE TO THE ANSWERS BELOW
I wrote the following code for the NSTimer and the timer starts when the game reaches the GameOver Scene.
-(void)restoreLives{
thirtyNewMinutes = 60 * 30;
update = [NSDate dateWithTimeIntervalSinceNow:thirtyNewMinutes];
if ([[NSDate date] compare:update] == NSOrderedDescending) {
NSLog(#"date1 is later than date2");
NSLog(#"SUCCESS");
NSLog(#"CurrentDate: %#", [NSDate date]);
// LifeText = #"Restored";
GameLives = GameLives + 1;
NSLog(#"LIVES = %ld", (long)GameLives);
// NSLog(#"Level 2 HighScore, %d", _Level1HighScoreNumber);
} else if ([[NSDate date] compare:update] == NSOrderedAscending) {
NSLog(#"date1 is earlier than date2");
NSLog(#"FAILURE");
NSLog(#"CurrentDate: %#", [NSDate date]);
NSLog(#"LIVES = %ld", (long)GameLives);
// Lives = 5;
// NSLog(#"dates are the same");
}
if (GameLives < 4){
[LifeTimer invalidate];
}
And then I created an NSTimer to run the method.
-(void)CheckTime{
LifeTimer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:#selector(restoreLives) userInfo:nil repeats:YES];
}
How would I get it to save the target time that you're speaking of?
And, hopefully I'm not overthinking this but from another perspective if I wanted to compare the current NSDate with the [NSDate dateWithTimeIntervalSinceNow:thirtyNewMinutes]; wouldn't I need to save the original date of [NSDate dateWithTimeIntervalSinceNow:thirtyNewMinutes]; when it was originally called so that if the app terminates and the timer runs the code again it compares it to the original time the code was called and doesn't reset the NSDate and compare it to 30 minutes from the time the user restarts the app and the timer begins again.
i.e. 7:15
NSDate comparison to update = [NSDate dateWithTimeIntervalSinceNow:thirtyMinutes];
is called. And timer is set to update lives at 7:45.
7:30 User terminates their app and restarts it at 7:35
When the NSTimer runs again won't it reset the time to be 30 minutes from 7:35 since it's 30 minutes from now? If this is the case how would I go about saving the original date? Please let me know, keep in mind I'm still a beginner with Objective C
A local notification works well if you want to inform the user of something, and can have a payload which you could use to keep track of the information you need, but it's probably not the best solution for you to do your timing work. If the user disables notifications for your app, it would break your functionality.
Instead, when it comes to keeping track of events based on a time, it's best to rely on date comparisons along with timers.
While your app is open, you should use an NSTimer to trigger what you need to do, which I think you have covered.
When you app goes to the background or terminates you should save the target time in some kind of persistent storage (NSUserDefaults, for example). When you app is relaunched or returns from the background, you should compare against that date and either start up the timer or trigger your code that the timer would fire yourself.
Try this to save/restore the date:
// Save the date with the key "NextFireDate"
[[NSUserDefaults standardUserDefaults] setObject:nextFireDate forKey:#"NextFireDate"];
// This forces the values to be saved by the system, which normally only happens periodically
[NSUserDefaults standardUserDefaults] synchronize];
...
// Retrieve the date with the key "NextFireDate"
NSDate *nextFireDate = [[NSUserDefaults standardUserDefaults] objectForKey:#"NextFireDate"];
You'd call the first whenever you go to the background/terminate (also invalidate your current timer) and the second when you finish launching or return from the background (and start a new timer with the retrieved date). NSUserDefaults is basically just a dictionary (that can accept scalars without having to box them yourself) that persists as long as your app is installed.
Related
I'm working with an app that processes device motion events and updates interface in 5 second increments. I would like to add an indicator to the app that would display the total time the app has been running. It seems that a stopwatch-like counter, like the native iOS Clock app is a reasonable way to count time that the app has been running and display it to the user.
What I'm not sure of is the technical implementation of such a stopwatch. Here's what I'm thinking:
if I know how long between interface updates, I can add up seconds between events and keep a count of seconds as a local variable. Alternatively, a 0.5 second interval scheduled timer can provide the count.
If I know the start date of the app, I can convert the local variable to date for each interface update using [[NSDate dateWithTimeInterval:(NSTimeInterval) sinceDate:(NSDate *)]
I can use a NSDateFormatter with a short time style to convert the updated date to a string using stringFromDate method
The resulting string can be assigned to a label in the interface.
The result is that the stopwatch is updated for each "tick" of the app.
It appears to me that this implementation is a bit too heavy and is not quite as fluid as the stopwatch app. Is there a better, more interactive way to count up time that the app has been running? Maybe there's something already provided by iOS for this purpose?
If you look in the iAd sample code from Apple in the basic banner project they have a simple timer:
NSTimer *_timer;
_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(timerTick:) userInfo:nil repeats:YES];
and the the method they have
- (void)timerTick:(NSTimer *)timer
{
// Timers are not guaranteed to tick at the nominal rate specified, so this isn't technically accurate.
// However, this is just an example to demonstrate how to stop some ongoing activity, so we can live with that inaccuracy.
_ticks += 0.1;
double seconds = fmod(_ticks, 60.0);
double minutes = fmod(trunc(_ticks / 60.0), 60.0);
double hours = trunc(_ticks / 3600.0);
self.timerLabel.text = [NSString stringWithFormat:#"%02.0f:%02.0f:%04.1f", hours, minutes, seconds];
}
It just runs from start up, pretty basic.
Almost what #terry lewis suggested but with an algorithm tweak:
1) schedule a timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(timerTick:) userInfo:nil repeats:YES];
2) when the timer fires, get the current time (that's the tweak, don't count ticks because if there is wobble in the timer, tick counting will accumulate the error), then update the UI. Also, NSDateFormatter is a simpler and more versatile way to format time for display.
- (void)timerTick:(NSTimer *)timer {
NSDate *now = [NSDate date];
static NSDateFormatter *dateFormatter;
if (!dateFormatter) {
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = #"h:mm:ss a"; // very simple format "8:47:22 AM"
}
self.myTimerLabel.text = [dateFormatter stringFromDate:now];
}
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 decided for my first application i would create an alarm application. So far i understand to create two NSDate objects and compare them with isEqualToDate and UILocalNotifications for notifications. How do i continually compare the set date to the current date. Do i create a loop, or is there a more efficient way on Objective C? How do i continually check in the background?
[very new to objective c, sorry and thank you]
This what iv'e started:
NSDate * now = [NSDate date];
NSDate * later = [[NSDate alloc] initWithString:#"2001-03-24 10:45:32 +0600"];
NSComparisonResult result = [later compare:mile];
NSLog(#"%#", later);
NSLog(#"%#", mile);
You don't need to create loops or compare dates yourself. One way to have your alarm sound could be via a local notification. A quick sample code to schedule yourself a local notification:
// Initialize your notification
NSUserNotification *notification = [[NSUserNotification alloc] init];
// Set the title of your notification
[notification setTitle:#"Alarm finished!"];
// Set the text of your notification
[notification setInformativeText:#"My Text"];
// Set the time and date on which the notification will be delivered (in this case 60 seconds after the current time)
[notification setDeliveryDate:[NSDate dateWithTimeInterval:60 sinceDate:[NSDate date]]];
// Set the sound, this can be either nil for no sound, NSUserNotificationDefaultSoundName for the default sound)
[notification setSoundName:NSUserNotificationDefaultSoundName];
// Schedule the notification
[[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];
If you don't want to use a local notification but want to execute something different once your alarm finishes, you could use NSTimer to execute your action once the alarm fires.
I'm developing a calendar app which saves and displays various shift rotations. I've been banging by head against the wall the last few days after recent unexplained errors that strangely coincided with an X-Code update to ver. 4.5.2. I couldn't find any similar issues anywhere on Google or Stackoveflow.
The shift rotations are set and saved by the user. The process of creating a shift rotation involves presenting a user with a range of dates, and the user associating each date with a particular shift.
I'm using Core Data with a sqlite store to manage the saved data. The object graph for the above elements involve several "Shift" NSManagedObject entities which contain the details of a shift, such as title, start and end times, and most importantly a date which allows the shift rotation to be displayed later. The "Shift" entities are kept together by another NSManagedObject called "ShiftRotation" which has a many-to-one relationship to the "Shifts".
My app was working properly until recently, when I made few changes to how I was using these "Shift" objects.
My problem is this. The user associates a "Shift" to a date. The date is stored inside a "date" property within the "Shift" NSManagedObject. The context is saved. When I access the "Shift" later, without making any changes, the date that was assigned to the "Shift" appears changed.
I went through my code line by line and added numerous NSLogs to see where or why it's changing, with no results. I noticed that there is no change to the date immediately before AND after I save the context within the class where the save is occurring. There's also no change when I access it from the main Calendar screen. However when I later retrieve the "Shifts" from another screen where I manage all shift rotations, they appear to have changed.
The date change is approximately 31 to 32 days in the future. I can't help but wonder if this is a bug in Core Data or I'm a missing something.
Any explanation or reason why accessing a date from a sqlite persistent store causes a significant and unpredictable date change?
Update: I tried to further narrow down what was happening and where it was happening by using NSNotificationCenter.
I'm using iOS 6.
I registered the root view controller to observe any changes to the managed object context. Particularly any changes to the Objects (i.e. NSManagedObjectContextObjectDidChange).
After I updated the properties of the "Shift" NSManagedObject I receive a notification indicating that the "Shift" object was updated. Here's some code and logs.
This is where the "Shift" entity is created and given a date:
- (NSMutableArray *)arrayWithDateStartingAtDate:(NSDate *)anyDate forNumberOfDays:(NSUInteger)days;
{
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSMutableArray *monthArray = [[NSMutableArray alloc] init];
NSDateComponents *comps = [[NSDateComponents alloc] init];
NSDate *tempDate;
NSManagedObjectContext *moc = [[CSEventStore sharedStore] managedObjectContext];
for (NSUInteger i = 0; i < days; i++) {
[comps setDay:i];
tempDate = [gregorian dateByAddingComponents:comps toDate:anyDate options:0];
Shift *shiftRDO = [Shift initShiftEntityRDOWithDate:tempDate InContext:moc];
[shiftRDO setShiftRotation:_shiftRotation];
NSMutableDictionary *dateDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:tempDate, #"date", shiftRDO, #"shift", nil];
[monthArray addObject:dateDict];
}
return monthArray;
}
This is where the call is made to update it:
[shiftEntity updateWithShiftType:_selectedShiftType inContext:context];
Which calls the method:
- (void)updateWithShiftType:(ShiftType *)shiftType inContext:(NSManagedObjectContext *)context
{
self.title = [NSString stringWithFormat:#"%#",shiftType.title];
self.symbol = [NSString stringWithFormat:#"%#",shiftType.symbol];
self.startTime = [shiftType.startTime dateByAddingTimeInterval:0];
self.endTime = [shiftType.endTime dateByAddingTimeInterval:0];
self.splitStartTime = [shiftType.splitStartTime dateByAddingTimeInterval:0];
self.splitEndTime = [shiftType.splitEndTime dateByAddingTimeInterval:0];
self.isWorkday = [shiftType.isWorkday copy];
self.isTimeOff = [shiftType.isTimeOff copy];
self.typeID = [shiftType typeID];
}
After the above code executes, the first notification is posted in the log:
<Shift: 0x8140b70> (entity: Shift; id: 0x81409e0 <x-coredata:///Shift/tE59A199A-444E-4079-BD8C-0D3E734607783> ; data: {
date = "2012-10-29 04:00:00 +0000";
endTime = "2012-11-02 19:35:38 +0000";
isTimeOff = 0;
isWorkday = 1;
shiftRotation = "0x81406a0 <x-coredata:///ShiftRotation/tE59A199A-444E-4079-BD8C-0D3E734607782>";
splitEndTime = nil;
splitStartTime = nil;
startTime = "2012-11-02 09:35:34 +0000";
symbol = none;
title = Days;
typeID = "991ACC8C-ECE0-4555-9002-7AC233F26CBF";
The "date" property does have the proper assigned date.
Now as soon as I save the context with the following method:
- (BOOL)saveContext
{
NSError *error = nil;
if (__managedObjectContext) {
if ([__managedObjectContext hasChanges] && ![__managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
// More details on error
NSArray* detailedErrors = [error userInfo][NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
}
else {
NSLog(#" %#", [error userInfo]);
}
return NO;
abort();
}
}
return YES;
}
I receive a second notification of a change with the following log:
<Shift: 0x8140b70> (entity: Shift; id: 0x82c9dd0 <x-coredata://7CE834F9-5056-457A-BB2C-B8BA638086F1/Shift/p23> ; data: {
date = "2012-12-02 05:00:00 +0000";
endTime = "2012-11-02 19:35:38 +0000";
isTimeOff = 0;
isWorkday = 1;
shiftRotation = "0x8260dc0 <x-coredata://7CE834F9-5056-457A-BB2C-B8BA638086F1/ShiftRotation/p11>";
splitEndTime = nil;
splitStartTime = nil;
startTime = "2012-11-02 09:35:34 +0000";
symbol = none;
title = Days;
typeID = "991ACC8C-ECE0-4555-9002-7AC233F26CBF";
}),
As you notice, the "date" changed to 2012-12-02, just by saving.
Update: I was finally able to track the cause of the unexplained change. It seems that I was inadvertently changing the "date" property after I was saving the context somewhere else in my code. Perhaps that would have not happened if I had followed carmin's convention (I would have given +1 if I had the reputation). The value was not changing in the Persistent store after all.
Perhaps sharing how I tracked down this bug might benefit anyone else that might have similar problems, not necessarily related to Core Data.
I used Key Value Observing, to observe the "date" property. I began observing immediately after creating the "Shift" entity:
[_shiftEntity addObserver:self forKeyPath:#"date" options:NSKeyValueObservingOptionNew context:NULL];
...and stopped observing after a point in my code where the change had already occurred:
[_shiftEntity removeObserver:self forKeyPath:#"date" context:NULL];
I then implemented the KVO method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(#"Object being observed: %#", [object description]);
NSLog(#"Value changed to: %#", change[NSKeyValueChangeNewKey]);
}
I placed a break point at the above method. When the change occurred, it stopped at this point, and told me what had change. Most importantly, when the code stopped executing, I looked at the call stack, and traced my way back through the path that the code took that caused the change. I was able to find exactly where my code was making the change.
I don't have much knowledge using KVO but this experience showed me its usefulness even for debugging purposes.
Possible naming conflict. I have learned to prefix all my names. For example, I call a core data property "pDate" instead of "date". I call a stack local "zDate", a parameter "aDate", a static "sDate", and an instance variable "iDate". (I do not prefix method names.) This avoids variable naming conflicts and makes code much easier to understand. Giving something a simple name is to look for a name conflict with the OS which should prefix all of its variables with __ (a double underscore), but that does not always happen. This may not be your problem, but it is possible that the OS uses the term "date" for another purpose and it seems to be modifying "date" to the time of save.
I have 2 questions.
If I understand local notifications the repeatinterval allows me to have a notification scheduled once and it repeated on the same interval each week or month or day of week. I am trying to get a repeatinterval to fire once on say a Tuesday and each week it will fire again on the same day i.e. Tuesday. This should go on every week without needing to schedule another notification. Is that correct. Is is not happening. I am either doing something wrong in code or I am testing it wrong.
In the simulator I run the app schedule the notificaiton. The notification comes up which I view. Then I quit the app and set the system date to 1 week in the future same day of week but no notification so can I test this notification this way by changing the computers system clock. I do not want to have to wait a week for each test.
Here is the code
- (void) scheduleNotificationWithItem:(NSDate *)date interval:(int)frequency {
UILocalNotification *localNotif = [[UILocalNotification alloc]init];
if (localNotif == nil) {
return;
}
localNotif.fireDate = [date addTimeInterval:frequency];
localNotif.timeZone = [NSTimeZone defaultTimeZone];
localNotif.repeatCalendar = [NSCalendar currentCalendar];
localNotif.repeatInterval = kCFCalendarUnitWeekday;
localNotif.applicationIconBadgeNumber = 1;
localNotif.alertBody = [NSString stringWithFormat:NSLocalizedString(#"%#.",nil),#"Weekly Reminder"];
localNotif.alertAction = NSLocalizedString(#"View Notification Details", nil);
localNotif.soundName = UILocalNotificationDefaultSoundName;
[[UIApplication sharedApplication]scheduleLocalNotification:localNotif];
[localNotif release];
}
Please help this is driving me crazy.
Thanks,
Dean
1) Setting your repeat interval to 'NSWeekCalendarUnit' should do the trick. It repeats on a weekly basis on the same day and time as the original notification.
2) I haven't tested in the simulator but changing the clock on an actual iPhone does fire future alerts.