bynkii (bynkii) wrote,
bynkii
bynkii

A look at AppleScriptObjC

What AppleScript Programmers have been waiting for, even if they didn't know it: AppleScriptObjC


As some of you might know, AppleScript Studio, (Studio for short), never impressed me. To be blunt, I thought that it was a nice try that quickly showed itself to have gone down a bad path. The problem was that to do anything but a really narrow range of tasks (for Cocoa anyway), you had to use the "call method" command with a gob of 'real' Cocoa code, and if you're going to do that, why bother with Studio? The debugger was essentially unusable, and much of the time, just rebuilding a project with no code changes whatsoever would fail.

I have a real limit as to how much work I'll do to make up for a bad tool, and that's what Studio was...a bad tool with good intentions. Especially because I'd been spoiled by really good AppleScript tools like Script Debugger. Compared to Script Debugger, Studio lived down to its unfortunate acronym.

However, while no one was looking, Apple was listening. With Mac OS X 10.6, they released AppleScriptObjC, which is finally the product that Studio never could be: A real, first-class way to use AppleScript to create Cocoa applications. No more half-baked implementations or "oh, you need call method to do THAT" nonsense. Access to all the frameworks now, and in the future, the same way that Cocoa developers using Objective-C, Ruby, or Python get them...for free.

I'm not going to do a tutorial on AppleScriptObjC, because I'm still wrapping my head around it. If you want a tutorial, there's a good one at MacScripter. What I am going to do is do a light comparison between an AppleScriptObjC application, and the Objective-C version, then one between an AppleScriptObjC application and the Studio version. As you'll see, AppleScriptObjC is a win.

One of the things that AppleScriptObjC does is use AppleScript in a more "Cocoa-y" fashion. That is, Apple made the decision that for AppleScriptObjC, the way you use AppleScript should match the way you'd use other languages. There are obviously going to be significant differences between Objective-C, Ruby, Python, and AppleScript, but in general, Apple tried to make using AppleScriptObjC 'feel' as close to using any other language as possible. (There are more details on this in the AppleScriptObjC release notes I linked to.)

DotView

DotView is a simple application that draws a solid circle, or dot, in a window. You can move the dot around with the mouse, there's a slider to change the dot size, and a color control to change the dot's color. It's a simple application, and so is a good way to show the similarities between Objective-C in Cocoa and AppleScript in AppleScriptObjC.

DotView window

DotView


Looking at the Objective-C version, there's not a lot of code. There's two .m files, DotView.m and main.m, and two header files, DotView.h and Preview.h. We'll focus on just the DotView.* files here, starting with the Objective-C version of DotView.h

<#import Cocoa/Cocoa.h>

@interface DotView : NSView {
     NSPoint center;
     NSColor *color;
     CGFloat radius;
}

// Standard view create/free methods
- (id)initWithFrame:(NSRect)frame;
- (void)dealloc;

// Drawing
- (void)drawRect:(NSRect)rect;
- (BOOL)isOpaque;

// Event handling
- (void)mouseUp:(NSEvent *)event;

// Custom methods for actions this view implements
- (IBAction)setRadius:(id)sender;
- (IBAction)setColor:(id)sender;

@end


Not much really, about 14 lines of code that does the setup for things like setting the center, color, and radius of the dot, the functions for drawing the dot, and the functions for resizing and changing the color of the dot.

As far as the AppleScriptObjC version goes, there isn't one. AppleScript is a higher level language, and doesn't use header files. The AppleScriptObjC version still has to deal with the items that are in the Objective-C header file, but it will do so differently. (note that in this usage, "higher" is not a sign of superiority or inferiority. Instead, it denotes how "far from the hardware" a language lives. So AppleScript is a higher level language than Objective-C which is a higher level language than Assembly.)

What about the 'real' code? Well, here's the Objective-C DotView.m contents:

#import <Cocoa/Cocoa.h>
#import "DotView.h"

@implementation DotView


- (id)initWithFrame:(NSRect)frame {
     [super initWithFrame:frame];
     center.x = 50.0;
     center.y = 50.0;
     radius = 10.0;
     color = [[NSColor redColor] retain];
     return self;
}

- (void)dealloc {
     [color release];
     [super dealloc];
}
// drawRect: should be overridden in subclassers of NSView to do necessary
// drawing in order to recreate the the look of the view. It will be called
// to draw the whole view or parts of it (pay attention the rect argument);
// it will also be called during printing if your app is set up to print.
// In DotView we first clear the view to white, then draw the dot at its
// current location and size.

- (void)drawRect:(NSRect)rect {
     NSRect dotRect;

     [[NSColor whiteColor] set];
     NSRectFill([self bounds]);
// Equiv to [[NSBezierPath bezierPathWithRect:[self bounds]] fill]

     dotRect.origin.x = center.x - radius;
     dotRect.origin.y = center.y - radius;
     dotRect.size.width = 2 * radius;
     dotRect.size.height = 2 * radius;

     [color set];
     [[NSBezierPath bezierPathWithOvalInRect:dotRect] fill];
}

- (BOOL)isOpaque {
     return YES;
}

// Recommended way to handle events is to override NSResponder (superclass
// of NSView) methods in the NSView subclass. One such method is mouseUp:.
// These methods get the event as the argument. The event has the mouse
// location in window coordinates; use convertPoint:fromView: (with "nil"
// as the view argument) to convert this point to local view coordinates.
//
// Note that once we get the new center, we call setNeedsDisplay:YES to
// mark that the view needs to be redisplayed (which is done automatically
// by the AppKit).

- (void)mouseUp:(NSEvent *)event {
     NSPoint eventLocation = [event locationInWindow];
     center = [self convertPoint:eventLocation fromView:nil];
     [self setNeedsDisplay:YES];
}

// setRadius: is an action method which lets you change the radius of the dot.
// We assume the sender is a control capable of returning a floating point
// number; so we ask for it's value, and mark the view as needing to be
// redisplayed. A possible optimization is to check to see if the old and
// new value is the same, and not do anything if so.

- (void)setRadius:(id)sender {
     radius = [sender doubleValue];
     [self setNeedsDisplay:YES];
}

// setColor: is an action method which lets you change the color of the dot.
// We assume the sender is a control capable of returning a color (NSColorWell
// can do this). We get the value, release the previous color, and mark the
// view as needing to be redisplayed. A possible optimization is to check to
// see if the old and new value is the same, and not do anything if so.

- (void)setColor:(id)sender {
     [color autorelease];
     color = [[sender color] retain];
     [self setNeedsDisplay:YES];
}

@end


I left some of the functional comments in, so it's easier to see what the different parts of DotView.m are doing, but if you remove the comments and blank lines, the entire 'main' part of the application is only 47 lines of code. Admittedly, it doesn't do a lot, but still, that's not a lot. What about the AppleScriptObjC version? Here:

property NSColor : class "NSColor"
property NSBezierPath : class "NSBezierPath"

script DotView
     property parent : class "NSView"

     property |center| : {x:0.0, y:0.0}
     property radius : 0.0
     property |color| : missing value

     on initWithFrame_(frame)
          continue initWithFrame_(frame)
          set my |center|'s x to 50.0
          set my |center|'s y to 50.0
          set my radius to 10.0
          set my |color| to NSColor's redColor()
          return me
     end initWithFrame_

     on drawRect_(rect)
          NSColor's whiteColor()'s |set|()
          tell NSBezierPath's bezierPathWithRect_(my |bounds|()) to fill()

          set origin to {(my |center|'s x) - (my radius), (my |center|'s y) - (my radius)}
          set |size| to {2 * (my radius), 2 * (my radius)}

          my |color|'s |set|()
          tell NSBezierPath's bezierPathWithOvalInRect_({origin, |size|}) to fill()
     end drawRect_

     on isOpaque()
          return true
     end isOpaque

     on mouseUp_(|event|)
          set eventLocation to |event|'s locationInWindow()
          set my |center| to my convertPoint_fromView_(eventLocation, missing value)
          tell me to setNeedsDisplay_(true)
     end mouseUp_

     on setRadius_(sender)
          set radius to sender's doubleValue()
          tell me to setNeedsDisplay_(true)
     end setRadius_

     on setColor_(sender)
          set my |color| to sender's |color|()
          tell me to setNeedsDisplay_(true)
     end setColor_

end script


This does the same thing as the Objective-C version, but in about 40 lines of code. If that doesn't make any sense, this is where AppleScript being a higher level language has its advantages. Because the AppleScript runtime, (the component that executes all AppleScript code), handles things like memory management for you, you don't have to write code to deal with de-allocating memory you allocated earlier. However, there's a price to be paid for this convenience, and that's usually in size, (all things being equal, an application written in an interpreted language like AppleScript tends to be larger, and need more memory than an application written in a compiled language like Objective-C), and speed, (interpreted languages tend to be slower than compiled ones). But, for code that really doesn't care about either, the difference is a wash, and is up to programmer preferences/job requirements.

The thing I wanted you to initially see, is that unlike Studio, which as we'll see, has a syntax implementation that "verbose" doesn't even begin to cover, AppleScriptObjC lets you get work done without having to bang out gobs of code for even simple things. The next thing we want to look at is the way that AppleScriptObjC and Objective-C have similar structure, even though the languages themselves are quite different. For example, let's look at setting up the variables for drawing the dot. To draw a solid colored circle, you need three basic bits of information:
  • You need the center of the circle, so you know where to start

  • You need the radius of the circle, so you know how big the circle will be

  • You need the color of the circle, so you know what color to use to draw and fill in the circle.


In the Objective-C version, this is done in the header, (.h) file with this code:

@interface DotView : NSView {
     NSPoint center;
     NSColor *color;
     CGFloat radius;
}


The code is using features of the NSView class to create three things:
  • An NSPoint variable, center

  • An NS color variable, *color

  • A CGFloat variable, radius


Those will be used in the DotView main code to tell the application what the center, color, and radius of the circle should be so it can be drawn correctly. Now, the AppleScriptObjC version:

property parent : class "NSView"

property |center| : {x:0.0, y:0.0}
property radius : 0.0
property |color| : missing value


The syntax is different, but still similar. We tell the script to use the features of NSView to create the same three variables: center, radius, and color. With AppleScriptObjC, we also have to set the initial values for the variables, whereas we didn't in Objective-C, but that's a syntax difference. The reason that AppleScriptObjC's center and radius variable names have vertical bars around them is because those particular words are normally reserved by AppleScript. The vertical bars tell the AppleScript runtime "Hey, for this application, use these variables the way I'm defining them here, not the way you normally would use them." In talking with some of the AppleScript team at Apple, they told me that the bars themselves are harmless. If you accidentally put them in where they aren't needed, no harm no foul, it shouldn't affect anything adversely. The reason that color is defined to be a missing variable is because its value will be set by a UI control, and so this is how AppleScriptObjC lets you reserve variables that you're going to "hook up" to the UI in your application.

The important point is, if you were to have never coded in anything but Objective-C in your life, and suddenly had to look at the AppleScriptObjC version of some Objective-C code, you'd have a far easier time of correctly interpreting what the code was doing than you'd ever have with Studio. Here, let's look at one more example. This time, we'll look at the code that creates the initial dot on application launch. First, the Objective-C code:

- (id)initWithFrame:(NSRect)frame {
     [super initWithFrame:frame];
     center.x = 50.0;
     center.y = 50.0;
     radius = 10.0;
     color = [[NSColor redColor] retain];
     return self;
}


Now the AppleScriptObjC code:

on initWithFrame_(frame)
     continue initWithFrame_(frame)
     set my |center|'s x to 50.0
     set my |center|'s y to 50.0
     set my radius to 10.0
     set my |color| to NSColor's redColor()
     return me
end initWithFrame_


The AppleScriptObjC code's a bit more verbose, but still, the similarities are undeniable. Thanks to the work the AppleScript team did to make AppleScript syntax work the way 'normal' Cocoa application syntax works, it is much, much easier to move between Objective-C and AppleScriptObjC's 'flavor' of AppleScript than it ever was to move between Objective-C and Studio's 'flavor' of AppleScript.

The advantages to this aren't just in more efficient code, or fewer lines. One big advantage to this similarity is that an AppleScriptObjC programmer can read the 'normal' Cocoa documentation far easier than a Studio programmer can, because the way you use the language now follows the 'correct' Cocoa methods more closely. So rather than having to recreate the entire Cocoa documentation set for AppleScriptObjC, the AppleScript team can create a smaller set of core AppleScriptObjC documentation to help you get started, and then you can use the normal Cocoa docs for everything else. That's a huge advantage.

But what about AppleScriptObjC and Studio? How does AppleScriptObjC compare to Studio? Quite favorably as it turns out. By 'quite favorably' I mean "leaves Studio in the dust, choking and wondering why those durn kids knocked its walker over". First, with Studio, you only had access to the parts of Cocoa that Studio explicitly knew about. If Apple introduced a new framework, you couldn't just use that in Studio, you had to wait for the Studio team to implement it. With AppleScriptObjC, there's none of that. AppleScriptObjC is able to use new frameworks and features as soon as they show up in the OS. Now, you could work around that limitation in Studio via the infamous "call method" which let you shell out to 'real' Cocoa code in the Studio application. It turns out, you did that a lot in a Studio application, to where a lot of people just gave in and learned Objective-C.

Task List

So from a feature standpoint, it's not even close. AppleScriptObjC wins easily over Studio. What about a code comparison? Unfortunately, I couldn't find a Studio version of DotView, but I did find another simple application that has both Studio and AppleScriptObjC versions: Task List. Task List is just what it sounds like: a simple application that lets you create and manage a list of tasks/to-dos

Task list
Task List


A nice simple application, so the comparison, as with DotView, will be simple. So, first, the AppleScriptObjC version:

script Task_ListAppDelegate
     property tableData : {}
     property removedTableData : {}
     property tableDataController : missing value
     property CalCalendarStore : class "CalCalendarStore"
     property NSMutableArray : class "NSMutableArray"
     property CalTaskClass : class "CalTask"

     on awakeFromNib()
          set removedTableData to NSMutableArray's array()
          set tableData to NSMutableArray's array()
          syncTaskList()
     end awakeFromNib

     on applicationWillTerminate_(application)
          syncTaskList()
     end applicationWillTerminate_

     on applicationWillResignActive_(application)
          syncTaskList()
     end applicationWillResignActive_

     on applicationWillBecomeActive_(application)
          syncTaskList()
     end applicationWillBecomeActive_

     on addTask_(sender)
          tableDataController's addObject_({priority:"3", task:"", status:"Not Started", calTask:missing value})
     end addTask_

     on removeTask_(sender)
          set deleted_objects to tableDataController's selectedObjects
          set deleted_object to item 1 of deleted_objects
          removedTableData's addObject_(deleted_object)
          tableDataController's remove_(missing value)
     end removeTask_

     on syncTaskList()
          set calendarStore to CalCalendarStore's defaultCalendarStore
          set theCalendars to calendarStore's calendars
          set todoPredicate to CalCalendarStore's taskPredicateWithCalendars_(theCalendars)
          set tasksInCalStore to CalCalendarStore's defaultCalendarStore's tasksWithPredicate_(todoPredicate)
          set tasksInTable to tableData's valueForKey_("calTask")
          set tasksToDelete to removedTableData's valueForKey_("calTask")

          set tasksAdded to NSMutableArray's array()

          -- Get tasks from iCal that aren't in the table, and delete tasks from iCal that we've been asked to kill
          repeat with aTask in tasksInCalStore
          if not (tasksInTable's containsObject_(aTask) as boolean) and not (tasksToDelete's containsObject_(aTask) as boolean) then
               -- Add a new task if it's not in the list of showing or deleted taks
               set priority to aTask's priority as string
               set |title| to aTask's |title| as string
               tableDataController's addObject_({priority:priority, task:|title|, |status|:"Not started", calTask:aTask})
               tell tasksAdded to addObject_(aTask)
          else if tasksToDelete's containsObject_(aTask) as boolean then
               -- Delete task we were asked to kill
               set returnValue to calendarStore's removeTask_error_(aTask, reference)
               if not item 1 of returnValue then
                    set err to item 2 of returnValue
                    error (err's localizedDescription())
               end if
          end if
     end repeat

     -- Update and create new calTasks based on table data
     repeat with tableDataItem in tableData
          set calTable to tableDataItem as record
          try
               set aCalTask to calTable's calTask
          on error
               set aCalTask to missing value
          end try

          if aCalTask is not equal to missing value and not (tasksInCalStore's containsObject_(aCalTask) as boolean) and not tasksAdded's containsObject_(aCalTask) as boolean then
               -- Delete this task, which was deleted in iCal
               tell tableDataController to removeObject_(tableDataItem)
               exit repeat
          else if aCalTask is equal to missing value then
               -- Create new CalTask for a newly created table row
               set aCalTask to CalTaskClass's task()
               set aCalTask's calendar to (get first item of calendarStore's calendars)
               set the calTask of tableDataItem to aCalTask
          end if

          -- Save out both the existing tasks and freshly created tasks
          if aCalTask is not equal to missing value then
               set the |title| of aCalTask to tableDataItem's task
               set the priority of aCalTask to (tableDataItem's priority's integerValue)
               set returnValue to calendarStore's saveTask_error_(aCalTask, missing value)
               end if
          end repeat
     end syncTaskList
end script


That's not bad, about 73 lines of code, if you remove non-code lines. What about the Studio version? I'm not pasting it in here, because it's about 185 lines of code or so to do the same thing. Well, less, as we'll see in a bit. So let's look at some common code here, the "awake from nib" function, which is analogous to the application launch. The AppleScriptObjC version:

on awakeFromNib()
     set removedTableData to NSMutableArray's array()
     set tableData to NSMutableArray's array()
     syncTaskList()
end awakeFromNib



That's pretty simple. Set a couple variables to arrays, and run something called syncTaskList. The Studio version:

on awake from nib theObject
     if name of theObject is "tasks" then
          -- Create the data source for our "tasks" table view
          set theDataSource to make new data source at end of data sources with properties {name:"tasks"}

          -- Create the data columns, "priority", "task" and "status". We also set the sort properties of each of the data columns, including the sort order, the type of data in each column and what type of sensitivity to use.
          make new data column at end of data columns of theDataSource with properties {name:"priority", sort order:ascending, sort type:numerical, sort case sensitivity:case sensitive}
          make new data column at end of data columns of theDataSource with properties {name:"task", sort order:ascending, sort type:alphabetical, sort case sensitivity:case sensitive}
          make new data column at end of data columns of theDataSource with properties {name:"status", sort order:ascending, sort type:alphabetical, sort case sensitivity:case sensitive}

          -- Set the data source as sorted
          set sorted of theDataSource to true

          -- Set the "priority" data column as the sort column
          set sort column of theDataSource to data column "priority" of theDataSource

          -- Finally, assign the data source of the table view to our data source
          set data source of theObject to theDataSource
     end if
end awake from nib


Oof. Even if I were to take the comments out, we can still see that the Studio version is a lot bigger, and seems to require you to do a lot more work for the initial application setup. That's because it does require more work for initial application setup. Even though you use Interface Builder to build the UI elements for both AppleScriptObjC and Studio, AppleScriptObjC is able to use things the way a 'real' Cocoa application does. So unlike Studio which makes you create manually create the AppleScript implementation of the data columns that will be used by the UI, and set all the properties of those data columns, AppleScriptObjC is able to use the information that you already put into Interface Builder, and that Interface Builder provides for free. The idea behind this is trick called "bindings". I'm not going to even try to explain bindings here, but they're what allow AppleScriptObjC to not have to need all the code that Studio needs. It's not that the UI in Studio doesn't have all the bindings info available to it, it's that bindings are invisible to Studio, and so with Studio, you're stuck using the older Data Source API. So, in a sense, even though you've built the UI in Interface Builder, you have to do a lot of redundant work in Studio to manually define what the UI elements in Task List are doing, work you don't have to do in AppleScriptObjC.

That's kind of the idea with Interface Builder, by the way. You create the UI elements, set them up, and then the code is able to just use them.

While Studio's use of AppleScript is more 'traditional', it's also far kludgier. This is repeated over and over throughout the code. Stuff that AppleScriptObjC can do in a few lines, Studio takes a book. Stuff that AppleScriptObjC doesn't even need code for, Studio needs lines and lines and lines.

Oh, and the syntax is so far removed from 'normal' Cocoa syntax, even allowing for specific language differences that there's almost no way to apply standard Cocoa documentation and sample code to Studio. The gulf between them is just too wide.

However, I do feel that I should come clean about something. When I said that AppleScriptObjC took about 73 lines of code to do the same thing that Studio needs almost 200 lines of code to do, I was lying. Blatantly lying. It really only needed 24 lines of code to do what Studio did. The other 49 or so lines of code let you integrate Task List into the iCal store, so you can see your iCal tasks in Task List, and have the tasks you add to Task List show up in iCal. I don't even want to think about what it would take to do that with Studio, assuming you even could without having to use "call method" and shell out to 'real' Cocoa code. However, AppleScriptObjC does lose to Studio in one important way: AppleScriptObjC and AppleScriptObjC applications will only run on Mac OS X 10.6 or later. If you want to create applications using AppleScript for Mac OS X 10.5 or earlier, you cannot use AppleScriptObjC.

AppleScriptObjC is a huge change, and it does bring with it a lot of new things you'll need to learn, and unlearn, especially if you are using Studio. But, it is also a huge leap forward in capability and features for AppleScript. It also shows, better than any amount of platitudes could, that AppleScript is not going away anytime soon. I just cannot see Apple doing this much work, and creating such a monstrously huge improvement to AppleScript just to knife it.
Subscribe
  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 0 comments