Posted  by 

Nspersistentdocument Core Data Tutorial For Mac

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

Nspersistentdocument core data tutorial for mac desktop

How to Use Cocoa Bindings and Core Data in a Mac App. This is a blog post by Andy Pereira, a software developer at USAA in San Antonio, TX, and freelance iOS and OS X developer. Lately we’re starting to write more Mac app development tutorials on raywenderlich.com, since it’s a natural “next step” for iOS developers to learn! Jul 04, 2018 Core Data is an object graph and persistence framework provided by Apple in the macOS and iOS operating systems. It was introduced in Mac OS X 10.4 Tiger and iOS with iPhone SDK 3.0.

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.”

Nspersistentdocument core data tutorial for mac download

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

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 by NSPersistentDocument;
  • 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.

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.

Core data tutorial iphone

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

Amd 8790m

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

Nspersistentdocument Core Data Tutorial For Mac Desktop


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)

Nspersistentdocument Core Data Tutorial For Mac Pro

end

Nspersistentdocument Core Data Tutorial For Machine Learning

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.