I am using a transient attribute to sort the core data objects into separate table view sections. It is called 'sectionIdentifier'. The getter for this attribute is inside a NSManagedObject subclass, called ToDoItem.m . The problem is that during app execution, the new added objects are always shown under the TODAY section. After a new launch of the app, all objects are shown under the expected row. I was told by an expert user, that when setting a new object, the sectionIdentifier must be invalidated, but I don't know how to invalidate it. Here is my NSManagedObject subclass code:
#import "ToDoItem.h"
#import "ToDoGroup.h"
#import "ToDoSubItem.h"
#implementation ToDoItem
#dynamic todoDescription;
#dynamic todoName;
#dynamic todoDueDate;
#dynamic sectionIdentifier;
#dynamic todogroup;
#dynamic todosubitems;
-(NSString *)sectionIdentifier{
[self willAccessValueForKey:#"sectionIdentifier"];
NSString *tmp = [self primitiveValueForKey:#"sectionIdentifier"];
[self didAccessValueForKey:#"sectionIdentifier"];
if (!tmp){
NSDate *date = self.todoDueDate;
NSDate *todayDate = [NSDate date];
NSLog(#"date= %#",date);
NSLog(#"todayDate = %#",todayDate);
NSCalendar *calendar = [NSCalendar currentCalendar];
NSInteger comps = (NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit);
NSDateComponents *date1Components = [calendar components:comps fromDate:date];
NSDateComponents *date2Components = [calendar components:comps fromDate:todayDate];
date = [calendar dateFromComponents:date1Components];
todayDate = [calendar dateFromComponents:date2Components];
if([date
compare:todayDate] == NSOrderedSame) {
tmp = #"1";//TODAY
}
else if([date
compare:todayDate] == NSOrderedDescending){
tmp = #"2";//OVERDUE
}
else if ([date
compare:todayDate] == NSOrderedAscending){
tmp =#"0";//UPCOMING
}
NSLog(#"Tmp= %#",tmp);
[self setPrimitiveValue:tmp forKey:#"sectionIdentifier"];
}
return tmp;
}
#end
Any help is welcome...
The problem is that the section identifier is calculated and cached from the todoDueDate, but not updated automatically when the todoDueDate changes.
The DateSectionTitles/APLEvent.m
sample code from Apple shows how such an automatic update can be achieved.
In your case, you should add the following methods to the managed object subclass
ToDoItem:
- (void)setTodoDueDate:(NSDate *)newDate
{
// If the todoDueDate changes, the section identifier become invalid.
[self willChangeValueForKey:#"todoDueDate"];
[self setPrimitiveValue:newDate forKey:#"todoDueDate"];
[self didChangeValueForKey:#"todoDueDate"];
// Set the section identifier to nil, so that it will be recalculated
// when the sectionIdentifier method is called the next time:
[self setPrimitiveValue:nil forKey:#"sectionIdentifier"];
}
+ (NSSet *)keyPathsForValuesAffectingSectionIdentifier
{
// If the value of todoDueDate changes, the section identifier may change as well.
return [NSSet setWithObject:#"todoDueDate"];
}
Related
What I'm trying to do is detect which sections of a UITableView are visible, and then change the date of a calendar based on which are visible.
The issue is that there are typically multiple sections viewable at the same time, and I only want to change the date based on the first section index that appears in visibleRowIndexes, not all of them.
Here's my current implementation (Note: I run this function in cellForRowAtIndexPath):
-(BOOL)whatSectionsAreVisible {
NSArray *visibleRowIndexes = [self.agendaTable indexPathsForVisibleRows];
for (NSIndexPath *index in visibleRowIndexes) {
NSNumber *daySection = #(index.section);
static NSDateFormatter *dateFormatter = nil;
if(!dateFormatter){
dateFormatter = [NSDateFormatter new];
dateFormatter.dateFormat = #"yyyy-MM-dd"; // Read the documentation for dateFormat
}
// Here is where I will map every index.section to an NSDate
NSDateComponents *comps = [[NSDateComponents alloc] init];
[comps setDay:daySection.intValue]; // <<== Extract int from daySection
[comps setMonth:6];
[comps setYear:2015];
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDate *date = [gregorian dateFromComponents:comps];
// Update the day selected according to visible section
[[NSNotificationCenter defaultCenter] postNotificationName:#"kJTCalendarDaySelected" object:date];
// Store currentDateSelected
[self.calendar setCurrentDateSelected:date];
NSLog(#"The visible section has an index of: %ld", (long)index.section);
if (index.section == 0) {
NSLog(#"index.row is indeed 0");
return YES;
}
}
return NO;
}
I tried doing something like NSNumber *daySection = #(index[0].section); instead of NSNumber *daySection = #(index.section);, but that doesn't seem to work.
Don't use a loop if you just want the first visible row. Get the first object of visibleRowIndexes as an NSIndexPath. Then get the section from that.
The indexPathsForVisibleRows method returns the index paths in ascending order. So, get rid of the for loop and execute the code in it only for the first object:
NSArray *visibleRowIndexes = [self.agendaTable indexPathsForVisibleRows];
NSIndexPath *index = [visibleRowIndex objectAtIndex:0];
I'm on Xcode 6.0.1, making a test app with Event Kit. The following code successfully populates each event's title, but its notes are returned as (null) even when hasNotes property returns YES. And, I can see the notes for the same event on iPhone's default calendar app.
What am I doing wrong?
- (void)viewDidLoad
{
[super viewDidLoad];
[eventStore requestAccessToEntityType:EKEntityTypeEvent
completion:^(BOOL granted, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error)
{
NSLog(#" !! error");
// display error message here
}
else if (!granted)
{
NSLog(#"Not Granted");
// display access denied error message here
}
else
{
// access granted
NSCalendar *calendar = [NSCalendar currentCalendar];
// Create the start date components
NSDateComponents *oneWeekAgoComponents = [[NSDateComponents alloc] init];
oneWeekAgoComponents.day = -1;
NSDate *oneWeekAgo = [calendar dateByAddingComponents:oneWeekAgoComponents toDate:[NSDate date] options:0];
// Create the end date components
NSDateComponents *oneMonthFromNowComponents = [[NSDateComponents alloc] init];
oneMonthFromNowComponents.month = 1;
NSDate *oneMonthFromNow = [calendar dateByAddingComponents:oneMonthFromNowComponents toDate:[NSDate date] options:0];
// Create the predicate from the event store's instance method
NSPredicate *predicate = [eventStore predicateForEventsWithStartDate:oneWeekAgo endDate:oneMonthFromNow calendars:nil];
// Fetch all events that match the predicate
_eventArray = [eventStore eventsMatchingPredicate:predicate];
[self.tableView reloadData];
}
});
}];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
EKEvent *event = [self.eventArray objectAtIndex:indexPath.row];
cell.textLabel.text = event.title;
if (event.hasNotes) {
cell.detailTextLabel.text = event.notes;
} else {
cell.detailTextLabel.text = #"";
}
return cell;
}
I haven't fully resolved it, but got a clue.
NSArray *events = [eventStore eventsMatchingPredicate:predicate];
This didn't get the notes. So, instead I enumerate through returned events by doing
[eventStore enumerateEventsMatchingPredicate:predicate usingBlock:^(EKEvent *event, BOOL *stop) {
NSLog(#"title: %#",event.title);
NSLog(#"hasNotes: %s",event.hasNotes ? "YES":"NO");
NSLog(#"notes: %#",event.notes);
NSLog(#"-----");
[_eventTitles addObject:event.title];
[_eventTitles addObject:event.hasNotes ? event.notes : #""];
}];
This one returns actual notes (null).
I had a similar issue like this but when accessing the calendar object from the EKEvent. I my case this was because I had released the EKEventStore instance before I attempted to access the calendar (in cellForRowAtIndexPath:).
According to the Apple documentation "Reading and Writing Calendar Events":
An event store instance must not be released before other Event Kit
objects; otherwise, undefined behavior may occur.
https://developer.apple.com/library/mac/documentation/DataManagement/Conceptual/EventKitProgGuide/ReadingAndWritingEvents.html#//apple_ref/doc/uid/TP40004775-SW1
I have a very weird question, but if I can explain it well, I guess the answer will not be so difficult.
This is my current scenario:
I have a tableView with six fixed sections.
The sections are populated with core data objects.
I am using a transient attribute to decide on which section the object must be shown. This procedure is done in a NSManagedObject subclass. The transient attribute is called sectionIdentifier.
Due to the reason of using a transient attribute to decide on which section should appear every object, I decided to create a special row on each section, with the intention that there will be always at least one row on every section. This option will reduce the problem to have a nil section that would throw exceptions in case of adding or deleting rows, at least this has been a problem sometimes while testing the app.
That means that at the first launch every section has one row (I will call it the special row). And then the user can add/delete rows perfectly.
The problem I have now is that I need this special row to be remove if the section has new rows. At the same time, if the user deletes the last row in the section, the app automatically creates a new special row, so the section is never empty.
I know that this is not the best way to manage it, but due to my iOS knowledge, this is the best way I found to avoid exceptions while adding or removing rows from an empty section.
What I need is to delete the special row from a section when the user has added at least one new row to the section.
The object has a string attribute called quitar and for the special rows: quitar = #"si". This way, all special rows are easy to identify.
I am using this code to delete the special row if there are new rows in the section:
NSManagedObjectContext *context = self.managedObjectContext;
NSEntityDescription *entity=[NSEntityDescription entityForName:#"ToDoItem" inManagedObjectContext:context];
NSFetchRequest *fetch=[[NSFetchRequest alloc] init];
[fetch setEntity:entity];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"quitar like 'si'"];
[fetch setPredicate:predicate];
NSError *fetchError;
NSArray *fetchedData=[self.managedObjectContext executeFetchRequest:fetch error:&fetchError];
for (NSManagedObject *item in fetchedData){
[context deleteObject:item];
}
I was told that you cannot use transient attributes in a NSPredicate, and you can see in my code, that all special rows are deleted, not only the special from the desired section.
I need your help to find a way to delete only the special row from the section I want, not from all the sections.
Thank you and sorry for my english.
EDITED
Please find below the way I assign sectionIdentifier in the NSManagedObject class ToDoItem:
-(NSString *)sectionIdentifier{
[self willAccessValueForKey:#"sectionIdentifier"];
NSString *tmp = [self primitiveValueForKey:#"sectionIdentifier"];
[self didAccessValueForKey:#"sectionIdentifier"];
if (!tmp){
NSCalendar *calendar = [NSCalendar currentCalendar];
NSInteger comps = (NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit);
NSDate *today = [NSDate date];
NSDate *date = self.todoDueDate;
NSDateComponents *components = [[NSDateComponents alloc] init];
[components setCalendar:calendar];
[components setYear:2065];
[components setMonth:11];
[components setDay:12];
NSDate *dateFuturoSomeday = [calendar dateFromComponents:components];
NSCalendar *calendar1 = [NSCalendar currentCalendar];
NSDateComponents *components1 = [[NSDateComponents alloc] init];
[components1 setCalendar:calendar1];
[components1 setYear:2065];
[components1 setMonth:11];
[components1 setDay:13];
NSDate *dateFuturoCompleted = [calendar1 dateFromComponents:components1];
NSDateComponents *date1Components = [calendar components:comps
fromDate: today];
NSDateComponents *date2Components = [calendar components:comps
fromDate: date];
NSDateComponents *date3Components = [calendar components:comps
fromDate: dateFuturoSomeday];
NSDateComponents *date4Components = [calendar components:comps
fromDate: dateFuturoCompleted];
today = [calendar dateFromComponents:date1Components];
date = [calendar dateFromComponents:date2Components];
dateFuturoSomeday = [calendar dateFromComponents:date3Components];
dateFuturoCompleted = [calendar dateFromComponents:date4Components];
NSInteger daysAfterToday = [calendar components:NSDayCalendarUnit
fromDate:today toDate:date options:0].day;
// NSString *section;
if (daysAfterToday < 0) {
tmp = #"0";
} else if (daysAfterToday == 0) {
tmp = #"1";
} else if (daysAfterToday > 0 && daysAfterToday < 2) {
tmp = #"2";
}
else if ([self.todoDueDate isEqualToDate:dateFuturoSomeday]){
tmp = #"4";
}
else if ([self.todoDueDate isEqualToDate:dateFuturoCompleted]){
tmp = #"5";
}
else {
tmp = #"3";
}
[self setPrimitiveValue:tmp forKey:#"sectionIdentifier"];
}
return tmp;
}
I have checked that at the cellForRowAtIndexPath method, I am able to detect the value from the transient attribute sectionIdentifier. That means that I can use it to identify the special row item, update the value of another attribute an delete it.
ToDoItem *todoitem = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString *sec = todoitem.sectionIdentifier;
NSString *quitar = todoitem.quitar;
if ([sec isEqualToString:#"1"] && [quitar isEqualToString:#"si" ] && ( numObjectsSec1 > 1 ) ){
NSLog(#"DAS OBJEKT KANN GELĂ–SCHT WERDEN %#", todoitem.todoName);
NSManagedObjectContext *context = self.managedObjectContext;
[todoitem setValue:#"si se puede borrar" forKey:#"borrar" ];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
[context deleteObject: todoitem];
};
I have an NSArray with something similar to:
6/1/13 | Data
6/2/13 | Data
7/1/13 | Data
9/1/13 | Data
What I need to somehow get the months to create section headers - but only if they are in the array and then break the dates up into the appropriate sections. Looking like:
(Section Header)June 2013
6/1/13 | Data
6/2/13 | Data
(Section Header)July 2013
7/1/13 | Data
(skips august as no dates from august are in array)
(Section Header)September 2013
9/1/13 | Data
I am attempting to implement:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return #"June 2013";
}
But obviously need this to dynamically update with whatever months are in the array. The dates are actually NSDates that are in the array - if that makes any difference.
I have cobbled together something that should at least compile, but which is completely untested. Basically this involves pre-processing your array and storing the results in other collections that can then serve as model objects for your UITableViewDataSource.
Add these properties to the class that is your data source. You have to declare them differently if you are using ARC.
#property(retain) NSMutableArray* tableViewSections;
#property(retain) NSMutableDictionary* tableViewCells;
Add this method to your data source and make sure that you invoke it at some time before UITableView invokes your first data source method. Important: Your array must contain the NSDate objects in sorted order (the example in your question implies that this is the case).
- (void) setupDataSource:(NSArray*)sortedDateArray
{
self.tableViewSections = [NSMutableArray arrayWithCapacity:0];
self.tableViewCells = [NSMutableDictionary dictionaryWithCapacity:0];
NSCalendar* calendar = [NSCalendar currentCalendar];
NSDateFormatter* dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
dateFormatter.locale = [NSLocale currentLocale];
dateFormatter.timeZone = calendar.timeZone;
[dateFormatter setDateFormat:#"MMMM YYYY"];
NSUInteger dateComponents = NSYearCalendarUnit | NSMonthCalendarUnit;
NSInteger previousYear = -1;
NSInteger previousMonth = -1;
NSMutableArray* tableViewCellsForSection = nil;
for (NSDate* date in sortedDateArray)
{
NSDateComponents* components = [calendar components:dateComponents fromDate:date];
NSInteger year = [components year];
NSInteger month = [components month];
if (year != previousYear || month != previousMonth)
{
NSString* sectionHeading = [dateFormatter stringFromDate:date];
[self.tableViewSections addObject:sectionHeading];
tableViewCellsForSection = [NSMutableArray arrayWithCapacity:0];
[self.tableViewCells setObject:tableViewCellsForSection forKey:sectionHeading];
previousYear = year;
previousMonth = month;
}
[tableViewCellsForSection addObject:date];
}
}
Now in your data source methods you can say:
- (NSInteger) numberOfSectionsInTableView:(UITableView*)tableView
{
return self.tableViewSections.count;
}
- (NSInteger) tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
id key = [self.tableViewSections objectAtIndex:section];
NSArray* tableViewCellsForSection = [self.tableViewCells objectForKey:key];
return tableViewCellsForSection.count;
}
- (NSString*) tableView:(UITableView*)tableView titleForHeaderInSection:(NSInteger)section
{
return [self.tableViewSections objectAtIndex:section];
}
[...]
The rest of the implementation is left as an exercise to you :-) Whenever the content of your array changes you obviously need to invoke setupDataSource: to update the contents of tableViewSections and tableViewCells.
You need to convert your existing single array and create a new array of dictionaries. Each dictionary in this new array will contain two entries - one for the month and the other entry will be an array containing the data for each row associated with the month.
If you need to add a new row to this structure, see of the month is already in the list. If so, update that month's array. Otherwise create a new dictionary with the new month and a new array containing the one new row.
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;
}
}];
}];
}