WWDC 2018:TextKit 最佳實踐

WWDC 2018 Session 221: TextKit Best Practicesgit

做者簡介:@halohily,網易有道 iOS 開發工程師。掘金主頁:halohilygithub

引言

文本內容在 app 內隨處可見,展現文本的方式也是多種多樣。關注過性能提高的同窗會發現,文本控件的高效使用對於整個頁面性能的提高相當重要。爲此,蘋果和開發者都在不斷努力。好比蘋果日漸完善的文本框架,以及第三方文本框架的表明 YYTextswift

這個 session 旨在指導開發者如何正確地使用 TextKit 進行文本內容的展現,按部就班分爲三個部分:緩存

  • 核心理論
  • 用以演示理論的小例子
  • 綜合運用的優秀實戰案例

1、核心理論

1.1 什麼是 TextKit ?

和平時使用的框架有些不一樣,咱們不須要使用 import 關鍵字來導入 TextKit。包含 UILabelUITextField 等控件的 UIKit 框架(用於 iOS),以及包含 NSTextView 等控件的 AppKit 框架(用於 Mac OS),都是基於 TextKit 構建。在使用上面的文本控件時,其實就是在使用 TextKit,它協同 Core TextCore Graphics 以及 Foundation,一塊兒爲咱們的 app 提供強大的文本展現能力。安全

利用 TextKit 的能力,你能夠很是容易地展現下面風格各異的文本。markdown

1.2 選擇正確的控件

對於不一樣類型的文本,咱們須要選擇合適的文本控件。那麼該如何決定呢?蘋果爲咱們提供了比較明確的指導,以下圖所示。在使用 UIKitAppKit 時,情形會稍有不一樣,因此分開進行描述。session

  • UIKit 的選擇路徑:

  • AppKit 的選擇路徑:

圖中的描述很是清晰易讀。須要注意的是,UILabel 用來展現較少的文本內容或者較少的行數,然而,在 AppKit 框架下是沒有 Label 控件的,這時能夠選擇 NSTextField 控件,經過禁用文本編輯屬性,來得到和 UILabel 同樣的特性。架構

1.2.1 文本繪製(string drawing)的正確使用

有的時候,你們可能爲了得到更優的性能(避免生成過多的視圖對象實例),經過調用以下方法來使用文本繪製:app

func draw(at: CGPoint)

func draw(in: CGRect)

func draw(with: CGRect, options: NSStringDrawingOptions = [], context: NSStringDrawingContext?)
複製代碼

然而,蘋果並不推薦常常這樣使用。若是你依然須要使用的話,蘋果也貼心地給出了一些建議:框架

  • 儘可能用於數量較少的文本
  • 限制調用 draw 方法的頻率(儘可能減小調用次數)
  • 限制定製化屬性的數量(儘可能減小定製化屬性)

爲何這種使用方式不被推薦呢?首先是由於 UILabelUITextView 等控件提供了良好的緩存機制,因此在合適的時候選擇這些控件,反而能夠得到更好的性能(相較 string drawing 而言),特別是在使用自動佈局的時候。

繪製 attributed string時,若是過多地調用 draw 方法,會明顯地下降性能。由於系統在每次繪製以前須要釋放以前全部的 attribute 對象。所以,對於額外的 attribute,請儘可能在肯定它們的視覺效果(例如字體、顏色)時才進行繪製。

最後,蘋果仍是不忘強調,若是使用了 string drawing,就會失去下圖所示的文本控件提供的全部特性。所以,請儘量地使用文本控件。

1.3 選擇正確的定製要點

1.3.1 TextKit 的架構組成

Cocoa 下的許多組件同樣,TextKit 也是基於 「model - view - controller」 設計結構的。而且這三層又各自包含 storage、layout、和 display 模塊:

  • Storage

深刻了解一下各個部分的組成,首先是與 Model 層通訊的 Storage 模塊,它包含的 NSTextStorage 持有字符串的數據和屬性信息。值得注意的是,它是MutableAttributedString 的子類,所以使用方式和咱們熟知的 AttributedString 一致。而 NSTextContainer 則負責模型化文本佈局的地理位置、區域信息。

  • Display

接下來是 Display 模塊,它和 View 層通訊。這個模塊咱們一般關注的是文本控件的正確選擇問題。

  • Layout

最後是 Layout 模塊,它和 Controller 層進行通訊。 NSLayoutManager 是這個模塊惟一的組成部分。它的強大讓蘋果用「野獸」來形容。它是整個展現過程的「大腦」,控制本身的佈局過程。

1.3.2 佈局過程

這是文本佈局過程的概覽圖:

  • 屬性修正

文本佈局發生在 TextStorage 進行屬性修正以後。對於這個過程當中的工做,舉個例子,確保這段文本所選擇的字體支持顯示文本中的全部字符,若是發現不支持的字符,則進行相應替換。好比上圖中的 Tempura (天麩羅) is a tasty Japanese food. 🍤 這段文本,字體指定了 Times New Roman。然而,這個字體是不支持日語字符和 emoji 字符的。所以,在屬性修正過程當中,日語字符被指定了支持日語的 Hiragino Mincho ProN 字體,而 emoji 字符則被指定了 Apple Color Emoji 字體。

  • glyphcharacter

屬性修正完成後,佈局過程就開始了。這裏對上述概念的含義作一些說明。

character 中文譯爲「字符」,字符是能夠轉換爲二進制存儲的通用數據,而 glyph 能夠譯爲字符的視覺表示符號。同一個 character 呈如今屏幕上,能夠表現爲不一樣的字體、視覺風格。而這些各異的視覺風格,就是由 glyph 來負責呈現,glyph 的生成,就是爲指定了視覺效果(如字體)的字符肯定展現所需的 glyph 的過程。下圖是一個示例:

能夠看到,characterglyph 的對應關係不老是一對一的。圖中的字符串 「ffi」 由三個字符組成,但整個字符串能夠由一個 glyph 表示。再看下圖的例子,一個單獨的字符 「n」,也能夠由兩個 glyph 來表示。

關於這部分概念,提供一篇參考資料:iOS 排版概念

再回到佈局過程的圖示中來,glyph 佈局,就是 NSLayoutManager 在視圖上擺放 glyph 的過程。

1.4 選擇正確的配置

以下圖是 一個完整 TextKit 組件的標準配置結構:Text Container 持有 Text View 的弱引用,而 Text View 經過根 Text Storage持有整個佈局樹結構。

若是有多個文本頁面或者文本行須要佈局,可使用成對的 Text ContainerText View 組合,每一對組合對應一個頁面或者一行。在這種狀況下,咱們能夠 hook 一樣的 container 和 text view 來共享佈局信息。

文本內容被添加以後,它鋪滿由第一個 text container 定義的區域。文本在 text view 上和 text container 成對展現。 當沒有剩餘空間時,新的 container 連同 text view 一塊兒被添加,而且文本在第二個頁面或者文本行進行展現。

多個 layout manager 容許你對一樣的文本有多種不一樣的顯示效果。這個文本在不一樣的視圖上能夠有彼此不一樣且獨立的佈局和分組,下圖是這種模式下的結構示意和效果示意。方框內的文本內容相同,但展現效果是不一樣的。

1.5 選擇正確的定製實現方式

就像錘子在工具箱中的重要地位同樣,咱們在開發時也有一些地位等同於錘子的工具。

  • 代理 就像基本的錘子,大多數時候,它能夠很好地完成工做。
  • 通知 也是一個有效的工具。
  • 最後,子類化 一樣是一把利器。它幾乎能夠做任何事。

對於這些方式的使用場景,在第二部分會運用具體例子進行闡述。

2、具體示例

文本組件在 app 中是無處不在的。在這部分,蘋果使用了 iOS 的 Apple News 和 Mac OS 的TextEditOur Journal 三個 app 中的具體頁面做爲示例來對前面所述的核心理論進行講解。

2.1 Apple News on iOS

這部份內容比較簡單。主要用來示意 Choosing the right control 這條理論。裏面主要的知識點以下:

  • 對於一行顏色不同的文本,可使用兩個 UILabel 進行展現,也能夠藉助 NSAttributedString 來實現。
  • UITextViewUIScrollView 的子類,默認支持滑動,若是想讓它與自動佈局良好協做地話,須要禁用滑動。

2.2 TextEdit on macOS

這部分主要用來示意 Choosing the right configuration 這條理論。

TextEdit 這個 app 支持富文本的展現、編輯,文本編輯部分的特性很像一個 textview,天然,它符合前面講述的標準配置結構。值得注意的是,文本編輯部分支持分頁展現,能夠看到頁面下滑時,textcontainer 被從新設置了尺寸,文本從第一頁跳到了第二頁。很天然,這是使用了多個 textcontainer 的 textview,可是依然由同一個 textstorge、layoutmanager 管理,他們容許文本自由地從一個 textcontainer 跳到另外一個。下圖便是它的配置結構圖:

2.3 Our Journal App on macOS

這部分主要用來示意 Choosing the right customization approach 這條理論。

2.3.1 文本計數功能

從圖中能夠看到,在界面底部添加了一個 TextField 來顯示鍵入文本的數量。app 運行時,咱們但願底部的文本計數隨着鍵入的數量變化。爲了實現這個效果,咱們選擇一個比較「輕巧」的工具 - 通知。經過接收 NSTextStorage 發出的通知,能夠從 NSTextStorage 得到文本的數量。收到通知後,更新計數 TextField 中的數字。

2.3.2 自動轉化粗體字

當咱們想強調一部分文字時,可使用鍵盤快捷鍵或者菜單設置這部分字體爲粗體。可是若是想支持例如 markdown 的標記語言,經過特定字符來指定特殊的格式,好比在文本先後加入一對雙星號來使文本變化爲粗體,該如何實現呢?在這個情景中,須要獲取文本改變的時機和位置,通知機制並不便於提供足夠的信息。因此此次使用「一記重錘」 - 代理。遵照 NSTextStorageDelegate 協議,實現 textStorage(_:didProcessEditing:range:changeInLength:) 方法。在方法的實現中定義一個粗體字的 attribute ,添加給應該被粗體化的文本。這樣一來,只要輸入了一對雙星號,就能夠立馬使文本變爲粗體。

2.3.3 代碼片斷文本

粗體標記完美實現了。那麼如何展現一個代碼片斷呢?像圖中所示,完成鍵入最後一個點符號,就能夠生成一個代碼塊文本,同時還會被標示爲 Swift 代碼。對於這樣一個複雜的情形,咱們須要兩把工具:

  • 子類化 NSTextStorage

子類繼承 NSTextStorage ,實現四個強制實現的方法,特別是 replaceCharacters(in:with:) 方法。內部實現是將 NSTextBlock 賦值給 ParagraphStyle 而後把這個 ParagraphStyle 做爲一個 attribute 添加到一個 NSTextStorage 中,注意對應的範圍是代碼塊文本。

對於上面所述的 NSTextBlock ,須要瞭解的是 NSTextBlock 不會去定製化繪製它本身,因此咱們須要一個它的子類去完成這件事: CodeBlock 類繼承自 NSTextBlock,在它的初始化方法中設置背景的襯墊,或者經過覆寫 drawBackground 方法,使用 StringDrawing 去繪製 「Swift Code」 這個標題。

這樣一來這個文本塊看起來就像一個代碼塊了。再回到繼承自 TextStorageCustomTextStorage,咱們能夠把 TextBlocks 屬性賦值爲剛剛添加的 CodeBlock

最後,咱們須要讓 textview 使用全新的CustomTextStorage,因此咱們爲 LayoutManager 替換 storage。

2.3.4 markdown效果預覽視圖

這樣一來,基本完成了一個支持 markdown 格式的編輯器。除此以外,通常 markdown 編輯器還有一個很實用的功能 - 兩個並排佈局的視圖,一個用來輸入文本,一個預覽效果,如圖所示:

咱們可使用兩個並排的 textview 來實現,只須要禁用用於預覽的 textview 的文本編輯功能。它們展現同樣的內容,可是右邊的樣式會特別一些。使用的配置如圖:

storage 是同一個,由於展現同樣的內容。可是其餘的部分都是兩套,而且用左邊 view 的 textstorage 爲右邊 view 的 layoutmanager 的 replaceTextStorage 賦值。這樣的效果是什麼呢?一旦在一邊編輯了文本,效果會在兩邊同時展現。可是通常在預覽視圖內咱們是不但願顯示 markdown 格式控制相關字符的,好比雙星號 ** 和 引用符號 > 等。因爲是共享的同一個 textstorge,這就意味着咱們必須在後面的過程當中(佈局過程)隱藏這些字符。爲了完成這個操做,就有了一個天然而然的選項--代理:遵照 NSLayoutManagerDelegate 代理協議,實現 layoutManager(_:shouldGenerateGlyphs:properties: characterIndexes:font:forGlyphRange:) 代理方法,咱們能夠獲取到將要被佈局的 glyphs,若是它是用來表示 markdown 字符的 glyph,把它賦值爲空。最後,把處理過的 glyphs 回傳。這樣一來,左邊展現可編輯的包含 markdown 控制字符的文本,右邊展現去除了 markdown 控制字符的效果文本。雖然事實上一個 markdown 編輯器並非這樣處理,但這是一個定製 TextKit 的很好的例子。

3、最佳實戰案例

在這部分中,蘋果給出了幾個指導性原則。

3.1 熟知默認 attribute

在這個例子中,咱們須要完成一個如上圖的文本展現。它當前的字體是 24 號的 Comic Sans MS。給 don't 這部分文本設置粗體的 attribute 以後,咱們發現剩餘的文本(即 hate)丟失了本來的字體設置。這是由於初始化 AttributedString 時,沒有提供 attribute 設置參數,那麼系統便會使用默認的設置。在這個案例中,使用默認設置初始化了文本,而後對 don't 部分進行了單獨設置,天然 hate 部分就使用了默認的設置。

咱們有兩種方式來解決這件事。一種是避免將整個文本同時進行設置,而是對於 don‘t 設置粗體,對於 hate 設置 Comic Sans MS,但這樣比較繁瑣。因此另外一種是初始化 AttributedString 時,附帶原有字體的參數,而後對 don‘t 部分再行設置。

除了字體外,咱們還須要瞭解其餘屬性的默認值。

3.2 使用準確的屬性描述

  • 避免將所有或部分文本重置爲默認屬性的操做。
  • 在更新你的 app 以支持即將到來的黑暗模式時,確保在這個模式下你的文本顏色正確。對於 appkit 開發者,這是很是重要的。

這裏特別注意上圖標記出的 ParagraphStyle 屬性。一個反面案例是: 爲了截掉 hate 部分的文本,給這部分文本單獨設置了 ParagraphStyle 的屬性。然而展現的結果卻不符合預期。這是由於在 layout 以前,會進行 attribute fixing,這在前文有述。一個文本段落,卻有多個 ParagraphStyle 的屬性值,這是違反一致性的,因此係統在 fix attribute 時,會選擇第一個 ParagraphStyle 屬性,也就是默認風格,而且把它應用於整個段落。

3.3 性能表現:使用間斷的佈局

爲了理解它,回到咱們的老朋友 - 佈局過程。glyph 生成以後進行 glyph 佈局。對於大段文本,若是使用總體的佈局,那麼 LayoutManager 必須完成全部的 glyph 生成、佈局過程,這樣一來,若是有大段文本的話你就須要長時間地等待。

對於 NSTextView,你能夠經過設置 allowsNonContiguousLayout 屬性來支持間斷佈局。

對於 UITextView,它是默認開啓的。須要注意的是,UITextViewUIScrollView 的子類,allowsNonContiguousLayout 屬性要求 UITextViewScroll Enabled 屬性是開啓的。由於若是不支持滑動的話,間斷佈局也就失去了意義。

這就引出了一個重要的問題。使用間斷佈局時,避免一次請求整個文本的佈局。因此若是你只有一個 textcontainer 的話,避免一次請求完整的佈局。

3.4 安全性

這裏蘋果給出了一個形象的例子:開發者就像武裝的士兵,而 iOS、Mac OS 就像堅固的堡壘,士兵和堡壘共同組成了堅固的安全性防護工事。這就意味着,iOS 應用的安全性須要開發者和蘋果共同協做。

爲此,蘋果爲開發者提供了一條準則:

  • 爲文本輸入設置限制

全部的文本輸入都被認爲是潛在的風險。當你容許文本輸入時,你就開放了複製和粘貼,可是你並不能預知什麼文本會被粘貼在那裏。它多是一段普通的文本,但也有多是極其長的文本,而這將會致使你的 app 出現不可預知的問題。

如何完成對文本的輸入進行驗證呢?在 UIKit 下,使用 UITextFieldDelegate,在 AppKit 下經過 NSFormatter

值得期待的是,蘋果預告了關於安全性提高的內容即將到來

總結

最後,用一張圖來總結這個 session 的內容:

查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄

相關文章
相關標籤/搜索