Search code examples
objective-cmacoscocoascripting-bridge

Scripting Bridge: Combining SBElementArrays


According to Apple's documentation on Scripting Bridge performance, we should strive to use batch operations on SBElementArrays, since Apple event calls are expensive.

... whenever possible you should always use one of the “batch operation” array methods instead of enumerating the array. These methods avoid the inefficiency of enumeration because they send a single Apple event rather than one Apple event per item in the array.

I'm using Scripting Bridge with the System Events application, and I'm able to get menu items from the menus successfully. It's much faster than the NSAppleScript method I was using previously.

What I'd like to do is combine together several SBElementArrays, each of which is holding menu items from different menus. The plan is to then run the batch operation once instead of doing it for each menu individually.

It seems to me like this shouldn't be that complicated, though obviously my knowledge in this area is limited at best. Unfortunately I'm running into serious errors.

First Attempt

If I attempt to create an empty SBElementArray element and then loop through the menu items adding each set of menuitems, like so:

SBElementArray* menuItemCombinedArray = [[SBElementArray alloc] init];
for (SystemEventsMenuBarItem* menu in menuBar.menus) {
    menuItemCombinedArray = [[menuItemCombinedArray arrayByAddingObjectsFromArray:menu.menuItems] mutableCopy];
}

NSArray* menuItemNameArray = [menuItemCombinedArray arrayByApplyingSelector:@selector(name)];

I get an error saying that [SBElementArray init] should never be used, which is a bit odd since SBElementArray is a subclass of NSMutableArray.

Second Attempt

Next I tried a hackier way, where I created the SBElementArray separately for the first menu, then looped through the remaining menus and added those SBObjects one at a time, like so:

SBElementArray* menus = menuBar.menus;
SystemEventsMenuBar* firstMenu = menus.firstObject;
SBElementArray* menuItemCombinedArray = firstMenu.menuItems;

[menus removeObjectAtIndex:0];

for (SystemEventsMenuBarItem* menu in menus) {
    SBElementArray* tempMenuItemsArray = menu.menuItems;
    for (int i = 0; i < tempMenuItemsArray.count; i++) {
        [menuItemCombinedArray addObject:[tempMenuItemsArray objectAtIndex:i]];
    }
}

NSArray* menuItemNameArray = [menuItemCombinedArray arrayByApplyingSelector:@selector(name)];

But now I get a different error: [SBElementArray addObject:]: can't add an object that already exists.'

Summary

From what I've read searching around, it sounds like Scripting Bridge in general, and SBElementArray specifically, are kind of wonky. But Scripting Bridge has been much faster for me than NSAppleScript, much closer to what I'm aiming for. I think if I could get this optimization working I'd be in great shape.

Thanks in advance for any help!


Solution

  • SBElementArray is not an Array - it's a lot of smoke-n-mirrors BS disguising the otherwise simple fact that Apple event IPC is not OOP but RPC plus simple relational queries.

    What you really have beneath all that SBElementArray dross is a single object specifier that describes a one-to-many relationship between 'objects' in the application's Apple Event Object Model, an idealized, virtual representation of the user's data presented in a programmatic user interface.

    The application also defines various Apple event handlers for performing operations on its AEOM - creating, deleting, moving, duplicating, etc. - the idea being that you send a request to the app, e.g. duplicate (every track whose artist is "Bjork") to (playlist "Icelandic"), and the receiving handler figures out precisely how to carry out that operation for you.

    How well this query-driven approach works in practice depends on how well the app's AEOM support is implemented: often the underlying Model layer implements collections as ordered Arrays rather than unordered Sets, and since you're basically performing set operations of the sort more commonly seen in RDBMSes, well, there's all sorts of opportunities for misorderings and other errors to creep in when moving array elements around in relation to one another. But the basic concept is not unsound (just a PITA to implement reliably); alas, the SB authors seem to think that "Relational Graphs Is Too Hard for Cocoa users" (which no doubt comes a big suprise to CoreData users), so try to hide it all beneath a stinky, incompetent ORM.

    Thus there's absolutely no point trying to apply NS[Mutable]Array semantics to the problem as you're doing, because SBElementArrays are not local (or remote) arrays, but crippled obfuscated wrappers around AEOM queries. In other words, to understand why what you're doing doesn't work and how to do it so it does, you need to understand how AEOM actually works, how SB lies about how it works, and how SB translates its lies into [very limited] AEOM behaviors.

    Thus, when you apply -[SBElementArray arrayByApplyingSelector:], it isn't actually performing array iteration at all; instead, it's constructing an object specifier of form |selector name| of |elements| of... and sending it to the application in a get event to resolve; the result being a list of values of the specified properties. Of course, this all turns out to be completely useless when you want to perform anything other than a simple get operation, e.g. set (rating of every track of playlist "Icelandic") to 100, because the SB API's too crippled and prejudiced to let you express this, even though it's a perfectly valid request.

    ...

    TL;DR: It's a complete waste of time trying to do anything non-trivial in SB, because the harder you push it, the more its pseudo-OO fakery falls apart. The only [officially supported] way to do Apple events correctly is via AppleScript, and as you say using AS via NSAppleScript is an exercise in groin-punching that's barely less painful than SB (although partly that will be because you're no doubt doing it wrong, i.e. generating custom AS source code via string-mashing and compiling and executing it on the fly instead of calling parameterized handlers in precompiled .scpt files loaded from your app bundle).

    Fortunately, 10.6 introduced the AppleScript-ObjC bridge which, while not without some shortcomings of its own, is by far the easiest and quickest way to integrate AS and ObjC code as it allows you to define AppleScript script objects that appear to your ObjC code almost as if they were native Cocoa classes and instances. That would be my recommended approach to you, and forget about SB for anything but trivial tasks (or just forget it altogether and stick with AS, which may be naff but at least it's mostly understood, less dishonest naff).