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