使用 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