天天看點

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。

  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”的“兩翼”下面有一些空白,是以它會把後面的小寫字母向左移讓它們更靠近點。進而大大提高了文本的易讀性,特别是在更長的文字中:

  

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

  連寫:我認為這主要是個藝術功能,但當某些字元組合(如“f”後面是“l”)使用組合符号(所謂的字形(glyph))繪制時,有些文本确實看起來更好(更美觀)。

  

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

  圖像附件:現在可以在文本視圖裡面添加圖像了。

  斷字:編輯文本時沒那麼重要,但如果要以好看易讀的方式展現文本時,這就相當重要。斷字意味着在行邊界處分割單詞,進而為整體文本建立一個更整齊的排版和外觀。個人經曆:iOS7 之前,開發者必須直接使用 CoreText。像這樣:首先以句子為基礎檢測文本語言,然後擷取句子中每個單詞可能的斷字點,然後在每一個可能的斷字點上插入定制的連字占位字元。準備好之後,運作 CoreText 的布局方法并手動将連字元插入到斷行。如果你想得到好的效果,之後你得檢查帶有連字元的文本沒有超出行邊界,如果超出了,在運作一次行的布局方法,這一次不要使用上次使用的斷字點。使用 TextKit 的話,就非常簡單了,設定 hyphenationFactor 屬性就可以啟用斷字。

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

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

  序列化:過去沒有内置的方法從磁盤讀取帶文本屬性的字元串。或者再寫回磁盤。現在有了。

  文本樣式:iOS7 的界面引入了一個全局預定義的文本類型的新概念。這些文本類型配置設定了一個全局預定義的外觀。理想情況下,這可以讓整個系統的标題和連續文本具有一緻的風格。通過設定應用,使用者可以定義他們的閱讀習慣(例如文本大小),那些使用文本樣式的應用将自動擁有正确的文本大小和外觀。

  文本效果:最後也是最不重要的。iOS7 有且僅有一個文本效果:凸版。使用此效果的文本看起來像是蓋在紙上面一樣。内陰影,等等。個人觀點:真的?靠…?在一個已經完全徹底不可饒恕地槍斃了所有無用的懷舊裝飾的作業系統上,誰會需要這個像文本蓋在紙上的外觀?

  結構

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

  

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

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

  字元串(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 的那幅圖作一下對比。

  

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

  驚人地相似。很明顯,最起碼主要部分,兩者是相同的。很明顯——除了右邊部分以及 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 時,它會預配置一個文本系統:一個文本存儲,引用一個文本管理器,而後者又引用一個文本容器。同樣地,一個文本系統棧也可以通過代碼直接建立:

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 的文本存儲:

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

  将第二個文本容器附加到布局管理器也差不多。比方說我們希望上面例子中的文本填充兩個文本視圖,而非一個。簡單:

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

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

  但有一點需要注意:由于在 otherTextView 中的文本容器可以無限地調整大小,thirdTextView 永遠不會得到任何文本。是以,我們必須指定文本應該從一個視圖回流到其它視圖,而不應該調整大小或者滾動:

otherTextView.scrollEnabled = NO;      

  不幸的是,看來将多個文本容器附加到一個文本管理器會禁用編輯功能。如果必須保留編輯功能的話,你隻可以将一個文本容器附加到一個文本管理器上。

  想要一個這個配置的可運作的例子的話,請在前面提到的 TextKitDemo 中檢視“Configuration”标簽頁。

示範程式2:文法高亮

  如果配置文本視圖不是那麼令人激動,那麼這裡有更有趣的:文法高亮!

  看看 TextKit 元件的責任劃分,就很清楚文法高亮應該在文本存儲上實作。因為 NSTextStorage 是一個類簇(附注3),建立它的子類需要做不少工作。我的想法是建立一個複合對象:實作所有的方法,但隻是将對它們的調用轉發給一個實際的執行個體,将輸入輸出參數或者結果修改為希望的樣子。

  NSTextStorage 繼承自 NSMutableAttributedString,并且必須實作以下四個方法——兩個 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;      

  一個類簇的子類的複合對象的實作也相當簡單。首先,找到一個滿足所有要求的最簡單的類。在我們的例子中,它是 NSMutableAttributedString,我們用它作為實作自定義存儲的實作:

@implementation TKDHighlightingTextStorage 
{
    NSMutableAttributedString *_imp;
}

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

  有了這個對象,隻需要一行代碼就可以實作兩個 getter 方法:

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

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

  實作兩個 setter 方法也幾乎同樣簡單。但也有一個小麻煩:文本存儲需要通知它的文本管理器變化發生了。是以 settter 方法必須也要調用 -edited:range:changeInLegth: 并傳給它變化的描述。聽起來更糟糕,實作變成:

- (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 檔案中載入時,可以像這樣将它插入文本視圖——但是記住從一個執行個體變量引用文本存儲:

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

  到目前為止,一切都很好。我們設法插入了一個自定義的文本存儲,接下來我們需要真正高亮文本的某些部分了。現在,一個簡單的高亮應該就是夠了:我們希望将所有 iWords 的顔色變成紅色——也就是那些以小寫“i”開頭,後面跟着一個大寫字母的單詞。

  一個友善實作高亮的辦法是覆寫 -processEditing。每次文本存儲有修改時,這個方法都自動被調用。每次編輯後,NSTextStorage 會用這個方法來清理字元串。例如,有些字元無法用標明的字型顯示時,文本存儲使用一個可以顯示它們的字型來進行替換。

  和其它一樣,為 iWords 增加一個簡單的高亮也相當簡單。我們覆寫 -processEditing,調用父類的實作,并設定一個正規表達式來查找單詞:

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

  就是這樣。我們建立了一個支援文法高亮的動态文本視圖。當使用者鍵入時,高亮将被實時應用。而且這隻需幾行代碼。酷吧?

  

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

  請注意僅僅使用 edited range 是不夠的。例如,當手動鍵入 iWords,隻有一個單詞的第三個字元被鍵入後,正規表達式才開始比對。但那時 editedRange 僅包含第三個字元,是以所有的處理隻會檢查這個字元。通過重新處理整個段落,我們可以完成高亮功能,又不會太過影響性能。

  想要一個這個配置的可運作的例子的話,請在前面提到的 TextKitDemo 中檢視“Highlighting”标簽頁。

示範程式3:布局修改

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

  Cocoa 文本系統:在 Mac OS上,性能不是問題,設計考量的全部是靈活性。可能是這樣:“這個東西可以做這個事情。如果你想的話,你可以覆寫它。性能不是問題。你也可以提供完全由自己實作的字元到字形的轉換,去做吧…”

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

  足夠的理念,讓我們來定制些東西。例如,調整行高如何?聽起來不可思議,但是在之前的 iOS 釋出版上調整行高至少是很黑客的行為,或者需要使用私有 API。幸運的是,現在(再一次)不用那麼搞腦子了。設定布局管理器的代理并實作僅僅一個方法即可:

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

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

  好,來一個更現實的場景。假設你的文本中有連結,你不希望這些連結被行包圍。如果可能的話,一個 URL 應該始終顯示為一個整體,一個單一的文本片段。沒有什麼比這更簡單的了。

  首先,我們通過使用自定義的文本存儲,就像前面讨論過的那個。但是,它尋找連結并将其标記,而不是檢測 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];
}];      

  有了這個,改變斷行行為就隻需要實作一個布局管理器的代理方法:

- (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”标簽頁。以下是截屏:

  

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

  順便說一句,上面截屏裡面的綠色輪廓線是無法用 TextKit 實作的。在這個示範程式中,我用了個小技巧來在布局管理器的子類中給文本畫輪廓線。也可以很容易以特定的方法來擴充 TextKit 的繪制功能。一定要看看!

示範程式4:文本互動

  前面已經涉及到了 NSTextStorage 和 NSLayoutManager,最後一個示範程式将涉及 NSTextContainer。這個類并不複雜,而且它除了指定文本可不可以放置在某個地方外,什麼都沒做。

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

  

iOS 7系列譯文:認識 TextKit 本文由 伯樂線上 - 和諧老約翰 翻譯自 Max Seelemann。示例

  正如你所看到的,所有的文本都放置在藍色橢圓外面。在文本視圖裡面實作這個行為很簡單,但是有個小麻煩:貝塞爾路徑的坐标必須使用容器的坐标系。以下是轉換方法:

- (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。

原文連結: Max Seelemann 翻譯: 伯樂線上 - 和諧老約翰

譯文連結: http://blog.jobbole.com/51965/

上一篇: CSS-Width
下一篇: CSS-Position

繼續閱讀