Unable to use reference from a Make New command in Core-data app with AppleScript support - cocoa

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>

Related

RestKit 0.20 — What is the preferred way to create a new NSManagedObject?

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.

Promising files from my view-based NSTableView

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.

NSKeyedArchiver: distinguishing between different instances of the same class

I'm implementing support for Lion's "Resume" feature in my OS X app.
I have a custom subclass of NSViewController in which I implemented the method
encodeRestorableStateWithCoder: as:
#implementation MyClass (Restoration)
-(void)encodeRestorableStateWithCoder:(NSCoder*)coder {
[coder encodeObject:_dataMember forKey:#"object_key"]; // I get the warning below when this line is executed for the second time
}
- (void)restoreStateWithCoder:(NSCoder *)coder {
_dataMember = [coder decodeObjectForKey:#"object_key"];
}
#end
However, since I have multiple instances of MyClass, different values are saved into the same key ("object_key") and I get the following warning from Cocoa:
NSKeyedArchiver warning: replacing existing value for key
'object_key'; probable duplication of encoding keys in class hierarchy
What is the best practice to overcome this problem?
Edit: I found here that each instance automatically has its own namespace to avoid collisions, so the problem might be in the way I'm manually calling encodeRestorableStateWithCoder to different instances with the same NSCoder object without telling it that these are different instances. However, I still can't figure out how to do that properly.
Thanks in advance!
To overcome this problem, it is possible to create a new NSMutableData where each of which is written by a separate (new) NSKeyArchiver, and store them all in an array which is stored in the original NSCoder object.
Here is an example for encoding the restorable state of subitems. The decoding part can be straight-forward given this code.
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
// Encode subitems states:
NSArray* subitems = self.items;
NSMutableArray* states = [NSMutableArray arrayWithCapacity: subitems.count];
for (SubItemClass* item in subitems)
{
NSMutableData* state = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:state];
[item encodeRestorableStateWithCoder:archiver];
[archiver finishEncoding];
[states addObject:state];
}
[coder encodeObject:states forKey:#"subitems"];
}

NSArrayController creating, modifying and then selecting a new object

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

Manual Core Data schema migration without "document changed" warning?

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

Resources