I am attempting to create a way for users to import contacts to their phone. How it works is this:
There are two managed object contexts. The "real" context has the current data in their address book. The "other" context has incoming data from another source. Both share the same PersistentStoreCoordinator.
I match people by e-mail, so if a contact in the "real" context matches one in the "other", I don't save the other.
When I start the program, I have two entries in the "real" context that I can fetch fine.
Then, I import two other contacts an add them to the "other" context.
When I perform a fetch operation on the "other" context, I get FOUR results - two from the "real" context and two I just added to the "other" context.
However, when I merge the changes, my scheme for detecting duplicates works.
Is there something I'm missing with my understanding of Core Data? How can I make it so that my querying of the "other" context just returns the new results.
The full code is really long, but here's the important part:
AppDelegate *appDel = (AppDelegate*)[[UIApplication sharedApplication] delegate];
// Check to see the original data
NSManagedObjectContext *realContext = [appDel managedObjectContext];
NSFetchRequest *usersFetch= [[[NSFetchRequest alloc] init] autorelease];
[usersFetch setEntity:[NSEntityDescription entityForName:#"User" inManagedObjectContext:realContext]];
[usersFetch setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"email" ascending:YES]]];
NSArray *users = [realContext executeFetchRequest:usersFetch error:&error];
[usersFetch release];
NSLog(#"%#",users); // Returns 2 original objects already in database
otherContext = [[NSManagedObjectContext alloc] init];
[otherContext setPersistentStoreCoordinator:[[appDel managedObjectContext] persistentStoreCoordinator]];
for (contacts in fetchedData){
User *newUser = (User*)[NSEntityDescription insertNewObjectForEntityForName:#"User" inManagedObjectContext:otherContext];
newUser.email = fetchedData.email;
newUser.firstName = fetchedData.firstName;
// etc.
}
NSFetchRequest *newUsersFetch = [[[NSFetchRequest alloc] init] autorelease];
[newUsersFetch setEntity:[NSEntityDescription entityForName:#"User" inManagedObjectContext:otherContext]];
[newUsersFetch setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"email" ascending:YES]]];
NSLog(#"%#",[otherContext registeredObjects]); // 2 objects that were just added
NSArray *newUsers = [otherContext executeFetchRequest:newUsersFetch error:&error];
NSLog(#"%#",[otherContext registeredObjects]); // 4 objects - added AND original
NSLog(#"Count: %i",[newUsers count]); // Count: 4
I think you may be conceptually confusing the managed object contexts with the persistent store. You can have an arbitrary number of context attached to a particular store and the changes made in any single context will eventually show up in all the others.
This is especially true of fetches which go directly to the store to find objects. Your code is working as expected if you call a save on the other context. Once you save an object, it goes into the persistent store and will show up in all entity wide fetches.
You should not be creating managed objects that might have duplicates. Instead, the normal practice is to fetch on new values to see if they already exist and only create a new managed object with the value if the value does not already exist in the store. To make that fast, you can do a fetch for a specific property and see if anything is returned.
Related
I have 3 entities saved in core data. I am loading these in several view controllers in the app - sometimes loading data from all 3. Below is how I am loading this data and assign it to an array. Once it is in the array, then I sort, filter, count or whatever I need to do depending on the current page of the app.
if (managedObjectContext == nil)
{
managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:managedObjectContext];
[request setEntity:entity];
[request setReturnsObjectsAsFaults:NO];
NSError *error = nil;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil)
{
// Handle the error.
NSLog(#"mutableFetchResults == nil");
}
[self setEventsArray:mutableFetchResults];
The problems I am having are:
I don't like to have lots of duplicate code - and this is appearing on every view controller where core data is needed.
From one entity, I am saving binary data of images which is causing a lag when I load that data
So, is there a way to load from core data using conditions such as eventId = [NSString stringWithFormat:#"%#", currentEventId]
OR (and probably more suitable) have a separate class that loads the data when the app starts. And then I can access the classes arrays (of the loaded data) to use for the current page. And then just reload the data if I save, edit or delete an object.
Any help is much appreciated.
Fro your first question, you should look at MagicalRecord which brings Ruby on Rails' Active Record to CoreData. it will shorten clear your core data code.
Pay attention that if your images are not small you should store them on a separate entity with a relationship to your main entity. this should help you with the lag problem since you will load the image trough the relationship only when you will explicitly ask it to. You can see here the answer of Marcus Zarra (wrote a great book on core data). There is always an option that your images are too big for core data.
Hope it helps
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.
Is there a method similar to insertNewObjectForEntityName that edits the current entity being passed in managed object context? I don't want to create another duplicate entity.
In addition, I don't want users to be able to enter two entities with identical attributes (one attribute, the event title). How can I make it so an alert pops up when they try to add a new entity with an identical title attribute?
Your first question it sounds like what you want to do is get an object that is already in the context with a fetch request, change some values on the object then call the -save method on your context.
For the second part, what you would do is when the user tries to add an item, search the context for an object with the same title, if the item exists, pop up an alert.
Edit: here is some code from my app (edited a bit) in which I set up and execute a fetch request:
NSFetchRequest *categoryRequest = [[NSFetchRequest alloc] init];
[categoryRequest setEntity:[NSEntityDescription entityForName:#"Category" inManagedObjectContext:[self managedObjectContext]]];
NSString *categoryName = #"Cooking";
NSPredicate *categoryNameMatchesPredicate = [NSPredicate predicateWithFormat:#"name MATCHES %#", categoryName];
[categoryRequest setPredicate:categoryNameMatchesPredicate];
NSError *error = nil;
NSArray *categoryArray = [[self managedObjectContext] executeFetchRequest:categoryRequest error:&error];
After this request, the array categoryArray contains all category entities with the name "Cooking". If there are no entities with the name "Cooking" the array will be empty.
It is probably faster to use -countForFetchRequest:error: and check for nonzero count before you actually execute the fetch request, but I am not sure it matters that much in a smallish iOS app.
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];
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.