How to call a Swift function (defined in XCode) from AppleScript

I’m new here so forgive me if this has been already discussed. I couldn’t find it (so far).

I’m working on a proof of principle and right now that calls for being able to create a new window with a different title on each Mission Control Desktop. I have not been able to tell either AppleScript or Swift to move a new window to another desktop nor to create a new window anywhere other than the current desktop. Fine, I’ll change desktops and then create the window. That means being able to run the app/script/whatever from a Desktop where it isn’t already running. My idea for that is a Keyboard Maestro hotkey that will run an AppleScript that will call the createNewWindowWithTitle function in my DeskSpaceID app.

When I run My XCode app, it currently makes an app window and then makes another window using the createNewWindowWithTitle function, just a minimal window given a title. If I run the function createNewWindowWithTitle from inside the AppDelegate.swift file, it works Just Fine. I’ve added Scriptable to the Info.plist file to enable AppleScript. When I use an AppleScript call like:

tell application "DeskSpaceID"
	createNewWindowWithTitle("My New Window", 400, 200)
end tell

to call the function in my app, DeskSpaceID I get an error:

DeskSpaceID got an error: Can’t continue createNewWindowWithTitle.

I’m not sure what “Can’t continue” is trying to tell me. I think it means that it got into DeskSpaceID.app but couldn’t run the function.

Here’s my complete AppDelegate.swift file:

//
//  AppDelegate.swift
//  DeskSpaceID
//
//  Created by August Mohr on 3/13/23.
//  Copyright © 2023 August Mohr. All rights reserved.
//

import Cocoa
import AppKit

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!

    func applicationWillFinishLaunching(_ notification: Notification) {
        UserDefaults.standard.set(false, forKey: "NSFullScreenMenuItemEverywhere")
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        createNewWindowWithTitle("New Window from DidFinishLaunching", width: 450, height: 100)
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
    
    @objc func createNewWindowWithTitle(_ title: String, width: CGFloat, height: CGFloat) {
        let screenMaxY = NSScreen.main?.visibleFrame.maxY ?? 800
        let y = screenMaxY - height
        let window = NSWindow(contentRect: NSRect(x: 0, y: y,
                width: width, height: height),
                styleMask: [NSWindow.StyleMask.titled],
                backing: NSWindow.BackingStoreType.buffered,
                defer: false)
        window.title = title
        window.makeKeyAndOrderFront(nil)
    }

}

Do I need to create a onCreateNewWindowWithTitle handler in either Swift or ApplScript? Some posts in various places that seem vaguely related to doing this seem to suggest that, but I’m new enough to this whole subject that I’m having trouble sorting out the layers.

Can someone point me to where this topic is explained or give me a hint about what’s not working right in what I’m doing?

BTW, I’m running on Mojave (about to upgrade to Catalina) using XCode 11.3.1. I have the XCode project set to use Swift 5 and XIB with a target of macOS 10.14 (Mojave).

If I need to upgrade to Catalina to do what I want, that’s OK. There are some folks who have a peripheral interest in my project who are also so still stuck on Mojave, so if I can make something that will work for them, great, but it’s not a requirement.

Thanks.

Welcome to the forum! I think you might be operating under an assumption that making your app “scriptable” will automatically open up access from AppleScript to the Swift-based functions/methods in your app. This is not the case.

Usually to make an app scriptable it requires a combination of turning on that switch you mentioned, adding specifications for commands, types, etc., to a “scripting dictionary” (.sdef) file for your app, and implementing custom methods in your app to handle scripting inquiries. You can read more about the whole process of making an app scriptable in many places, but here is one good introduction: Making Your Mac App’s Data Scriptable · objc.io

1 Like

Thanks @redsweater , from a first glance, that seems to be what I was asking for.

You are correct, I did think that simply turning on that switch might be enough to expose my apps internal functions to AppleScript. I had not found anything referring to a “scripting dictionary” (.sdef) file.

I did find references to handlers, but they were mostly in AppleScript files, which is not what I need. Mostly all I’ve found has been how to run AppleScript from my app, which is not what I need either.

I’ve been skipping articles with titles like the one you suggested because I’ve not been concerned about transferring data, especially from my app because it won’t have any data except the list of window titles and maybe which windows are in the current Desktop Workspace. I just want to run the function I’ve defined. I guess I’m not going to find articles related only to my special case.

Thanks again for the pointer to this article.

I found another tutorial that covers the same ground but with a more complex app than the “Save Data in an Array and Retrieve It” example above. This one presents an example of a To-Do-List app: Making A Mac App Scriptable Tutorial.

Unfortunately, it’s written in Swift 3 so my XCode 11.3 gave me the message:

Unsupported Swift Version

The target “Scriptable Tasks” contains source code developed with Swift 3.x. This version of Xcode does not support building or migrating Swift 3.x targets.

Use Xcode 10.1 to migrate the code to Swift 4.

I note that the latest XCode 10 version was 10.3, so I wondered about that 10.1 version recommendation. No, XCode 10.3 does not support migrating Swift 3 code to Swift 4 and gives the error message when opening the project. XCode 10.1 is available at: More Downloads (Apple sign in required) and will run the project just fine.

I hope this helps someone looking for the same info that I am.

I found a much better tutorial that is relatively up to date (no Swift 3 sample projects) and doesn’t attempt to teach Xcode or AppleScript along the way.

Boilerplate to Add AppleScript to Your macOS App in 2020

He recommends redacting the CocoaStandard.sdef file available at
/System/Library/ScriptingDefinitions/CocoaStandard.sdef and replaces the line

<dictionary title="Standard Terminology">

with the line

<dictionary xmlns:xi="http://www.w3.org/2003/XInclude" title="YOURAPP NAME Terminology">
1 Like

I had some email correspondence with him where he explained that this change was unnecessary unless you are using xi: elements, which I am not. So that means I can start with the CocoaStandard.sdef file as is.

I’m stuck on something conceptual about how SDEF files are supposed to work.

I have gotten an SDEF file created from CocoaStandard.sdef and redacted down to a set of commands that make sense in the app that I am trying to create. One of the commands I didn’t remove was quit. At this point I can used AppleScript to quit the app when it is running. I can also launch the app from AppleScript, so there is at least one AS command that it inherits without my having to define it in the SDEF file.

I have a function in AppDelegate.swift called createNewWindowWithTitle as shown up in post #2 above. When I call that function within applicationDidFinishLaunching, it works fine.

But what do I put in the SDEF file to be able to call it from AppleScript? Do I create an entirely new function within SDEF or can I call the existing function? In either case, how? I’ve been poring over what doc I can find and all the examples are either too simplistic to get close or too complicated to sort out and everyone writing seems to make the assumption that I just want one window for my app and that I want to interact with that one window.

I want to make 16 windows in my app, or 22, or even 40, and I don’t want to interact with them, I just want to query the name/title of which on is open in the current Desktop Workspace and I want to give the name of one in some other Desktop Workspace and activate it so that my activity moves to that Deskspace. When the app is foremost, I want to click the Windows menu and see a list of all the window titles.

There are various ways to do those name-based things, but first I have to create them. Right now my strategy is to step through from one Desktop Workspace to the next and run the app to create a window and get the title/name from some master list someplace.

That’s what I’m stuck on: run the app from AppleScript to make a new window in the current Desktop Workspace. I have a function that makes a window. What do I put in the SDEF file, a copy of that function or a call to that function?

Thanks

Expanding on the above…

I’m missing key concepts on how to call that function from whatever it is that the SDEF entry can trigger. SDEF is in XML. Is there a field where I enter the function name to call when accessed by AppleScript? Is there a field to enter the Swift code (in the function definition above) to execute? Or do I need an observer in AppDelegate.swift that somehow watches for when the entry in SDEF gets activated? Or what?

All the tutorials that I’ve found so far that get anywhere near this all focus on how to use AppleScript to automate interactions with the app’s primary window. But my windows don’t have any interactions and none of the windows are primary. They are all placeholders and that’s all they ever need to be.

Can any of the app-creation-savvy developer types here offer me suggestions of where to look for documentation or tutorials? Can you make enough sense out of what I don’t know how to ask about to be able to point me to the right terminology? Just finding out about SDEF files gave me a keyword to search on. What’s next?

Thanks

for my modest requirements, I just subclass NSScriptCommand. the subclasses call methods from app delegate. I use Sdef Editor to create/edit the .sdef file.

if you google NSScriptCommand you’ll find plenty of info on it. but, once again, for my scriptability needs I only need 2-3 simple commands with a few arguments. it may or may not suit your requirements.

Thanks. I suspect this may be enough for me too. I have a method defined in AppDelegate.swift that does what I want to do, I just haven’t been able to figure out how to reference it inside the call or make or new command in the .sdef file.

The two verbs you use to describe what you do (subclass, call) sound like they are probably exactly what I need to do, but never having done this part before, there’s no “just” about it, for me.

Could you show me a working example? Do you have to do anything to the method in AppDelegate.swift to make it callable?

Thanks!

I’m, obviously, using Objective-C - but I guess the principle should be the same in Swift:

-Create a subclass of NSScriptCommand

Then do the following (not in that particular order, but that’s what should be done):

-Implement performDefaultImplementation which will be calling your app delegate method.

-Depending on your needs you may or may not need to call self.commandDescription.commandName and/or evaluatedArguments

-Add your command in Sdef Editor, enter your subclass name there and whatever else is needed to be specified.

Your method in app delegate should be just a regular method that can be called from any normal class.

You can find examples here:

Thanks @leo_r,

That gives me more terminology to search for. I have not yet wrapped my head around what performDefaultImplementation is supposed to do or why it is named that but googling it gave me some links to StackOverflow that look like they might be helpful.