在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

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

初識(shí) TextKit

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

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

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

iOS 上文本的簡(jiǎn)短歷史

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

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

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

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

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

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

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

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

iOS 5:文本方面沒(méi)啥變化。(個(gè)人經(jīng)歷:在 WWDC,我和工程師們談及 iOS 上文本系統(tǒng)?;卮鹗牵骸拔覀儧](méi)有看到太多這方面的請(qǐng)求…” 靠!)

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

iOS 7:終于來(lái)了,TextKit。

功能

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

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

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

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

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

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

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

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

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

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

序列化:過(guò)去沒(méi)有內(nèi)置的方法從磁盤(pán)讀取帶文本屬性的字符串?;蛘咴賹?xiě)回磁盤(pán)?,F(xiàn)在有了。

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

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

結(jié)構(gòu)

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

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

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

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

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

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

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

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

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

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

Cocoa 文本系統(tǒng)

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

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

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

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

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

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

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

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

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

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

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

示例

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

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

演示程序1:配置

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

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

在 Storyboard 或者 Interface 文件中實(shí)例化 UITextView 時(shí),它會(huì)預(yù)配置一個(gè)文本系統(tǒng):一個(gè) Text Storage,引用一個(gè) Layout Manager,而后者又引用一個(gè) Text Container。同樣地,一個(gè)文本系統(tǒng)棧也可以通過(guò)代碼直接創(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];

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

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

記住這些之后,創(chuàng)建一個(gè)更高級(jí)的設(shè)置也非常簡(jiǎn)單。假設(shè)在一個(gè)視圖里面依舊有一個(gè)從 nib 實(shí)例化的 Text View,叫做 originalTextView。增加對(duì)相同文本的第二個(gè)文本視圖只需要復(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];

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

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

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

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

otherTextView.scrollEnabled = NO;

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

想要一個(gè)這個(gè)配置的可運(yùn)行的例子的話,請(qǐng)?jiān)谇懊嫣岬降?TextKitDemo 中查看 “Configuration” 標(biāo)簽頁(yè)。

演示程序2:語(yǔ)法高亮

如果配置 Text View 不是那么令人激動(dòng),那么這里有更有趣的:語(yǔ)法高亮!

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

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

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

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

@implementation TKDHighlightingTextStorage
{
    NSMutableAttributedString *_imp;
}

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

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

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

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

實(shí)現(xiàn)兩個(gè) setter 方法也幾乎同樣簡(jiǎn)單。但也有一個(gè)小麻煩:Text Storage 需要通知它的 Layout Manager 變化發(fā)生了。因此 settter 方法必須也要調(diào)用 -edited:range:changeInLegth: 并傳給它變化的描述。聽(tīng)起來(lái)更糟糕,實(shí)現(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];
}

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

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

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

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

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

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

其次遍歷所有的樣式匹配項(xiàng)并高亮它們:

    [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)建了一個(gè)支持語(yǔ)法高亮的動(dòng)態(tài) Text View。當(dāng)用戶鍵入時(shí),高亮將被實(shí)時(shí)應(yīng)用。而且這只需幾行代碼??岚桑?/p>

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

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

想要一個(gè)可以運(yùn)行的 Demo 的話,請(qǐng)?jiān)谇懊嫣岬降?TextKitDemo 中查看“Highlighting”標(biāo)簽頁(yè)。

演示程序3:布局修改

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

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

TextKit:性能看來(lái)真是個(gè)問(wèn)題。理念(起碼現(xiàn)在)更多的是像這樣:“我們用簡(jiǎn)單但是高性能的方法實(shí)現(xiàn)了這個(gè)功能。這是結(jié)果,但是我們給你一個(gè)機(jī)會(huì)去更改它的一些東西。但是你只能在不太損害性能的地方進(jìn)行修改。”

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

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

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

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

首先,就像前面討論過(guò)的那樣,我們使用自定義的 Text Storage。但是,它尋找鏈接并將其標(biāo)記,而不是檢測(cè) 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];
}];

有了這個(gè),改變斷行行為就只需要實(shí)現(xiàn)一個(gè) 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));   

想要一個(gè)可運(yùn)行的例子的話,請(qǐng)?jiān)谇懊嫣岬降?TextKitDemo 中查看“Layout”標(biāo)簽頁(yè)。以下是截屏:

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

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

演示程序4:文本交互

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

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

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

正如你所看到的,所有的文本都放置在藍(lán)色橢圓外面。在 Text View 里面實(shí)現(xiàn)這個(gè)行為很簡(jiǎn)單,但是有個(gè)小麻煩: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];
}

在這個(gè)例子中,我使用了一個(gè)用戶可移動(dòng)的視圖,它可以被自由移動(dòng),而文本會(huì)實(shí)時(shí)地圍繞著它重新排版。我們首先將它的 bounds(self.circleView.bounds)轉(zhuǎn)換到 Text View 的坐標(biāo)系統(tǒng)。

因?yàn)闆](méi)有 inset,文本會(huì)過(guò)于靠近視圖邊界,所以 UITextView 會(huì)在離邊界還有幾個(gè)點(diǎn)的距離的地方插入它的文本容器。因此,要得到以容器坐標(biāo)表示的路徑,必須從 origin 中減去這個(gè)插入點(diǎn)的坐標(biāo)。

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

想要一個(gè)可運(yùn)行的例子的話,請(qǐng)?jiān)谇懊嫣岬降?TextKitDemo 中查看“Interaction”標(biāo)簽頁(yè)。作為一個(gè)小噱頭,它也包含了一個(gè)跟隨當(dāng)前文本選擇的視圖。因?yàn)椋阋仓?,沒(méi)有一個(gè)小小的丑陋的煩人的回形針擋住你的話,那還是一個(gè)好的文本編輯器演示程序嗎?


  1. Pages 確實(shí)——據(jù) Apple 聲稱——絕對(duì)沒(méi)有使用私有 API。 我的理論:它要么使用了一個(gè) TextKit 的史前版本,要么復(fù)制了 UIKit 一半的私有源程序?;蛘邇烧叩幕旌稀?#160;

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

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

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