WWDC 2018 Session 221: TextKit Best Practicesgit
文本內容在 app 內隨處可見,展現文本的方式也是多種多樣。關注過性能提高的同窗會發現,文本控件的高效使用對於整個頁面性能的提高相當重要。爲此,蘋果和開發者都在不斷努力。好比蘋果日漸完善的文本框架,以及第三方文本框架的表明 YYText。swift
這個 session 旨在指導開發者如何正確地使用 TextKit
進行文本內容的展現,按部就班分爲三個部分:緩存
和平時使用的框架有些不一樣,咱們不須要使用 import
關鍵字來導入 TextKit
。包含 UILabel
、UITextField
等控件的 UIKit
框架(用於 iOS),以及包含 NSTextView
等控件的 AppKit
框架(用於 Mac OS),都是基於 TextKit
構建。在使用上面的文本控件時,其實就是在使用 TextKit
,它協同 Core Text
、Core Graphics
以及 Foundation
,一塊兒爲咱們的 app 提供強大的文本展現能力。安全
利用 TextKit
的能力,你能夠很是容易地展現下面風格各異的文本。markdown
對於不一樣類型的文本,咱們須要選擇合適的文本控件。那麼該如何決定呢?蘋果爲咱們提供了比較明確的指導,以下圖所示。在使用 UIKit
和 AppKit
時,情形會稍有不一樣,因此分開進行描述。session
UIKit
的選擇路徑:AppKit
的選擇路徑:圖中的描述很是清晰易讀。須要注意的是,UILabel
用來展現較少的文本內容或者較少的行數,然而,在 AppKit
框架下是沒有 Label
控件的,這時能夠選擇 NSTextField
控件,經過禁用文本編輯屬性,來得到和 UILabel
同樣的特性。架構
有的時候,你們可能爲了得到更優的性能(避免生成過多的視圖對象實例),經過調用以下方法來使用文本繪製:app
func draw(at: CGPoint) func draw(in: CGRect) func draw(with: CGRect, options: NSStringDrawingOptions = [], context: NSStringDrawingContext?) 複製代碼
然而,蘋果並不推薦常常這樣使用。若是你依然須要使用的話,蘋果也貼心地給出了一些建議:框架
draw
方法的頻率(儘可能減小調用次數)爲何這種使用方式不被推薦呢?首先是由於 UILabel
、UITextView
等控件提供了良好的緩存機制,因此在合適的時候選擇這些控件,反而能夠得到更好的性能(相較 string drawing 而言),特別是在使用自動佈局的時候。
繪製 attributed string
時,若是過多地調用 draw
方法,會明顯地下降性能。由於系統在每次繪製以前須要釋放以前全部的 attribute
對象。所以,對於額外的 attribute
,請儘可能在肯定它們的視覺效果(例如字體、顏色)時才進行繪製。
最後,蘋果仍是不忘強調,若是使用了 string drawing,就會失去下圖所示的文本控件提供的全部特性。所以,請儘量地使用文本控件。
TextKit
的架構組成像 Cocoa
下的許多組件同樣,TextKit
也是基於 「model - view - controller」 設計結構的。而且這三層又各自包含 storage、layout、和 display 模塊:
Storage
深刻了解一下各個部分的組成,首先是與 Model
層通訊的 Storage
模塊,它包含的 NSTextStorage
持有字符串的數據和屬性信息。值得注意的是,它是MutableAttributedString
的子類,所以使用方式和咱們熟知的 AttributedString
一致。而 NSTextContainer
則負責模型化文本佈局的地理位置、區域信息。
Display
接下來是 Display
模塊,它和 View
層通訊。這個模塊咱們一般關注的是文本控件的正確選擇問題。
Layout
最後是 Layout
模塊,它和 Controller
層進行通訊。 NSLayoutManager
是這個模塊惟一的組成部分。它的強大讓蘋果用「野獸」來形容。它是整個展現過程的「大腦」,控制本身的佈局過程。
這是文本佈局過程的概覽圖:
文本佈局發生在 TextStorage
進行屬性修正以後。對於這個過程當中的工做,舉個例子,確保這段文本所選擇的字體支持顯示文本中的全部字符,若是發現不支持的字符,則進行相應替換。好比上圖中的 Tempura (天麩羅) is a tasty Japanese food. 🍤
這段文本,字體指定了 Times New Roman
。然而,這個字體是不支持日語字符和 emoji 字符的。所以,在屬性修正過程當中,日語字符被指定了支持日語的 Hiragino Mincho ProN
字體,而 emoji 字符則被指定了 Apple Color Emoji
字體。
glyph
和 character
屬性修正完成後,佈局過程就開始了。這裏對上述概念的含義作一些說明。
character
中文譯爲「字符」,字符是能夠轉換爲二進制存儲的通用數據,而 glyph
能夠譯爲字符的視覺表示符號。同一個 character
呈如今屏幕上,能夠表現爲不一樣的字體、視覺風格。而這些各異的視覺風格,就是由 glyph
來負責呈現,glyph
的生成,就是爲指定了視覺效果(如字體)的字符肯定展現所需的 glyph
的過程。下圖是一個示例:
能夠看到,character
和 glyph
的對應關係不老是一對一的。圖中的字符串 「ffi」 由三個字符組成,但整個字符串能夠由一個 glyph
表示。再看下圖的例子,一個單獨的字符 「n」,也能夠由兩個 glyph
來表示。
關於這部分概念,提供一篇參考資料:iOS 排版概念
再回到佈局過程的圖示中來,glyph
佈局,就是 NSLayoutManager
在視圖上擺放 glyph
的過程。
以下圖是 一個完整 TextKit
組件的標準配置結構:Text Container
持有 Text View
的弱引用,而 Text View
經過根 Text Storage
持有整個佈局樹結構。
若是有多個文本頁面或者文本行須要佈局,可使用成對的 Text Container
和 Text View
組合,每一對組合對應一個頁面或者一行。在這種狀況下,咱們能夠 hook 一樣的 container 和 text view 來共享佈局信息。
文本內容被添加以後,它鋪滿由第一個 text container 定義的區域。文本在 text view 上和 text container 成對展現。 當沒有剩餘空間時,新的 container 連同 text view 一塊兒被添加,而且文本在第二個頁面或者文本行進行展現。
多個 layout manager 容許你對一樣的文本有多種不一樣的顯示效果。這個文本在不一樣的視圖上能夠有彼此不一樣且獨立的佈局和分組,下圖是這種模式下的結構示意和效果示意。方框內的文本內容相同,但展現效果是不一樣的。
就像錘子在工具箱中的重要地位同樣,咱們在開發時也有一些地位等同於錘子的工具。
代理
就像基本的錘子,大多數時候,它能夠很好地完成工做。通知
也是一個有效的工具。子類化
一樣是一把利器。它幾乎能夠做任何事。對於這些方式的使用場景,在第二部分會運用具體例子進行闡述。
文本組件在 app 中是無處不在的。在這部分,蘋果使用了 iOS 的 Apple News
和 Mac OS 的TextEdit
、Our Journal
三個 app 中的具體頁面做爲示例來對前面所述的核心理論進行講解。
這部份內容比較簡單。主要用來示意 Choosing the right control
這條理論。裏面主要的知識點以下:
UILabel
進行展現,也能夠藉助 NSAttributedString
來實現。UITextView
是 UIScrollView
的子類,默認支持滑動,若是想讓它與自動佈局良好協做地話,須要禁用滑動。這部分主要用來示意 Choosing the right configuration
這條理論。
TextEdit
這個 app 支持富文本的展現、編輯,文本編輯部分的特性很像一個 textview,天然,它符合前面講述的標準配置結構。值得注意的是,文本編輯部分支持分頁展現,能夠看到頁面下滑時,textcontainer 被從新設置了尺寸,文本從第一頁跳到了第二頁。很天然,這是使用了多個 textcontainer 的 textview,可是依然由同一個 textstorge、layoutmanager 管理,他們容許文本自由地從一個 textcontainer 跳到另外一個。下圖便是它的配置結構圖:
這部分主要用來示意 Choosing the right customization approach
這條理論。
從圖中能夠看到,在界面底部添加了一個 TextField 來顯示鍵入文本的數量。app 運行時,咱們但願底部的文本計數隨着鍵入的數量變化。爲了實現這個效果,咱們選擇一個比較「輕巧」的工具 - 通知。經過接收 NSTextStorage 發出的通知,能夠從 NSTextStorage 得到文本的數量。收到通知後,更新計數 TextField 中的數字。
當咱們想強調一部分文字時,可使用鍵盤快捷鍵或者菜單設置這部分字體爲粗體。可是若是想支持例如 markdown
的標記語言,經過特定字符來指定特殊的格式,好比在文本先後加入一對雙星號來使文本變化爲粗體,該如何實現呢?在這個情景中,須要獲取文本改變的時機和位置,通知機制並不便於提供足夠的信息。因此此次使用「一記重錘」 - 代理。遵照 NSTextStorageDelegate
協議,實現 textStorage(_:didProcessEditing:range:changeInLength:)
方法。在方法的實現中定義一個粗體字的 attribute ,添加給應該被粗體化的文本。這樣一來,只要輸入了一對雙星號,就能夠立馬使文本變爲粗體。
粗體標記完美實現了。那麼如何展現一個代碼片斷呢?像圖中所示,完成鍵入最後一個點符號,就能夠生成一個代碼塊文本,同時還會被標示爲 Swift
代碼。對於這樣一個複雜的情形,咱們須要兩把工具:
NSTextStorage
子類繼承 NSTextStorage
,實現四個強制實現的方法,特別是 replaceCharacters(in:with:)
方法。內部實現是將 NSTextBlock
賦值給 ParagraphStyle
而後把這個 ParagraphStyle
做爲一個 attribute
添加到一個 NSTextStorage
中,注意對應的範圍是代碼塊文本。
對於上面所述的 NSTextBlock
,須要瞭解的是 NSTextBlock
不會去定製化繪製它本身,因此咱們須要一個它的子類去完成這件事: CodeBlock
類繼承自 NSTextBlock
,在它的初始化方法中設置背景的襯墊,或者經過覆寫 drawBackground
方法,使用 StringDrawing 去繪製 「Swift Code」 這個標題。
這樣一來這個文本塊看起來就像一個代碼塊了。再回到繼承自 TextStorage
的 CustomTextStorage
,咱們能夠把 TextBlocks
屬性賦值爲剛剛添加的 CodeBlock
。
最後,咱們須要讓 textview 使用全新的CustomTextStorage
,因此咱們爲 LayoutManager
替換 storage。
這樣一來,基本完成了一個支持 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
的很好的例子。
在這部分中,蘋果給出了幾個指導性原則。
在這個例子中,咱們須要完成一個如上圖的文本展現。它當前的字體是 24 號的 Comic Sans MS
。給 don't
這部分文本設置粗體的 attribute 以後,咱們發現剩餘的文本(即 hate
)丟失了本來的字體設置。這是由於初始化 AttributedString
時,沒有提供 attribute 設置參數,那麼系統便會使用默認的設置。在這個案例中,使用默認設置初始化了文本,而後對 don't
部分進行了單獨設置,天然 hate
部分就使用了默認的設置。
咱們有兩種方式來解決這件事。一種是避免將整個文本同時進行設置,而是對於 don‘t
設置粗體,對於 hate
設置 Comic Sans MS
,但這樣比較繁瑣。因此另外一種是初始化 AttributedString 時,附帶原有字體的參數,而後對 don‘t
部分再行設置。
除了字體外,咱們還須要瞭解其餘屬性的默認值。
這裏特別注意上圖標記出的 ParagraphStyle
屬性。一個反面案例是: 爲了截掉 hate
部分的文本,給這部分文本單獨設置了 ParagraphStyle
的屬性。然而展現的結果卻不符合預期。這是由於在 layout 以前,會進行 attribute fixing,這在前文有述。一個文本段落,卻有多個 ParagraphStyle
的屬性值,這是違反一致性的,因此係統在 fix attribute 時,會選擇第一個 ParagraphStyle 屬性,也就是默認風格,而且把它應用於整個段落。
爲了理解它,回到咱們的老朋友 - 佈局過程。glyph 生成以後進行 glyph 佈局。對於大段文本,若是使用總體的佈局,那麼 LayoutManager 必須完成全部的 glyph 生成、佈局過程,這樣一來,若是有大段文本的話你就須要長時間地等待。
對於 NSTextView
,你能夠經過設置 allowsNonContiguousLayout
屬性來支持間斷佈局。
對於 UITextView
,它是默認開啓的。須要注意的是,UITextView
是UIScrollView
的子類,allowsNonContiguousLayout
屬性要求 UITextView
的 Scroll Enabled
屬性是開啓的。由於若是不支持滑動的話,間斷佈局也就失去了意義。
這就引出了一個重要的問題。使用間斷佈局時,避免一次請求整個文本的佈局。因此若是你只有一個 textcontainer 的話,避免一次請求完整的佈局。
這裏蘋果給出了一個形象的例子:開發者就像武裝的士兵,而 iOS、Mac OS 就像堅固的堡壘,士兵和堡壘共同組成了堅固的安全性防護工事。這就意味着,iOS 應用的安全性須要開發者和蘋果共同協做。
爲此,蘋果爲開發者提供了一條準則:
全部的文本輸入都被認爲是潛在的風險。當你容許文本輸入時,你就開放了複製和粘貼,可是你並不能預知什麼文本會被粘貼在那裏。它多是一段普通的文本,但也有多是極其長的文本,而這將會致使你的 app 出現不可預知的問題。
如何完成對文本的輸入進行驗證呢?在 UIKit
下,使用 UITextFieldDelegate
,在 AppKit
下經過 NSFormatter
。
值得期待的是,蘋果預告了關於安全性提高的內容即將到來。
最後,用一張圖來總結這個 session 的內容:
查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄