转载

教你如何用Swift编写Xcode插件

教你如何用Swift编写Xcode插件

  • 本文由CocoaChina译者@ztdj121 翻译

  • 作者: Krzysztof

  • 原文: Writing Xcode plugin in Swift

GitHub上的源代码

教你如何用Swift编写Xcode插件

在我的 AppCode 项目创建过程中,我想念最多的一件事是:能跳转到记录控制台信息的指定文件和行。

Xcode不提供这样的功能,而我不是一个喜欢抱怨的人,所以我决定自己写个插件。 我用Swift来编写这个插件。

想法

如果一个控制台记录了fileName.extension:XX 这样一个名字,转换成可点击的超链接,这个链接将会打开指定的文件并将那行代码高亮。

那样你可以使用自己的记录机制,只要添加这个简单的前缀,比如:

【代码】

func logMessage(message: String, filename: String = __FILE__, line: Int = __LINE__, funct: String = __FUNCTION__) {     print("/((filename as NSString).lastPathComponent):/(line) /(funct):/r/(message)") }

或者可以使用 CocoaLumberjack ,你要想一些好的日志,可以用我的自定义格式。

Swift版本(Objective-C版本是 KZBootstrap 的一部分)

import Foundation import CocoaLumberjack.DDDispatchQueueLogFormatter  class KZFormatter: DDDispatchQueueLogFormatter {    lazy var formatter: NSDateFormatter = {       let dateFormatter = NSDateFormatter()       dateFormatter.formatterBehavior = .Behavior10_4       dateFormatter.dateFormat = "HH:mm:ss.SSS"       return dateFormatter   }()      override func formatLogMessage(logMessage: DDLogMessage!) -> String {       let dateAndTime = formatter.stringFromDate(logMessage.timestamp)              var logLevel: String       let logFlag = logMessage.flag       if logFlag.contains(.Error) {           logLevel = "ERR"       } else if logFlag.contains(.Warning){           logLevel = "WRN"       } else if logFlag.contains(.Info) {           logLevel = "INF"       } else if logFlag.contains(.Debug) {           logLevel = "DBG"       } else if logFlag.contains(.Verbose) {           logLevel = "VRB"       } else {           logLevel = "???"       }              let formattedLog = "/(dateAndTime) |/(logLevel)| /((logMessage.file as NSString).lastPathComponent):/(logMessage.line): ( /(logMessage.function) ): /(logMessage.message)"       return formattedLog;   } }

实现—主要部分

要实现那些需求我们需要做到两点:

1、控制台NSTextStorage fixAttributesInRange--这样我们可以在找到正则表达式日志的时候随时更改属性。

2、NSTextView mouseDown--这样在控制台的链接里点击鼠标的时候,我们可以强迫Xcode打开文件并高亮那一行。

怎样把我们的功能注入到那些操作里去?

简单调整:

static func swizzleMethods() {   let original = class_getInstanceMethod(NSClassFromString("NSTextStorage"), Selector("fixAttributesInRange:"))   method_exchangeImplementations(original, class_getInstanceMethod(NSClassFromString("NSTextStorage"), Selector("kz_fixAttributesInRange:")))      let original2 = class_getInstanceMethod(NSClassFromString("NSTextView"), Selector("mouseDown:"))   method_exchangeImplementations(original2, class_getInstanceMethod(NSClassFromString("NSTextView"), Selector("kz_mouseDown:"))) }

我们如何确定一个NSTextStorage 是控制台实际的那个?

我们可以观察IDEControlGroupDidChangeNotification ,找到IDEConsoleTextView 并使用相关对象把存储标记为控制台的那个,这个随后就会排上用场。

guard let consoleTextView = KZPluginHelper.consoleTextView(), let textStorage = consoleTextView.valueForKey("textStorage") as? NSTextStorage else {     return } textStorage.kz_isUsedInXcodeConsole = true

我们怎样找到一个文件的路径,而只有日志中的相对路径?

我们可以用shell里的find命令,这就是你如何用swift语言运行且从一个shell命令中检索响应。

static func runShellCommand(command: String) -> String? {   let pipe = NSPipe()   let task = NSTask()   task.launchPath = "/bin/sh"   task.arguments = ["-c", String(format: "%@", command)]   task.standardOutput = pipe   let file = pipe.fileHandleForReading   task.launch()   guard let result = NSString(data: file.readDataToEndOfFile(), encoding: NSUTF8StringEncoding)?.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet()) else {       return nil   }   return result as String }

把链接放到日志中

  • 使用模式匹配来找到日志里的事件。

  • 使用shell里的find命令来检索工程的完整路径。

  • 添加自定义属性来存储字符串本身的信息。

private func injectLinksIntoLogs() {     let text = string as NSString     guard let path = KZPluginHelper.workspacePath() else {         return     }     let matches = pattern.matchesInString(string, options: .ReportProgress, range: editedRange)     for result in matches where result.numberOfRanges == 4 {         let fullRange = result.rangeAtIndex(0)         let fileNameRange = result.rangeAtIndex(1)         let extensionRange = result.rangeAtIndex(2)         let lineRange = result.rangeAtIndex(3)         guard let result = KZPluginHelper.runShellCommand("find /"/(path)/" -name /"/(text.substringWithRange(fileNameRange))./(text.substringWithRange(extensionRange))/" | head -n 1") else {             continue         }         addAttribute(NSLinkAttributeName, value: "", range: fullRange)         addAttribute(KZLinkedConsole.Strings.linkedPath, value: result, range: fullRange)         addAttribute(KZLinkedConsole.Strings.linkedLine, value: text.substringWithRange(lineRange), range: fullRange)         addAttribute(NSBackgroundColorAttributeName, value: NSColor.whiteColor(), range: fullRange)     } }

打开文件,然后滚到指定的行

打开一个文件像调用一样简单:

public func application(sender: NSApplication, openFile filename: String) -> Bool

滚到指定的行需要多一些的代码:

private func scrollTextView(textView: NSTextView, toLine line: Int) {     guard let text = (textView.string as NSString?) else {         return     }          var currentLine = 1     var index = 0     for (; index < text.length; currentLine++) {         let lineRange = text.lineRangeForRange(NSMakeRange(index, 0))         index = NSMaxRange(lineRange)                  if currentLine == line {             textView.scrollRangeToVisible(lineRange)             textView.setSelectedRange(lineRange)             break         }     } }

现在处理NSString比String简单很多,否则我还得介绍和Range的转换。

归因

写这个插件比较简单,因为我能看别人写的插件,主要和控制台有关,如果他们不是开源的,写这个插件会比较麻烦。

安装

用Alcatraz工具然后查找 KZLinkedConsole, 或者你可以只 编译工程 ,它就可以自动安装了。

总结

这是我第一次尝试写Xcode插件,必须说在Xcode工作时调试Xcode是很有趣的一件事。

我个人认为这个插件非常有用,因为我们经常有很多日志,能直接跳转到记录错误的那行是非常节省时间的。

一定要下载GitHub上的源代码,用Swift语言处理私有API是很有趣的。KVC(键值编码机制)可使它更简单地检索值,而不用引入Objective-C绑定。

如果你正在用cmd+shift+f,那你可能做错了什么。

本文仅用于学习和交流目的,转载请注明文章译者、出处以及本文链接。

感谢 博文视点 对本期翻译活动的支持。

教你如何用Swift编写Xcode插件

正文到此结束
Loading...