我最近一年来都在开发ios应用,不过感觉公司的app维护起来非常麻烦。

因为公司要为很多个企业订做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。

该模板的部分初始代码为:

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]) {
        // 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通知。所以我们可以先监听该通知,再打开添加文件窗口,这样就能得到添加文件窗口里所有视图对象了,修改代码为:

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 里。由于这个私有类没有frameworks可引用,所以只能通过NSClassFromString来Hook该私有类的函数。

在这里可以下载从Xcode 6.1 dump出来的私有类头文件:https://github.com/luisobo/Xcode-RuntimeHeaders/tree/xcode6-beta1

打开Xcode3TargetMembershipDataSource.h,部分代码如下:

1
2
3
4
5
6
7
@interface Xcode3TargetMembershipDataSource : NSObject {
    NSMutableArray *_wrappedTargets;
    //......
}
  
- (void)updateTargets;
//......

_wrappedTargets数组很有可能保存着targets的信息,updateTargets函数的作用应该是用来更新targets的值,所以可以试试hook updateTargets函数,代码如下:

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
//originalImp用来保存原私有类的方法
static IMP originalImp = NULL;
  
@implementation AllTargets
  
//......
  
- (void)doMenuAction
{
    [self hookClass];
}
  
- (void)hookMethod
{
    SEL method = @selector(updateTargets);
     
    //获取私有类的函数
    Class originalClass = NSClassFromString(@"Xcode3TargetMembershipDataSource");
    Method originalMethod = class_getInstanceMethod(originalClass, method);
    originalImp = method_getImplementation(originalMethod);
     
    //获取当前类的函数
    Class replacedClass = self.class;
    Method replacedMethod = class_getInstanceMethod(replacedClass, method);
  
    //交换两个函数
    method_exchangeImplementations(originalMethod, replacedMethod);
}
  
- (void)updateTargets
{
    //先调用原私有类的函数
    originalImp();
     
    //查看_wrappedTargets数组里保存了什么类型的对象
    NSMutableArray *wrappedTargets = [self valueForKey:@"wrappedTargets"];
    for (id wrappedTarget in wrappedTargets) {
        NSLog(@"target : %@", wrappedTarget);
    }
}

可以看到控制台输出了log,由于工程只有一个target,所以只有一个对象:

1
target : < Xcode3TargetWrapper: 0x7f8b59264ab0>

在Xcode的私有类里找到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; // @synthesize image=_image;
@property(readonly) NSString *name; // @synthesize name=_name;
@property BOOL selected; // @synthesize selected=_selected;
//......

可以看到,该类有三个属性:图片、名字和是否选中,我们只要把selected属性改为YES就行了。

我们把updateTargets函数修改为:

1
2
3
4
5
6
7
8
9
10
11
- (void)updateTargets
{
    //先调用原私有类的函数
    originalImp();
     
    //修改wrappedTarget的属性
    NSMutableArray *wrappedTargets = [self valueForKey:@"wrappedTargets"];
    for (id wrappedTarget in wrappedTargets) {
        [wrappedTarget setValue:@YES forKey:@"selected"];
    }
}

再次编译重启Xcode,打开添加文件窗口,可以发现所有targets都自动选中了。

四、添加菜单

考虑到有时可能要关闭这个功能,所以可以给菜单加上是否选中的状态,此外还可以给Xcode加上一个独立的Plugins菜单,大部分插件就可以放在这个菜单里,以方便管理。

创建菜单的代码如下:

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
- (void)addPluginsMenu
{
    //增加一个"Plugins"菜单到"Window"菜单前面
    NSMenu *mainMenu = [NSApp mainMenu];
    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 *subItem = [[NSMenuItem alloc] init];
    subItem.title = @"Auto Select All Targets";
    subItem.target = self;
    subItem.action = @selector(toggleMenu:);
    subItem.state = NSOnState;
    [pluginsMenuItem.submenu addItem:subItem];
}
  
- (void)toggleMenu:(NSMenuItem *)menuItem
{
    //改变菜单选中状态
    menuItem.state = !menuItem.state;
  
    //重新交换函数,hook与unhook
    [self hookMethod];
}

本插件的源码下载地址:https://github.com/poboke/AllTargets

Xcode 插件开发的更多相关文章

  1. Xcode插件开发案例教程

    引言 在平时开发过程中我们使用了很多的Xcode插件,虽然官方对于插件制作没有提供任何支持,但是加载三方的插件,默认还是被允许的.第三方的插件,存放在 ~/Library/Application Su ...

  2. Xcode插件开发

    一.安装模板 1.git clone https://github.com/kattrali/Xcode-Plugin-Template.git 2.cd Xcode-Plugin-Template ...

  3. Xcode7插件开发:从开发到拉到恶魔岛

    Xcode很强大,但是有些封闭,官方并没有提供Xcode插件开发的文档.喵神的教程比较全,也比较适合入门.本文的教程只是作为我在开发FKConsole的过程中的总结,并不会很全面. FKConsole ...

  4. ios最新的视频地址链接

    2016年最新iOS教程UI基础http://pan.baidu.com/s/1pLvnH8n资料链接:http://pan.baidu.com/s/1nvewKkh 密码:wktp 2016年最新i ...

  5. iOS_高效开发之道

    iOS_高效开发之道 话不多说, 总结一下个人感觉有利于提高iOS开发效率的几个小技巧. 本文将从下面几方面介绍: Xcode经常使用快捷键 Xcode调试技巧 Objc经常使用代码片段 Xcode插 ...

  6. Final Cut Pro X效果插件开发总结

    一.介绍 最近公司需要针对Final Cut Pro(FCP)开发一款效果插件,用于对公司自己开发的视频格式进行后期处理.Final Cut Pro是苹果公司推出的一款视频剪辑软件,因此需要在OSX平 ...

  7. 用 Xcode 开发 Cydia Substrate 插件(一)

    关于这方面的中文资料太少了,以至于可能很多对插件开发感兴趣的孩子们都不知从何下手,于是呢我就写了这篇文章,希望对你能有所帮助.如果你觉得文章内容有什么错误呢也请提出来. 准备开发环境 1. 从 App ...

  8. Xcode升级后插件失效的原理与修复办法

    转载:http://joeshang.github.io/2015/04/10/fix-xcode-upgrade-plugin-invalid/ Xcode 的插件大大丰富了 Xcode 的功能,而 ...

  9. Xcode 4 插件制作入门

    转自:http://www.onevcat.com/2013/02/xcode-plugin/ 2014.5.4更新 对于 Xcode 5,本文有些地方显得过时了.Xcode 5 现在已经全面转向了 ...

随机推荐

  1. phpstudy配置虚拟主机

    配置 phpstudy 虚拟主机 1在httpd.conf中  把#Include conf/extra/httpd-vhosts.conf前面的#去掉 2在站点域名管理 添加 要配置的 虚拟主机 添 ...

  2. 【iOS 7】使用UIScreenEdgePanGestureRecognizer实现swipe to pop效果

    在iOS 7还没有发布的时候,各种App实现各种的swipe to pop效果,比如这里有一份简单的demo. 在iOS 7上,只要是符合系统的导航结构: - (BOOL)application:(U ...

  3. java数据结构-非线性结构之树

    一.树状图 树状图是一种数据结构,它是由n(n>=1)个有限节点组成的具有层次关系的集合.因其结构看起来想个倒挂的树,即根朝上,叶子在下,故被称为"树". 特点: 1. 每个 ...

  4. NIO组件Selector详解

    Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件.这样,一个单独的线程可以管理多个channel,从而管理多个网络连接. 下面是 ...

  5. FragmentStatePagerAdapter.notifyDataSetChanged不刷新页面的解决的方法

    公司做医疗产品的,显示操作用的是android.所以我就用上下两个部分大致是固定的,仅仅有中间会有6个页面的切换,当中会有两个用户的切换.即普通用户和管理员用户,图片能够大致展示一下 其他页面是同样的 ...

  6. IT行业智力测试

    1.有10筐苹果,其中有1筐是次品,正品苹果每个10两,次品苹果每个9两,现有一称,问怎么一次称出次品是哪筐? 2.有甲.乙.丙.丁四个人,要在夜里过一座桥.他们通过这座桥分别需要耗时1.2.5.10 ...

  7. h5宣传页制作过程中遇到的问题

    音乐播放 ios下关闭不流畅; (ios下需重新image 模拟) 音乐设置自动播放属性后 部分机型下不能自动播放.目前解决方案: touchstart时触发播放 微信“分享给朋友”点击发送后,页面卡 ...

  8. Android 解决调用系统相册打不开图片 DecodeServices报解码错误

    这是由于系统相册不知道你图片目录是一个相册.打开前需要向系统相册“注册一下”,说白了就是让系统相册知道你这个图片所在的文件夹是个相册. private static void scanImageFil ...

  9. VS2010 Web项目需要缺少的Web组件才能加载

    用记事本打开 .csproj 文件,找到<UseIIS>True</UseIIS>改为<UseIIS>False</UseIIS> ,重新加载项目即可.

  10. Nico Game Studio 2.设置页面读写 纹理载入与选择

    进度十分之慢... 配置读写一样采用之前写的自动绑定的方法: 分享一下代码: SetControl是把数据写到control上的. SetObject是把数据写到对象上 GetData是从控件读取数据 ...