I have a problem with Core Data in my iOS app.
First off, the code for my ManagedObject:
#interface MyBook : NSManagedObject
#property (retain, nonatomic) NSString *book_name;
#end
#implementation MyBook
#synthesize book_name;
#end
And the code sample:
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:#"MyBook"];
fr.predicate = [NSPredicate predicateWithFormat:#"book_name == %#", #"Something"];
NSArray *result = [s.managedObjectContext executeFetchRequest:fr error:&error];
MyBook *book = nil;
if (result.count) {
book = [result objectAtIndex:0];
NSLog(#"Updating old book: %#", book);
} else {
book = [NSEntityDescription insertNewObjectForEntityForName:#"Book"
inManagedObjectContext:s.managedObjectContext];
NSLog(#"Creating new book");
shelf.book_name = // some unique book name is set here
}
NSLog(#"result: %#", book.book_name);
The code basically...
Tries to look up a book by name and grab that.
If the book does not exist yet, create one and set the book name.
Otherwise, grab the existing book.
The weird thing I'm experiencing is that when the book already exists, the property book_name is null. If the object was created, book_name will have the string I set before.
I guess access to the property is not causing a faulting on the fetched book instance. That's probably why [book valueForKey:#"book_name"] works and after that the property has been populated as well.
What am I missing here? I'm expecting Core Data to notice that I'm accessing the property and internally grabbing the data for me transparently.
Thank you
Big WHOOPS!
Just after posting this original question, I realized that I was using #synthesize instead of #dynamic.
However, I'll leave this question here, if anyone else experiences such an effect in the future. :)
Related
I'm fetching code & desc from web by calling an API. Then loading it into tableView and based on multiple selection I'm saving the selected values into two arrays i.e. selectedCode and selectedCodeDesc. My Entity is:
So I want to [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error){ but don't know how. I know this much:
- (IBAction)confirmPressed:(id)sender {
NSLog(#"Selected Are: %# - %#",selectedDX,selectedDesc);
for (NSString *code in selectedDX) {
if (!_dxToAddEdit) {
self.dxToAddEdit = [MainCode MR_createEntity];
}
[self.dxToAddEdit setCode:code];
[self.dxToAddEdit setCodeDescription:#""]; //what to give here
[self.dxToAddEdit setSuperBill:_forSuperBill];
}
//after this I'm calling the saveToPersistent
So what to give at setCodeDescription?
If I understood correctly and based on your description and example of code you can do the following:
NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];
// Sorry, I renamed selectedCode to selectedCodes and selectedCodeDesc to selectedCodeDescriptions for readability.
// Not sure whether selectedDX is actually selectedCodes.
for (NSInteger i=0; i<selectedCodes.count; ++i) {
NSString *code = selectedCodes[i];
NSString *description = selectedCodeDescriptions[i];
Diagnoses *newDiagnose = [Diagnoses MR_createEntityInContext:defaultContext];
newDiagnose.code = code;
newDiagnose.codeDescription = description;
newDiagnose.superBill = _forSuperBill;
}
[defaultContext MR_saveToPersistentStoreAndWait];
Actually, I would not save the response into two separated arrays. Because of:
Your code becomes difficult to read
Imagine that the model will change and instead of two properties it will contain 4. You will have to create additional arrays.
I would recommend you to parse the response directly into the managed objects. Of course, you may not save them into persistent storage just populate your table view.
I highly recommend you to read these tutorials about Core Data. It will give you insight how to work with Magical Record library. Although, the library simplifies a lot of work it would be better to know what is under the hood ;]
For a detail view I would like to let the user leave notes for each item. The app is for a data-driven website. In the web version, the web app stores notes in a separate table with a field for the itemid.
In Core Data I have an entity of items and another entity of notes. The notes entity has an attribute called itemid. When user creates a note the first time, it stores the itemid in the note record.
My question is when you pull up the item for editing how can you simultaneously pull up the right note based on the note having a certain itemid?
In a database situation you could do a join, or from a web page you could make two separate requests to the two tables but I am somewhat flummoxed by how to do this with Core Data.
Do you have to put a relationship to the note and therefore have the noteid in the item row?
If so would you be able to access the note attribute using the item object?
Thanks in advance for any suggestions.
This is what I am using to save information. I just don't know how to make sure I'm saving it for right note.
self.managedObjectContext = [IDModel sharedInstance].managedObjectContext;
NSString *noteText = _notesView.text;
NSNumber *itemId = self.item.itemid;
// Populate Record
[self.note setValue:noteText forKey:#"note"];
[self.note setValue:itemId forKey:#"itemid"];
Model (simplified):
Item:
name NSString
itemid: Integer 64
Note:
note NSString
noteid: Integer 64
itemid: Integer 64
Edit:
Code to try to link note and item while creating both...
//in save method
// Create Entity
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Notes" inManagedObjectContext:self.managedObjectContext];
// Initialize New Record ie newNote
NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
// Populate Record
[record setValue:note forKey:#"note"];
[record setValue:localid forKey:#"localnid"];
// Save Record
NSError *error = nil;
if ([self.managedObjectContext save:&error]) {
// If note saved, save new item…
if (itemlength>1) {
Items *newItem = [NSEntityDescription insertNewObjectForEntityForName:#“Item” inManagedObjectContext:self.managedObjectContext];
newItem.item = item;
newItem.note = self.note
//This is null as note does not seem to pick up newly created note.
if (![self.managedObjectContext save:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
}
}
Yes, you should use a relationship between the Item and Note entities. To create a relationship just Ctrl-drag from one entity to the other in the data model editor. Note that Xcode automatically adds an inverse relationship:
I've renamed the relationships for clarity - you can tailor the details of the relationship (name, one-one v one-many, delete rule, etc) in the panel on the right. In the example above, the Item entity has three properties: 2 attributes and 1 relationship. Given an Item object, say myItem, the values for these properties can be accessed using the Key Value Coding methods: valueForKey: and setValue:forKey:. For example, if attribute is defined as a string:
NSString *myStringValue = [myItem valueForKey:#"attribute"];
[myItem setValue:#"new value for attribute" forKey:#"attribute"];
That's very long-winded. So to make life easier, use the "Create NSManagedObject subclass..." option. Xcode will configure each entity to be a subclass of NSManagedObject and will create new class files (.h/.m or .swift) with details of the properties. For the example Item:
#property (nullable, nonatomic, retain) NSString *attribute;
#property (nullable, nonatomic, retain) NSString *attribute1;
#property (nullable, nonatomic, retain) Note *note;
The thing to realise is that the note relationship is of class Note. It's not a foreign key, or a noteid, that you have to use to lookup the corresponding Note object. It is the Note object. Under the hood, CoreData is adding primary keys and foreign keys to the underlying tables, but all that aggravation is abstracted away.
Having created the subclasses, you can use the dot-accessors for the object properties:
NSString *myStringValue = myItem.attribute;
myItem.attribute = #"new value for attribute";
For the relationship, if you have an Item object called myItem and a Note object called myNote, you can set the relationship value with:
myItem.note = myNote;
or equivalently:
myNote.item = myItem;
(Note: use one or the other, not both; CoreData automatically sets inverse relationships for you).
Now, you have the added complication of a web server from which the Item objects and Note objects are downloaded. And on your web server, your Notes table has a field for the itemid, which is used to link Items and Notes. At some point, you want to link up Note objects and Item objects using the itemid. The usual approach would be to do it once (as soon as the CoreData objects are synchronised from the server), set the relationship accordingly, and thenceforth use the relationship rather than the itemid to get the note for a given item. For example, if you are creating a new Note object, and the itemid from the server is "1234", you might do this:
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Item"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"itemid == %#", #"1234"];
NSError *error;
NSArray *results = [context executeFetchRequest:fetchRequest error:&error];
// should check for nil results/error
if (results.count > 0) {
// found (at least) one Item with itemid == #"1234"
// use the first to set the relationship
newNote.item = results[0];
}
Then whenever you have a particular Item object, you can access the corresponding note using
Note *myNote = myItem.note;
Furthermore, you can cascade the dot-notation, so get the value of attribute for the Note for myItem, use:
NSString *noteText = myItem.note.attribute;
EDIT
Your save method is very close: either set self.note = record before you save, or use newItem.note = record:
//in save method
// Create Entity
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Notes" inManagedObjectContext:self.managedObjectContext];
// Initialize New Record ie newNote
NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
// Populate Record
[record setValue:note forKey:#"note"];
[record setValue:localid forKey:#"localnid"];
// Save Record
NSError *error = nil;
if ([self.managedObjectContext save:&error]) {
// If note saved, save new item…
if (itemlength>1) {
Items *newItem = [NSEntityDescription insertNewObjectForEntityForName:#“Item” inManagedObjectContext:self.managedObjectContext];
newItem.item = item;
newItem.note = record;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
}
}
}
To reach your goal you need to create a NSFetchRequest passing it the right NSPredicate.
The fetch request will be run against your Note entity. The predicate will allow you to specify that the Note object you want to retrieve is the one for that specific noteid.
So, if you have a 1-to-1 relationship between Item and Note, the NSPredicate should like the following:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"relationshipToItem.propertyForItemId == %#", yourItemId];
[request setPredicate:predicate];
Here I suppose you've a created a relationship between the two entities, otherwise you need to do it manually. Can you provide how your model looks like?
I just started Swift and usually in Objective-C I create a category for each one of my NSManagedObject subclasses so the category isn't erased each time I have to generate my subclass. Moreover, it can simplify the creation of CoreData objects, especially when populated by data coming from a JSON.
Example :
My NSManagedObject subclass :
#class Book;
#interface Book : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSString * author;
#property (nonatomic, retain) NSString * plot;
#end
Its "helper" category implementation :
+ (Book*)bookFromDictionary:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context {
Book *book = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Book"];
request.predicate = [NSPredicate predicateWithFormat:#"name == %#", dictionary[#"name"]];
request.fetchLimit = 1;
NSError *error;
NSArray *result = [context executeFetchRequest:request error:&error];
if (!result || error) {
NSLog(#"Error getting Book : %#", error.localizedDescription);
} else if ([result count] == 0) {
book = [NSEntityDescription insertNewObjectForEntityForName:#"Book" inManagedObjectContext:context];
} else {
book = [result lastObject];
}
self.name = dictionary[#"name"];
self.author = dictionary[#"author"];
self.plot = dictionary[#"plot"];
return book;
}
I'm trying to reproduce this concept in Swift but I don't have any idea how.
It seems that extensions replace categories in Swift but if I implement an extension into a NSManagedObject subclass in Swift, it will be erased each time I have to generate my NSManagedObject subclass (because it's in the same file...). And that's why I used to create categories in Obj-C.
Can someone tell me what are the good practices about this in Swift ?
I would greatly appreciate any help !
A Swift extension is the right way to define additional methods for NSManagedObject subclasses.
You can put the extension into a separate file
(e.g. "BookExtensions.swift") so that they are not overwritten when
"Book.swift" is recreated in Xcode.
There are issues in generating the subclasses for NSManagedObjects in Swift. Typically, you have to go through them again manually to e.g. change types into optionals, include markers like #objc(Class) etc.
Therefore, I have changed my workflow: I am now including the custom methods in the generated file without using an extension (which is the Swift equivalent of a category). When updating my managed object model, I just change the the file marginally by adding, renaming, changing etc. the attributes and relationships.
Given an NSManagedObject subclass with a boolean property deleted (this is demonstrated in two different ways with code below since both approaches are not working):
[Code Listing 1]
#interface MyManagedObject : NSManagedObject
#property (nonatomic, retain) NSNumber *deleted;
// Or #property (nonatomic) BOOL deleted;
#end
created and inserted into Core Data as follows:
[Code Listing 2]
metadata.deleted = [NSNumber numberWithBool:NO];
// metadata.deleted = NO;
and fetched
[Code Listing 3]
// setup entity description
NSEntityDescription* entityDescription = [self entityDescription];
// setup the sorter
NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"createdAt" ascending:YES];
NSSortDescriptor* sortDescriptorSection = [[NSSortDescriptor alloc] initWithKey:#"myManagedObject.category.title" ascending:YES];
// Build request
NSFetchRequest* request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
[request setSortDescriptors:[NSArray arrayWithObjects:sortDescriptorSection, sortDescriptor, nil]];
[request setPredicate:[NSPredicate predicateWithFormat:#"deleted == %#", [NSNumber numberWithBool:NO]]];
// Fetch request
NSArray* items = [[self managedObjectContext] executeFetchRequest:request error:nil];
returns one item in the items array as expected. The problem is when deleted is modified:
[Code Listing 4]
MyManagedObject* myManagedObject; // Assume initialized
myManagedObject.deleted = [NSNumber numberWithBool:YES];
// myManagedObject.deleted = YES;
// Printing description of myManagedObject in debugger shows deleted = 0 at this point
[myManagedObject.managedObjectContext save:nil];
// Printing description of myManagedObject in debugger still shows deleted = 0 at this point
BOOL testValue = myManagedObject.deleted;
if (testValue) {
NSLog(#"value updated"); // This line is executed
}
Re-executing code listing 3 still yields one item in the items array even after a NSFetchResultsController watching the database has fired an update. If the application is terminated and relaunched, re-executing code listing 3 yields no items in the items NSArray.
Calling a Core Data property "deleted" conflicts with the isDeleted property of NSManagedObject.
Compare Core Data NSPredicate "deleted == NO" does not work as expected for a similar problem and some experiments.
Btw. calling a property "updated" causes also problems, compare Cannot use a predicate that compares dates in Magical Record.
You should not be using deleted as a property name for an NSManagedObject subclass.
Also, deleted is an NSNumber, not a BOOL. So, when you are using:
BOOL testValue = myManagedObject.deleted;
if (testValue) {
NSLog(#"value updated"); // This line is executed
}
You are testing if myManagedObject's deleted property is nil or not. If there is a value (even [NSNumber numberWithBool:YES]), testValue will be true.
On an unrelated note, I also advice to capture and log the error when calling NSManagedObjectContext's save method.
Note that this isn't just a problem with deleted and isDeleted. I wrote a database in which I had a "relationship" property, which was an int16 (choice of several relationship types), but then I also had an "isRelationship" boolean which looked at this property and several others to determine whether the content was about the individual or about a relationship. valueForKey:#"relationship" would return the bool for isRelationship - which meant that it also affected NSPredicate. The following predicate:
NSPredicate *pred = [NSPredicate predicateWithFormat:#"relationship IN %#",
#[[NSNumber numberWithShort:Relationship_Dating],
[NSNumber numberWithShort:Relationship_Married]]];
would fail to filter out friend or family relationships because they were all returning 1 from isRelationship instead of the actual relationship value.
Watch out for stupidly "clever" system behavior when "is" booleans are involved.
(I fixed this by changing the "-(bool) isRelationship" method to "-(bool) isRelational".)
I am using MagicalRecord (MR) to delete all records belonging to a selected client (I successfully delete the client record, then go after the appointment records for that client). In doing so, I am getting the error.
[_PFArray MR_deleteInContext:]: unrecognized selector sent to instance
Here is the code, along with the pertinent definitions:
// set up predicate using selectedClientKey
NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextForCurrentThread];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"aClientKey == %#", selectedClientKey];
ClientInfo *clientSelected = [ClientInfo MR_findFirstWithPredicate:predicate inContext:localContext];
if(clientSelected) {
[clientSelected MR_deleteInContext:localContext];
[localContext MR_saveToPersistentStoreAndWait];
}
// delete clients appointments...
predicate = [NSPredicate predicateWithFormat:#"aApptKey == %#", selectedClientKey]; // use client key
AppointmentInfo *apptSelected = [AppointmentInfo MR_findAllWithPredicate:predicate inContext:localContext];
if(apptSelected) {
[apptSelected MR_deleteInContext:localContext];
[localContext MR_saveToPersistentStoreAndWait];
}
Here is the definition of AppointmentInfo:
#interface AppointmentInfo : NSManagedObject
#property (nonatomic, retain) NSString * aApptKey;
#property (nonatomic, retain) NSDate * aEndTime;
#property (nonatomic, retain) NSString * aServiceTech;
#property (nonatomic, retain) NSDate * aStartTime;
On the findAllWithPredicate statement, I am getting this compiler warning:
CalendarViewController.m:80:43: Incompatible pointer types assigning
to 'NSMutableArray *' from 'NSArray *__strong'
I understand that the findAllWithPredicate statement will return a NSArray; however I have seen examples using NSManagedObject, which is what AppointmentInfo is. ClientInfo in the 3rd line down is also a NSManagedObject and it has NO compiler warning. I thought that it might be because there was only one (1) record returned from the find statement, but it makes no difference, one record or multiple records.
Am I getting the run error due to the compiler warning, or is there something else wrong? (I have looked at Google and SO, and found nothing that addresses this particular issue).
You are correct that findAllWithPredicate: will return an array. The examples you've seen are most likely using the findFirstWithPredicate: or similar style method. Find First, as the name implies, will return the first object in the results returned from the request. This is most likely what you want as well.
I figured it out... for those who might have the same issue, MR_findAll returns a NSArray which you have to "walk through" and delete each individually. Here's the corrected code from above:
// delete clients appointments...
predicate = [NSPredicate predicateWithFormat:#"aApptKey == %#", selectedClientKey]; // use client key
NSArray *apptDataArray = [AppointmentInfo MR_findAllWithPredicate:predicate inContext:localContext];
for(int i = 0; i < apptDataArray.count; i++) {
AppointmentInfo *ai = [apptDataArray objectAtIndex: i];
[ai MR_deleteEntity];
}
[localContext MR_saveToPersistentStoreAndWait];