使用 TextKit 实现语法高亮文本编辑器
语法高亮对于我们开发者来说是几乎离不开的功能,它能够帮助我们非常清晰的展现出文本,提高可读性。TextKit 的出现让语法高亮这类的功能实现起来变得特别简单,我们就来看看如何使用 TextKit 来实现我们自己的语法高亮 App 吧。
语法高亮简介
语法高亮功能用起来简单,但实现原理并不容易。会涉及到很多方面的知识 - 比如确定底层文本的存储结构,如何将文本的结构与 UI 进行关联。 往往实现一个完整的语法高亮功能,即使是有经验的开发者,也要花去很长时间。
不过好消息是 TextKit 的出现,让这一切变得简单起来。 它提供了一个叫做 NSTextStorage 的类,可以实现高亮功能,这是它的结构:
这样看大家可能会觉得比较抽象,那么让我带大家把它具体化。我们先撇开这个结构,来看看 UITextView 的一个构造方法:
init(frame frame: CGRect,textContainer textContainer: NSTextContainer?)
UITextView 除了我们常见的构造方法外,还提供了一个这样的构造方法,它接受一个 NSTextContainer 类型的参数。 再看看我们上面那张图中的 NSTextContainer, 稍微建立起一点关系了。
简单来说是这样,如果以 MVC 的思路来思考的话, NSTextStorage 相当于模型层,用于管理文本的底层存储,以及如何定义文本显示的样式。 高亮显示功能的主要代码都是在这个类中完成的。
NSLayoutManager 相当与控制器层,它负责把 NSTextStorage 的文本内容绘制到相应的视图上,并且它还负责文字的排版处理等。
NSTextContainer 定义了文本在 UITextView 上面的显示区域。
因为这些分层的概念,我们甚至可以把同一个 NSTextStorage 同时显示到两个不同的视图上面,这个内容我们这里不多做探讨。
NSTextStorage
现在咱们开始进入正题,首先需要实现一个继承自 NSTextStorage 的类:
class SwiftTextStorage: NSTextStorage { var _string = NSMutableAttributedString() }
定义了一个 NSMutableAttributedString 类型的属性,它用于我们的底层存储。然后需要实现几个约定的 getter 和 setter 方法:
class SwiftTextStorage: NSTextStorage { override var string:String { get { return _string.string } } override func attributesAtIndex(location: Int, effectiveRange range: NSRangePointer) -> [String : AnyObject] { return _string.attributesAtIndex(location, effectiveRange: range) } }
对这两个 getter 的实现也很简单,只是对 _string 对象相应方法的一个包装,然后我们在实现以下 setter 方法:
override func replaceCharactersInRange(range: NSRange, withString str: String) { _string.replaceCharactersInRange(range, withString: str) self.edited(NSTextStorageEditActions.EditedCharacters, range: range, changeInLength: str.characters.count - range.length) } override func setAttributes(attrs: [String : AnyObject]?, range: NSRange) { _string.setAttributes(attrs, range: range) self.edited(NSTextStorageEditActions.EditedAttributes, range: range, changeInLength: 0) }
这两个 setter 方法也是对 _string 的一个包装。
接下来,我们实现最关键的方法 processEditing , 这个方法会在 UITextView 的文本被更改的时候被调用,我们可以在这里匹配出要进行高亮的关键字:
override func processEditing() { super.processEditing() do { let regex = try NSRegularExpression(pattern: "Swift", options: []) let paragraphRange = (self.string as NSString).paragraphRangeForRange(self.editedRange) regex.enumerateMatchesInString(self.string, options: [], range: paragraphRange) { result, flags, stop in self.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: result!.range) } } catch { } }
我们这里声明了一个正则表达式,匹配字符串 Swift,然后使用正则表达式进行匹配,将匹配到的字符串改变为红色:
self.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: result!.range)
这个用到了 NSAttributedString 的特性。让我们很容易的位 UITextView 中的文本设置属性。
设置 UITextView
我们刚才完成了 NSTextStorage 子类的实现,现在我们就可以用它来构建 UITextView 了:
class ViewController: UIViewController { var textView:UITextView? var textStorage = SwiftTextStorage() override func viewDidLoad() { super.viewDidLoad() let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer() layoutManager.addTextContainer(textContainer) self.textView = UITextView(frame: CGRectMake(0, 20, self.view.frame.size.width, self.view.frame.size.height), textContainer: textContainer) self.textView?.text = "最近各大网站上最喜大普奔的新闻莫过于 Swift 正式开源这条了。这无疑是一个里程碑式的前进,苹果也向开源社区更加进了一步。那么 Swift 开源后有什么具体的改变呢,我们一一道来。" self.view.addSubview(self.textView!) } }
我们这里建立了 UITextView 的层级结构,首先使用 textStorage.addLayoutManager(layoutManager) 将 textStorage 和 layoutManager 关联起来。 然后用 layoutManager.addTextContainer(textContainer) 将 layoutManager 和 textContainer 关联起来。
最后使用 UITextView 的构造方法将 textContainer 实例关联起来。这样我们整体的 TextKit 结构就创建完成了。
现在我们可以运行程序,看到效果了:
总结
TextKit为我们提供了很不错的文本处理接口,这些特性在 iOS 7 之后才提供出来。 主要是因为性能的原因, 文本绘制在我们看来是最基本的功能,但它的底层实现并不那么简单。 随着 iPhone 硬件的升级,以及 iOS 系统的优化, TextKit 也随之更加强大。
更详细关于 TextKit 的内容,大家可以参看苹果官方的文档,进行更深入的研究。
本篇文章的示例代码大家可以在 Github 主页上面下载: https://github.com/swiftcafex/syntaxHighlightingSamples
</div>