七、让Cocoa App支持AppleScript

让我们来模拟一个场景

Demo地址
Sdef Editor.app地址

  • 假使我们有一个app,名叫 CocoaScripting。程序里有一个Person类,有名字有性别。一男一女两个人可以配对并且只能和一个人配对。
  • 假设app正在运行,并且启动时初始化了两个 person,名叫 Jack 和 Rose。
  • ViewController 是我们的主控制器,里面有两个属性:persons 和 pairs;
  • persons 由 Person 对象组成的数组,每个Person实例创建后都会添加到该数组里。
  • Person 类中有一个pair属性,ViewController里有一个pairs数组。Pair两个Person属性,为person1和person2。所以我们又建了一个Pair对象,将它加入pairs数组里。而且还在这个Pair对象中指出每一个配对者的pair指针。因此,pair和person是双重关联的。可以通过person的pair属性来找到与之匹配的另一个人。好,这就是我们做的这个简单的程序。

现在我们开始创建sdef格式的字典,并将其添加到项目中。起名叫做CocoaScripting.sdef
如果要app支持AppleScript,必须将以下行加入到Info.plist中

<key>NSAppleScriptEnabled</key> 
<string>YES</string> 
<key>OSAScriptingDefinition</key> 
<string>CocoaScripting.sdef</string> 
我们的脚本对这个程序该如何实现呢。

首先给CocoaScripting.sdef加一套常用的命令
让我们打开 Sdef Editor 来开始编辑吧。
选择 File > Open Standard Suite > NSCoreSuite ,然后从里面删除些我们不需要的内容、最后留下了下图

7110C34A-D358-418B-A3D3-9945D5C77983.png

然后我们创建一个新的suite,叫做CocoaScripting Suite.


B0B69EF8-B8B0-4C72-BBA9-D72A7CD58C33.png

我们主要操作的是类person,所以创建一个person,person需要有 name 属性等等。
code 是可以随意写的,但是不能和现有的code重复,所以建议使用一些大写字母。因为苹果所有的四个字母的code,都是小写的。
像name、id这些属性是AppleScript中已经定义的标准属性,必须使用已定义的正确的code。
将cocoa key 和code 匹配 person 对应的Cocoa 类是 Person。脚本通过KVC查找对应的代码,所以类需要存在正确的变量和方法。


screenshot_2.png
screenshot_1.png

可能有很多个人,所以我们创建一个 person 的element。也就是说,可以将person放到顶层,作为application的元素。


screenshot.png

建议将脚本性访问器与编程访问器分开。当脚本更改person的name属性时,它使用setName:,这与我们在Objective-C代码用于更改person的name的方法完全相同。但是我们的代码可能需要根据调用者的不同做出不同的响应,我们的代码或许会去做用户通过AppleScript不应该做的事情。所以我们可以在字典中指定一个不同的Cocoa键,并创建一组不同的脚本编辑框架访问器。

现在尝试下,通过脚本获取persons,persons 对应的Cocoa Key 是personArray,于是我们在Cocoa 工程里添加上这个方法。

- (NSArray *)personsArray
{
    return self.persons;
}

到现在,我们可以在AppleScript中对我们的App,进行一些访问了。
首先运行我们的Xcode工程,这样可以在Xcode的控制台得到一些提示。
在脚本编辑器中,尝试下以下命令:

 tell application "CocoaScripting"
    count persons -- 2
    name of person 1 -- "Jack"
    name of person 2 -- "Rose"
    name of every person -- {"Jack", "Rose"}
    name of every person whose name ends with "k" -- {"Jack"}
    exists person "Jack" -- true
    exists person "Tom" -- false
    delete person "Jack" --  Xcode 控制台: [<ViewController 0x600002c02d00> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key personsArray.
    get person 1 (* Error while returning the result of a script command: the result object...
<Person: 0x60000175f280>
...could not be converted to an Apple event descriptor of type 'person'. This instance of the class 'Person' returned nil when sent -objectSpecifier (is it not overridden?) and there is no coercible type declared for the scripting class 'person'.
    make new person 
*)
 end tell

delete person "Jack" 的报错,提示不满足KVC,因为delete 对应的方法没有写呀。比如我们不让用户通过AppleScript删除用户那么可以这样实现下

  - (void)removeObjectFromPersonsArrayAtIndex:(unsigned int)index
{
    [self returnError:OSAMessageNotUnderstood string:nil];
}

- (void)removeFromPersonsArrayAtIndex:(unsigned int)index
{
    [self returnError:OSAMessageNotUnderstood string:nil];
}

下面的get person 1,报错,说明我们仅仅实现personArray的getter方法是不行的。提示无法转换成对应的person类型,需要重写objectSpecifier方法

  • 实现objectSpecifier
    我们的应用程序声明的每个AppleScript类都应该在其对应的Objective-C类中有 objectSpecifier 方法的实现 。这就是当用户调用如get person 1或make new person之类的命令时,允许将如应用程序中person "Jack"之类的对象引用返回到脚本的原因。如果没有 objectSpecifier 实现,脚本将收到无意义的引用。实现了这个方法,脚本才能够理解你的对象。
 - (NSScriptObjectSpecifier *)objectSpecifier
{
    NSScriptClassDescription* appDesc = (NSScriptClassDescription*)[NSApp classDescription];
    return [[NSNameSpecifier alloc] initWithContainerClassDescription:appDesc containerSpecifier:nil key:@"personsArray" name:self.name];
    //return [[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:appDesc containerSpecifier:nil key:@"personsArray" uniqueID:[NSNumber numberWithInteger:self.cardID]];
}
NSApp 是全局应用的对象;我们获取它的 classDescription。containerSpecifier 为 nil 指的是顶级(应用)的容器,key为 @"personsArray".name 或者 uniqueID 是设置该对象的唯一标识. 以ID 为索引,对应的code为'ID  ', 两个空格不能省略。

让我们再次调用之前的AppleScript脚本试下吧


B696ED0D-F2EC-4A20-86B7-DD96FC0142B2.png
182D0804-EBE7-47FF-8439-29330C470361.png

让我们来创建一个新person

 tell application "CocoaScripting"
    
     make new person  -- [<ViewController 0x600002c03700> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key personsArray.
    
end tell

失败了,原因依然是没有实现new person 的方法,person在顶层是element 是一个数组,创建person的时候,默认是要加到数组里的,所以我们需要实现下

- (void)insertObject:(Person *)p inPersonsArrayAtIndex:(unsigned int)index;
- (void)insertInPersonsArray:(Person *)p;

而且也可以在创建时确定下要求下必须传入的参数,比如我们要求必须传入name和gender

// 指定创建person 的规则
- (BOOL)canGivePerson:(Person *)p name:(NSString *)name
{
    if (!name || [name isEqualToString:@""])
    {
        [self returnError:errOSACantAssign string:@"Can't give person empty name."];
        return NO;
    }
    if ([self existsPersonWithName:name])
    {
        [self returnError:errOSACantAssign string:@"Can't give person same name as existing person."];
        return NO;
    }
    return YES;
}

- (BOOL)canCreatePerson:(Person*)p
{
    if (![self canGivePerson:p name:p.name]) return NO;
    if (! (p.gender == GenderMale || p.gender == GenderFemale))
    {
        [self returnError:errOSACantAssign string:@"A person must have a gender."];
        return NO;
    }
    if ([[NSScriptCommand currentCommand] scriptErrorNumber] < 0)
    {
        return NO; // 其他命令断定无效
    }
    return YES;
}

调用AppleScript脚本进行验证:

tell application "CocoaScripting"
    
    make new person with properties {name:"Tom", gender:male}
end tell

接下来,对person进行配对,在OC代码ViewController里,这样实现

- (void)pair:(Person*)p1 with:(Person*)p2
{
    Pair *p = [[Pair alloc] init];
    p.person1 = p1;
    p.person2 = p2;
    p1.pair = p;
    p2.pair = p;
}

在AppleScript脚本中,我们希望通过 pair person1 to person2 这种形式来进行配对,那么pair 是一个动词(verb),在字典里就是命令(command),操作person1和person2两个person类,那么person1就是直接参数,person2是另外的参数。在Cocoa脚本中,动词(命令)以两种不同的方式实现。如果一个动词基本应用于单个对象,它可以作为对应于该对象的类的方法出现在Objective-C代码中。这称为对象优先分派。(另一种方法,即动词优先分派,本文没有做说明)。在字典中定义了该命令之后,在字典中可以指定任何类去作为该命令的直接对象的。所以字典会包含这个命令的定义:

F796B565-269C-47F1-8670-D25595D5D483.png

DB5F5371-9667-4BBB-AE24-B67DDCB4D034.png

在我们的 person 类的字典里这样写


BC451D8E-4B9B-40E0-8B4F-F6120B49E849.png

这意味着当用户调用pair命令时,一个消息scripterSaysPair:将被发送给代表该命令的直接对象的Person对象。这个方法的参数是一个NSScriptCommand对象,它的evaluatedArguments方法生成一个NSDictionary,其中包含command的附加参数,这些参数通过Cocoa Key进行访问。我们现在的情形下,只有一个额外的参数,它的Key 是 "otherPerson"。

//在Person类中调用
- (void)scripterSaysPair:(NSScriptCommand *)command
{
    Person* p1 = [command evaluatedReceivers];
    Person* p2 = [[command evaluatedArguments] valueForKey:@"otherPerson"];
    if (self != p1 || self == p2)
    {
        [self returnError:errOSACantAssign string:@"Invalid pairing."];
        return;
    }
    [self.master scripterWantsToPair:p1 with:p2];
}

// 在ViewController中调用
- (void)scripterWantsToPair:(Person *)firstPerson with: (Person *)secondPerson;
{
    if ([firstPerson pair] || [secondPerson pair])
    {
        [self returnError:errOSACantAssign string:@"Can't pair a person who is already paired."];
        return;
    }
    [self pair:firstPerson with:secondPerson];
}

当配对后,我们还想知道与之配对的person是哪个,我们已经在字典里加了 partner 属性,同样也在OC代码里实现对应的Key

- (id)personPartner
{
    Pair *myPair = [self pair];
    if (!myPair)
    {
        return [NSNull null]; // missing value
    }
    return (myPair.person1 == self ? myPair.person2 : myPair.person1);
}

- (void)setPersonPartner:(id)newPartner
{
    [self returnError:errOSACantAssign string:@"Partner property is read-only. To set a person's partner, pair the person with another person."];
}

- (BOOL)personPaired
{
    return ([self pair] != nil);
}

- (void)setPersonPaired:(BOOL)newPaired
{
    [self returnError:errOSACantAssign string:@"Paired property is read-only."];
}

让我们通过AppleScript脚本试一下吧

tell application "CocoaScripting"
    
    pair person 1 to person 2
    name of every person -- {"Mannie", "Jack", "Moe"}
    partner of person "Jack"
end tell
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • 关于键值编码 键值编码(KVC)是一种由NSKeyValueCoding非正式协议提供的机制,对象采用该机制来提供...
    渐z阅读 916评论 0 0
  • 最近一朋友正准备跳槽,就从各处搜索整理一些基础,便于朋友复习,也便于自己复习查看. 1. 回答person的ret...
    smile丽语阅读 1,730评论 0 7
  • django数据库配置_Django学习笔记(六)-python 1. 数据库默认设置 :sqlite3 """ ...
    a_嗝嗝阅读 193评论 0 0
  • 莎士比亚说:“金钱是个好士兵,有了它就可以使人勇气百倍。”那么反之,钱不够花,也几乎是每一个人的痛处。 悦悦和雪霞...
    super欣颖阅读 223评论 2 1