Mac GeekeryGet your geek on. |
|
blog advertising is good for you
recent popular content
User login
|
There’s nothing worse than an application that hides its data … no, your data from you. As a developer, you should take into account your experience as a user and avoid lock-in as much as possible, as well as enable useful “mashups” with other applications. To make it easier for users to use both your application and another with the same set of their data, you need to make it easy for users to get that data out of your application. You could support every common export type for your kind of data and hope that’s enough, or you could create a simple API to get at the data so that your users can do the work of making those exports. On the Mac, the easiest and most standard way of doing the latter is AppleScript. With AppleScript you can setup a basic description of where you keep your data and let Cocoa Scripting take over and pull it all out of your program for you without you needing to change much at all. You’ll get features like searching and basic property editing for free. It’s quick, easy, and wonderful. Unless you’re using Core Data. Like many other features in Mac OS X, gaining all the benefits of Core Data exempts you from the “easy” part that comes with the rest of the OS (like, oh, threading and autosave-enabled NSDocuments, for instance). It doesn’t make it impossible, it just makes it a very slight more interesting. In fact, making a read-only AppleScript interface is rather simple. Making a read/write interface is a minor pain in the posterior. Today, I’ll go over how to make a read-only interface and touch on the difficulties of making a read/write interface with Core Data. Later, I plan on covering the read/write interface when I’ve solved the ills of doing so and can explain how I got it to work. So much of this is “hold the bunny ears halfway up and lift your right leg towards the south” that it’s sometimes hard to say how something is working, or not. To discuss this in detail, I’m going to go over creating a small program from scratch to store quick notes. You can follow along with me in Xcode if you like, or just skim to get the basics and add it to an existing project. Ground Work
Now, open the document in Interface Builder. Create a new array controller and call it Notes. In the inspector, set it to Entity mode and tell it to use the Note entity and to auto-prepare the content. Add a table view, a text field, and a text view to the window and bind them to the array controller on the keys you made in the model. Remember to turn off graphics and fonts in the view so you get the “value” binding that’s plain text. In your document controller, you’ll need to make some data to play with. You could hook up buttons and connect them to add: and remove: but then you’d have to make new data every time or open a saved document or such. Just do something like the following in windowControllerDidLoadNib: to make it easier on yourself.
Now run the application and you should see four notes pop up in the table and clicking on each should give you the content listed above. If not, fix it. You should be able to get this far on your own for this tutorial to be of much use to you. Adding Support to Your CodeNow we need to add some basic methods to the model so as to identify it to Cocoa Scripting. You should have the code fleshed out and generally working before adding the scripting definition file, so we’ll get to that after this. AppleScript wants a unique identifier for your object, and it needs to be one that it can come back to at any time and get the same object with. Cocoa Scripting uses the To do this, you need two things on your model objects. First, some way of generating a unique ID. For this, we’ll use the internal Core Data ID for the object. There’s exactly one problem with this: that ID changes for unsaved objects when you save. Just be aware of that and ensure your users are aware as well. So, here’s that method:
- (NSString*) identifier
{
return [[[self objectID] URIRepresentation] absoluteString];
}
Second, you’ll need to implement An object in AppleScript is rarely alone. Almost every object is a child of another object. For instance, a circle in Sketch is a circle in a document in the application Sketch. Perhaps you’ve noticed this when looking at AppleScript error messages when you get something like “circle 1 of document 1 of application Sketch” as an object’s identifier. Well, Core Data objects belong to a Managed Object Context, not a document, and while they are commonly a one-to-one relationship, there’s nothing saying it has to be that way. Similarly, objects have no intrinsic understanding of what document they’re in, so it’s hard to make an object specifier when you don’t know your parent. But, it’s a solvable puzzle. You can ask – (NSScriptObjectSpecifier*) objectSpecifier You may have noticed something there about the Go to your document and create a method that returns a list of every note. It should try and ensure that there is a semi-reliable order to the array as well as Cocoa Scripting will be using the array to answer “the second note of the first document” specifiers for you. Luckily, we have an – (NSArray*) notes We’re doing read-only, so I filled in the other method for you as a reminder. You don’t need to include it if you don’t want to. You’ll see later why this specific item is problematic in Core Data. So, our document can give us a list of notes and our notes can identify themselves. Are we done? Pretty much. All that’s left is to tell Cocoa Scripting about our data and turn on OSA Scripting support for the application and then we’re good to go. Creating the Scripting DefinitionYou could do this by hand, but I’m not going to. I use SDEF Editor to create my SDEF files. Get it, open it, and under the File → Open Standard Suite menu pick Skeleton. This is where we start. The Skeleton option gives you the basic events suites for a document-based application. What we need to do now is add in our data.
Now that you’ve made the class, go into Standard Suite → Classes → document and then click on Elements. Add an element “note” that is Read Only with the Cocoa key “notes”. This is the array we made in the document class earlier. Now save this in the same folder as the project and give it a very simple name as you’re adding it to the Enabling ScriptingYou’re almost done. The last step is to tell Cocoa Scripting that you want to opt-in and where to locate your SDEF file. The following two keys should go somewhere in your <key>NSAppleScriptEnabled</key> <string>YES</string> <key>OSAScriptingDefinition</key> <string>YOUR-NAME.sdef</string> Now, build and run. Then open Script Editor. Be sure and add the Testing ItOkay, you have model objects that can be identified, a document that can list them, and a Scripting Definition that tells the world about them. You’re set. Now do something like the following: tell application "CD-AS Test" tell the first document set theNote to the first note set theContent to the properties of theNote end tell end tell You should get back something like the following (the exact note may differ):
{id:"x-coredata:///Note/t304A2D28-6641-4EB4-94A8-7A9993966108", name:"Note 4", class:note, note text:"Content 4"}
Now, you have limited read and write ability here. You can set However, you cannot set the ID of the items, as those are determined by Core Data. You also cannot create objects as to create a Core Data object you have to tell it which managed object context it’s a part of, and AppleScript cannot do that. You cannot delete objects, either, as that normally means sending them a That said, it’s a good solution for getting data out of a program, and since it takes minimal effort to accomplish, it’s a nice and quick feature to add to make folks happy.
About Adam Knight
Author Biography Adam Knight is one of the founders of Mac Geekery and is a geek at heart. Programmer by day, hacker by night, his daily life revolves around the Macintosh platform, which he has been a user and programmer for since the early days of System 7 when his LCII replaced his Apple //c. In-between tech jobs, he’s managed to learn the basics of any web hacker: PHP, MySQL, Perl, Apache, Linux, *BSD, and the intricacies of ./configure —prefix=~/bombshelter/. Today, codepoet is concentrating on blogging again, writing some software for the Mac by himself (including Notae) and for his company (such as Switchblade) and has a few other toys coming out soon. Bug him over AIM or email [link fixed]. |
Helpful tutorial. AppleScript is the next big item on my ToDo list for LicenseKeeper and your Core Data + Apple Script tips will probably save me hours of spinning my wheels. I look forward to your insights on adding “write” capabilities to Apple Script support.
http://www.quecheesoftware.com/downloads/WareroomDemo.html is a great example project of a CoreData application with complete AppleScript support for both reading and writing.
I am an AppleScript novice but a Cocoa expert. Your example seems to be doing everything the hard way, but maybe I don’t understand the issues.
Look at the problem from a Model-View-Controller standpoint:
AppleScript is just another View implementation just like the GUI view or a command line view or a web view.
The Model (in this case the “notes”) should not have dependencies on Views or visa versa. You violate this immediately by using the “internal Core Data ID” as an ID for AppleScript. First, the “internal Core Data ID” is internal and should not be used outside of the model. It is an implementation detail that is theoretically subject to change at any time. It also doesn’t meet the needs of AppleScript as you describe. Similarly, AppleScript’s need for an ID is a detail of its implementation (part of the View), and the model should not be responsible for providing some internal View resource.
You present the cleanest answer right in your tutorial: Use the Control layer! You are already using an array controller’s –arrangedObjects. What is your objection to just using an index into arranged objects as your AppleScript ID ?
AppleScript is indeed working through documents (Control layer) to indirectly access model data. Why not just let the document perform its intended function ? From the point of view of the View layer, retrieving data is a function of the Control layer. The View should not directly access the model. Your sample script even makes it explicit: “tell the first document set the Note to the first note”. You aren’t telling a note anything. You are telling a document to do something!
Guess what? The document does know the managed object context to be modified whenever data is added, changed, or deleted. Add methods to your document class. Implement those methods to access the model. Use the document methods to implement your AppleScripts…
In summary, what you are doing is probably so hard because you are fighting the design of Cocoa and Core Data for no good reason.
If AppleScript worked like it should, you would be entirely correct.
However, it does not. It does things in very counter-intuitive ways that necessitate the actions I took. I would spend a week or so trying to add AppleScript to a Core Data application before discussing it further. While you do have good ideas, they are incompatible with Apple’s rather limited and convoluted implementation of scripting.
AppleScript requires a unique ID.
If you use the index into the -arrangedObject as the specifier, then deleting an object would make other objects held by AppleScript potentially point at different objects, or perhaps nowhere.
Why?
A running AppleScript is in a different process and never actually has a pointer to any object in your process. It just knows “object id 0xAAA of document id 0xBBB”. So if you use indexes applescript creates an internal representation of “object 3 of document 1” (which via your suggestion would be an index into -arrangedObjects). The problem is if you delete object 2, the object at index 3 is no longer the same object. AppleScript is still holding onto a representation of “object 3 of document 1” and for it to be holding the same object, it should be holding on to “object 2 of document 1”.
The simple fact of the matter is AppleScript needs unique IDs. Using the coredata one is iffy, at best, due to 2 reasons i am aware of (1) the id is different before/after saving (2) you are exposing internals which may change.
It’s unfortunate, but today one most likely has to create their own AppleScript unique ID property to get around the (1)st problem which should not be ignored in a shipping app.
Is the Cocoa class for id supposed to be “identifier”?