當(dāng)為應(yīng)用添加 AppleScript 支持的時(shí)候 - OS X 10.10 中也可以是 JavaScript 支持(譯者注:10.10 中我們可以使用 JavaScript 作為腳本語(yǔ)言了),最好以應(yīng)用的數(shù)據(jù)作為開(kāi)始。這里的腳本并不是說(shuō)自動(dòng)按鈕點(diǎn)擊什么的;而是在說(shuō)將你的 model 層暴露給那些會(huì)在自己的工作流程中使用你的應(yīng)用的人。
有的用戶(hù)會(huì)向朋友和家人推薦應(yīng)用,雖然通常像這樣的用戶(hù)極少,但是他們是超級(jí)用戶(hù)。他們的博客和 twitter 上有關(guān)于應(yīng)用的內(nèi)容,人們關(guān)注了他們。他們會(huì)成為你的應(yīng)用的最大傳播者。
總體而言,添加腳本支持最重要的原因是它使得應(yīng)用更加專(zhuān)業(yè),而這所能得到的回報(bào)是值得我們努力的。
Noteland 是一個(gè)除了空白窗口之外沒(méi)有任何 UI 的應(yīng)用,但是它有 model 層,并且可以腳本化。你可以在 GitHub 上找到它。
Noteland 支持 AppleScript(10.10上還支持 JavaScript)。它是在 Xcode 5.1.1 中用 Objective-C 寫(xiě)的。我們最初試圖使用 Swift 和 Xcode 6 Beta 2,但是出現(xiàn)了困難。這完全可能是我們自己的錯(cuò)誤,因?yàn)楫吘刮覀內(nèi)匀辉趯W(xué)習(xí) Swift。
有兩個(gè)類(lèi),notes(筆記) 和 tags(標(biāo)簽)??赡苡卸鄠€(gè)筆記,而且一個(gè)筆記也許有多個(gè)標(biāo)簽。
NLNote.h 聲明了幾個(gè)屬性: uniqueID,text,creationDate,archived,tags 和一個(gè)只讀的 title 屬性。
Tags 類(lèi)更加簡(jiǎn)單。NLTag.h 聲明了兩個(gè)可腳本屬性: uniqueID 和 name。
我們希望用戶(hù)能夠創(chuàng)建,編輯和刪除筆記和標(biāo)簽,并且能夠訪問(wèn)和改變除了只讀以外的屬性。
第一個(gè)步驟是定義腳本接口,概念上可以理解為為腳本創(chuàng)建一個(gè) .h 文件,但是是以 AppleScript 能夠識(shí)別的格式進(jìn)行創(chuàng)建。
過(guò)去,我們需要?jiǎng)?chuàng)建和編輯 aete 資源(“aete” 代表 Apple Event Terminology)?,F(xiàn)在容易了很多:我們可以創(chuàng)建一個(gè) sdef(scripting definition 腳本定義)XML 文件。
你可能更傾向于使用 JSON 或者 plist,但是 XML 在這里會(huì)更加合適,至少它毫無(wú)疑問(wèn)戰(zhàn)勝了 aete 資源。事實(shí)上,曾有一段時(shí)間有 plist 版本,但是它要求你保持 兩個(gè) 不同的 plist 同步,這非常痛苦。
原來(lái)的資源的名字 (aete,Apple Event Terminology) 其實(shí)沒(méi)什么特別的意思。Apple event 是由 AppleScript 生成,發(fā)送和接受的低級(jí)別消息。這本身是一種很有趣的技術(shù),而且有腳本支持以外的用途。而且實(shí)際上,它從 90 年代初的 System 7 開(kāi)始就一直存在,而且在過(guò)渡到 OS X 的過(guò)程中存活了下來(lái)。
(猜測(cè):Apple event 的存活是由于很多印刷出版商依賴(lài)于 AppleScript,在 90 年代中后期的 '黑暗日子' 中,出版商們是 Apple 最忠實(shí)的用戶(hù)。)
一個(gè) sdef 文件總是以同樣的頭部作為開(kāi)始:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
頂級(jí)項(xiàng)是字典 (dictionary),“字典” 是 AppleScript 中專(zhuān)指一個(gè)腳本接口的詞。在字典中你會(huì)發(fā)現(xiàn)一個(gè)或多個(gè)套件 (suite)。
(提示:打開(kāi) AppleScript Editor,然后選擇 File > Open Dictionary...你會(huì)看到有腳本字典的應(yīng)用列表。如果你選擇 iTunes 作為例子,你會(huì)看到類(lèi),屬性和 iTunes 能識(shí)別的命令。)
<dictionary title="Noteland Terminology">
標(biāo)準(zhǔn)套件定義了應(yīng)用應(yīng)該支持的所有類(lèi)和操作。其中包括退出,關(guān)閉窗口,創(chuàng)建和刪除對(duì)象,查詢(xún)對(duì)象等等。
將它添加到你的 sdef 文件,從位于 /System/Library/ScriptingDefinitions/CocoaStandard.sdef 的標(biāo)準(zhǔn)套件中復(fù)制和粘貼。
從 <suite name="Standard Suite", 從頭到尾且包括結(jié)尾 </suite> 復(fù)制所有東西。
將它粘貼到你的 sdef 文件中 dictionary 元素的正下方。
然后,在你的 sdef 文件中,遍歷并刪除所有沒(méi)有用到的東西。Noteland 不基于文檔且無(wú)需打印,所以我們?nèi)サ袅舜蜷_(kāi)和保存命令,文件類(lèi),以及與打印有關(guān)的一切。
(建議:Xcode 在 XML 的縮進(jìn)方面做得很好,為了重新縮進(jìn),選中所有文本并且選擇 Editor > Structure > Re-Indent。)
當(dāng)你完成編輯后,使用命令行 xmllint 程序 xmllint path/to/noteland.sdef 以確保 XML 是正常的。如果它只顯示了 XML,沒(méi)有錯(cuò)誤和警告,那么就是正確的。(記住你可以在 Xcode 的窗口標(biāo)題欄拖拽文件的代理圖標(biāo)到終端,然后會(huì)粘貼文件的路徑。)
一個(gè)單一的應(yīng)用定義套件通常是最好的,雖然并不強(qiáng)制:當(dāng)確實(shí)合情合理的時(shí)候,你可以有超過(guò)一個(gè)的套件。Noteland 只定義一個(gè),下面是 Noteland 套件:
<suite name="Noteland Suite" code="Note" description="Noteland-specific classes.">
腳本字典所期望的是某些部件被包含在其他東西中。頂級(jí)容器是應(yīng)用程序?qū)ο蟊旧怼?/p>
在 Noteland 中,它的類(lèi)名是 NLApplication。對(duì)于應(yīng)用的類(lèi)你應(yīng)該總是使用 capp 作為編碼 (code) 值:這是一個(gè)標(biāo)準(zhǔn)的 Apple event 編碼。(注意它也存在于標(biāo)準(zhǔn)套件中。)
<class name="application" code="capp" description="Noteland’s top level scripting object." plural="applications" inherits="application">
<cocoa class="NLApplication"/>
該應(yīng)用包含一個(gè)筆記的數(shù)組。區(qū)分元素(這里可以有不止一項(xiàng))和屬性非常重要。換句話(huà)說(shuō),編碼中的數(shù)據(jù)應(yīng)該作為你字典中的一個(gè)元素。
<element type="note" access="rw">
<cocoa key="notes"/>
</element>`
Cocoa 腳本使用 KVC,字典用來(lái)指定鍵的名稱(chēng)。
<class name="note" code="NOTE" description="A note" inherits="item" plural="notes">
<cocoa class="NLNote"/>`
上面的編碼是 NOTE。這幾乎可以是任何東西,但是請(qǐng)注意,Apple 保留所有的小寫(xiě)編碼供自己使用,所以 note 是不被允許的。它可以是 NOT*, 或 NoTe, 或 XYzy,或者任何你想要的。(理想情況下自己的編碼不會(huì)與其他應(yīng)用的編碼沖突。但是我們沒(méi)有辦法確保這一點(diǎn),所以我們只能夠 猜測(cè)。也就是說(shuō), 猜想 NOTE 可能并不是一個(gè)很好的選擇。)
你的類(lèi)應(yīng)該繼承自 item。(理論上,你可以讓一個(gè)類(lèi)繼承自你的另一個(gè)類(lèi),不過(guò)我們沒(méi)有做過(guò)這個(gè)嘗試。)
note 類(lèi)有多個(gè)屬性:
<property name="id" code="ID " type="text" access="r" description="The unique identifier of the note.">
<cocoa key="uniqueID"/>
</property>
<property name="name" code="pnam" type="text" description="The name of the note — the first line of the text." access="r">
<cocoa key="title"/>
</property>
<property name="body" code="body" description="The plain text content of the note, including first line and subsequent lines." type="text" access="rw">
<cocoa key="text"/>
</property>
<property name="creationDate" code="CRdt" description="The date the note was created." type="date" access="r"/>
<property name="archived" code="ARcv" description="Whether or not the note has been archived." type="boolean" access="rw"/>
如果可能,最好為你的對(duì)象提供獨(dú)一無(wú)二的 ID。否則,腳本不得不依賴(lài)于可能發(fā)生改變的名字和位置。對(duì)唯一的 ID 使用編碼 'ID '。(注意有兩個(gè)空格;編碼應(yīng)該是四個(gè)字符。)而這個(gè)唯一 ID 的名字必須是 id。
只要有意義,提供 name 屬性就是標(biāo)準(zhǔn)的做法,編碼應(yīng)該是 pnam。在 Noteland 中它是一個(gè)只讀屬性,因?yàn)槊Q(chēng)只是筆記中文本的第一行,而且筆記的文本通過(guò)可讀寫(xiě)的 body 屬性編輯。
對(duì)于 creationDate 和 archived,我們并不需要提供 Cocoa 的鍵元素,因?yàn)殒I和屬性名字相同。
注意類(lèi)型:text, date 和 boolean。AppleScript 支持它們和其它幾個(gè),詳細(xì)地在本文檔中列出。
筆記可以有標(biāo)簽,下面是一個(gè)標(biāo)簽元素:
<element type="tag" access="rw">
<cocoa key="tags"/>
</element>
</class>`
Tags 是 NLTap 對(duì)象:
<class name="tag" code="TAG*" description="A tag" inherits="item" plural="tags">
<cocoa class="NLTag"/>`
Tags 只有兩個(gè)屬性,id 和 name:
<property name="id" code="ID " type="text" access="r" description="The unique identifier of the tag.">
<cocoa key="uniqueID"/>
</property>
<property name="name" code="pnam" type="text" access="rw">
<cocoa key="name"/>
</property>
</class>
下面的代碼是 Noteland 套件和整個(gè)字典的結(jié)束:
</suite>
</dictionary>
應(yīng)用不是默認(rèn)就能腳本化的。我們?cè)?Xcode 中,需要編輯應(yīng)用的 Info.plist。
因?yàn)閼?yīng)用使用了一個(gè)自定義的 NSApplication 子類(lèi),用來(lái)提供頂級(jí)容器,我們編輯主體類(lèi) (NSPrincipalClass) 來(lái)聲明 NLApplication (Noteland 的 NSApplication 子類(lèi)名字)。
我們還添加了一個(gè)腳本化的鍵(OSAScriptingDefinition)并且設(shè)置它為 YES。最后,我們添加一個(gè)名為(OSAScriptingDefinition) 的鍵來(lái)表示腳本定義文件的名字,并將它設(shè)置為 sdef 的文件命名為:noteland.sdef。
你可能會(huì)驚訝竟然只需要寫(xiě)那么少的代碼。
參見(jiàn) Noteland 工程中的 NLApplication.m 文件。它惰性地創(chuàng)建了一個(gè)筆記數(shù)組且提供了一些 dummy 數(shù)據(jù)。說(shuō)惰性只是因?yàn)樗鼪](méi)有連接腳本支持。
(注意這里沒(méi)有對(duì)象持久化,因?yàn)槲蚁胱?Noteland 盡可能自由,而不僅僅是腳本支持。你可以使用 Core Data 或 archiever(歸檔)或者其它東西來(lái)保存數(shù)據(jù)。)
它也可以跳過(guò) dummy 數(shù)據(jù)并提供一個(gè)數(shù)組。
在本例中,數(shù)組是 NSMutableArray 類(lèi)型的。它可以不必是 NSMutableArray,而是一個(gè) NSArray,但這樣的話(huà) Cocoa 腳本在筆記數(shù)組發(fā)生改變時(shí)將會(huì)替換整個(gè)數(shù)組。但是如果我們讓它作為 NSMutableArray 數(shù)組 且 提供下面兩個(gè)方法的話(huà),這個(gè)數(shù)組就不必被替換。取而代之,對(duì)象將會(huì)被添加到可變數(shù)組中,以及從中移除。
- (void)insertObject:(NLNote *)object inNotesAtIndex:(NSUInteger)index {
[self.notes insertObject:object atIndex:index];
}
- (void)removeObjectFromNotesAtIndex:(NSUInteger)index {
[self.notes removeObjectAtIndex:index];
}
另外需要注意,筆記數(shù)組在類(lèi)擴(kuò)展的 .m 文件中被聲明。不需要將它放到 .h 文件中。因?yàn)?Cocoa 腳本使用 KVC,而且不關(guān)心你的header,它會(huì)找到這個(gè)屬性的。
NLNote.h 聲明了筆記的各個(gè)屬性:uniqueID,text,creationDate,archived,title 和 tags。
在 init 方法中設(shè)置 uniqueID 和 creationDate,以及將標(biāo)簽數(shù)組設(shè)為空的 NSArray。這次我們使用 NSArray 而不是 NSMutableArray,僅僅為了說(shuō)明它也可以達(dá)到目的。
tilte 方法返回一個(gè)計(jì)算后的值:筆記中文本的第一行。(回想一下,這會(huì)成為腳本字典的 name。)
要注意 objectSpecifier 方法。這是你的類(lèi)的關(guān)鍵;腳本支持需要這個(gè)使其能夠理解你的對(duì)象。
幸運(yùn)的是,這個(gè)方法很容易實(shí)現(xiàn)。雖然對(duì)象說(shuō)明符 (object specifiers) 有不同類(lèi)型,通常情況下最好使用 NSUniqueIDSpecifier,因?yàn)樗芊€(wěn)定。(其它選項(xiàng)包括:NSNameSpecifier, NSPositionalSpecifier 等。)
對(duì)象說(shuō)明符需要了解容器相關(guān)的東西,而且容器是頂級(jí)應(yīng)用的對(duì)象。
代碼如下所示:
NSScriptClassDescription *appDescription = (NSScriptClassDescription *)[NSApp classDescription];
return [[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:appDescription containerSpecifier:nil key:@"notes" uniqueID:self.uniqueID];
NSApp 是全局應(yīng)用的對(duì)象;我們獲取它的 classDescription。鍵為 @"notes",containerSpecifier 為 nil 指的是頂級(jí)(應(yīng)用)的容器, uniqueID 是筆記的 uniqueID。
我們需要超前考慮一點(diǎn)。標(biāo)簽也會(huì)需要 objectSpecifier,而且標(biāo)簽是包含在筆記中的,所以標(biāo)簽需要引用包含它的筆記。
Cocoa 腳本處理標(biāo)簽的創(chuàng)建,但是我們可以重寫(xiě)讓自己自定義行為的方法。
NSObjectScripting.h 定義了 -newScriptingObjectOfClass:forValueForKey: withContentsValue:properties:。這正是我們需要的。在 NLNote.m 中,它看起來(lái)是這樣的:
NLTag *tag = (NLTag *)[super newScriptingObjectOfClass:objectClass forValueForKey:key withContentsValue:contentsValue properties:properties];
tag.note = self;
return tag;
我們使用父類(lèi)的實(shí)現(xiàn)來(lái)創(chuàng)建標(biāo)簽,然后設(shè)置標(biāo)簽的 note 屬性為該筆記。為了避免可能的循環(huán)引用,NLTag.h 的 note 是 weak 屬性。
(你可能認(rèn)為這并不太不優(yōu)雅,我們同意這么說(shuō)。我們希望取代那種為了子類(lèi)的 objectSpecifiers 而需要存在的容器。像是 objectSpecifierForScriptingObject: 這樣可能會(huì)更好。我們提出了一個(gè) bug rdar://17473124。)
NLTag 有 uniqueID, name, 和 note 屬性。
NLTag 的 objectSpecifier 在概念上和 NLNote中的代碼相同,除了容器是筆記而不是頂級(jí)應(yīng)用類(lèi)。
它看起來(lái)像下面這樣:
NSScriptClassDescription *noteClassDescription = (NSScriptClassDescription *)[self.note classDescription];
NSUniqueIDSpecifier *noteSpecifier = (NSUniqueIDSpecifier *)[self.note objectSpecifier];
return [[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:noteClassDescription containerSpecifier:noteSpecifier key:@"tags" uniqueID:self.uniqueID];
就是這樣。完成了。并沒(méi)有太多代碼,大量的工作都是設(shè)計(jì)接口和編輯 sdef 文件。
在過(guò)去,你需要編寫(xiě) Apple event 處理程序,并與 Apple event 描述符和各種一團(tuán)亂麻的玩意兒一起工作。換句話(huà)說(shuō),要完成這些你需要走很長(zhǎng)的路。值得慶幸的是,現(xiàn)在已經(jīng)不是過(guò)去的日子了。
接下來(lái)才是有趣的東西。
啟動(dòng) Noteland。啟動(dòng) /Applications/Utilities/AppleScript Editor.app。
運(yùn)行下面的腳本:
tell application "Noteland"
every note
end tell
在底部的結(jié)果窗口中,你會(huì)看到下面這樣的信息:
{note id "0B0A6DAD-A4C8-42A0-9CB9-FC95F9CB2D53" of application "Noteland", note id "F138AE98-14B0-4469-8A8E-D328B23C67A9" of application "Noteland"}
當(dāng)然,ID 會(huì)有所不同,但是這些跡象表明,它在工作。
試一試這個(gè)腳本:
tell application "Noteland"
name of every note
end tell
你會(huì)在結(jié)果窗中看到 {"Note 0", "Note 1"}。
再試一下這個(gè)腳本:
tell application "Noteland"
name of every tag of note 2
end tell
結(jié)果:{"Tiger Swallowtails", "Steak-frites"}。
(請(qǐng)注意 AppleScript 數(shù)組是基于 1 的,所以 2 指的是第二個(gè)筆記。當(dāng)我們明白這個(gè)以后,就一點(diǎn)也不奇怪了)
你也可以創(chuàng)建筆記:
tell application "Noteland"
set newNote to make new note with properties {body:"New Note" & linefeed & "Some text.", archived:true}
properties of newNote
end tell
結(jié)果將會(huì)是類(lèi)似這樣的(詳細(xì)信息有相應(yīng)改變):
{creationDate:date "Thursday, June 26, 2014 at 1:42:08 PM", archived:true, name:"New Note", class:note, id:"49D5EE93-655A-446C-BB52-88774925FC62", body:"New Note\nSome text."}`
你還可以創(chuàng)建新的標(biāo)簽:
tell application "Noteland"
set newNote to make new note with properties {body:"New Note" & linefeed & "Some text.", archived:true}
set newTag to make new tag with properties {name:"New Tag"} at end of tags of newNote
name of every tag of newNote
end tell
結(jié)果會(huì)是:{"New Tag"}。
完美工作!
將對(duì)象模型腳本化只是添加腳本支持的一部分;你也可以為命令添加支持。例如,Noteland 可以有一個(gè)將筆記寫(xiě)到硬盤(pán)文件的導(dǎo)出命令。RSS 閱讀器可能有一個(gè)刷新命令,郵件應(yīng)用可能有下載郵件命令,等等。
Matt Neuburg 的 AppleScript 權(quán)威指南 值得一讀,盡管它是 2006 年出版的,但是從那以后并沒(méi)有發(fā)生太大的改變。Matt 還寫(xiě)有一篇 Cocoa 應(yīng)用添加腳本支持的教程。該教程絕對(duì)值得一讀,它比這篇文章更加詳細(xì)。
這有一個(gè) WWDC 2014 Session 的視頻,是關(guān)于 JavaScript 的自動(dòng)化的,其中談到了新的 JavaScript OSA 語(yǔ)言。(多年以前 Apple 曾提出,總有一天會(huì)出現(xiàn) AppleScript 的程序員的特有語(yǔ)言,因?yàn)樽匀徽Z(yǔ)言對(duì)寫(xiě) C 和 C 類(lèi)語(yǔ)言的人說(shuō)略有一點(diǎn)怪。JavaScript 可以被認(rèn)為是程序員的特有語(yǔ)言。)
當(dāng)然,Apple 有關(guān)于這些技術(shù)的文檔:
此外,請(qǐng)參閱 Apple 的 Sketch 應(yīng)用,它實(shí)現(xiàn)了腳本化。