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.
Related
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);
}
}];
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.
I am trying my hand at some very basic implementation of MagicalRecord to get the hang of it and run into the following.
When I save an entry and then fetch entries of that type it will come up with the entry I just saved. However, when I save the entry, close the app, start it again, and then fetch, it comes up empty.
Code for saving:
- (void)createTestTask{
NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];
Task *task = [Task createInContext:localContext];
task.tName = #"First Task";
task.tDescription = #"First Task created with MagicalRecord. Huzzah!";
NSError *error;
[localContext save:&error];
if (error != Nil) {
NSLog(#"%#", error.description);
}
}
Code for fetching: (all I want to know here if anything is actually saved)
- (void) fetchTasks{
NSArray *tasks = [Task findAll];
NSLog(#"Found %d tasks", [tasks count]);
}
I am sure I am missing something here, but not anything I can seem to find on stackoverflow or in the Tutorials I looked at.
Any help is welcome.
I have to ask the obvious "Is it plugged in" question: Did you initialize the Core Data Stack with one of the +[MagicalRecord setupCoreDataStack] methods?
Did your stack initialize properly? That is, is your store and model compatible? When they aren't, MagicalRecord (more appropriately, Core Data) will set up the whole stack without the Persistent Store. This is annoying because it looks like everything is fine until it cannot save to the store...because there is no store. MagicalRecord has a +[MagicalRecord currentStack] method that you can use to examine the current state of the stack. Try that in the debugger after you've set up your stack.
Assuming you did that, the other thing to check is the error log. If you use
[localContext MR_saveToPersistentStoreAndWait];
Any errors should be logged to the console. Generally when you don't see data on a subsequent run of your app, it's because data was not saved when you thought you called save. And the save, in turn, does not happen because your data did not validate correctly. A common example is if you have a required property, and it's still nil at the time you call save. "Normal" core data does not log these problems at all, so you might think it worked, when, in fact, the save operation failed. MagicalRecord, on the other hand, will capture all those errors and log them to the console at least telling you what's going on with your data.
When i have started with magical record I was also facing this problem, problem is context which you are using to save data. here is my code which might help you
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *userInfoArray = [UserBasicInfo findByAttribute:#"userId" withValue:[NSNumber numberWithInt:loggedInUserId] inContext:localContext];
UserBasicInfo* userInfo;
if ([userInfoArray count]) {
userInfo = [userInfoArray objectAtIndex:0];
} else {
userInfo = [UserBasicInfo createInContext:localContext];
}
userInfo.activeUser = [NSNumber numberWithBool:YES];
userInfo.firstName = self.graphUser[#"first_name"];
userInfo.lastName = self.graphUser[#"last_name"];
userInfo.userId = #([jsonObject[#"UserId"] intValue]);
userInfo.networkUserId = #([jsonObject[#"NetworkUserId"] longLongValue]);
userInfo.userPoint = #([jsonObject[#"PointsEarned"] floatValue]);
userInfo.imageUrl = jsonObject[#"Picturelist"][0][#"PictureUrL"];
userInfo.imageUrlArray = [NSKeyedArchiver archivedDataWithRootObject:jsonObject[#"Picturelist"]];
} completion:^(BOOL success, NSError *error) {
}];
Use this when your done
[[NSManagedObjectContext MR_defaultContext]saveToPersistentStoreAndWait];
I'm (sort of) following the Stanford CS193P ios class and I'm trying to get a document context without having to pass from controller to controller. Prof Haggerty uses this method to get his context, but it doesn't work for me. I'm spelling everything correctly and I can get the context when I pass it, but not when I get it this way.
Am I missing something?? I just want to get the context for the database that I know I've created without having to pass in.s
- (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]]) {
[document saveToURL:url
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if (success) {
self.managedObjectContext = document.managedObjectContext;
[self refresh];
}
}];
} else if (document.documentState == UIDocumentStateClosed) {
[document openWithCompletionHandler:^(BOOL success) {
if (success) {
self.managedObjectContext = document.managedObjectContext;
}
}];
} else {
self.managedObjectContext = document.managedObjectContext;
}
}
I don't think you misspelled anything, but you might have the wrong expectation of what that method is doing. The method is not returning any context. It sets the context as a class property (but only in some cases!).
So after calling this method you should be able to access the self.managedObjectContext property.
However.. The method implementation is not very clean and therefore a bit dangerous. It will set the 'managedObjectContext' property only for successful scenarios. The method does not clear the self.managedObjectContext property in other cases, so in those cases it's unclear what the self.managedObjectContext is pointing to.
Since the method does not provide any success status you can never trust if the managedObjectContext has been set properly.
I had the same problem (also doing Stanford class). I've since found the solution, you can find it here iOS 7 Completion handler never gets called
OpenWithCompletionHandler is asynchronous, so the application might try to use the context even before completion handler block finishes executing. Hence, it seems like the method doesn't work. Using a run loop will solve your problem.
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");
}
}