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:
- Start with a Swift project
- Add ApplescriptObjc Framework
- Add Objective-C helper class to load ApplescriptObjc scripts form bundle
- Add Applescript Script Object in source
- Create an Objective-C protocol that exposes the Applescript Script Objects properties and methods to Objective-C
- Add a method to the helper class to create a class from string, then initialize and instance.
- Let Xcode create a bridge file for both the prototocl and the helper class.
- Call the helper class from Swift to load Applescript files from bundle
- Call the helper class from Swift to instantiate an instance of the Applescript Script Object in Swift
- 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)
- Build Settings
- 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
- Save
[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.