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