Xcode Source Editor Extension

  • 开发环境:Xcode 9

  • 开发语言:Swift 4

  • Demo gif:


    Kapture 2017-11-07 at 11.22.51.gif

1. 新建app

1.1 创建macOS app

image.png

1.2 app取名和选择语言

image.png

1.3 新建Target

image.png

1.4 选择xcode source editor extension

image.png

1.5 Target取名

image.png

1.6 选择Activate

image.png

1.7 配置Team[如果已经配置可忽略,否则必须配置]

  • 2个TARGETSTeam需要一致
image.png

1.8 显示

  • 选择新建的Target-> 运行
image.png
  • 选择Xcode运行
image.png
  • 会显示一个新的灰色的Xcode,选择一个工程运行
image.png
  • 选中Editor会显示插件名,只是个空插件
image.png

2. 关于xcode source editor extension

2.1 SourceEditorExtension.swift

  • func extensionDidFinishLaunching() :在extension启动的时候会被调用,刚加载好插件但还未点击插件按钮时,可以执行某些准备工作。
  • commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] : 返回字典类型的数组,可以为每个插件重写名字、标识符和自定义类名等信息,设置后会覆盖Info.plist文件中对应的NSExtension
    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.
        return [
                [.classNameKey : "插件.SourceEditorCommand", // 格式:Target名.Command文件名
                 .identifierKey : "com.Devin.XcodeSourceEditorDemo.SourceEditorCommand", // 格式: BundleIdentifier.任意字符串
                 .nameKey : "UITableView"]
                ]
    }

2.2 SourceEditorCommand.swift

  • 在这个文件里面可以实现extension的相关逻辑
  • perform(with:completionHandler:) : 在用户启动你的extension的时候被调用
  • XCSourceEditorCommandInvocation对象包含了一个buffer属性,这个属性主要是用来访问当前文件的源代码,和光标选中范围
  • completionHandler将会以参数为nil进行调用,告诉Xcode命令执行完毕。否则将会给它传递一个Error实例。

3. info.plist 配置

QQ20171107-155713.png

4. 示例代码

4.1 获取当前光标所在的类名

   /// 获取文件名
    /// 规则:1.根据光标所在的位置,获取距离光标上一行代码,最近的`class`
    ///      2.如果没有获取到。则获取第二行 `xxxx.swift` 文本
    ///      3.如果上述都不成立,则为`nil`
    /// - Parameters:
    ///   - selection: XCSourceTextRange
    ///   - comment: [String]
    /// - Returns: String?
    fileprivate func fileName(selection:XCSourceTextRange, comment: [String]) -> String? {
        let secondLine = comment[1]
        var filename:String?
        filename = selectionFileName(selection: selection, comment: comment).className ?? headerFileName(fromFileNameComment: secondLine)
        return filename
    }

    // "//  Classname.swift" -> "Classname"
    fileprivate func headerFileName(fromFileNameComment comment: String) -> String? {

        let comment = comment.trimmingCharacters(in: .whitespacesAndNewlines)

        let commentPrefix = "//"
        guard comment.hasPrefix(commentPrefix) else { return nil }

        let swiftExtensionSuffix = ".swift"
        guard comment.hasSuffix(swiftExtensionSuffix) else { return nil }

        let startIndex = comment.index(comment.startIndex, offsetBy: commentPrefix.characters.count)
        let endIndex = comment.index(comment.endIndex, offsetBy: -swiftExtensionSuffix.characters.count)

        return comment[startIndex..<endIndex].trimmingCharacters(in: .whitespacesAndNewlines)
    }

    fileprivate func selectionFileName(selection:XCSourceTextRange, comment: [String]) -> (className:String?, classLine:Int) {
        var ownfileName:String?
        var classIndex = 0
        // 获取当前光标上面所在最近的类
        guard selection.end.line > 0 else {
            return (nil,classIndex)
        }
        let selectionEnd = selection.end.line - 1
        guard selectionEnd < comment.count else {
            return (nil,classIndex)
        }
        let selectionBefore = comment[0...selectionEnd]
        for (index,element) in selectionBefore.reversed().enumerated() {
            if element.hasPrefix(classString) {
                classIndex = selectionBefore.count - index
                // 去掉 classString
                var newElement = element.replacingOccurrences(of: classString, with: "")
                // 去掉 \n
                newElement = newElement.trimmingCharacters(in: .whitespacesAndNewlines)
                // 去掉 空格
                newElement = newElement.replacingOccurrences(of: " ", with: "")
                let nsNewElement = newElement as NSString
                if newElement.contains(":") {
                    let getRange = nsNewElement.range(of: ":")
                    ownfileName = nsNewElement.substring(to: getRange.location)
                }else if newElement.contains("{") {
                    let getRange = nsNewElement.range(of: "{")
                    ownfileName = nsNewElement.substring(to: getRange.location)
                }else {
                    ownfileName = newElement
                }
                break
            }
        }
        return (ownfileName,classIndex)
    }

4.2 代码文本

   fileprivate func insertSelectionCode() -> String {
        let result = """
        \t\tmainTableView.register(<#T##cellClass: AnyClass?##AnyClass?#>, forCellReuseIdentifier: <#T##String#>)
        \t\tmainTableView.register(<#T##nib: UINib?##UINib?#>, forCellReuseIdentifier: <#T##String#>)
        \t\tview.addSubview(mainTableView)
        """
        return result
    }

    fileprivate func insertVarCode() -> String {
        let result = """
        \n
        \tlazy var mainTableView:UITableView = {
        \t\tvar mainTableView = UITableView(frame: <#T##CGRect#>, style: <#T##UITableViewStyle#>)
        \t\tmainTableView.dataSource = self
        \t\tmainTableView.delegate = self
        \t\tmainTableView.tableFooterView = UIView()
        \t\tmainTableView.separatorStyle = .none
        \t\treturn mainTableView
        \t}()
        \n
        """
        return result
    }

    fileprivate func insertEndCode(_ className:String) -> [String] {
        let result = """
        \n
        extension \(className): UITableViewDataSource, UITableViewDelegate {

        \t// MARK: - UITableViewDataSource
        \tfunc tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        \t\t<#code#>
        \t}

        \tfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        \t\t<#code#>
        \t}

        \tfunc numberOfSections(in tableView: UITableView) -> Int {
        \t\t<#code#>
        \t}

        \t// MARK: - UITableViewDelegate
        \tfunc tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        \t\t<#code#>
        \t}

        \tfunc tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        \t\t<#code#>
        \t}
        }
        """
        return [result]
    }

4.3 功能实现

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 selection = invocation.buffer.selections.firstObject as? XCSourceTextRange
      // 当前文件每行显示的内容
      let lines = invocation.buffer.lines
      let linesStr = lines.map{$0 as! String}

      guard selection != nil else {
          completionHandler(nil)
          return
      }

      // 获取类名
      let ownfileName = fileName(selection: selection!, comment: linesStr)
      // 插入代码 var
      lines.insert(insertVarCode(), at: selectionFileName(selection: selection!, comment: linesStr).classLine)
      // 光标处 插入代码
      lines.insert(insertSelectionCode(), at: selection!.end.line)

      // 尾部拼接代理
      if ownfileName != nil {
          lines.addObjects(from: insertEndCode(ownfileName!))
      }
      completionHandler(nil)
}

5. 打包DMG

5.1 获取app所在文件位置

image.png

5.2 桌面新建文件夹,把app和Applications替身文件夹放入其中

  • 制作Application的替身:cd到这个目录,建立一个软链接。
$ ln -s /Applications/   Applications
image.png

5.3 打包DMG

image.png
  • 选择上面新建的文件夹,然后打开。会在文件夹中生成dmg

6. 使用

6.1 把app 拖入Applications文件夹放入其中

6.2 运行app

6.3 然后,打开“系统偏好设置” -> "扩展" -> "Xcode Source Editor" -> 确认插件名字前已打钩

image.png

6.4 退掉Xcode,重新运行

image.png

6.5 设置快捷键

  • Xcode -> "Preferences" -> "Key Bindings" -> 搜索插件名字 -> 添加对应的快捷键:


    image.png

6.6 插件删除

  • 直接把app移到废纸楼即可


    image.png

7 Demo地址

Demo

8 其他Xcode Source Editor Extension

Awesome native Xcode extensions

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259