TextKit框架詳細解析 (三) —— 一個簡單布局示例(一)

版本記錄

版本號 時間
V1.0 2018.08.30

前言

TextKit框架是對Core Text的封裝,用簡潔的調(diào)用方式實現(xiàn)了大部分Core Text的功能。 TextKit是一個偏上層的開發(fā)框架,在iOS7以上可用,使用它可以方便靈活處理復雜的文本布局,滿足開發(fā)中對文本布局的各種復雜需求。TextKit實際上是基于CoreText的一個上層框架,其是面向?qū)ο蟮摹=酉聛韼灼覀兙鸵黄鹂匆幌逻@個框架。感興趣的看下面幾篇文章。
1. TextKit框架詳細解析 (一) —— 基本概覽和應用場景(一)
2. TextKit框架詳細解析 (二) —— 基本概覽和應用場景(二)

開始

注意!注意!注意?。罕疚牡膶懽鞅尘氨容^老,是iOS7,現(xiàn)在已經(jīng)沒人使用了,但是之所以這樣還是放出來,就想說一下TextKit框架的體系和應用范圍,個別人可以忽略本文。

隨著Apple增加更多功能和特性,iOS呈現(xiàn)文本的方式在過去幾年中變得越來越強大。 iOS 7的發(fā)布帶來了一些最重要的文本呈現(xiàn)更改。 現(xiàn)在,iOS 8以后以這種能力為基礎(chǔ),使其更易于使用。

在iOS 6之前的過去,Web視圖通常是使用混合樣式呈現(xiàn)文本的最簡單方法,例如粗體,斜體或甚至帶有顏色。

2012年,iOS 6為許多UIKit控件添加了屬性字符串支持。 這使得在不使用渲染HTML的情況下實現(xiàn)這種類型的布局變得更加容易。

在iOS 6中,UIKit基于WebKitCore Graphics的字符串繪制函數(shù)控制其文本功能,如下面的分層圖所示:

注意:在這個圖中,有什么事情讓你覺得奇怪嗎? 沒錯 - UITextView使用WebKit。 iOS 6將文本視圖上的屬性字符串呈現(xiàn)為HTML,這一事實對于尚未深入深入框架的開發(fā)人員來說并不明顯。

iOS 6中的屬性字符串確實對許多用例有用。 但是,對于高級布局和多行渲染文本,Core Text仍然是唯一真正的選擇 - 相對靠近底層且繁瑣的框架。

但是,從iOS 7開始,有一種更簡單的方法。 憑借當前簡約的設計重點,避開多余裝飾并更多地關(guān)注排版 - 例如剝離所有邊框和陰影的UIButton,只留下文本 - iOS 7添加了一個用于處理文本和文本屬性的全新框架并不奇怪: Text Kit。

現(xiàn)在架構(gòu)更加整潔,所有基于文本的UIKit控件(除了UIWebView)現(xiàn)在都使用Text Kit,如下圖所示:

Text Kit構(gòu)建于Core Text之上,繼承了Core Text框架的全部功能,并且令所有開發(fā)人員高興,將其包含在改進的面向?qū)ο蟮腁PI中。

在這個Text Kit教程中,您將探索Text Kit的各種功能,因為您為iPhone創(chuàng)建了一個簡單但功能豐富的筆記記錄應用程序,該應用程序具動態(tài)文本大小調(diào)整和動態(tài)文本樣式。

下面我們打開建立的工程并運行,界面應該如下所示:

該應用程序創(chuàng)建一個初始的Note實例數(shù)組,并將它們呈現(xiàn)在table view控制器中。sb和segue在table view中檢測單元格選擇,并處理到視圖控制器的轉(zhuǎn)換,用戶可以在其中編輯選定的note。

瀏覽源代碼并稍微使用應用程序,以了解應用程序的結(jié)構(gòu)及其運行方式。 完成后,請轉(zhuǎn)到下一部分,其中討論了應用中動態(tài)類型的使用。


Dynamic Type - 動態(tài)類型

Dynamic Type是iOS 7中改變游戲規(guī)則最多的功能之一,它將選擇權(quán)放在您的應用程序上,以符合用戶選擇的字體大小和權(quán)重。

在iOS 7中,打開Settings應用并導航到General/AccessibilityGeneral/Text Size以查看影響應用顯示文本方式的設置:

在iOS 8中,打開Settings應用并導航到General/Accessibility/Larger Text以訪問Dynamic Type文本大小。

iOS 7提供了通過增加字體權(quán)重來增強文本易讀性的功能,以及為支持動態(tài)文本的應用程序設置首選字體大小的選項。

注意:當Apple在WWDC 2013上發(fā)布Text Kit時,他們強烈建議開發(fā)人員采用動態(tài)類型。在WWDC 2014上,蘋果公司走得更遠了。他們強調(diào)所有內(nèi)置應用都支持動態(tài)類型。此外,他們使得動態(tài)類型在iOS 8中更容易使用。WWDC 2014會議226表 What’s New in Tables and Collection Views,涵蓋了對table views和collections的iOS 8動態(tài)類型支持。建議你觀看它!用戶希望為iOS 7及更高版本編寫的應用程序能夠遵守這些設置。

為了使用動態(tài)類型,您需要使用styles指定字體,而不是明確說明字體名稱和大小。 iOS 7為UIFont添加了一個新方法preferredFontForTextStyle,它使用用戶的字體首選項為給定樣式創(chuàng)建字體。

下圖給出了六種不同字體樣式的示例:

左側(cè)的文本使用最小的用戶可選文本大小,中心的文本使用最大的文本,右側(cè)的文本顯示啟用可訪問性粗體文本功能的效果。


Basic Support - 基本支持

實現(xiàn)對動態(tài)文本的基本支持相對簡單。 而不是在應用程序中使用顯式字體,而是請求特定樣式的字體。 在運行時,應用程序根據(jù)給定的樣式和用戶的文本首選項選擇合適的字體。

使用iOS 8,Apple實現(xiàn)動態(tài)類型比在iOS 7中更容易。特別是,table views中的默認標簽自動支持動態(tài)類型! 盡管如此,您可能希望支持iOS 7,和/或您可能希望在table views中使用自定義標簽。 首先,您將學習如何處理iOS 7的動態(tài)類型。然后,您將了解Apple如何在iOS 8中讓您的生活更輕松。


Why iOS 7 is Great, but iOS 8 is Even Greater

初始化項目的deployment設置為iOS 8,在繼續(xù)之前,構(gòu)建并運行應用程序并嘗試將默認文本大小更改為各種值。您將發(fā)現(xiàn)table view列表中的文本大小和單元格高度都會相應更改。而且你不需要做任何事情!但是請注意,notes本身并不反映文本大小設置的更改。

盡管不是很精彩,但iOS 7中的內(nèi)容仍然相當不錯。對于本文的大部分內(nèi)容,如果您使用的是iOS 7或iOS 8(只是確保您使用的是Xcode 6!),現(xiàn)在,將應用程序的deployment level設置為iOS 7,然后在iOS模擬器中進行操作。以下大部分內(nèi)容在iOS 8中也有用,所以即使你不打算支持早于iOS 8的iOS版本,它也是值得的。

注意:要在Xcode 6中將部署級別設置為iOS 7,請選擇View/Navigators/Show Project Navigator。在右側(cè)面板中,選擇項目,單擊info并在iOS Deployment Target彈出菜單中選擇iOS 7。此外,在右側(cè)面板中選擇目標,并將部署目標設置為iOS 7。確保模擬器充當iOS 7設備也很重要。所以在iOS模擬器中選擇Hardware/Device/iOS 7/iPhone 5s。

現(xiàn)在您已經(jīng)準備好作為iOS 7應用程序運行,繼續(xù)構(gòu)建并運行。像以前一樣玩文字大小設置,你會發(fā)現(xiàn)遺憾的是,應用程序忽略了你的設置?,F(xiàn)在,您將做一些事情來使其在iOS 7中運行。

打開NoteEditorViewController.swift并將以下內(nèi)容添加到viewDidLoad的末尾:

textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

請注意,您沒有指定Helvetica Neue等確切字體。 相反,您要求使用UIFontTextStyleBody文本樣式常量為正文添加適當?shù)淖煮w。

接下來,打開NotesListViewController.swift并在返回調(diào)用之前將以下內(nèi)容添加到tableView(_:cellForRowAtIndexPath :)方法:

cell.textLabel?.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)

同樣,您指定了文本樣式,iOS將返回適當?shù)淖煮w。

使用字體名稱的語義方法(例如UIFontTextStyleSubHeadline)有助于避免代碼中的硬編碼字體名稱和樣式 - 并確保您的應用程序能夠按預期正確響應用戶定義的排版設置。

再次構(gòu)建并運行應用程序,您會注意到table view和note屏幕現(xiàn)在支持當前文本大??;兩者之間的差異顯示在下面的屏幕截圖中:

這看起來很不錯 - 但是敏銳的讀者會注意到這只是解決方案的一半。 返回General/Text Size下的Settings應用,然后再次修改文本大小。 這一次,切換回SwiftTextKitNotepad - 無需重新啟動應用程序 - 您會注意到您的應用程序沒有響應新的文本大小。


Responding to Updates - 響應更新

打開NoteEditorViewController.swift并將以下代碼添加到viewDidLoad的末尾

NSNotificationCenter.defaultCenter().addObserver(self, 
    selector: "preferredContentSizeChanged:", 
    name: UIContentSizeCategoryDidChangeNotification,
    object: nil)

上面的代碼注冊該類以在首選內(nèi)容大小更改時接收通知,并在發(fā)生此事件時傳入要調(diào)用的方法(preferredContentSizeChanged)。

接下來,將以下方法添加到類中:

func preferredContentSizeChanged(notification: NSNotification) {
  textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
}

這只是根據(jù)新的首選大小設置文本視圖字體。

注意:您可能想知道為什么看起來您將字體設置為之前的相同值。 當用戶更改其首選字體大小時,您必須再次請求首選字體;它不會自動更新。 更改字體首選項時,通過preferredFontForTextStyle返回的字體將有所不同。

打開NotesListViewController.swift并通過向類中添加以下代碼來覆蓋viewDidLoad函數(shù):

override func viewDidLoad() {
  super.viewDidLoad()
  NSNotificationCenter.defaultCenter().addObserver(self,
      selector: "preferredContentSizeChanged:", 
      name: UIContentSizeCategoryDidChangeNotification, 
      object: nil)
}

嘿,是不是你剛剛添加到NoteEditorViewController.swift的代碼? 是的,它是 - 但你會以稍微不同的方式處理首選的字體更改。

將以下方法添加到類中:

func preferredContentSizeChanged(notification: NSNotification) {
  tableView.reloadData()
}

上面的代碼只是指示table view重新加載其可見單元格,這會更新每個單元格的外觀。 這將觸發(fā)對preferredFontForTextStyle()的調(diào)用并刷新字體選擇。

構(gòu)建并運行您的應用程序;更改文本大小設置,并驗證您的應用是否正確響應新用戶首選項。


Changing Layout - 改變布局

這部分看起來效果很好,但是當你選擇一個非常小的字體大小時,你的table view看起來有點稀疏,如右下圖所示:

這是動態(tài)類型的棘手方面之一(在iOS 7中)。 為了確保您的應用程序在各種字體大小中看起來很好,您的布局需要響應用戶的文本設置。 自動布局為您解決了很多問題,但這是您必須自己解決的一個問題。

您的表行高度需要隨著字體大小的變化而變化。 實現(xiàn)tableView(_:heightForRowAtIndexPath :)委托方法很好地解決了這個問題。

將以下代碼添加到NotesListViewController.swift,在標記為Table view data source的部分中:

let label: UILabel = {
  let temporaryLabel = UILabel(frame: CGRect(x: 0, y: 0, width: Int.max, height: Int.max))
  temporaryLabel.text = "test"
  return temporaryLabel
}()

override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
  label.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
  label.sizeToFit()
  return label.frame.height * 1.7
}

上面的代碼創(chuàng)建了一個UILabel的共享實例,table view用它來計算單元格的高度。 然后,在tableView(_:heightForRowAtIndexPath :)中,將標簽的字體設置為表視圖單元格使用的相同字體。 然后它在標簽上調(diào)用sizeToFit,強制標簽的frame緊緊圍繞文本,并導致frame高度與表格行高度成比例。

構(gòu)建并運行您的應用程序;再次修改文本大小設置,表格行現(xiàn)在動態(tài)調(diào)整大小以適合文本大小,如下面的屏幕截圖所示:

如果您愿意,現(xiàn)在可以在本教程的其余部分將deployment重置為iOS 8。


Letterpress Effect - 凸版印刷效果

凸版印刷效果為文本添加了細微的陰影和高光,使其具有深度感 - 就像文字略微壓入屏幕一樣。

注意:術(shù)語“凸版印刷 - letterpress”是對早期印刷機的一種認可,它印有一組刻在塊上的字母并將它們壓入頁面。 這些字母經(jīng)常在頁面上留下一個小縮進 - 這是一種意想不到但視覺上令人愉悅的效果,這種效果在今天的數(shù)字排版中經(jīng)常被復制。

打開NotesListViewController.swift并使用以下實現(xiàn)替換tableView(_:cellForRowAtIndexPath :):

override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell? {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

  let note = notes[indexPath.row]
  let font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
  let textColor = UIColor(red: 0.175, green: 0.458, blue: 0.831, alpha: 1)
  let attributes = [
    NSForegroundColorAttributeName : textColor,
    NSFontAttributeName : font,
    NSTextEffectAttributeName : NSTextEffectLetterpressStyle
  ]
  let attributedString = NSAttributedString(string: note.title, attributes: attributes)

  cell.textLabel?.attributedText = attributedString

  return cell
}

上面的代碼使用凸版印刷樣式為表格單元格的標題創(chuàng)建一個屬性字符串。

構(gòu)建并運行您的應用程序;您的table view現(xiàn)在將顯示具有良好凸版效果的文本,如下所示:

Letterpress是一種微妙的效果 - 但這并不意味著你應該過度使用它! 視覺效果可能會使您的文字更有趣,但它們并不一定能讓您的文字更清晰。


Exclusion Paths - 路徑排除

圍繞圖像或其他對象的流動文本是大多數(shù)文字處理器的標準特征。 Text Kit允許您使用排除路徑- exclusion paths在復雜路徑和形狀周圍渲染文本。

告訴用戶note的創(chuàng)建日期是很方便的;您將在顯示此信息的note的右上角添加一個小的曲線視圖。

您將首先添加視圖本身 - 然后您將創(chuàng)建一個排除路徑以使文本環(huán)繞它。

1. Adding the View - 添加視圖

打開NoteEditorViewController.swift并將以下屬性聲明添加到類中:

var timeView: TimeIndicatorView!

顧名思義,這里有時間指示器子視圖。

接下來,將此代碼添加到viewDidLoad的最后:

timeView = TimeIndicatorView(date: note.timestamp)
textView.addSubview(timeView)

這只是創(chuàng)建新視圖的實例并將其添加為子視圖。

TimeIndicatorView計算自己的大小,但不會自動執(zhí)行此操作。 當視圖控制器布局子視圖時,您需要一種機制來調(diào)用updateSize。

最后,將以下兩個方法添加到類中:

override func viewDidLayoutSubviews() {
  updateTimeIndicatorFrame()
}

func updateTimeIndicatorFrame() {
  timeView.updateSize()
  timeView.frame = CGRectOffset(timeView.frame, textView.frame.width - timeView.frame.width, 0)
}

viewDidLayoutSubviews調(diào)用updateTimeIndicatorFrame,它執(zhí)行兩項操作:調(diào)用updateSize設置子視圖的大小,并將子視圖放在文本視圖的右上角。

剩下的就是當視圖控制器收到內(nèi)容大小已更改的通知時調(diào)用updateTimeIndicatorFrame。 將preferredContentSizeChanged的實現(xiàn)替換為以下內(nèi)容:

func preferredContentSizeChanged(notification: NSNotification) {
  textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
  updateTimeIndicatorFrame()
}

構(gòu)建并運行您的項目,點擊列表項,時間指示器視圖將顯示在項目視圖的右上角,如下所示:

修改設備文本大小首選項,視圖將自動調(diào)整為適合。

但是,有些事情看起來不太對勁。 note的文本在時間指示器視圖后面呈現(xiàn),而不是整齊地圍繞它流動。 幸運的是,這是排除路徑旨在解決的確切問題。

打開TimeIndicatorView.swift并查看curvePathWithOrigin()。 時間指示器視圖在填充背景時使用此代碼,但您也可以使用它來確定文本流動的路徑。 啊哈 - 這就是為什么貝塞爾曲線的計算被分解成自己的方法!

剩下的就是定義排除路徑本身。 打開NoteEditorViewController.swift并將以下代碼塊添加到updateTimeIndicatorFrame的最后:

let exclusionPath = timeView.curvePathWithOrigin(timeView.center)
textView.textContainer.exclusionPaths = [exclusionPath]

上面的代碼基于在時間指示器視圖中創(chuàng)建的Bezier路徑創(chuàng)建排除路徑,但是具有相對于文本視圖的原點和坐標。

構(gòu)建并運行項目并從列表中選擇一個項目;現(xiàn)在,文本在時間指示器視圖周圍很好地流動。

這個簡單的例子只是劃分了排除路徑的能力。 您可能會注意到exclusionPaths屬性需要一個路徑數(shù)組,這意味著每個容器都可以支持多個排除路徑。

此外,排除路徑可以根據(jù)需要簡單或復雜。 需要渲染星形或蝴蝶形的文字嗎? 只要您可以定義路徑,exclusionPaths就會毫無問題地處理它!

當文本容器在排除路徑更改時通知布局管理器時,您可以實現(xiàn)動態(tài)或甚至動畫排除路徑 - 只是不要指望您的用戶欣賞在他們嘗試閱讀時在屏幕上移動的文本!


Dynamic Text Formatting and Storage - 動態(tài)文本格式和存儲

您已經(jīng)看到Text Kit可以根據(jù)用戶的文本大小首選項動態(tài)調(diào)整字體。 但是,如果字體可以根據(jù)實際文本本身動態(tài)更新,那會不會很酷?

例如,如果您想自動創(chuàng)建此應用程序,該怎么辦:

  • 使用波形符(?)包圍的任何文本都是一個花哨的字體
  • 使用下劃線字符(_)斜體包圍任何文本
  • 將短劃線字符( - )包圍的任何文本劃掉
  • 將所有大寫字母的文字設為紅色

通過利用Text Kit框架的強大功能,這正是您在本節(jié)中所要做的!

為此,您需要了解Text Kit中的文本存儲系統(tǒng)的工作原理。 這是一個圖表,顯示用于存儲,呈現(xiàn)和顯示文本的“Text Kit stack”

在幕后,Apple會在您創(chuàng)建UITextView,UILabelUITextField時自動為您創(chuàng)建這些類。在您的應用中,您可以使用這些默認實現(xiàn),也可以自定義任何部分以獲得自己的行為。讓我們來看看每個類:

  • NSTextStorage將要呈現(xiàn)的文本存儲為屬性字符串,并通知布局管理器文本內(nèi)容的任何更改。您可能希望子類化NSTextStorage,以便在文本更新時動態(tài)更改文本屬性。
  • NSLayoutManager獲取存儲的文本并在屏幕上呈現(xiàn)它,它在您的應用中充當布局“引擎”。
  • NSTextContainer描述應用呈現(xiàn)文本的屏幕區(qū)域的幾何形狀。每個文本容器通常與UITextView相關(guān)聯(lián)。您可能希望子類化NSTextContainer以定義要在其中呈現(xiàn)文本的復雜形狀。

要在此應用程序中實現(xiàn)動態(tài)文本格式設置功能,您需要子類化NSTextStorage,以便在用戶在文本中鍵入時動態(tài)添加文本屬性。

一旦您創(chuàng)建了自定義NSTextStorage,您將用您自己的實現(xiàn)替換UITextView的默認文本存儲實例。讓我們試一試!


Subclassing NSTextStorage - 子類化NSTextStorage

右鍵單擊項目導航器中的SwiftTextKitNotepad組,選擇New File ...,然后選擇iOS / Source / Cocoa Touch Class并單擊Next。

將類命名為SyntaxHighlightTextStorage,使其成為NSTextStorage的子類,并確認語言設置為Swift。 單擊Next,然后單擊Create。

打開SyntaxHighlightTextStorage.swift并在類聲明中添加一個新屬性:

let backingStore = NSMutableAttributedString()

文本存儲子類必須提供自己的持久性,因此使用NSMutableAttributedString后備存儲 - 稍后將對此進行更多介紹。

接下來將以下內(nèi)容添加到類中:

override var string: String {
  return backingStore.string
}

override func attributesAtIndex(index: Int, effectiveRange range: NSRangePointer) -> [NSObject : AnyObject] {
  return backingStore.attributesAtIndex(index, effectiveRange: range)
}

這兩個聲明中的第一個覆蓋string計算屬性,推遲到后備存儲。 同樣,attributesAtIndex方法也委托給后備存儲。

最后將剩余的強制覆蓋添加到同一個文件中:

override func replaceCharactersInRange(range: NSRange, withString str: String) {
  println("replaceCharactersInRange:\(range) withString:\(str)")

  beginEditing()
  backingStore.replaceCharactersInRange(range, withString:str)
  edited(.EditedCharacters | .EditedAttributes, range: range, changeInLength: (str as NSString).length - range.length)
  endEditing()
}

override func setAttributes(attrs: [NSObject : AnyObject]!, range: NSRange) {
  println("setAttributes:\(attrs) range:\(range)")

  beginEditing()
  backingStore.setAttributes(attrs, range: range)
  edited(.EditedAttributes, range: range, changeInLength: 0)
  endEditing()
}

同樣,這些方法委托給后備存儲。 但是,它們還包含對beginEditing,editedendEditing的調(diào)用。 文本存儲類需要這三種方法,以便在進行編輯時通知其關(guān)聯(lián)的布局管理器。

您可能已經(jīng)注意到,為了子類化文本存儲,您需要編寫相當多的代碼。 由于NSTextStorage是類集群的公共接口,因此您不能僅將其子類化并覆蓋一些方法來擴展其功能。 相反,您必須自己實現(xiàn)某些要求,例如屬性字符串數(shù)據(jù)的后備存儲。

注意:類集群是Apple整個框架中常用的設計模式。類集群只是Abstract Factory模式的Objective-C實現(xiàn),它提供了一個通用接口,用于創(chuàng)建相關(guān)或依賴對象的族,而無需指定具體類。 NSArrayNSNumber等熟悉的類實際上是類集群的公共接口。

Apple使用類集群將私有具體子類封裝在公共抽象超類下,并且它是這個抽象超類,它聲明客戶端必須使用的方法才能創(chuàng)建其私有子類的實例??蛻舳艘餐耆恢拦S正在分配哪個私有類,因為它只與公共接口進行交互。

使用類集群肯定簡化了界面,使學習和使用類變得更加容易,但重要的是要注意在可擴展性和簡單性之間進行權(quán)衡。創(chuàng)建集群的抽象超類的自定義子類通常要困難得多。

現(xiàn)在你有了一個自定義的NSTextStorage,你需要創(chuàng)建一個使用它的UITextView

后記

本篇主要講述了一個簡單布局示例,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容