iOS技術文檔No.3 AppKit_NSLayoutManager

iOS7.0之后的類,同樣是對富文本的操作,但是再iOS9.0之后廢棄了一些屬性,所以建議有需求的人有時間的人好好搞一下
這里還是貼上代碼及原帖地址demo鏈接

圖1.png

這是使用UITextView時用到的iOS7新增加的類:NSTextContainer、NSLayoutManager、NSTextStorage及其相互關系:

關系.png
關系1.png

這三個新出的類還沒有在官方出獨立的class reference,但是在新出的UIKit_Framework上已經(jīng)有這些類的相關說明及使用方法,當然官方還會更新。

以下是摘自文檔的部分語句:

首先是NSTextContainer:

The NSTextContainer class defines a region in which text is laid out.
An NSTextContainer object defines rectangular regions, and you can define exclusion paths inside the textcontainer'sboundingrectanglesothattextflowsaroundtheexclusionpathasitislaidout.
接著是NSLayoutManager:

An NSLayoutManager object coordinates the layout and display of characters held in an NSTextStorage object. It maps Unicode character codes to glyphs, sets the glyphs in a series of NSTextContainer objects, and displays them in a series of text view objects.
最后是NSTextStorage:

NSTextStorage is a semiconcrete subclass of NSMutableAttributedString that manages a set of client NSLayoutManagerobjects,notifyingthemofanychangestoitscharactersorattributessothattheycanrelay and redisplay the text as needed.

按照個人理解:

NSTextStorage保存并管理UITextView要展示的文字內容,該類是NSMutableAttributedString的子類,由于可以靈活地往文字添加或修改屬性,所以非常適用于保存并修改文字屬性。

NSLayoutManager用于管理NSTextStorage其中的文字內容的排版布局。

NSTextContainer則定義了一個矩形區(qū)域用于存放已經(jīng)進行了排版并設置好屬性的文字。

以上三者是相互包含相互作用的層次關系。

接下來是三種類的使用:

CGRect textViewRect = CGRectInset(self.view.bounds, 10.0, 20.0);  
  
// NSTextContainer  
NSTextContainer *container = [[NSTextContainer alloc] initWithSize:CGSizeMake(textViewRect.size.width, CGFLOAT_MAX)]; // new in iOS 7.0  
container.widthTracksTextView = YES; // Controls whether the receiveradjusts the width of its bounding rectangle when its text view is resized  
  
  
// NSLayoutManager  
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; // new in iOS 7.0  
[layoutManager addTextContainer:container];  
  
  
// NSTextStorage subclass  
self.textStorage = [[TextStorage alloc] init]; // new in iOS 7.0  
[self.textStorage addLayoutManager:layoutManager];  

首先是初始化類對象,然后通過add方法來建立三者之間的關系。
最后必須注意要在UITextView中通過initWithFrame:textContainer:方法來添加相應的NSTextContainer從而設置好對應的文字。

 
// UITextView  
UITextView *newTextView = [[UITextView alloc] initWithFrame:textViewRect textContainer:container];  
newTextView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;  
newTextView.scrollEnabled = YES;  
newTextView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;  
// newTextView.editable = NO;  
newTextView.font = [UIFont fontWithName:self.textStorage.fontName size:18.0];  
newTextView.dataDetectorTypes = UIDataDetectorTypeAll;  
self.textView = newTextView;  
[self.view addSubview:self.textView];  

如果要使用NSTextStorage類來改變文字的屬性,分別使用

 
[_textStorage beginEditing];
[_textStorage endEditing];

來向UITextStorage類或其子類發(fā)送開始或完成文字編輯的消息。在兩條語句之間進行相應的文字編輯,例如為文字添加letterpress style:

[_textStorage beginEditing];  
NSDictionary *attrsDic = @{NSTextEffectAttributeName: NSTextEffectLetterpressStyle};  
UIKIT_EXTERN NSString *const NSTextEffectAttributeName NS_AVAILABLE_IOS(7_0);          // NSString, default nil: no text effect  
NSMutableAttributedString *mutableAttrString = [[NSMutableAttributedString alloc] initWithString:@"Letterpress" attributes:attrsDic];  
NSAttributedString *appendAttrString = [[NSAttributedString alloc] initWithString:@" Append:Letterpress"];  
[mutableAttrString appendAttributedString:appendAttrString];  
[_textStorage setAttributedString:mutableAttrString];  
[_textStorage endEditing];  

可以看到通過attribute來改變文字的屬性是非常簡單的。

又如通過NSTextStorage類為文字添加顏色屬性:

[_textStorage beginEditing];  
/* Dynamic Coloring Text */  
self.textStorage.bookItem = [[BookItem alloc] initWithBookName:@"Dynamic Coloring.rtf"];  
self.textStorage.tokens = @{@"Alice": @{NSForegroundColorAttributeName: [UIColor redColor]},  
                            @"Rabbit": @{NSForegroundColorAttributeName: [UIColor greenColor]},  
                            DefaultTokenName: @{NSForegroundColorAttributeName: [UIColor blackColor]}  
                            };  
[_textStorage setAttributedString:_textStorage.bookItem.content];  
[_textStorage endEditing];  

其處理過程要看看重寫的NSTextStorage子類:
接口部分:

NSString *const DefaultTokenName;  
  
@interface TextStorage : NSTextStorage  
@property (nonatomic, strong) NSString     *fontName;  
@property (nonatomic, copy)   NSDictionary *tokens; // a dictionary, keyed by text snippets(小片段), with attributes we want to add  
@property (nonatomic, strong) BookItem     *bookItem;  
@end  

以及.m中的匿名類別:

#import "TextStorage.h"  
  
NSString *const DefaultTokenName = @"DefaultTokenName";  
  
@interface TextStorage ()  
{  
    NSMutableAttributedString *_storingText; // 存儲的文字  
    BOOL _dynamicTextNeedsUpdate;            // 文字是否需要更新  
}  
@end  

然后是基本的初始化方法:

// get fontName Snell RoundHand  
-(NSString *)fontName  
{  
    NSArray *fontFamily = [UIFont familyNames];  
    NSString *str = fontFamily[2];  
    // NSLog(@"%@", str);  
    return str;  
}  
  
// initial  
-(id)init  
{  
    self = [super init];  
    if (self) {  
        _storingText = [[NSMutableAttributedString alloc] init];  
    }  
    return self;  
}  

重點來了,重寫NSTextStorage類的子類必須重載以下四個方法:

// Must override NSAttributedString primitive method  
-(NSString *)string // 返回保存的文字  
-(NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range // 獲取指定范圍內的文字屬性  
  
// Must override NSMutableAttributedString primitive method  
-(void)setAttributes:(NSDictionary *)attrs range:(NSRange)range           // 設置指定范圍內的文字屬性  
-(void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str // 修改指定范圍內的文字  

具體實現(xiàn)如下:

// Must override NSAttributedString primitive method  
// 返回保存的文字  
-(NSString *)string  
{  
    return [_storingText string];  
}  
  
// 獲取指定范圍內的文字屬性  
-(NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range  
{  
    return [_storingText attributesAtIndex:location effectiveRange:range];  
}  

_storingText保存了NSTextStorage中的文字,string方法直接返回該變量的string值。
要獲取文字屬性時也可以直接從_storingText入手。

 
// Must override NSMutableAttributedString primitive method  
// 設置指定范圍內的文字屬性  
-(void)setAttributes:(NSDictionary *)attrs range:(NSRange)range  
{  
    [self beginEditing];  
    [_storingText setAttributes:attrs range:range];  
    [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; // Notifies and records a recent change.  If there are no outstanding -beginEditing calls, this method calls -processEditing to trigger post-editing processes.  This method has to be called by the primitives after changes are made if subclassed and overridden.  editedRange is the range in the original string (before the edit).  
    [self endEditing];  
}  
  
// 修改指定范圍內的文字  
-(void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str  
{  
    [self beginEditing];  
    [_storingText replaceCharactersInRange:range withString:str];  
    [self edited:NSTextStorageEditedAttributes | NSTextStorageEditedCharacters range:range changeInLength:str.length - range.length];  
    _dynamicTextNeedsUpdate = YES;  
    [self endEditing];  
} 

可以看到在設置或要改變文字的屬性時必須分別調用beginEditing和endEditing方法,在兩者之間進行相應的動作。

如果NSTextStorge類收到endEditing的通知,則調用processEditing方法進行處理。

// Sends out -textStorage:willProcessEditing, fixes the attributes, sends out -textStorage:didProcessEditing, and notifies the layout managers of change with the -processEditingForTextStorage:edited:range:changeInLength:invalidatedRange: method.  Invoked from -edited:range:changeInLength: or -endEditing.  
-(void)processEditing  
{  
    if (_dynamicTextNeedsUpdate) {  
        _dynamicTextNeedsUpdate = NO;  
        [self performReplacementsForCharacterChangeInRange:[self editedRange]];  
    }  
    [super processEditing];  
}  
- (void)performReplacementsForCharacterChangeInRange:(NSRange)changedRange
{
    NSRange extendedRange = NSUnionRange(changedRange, [[_storingText string]
                                                        lineRangeForRange:NSMakeRange(changedRange.location, 0)]);
    extendedRange = NSUnionRange(changedRange, [[_storingText string]
                                                lineRangeForRange:NSMakeRange(NSMaxRange(changedRange), 0)]);
    [self applyStylesToRange:extendedRange];
}
- (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"}];
    
    // 1. base our script font on the preferred body font size
    UIFontDescriptor* bodyFontDescriptor = [UIFontDescriptor
                                            preferredFontDescriptorWithTextStyle:UIFontTextStyleBody];
    NSNumber* bodyFontSize = bodyFontDescriptor.
    fontAttributes[UIFontDescriptorSizeAttribute];
    UIFont* scriptFont = [UIFont
                          fontWithDescriptor:scriptFontDescriptor size:[bodyFontSize floatValue]];
    
    // 2. create the attributes
    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]};
    
    // iterate over each replacement
    for (NSString* key in _replacements) {
        NSRegularExpression *regex = [NSRegularExpression
                                      regularExpressionWithPattern:key
                                      options:0
                                      error:nil];
        
        NSDictionary* attributes = _replacements[key];
        
        [regex enumerateMatchesInString:[_storingText string]
                                options:0
                                  range:searchRange
                             usingBlock:^(NSTextCheckingResult *match,
                                          NSMatchingFlags flags,
                                          BOOL *stop){
                                 // apply the style
                                 NSRange matchRange = [match rangeAtIndex:1];
                                 [self addAttributes:attributes range:matchRange];
                                 
                                 // reset the style to the original
                                 if (NSMaxRange(matchRange)+1 < self.length) {
                                     [self addAttributes:normalAttrs
                                                   range:NSMakeRange(NSMaxRange(matchRange)+1, 1)];
                                 }
                             }];
    }
}

這是iOS7新增的用于設置文字屬性和進行排版的三個主要的類,本文主要說了NSTextStorage類。另外兩個類我會繼續(xù)跟進學習。
對比起iOS6及以前,處理文字的排版布局和重置屬性變得更加簡便。
Snell Roundhand是一種我覺得非常華麗的字體,非常喜歡。最后上張程序運行結果的圖:

效果.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • Text Kit學習(入門和進階): http://www.cocoachina.com/industry/201...
    F麥子閱讀 4,102評論 1 13
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一種新的協(xié)議。它實...
    香橙柚子閱讀 23,989評論 8 183
  • 給你寫這篇文的時候,耳機里放的是毛不易的《消愁》,給你寫這句話的時候正好在唱好吧天亮之后總是瀟灑離場,清醒的人最荒...
    楠木霂閱讀 241評論 0 2
  • 清晨悄悄起床,到陽臺上梳頭,惺忪的睡眼還未完全睜開,蒼茫的霧色就涌入雙眼。一時間,晨風吹面,我才驚見這莽然浩廣的晨...
    冷歌閱讀 541評論 0 2
  • 【三月十九】 三月過半時,校園中喊不出名字的魁梧大樹的葉子都已枯黃,隨風一吹,幾片落地,就像電影情節(jié)那般落得隨意又...
    桑榆非晚0閱讀 301評論 0 1