先借用一句古話裝逼,
工欲善其事,必先利其器。
作為一個(gè)iOS開(kāi)發(fā)(diao si),首先肯定要將自己的武器打磨好,才能上戰(zhàn)場(chǎng),我們可以給這把武器針對(duì)自己的天賦加上合適的附魔,打上合適的寶石,以提高自己的DPS。顯然,Xcode 就是武器,雖然蘋(píng)果 并沒(méi)有對(duì)Xcode插件提供任何技術(shù)和文檔支持,但如今的Xcode 插件開(kāi)發(fā)流程已經(jīng)只需要幾步,你還有理由不去試一試么?
這不是我的戰(zhàn)場(chǎng),所以我沒(méi)準(zhǔn)備升級(jí)武器(前面都是廢話)。Duang,那就加個(gè)特技吧。

開(kāi)始
Xcode 插件對(duì)于你也許不再陌生,但類(lèi)似這樣的特技你一定不常見(jiàn)。

下載Demo https://github.com/dimsky/Burberry
是的!接下來(lái)我們就開(kāi)始把XCode 的成功或錯(cuò)誤的提示換成你喜歡的惡搞圖吧!
老規(guī)矩,開(kāi)始之前 ,先用兩分鐘完成一個(gè)Hello World! 當(dāng)然,老司機(jī)可以略過(guò)。
安裝插件 Alcatraz
開(kāi)發(fā)之前,我們需要先安裝一個(gè)插件 Alcatraz, 這是一個(gè)非常優(yōu)秀的XCode 插件管理器,我們可以通過(guò)它非常容易的進(jìn)行插件管理。
輸入以下命令在終端安裝:
curl -fsSL https://raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh |sh
等命令執(zhí)行完成,重啟XCode 完成安裝。然后會(huì)出現(xiàn)以下一個(gè)警告,選擇 Load Bundle 即可。

然后在XCode 的 Window 菜單中會(huì)出現(xiàn) Package Manager 選項(xiàng),當(dāng)然,你也可以通過(guò)快捷鍵(??9)快速打開(kāi)。

安裝插件模板
在很久以前,我們開(kāi)發(fā)一個(gè)Xcode 插件可能需要很多的配置修改操作,但幸運(yùn)的是已經(jīng)有人替我們完成了這一步,他創(chuàng)建了這樣一個(gè)模板,插件,到底是插件還是Xcode-Plugin..... - -|| 打開(kāi) package manager 安裝。

安裝完成之后 就可以通過(guò)新建導(dǎo)航創(chuàng)建 Xcode 插件了

肯定是選擇Swift ,當(dāng)然,取一個(gè)裝逼的名字也很重要。

創(chuàng)建完成之后就可以跑起來(lái)了,運(yùn)行后會(huì)重新打開(kāi)一個(gè)新的Xcode, 選擇加載插件,如果一切順利的話,打開(kāi)Edit菜單,就可以看到菜單上的變化了:

點(diǎn)擊 Do Action, 一個(gè)錯(cuò)誤的Hello World 的信息就彈出來(lái)了,別擔(dān)心,你已經(jīng)成功了 。(如果用Objective-c 彈出來(lái)的會(huì)是一個(gè)正常的Alert 窗口)

Hello World 就這樣完成了,是不是還沒(méi)到兩分鐘? 看來(lái)少年的APM 極高。
完成 Duang
蘋(píng)果官方并沒(méi)有對(duì)Xcode插件提供任何技術(shù)和文檔支持,怎么辦?
init(bundle: NSBundle) {
self.bundle = bundle
super.init()
center.addObserver(self, selector: Selector("createMenuItems"), name: NSApplicationDidFinishLaunchingNotification, object: nil)
}
從以上代碼不難發(fā)現(xiàn),在我們的Hello Wrold 中的菜單是通過(guò)監(jiān)聽(tīng)Notification來(lái)完成創(chuàng)建的,那我們應(yīng)該怎么才能知道build成功的提示會(huì)是哪個(gè)Notification呢?
NSNotificationCenter 在addObserver(...)方法中說(shuō)明當(dāng)name參數(shù)傳為nil時(shí),將可以監(jiān)聽(tīng)到所有的Notification。
那么就可以在?B build時(shí)去查找Xcode 所發(fā)出的通知。
在init(...)方法中添加監(jiān)聽(tīng)
center.addObserver(self, selector: Selector("handlerNotification:"), name: nil, object: nil)
下面把Notification的name裝進(jìn)一個(gè)集合,并在收到時(shí)打印出來(lái),注意,這里打印要用NSLog(...)。
var notificationSet: NSMutableSet = NSMutableSet();
func handlerNotification(notifi: NSNotification) {
if !self.notificationSet.containsObject(notifi.name) {
self.notificationSet.addObject(notifi.name)
NSLog("---> %@", notifi.name)
}
}
build 運(yùn)行,然后在操作Xcode的時(shí)候查看控制臺(tái)的信息,你會(huì)發(fā)現(xiàn)有很多Notification的name打印出來(lái),先清空,這些都不是我想要的,?B build,發(fā)現(xiàn)會(huì)打印出以下幾條,而最后兩條會(huì)在提示消失后打印,那就先從 NSWindowDidOrderOffScreenNotification 下手吧。

在斷點(diǎn)約束中寫(xiě)入
notifi.name == "NSWindowDidOrderOffScreenNotification"
執(zhí)行
po notifi.object
在運(yùn)行的Xcode ?B build ,這時(shí)會(huì)觸發(fā)斷點(diǎn)
你會(huì)發(fā)現(xiàn)一個(gè)新鮮玩意兒 DVTBezelAlertPanel

好不容易揪出來(lái)了,別急,只要你一層一層剝開(kāi)他的心,你就會(huì)發(fā)現(xiàn),就會(huì)明白...
LLDB 的image lookup命令將列出所有在內(nèi)存中實(shí)現(xiàn)的方法
image lookup -rn DVTBezelAlertPanel

顯然你已經(jīng)發(fā)現(xiàn)了這幾個(gè)方法
[DVTBezelAlertPanel initWithIcon:message:controlView:duration:]
[DVTBezelAlertPanel initWithIcon:message:parentWindow:duration:]
[DVTBezelAlertPanel controlView]
下面我們要做的是注入代碼,改變DVTBezelAlertPanel 的行為
我們知道 OC 的runtime可以做很多事情,比如在運(yùn)行時(shí)替換掉某個(gè)Xcode的方法,我們只要將該方法與我們自己實(shí)現(xiàn)的方法進(jìn)行運(yùn)行時(shí)調(diào)換,從而改為執(zhí)行我們自己的方法。然后,Duang!這便是運(yùn)行時(shí)的MethodSwizzle 點(diǎn)擊下載
打開(kāi) 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
代碼很簡(jiǎn)單,僅僅是做了一個(gè)簡(jiǎn)單的封裝。
我們需要?jiǎng)?chuàng)建一個(gè)自定義的方法來(lái)替換原有的方法
下面通過(guò)message參數(shù)判斷build 成功或失敗,修改配圖以及文字:
注意,image.template = NO ,當(dāng)為Yes 時(shí)圖片將只有黑色和透明色。
#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
然后我們要用這個(gè)方法來(lái)替換掉Xcode原有的方法,替換方法只需要執(zhí)行一次,所以我們?cè)诔跏蓟瘯r(shí)使用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一下 就會(huì)出現(xiàn)特技了!

也許還需要一個(gè)開(kāi)關(guān)
比如說(shuō)女神在你背后的時(shí)候 有些圖片又恰好出現(xiàn),是不是就不太合適了。
將開(kāi)關(guān)用NSUserDefaults 記錄下來(lái)。
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")
}

也許還可以為開(kāi)關(guān)加上一個(gè)快捷鍵。
當(dāng)然,在build之前你需要確保設(shè)置提示是打開(kāi)的才能看到特技。

接下來(lái)能做些什么?
接下來(lái)你可以把你的插件上傳至Alcatraz
然后呢?

你可以悄悄的把插件裝在你的同事或者基友的Xcode 里,再看他build 工程時(shí)的表情吧。
然后你可以把獲取圖片方式變?yōu)榫W(wǎng)絡(luò)請(qǐng)求,由你來(lái)控制如何顯示,或顯示什么,至于顯示什么嘛...
顯然Xcode 插件能做的不止這些,發(fā)揮你的想象力,做更多有用、好玩的東西。
如何刪除(卸載)Xcode 插件
如果是通過(guò)Alcatraz 來(lái)完成的插件安裝,點(diǎn)擊Remove 即可完成插件卸載。
但如果是通過(guò)運(yùn)行源代碼安裝的話,可能就需要手動(dòng)刪除了。
cd ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/ rm-r Burberry.xcplugin
然后重啟Xcode 完成刪除。
UUID
在 Xcode 5 以后, Apple 為了防止過(guò)期插件導(dǎo)致的在 Xcode 升級(jí)后 IDE 的崩潰,添加了一個(gè) UUID 的檢查機(jī)制。只有包含聲明了適配 UUID,才能夠被 Xcode 正確加載,所以Xcode 版本升級(jí)之后,插件開(kāi)發(fā)者也需要將新版本Xcode 的UUID 加入其中。
終端執(zhí)行,獲取Xcode UUID:
defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID

將UUID 添加至 plist 中的
