How can I do some trivial AppleScript from a Swift app?

I've got a Swift app that looks at a timeline of occurences. I'd like to be able pop up the Calendar.app, focused on the date a user has selected within the app.


Nothing fancy -- no data comes back from Calendar...


Using ScriptingBridge, I could imagine just running [on the CalendarApplication that is returned from let calendarApp = SBApplication(url: url)]

the method:


- (void) viewCalendarAtDate:(NSDate *)at;


or perhaps passing a string to the following script:


on showDateInCalendar(s as string)

tell application "Calendar"

view calendar at date s

end tell

end showDateInCalendar


There seem to be lebenty different ways to approach this problem, but they all seem documented in the style of you-have-to-have-understood-this-once-before-you-can-understand-it-again.


Any suggestions, or am I doomed to the command line and Python scripts?


Richard

Accepted Reply

You’re right that there’s a bunch of different ways to do this. The two that I recommend are:

  • NSUserScriptTask
    , if you want to make a general script attachment mechanism
  • NSAppleScript
    , if you want lots of control

The former has the advantage that it’s compatible with the App Sandbox.

Implementing the latter is a bit tricky if you’re not deeply familiar with the whole history of Apple events and AppleScript. Here’s some snippets to get you started.

First, here’s how to construct an

NSAppleScript
from source.
var script: NSAppleScript = {
    let script = NSAppleScript(source: """
        on displayMessage(message)
            tell application "Finder"
                activate
                display dialog message buttons {"OK"} default button "OK"
            end tell
        end displayMessage
        """
    )!
    let success = script.compileAndReturnError(nil)
    assert(success)
    return script
}()

Note that I’m storing this in a property. I do this because compiling scripts is relatively expensive, so it’s best to do it once and cache the results.

Here’s how how run that the

displayMessage
handler in that script, passing it a parameter.
let parameters = NSAppleEventDescriptor.list()
parameters.insert(NSAppleEventDescriptor(string: "Hello Cruel World!"), at: 0)

let event = NSAppleEventDescriptor(
    eventClass: AEEventClass(kASAppleScriptSuite),
    eventID: AEEventID(kASSubroutineEvent),
    targetDescriptor: nil,
    returnID: AEReturnID(kAutoGenerateReturnID),
    transactionID: AETransactionID(kAnyTransactionID)
)
event.setDescriptor(NSAppleEventDescriptor(string: "displayMessage"), forKeyword: AEKeyword(keyASSubroutineName))
event.setDescriptor(parameters, forKeyword: AEKeyword(keyDirectObject))

var error: NSDictionary? = nil
let result = self.script.executeAppleEvent(event, error: &error) as NSAppleEventDescriptor?

There’s some tricky things to note here:

  • In line 2 I pass 0 to the

    at
    parameter, which indicates that the value should be added to the end of the list.
  • There are lots of type conversions (lines 5, 6, 8 and 9) because Swift is importing the constants with the wrong type (because they’re anonymous enums in the Objective-C header).

  • In line 15 I cast the result of

    executeAppleEvent(_:error:)
    to
    NSAppleEventDescriptor?
    because of an annotation bug in the Objective-C header. The method is documented to return
    nil
    on error but the header doesn’t indicate that (r. 38702068).

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

You’re right that there’s a bunch of different ways to do this. The two that I recommend are:

  • NSUserScriptTask
    , if you want to make a general script attachment mechanism
  • NSAppleScript
    , if you want lots of control

The former has the advantage that it’s compatible with the App Sandbox.

Implementing the latter is a bit tricky if you’re not deeply familiar with the whole history of Apple events and AppleScript. Here’s some snippets to get you started.

First, here’s how to construct an

NSAppleScript
from source.
var script: NSAppleScript = {
    let script = NSAppleScript(source: """
        on displayMessage(message)
            tell application "Finder"
                activate
                display dialog message buttons {"OK"} default button "OK"
            end tell
        end displayMessage
        """
    )!
    let success = script.compileAndReturnError(nil)
    assert(success)
    return script
}()

Note that I’m storing this in a property. I do this because compiling scripts is relatively expensive, so it’s best to do it once and cache the results.

Here’s how how run that the

displayMessage
handler in that script, passing it a parameter.
let parameters = NSAppleEventDescriptor.list()
parameters.insert(NSAppleEventDescriptor(string: "Hello Cruel World!"), at: 0)

let event = NSAppleEventDescriptor(
    eventClass: AEEventClass(kASAppleScriptSuite),
    eventID: AEEventID(kASSubroutineEvent),
    targetDescriptor: nil,
    returnID: AEReturnID(kAutoGenerateReturnID),
    transactionID: AETransactionID(kAnyTransactionID)
)
event.setDescriptor(NSAppleEventDescriptor(string: "displayMessage"), forKeyword: AEKeyword(keyASSubroutineName))
event.setDescriptor(parameters, forKeyword: AEKeyword(keyDirectObject))

var error: NSDictionary? = nil
let result = self.script.executeAppleEvent(event, error: &error) as NSAppleEventDescriptor?

There’s some tricky things to note here:

  • In line 2 I pass 0 to the

    at
    parameter, which indicates that the value should be added to the end of the list.
  • There are lots of type conversions (lines 5, 6, 8 and 9) because Swift is importing the constants with the wrong type (because they’re anonymous enums in the Objective-C header).

  • In line 15 I cast the result of

    executeAppleEvent(_:error:)
    to
    NSAppleEventDescriptor?
    because of an annotation bug in the Objective-C header. The method is documented to return
    nil
    on error but the header doesn’t indicate that (r. 38702068).

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Quinn,


Thanks for the code -- it looks like what I need.


I tried to test it, and ran afoul of "Use of unresolved identifier ..." for

  • kASAppleScriptSuite
  • kASSubroutineEvent
  • keyASSubroutineName


Any clues on how to get the correct constants?


Richard

Having to call into AppleScript to control other apps from Swift is a right PITA, but there aren’t any native Swift-AE bridges I can recommend (ScriptingBridge is incompetent, and I’ve abandoned SwiftAutomation as the whole AE/OSA/AS stack’s dying anyway.)


NSAppleScript/NSUserAppleScriptTask/OSAKit are a huge chore, and osascript is only good for very simple tasks. Your best option is to use AppleScriptObjC if possible, as that handles all the ObjC/Swift↔︎AS bridging for you, allowing you to call your AS handlers much as if they were native Cocoa methods. Here’s a nice example using modern Swift:


https: //medium.com/@an23lm/applescript-and-swift-meant-to-be-ehhh-cbe03883a59


(One caution: when defining AS handler protocols, always bridge AS boolean/integer/real to NSNumber. The ASOC bridge only recognizes Cocoa classes, not [Obj-]C primitives.)

Any clues on how to get the correct constants?

Oh, yeah, I should’ve mentioned that because it’s super non-obvious. These constants are declared in

ASRegistry.h
, which is part of the OpenScripting framework, which is part of the Carbon framework (!), so to access them you need to add:
import Carbon

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

The examples on medium.com look very useful, but a bit overkill for my simple needs (and limited wetware memory capacity). Thanks, too, for the comparison comments; they'll help if I have to reapproach this problem in a more complex situation.

Richard

Quinn,


Works fine (after I turned off the Sandbox -- that caused Applescript to claim that "Finder" wasn't running).


Thanks again,


Richard

I tried the above ways but they didnt work for me. Here is the code that worked for me.

do {
      let filePath = "/Users/username/Downloads/switchUser.scpt"
      let task = try NSUserAppleScriptTask(url: URL(fileURLWithPath: filePath))
      task.execute()
    }
    catch {
      print("some error")
    }