Swift Xcode 插件开发

作者: dimsky | 来源:发表于2016-01-31 21:59 被阅读1488次

    先借用一句古话装逼,

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

    作为一个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寄存器和栈帧

    相关文章

      网友评论

      本文标题:Swift Xcode 插件开发

      本文链接:https://www.haomeiwen.com/subject/yhcskttx.html