Swift Xcode 插件開(kāi)發(fā)

先借用一句古話裝逼,

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

作為一個(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)。

類(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)。

就是這么個(gè)玩意兒

安裝插件模板

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

Xcode Plugin Template

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

新建 Xcode Plugin

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

Burberry

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

Do Action

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

Hello World

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 下手吧。

console

在斷點(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

debug

好不容易揪出來(lái)了,別急,只要你一層一層剝開(kāi)他的心,你就會(huì)發(fā)現(xiàn),就會(huì)明白...

LLDB 的image lookup命令將列出所有在內(nèi)存中實(shí)現(xiàn)的方法

image lookup -rn DVTBezelAlertPanel
image lookup

顯然你已經(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)特技了!

Duang
也許還需要一個(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)(Custom/Default)

也許還可以為開(kāi)關(guān)加上一個(gè)快捷鍵。

當(dāng)然,在build之前你需要確保設(shè)置提示是打開(kāi)的才能看到特技。


setting
接下來(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

將UUID 添加至 plist 中的

添加UUID
更多

那些不能錯(cuò)過(guò)的Xcode插件
LLDB
X86-64寄存器和棧幀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容