鍍金池/ 教程/ iOS/ 使 Mac 應(yīng)用數(shù)據(jù)腳本化
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

使 Mac 應(yīng)用數(shù)據(jù)腳本化

當(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

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。

Noteland 的對(duì)象模型

有兩個(gè)類,notes(筆記) 和 tags(標(biāo)簽)??赡苡卸鄠€(gè)筆記,而且一個(gè)筆記也許有多個(gè)標(biāo)簽。

NLNote.h 聲明了幾個(gè)屬性: uniqueID,text,creationDatearchived,tags 和一個(gè)只讀的 title 屬性。

Tags 類更加簡單。NLTag.h 聲明了兩個(gè)可腳本屬性: uniqueIDname。

我們希望用戶能夠創(chuàng)建,編輯和刪除筆記和標(biāo)簽,并且能夠訪問和改變除了只讀以外的屬性。

腳本定義文件 (.sdef)

第一個(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)套件

標(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ì)粘貼文件的路徑。)

Noteland 套件

一個(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,字典用來指定鍵的名稱。

Note 類

<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ì)于 creationDatearchived,我們并不需要提供 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>`

Tag 類

Tags 是 NLTap 對(duì)象:

<class name="tag" code="TAG*" description="A tag" inherits="item" plural="tags">
    <cocoa class="NLTag"/>`

Tags 只有兩個(gè)屬性,idname

<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)用程序配置

應(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。

代碼

NSApplication 子類

你可能會(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 類

NLNote.h 聲明了筆記的各個(gè)屬性:uniqueID,textcreationDate,archivedtitletags。

init 方法中設(shè)置 uniqueIDcreationDate,以及將標(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。

Note 作為容器

我們需要超前考慮一點(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 類

NLTaguniqueID, name, 和 note 屬性。

NLTagobjectSpecifier 在概念上和 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)不是過去的日子了。

接下來才是有趣的東西。

AppleScript Editor

啟動(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"}。

完美工作!

擴(kuò)展學(xué)習(xí)

將對(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)了腳本化。