Previous Page
Next Page

27.3. Cocoa Scripting

Adding scriptability to an application that you write has been, in the past, not a task for the faint of heart. An 'aete'-format dictionary is difficult to create and maintain. On the programming side, the system needs to be able to call into your application when an Apple event arrives, so as your application starts up it must register the appropriate functions with the Apple Event Manager, and when an Apple event arrives, your code must parse it (no mean feat, especially if it involves a reference to an object in your application) and respond appropriately.

For this reason, programmers have often relied on sample code and application frameworks for assistance in making an application scriptable. There was some concern among programmers, therefore, when Mac OS X first emerged, over how it would be possible to take advantage of the Cocoa application framework and make an application scriptable at the same time. Since those early days, support for scriptability has gradually been folded into Cocoa; this is called Cocoa scripting . Cocoa scripting is still not perfect, but at least it has passed its infancy, and in Tiger it is easier than ever, thanks in part to the introduction of the sdef-based dictionary.

Thus, if you're a Cocoa programmer, Cocoa scripting in Tiger is a good way to start adding scriptability to your application. Getting started is the hardest part, though, for several reasons:


Multiple workplaces

You have to coordinate the sdef dictionary with your code. If you make a mistake in either of them, or if you cause one of them not to match the other, some aspect of scriptability can fail mysteriously.


Scattered documentation

The documentation is copious, but it's scattered in many different places, and elementary tasks and common problems are often not explained clearly. Also, Cocoa scripting uses "key-value coding," which means that often there is no way to look up a troublesome method in the documentation (because it isn't documented, except as a kind of template).


Tiresome testing

Testing is tedious and difficult. Basically, you have to test by scripting your application; you must think of everything an end user might say with AppleScript to your application, and see whether your application responds coherently.

To help you get started with Cocoa scripting, here's a tutorial that adds the rudiments of scriptability to an existing Cocoa application, a little bit at a time. In order to make the example useful, this scriptability will include elements, properties, an enumeration, and a command. I'll assume you're using Tiger for development, and our example application will be scriptable on Tiger only; once you've achieved Tiger scriptability, it is possible to extend your scriptability to work on earlier systems (I'll talk about how to do that in the next section, "AppleScript Studio Scriptability").

Our Cocoa application, which is called Pairs, is very simple. We have a Person class, and we can create multiple instances of it. A Person has a name. Two Person instances can be paired, and this works like a monogamous marriage: once a Person is united to another, it can't be united to any other Person. The application presumably has some sort of interface, but I'm not going to concern myself with that. We assume that the application's basic functionality is working, and that it initializes itself on startup into some useful statefor example, it creates two initial Persons, named "Jack" and "Jill"and now we want to go back and make it scriptable.

Here's the structure of the application before we start adding scriptability. The main controller class, instantiated in the nib, is MyObject. Here is its interface:

@interface MyObject : NSObject
{
    NSMutableArray* persons;
    NSMutableArray* pairs;
    // outlets go here
}
// method declarations go here
@end

The persons mutable array is made up of Person objects; each Person instance, as it is created, is added to this array. Here is the interface for Person:

@class Pair;
@interface Person : NSObject {
    NSString* name;
    Pair* pair;
}
- (NSString *)name;
- (void)setName:(NSString *)aName;
- (Pair *)pair;
- (void)setPair:(Pair *)aPair;
// other method declarations go here
@end

As you can see, we have accessors for our name instance variable. We also have a pair instance variable in the Person class, plus there is a pairs array in MyObject. It happens that the rest of our implementation is as follows. A Pair has two Person pointers, called person1 and person2. To pair two Persons, we make a new Pair object, add it to the pairs array, and point its two Person pointers at the two Persons; we also point the pair pointer of each of these paired Persons at this Pair object. Thus, a Pair and its Persons are double-linked; a Person knows it is paired because its pair instance variable isn't nil, and it can find the Person to which it is paired by looking at its Pair's Persons and finding the one different from itself.

The question of whether this way of implementing pairings is a particularly good one is beside the point. What's important is that we do not intend to expose this to the end user. You don't have to show the user everything that goes on behind the scenes! The end user will be thinking in terms of persons, not pairs, and we want our scripting interface to match the user's conceptual thought processes, not to reveal our backstage implementation.

So how should our scripting interface look to the AppleScript programmer? Clearly there needs to be a person class, and a person should have a name property. There can be multiple persons, so there should be a persons element. This is not a document-based application, so the only coherent location for the persons element is at the top level of the object modelthat is, it will be an element of the application class.

Now we'll create our sdef-format dictionary and add it to the project. The best way to make the dictionary is with the wonderful Sdef Editor application (see Appendix C for this and other Cocoa scripting resources). Let's call the dictionary pairs.sdef. Then to make our application scriptable through this dictionary, we must add the following lines to our project's Info.plist file:

<key>NSAppleScriptEnabled</key>
<string>YES</string>
<key>OSAScriptingDefinition</key>
<string>pairs.sdef</string>

The first step in creating an sdef is to give it whatever common commands we intend to implement. For example, we want the user to be able to ask how many persons there are, using the count command. This won't be possible all by itself; we have to include the count command in the dictionary. Common commands can be found in the Standard Suite (see "Suites" in Chapter 20), which you can access in Sdef Editor by choosing File Open Standard Suite NSCoreSuite. The idea here is to put NSCoreSuite into the dictionary and then immediately remove from it everything we don't need; in this case, the remaining commands will be just count, delete, exists, and makethe bare minimum needed for working with a collection of persons. (There is no need to include get and set, because they are short-circuited, and we don't need an entry for quit because every application can do that.)

Now we make a new suite, which I'll call the Pairs Suite. I like to move the application class into this, and I'll simplify the application class, leaving just the name, frontmost, and version properties, which are implemented automatically. Now we can add the person class with its name property, and give the application class a person element.

I assume you can figure out how to work with Sdef Editor, so let's focus on the text version of the result. I'll present it in two parts. First we have the automatically generated Standard Suite:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="Pairs Dictionary">
    <suite name="Standard Suite" code="????"
    description="Common classes and commands for most applications.">
        <cocoa name="NSCoreSuite"/>
        <command name="count" code="corecnte" description="Return the number of
         elements of a particular class within an object.">
            <cocoa class="NSCountCommand"/>
            <direct-parameter description="the object whose elements are to be
             counted" type="specifier"/>
            <parameter name="each" code="kocl" description="The class of objects to
             be counted." type="type" optional="yes">
                <cocoa key="ObjectClass"/>
            </parameter>
            <result description="the number of elements" type="integer"/>
        </command>
        <command name="delete" code="coredelo" description="Delete an object.">
            <cocoa class="NSDeleteCommand"/>
            <direct-parameter description="the object to delete" type="specifier"/>
        </command>
        <command name="exists" code="coredoex"
        description="Verify if an object exists.">
            <cocoa class="NSExistsCommand"/>
            <direct-parameter description="the object in question"
             type="specifier"/>
            <result description="true if it exists, false if not" type="boolean"/>
        </command>
        <command name="make" code="corecrel" description="Make a new object.">
            <cocoa name="Create" class="NSCreateCommand"/>
            <parameter name="new" code="kocl"
            description="The class of the new object." type="type">
                <cocoa key="ObjectClass"/>
            </parameter>
            <parameter name="at" code="insh" description="The location at which to
             insert the object." type="location specifier" optional="yes">
                <cocoa key="Location"/>
            </parameter>
            <parameter name="with data" code="data" description="The initial data
             for the object." type="any" optional="yes">
                <cocoa key="ObjectData"/>
            </parameter>
            <parameter name="with properties" code="prdt" description="The initial
             values for properties of the object." type="record" optional="yes">
                <cocoa key="KeyDictionary"/>
            </parameter>
            <result description="to the new object" type="specifier"/>
        </command>
    </suite>

Next comes the Pairs Suite. The application's name, frontmost, and version properties were generated automatically; the application's person element, and the person class itself, were added by you. The material you've actually had to create is shown here in bold type:

    <suite name="Pairs Suite" code="pAIR" description="The Pairs suite">
        <class name="application" code="capp" description="An application&apos;s top
         level scripting object.">
            <cocoa class="NSApplication"/>
            <element description="The persons." type="person">
                <cocoa key="persons"/>
            </element>
            <property name="name" code="pnam"
            description="The name of the application." type="text" access="r"/>
            <property name="frontmost" code="pisf" description="Is this the
             frontmost (active) application?" type="boolean" access="r">
                <cocoa key="isActive"/>
            </property>
            <property name="version" code="vers"
            description="The version of the application." type="text" access="r"/>
        </class>
        <class name="person" code="pRSN" description="A person." plural="persons">
            <cocoa class="Person"/>
            <property name="name" code="pnam"
            description="The person&apos;s name." type="text">
                <cocoa key="name"/>
            </property>
        </class>
    </suite>
</dictionary>

Even though only a dozen lines were added by you, there's lots of room to go wrong here. (I speak from experience.) The following points are worth emphasizing:


You must make up some four-letter codes.

The codes 'pAIR' and 'pRSN' are arbitrary. It is crucial, however, that they not overlap with existing four-letter codes. One way to feel confident of this is to use some capital letters, as we've done here; Apple reserves to itself all four-letter codes consisting entirely of lowercase letters.


You must use existing four-letter codes.

The name property of the person class has code 'pnam'. This is a standard property, already defined in AppleScript, and it is essential to get the code right.


You must match Cocoa keys with your code.

The Cocoa key for the person element of the application class is "persons"; that's because persons is the name of the instance variable in MyObject through which the collection of Person objects is accessed. The Cocoa key for the person class is "Person"; that's because Person is the name of the Cocoa class implementing this AppleScript class. The Cocoa key for the person class's name property is "name"; that's because name is the name of the instance variable in Person representing the name property. A mistake hereeven a discrepancy in capitalization between the Objective-C code, on the one hand, and the Cocoa key in the sdef, on the otherwill cause scriptability to fail.

The reason Cocoa keys in the sdef are so crucial is that Cocoa scripting uses key-value coding to find its way through your code. Key-value coding (or KVC) is an informal protocol that takes advantage of Objective-C's dynamism and introspection. It uses a string as a key to hunt for names among your instance variables and methods. The object model is navigated by way of a path leading down from the application class. At every step of a path, your classes must be KVC-compliant (meaning that the right instance variables or methods are present) or things won't work.

In our application, so far, there is just one simple little path. The scriptability framework will start with the application class. It has a person element whose Cocoa key is "persons." So the framework looks in MyObject to see if it is KVC-compliant with respect to to the key "persons." Is it? Yes, because it has an instance variable named persons. That instance variable is an NSMutableArray; that's a built-in class which is itself KVC-compliant. The contents of this NSMutableArray are Person objects; that fits with the Cocoa key for this class, which says that everything in persons should be a Person. And Person is KVC-compliant with respect to "name," because it has a name accessor method and a setName: accessor method.

We build our application and run it, and point Script Editor at it, to test our scriptabilityand it doesn't work! For example, we say:

tell application "Pairs"
    get person 1
end tell

and we get an error message in the console:

[<NSApplication 0x314220> valueForUndefinedKey:]: this class is not key value coding-
compliant for the key persons

The reason is simple: the very first step in the path is incorrectly set up. As our sdef says, the application class's Cocoa key is "NSApplication." But we want the path to start in MyObject, not in NSApplication. We must not change the Cocoa key for the application class; rather, we need a way to tell the scriptability framework to jump from NSApplication to MyObject as it descends the path. One very simple way to do this is to make MyObject the delegate of NSApplication; you can specify this by a connection in the nib. We must also write some code in MyObject announcing that it, as the application delegate, implements certain keys:

- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key {
    if ([key isEqualToString: @"persons"]) return YES;
    return NO;
}

We add that code to MyObject, which is now also NSApplication's delegate. We build and run the application, and we test it in Script Editor; lo and behold, it works!

tell application "Pairs"
    count persons -- 2
    name of person 1 -- "Jack"
    name of person 2 -- "Jill"
    name of every person -- {"Jack", "Jill"}
    name of every person whose name ends with "k" -- {"Jack"}
    exists person "Jack" -- true
    exists person "Matt" -- false
    delete person "Jack"
    count persons -- 1
    get name of person 1 -- "Jill"
    set name of person 1 to "Mannie"
    name of every person -- {"Mannie"}
end tell

This shows the advantage of starting with an application framework. We've added to an existing application no more than a couple of lines of code and a dozen lines of dictionary, and presto, we're scripting our application. We can get and set a property; we can count elements, delete an element, and test by property for the existence of an element; we can even use a boolean test specifier.

Having achieved this initial intoxicating success, we should consider some improvements and refactoring before proceeding any further:


Better accessors

So far, our code lends itself more or less by accident to KVC. For example, access to the person element is possible only because there happens to be an instance variable called persons in MyObject. We should implement our accessors in a more deliberate fashion, in accordance with the expectations of key-value coding and the scriptability framework.


Separate accessors

It will be wise to separate the scriptability accessors from the programming accessors. For example, when the user changes the name property of a person, the scriptability framework is using the setName: accessor, which is exactly the same method our own Objective-C code would use to change the name instance variable of a Person. But our code might need to respond differently depending on who is calling; our code should be able to do things that a user should not be able to do through AppleScript (think of a read-only property). We should nominate a different Cocoa key in the dictionary and create a different set of scriptability framework accessors.


Separate code

As long as we're going to have separate accessors, we might want to separate the code that responds to scripting from the code that implements our application's internal functionality. A common architecture is to implement scriptability as an Objective-C category on the existing classes.


Add checks and error handling

At present, our application is very open to the user's commands; for example, a script can delete a person, give two persons the same name, or create a person with no name. We will want to close some of these doors and return a runtime error message to the script when the user tries to do something we disapprove of.


Implement objectSpecifier

Every AppleScript class that our application declares should have in its corresponding Objective-C class an implementation of the objectSpecifier method. This is what allows an object reference such as person "Matt" of application "Pairs" to be returned to a script when the user says something like get person 1 or make new person. Without an objectSpecifier implementation, the script will receive a meaningless reference.

Here's code that illustrates these points. First, I've changed two of the Cocoa keys in the dictionary: the person element of the application class now has Cocoa key "personsArray," and the name property of the person class now has Cocoa key "personName." These changes will allow our code to respond separately to the messages sent by the scriptability framework. I've moved all the scriptability code into its own file, where it is implemented through categories on the existing classes. I'll present it a piece at a time. First we have a general utility routine implemented as a category on NSObject, because every scriptable class will need it:

@implementation NSObject (MNscriptability)
 
- (void) returnError:(int)n string:(NSString*)s {
    NSScriptCommand* c = [NSScriptCommand currentCommand];
    [c setScriptErrorNumber:n];
    if (s) [c setScriptErrorString:s];
}
@end

Observe how to return an error to AppleScript: you fetch the pending command and assign it an error number and, optionally, an error message to accompany it. (See Figure 3-1 in Chapter 3; the system holds out the incoming Apple event to your application like an envelope, from which you read the message and into which you insert any response, whether it's a result or an error.)

Next, we have the category on Person:

@implementation Person (MNscriptability)
 
- (NSScriptObjectSpecifier *)objectSpecifier {
    NSScriptClassDescription* appDesc
        = (NSScriptClassDescription*)[NSApp classDescription];
    return [[[NSNameSpecifier alloc]
        initWithContainerClassDescription:appDesc
        containerSpecifier:nil
        key:@"personsArray"
        name:[self name]] autorelease];
}
- (NSString *)personName {return name;}
 
- (void)setPersonName:(NSString *)aName {
    if ([[NSScriptCommand currentCommand] isKindOfClass: [NSCreateCommand class]])
        [self setName:aName];
    else
        [master scripterWantsToChangeName:aName of:self];
}
@end

The implementation of objectSpecifier allows proper object references to be returned to AppleScript. We must specify the object's container, which in this case is the application class, and we must provide a key ("personsArray") matching the Cocoa key in the dictionary for how this element is accessed from that container.

Next we have the scriptability accessors for the name property, now keyed through "personName." A tricky architectural difficulty arises immediately, illustrating why it's so hard to get started with Cocoa scripting.

In good object-oriented programming, objects are assigned appropriate tasks. Some objects are just data ("model"); other objects control that data ("controller"). In my application, a Person in MyObject's persons array is just data; it is MyObject that should be responsible for creating and validating a Person. But key-value coding slams into your existing application like a sudden side wind, ignoring your architecture and surprising your code. When the user tries to change the name of an existing person, it is Person's setPersonName: that is called, even though it is MyObject that should decide whether the new name is valid. Accordingly, I've given Person a master instance variable pointing at its creator, which in this case is MyObject; when the user asks to change a person's name, the request is shuttled off to MyObject, which will decide the suitability of the requested change and comply if appropriate.

But it gets worse. We have an additional problem when the user says make new person, because at that moment the scriptability framework creates a Person object by calling alloc and init directly on our Person class; any designated initializer is ignored, and MyObject doesn't have a chance to perform initializations or pass judgment. There is no easy way to prevent this (such as saying to the framework, "When you want to create a Person, call such-and-such a method"). Furthermore, if the user's command also says with properties {name:"whatever"}, setPersonName: is called to set the new name. This puts our code in a quandary; there is no master, so there is no one to judge the suitability of the new name.

Fortunately, if the user is creating this person (a condition for which we can test, as the code demonstrates), the scriptability framework will send the resulting Person object to MyObject anyway, for insertion into the persons collection. So we set the name as requested, because MyObject will eventually get a chance to pass judgment on this proposed new personand initialize it properly.

Now let's talk about the category on MyObject. Here's the first part of it:

@implementation MyObject (MNscriptability)
 
- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key {
    if ([key isEqualToString: @"personsArray"]) return YES;
    return NO;
}
- (unsigned int)countOfPersonsArray {
    return [persons count];
}
- (Person *)objectInPersonsArrayAtIndex:(unsigned int)i {
    return [persons objectAtIndex: i];
}

First, we have our same old application:delegateHandlesKey: method. Next, we've implemented our own access to the persons array through the Cocoa key "personsArray"; we will report its size and return an object in it, so that the scriptability framework never gets its hands on the array directly.

Here's more of the MyObject category:

- (BOOL) canGivePerson:(Person*)p name:(NSString*)name {
    if (!name || [name isEqualToString:@""]) {
        [self returnError:errOSACantAssign
            string:@"Can't give person empty name."];
        return NO;
    }
    if ([self existsPersonWithName: name]) {
        [self returnError:errOSACantAssign
            string:@"Can't give person same name as existing person."];
        return NO;
    }
    return YES;
}
- (void) scripterWantsToChangeName:(NSString*)n of:(Person*)p {
    if ([n isEqualToString: [p name]]) return; // nothing to do
    if (![self canGivePerson:p name:n]) return;
    [p setName: n];
}
- (void)insertObject:(Person *)p inPersonsArrayAtIndex:(unsigned int)index {
    if (![self canGivePerson:p name:[p name]]) return;
    [p setMaster: self];
    [persons insertObject:p atIndex:index];
}
- (void)insertInPersonsArray:(Person *)p {
    if (![self canGivePerson:p name:[p name]]) return;
    [p setMaster: self];
    [persons addObject:p];
}

First we have a general name-checking routine. If the user wants to assign a person a name, either as part of creating that person or altering the name of an existing person, we report an error to AppleScript if the name is the empty string or matches that of an existing person. Then we have the method that will be called by Person if the user tries to change the name of an existing person: either we return an error or we comply by setting the name. Finally, we have two methods that may be called when the user says make new person; I don't actually know which is called when, but it appears they are both needed for KVC-compliance so I've implemented both.

This is indicative of another difficulty that besets the new scriptability programmer. There is no straightforward documentation stating directly what methods will be sought and called, and when, and in what order, and what the scriptability framework will do if it fails to find a method it's looking for (will it default to a different method, or will it throw an error declaring your class not KVC-compliant, or will it return a mysterious error to the script, or what?). So you can never be quite sure what you need to implement and what method the framework may decide to call at any moment. Here you can see me working around this difficulty by implementing the same functionality twice. And I do the same thing in the last part of the MyObject category:

-(void)removeObjectFromPersonsArrayAtIndex:(unsigned int)index {
    [self returnError:OSAMessageNotUnderstood string:nil];
}
-(void)removeFromPersonsArrayAtIndex:(unsigned int)index {
    [self returnError:OSAMessageNotUnderstood string:nil];
}
@end

That code is to prevent the user from deleting a person. I've implemented two methods that do the same thing because the framework seems to complain on different occasions if I fail to implement either one, and I don't know why; lacking clear documentation, it seems easiest to fall back on a double implementation and move on.

With all of that in place, the previous test script still works perfectly. In addition, our application can now successfully return an object reference; and it now responds coherently to the user's attempts to do things we don't permit:

tell application "Pairs"
    get name of every person -- {"Jack", "Jill"}
    delete person "Jack"
    -- error: Pairs got an error: person "Jack" doesn't understand the delete message
    make new person
    -- error: Pairs got an error: Can't give person empty name
    make new person at end with properties {name:"Moe"}
    -- person "Moe" of application "Pairs"
    set name of person "Jill" to "Mannie"
    set name of person 1 to "Mannie"
    -- error: Pairs got an error: Can't give person same name as existing person
    make new person with properties {name:"Mannie"}
    -- error: Pairs got an error: Can't give person same name as existing person
end tell

Now let's add some more features. Let's give a person an additional property: gender, which is either male or female. This is simply to illustrate how you implement an enumeration. The Person class will need an instance variable, gender, whose value is an int, and we should probably add accessors gender and setGender. To define the enumeration and its enumerators for AppleScript, you want something in the dictionary like this:

<enumeration name="genders" code="gEND" description="A gender." inline="2">
    <enumerator name="male" code="gMAL" description="Male gender."/>
    <enumerator name="female" code="gFEM" description="Female gender."/>
</enumeration>

Back in our Objective-C code, we define the same enumeration, like this:

enum {
    MALE='gMAL',
    FEMALE='gFEM'
} genders;

The match between the four-letter codes in the dictionary and our Objective-C code is crucial. Now, in the dictionary, we add the property to the person class:

<property name="gender" code="gNdR" description="The person&apos;s gender.">
    <cocoa key="personGender"/>
</property>

(Observe that I do not make the common mistake of giving the property the same four-letter code as the class.) The Cocoa key "personGender" means that in our Person class the accessors personGender and setPersonGender: will be called. Implementation of these in the category on Person is straightforward; in my implementation I've allowed (and indeed required) the user to supply a gender when creating a person, but I've made it illegal for the user to change the gender of an existing person.

Now let's implement a verb. Let's call this pair, and we'll have it apply to two person objects. One will be the direct object; the other will appear after a parameter, to:

pair person 1 to person 2

Verbs (commands) are implemented in two different ways in Cocoa scripting. If a verb basically applies to a single object, it can appear in the Objective-C code as a method in the class that corresponds to the class of that object. This is called object-first dispatch. (The other way of implementing a command, verb-first dispatch, is demonstrated later in this chapter.) Having defined the command in the dictionary, you then specify in the dictionary every class that can serve as the direct object to this command. So the dictionary will contain this definition of the command:

<command name="pair" code="pAiRpAiR">
    <direct-parameter description="One person." type="person"/>
    <parameter name="to" code="othR" description="The other person." type="person">
        <cocoa key="otherPerson"/>
    </parameter>
</command>

And in our person class, the dictionary now contains the following:

<responds-to name="pair">
    <cocoa method="scripterSaysPair:"/>
</responds-to>

What this means is that when the user invokes the pair command, a message scripterSaysPair: will be sent to the Person object who represents the direct object of the command. The parameter to this method is an NSScriptCommand object whose evaluatedArguments method yields an NSDictionary containing the command's additional parameters, accessible through their Cocoa keys; in this case, there is just one additional parameter, and its key will be "otherPerson." So now we can implement scripterSaysPair: in our category on the Person class:

- (void)scripterSaysPair:(NSScriptCommand*)command {
    Person* p1 = [command evaluatedReceivers];
    Person* p2 = [[command evaluatedArguments] valueForKey:@"otherPerson"];
    if (self != p1 || self == p2) {
        [self returnError:errOSACantAssign string:@"Invalid pairing."];
        return;
    }
    [master scripterWantsToPair:p1 with:p2];
}

After an error check, the command is passed on to MyObject for processing. The routine in MyObject (not shown here) does some more error-checking (making sure neither of the person objects is already paired) and then does whatever it usually does to pair two Persons. I use this architecture in order to distribute responsibilities appropriately; a Person can look to see whether the pair command makes basic sense, but it is MyObject, as master of the persons collection, who decides whether two Persons can be paired and, if so, pairs them.

It would be nice to complete the picture by adding a read-only boolean paired property to the person class, stating whether the person has been paired, along with a read-only partner property that returns a reference to the other person in the pair. These correspond to no instance variable of the Person class, which just goes to show that a sensible scripting interface needn't be like the underlying implementation.

- (id) personPartner {
    Pair* myPair = [self pair];
    if (!myPair) return [NSNull null];
    return ([myPair person1] == self ? [myPair person2] : [myPair person1]);
}
- (void) setPersonPartner:(id)newPartner {
    [self returnError:errOSACantAssign string:@"Partner property is read-only."];
}
- (BOOL) personPaired {
    return ([self pair] != nil);
}
- (void) setPersonPaired:(BOOL)newPaired {
    [self returnError:errOSACantAssign string:@"Paired property is read-only."];
}

The personPartner method returns an NSNull instance if the person hasn't been paired; that's how you cause missing value to be returned to AppleScript. Finally, observe that you must implement setter accessors even though the dictionary marks these properties as read-only.

When the user requests an object that doesn't exist (a person whose index number is too large, for example), an ugly and obnoxious "NSReceiverScriptError " runtime error message is returned by the scriptability framework. Various workarounds have been proposed, and I have elsewhere tentatively proposed one of my own, but the truth is that this is a serious flaw in the underlying framework and needs to be corrected at that level.



Previous Page
Next Page