前言

Xcode是一个每天都有成千上万开发者使用的IDE( 集成开发环境),它是一个非棒棒的工具,但是有时候为了提高开发效率你可能想自定义一些它的特性和行为。

在Xcode 7的时候,开发者可以在Xccode运行的时候通过注入代码去实现插件的功能。插件可以在一个Alcatraz这个优秀的APP上面提交和分发。不过这一切在在Xcode 8上已经不再可能。

Xcode 8验证每个库和包以防止恶意代码未经您的许可运行。当Xcode启动的时候,先前通过Alcatraz安装的插件不会再被加载。不是一切都没有了,苹果公司在今年的WWDC上宣布了可以通过开发Xcode source editor extensions使得每个人都可以扩展现有的源代码编辑功能。让我们一起来看看通过这些扩展我们能做什么。

1. 好的开端

Xcode 8 source editor extensions 是一个好的开始。如果你使用Xcode工作了一段时间,你可能会迫切地希望一些具体的工作可以在Xcode里面自动完成。Source editor extensions允许第三方应用程序去修改源文件,这可以帮助你提高工作效率。

现阶段,extensions只能和源代码交互。这就意味着不是每一个Alcatraz的插件都可以被source editor extension取代。但是谁知道未来会带来什么?(译者注:作者的意思苹果会不断完善这个功能的)。

每个extension都必须要要包含在一个macOS app里面。比如,你可以给你的extensions添加偏好设置和关于它可以用来做什么的解释说明,然后你可以把它提交到Mac App Store。还要注意的是,每个extension运行在独立的进程里面,如果extension崩溃了,Xcode不会崩溃,不过它会提示一个信息表明extension不能完成它的工作了。

此外,extensions还没有UI交互,它们仅仅能够直接的修改代码当开发者调用相关命令的时候。比如它们不能在后台运行。

我建议观看 WWDC 2016 session about source editor extensions,它不仅仅解释了如何开发 Xcode source editor extensions,同时也展示了一些技巧和快捷方式去加速开发。

2. 概述

在这个教程中,我们将开发一个extension用于清楚Swift语言中的闭包语法。Xcode自动使用括号完成了一个闭包语法。不过为了简单起见,我们可以省略它们。这个任务是很容易自动完成的如果把它封装到source editor extension里面。

我们到底要开发什么呢?简单的解释就是实现一个可以把闭包转化为更简单更清洁语法的extension。下面来看一个具体的例子。

1
2
3
4
5
6
7
8
// Before
session.dataTask(with: url) { (data, response, error) in
}
// After
session.dataTask(with: url) { data, response, error in
}

3. 安装工程

首先,需要安装Xcode 8,可以从Apple’s developer website下载。Xcode 8在OS X 10.11和macOS 10.12都可以运行。

创建一个新的类型为 Cocoa ApplicationOS X项目,项目命名为 CleanClosureSyntax 。确保你是选择的语言是 swift,因为在这个教程中将会使用 Swift 3
这里写图片描述
创建好项目之后,我们把注意力集中到创建 Xcode source editor extension上面来。打开File菜单,选择New->Target,在左边的面板上,选择OS X然后从列表中选择 Xcode Source Editor Extension
这里写图片描述
点击Next然后设置Product NameCleaner,如果Xcode提示你新创建的Scheme是否需要激活,点击Activate激活Scheme

4.项目结构

我们一起来分析一下Xcode给我们创建了什么。打开Cleaner去看它包含些什么。
这里写图片描述
该教程中,我们不会修改 SourceEditorExtension.swift ,但是如果以后你要更进一步的自定义extension的话,可能会使用到它。extensionDidFinishLaunching()在extension启动的时候会被调用,如果需要的话开发者可以在此方法里面做一些初始化的东西。commandDefinitions属性的getter方法可以在动态的展示或是隐藏特定的指令的时候使用。

SourceEditorCommand.swift是整个extension的核心。在这个文件里面可以实现extension的相关逻辑。perform(with:completionHandler:)方法在用户启动你的extension的时候被调用。XCSourceEditorCommandInvocation对象包含了一个buffer属性,这个属性主要是用来访问当前选中文件的源代码。如果一切顺利的话,completion handler将会以参数为nil进行调用,否则将会给它传递一个NSError实例。

5. 实现extension

现在工程里面已经包含了所有需要的targets,我们将要开始开发extension。概况的说,我们将要移除Swift文件中所有闭包里面的括号。主要分以下三步:

  • 找到包含闭包的行
  • 从特定的行里面移除两个括号
  • 重置被修改的行(译者注:原文是substitute back the modified line ,结合代码感觉重置比较恰当)

    我们可以使用正则表达式去遍历每一行代码是否含有闭包。如果你想进一步学习正则表达式可以参考Akiel的教程Swift and regular expressions 。开发者可以使用RegExr 去测试正则表达式。看下面的截图看看我是怎么测试正则表达式的。
    这里写图片描述
    打开SourceEditorCommand.swift 文件修改` perform(with:completionHandler:) 方法为如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (NSError?) -> Void ) -> Void {
var updatedLineIndexes = [Int]()
// 1. Find lines that contain a closure syntax
for lineIndex in 0 ..< invocation.buffer.lines.count {
let line = invocation.buffer.lines[lineIndex] as! NSString
do {
let regex = try RegularExpression(pattern: "\\{.*\\(.+\\).+in", options: .caseInsensitive)
let range = NSRange(0 ..< line.length)
let results = regex.matches(in: line as String, options: .reportProgress, range: range)
// 2. When a closure is found, clean up its syntax
_ = results.map { result in
let cleanLine = line.remove(characters: ["(", ")"], in: result.range)
updatedLineIndexes.append(lineIndex)
invocation.buffer.lines[lineIndex] = cleanLine
}
} catch {
completionHandler(error as NSError)
}
}
// 3. If at least a line was changed, create an array of changes and pass it to the buffer selections
if !updatedLineIndexes.isEmpty {
let updatedSelections: [XCSourceTextRange] = updatedLineIndexes.map { lineIndex in
let lineSelection = XCSourceTextRange()
lineSelection.start = XCSourceTextPosition(line: lineIndex, column: 0)
lineSelection.end = XCSourceTextPosition(line: lineIndex, column: 0)
return lineSelection
}
invocation.buffer.selections.setArray(updatedSelections)
}
completionHandler(nil)
}

找到使用闭包的行

首先创建了一个Int类型的数组用于存储我们修改过的行。这样我们就不需要更新所有的行,我们只需要替换掉被修改过的行。

枚举invocation.buffer对象中所有的行,找出和RegularExpression匹配的对象的。移除掉正则表达式中的转义字符,正则表达式如下:

1
{.*(.+).+in

正则将会匹配当字符串包含如下特性的时候

  • 有一个花括号({),后面跟着若干个字符但是不包括换行符(\n)。
  • 需要有一个开括号(),后面跟着若干字符,该部分包含的是闭包的参数。
  • 然后需要有一个闭括号()),后面跟着若干字符,这部分字符是可选返回类型
  • 最后关键字in需要找到

如果RegularExpression对象匹配失败,则在调用completionHandler时把error作为参数。如果某一行字符串匹配所有的条件,那就说明闭包已经找到。

移除闭包里面括号

当满足条件的闭包被找到后,调用NSString里面的工具方法去移除括号。在调用的时候需要传入闭包的范围(译者注:就是NSRange)从而避免移除闭包之外的其他括号。

更新行

最后一步代码检查是否至少有一行被找到。如果条件成立,调用setArray()在正确的位置重置新的代码。此时,传入nil作为参数调用completionHandler,这样Xcode便知道extension完成了正确的工作。

最后,实现NSStringremove(characters:range:)方法。添加NSStringextension(译者注:此处的extension不同于本文一直讲解的extension,此处是Swift的一种基本语法,是类的扩展)到文件里面。

1
2
3
4
5
6
7
8
9
10
extension NSString {
// Remove the given characters in the range
func remove(characters: [Character], in range: NSRange) -> NSString {
var cleanString = self
for char in characters {
cleanString = cleanString.replacingOccurrences(of: String(char), with: "", options: .caseInsensitiveSearch, range: range)
}
return cleanString
}
}

6. 测试

Xcode 8 带来了一个非常优秀方法用来测试extensions。首先,如果如果你的Xcode是运行在 OS X 10.11 El Capitan的话,打开Terminal,执行下面的命令,然后重启Mac。

1
sudo /usr/libexec/xpccachectl

做完以上工作后,选择正确的scheme后编译运行extensions。当询问你去运行哪一个App的时候,查找Xcode并且确保选择了Xcode 8 Beta版本(译者注:作者写此文章时候正式版还没有发布),一个新的灰色图标Xcode被打开,这样可以便于开发者区分哪一个Xcode是用来测试extension的。

在新的Xcode实例中,创建一个新的工程或是打开一个存在的工程。然后执行Editor > Clean Closure > Source Editor Command,需要确保在当前的文件里面含有一个闭包。这样就可以看到如下的效果,刚才开发的extension工作了!
这里写图片描述
Source Editor Command是命令默认的名字。开发者可以在extension的Info.plist文件里面修改。打开之后修改为 Clean Syntax
这里写图片描述
当然,同样可以设置设置快捷键去自动调用Clean Syntax命令。打开Xcode的Preferences,选择Key Bindings 。搜索Clean Syntax,可以看到命令出现了,点击它的右边然后输入你想使用的快捷键,例如:Command-Alt-Shift-+。现在,回到源文件,然后使用快捷键就可以直接调用它了。

7. 技巧和窍门

Xcode 8和source editor extensions 依然在测试阶段。下面的一些方法可能会帮助你调试一些你遇到的问题。

如果你的extension在Xcode的测试实例中变得不可选择,杀掉com.apple.dt.Xcode.AttachToXPCService进程然后再次运行extension
这里写图片描述
仅重置缓存区里面修改过的行,这使得extension运行得更快而且不容易被Xcode杀掉进程。如果Xcode检测到你的extension花费太长时间的话有可能会杀掉它。

如果想集成多个命令,那就必须分配给每个指令不同的标识,然后使用XCSourceEditorCommandInvocation对象的commandIdentifier属性来识别用户触发的是哪一个。

总结

创建 Xcode source editor extension 是非常容易的。可以通过创建source editor extension去提高开发的效率,开始吧,动手去做。苹果公司引入了新的方式,广大开发者可以分享签名过的插件通过Mac App Store,这样一方面减轻了自己的工作,另一方面也可以让其他开发者从中受益。

该教程的代码地址: GitHub
原文地址
译者补充说明:目前Xcode8很不稳定,我自己在测试的时候Editor下面的命令选项时有时没有。还需要继续跟进和研究,