手把手教你用Source Editor Extension開(kāi)發(fā)Xcode插件-實(shí)戰(zhàn)import排序的插件開(kāi)發(fā)

本文始發(fā)于我的博文手把手教你用Source Editor Extension開(kāi)發(fā)Xcode插件况木,現(xiàn)轉(zhuǎn)發(fā)至此蜂挪。

致敬復(fù)聯(lián)

目錄

  • 前言
  • Xcode 插件發(fā)展史
  • 實(shí)現(xiàn)插件
    • 創(chuàng)建 macOS 應(yīng)用
    • 編寫插件代碼
    • 修改插件命名
    • 調(diào)試插件
    • 分發(fā)插件
    • 設(shè)置快捷鍵
  • 進(jìn)階講解
    • Demo 邏輯
    • Plist 文件處理
    • XCSourceEditorCommand 協(xié)議
    • XCSourceEditorExtension 協(xié)議
  • 總結(jié)

前言

一個(gè)項(xiàng)目工程近速,隨著架構(gòu)的階段性穩(wěn)定肮疗、公共組件的抽離和代碼規(guī)范的制定等教届,勢(shì)必會(huì)進(jìn)入一個(gè)"重復(fù)勞動(dòng)"的階段。所謂"重復(fù)勞動(dòng)"烟具,即需求都有固定的模式和分解步驟去完成袄膏,大部分是重復(fù)勒魔、一致的代碼編寫甫煞,只有少部分工作需要思考、抽象冠绢、實(shí)現(xiàn)抚吠。但往往這些"重復(fù)勞動(dòng)"占據(jù)了大部分時(shí)間成本,而且由于其機(jī)械性所以最容易出現(xiàn)問(wèn)題弟胀。

于是將重復(fù)勞動(dòng)自動(dòng)化楷力,即用代碼寫代碼,是一個(gè)團(tuán)隊(duì)的重點(diǎn)工作之一孵户,讓成員將時(shí)間和精力放在更值得關(guān)注的事情上萧朝。最近投入寫各種腳本,而開(kāi)發(fā) IDE 插件夏哭,也可以實(shí)現(xiàn)這種代碼層面的自動(dòng)化检柬。

本來(lái)想做個(gè)插件,實(shí)現(xiàn)生成 cell 的 .xib 和 .swift 文件并自動(dòng)關(guān)聯(lián)等功能竖配,練練手厕吉,結(jié)果發(fā)現(xiàn)目前 Xcode 開(kāi)放的插件并不能支持。

Xcode插件史

在Xcode 8之前械念,Xcode 插件有著比較輝煌的發(fā)展,各種便利的插件运悲、專門的插件管理工具 Alcatraz 等龄减。

但從 Xcode 8 開(kāi)始,出于安全性考慮(比如說(shuō) Xcode ghost 事件)班眯,Apple 不再支持第三方的插件希停,但提供了解決方案—— Xcode Source Editor Extension烁巫,目前只能完成有限的文本編輯輔助。

本文Demo:https://github.com/sapphirezzz/ZXcodeExtension

實(shí)現(xiàn)插件

本文開(kāi)發(fā)環(huán)境:Xcode Version 10.2.1 (10E1001)

創(chuàng)建macOS應(yīng)用

打開(kāi)Xcode宠能,F(xiàn)ile->New->Project…亚隙,選擇 macOS->Application>Cocoa App,填寫 Product Name

創(chuàng)建macOS應(yīng)用

新建 Target违崇,F(xiàn)ile->New->Target…阿弃,選擇 macOS->Application Extension->Xcode Source Editor Extension,填寫 Product Name羞延,如 ZExtension渣淳。在彈窗中選擇 Activate。

注意:該 Target 的命名會(huì)成為后面使用插件時(shí)一級(jí)菜單名稱伴箩。

新建 Target

編寫插件代碼

修改 SourceEditorCommand.swift 文件入愧。

以下代碼實(shí)現(xiàn)將import排序的功能

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 linesToSort = invocation.buffer.lines.filter { line in
        return (line as? String)?.hasPrefix("import") ?? false
    }

    guard linesToSort.count > 0 else {
        completionHandler(nil)
        return
    }

    let firstLineIndex = invocation.buffer.lines.index(of: linesToSort[0]) // For insert
    guard firstLineIndex >= 0 else {
        completionHandler(nil)
        return
    }
    invocation.buffer.lines.removeObjects(in: linesToSort)
    let linesSorted = (linesToSort as? [String] ?? []).sorted() {$0 <= $1}
    linesSorted.reversed().forEach { (line) in
        invocation.buffer.lines.insert(line, at: firstLineIndex)
    }
    let selectionsUpdated: [XCSourceTextRange] = (0..<linesSorted.count).map { (index) in
        let lineIndex = firstLineIndex + index
        let endColumn = linesSorted[index].count - 1
        return XCSourceTextRange(start: XCSourceTextPosition(line: lineIndex, column: 0), end: XCSourceTextPosition(line: lineIndex, column: endColumn))
    }
    invocation.buffer.selections.setArray(selectionsUpdated)
    completionHandler(nil)
}

修改插件命名

在 ZExtension/Info.plist 中可以修改插件名稱,對(duì)應(yīng)的 Key 是 XCSourceEditorCommandName嗤谚,支持中文棺蛛。

不修改則默認(rèn)是 Source Editor Command。

調(diào)試插件

選擇該新建的 Scheme巩步,如 ZExtension旁赊,運(yùn)行(Command+R)。在彈窗中選擇 Xcode渗钉,點(diǎn) 擊Run彤恶。

調(diào)試插件

接下來(lái)會(huì)彈出灰色的Xcode界面,新建項(xiàng)目或者打開(kāi)測(cè)試項(xiàng)目鳄橘。本文用了測(cè)試項(xiàng)目 Test声离。

測(cè)試項(xiàng)目:https://github.com/sapphirezzz/ZXcodeExtension/tree/master/Test

測(cè)試項(xiàng)目Test

使用插件排序,點(diǎn)擊 Editor->ZExtension->Source Editor Command瘫怜。

使用插件

以下為插件運(yùn)行后的結(jié)果:

運(yùn)行結(jié)果

分發(fā)插件

  • 上架 Mac App Store

編寫的插件可以發(fā)布术徊,上架到 Mac App Store。在 Xcode->Xcode Extensions… 可以看到上架的插件鲸湃。筆者還沒(méi)有發(fā)布赠涮,先略過(guò)。

  • 內(nèi)部使用

在插件項(xiàng)目中暗挑,將 Products->ZXcodeExtension.app 文件拷貝到應(yīng)用程序笋除,并雙擊打開(kāi)。此時(shí)在系統(tǒng)偏好設(shè)置->擴(kuò)展->Xcode Source Editor炸裆,可以看到該插件垃它,并且已勾選。重啟 Xcode 就可以使用了。

設(shè)置快捷鍵

可以給插件設(shè)置快捷鍵国拇,方便使用洛史。

在 Xcode->Preferences…->Key Bindings->Editor Menu for Source Code,找到并設(shè)置酱吝。建議用 alt 如 alt+s也殖,避免和其他快捷鍵沖突。

進(jìn)階講解

實(shí)現(xiàn)之后务热,簡(jiǎn)單講解下一些細(xì)節(jié)忆嗜。

Demo邏輯

Demo中主要操作了兩個(gè)內(nèi)容:

  1. invocation.buffer.lines
  2. invocation.buffer.selections

lines 是當(dāng)前編輯文件的每一行的內(nèi)容,selections 是當(dāng)前編輯文件選中的內(nèi)容陕习。

Demo 邏輯是:

  1. 篩選出符合條件的行 linesToSort(以 import 開(kāi)頭)
  2. 記錄第一個(gè)符合條件的行的行數(shù)firstLineIndex霎褐,作為排序后的插入位置
  3. 從 invocation.buffer.lines 中刪除符合條件的行
  4. 將符合條件的行進(jìn)行排序得出 linesSorted
  5. 將排序后的行插入 invocation.buffer.lines
  6. 獲取所有改動(dòng)行信息 selectionsUpdated,設(shè)置 invocation.buffer.selections

主要是對(duì) XCSourceEditorCommand 協(xié)議的實(shí)現(xiàn)该镣。

Plist 文件處理

Info.plist 文件中重要的 key 是 NSExtension 的 NSExtensionAttributes冻璃,包含兩個(gè) key:

  1. XCSourceEditorCommandDefinitions
  2. XCSourceEditorExtensionPrincipalClass
XCSourceEditorCommandDefinitions

XCSourceEditorCommandDefinitions 是設(shè)置了每個(gè)命令(二級(jí)菜單)的信息:

  1. XCSourceEditorCommandClassName
  2. XCSourceEditorCommandIdentifier
  3. XCSourceEditorCommandName

第一個(gè)是處理這個(gè)命令的類名,該類需實(shí)現(xiàn) XCSourceEditorCommand 協(xié)議损合;第二個(gè)是每個(gè)命令的標(biāo)示省艳,用于 XCSourceEditorCommand 協(xié)議的方法區(qū)分處理命令;第三個(gè)是命令的展示名字嫁审。

XCSourceEditorExtensionPrincipalClass

該擴(kuò)展的類名跋炕,該類需實(shí)現(xiàn) XCSourceEditorExtension 協(xié)議。

XCSourceEditorCommand協(xié)議

/** A command provided by a source editor extension. There does not need to be a one-to-one mapping between command classes and commands: Multiple commands can be handled by a single class, by checking their invocation's commandIdentifier at runtime. */
@protocol XCSourceEditorCommand <NSObject>

根據(jù)官方注釋律适,一個(gè)實(shí)現(xiàn)了 XCSourceEditorCommand 的類可以處理多種命令辐烂,即多個(gè)二級(jí)菜單,通過(guò) invocation.commandIdentifier 來(lái)區(qū)分捂贿。而 commandIdentifier 是 Info.plist 中纠修,XCSourceEditorCommandDefinitions 里面每一項(xiàng)的 XCSourceEditorCommandIdentifier 所定義的。

/** Perform the action associated with the command using the information in \a invocation. Xcode will pass the code a completion handler that it must invoke to finish performing the command, passing nil on success or an error on failure.
 
 A canceled command must still call the completion handler, passing nil.
 
 \note Make no assumptions about the thread or queue on which this method will be invoked.
 */
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler;

這是 XCSourceEditorCommand 協(xié)議定義的方法厂僧。

  • XCSourceEditorCommandInvocation

commandIdentifier 屬性扣草,用于區(qū)分不同命令;buffer颜屠,XCSourceTextBuffer 類型辰妙,主要用它的 lines 和 selections 屬性。

  • completionHandler

實(shí)現(xiàn)邏輯之后甫窟,必須調(diào)用 completionHandler 以結(jié)束插件命令密浑,成功時(shí)傳參 nil,失敗時(shí)傳參 error 對(duì)象粗井。即使取消處理也需要調(diào)用并傳參 nil肴掷。

結(jié)合 Plist 文件和 XCSourceEditorCommand 協(xié)議敬锐,我們可以編寫處理多個(gè)命令的插件。

XCSourceEditorExtension協(xié)議

/** Invoked when the extension has been launched, which may be some time before the extension actually receives a command (if ever).
 
 \note Make no assumptions about the thread or queue on which this method will be invoked.
 */
- (void)extensionDidFinishLaunching;

插件被加載后的處理呆瞻。

總結(jié)

可以看出,目前 Xcode Source Editor Extension 解決方案能實(shí)現(xiàn)的插件功能很有限径玖,不支持UI交互痴脾,只能局限于文本處理上。希望以后蘋果能擴(kuò)展更多 API 供開(kāi)發(fā)者使用梳星。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末赞赖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子冤灾,更是在濱河造成了極大的恐慌前域,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件韵吨,死亡現(xiàn)場(chǎng)離奇詭異匿垄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)归粉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門椿疗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人糠悼,你說(shuō)我怎么就攤上這事届榄。” “怎么了倔喂?”我有些...
    開(kāi)封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵铝条,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我席噩,道長(zhǎng)班缰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任班挖,我火速辦了婚禮鲁捏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘萧芙。我一直安慰自己给梅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布双揪。 她就那樣靜靜地躺著动羽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渔期。 梳的紋絲不亂的頭發(fā)上运吓,一...
    開(kāi)封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天渴邦,我揣著相機(jī)與錄音,去河邊找鬼拘哨。 笑死谋梭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的倦青。 我是一名探鬼主播瓮床,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼产镐!你這毒婦竟也來(lái)了隘庄?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤癣亚,失蹤者是張志新(化名)和其女友劉穎丑掺,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體述雾,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡街州,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绰咽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菇肃。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖取募,靈堂內(nèi)的尸體忽然破棺而出琐谤,到底是詐尸還是另有隱情,我是刑警寧澤玩敏,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布斗忌,位于F島的核電站,受9級(jí)特大地震影響旺聚,放射性物質(zhì)發(fā)生泄漏织阳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一砰粹、第九天 我趴在偏房一處隱蔽的房頂上張望唧躲。 院中可真熱鬧,春花似錦碱璃、人聲如沸弄痹。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肛真。三九已至,卻和暖如春爽航,著一層夾襖步出監(jiān)牢的瞬間蚓让,已是汗流浹背乾忱。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留历极,地道東北人窄瘟。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像执解,于是被迫代替她去往敵國(guó)和親寞肖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容