Unsure if UIManagedDocument is giving me a managed object context? - ios

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");
}
}

Related

UIManagedDocument file being automatically deleted with saveToURL:forSaveOperation:completionHandler:

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);
}
}];

How can i get a NSManagedObjectContext without help of UIApllication delegate?

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 Can't get an nsmanageobjectcontext

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.

Transitioning iOS app to completely new core data data model

I have inherited an iOS app that needs to undergo major, major changes. In its current form, it uses a single UIManagedDocument (not synced with iCloud, but just used to avoid Core Data boilerplate) to store user data. I need to make drastic changes to the data model and I'd like to switch to a normal Core Data stack. The existing codebase was also pretty unusable, so I decided to create a new project but with the same app ID so it can go out as an update.
I'm not sure how to import existing users' data to the new data model.
The current model has 3 main entities and I only need 2 attributes from each of them (the rest of the old data can get thrown away). I've created my new data model, and then I copied all the old model files from previous project plus the old data model.
Then I wrote an importer class:
-(BOOL)isImportNeeded
{
return [[NSFileManager defaultManager] fileExistsAtPath:[[self URLToOldModel] path]];
}
-(NSURL*)URLToOldModel
{
NSURL* URL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
return [URL URLByAppendingPathComponent:#"Data"];
}
-(UIManagedDocument*)managedDocumentAtPath:(NSString*)path
{
return [[UIManagedDocument alloc] initWithFileURL:[self URLToOldModel]];
}
-(void)performWithDocument:(void (^)(void))onDocumentReady
{
if (self.managedDoc.documentState == UIDocumentStateClosed) {
// I put this code here for debug purposes
NSError* e = nil;
if (![self.managedDoc readFromURL:self.managedDoc.fileURL error:&e]) {
NSLog(#"Couldn't read UIManagedDocument: %#", [e localizedDescription]);
}
// [self.managedDoc openWithCompletionHandler:^(BOOL success){
// if (success)
// onDocumentReady();
// else
// NSLog(#"Could not open document");
// }];
}
else if (self.managedDoc.documentState == UIDocumentStateNormal)
{
onDocumentReady();
}
}
-(void)import
{
self.managedDoc = [self managedDocumentAtPath:[[self URLToOldModel] path]];
self.sourceManagedObjectContext = self.managedDoc.managedObjectContext;
[self performWithDocument:^{ ... }];
}
That NSLog prints out the following: Couldn't read UIManagedDocument: The operation couldn’t be completed. (Cocoa error 134100.) Drilling a bit more into the error's userInfo dictionary, I get this as the error reason: "The model used to open the store is incompatible with the one used to create the store".
I've made sure that the old data model is added to the project and is in the "Compile Sources" build phase.
I learned more about UIManagedDocument and realized that when it's getting initialized, it automatically creates a union of all data models in bundle and the way to stop this is by subclassing UIManagedDocument and overriding the managedObjectModel accessor. I did so, by creating a subclass with just the following method:
-(NSManagedObjectModel*)managedObjectModel
{
NSString* modelPath = [[NSBundle mainBundle] pathForResource:#"Data" ofType:#"momd"];
modelPath = [modelPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL* modelURL = [NSURL URLWithString:modelPath];
return [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
}
However, I still get the same error.
A friend of mine suggested this, which I've used in every core data app I've ever worked on - no idea how it escaped me:
self.managedDoc.persistentStoreOptions = #{NSIgnorePersistentStoreVersioningOption: #YES,
NSMigratePersistentStoresAutomaticallyOption: #YES,
NSInferMappingModelAutomaticallyOption: #YES};
That did it.

Prevent UIDocument openWithCompletionHandler being called when already opening

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.

Resources