Nspersistentdocument Core Data Tutorial For Mac

broken image


iOS Swift Tutorial Cocoa Tutorial: Core Data Introduction in iOS and Mac OS Programming Part 1. TUTORIAL 1 OF 3 In this tutorial you are going to learn the basics of Core Data and how to create. This tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages is an alternative programming model. For new development, we recommend Razor Pages over MVC with controllers and views. Some developers prefer to use one pattern throughout the data model. In this tutorial, the variation illustrates that. 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 NSPersistentDocument based class that allows to use package documents embedding a Core Data store. I short, what this post adds to the previous one is: improved encapsulation; NSDocumentController subclass to correctly.

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.

Nspersistentdocument Core Data Tutorial For Mac Shortcut

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

Nspersistentdocument Core Data Tutorial For Macbook

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

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)

Nspersistentdocument core data tutorial for mac osx

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.

Sorry this isn't in tutorial format, but I want to capture this so I don't have to search quite so long next time ;-)!

While I've done several iOS apps, I'm getting started on my first Mac app. If you've spent a year or so with iOS and you think that jumping to the Mac's AppKit will be a breeze, brace yourself! Yes, they both use Objective-C. Yes, they both have a lot in common. But there are some major differences between UIKit and AppKit. And then there's Cocoa Bindings that really make Mac programming fun, but there is definitely a learning curve.

So, on with this post…I'm using Core Data with Bindings to my interface. On one view, I'm binding to an NSImageView, using the imageWell capability which lets you drag images right onto the image view and magically have them populate an NSManagedObject's Binary Data attribute. You hook up your NSImageView to the Value binding of your selection and give it a value transformer of 'NSUnarchiveFromData', and it magically transforms binary data back to an image.

It's all smooth as butter, but now I'm ready to import some contacts using the ABPeoplePickerView. This also works really well once you get all the right frameworks added and classes imported.

You can easily loop through the selectetRecords property of your people picker view to get ABPerson records that have all of the goodies from the Mac Addressbook. You can also easily get the person's image from the addressbook just by accessing the imageData property like: person.imageData

That gives you a pure NSData object. Now you want to store that in your Core Data store. You might think you could just do this:

Nspersistentdocument core data tutorial for macbook

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.

Sorry this isn't in tutorial format, but I want to capture this so I don't have to search quite so long next time ;-)!

While I've done several iOS apps, I'm getting started on my first Mac app. If you've spent a year or so with iOS and you think that jumping to the Mac's AppKit will be a breeze, brace yourself! Yes, they both use Objective-C. Yes, they both have a lot in common. But there are some major differences between UIKit and AppKit. And then there's Cocoa Bindings that really make Mac programming fun, but there is definitely a learning curve.

So, on with this post…I'm using Core Data with Bindings to my interface. On one view, I'm binding to an NSImageView, using the imageWell capability which lets you drag images right onto the image view and magically have them populate an NSManagedObject's Binary Data attribute. You hook up your NSImageView to the Value binding of your selection and give it a value transformer of 'NSUnarchiveFromData', and it magically transforms binary data back to an image.

It's all smooth as butter, but now I'm ready to import some contacts using the ABPeoplePickerView. This also works really well once you get all the right frameworks added and classes imported.

You can easily loop through the selectetRecords property of your people picker view to get ABPerson records that have all of the goodies from the Mac Addressbook. You can also easily get the person's image from the addressbook just by accessing the imageData property like: person.imageData

That gives you a pure NSData object. Now you want to store that in your Core Data store. You might think you could just do this:

When you try to display your Core Data object in your bound interface, that NSImageView will just stare dumbly back at you.

Here's how you need to store that image to get it to work. The trick is converting to an NSImage and using the NSArchiver class to archive it for storage:

Hope that helps!!





broken image