教你如何用Swift编写Xcode插件
(文章转载自:http://ios.jobbole.com/83653/)
教你如何用Swift编写Xcode插件

在我的AppCode项目创建过程中,我想念最多的一件事是:能跳转到记录控制台信息的指定文件和行。
Xcode不提供这样的功能,而我不是一个喜欢抱怨的人,所以我决定自己写个插件。我用Swift来编写这个插件。
想法
如果一个控制台记录了fileName.extension:XX 这样一个名字,转换成可点击的超链接,这个链接将会打开指定的文件并将那行代码高亮。
那样你可以使用自己的记录机制,只要添加这个简单的前缀,比如:
【代码】
|
1
2
3
|
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的一部分)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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打开文件并高亮那一行。
怎样把我们的功能注入到那些操作里去?
简单调整:
|
1
2
3
4
5
6
7
|
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 并使用相关对象把存储标记为控制台的那个,这个随后就会排上用场。
|
1
2
3
4
|
guard let consoleTextView = KZPluginHelper.consoleTextView(),
let textStorage = consoleTextView.valueForKey("textStorage") as? NSTextStorage else {
return
}
|
我们怎样找到一个文件的路径,而只有日志中的相对路径?
我们可以用shell里的find命令,这就是你如何用swift语言运行且从一个shell命令中检索响应。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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命令来检索工程的完整路径。
- 添加自定义属性来存储字符串本身的信息。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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)
}
}
|
打开文件,然后滚到指定的行
打开一个文件像调用一样简单:
|
1
|
public func application(sender: NSApplication, openFile filename: String) -> Bool
|
滚到指定的行需要多一些的代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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,那你可能做错了什么。
浙公网安备 33010602011771号