iOS技术文档No.3 AppKit_NSLayoutManager

iOS7.0之后的类,同样是对富文本的操作,但是再iOS9.0之后废弃了一些属性,所以建议有需求的人有时间的人好好搞一下
这里还是贴上代码及原帖地址demo链接

图1.png

这是使用UITextView时用到的iOS7新增加的类:NSTextContainer、NSLayoutManager、NSTextStorage及其相互关系:

关系.png
关系1.png

这三个新出的类还没有在官方出独立的class reference,但是在新出的UIKit_Framework上已经有这些类的相关说明及使用方法,当然官方还会更新。

以下是摘自文档的部分语句:

首先是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则定义了一个矩形区域用于存放已经进行了排版并设置好属性的文字。

以上三者是相互包含相互作用的层次关系。

接下来是三种类的使用:

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类或其子类发送开始或完成文字编辑的消息。在两条语句之间进行相应的文字编辑,例如为文字添加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 // 修改指定范围内的文字  

具体实现如下:

// 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类。另外两个类我会继续跟进学习。
对比起iOS6及以前,处理文字的排版布局和重置属性变得更加简便。
Snell Roundhand是一种我觉得非常华丽的字体,非常喜欢。最后上张程序运行结果的图:

效果.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • Text Kit学习(入门和进阶): http://www.cocoachina.com/industry/201...
    F麦子阅读 4,009评论 1 13
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一种新的协议。它实...
    香橙柚子阅读 23,798评论 8 183
  • 给你写这篇文的时候,耳机里放的是毛不易的《消愁》,给你写这句话的时候正好在唱好吧天亮之后总是潇洒离场,清醒的人最荒...
    楠木霂阅读 234评论 0 2
  • 清晨悄悄起床,到阳台上梳头,惺忪的睡眼还未完全睁开,苍茫的雾色就涌入双眼。一时间,晨风吹面,我才惊见这莽然浩广的晨...
    冷歌阅读 518评论 0 2
  • 【三月十九】 三月过半时,校园中喊不出名字的魁梧大树的叶子都已枯黄,随风一吹,几片落地,就像电影情节那般落得随意又...
    桑榆非晚0阅读 292评论 0 1