AppleScriptObjC in Script Debugger 6

Direct access to the Cocoa frameworks — AppleScriptObjC — has been one of the biggest boosts to AppleScript’s abilities in a long time. Like the previous big expansion of abilities — the introduction of do shell script and access to shell commands — it can also involve a significant learning curve.

With AppleScriptObjC there are some compounding factors, apart from the fact that Objective-C is new territory to most scripters. Scripting applications involves sending Apple events to them, and dealing with Apple events returned from those applications. There are mechanisms for examining these events and logging them. But AppleScriptObjC all happens inside AppleScript itself: there are no Apple events, so nothing appears in the event log, other than the final result of any script.

If that didn’t make debugging difficult enough, there is another catch. Objective-C scripting deals in pointers — essentially what gets passed around is the address in memory of objects. When these pointers get passed to AppleScript, they get wrapped in a special AppleScript container. So when you run a script that returns a Cocoa object in a standard script editor, you will see something like this: «class ocid» id «data optr00000000C0E0017AFF7F0000» That’s not particularly illuminating.

And just when you have your script working, you are likely to get an error saying the script cannot be saved because pointers cannot be stored in scripts.

There are other issues, too. AppleScriptObjC is case-sensitive and much of the terminology is verbose, so it is very easy to make typos. And with interleaved terminology, it is not uncommon for parameter names to clash with terminology defined in AppleScript or scripting additions and scriptable applications, so you need to be on the lookout for where parameters need escaping with pipe characters. Finally, there is the issue of documentation, both finding it and searching it.

Script Debugger 6 offers help on all these fronts, but just how might not be immediately obvious. This is an overview of the new features, with some tips on how to get the most from them.

WARNING: A bug in AppleScript prevents Script Debugger 6 from supporting AppleScriptObjC fully under OS X 10.10. You can proceed at your own risk, but Script Debugger may crash when running scripts that use AppleScriptObjC under OS X 10.10.

Displaying AppleScriptObjC Values

The most important new AppleScriptObjC feature, and the most obvious, is how Script Debugger displays results. Instead of «class ocid»..., you will see much more. Here is a simple script to get the name of all the files on your desktop in an array:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set posixPath to POSIX path of (path to desktop)
set fileManager to current application's NSFileManager's defaultManager()
set theFiles to fileManager's contentsOfDirectoryAtPath:posixPath |error|:(missing value)

If you run that in a normal script editor, the result will be something like «class ocid» id «data optr00000000C0E0017AFF7F0000».

Enter the script in Script Debugger 6, and make sure the Results & Variables Tab is visible, with the control in the top-right of the pane set to Source. Run the script.

TIP: Script Debugger provides pre-configured templates for scripts that use AppleScriptObjC. You don’t have to use them, but they can save you some typing and configuration.

Sample AppleScriptObjC script

A sample AppleScriptObjC script, showing the Results & Variables Tab with both Best and Desc view selected. Collections such as arrays can be explored as if they were AppleScript lists.

You may notice that the Source label has changed to Desc. When you are dealing with AppleScriptObjC objects, and therefore pointers, there is no AppleScript source, so Script Debugger shows the Description. Every Cocoa object has a description method, to give some idea of what it contains. If you have ever used the NSLog() function, which can be used to print a description to the Console, you may notice that what Script Debugger is showing for the sample script doesn’t match the Objective-C formatting. For classes that have an AppleScript equivalent or near-equivalent, like NSArray here, Script Debugger will reformat the description into a more AppleScript-like form.

That’s a big improvement over «class ocid»..., but it gets better. Click on Best instead of Desc/Source. Script Debugger will now display an explorer view of the contents of the array, similarly to how it would if it were an AppleScript list.

TIP: If you hold down the shift key when in Best or Source/Desc view and click on the other heading, both views can be displayed at once, on top of each other. This can be particularly useful in AppleScriptObjC.

If you go to the Variable list below the Result area, you can see the variable theFiles listed, and if you click on the disclosure triangle next to it, the contents of the array are again displayed.

Now turn on Debugging, using either the toolbar button or by choosing Script -> Enable Debugging. Step through the script, using the Step Over button, or by choosing Script -> Step Over. The first line of the script is normal AppleScript, so it will show Source instead of Desc. Step again, and the file manager will appear, something like <NSFileManager: 0x608000000060>. This is the normal description of the file manager (the number reflects its address in memory). Step again and you can swap between Best and Desc to switch between the description and explorer views.

This combination of step-by-step execution, showing the Best and Desc views plus the Variable list, transform what was an essentially blind process into one you can easily follow.

Syntax Highlighting

You might notice something a bit different in the the screenshot above: the method calls and handler names are all formatted in a distinctive color, rather than using the same formatting as variables. This is not strictly an AppleScriptObjC feature — it works in all code. But it is particularly helpful in AppleScriptObjC code.

You can turn this on or off, or change the color used, in Preferences. Open Preferences, go to the Font & Colors tab, and you will see a checkbox labeled Apply color to method and handler names, alongside a color well. When checked, methods and handler names appear in a different color, red by default. Click on the color well to change the color.

Writing AppleScriptObjC Code

But what about writing the scripts in the first place? Script Debugger can help here, too. The most important feature in this case is code completion. If you are writing AppleScriptObjC code, you should always use code completion.

Let me emphasize the point. Code completion not only helps avoid typos, which are much more likely because of the longer names and case-sensitivity of Cocoa terms, but it can also help you choose the right method, because in some cases it can filter out methods that aren’t applicable. In addition, it provides information on parameter and return types, so it can save you time looking up documentation. It also checks the context and automatically adds pipes around parameters that would otherwise clash with terminology defined elsewhere. And of course it saves typing — not just the long names, but by inserting “current application’s” or “its” where necessary.

Code Completion

Using Script Debugger’s code completion for AppleScriptObjC is hard to describe in words — you can see better how it works by watching the video. But here is some further information, and some tips for getting the most out of it.

First, although case matters in AppleScriptObjC, code completion is case-insensitive. You can type NSS or nss, and get the same result; the correct case will be inserted. You can also use the tab key within the completion list; this is very helpful where there are multiple methods with similar names. How this works is described in Script Debugger’s Help.

The list of terms you are offered at any time can vary based on a number of factors. First of all, terms are restricted to those defined in frameworks the script is loading via a use framework statement. So if you are scripting the AppKit framework, your use framework "AppKit" statement will also load its terminology for code completion. And you need to compile your script after adding a new use framework statement to make the framework’s terminology available.

Script Debugger includes terminology for the following frameworks: Foundation, AppKit, Quartz, AVFoundation, AddressBook, Contacts, CoreImage, CoreLocation, CoreWLAN, EventKit, JavaScriptCore, MapKit, OSAKit, and WebKit. The information is collated from the frameworks themselves and from header files, with some filtering applied to remove many methods that cannot be used from AppleScriptObjC.

If you have a use AppleScript version... statement in your script, methods and classes introduced in versions of the system released after the specified version of AppleScript will not appear in completion lists. Because new Objective-C classes and methods are regularly added to the OS as new versions are released, this is an important way you can avoid accidentally including code that will not run under the version of the OS you are targeting.

Completion lists are further filtered based on context, where possible. For example, if you have something like this:

set foo to current application's NSString's 

Script Debugger knows that it should restrict the following completion to class methods. You can increase the chance of Script Debugger offering what you need by consistently using the direct possessive style, so that the target comes first: prefer x's y over y of x.

One of the other important things to happen when you use code completion is that Script Debugger checks that any terms entered do not clash with terminology defined by AppleScript, a scripting addition, or an application whose tell block you are writing within. If it finds such a clash, it will automatically escape the term by surrounding it with pipe characters. With interleaved terminology for methods, potential clashes are all too common. This means code completion is a good idea even for very short terms, because they are just as likely to clash with defined terminology.

TIP: Even if you do not use code completion, Script Debugger will try to help out. For example, if you type a parameter name that clashes, when you type the trailing colon the pipes will be inserted around the term for you.

Some Objective-C methods have out parameters: the most common are those where the last parameter is error. When you enter such a method using code completion, the placeholder for these parameters will contain the text missingValueOrReference. This is because you can only provide one of two values: (missing value) if you are not interested in the value, or reference if you want the value to be returned along with the result of the method. In such cases, when you select the placeholder, usually by tabbing to it, you can type esc or return, and a popup menu will appear showing the two values, (missing value) and reference, and you can select which to insert.

A similar popup is available when a parameter has a value of trueOrFalse.

If you have watched the video on code completion, you will have seen that completion can deal with the need to target current application in two ways, either by inserting current application's in-line where needed, or by automatically declaring properties for Cocoa terms. This is controlled by the checkbox in Preferences -> Editor labelled Use properties for Cocoa terms in completion, clippings. You can also set it on a document-by-document basis by checking or unchecking the menu item Edit -> AppleScriptObjC -> Use Properties for Cocoa Terms.

AppleScriptObjC Use Properties Off

A simple script written using code completion, with the option to define terms as properties turned off.

AppleScriptObjC Use Properties On

The same as above, this time written with the option to define terms as properties turned on.

In the case of enums, choosing to declare properties can introduce some subtle differences. If the enum is one that is from a known framework, the property uses the enum’s underlying value in the declaration. So where you might expect to see this:

property NSDirectoryEnumerationSkipsHiddenFiles : a reference to NSDirectoryEnumerationSkipsHiddenFiles

you will actually see this:

property NSDirectoryEnumerationSkipsHiddenFiles : a reference to 4

There are a few reasons for this: it’s a fraction more efficient than evaluating at run-time, it leaves you covered in the event that Apple changes an enum (something that’s happened before, and is happening again in macOS Sierra), it is more reliable if the script is to run under Yosemite or earlier, and it makes code simpler and clearer where multiple enums need to be summed. The reason the value is a reference to 4 rather than simply 4 relates to one of the other new Script Debugger features: the ability to change existing code from one form of referencing current application to the other.

Refactoring

This ability to refactor code is provided by three commands in the Edit -> AppleScriptObjC submenu. The first two will convert existing code that uses in-line targeting of current application to property-based declarations. Both require that the script be compiled first, and the difference between them is that Migrate to Properties will operate on the full script, whereas Migrate to Properties in Selection will operate only on the selected code.

AppleScriptObjC Edit Submenu

The AppleScriptObjC submenu of the Edit menu. These commands let you switch between the two different methods of declaring Cocoa terms.

Although declaring Cocoa terms as properties has its advantages, there is one situation where it is not so helpful, and that is where you want to copy code to use elsewhere. That is where the remaining command on the AppleScriptObjC submenu, Copy as Standalone Code, comes in. Again, the script needs to have been compiled. When you choose the command, the selected code will be copied to the clipboard with current application's inserted inline for any declared Cocoa terms.

These refactoring commands work best if you use the style of coding outlined above.

It’s worth repeating: If you are writing AppleScriptObjC code, you should always use code completion.

Persistent Properties

One other important setting is Script -> Persistent Properties. When Persistent Properties is off, the script cannot be saved dialog no longer appears when you try to save after running a script that has AppleScriptObjC objects stored in properties or other top-level variables. It is also off by default if you start a script from one of the AppleScriptObjC templates.

TIP: With properties using the form a reference to, it is possible to make a script that uses AppleScriptObjC have persistent properties, by making sure none of the other properties contain AppleScriptObjC values, and relegating all other use of AppleScriptObjC to a handler other than the run handler.

Threading

Finally, there is an important limitation. Script Debugger runs scripts on a background thread, so any code that needs to be run on the main thread needs to use the method performSelectorOnMainThread:withObject:waitUntilDone: to do it. If not, you are likely to freeze or crash Script Debugger.

Here is an example of showing an alert, something that must be done on the main thread. Notice the use of a property to return a value.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit" -- required for NSAlert
property returnCode : missing value

set buttonNames to {"Cancel", "Maybe", "Possibly", "Perhaps", "Probably", "OK"}
set theAlert to (my makeAlert:"Stay alert" message:"This is the 9 o'clock news" buttons:buttonNames)
-- make sure the alert is shown on the main thread
my performSelectorOnMainThread:"displayAlert:" withObject:theAlert waitUntilDone:true
-- get values after alert is closed
set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
set buttonName to item buttonNumber of buttonNames
return buttonName

on makeAlert:mainText message:theExplanation buttons:buttonsList
    set buttonsList to reverse of buttonsList -- because they get added in reverse order cf AS
    -- create an alert
    set theAlert to current application's NSAlert's alloc()'s init()
    -- set up the alert
    tell theAlert
        its setMessageText:mainText
        its setInformativeText:theExplanation
        repeat with anEntry in buttonsList
            (its addButtonWithTitle:anEntry)
        end repeat
    end tell
    return theAlert
end makeAlert:message:buttons:

on displayAlert:theAlert
    -- show the alert
    set my returnCode to theAlert's runModal()
end displayAlert:

Other AppleScriptObjC Aids

Also look at Script Debugger’s Clippings and Text Substitution features. Script Debugger 6 includes several AppleScriptObjC-specific clippings in a submenu of their own, and there are several AppleScriptObjC-specific expansion tags you can use in your own clippings. These are detailed in Script Debugger’s Help. If you go to Preferences -> Text Substitution, you will see several AppleScriptObjC-related substitutions using the same expansion tags. Because not everyone will use AppleScriptObjC, these are off by default. In both cases, the insertion of current application will be done in the same way is it would using code completion.

AppleScriptObjC Documentation

Last is the issue of documentation. You can look up Cocoa documentation on the Web, or you can use Xcode’s documentation browser. Script Debugger supports another option, which is the third-party application Dash. With Dash.app installed, go to Preferences and in the Editor tab, check Option-click opens terms in Dash. This gives you a way to quickly look up terms in Dash.app by option-clicking on them in your code. (Dash covers many languages, including Objective-C and AppleScript, and has both paid and free modes.)

One thought on “AppleScriptObjC in Script Debugger 6”

Comments are closed.