使用 TextKit 实现语法高亮文本编辑器

jopen 9年前

语法高亮对于我们开发者来说是几乎离不开的功能,它能够帮助我们非常清晰的展现出文本,提高可读性。TextKit 的出现让语法高亮这类的功能实现起来变得特别简单,我们就来看看如何使用 TextKit 来实现我们自己的语法高亮 App 吧。

语法高亮简介

语法高亮功能用起来简单,但实现原理并不容易。会涉及到很多方面的知识 - 比如确定底层文本的存储结构,如何将文本的结构与 UI 进行关联。 往往实现一个完整的语法高亮功能,即使是有经验的开发者,也要花去很长时间。

不过好消息是 TextKit 的出现,让这一切变得简单起来。 它提供了一个叫做 NSTextStorage 的类,可以实现高亮功能,这是它的结构:

这样看大家可能会觉得比较抽象,那么让我带大家把它具体化。我们先撇开这个结构,来看看 UITextView 的一个构造方法:

init(frame frame: CGRect,textContainer textContainer: NSTextContainer?)
</div>

UITextView 除了我们常见的构造方法外,还提供了一个这样的构造方法,它接受一个 NSTextContainer 类型的参数。 再看看我们上面那张图中的 NSTextContainer, 稍微建立起一点关系了。

简单来说是这样,如果以 MVC 的思路来思考的话, NSTextStorage 相当于模型层,用于管理文本的底层存储,以及如何定义文本显示的样式。 高亮显示功能的主要代码都是在这个类中完成的。

NSLayoutManager 相当与控制器层,它负责把 NSTextStorage 的文本内容绘制到相应的视图上,并且它还负责文字的排版处理等。

NSTextContainer 定义了文本在 UITextView 上面的显示区域。

因为这些分层的概念,我们甚至可以把同一个 NSTextStorage 同时显示到两个不同的视图上面,这个内容我们这里不多做探讨。

NSTextStorage

现在咱们开始进入正题,首先需要实现一个继承自 NSTextStorage 的类:

class SwiftTextStorage: NSTextStorage {        var _string = NSMutableAttributedString()      }
</div>

定义了一个 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)        }        }
</div>

对这两个 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)    }
</div>

这两个 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 {                }        }
</div>

我们这里声明了一个正则表达式,匹配字符串 Swift,然后使用正则表达式进行匹配,将匹配到的字符串改变为红色:

self.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: result!.range)
</div>

这个用到了 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!)                }        }
</div>

我们这里建立了 UITextView 的层级结构,首先使用 textStorage.addLayoutManager(layoutManager) 将 textStoragelayoutManager 关联起来。 然后用 layoutManager.addTextContainer(textContainer) 将 layoutManager 和 textContainer 关联起来。

最后使用 UITextView 的构造方法将 textContainer 实例关联起来。这样我们整体的 TextKit 结构就创建完成了。

现在我们可以运行程序,看到效果了:

总结

TextKit为我们提供了很不错的文本处理接口,这些特性在 iOS 7 之后才提供出来。 主要是因为性能的原因, 文本绘制在我们看来是最基本的功能,但它的底层实现并不那么简单。 随着 iPhone 硬件的升级,以及 iOS 系统的优化, TextKit 也随之更加强大。

更详细关于 TextKit 的内容,大家可以参看苹果官方的文档,进行更深入的研究。

本篇文章的示例代码大家可以在 Github 主页上面下载: https://github.com/swiftcafex/syntaxHighlightingSamples

</div>

来自: http://www.swiftcafe.io/2016/01/14/syntax-highlighting/