Xcode插件AllTargets开发教程
公司要为很多个企业定制app,每个app的功能基本相同,只是界面上的一些图片和文字要换掉,功能也有一些小改动。考虑到代码维护的问题,比较好的做法就是只维护一份代码,然后用不同的配置文件来管理各个target的内容。
当工程里达到上百个target的时候,为工程新增文件就成了一件非常痛苦的事情。
我必须一个一个地去勾选所有的targets,往往要花上几分钟的时间来重复无聊的操作,既浪费时间又影响心情,而Xcode也没有自带全选targets的功能。因此我萌生了一个想法:写一个能自动勾选所有targets的插件。
Google一下Xcode的制作教程,找到了VVDocumenter插件作者写的一篇教程:《Xcode 4 插件制作入门》 。 这篇教程很适合入门,但是里面有些东西由于年代久远,已经不兼容最新的Xcode 6.1了。不过教程里很多细节都写得很详细,建议先看完这篇教程。我看了教程后加上自己的摸索,终于完成了插件的开发,因此在这里把插件的开发过程分享出来。
一、安装插件模板 Alcatraz
是一款开源的Xcode包管理器,源码下载地址为:https://github.com/supermarin/Alcatraz 。 编译完成之后,重启Xcode,然后点击Xcode顶部菜单Windows
中的Package Manager
就可以打开Alcatraz包管理器面板。 搜索关键字Xcode Plugin
,可以找到一个Xcode Plugin
模板,该模板可以用来创建Xcode 6+的插件。
点击左边的图标按钮就可以把模板安装到Xcode里。
新建一个Xcode工程,选择Xcode Plugin
模板,本例子的工程名为AllTargets。
该模板的部分初始代码为:
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 - (id )initWithBundle:(NSBundle *)plugin { if (self = [super init]) { self .bundle = plugin; NSMenuItem *menuItem = [[NSApp mainMenu] itemWithTitle:@"Edit" ]; if (menuItem) { [[menuItem submenu] addItem:[NSMenuItem separatorItem]]; NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:@"Do Action" action:@selector (doMenuAction) keyEquivalent:@"" ]; [actionMenuItem setTarget:self ]; [[menuItem submenu] addItem:actionMenuItem]; } } return self ; } - (void )doMenuAction { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Hello, World" ]; [alert runModal]; }
初始代码会在Xcode的Edit
菜单里加入一个名字为Do Action
的子菜单,当你点击这个子菜单的时候,会调用doMenuAction方法弹出一个提示框,提示内容为Hello, World
。
二、需求分析 在Xcode里按住 command + alt + A 打开添加文件窗口:
所有的targets都位于白色矩形视图里,可以猜测该矩形视图是一个NSTableView(大小差不多为320*170),勾选的按钮是一个NSCell。 首先要获得NSTableView对象,《Xcode 4 插件制作入门》 里提到可以使用递归打印subviews的方法来得到某个NSView对象。
不过我发现一种更简便的方法,在本例子中比较适用。在没打开添加文件窗口之前,NSTableView是不会创建的,而创建视图时,初始化大小会调用NSViewDidUpdateTrackingAreasNotification通知。所以我们可以先监听该通知,再打开添加文件窗口,这样就能得到添加文件窗口里所有视图对象了,修改代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - (void )doMenuAction { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (notificationListener:) name:NSViewDidUpdateTrackingAreasNotification object:nil ]; } - (void )notificationListener:(NSNotification *)notification { NSView *view = notification.object; if ([view respondsToSelector:@selector (frame)]) { NSLog (@"view : %@, frame : %@" , view, [NSValue valueWithRect:view.frame]); } }
编译代码后重启Xcode,打开控制台(Control+空格,输入console),并清空控制台里的log。 点击Xcode的Do Action
子菜单开始监听消息,这时打开添加文件的窗口会看到控制台输出一堆log。 把log复制到MacVim里,搜索NSTableView
,可以找到一条结果:
1 view : <NSTableView: 0x7fb206c65f40>, frame : NSRect: {{0, 0}, {321, 170}}
可以发现,此TableView的大小为321*170,看来正是我们正在寻找的对象。
三、hook私有类 由于NSCell的值是由NSTableView的数据源所控制的,所以我们必须找到NSTableView的数据源,修改一下代码打印出数据源:
1 2 3 4 5 6 7 8 - (void )notificationListener:(NSNotification *)notification { NSView *view = notification.object; if ([view.className isEqualToString:@"NSTableView" ]) { NSTableView *tableView = (NSTableView *)view; NSLog (@"dataSource : %@" , tableView.dataSource); } }
可以看到控制台输出了log:
1 dataSource : <Xcode3TargetMembershipDataSource: 0x7fadb7352830>
Xcode3TargetMembershipDataSource
是Xcode的私有类,位于/Applications/Xcode.app/Contents/PlugIns/Xcode3UI.ideplugin/Contents/MacOS/Xcode3UI
里。要引用Xcode3TargetMembershipDataSource.h
这个头文件的话,就要把Xcode3UI这个文件拖到工程的Frameworks里,不要勾选Copy items if needed
,这样就会直接引用Xcode应用里的文件。
在这里可以下载从Xcode 6.1 dump出来的私有类头文件:https://github.com/luisobo/Xcode-RuntimeHeaders/tree/xcode6-beta1
打开Xcode3TargetMembershipDataSource.h
,部分代码如下:
Xcode3TargetMembershipDataSource.h 1 2 3 4 5 6 7 8 @interface Xcode3TargetMembershipDataSource : NSObject <NSTableViewDataSource , NSTableViewDelegate >;{ NSMutableArray *_wrappedTargets; } - (void )updateTargets;
_wrappedTargets
数组很有可能保存着targets的信息,updateTargets
方法的作用应该是用来更新targets的值,所以可以试试hookupdateTargets
方法。JRSwizzle 是一个专门处理Method Swizzling的开源库,这里可以直接使用。
将Xcode3TargetMembershipDataSource.h
文件拖到工程里,记得勾上Copy items if needed
。该头文件里可能引用了一些未知的类名会导致报错,所以可以把报错的语句注释掉。
然后新建一个Xcode3TargetMembershipDataSource+Hook
的Category。
Category的代码如下:
Xcode3TargetMembershipDataSource+Hook.h 1 2 3 4 5 6 7 #import "Xcode3TargetMembershipDataSource.h" @interface Xcode3TargetMembershipDataSource (Hook )+ (void )hook; @end
Xcode3TargetMembershipDataSource+Hook.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #import "Xcode3TargetMembershipDataSource+Hook.h" #import "JRSwizzle.h" @implementation Xcode3TargetMembershipDataSource (Hook )+ (void )hook { [self jr_swizzleMethod:@selector (updateTargets) withMethod:@selector (updateTargetsHook) error:nil ]; } - (void )updateTargetsHook { [self updateTargetsHook]; NSMutableArray *wrappedTargets = [self valueForKey:@"wrappedTargets" ]; for (id wrappedTarget in wrappedTargets) { NSLog (@"target : %@" , wrappedTarget); } } @end
AllTargets.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #import "Xcode3TargetMembershipDataSource+Hook.h" - (id )initWithBundle:(NSBundle *)plugin { if (self = [super init]) { self .bundle = plugin; [Xcode3TargetMembershipDataSource hook]; } return self ; }
再次运行后可以看到控制台输出了log,由于工程只有一个target,所以只有一个对象:
1 target : <Xcode3TargetWrapper: 0x7f8b59264ab0>>
在Xcode的私有类里找到Xcode3TargetWrapper.h
,内容如下:
Xcode3TargetWrapper.h 1 2 3 4 5 6 7 8 9 10 11 12 13 @interface Xcode3TargetWrapper : NSObject { PBXTarget *_pbxTarget; Xcode3Project *_project; NSString *_name; NSImage *_image; BOOL _selected; } @property (readonly ) NSImage *image; @property (readonly ) NSString *name; @property BOOL selected;
可以看到,该类有三个属性:图片、名字和是否选中,我们只要把selected
属性改为YES
就行了。
我们把updateTargetsHook
方法修改为:
Xcode3TargetMembershipDataSource+Hook.m 1 2 3 4 5 6 7 8 9 10 11 12 13 - (void )updateTargetsHook { [self updateTargetsHook]; NSMutableArray *wrappedTargets = [self valueForKey:@"wrappedTargets" ]; for (id wrappedTarget in wrappedTargets) { [wrappedTarget setValue:@YES forKey:@"selected" ]; } }
再次编译重启Xcode,打开添加文件窗口,可以发现所有targets都自动选中了:
四、添加菜单 考虑到有时可能要关闭这个功能,所以可以给菜单加上是否选中的状态,此外还可以给Xcode加上一个独立的Plugins菜单,大部分插件就可以放在这个菜单里,以方便管理。
由于Xcode启动后加载插件时,Xcode的菜单栏可能还没加载出来,所以要确保Xcode加载出菜单栏才创建插件的菜单,可以使用NSMenuDidChangeItemNotification
通知来监听菜单栏的改变,判断Xcode菜单栏加载出来后再添加菜单,具体代码如下:
AllTargets.m 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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 - (id )initWithBundle:(NSBundle *)plugin { if (self = [super init]) { self .bundle = plugin; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (addPluginsMenu) name:NSMenuDidChangeItemNotification object:nil ]; [Xcode3TargetMembershipDataSource hook]; } return self ; } - (void )addPluginsMenu { NSMenu *mainMenu = [NSApp mainMenu]; if (!mainMenu) { return ; } NSMenuItem *pluginsMenuItem = [mainMenu itemWithTitle:@"Plugins" ]; if (!pluginsMenuItem) { pluginsMenuItem = [[NSMenuItem alloc] init]; pluginsMenuItem.title = @"Plugins" ; pluginsMenuItem.submenu = [[NSMenu alloc] initWithTitle:pluginsMenuItem.title]; NSInteger windowIndex = [mainMenu indexOfItemWithTitle:@"Window" ]; [mainMenu insertItem:pluginsMenuItem atIndex:windowIndex]; } NSMenuItem *subMenuItem = [[NSMenuItem alloc] init]; subMenuItem.title = @"Auto Select All Targets" ; subMenuItem.target = self ; subMenuItem.action = @selector (toggleMenu:); subMenuItem.state = NSOnState ; [pluginsMenuItem.submenu addItem:subMenuItem]; [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMenuDidChangeItemNotification object:nil ]; } - (void )toggleMenu:(NSMenuItem *)menuItem { menuItem.state = !menuItem.state; [Xcode3TargetMembershipDataSource hook]; }
五、下载地址 插件代码下载地址为:https://github.com/poboke/AllTargets