博文地址:http://ifujun.com/xcode7-cha-jian-kai-fa-cong-kai-fa-dao-pulldao-alcatraz/git
Xcode很強大,可是有些封閉,官方並無提供Xcode插件開發的文檔。喵神的教程比較全,也比較適合入門。本文的教程只是做爲我在開發FKConsole
的過程當中的總結,並不會很全面。github
FKConsole
是我開發的一個用於在Xcode控制檯顯示中文的插件,很小,很簡單。這個插件開發的初衷是由於一個朋友有這種需求,而又沒有找到相應的插件。若是不使用插件,就要在工程中嵌入文件,他並不樂意。因此FKConsole
在設計上只會去修改Xcode控制檯內的文字顯示,毫不會去修改你的文件,這點你們能夠放心。objective-c
由於如今已經有不少人作Xcode插件開發了,因此插件模板這種東西也就應運而生了。json
Xcode-Plugin-Template是一個Xcode插件開發的基本模板,可使用Alcatraz直接安裝,支持Xcode 6+。xcode
安裝完成以後,在建立工程的時候,會出現一個Xcode Plugin的選項,這個就是Xcode的插件工程模板。ruby
模板會生成NSObject_Extension
和你的工程名稱同樣的兩個文件(.m)。框架
NSObject_Extension.m
中的+ (void)pluginDidLoad:(NSBundle *)plugin
方法也是整個插件的入口。ide
通常來講,咱們但願咱們的插件是存活於整個Xcode的生命週期的,因此通常是一個單例,這個在另外一個文件中會有體現。工具
這篇博文是記錄FKConsole
開發過程的,天然以此舉例。測試
Xcode啓動以後,會發出NSApplicationDidFinishLaunchingNotification
的通知,模板上已經作了監聽,咱們在程序啓動以後要在頭部工具欄上加一個FKConsole
的選項,以設置FKConsole
插件的開關。
Mac軟件開發和iOS開發有一些不一樣,它使用的是AppKit
的UI庫,而不是UIKit
,因此可能會感受有些彆扭。
NSApp
中的[NSApp mainMenu]
方法能夠獲取到頭部的主按鈕,裏面會包含頗有NSMenuItem
,咱們將在Xcode的Window
選項以前插入一個Plugins
選項(參考破博客的作法),而後在這個選項中添加一個FKConsole
的選項。(之因此添加一個Plugins
選項是由於有些插件會添加到Edit
中,有些會添加到View
、Window
中,我找半天都沒找到選項在哪,還不如直接建一個Plugins
選項,用戶一眼就能知道插件在哪。)
NSMenu *mainMenu = [NSApp mainMenu]; if (!mainMenu) { return; } NSMenuItem *pluginsMenuItem = [mainMenu itemWithTitle:@"Plugins"]; if (!pluginsMenuItem) { pluginsMenuItem = [[NSMenuItem alloc] init]; pluginsMenuItem.title = @"Plugins"; pluginsMenuItem.submenu = [[NSMenu alloc] initWithTitle:pluginsMenuItem.title]; NSInteger windowIndex = [mainMenu indexOfItemWithTitle:@"Window"]; [mainMenu insertItem:pluginsMenuItem atIndex:windowIndex]; } NSMenuItem *subMenuItem = [[NSMenuItem alloc] init]; subMenuItem.title = @"FKConsole"; subMenuItem.target = self; subMenuItem.action = @selector(toggleMenu:); subMenuItem.state = value.boolValue?NSOnState:NSOffState; [pluginsMenuItem.submenu addItem:subMenuItem];
咱們須要一個狀態來表示插件的開關,恰好NSMenuItem
上有一個state
能夠表示狀態,而恰好顯示效果也不錯,咱們就用它了。
按鈕添加完以後,咱們如今須要獲取到控制檯的實例。很遺憾,蘋果並無給出文檔。
很抱歉,我沒有找到Mac軟件開發上相似於Reveal的那種圖層查看工具。喵神推薦了一個NSView
的Dumping Category
,代碼以下:
來自於http://onevcat.com/2013/02/xcode-plugin/。
-(void)dumpWithIndent:(NSString *)indent { NSString *class = NSStringFromClass([self class]); NSString *info = @""; if ([self respondsToSelector:@selector(title)]) { NSString *title = [self performSelector:@selector(title)]; if (title != nil && [title length] > 0) { info = [info stringByAppendingFormat:@" title=%@", title]; } } if ([self respondsToSelector:@selector(stringValue)]) { NSString *string = [self performSelector:@selector(stringValue)]; if (string != nil && [string length] > 0) { info = [info stringByAppendingFormat:@" stringValue=%@", string]; } } NSString *tooltip = [self toolTip]; if (tooltip != nil && [tooltip length] > 0) { info = [info stringByAppendingFormat:@" tooltip=%@", tooltip]; } NSLog(@"%@%@%@", indent, class, info); if ([[self subviews] count] > 0) { NSString *subIndent = [NSString stringWithFormat:@"%@%@", indent, ([indent length]/2)%2==0 ? @"| " : @": "]; for (NSView *subview in [self subviews]) { [subview dumpWithIndent:subIndent]; } } }
效果相似於以下:
除了這種作法以外,我用的是chisel,這是facebook開源的一個LLDB的命令行輔助調試的工具。裏面包含有一個pviews
命令,能夠直接遞歸打印整個key window
,效果以下:
咱們在裏面找到了一個叫作IDEConsoleTextView
的類,這是在上圖中看到的全部View中惟一包含Console
這個關鍵字的,咱們查看一下它的frame,肯定控制檯就是它。
蘋果並無給將這個IDEConsoleTextView
放到AppKit
中,它是一個私有類,咱們如今想要修改它,那麼就須要拿到它的頭文件。
Github上有不少dump出來的Xcode header,你們能夠看一下:https://github.com/search?utf8=%E2%9C%93&q=xcode+header。咱們在header中找到了IDEConsoleTextView.h
,處於IDEKit
中。
在頭文件中能夠看到,IDEConsoleTextView
是繼承自DVTCompletingTextView
-\>DVTTextView
-\>NSTextView
。NSTextView
中保存文字內容使用的是NSTextStorage *textStorage
,因此咱們要修改的是IDEConsoleTextView
的textStorage
。可是咱們在NSTextStorage
的頭文件中並無找到具體文字保存的屬性,那咱們這就去找。
咱們循環遍歷全部的NSView
,找到IDEConsoleTextView
,咱們看一下它的信息:
咱們沒有找到它的textStorage
屬性,咱們嘗試在控制檯中打一下:
它是有這個屬性的,只是在debug區沒有看到。
textStorage
的delegate中有兩個方法,分別是:
// Sent inside -processEditing right before fixing attributes. Delegates can change the characters or attributes. -(void)textStorage:(NSTextStorage *)textStorage willProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta NS_AVAILABLE(10_11, 7_0); // Sent inside -processEditing right before notifying layout managers. Delegates can change the attributes. -(void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta NS_AVAILABLE(10_11, 7_0);
textStorage
中字符或者描述被修改以後,會觸發這個代理,那咱們實現一下這個代理方法:
self.fkConsoleTextView.textStorage.delegate = self; -(void)textStorage:(NSTextStorage *)textStorage willProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta { }
OK,此次咱們找到了,IDEConsoleTextView
中有一個_contents
屬性,這是一個繼承自NSMutableAttributedString
的類,這個裏面的mutableString
保存文字,mutableAttributes
保存對文字的描述。咱們須要修改的就是這個mutableString
屬性。
咱們在代理方法中使用valueForKeyPath:
能夠獲取到mutableString
屬性,那麼,如今咱們將它進行轉換。
FKConsole
是用來調整控制檯中文顯示的,目的是將相似於這種的Unicode編碼(\U6d4b\U8bd5"
)修改成("測試啊"
)這種的正常顯示。
我在stackoverflow上找到一種解決辦法。代碼相似於這樣:
來自於http://stackoverflow.com/questions/13240620/uilabel-text-with-unicode-nsstring
/- (NSString *)stringByReplaceUnicode:(NSString *)string { NSMutableString *convertedString = [string mutableCopy]; [convertedString replaceOccurrencesOfString:@"\\U" withString:@"\\u" options:0 range:NSMakeRange(0, convertedString.length)]; CFStringRef transform = CFSTR("Any-Hex/Java"); CFStringTransform((__bridge CFMutableStringRef)convertedString, NULL, transform, YES); return convertedString; }
咱們使用setValue:forKeyPath:
的方式去修改mutableString
屬性。
運行,確實能夠,可是有一些問題。
若是使用findView的方式去查找IDEConsoleTextView
,而後去設置代理的話,那麼,在何時去findView呢,若是這時候又新打開幾個頁面呢,這是不肯定的。
修改後的文字長度和原先的不同,哪怕修改了editedRange
也沒有用。這樣的話,若是在控制檯上輸入文字或者調試命令,可能會崩潰,崩潰的主要緣由是IDEConsoleTextView
用_startLocationOfLastLine
和_lastRemovableTextLocation
這兩個屬性去控制文字起始位置和刪除位置,在設置mutableString
以後,因爲長度不一,可能會發生字符串取值越界的問題,而NSTextStorage
的代理中又是獲取不到持有它的IDEConsoleTextView
的。
針對第一個問題,咱們可使用通知的方式去解決。
參照喵神的博客,能夠監聽所有的通知,而後去查找哪一個是你所須要的。
-(id)init { if (self = [super init]) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationListener:) name:nil object:nil]; } return self; } -(void)notificationListener:(NSNotification *)noti { NSLog(@" Notification: %@", [noti name]); }
咱們這裏只須要監聽NSTextDidChangeNotification
就行,而後在方法內去判斷一下,以後再設置代理。
-(void)textStorageDidChange:(NSNotification *)noti { if ([noti.object isKindOfClass:NSClassFromString(@"IDEConsoleTextView")] && ((IDEConsoleTextView *)noti.object).textStorage.delegate != self) { ((IDEConsoleTextView *)noti.object).textStorage.delegate = self; } }
這樣就解決了第一個問題。
這裏有興趣的話,能夠參考我另一篇博客:Objective-C runtime常見用法,裏面以舉例的方式講解了常見的runtime用法。
針對第二個問題,我採用的辦法是在適當的時候去修改IDEConsoleTextView
的_startLocationOfLastLine
和_lastRemovableTextLocation
屬性。經實驗,崩潰的方法主要是IDEConsoleTextView
的這些方法:
(void)insertText:(id)arg1;
(void)insertNewline:(id)arg1;
(void)clearConsoleItems;
(BOOL)shouldChangeTextInRanges:(id)arg1 replacementStrings:(id)arg2;
我給IDEConsoleTextView
在運行時添加了如下的方法:
(void)fk_insertText:(id)arg1;
(void)fk_insertNewline:(id)arg1;
(void)fk_clearConsoleItems;
(BOOL)fk_shouldChangeTextInRanges:(id)arg1 replacementStrings:(id)arg2;
以後,使用JRSwizzle來交換、混合方法,相似於這樣:
-(void)addMethodWithNewMethod:(SEL)newMethod originMethod:(SEL)originMethod { Method targetMethod = class_getInstanceMethod(NSClassFromString(@"IDEConsoleTextView"), newMethod); Method consoleMethod = class_getInstanceMethod(self.class, newMethod); IMP consoleIMP = method_getImplementation(consoleMethod); if (!targetMethod) { class_addMethod(NSClassFromString(@"IDEConsoleTextView"), newMethod, consoleIMP, method_getTypeEncoding(consoleMethod)); if (originMethod) { NSError *error; [NSClassFromString(@"IDEConsoleTextView") jr_swizzleMethod:newMethod withMethod:originMethod error:&error]; NSLog(@"error = %@", error); } } }
在fk_
開頭的系列方法中,添加了對IDEConsoleTextView
的檢查:
-(void)fk_checkTextView:(IDEConsoleTextView *)textView { if (textView.textStorage.length < [[textView valueForKeyPath:kStartLocationOfLastLineKey] longLongValue]) { [textView setValue:@(textView.textStorage.length) forKeyPath:kStartLocationOfLastLineKey]; } if (textView.textStorage.length < [[textView valueForKeyPath:kLastRemovableTextLocationKey] longLongValue]) { [textView setValue:@(textView.textStorage.length) forKeyPath:kLastRemovableTextLocationKey]; } } -(void)fk_insertText:(id)arg1 { [self fk_checkTextView:(IDEConsoleTextView *)self]; [self fk_insertText:arg1]; }
這樣,就解決了第二個問題。
OK,FKConsole
這就基本開發完成了。
上文也提到了,Alcatraz
是一個開源的Xcode包管理器。事實上,Alcatraz
也成爲了咱們目前安裝Xcode插件的最主要的工具。
如今咱們將FKConsole
提交到Alcatraz
上。
alcatraz-packages是Alcatraz
的包倉庫列表,packages.json
保存了全部Alcatraz
支持的插件、色彩主題、模板。
咱們fork一下alcatraz-packages到咱們的代碼倉庫中。以後,仿照這種格式,添加上咱們的項目。
{ "name": "FKConsole", "url": "https://github.com/Forkong/FKConsole", "description": "FKConsole is a plugin for Xcode to adjust console display(about Chinese).", "screenshot": "https://raw.githubusercontent.com/Forkong/FKConsole/master/Screenshots/demo.gif" }
rspec
是用ruby寫的一個測試框架,這裏做者寫了一個用於測試你修改事後的packages.json
是否合法的腳本。直接切到alcatraz-packages
目錄下,運行rspec
命令便可。經過的話,會這樣顯示:
rspec
使用ruby的gem就能直接裝上。
校驗沒有問題以後,咱們Pull Request
,咱們的提交就出如今alcatraz-packages的Pull Request
上了:
https://github.com/alcatraz/alcatraz-packages/pull/461
(你們千萬不要像我同樣,沒看清除,直接添加到最後面了。它是有三個分類的,必定要看清楚,要添加到插件的分類上。)