?

Log in

No account? Create an account
entries friends calendar About MEEE! Previous Previous Next Next
January 21st, 2010 - Schadenfreude is my life — LiveJournal
...because Misanthropy is FUN
bynkii
(Just for those who care: no, i'm not an experienced Cocoa or ObjC programmer. It should have been obvious if you knew enough to ask, but just to clear it up.)

This post, we're going to take the initial save file setup we had from last week, and expand that to actually writing data to a new file. Doing this is going to require splitting up the overall writing code across three handlers in my application. In this case, we have made some minor changes to the code that executes after the user clicks "Okay" in the save panel dialog:

if theSavePanelResult is 1 then
     --if they clicked the 'save' button, then we want to get the path and set some flags
     set theDataFileURL to theSavePanel's |URL|()
     --get the encoded URL, which is not the file path, but we'll need it
     set theDataFilePath to theSavePanel's filename()
     --get the posix path to the file
     set createDataFile to true
     --we're creating a new data file, so this has to be true
     set appendToExisting to false
     --we are not appending data, so set to false
     set theFileManager to my NSFileManager's defaultManager()
     --create a file manager object so that we can create a blank file
     set theCreateFileResults to theFileManager's createFileAtPath_contents_attributes_(theDataFilePath, missing value, missing value)
     --creates a blank file at the path specified. Do NOT use "my theFileManager's..." because errors will happen
     set theFileHandle to my NSFileHandle's fileHandleForWritingToURL_error_(theDataFileURL, missing value)
     --use this to write to the file URL

end if


So as you look at it, you can see that all we did was make the file handle setup the last line. Note that I've set up theFileHandle as a property, because it's the lazy way to avoid scope issues. So we clicked our button to create a new file, we gave it a name, and a location, and we have a file path to get data into it. How do we build the data? Well, first, we have to define what that data is. In this application's case, it's series of lines of tab-delimited text. Each item is a bit of info about the current WiFi network, along with a timestamp for when the data was read. This can be entered as part of the track functionality, writing one line of data per second, or when you hit the refresh button, to write the data manually. The file handle is closed when you either turn off the track function, or deselect the save to file checkbox in the application UI. So let's look at building the data string.

Since I have a nice handler to grab the wifi data and populate the UI with the manual refresh or with the track button, we'll add some code to that. Here's the entire loadData handler:

on loadData(theCurrentInterface)
     currentSSID's setStringValue_(theCurrentInterface's ssid())
     --set the contents of SSID field to the current SSID
     set theCurrentAuthMode to (theCurrentInterface's securityMode() as text)
     --it's an NSNumber, will deal with it later. Forcing to text works for now
     if theCurrentAuthMode is "0" then
     --mode number to mode name

          authMode's setStringValue_("Open")

     else if theCurrentAuthMode is "1" then

          authMode's setStringValue_("WEP")

     else if theCurrentAuthMode is "2" then

          authMode's setStringValue_("WPA1 Personal")

     else if theCurrentAuthMode is "3" then

          authMode's setStringValue_("WPA2 Personal")

     else if theCurrentAuthMode is "4" then

          authMode's setStringValue_("WPA1 Enterprise")

     else if theCurrentAuthMode is "5" then

          authMode's setStringValue_("WPA2 Enterprise")

     else if theCurrentAuthMode is "6" then

          authMode's setStringValue_("WiFi Protected Setup")

     else if theCurrentAuthMode is "7" then

          authMode's setStringValue_("Dynamic Wep 802.1X")

     else

          authMode's setStringValue_("Unknown/Invalid")

     end if

     currentChannel's setStringValue_(theCurrentInterface's channel())
     --set the channel
     currentDataRate's setStringValue_(theCurrentInterface's txRate())
     --set the data rate in Mbps
     signalStrength's setStringValue_(theCurrentInterface's rssi())
     --set the signal strength in dbm
     signalNoise's setStringValue_(theCurrentInterface's noise())
     --set the signal noise in dbm
     currentWAPMAC's setStringValue_(theCurrentInterface's bssid())
     --set the MAC of the base station
     theTime's setStringValue_((time string of (current date)))
     --let's add the code to save to a new file
     if (createDataFile is true) and (theSaveFileFlag is true) then

          set theTempString to (theTime's stringValue() as text) & " "
          & (currentSSID's stringValue() as text) & " "
          & (authMode's stringValue() as text) & " "
          & (currentChannel's stringValue() as text) & " "
          & (currentDataRate's stringValue() as text) & " "
          & (signalStrength's stringValue() as text) & " "
          & (signalNoise's stringValue() as text) & " "
          & (currentWAPMAC's stringValue() as text) & "
"
          set theFileString to my NSString's stringWithString_(theTempString)
          set theFileData to theFileString's dataUsingEncoding_(NSUTF8StringEncoding of current application)
          theFileHandle's writeData_(theFileData)
          --you could probably also use fileHandleForWritingToPath here, but since URLs are the way of the future
          --we should use them where we can
          --we stash the close function where it's going to be actually used

     end if

end loadData


There's really not much going on here. This handler, (and you can tell it's a 'normal' AppleScript handler because it doesn't have an underscore in the name), is passed a WiFi interface object, theCurrentInterface. It then pulls data from that to set various text fields in the Application UI. For example, we grab the SSID of the current WiFi network via theCurrentInterface's ssid(), and use that to set the contents of that text field in the UI via currentSSID's setStringValue_(theCurrentInterface's ssid())

Since the enumeration for the authentication mode can be one of 9 values, including "other", we have a series of if then else statements to handle that. To get the time string we use for theTime, instead of using the Cocoa technique, and NSDate/NSDateComponents, we use a more traditional AppleScript way: time String of (current date). It works just as well as any other method for our needs, and is simpler to code. adding that to the UI is then a single line: theTime's setStringValue_((time string of (current date)))

So now, we have all the information we need to build our string that we want to write to file. To do that, we again, combine Cocoa and traditional AppleScript to build a tab-delimited line with a trailing return:

set theTempString to (theTime's stringValue() as text) & " " & (currentSSID's stringValue() as text) & " " & (authMode's stringValue() as text) & " " & (currentChannel's stringValue() as text) & " " & (currentDataRate's stringValue() as text) & " " & (signalStrength's stringValue() as text) & " " & (signalNoise's stringValue() as text) & " " & (currentWAPMAC's stringValue() as text) & "
"

The blank spaces in between the quotes are the presentation of the tab formatter, \t. The odd quotation mark by itself is the presentation of the return formatter, \r.

So now we spend three statements on converting theTempString to an NSData object and writing that to a file. First, we create the NSString object: set theFileString to my NSString's stringWithString_(theTempString). Pretty clear, we use the stringWithString function, passing it the temp string. Next, we encode theFileString and convert it to an NSData Object:

set theFileData to theFileString's dataUsingEncoding_(NSUTF8StringEncoding of current application)

We're using UTF8 encoding, because that's the better way to do things, but there are a variety of encodings available, including MacRoman, etc. One thing to watch here is that you have to set the encoding as "<encoding method> of current application", or it fails miserably. Finally, we write that NSData object to the file we created via the file handler:

theFileHandle's writeData_(theFileData)

Every time this handler is called, as long as createFileData and theSaveFileFlag are both true, we get a line of data written.

So what happens when we're done writing? Well, it wouldn't make a lot of sense to put that code here, so we put it in the functions that we use when we're done tracking data, or when we deselect the save file checkbox. First, when we stop tracking:

on timerFired_(thetimer) --this handler runs the actual code for the timer
     if trackButtonSTate is 1 then --'on'

          loadData(theCurrentInterface) of me --grab stats once per second

     else if trackButtonSTate is 0 then --"off"

          thetimer's invalidate() --kill the timer
          theFileHandle's closeFile() --close the file handle we've been writing to
          set theSaveFileFlag to false --kill the save file flag
          set createDataFile to false --kill the new file flag
          set appendToExisting to false --kill the append file flag
          saveToFileCheckBox's setIntValue_(0) --disable the checkbox
          my createNewDataButton's setEnabled_(false) --disable the button to create a new data file
          my appendToExistingDataButton's setEnabled_(false) --disable the button to append to an existing data file

     end if

end timerFired_


We put the file handle cleanup code in the same block as the timer cleanup code. When you disable tracking, it's going to run the code to kill the timer that's controlling how fast the tracking is happening anyway, so it makes sense to put it here. It's only one line to shut down the file handle:

theFileHandle's closeFile()

That's it, the file handle is closed, file writing is done. The rest is just cleaning up properties and resetting controls to give a better indication to the user that they're no longer recording data to a file.

The same basic thing happens if they disable the save to file checkbox:

on saveToFile_(sender)
--when you click the "Save to file" checkbox this ONLY controls the button states, not what the buttons do
     if sender's intValue() is 1 then --if you're checking the checkbox

          set theSaveFileFlag to true
          my createNewDataButton's setEnabled_(true) --enable the button to create a new data file
          --the "my" is critical to having the now enabled button be able to send events
          --without "my", the button knows it's enabled, nothing else does
          my appendToExistingDataButton's setEnabled_(true) --enable the button to append to an existing data file

     else if sender's intValue() is 0 then --if you're de-checking the checkbox

          theFileHandle's closeFile() --close the file handle we've been writing to
          set theSaveFileFlag to false --kill this file flag
          set createDataFile to false --kill the new file flag
          set appendToExisting to false --kill the append file flag
          my createNewDataButton's setEnabled_(false) --disable the button to create a new data file
          my appendToExistingDataButton's setEnabled_(false) --disable the button to append to an existing data file

     end if

end saveToFile_


Nothing new here, close the file handle, reset things, and bob's your uncle.

Once you wrap your head around things, especially the encoding method thing, writing data to a file is pretty easy. Really, that was the biggest frustration for me, because even reading the Cocoa docs on this, there was nothing to really indicate that had to happen. Again, many thanks to Shane, Craig, and everyone else on the AppleScriptObjC list for all their help.
Leave a comment
bynkii
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.
Leave a comment