本文由 简悦 SimpRead 转码, 原文地址 www.kodeco.com
允许用户编写脚本来控制你的 OS X 应用程序--赋予它前所未有的可用性。发现 h......
更新 9/21/16: 本教程已针对 Xcode 8 和 Swift 3 进行了更新。
作为一名应用程序开发人员,几乎不可能想到人们想要使用您的应用程序的所有方式。如果能让您的用户创建脚本,根据他们的个人需求定制您的应用程序,岂不是很酷?
有了 Applescript 和 Javascript for Automation (JXA),您就可以做到这一点!在本教程中,你将了解如何为示例应用程序添加脚本功能。首先,你将学习如何使用脚本控制现有应用程序,然后扩展示例应用程序以允许自定义脚本操作。
开始学习
下载示例项目,在 Xcode 中打开并构建和运行,看看它看起来如何:
该应用程序显示了未来几天内到期的任务列表,以及与每个任务相关的标签。它使用大纲视图按到期日期对任务进行分组。
您可能已经注意到,您无法添加、编辑或删除任何任务。这是设计上的问题--这些操作将由用户自动化脚本处理。
看看项目中的文件:
- 有 2 个模型类文件:Task.swift 和 Tag.swift。这些是您要编写脚本的类。
- ViewController 组处理显示并监视数据的变化。
- Data组中有一个包含示例任务的文件和一个DataProvider,后者负责读取这些任务并处理到达的任何编辑。
- AppDelegate 使用一个
DataProvider对象来保存应用程序的任务记录。 - ScriptableTasks.sdef文件是一个至关重要的文件......稍后你将详细了解它。
本教程还有示例脚本;在此下载。本软件包中有两个文件夹:一个是 AppleScript 文件夹,另一个是 JavaScript 文件夹。由于本教程的重点并不在于如何编写脚本,您将使用下载的每个脚本来测试您将添加到Scriptable Tasks中的功能。
闲话少说,是时候开始编写脚本了!:]
使用脚本编辑器
打开 Applications/Utilities 中的 Script Editor 应用程序,并打开一个新文档:
你会在顶部工具栏中看到一组四个按钮: 记录(Record)、停止(Stop)、运行(Run)和编译(Compile)。编译会检查你的脚本语法是否正确,而运行则会执行你所期望的操作。
在窗口底部,你会看到三个切换视图的图标。Description 可以让你添加一些关于脚本的信息,而 Result 则显示运行脚本的最终结果。最有用的选项是第三个按钮: Log(日志)。
日志提供另外四个选项: 结果、消息、事件和回复。其中回复信息量最大,因为它会显示每条命令的日志和该命令的返回值。在测试任何脚本时,我强烈推荐使用 Log in Replies 模式。
注意: 如果你打开一个 AppleScript 文件,发现其中包含如下代码:
«class TaSk» whose «class TrFa» is false and «class CrDa»,点击Compile,它就会被翻译成可读的 AppleScript,前提是你已经安装了目标应用程序。
本教程将介绍两种脚本语言。第一种是 1991 年随 Mac System 7 一起推出的 AppleScript,它使用类似英文的语法,使编码员和非编码员都能使用。
第二种是 OSX Yosemite 引入的 JavaScript for Automation (JXA),它允许编码员使用熟悉的 JavaScript 语法来构建自动化任务。
本教程中的脚本将以 AppleScript 和 JXA 两种语言呈现,因此你可以自由选择自己喜欢的语言。]
注: 在本教程中,脚本代码片段首先以 AppleScript 呈现,紧接着是 JavaScript 版本。
使用 TextEdit 探索应用程序脚本
你的 Mac 上已经安装了一个支持脚本的小应用程序: TextEdit。在 Script Editor 中,选择 Window/Library 并查找 TextEdit 条目。如果没有,请单击顶部的Plus按钮,导航到Applications文件夹并添加TextEdit。然后双击 TextEdit 条目,打开 TextEdit 词典:
每个可编写脚本的应用程序都有一个字典,存储在脚本定义 (SDEF) 文件中。字典会告诉你应用程序有哪些对象、对象有哪些属性以及应用程序响应哪些命令。在上面的屏幕截图中,你可以看到 TextEdit 拥有段落,而段落拥有颜色和字体属性。你将使用这些信息为一些文本添加样式。
从 AppleScript 或者 JavaScript 文件夹打开 1. TextEdit Write.scpt 。运行该脚本;你将看到 TextEdit 创建并保存文档。
现在你有了一个新文档,但它需要一些样式。打开 2. TextEdit Read Edit.scpt,运行该脚本,你会看到文档重新打开,并按照脚本进行了样式设置。
虽然深入研究实际脚本超出了本教程的范围,但请随时详细阅读脚本,了解它们是如何作用于 TextEdit 文档的。
正如导言中提到的,所有应用程序在某种程度上都可以使用脚本。要查看其实际效果,请确保 "可编写脚本的任务 "正在运行。然后,在脚本编辑器中打开一个新的脚本窗口,并根据你使用的语言输入以下脚本之一:
## AppleScript
tell application "Scriptable Tasks" to quit
或
// JavaScript
Application("Scriptable Tasks"). quit();
点击Run,Scriptable Tasks 就会退出。将脚本更改为以下内容,然后再次单击Run:
tell application "Scriptable Tasks" to launch
或
Application("Scriptable Tasks").launch();
应用程序会重新启动,但不会进入前台。要使应用程序成为焦点,请将上述脚本中的 "启动 "改为 "激活",然后单击Run。
现在,你已经看到应用程序可以响应脚本命令,是时候在你的应用程序中添加这种能力了。
使您的应用程序可编写脚本
应用程序的脚本定义文件定义了应用程序的功能;它有点像 API。该文件位于应用程序项目中,规定了几项内容:
- 标准脚本对象和命令,如
window、make、delete、count、open和quit。 - 您自己的脚本对象、属性和自定义命令。
为了使应用程序中的类可以编写脚本,您需要对应用程序进行一些更改。
首先,脚本接口使用键值编码(Key-Value-Coding)来获取和设置对象的属性。在 Objective-C 中,所有对象都会自动遵守 KVC 协议,但 Swift 对象不会这样做,除非你将它们设为 NSObject 的子类。
其次,可编写脚本的类需要一个脚本接口能识别的 Objective-C 名称。为避免命名空间冲突,Swift 对象的名称会被混淆,以提供唯一的表示。通过在类定义前添加 @objc(YourClassName),就可以为它们提供一个脚本引擎可以使用的名称。
可脚本类需要对象指定符来帮助定位应用程序或父对象中的特定对象,最后,应用程序委托必须能够访问数据存储,以便将应用程序的数据返回给脚本。
你不一定要从头开始创建自己的脚本定义文件,因为 Apple 提供了一个标准的 SDEF 文件供你使用。在 /System/Library/ScriptingDefinitions/ 目录下查找CocoaStandard.sdef。在 Xcode 中打开该文件并查看;它是一个 XML 文件,包含特定的标头、字典和标准套件。
这是一个有用的起点,您可以将 XML 复制并粘贴到您自己的 SDEF 文件中。不过,为了代码的简洁,让 SDEF 文件中充满应用程序不支持的命令和对象并不是一个好主意。为此,示例项目中包含了一个已删除所有不必要条目的 SDEF 初始文件。
关闭 CocoaStandard.sdef 并打开 ScriptableTasks.sdef。在 "此处插入可脚本任务套件 "注释的结尾处添加以下代码:
<!-- 1 -->
<suite name="Scriptable Tasks Suite" code="ScTa" description="Scriptable Tasks suite.">
<!-- 2 -->
<class name="application" code="capp" description="An application's top level scripting object.">
<cocoa class="NSApplication"/>
<!-- 3 -->
<element type="task" access="r">
<cocoa key="tasks"/>
</element>
</class>
<!-- Insert command here -->
<!-- 4 -->
<class name="task" code="TaSk" description="A task item" inherits="item" plural="tasks">
<cocoa class="Task"/>
<!-- 5 -->
<property name="id" code="ID " type="text" access="r"
description="The unique identifier of the task.">
<cocoa key="id"/>
</property>
<property name="name" code="pnam" type="text" access="rw"
description="The title of the task.">
<cocoa key="title"/>
</property>
<!-- 6 -->
<property name="daysUntilDue" code="CrDa" type="number" access="rw"
description="The number of days before this task is due."/>
<property name="completed" code="TrFa" type="boolean" access="rw"
description="Has the task been completed?"/>
<!-- 7 -->
<!-- Insert element of tags here -->
<!-- Insert responds-to command here -->
</class>
<!-- Insert tag class here -->
</suite>
这块 XML 做了很多工作。一点一点来
-
最外层的元素是 "套件",因此您的 SDEF 文件现在有两个套件: "标准套件" 和 "可脚本任务套件"。SDEF 文件中的所有内容都需要一个四字符代码。Apple 代码几乎都是小写字母,您将在特定用途中使用其中的一些代码。对于您自己的套件、类和属性,最好随机混合使用大写、小写和符号,以避免冲突。
-
下一部分定义应用程序,必须使用代码
"capp"。您必须指定应用程序的类;如果您已经子类化了NSApplication,则应在此处使用子类名称。 -
应用程序包含
元素。在此应用程序中,元素存储在应用程序委托中名为tasks的数组中。用脚本术语来说,元素就是应用程序或其他对象可以包含的对象。 -
最后一块定义了应用程序所包含的 "任务 "类。访问多个任务的复数名称是 "tasks"。应用程序中支持该对象类型的类是
Task。 -
前两个属性很特殊。请看它们的代码:
"ID"和"pnam"。ID"(注意字母后面的两个空格)是对象的唯一标识符。"pnam"指定对象的name属性。您可以直接使用其中任一属性访问对象。ID "是只读属性,因为脚本不应更改唯一标识符,而 "pnam "是读写属性。这两个属性都是文本属性。pnam "属性映射到 "任务 "对象的 "标题 "属性。
-
其余两个属性分别是
daysUntilDue的数字属性和completed的布尔属性。它们在对象和脚本中使用相同的名称,因此无需指定cocoa key。 -
"插入..."注释是占位符,用于在需要向该文件添加更多内容时使用。
打开 Info.plist,在条目下方的空白处单击右键并选择 Add Row。键入大写字母 S,建议列表将滚动到 Scriptable。选择它并将设置更改为YES。
重复此过程,选择下一项: Scripting定义文件名。将其设置为 SDEF 文件的名称: ScriptableTasks.sdef。
如果您希望以源代码的形式编辑 Info.plist,也可以在主 dict 中添加以下条目:
<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>ScriptableTasks.sdef</string>
现在,您必须修改应用程序委托,以处理通过脚本发出的请求。
打开 AppDelegate.swift 文件,在文件末尾添加以下内容:
extension AppDelegate {
// 1
override func application(_ sender: NSApplication, delegateHandlesKey key: String) -> Bool {
return key == "tasks"
}
// 2
func insertObject(_ object: Task, inTasksAtIndex index: Int) {
tasks = dataProvider.insertNew(task: object, at: index)
}
func removeObjectFromTasksAtIndex(_ index: Int) {
tasks = dataProvider.deleteTask(at: index)
}
}
下面是上面代码中的内容:
- 当脚本请求获取
tasks数据时,此方法将确认应用程序委托可以处理该数据。 - 如果脚本试图插入、编辑或删除数据,这些方法将把这些请求传递给
dataProvider.
要使脚本可以使用 Task 模型类,还需要进行一些编码。
打开 Task.swift 并将类定义行改为以下内容:
@objc(Task) class Task: NSObject {
Xcode 会立即抱怨 "init "需要 "override "关键字,所以让 Fix-It 来做吧。这是必需的,因为该类现在有了一个超类:
override init() {
Task.swift还需要一个改动:对象指定器。在 Task 类中插入以下方法:
override var objectSpecifier: NSScriptObjectSpecifier {
// 1
let appDescription = NSApplication.shared().classDescription as! NSScriptClassDescription
// 2
let specifier = NSUniqueIDSpecifier(containerClassDescription: appDescription,
containerSpecifier: nil, key: "tasks", uniqueID: id)
return specifier
}
依次查看每个编号注释:
- 获取应用程序类的描述,因为应用程序是任务的容器。
- 根据应用程序中的 id 获取任务描述。这就是为什么任务类有一个
id属性--以便正确指定每个任务。
您终于可以开始编写应用程序脚本了!
编写应用程序脚本
在开始之前,确保退出脚本编辑器可能打开的任何正在运行的应用程序实例。
构建并运行 "可编写脚本的任务";右键单击 Dock 中的图标,然后在 Finder 中选择选项/显示。退出Script Editor应用程序并重新启动,让它接收对你的应用程序所做的更改。
打开 Library 窗口,将 Scriptable Tasks 应用程序从 Finder 拖入 Library 窗口。
如果出现应用程序不可用脚本的错误提示,请尝试退出脚本编辑器并重新启动,因为它有时会无法注册刚创建的应用程序。如果仍然无法导入,请返回并仔细检查对 SDEF 文件所做的更改。
双击库中的 Scriptable Tasks,查看应用程序的字典:
您将看到标准套件和可脚本任务套件。单击可编写脚本的任务套件,您将看到您输入 SDEF 文件的内容。应用程序包含任务,一个任务有四个属性。
使用工具栏中的Language弹出窗口,将字典中的脚本语言更改为JavaScript。你将看到相同的信息,但有一个重要的变化。类和属性的情况发生了变化。我不知道这是为什么,但这是你需要注意的 "问题 "之一。
在Script Editor中新建一个脚本文件,并将编辑器设置为显示Log/Replies。测试以下任一脚本,确保在语言弹出窗口中选择适当的语言:
tell application "Scriptable Tasks"
get every task
end tell
或
app = Application("Scriptable Tasks");
app.tasks();
在日志中,你将看到按 ID 列出的任务列表。要获取更多有用信息,请按如下方式编辑脚本:
tell application "Scriptable Tasks"
get the name of every task
end tell
或
app = Application("Scriptable Tasks");
app.tasks.name();
再试试先前下载的几个示例脚本。运行脚本时,确保将脚本编辑器设置为显示 Log/Replies ,这样就可以看到运行过程中的结果。
每个脚本在再次运行前都会退出应用程序;这样做是为了在编辑后重置数据,以便示例脚本按预期运行。通常情况下,你不会在自己的脚本中这样做。
注: 脚本编辑器会在您创建应用程序的更新版本时变得非常混乱,因为如果您打开了正在使用应用程序的脚本,脚本编辑器会一直尝试保持一个版本在运行。这通常会导致应用程序变成旧版本,因此在每次构建之前,请退出应用程序。
如果你在任何时候看到两个版本的 "脚本任务 "应用程序运行,或者在任何示例中出现脚本错误,你就可以确定脚本编辑器使用了错误版本的应用程序。最简单的解决方法是退出应用程序的所有副本并退出脚本编辑器。清理 Xcode 构建(Product/Clean),然后再次构建并运行。
重新启动脚本编辑器,当它打开脚本时,点击编译,然后点击运行。如果还不行,请删除 ~/Library/Developer/Xcode/DerivedData 中应用程序的衍生数据。
试试下面两个示例脚本:
3. 获取任务.scpt
该脚本使用各种过滤器检索任务数和任务名称。请注意以下几点:
- JavaScript 从 0 开始计数,AppleScript 从 1 开始计数。
- 文本搜索不区分大小写。
4. 添加编辑任务.scpt
该脚本添加新任务,切换第一个任务的 "已完成 "标志,并尝试创建与另一个任务同名的任务。
嗯......创建同名任务成功了!现在你有了两个 "喂猫 "任务。猫咪会很高兴,但就本应用程序而言,任务名称应该是唯一的。如果试图添加一个已被使用的任务名,就会产生错误。
回到 Xcode,查看 AppDelegate.swift,可以看到当脚本要插入一个对象时,应用程序委托会将调用传递给它的 "dataProvider"。在 DataProvider.swift 中,查看 insertNew(task:at:),它会将现有任务插入数组或将新任务追加到末尾。
是时候在这里添加检查了。用以下代码替换该函数:
mutating func insertNew(task: Task, at index: Int) -> [Task] {
// 1
if taskExists(withTitle: task.title) {
// 2
let command = NSScriptCommand.current()
command?.scriptErrorNumber = errOSACantAssign
command?.scriptErrorString = "Task with the title '\(task.title)' already exists"
} else {
// 3
if index >= tasks.count {
tasks.append(task)
} else {
tasks.insert(task, at: index)
}
postNotificationOfChanges()
}
return tasks
}
以下是每个注释部分的作用:
- 使用现有函数检查是否已存在具有此名称的任务。
- 如果名称不唯一,则
- 获取调用此函数的脚本命令的引用。
- 设置命令的
errorNumber和errorString属性;errOSACantAssign是标准 AppleScript 错误代码之一。这些信息将被发送回调用脚本。
- 如果名称_是_唯一的:
- 像以前一样处理任务。
- 发布数据更改通知。ViewController 会看到并更新显示内容。
如果正在运行,则退出应用程序,然后构建并运行您的应用程序。运行 4. 添加编辑任务 这次您应该会看到一个错误对话框,并且不会创建重复任务。对不起,猫...
5. 删除任务.scpt
该脚本用于删除任务,检查特定任务是否存在并在可能的情况下将其删除,最后删除所有已完成的任务。
使用嵌套对象
在示例应用程序中,第二列显示了分配给每个任务的标记列表。到目前为止,您还无法通过脚本来处理它们,是时候解决这个问题了!
对象指定器可以处理对象的层次结构。这就是我们这里的情况,应用程序拥有任务,而每个任务拥有自己的标记。
与 "任务 "类一样,您需要让 "标记 "可用于编写脚本。
打开 Tag.swift 并进行以下修改:
- 将类定义行改为
@objc(Tag) class Tag: NSObject { - 在
init中添加override关键字。 - 添加对象指定方法:
override var objectSpecifier: NSScriptObjectSpecifier {
// 1
guard let task = task else { return NSScriptObjectSpecifier() }
// 2
guard let taskClassDescription = task.classDescription as? NSScriptClassDescription else {
return NSScriptObjectSpecifier()
}
// 3
let taskSpecifier = task.objectSpecifier
// 4
let specifier = NSUniqueIDSpecifier(containerClassDescription: taskClassDescription,
containerSpecifier: taskSpecifier, key: "tags", uniqueID: id)
return specifier
}
上述代码相对简单:
- 检查标签是否有指定任务。
- 检查该任务的类描述是否正确。
- 获取父任务的对象规范。
- 为任务中包含的标记构建对象说明符并返回。
在 SDEF 文件的 "此处插入标签类 "注释处添加以下内容:
<class name="tag" code="TaGg" description="A tag" inherits="item" plural="tags">
<cocoa class="Tag"/>
<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"
description="The name of the tag.">
<cocoa key="name"/>
</property>
</class>
这与 Task 类的数据非常相似,但标签只有两个公开的属性: id 和 name。
现在必须编辑 Task 部分,以表明它包含标记元素。
在任务类 XML 的 Insert element of tags here 注释处添加以下代码:
<element type="tag" access="rw">
<cocoa key="tags"/>
</element>
退出应用程序,然后再次构建并运行应用程序。
返回到Script Editor(脚本编辑器);如果Scriptable Tasks dictionary(可脚本任务字典)处于打开状态,请关闭并重新打开它。查看其中是否包含标记信息。
如果没有,请从Library中删除Scriptable Tasks条目,然后通过将应用程序拖入窗口来重新添加:
尝试以下脚本之一:
tell application "Scriptable Tasks"
get the name of every tag of task 1
end tell
或
app = Application("Scriptable Tasks");
app.tasks[0].tags.name();
现在,应用程序可以让您检索标签,但如何添加新标签呢?
您可能已经注意到,在 Tag.swift 中,每个 Tag 对象都有一个指向其所属任务的弱引用。这有助于在获取对象规范时创建链接,因此在为任务分配新标签时必须设置此任务属性。
打开 Task.swift 并在 Task 类中添加以下方法:
override func newScriptingObject(of objectClass: AnyClass,
forValueForKey key: String,
withContentsValue contentsValue: Any?,
properties: [String: Any]) -> Any? {
let tag: Tag = super.newScriptingObject(of: objectClass, forValueForKey: key,
withContentsValue: contentsValue,
properties: properties) as! Tag
tag.task = self
return tag
}
此方法会发送到新对象的容器中,这就是为什么要把它放到 Task 类而不是 Tag 类中。调用将传递给 super 以获取新标签,然后分配 task 属性。
退出并构建和运行应用程序。现在运行示例脚本 6. 带有标签的任务.scpt,该脚本会列出标签名称,列出带有指定标签的任务,并删除和创建标签。
添加自定义命令
在使应用程序具有脚本功能时,还有一个步骤可以采取:添加自定义命令。在早期的脚本中,您可以直接切换任务的 "已完成 "标记。但如果脚本不直接更改属性,而是使用命令来完成这项操作,岂不是更好、更安全?
请看下面的脚本:
mark the first task as "done"
mark task "Feed the cat" as "not done"
我相信你已经在找 SDEF 文件了,而且你是对的:命令必须先在那里定义。
这里有两个步骤:
- 告诉应用程序该命令的存在及其参数。
- 告诉任务类它响应命令,以及调用什么方法来实现它。
在 Scriptable Tasks 套件内,但在任何类之外,在 Insert command here 注释处添加以下内容:
<command name="mark" code="TaSktext">
<direct-parameter description="One task" type="task"/>
<parameter name="as" code="DFLG" description="'done' or 'not done'" type="text">
<cocoa key="doneFlag"/>
</parameter>
</command>
"等一下!"你说。"你刚才说代码必须是_4_个字符,而现在我有一个 8 个字符的代码?这是怎么回事?"
在定义方法时,你需要提供一个由两部分组成的代码。这部分代码结合了参数的代码或类型,在本例中就是一个 Task 对象和一些文本。
在 Task 类定义中,在 Insert responds-to command here 注释处添加以下代码:
<responds-to command="mark">
<cocoa method="markAsDone:"/>
</responds-to>
现在返回 Task.swift 并添加以下方法:
func markAsDone(_ command: NSScriptCommand) {
if let task = command.evaluatedReceivers as? Task,
let doneFlag = command.evaluatedArguments?["doneFlag"] as? String {
if self == task {
if doneFlag == "done" {
completed = true
} else if doneFlag == "not done" {
completed = false
}
// if doneFlag doesn't match either string, leave un-changed
}
}
}
markAsDone(_:)的参数是一个NSScriptCommand ,它有两个相关属性: evaluatedReceivers "和 evaluatedArguments。您可以尝试从中获取任务和字符串参数,并使用它们对任务进行相应调整。
退出并重新构建和运行应用程序。检查脚本编辑器中的字典,如果未显示 mark 命令,请删除并重新导入:
现在您应该可以运行 7. Custom Command.scpt 脚本,并查看新命令的运行情况。
注意: Swift 3 更改了向对象发送命令的方式。AppleScript 仍按预期运行,但 JavaScript 中的
mark命令不起作用。我在 JavaScript 版本 7. Custom Command.scpt 中添加了手动切换completed属性的功能。自定义命令.scpt_的 JavaScript 版本中添加了手动切换 "已完成 "属性的功能,但也保留了原来的功能。希望更新后能正常工作。
何去何从?
您可以下载 此处的示例项目 的最终版本。
在这篇制作 Mac 应用程序脚本的教程中,没有足够的篇幅介绍应用程序之间的通信,但要了解应用程序之间的通信,请查看 8. 应用间通信.scpt 中的一些示例。该脚本收集了今天和明天要完成的未完成任务列表,将它们插入到一个新的 TextEdit 文件中,为文本设置样式并保存文件。
要了解有关可脚本应用程序的更多信息,Apple 官方文档Scriptable Applications和 Apple 的Overview of Cocoa Support for Scriptable Applications都是很好的开始。
有兴趣进一步了解 JXA?请查看 Introduction to JavaScript for Automation Release Notes。
希望您喜欢本篇 Mac 应用程序脚本化教程;如果您有任何问题或评论,请加入下面的论坛讨论!