Xcode提供了XcodeKit,来允许我们编写Xcode的扩展。但是这个Extension功能有限,只能处理当前打开的文件的源码。 我们可以用来:格式化、自动生成代码等。 常用的Extension有XCFormat,可以格式化Swift和OC代码,可以在AppStore中下载。 整体开发插件的功能还是比较简单,我们从头演示一个移除一行开头的数字的插件。
创建Xcode Extension工程
创建一个macOS工程
这个macOS工程,对应从App Store下载完Extension后,在桌面上生成的那个App。你可以在这个App中做Extension设置相关的界面。
新建一个Xcode Source Editor Extention Target
运行Extension Target
点击运行Extension Target,选择Xcode,此时会打开一个灰色的Xcode。
打开我们的插件的macOS工程。
选择Editor
菜单,底部就出现了我们的自定义菜单。
如果菜单没有出现,那么请修改一下Extension Target中XcodeKit的链接方式,选择Embed & Sign
。
Xcode Source Editor Extention
默认的Xcode Source Editor Extention
会为你生成两个文件:
SourceEditorExtension
: 这个文件定义你小菜单中的内容和extension启动时的回调。SourceEditorCommand
: 是小菜单中的命令对应的实现类。
小菜单的定义
小菜单的定义有两种方式,一种是通过SourceEditorExtension
的代码定义。一种是通过Info.plist
定义。代码定义的优先级更高,会覆盖Info.plist中的定义。
在Info.plist中定义
每个命令就是小菜单中的一个命令对应。
- XCSourceEditorCommandClassName: 这个命令对应的实现类。格式是moduleName + 实现类的类名:
$(PRODUCT_MODULE_NAME).YourImplClassName
。 - XCSourceEditorCommandIdentifier:你的这个命令的一个唯一id。格式是bundleId +一个字符串:
$(PRODUCT_BUNDLE_IDENTIFIER).SourceEditorCommand
- XCSourceEditorCommandName:菜单中出现的命令的名字
在SourceEditorExtension中定义小菜单
class SourceEditorExtension: NSObject, XCSourceEditorExtension {
/*
func extensionDidFinishLaunching() {
// If your extension needs to do any work at launch, implement this optional method.
}
*/
var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] {
// If your extension needs to return a collection of command definitions that differs from those in its Info.plist, implement this optional property getter.
let command: [XCSourceEditorCommandDefinitionKey: Any] = [
//格式moduleName.命令实现类的类名
.classNameKey: "GxEditorToolsExtension.SourceEditorCommand",
//格式bundleId.自定义字符串
.identifierKey: "com.haixue.GxXcodeEditorTools.GxEditorToolsExtension.removeLinePrefixNum",
//自定义名称
.nameKey: "我的小菜单"
]
return [command]
}
}
命令的实现
实现类都遵循XCSourceEditorCommand
协议。
当我们点击小菜单,会通过command的XCSourceEditorCommandClassName
找到对应的实现类。调用实现类的func perform(...)
方法。
我们看到新建项目默认为我们生成了一个command模板SourceEditorCommand。
class SourceEditorCommand: NSObject, XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
// Implement your command here, invoking the completion handler when done. Pass it nil on success, and an NSError on failure.
completionHandler(nil)
}
}
实现我们自己的命令
import Foundation
import XcodeKit
class RemoveLinePrefixNumCommand: NSObject, XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
// Implement your command here, invoking the completion handler when done. Pass it nil on success, and an NSError on failure.
let lines = invocation.buffer.lines
let result = lines.map { "\($0)" }.map{ self.removePrefixNum(str: $0) }
invocation.buffer.lines.removeAllObjects()
invocation.buffer.lines.addObjects(from: result)
completionHandler(nil)
}
func removePrefixNum(str: String) -> String {
var chars: [Character] = []
//空格和数字
let sets: Set<Character> = [" ", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
var shouldCheck = true
for c in str {
if shouldCheck {
if sets.contains(c) {
chars.append(" ")
} else {
chars.append(c)
shouldCheck = false
}
} else{
chars.append(c)
}
}
return String(chars)
}
}
//可以看到invocation有用的也只有一个Buffer对象
@interface XCSourceEditorCommandInvocation : NSObject
/** The identifier of the command the user invoked. */
@property (readonly, copy) NSString *commandIdentifier;
/** The buffer of source text on which the command can operate. */
@property (readonly, strong) XCSourceTextBuffer *buffer;
@property (copy) void (^cancellationHandler)(void);
@end
///buffer,又只能操作tab、lines、selections。
@interface XCSourceTextBuffer : NSObject
@property (readonly, copy) NSString *contentUTI;
@property (readonly) NSInteger tabWidth;
@property (readonly) NSInteger indentationWidth;
@property (readonly) BOOL usesTabsForIndentation;
@property (readonly, strong) NSMutableArray <NSString *> *lines;
@property (readonly, strong) NSMutableArray <XCSourceTextRange *> *selections;
@property (copy) NSString *completeBuffer;
@end
xcodeKit只能操作文本,像语法解析等都得借助别的工具来实现。
调试
在工程中打上断点,点击运行。
在灰色xcode中打开我们工程,点击小菜单。就会进入我们断点。
集成到Xcode的Editor菜单
进入系统偏好设置--扩展,选中我们的插件钩上即可
制作安装包
archive
,然后选择copy app,得到一个.app
新建一个文件夹,将.app和应用程序的提升文件一起放入,重命名提升文件为Application。
打开磁盘工具
选择刚刚的文件夹,就制作成了dmg。