更詳細的內容能夠參考官方文檔 《Text Programming Guide for iOS》。html
「Text Kit指的是UIKit框架中用於提供高質量排版服務的一些類和協議,它讓程序可以存儲,排版和顯示文本信息,並支持排版所須要的全部特性,包括字距調整、連寫、換行和對齊等。」
之前,若是咱們想實現複雜的文本排版,例如在textView中顯示不一樣樣式的文本,或者圖片和文字混排,你可能就須要藉助於UIWebView或者深刻研究一下Core Text。在iOS6中,UILabel、UITextField、UITextView增長了一個NSAttributedString屬性,能夠稍微解決一些排版問題,可是支持的力度還不夠。如今Text Kit徹底改變了這種現狀。
Text Kit是基於Core Text構建的快速、先進的文本排版和渲染引擎,而且與UIKit很好的集合。UITextView,UITextField、UILabel都已經基於Text Kit從新構建,因此它們都支持分頁文本、文本包裝、富文本編輯、交互式文本着色、文本摺疊和自定義截取等特性。全部這些UI控件如今都以一樣的方式構建,在它們後面,一個NSTextStorage對象保存着文本的主要信息,它自己是NSMutableAttributedString的子類,支持分批編輯。這就意味着你能夠改變一個範圍內的字符的樣式而不用總體替換文本內容。
- [self.textView.textStorage beginEditing];
- [self markWord:@"Alice" inTextStorage:self.textView.textStorage];
- [self.textView.textStorage endEditing];
Text storage管理者一系列的NSLayoutManager對象,當它的字符或者屬性改變時會通知到本身所管理的layout Manager對象以便它們做出相應的反應。在layout manager上面是一個NSTextContainer對象,用於爲layout manager定義座標系和一些幾何特性。例如,若是你想UITextView中的文本環繞在一張圖片四周,你能夠給text container設定一個排除路徑(exclusion path)。
- UIBezierPath *exclusion = ButterflyBezierPath;
- self.textView.textContainer.exclusionPaths = @[exclusion];
Text container可以處理擊中測試(hit tests),因此能夠定位到點擊的字符在文本中的位置。此外它還提供一些代理方法讓開發者可以本身定義連接點擊後的處理事件。
經過基於Text Kit從新構建UILabel、UITextField和UITextView,蘋果給開發者更大的靈活性和能力來設計富文本視圖,同時簡化了這些控件的使用,由於它們是以一樣的方式設計的,全部這些好處都是站在巨人(Core Text)的肩上。一般更強大的功能和靈活性也就意味着須要更多的設置和管理,可是,若是你只是想顯示一段簡單的文本,你仍是能夠像之前同樣使用。
- self.textLabel.text = @"Hello Text Kit";
Text Kit進階
上一篇文章Text Kit入門中咱們主要了解了什麼是Text Kit及它的一些架構和基本特性,這篇文章中會涉及關於Text Kit的更多具體應用。
Text Kit是創建在Core Text框架上的,咱們知道CoreText.framework是一個龐大而複雜的框架,而Text Kit在繼承了Core Text強大功能的同時給開發者提供了比較友好的面向對象的API。
本文主要介紹Text Kit下面四個特性:
動態字體(Dynamic type)
凸版印刷體效果(Letterpress effects)
路徑排除(Exclusion paths)
動態文本格式化和存儲(Dynamic text formatting and storage)
動態字體(Dynamic type)
動態字體是iOS7中新增長的比較重要的特性之一,程序應該按照用戶設定的字體大小和粗細來顯示文本內容。
分別在設置\通用\輔助功能和設置\通用\文字大小中能夠設置文本在應用程序中顯示的粗細和大小。
iOS7對系統字體在顯示上作了一些優化,讓不一樣大小的字體在屏幕上都能清晰的顯示。一般用戶設置了本身偏好的字體,他們但願在全部程序中都看到文本顯示是根據他們的設定進行調整。爲了實現這個,開發者須要在本身的應用中給文本控件設置當前用戶設置字體,而不是指定死字體及大小。能夠經過UIFont中新增的preferredFontForTextStyle:方法來獲取用戶偏好的字體。
iOS7中給出了6中字體樣式供選擇:
UIFontTextStyleHeadline
UIFontTextStyleBody
UIFontTextStyleSubheadline
UIFontTextStyleFootnote
UIFontTextStyleCaption1
UIFontTextStyleCaption2
爲了讓咱們的程序支持動態字體,須要按一下方式給文本控件(一般是指UILabel,UITextField,UITextView)設定字體:
- self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
這樣設置以後,文本控件就會以用戶設定的字體大小及粗細顯示,可是若是程序在運行時,用戶切換到設置裏修改了字體,這是在切回程序,字體並不會自動跟着變。這時就須要咱們本身來更新一下控件的字體了。
在系統字體修改時,系統會給運行中的程序發送UIContentSizeCategoryDidChangeNotification通知,咱們只須要監聽這個通知,並從新設置一下字體便可。
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(preferredContentSizeChanged:)
- name:UIContentSizeCategoryDidChangeNotification
- object:nil];
- - (void)preferredContentSizeChanged:(NSNotification *)notification{
- self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
- }
固然,有的時候要適應動態修改的字體並非這麼設置一下就完事了,控件的大小可能也須要進行相應的調整,這時咱們程序中的控件大小也不該該寫死,而是須要根據字體大小來計算.
凸版印刷體效果(Letterpress effects)
凸版印刷替效果是給文字加上奇妙陰影和高光,讓文字看起有凹凸感,像是被壓在屏幕上。固然這種看起來很高端大氣上檔次的效果實現起來確實至關的簡單,只須要給AttributedString加一個NSTextEffectAttributeName屬性,並指定該屬性值爲NSTextEffectLetterpressStyle就能夠了。
- tionary *attributes = @{
- NSForegroundColorAttributeName: [UIColor redColor],
- NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline],
- NSTextEffectAttributeName: NSTextEffectLetterpressStyle
- };
- self.titleLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Title" attributes:attributes];
在iOS7系統自帶的備忘錄應用中,蘋果就使用了這種凸版印刷體效果。
路徑排除(Exclusion paths)
在排版中,圖文混排是很是常見的需求,但有時候咱們的圖片並必定都是正常的矩形,這個時候咱們若是須要將文本環繞在圖片周圍,就能夠用路徑排除(exclusion paths)了。
Explosion pats基本原理是將須要被文本留出來的形狀的路徑告訴文本控件的NSTextContainer對象,NSTextContainer在文字排版時就會避開該路徑。
- UIBezierPath *floatingPath = [self pathOfImage];
- self.textView.textContainer.exclusionPaths = @[floatingPath];
因此實現Exclusion paths的主要工做就是獲取這個path。
動態文本格式化和存儲(Dynamic text formatting and storage)
好了,到如今咱們知道了Text Kit能夠動態的根據用戶設置的字體大小進行調整,可是若是具體某個文本顯示控件中的文本樣式可以動態調整是否是會更酷一些呢?
實現這些纔是真正體現Text Kit強大之處的時候,在此以前你須要理解Text Kit中的文本存儲系統是怎麼工做的,下圖顯示了Text Kit中文本的保存、渲染和現實之間的關係。
當你使用UITextView、UILabel、UITextField控件的時候,系統會自動建立上面這些類,你能夠選擇直接使用這麼默認的實現或者爲你的控件自定義這幾個中的任何一個。
1.NSTextStorage自己繼承與NSMutableAttributedString,它是以attributed string的形式保存須要渲染的文本,並在文本內容改變的時候通知到對應的layout manager對象。一般你須要建立NSTextStorage的子類來在文本改變時進行文本顯示樣式的更新。
2.NSLayoutManager做爲文本控件中的排版引擎接收保存的文本並在屏幕上渲染出來。
3.NSTextContainer描述了文本在屏幕上顯示時的幾何區域,每一個text container與一個具體的UITextView相關聯。若是你須要定義一個很複雜形狀的區域來顯示文本,你可能須要建立NSTextContainer子類。
要實現咱們上面描述的動態文本格式化功能,咱們須要建立NSTextStorage子類以便在用戶輸入文本的時候動態的增長文本屬性。自定義了text storage後,咱們須要替換調UITextView默認的text storage。
建立NSTextStorage的子類
咱們建立NSTextStorage子類,命名爲MarkupTextStorage,在實現文件中添加一個成員變量:
- #import "MarkupTextStorage.h"
-
- @implementation MarkupTextStorage
- {
- NSMutableAttributedString *_backingStore;
- }
-
- - (id)init
- {
- self = [super init];
- if (self) {
- _backingStore = [[NSMutableAttributedString alloc] init];
- }
- return self;
- }
-
- @end
NSTextStorage的子類須要重載一些方法提供NSMutableAttributedString類型的backing store信息,因此咱們繼續添加下面代碼:
- - (NSString *)string
- {
- return [_backingStore string];
- }
-
- - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
- {
- return [_backingStore attributesAtIndex:location effectiveRange:range];
- }
-
- - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
- {
- [self beginEditing];
- [_backingStore replaceCharactersInRange:range withString:str];
- [self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes
- range:range changeInLength:str.length - range.length];
- [self endEditing];
- }
-
- - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
- {
- [self beginEditing];
- [_backingStore setAttributes:attrs range:range];
- [self edited:NSTextStorageEditedAttributes
- range:range changeInLength:0];
- [self endEditing];
- }
後面兩個方法都是代理到backing store,而後須要被beginEditing edited endEditing包圍,並且必須在文本編輯時按順序調用來通知text storage對應的layout manager。
你可能發現子類化NSTextStorage須要寫很多的代碼,由於NSTextStorage是一個類集羣中的一個開發接口,不能只是繼承它而後重載不多的方法來拓展它的功能,而是須要本身實現不少細節。
類集羣(Class cluster)是蘋果Cocoa(Touch)框架中經常使用的設計模式之一。
類集羣是Objective-C中對抽象工廠模式的簡單實現,爲建立一些列相關或獨立對象提供了統一的接口而不用指定具體的類。經常使用的像NSArray和NSNumber事實上也是一系列類集羣的開放接口。
蘋果使用類集羣是爲了將一些類具體類隱藏在開放的抽象父類之下,外面經過抽象父類的方法來建立私有子類的實例,而且外界也徹底不知道工廠分配到了哪一個私有類,由於它們始終只和開放接口交互。
使用類集羣確實簡化了接口,讓類更容易被使用,可是要知道魚和熊掌不可兼得,你又想簡單又想可拓展性強,哪有那麼好的事啊?因此建立一個類集羣中的抽象父類就沒有那麼簡單了。
好了,上面解釋了這麼多其實主要就說明了爲何子類化NSTextStorage須要寫這麼多代碼,下面要在UITextView使用咱們自定義的text storage了。
設置UITextView
- - (void)createMarkupTextView
- {
- NSDictionary *attributes = @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
- NSString *content = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"content" ofType:@"txt"]
- encoding:NSUTF8StringEncoding
- error:nil];
- NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:content
- attributes:attributes];
- _textStorage = [[MarkupTextStorage alloc] init];
- [_textStorage setAttributedString:attributedString];
-
- CGRect textViewRect = CGRectMake(20, 60, 280, self.view.bounds.size.height - 100);
-
- NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
-
- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(textViewRect.size.width, CGFLOAT_MAX)];
- [layoutManager addTextContainer:textContainer];
- [_textStorage addLayoutManager:layoutManager];
-
- _textView = [[UITextView alloc] initWithFrame:textViewRect
- textContainer:textContainer];
- _textView.delegate = self;
- [self.view addSubview:_textView];
- }
很長的代碼,下面咱們來看看都作了些啥:
1.建立了一個自定義的text storage對象,並經過attributed string保存了須要顯示的內容;
2.建立了一個layout manager對象;
3.建立了一個text container對象並將它與layout manager關聯,而後該text container再和text storage對象關聯;
4.經過text container建立了一個text view並顯示。
你能夠將代碼和前面那對象間的關係圖對應着理解一下。
動態格式化
繼續在MarkupTextStorage.m文件中添加以下方法:
- - (void)processEditing
- {
- [self performReplacementsForRange:[self editedRange]];
- [super processEditing];
- }
processEditing在layout manager中文本修改時發送通知,它一般也是處理一些文本修改邏輯的好地方。
繼續添加:
- - (void)performReplacementsForRange:(NSRange)changedRange
- {
- NSRange extendedRange = NSUnionRange(changedRange, [[_backingStore string]
- lineRangeForRange:NSMakeRange(changedRange.location, 0)]);
- extendedRange = NSUnionRange(changedRange, [[_backingStore string]
- lineRangeForRange:NSMakeRange(NSMaxRange(changedRange), 0)]);
- [self applyStylesToRange:extendedRange];
- }
這個方法用於擴大文本匹配的範圍,由於changedRange只是標識出一個字符,lineRangeForRange會將範圍擴大到當前的一整行。
下面就剩下匹配特定格式的文原本顯示對應的樣式了:
- - (NSDictionary*)createAttributesForFontStyle:(NSString*)style
- withTrait:(uint32_t)trait {
- UIFontDescriptor *fontDescriptor = [UIFontDescriptor
- preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];
-
- UIFontDescriptor *descriptorWithTrait = [fontDescriptor
- fontDescriptorWithSymbolicTraits:trait];
-
- UIFont* font = [UIFont fontWithDescriptor:descriptorWithTrait size: 0.0];
- return @{ NSFontAttributeName : font };
- }
-
- - (void)createMarkupStyledPatterns
- {
- UIFontDescriptor *scriptFontDescriptor =
- [UIFontDescriptor fontDescriptorWithFontAttributes:
- @{UIFontDescriptorFamilyAttribute: @"Bradley Hand"}];
-
-
- UIFontDescriptor* bodyFontDescriptor = [UIFontDescriptor
- preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];
- NSNumber* bodyFontSize = bodyFontDescriptor.
- fontAttributes[UIFontDescriptorSizeAttribute];
- UIFont* scriptFont = [UIFont
- fontWithDescriptor:scriptFontDescriptor size:[bodyFontSize floatValue]];
-
-
- NSDictionary* boldAttributes = [self
- createAttributesForFontStyle:UIFontTextStyleBody
- withTrait:UIFontDescriptorTraitBold];
- NSDictionary* italicAttributes = [self
- createAttributesForFontStyle:UIFontTextStyleBody
- withTrait:UIFontDescriptorTraitItalic];
- NSDictionary* strikeThroughAttributes = @{ NSStrikethroughStyleAttributeName : @1,
- NSForegroundColorAttributeName: [UIColor redColor]};
- NSDictionary* scriptAttributes = @{ NSFontAttributeName : scriptFont,
- NSForegroundColorAttributeName: [UIColor blueColor]
- };
- NSDictionary* redTextAttributes =
- @{ NSForegroundColorAttributeName : [UIColor redColor]};
-
- _replacements = @{
- @"(\\*\\*\\w+(\\s\\w+)*\\*\\*)" : boldAttributes,
- @"(_\\w+(\\s\\w+)*_)" : italicAttributes,
- @"(~~\\w+(\\s\\w+)*~~)" : strikeThroughAttributes,
- @"(`\\w+(\\s\\w+)*`)" : scriptAttributes,
- @"\\s([A-Z]{2,})\\s" : redTextAttributes
- };
- }
-
- - (void)applyStylesToRange:(NSRange)searchRange
- {
- NSDictionary* normalAttrs = @{NSFontAttributeName:
- [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
-
-
- for (NSString* key in _replacements) {
- NSRegularExpression *regex = [NSRegularExpression
- regularExpressionWithPattern:key
- options:0
- error:nil];
-
- NSDictionary* attributes = _replacements[key];
-
- [regex enumerateMatchesInString:[_backingStore string]
- options:0
- range:searchRange
- usingBlock:^(NSTextCheckingResult *match,
- NSMatchingFlags flags,
- BOOL *stop){
-
- NSRange matchRange = [match rangeAtIndex:1];
- [self addAttributes:attributes range:matchRange];
-
-
- if (NSMaxRange(matchRange)+1 < self.length) {
- [self addAttributes:normalAttrs
- range:NSMakeRange(NSMaxRange(matchRange)+1, 1)];
- }
- }];
- }
- }
在createMarkupStyledPatterns初始化方法中調用createMarkupStyledPatterns,經過正則表達式來給特定格式的字符串設定特定顯示樣式,造成一個對應的字典。而後在applyStylesToRange:中利用已定義好的樣式字典來給匹配的文本端增長樣式。
到這裏本篇文章的內容就結束了,其實前面三點都很簡單,稍微過一下就能用。最後一個動態文本格式化內容稍微多一點,能夠結合個人代碼TextKitDemo來看。
參考連接: