Swift Xcode 插件开发

先借用一句古话装逼,

工欲善其事,必先利其器。

作为一个iOS开发(diao si),首先肯定要将自己的武器打磨好,才能上战场,我们可以给这把武器针对自己的天赋加上合适的附魔,打上合适的宝石,以提高自己的DPS。显然,Xcode 就是武器,虽然苹果 并没有对Xcode插件提供任何技术和文档支持,但如今的Xcode 插件开发流程已经只需要几步,你还有理由不去试一试么?

这不是我的战场,所以我没准备升级武器(前面都是废话)。Duang,那就加个特技吧。

什么玩意儿

开始

Xcode 插件对于你也许不再陌生,但类似这样的特技你一定不常见。

类似这样的特技你一定很少见

下载Demo https://github.com/dimsky/Burberry

是的!接下来我们就开始把XCode 的成功或错误的提示换成你喜欢的恶搞图吧!

老规矩,开始之前 ,先用两分钟完成一个Hello World! 当然,老司机可以略过。

安装插件 Alcatraz

开发之前,我们需要先安装一个插件 Alcatraz, 这是一个非常优秀的XCode 插件管理器,我们可以通过它非常容易的进行插件管理。
输入以下命令在终端安装:

 curl -fsSL https://raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh |sh

等命令执行完成,重启XCode 完成安装。然后会出现以下一个警告,选择 Load Bundle 即可。

加载插件

然后在XCode 的 Window 菜单中会出现 Package Manager 选项,当然,你也可以通过快捷键(⌘⇧9)快速打开。

就是这么个玩意儿

安装插件模板

在很久以前,我们开发一个Xcode 插件可能需要很多的配置修改操作,但幸运的是已经有人替我们完成了这一步,他创建了这样一个模板,插件,到底是插件还是Xcode-Plugin..... - -|| 打开 package manager 安装。

Xcode Plugin Template

安装完成之后 就可以通过新建导航创建 Xcode 插件了

新建 Xcode Plugin

肯定是选择Swift ,当然,取一个装逼的名字也很重要。

Burberry

创建完成之后就可以跑起来了,运行后会重新打开一个新的Xcode, 选择加载插件,如果一切顺利的话,打开Edit菜单,就可以看到菜单上的变化了:

Do Action

点击 Do Action, 一个错误的Hello World 的信息就弹出来了,别担心,你已经成功了 。(如果用Objective-c 弹出来的会是一个正常的Alert 窗口)

Hello World

Hello World 就这样完成了,是不是还没到两分钟? 看来少年的APM 极高。

完成 Duang

苹果官方并没有对Xcode插件提供任何技术和文档支持,怎么办?

init(bundle: NSBundle) {
    self.bundle = bundle
    super.init()
    center.addObserver(self, selector: Selector("createMenuItems"), name: NSApplicationDidFinishLaunchingNotification, object: nil)
}

从以上代码不难发现,在我们的Hello Wrold 中的菜单是通过监听Notification来完成创建的,那我们应该怎么才能知道build成功的提示会是哪个Notification呢?
NSNotificationCenter 在addObserver(...)方法中说明当name参数传为nil时,将可以监听到所有的Notification。
那么就可以在⌘B build时去查找Xcode 所发出的通知。
在init(...)方法中添加监听

center.addObserver(self, selector: Selector("handlerNotification:"), name: nil, object: nil)

下面把Notification的name装进一个集合,并在收到时打印出来,注意,这里打印要用NSLog(...)。

var notificationSet: NSMutableSet = NSMutableSet();
func handlerNotification(notifi: NSNotification) {
    if !self.notificationSet.containsObject(notifi.name) {
        self.notificationSet.addObject(notifi.name)
        NSLog("---> %@", notifi.name)
    }
}

build 运行,然后在操作Xcode的时候查看控制台的信息,你会发现有很多Notification的name打印出来,先清空,这些都不是我想要的,⌘B build,发现会打印出以下几条,而最后两条会在提示消失后打印,那就先从 NSWindowDidOrderOffScreenNotification 下手吧。

console

在断点约束中写入
notifi.name == "NSWindowDidOrderOffScreenNotification"
执行
po notifi.object
在运行的Xcode ⌘B build ,这时会触发断点
你会发现一个新鲜玩意儿 DVTBezelAlertPanel

debug

好不容易揪出来了,别急,只要你一层一层剥开他的心,你就会发现,就会明白...

LLDB 的image lookup命令将列出所有在内存中实现的方法

image lookup -rn DVTBezelAlertPanel
image lookup

显然你已经发现了这几个方法
[DVTBezelAlertPanel initWithIcon:message:controlView:duration:]
[DVTBezelAlertPanel initWithIcon:message:parentWindow:duration:]
[DVTBezelAlertPanel controlView]

下面我们要做的是注入代码,改变DVTBezelAlertPanel 的行为
我们知道 OC 的runtime可以做很多事情,比如在运行时替换掉某个Xcode的方法,我们只要将该方法与我们自己实现的方法进行运行时调换,从而改为执行我们自己的方法。然后,Duang!这便是运行时的MethodSwizzle 点击下载

打开 NSObject+MethodSwizzler.m

#import "NSObject+MethodSwizzler.h"
#import <objc/runtime.h>

@implementation NSObject (MethodSwizzler)

+ (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod
{
    Class cls = [self class];
    
    Method originalMethod;
    Method swizzledMethod;
    
    if (isClassMethod) {
        originalMethod = class_getClassMethod(cls, originalSelector);
        swizzledMethod = class_getClassMethod(cls, swizzledSelector);
    } else {
        originalMethod = class_getInstanceMethod(cls, originalSelector);
        swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
    }
    
    if (!originalMethod) {
        NSLog(@"Error: originalMethod is nil, did you spell it incorrectly? %@", originalMethod);
        return;
    }
    
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end

代码很简单,仅仅是做了一个简单的封装。

我们需要创建一个自定义的方法来替换原有的方法
下面通过message参数判断build 成功或失败,修改配图以及文字:
注意,image.template = NO ,当为Yes 时图片将只有黑色和透明色。

#import "NSObject+Burberry.h"
#import <AppKit/AppKit.h>
#import "Burberry-Swift.h"

@implementation NSObject (Burberry)

- (id)bur_initWithIcon:(id)icon
                message:(NSString *)message
           parentWindow:(id)parentWindow
               duration:(double)duration {
     NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.dimsky.Burberry"];
    if (icon && [Burberry isEnable] && [message containsString:@"Succeeded"]) {
        BurberryImage *burberryImage = [ImageStore makeImage];
        NSImage *image = [bundle imageForResource:burberryImage.imageName];
        if ([self isKindOfClass:[NSPanel class]]) {
            [self bur_initWithIcon:image message:burberryImage.message parentWindow:parentWindow duration:duration];
            NSPanel *panel = (id)self;
            if ([panel.contentView isKindOfClass:[NSVisualEffectView class]]) {
                NSVisualEffectView *e = (id)panel.contentView;
                e.material = NSVisualEffectMaterialTitlebar;
                image.template = NO;
            }
        }
        return self;
    } else if (icon && [Burberry isEnable] && [message containsString:@"Failed"]) {
        NSImage *image = [bundle imageForResource:@"failed.pdf"];
        [self bur_initWithIcon:image message:@"What The Fuck!" parentWindow:parentWindow duration:duration];
        if ([self isKindOfClass:[NSPanel class]]) {
            NSPanel *panel = (id)self;
            if ([panel.contentView isKindOfClass:[NSVisualEffectView class]]) {
                NSVisualEffectView *e = (id)panel.contentView;
                e.material = NSVisualEffectMaterialTitlebar;
                image.template = NO;
            }
        }
        return self;
    }
    return [self bur_initWithIcon:icon message:message parentWindow:parentWindow duration:duration];
}

@end

然后我们要用这个方法来替换掉Xcode原有的方法,替换方法只需要执行一次,所以我们在初始化时使用dispatch_once完成替换。

override class func initialize() {
    struct Static {
        static var token: dispatch_once_t = 0
    }
    dispatch_once(&Static.token) {
        swizzleMethods()
    }
}

class func swizzleMethods() {
    guard let originalClass = NSClassFromString("DVTBezelAlertPanel") as? NSObject.Type else {
        return
    }
    originalClass.swizzleWithOriginalSelector("initWithIcon:message:parentWindow:duration:", swizzledSelector: "bur_initWithIcon:message:parentWindow:duration:", isClassMethod: false)
}

恭喜 你只需要build一下 就会出现特技了!

Duang
也许还需要一个开关

比如说女神在你背后的时候 有些图片又恰好出现,是不是就不太合适了。

将开关用NSUserDefaults 记录下来。

 func createMenuItems() {
    removeObserver()

    let item = NSApp.mainMenu!.itemWithTitle("Edit")
    if item != nil {
        let title = Burberry.isEnable() ? "Burberry Default" : "Burberry Custom"
        let actionMenuItem = NSMenuItem(title:title, action:"doMenuAction:", keyEquivalent:"")
        actionMenuItem.target = self
        item!.submenu!.addItem(NSMenuItem.separatorItem())
        item!.submenu!.addItem(actionMenuItem)
    }
}

func doMenuAction(menuItem: NSMenuItem) {
    Burberry.setIsEnable(!Burberry.isEnable())
    menuItem.title = Burberry.isEnable() ? "Burberry Default" : "Burberry Custom"
}

class func isEnable() -> Bool {
   return NSUserDefaults.standardUserDefaults().boolForKey("com.dimsky.burberry")
}

class func setIsEnable(shouldBeEnabled: Bool) {
    NSUserDefaults.standardUserDefaults().setBool(shouldBeEnabled, forKey: "com.dimsky.burberry")
}
开关(Custom/Default)

也许还可以为开关加上一个快捷键。

当然,在build之前你需要确保设置提示是打开的才能看到特技。


setting
接下来能做些什么?

接下来你可以把你的插件上传至Alcatraz

然后呢?


你懂的

你可以悄悄的把插件装在你的同事或者基友的Xcode 里,再看他build 工程时的表情吧。
然后你可以把获取图片方式变为网络请求,由你来控制如何显示,或显示什么,至于显示什么嘛...

显然Xcode 插件能做的不止这些,发挥你的想象力,做更多有用、好玩的东西。

如何删除(卸载)Xcode 插件

如果是通过Alcatraz 来完成的插件安装,点击Remove 即可完成插件卸载。
但如果是通过运行源代码安装的话,可能就需要手动删除了。

cd ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/  rm-r Burberry.xcplugin

然后重启Xcode 完成删除。

UUID

在 Xcode 5 以后, Apple 为了防止过期插件导致的在 Xcode 升级后 IDE 的崩溃,添加了一个 UUID 的检查机制。只有包含声明了适配 UUID,才能够被 Xcode 正确加载,所以Xcode 版本升级之后,插件开发者也需要将新版本Xcode 的UUID 加入其中。

终端执行,获取Xcode UUID:

defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID
获取UUID

将UUID 添加至 plist 中的

添加UUID
更多

那些不能错过的Xcode插件
LLDB
X86-64寄存器和栈帧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容