公司要为很多个企业定制app,每个app的功能基本相同,只是界面上的一些图片和文字要换掉,功能也有一些小改动。考虑到代码维护的问题,比较好的做法就是只维护一份代码,然后用不同的配置文件来管理各个target的内容。
当工程里达到上百个target的时候,为工程新增文件就成了一件非常痛苦的事情。
我必须一个一个地去勾选所有的targets,往往要花上几分钟的时间来重复无聊的操作,既浪费时间又影响心情,而Xcode也没有自带全选targets的功能。因此我萌生了一个想法:写一个能自动勾选所有targets的插件。
google一下Xcode的制作教程,找到了VVDocumenter插件作者写的一篇教程:《Xcode 4 插件制作入门》。
这篇教程很适合入门,但是里面有些东西由于年代久远,已经不兼容最新的Xcode 6.1了。不过教程里很多细节都写得很详细,建议先看完这篇教程。我看了教程后加上自己的摸索,终于完成了插件的开发,因此在这里把插件的开发过程分享出来。
本插件的源码下载地址:https://github.com/poboke/AllTargets
一、安装插件模板
Alcatraz
是一款开源的Xcode包管理器,源码下载地址为:https://github.com/supermarin/Alcatraz
编译完成之后,重启Xcode,然后点击Xcode顶部菜单"Windows"中的"Package Manager"就可以打开Alcatraz包管理器面板。
搜索关键字"Xcode Plugin",可以找到一个"Xcode Plugin"模板,该模板可以用来创建Xcode 6+的插件。
点击左边的图标按钮就可以把模板安装到Xcode里。
新建一个Xcode工程,选择"Xcode Plugin"模板,本例子的工程名为AllTargets。
该模板的部分初始代码为:
- (id)initWithBundle:(NSBundle *)plugin
{
if (self = [super init]) {
// reference to plugin's bundle, for resource access
self.bundle = plugin;
// Create menu items, initialize UI, etc.
// Sample Menu Item:
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;
}
// Sample Action, for menu item:
- (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通知。所以我们可以先监听该通知,再打开添加文件窗口,这样就能得到添加文件窗口里所有视图对象了,修改代码为:
- (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",可以找到一条结果:
view : <NSTableView: 0x7fb206c65f40>, frame : NSRect: {{0, 0}, {321, 170}}
可以发现,此TableView的大小为321*170,看来正是我们正在寻找的对象。
三、hook私有类
由于NSCell的值是由NSTableView的数据源所控制的,所以我们必须找到NSTableView的数据源,修改一下代码打印出数据源:
- (void)notificationListener:(NSNotification *)notification
{
NSView *view = notification.object;
if ([view.className isEqualToString:@"NSTableView"]) {
NSTableView *tableView = (NSTableView *)view;
NSLog(@"dataSource : %@", tableView.dataSource);
}
}
可以看到控制台输出了log:
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",部分代码如下:
@interface Xcode3TargetMembershipDataSource : NSObject <NSTableViewDataSource, NSTableViewDelegate>;
{
NSMutableArray *_wrappedTargets;
//......
}
- (void)updateTargets;
//......
_wrappedTargets数组很有可能保存着targets的信息,updateTargets函数的作用应该是用来更新targets的值,所以可以试试hook updateTargets
函数。
JRSwizzle是一个专门处理Method Swizzling的开源库,这里可以直接使用。
将Xcode3TargetMembershipDataSource.h文件拖到工程里,记得勾上"Copy items if needed"。该头文件里可能引用了一些未知的类名会导致报错,所以可以把报错的语句注释掉。
然后新建一个"Xcode3TargetMembershipDataSource+Hook"的Category。
Category的代码如下:
// Xcode3TargetMembershipDataSource+Hook.h
#import "Xcode3TargetMembershipDataSource.h"
@interface Xcode3TargetMembershipDataSource (Hook)
+ (void)hook;
@end
// Xcode3TargetMembershipDataSource+Hook.m
#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];
// 查看_wrappedTargets数组里保存了什么类型的对象
NSMutableArray *wrappedTargets = [self valueForKey:@"wrappedTargets"];
for (id wrappedTarget in wrappedTargets) {
NSLog(@"target : %@", wrappedTarget);
}
}
@end
// AllTargets.m
#import "Xcode3TargetMembershipDataSource+Hook.h"
//......
- (id)initWithBundle:(NSBundle *)plugin
{
if (self = [super init]) {
// Reference to plugin's bundle, for resource access
self.bundle = plugin;
[Xcode3TargetMembershipDataSource hook];
}
return self;
}
再次运行后可以看到控制台输出了log,由于工程只有一个target,所以只有一个对象:
target : <Xcode3TargetWrapper: 0x7f8b59264ab0>>
在Xcode的私有类里找到"Xcode3TargetWrapper.h",内容如下:
@interface Xcode3TargetWrapper : NSObject
{
PBXTarget *_pbxTarget;
Xcode3Project *_project;
NSString *_name;
NSImage *_image;
BOOL _selected;
}
@property(readonly) NSImage *image; // @synthesize image=_image;
@property(readonly) NSString *name; // @synthesize name=_name;
@property BOOL selected; // @synthesize selected=_selected;
//......
可以看到,该类有三个属性:图片、名字和是否选中,我们只要把selected属性改为YES就行了。
我们把updateTargetsHook函数修改为:
// Xcode3TargetMembershipDataSource+Hook.m
//......
- (void)updateTargetsHook
{
// 先调用原来的方法
[self updateTargetsHook];
// 修改wrappedTarget的属性值
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
//......
- (id)initWithBundle:(NSBundle *)plugin
{
if (self = [super init]) {
// Reference to plugin's bundle, for resource access
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;
}
// 增加一个"Plugins"菜单到"Window"菜单前面
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];
}
// 添加"Auto Select All Targets"子菜单
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
2016/05/20 13:52:06
[…] //www.poboke.com/study/write-a-xcode-plugin-to-auto-select-all-targets.html […]
2016/05/20 13:46:59
[…] 所以我只能自己写个插件来让注释自动缩进了,首先新建一个Xcode插件工程,比较详细的创建过程可以见《Xcode插件AllTargets开发教程》。 […]
2015/06/20 14:43:06
[…] 5)Xcode插件AllTargets开发教程 […]
2015/06/20 14:29:18
[…] 5)Xcode插件AllTargets开发教程 […]
2015/06/12 11:15:52
2015/05/11 08:45:27
好久没来了 这么低调了 你要战斗啊我们需要你。
2015/05/11 08:57:05
写文章是很花时间的,所以有时间才能写
2015/04/06 19:10:59
2015/04/18 08:56:38
其实这些逻辑也不难,是比较基础的逆向