So im really new to core data, but i went through a tutorial and pretty much understand it, well at least the idea behind most of the things. But I still have 1 question that i cant find anywhere. It seems really simple but here it is. If I were to have two strings inside one entity lets say:
1.name
2.position
If the name is already entered how might i allow a user to enter text into a textField and assign it to their position at a later time? Even if there were 20 names, considering no duplicates?
I was thinking it might be something like this...But it doesnt seem to work.
UserInfo *userInfo = (UserNumber *)[NSEntityDescription insertNewObjectForEntityForName:#"UserInfo" inManagedObjectContext:managedObjectContext];
if ([userName isEqualToString:"#James"]) {
userInfo.Position = nameField.text;
}
On the code above you are casting (UserNumber*) to an object that you are declaring as (UserInfo*)? Which is what and is there any reason why you are doing that?
If I understand your question correctly, you want to create a record with only the username pre-populated and then allow that record to be updated at a later stage.
I will assume your entity is called UserInfo and that there are 2 NSString properties created for it - userName and position. I also assume you have created the class files for UserInfo and imported the header into the relevant view controllers.
Here's how you would do it:
1) Firstly, assuming you have username typed in a UITextField *userNameField, let's create a new record.
UserInfo *userInfo = (UserInfo*)[NSEntityDescription insertNewObjectForEntityForName:#"UserInfo" inManagedObjectContext:self.managedObjectContext];
[userInfo setValue:userNameField.text forKey:#"userName"];
This will create a new instance of UserInfo in your managed object context and set the value of userName to the value on userNameField.text
Then at a later stage a user will get to a point where they can update their records in your app (you may need to think about authentication somewhere here). You will fetch the record that matches your specified username:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSPredicate *userNamePredicate = [NSPredicate predicateWithFormat:#"(userName == %#)", userNameField.text];
[fetchRequest setPredicate:userNamePredicate];
NSEntityDescription *userInfo = [NSEntityDescription entityForName:#"UserInfo" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:userInfo];
NSError *error;
NSArray *fetchRequestArray = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
If the fetchRequest found match(es) to your userNameField.text paramater, they will be saved in the fetchRequestArray. There should only be a maximum of one object there if you take the necessary steps to make the userName property mandatory AND unique.
Access the object by grabbing the objectAtIndex:0 in the array and change it's position property:
UserInfo *userInfoToBeEdited = [fetchRequestArray objectAtIndex:0];
[userInfoToBeEdit setValue:positionTextField.text forKey:#"position"];
In both cases above, remember to invoke CoreData's save method when you are ready to commit your changes. Before save is invoked your changes are only kept in your managed object context which is basically a scratch pad for your persistent data.
[EDIT TO ADD SAVE METHOD]
As per your comment, I usually have the save method below in my AppDelegate (copy/paste directly from Apple template)
- (void)saveContext
{
error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
{
[self seriousErrorAlert];
}
}
}
And then whenever I need to save changes, from any view controller I simply grab a reference to my AppDelegate and fire it off:
AppDelegate *theDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[theDelegate saveContext];
Related
I have a coredata project that I'm trying to programmatically update a number.
I'm retrieving objects from CoreData and then storing it into an array.
Then, I'm looping through that array to see if the current user's IP is present in the database and trying to update the number of times accessed for that specific array.
The problem is, it's updating all the objects, not just the current object in the looped array.
First, I get the info from core data like so:
- (void)fetchRecords {
// Define our table/entity to use
NSEntityDescription *entity = [NSEntityDescription entityForName:#"IPAddr" inManagedObjectContext:managedObjectContext];
// Setup the fetch request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
// Define how we will sort the records
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"ipDate" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[request setSortDescriptors:sortDescriptors];
// Fetch the records and handle an error
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (!mutableFetchResults) {
// Handle the error.
// This is a serious error and should advise the user to restart the application
}
// Save our fetched data to an array
[self setIpArray: mutableFetchResults];
}
Now, I'm trying to find if the current User IP is present in the fetched results, and if it's present, update the number of times accessed:
// see if the ip is present and update if necessary
-(void)ipPresent {
NSString * theCurrentIP = [self getGlobalIPAddress];
for (IPAddr *allips in ipArray)
{
if ([allips.ipNum isEqualToString:theCurrentIP]) {
NSLog(#"The IP %# was found.", theCurrentIP);
// update the ip
NSError *error = nil;
NSNumber *ipToUpdate = allips.ipAccess;
NSNumber *addIpAccess = [[NSNumber alloc] initWithInt:1];
NSNumber *updateIpAddress = [NSNumber numberWithFloat:([ipToUpdate floatValue] + [addIpAccess floatValue])];
[self.ipArray setValue:updateIpAddress forKey:#"ipAccess"];
if ([self.managedObjectContext save:&error]) { // write to database
NSLog(#"The IP Was Updated from %# to %#", ipToUpdate, updateIpAddress);
} else if (![self.managedObjectContext save:&error]) {
NSLog(#"failed with error: %#", error);
}
break;
} else {
NSLog(#"The IP %# was NOT found.", theCurrentIP);
}
}
}
I'm pretty sure the issue is with this line:
[self.ipArray setValue:updateIpAddress forKey:#"ipAccess"];
Again, it's updating ALL the entities and not just the one in the current loop.
Indeed. You are using the wrong method. self.ipArray is a NSMutableArray.
The method
- (void)setValue:(id)value forKey:(NSString *)key
is used for Key-Value Coding (which is what makes it work for Core Data objects), but when applied to an array, it will invoke setValue:forKey: on each entry in the array.
Now, you can see that you could also call setValue:forKey on the one single array element allips since its property is obviously KVC compliant -- otherwise you would be having a different problem, not see the values being set.
Note, that you could also just assign the property...
allips.ipAccess = updateIpAddress;
EDIT
Sorry, probably should have read slower... You do understand that you don't have to use a mutable array, right? You are not actually changing the array, just the elements in the array. An immutable collection means that the collection contents can not change, but when you have a pointer to an object, as long as that object is not immutable, you can still mutate its properties.
Thus, if you had an immutable array of Foo objects, you could do this...
for (Foo *foo in myImmutableArray) {
Bar *bar = [self getSomeNewBar];
[foo setBar:bar];
// If Foo is KVC compliant, you can do this too...
[foo setValue:bar for Key:#"bar"];
}
If, however, you call setValue:forKey on the array, it will be invoked for each element of the array. Note, that setValue:forKey is actually declared in the immutable NSArray.
EDIT
That comment was hard to read.
The core data object is just another object. It looks like you have subclassed it, and provided it with properties for the attributes. Just replace
[self.ipArray setValue:updateIpAddress forKey:#"ipAccess"];
with
[allips setValue:updateIpAddress forKey:#"ipAccess"];
or
allips.ipAccess = updateIpAddress;
Either of those should modify your core data object, as they would any object that had a read/write property named "ipAccess"
Assuming, of course, that I didn't read it wrong again... and allips is your core data object...
I see that there is a method of getting the unique id of a managed object:
NSManagedObjectID *moID = [managedObject objectID];
But, as I have read, that changes when it is saved.
What is a better way of creating my own unique ID and saving it in each object, making sure that it IS unique and does't change?
You cannot save the NSManagedObjectID in a CoreData entity, and there's no properties in it that can be intended as integer or string ID.
Therefore building your own unique identifier algorithm is an acceptable solution, if it's impossible for you to keep track when the entity is saved, I did this for some applications.
For example I had a similiar problem time ago, when I needed to fill a UITableView cells with a reference to the entity, and retrieve the entity after clicking/touching the cell.
I found a suggestion by using the uri representation, but however I still needed to save the context before, but I manage to use the NSFetchedResultsController and found a more solid solution rather than an application built id.
[[myManagedObject objectID] URIRepresentation];
Then you can later retrieve the managed object id itself:
NSManagedObjectID* moid = [storeCoordinator managedObjectIDForURIRepresentation:[[myManagedObject objectID] URIRepresentation]];
And with the moid I could retrieve your managed object.
I have a created date property in my object, so I just ended up using that date including seconds, which, seems to me like it will be unique and work how I want.
You can create an id field for your object, and populate it during init using GUID. for how to create a GUID and optionally export it to string see UUID (GUID) Support in Cocoa
If it helps anyone else searching for this, this is how I do it:
Create an ID property on the object
Get the last used ID when creating an object with this code:
Playlist *latest;
// Define entity to use and set up fetch request
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Playlist" inManagedObjectContext:managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
// Define sorting
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"listID" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
// Fetch records and handle errors
NSError *error;
NSArray *fetchResults = [managedObjectContext executeFetchRequest:request error:&error];
if (!fetchResults) {
NSLog(#"ERROR IN FETCHING");
}
if ([fetchResults count] == 0) {
latestID = 0;
} else {
latest = [fetchResults objectAtIndex:0];
latestID = latest.listID;
}
Increment this by one on the new object.
Maybe I'm not going about showing a detail for a selected row using CoreData, but I can't figure out why I'm getting a "BAD_ACCESS" error. I've googled around and can't find what I'm looking for.
Basically I use CoreData to populate the data for a Table View. It retrieves all of the title attributes for all of the entities. When the user clicks on a row, I have a Detail View that needs to show the description for that entity. I think I need to make a new NSManagedObjectContext and a new NSEntityDescription for a new NSFetchRequest in my DetailViewController and then use a NSPredicate to say "where title = [user selected title]". I get an error when I select a row. See code:
- (void)viewDidLoad
{
// Do any additional setup after loading the view from its nib.
// Get the objects from Core Data database
Caregiver_Activity_GuideAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Definition"
inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"(title = %#)", self.title];
[request setPredicate:pred];
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(#"There was an error!");
// Do whatever error handling is appropriate
}
for (NSManagedObject *oneObject in objects) {
[definitionDescriptionTextView setText:[oneObject valueForKey:#"desc"]];
}
[objects release];
[request release];
[super viewDidLoad];
}
I comment out that code and everything works. But when I try to debug with breakpoints, nothing catches. So I'm more confused.
I know CoreData is probably overkill for what I'm doing but this is a learning app for me.
EDIT: I didn't include that I'm using a sqlite database that is pre-populated with the entities.
You can also download my project on my github page.
Normally, with a Core Data backed Master-Detail interface, you don't fetch for the Detail view.
When you select a row in the Master tableview, you are selecting a particular managed object instance. You then pass that managed object instance to the detail view. There is no need to refetch the object that you selected in the tableview.
A good example of this would be the Contacts app. The Master tableview would be a list of Contact objects (displaying the name.) When you select a row, the Master tableview controller takes the specific Contact object associated with the selected row and then passes it to the Detail view controller which then populates the Detail view using data taking from the properties of the passed Contact object.
So, that entire code block where the error occurs is unnecessary.
However, the immediate error in this code is that you are releasing an object you didn't create. In this line:
NSArray *objects = [context executeFetchRequest:request error:&error];
... you are not creating a NSArray instance with a init, new or create method. Instead, you are merely receiving an autoreleased NSArray instance created and returned by the context NSManagedObjectContext instance. When you release an object you did not create here:
[objects release];
... you cause the crash.
Conversely, you do create a NSFetchRequest here:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
... because you used init so you do have to balance that with:
[request relwase];
BTW, this type of code should not be put in viewDidLoad as the method is only called when the view is read in the first time from the nib file on disk. That is only guaranteed to happen once as the view may remain in memory when the user switches to another view. Instead, put code that needs to run each time the view appears in viewWillAppear.
I'm getting this weird error from Core Date and I cant understand why.
The code below is executed when I delete a row of a UITableView.
I pass a string and an object to the method below and it fetches the article in a database table that has that string and has a foreign key to that object. Then I delete that object and reload the table.
- (void)deleteFavorite:(NSString *)link inFolder:(Favorites *)f {
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *favsDecriptor = [NSEntityDescription entityForName:#"Favorites" inManagedObjectContext:context];
[request setEntity:favsDecriptor];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(belongsTo == %#) AND (link = %#)", f, link];
[request setPredicate:predicate];
NSError *error = nil;
NSMutableArray *fav = [[NSMutableArray alloc] init];
fav = [[context executeFetchRequest:request error:&error] retain];
if (![context save:&error]) {
NSLog(#"Cannot fetch the story from the fetch request.");
}
NSLog([[fav objectAtIndex:0] title]);
error = nil;
[context deleteObject:[fav objectAtIndex:0]];
if (![context save:&error]) {
NSLog(#"Can't delete the fav! %#", error);
}
}
The app instantly crashes and I get this message in the console.
But when I launch the app afterwards, the row has been deleted.
Detected an attempt to call a symbol in system libraries that is not present on the iPhone:
_Unwind_Resume called from function _PFFaultHandlerLookupRow in image CoreData.
Please help!
Thanks in advance to everyone!
This is probably related to a bug within Core Data itself. I had the same error come up (I asked about it here in SO) and my only fix was to change the keywords in the predicate that still allowed the same results. It took some experimenting to find the right combination. Not ideal, but that's the best answer I can offer based on my experience.
Is it possible that you are holding a reference to the delete object or that the deleted object is an observer and is getting a callback after its been deleted? I had something similar to this recently, though slightly different error message. In my case, I also crashed upon deletion (under some conditions) but when I relaunched the object-to-be-deleted had, in fact, been deleted.
If you haven't already done so, under the Run menu select Stop on Objective-C Exceptions. This helped me track down the root cause of my crash. In my case it was KVO observer getting callback of change of value of a property of deleted NSManagedObject.
I have a "Song" Entity and a "Tag" entity and they have a many to many relationship between them. A Song can have multiple Tags and a Tag can be applied to multiple Songs.
I want to check if a Song has a particular Tag associated with it. If the Song has the Tag associted with it, I want to show a checkmark in the table view.
For a similar logic, in Apple "TaggedLocations" sample code, the following check is made to check for the presence of the relationship.
if ([event.tags containsObject:tag]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
This may be inefficient if there are a lot of Tags in the database as this will fetch all of them in the memory. Please correct me if I am wrong here.
Is there a more efficient way to check if the Song is associated with a particular Tag instead of checking in Song.Tags?
It's actually pretty easy to do, if completely undocumented. You want to create a fetch request with a predicate that has a set operation. If we imagine that your Tag model has a property called tagValue, the predicate you care about is "ANY tags.tagValue == 'footag'"
NSString *tagSearch = #"footag";
// However you get your NSManagedObjectContext. If you use template code, it's from
// the UIApplicationDelegate
NSManagedObjectContext *context = [delegate managedObjectContext];
// Is there no shortcut for this? Maybe not, seems to be per context...
NSEntityDescription *songEntity = [NSEntityDescription entityForName:#"Song" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:songEntity];
// The request looks for this a group with the supplied name
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY tags.tagValue == %#", tagSearch];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
[request release];
You are correct, using that code will retrieve the entire set and the object comparison may be quite complex, depending on how many properties and relationship are part of the object's entity.
Anyway, you can not avoid a set comparison for inclusion. Probably, the best you can do is to avoid fetching all of the properties/relationships by asking Core Data to retrieve NSManagedObjectID Objects only.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Tag" inManagedObjectContext:[self managedObjectContext]]];
[fetchRequest setResultType:NSManagedObjectIDResultType];
NSManagedObjectID objects are guaranteed to be unique, therefore you can safely use them to check for set inclusion. This should be much more efficient for a performance perspective.