How to use an NSOutlineview with Coredata, grouping entities by their attributes? - objective-c

I'm fried. I've been stuck on this for two days, googling, and reading, googling and reading.
I have an entity called Session and with an attribute called sessionYear (NSNumber).
I'd like to create an NSOutlineView that will group them by year and then sort them by month, sessionMonth (NSString). Like this:
1984
October
November
December
1989
January
February
2002
March
July
October
I've found lots of information about grouping entities under other entities, and various methods of using array controllers and dictionaries. Unfortunately, much of what I found was outdated or not ostensibly applicable to my situation. I'm new to developing and coredata in general, and would appreciate any guidance on this.
Again, the years are attributes of the entities.
Eventually, I want to be able to click on these entities and populate a tableview. I'd be interested in doing this with bindings if possible, but I've been unable to find solid information. Any help, or links to resources are appreciated.
EDIT:
I've used the NSOutlineViewDelegate approach with the appending four methods. However, now it seems the sessionMonths appear in the view when the year is expanded and then immediately disappear. If another (different) year is expanded all sessionMonths appear where they should and again immediately disappear. I've come so far... for only a glimpse. Any suggestions about where to start?
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
if (item == nil) {
return [savedSessionsOutlineViewData count];
}
return [item count];
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
if (item == nil) {
item = savedSessionsOutlineViewData;
}
if ([item isKindOfClass:[NSMutableArray class]]) {
return [item objectAtIndex:index];
}
else if ([item isKindOfClass:[NSDictionary class]]) {
NSArray *keys = [item allKeys];
return [item objectForKey:[keys objectAtIndex:index]];
}
return nil;
}
- (id)outlineView:(NSOutlineView *)outline objectValueForTableColumn:(NSTableColumn *)column byItem:(id)item {
// If the item is a "yearArray" holding sessions.
if ([item isKindOfClass:[NSMutableArray class]]) {
NSArray *keys = [savedSessionsOutlineViewData allKeysForObject:item];
return [keys objectAtIndex:0];
} else if ([item isKindOfClass:[Session class]]) {
//NSLog (#"Item returned isKindOfClass:Session");
return [item valueForKey:#"sessionMonth"];
}
return nil;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
if ([item isKindOfClass:[NSMutableArray class]] || [item isKindOfClass:[NSDictionary class]]) {
if ([item count] > 0) {
return YES;
}
}
return nil;
}

In the attributes inspector for the NSOutline view, set the highlight to source list.

The best approach may be to create separate arrays for the top level groups and for each set of children. So get a list of all the unique years you want to display. You could use the collection operator #distinctUnionOfObjects.sessionYear to get an array of NSNumbers from the existing array of Session(s).
Some sample code below - I have not run this but took some code from an existing app and renamed to fit your model. There may be some mistakes..
#implementation SessionOutlineViewController <NSOutlineViewDataSource> {
NSArray *sortedYearsArray;
NSMutableDictionary *childrenDictionary; // for each key (year) we put an array of sessions in this dictionary
}
#end
#implementation SessionOutlineViewController
- (void)initialise {
NSArray* sessions = [self getData:#"Session" sortKey:#"date" predicate:nil];
// Get an array of distinct years
NSArray *yearsArray = [sessions valueForKeyPath:#"#distinctUnionOfObjects.sessionYear"];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"description" ascending:YES];
NSArray * sortedYearsArray =[yearsArray sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]];
for (NSNumber year in sortedYearsArray) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"sessionYear == %#", year];
NSArray * sessionForYear = [self getData:#"Session" sortKey:#"date" predicate:predicate];
[childrenDictionary setObject:sessionsForYear forKey:year];
}
}
- (NSArray*)getData:(NSString*)entityName sortKey:(NSString*)sortKey predicate:(NSPredicate*)predicate
{
NSFetchRequest *req = [[NSFetchRequest alloc] init];
NSEntityDescription * entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:_managedObjectContext];
if (entity == nil) {
NSLog(#" error entity %# NOT FOUND!", entityName);
return nil;
}
[req setEntity:entity];
[req setIncludesPropertyValues:YES];
if (predicate != nil)
[req setPredicate:predicate];
if (sortKey != nil) {
NSSortDescriptor *indexSort = [[NSSortDescriptor alloc] initWithKey:sortKey ascending:YES];
NSArray *sorters = [NSArray arrayWithObject:indexSort]; indexSort = nil;
[req setSortDescriptors:sorters];
}
NSError *error;
NSArray *result = [managedObjectContext executeFetchRequest:req error:&error];
if (result == nil)
return nil;
return result;
}
#end
#implementation SessionOutlineViewController (NSOutlineViewDataSource)
- (NSArray *)childrenForItem:(id)item {
NSArray *children;
if (item == nil) {
children = sortedYearsArray;
} else {
children = [childrenDictionary objectForKey:item];
}
return children;
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
return [[self childrenForItem:item] objectAtIndex:index];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
if ([outlineView parentForItem:item] == nil) {
return YES;
} else {
return NO;
}
}
- (NSInteger) outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
return [[self childrenForItem:item] count];
}
#end

Related

Reusable method to check if all objects are unequal using valueForKey

I have been working with this method for hours. It is supposed to simply check if the attribute of the cards passed in as otherCards isEqual to the same attribute of this (self) instance of class. But I am tearing my hair out - it is giving false results
(BOOL)otherCards: (NSArray *)otherCards allHaveUnequalAttribute: (NSString *)key
{
NSArray *values = [NSArray arrayWithArray:[otherCards valueForKey:key]];
int countUnequalMatches = 0;
for (NSNumber *setValue in values) {
if (![[self valueForKey:key] isEqual:setValue]) {
countUnequalMatches++;}
}
NSLog(#"countUnequalMatches %d values count: %d", countUnequalMatches, [values count]);
if (countUnequalMatches == [values count]) return YES ?: NO;
}
It is called like this:
if ([self otherCards:otherCards allHaveUnequalAttribute:CARD_SHAPE]) {
NSLog(#"All unequal");
} else {
NSLog(#"One or more card equal");
}
I found the bug myself
if (countUnequalMatches == [values count]) return YES ?: NO;
should be:
if (countUnequalMatches == [otherCards count]) return YES ?: NO;
as I used the Set as an way to count unique items.
This is the sort of thing that predicates are very well suited to handle.
-(BOOL)otherCards: (NSArray *)otherCards allHaveUnequalAttribute: (NSString *)key
{
NSPredicate *pred = [NSPredicate predicateWithFormat:#"%K == %#", key, [self valueForKey:key]];
NSArray *equalCards = [otherCards filteredArrayUsingPredicate:pred];
return equalCards.count == 0;
}

How can I sort or add an item to a UITableView the opposite way?

So, I have an UITableView which holds entries for an app I am making. The entriesViewController is its own class, with a .xib file. I have a button that adds a new item.
It does this with the following code:
-(IBAction)newItem:(id)sender {
LEItem *newItem = [[LEItemStore sharedStore] createItem];
NSLog(#"New Item = %#", newItem);
[TableView reloadData];
}
Now this works, and adds the item, however it puts it at the bottom of the list. Since this app logs things for days, I do not want the items in this order. The newest items should be placed at the top of the list. How do I do this? I didn't see any easy way to add items to the table view at the top, but I might be missing something pretty basic.
This doesn't seem like it should be hard, I am probably just overlooking something.
Ideas are welcome.
Edit:
Here is LEItem Store:
//
// LEItemStore.m
//
// Created by Josiah Bruner on 10/16/12.
// Copyright (c) 2012 Infinite Software Technologies. All rights reserved.
//
#import "LEItemStore.h"
#import "LEItem.h"
#implementation LEItemStore
+ (LEItemStore *)sharedStore
{
static LEItemStore *sharedStore = nil;
if (!sharedStore)
sharedStore = [[super allocWithZone:nil] init];
return sharedStore;
}
+ (id)allocWithZone:(NSZone *)zone
{
return [self sharedStore];
}
-(id)init
{
self = [super init];
if (self) {
NSString *path = [self itemArchivePath];
allItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
if (!allItems)
{
allItems = [[NSMutableArray alloc] init];
}
}
return self;
}
- (NSArray * )allItems
{
return allItems;
}
-(LEItem *)createItem
{
LEItem *p = [LEItem addNewItem];
[allItems addObject:p];
return p;
}
- (void)removeItem:(LEItem *)p
{
[allItems removeObjectIdenticalTo:p];
}
-(void)moveItemAtIndex:(int)from toIndex:(int)to
{
if (from == to) {
return;
}
LEItem *p = [allItems objectAtIndex:from];
[allItems removeObjectAtIndex:from];
[allItems insertObject:p atIndex:to];
}
- (NSString *)itemArchivePath {
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
return [documentDirectory stringByAppendingPathComponent:#"item.archive"];
}
-(BOOL)saveChanges {
NSString *path = [self itemArchivePath];
return [NSKeyedArchiver archiveRootObject:allItems toFile:path];
}
#end
It looks like the simplest solution would be to modify -[LEItemStore createItem] to this:
-(LEItem *)createItem {
LEItem *p = [LEItem addNewItem];
[allItems insertObject:p atIndex:0];
return p;
}
You can do it even without rearrange the array internally.If you implement the data source and you define this method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
Assuming that in your array the oldest objects are at the lowest indexes,supposing that your table view has M rows, return a cell with the format of the object at index M-rowIndex-1.
Unless I'm missing something, after you create the new item, instead of using
[allItems addObject:p];
you just need:
[allItems insertObject:p atIndex:0];
Do you have any type of createdDate or other sortable property on the item? Simply sort your retained list of items (or NSFetchedResultsController) or whatever you are binding to by that property.
You can override the comparison mechanism in your LEItem class, and have it compare dates easily:
-(NSComparisonResult)compare:(LEItem*)otherItem {
return [self.dateCreated compare:otherItem.dateCreated];
}
Then, it's just a matter of using sortArrayUsingSelector: with the selector compare:.

NSManagedObject to NSDictionary

Trying to serialise NSManagedObject to NSDictionary including related data.
I found some code for that here:
http://vladimir.zardina.org/2010/03/serializing-archivingunarchiving-an-nsmanagedobject-graph/
Unfortunately, there is no support for NSOrderedSet. Tried to implement it myself, but have a crash with message doesn't recognise selector on line if (!relatedObject.traversed) {.
- (NSDictionary*) toDictionary
{
self.traversed = YES;
NSArray* attributes = [[[self entity] attributesByName] allKeys];
NSArray* relationships = [[[self entity] relationshipsByName] allKeys];
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:
[attributes count] + [relationships count] + 1];
[dict setObject:[[self class] description] forKey:#"class"];
for (NSString* attr in attributes) {
NSObject* value = [self valueForKey:attr];
if (value != nil) {
[dict setObject:value forKey:attr];
}
}
for (NSString* relationship in relationships) {
NSObject* value = [self valueForKey:relationship];
if ([value isKindOfClass:[NSSet class]]) {
// To-many relationship
// The core data set holds a collection of managed objects
NSSet* relatedObjects = (NSSet*) value;
// Our set holds a collection of dictionaries
NSMutableSet* dictSet = [NSMutableSet setWithCapacity:[relatedObjects count]];
for (ExtendedManagedObject* relatedObject in relatedObjects) {
if (!relatedObject.traversed) {
[dictSet addObject:[relatedObject toDictionary]];
}
}
[dict setObject:dictSet forKey:relationship];
}
else if ([value isKindOfClass:[NSOrderedSet class]]) {
// To-many relationship
// The core data set holds a collection of managed objects
NSOrderedSet* relatedObjects = (NSOrderedSet *)value;
// Our set holds a collection of dictionaries
NSMutableSet* dictSet = [NSMutableSet setWithCapacity:[relatedObjects count]];
for (ExtendedManagedObject* relatedObject in relatedObjects) {
if (!relatedObject.traversed) {
[dictSet addObject:[relatedObject toDictionary]];
}
}
[dict setObject:dictSet forKey:relationship];
}
else if ([value isKindOfClass:[ExtendedManagedObject class]]) {
// To-one relationship
ExtendedManagedObject* relatedObject = (ExtendedManagedObject*) value;
if (!relatedObject.traversed) {
// Call toDictionary on the referenced object and put the result back into our dictionary.
[dict setObject:[relatedObject toDictionary] forKey:relationship];
}
}
}
return dict;
}
- (void) populateFromDictionary:(NSDictionary*)dict
{
NSManagedObjectContext* context = [self managedObjectContext];
for (NSString* key in dict) {
if ([key isEqualToString:#"class"]) {
continue;
}
NSObject* value = [dict objectForKey:key];
if ([value isKindOfClass:[NSDictionary class]]) {
// This is a to-one relationship
ExtendedManagedObject* relatedObject =
[ExtendedManagedObject createManagedObjectFromDictionary:(NSDictionary*)value
inContext:context];
[self setValue:relatedObject forKey:key];
}
else if ([value isKindOfClass:[NSSet class]]) {
// This is a to-many relationship
NSSet* relatedObjectDictionaries = (NSSet*) value;
// Get a proxy set that represents the relationship, and add related objects to it.
// (Note: this is provided by Core Data)
NSMutableSet* relatedObjects = [self mutableSetValueForKey:key];
for (NSDictionary* relatedObjectDict in relatedObjectDictionaries) {
ExtendedManagedObject* relatedObject =
[ExtendedManagedObject createManagedObjectFromDictionary:relatedObjectDict
inContext:context];
[relatedObjects addObject:relatedObject];
}
}
else if (value != nil) {
// This is an attribute
[self setValue:value forKey:key];
}
}
}
it is fast and easy way
NSMutableArray *array = [NSMutableArray arrayWithCapacity:ManagedObjectItems.count];
[[ManagedObjectItems allObjects] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
Diary_item_food *food = obj;
NSArray *keys = [[[food entity] attributesByName] allKeys];
NSDictionary *dict = [obj dictionaryWithValuesForKeys:keys];
[array addObject:dict];
}];
I found the ready gist on Gihub: https://gist.github.com/nuthatch/5607405
Even easier way, query for the objectID and use NSDictionaryResultType on the fetch request.
Update: Only if you don't need related data.

cancel button on search bar does not cancel correctly

I have a search bar, i can search now, but when I enter a text to search, and click the cancel button. It does not give me back my first stage, meaning full of the items in the table.
For example: I search the item with word: a, it gives me all the a items, yes, it is right now, but when i hit the cancel button, i want the programme gives me all the items exist, not just a items.
Here is the code: please help me out. Thank you so much.
- (void)searchBarCancelButtonClicked:(UISearchBar *)aSearchBar
{
searchBar.text = #"";
[searchBar resignFirstResponder];
letUserSelectRow = YES;
searching = NO;
self.tableView.scrollEnabled = YES;
NSLog(#"what text after cancel now: %#", searchBar.text);
[self.tableView reloadData];
}
- (NSMutableArray *) searchTableView {
NSString *searchText = searchBar.text;
NSLog(#"search text: %#", searchText);
NSMutableArray *resultArray = [[NSMutableArray alloc] init];
NSMutableArray *tempArr = [[NSMutableArray alloc] init];
for (NSDictionary *dTemp in arrayData)
{
NSString *tempStr = [dTemp objectForKey:#"url"];
NSLog(#"sTemp string: %#",[ NSString stringWithFormat:#"%#", tempStr]);
NSRange titleResultsRange = [tempStr rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (titleResultsRange.length > 0)
{
NSLog(#"1 count :%d", [resultArray count]);
[resultArray addObject:dTemp];
NSLog(#"2 count :%d", [resultArray count]);
[tempArr addObject:resultArray];
[resultArray release];
resultArray = [NSMutableArray new];
}
}
if (resultArray != nil) {
[resultArray release];
}
return tempArr;
}
- (void)searchBar:(UISearchBar *)aSearchBar textDidChange:(NSString *)searchText
{
NSLog(#"what text after cancel now: %#", searchBar.text);
if([searchText length] > 0) {
[sortedArray removeAllObjects];
searching = YES;
letUserSelectRow = YES;
self.tableView.scrollEnabled = YES;
NSMutableArray *searchArray = [self searchTableView];
sortedArray = [[NSMutableArray alloc] initWithArray:searchArray copyItems:YES];
for (int i = 0; i<[sortedArray count]; i++) {
NSLog(#"this is the search array: %#", [[sortedArray objectAtIndex:i] class]);
}
NSLog(#"sorted array: %d", [sortedArray count]);
}
else {
searching = NO;
letUserSelectRow = NO;
self.tableView.scrollEnabled = NO;
}
[self.tableView reloadData];
}
You don't need to override any of UISearchBar methods to accomplish this. The new way of doing this relies on the UISearchDisplay controller instead (specifically on shouldReloadTableForSearchString).
Declare your view controller to conform to UISearchDisplayDelegate protocol, and keep two instance variables: your model as NSArray (all data) and a filtered array as NSMutableArray (a subset of your data). The code you presently have in "searchTableView" would filter the content of the model and place it into the filtered NSMutableArray. Then you would override the following UITableView methods: -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section and -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath. In each, before returning, make a comparison to determine whether your tableView argument is equal to self.searchDisplayController.searchResultsTableView. If it is, the user is looking at the filtered list and your should use the content of the filtered NSMutableArray to create the view, otherwise, the user is looking at the whole data set and you should use the content of the NSArray that holds your model. Take a look at the following Apple code for a simple example of what I described:
http://developer.apple.com/library/ios/#samplecode/TableSearch/Introduction/Intro.html

storing address book records

I have creating an address book application . My AddressController.h class is ---
#interface AddressController : NSObject {
IBOutlet id nameField;
IBOutlet id addressField;
IBOutlet id tableView;
NSMutableArray *records;
}
- (IBAction)addRecord:(id)sender;
- (IBAction)deleteRecord:(id)sender;
- (IBAction)insertRecord:(id)sender;
#end
Implementation class is as follow:-
#implementation AddressController
- (id)init
{
records = [[NSMutableArray alloc] init];
return self;
}
- (NSDictionary *)createRecord
{
NSMutableDictionary *record = [[NSMutableDictionary alloc] init];
[record setObject:[nameField stringValue] forKey:#"Name"];
[record setObject:[addressField stringValue] forKey:#"Address"];
[record autorelease];
return record;
}
- (IBAction)addRecord:(id)sender
{
[records addObject:[self createRecord]];
[tableView reloadData];
}
- (IBAction)deleteRecord:(id)sender
{
int status;
NSEnumerator *enumerator;
NSNumber *index;
NSMutableArray *tempArray;
id tempObject;
if ( [tableView numberOfSelectedRows] == 0 )
return;
NSBeep();
status = NSRunAlertPanel(#"Warning!", #"Are you sure that you want to delete the selected record(s)?", #"OK", #"Cancel", nil);
if ( status == NSAlertDefaultReturn )
{
enumerator = [tableView selectedRowEnumerator]; //enumerator here gets indexes of selected rows
tempArray = [NSMutableArray array];
while ( (index = [enumerator nextObject]) )
{
tempObject = [records objectAtIndex:[index intValue]]; // we store selected rows in temporary array
[tempArray addObject:tempObject];
}
[records removeObjectsInArray:tempArray]; // we delete records from 'records' array which are present in temporary array
[tableView reloadData];
}
}
- (IBAction)insertRecord:(id)sender
{
int index = [tableView selectedRow];
[records insertObject:[self createRecord] atIndex:index];
[tableView reloadData];
}
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [records count];
}
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
id theRecord, theValue;
theRecord = [records objectAtIndex:rowIndex];
theValue = [theRecord objectForKey:[aTableColumn identifier]];
return theValue;
}
- (void)awakeFromNib
{
[tableView reloadData];
}
#end
I am able to add and delete records to/from address book. But when I start the application again all records are gone. I want to store records somewhere (like in user defaults ) so that when I start the application again existing records are shown in the address book.
I am not getting the idea how to do it using user defaults.
Please suggest solution.
Do not use user defaults for this. You may want to explore Core Data. Another option to explore is NSCoding.
NSCoding will have less of a learning curve, fairly simple to implement. Core Data is tougher to grasp, but would be the wiser choice in the long run.