Nspersistentdocument Core Data Tutorial For Mac
I'm working on an OSX (Mac OS) document based app with Core Data. I'm having problems importing in a PrivateContext. Importing in the MainContext works fine, but of course freezes the UI. The app has a NSPersistentDocument (Document.h and.m). In this document I have autosavesInPlace enabled (YES). Introduction to Core Data, Part IV Storing Fetch Requests in your Data Model. By Jeff LaMarche. In the last Core Data article we talked about creating Fetch Requests programmatically using format strings and predicates. Using format strings to create Fetch Requests is a little bit inelegant, although it does give you a tremendous amount of. The Core Data store is created on startup from a plist file. Our aim is to create an editable version of that core data store that we can managed on the Mac and then imported into the iOS app. Creating An OS X Target. The first step is to add an OS X target to the WorldFacts Xcode project. Introduction to Core Data, Part IV Storing Fetch Requests in your Data Model. By Jeff LaMarche. In the last Core Data article we talked about creating Fetch Requests programmatically using format strings and predicates. Using format strings to create Fetch Requests is a little bit inelegant, although it does give you a tremendous amount of.
You all know how easy it is to create a Core Data application by using XCode and Interface Builder. Things get trickier, though, if you want to create a document-based application that uses package documents. This post will guide you through the process.
A bit of context
Bundles are a great way to package files in a directory structure. I won’t go into detail here about all the advantages that bundles offer and what important role they play in MacOS X. So, I will assume that the reader knows what a bundle is and simply tell my story.
One of the feature I wanted to implement in a simple Core Data document-based application I am developing was the possibility of using package documents to store together all the files related to some specific sort of document.
After the miserable failure of my first, naive attempt at subclassing NSPersistentDocument
, I resorted to the web for insights. My feeling that the task at hand was going to be hard, was immediately confirmed by this CocoaBuilder thread. Indeed, a possible implementation was proposed, based on overriding a few NSPersistentDocument
methods, but it did not really work, since it crashed at the second attempt at saving a document. So I kept on googling around and found about a completely different approach to the problem, namely subclassing NSDocument (instead of NSPersistenDocument). That amounted to sort of reimplementing from scratch NSPersistentDocument
, but did not work for me either. So I realized I had to go deeper down into NSPersistentDocument
design, if I wanted to come up with anything useful, be it simply an understanding of the reason why mixing NSPersistentDocument
and packages was possibly beyond reach.
Design of a (Core Data) persistent package document
If you look at Apple documents about NSPersistentDocument
, you’ll find a very streamlined document class, with three methods that literally scream for you to override them. They are:
– readFromURL:ofType:error:
– revertToContentsOfURL:ofType:error:
– writeToURL:ofType:forSaveOperation:originalContentsURL:error:
A fourth method documented in NSPersistentDocument
, that is relevant to our present discussion, is:
- configurePersistentStoreCoordinatorForURL:ofType:error:
Briefly, each of the methods in the upper block is called on response to an action on the user’s part (creating a new document, saving or reverting the current document), while the fourth is called once for each document, usually when it’s first written to or read from disk.
In Apple document, you can further read:
“You can customize the architecture of the persistence stack by overriding the methods managedObjectModel and configurePersistentStoreCoordinator:forURL:ofType:error:
. You might wish to do this, for example, to specify a particular managed object model, or to distribute storage of different entities among different file stores within a file wrapper.”
Actually, mixing file wrappers and configurePersistentStoreCoordinator:forURL:ofType:error:
does not seem to work, as you can read here. So I did not even bother to try it. If you don’t use file wrappers, you will quickly hit a wall, because NSPersistentDocument swaps documents around, and if you don’t use file wrapper it will do that with the Core Data data file instead of with the whole package.
An approach that leads nowhere
Nspersistentdocument Core Data Tutorial For Mac Osx
An approach I tried out is the following. Apparently, it should be straightforward to override the above mentioned methods, so that you can:
- “intercept” a call to them before the url (you see, each of the methods accepts an
NSURL
as its first argument) is actually accessed byNSPersistentDocument
; - change the url on-the-fly to make it point to the actual Core Data store inside of the package document, and possibly create the directory underlying the latter;
- call the base class implementation with the new url and return its output;
and get, hopefully, the kind of behaviour desired.
That was, by the way, the approach followed in the CocoaBuilder link I mentioned above, but in reality, apart from a few shortcomings of that implementation, the complex interplay going on among these methods makes things a little trickier.
In fact, all three methods of the above code block, apart from being called directly from NSApplicationMain, also call one another (revertToContentsFromURL:
calls readFromURL:
; both readFromURL:
and writeToURL:
call configurePersistentStoreCoordinator:
) and you must pay attention to not “fix” your URL twice. So, either you implement different behaviours to take into account the fact that, when called from another NSPersistentDocument
method, the url will already be pointing to the right place, or you define an idempotent method that returns the path to the inner data file.
If you take this approach, another point you need to consider is that writeToURL:
has an originalContentsURL:
argument that is not null on all calls except the first one (of course, when you firstly save a document, there is no “original content” yet). You’ll also have to deal with this url and “fix” it the same way as done with the other one.
Core Data Tutorial Iphone
Finally, you have the option of using the fileURL:/setFileURL:
methods. Setting fileURL will make your NSPersistentDocument
remember the actual location of the data file. So, if you call setFileURL
in your override of readFromURL:
(i.e., when opening a document), then successive calls to writeToURL:
will have the url parameter already set up to point to the Core Data data file. This appears to be really handy, although it does not apply to the originalContentsURL:
parameter. Anyhow, be consequent…
With all this in mind, I have tried hard to devise an implementation of a NSPersistentDocument
subclass. However, in the end, no matter what I tried, I could not succeed in getting a reliable behaviour by following this approach. There was something that was wrong somewhere, possibly you cannot play around with the url your NSPersistentDocument
subclass is managing from within that same class, or at least, I have not found the correct way to do it.
Carrion: deluxe edition for mac. A working solution
The solution was, anyway, closer than I thought. It seemed clear to me that the right way had to do with changing how NSPersistentDocument
was initialized. So, I overrode the initWithContentsOfURL:ofType:error
method like this (it’s ruby, but porting back to ObjC is straightforward):
def initWithContentsOfURL_ofType_error(url, type, err)
url = dataFilePath(url)
ok, err = super_initWithContentsOfURL_ofType_error(url, type, nil)
if (!ok)
# YOUR ERROR MANAGEMENT HERE
end
ok
end
This method is executed each time you open a document, and it makes all of the other methods of the class receive and NSURL
pointing at the right place.
I had still to deal with a few issues. First of all, I had to create somewhere the directory corresponding to the bundle. The right place to do this was the writeToURL:
method.
Secondly, I had to consider the case of a newly created document, which calls the init
method into action. Unfortunately, in this case the approach taken in initWithContentsOfURL:ofType:error
would not do, since no url is specified when creating a new document. Again, the right place to tackle this was writeToURL:
, where I needed a way to tell whether the method was called for the first time. This was ready accomplished by looking at the fact that, as mentioned above, the originalDocumentsURL:
argument to writeToURL:
is set to null on the very first call. This gave me the following code for writeToURL:
def writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, error)
if (content nil)
path = url.relativePath
url = dataStorePathFromPackageURL(url)
if (!OSX::NSFileManager.defaultManager.createDirectoryAtPath_attributes(path, nil))
return false
end
end
ok, error = super_writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, nil)
if (!ok)
# YOUR ERROR MANAGEMENT HERE
end
ok
end
A few more bits to check that the package directory exists in readFromURL:
, and it was done. There is no need to override revertToContentsOfURL:
, but you can if you would like to do anything special when reverting a document.
The code
class MyDocument < OSX::NSPersistentDocument
def initWithContentsOfURL_ofType_error(url, type, err)
url = dataFilePath(url)
ok, err = super_initWithContentsOfURL_ofType_error(url, type, nil)
if (!ok)
# YOUR ERROR MANAGEMENT HERE
end
ok
end
def writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, error)
if (content nil)
path = url.relativePath
url = dataStorePathFromPackageURL(url)
if (!OSX::NSFileManager.defaultManager.createDirectoryAtPath_attributes(path, nil))
return false
end
end
ok, error = super_writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, nil)
if (!ok)
# YOUR ERROR MANAGEMENT HERE
end
ok
end
def readFromURL_ofType_error(url, type, error)
path= packagePathFromDataStoreURL(url)
if (!OSX::NSFileManager.defaultManager.fileExistsAtPath_isDirectory(path, nil))
result, err = super_readFromURL_ofType_error(url, type, nil)
if (!result)
# YOUR ERROR MANAGEMENT HERE
end
result
end
# here go the rubycocoa template generated methods
# (managedObjectModel, setManagedObjectContext, windowControllerDidLoadNib)
end
As usual, in the code above, error management is poor to not existing. In particular, you should take care to never return false from NSPersistentDocument
methods that returns an NSError
without correctly ensuring that one is returned.