I'm writing a Mac app with a view-based table view. It's a list of images, which I intend for the user to be able to drag to the Finder to save each image to a file.
The data source owns an array of custom model objects. The model objects all conform to the NSPasteboardWriting protocol as follows:
- (NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard {
//self.screenshotImageDataType is set after the image is downloaded, by examining the data with a CGImageSource.
//I have verified that it is correct at this point. Its value in my test was #"public.jpeg" (kUTTypeJPEG).
return #[ self.screenshotImageDataType, (__bridge NSString *)kUTTypeURL, (__bridge NSString *)kPasteboardTypeFilePromiseContent ];
}
- (id)pasteboardPropertyListForType:(NSString *)type {
if (UTTypeEqual((__bridge CFStringRef)type, (__bridge CFStringRef)self.screenshotImageDataType)) {
return self.screenshotImageData;
} else if (UTTypeEqual((__bridge CFStringRef)type, kUTTypeURL)) {
return [self.screenshotImageURL pasteboardPropertyListForType:type];
} else if (UTTypeEqual((__bridge CFStringRef)type, kPasteboardTypeFilePromiseContent)) {
return self.screenshotImageDataType;
}
id plist = [self.screenshotImage pasteboardPropertyListForType:type]
?: [self.screenshotImageURL pasteboardPropertyListForType:type];
NSLog(#"plist for type %#: %# %p", type, [plist className], plist);
return [self.screenshotImage pasteboardPropertyListForType:type]
?: [self.screenshotImageURL pasteboardPropertyListForType:type];
}
The URLs that my objects own are web URLs, not local files. They're the URLs the images were downloaded from.
The table view's data-source-and-delegate-in-one implements a method relating to file promises:
- (NSArray *)tableView:(NSTableView *)tableView
namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestinationURL
forDraggedRowsWithIndexes:(NSIndexSet *)rows
{
return [[self.screenshots objectsAtIndexes:rows] valueForKeyPath:#"screenshotImageURL.lastPathComponent"];
}
Logging the value of that expression produces a valid filename with the correct filename extension.
Finally, in windowDidLoad, I have sent the message that turns this whole mess on:
//Enable copy drags to non-local destinations (i.e., other apps).
[self.tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
The stage is set. Here's what happens when the curtains go up:
When I drag to a window owned by the Finder, the view I'm dragging to highlights, indicating that it will accept the drag.
However, when I drop the image, no file is created.
Why doesn't the file whose contents I've promised get created?
The documentation from the NSDraggingInfo protocol's namesOfPromisedFilesDroppedAtDestination: method gives a hint:
The source may or may not have created the files by the time this method returns.
Apparently, at least in table view context, this translates to “create the files yourself, you lazy bum”.
I amended my table view data source's tableView:namesOfPromisedFilesDroppedAtDestination:forDraggedRowsWithIndexes: method to tell each dragged model object to write itself to a file (consisting of the destination directory URL + the filename from the model object's source URL), and implemented that functionality in the model object class. All works now.
Related
I am able to support the Make New command of AppleScript for my app, however the returned 'specified object' (an NSUniqueIDSpecifier) for the core data managed object is useless. The following AppleScript returns the error message:
error "SpellAnalysis got an error: Invalid key form." number -10002 from level id "x-coredata:///Levels/tC5A49E01-1CE1-4ED6-8F6B-BC0AE90E279A2"
tell application "SpellAnalysis"
set thisLevel to make new «class Slev» with properties {«class Saln»:3}
get properties of thisLevel
end tell
So the newly created Levels object can not be acted upon in AppleScript. I've combed the Web for a solution to this and the closest thing I have found is Bill Cheeseman's example app, "WareroomDemo" which specifically deals with Cocoa Scriptability for Core Data apps (the Sketch example does not use Core Data). Unfortunately, it is a dated example, running only on pre-64-bit XCode and I can't actually run it--I can only look at the code. His app's Make Command may have the same limitations for all I know.
The returned 'objectSpecifier' is unable to refer to the created object either as a safe-guard against corrupting Core Data's organizing scheme, or perhaps because the returned object is an un-cashed 'fault'. I think the latter possibility is unlikely because I can force the fault to cash (by getting a property value on the managed object) , yet I get the same error message with the AppleScript.
Here is the method that creates my class:
- (id)newScriptingObjectOfClass:(Class)class forValueForKey:(NSString *)key withContentsValue:(id)contentsValue properties:(NSDictionary *)properties { // Creates a new Lesson object in response to the AppleScript 'make' command.
// Documentation for 'newScriptingObject…' states that to create a new class object when using Core Data, you intercede using the following method (or you can subclass the NSCreateCommand's 'performDefaultImplementation' method and put your NSManagedObject init code there):
if (class == [Levels class]) {
//NSLog(#"class: %#",class);
NSEntityDescription *levelsEntity = [NSEntityDescription
entityForName:#"Levels"
inManagedObjectContext:levelsDBase];
NSManagedObject *levelObject = [[NSManagedObject alloc] initWithEntity:levelsEntity insertIntoManagedObjectContext:levelsDBase];
SLOG(#"lessonObject: %#", lessonObject);
NSString *levelNumberString = [[properties objectForKey:#"levelNumber"] stringValue];
SLOG(#"levelNumberString: %#", levelNumberString);
[levelObject setValue:levelNumberString forKey:#"levelNumber"];
return levelObject; // When using Core Data, it seems that you return the newly created object directly
}
return [super newScriptingObjectOfClass:(Class)class forValueForKey:(NSString *)key withContentsValue:(id)contentsValue properties:(NSDictionary *)properties];
}
Here is my object specifier method:
- (NSScriptObjectSpecifier *)objectSpecifier {
// This NSScriptObjectSpecifiers informal protocol returns a unique ID specifier specifying the absolute string of the URI representation of this managed object. // AppleScript return value: 'level id <id>'.
// The primary container is the application.
NSScriptObjectSpecifier *containerRef = nil; // I understand that if the application is the container, this is value you use for the container reference
NSString *uniqueID = [[[self objectID] URIRepresentation] absoluteString];
return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:containerRef key:#"levelsArray" uniqueID:uniqueID] autorelease];
}
The problem lies with the specifier method. The Sketch example actually uses the technique that I needed. I overlooked it many times because I didn't see how it would apply to Core Data managed objects. Instead of returning the objects uniqueID, you make it return the managedObject index using the 'indexOfObjectIdenticalTo:' method as follows:
- (NSScriptObjectSpecifier *)objectSpecifier {
NSArray *levelsArray = [[NSApp delegate] levelsArray]; // Access your exposed to-many relationship--a mutable array
unsigned index = [levelsArray indexOfObjectIdenticalTo:self]; // Determin the current objects index
if (index != (unsigned)NSNotFound) {
// The primary container is the document containing this object's managed object context.
NSScriptObjectSpecifier *containerRef = nil; // the appliation
return [[[NSIndexSpecifier allocWithZone:[self zone]] initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:containerRef key:#"levelsArray" index:index] autorelease];
} else {
return nil;
}
}
Note that this method resides within a subclass of your Core Data managedObject--in this case, the 'Levels' class. The 'self' within the 'indexOfObjectIndenticalToSelf:' method refers to the current managedObject ('Levels') being handled. Also, be sure to provide the specifier (accessor) type to your 'sdef' file, like this:
<element type="level">
<cocoa key="levelsArray"/>
<accessor style="index"/>
</element>
in getting familiar with core data i have found myself puzzled by the question of what to pass various view controllers (VCs) when trying to add data.
for example, in the CoreDataRecipes project that apple provides as an example (http://developer.apple.com/library/ios/#samplecode/iPhoneCoreDataRecipes/Introduction/Intro.html) they use the following approach
when the user wants to add a recipe to the list of recipes presented in the master table view, and hits the Add button, the master table view controller (called RecipeListTableViewController) creates a new managed object (Recipe) as follows:
- (void)add:(id)sender {
// To add a new recipe, create a RecipeAddViewController. Present it as a modal view so that the user's focus is on the task of adding the recipe; wrap the controller in a navigation controller to provide a navigation bar for the Done and Save buttons (added by the RecipeAddViewController in its viewDidLoad method).
RecipeAddViewController *addController = [[RecipeAddViewController alloc] initWithNibName:#"RecipeAddView" bundle:nil];
addController.delegate = self;
Recipe *newRecipe = [NSEntityDescription insertNewObjectForEntityForName:#"Recipe" inManagedObjectContext:self.managedObjectContext];
addController.recipe = newRecipe;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController];
[self presentModalViewController:navigationController animated:YES];
[navigationController release];
[addController release];
}
this newly created object (a Recipe) is passed to the RecipeAddViewController. the RecipeAddViewController has two methods, save and cancel, as follows:
- (void)save {
recipe.name = nameTextField.text;
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:recipe];
}
- (void)cancel {
[recipe.managedObjectContext deleteObject:recipe];
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:nil];
}
i am puzzled about this design approach. why should the RecipeListViewController create the object before we know if the user wants to actually enter a new recipe name and save the new object? why not pass the managedObjectContext to the addRecipeController, and wait until the user hits save to create the object and populate its fields with data? this avoids having to delete the new object if there is no new recipe to add after all. or why not just pass a recipe name (a string) back and forth between the RecipeListViewController and the RecipeAddController?
i'm asking because i am struggling to understand when to pass strings between segues, when to pass objects, and when to pass managedObjectContexts...
any guidance much appreciated, incl. any links to a discussion of the design philosophies at issue.
Your problem is that NSManagedObjects can't live without a context. So if you don't add a Recipe to a context you have to save all attributes of that recipe in "regular" instance variables. And when the user taps save you create a Recipe out of these instance variables.
This is not a huge problem for an AddViewController, but what viewController do you want to use to edit a recipe? You can probably reuse your AddViewController. But if you save all data as instance variables it gets a bit ugly because first you have to get all data from the Recipe, save it to instance variables, and when you are done you have to do the reverse.
That's why I usually use a different approach. I use an editing context for editing (or adding, which is basically just editing).
- (void)presentRecipeEditorForRecipe:(MBRecipe *)recipe {
NSManagedObjectContext *editingContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
editingContext.parentContext = self.managedObjectContext;
MBRecipe *recipeForEditing;
if (recipe) {
// get same recipe inside of the editing context.
recipeForEditing = (MBRecipe *)[editingContext objectWithID:[recipe objectID]];
NSParameterAssert(recipeForEditing);
}
else {
// no recipe for editing. create new one
recipeForEditing = [MBRecipe insertInManagedObjectContext:editingContext];
}
// present editing view controller and set recipeForEditing and delegate
}
Pretty straight forward code. It creates a new children context which is used for editing. And gets a recipe for editing from that context.
You must not save the context in your EditViewController! Just set all desired attributes of Recipe, but leave the context alone.
After the user tapped "Cancel" or "Done" this delegate method is called. Which either saves the editingContext and our context or does nothing.
- (void)recipeEditViewController:(MBRecipeEditViewController *)editViewController didFinishWithSave:(BOOL)didSave {
NSManagedObjectContext *editingContext = editViewController.managedObjectContext;
if (didSave) {
NSError *error;
// save editingContext. this will put the changes into self.managedObjectContext
if (![editingContext save:&error]) {
NSLog(#"Couldn't save editing context %#", error);
abort();
}
// save again to save changes to disk
if (![self.managedObjectContext save:&error]) {
NSLog(#"Couldn't save parent context %#", error);
abort();
}
}
else {
// do nothing. the changes will disappear when the editingContext gets deallocated
}
[self dismissViewControllerAnimated:YES completion:nil];
// reload your UI in `viewWillAppear:`
}
I'm curious to know what the best way is to create a new NSManagedObject in RestKit 0.20? Currently my code looks something like this:
#pragma mark - navigation buttons
- (void)createButtonDidTouch
{
// create new album object
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
NSManagedObjectContext *parentContext = RKObjectManager.sharedManager.managedObjectStore.mainQueueManagedObjectContext;
context.parentContext = parentContext;
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Album" inManagedObjectContext:parentContext];
Album *newAlbum = [[Album alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:context];
// pass object to create view to manipulate
AlbumCreateViewController *createViewController = [[AlbumCreateViewController alloc] initWithData:newAlbum];
createViewController.delegate = self;
createViewController.managedObjectContext = context;
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:createViewController];
navController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentViewController:navController animated:YES completion:nil];
}
#pragma mark - create view controller delegate
- (void)createViewControllerDidSave:(NSManagedObject *)data
{
// dismiss the create view controller and POST
// FIXME: add restkit code to save the object
NSLog(#"save the object...");
NSDictionary *userInfo = [KeychainUtility load:#"userInfo"];
NSString *path = [NSString stringWithFormat:#"/albums/add/%#/%#", userInfo[#"userID"], userInfo[#"apiKey"]];
[RKObjectManager.sharedManager postObject:data path:path parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
operation.targetObject = data;
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"create album error: %#", error);
}];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)createViewControllerDidCancel:(NSManagedObject *)data
{
// dismiss the create view controller
NSLog(#"delete the object...");
// FIXME: add restkit code to delete the object
[self dismissViewControllerAnimated:YES completion:nil];
}
I'm also curious to know what my responsibilities are for saving / deleting this object. If I POST to the server via RestKit is the managed object context saved?
What if I decide to cancel this creation process — what's the preferred way to delete this object?
Basically how much is RestKit doing for me, and what should I make sure I'm doing. I haven't found much documentation on this and would like to be clear on it.
When you initialize an RKManagedObjectRequestOperation for a given object, RestKit will obtain a permanent object ID for that object and then create a child managed object context whose parent context is the context the object is inserted into. The operation then executes the HTTP request to completion and obtains a response.
If the response is successful and the mapping of the response is successful (note that the mapping occurs within this private child context), then the private child context is saved. The type of save invoked is determined by the value of the savesToPersistentStore property (see http://restkit.org/api/0.20.0/Classes/RKManagedObjectRequestOperation.html#//api/name/savesToPersistentStore).
When YES, the context is saved recursively all the way back to the persistent store via the NSManagedObjectContext category method saveToPersistentStore (see http://restkit.org/api/0.20.0/Categories/NSManagedObjectContext+RKAdditions.html).
When NO, the context is saved via a vanilla [NSManagedObjectContext save:] message, which 'pushes' the changes back to the parent context. They will remain local to that context until you save them back. Keep in mind that managed object context parent/child hierarchies can be as long as you create within the application.
If the HTTP request failed or there was an error during the mapping process, the private context is not saved and the operation is considered failed. This means that no changes are saved back to the original MOC, leaving your object graph just as it was before the operation was started (except the object being sent, if temporary, now has a permanent object ID but is still unsaved).
The way you do it should works (calling each time the MOC in each of your VC), but is not "recommended".
What Apple suggests, just like any Core Data app, is the "pass the baton" style.
Nested contexts make it more important than ever that you adopt the
“pass the baton” approach of accessing a context (by passing a context
from one view controller to the next) rather than retrieving it
directly from the application delegate.
See here:
http://developer.apple.com/library/ios/#releasenotes/DataManagement/RN-CoreData/_index.html
As for your second question, RestKit should manage saving/updating your Core Data stack upon success of your api calls if everything is well mapped/setup.
From blake the RK creator:
if you POST or PUT a Core Data object, RK obtains a permanent object
ID for it and then creates a secondary managed object context, fires
the request, and maps the response (if possible). if the response and
the mapping are successful, it will either save it back to the parent
context or all the way back to the persistent store (i.e. into SQLite)
based on the value of the savesToPersistentStore.
The main view of my NSPersistentDocument based application is a table view (bound to an NSArrayController) showing the list of records, below it there is an "add record" button. I want the button to cause the following (supposedly trivial) behavior.
Create an new object
Set some defaults to the new object (that are stored in the main document and not available globally)
Add it to the table view.
Here are the things that I tried or dismissed:
use the NSArrayController "add" action - problem: will not return the new object and implementation is deferred so it is impossible to modify the newly created object
Override the init of the data class - will not work - I need to access data that is stored in the document class instance
Subclass NSArrayController and override "newObject" - again - will not work because I need to access data that is stored in the document.
Following code "almost" worked:
- (IBAction)newRecord:(id)sender
{
MyDataClass *newRecord = [recordsArrayController newObject];
newRecord.setting1=self.defaultSetting1;
newRecord.setting2=self.defaultSetting2;
// ... etc...
[recordsArrayController addObject:newRecord];
[recordsTable scrollRowToVisible:[recordsTable selectedRow]];
[newRecord release];
}
This code actually works well, for unsaved documents. But if I save the document and re-open it then clicking on the add button results in the new record showing twice in the table.
Obviously the "addObject" is redundant (although it works fine in unsaved documents) but without it the new object is not selected.
Simple case that should work:
MyDataClass *newRecord = [controller newObject];
// configure newRecord
[controller addObject:newRecord];
[newRecord release];
In order for the new object to be selected, the controller needs to have been configured for -setSelectsInsertedObjects:YES previously.
But, there's an alternative which I'd consider more proper. Subclass NSArrayController like so (slighty pseudo-code):
#interface MyRecordController : NSArrayController
#property id recordSetting1;
#property id recordSetting2;
#end
#implementation MyRecordController
#synthesize recordSetting1;
#synthesize recordSetting2;
- (id)newObject
{
id result = [super newObject];
newRecord.setting1 = self.recordSetting1;
newRecord.setting2 = self.recordSetting2;
return result;
}
#end
So, your code then becomes:
- (IBAction)newRecord:(id)sender
{
recordsArrayController.recordSetting1 = self.defaultSetting1;
recordsArrayController.recordSetting2 = self.defaultSetting2;
[recordsArrayController add:self];
}
Really, all you need to do is modify your code to omit the addObject: call. To make your new object selected, just do this:
[recordsArrayController setSelectedObjects:[NSArray arrayWithObject:newObject]];
before you do your call to swcrollRowToVisible:. You're right that the addObject: call is unneeded. As you've seen, it's ending up in the array controller twice.
Also, you won't need to call [newRecord release]. The documentation says the object is retained by the array controller. It's not failing now because it's being retained a second time when you do addObject:.
The data model for my Core Data document-based app (10.5 only) is in a
framework, so automatic schema upgrades using a Core Data mapping
model don't appear to work. It appears that the Core Data machinery
doesn't find the appropriate data models or mapping model when they
are not in the app's main bundle. So, instead of using the automatic
migration, I'm running a migration manually in
configurePersistentStoreCoordinatorForURL:ofType:... in my
NSPersistenDocument subclass (code below). I migrate the persistent
store to a temporary file and then overwrite the existing file if the
migration succeeds. The document then presents an error with the
message "This document's file has been changed by another application
since you opened or saved it." when I try to save. As others on this
list have pointed out, this is due to my modification of the
document's file "behind its back". I tried updating the document's
file modification date, as shown below, but I then get an error dialog
with the message "The location of the document "test.ovproj" cannot be
determined." when I try to save. I'm less sure of the reason for this
error, but trading one unnecessary message (in this case) for an other
isn't quite what I was going for.
Can anyone offer some guidance? Is there a way to manually upgrade the
schema for a document's persistent store without triggering one of
these (in this case unnecessary) warnings?
code for upgrading the data store in my subclasses
-configurePersistentStoreCoordinatorForURL:ofType:... :
if(upgradeNeeded) {
NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:VUIModelBundles() orStoreMetadata:meta];
if(sourceModel == nil) {
*error = [NSError errorWithDomain:VUIErrorDomainn ode:VUICoreDataErrorCode localizedReason:BWLocalizedString(#"Unable to find original data model for project.")];
return NO;
}
NSManagedObjectModel *destinationModel = [self managedObjectModel];
NSMigrationManager *migrationManager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:destinationModel];
NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:VUIModelBundles() forSourceModel:sourceModel destinationModel:destinationModel];
if(mappingModel == nil) {
*error = [NSError errorWithDomain:VUIErrorDomain code:VUICoreDataErrorCode localizedReason:BWLocalizedString(#"Unable to find mapping model to convert project to most recent project format.")];
return NO;
}
#try {
//move file to backup
NSAssert([url isFileURL], #"store url is not a file URL");
NSString *tmpPath = [NSString tempFilePath];
id storeType = [meta objectForKey:NSStoreTypeKey];
if(![migrationManager migrateStoreFromURL:url
type:storeType
options:storeOptions
withMappingModel:mappingModel
toDestinationURL:[NSURLfileURLWithPath:tmpPath]
destinationType:storeType
destinationOptions:storeOptions
error:error]) {
return NO;
} else {
//replace old with new
if(![[NSFileManager defaultManager] removeItemAtPath:[url path] error:error] ||
![[NSFileManager defaultManager] moveItemAtPath:tmpPath toPath:[url path] error:error]) {
return NO;
}
// update document file modification date to prevent warning (#292)
NSDate *newModificationDate = [[[NSFileManager defaultManager] fileAttributesAtPath:[url path] traverseLink:NO] bjectForKey:NSFileModificationDate];
[self setFileModificationDate:newModificationDate];
}
}
#finally {
[migrationManager release];
}
}
}
return [super configurePersistentStoreCoordinatorForURL:url ofType:fileType modelConfiguration:configuration storeOptions:storeOptions error:error];
I haven't run across this particular situation, but I have a few guesses. First, instead of using -removeItemAtPath: and -moveItemAtPath: when you want to switch files, use the FSExchangeObjects() function instead. NSDocument uses FSRefs to track the file and unless you use FSExchangeObjects(), it'll realize that it's looking at a completely different file.
Second, you can manually set your document's managed object model by overriding -managedObjectModel, in particular using the +mergedModelFromBundles: method to load the models from your framework. According to the docs, it should by default merge any models in the main bundle and in all linked frameworks, so this should only be necessary for dynamically loaded bundles. Don't know why that's not working for you, but I haven't tried this. To figure out what bundles to search, NSBundle's +bundleForClass: method is your friend.
Beware FSExchangeObjects()! It does not support all volumes types, see bSupportsFSExchangeObjects. I'm looking for a replacement myself. Option seem to be MoreFilesX's FSExchangeObjectsCompat or 10.5's FSReplaceObjects().
10 years later...
I encountered the same issue, and with the new API for NSDocument, you can update the document's fileModificationDate with the new date of the updated file after doing the migration
migrate()
if let newModificationDate = try? NSFileManager.defaultManager().attributesOfItemAtPath(url.path!)[NSFileModificationDate] as? NSDate {
self.fileModificationDate = newModificationDate
}
after that you can call super.configurePersistentStoreCoordinatorForURL...
That's because NSDocument is setting the file modification date even before calling readFromURL:ofType
See Document Initialization Message Flow