30

I have a simple AppleScript that sends an email. How can I call it from within a Swift application?

(I wasn't able to find the answer via Google.)

zekel
  • 9,227
  • 10
  • 65
  • 96
codingguy3000
  • 2,695
  • 15
  • 46
  • 74

7 Answers7

72

As Kamaros suggests, you can call NSApplescript directly without having to launch a separate process via NSTask (as CRGreen suggests.)

Swift Code

let myAppleScript = "..."
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: myAppleScript) {
    if let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(
                                                                       &error) {
        print(output.stringValue)
    } else if (error != nil) {
        print("error: \(error)")
    }
}
Community
  • 1
  • 1
zekel
  • 9,227
  • 10
  • 65
  • 96
  • 1
    Is it possible to add in parameters from swift? – Ousmane Traore May 04 '16 at 11:26
  • 4
    @OusmaneTraore Yes it is, just use __String Interpolation__ when you define `myAppleScript`. For example, if you want to use the width of the view inside of AppleScript, you would do `let myAppleScript = "set view_width to \(view.width)"` – kabiroberai May 14 '16 at 08:50
  • Is there a way to add parameters without string interpolation? I want to provide a defined parameter and then run any arbitrary AppleScript that conforms to that API. – pkamb Sep 25 '18 at 17:20
  • @zekel getting "Not authorized to send Apple events to Microsoft Excel." while trying your code. – meMadhav Jun 24 '20 at 09:47
  • Worked perfectly thank you! The first time I tried to access an application without the Apple Script I was asked to grant permission but otherwise no problems. – Patrick Mar 23 '22 at 07:50
  • is it possible that I can print the Replies log with swift? Or printing out Events log, Messages log? I mean when you run code in Script Editor app, you get 3 different kinds of log (Messages, Events, Replies), I want to print out all of those logs, is it possible? – Zui Zui Aug 06 '23 at 11:19
35

Tested: one can do something like this (arbitrary script path added):

import Foundation
 
let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = ["~/Desktop/testscript.scpt"]
 
task.launch()
CRGreen
  • 3,406
  • 1
  • 14
  • 24
  • This worked for me after disabling "App Sandbox" in Xcode. – meMadhav Jun 24 '20 at 09:54
  • 2
    @meMadhav As with all sandboxed apps, you need permission from the user to read their files. If you want to read a file on the desktop, you can use the pre-defined "read/write" permission for Desktop. If you want an arbitrary folder, you should have the user select it from the open dialog if it's a GUI app, or if it's a CLI app, have them send it as an argument on the command line. – Ky - Jun 30 '20 at 14:27
  • 1
    For me it only worked with absolute path to the script. – sangress Feb 08 '21 at 07:46
  • How could I send arguments? – Mamad Farrahi May 26 '22 at 18:06
6

For anyone who is getting the warning below for Swift 4, for the line while creating an NSAppleEventDescriptor from zekel's answer

Non-optional expression of type 'NSAppleEventDescriptor' used in a check for optionals

You can get rid of it with this edited short version:

let myAppleScript = "..."
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: myAppleScript) {
    if let outputString = scriptObject.executeAndReturnError(&error).stringValue {
        print(outputString)
    } else if (error != nil) {
        print("error: ", error!)
    }
}

However, you may have also realized; with this method, system logs this message to console everytime you run the script:

AppleEvents: received mach msg which wasn't complex type as expected in getMemoryReference.

Apparently it is a declared bug by an Apple staff developer, and is said to be 'just' a harmless log spam and is scheduled to be removed on future OS updates, as you can see in this very long apple developer forum post and SO question below:

AppleEvents: received mach msg which wasn't complex type as expected in getMemoryReference

Thanks Apple, for those bazillions of junk console logs thrown around.

pkamb
  • 33,281
  • 23
  • 160
  • 191
m41w4r3.exe
  • 393
  • 2
  • 15
  • Didn't work for me. I was trying to run a simple script with `say "Hello", but I got an error `error: { NSAppleScriptErrorBriefMessage = "A \U201c/\U201d can\U2019t go here."; NSAppleScriptErrorMessage = "A \U201c/\U201d can\U2019t go here."; NSAppleScriptErrorNumber = "-2740"; NSAppleScriptErrorRange = "NSRange: {0, 1}"; }` – user3579815 Apr 05 '21 at 03:06
  • Thank you! But is it possible that I can print the Replies log with Swift? Or printing out Events log, Messages log? I mean when you run code in Script Editor app, you get 3 different kinds of log (Messages, Events, Replies), I want to print out all of those logs, is it possible? – Zui Zui Aug 06 '23 at 11:20
4

You can try NSAppleScript, from Apple's Technical Note TN2084 Using AppleScript Scripts in Cocoa Applications https://developer.apple.com/library/mac/technotes/tn2084/_index.html

NSAppleScript* scriptObject = [[NSAppleScript alloc] initWithSource:
            @"\
            set app_path to path to me\n\
            tell application \"System Events\"\n\
            if \"AddLoginItem\" is not in (name of every login item) then\n\
            make login item at end with properties {hidden:false, path:app_path}\n\
            end if\n\
            end tell"];

returnDescriptor = [scriptObject executeAndReturnError: &errorDict];
Yongxu Ren
  • 111
  • 6
4

I struggled few hours, but nothing worked. Finally I managed to run AppleScript through shell:

let proc = Process()
proc.launchPath = "/usr/bin/env"
proc.arguments = ["/usr/bin/osascript", "scriptPath"]
proc.launch()

Dunno is this the best way to do it, but at least it works.

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
3

As of March 2018, I think the strongest answer on this thread is still the accepted answer from 2011. The implementations that involved using NSAppleScript or OSAScript suffered the drawbacks having some minor, but highly unpleasant, memory leaks without really providing any additional benefits. Anyone struggling with getting that answer to execute properly (in Swift 4) may want to try this:

let manager = FileManager()
// Note that this assumes your .scpt file is located somewhere in the Documents directory
let script: URL? = try? manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
if let scriptPath = script?.appendingPathComponent("/path/to/scriptName").appendingPathExtension("scpt").path {
    let process = Process()
    if process.isRunning == false {
        let pipe = Pipe()
        process.launchPath = "/usr/bin/osascript"
        process.arguments = [scriptPath]
        process.standardError = pipe
        process.launch()
    }
}
Ben
  • 216
  • 3
  • 4
0

Update

The simple, accepted answer from 2011 has gotten more complex. Apple has deprecated the launch() function as of as of 10.14, suggesting that we "use run() instead". Unfortunately, it's not a direct replacement. Fortunately, most of the original answer still holds, with a simple change.

Instead of the original line:

task.launch()

when you use run() you have to use it with try to catch possible errors. The line becomes:

try task.run()

The accepted answer above then becomes:

let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = ["~/Desktop/testscript.scpt"]
 
try task.run()

However, this only works if the code is at the top level, not if it is inside a function. run() throws errors while launch() does not. If you use try inside a function, the function also has to be declared to throw errors. Or you can handle possible errors from run() inside the function.

This page from "Hacking With Swift" has examples of how to catch STDOUT and STDERR from the process.

I hope this helps. I'm very new at Swift and this is what worked for me.

August
  • 343
  • 3
  • 10