Is there a way to find the NSDate for the next local notification that will fire?
For example, I've set up three local notifications:
Notification #1: Set to fire yesterday at 3:00 PM with repeat interval of daily.
Notification #2: Set to fire today at 5:00 PM with repeat interval of daily.
Notification #3: Set to fire tomorrow at 6:00 PM with repeat interval of daily.
Given that is is currently 4:00 PM, the next local notification that will fire is notification #2.
How can I retrieve this local notification and get its date?
I know that I can retrieve these local notifications in an array, but how do I get the next one that will fire based on today's date?
The main goal for your task is to determine the "next fire date" after a given
date for each notification.
The NSLog() output of a UILocalNotification shows this next fire date,
but unfortunately it seems not to be available as a (public) property.
I have taken the code from https://stackoverflow.com/a/18730449/1187415 (with small
improvements) and rewritten that as a category method for UILocalNotification.
(This is not perfect. It does not cover the case that a time zone has been
assigned to the notification.)
#interface UILocalNotification (MyNextFireDate)
- (NSDate *)myNextFireDateAfterDate:(NSDate *)afterDate;
#end
#implementation UILocalNotification (MyNextFireDate)
- (NSDate *)myNextFireDateAfterDate:(NSDate *)afterDate
{
// Check if fire date is in the future:
if ([self.fireDate compare:afterDate] == NSOrderedDescending)
return self.fireDate;
// The notification can have its own calendar, but the default is the current calendar:
NSCalendar *cal = self.repeatCalendar;
if (cal == nil)
cal = [NSCalendar currentCalendar];
// Number of repeat intervals between fire date and the reference date:
NSDateComponents *difference = [cal components:self.repeatInterval
fromDate:self.fireDate
toDate:afterDate
options:0];
// Add this number of repeat intervals to the initial fire date:
NSDate *nextFireDate = [cal dateByAddingComponents:difference
toDate:self.fireDate
options:0];
// If necessary, add one more:
if ([nextFireDate compare:afterDate] == NSOrderedAscending) {
switch (self.repeatInterval) {
case NSDayCalendarUnit:
difference.day++;
break;
case NSHourCalendarUnit:
difference.hour++;
break;
// ... add cases for other repeat intervals ...
default:
break;
}
nextFireDate = [cal dateByAddingComponents:difference
toDate:self.fireDate
options:0];
}
return nextFireDate;
}
#end
Using that, you can sort an array of local notifications according to the
next fire date:
NSArray *notifications = #[notif1, notif2, notif3];
NSDate *now = [NSDate date];
NSArray *sorted = [notifications sortedArrayUsingComparator:^NSComparisonResult(UILocalNotification *obj1, UILocalNotification *obj2) {
NSDate *next1 = [obj1 myNextFireDateAfterDate:now];
NSDate *next2 = [obj2 myNextFireDateAfterDate:now];
return [next1 compare:next2];
}];
Now sorted[0] will be the next notification that fires.
Related
I'm making an app in which I sync certain events to a calendar on iPhone.
The problem is, I have no way of telling which events were altered/removed/...
So I need to remove all the events between today and the end date of the last event of the calendar before 'syncing' (read inserting) the new events.
As far as I've seen, the only way to do an action on multiple events at once, is by using enumerateEventsMatchingPredicate:usingBlock: and predicateForEventsWithStartDate:endDate:calendars:
But for this I need a specific end date. (and thus, the end date of the last event in my calendar)
I could always save the event identifier of the last event I insert into this calendar, but I would rather not do this:
If the user uninstalls my app and installs it again later, I don't have the last event identifier anymore. (given that (s)he didn't remove the calendar manually of course)
I could just remove the calendar every time I need to sync the calendar, but then I would lose all passed events.
Any ideas or tips are much appreciated!
For your comment on :
I can't seem to find any way to fetch all the events of a calendar.
Actually you can fetch all the events from calendar :
NSDate *start = ...
NSDate *finish = ...
// use Dictionary for remove duplicates produced by events covered more one year segment
NSMutableDictionary *eventsDict = [NSMutableDictionary dictionaryWithCapacity:1024];
NSDate* currentStart = [NSDate dateWithTimeInterval:0 sinceDate:start];
int seconds_in_year = 60*60*24*365;
// enumerate events by one year segment because iOS do not support predicate longer than 4 year !
while ([currentStart compare:finish] == NSOrderedAscending) {
NSDate* currentFinish = [NSDate dateWithTimeInterval:seconds_in_year sinceDate:currentStart];
if ([currentFinish compare:finish] == NSOrderedDescending) {
currentFinish = [NSDate dateWithTimeInterval:0 sinceDate:finish];
}
NSPredicate *predicate = [eventStore predicateForEventsWithStartDate:currentStart endDate:currentFinish calendars:nil];
[eventStore enumerateEventsMatchingPredicate:predicate
usingBlock:^(EKEvent *event, BOOL *stop) {
if (event) {
[eventsDict setObject:event forKey:event.eventIdentifier];
}
}];
currentStart = [NSDate dateWithTimeInterval:(seconds_in_year + 1) sinceDate:currentStart];
}
NSArray *events = [eventsDict allValues];
I am listing the events in my app. User can create, edit and delete the events. In viewDidLoad method I fetch all events I need and push them into an array. It works like expected.
For creating, editing and deleting events I use EKEventEditViewController and EKEventViewController which works pretty well. In delegate methods of the controllers I make the changes I need on my array and reload my view.
Of course I would like also know and handle, if user make some changes from another app (like built-in calendar app). So I observe EKEventStoreChangedNotification. From that notification I get only "changes have been occurred" and not which event or from which app. Actually what I want to know is, if the change has been occurred from my app or another app and which events have been changed. Since I already handle the changes(from my app) in EKEventEditViewControllerDelegate method, I do not need to handle them again.
If I do not know which objects have been changed, I have to fetch ans sort all of them.
For now I have only 5 events in the calendar(development device), of course it is not a problem to fetch and sort all events, but if user has more then 1000, it is overkill for maybe only one event change.
So my question is: How to handle EKEventStoreChangedNotification?
You can detect exactly which event has been changed by the following code [Disclaimer code is not my idea, I have found it in another Stack Overflow answer and modified it a little bit].
I'm using a lib called "JSCalendarManager" for interaction with eventstore and in my case as the events created using my App and synced with iCalendar I already saved their eventIdentifier in local DB , I can retrieve my time bound to search for events in iCalendar and get match for changed one.
+(void)iCloudStoreChanged:(NSNotification*)eventStoreChangeNotification{
NSArray* allScheduleRecords =[self getAllScheduleRecordSyncedToICalendar];
NSDate* startDate = [NSDate new];
NSDate* endDate = [NSDate new];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"yyyy-MM-dd HH:mm:ss"];
if (allScheduleRecords.count >= 2) {
startDate = [dateFormatter dateFromString:[[allScheduleRecords firstObject] objectForKey:#"meetingTime"]];
endDate = [dateFormatter dateFromString:[[allScheduleRecords lastObject] objectForKey:#"meetingTime"]];
}else if (allScheduleRecords.count > 0){
startDate = [dateFormatter dateFromString:[[allScheduleRecords firstObject] objectForKey:#"meetingTime"]];
NSDate *today = [NSDate date];
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *components = [gregorian components:(NSEraCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit) fromDate:today];
components.day = 1;
endDate = [gregorian dateFromComponents:components];
}else{
}
NSArray *ekEventStoreChangedObjectIDArray = [eventStoreChangeNotification.userInfo objectForKey:#"EKEventStoreChangedObjectIDsUserInfoKey"];
[calendarManager findEventsBetween:startDate
and:endDate
withSearchHandler:^(BOOL found, NSError *error, NSArray *eventsArray) {
[eventsArray enumerateObjectsUsingBlock:^(EKEvent *ekEvent, NSUInteger idx, BOOL *stop) {
// Check this event against each ekObjectID in notification
[ekEventStoreChangedObjectIDArray enumerateObjectsUsingBlock:^(NSString *ekEventStoreChangedObjectID, NSUInteger idx, BOOL *stop) {
NSObject *ekObjectID = [(NSManagedObject *)ekEvent objectID];
if ([ekEventStoreChangedObjectID isEqual:ekObjectID]) {
// Log the event we found and stop (each event should only exist once in store)
NSLog(#"calendarChanged(): Event Changed: title:%#", ekEvent.title);
[self updateAppointmentForEvent:ekEvent];
*stop = YES;
}
}];
}];
}];}
Instead of fetching all events, can you not update only the events that are onscreen/active.
What is the best way to get the time of the next local notification set for my app?
I know the following loop can be used to get notifications, but does this always sort in time order so I could just get time of item [0], or is it in order of when they were added? And how can just time be pulled from this? Will I need to get whole date and format the time out, or is there a better way?
UIApplication *app = [UIApplication sharedApplication];
NSArray *eventArray = [app scheduledLocalNotifications];
for (int i=0; i<[eventArray count]; i++)
{
UILocalNotification* oneEvent = [eventArray objectAtIndex:i];
//oneEvent is a local notification
//get time of first one
}
Many thanks!
Sam
This is really two problems. First, how to get the next notification. Second, how to get just the time components of that notification's date.
Number one, sorting an array by a date property of the contained objects
NSSortDescriptor * fireDateDesc = [NSSortDescriptor sortDescriptorWithKey:#"fireDate" ascending:YES];
NSArray * notifications = [[UIApplication sharedApplication] scheduledLocalNotifications] sortedArrayUsingDescriptors:#[fireDateDesc]]
UILocalNotification * nextNote = [notifications objectAtIndex:0];
Two, get just the hours and minutes from the date
NSDateComponents * comps = [[NSCalendar currentCalendar] components:(NSHourCalendarUnit|NSMinuteCalendarUnit|NSSecondCalendarUnit)
fromDate:[notification fireDate]];
// Now you have [comps hour]; [comps minute]; [comps second];
// Or if you just need a string, use NSDateFormatter:
NSDateFormatter * formatter = [NSDateFormatter new];
[formatter setDateFormat:#"HH:mm:ss"];
NSString * timeForDisplay = [formatter stringFromDate:[notification fireDate]];
You can't guarantee the order of the scheduledLocalNotifications array. If you need to grab the latest notification more than once in your app, I recommend creating a utility category on UIApplication with a method that contains the for loop. That way you can just call:
notif = [[UIApplication sharedApplication] nextLocalNotification];
Don't repeat yourself.
I got the problem. I need to know when Events in my EventStore are changed, so for this case I use EKEventStoreChangedNotification but this notification return to me incomprehensible dictionary in userInfo
It's look like this:
EKEventStoreChangedObjectIDsUserInfoKey = ("x-apple-eventkit:///Event/p429" );
I don't know how I can use this data to taking access for changed object. Please help me
This will detect changed events and log the event titles over a date range. Although, I ended up not doing this because in practice I don't know the date range. I need to compare with all the events I'm working with, which means I need to refresh them anyway since the object IDs might have changed. This ends up making each event not so useful and now I just refresh every few seconds when changes come in and ignore the details. I hope Apple improves these notifications.
#pragma mark - Calendar Changed
- (void)calendarChanged:(NSNotification *)notification {
EKEventStore *ekEventStore = notification.object;
NSDate *now = [NSDate date];
NSDateComponents *offsetComponents = [NSDateComponents new];
[offsetComponents setDay:0];
[offsetComponents setMonth:4];
[offsetComponents setYear:0];
NSDate *endDate = [[NSCalendar currentCalendar] dateByAddingComponents:offsetComponents toDate:now options:0];
NSArray *ekEventStoreChangedObjectIDArray = [notification.userInfo objectForKey:#"EKEventStoreChangedObjectIDsUserInfoKey"];
NSPredicate *predicate = [ekEventStore predicateForEventsWithStartDate:now
endDate:endDate
calendars:nil];
// Loop through all events in range
[ekEventStore enumerateEventsMatchingPredicate:predicate usingBlock:^(EKEvent *ekEvent, BOOL *stop) {
// Check this event against each ekObjectID in notification
[ekEventStoreChangedObjectIDArray enumerateObjectsUsingBlock:^(NSString *ekEventStoreChangedObjectID, NSUInteger idx, BOOL *stop) {
NSObject *ekObjectID = [(NSManagedObject *)ekEvent objectID];
if ([ekEventStoreChangedObjectID isEqual:ekObjectID]) {
// Log the event we found and stop (each event should only exist once in store)
NSLog(#"calendarChanged(): Event Changed: title:%#", ekEvent.title);
*stop = YES;
}
}];
}];
}
I have a ViewController that performs a random shuffle on an array and spits out text to a Label (in the viewDidLoad method). The problem is that whenever I navigate to the same ViewController it performs the shuffle again and I only need it to shuffle ONCE every day.
So I need to check if this ViewController has been loaded before on the SAME day (i.e. since midnight) and I can then put the shuffle into an if statement. Could I schedule the shuffle at midnight regardless of the app being open or not?
I have looked into setting a boolean to NSUserDefaults: something like hasLoadedSinceMidnight but then can't work out how to RESET the boolean at midnight.
You could implement the AppDelegate's significantTimeChange method:
-(void)applicationSignificantTimeChange:(UIApplication *)application {
//tell your view to shuffle
}
This method is called every midnight and during significant time changes such as a time zone change. If your app is closed when the event is received, this method will be called when your app is next opened.
More information can be viewed here
An additional way to do the same thing inside of your ViewController instead of in the AppDelegate would be to add:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(performAction) name:UIApplicationSignificantTimeChangeNotification object:nil];
and then you can just perform your shuffle operation in the -(void)performAction; method of that ViewController.
Instead of storing a BOOL, you could store the date/time (NSDate) of the last shuffle.
Then check whether or not it has passed midnight since the last shuffle by comparing the stored date and the current date in the viewDidAppear.
See NSTime Documentation: http://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSDate_Class/Reference/Reference.html
And NSDateFormatter Documentation:
https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSDateFormatter_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40003643
UPDATE:
As requested, here is some sample code. Admittedly, there very well may be better solutions to this, but I believe this code snippet will help you get you past your problem. What this does is check if there is a date saved using NSUserDefaults, then compare with the current date. If the dates do not match, shuffle the array, then save the current date (again using NSUserDefaults). (I've taken the liberty of making the assumption that time will indeed continue to move forward, so it doesn't check to make sure that lastSavedDate is before the currentDate.)
NSDate *currentDate = [[NSDate alloc] init];
NSDate *lastShuffleDate = [[NSUserDefaults standardUserDefaults] objectForKey:#"lastShuffleDate"];
// check to see if there is a prior shuffle date
// if there is not, shuffle the array and save the current date
if (!lastShuffleDate) {
NSLog(#"No object set for 'lastShuffleDate'");
//[self shuffleMyArray];
[[NSUserDefaults standardUserDefaults] setObject:currentDate forKey:#"lastShuffleDate"];
return;
}
// set up the date formatter
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US"];
[dateFormatter setLocale:usLocale];
[dateFormatter setDateStyle:NSDateFormatterShortStyle];
NSLog(#"Current Date: %#", [dateFormatter stringFromDate:currentDate]);
NSLog(#"Saved Date: %#", [dateFormatter stringFromDate:lastShuffleDate]);
// check to see if the dates are the same by comparing the dates as a string
if (![[dateFormatter stringFromDate:currentDate] isEqualToString:[dateFormatter stringFromDate:lastShuffleDate]]) {
NSLog(#"Dates are different...!");
//[self shuffleMyArray];
} else {
NSLog(#"Dates are the same... (midnight has not passed)");
}
// save the time of the last shuffle
[[NSUserDefaults standardUserDefaults] setObject:currentDate forKey:#"lastShuffleDate"];
At this point, there is no real reason for you to check the times, but I've included it in case you were curious.
// remote dateStyle and set timeStyle to check times
[dateFormatter setDateStyle:NSDateFormatterNoStyle];
[dateFormatter setTimeStyle:NSDateFormatterShortStyle];
NSLog(#"Current Time: %#", [dateFormatter stringFromDate:currentDate]);
NSLog(#"Saved Time: %#", [dateFormatter stringFromDate:lastShuffleDate]);