Packages and Core Data documents

9 July 2007

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

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.

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.

About these ads

6 Responses to “Packages and Core Data documents”

  1. Dan Messing Says:

    What happens if the file moves on disk (ie – drop the document into another folder in the Finder while the document is open), and you attempt a Save, or Save As operation?

  2. acaro Says:

    Surprisingly, it works… My traces shows that, after moving the package on disk, the document is saved to some temporary file (what I don’t understand fully), but in the end the original data file inside of the package is also updated. When I attempt a Save as, I can save the document, the only glitch being that I have to explicitly set the location (e.g., click on the desktop icon in the file save panel) otherwise the save button is not enabled. Ok, I admit that it’s not perfect, but it’s the closest thing to a working implementation that I got… :-) Thanks for your test case!


  3. Very nice article. There is just one minor thing, when you write:

    ok, err = super_initWithContentsOfURL_ofType_error(url, type, nil)

    Since you’re passing ‘nil’ for the argument error, RubyCocoa will never return you the error, but just the normal return value. If you want to retrieve both, the rule is to omit the passed-by-reference argument in the call:

    ok, err = super_initWithContentsOfURL_ofType_error(url, type)

    Also, just to be informational, when you override a method that has a passed-by-reference argument, as you’re doing in this article, if you want to set the value of the argument, you can use ObjcPtr#assign.

    There are some documentation on the RubyCocoa website about both points:

    http://rubycocoa.sourceforge.net/WorkingWithPointers

    http://rubycocoa.sourceforge.net/AssignValueToPointerArgument

  4. acaro Says:

    Hi Laurent, thank you very much for your comment and your remarks! I had effectively missed those RubyCocoa bits… I look forward to integrating your suggestion in my code soon!


  5. [...] 19th, 2007 This post is a follow-up to another post I wrote on the very same subject. I am showing here the full implementation of a [...]

  6. billibala Says:

    Great article, Laurent!

    I’ve done all the studied you mentioned in your article and, luckily, I found your article in google. I’ll try your suggestion in my application. Thanks a lot!!!


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: