man -w machine
KValueAScript   1. intro | 2. cocoa bindings | 3. scriptable | 4. links

1. Intro


This tutorial is a short introduction to Cocoa bindings and it also shows how to make your application scriptable.
You can download the whole project here.

By the way Cocoa bindings was called Controller layer a little while ago, so don't be confused if you find this phrase on other websites.

2. Cocoa bindings


A new Project is born

Start XCode and create a new - not document based - Cocoa application project. Name it "KValueAScript".

The beautiful model

First we start to implement the model - a person with its first and last name:

FFPerson.h
@interface FFPerson : NSObject {
  NSString* m_first;
  NSString* m_last;
}

- (id)initWithFirstName:(NSString*)first andLastName:(NSString*)last;
+ (FFPerson*)personWithFirstName:(NSString*)first andLastName:(NSString*)last;

- (void)setFirstName:(NSString*)first;
- (NSString*)firstName;
- (void)setLastName:(NSString*)last;
- (NSString*)lastName;

@end

Important in this class is that the getters and setters to the member variables are conform to key-value-coding - this will be helpful later on.
The "FFPerson.m" is pretty straightforward so I left it out.

The interface

Open the "mainmenu.nib" and add a tableview and a label so that it looks like this:



We want to handle several persons so we need an ArrayController. Create it by dragging the NSArrayController icon from the Cocoa-Controls window to the "mainmenu.nib" window : class instances and rename it to "PersonsController".
Then import the "FFPerson.h" to make the .nib know about the model.
Select the "PersonsController", open the property inspector and there the attribute pane. Now set the Object class name to "FFPerson" and add the following keys: "firstName", "lastName".
The result should be:



Time to connect the "personsController" with the table. Select the first column of the table and change the Header Title to "First". The identifier field is itentionally left blank because it isn't required for bindings - in contrast to the usual datasource practice:



Open the Bindings pane and change it so that it's similar to:



This means more or less "Display the firstName of all arrangedObjects from the personsController".
Repeat this and the previous step for the right column.

We have the model and the view, now only the controller is left. Create a NSObject subclass called "FFController" and instantiate it. Add an outlet "m_personsController" with type NSArrayController and connect it with the "personsController":



Create the files for "FFController" (right click on the class - not the instance).

The last thing ("fine tuning") to do is to bind the label to the "personsController" so that it always shows the current number of persons:



"@count" is a forward invocation of the count method of the controller array.

Save and close the Interface builder.

Get things rollin

An empty table is boring so we need to provide some example data:

FFController.m
@implementation FFController

- (void)awakeFromNib {
  [m_personsController addObject:[FFPerson personWithFirstName:@"Dieter" andLastName:@"Kuhn"]];
  [m_personsController addObject:[FFPerson personWithFirstName:@"Otto" andLastName:@"Dodo"]];
  [m_personsController addObject:[FFPerson personWithFirstName:@"Joseph" andLastName:@"Meier"]];
  [m_personsController addObject:[FFPerson personWithFirstName:@"John" andLastName:@"Doe"]];
}

Build & run the target.

2. Scriptable


It would be nice if we could access the persons from the outside via AppleScript. Said and done.

SDEF me

A SDEF file is simply speaking a list of "words" the application unterstands. The structure is always:

So here is our .sdef:

KValueAScript.sdef
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">

<dictionary title="Standard Terminology">
<suite name="KValueAScript" code="????" description="Classes and commands.">  
<classes>
  <class name="application" code="capp" inherits="NSApplication" description="Main controller">
    <cocoa class="FFApplication"/>
    <elements>
      <element type="person" access="r">
        <cocoa method="persons"       </element>
    </elements>
  </class>
  <class name="person" code="fper" inherits="NSCoreSuite.AbstractObject" description="A single person">
    <cocoa class="FFPerson"/>
    <properties>
      <property name="firstName" code="frst" type="string" description="First name">
        <cocoa method="firstName"/> <!-- Example. Not required because of KV-coding -->
      </property>
      <property name="lastName" code="last" type="string" description="Last name"/>
    </properties>
  </class>
</classes>

<commands>
  <command name="hello" code="kvashell" description="Says hello">
    <cocoa class="FFScript"/>
  </command>
  <command name="say" code="kvassayt" description="Make some noise">
    <cocoa class="FFScript"/>
    <direct-parameter type="object" description="The text"/>
  </command>
  <command name="wert" code="kvaswert" description="Returns a value">
    <cocoa class="FFScript"/>
    <result type="string" description="value"/>
  </command>
</commands>
</suite>
</dictionary>

This will become alot clearer when you've finished studying the implementation details and the script example.

Person, who?

The "persons" are accessed through the application class - since they are elements. Now follows a solution that allows to keep the actual data out of the application class itself:

FFApplication.m
#import "FFApplication.h"

@implementation FFApplication

- (id)init {
  self = [super init];
  if (self)
    m_reggedContainers = [[NSMutableDictionary alloc] init];
  return self;
}

- (void)dealloc {
  [m_reggedContainers release];
  [super dealloc];
}

#pragma mark -
#pragma mark Container management

- (void)registerContainer:(id)container forKey:(id)key {
  [m_reggedContainers setObject:container forKey:key];
}

- (id)valueForKey:(NSString*)key {
  return [m_reggedContainers objectForKey:key];
}

- (NSScriptObjectSpecifier*)specifierForObject:(id)obj fromContainerWithKey:(id)key {
  // Search container
  id container = [m_reggedContainers objectForKey:key];
  if (container == nil)
    return nil;
  
  // Search object
  unsigned oidx = [container indexOfObjectIdenticalTo:obj];
  if (oidx == NSNotFound)
    return nil;
  
  // Create specifier
  NSScriptClassDescription* cdesc = (NSScriptClassDescription*)
    [NSScriptClassDescription classDescriptionForClass:[FFApplication class]];
  
  return [[[NSIndexSpecifier allocWithZone:[self zone]]
      initWithContainerClassDescription:cdesc containerSpecifier:nil
      key:key index:oidx] autorelease];
}

@end

In the code above all objects of a container are identified by an index (e.g. "last"). You must extend it if you also want to support key-based identification.

Even though the access goes through the container - application in our case - yet the the identification method must be specified by the actual class itself. In our case this is simple - we only have to call the specification methods from the application:

FFPerson.m - new method
- (NSScriptObjectSpecifier*)objectSpecifier {
  return [NSApp specifierForObject:self fromContainerWithKey:@"persons"];
}

Now we only need to inform the application that persons exists:

FFController.m - added code
- (void)awakeFromNib {
  :
  [NSApp registerContainer:[m_personsController content] forKey:@"persons"];
}

Listen to my commands!

In the .sdef we defined this 3 commands:
When you define single parameter and argument commands you should reconsider your design, if it isn't better - cleaner - to use properties instead. This is especially the case when only only one class is affected.

FFScript.h
@interface FFScript : NSScriptCommand
// Nothing to declare here...
@end

No need for member variables or exported methods, only the NSScriptCommand inheritance is important.

FFScript.m
#import "FFScript.h"

@implementation FFScript

- (id)performDefaultImplementation {
  NSString* txt;
  
  switch ([[self commandDescription] appleEventCode]) {
    case FOUR_CHAR_CODE('hell') :
      txt = @"Hello world";
      break;
    case FOUR_CHAR_CODE('sayt') :
      txt = [NSString stringWithFormat:@"You said '%@'", [self directParameter]];
      break;
    case FOUR_CHAR_CODE('wert') :
      return @"I am evil";
    default: // Something strange happend
      NSLog(@"unknown command . commandName=%@, directparm=%@",
        [[self commandDescription] commandName],[self directParameter]);
      return nil;
  }
  NSLog(@"Script command text: %@", txt);
  return nil;
}

The appleEventCode (last 4 code characters in the .sdef) is one possible method to identify the target function - the easiest in my eyes. If you want to want to know all the others then lookup the NSScriptCommand of the Cocoa API. You'll also find out how to pass multiple arguments.

Scripted

A demonstration script with several different access methods:

all.scpt - modified for the tutorial
tell application "KValueAScript"
  --* Elements and their properties *--
  count of persons
  -- 4
count of [NSApp valueForKey:@"persons"]
 
  firstName of person 2
  -- "Otto"
1. $tmp = 2nd object from [NSApp valueForKey:@"persons"]
2. [$tmp firstName]
 
  set fp to first person
  lastName of fp
  -- "Kuhn"
1. fp = 1st object from [NSApp valueForKey:@"persons"]
2. [fp objectSpecifier] (from FFPerson.m which calls specifierForObject: from FFApplication.m
3. [fp lastName] (actually its a second valueForKey call)
 
  repeat with p in persons
    firstName of p
  end repeat
  -- "Dieter", "Otto"...
1. $tmp = [NSApp valueForKey:@"persons"]
2. p = next object from $tmp
($tmp is cached) 3. [p firstName]
4. Goto step 2. till all objects were processed.
 
  --* Commands *--
  hello
  -- Script command text: "hello world"
  say "howdy"
  -- Script command text: "howdy"
  set x to wert
  -- "I am evil"
end tell

4. Links


Ok, here are the obligatory links:

Cocoa bindings Scriptable
be smart.