Handling EKEventStoreChangedNotification notification - ios

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.

Related

EventKit last event of calendar

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];

Receiving An Intermittent EXC_BAD_ACCESS When Updating an EKEvent

I have a Cordova app with a custom plugin that is creating and updating EKEvents in the user's iCloud calendar.
I am using the following function to find a specific EKEvent based on its URL:
- (EKEvent*) getEventWithURL:(NSString *)pstrURL store:(EKEventStore *)pStore
{
EKCalendar* calendar = [self getCalendarByName:mstrCalendarName store:pStore createIfDoesNotExist:false];
NSArray* calendars = [[NSArray alloc] initWithObjects: calendar, nil];
NSDateFormatter *sDate = [[NSDateFormatter alloc] init];
[sDate setDateFormat:#"yyyy-MM-dd HH:mm"];
NSDate *myStartDate = [sDate dateFromString:#"2013-11-01 00:00"];
NSDateFormatter *eDate = [[NSDateFormatter alloc] init];
[eDate setDateFormat:#"yyyy-MM-dd HH:mm"];
NSDate *myEndDate = [eDate dateFromString:#"2014-12-31 23:59"];
NSPredicate *predicate = [pStore predicateForEventsWithStartDate:myStartDate endDate:myEndDate calendars: calendars];
// Fetch all events that match the predicate.
NSArray *events = [pStore eventsMatchingPredicate:predicate];
EKEvent *foundEvent = nil;
EKEvent *event;
for (id oEvent in events)
{
event = (EKEvent *)oEvent;
if ([event.URL isEqual:[NSURL URLWithString:pstrURL]])
{
foundEvent = event;
break;
}
}
return foundEvent;
}
It is then modified (start and end dates are changed) and saved in another method with the following code:
EKEvent *myEvent = nil;
BOOL saved = false;
EKCalendar* calendar = nil;
if(pstrCalendarTitle == nil)
{
calendar = pStore.defaultCalendarForNewEvents;
}
else
{
calendar = [self getCalendarByName: pstrCalendarTitle store: pStore createIfDoesNotExist:true];
}
// find event if it exists
myEvent = [self getEventWithURL:[NSString stringWithFormat: #"custom://%#", pstrTSDID ] store:pStore];
// if an event wasn't found, create a new one
if (myEvent == nil)
{
myEvent = [EKEvent eventWithEventStore: pStore];
}
// set all the fields to new values
myEvent.title = pstrTitle;
myEvent.location = pstrLocation;
myEvent.notes = pstrMessage;
myEvent.startDate = pdtStartDate;
myEvent.endDate = pdtEndDate;
myEvent.calendar = calendar;
myEvent.URL = [NSURL URLWithString:[NSString stringWithFormat: #"custom://%#", pstrTSDID ]];
// only add an alarm if one hasn't been created already
if ([[myEvent alarms] count] == 0)
{
EKAlarm *reminder = [EKAlarm alarmWithRelativeOffset:-2*60*60];
[myEvent addAlarm:reminder];
}
When creating a whole bunch of EKEvents (about 30 in a row) I don't get the EXC_BAD_ACCESS error, however when updating events I get the EXC_BAD_ACCESS error intermittently. Sometimes it is on the first update, and sometimes I am able to update 10 before seeing the error, which then crashes my app.
I suspect it may have something to do with the foundEvent variable not being retained, however my project is using ARC so it is my understanding that I don't need to do any memory management tasks. Unless ARC is getting confused with the way the event variable is being cast and then passed around in the loop in getEventWithURL?
For full disclosure, I do have Enable Zombie Objects enabled and the stack trace that I see doesn't reference any of my methods specifically, it starts at start_wqthread and then references some EKEventStore _databasechangedexternally internal methods.
For what it's worth I wasn't able to figure out what the issue was with modifying the events so I am instead deleting the event (if it exists) and creating a new one. My getEventWithURL method is now called deleteEventWithURL which does the removal of the event from the store. The app is no longer crashing after making this change.

Not reloading the right data in UITabeView

I wrote this code to reload the UItableView with an events that has the same date as the current date when the user click on todays events UIButton in the main view controller but the problem is the below code is not reloading the right data (it just gives the initial data without comparing the date of the event with the date of the calendar in the IPhone), my data comes from a json file within the project and consists from NSArray of events, each has a different value for each key and one of these keys is the data of that event ("date"), can anyone plz clarify for me why the below code is not returning the right data ??
#implementation MainViewController {
NSArray *_events;
}
....
- (IBAction)upcomingEvents:(id)sender {
NSDate *currDate = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
[dateFormatter setDateFormat:#"dd-MM-YYYY"];
NSString *dateString = [dateFormatter stringFromDate:currDate];
for (Events *event in _events){
if([event.date isEqualToString:dateString]){
[self.myTableView reloadData];
}
}
}
If you're using a UITableViewDataSource you should make sure that it returns only the events that match your condition [event.date isEqualToString:dateString]
You can do
NSArray * dateEvents = [_events filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(Events * event, NSDictionary *bindings)
{
return [event.date isEqualToString:dateString];
}];
Then you can use dateEvents for your UITableViewDataSource.

Get time only of next iOS local notification

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.

How to detect which EKevent was changed

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;
}
}];
}];
}

Resources