Swift Scripting (Part 4) – The Exciting Conclusion

Previously, we’ve covered the groundwork necessary to use Swift to control scriptable applications. (See parts one, two, and three.) Now we get to the payoff. We’ll have a look at a sample script that leverages the scripting interfaces of the Finder and the Image Events helper application.

Preliminaries

To run the sample script on your own system, you’ll need to build and install the ScriptingUtilities, FinderScripting, and ImageEventsScripting frameworks. If you don’t want to create all of these from scratch, you can find ready-to-build Xcode projects in the SwiftScripting project on GitHub. Whichever path you choose to build the frameworks, be sure to install them in /Library/Frameworks.

Shrinking Images

The sample script operates on the current selection in the Finder. The result of running the script will be the creation of half-scale versions of any images that were selected. Any non-image in the Finder selection will be ignored. The “test” for images will solely rely on file extension, so we’re not talking about a fool-proof form of input verification.

Here’s the script

#!/usr/bin/swift -F /Library/Frameworks

// Image Events sample script.
// Operates on selected images in the Finder
// Outputs half-scale images in same folder as originals

import Foundation
import FinderScripting
import ImageEventsScripting
import ScriptingUtilities

let imageExtensions = ["png", "jpg", "jpeg", "tif", "tiff", "gif"]

let finder = application(name: "Finder") as! FinderApplication
let imageEvents = application(name: "Image Events") as! ImageEventsApplication

for item in (finder.selection!.get() as! NSArray) {
    if let file = item as? FinderFile {
        let fileExtension = file.nameExtension!.lowercaseString
        if imageExtensions.contains(fileExtension) {
            let fileURL = NSURL(string: file.URL!)
            let outputDirectory = fileURL!.URLByDeletingLastPathComponent!
            let outputFilename = "\((file.name! as NSString).stringByDeletingPathExtension) (half-scale).\(fileExtension)"
            let outputURL = outputDirectory.URLByAppendingPathComponent(outputFilename)
            
            let image = imageEvents.open!(fileURL!) as! ImageEventsImage
            image.scaleByFactor!(0.5, toSize: 0)
            
            image.closeSaving!(.Yes, savingIn: outputURL.path!)
        }
    }
}

The code is straight forward. On lines 14 and 15, the application function supplied by the ScriptingUtilities framework is used for convenience when assigning the finder and imageEvents constants. The work of the script occurs inside the for loop and we’re able to take full advantage of Foundation classes along side the scripting API of the Finder and Image Events. Go ahead and enter the above script into a text file and give it the name Scale Image by Half.swift. Remember to make the script executable using the chmod

chmod +x 'Scale Image by Half.swift'

Script Menu

While you could run the Scale Image by Half.swift script from anywhere on the system, it would be awkward to select some images in the Finder and then bring up a Terminal window to invoke the script. A better solution is to use the system-wide Script Menu to host your bits of automation. You enable the Script Menu via the Preferences of the Script Editor application.

Script Editor Preferences

With the Script Menu enabled, you now have a place to put all of your scripts and keep them organized by application. I like to configure the Script Menu such that the scripts for the frontmost application are displayed at the top of the menu. The screenshot below shows the Script Menu as it appears on my system with the Finder frontmost.

Script Menu with Finder Frontmost

If you select the Open Finder Scripts Folder option from the Script Menu, a Finder window will be opened at the appropriate location. This is where you want to put the Scale Image by Half.swift script. Once the script is placed in this location, it will be available as a menu option when the Finder is frontmost. To exercise the script, select one or more images in the Finder and then choose the Scale Image by Half.swift option from the Script Menu. If all goes well, you should see half-scale images appear alongside the originals.

Happy Scripting!

Swift Scripting (Part 2)

In part one of this series, we covered the basics of the Scripting Bridge and how one might leverage this technology with Swift. We left off with some potential problem areas. Specifically, we saw that the default approach for leveraging the sdp-generated Objective-C interface could lead to ambiguity on the Swift side of things.

Let’s now take a closer look at an example of such an ambiguity. The excerpt below is from the sdp-generated header file for the Acorn image editor. Incidentally, Acorn is an excellent product and if you’re searching for an image editor you should check it out!

@interface AcornApplication : SBApplication
// The name of the application.
@property (copy, readonly) NSString *name;

// The version number of the application.
@property (copy, readonly) NSString *version;  

 // Have Acorn taunt you.
- (NSString *) taunt; 
@end

Here’s some sample code that demonstrates how we might access the version property of the application.

import Foundation
import ScriptingBridge

let acorn: AnyObject = SBApplication(bundleIdentifier: "com.flyingmeat.Acorn4")!
print(acorn.version)

The above code will not compile. The problem is that the acorn variable must be declared to be of type AnyObject. Since there are multiple, conflicting definitions of version declared in different classes, the compiler cannot resolve the version property uniquely. Here we can resort to using valueForKey("version") to work around this issue. In other cases, where we want to invoke a method as opposed to simply retrieving the value of a property, this ambiguity can force awkward method invocations such as

object.someMethod() as Void

The above construct would be necessary if there were multiple someMethod implementations defined with different return types, where the desired invocation returns Void. As mentioned in part one, we can avoid the ambiguity entirely by defining one or more Swift protocols that cover the Objective-C API. For the subset of the Acorn API shown above, an equivalent set of Swift protocol looks like this

import AppKit
import ScriptingBridge

@objc public protocol SBObjectProtocol: NSObjectProtocol {
    func get() -> AnyObject!
}

@objc public protocol SBApplicationProtocol: SBObjectProtocol {
    func activate()
    var delegate: SBApplicationDelegate! { get set }
}

@objc public protocol AcornApplication: SBApplicationProtocol {
    // The name of the application.
    optional var name: String { get } 

    // The version number of the application.
    optional var version: String { get } 

    // Have Acorn taunt you.
    optional func taunt() -> String 
}
extension SBApplication: AcornApplication {}

Here we’ve defined two base protocols to represent the commonly used functionality of the SBObject and SBApplication classes. The AcornApplication protocol extends the SBApplication protocol and includes the version property. Note that all properties and methods in the AcornApplication protocol are declared as optional. Also note that the extension on SBApplication indicating that it conforms the AcornApplication has an empty implementation.

With these protocols defined, we can alter the code to use explicit typing and access the version property directly.

import Foundation
import ScriptingBridge

@objc public protocol SBObjectProtocol: NSObjectProtocol {...}
@objc public protocol SBApplicationProtocol: SBObjectProtocol {...}
@objc public protocol AcornApplication: SBApplicationProtocol {...}
extension SBApplication: AcornApplication {}

let acorn = SBApplication(bundleIdentifier: "com.flyingmeat.Acorn4") as! AcornApplication
println(acorn.version!)

Here we have explicitly-typed the acorn variable and we have direct, unambiguous access to the version property. A downside of this approach is that we need to deal with the optional nature of the properties and methods in the protocol. Here we’ve done that by using the ! when dereferencing the version property.

In part three of this series, we’ll look at how the generation of the necessary Swift protocols can be automated and how the resulting code can be packaged up as a Framework to support reuse in a variety of scripts.