5 iOS 7

卷首語

歡迎來到 objc.io 第五期泥技!

我們希望你跟我們一樣為 iOS 7 的發(fā)布而感到興奮浆兰。選擇這個做為本期的主題毫無壓力,因為這里面有太多我們想談?wù)摰臇|西了。同往常一樣簸呈,可以討論的話題遠遠比我們本期所能討論的要多得多榕订,所以毫無疑問地,這些絕不是您學(xué)習(xí) iOS 7 的最終向?qū)杀恪N覀儍H挑選了一些亮點和重點來撰文討論劫恒。

這一次,我們擁有比往常都要豪華的嘉賓陣容轿腺。Holger Riegel 和 Tobias Kre? 詳細地為我們描述了他們將應(yīng)用升級到 iOS 7 的全過程两嘴。當(dāng)我們?yōu)?iOS 7 重新設(shè)計應(yīng)用的時候,一個非常值得考慮的事情就是添加力學(xué)特性-UIDynamics族壳,Ash Furrow 為我們展示了如何在 Collection View 中使用這一新的特性憔辫。Chris 撰文討論新的 View Controller 切換動畫的 API,Mattt Thompson 寫了一篇如何從使用 NSURLConnection 過渡到使用新的 NSURLSession 來進行網(wǎng)絡(luò)方面的請求决侈,而 David 為我們展現(xiàn)了 iOS 7中多任務(wù)的新潛力螺垢。Max Seelemann 的文章為我們介紹了 iOS 新系統(tǒng)中最具創(chuàng)新性和革命性的框架:TextKit。最后赖歌,Peter Steinberger 將帶我們?nèi)ヒ娮R一下 iOS 7 中他非常喜歡的一些新特性枉圃。

我們?nèi)匀辉趯ふ腋嗟募钨e作者。還有很多主題等待我們?nèi)グl(fā)掘庐冯,歡迎你的加入孽亲。

此致!Chris展父,Daniel 以及 Florian

2013年10月返劲,于柏林


初識 TextKit

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

但是首先我們得先闡明一個觀點:TextKit 可能是近期對 UIKit最重要的補充了匈挖。iOS 7 的新界面用純文本按鈕替換了大量的圖標(biāo)和邊框苍鲜∷蓟遥總的來說,文本和文本布局在新 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)≌中回答是:“哦…提交個請求啊央。”不出所料…)

iOS 5:文本方面沒啥變化涨醋。(個人經(jīng)歷:在 WWDC瓜饥,我和工程師們談及 iOS 上文本系統(tǒng)《В回答是:“我們沒有看到太多這方面的請求…” 靠Q构獭)

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

iOS 7:終于來了酪我,TextKit消痛。

功能

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

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

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

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

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

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

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

序列化:過去沒有內(nèi)置的方法從磁盤讀取帶文本屬性的字符串〔⒒龋或者再寫回磁盤〖婪福現(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 的簡圖:

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

字符串(String):要繪制文本豌鸡,那么必然在某個地方有個字符串來存儲這段文本嘿般。在默認的結(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)只提供了兩個存取器方法存取它們屈暗,并另外提供了兩個方法來分別修改文本和屬性拆讯。后面我們將進一步了解這些方法。現(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 是中心組件呐萌,它把所有組件粘合在一起:

這個管理器監(jiān)聽 Text Storage 中文本或?qū)傩愿淖兊耐ㄖ坏┙邮盏酵ㄖ陀|發(fā)布局進程谊娇。

從 Text Storage 提供的文本開始肺孤,它將所有的字符翻譯為字形(Glyph)[^2]。

一旦字形全部生成济欢,這個管理器向它的 Text Containers 查詢文本可用以繪制的區(qū)域赠堵。

然后這些區(qū)域被行逐步填充,而行又被字形逐步填充法褥。一旦一行填充完畢茫叭,下一行開始填充。

對于每一行挖胃,布局管理器必須考慮斷行行為(放不下的單詞必須移到下一行)杂靶、連字符、內(nèi)聯(lián)的圖像附件等等酱鸭。

當(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 的那幅圖作一下對比瓢姻。

驚人地相似祝蝠。很明顯,最起碼主要部分幻碱,兩者是相同的绎狭。很明顯——除了右邊部分以及NSTextView和UITextView——主要的類全部相同。TextKit 是(起碼部分是)從 Cocoa 文本系統(tǒng)移植到 iOS褥傍。(我之前一直請求的那個儡嘶,耶!)

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

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

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

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

盡管有這些區(qū)別颠黎,總的來說系統(tǒng)還是一樣的另锋。NSTextStorage在兩個系統(tǒng)是是一模一樣的,NSLayoutManager和NSTextContainer也沒有太大的不同盏缤。這些變動砰蠢,在沒有太多去除對一些特例的支持的情況下,看來(某些情況下大大地)使文本系統(tǒng)的使用變得更為容易唉铜。我認為這是件好事台舱。

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

示例

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

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

演示程序1:配置

讓我們從最簡單的開始:配置文本系統(tǒng)瞳浦。正如你在上面 TextKit 簡圖中看到的担映,NSTextStorage、NSLayoutManager和NSTextContainer之間的箭頭都是有兩個頭的叫潦。我試圖描述它們的關(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)建:


這是最簡單的方式。手工創(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:


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


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


不幸的是,看來將多個 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:


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


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


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


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


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

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

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


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


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


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

請注意僅僅使用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é)果端壳,但是我們給你一個機會去更改它的一些東西告丢。但是你只能在不太損害性能的地方進行修改「澹”

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


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

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

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


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


想要一個可運行的例子的話,請在前面提到的TextKitDemo中查看“Layout”標(biāo)簽頁蝗罗。以下是截屏:

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

演示程序4:文本交互

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

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

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


在這個例子中甜奄,我使用了一個用戶可移動的視圖,它可以被自由移動王滤,而文本會實時地圍繞著它重新排版贺嫂。我們首先將它的 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)于字形的這篇文章弥雹。

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


翻譯作者:和諧老約翰

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末械念,一起剝皮案震驚了整個濱河市头朱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌龄减,老刑警劉巖项钮,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡烁巫,警方通過查閱死者的電腦和手機署隘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亚隙,“玉大人磁餐,你說我怎么就攤上這事“⑵” “怎么了诊霹?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渣淳。 經(jīng)常有香客問我畅哑,道長,這世上最難降的妖魔是什么水由? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任赛蔫,我火速辦了婚禮砂客,結(jié)果婚禮上呵恢,老公的妹妹穿的比我還像新娘。我一直安慰自己渗钉,他們只是感情好彤恶,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布鳄橘。 她就那樣靜靜地躺著,像睡著了一般瘫怜。 火紅的嫁衣襯著肌膚如雪术徊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天赠涮,我揣著相機與錄音暗挑,去河邊找鬼。 笑死垃它,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嗤瞎。 我是一名探鬼主播虹菲,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掉瞳,長吁一口氣:“原來是場噩夢啊……” “哼霎褐!你這毒婦竟也來了该镣?” 一聲冷哼從身側(cè)響起省艳,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤眷蜓,失蹤者是張志新(化名)和其女友劉穎上岗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赞赖,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年鲁捏,在試婚紗的時候發(fā)現(xiàn)自己被綠了渔期。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姨夹。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡磷账,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逃糟,到底是詐尸還是另有隱情,我是刑警寧澤菇肃,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布取募,位于F島的核電站玩敏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏旺聚。R本人自食惡果不足惜砰粹,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弄痹。 院中可真熱鬧嵌器,春花似錦、人聲如沸嘴秸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽执解。三九已至,卻和暖如春衰腌,著一層夾襖步出監(jiān)牢的瞬間右蕊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工帕翻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留萝风,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓睬塌,卻偏偏與公主長得像歇万,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容