iPhone Core Data objects added to NSMutableArray - iphone-sdk-3.0

I am trying to have a bunch of Core Data objects (NSStrings) be added to an NSMutableArray so that I pickerView can use the strings as its data values.
Currently I am able to save strings from a textField one at a time into the sqlite file, in Core Data.
I am having trouble pulling those Objects back out of the Core Data sqlite file and into a NSMutable array.
This is the code to add objects to the sqlite file...
-(IBAction)addToPicker
{
CoreDataPickerAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSManagedObject *thePerson = [NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:context];
[thePerson setValue:textField.text forKey:#"eventName"];
[context save:&error];
textField.text=#"";
self.viewDidLoad;
[textField resignFirstResponder];
}
Right now I have...
- (void)viewDidLoad {
NSMutableArray *array = [[NSMutableArray alloc] init];
//code needed to send all the objects in the sqlite Core Data store into array to be read by the pickerView
self.pickerData = array; // the picker uses NSMutableArray pickerData for its data
[array release];
[super viewDidLoad];
}
any help would greatly be appreciated.
Chris

You can use the NSFetchRequest to fetch the data into the array. Take a look at the Core Data Programming guide - Fetching Managed Object. You just need to read the first section of the link.
Executing a fetch request will return an array
NSArray *array = [moc executeFetchRequest:request error:&error];

Related

store result from wcf service in core data

I am using a WCF service in my app.When the app is run for the first time on the iPad,I want it to call a WCF service and display the result in a UITableView.Alongwith displaying the data in UITableView,i want to store the data in Core Data so when the user is "offline"(not connected to wifi)the data will be displayed from the Core Data.The AppDelegate.m looks like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults objectForKey:#"firstRun"])
{
self.firstRun = TRUE;
[defaults setObject:[NSDate date] forKey:#"firstRun"];
}
else
{
self.firstRun = FALSE;//flag does exist so this ISNT the first run
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
The code in UITableView looks like this:
- (void)viewDidLoad
{
[super viewDidLoad];
[my_table setDataSource:self];
[my_table setDelegate:self];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.firstRun){
NSLog(#"IS FIRST RUN");
EDViPadDocSyncService *service = [[EDViPadDocSyncService alloc]init];
[service getAllCategories:self action:#selector(handleGetAllCategories:)];
}
else
{
NSLog(#"NOT FIRST RUN");
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Categories" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSError *errormsg;
self.allCats = [managedObjectContext executeFetchRequest:fetchRequest error:&errormsg];
NSLog(#"allCATS=%#",self.allCats);
self.title = #"Categories";
}
}
-(void)handleGetAllCategories:(id)value
{
if([value isKindOfClass:[NSError class]])
{
NSLog(#"This is an error %#",value);
return;
}
if([value isKindOfClass:[SoapFault class]])
{
NSLog(#"this is a soap fault %#",value);
return;
}
NSMutableArray *result = (NSMutableArray*)value;
NSMutableArray *categoryList = [[NSMutableArray alloc] init];
NSMutableArray *docCount = [[NSMutableArray alloc]init];
NSMutableArray *catIdList = [[NSMutableArray alloc]init];
self.myData = [[NSMutableArray array] init];
self.myDocCount = [[NSMutableArray array]init];
self.catId = [[NSMutableArray array]init];
for (int i = 0; i < [result count]; i++)
{
EDVCategory *catObj = [[EDVCategory alloc]init];
catObj = [result objectAtIndex:i];
[categoryList addObject:[catObj categoryName]];
[docCount addObject:[NSNumber numberWithInt:[catObj docCount]]];
[catIdList addObject:[NSNumber numberWithInt:[catObj categoryId]]];
}
self.myData = categoryList;
self.myDocCount = docCount;
self.catId = catIdList;
[my_table reloadData];
/*store data in Core Data - START*/
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *newCategory;
for(int j=0;j<[result count];j++)
{
newCategory = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:context];
/*HOW TO STORE DATA FOR THE "CATEGORIES" OBJECT IN CORE DATA*/
}
/*store data in Core Data - END*/
}
I am not able to figure out how to store the data received from the wcf service to the core data object directly.I know how to store it from a text box on the screen to a core data object.eg.:-
coreDataAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSManagedObject *newContact;
newCat = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:context];
[newCat setValue:name.text forKey:#"name"];
name.text = #"";
[context save:&error];
But this doesn't help in my case.Any help is appreciated.
You are mixing networking and UI code. It is a recipe for unmaintainable code.
Your UI should be looking at Core Data and only Core Data to display its data.
Separately, and asynchronously you should be requesting data from WCF and pushing it into Core Data.
Your UI does not need to care about first run vs. subsequent run. It just looks at Core Data via a NSFetchedResultsController.
Your network code is the only part that cares about new vs. update.
Update 1
how can I achieve this? When the app is running and connected to WiFi,it has to get the latest data from the WCF service.
NSURLConnection can do async requests built-in. I generally recommend writing your networking code as NSOperation subclasses and then put them into a queue.
It appears that WCF can return XML and takes standard HTTP requests. Therefore you can write NSOperation subclasses that build your request, send it to the server and wait for a reply. When the reply comes you parse the XML and insert it into Core Data. When you save the Core Data NSManagedObjectContext your NSFetchedResultsController instances will automatically fire and allow you to update your UI.
I have several code samples that perform these feats although they are written for JSON responses as opposed to XML responses. It would not be difficult to take those examples and alter them to your needs.
You can start with this stackoverflow question and its response.
To store the data into the attributes of your NSManagedObject, simply set the values using KVC:
EDVCategory *catObject = [result objectAtIndex:j];
[newCategory setValue:[catObject categoryName] forKey#"categoryName"];
[newCategory setValue:[catObject docCount] forKey#"docCount"];
[newCategory setValue:[catObject categoryID] forKey#"categoryID"];
// after the loop
[context save:&nil];

Core Data Saving Attributes Of Entities

This question is about Core Data.
I created a Entity called TV with three attributes called name, price and size. I also created a subclass of NSMutableObject with TV.h and TV.m files.
I imported the TV.h to my DetailViewController.h which handles my sliders und UIElements I want to take the values of.
So I did a fetch request, and everything works fine, BUT:
Everytime I update the UISlider (valueDidChange:), Xcode creates a COPY of my entity and adds it to my TV-Object.
All I want Xcode is just to edit and save to the current entity, not to edit and save in a new entity.
Help is very appreciated!
Thank you in advance.
My Code:
DetailViewController.m
- (IBAction)collectSliderValue:(UISlider *)sender {
if (__managedObjectContext == nil) {
NSLog(#"Problem ...");
__managedObjectContext = [(MasterViewController *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSLog(#"... solved!");
}
if (sender == sizeSlider) {
NSError *error = nil;
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"TV" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
TV * currentTV = [[TV alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
currentTV.size = [[NSNumber alloc] initWithInt:(sender.value + 0.5f)];
currentTV.name = #"New TV!";
NSError *error11;
[__managedObjectContext save:&error11];
for (NSManagedObject *info in fetchedObjects)
{
NSLog(#"Name = %#", [info valueForKey:#"name"]);
NSLog(#"Size = %#", [info valueForKey:#"size"]);
NSLog(#"Price = %#", [info valueForKey:#"price"]);
}
[fetchRequest release];
}
//Editing begins ...
TV * currentTV = [[TV alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
Editing doesn't begin, you are creating a new object right there. Your view controller needs an instance variable to hold the current TV entity that you are modifying.
From the template project you have created, the variable detailItem contains the managed object that you are currently editing. You should specifically set this as a TV object, and refer to this instead of currentTV in your detailViewController code. You must remove all of the fetch request and managed object context code - this is not relevant in your detail view controller, it should be managed by the master view controller.
So, in DetailViewController.h:
#property (strong, nonatomic) id detailItem;
becomes
#property (strong, nonatomic) TV detailItem;
And in your collectSliderValue method, it should look something much more simple like this:
- (IBAction)collectSliderValue:(UISlider *)sender
{
if (sender == sizeSlider)
self.detailItem.size = [NSNumber numberWithFloat:sender.value];
}
The saving of the managed object context shouldn't occur until back in your detail view controller, this is taken care of in your application delegate.
In your master detail controller .m file you may also need to import the TV.h file so that it knows that TV is a NSManagedObject subclass. Also, cast to TV when you are setting the detail item:
self.detailViewController.detailItem = (TV*)selectedObject;

Can you set the tableView datasource values in "connectionDidFinishLoading"?

I'm working on my first JSON example in objective-c and came across this great tutorial that I'm trying to reproduce. Along the way I decided to push the JSON returned into my already working tableView (just to ensure I could do something w/ the data in the view).
- (void)viewDidLoad {
[super viewDidLoad];
responseData = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.unpossible.com/misc/lucky_numbers.json"]];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[connection release];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
[responseData release];
NSArray *luckyNumbers = [responseString JSONValue];
NSMutableString *text = [NSMutableString stringWithString:#"Nums "];
for (int i = 0; i < [luckyNumbers count]; i++)
[text appendFormat:#"%#", [luckyNumbers objectAtIndex:i]];
self.movies = [[NSArray alloc] initWithObjects:#"First", text, #"Last", nil];
}
What I've found is that when I set the array in "connectionDidFinishLoading" it shows up as nothing in the running application - yet if I set this directly in the "viewDidLoad" method with 3 simple string values it shows up fine.
When I debug the running application I see the JSON response and the string looks valid (no issues that I can see).
Is the datasource for my tableView already set in stone before this "connectionDidFinishLoading" method or did I miss something?
Your UITableView will call upon its DataSource for data once initially, presumably sometime after viewDidLoad. After that first load, it will only request data as it needs it (i.e. as you scroll to different cells.) If you want to make it refresh its contents when your data is ready (like after you've received your URL data), call [tableView reloadData].
My initial question was solved by this solution:
At the end of my "connectionDidFinishLoading" method I call a method on the appDelegate called "jsonFinished".
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//do all the json work and set the array that I'm using as my datasource
self.movies = [[NSArray alloc] initWithObjects:#"First", "Last", nil];
[appDelegate jsonFinished]; //have the app delegate do the refresh call back
}
Then inside the appDelegate I simply provide an implementation for the "jsonFinished" method that does a refresh of the UITableView
- (void)jsonFinished
{
moviesController.refreshDisplay;
}
And in the "refreshDisplay" method I do the reloadData on the tableView
- (void)refreshDisplay
{
[moviesTableView reloadData];
}
And now after the data is loaded the appDelegate fires off the method that reloads the data for tableView

NSTokenFieldCell Subclass to force use of Core Data To-Many Relationship

I have come across an interesting conundrum (of course, I could just being doing something horribly wrong).
I would like an NSTokenField to "represent" a relationship in a Core Data Application. The premise is such: You click on a Note from a TableView (loaded from the Notes Array Controller). The token field is then bound (through "value") to the Notes Array Controller selection.Tags. Tags is a to-many relationship on the entity Notes.
Obviously, an NSTokenField will not accept the NSSet that the Array Controller Provides it. To get around this, I subclassed NSTokenFieldCell and overrode its objectValue and setObjectValue: methods. I thought that I could simply translate the NSSet that was being provided to the NSArray that the NSTokenFieldCell expected. (Note: I originally tried overriding these methods on a NSTokenField subclass; however, they were not being called.)
So, I came up with said code:
- (void)setObjectValue:(NSSet*)object {
tagsList = [object copy];
NSMutableArray *displayList = [[NSMutableArray alloc] init];
for (id newObject in tagsList) {
[displayList addObject:[newObject valueForKey:#"Name"]];
}
[super setObjectValue:displayList];
}
- (id)objectValue {
NSArray *displayList = [super objectValue];
NSEntityDescription *tagEntity = [NSEntityDescription
entityForName:#"Tag"
inManagedObjectContext:[appDelegate
managedObjectContext]];
NSMutableSet *returnValue = [[NSMutableSet alloc] init];
for (NSString *token in displayList) {
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:tagEntity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"Name == %#", token];
[request setPredicate:predicate];
NSError *error;
NSArray *results = [[appDelegate managedObjectContext] executeFetchRequest:request error:&error];
if (results == nil) {
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:[appDelegate managedObjectContext]];
[object setValue:token forKey:#"Name"];
[returnValue addObject:object];
} else {
[returnValue addObject:[results objectAtIndex:0]];
}
}
return returnValue;
}
It crashes. :( And, surprisingly it crashes on the line that calls [super objectValue]. It gives me the error:
-[NSConcreteAttributedString countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance ...
Sigh. The sad thing is that when I go into the Core Data XML file and give the Note a Tag, it displays correctly, and [super setObjectValue:] is passed an array of strings. However, as soon as I enter something else and mouse away, I get the error.
I am not sure what to do about this. Can anyone spot anything horribly wrong with this? Thanks.
UPDATE:
If it makes a difference, I do not have a delegate configured for the TokenField.
In typical SO fashion, I found the answer to my own question. It was silly to begin with. I simply needed another ArrayController bound to the Notes selection.Tags set. Then, I bound the NSTokenField to the ArrangedObjects of that Controller, implemented some delegate methods. Boom. Simple.
Silly me.

Adding unique objects to Core Data

I'm working on an iPhone app that gets a number of objects from a database. I'd like to store these using Core Data, but I'm having problems with my relationships.
A Detail contains any number of POIs (points of interest). When I fetch a set of POI's from the server, they contain a detail ID. In order to associate the POI with the Detail (by ID), my process is as follows:
Query the ManagedObjectContext for the detailID.
If that detail exists, add the poi to it.
If it doesn't, create the detail (it has other properties that will be populated lazily).
The problem with this is performance. Performing constant queries to Core Data is slow, to the point where adding a list of 150 POI's takes a minute thanks to the multiple relationships involved.
In my old model, before Core Data (various NSDictionary cache objects) this process was super fast (look up a key in a dictionary, then create it if it doesn't exist)
I have more relationships than just this one, but pretty much every one has to do this check (some are many to many, and they have a real problem).
Does anyone have any suggestions for how I can help this? I could perform fewer queries (by searching for a number of different ID's), but I'm not sure how much this will help.
Some code:
POI *poi = [NSEntityDescription
insertNewObjectForEntityForName:#"POI"
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
poi.POIid = [attributeDict objectForKey:kAttributeID];
poi.detailId = [attributeDict objectForKey:kAttributeDetailID];
Detail *detail = [self findDetailForID:poi.POIid];
if(detail == nil)
{
detail = [NSEntityDescription
insertNewObjectForEntityForName:#"Detail"
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
detail.title = poi.POIid;
detail.subtitle = #"";
detail.detailType = [attributeDict objectForKey:kAttributeType];
}
-(Detail*)findDetailForID:(NSString*)detailID {
NSManagedObjectContext *moc = [[UIApplication sharedApplication].delegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Detail" inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"detailid == %#", detailID];
[request setPredicate:predicate];
NSLog(#"%#", [predicate description]);
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array == nil || [array count] != 1)
{
// Deal with error...
return nil;
}
return [array objectAtIndex:0];
}
Check out the section titled "Batch Faulting" on the page titled "Core Data Performance" in Xcode's Core Data Programming Guide that Norman linked to in his answer.
Only fetching those managedObjects whose ids are IN a collection (NSSet, NSArray, NSDictionary) of ids of the objects returned by the server may be even more efficient.
NSSet *oids = [[NSSet alloc] initWithObjects:#"oid1", #"oid2", ..., nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"oid IN %#", oids];
[oids release];
UPDATE: I worked this tip into a solution for the acani usersView. Basically, after downloading a JSON response of users, the iPhone uses the popular open source JSON framework to parse the response into an NSArray of NSDictionary objects, each representing a user. Then, it makes an NSArray of their uids and does a batch fetch on Core Data to see if any of them already exist on the iPhone. If not, it inserts it. If so, it updates the ones that do exist only if their updated attribute is older than that of the one from the server.
I've gotten all this to work really well, thanks to Norman, who put me on the right path. I'll post my helper class here for others.
Basically, my helper class will look up if an NSManagedObject exists for some ID, and can create it for some ID. This executes quickly enough for me, with 1,000 find/create operations taking around 2 seconds on my iPhone (I also did a few other things there, pure find/create is likely faster).
It does this by caching a dictionary of all the NSManagedObjects, and checking that cache rather than executing a new NSFetchRequest.
A couple of modifications that could help things speed up even further:
1. Get only selected properties for the NSManagedObjects
2. Only get the identifier property for the NSManagedObject into a dictionary, instead of the whole object.
In my performance testing, the single query wasn't the slow part (but with only 1,000 items, I'd expect it to be fast). The slow part was the creation of the items.
#import "CoreDataUniquer.h"
#implementation CoreDataUniquer
//the identifying property is the field on the NSManagedObject that will be used to look up our custom identifier
-(id)initWithEntityName:(NSString*)newEntityName andIdentifyingProperty:(NSString*)newIdProp
{
self = [super init];
if (self != nil) {
entityName = [newEntityName retain];
identifyingProperty = [newIdProp retain];
}
return self;
}
-(NSManagedObject*)findObjectForID:(NSString*)identifier
{
if(identifier == nil)
{
return nil;
}
if(!objectList)
{
NSManagedObjectContext *moc = [(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:entityName inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
objectList = [[NSMutableDictionary dictionary] retain];
for (NSManagedObject* p in array) {
NSString* itemId = [p valueForKey:identifyingProperty];
[objectList setObject:p forKey:itemId];
}
}
NSManagedObject* returnedObject = [objectList objectForKey:identifier];
return returnedObject;
}
-(NSManagedObject*)createObjectForID:(NSString*)identifier
{
NSManagedObject* returnedObject = [NSEntityDescription
insertNewObjectForEntityForName:entityName
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
[returnedObject setValue:identifier forKey:identifyingProperty];
[objectList setObject:returnedObject forKey:identifier];
return returnedObject;
}
- (void) dealloc
{
DESTROY(entityName);
DESTROY(identifyingProperty);
[super dealloc];
}
#end
This page provides some help on optimizing performance:
http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles/cdPerformance.html#//apple_ref/doc/uid/TP40003468-SW1
While not very efficient, why not just build them in-memory with a NSDictionary? Read everything from Core Data into a NSDictionary then merge in your data, replacing everything in Core Data.