天天看點

WWDC 2018:TextKit 最佳實踐

WWDC 2018 Session 221: TextKit Best Practices

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

引言

文本内容在 app 内随處可見,展示文本的方式也是多種多樣。關注過性能提升的同學會發現,文本控件的高效使用對于整個頁面性能的提升至關重要。為此,蘋果和開發者都在不斷努力。比如蘋果日漸完善的文本架構,以及第三方文本架構的代表 YYText。

這個 session 旨在指導開發者如何正确地使用

TextKit

進行文本内容的展示,循序漸進分為三個部分:

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

一、核心理論

1.1 什麼是 TextKit ?

和平時使用的架構有些不同,我們不需要使用

import

關鍵字來導入

TextKit

。包含

UILabel

UITextField

等控件的

UIKit

架構(用于 iOS),以及包含

NSTextView

等控件的

AppKit

架構(用于 Mac OS),都是基于

TextKit

建構。在使用上面的文本控件時,其實就是在使用

TextKit

,它協同

Core Text

Core Graphics

以及

Foundation

,一起為我們的 app 提供強大的文本展示能力。

利用

TextKit

的能力,你可以非常容易地展示下面風格各異的文本。

1.2 選擇正确的控件

對于不同類型的文本,我們需要選擇合适的文本控件。那麼該如何決定呢?蘋果為我們提供了比較明确的指導,如下圖所示。在使用

UIKit

AppKit

時,情形會稍有不同,是以分開進行描述。

  • UIKit

    的選擇路徑:
  • AppKit

    的選擇路徑:

圖中的描述非常清晰易讀。需要注意的是,

UILabel

用來展示較少的文本内容或者較少的行數,然而,在

AppKit

架構下是沒有

Label

控件的,這時可以選擇

NSTextField

控件,通過禁用文本編輯屬性,來獲得和

UILabel

一樣的特性。

1.2.1 文本繪制(string drawing)的正确使用

有的時候,大家可能為了獲得更優的性能(避免生成過多的視圖對象執行個體),通過調用如下方法來使用文本繪制:

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,就會失去下圖所示的文本控件提供的所有特性。是以,請盡可能地使用文本控件。

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

字型。

  • glyph

    character

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

character

中文譯為“字元”,字元是可以轉換為二進制存儲的通用資料,而

glyph

可以譯為字元的視覺表示符号。同一個

character

呈現在螢幕上,可以表現為不同的字型、視覺風格。而這些各異的視覺風格,就是由

glyph

來負責呈現,

glyph

的生成,就是為指定了視覺效果(如字型)的字元确定展示所需的

glyph

的過程。下圖是一個示例:

可以看到,

character

glyph

的對應關系不總是一對一的。圖中的字元串 “ffi” 由三個字元組成,但整個字元串可以由一個

glyph

表示。再看下圖的例子,一個單獨的字元 “n”,也可以由兩個

glyph

來表示。

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

再回到布局過程的圖示中來,

glyph

布局,就是

NSLayoutManager

在視圖上擺放

glyph

的過程。

1.4 選擇正确的配置

如下圖是 一個完整

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 允許你對同樣的文本有多種不同的顯示效果。這個文本在不同的視圖上可以有彼此不同且獨立的布局和分組,下圖是這種模式下的結構示意和效果示意。方框内的文本内容相同,但展示效果是不同的。

1.5 選擇正确的定制實作方式

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

  • 代理

    就像基本的錘子,大多數時候,它可以很好地完成工作。
  • 通知

    也是一個有效的工具。
  • 最後,

    子類化

    同樣是一把利器。它幾乎可以作任何事。

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

二、具體示例

文本元件在 app 中是無處不在的。在這部分,蘋果使用了 iOS 的

Apple News

和 Mac OS 的

TextEdit

Our Journal

三個 app 中的具體頁面作為示例來對前面所述的核心理論進行講解。

2.1 Apple News on iOS

這部分内容比較簡單。主要用來示意

Choosing the right control

這條理論。裡面主要的知識點如下:

  • 對于一行顔色不一樣的文本,可以使用兩個

    UILabel

    進行展示,也可以借助

    NSAttributedString

    來實作。
  • UITextView

    UIScrollView

    的子類,預設支援滑動,如果想讓它與自動布局良好協作地話,需要禁用滑動。

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” 這個标題。

這樣一來這個文本塊看起來就像一個代碼塊了。再回到繼承自

TextStorage

CustomTextStorage

,我們可以把

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.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

,它是預設開啟的。需要注意的是,

UITextView

UIScrollView

的子類,

allowsNonContiguousLayout

屬性要求

UITextView

Scroll Enabled

屬性是開啟的。因為如果不支援滑動的話,間斷布局也就失去了意義。

這就引出了一個重要的問題。使用間斷布局時,避免一次請求整個文本的布局。是以如果你隻有一個 textcontainer 的話,避免一次請求完整的布局。

3.4 安全性

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

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

  • 為文本輸入設定限制

所有的文本輸入都被認為是潛在的風險。當你允許文本輸入時,你就開放了複制和粘貼,但是你并不能預知什麼文本會被粘貼在那裡。它可能是一段普通的文本,但也有可能是極其長的文本,而這将會導緻你的 app 出現不可預知的問題。

如何完成對文本的輸入進行驗證呢?在

UIKit

下,使用

UITextFieldDelegate

,在

AppKit

下通過

NSFormatter

值得期待的是,蘋果預告了關于安全性提升的内容即将到來。

總結

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

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