Count Distinct items in Core Data - iphone

I have a Core Data entity with a property named 'value' which is often repeated. I wish to retrieve only unique values (done) and also how often each one appears, so that I can sort by that property (I'm building an autocomplete function based on existing user input, so knowing how frequently a certain input has appeared is essential).
My fetch request currently looks like this:
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:self.managedObjectContext];
NSDictionary *entityProperties = [entity propertiesByName];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:10];
[fetchRequest setFetchLimit:20];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setResultType:NSDictionaryResultType];
[fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:[entityProperties objectForKey:#"value"]]];
NSSortDescriptor *sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"value" ascending:YES] autorelease];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"value BEGINSWITH[c] %#",predicateString];
[fetchRequest setPredicate:predicate];
return fetchRequest;
I'm a bit stuck with this one. Any ideas?
Thanks in advance!

I don't like this one, but...
You could do an other fetchRequest with a predicate that exactly match the value you are seeking. Allowing duplicate, then have the count of the array.
This one is better, but more work up front.
An other way could be to have a derived property in your data model that keep track of your duplicated count as you create them.
(with that option you could easily sort by duplicated count)
Ok for a Derived property.
First you will need to subclass NSManagedObject and use that subclass in your data model. (in Xcode 3 there was a way to create that quickly, but I don't know that in Xcode 4) But if you name it the same as your entity I think core data will pick it up.
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#interface Person : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSString * firstLetterOfName;
#property (nonatomic, retain) NSString * phoneNumber;
#end
And in your implementation you will need to do something like this (there is code snippet in Xcode 4 to get you started, but there is a typo in one of them, unless it have been corrected)
#import "Person.h"
#implementation Person
#dynamic name, phoneNumber, firstLetterOfName;
- (void)setName:(NSString *)value {
[self willChangeValueForKey:#"name"];
[self setPrimitiveValue:value forKey:#"name"];
self.firstLetterOfName = [value substringToIndex:1];
[self didChangeValueForKey:#"name"];
}
#end
You can see that the firstLetterOfName is set each time the Name is set.
You can do the same kind of thing with relationship.
So when you add an item to a relationship you should be able to look up your relationship.
Here is an exemple of something similar, where I need to find if the object I'm adding have the lowest price of it's group because of a derived property call isMeilleurPrixAvecPrixElment. (this is old code, so I don't recall every detail of it, it have been done in OSX.4)
- (void)addPrixHistoriqueObject:(PrixElement_MO *)value
{
NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
[self willChangeValueForKey:#"prixHistorique" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
if ([self isPrixRegulierAvecPrixElement:value])
[self enleveL_AutrePrixRegulierPourCommerceDeCePrixElement:value];
if ([self isMeilleurPrixAvecPrixElment:value])
[self echangeMeilleurPrixAvecCePrixElement:value];
[[self primitivePrixHistorique] addObject:value];
[self didChangeValueForKey:#"prixHistorique" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
[changedObjects release];
}
In respond to a comment
depending on your data model and the importance of that aspect in your application, I can think of 3 solutions.
1- Redesing your data model around that aspect.
2- When setting the values query the rest of your entity with a predicate and update a property that have the count.
3- (I'm not sure of that one, but is worth trying) NSManagedObect is an object, so maybe you could have a static dictionary that have the value as a key and the count as value.
I would maybe try number 3 first (it look like the easy one), but I've never done something like that. So I'm not sure for the presitance of a class variable in core data.

Related

Core Data NSSet Issue

I auto-generated the cored data NSManagedObject class, and what I am trying to do is to display all phone numbers for a certain person. I am trying to display this in my table view class to populate the table.
Person to Numbers is a one-to-many relationship, and the numbers are in the set which I did by the addNumbersObject method. I just do not understand how to fetch this in the fetchresultscontroller and display them in the table view.
Currently I am just fetching all people.
Any ideas or suggestions?
Core Data Class:
#class Number
#interface Person : NSManagedObject
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSSet *numbers;
#end
#interface Person (CoreDataGeneratedAccessors)
- (void)addNumbersObject:(Number *)value;
- (void)removeNumbersObject:(Number *)value;
- (void)addNumbers:(NSSet *)values;
- (void)removeNumbers:(NSSet *)values;
#end
Table View Class:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
return _fetchedResultsController;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_context sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
[fetchRequest release];
[theFetchedResultsController release];
return _fetchedResultsController;
}
Try using a predicate on your fetchRequest.
I am not sure from your question if you want to display all the phone numbers for all the people or how you want that formatted, but this is how to get all the numbers for one person.
To get all the phone numbers for one specific person in one table view:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Number" inManagedObjectContext:_context];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"person.name == %#", personName]]; //Assuming person.name is unique
Then sort that person's phone number like you want it.
There are a lot of ways you might "display all phone numbers for a person" -- think specifically about what you want your table to contain and how it should be organized, and you might have an easier time finding the answer you're looking for.
In the meantime, here's one approach: Use table sections to represent Persons, and rows within each for their associated Numbers. You can do this like so:
Set Number as the entity for your fetch request.
Set "person.name" as the sort key. (You might want to add a second sort descriptor so that Numbers for each person are in a sensible order.)
Also set "person.name" as the section name key path.
Implement your table view data source's -tableView:titleForHeaderInSection: thusly:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}

Core Data Managed Object Context Saving Problem

I am trying to save an array into core data using NSData but my ManagedObjectContext says there are 0 objects and when I call it, I have it appearing as NULL. I have an entity called Event and 3 attributes in it (chatArray,...,...). I have tried for 11 hours and can't figure it out. I believe I am setting it wrong because the NSData is correct. How should I be setting this???
UPDATE
I am developing a chat application and I have the chat messages in a table view (It's an array of data). I need to save all the chat history when you exit the app and reload it. I have the messages coming in as strings and add it to the array for the table. If I didn't do an array, and I added the messages as strings of text to core data how would I add them to the array for the table view when you reload the app?
#property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
#property (nonatomic, retain) NSManagedObject *managedObject;
//
NSArray *array = [[[NSArray alloc]initWithObjects:#"one",#"two",#"three", nil]autorelease];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array];
NSLog(#"data %#",data);
NSLog(#"Array %#",[NSKeyedUnarchiver unarchiveObjectWithData:data]);
[(Event*)managedObject setValue:data forKey:#"chatArray"];
if ([self managedObject])
{
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Event" inManagedObjectContext:managedObjectContext]];
[(Event *)managedObject setChatArray:data]; }
else {
Event *event = [[[Event alloc] initInsertingIntoManagedObjectContext:managedObjectContext]autorelease];
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Event" inManagedObjectContext:managedObjectContext]];
[event setChatArray:data];
}
NSError *error;
[managedObjectContext save:&error];
NSArray *myArray = [NSKeyedUnarchiver unarchiveObjectWithData:[(Event*)managedObject valueForKey:#"chatArray"]];
NSLog(#"chatArray %#",myArray);
Brandon,
First, in Core Data, BLOBs should be stored in leaf nodes (i.e. an entity that just contains the BLOB and a back to one relationship. (This pattern/convention has emerged because it is almost trivial to get a retain cycle of large blobs when there are other relations in the entity.)
Second, why are you storing these strings as an array and not as an entity with a time stamp, etc.?A BLOB is not particularly more efficient than individual rows plus the system can both search the messages and more flexibly store the rows. SQLite handles strings specially.
Third, it appears that you are composing your class rather than inheriting from your model entity, why? This makes your code more complex.
Finally, It is really hard to tell what you are trying to do. Could you include your full .h file? And the full method declaration?
Andrew

Sorting a to-many relationship when calling NSFetchRequest

I have two entities. Client and Project. A Client has many Projects, but a Project can only be assigned to one Client. This has been designed with the datamodel.
Client entity has attribute clientName and relationship to-many projects
Project entity has attribute projectName and relationship to-one client
I want to retrieve all Clients from my ManagedObjectContext where the Clients are sorted by clientName ascending and then have the Projects for that Client by projectName ascending.
This is my current code, where I KNOW it is wrong since there is no way for Client entity to sort by the projectName:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Client" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Creating the sort descriptors array.
NSSortDescriptor *clientSort = [[NSSortDescriptor alloc] initWithKey:#"clientName" ascending:YES];
// this next row is super wrong
NSSortDescriptor *projectSort = [[NSSortDescriptor alloc] initWithKey:#"projectName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:clientSort. projectSort, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError *error;
// Assign to NSArray of ViewController
clientArray = [[managedObjectContext executeFetchRequest:fetchRequest error:&error] mutableCopy];
After this has been assigned to clientArray, I want to go to another method and ask for the Projects and have them in ascending order. Example:
Client *temp = (Client *)[clientArray objectAtIndex:selectedClient];
NSArray *projectsArray = [temp.projects allObjects];
Project *project = [projectsArray objectAtIndex:selectedProject];
return project.projectName;
I saw an implementation where they got the Clients and then sorted the projects manually... But this is kind of expensive and I'm hoping there are functions to do this when the Context gets queried.
You could add your own custom property to Client
#interface Client : NSManagedObject {
}
// ... other properties
#property (nonatomic,readonly) NSArray* orderedProjects;
#end
#implementation
// ... #dynamic etc
#dynamic orderedProjects;
- (NSArray*)orderedProjects {
NSArray* sort = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"projectName" ascending:YES]]
return [[self.projects allObjects] sortedArrayUsingDescriptors:sort];
}
#end
Now you just have to fetch all clients, which now each have sorted projects. Performance wise I'm not sure this is the best approach but it makes for cleaner code I think.
Try using a key path for the sort, such as project.projectName in the sort descriptor. This is a bit of a shot in the dark because I haven't tried this myself so don't kill me if I'm wrong.

Core Data, UITableView, and UISegmentedControl

I am new to working with Core Data, and am working with a UITableView. I have a toolbar with a UISegmentedController, and I want to filter the items in the UITableView based on the selected index. In other words, suppose I have a UITableView that displays Books (stored in Core Data) and a UISegmentedController with segments to display books in "English", "Spanish", and "French".
What is the approach here to get everything hooked up? When one of the segments is clicked, what do I do in the UISegmentedControl's target to change things around?
Sorry if it's a stupid question!
I would use a separate NSFetchedResultsController for each segment. This will allow you to take advantage of the built in cache for each segment and improve performance.
In addition to Apple's documentation (and my book), you can also read up on them from my article Touching The Core in the PragPub magazine.
Its a good idea to use three different Arrays for each of your filters. Cache them somewhere so their is no delay when the user selects a filter. To find the information you are looking for from your CoreData store use NSPredicate.
You can use NSFetchedResultsController, when you clicked on segment just set the different perdicate and perform fetch again.
I have implemented this as follows using the guidelines from Marcus above(I'm new to this so it may not be the best approach). I have a segment controller with three options for 'open', 'in progress' and 'closed'.
In the ViewController.h, create an iVar for each one of your segment options, and one iVar for the main controller that will store the current controller.
#property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
#property (nonatomic, retain) NSFetchedResultsController *inprogressFetchedResultsController;
#property (nonatomic, retain) NSFetchedResultsController *openFetchedResultsController;
#property (nonatomic, retain) NSFetchedResultsController *closedFetchedResultsController;
In the ViewController.m you need to create methods for lazy loading of these controllers, so I have three in total. They are basically the same except for the predicate and the cacheName, I have only shown one below.
- (NSFetchedResultsController *)closedFetchedResultsController
{
if (_closedFetchedResultsController != nil) {
return _closedFetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Ticket" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"priority.name" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"status = 'Closed'"];
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"priority.name" cacheName:#"ClosedTickets"];
aFetchedResultsController.delegate = self;
self.closedFetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.closedFetchedResultsController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _closedFetchedResultsController;
}
Create an IBAction for your segment so that when it is changed it changes the fetched results controller and reloads the table.
- (IBAction)statusChanged:(id)sender {
switch (self.segmentControl.selectedSegmentIndex) {
case 0:
self.fetchedResultsController = self.inprogressFetchedResultsController;
break;
case 1:
self.fetchedResultsController = self.openFetchedResultsController;
break;
case 2:
self.fetchedResultsController = self.closedFetchedResultsController;
break;
default:
break;
}
[self.tableView reloadData];
}
That's it!
N.B. I also added this line to my ViewDidLoad method so that it would load the correct option into the fetchedResultsController initially.
self.fetchedResultsController = self.inprogressFetchedResultsController;

implementing core data to an existing iPhone-project

I´ve some trouble with implementing Core Data to my existing iPhone-Project. First I wanna give you a more detailed view on it:
Some of my classes are nested into each other: The class "Game" has an NSArray with objects of class "Player", the class "Player" has an NSArray with objects of class "Item" in turn.
What I wanna do is saving an instance of my "uppest" class "Game" (e.g. when leaving my app).
I tried out some tutorials about Core Data, but there are still some questions:
Do I have to create an entity for each of my classes or just for "Game"?
If I have to do it for each one: I think I will have to create ALL relationships between my classes, but: How to create the relationships e.g. between "Game" an "Player" (please remind: I hold MANY players in ONE NSArray)..
What about changing my existing project? What I allready did is copying the missing methods into my AppDelegate. But what will I have to do with my classes, especially with Getter/Setter-methods? Just change "#synthesize" to "#dynamic" in the implementation?
I hope for some light in my dark ;)
Thanks a lot right now
Mac1988
What I recommend is to setup your database model in xcode, then when you have done that... choose the entities and choose from the menu File > New File. Then choose the "Managed Object Class" from the "Cocoa touch class". After "Next" choose where to save the files, and at the next step XCode will ask you which entities should be generated to files.
After you have done that, you can implement the functions you need into your e.g. you delegate. My recommendation is to leave your existing stuff as it is and use the core data classes as their own. Just pull the data you need from you existing classes/arrays and put them in to the database as you need them. When retrieving, the other way around... get them from the DB and add them to your functions / classes.
Example from one of my projects:
The .h file
#class quicklistSet;
#interface rankedAppDelegate : NSObject <UIApplicationDelegate, UITabBarControllerDelegate> {
[...]
NSMutableArray *_searchHistory;
NSMutableArray *_quickList;
}
[...]
#property (nonatomic, retain) NSMutableArray *_searchHistory;
#property (nonatomic, retain) NSMutableArray *_quickList;
/* Quicklist functions */
- (void)addToQuicklist:(quicklistSet *)theQuicklistSet;
- (BOOL)checkIfQuicklistExists:(quicklistSet*)theQuicklistSet;
- (NSMutableArray *)getQuicklists;
- (void)deleteQuicklist:(NSNumber*)theAppId;
#end
The .m file
#import "quicklistSet.h"
#import "quicklist.h"
#implementation rankedAppDelegate
#synthesize window;
#synthesize tabBarController;
#synthesize _searchHistory, _quickList;
[...]
/* Quicklist functions */
- (void)addToQuicklist:(quicklistSet *)theQuicklistSet
{
BOOL exists = [self checkIfQuicklistExists:theQuicklistSet];
if(!exists)
{
quicklist *theQuicklist = (quicklist *)[NSEntityDescription insertNewObjectForEntityForName:#"quicklist"
inManagedObjectContext:self.managedObjectContext];
[theQuicklist setAppName:[theQuicklistSet _appName]];
[theQuicklist setAppId:[theQuicklistSet _appId]];
[theQuicklist setAppImage:[theQuicklistSet _appImage]];
[theQuicklist setCountryId:[theQuicklistSet _countryId]];
[theQuicklist setCategoryId:[theQuicklistSet _categoryId]];
[theQuicklist setLastCheck:[theQuicklistSet _lastCheck]];
[theQuicklist setLastRank:[theQuicklistSet _lastRank]];
[_quickList addObject:theQuicklist];
[self saveAction];
}
else {
NSLog(#"Existing quicklistSet: %#", [theQuicklistSet _appName]);
}
}
- (BOOL)checkIfQuicklistExists:(quicklistSet*)theQuicklistSet
{
// Get the categories
NSMutableArray *quicklistArray = [self getQuicklists];
BOOL exists = NO;
for(quicklist *dbQuicklist in quicklistArray)
{
if([[dbQuicklist appId] isEqualToNumber:[theQuicklistSet _appId]])
{
exists = YES;
continue;
}
}
return exists;
}
- (NSMutableArray *)getQuicklists
{
if(_quickList == NULL)
{
NSLog(#"Array is null");
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"quicklist"
inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSError *error;
NSArray *items = [[self.managedObjectContext
executeFetchRequest:fetchRequest error:&error] retain];
NSMutableArray *returnArray = [[[NSMutableArray alloc] initWithArray:items] retain];
_quickList = returnArray;
[fetchRequest release];
}
else {
NSLog(#"Not null. Count: %d", [_quickList count]);
}
return _quickList;
}
- (void)deleteQuicklist:(NSNumber*)theAppId
{
NSLog(#"Delete row");
// Create a new managed object context for the new book -- set its persistent store coordinator to the same as that from the fetched results controller's context.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"quicklist"
inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"appId=%#",theAppId];
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray *items = [self.managedObjectContext
executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
if([items count] > 0)
{
NSManagedObject *eventToDelete = [items objectAtIndex:0];
[self.managedObjectContext deleteObject:eventToDelete];
[self saveAction];
}
}
/* END Quciklist functions */
[...]
#end
EDIT:
The quicklistSet was my existsing class, the quicklist is my coredata class.
Yes, you want to create an entity for all of the classes you mentioned.
You've already got the answer to this in your question: make a one-to-many relationship. For example, for the players relationship of Game, click the "To-many relationship" checkbox in the data model editor.
You'll want to have your data classes (Game, Player, Item) inherit from NSManagedObject. You'll probably want to remove all of the instance variables that correspond to the attributes you added in Core Data. For the to-many relationships (players, items), you'll definitely want to get rid of the NSArray member variable you were using. Instead, do like you were saying and create #dynamic accessors for the players and items properties. Note that you want to use an NSSet instead of an NSArray for players and items.
For example, the header for your Game class might look like this:
#interface Game : NSManagedObject {
}
#property(nonatomic, retain) NSSet *players
#property(nonatomic, retain) NSString *someOtherProperty;
#property(nonatomic, retain) NSNumber *yetAnotherProperty;
#end
And then your implementation file might look like:
#import "Game.h"
#implementation Game
#dynamic players, someOtherProperty, yetAnotherProperty;
- (void)awakeFromInsert {
// do initialization here
}
// other methods go here
#end
Also, be careful when modifying the players and items properties. The Using Managed Objects section of the Core Data Programming guide has a lot of good details, but basically to add a Player to a Game instance, you would do
[game addPlayerObject:newPlayer];
To actually create the new player, you would do something like:
NSManagedObject *newPlayer = [NSEntityDescription insertNewObjectForEntityForName:#"Player" inManagedObjectContext:context];