I'm using Core Data with UIManagedDocument for an inventory-keeping app. The problem I'm having is that the "saveToURL:..." method is actually deleting my UIManagedDocument file in the Documents directory when I save using UIDocumentSaveForOverwriting after adding an item to core data. This only happens at first launch from a new build. I created a core data/UIManagedDocument helper singleton to use throughout the app.
Here's how I initialize the UIManagedDocument instance:
#interface VDFCoreDataHelper : NSObject
#property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
#property (strong, nonatomic) UIManagedDocument *managedDocument;
#implementation VDFCoreDataHelper
- (void)createManagedDocument
{
NSURL *docsURL = [self getDocsURL];
if (![[NSFileManager defaultManager] fileExistsAtPath:[docsURL path]]) {
NSLog(#"new doc made");
_managedDocument = [[UIManagedDocument alloc] initWithFileURL:docsURL];
[self saveManagedDocumentForCreation];
[self openManagedDocument];
} else {
NSLog(#"existing doc");
_managedDocument = [[UIManagedDocument alloc] initWithFileURL:docsURL];
[self openManagedDocument];
}
}
CreateManagedDocument is called in the init method.
I have two save methods. One for creating and one for overwriting. The first one is called when I created the managed document.
At this point, I've only saved once and a UIManagedDocument directory and persistent store files exist in my documents folder.
When I want to insert an Item object (an Item entity exists), I call this method:
- (void)insertManagedObject:(NSManagedObject *)object success:(void (^)(BOOL successful))successBlock
{
NSManagedObjectContext *context = [self context];
[context insertObject:object];
NSError *error;
[context save:&error];
if (self.managedDocument.documentState == UIDocumentStateNormal) {
[self.managedDocument saveToURL:[self getDocsURL] forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success){
successBlock(success);
}];
}
}
After "saveToURL:forSaveOperation:" for over writing is called, my managed document directory and files in my Documents folder are all automatically deleted. The managedObjectContext, Item object, and managedDocument object are all valid at this point. The document's URL points to the correct destination, but all the files are gone.
After my "insertManagedObject" method is finished, I use the navigation controller to pop back to the rootViewController which contains a table view listing the items. The data that I added are kept in memory and the fetchedResultsController loads it, but the data is not saved to disk because there isn't a persistent store any longer. When I exit the app and re-enter, nothing shows up and a new managed document is created again.
This only happens if I clear the build and launch it for the first time. If I launched and immediately exit, and then enter the app again, everything works fine. It's this "saveToURL:...: method deleting my persistent store.
I've tried subclassing UIManagedDocument and logging the errors, but it doesn't show any error whatsoever. I've tried commenting out some of the code, but they don't make a difference.
If I don't use "saveToURL", the persistent store doesn't get deleted, but upon re-launch, the fetchResultsController.fetchObjects returns an empty array and tries to access a non-existent indexPath, crashing the app.
I'm considering ditching the UIManagedDocument right about now. Hopefully, someone can tell me what I may be doing wrong, or has had the same problem.
Thanks.
I was struggling with the exactly same problem as you. But wasn't finding any help...
The deleting part with no error whatsoever was driving me nuts, and I almost ditched UIManagedDocument.
But I did something that actually works!.
Actually I think the problem is trying to access a document after creating it, calling to the selector:
[_document saveToURL:self.documentURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {}];
Then all I did was after saving the document I close it, get a new Instance and then reopen it. like this:
[_coreDocument saveToURL:self.documentURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
NSLog(#"Document created");
[_coreDocument closeWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(#"Closed recently created document, will open it again");
_coreDocument = nil;
_coreDocument = [[CheckinManagedDocument alloc] initWithFileURL:self.documentURL];
[_coreDocument openWithCompletionHandler:^(BOOL success) {
NSLog(#"Document oppened afer creating and closing it");
[self documentIsReadyForUse];
}];
}
}];
} else {
NSLog(#"Could not save the document at path: %#", self.documentURL.path);
}
}];
Related
The widgets in our iOS App are custom, and therefore I added a feature to remove parts of the widget. To save settings for the widgets etc. our widgets share Core Data via App groups. However when I delete something from the widget it doesnt seem to always sync correctly. This happens primarily when the app is active in memory.
When I delete something I call this:
-(void)removeWidgetFromUser:(UserModel *)user Widget:(Widget *)widget{
if(widget != nil){
[widgetContext deleteObject:widget];
NSError *error;
if (![widgetContext save:&error]) {
NSLog(#"Unable to remove widget %#", error);
}
}
}
Then I use wormhole to sync the core data in my app and it calls this:
-(void)updateCoreData{
[self.managedObjectContext refreshAllObjects];
}
I am sure both the methods get called. But sometimes the app sees a widget I just removed, and then it also happens to reappear in my Widget.
EDIT:
I think whats happening is that the CoreData context in my app doesnt update correctly and then the widget actually syncs with the CoreData in my app. Therefore the deleted widget re-appears after some time. Still figuring it out...
I finally did it. By implementing the following code:
- (id)initWithCoder:(NSCoder *)decoder {
NSManagedObjectContext *context = [SharedCoreDataObjects sharedInstance].managedObjectContext; // use your NSManagedObjectContext
NSPersistentStoreCoordinator *coordinator = [SharedCoreDataObjects sharedInstance].persistentStoreCoordinator; //use your NSPersistentStoreCoordinator
NSURL *url = (NSURL *)[decoder decodeObjectForKey:#"URIRepresentation"];
NSManagedObjectID *managedObjectID = [coordinator managedObjectIDForURIRepresentation:url];
self = [context existingObjectWithID:managedObjectID error:nil];
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:[[self objectID] URIRepresentation] forKey:#"URIRepresentation"];
}
in my NSManagedObjects I was able use MMWormhole to send the NSManagedObjectContextDidSaveNotification
to the App and then call
[context mergeChangesFromContextDidSaveNotification:messageObject];
To let the context merge the changes. This seems to work perfectly for now!
I have a simple app that i'm building to consolidate my little knowledge about core data. My main problem is that my method for get the NSManagedObjectContext returns nil. The way to get this is showed in one of the Stanford classes about core data. I'm doing something wrong, but i don't know what(My code looks identical with the one showed in class)... Of course, the results are a crash in my app, that complains about "+entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name...", when i try to save the data, but my context is nil.
The code to get the NSManagedObjectContext:
- (void)setupFile {
NSURL *URL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentationDirectory
inDomains:NSUserDomainMask] lastObject];
URL = [URL URLByAppendingPathComponent:#"FlashCardCoreData"];
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:URL];
if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]] == NO) {
[document saveToURL:URL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
self.sharedContext = document.managedObjectContext;
}
}];
} else if (document.documentState == UIDocumentStateClosed) {
// open the document
[document openWithCompletionHandler:^(BOOL success) {
if (success) {
self.sharedContext = document.managedObjectContext;
}
}];
} else {
self.sharedContext = document.managedObjectContext;
}
}
This class is a singleton class for share a global context that i can use in all of the controllers and forms for displaying/saving the objects...
Note: I didn't used the global context shared in the app delegate because i did't choose my app for use the core data(hence it's not an available property in the delegate), and i don't know how can i change that once my app is already created.
Can anyone help me? Any suggest will be appreciated.
Thanks for the attention.
The easiest way to see the correct way to implement CoreData is to simply create a new project and check the "Use Core Data" check box when setting up options for the project.
This will inject all of the boiler plate code that you need for initializing core data. You can then copy it into your singleton class.
You then need to make your Managed Object Data Model.
Once the Data Model is fully configured, simply create an NSManagedObject subclass for the Data Model. This will automatically pull all of the properties/relationships from the Data Model file.
Here is a great tutorial for setting up and implementing Core Data.
EDIT: based on the (very helpful) comment below, after further review, I'd just like to add (for other rookies like myself) that when using UIManagedDocument to obtain your core data functionality, pay attention to WHEN your instance is being opened in your app.
The error I was getting was because my managed object context WAS nil when my fetched results controller was being set up. I moved the method call which set up the fetched results controller to after the UIManagedDocument instance was opened.
It might be very basic, and just a common-sense issue to most, but for us rookies, when core data is not being set up in the app delegate, we need to learn that the document has to be in a usable state before we can set the fetchedResultsController.
I'm working through Paul Hegarty's Stanford lectures on Core Data, specifically the demo lecture for the "Photomania" app.
Instead of using NSDictionaries of photos, I modified the app to include only Model objects that store NSStrings (for example, a person's name, etc.) It's just a learning exercise for me.
I believe I've successfully recreated a UIManagedDocument using his code, and set the view controller's managed object property to that of the document's managed object context through the two methods below (they're his methods, not mine, of course).
I put this code in a tableview controller that will appear on screen so I could test in viewDidLoad if the managed object context exists (it's a subclass of his CoreDataTableViewController class). Is that even a valid test for a managed object context?
I understand the code that creates or opens the UIManagedDocument, but I don't understand why that managed object context is nil (if i modify the little test in viewDidLoad, it will tell me that the context is == nil ).
At this point, nothing has been written to the context, I haven't included any fetched results either.
I'm breaking it down into sections because my simplified version of the lecture app kept giving me the error that the managed object context and fetched results controller were nil.
I want to test, in this case, if I'm getting a valid managed object context before moving forward. I have a strong feeling there's some fundamental piece of info I'm missing in what's essentially copied code (for learning purposes).
Does anyone know why the managed object context is nil? Or SHOULD it be that way at this point because I've missed something when setting it? Or when I set the UIManagedDocument's context as the table view controller's ( self) context?
Any information would be greatly appreciated.
-(void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
_managedObjectContext = managedObjectContext;
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.managedObjectContext) [self useDemoDocument];
}
-(void)useDemoDocument
{
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Demo Document"];
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
//create it
[ document saveToURL:url
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if (success) {
self.managedObjectContext = document.managedObjectContext;
}
}];
} else if (document.documentState == UIDocumentStateClosed){
//open it
[document openWithCompletionHandler:^(BOOL success) {
if (success) {
self.managedObjectContext = document.managedObjectContext;
}
}];
} else {
//try to use it
self.managedObjectContext = document.managedObjectContext;
}
}
-(void)viewDidLoad
{
if (self.managedObjectContext) {
NSLog(#"there is a managed object context");
}
}
I have a singleton class (DTTSingleton) with the following methods:
+ (UIManagedDocument *)managedDocument
{
static UIManagedDocument *managedDocument = nil;
static dispatch_once_t mngddoc;
dispatch_once(&mngddoc, ^
{
if(!managedDocument)
{
NSURL *url = [[DTTHelper applicationDocumentsDirectory] URLByAppendingPathComponent:kDTTDatabaseName];
managedDocument = [[DTTManagedDocument alloc] initWithFileURL:url];
}
});
return managedDocument;
}
+ (void)useDefaultDocumentWithBlock:(completion_block_t)completionBlock
{
if (![[NSFileManager defaultManager] fileExistsAtPath:[DTTSingleton.managedDocument.fileURL path]])
{
[DTTSingleton.managedDocument saveToURL:DTTSingleton.managedDocument.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success)
{
if (success)
{
completionBlock(DTTSingleton.managedDocument.managedObjectContext);
}
else
{
NSLog(#"Failed to save!");
}
}];
}
else if (DTTSingleton.managedDocument.documentState == UIDocumentStateClosed)
{
[DTTSingleton.managedDocument openWithCompletionHandler:^(BOOL success)
{
if (success)
{
completionBlock(DTTSingleton.managedDocument.managedObjectContext);
}
else
{
NSLog(#"Failed to open!");
}
}];
}
else if (DTTSingleton.managedDocument.documentState == UIDocumentStateNormal)
{
completionBlock(DTTSingleton.managedDocument.managedObjectContext);
}
}
And in my UITableViewController I have the following code in the viewDidLoad method:
[DTTSingleton useDefaultDocumentWithBlock:^(NSManagedObjectContext *moc)
{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"SomeEntity"];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc cacheName:nil];
}];
[DTTSingleton useDefaultDocumentWithBlock:^(NSManagedObjectContext *moc)
{
NSLog(#"When this is called it errors because DTTSingleton is already trying to open it!");
}];
When executed I get the error:
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'attempt to open or a
revert document that already has an open or revert operation in flight
I understand why I'm getting this error, it's because I'm trying to open the document when another opening process is already running. So my question are...
1) How do I ensure openWithCompletionHandler is only called once?
2) How do I ensure the second block is executed once the document has opened?
Thanks for any help!
I'm not sure if you've seen this yet, but a good resource would probably be here: http://adevelopingstory.com/blog/2012/03/core-data-with-a-single-shared-uimanageddocument.html
In the event that you're just trying to create your own (rather than using the above link - or similar) and are looking for some input, I can point out a few things I see (though I do not claim by any means to be an expert)...
Anyways, I believe your issue stems here:
// ....
} else if(DTTSingleton.managedDocument.documentState == UIDocumentStateClosed) {
[DTTSingleton.managedDocument openWithCompletionHandler:^(BOOL success) {
if(success) {
completionBlock(DTTSingleton.managedDocument.managedObjectContext);
} else {
NSLog(#"Failed to open!");
}
}];
}
The method openWithCompletionHandler attempts to open a connection to the document asynchronously. The issue with this is that, on your first call to open the document in your UITableView, the code you're using notices the document is closed - so it attempts to open it. This is all fine and dandy, but the code you're using here then re-issues yet another attempt to create an instance of the singleton. More than likely, this is happening so fast (and close together) that it, yet again, attempts to open the document asynchronously.
To test this, try putting a breakpoint after the UIDocumentStateClosed check for the line:
[DTTSingleton.managedDocument openWithCompletionHandler:^(BOOL success)
I believe you'll see this being executed numerous times...
I'm not skilled enough to explain how to solve this, but I would seriously recommend using the approach shown in the link above where he applies a block to assign/track the existence of an open document
Hopefully that helps?
Edit:
* Added the suggestion for the breakpoint.
Edit: Here is another stackoverflow ticket with a similar issue (and suggestion/conclusion) - so I may not be too far off here after all =)
Just thought I'd post back as say the issues I was having have been solved by disabling buttons that access Core Data until the document is ready, this way you'll never try to open the document at the same time as another process. And as for Core Data access in a life cycle handler like viewDidLoad, I implemented a system where by if the document is opening (state kept manually with a variable) it delays the call by looping until the document is open, in essence queuing the calls. Don't forget to use performSelector:withObject:afterDelay: in the call within to loop otherwise you'll get an application crash.
Thanks for your suggestions John.
I have noticed that when I call
[context save:nil];
the saving doesn't occur instantly. I tested that when I try to save and quit the app in one or two seconds. It only works if I keep the app open for 5+ seconds or so.
I have 2 questions:
How can I know when the save is complete? A simple NSLog() will be enough, just for testing purposes.
Can I force a save? Should I?
Testing if [context save] is synchronous
I have tested that many times and this is not the behavior that I'm getting. If I have this code:
[context save:nil]
NSLog(#"Saved");
I see the "Saved" log, quit the app, and when I launch it again and try to fetch the data - nothing there. This doesn't happen if I wait about 5-10 seconds after I see the "Saved" message.
Thoughts?
Some code
- (void)storeSales:(NSArray *)sales {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"EE LLLL d HH:mm:ss Z YYYY"];
for (NSDictionary *saleDictionary in sales) {
Sale *sale = [NSEntityDescription insertNewObjectForEntityForName:#"Sale" inManagedObjectContext:self.context];
sale.productName = [saleDictionary objectForKey:#"description"];
sale.date = [formatter dateFromString:[saleDictionary objectForKey:#"occured_at"]];
NSLog(#"Stored new sale in database.");
}
[self.context save:nil];
}
How I setup the UIManagedDocument
#property (nonatomic, strong) UIManagedDocument *document;
#property (nonatomic, strong) NSManagedObjectContext *context;
...
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"SalesBot Database"];
self.document = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {}];
} else if (self.document.documentState == UIDocumentStateClosed) {
[self.document openWithCompletionHandler:^(BOOL success) {}];
}
self.context = self.document.managedObjectContext;
UPDATE 1
I tried using NSNotificationCenter to receive NSManagedObjectContextDidSaveNotification - but - I'm receiving it twice! Once right after [context save:nil] and again 5-10 seconds later!
So it seems like you are using UIManagedDocument above Core Data. This explains some things.
At first, use -[UIDocument saveToURL:forSaveOperation:completionHandler:] to save your document. From docs:
You should typically use the standard UIDocument methods to save the document.
If you save the child context directly, you only commit changes to the parent context and not to the document store. If you save the parent context directly, you sidestep other important operations that the document performs.
UIManagedDocument works with two managed object contexts. One is working on main thread, the second is saving changes to file in background thread. This is why your changes were saved, but after reopening were lost. The second context did not finish save operation.
This explains also why your notification was triggered two times. One for each context.
The save method is not an asynchronous process. Anything that occurs after you call the save method will be executed after it has saved.
NSLog(#"About to force a save...");
[context save:nil];
NSLog(#"Now I know the save is complete!");
You can observe notification NSManagedObjectContextDidSaveNotification.