How to dynamically typecast objects to support different versions of an application's ScriptingBridge header files?

StackOverflow https://stackoverflow.com/questions/18878748

Question

Currently I'm trying to implement support for multiple versions of iTunes via ScriptingBridge.

For example the method signature of the property playerPosition changed from (10.7)

@property NSInteger playerPosition;  // the player’s position within the currently playing track in seconds.

to (11.0.5)

@property double playerPosition;  // the player’s position within the currently playing track in seconds

With the most current header file in my application and an older iTunes version the return value of this property would always be 3. Same thing goes the other way around.

So I went ahead and created three different iTunes header files, 11.0.5, 10.7 and 10.3.1 via

sdef /path/to/application.app | sdp -fh --basename applicationName

For each version of iTunes I adapted the basename to inlcude the version, e.g. iTunes_11_0_5.h. This results in the interfaces in the header files to be prefixed with their specific version number. My goal is/was to typecast the objects I'd use with the interfaces of the correct version.

The path to iTunes is fetched via a NSWorkspace method, then I'm creating a NSBundle from it and extract the CFBundleVersion from the infoDictionary.

The three different versions (11.0.5, 10.7, 10.3.1) are also declared as constants which I compare to the iTunes version of the user via

[kiTunes_11_0_5 compare:versionInstalled options:NSNumericSearch]

Then I check if each result equals NSOrderedSame, so I'll know which version of iTunes the user has installed.

Implementing this with if statement got a bit out of hand, as I'd need to do these typecasts at many different places in my class and I then started to realize that this will result in a lot of duplicate code and tinkered around and thought about this to find a different solution, one that is more "best practice".

Generally speaking, I'd need to dynamically typecast the objects I use, but I simply can't find a solution which wouldn't end in loads of duplicated code.

Edit

if ([kiTunes_11_0_5 compare:_versionString options:NSNumericSearch] == NSOrderedSame) {
    NSLog(@"%@, %@", kiTunes_11_0_5, _versionString);
    playerPosition = [(iTunes_11_0_5_Application*)_iTunes playerPosition];
    duration = [(iTunes_11_0_5_Track*)_currentTrack duration];
    finish = [(iTunes_11_0_5_Track*)_currentTrack finish];    
} else if [... and so on for each version to test and cast]
Était-ce utile?

La solution 2

Generating multiple headers and switching them in and out based on the application's version number is a really bad 'solution': aside from being horribly complicated, it is very brittle since it couples your code to specific iTunes versions.

Apple events, like HTTP, were designed by people who understood how to construct large, flexible long-lived distributed systems whose clients and servers could evolve and change over time without breaking each other. Scripting Bridge, like a lot of the modern 'Web', was not.

...

The correct way to retrieve a specific type of value is to specify your required result type in the 'get' event. AppleScript can do this:

tell app "iTunes" to get player position as real

Ditto objc-appscript, which provides convenience methods specifically for getting results as C numbers:

ITApplication *iTunes = [ITApplication applicationWithBundleID: @"com.apple.itunes"];
NSError *error = nil;
double pos = [[iTunes playerPosition] getDoubleWithError: &error];

or, if you'd rather get the result as an NSNumber:

NSNumber *pos = [[iTunes playerPosition] getWithError: &error];

SB, however, automatically sends the 'get' event for you, giving you no what to tell it what type of result you want before it returns it. So if the application decides to return a different type of value for any reason, SB-based ObjC code breaks from sdp headers onwards.

...

In an ideal world you'd just ditch SB and go use objc-appscript which, unlike SB, knows how to speak Apple events correctly. Unfortunately, appscript is no longer maintained thanks to Apple legacying the original Carbon Apple Event Manager APIs without providing viable Cocoa replacements, so isn't recommended for new projects. So you're pretty much stuck with the Apple-supplied options, neither of which is good or pleasant to use. (And then they wonder why programmers hate everything AppleScript so much...)

One solution would be to use AppleScript via the AppleScript-ObjC bridge. AppleScript may be a lousy language, but at least it knows how to speak Apple events correctly. And ASOC, unlike Cocoa's crappy NSAppleScript class, takes most of the pain out of gluing AS and ObjC code together in your app.

For this particular problem though, it is possible to monkey-patch around SB's defective glues by dropping down to SB's low-level methods and raw four-char codes to construct and send the event yourself. It's a bit tedious to write, but once it's done it's done (at least until the next time something changes...).

Here's a category that shows how to do this for the 'player position' property:

@implementation SBApplication (ITHack)

-(double)iTunes_playerPosition {
    // Workaround for SB Fail: older versions of iTunes return typeInteger while newer versions
    // return typeIEEE64BitFloatingPoint, but SB is too stupid to handle this correctly itself

    // Build a reference to the 'player position' property using four-char codes from iTunes.sdef

    SBObject *ref = [self propertyWithCode:'pPos'];

    // Build and send the 'get' event to iTunes (note: while it is possible to include a
    // keyAERequestedType parameter that tells the Apple Event Manager to coerce the returned
    // AEDesc to a specific number type, it's not necessary to do so as sendEvent:id:parameters:
    // unpacks all numeric AEDescs as NSNumber, which can perform any needed coercions itself)

    NSNumber *res = [self sendEvent:'core' id:'getd' parameters: '----', ref, nil];

    // The returned value is an NSNumber containing opaque numeric data, so call the appropriate
    // method (-integerValue, -doubleValue, etc.) to get the desired representation

    return [res doubleValue];

}

@end

Notice I've prefixed the method name as iTunes_playerPosition. Unlike objc-appscript, which uses static .h+.m glues, SB dynamically creates all of its iTunes-specific glue classes at runtime, so you can't add categories or otherwise patch them directly. All you can do is add your category to the root SBObject/SBApplication class, making them visible across all classes in all application glues. Swizzling the method names should avoid any risk of conflict with any other applications' glue methods, though obviously you still need to take care to call them on the right objects otherwise you'll likely get unexpected results or errors.

Obviously, you'll have to repeat this patch for any other properties that have undergone the same enhancement in iTunes 11, but at least once done you won't have to change it again if, say, Apple revert back to integers in a future release or if you've forgotten to include a previous version in your complicated switch block. Plus, of course, you won't have to mess about generating multiple iTunes headers: just create one for the current version and remember to avoid using the original -playerPosition and other broken SB methods in your code and use your own robust iTunes_... methods instead.

Autres conseils

[All code directly entered into answer.]

You could tackle this with a category, a proxy, or a helper class, here is a sketch of one possible design for the latter.

First create a helper class which takes and instance of your iTunes object and the version string. Also to avoid doing repeated string comparisons do the comparison once in the class setup. You don't give the type of your iTunes application object so we'll randomly call it ITunesAppObj - replace with the correct type:

typedef enum { kEnumiTunes_11_0_5, ... } XYZiTunesVersion;

@implementation XYZiTunesHelper
{
   ITunesAppObj *iTunes;
   XYZiTunesVersion version;
}

- (id) initWith:(ITunesAppObj *)_iTunes version:(NSString *)_version
{
   self = [super self];
   if (self)
   {
      iTunes = _iTunes;
      if ([kiTunes_11_0_5 compare:_version options:NSNumericSearch] == NSOrderedSame)
         version = kEnumiTunes_11_0_5;
      else ...
   }
   return self;
}

Now add an item to this class for each item which changes type between versions, declaring it with whatever "common" type you pick. E.g. for playerPosition this might be:

@interface XYZiTunesHelper : NSObject

@property double playerPosition;
...

@end


@implementation XYZiTunesHelper

// implement getter for playerPosition
- (double) playerPosition
{
   switch (version)
   {
      case kEnumiTunes_11_0_5:
         return [(iTunes_11_0_5_Application*)_iTunes playerPosition];

      // other cases - by using an enum it is both fast and the
      // compiler will check you cover all cases
   }
}

// now implement the setter...

Do something similar for track type. Your code fragment then becomes:

XYZiTunesHelper *_iTunesHelper = [[XYZiTunesHelper alloc] init:_iTunes
                                                      v ersion:_versionString];

...

playerPosition = [_iTunesHelper playerPosition];
duration = [_currentTrackHelper duration];
finish = [_currentTrackHelper finish];    

The above is dynamic as you requested - at each call there is a switch to invoke the appropriate version. You could of course make the XYZiTunesHelper class abstract (or an interface or a protocol) and write three implementations of it one for each iTunes version, then you do the test once and select the appropriate implementation. This approach is more "object oriented", but it does mean the various implementations of, say, playerPosition are not together. Pick whichever style you feel most comfortable with in this particular case.

HTH

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top