版权声明:本文为博主原创文章,未经博主允许不得转载;如需转载,请保持原文链接。
尝试实现Xcode的Extension扩展
本文旨在学习如何实现一个XcodeExtension扩展
其实网上有大神出了博客,直接参考大神的博客就可以弄出来
从头构建你的第一个 Xcode 扩展 Xcode 插件集:xTextHandler
但是出于想要折腾的心理,还是自己再造个轮子,其实代码都挺简单的,官方开放的接口不多,很容易上手
一个插件能做下面的事情:
- 获取 Xcode 正在编辑的文本
- 获取所有的选中区域
- 替换 Xcode 正在编辑的文本
- 选中 Xcode 正在编辑的文本
- 在 Xcode 的 Editor 菜单里面给你的插件生成一个子菜单,用于调用插件
- 可以在 Xcode 的 Key Binding 里面给插件分配一个快捷键
注:Extension必须使用Xcode8以上;
动手写一个Extension
创建Extension
打开Xcode8,新建一个工程选择Cocoa application

填写工程名称:StrHandleExtension,去掉 test 相关选项

然后选中工程文件,新建一个 Target,从工程模板中选择 Xcode Source Editor Extension,填写 Target 名为:StrHandle

这个时候Xcode会自动生成两个类SourceEditorExtension和SourceEditorCommand
此时的目录结构是这样的:

这个时候,其实这个Extension已经可以运行了;在运行对象框中选中 StrHandle 对象,Command R 运行,它需要附属到一个应用程序上,请注意选择 Xcode 8。运行后,在新起的灰色的 Xcode 8 中打开一个项目,在代码编辑器中,打开 Editor 菜单,不出意外的可以看到我们的新扩展程序的菜单了
但是有一点坑的是,运行的target必须要有signing Team

不然Extension是不会加载的;
注:如果你运行的是 EI Capitan 系统,请在运行此扩展前在终端中输入以下命令并重启电脑:sudo /usr/libexec/xpccachectl
细节实现
两个类都继承自NSObject;
1、SourceEditorExtension服从XCSourceEditorExtension协议,这个协议有两个方法
//Extension启动成功后调用
- (void)extensionDidFinishLaunching
{
// If your extension needs to do any work at launch, implement this optional method.
}
//返回数组配置command,可以配置多个;
- (NSArray <NSDictionary <XCSourceEditorCommandDefinitionKey, id> *> *)commandDefinitions
{
return @[];
}
第二个方法可以直接在Plist里面配置:

XCSourceEditorCommandDefinitions是一个数组,定义了一系列这个扩展程序支持的命令,每个命令包含一个 Identifier, 一个入口类名,及命令名,我们可以将命令名修改为自己喜欢的名字,我将名字改为StrHandle1,并新加了一个item命名为StrHandle2;
2、SourceEditorCommand服从XCSourceEditorCommand协议
主要的逻辑在XCSourceEditorCommand类中,当插件被触发之后,你有机会在代理方法里面拦截到这个消息(XCSourceEditorCommandInvocation),做出处理之后将内容返回给 Xcode;
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler
{
// Implement your command here, invoking the completion handler when done. Pass it nil on success, and an NSError on failure.
completionHandler(nil);
}
invocation(XCSourceEditorCommandInvocation),它提供了源代码文件的文本内容相关的信息,对它的修改将直接生效
completionHandler,这是一个回调 Block,用于通知 Xcode,扩展程序的操作已经完成
XCSourceEditorCommandInvocation 里面会存放一些 meta 数据,其中最重要的是 identifier 和 buffer,identifier 就是用来做区分的,buffer 则是整个插件中最最重要的概念
XCSourceTextBuffer 最重要的环节有两个,
/** The lines of text in the buffer, including line endings. Line breaks within a single buffer are expected to be consistent. Adding a "line" that itself contains line breaks will actually modify the array as well, changing its count, such that each line added is a separate element. */
@property (readonly, strong) NSMutableArray <NSString *> *lines;
/** The text selections in the buffer; an empty range represents an insertion point. Modifying the lines of text in the buffer will automatically update the selections to match. */
@property (readonly, strong) NSMutableArray <XCSourceTextRange *> *selections;
分别表示了传递过来的文本,以及选中区域。所以要做的事情已经一目了然了,流程如下:
- 在菜单或快捷键触发插件
XCSourceEditorCommand拦截了消息- 从
invocation中拿到buffer - 在
buffer中根据当前行,获取到你要的数据,把数据替换后塞回去
为了方便操作,我们可以为扩展命令添加键盘绑定。如下图,在 Xcode 中打开偏好,在 Key Bindings 下找到我们的新菜单命令,添加快捷键:Control Shift S

具体逻辑
这里就可以写自己想要实现的功能了,我这里写的是字符串大写或小写替换功能;
代码比较简单,就不放到github上去了
typedef id(*_IMP) (id, SEL, ...);
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler
{
NSString *identiifer = invocation.commandIdentifier;
if ([identiifer isEqualToString:@"uppercaseString"])
{
[self uppercaseStringHandle:invocation];
}
else if ([identiifer isEqualToString:@"lowercaseString"])
{
[self lowercaseStringHandle:invocation];
}else
{
[self capitalizedStringHandle:invocation];
}
completionHandler(nil);
}
- (void)uppercaseStringHandle:(XCSourceEditorCommandInvocation *)invocation{
[self handleSelector:@selector(uppercaseStringWithLocale:) withObject:invocation];
}
- (void)lowercaseStringHandle:(XCSourceEditorCommandInvocation *)invocation{
[self handleSelector:@selector(lowercaseStringWithLocale:) withObject:invocation];
}
- (void)capitalizedStringHandle:(XCSourceEditorCommandInvocation *)invocation{
[self handleSelector:@selector(capitalizedStringWithLocale:) withObject:invocation];
}
- (void)handleSelector:(SEL)selector withObject:(XCSourceEditorCommandInvocation *)invocation{
Method m2 = class_getInstanceMethod([NSString class], selector);
_IMP imp = (_IMP)method_getImplementation(m2);
for (XCSourceTextRange *range in invocation.buffer.selections) {
for (NSInteger line = range.start.line; line <= range.end.line; line++) {
NSString *text = invocation.buffer.lines[line];
if (line == range.start.line && line == range.end.line){
NSString *subText = [text substringWithRange:NSMakeRange(range.start.column, range.end.column-range.start.column)];
subText = imp(subText,selector,[NSLocale currentLocale]);
text = [text stringByReplacingCharactersInRange:NSMakeRange(range.start.column, range.end.column-range.start.column) withString:subText];
}
else if (line == range.start.line)
{
NSString *subText = [text substringFromIndex:range.start.column];
subText = imp(subText,selector,[NSLocale currentLocale]);
text = [text stringByReplacingCharactersInRange:NSMakeRange(range.start.column, text.length-range.start.column) withString:subText];
}
else if (line == range.end.line)
{
NSString *subText = [text substringToIndex:range.end.column];
subText = imp(subText,selector,[NSLocale currentLocale]);
text = [text stringByReplacingCharactersInRange:NSMakeRange(0, range.end.column) withString:subText];
}
else
{
text = [text uppercaseStringWithLocale:[NSLocale currentLocale]];
}
invocation.buffer.lines[line] = text;
}
}
}