給自己的Xcode寫個自動生成代碼小插件

作為界面開發(fā)工程師蓄坏,各種UI相關的代碼董习,大部分都是類似的榔袋,手動敲起來也沒啥意思,很耽誤開發(fā)的時間

我們一般可以編寫一些代碼塊铡俐,可以快捷的去使用凰兑;同時也可以編寫Xcode的小插件來自動生成這些代碼

在github上也找到了類似的插件,自己剛好最近也想著學習下Swift語言审丘,所以就站在巨人的肩上吏够,把原作者的插件用Swift去寫了一遍;本文就記錄下學習寫插件的過程

1. Xcode Source Editor Extension介紹

在macOS 10.12及以上滩报,蘋果提供了XcodeKit用來給Xcode增加源碼編輯器擴展锅知;

使用XcodeKit框架,您可以使用源代碼編輯器擴展名自定義Xcode脓钾,以向源代碼編輯器添加功能和特殊行為售睹。源代碼編輯器擴展可以讀取和修改源文件的內容,以及在編輯器中讀取和修改當前的文本選擇可训。

Using the XcodeKit framework, you can customize Xcode with source editor extensions to add functionality and specialized behavior to the source editor. Source editor extensions provide a group of editor commands alongside the built-in commands in the Editor menu in Xcode. Source editor extensions can read and modify the contents of a source file, as well as read and modify the current text selection within the editor. Include source editor extensions in developer apps distributed on the Mac App Store.

1.1 創(chuàng)建macOS工程
Xcode菜單選擇 File -- New -- Project昌妹,選擇macOS捶枢,創(chuàng)建App

圖片.png

1.2 添加Extension
Xcode菜單選擇 File -- New -- Target,選擇macOS飞崖,創(chuàng)建Xcode Source Editor Extension烂叔,如下圖所示:

圖片.png

這里我們是寫Xcode代碼相關的插件,所以選擇的是Xcode Source Editor Extension固歪;如果你想編寫其他類型的插件蒜鸡,可以按需選擇對于的Extension

創(chuàng)建好target之后,會自動生成2個文件SourceEditorExtensionSourceEditorCommand
我們可以按需在這里添加插件的功能及對應功能的實現(xiàn)

2.關鍵類介紹

2.1 SourceEditorExtension
SourceEditorExtension遵循XCSourceEditorExtension用來創(chuàng)建Xcode源代碼編輯器擴展的協(xié)議牢裳,也就是Xcode的Editor增加的Extension的功能菜單

增加功能菜單支持2種方式逢防,一種是代碼的方式,一種是在Extension的Info.plist文件中去配置

2.1.1 代碼的方式

實現(xiàn)commandDefinitions方法贰健,里面返回功能菜單的數(shù)組

  • XCSourceEditorCommandDefinitionKey.classNameKey : 功能的實現(xiàn)的類名胞四,我這里就是kSourceEditorClassName這里需要注意一下,需要帶上模塊名let kSourceEditorClassName = "HCXcodeTools.SourceEditorCommand"
  • XCSourceEditorCommandDefinitionKey.identifierKey : 功能的唯一ID伶椿,一般是Extension的bundleId+一個后綴辜伟,這里主要是在執(zhí)行該功能的時候,可以通過這個Id去區(qū)分是哪個功能
  • XCSourceEditorCommandDefinitionKey.nameKey : 功能的名稱脊另,展示在Extension功能菜單的名稱
class SourceEditorExtension: NSObject, XCSourceEditorExtension {
/*
    func extensionDidFinishLaunching() {
        // If your extension needs to do any work at launch, implement this optional method.
    }
*/
    var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] {
        // If your extension needs to return a collection of command definitions that differs from those in its Info.plist, implement this optional property getter.
        let addLazyCodeItem : [XCSourceEditorCommandDefinitionKey: Any] = [
            XCSourceEditorCommandDefinitionKey.classNameKey : kSourceEditorClassName,
            XCSourceEditorCommandDefinitionKey.identifierKey: kAddLazyCodeIdentifier,
            XCSourceEditorCommandDefinitionKey.nameKey: kAddLazyCodeName
        ]
        let initViewItem : [XCSourceEditorCommandDefinitionKey: Any] = [
            XCSourceEditorCommandDefinitionKey.classNameKey : kSourceEditorClassName,
            XCSourceEditorCommandDefinitionKey.identifierKey: kInitViewIdentifier,
            XCSourceEditorCommandDefinitionKey.nameKey: kInitViewName
        ]
        let addImportItem : [XCSourceEditorCommandDefinitionKey: Any] = [
            XCSourceEditorCommandDefinitionKey.classNameKey : kSourceEditorClassName,
            XCSourceEditorCommandDefinitionKey.identifierKey : kAddImportIdentifier,
            XCSourceEditorCommandDefinitionKey.nameKey : kAddImportName
        ]
        
        return [addLazyCodeItem,
                addImportItem,
                initViewItem
        ]
    }
    
}
2.1.2 通過Info.plist配置的方式
圖片.png
  • XCSourceEditorExtensionPrincipalClass : 這個對應的就是功能實現(xiàn)回調的類导狡;當我們執(zhí)行Extension的功能的時候,會執(zhí)行該類定義的方法偎痛,下面會介紹
  • XCSourceEditorCommandDefinitions : 這里就是定義Extension的功能菜單
2.1.3 運行看看效果
圖片.png

如果你的Xcode菜單是灰掉的旱捧,那么可能就是Extension運行報錯了
解決方法:
1.調試 Extension,直接運行看看控制臺輸出踩麦;具體操作如下圖所示

圖片.png

2.分析控制臺日志枚赡,針對性的去處理

dyld: Library not loaded: @rpath/XcodeKit.framework/Versions/A/XcodeKit
  Referenced from: 

比如我這里是報XcodeKit.framework這個庫找不到,那么就去項目中
看看是否添加了這個庫谓谦,如果沒添加就添加一下贫橙,有添加,檢查一下是否是Embed & Sign(如下圖所示)反粥;如果是這樣配置的卢肃,那么刪掉再重新加一下

圖片.png

3.當Extension能正常運行了,Xcode的菜單就不會顯示是灰色的了

菜單定義好了才顿,接下來就去實現(xiàn)對應的菜單的功能了

2.2 SourceEditorCommand
這個就是功能菜單執(zhí)行的回調的類了莫湘,當我們在Xcode的某個類文件,執(zhí)行功能的時候郑气,就會調用perform函數(shù)幅垮,XCSourceEditorCommandInvocation就是源碼編輯命令的內容,包含了源碼的行信息尾组,選中信息等等

func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void

在perform函數(shù)军洼,我們就可以根據(jù)commandIdentifier來判斷是調用了哪個功能菜單巩螃,然后分發(fā)給對應的實現(xiàn)去處理;這里也就是為什么上面說的再定義的時候需要設置identifierKey為唯一的ID

class SourceEditorCommand: NSObject, XCSourceEditorCommand {
    
    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
        // Implement your command here, invoking the completion handler when done. Pass it nil on success, and an NSError on failure.
        let identifier = invocation.commandIdentifier
        print(identifier)
        if identifier == kAddLazyCodeIdentifier {
            AddLazyCodeManager.sharedInstance.processCodeWithInvocation(invocation: invocation)
        } else if identifier == kInitViewIdentifier {
            InitViewManager.sharedInstance.processCodeWithInvocation(invocation: invocation)
        } else if identifier == kAddImportIdentifier {
            AddImportManager.sharedInstance.processCodeWithInvocation(invocation: invocation)
        }
        completionHandler(nil)
    }
    
}
2.2.1 XCSourceEditorCommandInvocation

源碼編輯器的內容對象匕争,包含了內容緩沖區(qū)buffer以及功能的ID避乏,我們主要就是通過操作XCSourceTextBuffer *buffer來修改編輯器的內容

@interface XCSourceEditorCommandInvocation : NSObject

- (instancetype)init NS_UNAVAILABLE;

@property (readonly, copy) NSString *commandIdentifier;

@property (readonly, strong) XCSourceTextBuffer *buffer;

@property (copy) void (^cancellationHandler)(void);

@end
2.2.2 XCSourceTextBuffer

編輯器內容緩沖區(qū),包含了源碼編輯器的配置信息甘桑,以及正在編輯的源碼文件的行信息NSMutableArray <NSString *> *lines拍皮,以及用戶在源碼文件中選中的區(qū)域信息NSMutableArray <XCSourceTextRange *> *selections

XCSourceTextRange則包含了選中區(qū)域的開始行和列XCSourceTextPosition start以及結束的行和列信息XCSourceTextPosition end

/** A single text position within a buffer. All coordinates are zero-based. */
typedef struct {
    NSInteger line;
    NSInteger column;
} XCSourceTextPosition;
/** A buffer representing some editor text. Mutations to the buffer are tracked and committed when a command returns YES and has not been canceled by the user. */
@interface XCSourceTextBuffer : NSObject

/** An XCSourceTextBuffer is not directly instantiable. */
- (instancetype)init NS_UNAVAILABLE;

/** The UTI of the content in the buffer. */
@property (readonly, copy) NSString *contentUTI;

/** The number of space characters represented by a tab character in the buffer. */
@property (readonly) NSInteger tabWidth;

/** The number of space characters used for indentation of the text in the buffer. */
@property (readonly) NSInteger indentationWidth;

@property (readonly) BOOL usesTabsForIndentation;

@property (readonly, strong) NSMutableArray <NSString *> *lines;

@property (readonly, strong) NSMutableArray <XCSourceTextRange *> *selections;

@property (copy) NSString *completeBuffer;

@end

我們就是通過修改buffer的內容來達到修改源碼編輯器文件的內容,從而實現(xiàn)一些功能跑杭,比如插入懶加載代碼铆帽、初始化類文件的代碼、import頭文件等等操作

3. 如何調試

官方提供的調試方式
我則習慣直接Xcode 運行項目德谅,然后通過Xcode的Debug -- Attach to Process的方式去調試我們寫的插件的功能

圖片.png

在實際調試過程中爹橱,如果Attach to Process報錯,那么就大退一下Xcode窄做,然后在運行 或者在終端執(zhí)行kill -9 95148 95148是extension的processId愧驱;接下來我們就可以邊寫代碼,邊調試來完善提供的功能了

4. 編寫小插件Commond實現(xiàn)代碼

以選中代碼源文件的某一行來導入頭文件的功能為例:

  • 解析選中的行列信息得到選中的文本內容
  • 拼裝需要插入的內容文本
  • 查找需要插入的位置椭盏,就是遍歷源碼編輯器內容的lines信息得到最后一個import行的行號+1
  • 將拼裝的內容文本插入到對應的位置
class AddImportManager : HCEditorCommondHandler {
    static let sharedInstance = AddImportManager()
    func processCodeWithInvocation(invocation : XCSourceEditorCommandInvocation) -> Void {
        print("add import")
        guard invocation.buffer.selections.count > 0 else {
            return
        }
        let selectRange: XCSourceTextRange = invocation.buffer.selections.firstObject as! XCSourceTextRange
        let startLine = selectRange.start.line // 選中的開始行
        let endLine = selectRange.end.line // 選中的結束行
        let startColumn = selectRange.start.column // 選中的內容開始列
        let endColumn = selectRange.end.column // 選中的內容結束列
        guard startLine == endLine && startColumn != endColumn else { // 支持單行選中组砚,并且需要選中內容
            return
        }
        let selectedLineString: NSString = invocation.buffer.lines.object(at: startLine) as! NSString
        let selectedContentString : NSString = selectedLineString.substring(with: NSMakeRange(startColumn, endColumn - startColumn)).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) as NSString
        guard selectedContentString.length > 0 else {
            return
        }
        // 拼接導入頭文件的內容
        let insertString: NSString = NSString.init(format: "#import \"%@.h\"", selectedContentString)
        var alreadyIndex: NSInteger = 0
        alreadyIndex = invocation.buffer.lines.indexOfFirstItemContainString(string: insertString) // 獲取是否已經(jīng)導入過了
        if alreadyIndex != NSNotFound { // 已經(jīng)導入過頭文件了
            return
        }
        // 查找import的最后一行的index
        var lastImportLine = NSNotFound
        for index in 0...invocation.buffer.lines.count-1 {
            let lineString = invocation.buffer.lines[index]
            if lineString is NSString {
                var tempString: NSString = lineString as! NSString
                tempString = tempString.deleteSpaceAndNewLine()
                if tempString.contains("import") {
                    lastImportLine = index
                }
            }
        }
        // 設置插入的行號,如果buffer中已經(jīng)有import過則lastImportIndex不為NSNotFound掏颊,此時插入到lastImportIndex的后一行糟红;否則就插入在首行
        var insertLine = 0
        if lastImportLine != NSNotFound {
            insertLine = lastImportLine + 1
        }
        invocation.buffer.lines.insert(insertString, at: insertLine)
    }
}

其他的功能也都類似,大部分都是在操作lines信息乌叶,來讀取選中的內容盆偿,以及插入需要插入的文本內容;我們邊編碼邊調試最終就能完成對應的功能准浴。

具體的代碼實現(xiàn)HCXcodeToolsExtension

5. 集成到Xcode的Editor菜單

進入系統(tǒng)偏好設置--擴展事扭,選中我們的插件鉤上即可

圖片.png

如果更新Xcode之后發(fā)現(xiàn)擴展里面沒有Xcode Source Editor的選項,那么有一個騷操作可以解決:將Xcode.app命名改一下再改回去就出來了

設置操作的快捷鍵
按照自己的操作習慣設置對應功能的快捷鍵兄裂,然后就可以愉快的玩起來了

圖片.png

圖片.png
6. 打包出dmg

我們如果想要把插件給其他人用,可以直接讓他運行源代碼阳藻,然后按照上面的步驟集成到Xcode的Editor菜單去

這里我們使用一種將app打包成dmg的方式晰奖,讓別人直接安裝、配置一下就可以使用

6.1 準備打包需要的文件
  • app包 : 直接Xcode運行腥泥,然后選中xxx.app -- 右鍵Show In Finder就可以找到了
  • Mac Application的快捷方式 : 選中Mac的應用程序文件夾--右鍵選擇“制作替身”即可


    圖片.png

新建一個文件夾匾南,將以上兩個文件放進來


圖片.png
6.2 使用磁盤工具導出dmg文件

打開磁盤工具

圖片.png

新建基于文件夾的鏡像

圖片.png

選中我們上面的文件夾,然后點擊存儲即可


圖片.png

導出完成蛔外,就生成了dmg文件


圖片.png

安裝包地址:HCXcodeTools.dmg
看看成果
雙擊解壓dmg文件蛆楞,將app拖到應用程序溯乒,運行下,在按照上面的步驟設置一下豹爹,配置下快捷鍵就可以了

圖片.png

7 總結

蘋果提供的Extension的種類還是挺多的裆悄,自己學習Swift語言,光看語法也沒啥意思臂聋,就想著用Swift寫一個Xcode的小插件來順便學習下Swift的語法

學習了下打包dmg文件分發(fā)mac app光稼,還挺簡單的

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市孩等,隨后出現(xiàn)的幾起案子艾君,更是在濱河造成了極大的恐慌,老刑警劉巖肄方,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冰垄,死亡現(xiàn)場離奇詭異,居然都是意外死亡权她,警方通過查閱死者的電腦和手機虹茶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伴奥,“玉大人写烤,你說我怎么就攤上這事∈搬悖” “怎么了洲炊?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尼啡。 經(jīng)常有香客問我暂衡,道長,這世上最難降的妖魔是什么崖瞭? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任狂巢,我火速辦了婚禮,結果婚禮上书聚,老公的妹妹穿的比我還像新娘唧领。我一直安慰自己,他們只是感情好雌续,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布斩个。 她就那樣靜靜地躺著,像睡著了一般驯杜。 火紅的嫁衣襯著肌膚如雪受啥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音滚局,去河邊找鬼居暖。 笑死,一個胖子當著我的面吹牛藤肢,可吹牛的內容都是我干的太闺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼谤草,長吁一口氣:“原來是場噩夢啊……” “哼跟束!你這毒婦竟也來了?” 一聲冷哼從身側響起丑孩,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤冀宴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后温学,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體略贮,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年仗岖,在試婚紗的時候發(fā)現(xiàn)自己被綠了逃延。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡轧拄,死狀恐怖揽祥,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情檩电,我是刑警寧澤拄丰,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站俐末,受9級特大地震影響料按,放射性物質發(fā)生泄漏。R本人自食惡果不足惜卓箫,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一载矿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烹卒,春花似錦闷盔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坠非,卻和暖如春敏沉,著一層夾襖步出監(jiān)牢的瞬間果正,已是汗流浹背炎码。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工盟迟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人潦闲。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓攒菠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親歉闰。 傳聞我的和親對象是個殘疾皇子辖众,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359