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 WorldHello 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
下手吧。
在断点约束中写入
notifi.name == "NSWindowDidOrderOffScreenNotification"
执行
po notifi.object
在运行的Xcode ⌘B build ,这时会触发断点
你会发现一个新鲜玩意儿 DVTBezelAlertPanel
好不容易揪出来了,别急,只要你一层一层剥开他的心,你就会发现,就会明白...
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