I populate and save: an initial NSManagedObjectContext
setup an NSFetchedResultsController with a different NSManagedObjectContext, which filters on a boolean "show" attribute.
Finally update "show" on yet another NSManagedObjectContext and save:.
I expect that this should cause my NSFetchedResultsController to call NSFetchedResultsControllerDelegate's controllerDidChangeContent:. I never get that call. NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext's accepted answer indicates that in addition to controllerDidChangeContent:, I should get an NSManagedObjectContextObjectsDidChangeNotification, but I don't receive that either.
A complete code example is included below and on github. I've filed a radar with Apple.
#interface HJBFoo : NSManagedObject
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSNumber *show;
#end
#interface HJBAppDelegate () <NSFetchedResultsControllerDelegate>
#property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
#property (nonatomic, strong) NSManagedObjectContext *initialManagedObjectContext;
#property (nonatomic, strong) NSManagedObjectContext *fetchedResultsControllerManagedObjectContext;
#property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
#end
#implementation HJBAppDelegate
#pragma mark - UIApplicationDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = [UIViewController new];
NSAttributeDescription *nameAttributeDescription = [NSAttributeDescription new];
[nameAttributeDescription setAttributeType:NSStringAttributeType];
[nameAttributeDescription setIndexed:NO];
[nameAttributeDescription setOptional:NO];
[nameAttributeDescription setName:#"name"];
NSAttributeDescription *showAttributeDescription = [NSAttributeDescription new];
[showAttributeDescription setAttributeType:NSBooleanAttributeType];
[showAttributeDescription setIndexed:YES];
[showAttributeDescription setOptional:NO];
[showAttributeDescription setName:#"show"];
NSEntityDescription *fooEntityDescription = [NSEntityDescription new];
[fooEntityDescription setManagedObjectClassName:#"HJBFoo"];
[fooEntityDescription setName:#"HJBFoo"];
[fooEntityDescription setProperties:#[
nameAttributeDescription,
showAttributeDescription,
]];
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel new];
[managedObjectModel setEntities:#[
fooEntityDescription,
]];
self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error = nil;
if ([self.persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType
configuration:nil
URL:nil
options:nil
error:&error]) {
self.initialManagedObjectContext = [NSManagedObjectContext new];
[self.initialManagedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
HJBFoo *foo1 = [NSEntityDescription insertNewObjectForEntityForName:#"HJBFoo"
inManagedObjectContext:self.initialManagedObjectContext];
foo1.name = #"1";
foo1.show = #YES;
HJBFoo *foo2 = [NSEntityDescription insertNewObjectForEntityForName:#"HJBFoo"
inManagedObjectContext:self.initialManagedObjectContext];
foo2.name = #"2";
foo2.show = #NO;
error = nil;
if ([self.initialManagedObjectContext save:&error]) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"HJBFoo"];
[fetchRequest setReturnsObjectsAsFaults:NO];
error = nil;
NSArray *initialFoos = [self.initialManagedObjectContext executeFetchRequest:fetchRequest
error:&error];
if (initialFoos) {
NSLog(#"Initial: %#", initialFoos);
self.fetchedResultsControllerManagedObjectContext = [NSManagedObjectContext new];
[self.fetchedResultsControllerManagedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
NSFetchRequest *shownFetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"HJBFoo"];
[shownFetchRequest setPredicate:[NSPredicate predicateWithFormat:#"show == YES"]];
[shownFetchRequest setSortDescriptors:#[
[NSSortDescriptor sortDescriptorWithKey:#"name"
ascending:YES],
]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:shownFetchRequest
managedObjectContext:self.fetchedResultsControllerManagedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
self.fetchedResultsController.delegate = self;
error = nil;
if ([self.fetchedResultsController performFetch:&error]) {
NSLog(#"Initial fetchedObjects: %#", [self.fetchedResultsController fetchedObjects]);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(managedObjectContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(managedObjectContext2ObjectsDidChange:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:self.fetchedResultsControllerManagedObjectContext];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC),
dispatch_get_main_queue(),
^(void){
NSManagedObjectContext *managedObjectContext3 = [NSManagedObjectContext new];
[managedObjectContext3 setPersistentStoreCoordinator:self.persistentStoreCoordinator];
NSFetchRequest *foo2FetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"HJBFoo"];
[foo2FetchRequest setFetchLimit:1];
[foo2FetchRequest setPredicate:[NSPredicate predicateWithFormat:#"name == %#",
#"2"]];
NSError *editingError = nil;
NSArray *editingFoos = [managedObjectContext3 executeFetchRequest:foo2FetchRequest
error:&editingError];
if (editingFoos) {
HJBFoo *editingFoo2 = [editingFoos objectAtIndex:0];
editingFoo2.show = #YES;
editingError = nil;
if ([managedObjectContext3 save:&editingError]) {
NSLog(#"Save succeeded. Expected (in order) managedObjectContextDidSave, controllerDidChangeContent, managedObjectContext2ObjectsDidChange");
} else {
NSLog(#"Editing save failed: %# %#", [error localizedDescription], [error userInfo]);
}
} else {
NSLog(#"Editing fetch failed: %# %#", [error localizedDescription], [error userInfo]);
}
});
} else {
NSLog(#"Failed initial fetch: %# %#", [error localizedDescription], [error userInfo]);
}
} else {
NSLog(#"Failed to performFetch: %# %#", [error localizedDescription], [error userInfo]);
}
} else {
NSLog(#"Failed to save initial state: %# %#", [error localizedDescription], [error userInfo]);
}
} else {
NSLog(#"Failed to add persistent store: %# %#", [error localizedDescription], [error userInfo]);
}
[self.window makeKeyAndVisible];
return YES;
}
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"controllerDidChangeContent: %#",
[self.fetchedResultsController fetchedObjects]);
}
#pragma mark - notifications
- (void)managedObjectContextDidSave:(NSNotification *)notification {
NSManagedObjectContext *managedObjectContext = [notification object];
if (([managedObjectContext persistentStoreCoordinator] == self.persistentStoreCoordinator) &&
(managedObjectContext != self.fetchedResultsControllerManagedObjectContext)) {
NSLog(#"managedObjectContextDidSave: %#", notification);
[self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
}
- (void)managedObjectContext2ObjectsDidChange:(NSNotification *)notification {
NSLog(#"managedObjectContext2ObjectsDidChange: %#", notification);
}
#end
#implementation HJBFoo
#dynamic name;
#dynamic show;
#end
It seems to me that applying the fix/workaround from NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext solves your problem as well. Your managedObjectContextDidSave method would then look like this:
- (void)managedObjectContextDidSave:(NSNotification *)notification {
NSManagedObjectContext *managedObjectContext = [notification object];
if (([managedObjectContext persistentStoreCoordinator] == self.persistentStoreCoordinator) &&
(managedObjectContext != self.fetchedResultsControllerManagedObjectContext)) {
NSLog(#"managedObjectContextDidSave: %#", notification);
// Fix/workaround from https://stackoverflow.com/questions/3923826/nsfetchedresultscontroller-with-predicate-ignores-changes-merged-from-different/3927811#3927811
for(NSManagedObject *object in [[notification userInfo] objectForKey:NSUpdatedObjectsKey]) {
[[self.fetchedResultsControllerManagedObjectContext objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
}
and the NSLog output looks as expected:
Initial: (
"<HJBFoo: 0xaa19670> (entity: HJBFoo; id: 0xaa1afd0 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p2> ; data: {\n name = 1;\n show = 1;\n})",
"<HJBFoo: 0xaa1a200> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n name = 2;\n show = 0;\n})"
)
Initial fetchedObjects: (
"<HJBFoo: 0x74613f0> (entity: HJBFoo; id: 0xaa1afd0 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p2> ; data: <fault>)"
)
managedObjectContextDidSave: NSConcreteNotification 0xaa1f480 {name = NSManagingContextDidSaveChangesNotification; object = <NSManagedObjectContext: 0xaa1ed90>; userInfo = {
inserted = "{(\n)}";
updated = "{(\n <HJBFoo: 0xaa1f2d0> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n name = 2;\n show = 1;\n})\n)}";
}}
Save succeeded. Expected (in order) managedObjectContextDidSave, controllerDidChangeContent, managedObjectContext2ObjectsDidChange
controllerDidChangeContent: (
"<HJBFoo: 0x74613f0> (entity: HJBFoo; id: 0xaa1afd0 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p2> ; data: {\n name = 1;\n show = 1;\n})",
"<HJBFoo: 0xaa1f9c0> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n name = 2;\n show = 1;\n})"
)
managedObjectContext2ObjectsDidChange: NSConcreteNotification 0xaa1fbb0 {name = NSObjectsChangedInManagingContextNotification; object = <NSManagedObjectContext: 0x745fa50>; userInfo = {
managedObjectContext = "<NSManagedObjectContext: 0x745fa50>";
refreshed = "{(\n <HJBFoo: 0xaa1f9c0> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n name = 2;\n show = 1;\n})\n)}";
}}
So the following things happen:
Changes are made in the "background" context managedObjectContext3 and saved.
You receive a NSManagedObjectContextDidSaveNotification and managedObjectContextDidSave: is called. This notification contains the object that was updated in the background context.
Calling
[self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
now would not do anything, because the updated object has not been loaded into the fetchedResultsControllerManagedObjectContext or is a fault. In particular, this context would not post a NSManagedObjectContextObjectsDidChangeNotification and the fetched results controller would not update.
But calling willAccessValueForKey for the updated objects first forces the fetchedResultsControllerManagedObjectContext to load these objects and fire a fault.
Calling
[self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
after that actually updates the objects in the fetchedResultsControllerManagedObjectContext.
Therefore a NSManagedObjectContextObjectsDidChangeNotification is posted and the fetched results controller calls the corresponding delegate functions.
Related
I created an IBAction that I link to saveButton, when I call all core data methods inside the IBAction it works fine in following code:
- (IBAction)saveButtonTap:(id)sender{
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *object = [NSEntityDescription insertNewObjectFor:#"Person" inManagedContext:context];
[self setValue:firstNameTextField.text forKey:#"firstName"];
[self setValue:lastNameTextField.text forKey:#"lastName"];
NSError *error = nil;
if (![context save:&error])
NSLog(#"Can't save transaction - %# || %# ", error, [error localizedDescription]);
}
And when I separate the core data saving methods as the below, the saved firstName and lastName, showing null when fetched. It didn't return error, but I have no idea after googling all day. Could someone point anything wrong with my code?
Person.h + Person.m
#interface Person : NSManagedObject
#property(strong) NSString *firstName;
#property(strong) NSString *lastName;
- (void)save;
#end
#implementation Person
#synthesize firstName;
#synthesize lastName;
- (void)save{
[self setValue:self.firstName forKey:#"firstName"];
[self setValue:self.lastName forKey:#"lastName"];
}
#end
mainViewController.h + mainViewController.m
#interface mainViewController : UIViewController
#property(strong) ManagedObjectContext *context;
#property(strong) Person *person;
- (IBAction)saveButtonTap:(id)sender;
#end
#implementation mainViewController
#synthesize context;
#synthesize person;
- (void)viewDidLoad{
// ... some view did Load rituals
if(context == nil){
context = [self managedObjectContext]; // Assume this method calls for managed object context from shared application.
}
if(person == nil){
person = [NSEntityDescription insertNewObjectFor:#"Person" inManagedContext:context];
}
}
- (IBAction)saveButtonTap:(id)sender{
person.firstName = firstNameTextField.text;
person.lastName = lastNameTextField.text;
[self.person save];
NSError *error = nil;
if (![context save:&error])
NSLog(#"Can't save transaction - %# || %# ", error, [error localizedDescription]);
}
#end
There is no need of save function for Person.
person.firstName = firstNameTextField.text
is equivalent to
[self setValue:self.firstName forKey:#"firstName"];
What you need, is just this:
(IBAction)saveButtonTap:(id)sender{
person.firstName = firstNameTextField.text;
person.lastName = lastNameTextField.text;
NSError *error = nil;
if (![context save:&error])
NSLog(#"Can't save transaction - %# || %# ", error, [error localizedDescription]);
}
But, I am not sure whether this is the cause of the issue. Try it and let me know.
I'm trying to save a string into a database every time a button is pressed but when I run the project, I get that on my console: 'NSInvalidArgumentException', reason: '+entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name 'Info''.
Referring to the Data Model, I have created a .xcdatamodeld with an Entity named 'Info' and, inside it, an attribute named 'path' with a type of string.
I've created three functions. "enterdata" Checks if the name is avaliable or not by calling "findData". If the name is avaliable, a new data is recorded throught "newData", if not, it looks for a different name.
I've been looking for some similar questions and I've found out this. It says that de ManagedObjectContext has to be passed to the View Controller but I don't understand what does it mean.
Here's my .h code:
#property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
Here's my .m code:
#import <CoreData/CoreData.h>
#synthesize managedObjectContext;
int iSavedNum = 1;
bool bCanSave;
//Enter data
- (IBAction) enterdata:(id)sender {
//Search if data is already registered
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *path = [NSString stringWithFormat:#"%#/info%i.png",docDir, iSavedNum];
[self findData:path :#"path"];
//If data is already saved, save it with new name.
if (bCanSave == NO) {
for (iSavedNum = 1; bCanSave == YES; iSavedNum++) {
[self findData:path :#"path"];
if (bCanSave == YES) {
[self newData:path :#"path"];
}
}
} else {
[self newData:path :#"path"];
}
}
//Input new data
- (void) newData:(NSString *)value:(NSString *)key {
//Create ManagedObjectContext and ManagedObjectModel
__0AppDelegate *appDelegate = (__0AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSManagedObjectModel *newRecord;
//Put the data to the Entity
NSString *entityName = #"Info";
newRecord = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context];
[newRecord setValue:value forKey:key];
//Errors management and cheking
NSError *error;
[context save:&error];
NSLog(#"Info Saved. Value: %# Key: %#", value, key);
}
//Find Data
- (void) findData:(NSString *)valor:(NSString *)key {
//Create ManagedObjectContext
__0AppDelegate *appDelegate = (__0AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
//Call the Entity and make a request
NSString *entityName = #"Info";
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:entityName inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
//Create predicate to call specific info
NSPredicate *pred = [NSPredicate predicateWithFormat:#"(%# = %#)", key, valor];
[request setPredicate:pred];
//Errors management and creation of an array with found info
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
//Set if the name is avaliable or not
if ([objects count] == 0) {
bCanSave = YES;
} else {
bCanSave = NO;
}
}
It tells you exactly what the error is:
nil is not a legal NSManagedObjectContext parameter
That means that on this line:
newRecord = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];
The variable context is nil. This means that your managedObjectContext method isn't working correctly. You don't show this so there's not much more we can add.
In application:didFinishLaunchingWithOptions: in appDelegate
/*initiate the managed Object Context */
CoreDataManager *coreDataManager = [CoreDataManager sharedDataManager];
coreDataManager.managedObjectContext = self.managedObjectContext;
where CoreDataManager is my core date manager which explicitly contains all the core data save, delete methods
Or
yourClassObject.managedObjectContext = self.managedObjectContext;
so context get initialized
Im pretty new to Core Data programming and Cocoa in general, so no wonder I'm having troubles :)
So here is my managedObjectModel method:
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel != nil)
{
return managedObjectModel;
}
NSString *modelPath = [[NSBundle mainBundle] pathForResource:#"Model" ofType:#"momd"];
NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
NSAssert(modelURL != nil,#"modelURL == nil");
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return managedObjectModel;
}
Here is the part of the code that crashes:
NSManagedObjectModel *mom = [self managedObjectModel];
managedObjectModel = mom;
if (applicationLogDirectory() == nil)
{
NSLog(#"Could not find application logs directory\nExiting...");
exit(1);
}
NSManagedObjectContext *moc = [self managedObjectContext];
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
NSEntityDescription *newShotEntity = [[mom entitiesByName] objectForKey:#"Entity"];
Entity *shEnt = [[Entity alloc] initWithEntity:newShotEntity insertIntoManagedObjectContext:moc];
shEnt.pid = [processInfo processIdentifier]; // EXC_BAD_ACCESS (code=1, address=0x28ae) here !!!
NSError *error;
if (![moc save: &error])
{
NSLog(#"Error while saving\n%#",
([error localizedDescription] != nil) ? [error localizedDescription] : #"Unknown Error");
exit(1);
}
Im really confused why I'm having this error, since when I hardcoded the Data Model instead of using .xcdatamodeld file it was working just fine!
Any kind of help is really appreciated!
EDIT 1: since I'm having all those questions asked I want to make everything clear, sorry for not providing all this before.
// Entity.h
#import <CoreData/CoreData.h>
#interface Entity : NSManagedObject
#property (strong) NSDate *date;
#property (assign) NSInteger pid;
#end
//Entity.m
#import "Entity.h"
#interface Entity ()
#property (strong) NSDate *primitiveDate;
#end
#implementation Entity
#dynamic date,primitiveDate,pid;
- (void) awakeFromInsert
{
[super awakeFromInsert];
self.primitiveDate = [NSDate date];
}
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:#"pid"]) {
self.pid = 0;
}
else {
[super setNilValueForKey:key];
}
}
#end
Using scalar values in core data is a bit more work than using the recommended NSNumber. This is described in detail in this section of the Core Data Programming Guide.
I strongly recommend you switch this property to NSNumber. Your assignment statement would then be:
shEnt.pid = [NSNumber numberWithInt:[processInfo processIdentifier]];
I'm developing an iOS app using Core Data. And I have a Log entity with one-to-many relationships with Audio, Photo entities, and one-to-one relationship with Status entity. The log also has text, longitude, latitude properties. I can create the log, change its properties, add status entity, these changes would display right, until I quit the App. All the changes would disappear, and I was looking at the sqlite database, all these changes were never persisted in the database. In the database, the status object will just be created, but not linked to the log object.
But if I add an audio or photo object into the log.audioSet or log.photoSet, the changes I made to log, including the changes to text or status, will suddenly be saved into the database.
So it seems the changes are only maintained in the NSManagedObjectContext, until a related one_to_many entity is added and the [[LTLogStore sharedStore] saveChanges] will suddenly start to work.
I am using a singleton to manage the NSManagedObjectContext. Any ideas?
I would post some code if it's relevant. Thanks.
UPDATE: I'm not sure these code is enough. But basically everything works, and displays, it just doesn't save to the database. I'm using the mogenerator to set the text and latitude, but since everything is in the context. I am not sure this is the code you might need.
CODE:
#interface LTLogStore : NSObject{
}
+ (LTLogStore *)sharedStore;
- (void)removeItem:(Log *)p;
- (Log *)createItem;
- (BOOL)saveChanges;
#property(nonatomic, strong) NSFetchedResultsController *resultsController;
#property(nonatomic, strong) NSManagedObjectModel *model;
#property(nonatomic, strong) NSManagedObjectContext *context;
#end
#implementation LTLogStore
#synthesize resultsController;
#synthesize context, model;
+ (LTLogStore *)sharedStore
{
static LTLogStore *sharedStore = nil;
if(!sharedStore){
sharedStore = [[super allocWithZone:nil] init];
}
return sharedStore;
}
+ (id)allocWithZone:(NSZone *)zone
{
return [self sharedStore];
}
- (id)init
{
self = [super init];
if(self) {
model = [NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *psc =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// Where does the SQLite file go?
NSString *path = [self itemArchivePath];
NSURL *storeURL = [NSURL fileURLWithPath:path];
NSError *error = nil;
if (![psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]) {
[NSException raise:#"Open failed"
format:#"Reason: %#", [error localizedDescription]];
}
// Create the managed object context
context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:psc];
// The managed object context can manage undo, but we don't need it
[context setUndoManager:nil];
}
return self;
}
- (NSFetchedResultsController *)resultsController {
if (resultsController !=nil) {
return resultsController;
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *e = [[model entitiesByName] objectForKey:#"Log"];
[request setEntity:e];
NSSortDescriptor *sd = [NSSortDescriptor
sortDescriptorWithKey:#"created_at"
ascending:NO];
[request setSortDescriptors:[NSArray arrayWithObject:sd]];
[request setReturnsObjectsAsFaults:NO];
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:nil cacheName:#"Root"];
NSError *error;
BOOL success = [fetchedResultsController performFetch:&error];
if (!success) {
//handle the error
}
return fetchedResultsController;
}
- (NSString *)itemArchivePath
{
NSArray *documentDirectories =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
// Get one and only document directory from that list
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
NSString *storePath = [documentDirectory stringByAppendingPathComponent:#"store.data"];
return storePath;
}
- (BOOL)saveChanges
{
NSError *err = nil;
BOOL successful = [context save:&err];
NSLog(#"Saving changes to the database");
if (!successful) {
NSLog(#"Error saving: %#", [err localizedDescription]);
}
return successful;
}
- (void)removeItem:(Log *)l
{
[context deleteObject:l];
[self saveChanges];
}
- (Log *)createItem
{
Log *p = [NSEntityDescription insertNewObjectForEntityForName:#"Log"
inManagedObjectContext:context];
[self saveChanges];
return p;
}
#end
#interface Log : _Log {
}
//these two are some custom convenience methods for location attributes, but it does the work of setting the longitude and latitude value in the log object, but calling the [[LTLogStore sharedStore] saveChanges] still won't save it into the database.
-(CLLocation*)location;
-(void)setLocation:(CLLocation*)location;
//this all works
-(Audio*)newAudio;
-(Audio*)newAudioWithPath:(NSString*)audioPath;
//after calling this method, even the log.text changes will be saved to the database.
-(void)addAudioWithPath:(NSString*)audioPath;
-(void)removeAudio:(Audio*)audio;
#end
#import "Log.h"
#import "Audio.h"
#import "LTLogStore.h"
#implementation Log
-(CLLocation*)location{
if (!self.longitude || !self.latitude) {
return nil;
}
CLLocation *l = [[CLLocation alloc] initWithLatitude:[self.latitude doubleValue] longitude:[self.longitude doubleValue]];
return l;
}
-(void)setLocation:(CLLocation*)location{
if (location==nil) {
self.latitude = nil;
self.longitude = nil;
}
self.latitude = [NSNumber numberWithDouble: location.coordinate.latitude];
self.longitude = [NSNumber numberWithDouble:location.coordinate.longitude];
[[LTLogStore sharedStore] saveChanges];
}
-(Audio*)newAudio{
Audio *a = [Audio new];
a.log = self;
return a;
}
-(Audio*)newAudioWithPath:(NSString*)audioPath{
Audio *new = [self newAudio];
[new setKey:audioPath];
return new;
}
-(void)addAudioWithPath:(NSString*)audioPath{
Audio *new = [self newAudio];
[new setKey:audioPath];
[[LTLogStore sharedStore] saveChanges];
}
-(void)removeAudio:(Audio*)audio{
[self.audiosSet removeObject:audio];
[[[LTLogStore sharedStore] context] deleteObject:audio];
[[LTLogStore sharedStore] saveChanges];
}
#end
UPDATE:
Problem solved, see answer.
UPDATE QUESTION: Why is my overriding causing the problem? Can someone explain the cause behind the magic of Core Data or maybe KVO behind scene?
Problem solved, I overrode the willChangeValueForKey method in the Log class, which caused the problem, I thought the code is irrelevant. But it IS:
- (void)willChangeValueForKey:(NSString *)key{
//I added the following line to fix my problem
[super willChangeValueForKey:key];
//this is the original line, I want to have this
//because I want to have a isBlank property
//so I can see if the user modified the log
_isBlank = false;
//I tried to also add the following line to be safe.
//turns out this line is not needed, and it will make the problem occur again
//[super didChangeValueForKey:key];
}
I'm new to Objective-C, XCode and iPhone development in general and I'm having some issues with Core Data and NSXMLParser.
Having followed Apples' tutorials SeismicXML (for NSXMLParser) and the Core Data on iPhone tutorial I've ran into an issue when assigning values to my Managed Object Models' entities properties.
To explain the situation my code only varies from the SeismicXML example by using CoreData to assign the currentParsedCharacterData to my managed objects rather than the standard NSObject which the SeismicXML project uses.
Below is the description output from my managed object.
county = "-53.25354768,4.256547";
friendly = "-53.25354768,4.256547";
image = nil;
latitude = -53.253547684;
link = "-53.25354768,4.256547";
longitude = nil;
name = "-53.25354768,4.256547";
postcode = "-53.25354768,4.256547";
shopDescription = nil;
shopID = 0;
tag = "-53.25354768,4.256547";
tags = (
);
telephone = "-53.25354768,4.256547";
town = "-53.25354768,4.256547";
What appears to be happening is that all of the attributes/properties are assigned the value of the last node in my XML feed; which happens to be longitude, latitude. Yet when logging parsed character data at the time of the property assignment it is the expected (and correct) value but when outputting the description of this object all string values are wrong and number values/otherwise are simply 0 or nil.
Any suggestions would be extremely appreciated. If need be I can knock up a smaller project which shows this behaviour with the same XML feed that I am using.
EDIT:
Here is an abbreviated example of what I am doing to get information into the managed object which results in the same error.
For convenience sake I have a zip of the project http://willb.ro/CoreDataProblemExample.zip
Debug Output 2009-11-16 14:31:20.357
ShittyExample[4360:4d07] Company
Description:
(entity: Company; id: 0x3f6e9e0
; data: {
companyDescription = "Top Shop are a leading brandname in the retail
sec";
companyID = 66136112;
name = "Top Shop are a leading brandname in the retail sec"; })
//XML
<channel>
<company id="1">
<name>Top Shop</name>
<description>Top Shop are a leading brandname in the retail sector.</description>
</company>
</channel>
// FeedImporter.h
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#class RootViewController, Company;
#interface FeedImporter : NSObject {
NSManagedObjectContext *managedObjectContext;
RootViewController *rootViewController;
NSMutableArray *companyList;
// for downloading the xml data
NSURLConnection *companyFeedConnection;
NSMutableData *companyData;
// these variables are used during parsing
Company *currentCompanyObject;
NSMutableArray *currentParseBatch;
NSUInteger parsedCompaniesCounter;
NSMutableString *currentParsedCharacterData;
BOOL accumulatingParsedCharacterData;
BOOL didAbortParsing;
}
#property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
#property (nonatomic, retain) RootViewController *rootViewController;
#property (nonatomic, retain) NSMutableArray *companyList;
#property (nonatomic, retain) NSURLConnection *companyFeedConnection;
#property (nonatomic, retain) NSMutableData *companyData;
#property (nonatomic, retain) Company *currentCompanyObject;
#property (nonatomic, retain) NSMutableString *currentParsedCharacterData;
#property (nonatomic, retain) NSMutableArray *currentParseBatch;
- (void)parseFeed;
- (void)addCompaniesToList:(NSArray *)companies;
- (void)handleError:(NSError *)error;
#end
// FeedImporter.m
#import "FeedImporter.h"
#import "RootViewController.h"
#import <CFNetwork/CFNetwork.h>
#import "Company.h"
#implementation FeedImporter
#synthesize managedObjectContext;
#synthesize rootViewController;
#synthesize companyList;
#synthesize companyFeedConnection;
#synthesize companyData;
#synthesize currentCompanyObject;
#synthesize currentParseBatch;
#synthesize currentParsedCharacterData;
- (void)dealloc {
[super dealloc];
[managedObjectContext release];
[rootViewController release];
[companyList release];
[companyFeedConnection release];
[companyData release];
[currentCompanyObject release];
[currentParseBatch release];
[currentParsedCharacterData release];
}
- (id)init {
if(self = [super init]) {
// Custom loading logic goes here..
}
return self;
}
- (void)parseFeed {
static NSString *feedURLString = #"http://willb.ro/companies.xml";
NSURLRequest *companyURLRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:feedURLString]];
self.companyFeedConnection = [[[NSURLConnection alloc] initWithRequest:companyURLRequest delegate:self] autorelease];
NSAssert(self.companyFeedConnection != nil, #"Failure to create URL connection.");
// Start the status bar network activity indicator. We'll turn it off when the connection finishes or experiences an error.
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}
#pragma mark NSURLConnection delegate methods
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.companyData = [NSMutableData data];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[companyData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if ([error code] == kCFURLErrorNotConnectedToInternet) {
// if we can identify the error, we can present a more precise message to the user.
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:NSLocalizedString(#"No Connection Error", #"Error message displayed when not connected to the Internet.") forKey:NSLocalizedDescriptionKey];
NSError *noConnectionError = [NSError errorWithDomain:NSCocoaErrorDomain code:kCFURLErrorNotConnectedToInternet userInfo:userInfo];
[self handleError:noConnectionError];
} else {
// otherwise handle the error generically
[self handleError:error];
}
self.companyFeedConnection = nil;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
self.companyFeedConnection = nil;
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[NSThread detachNewThreadSelector:#selector(parseCompanyData:) toTarget:self withObject:companyData];
self.companyData = nil;
}
- (void)parseCompanyData:(NSData *)data {
// You must create a autorelease pool for all secondary threads.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
self.currentParseBatch = [NSMutableArray array];
self.currentParsedCharacterData = [NSMutableString string];
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:self];
[parser parse];
if ([self.currentParseBatch count] > 0) {
[self performSelectorOnMainThread:#selector(addCompaniesToList:) withObject:self.currentParseBatch waitUntilDone:NO];
}
self.currentParseBatch = nil;
self.currentCompanyObject = nil;
self.currentParsedCharacterData = nil;
// Save to our MOC...
NSError *saveError;
if(![self.managedObjectContext save:&saveError]) {
// Handle MOM save error
NSLog(#"error while saving shop to managed object model");
NSError* error;
if(![[self managedObjectContext] save:&error]) {
NSLog(#"Failed to save to data store: %#", [error localizedDescription]);
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
}
else {
NSLog(#" %#", [error userInfo]);
}
}
}
else
{
NSLog(#"MOC saved sucessfully");
}
[parser release];
[pool release];
}
#pragma mark Parser constants
// Limit the number of parsed companies to 50.
static const const NSUInteger kMaximumNumberOfCompaniesToParse = 50;
static NSUInteger const kSizeOfCompanyBatch = 10;
static NSString * const kChannelElementName = #"channel";
static NSString * const kCompanyElementName = #"company";
static NSString * const kCompanyNameElementName = #"name";
static NSString * const kCompanyDescriptionElementName = #"description";
- (void)addCompaniesToList:(NSArray *)companies {
[self.companyList addObjectsFromArray:companies];
// The table needs to be reloaded to reflect the new content of the list.
[rootViewController.tableView reloadData];
}
#pragma mark NSXMLParser delegate methods
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
if (parsedCompaniesCounter >= kMaximumNumberOfCompaniesToParse) {
didAbortParsing = YES;
[parser abortParsing];
}
if ([elementName isEqualToString:kCompanyElementName]) {
Company *company = (Company *)[NSEntityDescription insertNewObjectForEntityForName:#"Company" inManagedObjectContext:self.managedObjectContext];
self.currentCompanyObject = company;
[company release];
int companyIDInt = (int)[attributeDict valueForKey:#"id"];
NSNumber *companyID = [NSNumber numberWithInt:companyIDInt];
[self.currentCompanyObject setCompanyID:companyID];
}
else if ([elementName isEqualToString:kCompanyElementName] || [elementName isEqualToString:kCompanyNameElementName] || [elementName isEqualToString:kCompanyDescriptionElementName]) {
accumulatingParsedCharacterData = YES;
[currentParsedCharacterData setString:#""];
}
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
if ([elementName isEqualToString:kCompanyElementName]) {
//NSLog(#"currentEarthquakeObject: %#", currentEarthquakeObject);
[self.currentParseBatch addObject:self.currentCompanyObject];
parsedCompaniesCounter++;
if (parsedCompaniesCounter % kSizeOfCompanyBatch == 0) {
[self performSelectorOnMainThread:#selector(addCompaniesToList:) withObject:self.currentParseBatch waitUntilDone:NO];
self.currentParseBatch = [NSMutableArray array];
}
//NSLog(#"Reached end of company. Follows is a description of our company object: %#", [self.currentCompanyObject description]);
NSLog(#"Company Description: %#", [self.currentCompanyObject description]);
}
else if ([elementName isEqualToString:kCompanyNameElementName]) {
// Company Name
[self.currentCompanyObject setName:self.currentParsedCharacterData];
//NSLog(#"%#",self.currentParsedCharacterData);
}
else if ([elementName isEqualToString:kCompanyDescriptionElementName]) {
// Company Description
[self.currentCompanyObject setCompanyDescription:self.currentParsedCharacterData];
//NSLog(#"%#",self.currentParsedCharacterData);
}
accumulatingParsedCharacterData = NO;
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
if (accumulatingParsedCharacterData) {
[self.currentParsedCharacterData appendString:string];
}
}
- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
if (didAbortParsing == NO) {
[self performSelectorOnMainThread:#selector(handleError:) withObject:parseError waitUntilDone:NO];
}
}
- (void)handleError:(NSError *)error {
NSString *errorMessage = [error localizedDescription];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(#"Error Title", #"Title for alert displayed when download or parse error occurs.") message:errorMessage delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alertView show];
[alertView release];
}
#end
It seems you are assigning where you ought to be copying. I'd have to see more of your code to be sure, but I'm almost certain a copy somewhere would solve your issues.
As stated above, i had the exact same problem. But would now recommend to bypass the useless NSXMLParser (if using it WITH core data!!!) and using GDataXML instead. GDataXML is modelled on the NSXMLDocument class (which is a more elegant and workable solution to use with core data, only that you can't use NSXMLDocument with iOS).
See here for an excellent tutorial:
http://www.raywenderlich.com/725/how-to-read-and-write-xml-documents-with-gdataxml
:)