iOS上文本处理之简史
iOS 文字簡史
- iPhone OS 2
- UILabel
- UITextField
- UITextView
- iPhone OS 3
- New Feature: 復制 && 粘貼
- iOS 3.2
- CoreText
- iOS 4
- None
- iOS 5
- None
- iOS 6
- UILabel 支持 NSAttributedString
- UITextView 支持 NSAttributedString
- iOS 7
- TextKit
TextKit 出現前
Q: 想繪制一段話,這段話需要有不同的字體,可以調節行高,可以調節字間距,可以做各種富文本編輯工作,可以高亮某些特殊字
A1: 使用很多的 Label 來完成復雜的排布,顯然這是最 Ugly 的方法。
A2: 使用 NSAttributedString 來自定義文字樣式,然后塞給 UILabel 和 UITextView 完成繪制工作,問題是渲染邏輯和排布邏輯部分被完全封裝在 UILabel 和 UITextView 中,我們無法干涉。
A3: 使用 CoreText 來自己繪制文字,代碼冗余,都是 C 代碼,需要自己做上層面向對象封裝才便于重用。
PS: Cocoa 和 CocoaTouch 框架中,以 Core 開頭的 framework 基本都是 C 語言的底層封裝,如:CoreFoundation, CoreText,也有例外,如:CoreAnimation
TextKit 出現后
完成上面的需求變得更為簡潔,這個我們需要先看下 TextKit 的結構。
NSTextStorage[New]----->NSString|NSLayoutManager[New]--->CoreText|NSTextContainer[New]|UITextView------------>UITextInput
UIKit 文本系統新增的三個類 NSTextStorage, NSLayoutManager, NSTextContainer 分別對應了 MVC[1] 中的不同角色。
Demo
TextKitDemo GitHub 地址
Demo 實現了
- 特殊字符高亮顯示
- 首行縮進
- 行間距設置
- 段間距設置
=============
認識 TextKit
http://blog.jobbole.com/51965/
iOS7 的發布給開發者的案頭帶來了很多新工具。其中一個就是 TextKit(文本工具箱)。TextKit 由許多新的 UIKit 類組成,顧名思義,這些類就是用來處理文本的。在這里,我們將介紹 TextKit 的來由、它的組成,以及通過幾個例子解釋開發者怎樣將它派上大用場。
但是首先我們得有一點背景知識:TextKit 可能是近期對 UIKit 最重要的補充了。iOS7 的新界面用純文本按鈕替換了大量的圖標和邊框。總的來說,文本和文本布局在新的操作系統的外觀方面比以前重要多了。iOS7 的重新設計完全是被文本驅動,這樣說也許并不夸張——而文本全部是TextKit來處理的。
告訴你這個變動到底有多大吧:iOS7 之前的所有版本,(幾乎)所有的文本都是 WebKit 來處理的。對:WebKit,web 瀏覽器引擎。所有UILabel、UITextField,以及 UITextView 都在后臺以某種方式使用 web 視圖來進行文本布局和渲染。為了新的界面風格,它們全都被重新設計以使用TextKit。
iOS上文本的簡短歷史
這些新類并不是用來替換開發者以前使用的類。對 SDK 來說,TextKit 提供的是全新的功能。iOS7 之前,TextKit 提供的功能必須都手動完成。這是現有功能之間缺失的環節。
長期以來,只有一個基本的文本布局和渲染框架:CoreText。也有一個途徑讀取用戶的鍵盤輸入:UITextInput 協議。iOS6 甚至有一個途徑來簡單地獲取系統的文本選擇:繼承 UITextView。
(這可能是重點,我應該公開我開發文本編輯器的十年經驗了)在渲染文本和讀取鍵盤輸入之間存在著巨大(跟我讀:巨大)的缺口。這個缺口可能也是導致很少有富文本或者語法高亮編輯器的原因了——毫無疑問,開發一個好用的文本編輯器得耗費幾個月的時間。
就這樣——如下是 iOS 文本(不那么)簡短歷史的簡短概要:
iOS 2:這是第一個公開的 SDK,包括一個簡單的文本顯示組件( UILabel ),一個簡單的文本輸入組件( UITextField ),以及一個簡單的、可滾動、可編輯的并且支持更大量文本的組件:UITextView。這些組件都只支持純文本,沒有文本選擇支持(僅支持插入點),除了設置字體和文本顏色外幾乎沒有其他可定制功能。
iOS 3:新特性有復制和粘貼,以及復制粘貼所需要的文本選擇功能。數據探測器(Data Detector)為文本視圖提供了一個高亮電話號碼和鏈接的方法。然而,除了打開或關閉這些特性外,開發者基本上沒有什么別的事情可以做。
iOS 3.2:iPad 的出現帶來了 CoreText,也就是前面提到的低級文本布局和渲染引擎(從Mac OS X 10.5 移植過來的),以及 UITextInput,前面也提到的鍵盤存取協議。Apple 將 Pages 作為移動設備上文本編輯功能的樣板工程(附注1)。然而,由于我前面提到的框架缺口,只有很少的應用使用它們。
iOS 4:iOS 3.2 發布僅僅幾個月后就發布了,文本方面沒有一丁點新功能。(個人經歷:在 WWDC,我走近工程師們,告訴他們我想要一個完善的 iOS 文本布局系統。回答是:“哦…提交個請求。”不出所料…)
iOS 5:文本方面沒啥變化。(個人經歷:在 WWDC,我和工程師們談及 iOS 上文本系統。回答是:“我們沒有看到太多的請求…” 靠!)
iOS 6:有些動作了:屬性文本編輯被加入了UITextView。很不幸的是,它很難定制。默認的UI有粗體、斜體和下劃線。用戶可以設置字體大小和顏色。粗看起來相當不錯,但還是沒法控制布局或者提供一個便利的途徑來定制文本屬性。然而對于(文本編輯)開發者,有一個大的新功能:可以繼承 UITextView 了,這樣的話,除了以前版本提供的鍵盤輸入外,開發者可以“免費”獲得文本選擇功能。必須實現一個完全自定義的文本選擇功能,可能是很多對非純文本工具開發的嘗試半途而廢的原因。(個人經歷:我,WWDC,工程師們。我想要一個 iOS 的文本系統。回答:“嗯。吖。是的。也許?看,它只是不執行…” 所以畢竟還是有希望,對吧?)
iOS 7:終于來了,TextKit。
功能
所以咱們到了。iOS7 帶著 TextKit 登陸了。咱們看看它可以做什么!深入之前,我還想提一下,嚴格來說,這些事情中的大部分以前都可以做。如果你有大量的資源和時間來用CoreText構建一個文本引擎,這些都是可以做的。但是如果以前你想構建一個完善的富文本編輯器,你得花費幾個月的時間。現在就非常簡單,你只需要到在Xcode里打開一個界面文件,然后將UITextView拖到你的試圖控制器,就可以獲得所有的功能:
字距調整(Kerning):所有的字符都有簡單的二次的形狀,這些形狀必須被精確地放置,彼此相鄰的,別這樣想了。例如,現代文本布局會考慮到一個大寫的“T”的“兩翼”下面有一些空白,所以它會把后面的小寫字母向左移讓它們更靠近點。從而大大提高了文本的易讀性,特別是在更長的文字中:
連寫:我認為這主要是個藝術功能,但當某些字符組合(如“f”后面是“l”)使用組合符號(所謂的字形(glyph))繪制時,有些文本確實看起來更好(更美觀)。
圖像附件:現在可以在文本視圖里面添加圖像了。
斷字:編輯文本時沒那么重要,但如果要以好看易讀的方式展現文本時,這就相當重要。斷字意味著在行邊界處分割單詞,從而為整體文本創建一個更整齊的排版和外觀。個人經歷:iOS7 之前,開發者必須直接使用 CoreText。像這樣:首先以句子為基礎檢測文本語言,然后獲取句子中每個單詞可能的斷字點,然后在每一個可能的斷字點上插入定制的連字占位字符。準備好之后,運行 CoreText 的布局方法并手動將連字符插入到斷行。如果你想得到好的效果,之后你得檢查帶有連字符的文本沒有超出行邊界,如果超出了,在運行一次行的布局方法,這一次不要使用上次使用的斷字點。使用 TextKit 的話,就非常簡單了,設置 hyphenationFactor 屬性就可以啟用斷字。
可定制性:對我來說,甚至比改進過的排版還多,這是個新的功能。以前開發者必須在使用現有的功能和自己全部重頭寫之間做出選擇。現在提供了一整套類,它們有代理協議,或者可以被覆蓋從而改變部分行為。例如,不必重寫整個文本組件,你現在就可以改變指定單詞的斷行行為。我認為這是個勝利。
更多的富文本屬性:現在可以設置不同的下劃線樣式(雙線、粗線、虛線、點線,或者它們的組合)。提高文本的基線非常容易,這可用來設置上標數字。開發者也不再需要自己為定制渲染的文本繪制背景顏色了(CoreText 不支持這些功能)。
序列化:過去沒有內置的方法從磁盤讀取帶文本屬性的字符串。或者再寫回磁盤。現在有了。
文本樣式:iOS7 的界面引入了一個全局預定義的文本類型的新概念。這些文本類型分配了一個全局預定義的外觀。理想情況下,這可以讓整個系統的標題和連續文本具有一致的風格。通過設置應用,用戶可以定義他們的閱讀習慣(例如文本大小),那些使用文本樣式的應用將自動擁有正確的文本大小和外觀。
文本效果:最后也是最不重要的。iOS7 有且僅有一個文本效果:凸版。使用此效果的文本看起來像是蓋在紙上面一樣。內陰影,等等。個人觀點:真的?靠…?在一個已經完全徹底不可饒恕地槍斃了所有無用的懷舊裝飾的操作系統上,誰會需要這個像文本蓋在紙上的外觀?
結構
可能概覽一個系統最好的方法是畫一幅圖。這是UIKit文本系統——TextKit的簡圖,:
從上圖可以看出來,要讓一個文本引擎工作,需要幾個參與者。我們將從外到里介紹它們:
字符串(String):要繪制文本,那么必然在某個地方有個字符串存儲它。在默認的結構中,NSTextStorage 保存并管理這個字符串,在這種情況中,它可以遠離繪制。但并不一定非得這樣。使用 TextKit 時,文本可以來自任何適合的來源。例如,對于一個代碼編輯器,字符串可以是一棵包含所有顯示的代碼的結構信息的注釋語法樹(annotated syntax tree, AST)。使用一個定制的文本存儲,這個文本只在后面動態地添加字體或顏色高亮等文本屬性裝飾。這是第一次,開發者可以直接為文本組件使用自己的模型。只需要一個特別設計的文本存儲。即:
NSTextStorage:如果你把文本系統看做一個模型-視圖-控制器(MVC)架構,這個類代表的是模型。文本存儲是中心對象,它知道所有的文本和屬性信息。它只提供了兩個存取器方法存取它們,并提供了另外兩個方法來修改它們。后面我們將進一步了解它們。現在重要的是你得理解 NSTextStorage 是從它的父類 NSAttributedString 繼承了這些方法。這就很清楚了,文本存儲——從文本系統看來——僅僅是一個帶有屬性的字符串,以及幾個擴展。這兩者唯一的重大不同點是文本存儲包含了一個方法來發送內容改變的通知。我們會馬上介紹這部分內容。
UITextView:堆棧的另一頭是實際的視圖。在 TextKit 中,文本視圖有兩個目的:第一,它是文本系統用來繪制的視圖。文本視圖它自己并不會做任何繪制;它僅僅提供一個供其它類繪制的區域。作為視圖層級機構中唯一的組件,第二個目的是處理所有的用戶交互。具體來說,文本視圖實現 UITextInput 的協議來處理鍵盤事件,它為用戶提供了一種途徑來設置一個插入點或選擇文本。它并不對文本做任何實際上的改變,僅僅將這些改變請求轉發給剛剛討論的文本存儲。
NSTextContainer:每個文本視圖定義了一個文本可以繪制的區域。為此,每個文本視圖都有一個文本容器,它精確地描述了這個可用的區域。在簡單的情況下,這是一個垂直的無限相當大的矩形區域。文本被填充到這個區域,并且文本視圖允許用戶滾動它。然而,在更高級的情況下,這個區域可能是一個無限大的矩形。例如,當渲染一本書時,每一頁都有最大的高度和寬度。文本容器會定義這個大小,并且不接受任何超出的文本。相同情況下,一幅圖像可能占據了頁面的一部分,文本應該沿著它的邊緣重新排版。這也是由文本容器來處理的,我們會在后面的例子中看到這一點。
NSLayoutManager:布局管理器是中心組件,它把所有組件粘合在一起:
- 1、這個管理器監聽文本存儲中文本或屬性改變的通知,一旦接收到通知就觸發布局進程。
- 2、從文本存儲提供的文本開始,它將所有的字符翻譯為字形(Glyph)(附注2).
- 3、一旦字形全部生成,這個管理器向它的文本容器(們)查詢文本可用以繪制的區域
- 4、然后這些區域被行逐步填充,而行又被字形逐步填充。一旦一行填充完畢,下一行開始填充。
- 5、對于每一行,布局管理器必須考慮斷行行為(放不下的單詞必須移到下一行)、連字符、內聯的圖像附件等等。
- 6、當布局完成,文本的當前顯示狀態被設為無效,然后文本管理器將前面幾步排版好的文本設給文本視圖。
CoreText:沒有直接包含在 TextKit 中,CoreText 是進行實際排版的庫。對于布局管理器的每一步,CoreText 被這樣或那樣的方式調用。它提供了從字符到字形的翻譯,用它們來填充行,以及建議斷字點。
Cocoa 文本系統
創建像 TextKit 這樣龐大復雜的系統肯定不是件簡單快速的事情,而且肯定需要豐富的經驗和知識。在 iOS 的前面6個主版本中,一直沒有提供一個“真正的”文本組件,這也說明了這一點。Apple 把它視為一個大的新特性,當然沒啥問題。但是它真的是全新的嗎?
這里有個數字:在 UIKit 的 131 個公共類中,只有 9 個的名字沒有使用UI作為前綴。這 9 個類使用的是舊系統的的、舊世界的(跟我讀:Mac OS)前綴 NS。而且這九個類里面,有七個是用來處理文本的。巧合?好吧…
這是 Cocoa 文本系統的簡圖。不妨和上面 TextKit 的那幅圖作一下對比。
驚人地相似。很明顯,最起碼主要部分,兩者是相同的。很明顯——除了右邊部分以及 NSTextView 和 UITextView ——主要的類全部相同。TextKit 是(起碼部分是)從 Cocoa 文本系統移植到 iOS。(我之前一直請求的那個,耶!)
進一步比較還是能看出一些不同的。最值得注意的有:
在 iOS 上沒有 NSTypesetter 和 NSGlyphGenerator 這兩個類。在 Mac OS 上有很多方法來定制排版,這被極大地簡化了。這可以去掉一些抽象概念,并將這個過程合并到 NSLayoutManager 中來。保留下來的是少數的代理方法,以用來更改文本布局和斷行行為。
這些類的 iOS 實現提供了幾個新的而且非常便利的功能。在 Cocoa 中,必須手工地將確定的區域從文本容器分離出來(見上)。而 UIKit 類提供了一個簡單的 exclusionPaths 屬性就可以做到這一點。
有些功能未能提供,比如,內嵌表格,以及對非圖像的附件的支持。
盡管有這些區別,總的來說系統還是一樣的。NSTextStorage 在兩個系統是是一模一樣的,NSLayoutManager 和 NSTextContainer 也沒有太大的不同。這些變動,在沒有太多去除對一些特例的支持的情況下,看來(某些情況下大大地)使文本系統的使用變得更為容易。我認為這是件好事。
事后回顧我從 Apple 工程師那里得到的關于將 Cocoa 文本系統移植到 iOS 的答案,我們可以得到一些背景信息。拖到現在并削減功能的原因很簡單:性能、性能、性能。文本布局可能是極度昂貴的任務——內存方面、電量方面以及時間方面——特別是在移動設備上。Apple 必須采用更簡單的解決方案,并等到處理能力能夠至少部分支持一個完善的文本布局引擎。
示例
為了說明 TextKit 的能力,我創建了一個小的演示項目,你可以在 GitHub 上找到它。在這個演示程序中,我只完成了一些以前不容易完成的功能。我必須承認編碼工作只花了我禮拜天的一個上午的時間;如果以前要做同樣的事情,我得花幾天甚至幾個星期。
TextKit 包括了超過 100 個方法,一篇文章根本沒辦法盡數涉及。而事實上,大多數時候,你需要的僅僅是一個正確的方法,TextKit 的使用和定制性也仍有待探索。所以我決定做四個更小的演示程序,而非一個大的演示程序來展示所有功能。每個演示程序中,我試著演示針對不同的方面和不同的類進行定制。
演示程序1:配置
讓我們從最簡單的開始:配置文本系統。正如你在上面 TextKit 簡圖中看到的,NSTextStorage、NSLayoutManager 和 NSTextContainer 之間的箭頭都是有兩個頭的。我試圖描述它們的關系是 1 對 N 的關系。就是那樣:一個文本存儲可以擁有多個布局管理器,一個布局管理器也可以擁有多個文本容器。這些多重性帶來了很好的特性:
- 將多個文本管理器附加到一個文本存儲上,可以產生相同文本的多種視覺表現,而且它們可以并排顯示。每一個表現可以獨立地布置和修改大小。如果相應的文本視圖可編輯,那么在某個視圖上做的所有修改都會馬上反映到所有視圖上。
- 將多個文本容器附加到一個文本管理器上,可以將一個文本分布到多個視圖展現出來。例如很有用的基于頁面的布局:每個頁面包含一個單獨的視圖。一個文本管理器利用這些視圖的文本容器,將文本分布到這些視圖上。
在 storyboard 或者 interface 文件中實例化 UITextView 時,它會預配置一個文本系統:一個文本存儲,引用一個文本管理器,而后者又引用一個文本容器。同樣地,一個文本系統棧也可以通過代碼直接創建:
| 1 2 3 4 5 6 7 8 9 10 | NSTextStorage *textStorage = [NSTextStorage new]; NSLayoutManager *layoutManager = [NSLayoutManager new]; [textStorage addLayoutManager: layoutManager]; NSTextContainer *textContainer = [NSTextContainer new]; [layoutManager addTextContainer: textContainer]; UITextView *textView = [[UITextView alloc] initWithFrame:someFrame ???????????????????????????????????????????textContainer:textContainer]; |
這是最簡單的方式。手工創建一個文本系統,唯一需要記住的事情是你的視圖控制器必須 retain 文本存儲。在棧底的文本視圖只保留了對文本存儲和布局管理器的弱引用。當文本存儲被釋放時,布局管理器也被釋放了,這樣留給文本視圖的就只有一個斷開的容器了。
這個規則有一個例外。只有從一個 interface 文件或 storyboard 實例化一個文本視圖時,文本視圖確實會 retain 文本存儲。框架使用了一些黑魔法以確保所有的對象都被 retain,而無需建立一個 retain 環。
記住這些之后,創建一個更高級的設置也非常簡單。假設在一個視圖里面依舊有一個從 nib 實例化的文本視圖,叫做 originalTextView。增加對相同文本的第二個文本視圖只需要復制上面的代碼,并重用 originalTextView 的文本存儲:
| 1 2 3 4 5 6 7 8 9 10 | NSTextStorage *sharedTextStorage = originalTextView.textStorage; NSLayoutManager *otherLayoutManager = [NSLayoutManager new]; [sharedTextStorage addLayoutManager: otherLayoutManager]; NSTextContainer *otherTextContainer = [NSTextContainer new]; [otherLayoutManager addTextContainer: otherTextContainer]; UITextView *otherTextView = [[UITextView alloc] initWithFrame:someFrame ????????????????????????????????????????????????textContainer:otherTextContainer]; |
將第二個文本容器附加到布局管理器也差不多。比方說我們希望上面例子中的文本填充兩個文本視圖,而非一個。簡單:
| 1 2 3 4 5 | NSTextContainer *thirdTextContainer = [NSTextContainer new]; [otherLayoutManager addTextContainer: thirdTextContainer]; UITextView *thirdTextView = [[UITextView alloc] initWithFrame:someFrame ????????????????????????????????????????????????textContainer:thirdTextContainer]; |
但有一點需要注意:由于在 otherTextView 中的文本容器可以無限地調整大小,thirdTextView 永遠不會得到任何文本。因此,我們必須指定文本應該從一個視圖回流到其它視圖,而不應該調整大小或者滾動:
| 1 | otherTextView.scrollEnabled = NO; |
不幸的是,看來將多個文本容器附加到一個文本管理器會禁用編輯功能。如果必須保留編輯功能的話,你只可以將一個文本容器附加到一個文本管理器上。
想要一個這個配置的可運行的例子的話,請在前面提到的 TextKitDemo 中查看“Configuration”標簽頁。
演示程序2:語法高亮
如果配置文本視圖不是那么令人激動,那么這里有更有趣的:語法高亮!
看看 TextKit 組件的責任劃分,就很清楚語法高亮應該在文本存儲上實現。因為 NSTextStorage 是一個類簇(附注3),創建它的子類需要做不少工作。我的想法是建立一個復合對象:實現所有的方法,但只是將對它們的調用轉發給一個實際的實例,將輸入輸出參數或者結果修改為希望的樣子。
NSTextStorage 繼承自 NSMutableAttributedString,并且必須實現以下四個方法——兩個 getter 和兩個 setter:
| 1 2 3 4 5 | - (NSString *)string; - (NSDictionary *)attributesAtIndex:(NSUInteger)location ?????????????????????effectiveRange:(NSRangePointer)range; - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str; - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range; |
一個類簇的子類的復合對象的實現也相當簡單。首先,找到一個滿足所有要求的最簡單的類。在我們的例子中,它是 NSMutableAttributedString,我們用它作為實現自定義存儲的實現:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | @implementation TKDHighlightingTextStorage { ????NSMutableAttributedString *_imp; } - (id)init { ????self = [super init]; ????if (self) { ????????_imp = [NSMutableAttributedString new]; ????} ????return self; } |
有了這個對象,只需要一行代碼就可以實現兩個 getter 方法:
| 1 2 3 4 5 6 7 8 9 | - (NSString *)string { ????return _imp.string; } - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range { ????return [_imp attributesAtIndex:location effectiveRange:range]; } |
實現兩個 setter 方法也幾乎同樣簡單。但也有一個小麻煩:文本存儲需要通知它的文本管理器變化發生了。因此 settter 方法必須也要調用 -edited:range:changeInLegth: 并傳給它變化的描述。聽起來更糟糕,實現變成:
| 1 2 3 4 5 6 7 8 9 10 11 12 | - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str { ????[_imp replaceCharactersInRange:range withString:str]; ????[self edited:NSTextStorageEditedCharacters range:range ??????????????????????????????????????changeInLength:(NSInteger)str.length - (NSInteger)range.length]; } - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range { ????[_imp setAttributes:attrs range:range]; ????[self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; } |
就這樣,我們在文本系統棧里面有了一個文本存儲的全功能替換版本。在從 interface 文件中載入時,可以像這樣將它插入文本視圖——但是記住從一個實例變量引用文本存儲:
| 1 2 | _textStorage = [TKDHighlightingTextStorage new]; [_textStorage addLayoutManager: self.textView.layoutManager]; |
到目前為止,一切都很好。我們設法插入了一個自定義的文本存儲,接下來我們需要真正高亮文本的某些部分了。現在,一個簡單的高亮應該就是夠了:我們希望將所有 iWords 的顏色變成紅色——也就是那些以小寫“i”開頭,后面跟著一個大寫字母的單詞。
一個方便實現高亮的辦法是覆蓋 -processEditing。每次文本存儲有修改時,這個方法都自動被調用。每次編輯后,NSTextStorage 會用這個方法來清理字符串。例如,有些字符無法用選定的字體顯示時,文本存儲使用一個可以顯示它們的字體來進行替換。
和其它一樣,為 iWords 增加一個簡單的高亮也相當簡單。我們覆蓋 -processEditing,調用父類的實現,并設置一個正則表達式來查找單詞:
| 1 2 3 4 5 6 7 8 9 | - (void)processEditing { ????[super processEditing]; ????static NSRegularExpression *iExpression; ????NSString *pattern = @"i[\\p{Alphabetic}&&\\p{Uppercase}][\\p{Alphabetic}]+"; ????iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:pattern ???????????????????????????????????????????????????????????????????????????options:0 ?????????????????????????????????????????????????????????????????????????????error:NULL]; |
然后,首先清除之前的所有高亮:
| 1 2 | NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange]; [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange]; |
其次遍歷所有的樣式匹配項并高亮它們:
| 1 2 3 4 5 6 7 | ????[iExpression enumerateMatchesInString:self.string ??????????????????????????????????options:0 range:paragaphRange ???????????????????????????????usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) ????{ ????????[self addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:result.range]; ????}]; } |
就是這樣。我們創建了一個支持語法高亮的動態文本視圖。當用戶鍵入時,高亮將被實時應用。而且這只需幾行代碼。酷吧?
請注意僅僅使用 edited range 是不夠的。例如,當手動鍵入 iWords,只有一個單詞的第三個字符被鍵入后,正則表達式才開始匹配。但那時 editedRange 僅包含第三個字符,因此所有的處理只會檢查這個字符。通過重新處理整個段落,我們可以完成高亮功能,又不會太過影響性能。
想要一個這個配置的可運行的例子的話,請在前面提到的 TextKitDemo 中查看“Highlighting”標簽頁。
演示程序3:布局修改
如前所述,布局管理器是核心的布局主力。Mac OS 上 NSTypesetter 的高度可定制功能被并入 iOS 上的 NSLayoutManager。雖然 TextKit 不具備像 Cocoa 文本系統那樣的完全可定制性,但它提供很多代理方法來允許做一些調整。如前所述,TextKit 與 CoreText 更緊密地集成在一起,主要是基于性能方面的考慮。但是兩個文本系統的理念在一定程度上是不一樣的:
Cocoa 文本系統:在 Mac OS上,性能不是問題,設計考量的全部是靈活性。可能是這樣:“這個東西可以做這個事情。如果你想的話,你可以覆蓋它。性能不是問題。你也可以提供完全由自己實現的字符到字形的轉換,去做吧…”
TextKit:性能看來真是個問題。理念(起碼現在)更多的是像這樣:“我們用簡單但是高性能的方法實現了這個功能。這是結果,但是我們給你一個機會去更改它的一些東西。但是你只能在不太損害性能的地方進行修改。”
足夠的理念,讓我們來定制些東西。例如,調整行高如何?聽起來不可思議,但是在之前的 iOS 發布版上調整行高至少是很黑客的行為,或者需要使用私有 API。幸運的是,現在(再一次)不用那么搞腦子了。設置布局管理器的代理并實現僅僅一個方法即可:
| 1 2 3 4 5 6 | - (CGFloat)????? layoutManager:(NSLayoutManager *)layoutManager ??lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex ??withProposedLineFragmentRect:(CGRect)rect { ????return floorf(glyphIndex / 100); } |
在以上的代碼中,我修改了行間距,讓它與文本長度同時增長。這導致頂部的行比底部的行排列得更緊密。我承認這沒什么實際的用處,但是它是可以做到的(而且肯定會有更實用的用例的)。
好,來一個更現實的場景。假設你的文本中有鏈接,你不希望這些鏈接被行包圍。如果可能的話,一個 URL 應該始終顯示為一個整體,一個單一的文本片段。沒有什么比這更簡單的了。
首先,我們通過使用自定義的文本存儲,就像前面討論過的那個。但是,它尋找鏈接并將其標記,而不是檢測 iWords,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | static NSDataDetector *linkDetector; linkDetector = linkDetector ?: [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:NULL]; NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)]; [self removeAttribute:NSLinkAttributeName range:paragaphRange]; [linkDetector enumerateMatchesInString:self.string ???????????????????????????????options:0 ?????????????????????????????????range:paragaphRange ????????????????????????????usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { ????[self addAttribute:NSLinkAttributeName value:result.URL range:result.range]; }]; |
有了這個,改變斷行行為就只需要實現一個布局管理器的代理方法:
| 1 2 3 4 5 6 7 8 9 | - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { ????NSRange range; ????NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName ??????????????????????????????????????????????????atIndex:charIndex ???????????????????????????????????????????effectiveRange:&range]; ????return !(linkURL && charIndex > range.location && charIndex <= NSMaxRange(range)); } |
想要一個可運行的例子的話,請在前面提到的 TextKitDemo 中查看“Layout”標簽頁。以下是截屏:
順便說一句,上面截屏里面的綠色輪廓線是無法用 TextKit 實現的。在這個演示程序中,我用了個小技巧來在布局管理器的子類中給文本畫輪廓線。也可以很容易以特定的方法來擴展 TextKit 的繪制功能。一定要看看!
演示程序4:文本交互
前面已經涉及到了 NSTextStorage 和 NSLayoutManager,最后一個演示程序將涉及 NSTextContainer。這個類并不復雜,而且它除了指定文本可不可以放置在某個地方外,什么都沒做。
不要將文本放置在某些區域,這是很常見的需求,例如,在雜志應用中。對于這種情況,iOS 上的 NSTextContainer 提供了一個 Mac 開發者夢寐以求的屬性:exclusionPaths,它允許開發者設置一個 NSBezierPath 數組來指定不可填充文本的區域。要了解這到底是什么東西,看一眼下面的截屏:
正如你所看到的,所有的文本都放置在藍色橢圓外面。在文本視圖里面實現這個行為很簡單,但是有個小麻煩:貝塞爾路徑的坐標必須使用容器的坐標系。以下是轉換方法:
| 1 2 3 4 5 6 7 8 9 10 11 | - (void)updateExclusionPaths { ????CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds ?????????????????????????????????????????fromView:self.circleView]; ????ovalFrame.origin.x -= self.textView.textContainerInset.left; ????ovalFrame.origin.y -= self.textView.textContainerInset.top; ????UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:ovalFrame]; ????self.textView.textContainer.exclusionPaths = @[ovalPath]; } |
在這個例子中,我使用了一個用戶可移動的視圖,它可以被自由移動,而文本會實時地圍繞著它重新排版。我們首先將它的bounds(self.circleView.bounds)轉換到文本視圖的坐標系統。
因為沒有 inset,文本會過于靠近視圖邊界,所以 UITextView 會在離邊界還有幾個點的距離的地方插入它的文本容器。因此,要得到以容器坐標表示的路徑,必須從 origin 中減去這個插入點的坐標。
在此之后,只需將貝塞爾路徑設置給文本容器即可將對應的區域排除掉。其它的過程對你來說是透明的,TextKit 會自動處理。
想要一個可運行的例子的話,請在前面提到的 TextKitDemo 中查看“Interaction”標簽頁。作為一個小噱頭,它也包含了一個跟隨當前文本選擇的視圖。應為,你也知道,沒有一個小小的丑陋的煩人的回形針擋住你的話,那還是一個好的文本編輯器演示程序嗎?
1. Pages 確實——據 Apple 聲稱——絕對沒有使用私有 API。*咳* 我的理論:它要么使用了一個 TextKit 的史前版本,要么復制了 UIKit 一半的私有源程序。或者兩者的混合。
2. 字形:如果說字符是一個字母的“語義”表達,字形則是它的可視化表達。取決于所使用的字體,字形要么是貝塞爾路徑,或者位圖圖像,它定義了要繪制出來的形狀。也請參考卓越的 Wikipedia 上關于字形的這篇文章。
3. 在一個類簇中,只有一個抽象的父類是公共的。分配一個實例實際上就是創建其中一個私有類的對象。因此,你總是為一個抽象類創建子類,并且需要實現所有的方法。也請參考 class cluster documentation。
總結
以上是生活随笔為你收集整理的iOS上文本处理之简史的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 优酷限免胡歌所有剧!《仙剑》等经典剧限时
- 下一篇: 五菱宏光 MINIEV 限时降价:起售价