3

目前我正在尝试通过 ScriptingBridge 实现对多个版本的 iTunes 的支持。

例如,属性的方法签名playerPosition从 (10.7) 更改

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

到 (11.0.5)

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

对于我的应用程序中最新的头文件和较旧的 iTunes 版本,此属性的返回值将始终为 3。同样的事情反过来。

所以我继续创建了三个不同的 iTunes 头文件,11.0.5、10.7 和 10.3.1 via

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

对于每个版本的 iTunes,我调整了基本名称以包含版本,例如 iTunes_11_0_5.h。这导致头文件中的接口以它们的特定版本号为前缀。我的目标是/是对我要与正确版本的接口一起使用的对象进行类型转换。

iTunes 的路径是通过一种NSWorkspace方法获取的,然后我从中创建一个 NSBundle 并CFBundleVersion从 infoDictionary 中提取它。

三个不同的版本(11.0.5、10.7、10.3.1)也被声明为常量,我通过以下方式与用户的 iTunes 版本进行比较

[kiTunes_11_0_5 compare:versionInstalled options:NSNumericSearch]

然后我检查每个结果是否等于NSOrderedSame,这样我就知道用户安装了哪个版本的 iTunes。

用 if 语句实现这个有点失控,因为我需要在课堂上的许多不同地方进行这些类型转换,然后我开始意识到这会导致很多重复的代码,并进行了修改和思考这是为了找到一个不同的解决方案,一个更“最佳实践”的解决方案。

一般来说,我需要对我使用的对象进行动态类型转换,但我根本找不到一个不会导致大量重复代码的解决方案。

编辑

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]
4

2 回答 2

2

[所有代码直接输入答案。]

您可以使用categoryproxy或 helper 类来解决这个问题,这里是后者的一种可能设计的草图。

首先创建一个辅助类,它采用 iTunes 对象和版本字符串的实例。此外,为了避免重复进行字符串比较,请在类设置中进行一次比较。您没有提供 iTunes 应用程序对象的类型,因此我们将随机调用它ITunesAppObj- 替换为正确的类型:

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;
}

现在为每个在版本之间更改类型的项目添加一个项目,用您选择的任何“通用”类型声明它。例如,playerPosition这可能是:

@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...

对轨道类型做类似的事情。然后您的代码片段变为:

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

...

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

以上是您要求的动态- 在每次调用时都有一个switch调用适当的版本。您当然可以将XYZiTunesHelper类抽象化(或接口或协议)并为每个 iTunes 版本编写三个实现,然后进行一次测试并选择适当的实现。这种方法更“面向对象”,但它确实意味着不同的实现,比如说,playerPosition不是在一起的。在这种特殊情况下,选择您觉得最舒服的风格。

高温高压

于 2013-09-19T18:48:30.943 回答
1

生成多个标头并根据应用程序的版本号将它们切换进出是一个非常糟糕的“解决方案”:除了非常复杂之外,它还非常脆弱,因为它将您的代码与特定的 iTunes 版本相结合。

Apple 事件(如 HTTP)是由了解如何构建大型、灵活的长寿命分布式系统的人设计的,这些系统的客户端和服务器可以随着时间的推移而发展和变化,而不会相互破坏。Scripting Bridge 与许多现代“Web”一样,并非如此。

...

检索特定类型值的正确方法是在“获取”事件中指定所需的结果类型。AppleScript 可以做到这一点:

tell app "iTunes" to get player position as real

同上 objc-appscript,它提供了专门用于以 C 数字形式获取结果的便捷方法:

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

或者,如果您希望将结果作为 NSNumber:

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

然而,SB 会自动为您发送“get”事件,在它返回之前不告诉它您想要什么类型的结果。因此,如果应用程序出于任何原因决定返回不同类型的值,则基于 SB 的 ObjC 代码会从 sdp 标头开始中断。

...

在一个理想的世界里,你只需抛弃 SB 并使用 objc-appscript,它与 SB 不同,它知道如何正确说出 Apple 事件。不幸的是,由于 Apple 保留了原始的 Carbon Apple 事件管理器 API,但没有提供可行的 Cocoa 替代品,因此不再维护 appscript,因此不建议将其用于新项目。因此,您几乎被 Apple 提供的选项卡住了,这些选项都不好用,也不好用。(然后他们想知道为什么程序员如此讨厌 AppleScript 的一切......)

一种解决方案是通过 AppleScript-ObjC 桥使用 AppleScript。AppleScript 可能是一种糟糕的语言,但至少它知道如何正确说出 Apple 事件。与 Cocoa 的蹩脚的 NSAppleScript 类不同,ASOC 消除了在您的应用程序中将 AS 和 ObjC 代码粘合在一起的大部分痛苦。

但是,对于这个特殊问题,可以通过下降到 SB 的低级方法和原始的四字符代码来自己构造和发送事件,从而解决 SB 有缺陷的胶水问题。写起来有点乏味,但是一旦完成它就完成了(至少直到下一次发生变化......)。

这是一个类别,显示了如何为“玩家位置”属性执行此操作:

@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

请注意,我已将方法名称作为前缀iTunes_playerPosition。与使用静态 .h+.m 胶水的 objc-appscript 不同,SB 在运行时动态创建其所有 iTunes 特定的胶水类,因此您不能添加类别或直接修补它们。您所能做的就是将您的类别添加到根 SBObject/SBApplication 类,使它们在所有应用程序粘合中的所有类中可见。Swizzling 方法名称应该避免与任何其他应用程序的粘合方法发生冲突的风险,但显然您仍然需要注意在正确的对象上调用它们,否则您可能会得到意外的结果或错误。

显然,对于在 iTunes 11 中经历了相同增强的任何其他属性,您必须重复此补丁,但至少一旦完成,您就不必再次更改它,例如,Apple 将来恢复为整数如果您忘记在复杂的 switch 块中包含以前的版本。另外,当然,您不必为生成多个 iTunes 标题而烦恼:只需为当前版本创建一个,并记住避免-playerPosition在您的代码中使用原始和其他损坏的 SB 方法,iTunes_...而是使用您自己的健壮方法。

于 2013-09-28T16:06:39.187 回答