鍍金池/ 教程/ iOS/ 初識 TextKit
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

初識 TextKit

iOS 7 的發(fā)布給開發(fā)者的案頭帶來了很多新工具。其中一個就是 TextKit。TextKit 由許多新的 UIKit 類組成,顧名思義,這些類就是用來處理文本的。在這里,我們將介紹 TextKit 的來由、它的組成,以及通過幾個例子解釋開發(fā)者怎樣將它派上大用場。

但是首先我們得先闡明一個觀點:TextKit 可能是近期對 UIKit 最重要的補充了。iOS 7 的新界面用純文本按鈕替換了大量的圖標(biāo)和邊框??偟膩碚f,文本和文本布局在新 OS 系統(tǒng)的視覺效果中所占有的重要性大大提高了。iOS7 的重新設(shè)計完全是被文本驅(qū)動,這樣說也許并不夸張——而文本全部是 TextKit 來處理的。

告訴你這個變動到底有多大吧:iOS7 之前的所有版本,(幾乎)所有的文本都是 WebKit 來處理的。對:WebKit,web 瀏覽器引擎。所有 UILabel、UITextField,以及 UITextView 都在后臺以某種方式使用 web views 來進行文本布局和渲染。為了新的界面風(fēng)格,它們?nèi)急恢匦略O(shè)計以使用 TextKit。

iOS 上文本的簡短歷史

這些新類并不是用來替換開發(fā)者以前使用的類。對 SDK 來說,TextKit 提供的是全新的功能。iOS 7 之前,TextKit 提供的功能必須都手動完成。這是現(xiàn)有框架缺失的功能。

長期以來,只有一個基本的文本布局和渲染框架:CoreText。同樣也只有一個途徑讀取用戶的鍵盤輸入:UITextInput 協(xié)議。在 iOS6 中,為了簡單地獲取系統(tǒng)的文本選擇,也只有一個選擇:繼承 UITextView。

(這可能就是為什么我要公開自己十年開發(fā)文本編輯器的經(jīng)驗的原因了)在渲染文本和讀取鍵盤輸入之間存在著巨大(跟我讀:巨大)的缺口。這個缺口可能也是導(dǎo)致很少有富文本或者語法高亮編輯器的原因了——毫無疑問,開發(fā)一個好用的文本編輯器得耗費幾個月的時間。

就這樣——如下是 iOS 文本(不那么)簡短歷史的簡短概要:

iOS 2:這是第一個公開的 SDK,包括一個簡單的文本顯示組件(UILabel),一個簡單的文本輸入組件(UITextField),以及一個簡單的、可滾動、可編輯的并且支持更大量文本的組件:UITextView。這些組件都只支持純文本,沒有文本選擇支持(僅支持插入點),除了設(shè)置字體和文本顏色外幾乎沒有其他可定制功能。

iOS 3:新特性有復(fù)制和粘貼,以及復(fù)制粘貼所需要的文本選擇功能。數(shù)據(jù)探測器(Data Detector)為文本視圖提供了一個高亮電話號碼和鏈接的方法。然而,除了打開或關(guān)閉這些特性外,開發(fā)者基本上沒有什么別的事情可以做。

iOS 3.2:iPad 的出現(xiàn)帶來了 CoreText,也就是前面提到的低級文本布局和渲染引擎(從Mac OS X 10.5 移植過來的),以及 UITextInput,就是前面也提到的鍵盤存取協(xié)議。Apple 將 Pages 作為移動設(shè)備上文本編輯功能的樣板工程1。然而,由于我前面提到的框架缺口,只有很少的應(yīng)用使用它們。

iOS 4:iOS 3.2 發(fā)布僅僅幾個月后就發(fā)布了,文本方面沒有一丁點新功能。(個人經(jīng)歷:在 WWDC,我走近工程師們,告訴他們我想要一個完善的 iOS 文本布局系統(tǒng)?;卮鹗牵骸芭丁峤粋€請求?!辈怀鏊稀?/em>

iOS 5:文本方面沒啥變化。(個人經(jīng)歷:在 WWDC,我和工程師們談及 iOS 上文本系統(tǒng)。回答是:“我們沒有看到太多這方面的請求…” 靠?。?/em>

iOS 6:有些動作了:屬性文本編輯被加入了 UITextView。很不幸的是,它很難定制。默認(rèn)的 UI 有粗體、斜體和下劃線。用戶可以設(shè)置字體大小和顏色。粗看起來相當(dāng)不錯,但還是沒法控制布局或者提供一個便利的途徑來定制文本屬性。然而對于(文本編輯)開發(fā)者,有一個大的新功能:可以繼承 UITextView 了,這樣的話,除了以前版本提供的鍵盤輸入外,開發(fā)者可以“免費”獲得文本選擇功能。而在這以前,開發(fā)者必須實現(xiàn)一個完全自定義的文本選擇功能,這可能是很多非純文本工具的開發(fā)半途而廢的原因。(個人經(jīng)歷:我,WWDC,工程師們。我想要一個 iOS 的文本系統(tǒng)。回答:“嗯。吖。是的。也許?看,它只是不執(zhí)行…” 所以畢竟還是有希望,對吧?)

iOS 7:終于來了,TextKit。

功能

所以我們來了。iOS7 帶著 TextKit 登陸了。咱們看看它可以做什么!深入之前,我還想提一下,嚴(yán)格來說,這些新功能中的大部分以前都可以實現(xiàn)。如果你有大量的資源和時間來用 CoreText 構(gòu)建一個文本引擎,這些都是可以實現(xiàn)的。但是在以前,構(gòu)建一個完善的富文本編輯器可能花費你幾個月的時間,現(xiàn)在卻非常簡單。你只需要到在 Xcode 里打開一個界面文件,然后將 UITextView 拖到你的試圖控制器,就可以獲得所有以下這些功能:

字距調(diào)整(Kerning):所有的字符都有一個矩形的外邊框,這些邊框必須彼此相鄰來放置,這樣的想法已經(jīng)過時了。例如,現(xiàn)代文本布局會考慮到一個大寫的“T”的“兩翼”下面有一些空白,所以它會把后面的小寫字母向左移讓它們更靠近點。這樣做的結(jié)果大大提高了文本的易讀性,特別是在更長的文字中:

http://wiki.jikexueyuan.com/project/objc/images/5-1.png" alt="" />

連寫:我認(rèn)為這主要是個藝術(shù)功能,但當(dāng)某些字符組合(如“f”后面是“l(fā)”)使用組合符號(所謂的字形(glyph))繪制時,有些文本確實看起來更好(更美觀)。

http://wiki.jikexueyuan.com/project/objc/images/5-2.png" alt="" />

圖像附件:現(xiàn)在可以向 Text View 中添加圖像了。

斷字:編輯文本時沒那么重要,但如果要以好看易讀的方式展現(xiàn)文本時,這就相當(dāng)重要。斷字意味著在行邊界處分割單詞,從而為整體文本創(chuàng)建一個更整齊的排版和外觀。個人經(jīng)歷: iOS 7 之前,開發(fā)者必須直接使用 CoreText。像這樣:首先以句子為基礎(chǔ)檢測文本語言,然后獲取句子中每個單詞可能的斷字點,然后在每一個可能的斷字點上插入定制的連字占位字符。準(zhǔn)備好之后,運行 CoreText 的布局方法并手動將連字符插入到斷行。如果你想得到好的效果,之后你得檢查帶有連字符的文本沒有超出行邊界,如果超出了,在運行一次行的布局方法,這一次不要使用上次使用的斷字點。使用 TextKit 的話,就非常簡單了,設(shè)置 hyphenationFactor 屬性就可以啟用斷字。

http://wiki.jikexueyuan.com/project/objc/images/5-3.png" alt="" />

可定制性:對我來說,甚至比改進過的排版還多,這是個全新的功能。以前開發(fā)者必須在使用現(xiàn)有的功能和自己全部重頭寫之間做出選擇?,F(xiàn)在提供了一整套類,它們有代理協(xié)議,或者可以被覆蓋從而改變部分行為。例如,不必重寫整個文本組件,你現(xiàn)在就可以改變指定單詞的斷行行為。我認(rèn)為這是個勝利。

更多的富文本屬性:現(xiàn)在可以設(shè)置不同的下劃線樣式(雙線、粗線、虛線、點線,或者它們的組合)。提高文本的基線非常容易,這可用來設(shè)置上標(biāo)數(shù)字。開發(fā)者也不再需要自己為定制渲染的文本繪制背景顏色了(CoreText 不支持這些功能)。

序列化:過去沒有內(nèi)置的方法從磁盤讀取帶文本屬性的字符串?;蛘咴賹懟卮疟P。現(xiàn)在有了。

文本樣式:iOS 7 的界面引入了一個全局預(yù)定義的文本類型的新概念。這些文本類型分配了一個全局預(yù)定義的外觀。理想情況下,這可以讓整個系統(tǒng)的標(biāo)題和連續(xù)文本具有一致的風(fēng)格。通過設(shè)置應(yīng)用,用戶可以定義他們的閱讀習(xí)慣(例如文本大?。?,那些使用文本樣式的應(yīng)用將自動擁有正確的文本大小和外觀。

文本效果:最后也是最不重要的。iOS 7 有且僅有一個文本效果:凸版。使用此效果的文本看起來像是蓋在紙上面一樣。內(nèi)陰影,等等。個人觀點:真的?靠…?在一個已經(jīng)完全徹底不可饒恕地槍斃了所有無用的懷舊裝飾(skeuomorphism)的 iOS 系統(tǒng)上,誰會需要這個像文本蓋在紙上的效果?

結(jié)構(gòu)

可能概覽一個系統(tǒng)最好的方法是畫一幅圖。這是 UIKit 文本系統(tǒng)——TextKit 的簡圖:

http://wiki.jikexueyuan.com/project/objc/images/5-4.png" alt="" />

從上圖可以看出來,要讓一個文本引擎工作,需要幾個參與者。我們將從外到里介紹它們:

字符串(String):要繪制文本,那么必然在某個地方有個字符串來存儲這段文本。在默認(rèn)的結(jié)構(gòu)中,NSTextStorage 保存并管理這個字符串,在這種情況中,它可以遠離繪制。但并不一定非得這樣。使用 TextKit 時,文本可以來自任何適合的來源。例如,對于一個代碼編輯器,字符串可以是一棵包含所有顯示的代碼的結(jié)構(gòu)信息的注釋語法樹(annotated syntax tree,縮寫為 AST)。使用一個自定義的 NSTextStorage 就可以讓文本在稍后動態(tài)地添加字體或顏色高亮等文本屬性裝飾。這是第一次,開發(fā)者可以直接為文本組件使用自己的模型。要想實現(xiàn)這個功能,我們需要一個特別設(shè)計的 NSTextStorage,即:

NSTextStorage:如果你把文本系統(tǒng)看做一個模型-視圖-控制器(MVC)架構(gòu),這個類代表的是模型。NSTextStorage 是一個中樞,它管理所有的文本和屬性信息。系統(tǒng)只提供了兩個存取器方法存取它們,并另外提供了兩個方法來分別修改文本和屬性。后面我們將進一步了解這些方法?,F(xiàn)在重要的是你得理解 NSTextStorage 是從它的父類 NSAttributedString 繼承了這些方法。這就很清楚了,NSTextStorage——從文本系統(tǒng)看來——僅僅是一個帶有屬性的字符串,附帶一些擴展。這兩者唯一的重大不同點是 NSTextStorage 包含了一個方法,可以把所有對其內(nèi)容進行的修改以通知的形式發(fā)送出來。我們等一下會介紹這部分內(nèi)容。

UITextView:堆棧的另一頭是實際的視圖。在 TextKit 中,有兩個目的:第一,它是文本系統(tǒng)用來繪制的視圖。文本視圖它自己并會做任何繪制;它僅僅提供一個供其它類繪制的區(qū)域。作為視圖層級機構(gòu)中唯一的組件,第二個目的是處理所有的用戶交互。具體來說,Text View 實現(xiàn) UITextInput 的協(xié)議來處理鍵盤事件,它為用戶提供了一種途徑來設(shè)置一個插入點或選擇文本。它并不對文本做任何實際上的改變,僅僅將這些改變請求轉(zhuǎn)發(fā)給剛剛討論的 Text Storage。

NSTextContainer:每個 Text View 定義了一個文本可以繪制的區(qū)域。為此,每個 Text View 都有一個 Text Container,它精確地描述了這個可用的區(qū)域。在簡單的情況下,這是一個垂直的無限大的矩形區(qū)域。文本被填充到這個區(qū)域,并且 Text View 允許用戶滾動它。然而,在更高級的情況下,這個區(qū)域可能是一個無限大的矩形。例如,當(dāng)渲染一本書時,每一頁都有最大的高度和寬度。 Text Container 會定義這個大小,并且不接受任何超出的文本。相同情況下,一幅圖像可能占據(jù)了頁面的一部分,文本應(yīng)該沿著它的邊緣重新排版。這也是由 Text Container 來處理的,我們會在后面的例子中看到這一點。

NSLayoutManager:Layout Manager 是中心組件,它把所有組件粘合在一起:

  1. 這個管理器監(jiān)聽 Text Storage 中文本或?qū)傩愿淖兊耐ㄖ?,一旦接收到通知就觸發(fā)布局進程。
  2. 從 Text Storage 提供的文本開始,它將所有的字符翻譯為字形(Glyph)2。
  3. 一旦字形全部生成,這個管理器向它的 Text Containers 查詢文本可用以繪制的區(qū)域。
  4. 然后這些區(qū)域被行逐步填充,而行又被字形逐步填充。一旦一行填充完畢,下一行開始填充。
  5. 對于每一行,布局管理器必須考慮斷行行為(放不下的單詞必須移到下一行)、連字符、內(nèi)聯(lián)的圖像附件等等。
  6. 當(dāng)布局完成,文本的當(dāng)前顯示狀態(tài)被設(shè)為無效,然后 Layout Manager 將前面幾步排版好的文本設(shè)給 Text View。

CoreText:沒有直接包含在 TextKit 中,CoreText 是進行實際排版的庫。對于布局管理器的每一步,CoreText 被這樣或那樣的方式調(diào)用。它提供了從字符到字形的翻譯,用它們來填充行,以及建議斷字點。

Cocoa 文本系統(tǒng)

創(chuàng)建像 TextKit 這樣龐大復(fù)雜的系統(tǒng)肯定不是件簡單快速的事情,而且肯定需要豐富的經(jīng)驗和知識。在 iOS 的前面 6 個主版本中,一直沒有提供一個“真正的”文本組件,這也說明了這一點。Apple 把它視為一個大的新特性,當(dāng)然沒啥問題。但是它真的是全新的嗎?

這里有個數(shù)字:在 UIKit 的 131 個公共類中,只有 9 個的名字沒有使用UI作為前綴。這 9 個類使用的是舊系統(tǒng)的的、舊世界的(跟我讀:Mac OS)前綴 NS。而且這九個類里面,有七個是用來處理文本的。巧合?好吧…

這是 Cocoa 文本系統(tǒng)的簡圖。不妨和上面 TextKit 的那幅圖作一下對比。

http://wiki.jikexueyuan.com/project/objc/images/5-5.png" alt="" />

驚人地相似。很明顯,最起碼主要部分,兩者是相同的。很明顯——除了右邊部分以及 NSTextViewUITextView ——主要的類全部相同。TextKit 是(起碼部分是)從 Cocoa 文本系統(tǒng)移植到 iOS。(我之前一直請求的那個,耶?。?/em>

進一步比較還是能看出一些不同的。最值得注意的有:

  • 在 iOS 上沒有 NSTypesetterNSGlyphGenerator 這兩個類。在 Mac OS 上有很多方法來定制排版,在 iOS 中被極大地簡化了,去掉了一些抽象概念,并將這個過程合并到 NSLayoutManager 中來。保留下來的是少數(shù)的代理方法,以用來更改文本布局和斷行行為。

  • 這些 Cocoa 的類移植到 iOS 系統(tǒng)后新增了幾個非常便利的功能。在 Cocoa 中,必須手工地將確定的區(qū)域從 Text Container 分離出來(見上)。而 UIKit 類提供了一個簡單的 exclusionPaths 屬性就可以做到這一點。

  • 有些功能未能提供,比如,內(nèi)嵌表格,以及對非圖像的附件的支持。

盡管有這些區(qū)別,總的來說系統(tǒng)還是一樣的。NSTextStorage 在兩個系統(tǒng)是是一模一樣的,NSLayoutManagerNSTextContainer 也沒有太大的不同。這些變動,在沒有太多去除對一些特例的支持的情況下,看來(某些情況下大大地)使文本系統(tǒng)的使用變得更為容易。我認(rèn)為這是件好事。

事后回顧我從 Apple 工程師那里得到的關(guān)于將 Cocoa 文本系統(tǒng)移植到 iOS 的答案,我們可以得到一些背景信息。拖到現(xiàn)在并削減功能的原因很簡單:性能、性能、性能。文本布局可能是極度昂貴的任務(wù)——內(nèi)存方面、電量方面以及時間方面——特別是在移動設(shè)備上。Apple 必須采用更簡單的解決方案,并等到處理能力能夠至少部分支持一個完善的文本布局引擎。

示例

為了說明 TextKit 的能力,我創(chuàng)建了一個小的演示項目,你可以在 GitHub 上找到它。在這個演示程序中,我只完成了一些以前不容易完成的功能。我必須承認(rèn)寫這些代碼只花了我禮拜天的一個上午的時間;如果以前要做同樣的事情,我得花幾天甚至幾個星期。

TextKit 包括了超過 100 個方法,一篇文章根本沒辦法盡數(shù)涉及。而事實上,大多數(shù)時候,你需要的僅僅是一個正確的方法,TextKit 的使用和定制性也仍有待探索。所以我決定做四個更小的演示程序,而非一個大的演示程序來展示所有功能。每個演示程序中,我試著演示針對不同的方面和不同的類進行定制。

演示程序1:配置

讓我們從最簡單的開始:配置文本系統(tǒng)。正如你在上面 TextKit 簡圖中看到的,NSTextStorage、NSLayoutManagerNSTextContainer 之間的箭頭都是有兩個頭的。我試圖描述它們的關(guān)系是 1 對 N 的關(guān)系。就是那樣:一個 Text Storage 可以擁有多個 Layout Manager,一個 Layout Manager 也可以擁有多個 Text Container。這些多重性帶來了很好的特性:

  • 將多個 Layout Manager 附加到同一個 Text Storage 上,可以產(chǎn)生相同文本的多種視覺表現(xiàn),而且可以把它們放到一起來顯示。每一個表現(xiàn)都有獨立的位置和大小。如果相應(yīng)的 Text View 可編輯,那么在某個 Text View 上做的所有修改都會馬上反映到所有 Text View 上。
  • 將多個 Text Container 附加到同一個 Layout Manager 上,這樣可以將一個文本分布到多個視圖展現(xiàn)出來。很有用的一個例子,基于頁面的布局:每個頁面包含一個單獨的 Text View。所有這些視圖的 Text Container 都引用同一個 Layout Manager,這時這個 Layout Manager 就可以將文本分布到這些視圖上來顯示。

在 Storyboard 或者 Interface 文件中實例化 UITextView 時,它會預(yù)配置一個文本系統(tǒng):一個 Text Storage,引用一個 Layout Manager,而后者又引用一個 Text Container。同樣地,一個文本系統(tǒng)棧也可以通過代碼直接創(chuàng)建:

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];

這是最簡單的方式。手工創(chuàng)建一個文本系統(tǒng),唯一需要記住的事情是你的 View Controller 必須 retain 這個 Text Storage。在棧底的 Text View 只保留了對 Text Storage 和 Layout Manager 的弱引用。當(dāng) Text Storage 被釋放時,Layout Manager 也被釋放了,這樣留給 Text View 的就只有一個斷開的 Text Container 了。

這個規(guī)則有一個例外。只有從一個 interface 文件或 storyboard 實例化一個 Text View 時,Text View 確實會自動 retain Text Storage??蚣苁褂昧艘恍┖谀Хㄒ源_保所有的對象都被 retain,而無需手動建立一個 retain 環(huán)。

記住這些之后,創(chuàng)建一個更高級的設(shè)置也非常簡單。假設(shè)在一個視圖里面依舊有一個從 nib 實例化的 Text View,叫做 originalTextView。增加對相同文本的第二個文本視圖只需要復(fù)制上面的代碼,并重用 originalTextView 的 Text Storage:

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];

將第二個 Text Container 附加到 Layout Manager 也差不多。比方說我們希望上面例子中的文本填充兩個 Text View,而非一個。簡單:

NSTextContainer *thirdTextContainer = [NSTextContainer new];
[otherLayoutManager addTextContainer: thirdTextContainer];

UITextView *thirdTextView = [[UITextView alloc] initWithFrame:someFrame
                                                textContainer:thirdTextContainer];

但有一點需要注意:由于在 otherTextView 中的 Text Container 可以無限地調(diào)整大小,thirdTextView 永遠不會得到任何文本。因此,我們必須指定文本應(yīng)該從一個視圖回流到其它視圖,而不應(yīng)該調(diào)整大小或者滾動:

otherTextView.scrollEnabled = NO;

不幸的是,看來將多個 Text Container 附加到一個 Layout Manager 會禁用編輯功能。如果必須保留編輯功能的話,那么一個 Text Container 只能附加到一個 Layout Manager 上。

想要一個這個配置的可運行的例子的話,請在前面提到的 TextKitDemo 中查看 “Configuration” 標(biāo)簽頁。

演示程序2:語法高亮

如果配置 Text View 不是那么令人激動,那么這里有更有趣的:語法高亮!

看看 TextKit 組件的責(zé)任劃分,就很清楚語法高亮應(yīng)該由 Text Storage 實現(xiàn)。因為 NSTextStorage 是一個類簇3,創(chuàng)建它的子類需要做不少工作。我的想法是建立一個復(fù)合對象:實現(xiàn)所有的方法,但只是將對它們的調(diào)用轉(zhuǎn)發(fā)給一個實際的實例,將輸入輸出參數(shù)或者結(jié)果修改為希望的樣子。

NSTextStorage 繼承自 NSMutableAttributedString,并且必須實現(xiàn)以下四個方法——兩個 getter 和兩個 setter:

- (NSString *)string;
- (NSDictionary *)attributesAtIndex:(NSUInteger)location
                     effectiveRange:(NSRangePointer)range;
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str;
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range;

一個類簇的子類的復(fù)合對象的實現(xiàn)也相當(dāng)簡單。首先,找到一個滿足所有要求的最簡單的類。在我們的例子中,它是 NSMutableAttributedString,我們用它作為實現(xiàn)自定義存儲的實現(xiàn):

@implementation TKDHighlightingTextStorage
{
    NSMutableAttributedString *_imp;
}

- (id)init
{
    self = [super init];
    if (self) {
        _imp = [NSMutableAttributedString new];
    }
    return self;
}

有了這個對象,只需要一行代碼就可以實現(xiàn)兩個 getter 方法:

- (NSString *)string
{
    return _imp.string;
}

- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
    return [_imp attributesAtIndex:location effectiveRange:range];
}

實現(xiàn)兩個 setter 方法也幾乎同樣簡單。但也有一個小麻煩:Text Storage 需要通知它的 Layout Manager 變化發(fā)生了。因此 settter 方法必須也要調(diào)用 -edited:range:changeInLegth: 并傳給它變化的描述。聽起來更糟糕,實現(xiàn)變成:

- (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];
}

就這樣,我們在文本系統(tǒng)棧里面有了一個 Text Storage 的全功能替換版本。在從 Interface 文件中載入時,可以像這樣將它插入文本視圖——但是記住從一個實例變量引用 Text Storage:

_textStorage = [TKDHighlightingTextStorage new];
[_textStorage addLayoutManager: self.textView.layoutManager];

到目前為止,一切都很好。我們設(shè)法插入了一個自定義的文本存儲,接下來我們需要真正高亮文本的某些部分了?,F(xiàn)在,一個簡單的高亮應(yīng)該就是夠了:我們希望將所有 iWords 的顏色變成紅色——也就是那些以小寫“i”開頭,后面跟著一個大寫字母的單詞。

一個方便實現(xiàn)高亮的辦法是覆蓋 -processEditing。每次文本存儲有修改時,這個方法都自動被調(diào)用。每次編輯后,NSTextStorage 會用這個方法來清理字符串。例如,有些字符無法用選定的字體顯示時,Text Storage 使用一個可以顯示它們的字體來進行替換。

和其它一樣,為 iWords 增加一個簡單的高亮也相當(dāng)簡單。我們覆蓋 -processEditing,調(diào)用父類的實現(xiàn),并設(shè)置一個正則表達式來查找單詞:

- (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];

然后,首先清除之前的所有高亮:

    NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
    [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];

其次遍歷所有的樣式匹配項并高亮它們:

    [iExpression enumerateMatchesInString:self.string
                                  options:0 range:paragaphRange
                               usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop)
    {
        [self addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:result.range];
    }];
}

就是這樣。我們創(chuàng)建了一個支持語法高亮的動態(tài) Text View。當(dāng)用戶鍵入時,高亮將被實時應(yīng)用。而且這只需幾行代碼??岚??

http://wiki.jikexueyuan.com/project/objc/images/5-6.png" alt="" />

請注意僅僅使用 edited range 是不夠的。例如,當(dāng)手動鍵入 iWords,只有一個單詞的第三個字符被鍵入后,正則表達式才開始匹配。但那時 editedRange 僅包含第三個字符,因此所有的處理只會影響這一個字符。通過重新處理整個段落可以解決這個問題,這樣既完成高亮功能,又不會太過影響性能。

想要一個可以運行的 Demo 的話,請在前面提到的 TextKitDemo 中查看“Highlighting”標(biāo)簽頁。

演示程序3:布局修改

如前所述,Layout Manager 是核心的布局主力。Mac OS 上 NSTypesetter 的高度可定制功能被并入 iOS 上的 NSLayoutManager。雖然 TextKit 不具備像 Cocoa 文本系統(tǒng)那樣的完全可定制性,但它提供很多代理方法來允許做一些調(diào)整。如前所述,TextKit 與 CoreText 更緊密地集成在一起,主要是基于性能方面的考慮。但是兩個文本系統(tǒng)的理念在一定程度上是不一樣的:

Cocoa 文本系統(tǒng):在 Mac OS上,性能不是問題,設(shè)計考量的全部是靈活性??赡苁沁@樣:“這個東西可以做這個事情。如果你想的話,你可以覆蓋它。性能不是問題。你也可以提供完全由自己實現(xiàn)的字符到字形的轉(zhuǎn)換,去做吧…”

TextKit:性能看來真是個問題。理念(起碼現(xiàn)在)更多的是像這樣:“我們用簡單但是高性能的方法實現(xiàn)了這個功能。這是結(jié)果,但是我們給你一個機會去更改它的一些東西。但是你只能在不太損害性能的地方進行修改?!?/p>

理念的東西就講這么多,現(xiàn)在讓我們來搞些實際的東西。例如,調(diào)整行高如何?聽起來不可思議,但是在之前的 iOS 發(fā)布版上調(diào)整行高需要使用黑科技或者私有 API。幸運的是,現(xiàn)在(再一次)不用那么費腦子了。設(shè)置 Layout Manager 的代理并實現(xiàn)僅僅一個方法即可:

- (CGFloat)      layoutManager:(NSLayoutManager *)layoutManager
  lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex
  withProposedLineFragmentRect:(CGRect)rect
{
    return floorf(glyphIndex / 100);
}

在以上的代碼中,我修改了行間距,讓它與文本長度同時增長。這導(dǎo)致頂部的行比底部的行排列得更緊密。我承認(rèn)這沒什么實際的用處,但是它是可以做到的(而且肯定會有更實用的用例的)。

好,來一個更現(xiàn)實的場景。假設(shè)你的文本中有鏈接,你不希望這些鏈接被斷行分割。如果可能的話,一個 URL 應(yīng)該始終顯示為一個整體,一個單一的文本片段。沒有什么比這更簡單的了。

首先,就像前面討論過的那樣,我們使用自定義的 Text Storage。但是,它尋找鏈接并將其標(biāo)記,而不是檢測 iWords,如下:

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];
}];

有了這個,改變斷行行為就只需要實現(xiàn)一個 Layout Manager 的代理方法:

- (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”標(biāo)簽頁。以下是截屏:

http://wiki.jikexueyuan.com/project/objc/images/5-7.png" alt="" />

順便說一句,上面截屏里面的綠色輪廓線是無法用 TextKit 實現(xiàn)的。在這個演示程序中,我用了個小技巧來在 Layout Manager 的子類中給文本畫輪廓線。以特定的方法來擴展 TextKit 的繪制功能也不是件難事,你一定要看看!

演示程序4:文本交互

前面已經(jīng)涉及到了 NSTextStorageNSLayoutManager,最后一個演示程序?qū)⑸婕?NSTextContainer。這個類并不復(fù)雜,而且它除了指定文本可不可以放置在某個地方外,什么都沒做。

不要將文本放置在某些區(qū)域,這是很常見的需求,例如,在雜志應(yīng)用中。對于這種情況,iOS 上的 NSTextContainer 提供了一個 Mac 開發(fā)者夢寐以求的屬性:exclusionPaths,它允許開發(fā)者設(shè)置一個 NSBezierPath 數(shù)組來指定不可填充文本的區(qū)域。要了解這到底是什么東西,看一眼下面的截屏:

http://wiki.jikexueyuan.com/project/objc/images/5-8.png" alt="" />

正如你所看到的,所有的文本都放置在藍色橢圓外面。在 Text View 里面實現(xiàn)這個行為很簡單,但是有個小麻煩:Bezier Path 的坐標(biāo)必須使用容器的坐標(biāo)系。以下是轉(zhuǎn)換方法:

- (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)轉(zhuǎn)換到 Text View 的坐標(biāo)系統(tǒng)。

因為沒有 inset,文本會過于靠近視圖邊界,所以 UITextView 會在離邊界還有幾個點的距離的地方插入它的文本容器。因此,要得到以容器坐標(biāo)表示的路徑,必須從 origin 中減去這個插入點的坐標(biāo)。

在此之后,只需將 Bezier Path 設(shè)置給 Text Container 即可將對應(yīng)的區(qū)域排除掉。其它的過程對你來說是透明的,TextKit 會自動處理。

想要一個可運行的例子的話,請在前面提到的 TextKitDemo 中查看“Interaction”標(biāo)簽頁。作為一個小噱頭,它也包含了一個跟隨當(dāng)前文本選擇的視圖。因為,你也知道,沒有一個小小的丑陋的煩人的回形針擋住你的話,那還是一個好的文本編輯器演示程序嗎?


  1. Pages 確實——據(jù) Apple 聲稱——絕對沒有使用私有 API。 我的理論:它要么使用了一個 TextKit 的史前版本,要么復(fù)制了 UIKit 一半的私有源程序。或者兩者的混合。 

  2. 字形(Glyphs):如果說字符是一個字母的“語義”表達,字形則是它的可視化表達。取決于所使用的字體,字形要么是貝塞爾路徑,或者位圖圖像,它定義了要繪制出來的形狀。也請參考卓越的 Wikipedia 上關(guān)于字形的[這篇文章][12]。 

  3. 在一個類簇中,只有一個抽象的父類是公共的。分配一個實例實際上就是創(chuàng)建其中一個私有類的對象。因此,你總是為一個抽象類創(chuàng)建子類,并且需要實現(xiàn)所有的方法。也請參考 [class cluster documentation][13]。 

上一篇:Metal下一篇:字符串渲染