Marrying JXA with ObjC

Marrying JXA with ObjC

There are a lot of things that you can do with JXA. And there are a lot also that you cannot do, for example everything for which there are not scripting functions available. You can find a very simple example in the [Basics part]https://bru6.de/jxa/docs/basics/#writing-and-reading-unicode-data which write Unicode data to a file:

1
2
3
4
5
ObjC.import('Foundation');
const filename = "/users/me/Path to Folder/My File.txt";
const str = $.NSString.alloc.initWithUTF8String('Unicode Text 广州');
str.writeToFileAtomicallyEncodingError(filename,
    true, $.NSUTF8StringEncoding, null);

Although it should be possible to write UTF-8 encoded text directly to files with JavaScript (and it is in AppleScript), the above currently seems to be the only way to get that working. It is also a good example to explain some of the concepts of the JXA-ObjC binding.

Preparing to use ObjC from JXA #

In order to use ObjC in a JXA script, you have to import the relevant framework(s). In the example above, this is the Foundation framework, and it is included with Objc.import('Foundation'). Obviously, ObjC` is a global object available automatically in every JXA script.

Another global object is $ (Apple calls this one as well as Obj sometimes “property”, which is obviously not what they are). It gives you access to all ObjC classes, their methods and properties of the frameworks that have be imported before.

To find out what a framework provides you should consult Apple’s developer documentation. For example, the documentation for foundation is available here. When you consult the documentation, make sure that the language selector on the right hand side is set to “Objective-C”. Otherwise, you’ll be reading the Swift documentation, which is not helpful in this context.

Calling ObjC methods from JXA #

How you call a method depends on the number of its parameters. If it has no parameter, you do not really call it, you just write it down as if it were a property. So, to create a new object of the class NSString, you’d write
let string = $.NSString.alloc, since the alloc method does not take an argument. DO NOT use empty parenthesis here, they’ll break everything.

And you do need to call alloc in this context, because you have to get an object, i.e. an instance of the class NSString. So the JavaScript line above just says “send the NSString class the message ‘alloc’ and store the result of this in my local variable string”.

But you’ll never use alloc without some form of init. As Apple explains, “you must use an init… method to complete the initialization process”.

Many of these init… methods have at least one parameter, so you cannot call them as if they were properties. Instead, you have to look up their parameter definitions. The method initWithUTF8String from above takes only one parameter, so you call it like a method passing this single parameter.

Stitching it all together, you end up with the line
const str = $.NSString.alloc.initWithUTF8String('Unicode Text 广州');
to build a JavaScript variable containing a UTF8 NSString.

If a method has more than one parameter, the real fun begins. So have a look at the declaration of writeToFile:atomically:encoding:error: in the documentation for NSString and compare it to the last line in the small JavaScript example above. As you can see, all the parameters are run together, the first letter of each one capitalized. Thus, writeToFile:atomically:encoding:error: in ObjC becomes writeToFileAtomicallyEncodingError in JXA-ObjC parlance. After you’ve build that monster, you can append all the parameters in parenthesis like shown before.

To summarize:
If an ObjC method

  • has no parameter, call it by simply writing it down as if it were a property. DO NOT append empty parenthesis!
  • has one parameter, call it like method(parameter).
  • has more than one parameter, look up its declaration. Uppercasing the first letter of every parameter name and removing the colons between them gives you the name of the method to call. Pass all parameters in parenthesis.

Passing parameters between JXA and ObjC #

Parameters for ObjC methods are automatically converted from JavaScript variables if possible. Thus, you can pass a string from JXA to an ObjC method expecting an NSString. However, the converse is not true: If an ObjC method returns an NSString object, you must convert it to a JavaScript string like so:

1
2
3
4
const str = $.NSString.alloc.initWithUTF8String('Unicode Text 广州');`
/* str is a `NSString` object */
const jxaString = str.js;
/* now jxaString is a JavaScript string */

Appending .js to an ObjC object converts it to a JavaScript object. Alternatively, you could call ObjC.unwrap() passing it the ObjC object as parameter.

ObjC methods returning structs #

If an ObjC method returns a struct, you access its part like the elements of a JavaScript object:

1
2
3
4
const NSrect = $.NSMakeRect(20, 40, 600, 800)
/* NSrect: 
{"origin":{"x":20,"y":40},
 "size":{"width":600,"height":800}} */

Passing references to ObjC methods #

Some ObjC methods expect a reference to an ObjC object, for example an error parameter. In that case, pass it a nil reference created like so
error = $(); After the method returns, you can access the parameter like a normal JavaScript object, for example error.code. If an ObjC method requires a reference to a basic JavaScript type like boolean or integer, pass it an object created by the Ref() method in JXA like so
let ref = Ref(); and access the value of this object as ref[0] after you called the method.