鍍金池/ 教程/ iOS/ 字符串渲染
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

字符串渲染

在本期中我們已經(jīng)討論了很多關(guān)于字符串不同的話題,從編碼到本地化再到語法分析。但多數(shù)情況下,字符串最終還是需要被繪制到屏幕上供用戶查看、交互。這篇文章涵蓋了最基本、最好的練習(xí),以及在用戶界面上呈現(xiàn)字符串可能遇到的常見陷阱。

如何將字符串繪制到屏幕上

簡(jiǎn)單起見,我們先看看 UIKit 在字符串渲染方面為我們提供了哪些控件。之后我們將討論一下對(duì)于字符串的渲染,iOS 和 OS X 系統(tǒng)中有哪些相似和不同。

UIKit 提供了很多可以在屏幕上顯示和編輯文本的類。每一個(gè)類都是為特定使用情況準(zhǔn)備的,所以為了避免不必要的問題,為你手上的任務(wù)挑選正確的工具是非常重要的。

UILabel

UILabel 是將文本繪制到屏幕上最簡(jiǎn)單的方式。它是 UIView 的一個(gè)子類,用來顯示少量的只讀文本。文本可以被展示在一行或多行,如果文本不能適應(yīng)指定的空間我們還可以使用不同的方式裁剪。盡管 label 使用的方式很簡(jiǎn)單,但是這里有幾個(gè)技巧還是值得提一提的。

label 默認(rèn)只顯示一行,但是你可以將 numberOfLines 屬性設(shè)為其他值來改變這一行為。將它設(shè)置為一個(gè)大于 1 的值,文本的行數(shù)將會(huì)被限制為這個(gè)指定的值;如果設(shè)置為 0,則是告訴 label 不管文本占多少行都顯示出來。

通過設(shè)置 text 屬性,Label 可以顯示簡(jiǎn)單的純文本,而設(shè)置 attributedText 屬性則可以讓 label 顯示富文本。當(dāng)使用純文本的時(shí)候,你可以使用 label 的 font,textColor,textAlignment,shadowColorshadowOffset 屬性改變它的外觀,如果你希望改變程序內(nèi)所有 Label 的風(fēng)格,你也可以使用 [UILabel appearance] 這個(gè)方法來進(jìn)行全局的更改。

Attributed strings 提供了更加靈活的格式,字符串的不同部分可以使用不同的格式。讓我們看看常見布局部分,下面給出 attributed strings 一些示例。(下文“常見布局”那一節(jié)給出了具體的關(guān)于 Attributed String 的一些例子。)

除了通過上文提到的那些屬性來調(diào)整 UILabel 的顯示風(fēng)格外,你還可以通過設(shè)置 UILabeladjustsFontSizeToWidthminimumScaleFactor,adjustsLetterSpacingToFitWidth 這 3 個(gè) BOOL 值的屬性讓 UILabel 根據(jù)所顯示的文本的內(nèi)容自動(dòng)地進(jìn)行調(diào)整。如果你非常在意用戶界面的美觀,那么你就不要開啟這些屬性,因?yàn)檫@會(huì)使文字的顯示效果變得不那么美觀,但是有的時(shí)候,比如在進(jìn)行程序不同語言本土化的時(shí)候,你會(huì)遇到一些很棘手的問題,除了使用這些選項(xiàng)外很難找到別的解決辦法。不信的話,你可以打開 iPhone,在設(shè)置中把系統(tǒng)語言改為德語,然后你就會(huì)發(fā)現(xiàn)蘋果官方出品的程序里到處都是被壓扁變了形的丑陋不堪的文本。這種處理方法并不完美,但有時(shí)卻很有用。

如果你使用這些選項(xiàng)讓 UIKit 壓縮你的文本以適配,如果壓縮的時(shí)候想讓文本保持在同一條基線上或需要對(duì)齊到左上角,那么你可以定義 baselineAdjustment 屬性。然而,這個(gè)選項(xiàng)只對(duì)單行 label 起作用。

當(dāng)你使用上述方法讓文本自動(dòng)縮放以適配 UILabel 時(shí),你可以使用 baselineAdjustment 這個(gè)屬性來調(diào)整縮放時(shí)文本是水平對(duì)齊還是對(duì)齊到 Label 的左上角。注意,這個(gè)屬性僅在單行的 Lable (即 numberOfLines 屬性值為1時(shí))中生效。

UITextField

像 label 一樣,text fields 可以處理純文本或帶屬性的文本。但 label 只能顯示文本而已,text field 還可以處理用戶的輸入。然而 text field 只限于單行文本。UITextFieldUIControl 的一個(gè)子類,它會(huì)掛鉤 (hook into)到響應(yīng)鏈,并且當(dāng)用戶開始或結(jié)束編輯時(shí)分發(fā)(deliver)這些行為消息,如果想要得到更多的控制權(quán),你可以實(shí)現(xiàn) text field 的代理。

Text field 有一系列控制文本輸入行為的選項(xiàng)。UITextField 實(shí)現(xiàn)了 UITextInputTraits 協(xié)議,這個(gè)協(xié)議需要你指定鍵盤外觀和操作的各種細(xì)節(jié),比如,需要顯示哪種鍵盤,返回按鈕的響應(yīng)事件是什么。

當(dāng)沒有文本輸入的時(shí)候 Text field 還可以顯示一個(gè)占位符,在右手邊顯示一個(gè)標(biāo)準(zhǔn)的清除按鈕,控制任意左右兩個(gè)輔助視圖。你還可以為其設(shè)置一個(gè)背景圖片,這樣我們就可以用一個(gè)可變大小的圖片為 text field 自定義邊框風(fēng)格了。

但每當(dāng)你需要輸入多行文本的時(shí)候,你就需要使用到 UITextField 的大哥了...

UITextView

Text view 是顯示或編輯大量文本的理想選擇。 UITextViewUIScrollView 的一個(gè)子類,所以它能允許用戶前后滾動(dòng)達(dá)到處理溢出文本的目的。和 text field 一樣, text view 也能處理純文本和帶屬性的文本。Text view 也實(shí)現(xiàn)了 UITextInputTraits 協(xié)議來控制鍵盤的行為和外觀。

text view 除了處理多行文本的能力外,它最大的賣點(diǎn)就是你可以使用、定制整個(gè) Text Kit 堆棧。你可以為 layout manager、text containertext storage 自定義行為或者替換為你自定義的子類。你可以看看 Max 的這篇 Text Kit 方面的文章。

不幸的是,UITextView 在 iOS 7 中還有些問題,目前還是 1.0 版本。它是基于 OS X Text Kit 從頭開始重新實(shí)現(xiàn)的。在 iOS 7 之前,它是基于 Webkit 的,并且功能很少。我們可以看看 PeterBrent 關(guān)于這方面的文章。

Mac中又是什么情況呢?

現(xiàn)在我們已經(jīng)討論過了 UIKit 中基本的 text 類,下面繼續(xù)解釋一下這些類在 AppKit 中結(jié)構(gòu)的不同之處。

首先,AppKit 中并沒有類似 UILabel 的控件。而顯示文本最基本的類是 NSTextField。我們將 text field 設(shè)為不可編輯、不可選擇,這樣便等同于 iOS 中的 UILabel 了。雖然 NSTextField 聽起來類似于 UITextField,但 NSTextField 并不限制于單行文本。

NSTextView,換句話說,就是等同于 UITextView,它也為我們揭露了整個(gè) Cocoa Text System 堆棧。但它還包含了很多額外的功能。很大的原因是因?yàn)?Mac 是一個(gè)具有指針設(shè)備(鼠標(biāo))的電腦。最值得注意的是包含了設(shè)置、編輯制表符的標(biāo)尺。

Core Text

上面我們討論的所有類最終都使用 Core Text 布局、繪制真實(shí)的符號(hào)。Core Text 是一個(gè)非常強(qiáng)大的 framework ,它已經(jīng)超出我們這篇文章討論的范圍。但是如果你曾經(jīng)需要通過完全自定義的方式繪制文本(例如,貝塞爾曲線),那你需要詳細(xì)的了解一下。

Core Text 在任何繪圖方面都為你提供了充分的靈活性。然而,Core Text 非常難于操作。它是一個(gè)復(fù)雜的 Core Foundation / C API。Core Text 在排版方面給了你充分的訪問權(quán)限。

在 Table View 中顯示動(dòng)態(tài)文本

可能和所有人都打過交道的字符串繪制就是最常見的可變高度的 table view cells。你能在社交媒體應(yīng)用中見到這種。 table view 的 delegate 有一個(gè)方法:tableView:heightForRowAtIndexPath:,這便是用來計(jì)算高度的。iOS 7之前,很難通過一種可靠的方式使用它。

在我們的示例中,我們將會(huì)在 table view 中顯示一列語錄:

http://wiki.jikexueyuan.com/project/objc/images/9-2.png" alt="" />

首先,為了實(shí)現(xiàn)完全的自定義,我們創(chuàng)建一個(gè) UITableViewCell 的子類。在這個(gè)子類中,我們需要親自為我們的 label 布局:

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.textLabel.frame = CGRectInset(self.bounds, 
                                       MyTableViewCellInset,
                                       MyTableViewCellInset);
}

MyTableViewCellInset 被定義為一個(gè)常量,所以我們可以將它用在 table view 的 delegate 的高度計(jì)算中。最簡(jiǎn)單、準(zhǔn)確計(jì)算高度的方法是將字符串轉(zhuǎn)換成帶屬性的字符串,然后計(jì)算出帶屬性字符串的高度。我們使用 table view 的寬度減去兩倍的 MyTableViewCellInset 常量(前面和后面的空間)。為了計(jì)算真實(shí)的高度,我們需要使用 boundingRectWithSize:options:context: 這個(gè)方法。

第一個(gè)參數(shù)是限制 text 大小的。我們只需要關(guān)心寬度的限制,因此我們?yōu)楦叨葌饕粋€(gè)最大值常量 CGFLOAT_MAX。第二個(gè)參數(shù)是非常重要的:如果你傳一個(gè)其他值,bounding rect 無疑會(huì)出錯(cuò)。如果你想要調(diào)整字體縮放或進(jìn)行追蹤,你可以使用第三個(gè)參數(shù)。最終,一旦我們得到 boundingRect,我們需要再次加上 inset:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CGFloat labelWidth = self.tableView.bounds.size.width - MyTableViewCellInset*2;
    NSAttributedString *text = [self attributedBodyTextAtIndexPath:indexPath];
    NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin |
                                     NSStringDrawingUsesFontLeading;
    CGRect boundingRect = [text boundingRectWithSize:CGSizeMake(labelWidth, CGFLOAT_MAX)
                                             options:options
                                             context:nil];

    return (CGFloat) (ceil(boundingRect.size.height) + MyTableViewCellInset*2);    
}

對(duì)于 bounding rect 的結(jié)果還有兩件敏感的事情,除非你讀了文檔,不然這兩件事你不一定會(huì)知道:返回值 size 是小數(shù),文檔中讓我們使用 ceil 將結(jié)果四舍五入。最終的結(jié)果可能是會(huì)比實(shí)際的大一點(diǎn)。

請(qǐng)注意,因?yàn)槲覀兊?text 是純文本,我們創(chuàng)建的 attributedBodyTextAtIndexPath: 方法也會(huì)在 tableView:cellForRowAtIndexPath: 中用到。這樣,我們需要確保他們保持同步。

還有,通過閱讀文檔(如下截圖),我們發(fā)現(xiàn) iOS 7 發(fā)布后,很多方法都被棄用了。如果你通過查找網(wǎng)頁或 StackOverflow,你會(huì)發(fā)現(xiàn)很多測(cè)量字符高度的變通方法。因?yàn)樘O果對(duì)文本框架進(jìn)行了重大檢修(在內(nèi)部實(shí)現(xiàn)中,所有的東西都使用 TextKit 進(jìn)行繪制了,而不是 WebKit),所以請(qǐng)使用新方法。

http://wiki.jikexueyuan.com/project/objc/images/9-3.png" alt="" />

另一個(gè)動(dòng)態(tài)調(diào)整 table view cell 大小的選擇就是使用 Auto Layout,你可以在這篇博文中找到更詳細(xì)的說明。然后你可以利用 contained lables 的 intrinsicContentSize。然而,現(xiàn)在自動(dòng)布局比手動(dòng)計(jì)算要慢很多??墒菍?duì)于原型開發(fā),這很完美:它允許你快速調(diào)整 constraints 并且移動(dòng)事物(特別當(dāng)你 cell 中不止一個(gè)控件時(shí)這顯得特別重要)。一旦你完成產(chǎn)品的設(shè)計(jì)迭代,然后你就可以用手動(dòng)布局的方式重新編寫代碼。

使用 Text Kit 和 NSAttributedString 進(jìn)行布局

使用 Text Kit,你將會(huì)擁有令人驚訝的靈活性來創(chuàng)建專業(yè)級(jí)別的文本布局。隨著這些靈活性帶來的是如何組合為數(shù)眾多的選項(xiàng)來完成復(fù)雜的布局。

我們準(zhǔn)備給出幾個(gè)示例并強(qiáng)調(diào)一些常見的布局問題,同時(shí)給出解決方案。

經(jīng)典的文本

首先,讓我們看一些經(jīng)典的文本。我們將會(huì)使用 Jacomy-Régnier 的 Histoire des nombres et de la numération mécanique,并設(shè)為 Bodoni 字體。最終截屏效果如下所示:

http://wiki.jikexueyuan.com/project/objc/images/9-4.png" alt="" />

這些都是由 Text Kit 完成的。兩段文字之間的裝飾也是文本,使用的是 Bodoni Ornaments 字體。

我們?yōu)槲捏w風(fēng)格使用調(diào)整好的 text。第一段從最左邊開始,接下來的段落都會(huì)插入空格.

這有三種不同的風(fēng)格:文體風(fēng)格,首行縮進(jìn)的變化文體風(fēng)格,裝飾物風(fēng)格。

讓我們先設(shè)置 body1stAttributes

CGFloat const fontSize = 15;

NSMutableDictionary *body1stAttributes = [NSMutableDictionary dictionary];
body1stAttributes[NSFontAttributeName] = [UIFont fontWithName:@"BodoniSvtyTwoITCTT-Book" 
                                                         size:fontSize];
NSMutableParagraphStyle *body1stParagraph = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
body1stParagraph.alignment = NSTextAlignmentJustified;
body1stParagraph.minimumLineHeight = fontSize + 3;
body1stParagraph.maximumLineHeight = body1stParagraph.minimumLineHeight;
body1stParagraph.hyphenationFactor = 0.97;
body1stAttributes[NSParagraphStyleAttributeName] = body1stParag
raph;

將字體設(shè)置為 BodoniSvtyTwoITCTT。這是字體的 PostScript 名。如果想尋找字體名,我們可以使用 +[UIFont familyNames] 首先得到可用的字體系列集合。一個(gè)字體系列就是我們所熟知的字型。每個(gè)字型或字體系列有一個(gè)或多個(gè)字體。為了得到這些字體的名字,我們可以使用 +[UIFont fontNamesForFamilyName:]。注意一下,當(dāng)你處理多樣字體時(shí),UIFontDescriptor 類非常有用,比如,當(dāng)你想要知道一個(gè)給定的字體是什么版本的斜體。

許多設(shè)置位于 NSParagraphStyle。我們創(chuàng)建一個(gè)默認(rèn)風(fēng)格的可變拷貝并做些調(diào)整。在我們的例子中,我們將會(huì)為字體大小加上 3 pt

接著,我們會(huì)為這些段落的屬性創(chuàng)建一個(gè)拷貝并修改他們來創(chuàng)建 boddyAttributes,(注意,這是我們段落的屬性,跟上文的 body1stParagraph 已經(jīng)不是同一個(gè)了):

NSMutableDictionary *bodyAttributes = [body1stAttributes mutableCopy];
NSMutableParagraphStyle *bodyParagraph = 
  [bodyAttributes[NSParagraphStyleAttributeName] mutableCopy];
bodyParagraph.firstLineHeadIndent = fontSize;
bodyAttributes[NSParagraphStyleAttributeName] = bodyParagraph;

我們簡(jiǎn)單的創(chuàng)建了一個(gè)屬性字典的可變拷貝,同時(shí)為了改變段落風(fēng)格我們也需要?jiǎng)?chuàng)建一個(gè)可變拷貝。將 firstLineHeadIndent 設(shè)為和字體大小一樣,我們便會(huì)得到想要的空格縮進(jìn)。

接著,裝飾段落風(fēng)格:

NSMutableDictionary *ornamentAttributes = [NSMutableDictionary dictionary];
ornamentAttributes[NSFontAttributeName] = [UIFont fontWithName:@"BodoniOrnamentsITCTT"
                                                          size:36];
NSMutableParagraphStyle *ornamentParagraph = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
ornamentParagraph.alignment = NSTextAlignmentCenter;
ornamentParagraph.paragraphSpacingBefore = fontSize;
ornamentParagraph.paragraphSpacing = fontSize;
ornamentAttributes[NSParagraphStyleAttributeName] = ornamentParagraph;

這個(gè)很容易理解。我們使用裝飾字體并將文本居中對(duì)齊。此外,在裝飾字符的前后我們都要加空白段落。

數(shù)據(jù)表格

接下來是顯示數(shù)字的 table。我們想要將分?jǐn)?shù)的小數(shù)點(diǎn)對(duì)齊顯示,即英語中的 “.”:

http://wiki.jikexueyuan.com/project/objc/images/9-5.png" alt="" />

為了達(dá)到這個(gè)目的,我們需要指定 table 將中心停在分隔符上。

對(duì)于上面這個(gè)示例,我們簡(jiǎn)單的做一下:

NSCharacterSet *decimalTerminator = [NSCharacterSet 
  characterSetWithCharactersInString:decimalFormatter.decimalSeparator];
NSTextTab *decimalTab = [[NSTextTab alloc] 
   initWithTextAlignment:NSTextAlignmentCenter
                location:100
                 options:@{NSTabColumnTerminatorsAttributeName:decimalTerminator}];
NSTextTab *percentTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight
                                                        location:200
                                                         options:nil];
NSMutableParagraphStyle *tableParagraphStyle = 
  [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
tableParagraphStyle.tabStops = @[decimalTab, percentTab];

列表

另一個(gè)常見的使用情況就像 list 這樣:

http://wiki.jikexueyuan.com/project/objc/images/9-6.png" alt="" />

(圖片來自 ,作者為 Henry M. Robert)

縮進(jìn)相對(duì)容易設(shè)置。我們需要確保序列號(hào) “(1)” 和 text 或者著重號(hào)和 text 之間有一個(gè)制表符。然后我們像這樣調(diào)整段落的風(fēng)格:

NSMutableDictionary *listAttributes = [bodyAttributes mutableCopy];
NSMutableParagraphStyle *listParagraph = 
  [listAttributes[NSParagraphStyleAttributeName] mutableCopy];
listParagraph.headIndent = fontSize * 3;
listParagraph.firstLineHeadIndent = fontSize;
NSTextTab *listTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentNatural
                                                     location:fontSize * 3 
                                                      options:nil];
listParagraph.tabStops = @[listTab];
listAttributes[NSParagraphStyleAttributeName] = listParagraph;

我們將 headIndent 設(shè)置為真實(shí)文本的縮進(jìn),將 firstLineHeadIndent 設(shè)置為我們希望著重號(hào)具有的縮進(jìn)。最終,和 headIndent 一樣,我們需要在相同的位置增加一個(gè)制表符。著重號(hào)后的制表符會(huì)確保這行文本從正確的位置開始繪制。