How to: Integrating AppleScriptObjc Class into in a Swift Language Xcode Project

This is a short how-to for integrating ApplescriptObjc/Cocoa-Applescript with a Swift Language project. (It’ rather slapdash being largely just my own notes, meticulously mis-formated.) Link to github project pending.

It is tested on, MacOS 10.10 (14A361c) beta , Xcode Version 6.1 (6A1030) and Swift 1.0GM. In principle, the compiled code will run under MacOS 10.9.x but I haven’t tested it yet.

Swift language is still actively evolving so it is possible the code and steps below will break at some undetermined point in the near future.

Problem Story: For Objective-C programmers like myself, working on any logic more than a few dozen lines long in Applescript become torturous. It’s easier to do most of the heavy lifting in Objective-c and reserve ApplescriptObjc for just the inter-application communications. This leverages the strengths of each language and has proven very useful for small projects and prototyping future hard coded Apple event integration.

Fortunately, wrapping an ApplescriptObjc up for use in Objective-C proved to be fairly simple.

Then along came Swift.

I decided to see how hard it would be to do the same trick in Swift. Turns out, works pretty well so far. The basic idea is to create an Objective-C Protocol to bridge ApplescriptObjc–>ObjectiveC, then instantiate the script object as an Objective-C object, then use the Objective-C to Swift bridge to make that object visible to Swift.

Solution:
Details below but the basic concept is:

  1. Start with a Swift project
  2. Add ApplescriptObjc Framework
  3. Add Objective-C helper class to load ApplescriptObjc scripts form bundle
  4. Add Applescript Script Object in source
  5. Create an Objective-C protocol that exposes the Applescript Script Objects properties and methods to Objective-C
  6. Add a method to the helper class to create a class from string, then initialize and instance.
  7. Let Xcode create a bridge file for both the prototocl and the helper class.
  8. Call the helper class from Swift to load Applescript files from bundle
  9. Call the helper class from Swift to instantiate an instance of the Applescript Script Object in Swift
  10. From Swift, set properties and call handlers/methods of the Applescript Script Object

I should note that, although this process uses many long standing techniques of the Objective-C run time to create classes on the fly, it uses two layers of bridging: ApplescriptObjc → Objective-C → Swift, and with that much translation going on, it’s likely to be a fragile and hard to debug solution. I suggest constant and progressive testing to make sure each layer works before you try to “cross the next bridge.”

This is probably more detail than anyone needs but (1) I already had it written for my own future reference and (2) I’ve gotten stuck in the past because someone glossed over a detail I missed. Belt and suspenders is my motto.

- Create Project

  • Xcode > File > New Project.
  • Choose OSX > Application > Cocoa Application > Next.
  • Set Language to Swift.
  • Create and Save Project as Normal.
  • Configure Build
    • Build Settings
      • In the Project Navigator, select “YourProjectName” > TargetName > Build Settings
      • Build Settings > Packaging > Defines Module, set to “Yes” (Default is “No”)
    • Build Phases
      • Add AppleScriptObjC.framework
      • In the Project Navigator, select project-name > Target-Name > Build Phases > Link Binary Libraries
      • Click “+”
      • In dialog, select “AppleScriptObjC.framework”
      • In the framework list, confirm status of AppleScriptObjC.framework is “Required” (should be the default)

- Add Applescript file
- File > New File
- In dialog, OS X > Other > Empty > Next
- In Save Dialog
- Set file name “Script-Obj-Name.applescript” (note the suffix is absolutely required.)
- In “Targets” list, check main target and test target and any other target you may want to use the Applescript in.
- Navigate to the save location of your choice, click “Create”
- Open the Applescript file and create a script object as follows:

script DemoScriptObj
  property parent: class "NSObject"
  property demoProp: "default property value"

  on demoHandler()
      tell me to log my demoProp
  end demoHandler
  
end script
   - Save file (saving after edits helps Xcode's code sense with autocompletion.)

- Add Script Protocol file
- File > New File
- In dialog, OS X > Source > Objective-C File > Next
- “Choose Options for your New File”
- Recommend name “ScriptObjNameClassProtocol” (can be any arbitrary valid objective-c name.)
- File Type: select Protocol
- Click “Next”
- Save File Dialog
- Choose save location
- In “Targets” list, check main target and test target and any other target you may want to use the applescript in.
- Navigate to the save location of your choice, click “Create”
- You will be presented with a dialog “Would you like to configure an Objective-C bridging header?” Click “Yes”
- An Objective-C header file will appear in your project.
- Open the new protocol file
- By default the header itself will only import Foundation Framework. Add any other frameworks you might need. (This demo will not do so.)
- Inside the “@protocol@end” add objective-c versions of your script object’s properties and methods that you wish to call from objective-c or swift. (see sample code)

[code]// DemoScriptObjClassProtocol.h
// DemoAsocSwift

#import <Foundation/Foundation.h>

@protocol DemoScriptObjClassProtocol
@property (strong,nonatomic) NSString *demoProp;

- (void) demoHandler;

@end[/code]
- Objective-C Class Factory/Helper
"This factor/help class might be unnecessary but if so, I don’t understand Swift and how Swift interacts with Objective-C to implement this completely in Swift. For now it works. "
- Create the Class File
- File > New File
- In dialog, OS X > Cocoa Class > Next
- “Choose Options for Your New File”
- Class: Any valid Objective-c class name, ( I used AsocInstancesFactory )
- Subclass of: NSObject
- Language: Objective-C
- Click “Next”
- In Save dialog, confirm the new class file is added to the same targets used by the Applescript class you created previously.
- Click “Create”
- Two files, a .h and a .m will be added to project. Group them to your convenience.
- Write factory Methods
- At the top of the .h file import the ScriptObjClassProtocol.h file created above. (code example)
- At the top of the .m file, import the <AppleScriptObjC/AppleScriptObjC.h> framework
- Add a class method to load Applescripts. (Note: NSBundle under Swift does not appear to have the loadAppleScriptObjectiveCScripts method at all, that I can find. )

[code]// AsocInstancesFactory.m
// DemoAsocSwift
Copyright (c) 2014 TechZenSoftware. All rights reserved.
//

#import “AsocInstancesFactory.h”
#import <AppleScriptObjC/AppleScriptObjC.h>

@implementation AsocInstancesFactory

  • (void) loadAsoc{
    [[NSBundle mainBundle] loadAppleScriptObjectiveCScripts];
    }

  • (id) createDemoAsocScriptInstances{
    Class DemoScriptObjClass = NSClassFromString(@“DemoScriptObj”);
    id demoObj=[[DemoScriptObjClass alloc] init];
    return demoObj;
    }
    @end[/code]

  • Add a class method to initialize and return an instance. Since Swift lacks an equivelent of NSClassFromString() we need to perform the class creation in Objective-C.

+ (id) createDemoAsocScriptInstances{ Class DemoScriptObjClass = NSClassFromString(@"DemoScriptObj"); id <DemoScriptObjClassProtocol> demoObj=[[DemoScriptObjClass alloc] init]; return demoObj; }
- Configure Bridging header
- Locate the file bridging file created earlier. It will be named something like “MainTargetNameSwift-Bridging-header.h” e.g. DemoAsocSwift-Bridging-Header.h
- Add

#import "DemoscriptObjClassProtocol.h" #import "AsocInstancesFactory.h"

  • This will tell the bridge which properties and methods to bridge. Any properties or methods not included in the bridging header are invisible to Swift.
    • Save
      - AppDelegate.swift
    • Note: Because of the setting for packaging in module in the Build Settings step, we don’t have to import anything into Swift files.
    • Call the ApplescriptObjc properties and methods from the Swift AppDelegate

[code]// AppDelegate.swift
// DemoAsocSwift

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

@IBOutlet weak var window: NSWindow!

func applicationDidFinishLaunching(aNotification: NSNotification?) {
AsocInstancesFactory.loadAsoc()
var demoObj: DemoScriptObjClassProtocol = AsocInstancesFactory.createDemoAsocScriptInstances()
demoObj.demoProp=“prop value from app delegate” as NSString
demoObj.demoHandler()
}[/code]
That’s really it. Once you understand the progression of Script–>Protocol–>Objective-C–>Bridging Header → Swift, you should be able to move fairly smoothly between all three languages in a single project.

Hi,

my implementation is similar, but I’m using a category of NSObject (aka informal protocol) and a Objective-C singleton.
I call my proxy Class ASObjC and the AppleScriptObjC script Sample


@import Foundation;
@import AppleScriptObjC;

@interface NSObject (Sample)

- (void)demoHandler;

@end

@interface ASObjC : NSObject

+ (ASObjC *)sharedASObjC;

@property id Sample;

@end


.m


#import "ASObjC.h"

@implementation ASObjC

+ (void)initialize
{
    if (self == [ASObjC class]) {
        [[NSBundle mainBundle] loadAppleScriptObjectiveCScripts];
    }
}

+ (ASObjC *)sharedASObjC
{
    static id sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[ASObjC alloc] init];
    });
    
    return sharedInstance;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _Sample = NSClassFromString(@"Sample");
    }
    return self;
}

@end

You can call the demo handler from Swift this way


let asobjc = ASObjC.sharedASObjC()
asobjc.Sample.demoHandler()


It’s a category method added by the AppleScriptObjC framework (one of several stupid design decisions that make ASOC much more klunky and less capable than it ought to be). Don’t know how Swift deals with those, but given that it’s supposed to be 100% Cocoa-compatible there ought to be a way. (I really want to learn Swift but just haven’t had a chance: having totally wasted six weeks trying to persuade the AS team not to bork JXA, I must catch up on my own work first.)

FWIW, I’ve been humming and hawwing over whether to write a Swift-to-Apple event bridge, partly as a learning exercise, but also to show how to do it right before the AS team make yet another mess of it. The problem is that NSAppleEventDescriptor is missing some essential functionality - e.g. it has no equivalent to the Carbon AESendMessage() function - and all of the Carbon APIs are either legacy or deprecated so shouldn’t be used for new development.

If you’d like to see such a project, please go file a Radar request asking that NSAppleEventDescriptor be extended with methods for packing and unpacking date (typeLongDateTime) descriptors and for sending Apple events, as those are (off the top of my head) the most egregious omissions. The more folk who put in such a request, the more likely Apple are to do something about it. (BTW, it’s a good idea to paste your Radar reports to OpenRadar, since Radar itself is a black hole to users.) I’ll file a feature request myself at some point, but I want to code it up as a category-based patch first so that I know it all works correctly.

Getting Apple to provide fully supported low-level Apple event APIs again won’t address the baked-in brain damage of SB and JXA, of course, but at least it’d allow users to provide and use their own alternatives without fear of it all going titsup when Apple starts its next Carbon pogrom.

What was stupid about putting it on NSBundle? One of the nice things about putting it there is that you can load AS classes at any time, and from any bundle. It means you can have a sort of plug-in mechanism using .scptd bundles, for example.

For one thing, it inextricably couples ASOC classes to bundles: you can’t just point it at any old file or folder and have it load that. Another thing is that there’s absolutely no reason to use class categories when a simple function would do: it’s just adding complexity and coupling for its own sake (i.e. programmers showing off). See also: Coupling and Cohesion, aka Software Design Principles 101.

Please go study some other well established -ObjC bridges to see how they work. They tend to be designed, written, and supported by people who actually eat their own dogfood - it makes a difference.

I’ll take it that you’ve never actually tried it then, or you’d know you’re wrong about folders.

Or it could be a case of when ASObjC first shipped, it only had access to methods, so making it a function would have made it inaccessible to its clients. See also: Common Sense.


Yes, actually using the stuff makes a difference – see above points for a couple of examples.

I stand corrected: I’m so used to the standard OS X bundle structure it never occurred to me that a bare folder would work too. It’s still annoyingly limited in that you can’t read the AS code directly from a string or a file[-like object]: when writing executable shell scripts you want to embed all the AS code in the Python/Ruby/etc. code so it’s completely self-contained and portable. Having a separate folder slopping about is just one more thing to get lost or go wrong[1].

Wut? The -load… method is called from ObjC, not AS. Also, if you must use a method, not a function, use a singleton class.

To reiterate my point about Coupling and Cohesion: the ASOC loading behavior belongs in ASOC, not in Foundation. There’s no good reason to couple ASOC tightly to NSBundle, and sticking an ASOC method into NSBundle breaks NSBundle’s own cohesion.

Injecting arbitrary names and behaviors into namespaces that your code doesn’t own or control is debatable design at best, and plain bad practice when the new behavior isn’t even relevant to the class in question. (As an AppleScripter who’s doubtless dealt with keyword collisions and similar problems, you ought to appreciate the importance of strong, clean separation between namespaces (Ruby’s even worse for allowing unrelated libraries to foul up each others’ namespaces, incidentally). You’d rather expect highly-trained engineers at one of the world’s largest technology companies to follow best design practices; even I get this stuff, and I’ve never had a CS lesson in my life.

[1] Of course, the AS team would no doubt say that’s not a valid use case since you should use Scripting Bridge - but then we wouldn’t be faffing with ASOC in the first place if SB wasn’t fundamentally crap. And so it goes.

Hi,

I found this topic very helpful.

As someone who is more familiar with swift then Objective-C I decided to try and do as much as possible in swift. It turns out that the only thing I couldn’t do in swift is to call loadAppleScriptObjCScripts. I’ve put this in a trivial Objective-C framework so it’s reusable across projects. In swift I now have a simple import of the framework without needing to resort to a bridging header.

Here’s the code from a project that extracts the cell values from a Numbers spreadsheet. Swift seems to have some magic that allows the NumbersDocumentWrapper protocol to automatically be adopted by AnyObject. People not so familiar with Objective-C might find it useful.

First the Script Object:

script NumbersDocumentWrapper
property parent: class "NSObject"
property doc: null

to openNumbersDoc: docPath
tell application "Numbers"
activate
set my doc to open docPath as text
end tell
end openNumbersDoc:

to cells()
tell application "Numbers"
return value of every cell of first table of first sheet of my doc
end tell
end cells

end script

And the swift.main:

[code]import Foundation
import AppleScriptLoaderOC

let documentWrapperClass = “NumbersDocumentWrapper”

@objc protocol NumbersDocWrapper {
func openNumbersDoc(docPath: String)
func cells() → [AnyObject]
}

loadAppleScriptObjCScriptsForSwift() // This is defined in the AppleScriptLoaderOC framework

if let docWrapperClass = NSClassFromString(documentWrapperClass) as? NSObject.Type {
var wrapper : AnyObject = docWrapperClass()

wrapper.openNumbersDoc(“/Users/xxxx/temp/test1.numbers”)
var test = wrapper.cells()

println(wrapper.cells())
} else {
println(“Could not find class named (documentWrapperClass)”)
}

println(“Done”)[/code]

Is this the reason in JavascriptFA ,I cannot get the following to work get:

var trackData   =   iTunes.currentTrack.artworks[0].rawData()
-->  Error: Couldn't convert <NSAppleEventDescriptor: 'tdta' ...

or

  var trackData   =   iTunes.currentTrack.artworks[0].data()
--> <>


No, that’s nothing to do with NSAppleEventDescriptor. The issue I was referring to was that Cocoa’s NSAppleEventDescriptor class lacks methods for essential tasks such as packing and unpacking dates and sending Apple events, which means you have to use either legacy/deprecated Carbon APIs (which Apple doesn’t recommend for new work) or Scripting Bridge (which is broken crap) instead. It was the reason I finally pulled the plug on the appscript project. It’s a huge pain for people who don’t like AppleScript or have simply outgrown it for general coding but still require production-quality application scripting capabilities. [1]

Your JXA example doesn’t work because JXA was written by programmers who don’t know what they’re doing. In this case, the problem is how to deal with Apple event descriptor types for which there is no native equivalent. AppleScript uses raw «data TYPExxxx...» representation; my JavaScriptOSA implementation uses raw [object <NSAppleEventDescriptor>] representation; JavaScript for Automation falls on its face. It does that a lot.

Honestly, my recommendation is just stick to using AppleScript: it’s the only supported option that actually works right. JXA’s Apple event and OSA support is crippled and broken, its library system is crap, the documentation also crap, and user support virtually non-existent. (I’ve seen exactly two JXA user questions answered by an AppleScript team member since 10.10 shipped; heck, I’ve answered more myself, and I know what a waste of time it is.) I don’t know if JXA’s Cocoa support is any good or not, but wait another year and Swift’ll be a far more compelling option for Cocoa programming anyway.

[1] I’ve recently submitted a couple of enhancement requests, with full code patches, to Radar:

rdar://problem/19169736
rdar://problem/19169791

If those get included in 10.11 then I’ll probably move AppleEventBridge from “completely unsupported” to “essential support only” status, and maybe add a --swift option to its glue generator as well. (Current ObjC glues should work, but native Swift files would be nicer.)

(Yeah, I probably should’ve done this right after 10.6 came out, but given the AppleScript team’s determination to drive the entire system into the dirt I may be running out of windmills to tilt at anyway…)

There is a 3rd option:

I think you referring to AESend() which is part of the Carbon’s high level toolbox (interaction.h). AESend() is one of those “Carbon” functions which is available in 64-bit programming sitting in a wasteland surrounded by deprecated functions. Not entirely true to get the deprecated status but it’s not recommended to use. The third option is AESendMessage() which is part of CoreServices (AE/AEMach.h). AESendMessage which is odd as well because the documentation of it can only be found in the comments of the headers files in the AE framework.

Since 10.8 I write osaxen using only CoreServices and without needing Carbon at all. Maybe underneath it’s still using Carbon but its pretty safe, or safer, to use CoreServices instead of Carbon.

Nope, AESend and AESendMessage are both part of the Carbon Apple Event Manager, which is a legacy API. Go by the published documentation, not headers which can be all over the place. (Also, AESend is ancient Sys7-era evil that no-one in their right minds should touch, and long superseded by AESendMessage.)

Patch code can be seen here, FWLIW. (See the NSAppleEventDescriptor+AEDesc[More]Extensions categories.)

The link you provided the AESendMessage was still part of “Carbon” because the complete AE.Framework was as subframework of ApplicationServices.framework. The whole documentation was put into legacy but at the same time (since 10.5) the AE.Framework was moved to the CoreServices.framework, the backbone of Cocoa, Cocoa Touch and Carbon. It’s safe to use any non deprecated function or data type from the CoreServices.framework unless otherwise noted.

If you prefer links to my sources: Here it is. Also the API Overview does talk about CarbonCore.framework deprecation but not about AE.framework deprecation of the CoreServices.framework.

I’m not saying it’s part of the Carbon umbrella or not, it’s just not deprecated.

edit: p.s. You probably already knew but AESend() is high level and needs to link against a window server and Carbon, AESendMessage is low level and does not need to.

Never said AESendMessage was deprecated, only that it’s legacy.

Other APIs such as Date and Time Utilities, File Manager, Process Manager, and the Component Manager (upon which AS and JXA are built) are deprecated, but can either be worked around or are only used in OSA language component development, not Apple event communication.

However, at least with deprecated APIs you know what their future entails and can make your plans accordingly. Whereas legacy APIs are a total grey area with no clear direction and often no clear alternative (and even less motive for Apple to pull their finger out and provide one). The only reason I’m still developing product on top of them is that, should poop meet whirly, I can ditch Apple events entirely and migrate everything to Adobe’s C++ SDKs in a year, albeit at considerable pain and cost [1].

[1] Or else go stack supermarket shelves for a living - at least you know where you are with a can of beans. :confused:

Hi,

I’m currently converting an Applescript I’ve done into Swift (because the Applescript takes a lot of time to do the data processing). I still want to use Applescript though to interact with iTunes, so I’m using StefanK’s implementation of the bridge above.

This works fine when I don’t need any parameters parsed to Applescript, however, doesn’t appear to have the capability to take values in. I have a few methods that require a parameter - one requires just a boolean value, others require a set of strings.

In Applescript examples of functions are:


    on inPlaylist(checkPlaylist)
        set playlistName to item 1 of checkPlaylist
        set persistent_id to item 2 of checkPlaylist
        --cut for brevity
    end inPlaylist

    on testSong(persistent_id)
        --cut for brevity
    end testSong

So I’d like to be able to call them in Swift using something like:


        var playlist_and_songid: [String] = ["playlist_name", "E745DA8E7BA1AABD"]
        print (asobjc.testApplescript.inPlaylist(songInPlaylist))

         asobjc.testApplescript.testSong("E745DA8E7BA1AABD")

With the following added to the .h file:


- (BOOL *)inPlaylist:(NSArray<NSString *> *)checkPlaylist;
- (BOOL *)testSong:(NSString *)persistent_id;

But these don’t seem to work. Any help on this would be appreciated - I’m unsure if this not working is because of my implementation, or if this is a limitation in the bridge.

(I should say while I am somewhat OK on both Applescript and Swift, I have a minimal working knowledge of Objective C).

Thanks.

There are two major issues:

  1. A boolean return value must be specified as NSNumber* (even correct BOOL without asterisk (*) does not work)

- (NSNumber *)inPlaylist:(NSArray<NSString *> *)checkPlaylist;
- (NSNumber *)testSong:(NSString *)checkPlaylist;

  1. On the AppleScript side a handler taking a parameter must be declared with an underscore followed be a pair of parentheses
on inPlaylist_(checkPlaylist)

or with a colon but without parentheses

on inPlaylist:checkPlaylist

and further you have to coerce Foundation types to AppleScript types (here NSString to AppleScript text)

 
on inPlaylist:checkPlaylist
	set playlistName to item 1 of checkPlaylist as text
	set persistent_id to item 2 of checkPlaylist as text
	log (playlistName)
	log (persistent_id)
	return true
end inPlaylist:

on testSong:persistent_id
	return persistent_id as text is not "E745DA8E7BA1AABD"
end testSong:

To call a method from Swift which returns a boolean you have to call boolValue explicitly

asobjc.testApplescript.inPlaylist(["playlist_name", "E745DA8E7BA1AABD"]).boolValue
asobjc.testApplescript.testSong("E745DA8E7BA1AABD").boolValue

Awesome.

I’ve tried using the above and have been able to get it all working. Thanks for your quick reply and clear advice.