Low-level APIs

// TO DO: leave this as ObjC for now; can always translate to Swift later if needed

Introduction

AppleEventBridge's lower-level AEM classes provides a object-oriented wrapper around the low-level Apple Event Manager and NSAppleEventDescriptor APIs. It provides the following services:

AEM provides a direct foundation for the high-level AppleEventBridge package. The AEM API can also be used directly by developers and end-users for controlling scriptable applications in situations where AppleEventBridge is unavailable or unsuitable. Some classes (e.g. AEMCodecs) may also be used when working with OSA-related classes such as NSAppleScript.

Note that this documentation is an API reference, not a full user guide. Some familiarity with Apple events and the Apple Event Manager is required in order to understand and use `AEM` classes.

API overview

The major AEM classes are as follows:

The following macros are exported for use in constructing application references:

[TO DO: AEMCustomRoot]

All AEM specifiers are constructed from these base objects using chained property/method calls.

Packing and unpacking data

The AEMCodecs class provides methods for converting Cocoa objects to NSAppleEventDescriptor instances, and vice-versa. See AEMCodecs.h for API documentation.

When using AEM to send events to other applications, clients don't normally need to work directly with this class; AEMApplication will automatically create an instance of AEMCodecs to be used by default.

AEMCodecs can be subclassed to modify the default packing and/or unpacking behaviour if necessary. For example, if dealing with a legacy application that requires text values to be supplied as typeChar instead of typeUnicodeText descriptors, the following subclass will modify the default packing behaviour to suit:

  @interface StringCodecs : AEMCodecs
  @end

  @implementation StringCodecs

  /* Pack strings as typeChar descriptors instead of typeUnicodeText */
  - (NSAppleEventDescriptor *)pack:(id)anObject {
      if ([anObject isKindOfClass: [NSString class]])
          return [[NSAppleEventDescriptor descriptorWithString: anObject] 
                  coerceToDescriptorType: typeChar];
      else
          return [super pack:anObject];
  }

  @end

Instances of this custom subclass can be passed to AEMApplication's -eventWithEventClass:eventID:returnID:codecs: and -eventWithEventClass:eventID:codecs: methods to be used when adding attributes and parameters that event and unpacking its reply.

[TO DO: note about custom classes, with reference to chapter]

Building queries

About object specifiers

An object specifier (also known in AppleScript as a "reference") is a simple first-class query, constructed as a linked list of one or more Apple event descriptors of [primarily] typeObjectSpecifier. Object specifiers are used to identify properties and elements in the application's AEOM. Each object specifer contains four fields:

The Apple Event Manager provides several ways to construct object specifiers and assemble them into a complete reference, but these are all rather verbose and low-level. AppleEventBridge's AEM layer hides all these details behind an object-oriented wrapper that uses chained property and method calls to gather the data needed to create object specifiers and assemble them into linked lists.

For example, consider the AppleScript reference text of document 1. The code for constructing this object specifier using NSAppleEventDescriptor would be:

  NSAppleEventDescriptor *ref0, *ref1, *ref2;

  // Application root:
  ref0 = [NSAppleEventDescriptor nullDescriptor];

  // Pack 'document 1' element specifier:
  ref1 = [[NSAppleEventDescriptor recordDescriptor] coerceToDescriptorType: 'obj '];
  [ref1 setDescriptor: [NSAppleEventDescriptor descriptorWithTypeCode: 'docu'] forKeyword: 'want'];
  [ref1 setDescriptor: [NSAppleEventDescriptor descriptorWithEnumCode: 'indx'] forKeyword: 'form'];
  [ref1 setDescriptor: [NSAppleEventDescriptor descriptorWithInt32: 1] forKeyword: 'seld'];
  [ref1 setDescriptor: ref0 forKeyword: 'from'];

  // Pack 'text' property specifier:
  ref2 = [[NSAppleEventDescriptor recordDescriptor] coerceToDescriptorType: 'obj ';
  [ref2 setDescriptor: [NSAppleEventDescriptor descriptorWithTypeCode: 'prop'] forKeyword: 'want'];
  [ref2 setDescriptor: [NSAppleEventDescriptor descriptorWithEnumCode: 'prop'] forKeyword: 'form'];
  [ref2 setDescriptor: [NSAppleEventDescriptor descriptorWithTypeCode: 'ctxt'] forKeyword: 'seld'];
  [ref2 setDescriptor: ref1 forKeyword: 'from'];

  NSLog(@"ref2 = %@", ref2);

This code works by creating an empty record descriptor (typeAERecord), coercing it to the required type (typeObjectSpecifier), then adding the appropriate properties. Each object specifier descriptor is nested within the next to form a linked list of object specifier records; the last (innermost) descriptor indicates the reference's root object in the AEOM (in this case, the application object, which is represented by a null descriptor).

Now, compare the above with the AEM equivalent:

  id ref = [[[AEMApp elements: 'docu'] at: 1] property: 'ctxt'];

As you can see, AEM still uses low-level four-character codes to identify the text property and document class, but is otherwise a high-level object-oriented API. Once again, each reference begins with a root object, in this case AEMApp. New AEM specifiers are constructed by method calls; each call returning a new specifier object whose own methods can be called, and so on. This allows clients to build up a chain of AEM specifier objects that AEM can later pack into Apple event descriptors for sending to applications.

One more thing to notice: in AEM, specifying a class of elements and indicating which of those elements should be selected are performed by separate method calls, although the information provided will eventually be packed into a single descriptor of typeObjectSpecifier. This two-step approach makes it easier to integrate AEM with the higher-level AppleEventBridge bridge, which also uses two calls to construct element specifiers (one to specify the element class, e.g. -document, and another to specify the selection, e.g. -at: 1).

Note that [AEMApp elements: 'docu'] is itself a valid reference, identifying all the document elements of the application class. You do not have to call an explicit all selector (indeed, none is provided) as AEM automatically handles the details for you. AEM even allows for some convenient shorthand, e.g. writing:

  [[AEMApp elements: 'docu'] byTest: ...].first

is equivalent to writing:

  [[[AEMApp elements: 'docu'] byTest: ...] elements: 'docu'].first

This allows clients to specify the first document that matches the given condition without having to specify the element class a second time. In AppleScript, the equivalent to this is:

  first document whose ...

which is short for:

  first document of (documents whose ...)

Reference forms

AEM defines a number of classes representing each of the AEOM reference forms (see AEMQuery.h, AEMSpecifier.h, and AEMTestClause.h). There are nine AEOM reference forms, each represented by a different AEMSpecifier subclass:

The following diagram shows the AEM reference class hierarchy (slightly simplified for legibility):

AEM query class hierarchy

Clients shouldn't instantiate these classes directly; instead, AEM will instantiate them as appropriate when the client calls the methods of other AEM query objects, starting with the AEMApp, AEMCon and AEMIts objects that form the root of all AEM queries.

In fact, it isn't really necessary to remember the class hierarchy at all, only to know which concrete classes (shown in bold on the above diagram) support which methods. All public methods are inherited from just three abstract superclasses: AEMObjectSpecifier, AEMMultipleElementsSpecifier, and AEMTestClause (highlighted above). The following sections list these methods for reference.

AEMObjectSpecifier methods

The abstract AEMObjectSpecifier class implements behaviors supported by all object specifiers.

AEMMultipleElementsSpecifier methods

The abstract AEMMultipleElementsSpecifier class extends AEMObjectSpecifier with additional behaviors appropriate to object specifiers that identify multiple elements.

AEMTestClause methods

The abstract AEMTestClause class implements Boolean logic tests applicable to all test specifiers:

  - (AEMANDTest *)AND:(id)remainingOperands;
  - (AEMORTest  *)OR:(id)remainingOperands;
  - (AEMNOTTest *)NOT;

(The -AND: and -OR: methods' remainingOperands argument may be either a single AEMTestClause instance or an NSArray of AEMTest instances.)

Creating application objects

An AEMApplication instance represents an application to which Apple events will be sent. AEMApplication instances may be initialized using the application's name or full path (the .app suffix is optional), local file:// or remote eppc:// URL, bundle ID, Unix process ID, or an existing Apple event address descriptor:

  - (instancetype)initWithName:(NSString *)name
                 launchOptions:(NSWorkspaceLaunchOptions)options
                         error:(NSError * __autoreleasing *)error;

  - (instancetype)initWithName:(NSString *)name;

  - (instancetype)initWithURL:(NSURL *)url
                launchOptions:(NSWorkspaceLaunchOptions)options
                        error:(NSError * __autoreleasing *)error;

  - (instancetype)initWithURL:(NSURL *)url;

  - (instancetype)initWithBundleID:(NSString *)bundleID
                     launchOptions:(NSWorkspaceLaunchOptions)options
                             error:(NSError * __autoreleasing *)error;

  - (instancetype)initWithBundleID:(NSString *)bundleID;

  - (instancetype)initWithProcessID:(pid_t)pid;

  - (instancetype)initWithDescriptor:(NSAppleEventDescriptor *)desc;

Alternatively, to target the current (i.e. host) process:

  - (instancetype)init;

[TO DO: alternatively, should AEMApplication implement -initCurrentApplication for consistency with high-level glue APIs, and just stub out -init to return nil?]

Applications identified by name/path, file:// URL, or bundle ID will be launched automatically if not already running. You can use the options argument to customize the launching behavior (e.g. to hide the process upon launch); see the AppKit documentation for NSWorkspaceLaunchOptions for details. Once running, AEMApplication identifies the target application by its process ID for reliability. If the application cannot be launched (e.g. it can't be found) then the initializer returns nil; if the error argument is not nil then an NSError containing additional error information is also returned.

Applications identified by eppc:// URL, process ID, or AEAddressDesc are not launched automatically, so must be running before the AEMApplication instance is used, or an error will occur when an Apple event is sent.

[TO DO: what utility methods to document? note: transaction methods aren't currently documented as no apps seem to use those nowadays]

Sending Apple events

Sending an Apple event is a four-step process:

  1. Create a new AppleEvent descriptor.

  2. Add any parameters and/or attributes to the descriptor.

  3. Send the Apple event to the target process.

  4. Extract the return value or error information from the reply event (if any).

The AEM APIs streamline this process as follows:

  1. Once an AEMApplication instance is created for the target process, send it the following message to create a new AppleEvent descriptor:

    - (id)eventWithEventClass:(AEEventClass)eventClass eventID:(AEEventID)eventID;
    

    The eventClass and eventID arguments are four-char codes representing the event handler's "name", the same as in AECreateAppleEvent/+[NSAppleEventDescriptor appleEventWithEventClass:eventID:targetDescriptor:returnID:transactionID].

    This method normally returns a new AEMEvent instance representing a new AppleEvent descriptor targeted at this application.

    [TO DO: note that the above method is a shortcut? additional methods are available for customizing return event ID or codecs object, but the former is only used when sending events asynchronously (generally not done nowadays) and the latter is only relevant when implementing high-level bridges on top of AEM APIs]

    Should you need to customize event creation or dispatch, the Class objects used to construct this return value can be replaced by assigning alternate classes to an AEMApplication instance's AEMEventClass and/or AppleEventDescriptorClass properties. (Caution: Any replacement classes must implement identical public interfaces to AEMEvent and NSAppleEventDescriptor respectively, otherwise "unrecognized selector" exceptions will occur.)

  2. The following AEMEvent methods can be used to add any attributes and/or parameters to the AppleEvent descriptor:

        - (instancetype)setAttribute:(id)value forKeyword:(AEKeyword)key error:(NSError * __autoreleasing *)error;
        - (instancetype)setParameter:(id)value forKeyword:(AEKeyword)key error:(NSError * __autoreleasing *)error;
    

    The AEMEvent instance is returned on success. If a value cannot be added for any reason (e.g. if AEMCodecs is unable to pack it), nil is returned along with an optional NSError containing additional information. If error details are not required, the following shortcuts can be used instead:

        - (instancetype)setAttribute:(id)value forKeyword:(AEKeyword)key;
        - (instancetype)setParameter:(id)value forKeyword:(AEKeyword)key;
    

    [TO DO: also mention getters? (they're not used in sending events)]

    In addition, the -[AEMEvent setUnpackFormat:type:] method may be used to specify how the reply event's result descriptor should be unpacked:

        - (void)setUnpackFormat:(AEMUnpackFormat)format type:(DescType)type;
    

    The type argument is the AE type to which the returned descriptor should be coerced before unpacking; for example, to ensure the return value is unpacked as NSString, use typeUnicodeText (the default is typeWildCard). The format argument should be one of the following values:

    • kAEMUnpackAsItem – unpack the result descriptor as the specified type (this is the default)
    • kAEMUnpackAsList – coerce the result descriptor to typeAEList, then unpack each of its items as the specified type (the result is an NSArray of zero or more objects of the specified type)
    • kAEMDontUnpack – return the result descriptor (if any) without unpacking it.
  3. The following AEMEvent method is used to dispatch the Apple event:

        - (id)sendWithMode:(AESendMode)sendMode 
                   timeout:(long)timeoutInTicks
                     error:(NSError **)error;
    

    The sendMode argument should be composed via bitwise-OR of zero or more of the following flags (see the Apple Event Manager documentation for details):

        kAENoReply
        kAEQueueReply
        kAEWaitReply
        kAEDontReconnect
        kAEWantReceipt
        kAENeverInteract
        kAECanInteract
        kAEAlwaysInteract
        kAECanSwitchLayer
    

    The timeoutInTicks argument is the number of ticks (1 tick = 1/60 sec) that the Apple Event Manager should wait for the target process to reply. If the process doesn't reply within that time, a timeout error is returned instead. The following constants may also be used: kDefaultTimeout or kNoTimeOut.

    On success, the reply event's return value is returned, or an NSNull or empty NSArray (depending on the unpack format specified) if no return value was given. If the event fails due to an Apple Event Manager error or an application error, nil is returned; if the error argument is not nil then an NSError object containing the OSStatus code and any other error details is also returned.

    The following shortcuts can also be used to dispatch the event with default mode (kAEWaitReply) and timeout (kDefaultTimeout) values:

        - (id)sendWithError:(NSError **)error;
        - (id)send;
    

    (Tip: Should you need to send an event without processing the reply event, extract the underlying NSAppleEventDescriptor from AEMEvent.descriptor and invoke its -sendAppleEventWithMode:timeout:error: method directly. The result is an NSAppleEventDescriptor instance containing the full reply event (or nil if an Apple Event Manager error occurred). This can be useful with applications such as Final Cut Pro that use non-standard parameter keys in their reply events.)

    Note that -send... methods are intended to be invoked once per AEMEvent instance. (The Apple Event Manager documentation doesn't specify behavior where multiple identical Apple events are received by a process; at miminum, each event should have a unique return ID to ensure reply events are correctly returned.)

Examples

Targeting applications

  // application "TextEdit"
  AEMApplication *textedit = [[AEMApplication alloc] initWithName: @"TextEdit"];

  // application id "com.apple.TextEdit"
  textedit = [[AEMApplication alloc] initWithBundleID: @"com.apple.TextEdit"];

  // application "Macintosh HD:Applications:TextEdit.app"
  NSURL *fileURL = [NSURL fileURLWithPath: @"/Applications/TextEdit.app"];
  textedit = [[AEMApplication alloc] initWithURL: fileURL];

  // application "TextEdit" of machine "eppc://jsmith@some-mac.local"
  NSURL *url = [NSURL URLWithString: @"eppc://jsmith@some-mac.local/TextEdit"];
  textedit = [[AEMApplication alloc] initWithURL: url];

Building queries

  // text of every document
  AEMQuery *textRef = [[AEMApp elements: 'docu'] property: 'ctxt'];

  // end of every paragraph of text of document 1
  [[[[AEMApp elements: 'docu'] at: 1]
             property: 'ctxt']
             elements: 'cpar'].end;

  // paragraphs 2 thru last of first document
  [[[AEMApp elements: 'docu'].first
            elements: 'cpar'] byRange: [[AEMCon elements: 'cpar'] at: 2]
                                   to:  [AEMCon elements: 'cpar'].last];

  // paragraphs of document 1 where it != "\n"
  [[[[AEMApp elements: 'docu'] at: 1]
             elements: 'cpar'] byTest: [AEMIts notEquals: @"\n"]];

Sending events

  // quit TextEdit
  AEMEvent *evt = [textedit eventWithEventClass: 'aevt' eventID: 'quit'];
  [evt send];

  // count documents of TextEdit
  AEMEvent *evt = [textedit eventWithEventClass: 'core' eventID: 'cnte'];
  [evt setParameter: [AEMApp elements: 'docu'] forKeyword: '----'];
  [evt send];

  // make new document at end of documents of TextEdit
  AEMEvent *evt = [textedit eventWithEventClass: 'core' eventID: 'crel'];
  [evt setParameter: [[AEMType typeWithCode: 'docu'] forKeyword: 'kocl'];
  [evt setParameter: [AEMApp elements: 'docu'].end   forKeyword: 'insh'];
  [evt send];

  // get name of front document of TextEdit, with error checking
  NSError *error = nil;
  AEMEvent *evt = [textedit eventWithEventClass: 'core' eventID: 'getd'];
  AEMQuery *nameRef = [[AEMApp elements: 'docu'].first property: 'pnam'];
  // (Note: -setParameter:forKeyword: returns nil on error.)
  evt = [evt setParameter: nameRef forKeyword: '----'];
  [evt setUnpackFormat: kAEMUnpackAsItem type: typeUnicodeText];
  NSString *result = [evt sendWithError: &error];
  if (result) {
      NSLog(@"Result: %@", result);
  } else {
      NSLog(@"Error: %@", error); // e.g. -1728 if no documents are open
  }