Xcode相关走进swiftSwift开发札记

Swift Xcode 插件开发

2016-01-31  本文已影响1488人  dimsky

先借用一句古话装逼,

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

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

上一篇下一篇

猜你喜欢

热点阅读